Skip to content

Commit

Permalink
Initial action code (#1)
Browse files Browse the repository at this point in the history
Initial action code

Co-Authored-By: Steve Winton <[email protected]>
  • Loading branch information
clareliguori and Steve Winton authored Nov 1, 2019
1 parent bffbc85 commit ffa97ba
Show file tree
Hide file tree
Showing 8 changed files with 5,851 additions and 0 deletions.
18 changes: 18 additions & 0 deletions .eslintrc.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
{
"env": {
"commonjs": true,
"es6": true,
"node": true,
"jest": true
},
"extends": "eslint:recommended",
"globals": {
"Atomics": "readonly",
"SharedArrayBuffer": "readonly"
},
"parserOptions": {
"ecmaVersion": 2018
},
"rules": {
}
}
65 changes: 65 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
node_modules/

# Editors
.vscode

# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*

# Runtime data
pids
*.pid
*.seed
*.pid.lock

# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov

# Coverage directory used by tools like istanbul
coverage

# nyc test coverage
.nyc_output

# Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files)
.grunt

# Bower dependency directory (https://bower.io/)
bower_components

# node-waf configuration
.lock-wscript

# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release

# Other Dependency directories
jspm_packages/

# TypeScript v1 declaration files
typings/

# Optional npm cache directory
.npm

# Optional eslint cache
.eslintcache

# Optional REPL history
.node_repl_history

# Output of 'npm pack'
*.tgz

# Yarn Integrity file
.yarn-integrity

# dotenv environment variables file
.env

# next.js build output
.next
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,64 @@

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

## Usage

```yaml
- name: Deploy to Amazon ECS
uses: aws/amazon-ecs-deploy-task-definition-for-github-actions@release
with:
task-definition: task-definition.json
service: my-service
cluster: my-cluster
wait-for-service-stability: true
```
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).

