From 35bd757ac3e72d88713c82eac1443de61777fd7b Mon Sep 17 00:00:00 2001 From: Filipe Date: Tue, 12 Sep 2023 09:36:09 -0300 Subject: [PATCH 1/2] feat: use existing task definition --- action.yml | 2 +- index.js | 57 +++++++++++++++++++++++++++++----------- index.test.js | 72 ++++++++++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 114 insertions(+), 17 deletions(-) diff --git a/action.yml b/action.yml index c8e5b47ee..e6bfd7802 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 service: description: 'The name of the ECS service to deploy to. The action will only register the task definition if no service is given.' diff --git a/index.js b/index.js index 7f759a088..a0abbdca1 100644 --- a/index.js +++ b/index.js @@ -269,23 +269,50 @@ async function run() { const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false'; const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true'; - // 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 a032634b8..0f2d842ea 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); From 48637fd225661d0c7420afb282156a483dcf727b Mon Sep 17 00:00:00 2001 From: Filipe Date: Tue, 12 Sep 2023 09:51:40 -0300 Subject: [PATCH 2/2] docs: modify readme --- README.md | 12 ++++++++++++ 1 file changed, 12 insertions(+) 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.