diff --git a/README.md b/README.md index b05d5060f..ab3f82bfa 100644 --- a/README.md +++ b/README.md @@ -56,6 +56,18 @@ If you do not wish to store your task definition as a file in your git repositor aws ecs describe-task-definition --task-definition my-task-definition-family --query taskDefinition > task-definition.json ``` +If you don't want to create new revisions of your task definition, you can define the task definition family and revision as inputs for the action. By default, the action will use the latest revision of the task definition family if the revision is not specified. + +```yaml + - name: Deploy to Amazon ECS + uses: aws-actions/amazon-ecs-deploy-task-definition@v1 + with: + task-definition: my-task-definition-family + service: my-service + cluster: my-cluster + wait-for-service-stability: true +``` + ### Task definition container image values It is highly recommended that each time your GitHub Actions workflow runs and builds a new container image for deployment, a new container image ID is generated. For example, use the commit ID as the new image's tag, instead of updating the 'latest' tag with the new image. Using a unique container image ID for each deployment allows rolling back to a previous container image. diff --git a/action.yml b/action.yml index e1f7db5b6..ed730af9b 100644 --- a/action.yml +++ b/action.yml @@ -5,7 +5,7 @@ branding: color: 'orange' inputs: task-definition: - description: 'The path to the ECS task definition file to register' + description: 'The path to the ECS task definition file to register or the name of the task definition family to use. If the task definition family is given, the action will use the latest ACTIVE revision of the task definition.' required: true desired-count: description: 'The number of instantiations of the task to place and keep running in your service.' diff --git a/index.js b/index.js index ff5f57c93..43405bcb8 100644 --- a/index.js +++ b/index.js @@ -276,23 +276,50 @@ async function run() { const desiredCount = parseInt((core.getInput('desired-count', {required: false}))); - // Register the task definition - core.debug('Registering the task definition'); const taskDefPath = path.isAbsolute(taskDefinitionFile) ? - taskDefinitionFile : - path.join(process.env.GITHUB_WORKSPACE, taskDefinitionFile); - const fileContents = fs.readFileSync(taskDefPath, 'utf8'); - const taskDefContents = maintainValidObjects(removeIgnoredAttributes(cleanNullKeys(yaml.parse(fileContents)))); - let registerResponse; - try { - registerResponse = await ecs.registerTaskDefinition(taskDefContents).promise(); - } catch (error) { - core.setFailed("Failed to register task definition in ECS: " + error.message); - core.debug("Task definition contents:"); - core.debug(JSON.stringify(taskDefContents, undefined, 4)); - throw(error); + taskDefinitionFile : + path.join(process.env.GITHUB_WORKSPACE, taskDefinitionFile); + + const isExistingTaskDef = fs.existsSync(taskDefPath); + + let taskDefArn; + + if (!isExistingTaskDef) { + core.debug(`Searching for task definition ${taskDefinitionFile} in ECS`); + try { + const describeResponse = await ecs.describeTaskDefinition({ + taskDefinition: taskDefinitionFile + }).promise(); + const taskDef = describeResponse.taskDefinition; + + if (!taskDef) { + throw new Error(`Task definition ${taskDefinitionFile} not found in ECS`); + } + + core.debug(`Found task definition ${taskDef.taskDefinitionArn}`); + taskDefArn = taskDef.taskDefinitionArn; + } catch (error) { + core.setFailed("Failed to describe task definition in ECS: " + error.message); + throw(error); + } } - const taskDefArn = registerResponse.taskDefinition.taskDefinitionArn; + + if (isExistingTaskDef) { + core.debug('Registering the task definition'); + const fileContents = fs.readFileSync(taskDefPath, 'utf8'); + const taskDefContents = maintainValidObjects(removeIgnoredAttributes(cleanNullKeys(yaml.parse(fileContents)))); + let registerResponse; + try { + registerResponse = await ecs.registerTaskDefinition(taskDefContents).promise(); + } catch (error) { + core.setFailed("Failed to register task definition in ECS: " + error.message); + core.debug("Task definition contents:"); + core.debug(JSON.stringify(taskDefContents, undefined, 4)); + throw(error); + } + taskDefArn = registerResponse.taskDefinition.taskDefinitionArn; + } + core.setOutput('task-definition-arn', taskDefArn); // Update the service with the new task definition diff --git a/index.test.js b/index.test.js index ba4f6ac97..00f80c69c 100644 --- a/index.test.js +++ b/index.test.js @@ -7,12 +7,14 @@ jest.mock('@actions/core'); jest.mock('fs', () => ({ promises: { access: jest.fn() }, readFileSync: jest.fn(), + existsSync: jest.fn().mockReturnValue(true) })); const mockEcsRegisterTaskDef = jest.fn(); const mockEcsUpdateService = jest.fn(); const mockEcsDescribeServices = jest.fn(); const mockEcsWaiter = jest.fn(); +const mockEcsDescribeTaskDef = jest.fn(); const mockCodeDeployCreateDeployment = jest.fn(); const mockCodeDeployGetDeploymentGroup = jest.fn(); const mockCodeDeployWaiter = jest.fn(); @@ -27,7 +29,8 @@ jest.mock('aws-sdk', () => { registerTaskDefinition: mockEcsRegisterTaskDef, updateService: mockEcsUpdateService, describeServices: mockEcsDescribeServices, - waitFor: mockEcsWaiter + waitFor: mockEcsWaiter, + describeTaskDefinition: mockEcsDescribeTaskDef })), CodeDeploy: jest.fn(() => ({ createDeployment: mockCodeDeployCreateDeployment, @@ -86,6 +89,14 @@ describe('Deploy to ECS', () => { }; }); + mockEcsDescribeTaskDef.mockImplementation(() => { + return { + promise() { + return Promise.resolve({ taskDefinition: { taskDefinitionArn: 'task:def:arn' } }); + } + }; + }) + mockEcsUpdateService.mockImplementation(() => { return { promise() { @@ -151,6 +162,65 @@ describe('Deploy to ECS', () => { }); }); + test("try to get existing task definition ARN when user provides family instead of file", async () => { + fs.existsSync.mockReturnValueOnce(false); + + core.getInput = jest + .fn() + .mockReturnValueOnce('task-def-family') + .mockReturnValueOnce('service-456') + .mockReturnValueOnce('cluster-789'); + + await run(); + + expect(core.setFailed).toHaveBeenCalledTimes(0); + expect(mockEcsDescribeTaskDef).toHaveBeenNthCalledWith(1, { + taskDefinition: "task-def-family" + }); + expect(mockEcsRegisterTaskDef).not.toHaveBeenCalled(); + expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); + expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { + cluster: 'cluster-789', + services: ['service-456'] + }); + expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { + cluster: 'cluster-789', + service: 'service-456', + taskDefinition: 'task:def:arn', + forceNewDeployment: false + }); + expect(mockEcsWaiter).toHaveBeenCalledTimes(0); + expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://console.aws.amazon.com/ecs/home?region=fake-region#/clusters/cluster-789/services/service-456/events"); + }); + + test("should be able to throw an error when the task definition family is not found", async () => { + fs.existsSync.mockReturnValueOnce(false); + + core.getInput = jest + .fn() + .mockReturnValueOnce('task-def-family') + .mockReturnValueOnce('service-456') + .mockReturnValueOnce('cluster-789'); + + mockEcsDescribeTaskDef.mockImplementation(() => { + return { + promise() { + return Promise.resolve({ taskDefinition: null }); + } + }; + }); + + await run() + + expect(mockEcsDescribeTaskDef).toHaveBeenNthCalledWith(1, { + taskDefinition: "task-def-family" + }); + expect(core.setFailed).toHaveBeenCalledTimes(2); + expect(mockEcsRegisterTaskDef).not.toHaveBeenCalled(); + expect(core.setOutput).not.toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); + expect(core.info).not.toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://console.aws.amazon.com/ecs/home?region=fake-region#/clusters/cluster-789/services/service-456/events"); + }); + test('registers the task definition contents and updates the service', async () => { await run(); expect(core.setFailed).toHaveBeenCalledTimes(0);