diff --git a/__tests__/integration/integration.test.ts b/__tests__/integration/integration.test.ts index 5cab57c8..358abe9c 100644 --- a/__tests__/integration/integration.test.ts +++ b/__tests__/integration/integration.test.ts @@ -287,4 +287,26 @@ describe('integration tests', () => { } ) }) + + test('can deploy a release and wait for completion', async () => { + const config: ClientConfiguration = { + userAgentApp: 'Test', + instanceURL: apiClientConfig.instanceURL, + apiKey: apiClientConfig.apiKey + } + + const client = await Client.create(config) + + await createReleaseForTest(client) + + const result = await createDeploymentFromInputs(client, { + ...standardInputParameters, + releaseNumber: localReleaseNumber, + environments: ['Dev'], + waitForDeployment: true, + deploymentTimeoutMinutes: 2 + }) + + expect(result.length).toBe(1) + }) }) diff --git a/action.yml b/action.yml index c2b0e9f8..d3db442d 100644 --- a/action.yml +++ b/action.yml @@ -18,6 +18,12 @@ inputs: use_guided_failure: default: "false" description: 'Whether to use guided failure mode if errors occur during the deployment.' + wait_for_deployment: + default: "false" + description: 'Whether to wait for the deployment(s) to complete before finishing the action. If true, the action will fail if any deployment fails.' + deployment_timeout: + default: "60" + description: 'Maximum time (in minutes) to wait for deployment(s) to complete. Only used when wait_for_deployment is true. Defaults to 60 minutes.' variables: description: 'A multi-line list of prompted variable values. Format: name:value' deploy_at: diff --git a/src/api-wrapper.ts b/src/api-wrapper.ts index 9fa3da7f..7788c7f8 100644 --- a/src/api-wrapper.ts +++ b/src/api-wrapper.ts @@ -7,7 +7,10 @@ import { DeploymentRepository, DeploymentEnvironmentV2, DeploymentEnvironment, - ResourceCollection + ResourceCollection, + ServerTask, + ServerTaskWaiter, + TaskState } from '@octopusdeploy/api-client' export interface DeploymentResult { @@ -73,6 +76,29 @@ export async function createDeploymentFromInputs( } }) + if (parameters.waitForDeployment) { + client.info(`⏳ Waiting for deployment${response.DeploymentServerTasks.length > 1 ? 's' : '' } to complete...`) + const waiter = new ServerTaskWaiter(client, parameters.space) + const completedTasks = await waiter.waitForServerTasksToComplete( + results.map(r => r.serverTaskId), + 5000, + parameters.deploymentTimeoutMinutes * 60 * 1000, + (serverTask: ServerTask): void => { + client.info(`Waiting for task ${serverTask.Id}. Current status: ${serverTask.State}`) + } + ) + + const failedTasks = completedTasks.filter(t => t.State !== TaskState.Success) + if (failedTasks.length > 0) { + const summary = failedTasks.map(t => `${t.Id} (${t.State})`).join(', ') + throw new Error(`One or more deployments did not complete successfully: ${summary}`) + } + + client.info(`✅ The deployment${ + response.DeploymentServerTasks.length > 1 ? 's' : '' + } completed successfully!`) + } + return results } diff --git a/src/input-parameters.ts b/src/input-parameters.ts index 514de7e7..d7a305a3 100644 --- a/src/input-parameters.ts +++ b/src/input-parameters.ts @@ -27,6 +27,8 @@ export interface InputParameters { variables?: PromptedVariableValues runAt?: Date noRunAfter?: Date + waitForDeployment?: boolean + deploymentTimeoutMinutes: number } export function getInputParameters(): InputParameters { @@ -51,7 +53,9 @@ export function getInputParameters(): InputParameters { useGuidedFailure: getBooleanInput('use_guided_failure') || undefined, variables: variablesMap, runAt: getInput('deploy_at') ? new Date(getInput('deploy_at')) : undefined, - noRunAfter: getInput('deploy_at_expiry') ? new Date(getInput('deploy_at_expiry')) : undefined + noRunAfter: getInput('deploy_at_expiry') ? new Date(getInput('deploy_at_expiry')) : undefined, + waitForDeployment: getBooleanInput('wait_for_deployment') || undefined, + deploymentTimeoutMinutes: parseInt(getInput('deployment_timeout') || '60', 10) } const errors: string[] = [] @@ -73,6 +77,11 @@ export function getInputParameters(): InputParameters { ) } + const deploymentTimeout = parseInt(getInput('deployment_timeout') || '60', 10) + if (isNaN(deploymentTimeout) || deploymentTimeout <= 0) { + errors.push(`deployment_timeout '${getInput('deployment_timeout')}' must be a positive integer (minutes).`) + } + const deployAt = getInput('deploy_at') if (deployAt && isNaN(new Date(deployAt).getTime())) { errors.push(`deploy_at '${deployAt}' is not a valid ISO 8601 date-time string.`)