diff --git a/README.md b/README.md index 3d9f2fd4..c10fb313 100644 --- a/README.md +++ b/README.md @@ -326,6 +326,8 @@ To tag your tasks: This action emits debug logs to help troubleshoot deployment failures. To see the debug logs, create a secret named `ACTIONS_STEP_DEBUG` with value `true` in your repository. +The input `show-service-events` helps you to check logs from the service deployment events without going to the AWS console. This is just for the `ECS deployment controller`. Is desirable to configure [deployment circuit breaker](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-circuit-breaker.html) to get a 'FAILED' rolloutState. + ## License Summary This code is made available under the MIT license. diff --git a/action.yml b/action.yml index ebc399b9..f9b0b456 100644 --- a/action.yml +++ b/action.yml @@ -16,6 +16,12 @@ inputs: cluster: description: "The name of the ECS service's cluster. Will default to the 'default' cluster." required: false + show-service-events: + description: "Whether to see or not the service deployment events when deployment rolloutState is 'FAILED'. Useful to see errors when a deployment fails." + required: false + show-service-events-frequency: + description: "The frequency for showing a log line of the service events (default: 15 seconds)." + required: false wait-for-service-stability: description: 'Whether to wait for the ECS service to reach stable state after deploying the new task definition. Valid value is "true". Will default to not waiting.' required: false diff --git a/dist/index.js b/dist/index.js index 4f117848..cd3a7579 100644 --- a/dist/index.js +++ b/dist/index.js @@ -137,7 +137,7 @@ async function tasksExitCode(ecs, clusterName, taskArns) { } // Deploy to a service that uses the 'ECS' deployment controller -async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { +async function updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { core.debug('Updating the service'); let params = { @@ -155,10 +155,65 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe } await ecs.updateService(params); - const region = await ecs.config.region(); - const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com'; + // Create a while loop to print the events of the service if deployment rollout state is failed + // or if there are failed tasks but rollout state is still in progress + if (showServiceEvents && showServiceEvents.toLowerCase() === 'true') { + core.debug(`Deployment started. The option show-service-events is set to true. Showing logs each ${showServiceEventsFrequency} seconds.`); + const initialState = 'IN_PROGRESS'; + let describeResponse = await ecs.describeServices({ + services: [service], + cluster: clusterName + }); + const deployTime = describeResponse.services[0].events[0].createdAt + let newEvents = []; + + while (initialState == 'IN_PROGRESS') { + const showServiceEventsFrequencyMilisec = (showServiceEventsFrequency * 1000) + await delay(showServiceEventsFrequencyMilisec); + let describeResponse = await ecs.describeServices({ + services: [service], + cluster: clusterName + }); - core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`); + let serviceResponse = describeResponse.services[0]; + let rolloutState = serviceResponse.deployments[0].rolloutState; + let rolloutStateReason = serviceResponse.deployments[0].rolloutStateReason; + let failedTasksCount = serviceResponse.deployments[0].failedTasks; + let indexEventContainDeployDate = getPosition(deployTime.toString(), serviceResponse.events); + newEvents = serviceResponse.events.slice(0, indexEventContainDeployDate); + + if (rolloutState == 'COMPLETED') { + printEvents(newEvents.reverse()); + break; + } else if (rolloutState == 'FAILED') { + printEvents(newEvents.reverse()); + throw new Error(`Rollout state is ${rolloutState}. Reason: ${rolloutStateReason}.`); + } else if (rolloutState == 'IN_PROGRESS' && failedTasksCount > 0) { + printEvents(newEvents.reverse()); + let tasksList = await ecs.listTasks({ + serviceName: service, + cluster: clusterName, + desiredStatus: 'STOPPED' + }); + let describeTaskResponse = await ecs.describeTasks({ + tasks: [tasksList.taskArns[0]], + cluster: clusterName, + }); + let stopCode = describeTaskResponse.tasks[0].stopCode; + let stoppedReason = describeTaskResponse.tasks[0].stoppedReason; + let containerLastStatus = describeTaskResponse.tasks[0].containers[0].lastStatus; + core.info(`Task status: ${stopCode}. The reason is: ${stoppedReason}.`); + if (containerLastStatus == 'STOPPED') { + core.info(`Container status: ${containerLastStatus}. The reason is: ${describeTaskResponse.tasks[0].containers[0].reason}.`); + } + throw new Error(`There are failed tasks. This means the deployment didn't go well. Please check the logs of task, service or container for more information.`); + } + } + } else { + const region = await ecs.config.region(); + const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com'; + core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`); + } // Wait for service stability if (waitForService && waitForService.toLowerCase() === 'true') { @@ -176,6 +231,29 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe } } +// Function to print the events of the service +function printEvents(events) { + core.debug('Showing the service events:') + for (let i = 0; i < events.length; i++) { + core.info(events[i].createdAt.toString().split('(')[0]); + core.info(events[i].message); + } +} + +// Function to get the position of an element in an array +function getPosition(elementToFind, arrayElements) { + for (let i = 0; i < arrayElements.length; i += 1) { + if (arrayElements[i].createdAt.toString().includes(elementToFind)) { + return i; + } + } +} + +// Fuction to create a delay + function delay(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } + // Find value in a CodeDeploy AppSpec file with a case-insensitive key function findAppSpecValue(obj, keyName) { return obj[findAppSpecKey(obj, keyName)]; @@ -392,7 +470,8 @@ async function run() { const taskDefinitionFile = core.getInput('task-definition', { required: true }); const service = core.getInput('service', { required: false }); const cluster = core.getInput('cluster', { required: false }); - const waitForService = core.getInput('wait-for-service-stability', { required: false }); + const waitForService = core.getInput('wait-for-service-stability', { required: false }); + let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30; if (waitForMinutes > MAX_WAIT_MINUTES) { @@ -406,6 +485,9 @@ async function run() { const enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true'; const propagateTags = core.getInput('propagate-tags', { required: false }) || 'NONE'; + const showServiceEvents = core.getInput('show-service-events', { required: false }); + let showServiceEventsFrequency = core.getInput('show-service-events-frequency', { required: false }) || 15; + // Register the task definition core.debug('Registering the task definition'); const taskDefPath = path.isAbsolute(taskDefinitionFile) ? @@ -456,7 +538,7 @@ async function run() { if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') { // Service uses the 'ECS' deployment controller, so we can call UpdateService core.debug('Updating service...'); - await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags); + await updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags); } else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') { // Service uses CodeDeploy, so we should start a CodeDeploy deployment diff --git a/index.js b/index.js index 8e9f90d9..dd1abf5a 100644 --- a/index.js +++ b/index.js @@ -131,7 +131,7 @@ async function tasksExitCode(ecs, clusterName, taskArns) { } // Deploy to a service that uses the 'ECS' deployment controller -async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { +async function updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { core.debug('Updating the service'); let params = { @@ -149,10 +149,65 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe } await ecs.updateService(params); - const region = await ecs.config.region(); - const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com'; + // Create a while loop to print the events of the service if deployment rollout state is failed + // or if there are failed tasks but rollout state is still in progress + if (showServiceEvents && showServiceEvents.toLowerCase() === 'true') { + core.debug(`Deployment started. The option show-service-events is set to true. Showing logs each ${showServiceEventsFrequency} seconds.`); + const initialState = 'IN_PROGRESS'; + let describeResponse = await ecs.describeServices({ + services: [service], + cluster: clusterName + }); + const deployTime = describeResponse.services[0].events[0].createdAt + let newEvents = []; - core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`); + while (initialState == 'IN_PROGRESS') { + const showServiceEventsFrequencyMilisec = (showServiceEventsFrequency * 1000) + await delay(showServiceEventsFrequencyMilisec); + let describeResponse = await ecs.describeServices({ + services: [service], + cluster: clusterName + }); + + let serviceResponse = describeResponse.services[0]; + let rolloutState = serviceResponse.deployments[0].rolloutState; + let rolloutStateReason = serviceResponse.deployments[0].rolloutStateReason; + let failedTasksCount = serviceResponse.deployments[0].failedTasks; + let indexEventContainDeployDate = getPosition(deployTime.toString(), serviceResponse.events); + newEvents = serviceResponse.events.slice(0, indexEventContainDeployDate); + + if (rolloutState == 'COMPLETED') { + printEvents(newEvents.reverse()); + break; + } else if (rolloutState == 'FAILED') { + printEvents(newEvents.reverse()); + throw new Error(`Rollout state is ${rolloutState}. Reason: ${rolloutStateReason}.`); + } else if (rolloutState == 'IN_PROGRESS' && failedTasksCount > 0) { + printEvents(newEvents.reverse()); + let tasksList = await ecs.listTasks({ + serviceName: service, + cluster: clusterName, + desiredStatus: 'STOPPED' + }); + let describeTaskResponse = await ecs.describeTasks({ + tasks: [tasksList.taskArns[0]], + cluster: clusterName, + }); + let stopCode = describeTaskResponse.tasks[0].stopCode; + let stoppedReason = describeTaskResponse.tasks[0].stoppedReason; + let containerLastStatus = describeTaskResponse.tasks[0].containers[0].lastStatus; + core.info(`Task status: ${stopCode}. The reason is: ${stoppedReason}.`); + if (containerLastStatus == 'STOPPED') { + core.info(`Container status: ${containerLastStatus}. The reason is: ${describeTaskResponse.tasks[0].containers[0].reason}.`); + } + throw new Error(`There are failed tasks. This means the deployment didn't go well. Please check the logs of task, service or container for more information.`); + } + } + } else { + const region = await ecs.config.region(); + const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com'; + core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`); + } // Wait for service stability if (waitForService && waitForService.toLowerCase() === 'true') { @@ -170,6 +225,29 @@ async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForSe } } +// Function to print the events of the service +function printEvents(events) { + core.debug('Showing the service events:') + for (let i = 0; i < events.length; i++) { + core.info(events[i].createdAt.toString().split('(')[0]); + core.info(events[i].message); + } +} + +// Function to get the position of an element in an array +function getPosition(elementToFind, arrayElements) { + for (let i = 0; i < arrayElements.length; i += 1) { + if (arrayElements[i].createdAt.toString().includes(elementToFind)) { + return i; + } + } +} + +// Fuction to create a delay + function delay(time) { + return new Promise(resolve => setTimeout(resolve, time)); + } + // Find value in a CodeDeploy AppSpec file with a case-insensitive key function findAppSpecValue(obj, keyName) { return obj[findAppSpecKey(obj, keyName)]; @@ -386,7 +464,8 @@ async function run() { const taskDefinitionFile = core.getInput('task-definition', { required: true }); const service = core.getInput('service', { required: false }); const cluster = core.getInput('cluster', { required: false }); - const waitForService = core.getInput('wait-for-service-stability', { required: false }); + const waitForService = core.getInput('wait-for-service-stability', { required: false }); + let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30; if (waitForMinutes > MAX_WAIT_MINUTES) { @@ -400,6 +479,9 @@ async function run() { const enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true'; const propagateTags = core.getInput('propagate-tags', { required: false }) || 'NONE'; + const showServiceEvents = core.getInput('show-service-events', { required: false }); + let showServiceEventsFrequency = core.getInput('show-service-events-frequency', { required: false }) || 15; + // Register the task definition core.debug('Registering the task definition'); const taskDefPath = path.isAbsolute(taskDefinitionFile) ? @@ -450,7 +532,7 @@ async function run() { if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') { // Service uses the 'ECS' deployment controller, so we can call UpdateService core.debug('Updating service...'); - await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags); + await updateEcsService(ecs, clusterName, service, taskDefArn, showServiceEvents, showServiceEventsFrequency, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags); } else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') { // Service uses CodeDeploy, so we should start a CodeDeploy deployment diff --git a/index.test.js b/index.test.js index 2cae5c1b..eaa98652 100644 --- a/index.test.js +++ b/index.test.js @@ -14,11 +14,13 @@ jest.mock('fs', () => ({ const mockEcsRegisterTaskDef = jest.fn(); const mockEcsUpdateService = jest.fn(); const mockEcsDescribeServices = jest.fn(); +const mockEcsListTasks = jest.fn(); +const mockEcsDescribeTasks = jest.fn(); +const mockEcsWaiter = jest.fn(); const mockCodeDeployCreateDeployment = jest.fn(); const mockCodeDeployGetDeploymentGroup = jest.fn(); const mockRunTask = jest.fn(); const mockWaitUntilTasksStopped = jest.fn().mockRejectedValue(new Error('failed')); -const mockEcsDescribeTasks = jest.fn(); const config = { region: () => Promise.resolve('fake-region'), }; @@ -50,7 +52,6 @@ describe('Deploy to ECS', () => { beforeEach(() => { jest.clearAllMocks(); - core.getInput = jest .fn() .mockReturnValueOnce('task-definition.json') // task-definition @@ -709,6 +710,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('cluster-789') // cluster .mockReturnValueOnce('false') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes + .mockReturnValueOnce('') // show-deployment-events + .mockReturnValueOnce('') // show-deployment-events-frequency .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // run-task .mockReturnValueOnce('') // desired count @@ -1059,7 +1062,9 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('false') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('true') // force-new-deployment - .mockReturnValueOnce('4'); // desired count is number + .mockReturnValueOnce('4') // desired count is number + .mockReturnValueOnce('false'); // show-service-events + await run(); expect(core.setFailed).toHaveBeenCalledTimes(0); @@ -1129,6 +1134,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // cluster .mockReturnValueOnce('') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes + .mockReturnValueOnce('false') // show-service-events + .mockReturnValueOnce(0) // show-service-events-frequency .mockReturnValueOnce('') // enable-ecs-managed-tags .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('') // force-new-deployment @@ -1164,6 +1171,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment + .mockReturnValueOnce('false') // show-service-events + .mockReturnValueOnce(0) // show-service-events-frequency .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('false') // enable-ecs-managed-tags .mockReturnValueOnce('') // propagate-tags @@ -1204,6 +1213,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('true') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment + .mockReturnValueOnce('false') // show-service-events + .mockReturnValueOnce(0) // show-service-events-frequency .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // enable-ecs-managed-tags .mockReturnValueOnce('') // propagate-tags @@ -1254,6 +1265,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment + .mockReturnValueOnce('false') // show-service-events + .mockReturnValueOnce(0) // show-service-events-frequency .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // enable-ecs-managed-tags .mockReturnValueOnce('') // propagate-tags @@ -1280,6 +1293,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // enable-ecs-managed-tags .mockReturnValueOnce('') // force-new-deployment + .mockReturnValueOnce('false') // show-service-events + .mockReturnValueOnce(0) // show-service-events-frequency .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // propagate-tags .mockReturnValueOnce('true') // run-task @@ -1313,6 +1328,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment + .mockReturnValueOnce('false') // show-service-events + .mockReturnValueOnce(0) // show-service-events-frequency .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // enable-ecs-managed-tags .mockReturnValueOnce('') // propagate-tags @@ -1356,6 +1373,8 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment + .mockReturnValueOnce('false') // show-service-events + .mockReturnValueOnce(0) // show-service-events-frequency .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // enable-ecs-managed-tags .mockReturnValueOnce('') // propagate-tags @@ -1390,6 +1409,176 @@ describe('Deploy to ECS', () => { expect(core.setFailed).toBeCalledWith("arn:aws:ecs:fake-region:account_id:task/arn is TASK_FAILED"); }); + test('deployment is sucessful with rolloutState in COMPLETED', async () => { + core.getInput = jest + .fn() + .mockReturnValueOnce('task-definition.json') // task-definition + .mockReturnValueOnce('service-456') // service + .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('false') // wait-for-service-stability + .mockReturnValueOnce('') // wait-for-minutes + .mockReturnValueOnce('false') // force-new-deployment + .mockReturnValueOnce('true') // show-service-events + .mockReturnValueOnce(1) // show-service-events-frequency + + mockEcsDescribeServices.mockImplementation(() => { + () => Promise.resolve({ + services: [{ + status: 'ACTIVE', + deployments: [ + { + status: 'PRIMARY', + rolloutState: 'COMPLETED', + } + ], + events: [ + { + createdAt: "2020-01-01T00:00:00Z", + message: "beggining deployment" + }, + { + createdAt: "2020-01-02T00:00:00Z", + message: "deployment completed" + } + ] + }] + }); + }); + await run(); + expect(mockEcsDescribeServices).toHaveBeenCalledTimes(3); + }); + + test('error caught if deployment state is failed', async () => { + core.getInput = jest + .fn() + .mockReturnValueOnce('task-definition.json') // task-definition + .mockReturnValueOnce('service-456') // service + .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('false') // wait-for-service-stability + .mockReturnValueOnce('') // wait-for-minutes + .mockReturnValueOnce('false') // force-new-deployment + .mockReturnValueOnce('true') // show-service-events + .mockReturnValueOnce(1) // show-service-events-frequency + .mockReturnValueOnce('') // desired-count + .mockReturnValueOnce('') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-tags + .mockReturnValueOnce('true') // run-task + .mockReturnValueOnce('true'); // wait-for-task-stopped + + mockEcsDescribeServices.mockImplementation(() => { + return { + promise() { + return Promise.resolve({ + services: [{ + status: 'ACTIVE', + deployments: [ + { + status: 'PRIMARY', + rolloutState: 'FAILED', + rolloutStateReason: 'tasks failed to start', + } + ], + events: [ + { + createdAt: "2020-01-01T00:00:00Z", + message: "deployment failed: tasks failed to start" + } + ] + }] + }); + } + }; + }); + await run(); + expect(mockEcsDescribeServices).toHaveBeenCalledTimes(3); + expect(core.setFailed).toHaveBeenCalledTimes(1); + expect(core.setFailed).toBeCalledWith("Rollout state is FAILED. Reason: tasks failed to start."); + + }); + + test('error caught if deployment state is in progress, but there are failed tasks', async () => { + core.getInput = jest + .fn() + .mockReturnValueOnce('task-definition.json') // task-definition + .mockReturnValueOnce('service-456') // service + .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('false') // wait-for-service-stability + .mockReturnValueOnce('') // wait-for-minutes + .mockReturnValueOnce('false') // force-new-deployment + .mockReturnValueOnce('true') // show-service-events + .mockReturnValueOnce(1) // show-service-events-frequency + .mockReturnValueOnce('') // desired-count + .mockReturnValueOnce('') // enable-ecs-managed-tags + .mockReturnValueOnce('') // propagate-tags + .mockReturnValueOnce('') // run-task + .mockReturnValueOnce(''); // wait-for-task-stopped + + mockEcsDescribeServices.mockImplementation(() => { + return { + promise() { + return Promise.resolve({ + failures: [], + services: [{ + status: 'ACTIVE', + deployments: [ + { + status: 'PRIMARY', + rolloutState: 'IN_PROGRESS', + failedTasks: 1 + } + ], + events: [ + { + createdAt: "2020-01-01T00:00:00Z", + message: "deployment failed: tasks failed to start" + } + ] + }] + }); + } + }; + }); + + mockEcsListTasks.mockImplementation(() => { + return { + promise() { + return Promise.resolve({ + taskArns: ['task:arn:1'] + }); + } + }; + }); + + mockEcsDescribeTasks.mockImplementation(() => { + return { + promise() { + return Promise.resolve({ + tasks: [ + { + containers: [ + { + name: 'container-1', + lastStatus: 'STOPPED', + exitCode: 1, + reason: 'CannotPullContainerError' + } + ], + stopCode: 'TaskFailedToStart', + stoppedReason: 'CannotStartContainerError' + } + ] + }); + } + }; + }); + + await run(); + expect(mockEcsListTasks).toHaveBeenCalledTimes(1); + expect(mockEcsDescribeTasks).toHaveBeenCalledTimes(1); + expect(core.setFailed).toBeCalledWith("There are failed tasks. This means the deployment didn't go well. Please check the logs of task, service or container for more information."); + + }); + test('error caught if AppSpec file is not formatted correctly', async () => { mockEcsDescribeServices.mockImplementation( () => Promise.resolve({