Skip to content

Commit ffa97ba

Browse files
clareliguoriSteve Winton
and
Steve Winton
authored
Initial action code (#1)
Initial action code Co-Authored-By: Steve Winton <[email protected]>
1 parent bffbc85 commit ffa97ba

8 files changed

+5851
-0
lines changed

.eslintrc.json

+18
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
{
2+
"env": {
3+
"commonjs": true,
4+
"es6": true,
5+
"node": true,
6+
"jest": true
7+
},
8+
"extends": "eslint:recommended",
9+
"globals": {
10+
"Atomics": "readonly",
11+
"SharedArrayBuffer": "readonly"
12+
},
13+
"parserOptions": {
14+
"ecmaVersion": 2018
15+
},
16+
"rules": {
17+
}
18+
}

.gitignore

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
node_modules/
2+
3+
# Editors
4+
.vscode
5+
6+
# Logs
7+
logs
8+
*.log
9+
npm-debug.log*
10+
yarn-debug.log*
11+
yarn-error.log*
12+
13+
# Runtime data
14+
pids
15+
*.pid
16+
*.seed
17+
*.pid.lock
18+
19+
# Directory for instrumented libs generated by jscoverage/JSCover
20+
lib-cov
21+
22+
# Coverage directory used by tools like istanbul
23+
coverage
24+
25+
# nyc test coverage
26+
.nyc_output
27+
28+
# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
29+
.grunt
30+
31+
# Bower dependency directory (https://bower.io/)
32+
bower_components
33+
34+
# node-waf configuration
35+
.lock-wscript
36+
37+
# Compiled binary addons (https://nodejs.org/api/addons.html)
38+
build/Release
39+
40+
# Other Dependency directories
41+
jspm_packages/
42+
43+
# TypeScript v1 declaration files
44+
typings/
45+
46+
# Optional npm cache directory
47+
.npm
48+
49+
# Optional eslint cache
50+
.eslintcache
51+
52+
# Optional REPL history
53+
.node_repl_history
54+
55+
# Output of 'npm pack'
56+
*.tgz
57+
58+
# Yarn Integrity file
59+
.yarn-integrity
60+
61+
# dotenv environment variables file
62+
.env
63+
64+
# next.js build output
65+
.next

README.md

+58
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,64 @@
22

33
Registers an Amazon ECS task definition and deploys it to an ECS service.
44

5+
## Usage
6+
7+
```yaml
8+
- name: Deploy to Amazon ECS
9+
uses: aws/amazon-ecs-deploy-task-definition-for-github-actions@release
10+
with:
11+
task-definition: task-definition.json
12+
service: my-service
13+
cluster: my-cluster
14+
wait-for-service-stability: true
15+
```
16+
17+
Note, the action can also be passed a `task-definition` generated dynamically via [the `aws/amazon-ecs-render-task-definition-for-github-actions` action](https://github.com/aws/amazon-ecs-render-task-definition-for-github-actions).
18+
19+
## Credentials and Region
20+
21+
This action relies on the [default behavior of the AWS SDK for Javascript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) to determine AWS credentials and region. Use the `aws/configure-aws-credentials-for-github-actions` action to configure the GitHub Actions environment with environment variables containing AWS credentials and your desired region.
22+
23+
We recommend following [Amazon IAM best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) for the AWS credentials used in GitHub Actions workflows, including:
24+
* Do not store credentials in your repository's code. You may use [GitHub Actions secrets](https://help.github.com/en/github/automating-your-workflow-with-github-actions/virtual-environments-for-github-actions#creating-and-using-secrets-encrypted-variables) to store credentials and redact credentials from GitHub Actions workflow logs.
25+
* [Create an individual IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#create-iam-users) with an access key for use in GitHub Actions workflows, preferably one per repository. Do not use the AWS account root user access key.
26+
* [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) to the credentials used in GitHub Actions workflows. Grant only the permissions required to perform the actions in your GitHub Actions workflows. See the Permissions section below for the permissions required by this action.
27+
* [Rotate the credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#rotate-credentials) used in GitHub Actions workflows regularly.
28+
* [Monitor the activity](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#keep-a-log) of the credentials used in GitHub Actions workflows.
29+
30+
## Permissions
31+
32+
This action requires the following minimum set of permissions:
33+
34+
```
35+
{
36+
"Version":"2012-10-17",
37+
"Statement":[
38+
{
39+
"Sid":"RegisterTaskDefinition",
40+
"Effect":"Allow",
41+
"Action":[
42+
"ecs:RegisterTaskDefinition"
43+
],
44+
"Resource":"*"
45+
},
46+
{
47+
"Sid":"DeployService",
48+
"Effect":"Allow",
49+
"Action":[
50+
"ecs:UpdateService",
51+
"ecs:DescribeServices"
52+
],
53+
"Resource":[
54+
"arn:aws:ecs:region:aws_account_id:service/cluster-name/service-name"
55+
]
56+
}
57+
]
58+
}
59+
```
60+
61+
Note: the policy above assumes the account has opted in to the ECS long ARN format.
62+
563
## License Summary
664

765
This code is made available under the MIT license.

action.yml

+21
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
1+
name: 'Amazon ECS "Deploy Task Definition" Action for GitHub Actions'
2+
description: 'Registers an Amazon ECS task definition, and deploys it to an ECS service'
3+
inputs:
4+
task-definition:
5+
description: 'The path to the ECS task definition file to register'
6+
required: true
7+
service:
8+
description: 'The name of the ECS service to deploy to. The action will only register the task definition if no service is given.'
9+
required: false
10+
cluster:
11+
description: "The name of the ECS service's cluster. Will default to the 'default' cluster"
12+
required: false
13+
wait-for-service-stability:
14+
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.'
15+
required: false
16+
outputs:
17+
task-definition-arn:
18+
description: 'The ARN of the registered ECS task definition'
19+
runs:
20+
using: 'node12'
21+
main: 'dist/index.js'

index.js

+59
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,59 @@
1+
const path = require('path');
2+
const core = require('@actions/core');
3+
const aws = require('aws-sdk');
4+
5+
async function run() {
6+
try {
7+
const ecs = new aws.ECS();
8+
9+
// Get inputs
10+
const taskDefinitionFile = core.getInput('task-definition', { required: true });
11+
const service = core.getInput('service', { required: false });
12+
const cluster = core.getInput('cluster', { required: false });
13+
const waitForService = core.getInput('wait-for-service-stability', { required: false });
14+
15+
// Register the task definition
16+
core.debug('Registering the task definition');
17+
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
18+
taskDefinitionFile :
19+
path.join(process.env.GITHUB_WORKSPACE, taskDefinitionFile);
20+
const taskDefContents = require(taskDefPath);
21+
const registerResponse = await ecs.registerTaskDefinition(taskDefContents).promise();
22+
const taskDefArn = registerResponse.taskDefinition.taskDefinitionArn;
23+
core.setOutput('task-definition-arn', taskDefArn);
24+
25+
// Update the service with the new task definition
26+
if (service) {
27+
core.debug('Updating the service');
28+
const clusterName = cluster ? cluster : 'default';
29+
await ecs.updateService({
30+
cluster: clusterName,
31+
service: service,
32+
taskDefinition: taskDefArn
33+
}).promise();
34+
35+
// Wait for service stability
36+
if (waitForService && waitForService.toLowerCase() === 'true') {
37+
core.debug('Waiting for the service to become stable');
38+
await ecs.waitFor('servicesStable', {
39+
services: [service],
40+
cluster: clusterName
41+
}).promise();
42+
} else {
43+
core.debug('Not waiting for the service to become stable');
44+
}
45+
} else {
46+
core.debug('Service was not specified, no service updated');
47+
}
48+
}
49+
catch (error) {
50+
core.setFailed(error.message);
51+
}
52+
}
53+
54+
module.exports = run;
55+
56+
/* istanbul ignore next */
57+
if (require.main === module) {
58+
run();
59+
}

index.test.js

+142
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,142 @@
1+
const run = require('.');
2+
const core = require('@actions/core');
3+
4+
jest.mock('@actions/core');
5+
6+
const mockEcsRegisterTaskDef = jest.fn();
7+
const mockEcsUpdateService = jest.fn();
8+
const mockEcsWaiter = jest.fn();
9+
jest.mock('aws-sdk', () => {
10+
return {
11+
ECS: jest.fn(() => ({
12+
registerTaskDefinition: mockEcsRegisterTaskDef,
13+
updateService: mockEcsUpdateService,
14+
waitFor: mockEcsWaiter
15+
}))
16+
};
17+
});
18+
19+
describe('Deploy to ECS', () => {
20+
21+
beforeEach(() => {
22+
jest.clearAllMocks();
23+
24+
core.getInput = jest
25+
.fn()
26+
.mockReturnValueOnce('task-definition.json') // task-definition
27+
.mockReturnValueOnce('service-456') // service
28+
.mockReturnValueOnce('cluster-789'); // cluster
29+
30+
process.env = Object.assign(process.env, { GITHUB_WORKSPACE: __dirname });
31+
32+
jest.mock('./task-definition.json', () => ({ family: 'task-def-family' }), { virtual: true });
33+
34+
mockEcsRegisterTaskDef.mockImplementation((params) => {
35+
return {
36+
promise() {
37+
return Promise.resolve({ taskDefinition: { taskDefinitionArn: 'task:def:arn' } });
38+
}
39+
};
40+
});
41+
42+
mockEcsUpdateService.mockImplementation((params) => {
43+
return {
44+
promise() {
45+
return Promise.resolve({});
46+
}
47+
};
48+
});
49+
50+
mockEcsWaiter.mockImplementation((params) => {
51+
return {
52+
promise() {
53+
return Promise.resolve({});
54+
}
55+
};
56+
});
57+
});
58+
59+
test('registers the task definition contents and updates the service', async () => {
60+
await run();
61+
expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
62+
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
63+
expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, {
64+
cluster: 'cluster-789',
65+
service: 'service-456',
66+
taskDefinition: 'task:def:arn'
67+
});
68+
expect(mockEcsWaiter).toHaveBeenCalledTimes(0);
69+
});
70+
71+
test('registers the task definition contents at an absolute path', async () => {
72+
core.getInput = jest.fn().mockReturnValueOnce('/hello/task-definition.json');
73+
jest.mock('/hello/task-definition.json', () => ({ family: 'task-def-family-absolute-path' }), { virtual: true });
74+
75+
await run();
76+
77+
expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family-absolute-path'});
78+
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
79+
});
80+
81+
test('waits for the service to be stable', async () => {
82+
core.getInput = jest
83+
.fn()
84+
.mockReturnValueOnce('task-definition.json') // task-definition
85+
.mockReturnValueOnce('service-456') // service
86+
.mockReturnValueOnce('cluster-789') // cluster
87+
.mockReturnValueOnce('TRUE'); // wait-for-service-stability
88+
89+
await run();
90+
91+
expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
92+
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
93+
expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, {
94+
cluster: 'cluster-789',
95+
service: 'service-456',
96+
taskDefinition: 'task:def:arn'
97+
});
98+
expect(mockEcsWaiter).toHaveBeenNthCalledWith(1, 'servicesStable', {
99+
services: ['service-456'],
100+
cluster: 'cluster-789'
101+
});
102+
});
103+
104+
test('defaults to the default cluster', async () => {
105+
core.getInput = jest
106+
.fn()
107+
.mockReturnValueOnce('task-definition.json') // task-definition
108+
.mockReturnValueOnce('service-456'); // service
109+
110+
await run();
111+
112+
expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
113+
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
114+
expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, {
115+
cluster: 'default',
116+
service: 'service-456',
117+
taskDefinition: 'task:def:arn'
118+
});
119+
});
120+
121+
test('does not update service if none specified', async () => {
122+
core.getInput = jest
123+
.fn()
124+
.mockReturnValueOnce('task-definition.json'); // task-definition
125+
126+
await run();
127+
128+
expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
129+
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
130+
expect(mockEcsUpdateService).toHaveBeenCalledTimes(0);
131+
});
132+
133+
test('error is caught by core.setFailed', async () => {
134+
mockEcsRegisterTaskDef.mockImplementation(() => {
135+
throw new Error();
136+
});
137+
138+
await run();
139+
140+
expect(core.setFailed).toBeCalled();
141+
});
142+
});

0 commit comments

Comments
 (0)