## Credentials and Region

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.

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:
* 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.
* [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.
* [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.
* [Rotate the credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#rotate-credentials) used in GitHub Actions workflows regularly.
* [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.

## Permissions

This action requires the following minimum set of permissions:

```
{
"Version":"2012-10-17",
"Statement":[
{
"Sid":"RegisterTaskDefinition",
"Effect":"Allow",
"Action":[
"ecs:RegisterTaskDefinition"
],
"Resource":"*"
},
{
"Sid":"DeployService",
"Effect":"Allow",
"Action":[
"ecs:UpdateService",
"ecs:DescribeServices"
],
"Resource":[
"arn:aws:ecs:region:aws_account_id:service/cluster-name/service-name"
]
}
]
}
```

Note: the policy above assumes the account has opted in to the ECS long ARN format.

## License Summary

This code is made available under the MIT license.
21 changes: 21 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
name: 'Amazon ECS "Deploy Task Definition" Action for GitHub Actions'
description: 'Registers an Amazon ECS task definition, and deploys it to an ECS service'
inputs:
task-definition:
description: 'The path to the ECS task definition file to register'
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.'
required: false
cluster:
description: "The name of the ECS service's cluster. Will default to the 'default' cluster"
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
outputs:
task-definition-arn:
description: 'The ARN of the registered ECS task definition'
runs:
using: 'node12'
main: 'dist/index.js'
59 changes: 59 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
const path = require('path');
const core = require('@actions/core');
const aws = require('aws-sdk');

async function run() {
try {
const ecs = new aws.ECS();

// Get inputs
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 });

// Register the task definition
core.debug('Registering the task definition');
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
taskDefinitionFile :
path.join(process.env.GITHUB_WORKSPACE, taskDefinitionFile);
const taskDefContents = require(taskDefPath);
const registerResponse = await ecs.registerTaskDefinition(taskDefContents).promise();
const taskDefArn = registerResponse.taskDefinition.taskDefinitionArn;
core.setOutput('task-definition-arn', taskDefArn);

// Update the service with the new task definition
if (service) {
core.debug('Updating the service');
const clusterName = cluster ? cluster : 'default';
await ecs.updateService({
cluster: clusterName,
service: service,
taskDefinition: taskDefArn
}).promise();

// Wait for service stability
if (waitForService && waitForService.toLowerCase() === 'true') {
core.debug('Waiting for the service to become stable');
await ecs.waitFor('servicesStable', {
services: [service],
cluster: clusterName
}).promise();
} else {
core.debug('Not waiting for the service to become stable');
}
} else {
core.debug('Service was not specified, no service updated');
}
}
catch (error) {
core.setFailed(error.message);
}
}

module.exports = run;

/* istanbul ignore next */
if (require.main === module) {
run();
}
142 changes: 142 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,142 @@
const run = require('.');
const core = require('@actions/core');

jest.mock('@actions/core');

const mockEcsRegisterTaskDef = jest.fn();
const mockEcsUpdateService = jest.fn();
const mockEcsWaiter = jest.fn();
jest.mock('aws-sdk', () => {
return {
ECS: jest.fn(() => ({
registerTaskDefinition: mockEcsRegisterTaskDef,
updateService: mockEcsUpdateService,
waitFor: mockEcsWaiter
}))
};
});

describe('Deploy to ECS', () => {

beforeEach(() => {
jest.clearAllMocks();

core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json') // task-definition
.mockReturnValueOnce('service-456') // service
.mockReturnValueOnce('cluster-789'); // cluster

process.env = Object.assign(process.env, { GITHUB_WORKSPACE: __dirname });

jest.mock('./task-definition.json', () => ({ family: 'task-def-family' }), { virtual: true });

mockEcsRegisterTaskDef.mockImplementation((params) => {
return {
promise() {
return Promise.resolve({ taskDefinition: { taskDefinitionArn: 'task:def:arn' } });
}
};
});

mockEcsUpdateService.mockImplementation((params) => {
return {
promise() {
return Promise.resolve({});
}
};
});

mockEcsWaiter.mockImplementation((params) => {
return {
promise() {
return Promise.resolve({});
}
};
});
});

test('registers the task definition contents and updates the service', async () => {
await run();
expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, {
cluster: 'cluster-789',
service: 'service-456',
taskDefinition: 'task:def:arn'
});
expect(mockEcsWaiter).toHaveBeenCalledTimes(0);
});

test('registers the task definition contents at an absolute path', async () => {
core.getInput = jest.fn().mockReturnValueOnce('/hello/task-definition.json');
jest.mock('/hello/task-definition.json', () => ({ family: 'task-def-family-absolute-path' }), { virtual: true });

await run();

expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family-absolute-path'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
});

test('waits for the service to be stable', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json') // task-definition
.mockReturnValueOnce('service-456') // service
.mockReturnValueOnce('cluster-789') // cluster
.mockReturnValueOnce('TRUE'); // wait-for-service-stability

await run();

expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, {
cluster: 'cluster-789',
service: 'service-456',
taskDefinition: 'task:def:arn'
});
expect(mockEcsWaiter).toHaveBeenNthCalledWith(1, 'servicesStable', {
services: ['service-456'],
cluster: 'cluster-789'
});
});

test('defaults to the default cluster', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json') // task-definition
.mockReturnValueOnce('service-456'); // service

await run();

expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, {
cluster: 'default',
service: 'service-456',
taskDefinition: 'task:def:arn'
});
});

test('does not update service if none specified', async () => {
core.getInput = jest
.fn()
.mockReturnValueOnce('task-definition.json'); // task-definition

await run();

expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family'});
expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn');
expect(mockEcsUpdateService).toHaveBeenCalledTimes(0);
});

test('error is caught by core.setFailed', async () => {
mockEcsRegisterTaskDef.mockImplementation(() => {
throw new Error();
});

await run();

expect(core.setFailed).toBeCalled();
});
});
Loading

0 comments on commit ffa97ba

Please sign in to comment.