Skip to content

Commit

Permalink
Merge pull request #8 from agendrix/initial-release
Browse files Browse the repository at this point in the history
Initial release
  • Loading branch information
LouisBoudreau authored Apr 13, 2021
2 parents 0aefe8e + 5e47edb commit 9baf074
Show file tree
Hide file tree
Showing 21 changed files with 2,196 additions and 4,193 deletions.
10 changes: 3 additions & 7 deletions .github/dependabot.yml
Original file line number Diff line number Diff line change
@@ -1,13 +1,9 @@
version: 2
updates:
# Enable version updates for npm
- package-ecosystem: 'npm'
- package-ecosystem: "npm"
# Look for `package.json` and `lock` files in the `root` directory
directory: '/'
directory: "/"
# Check the npm registry for updates every day (weekdays)
schedule:
interval: 'daily'
# Only allow security updates
allowed_updates:
- match:
update_type: "security"
interval: "weekly"
9 changes: 1 addition & 8 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -14,11 +14,4 @@ jobs:
- run: |
yarn
- run: |
yarn all
test: # make sure the action works on a clean machine without building
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- uses: ./
with:
milliseconds: 1000
yarn all
2 changes: 1 addition & 1 deletion .prettierrc.json
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
{
"printWidth": 80,
"printWidth": 120,
"tabWidth": 2,
"useTabs": false,
"semi": true,
Expand Down
136 changes: 39 additions & 97 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,105 +1,47 @@
# Create a JavaScript Action using TypeScript
# Wait for an ECS service deployment to complete

Use this template to bootstrap the creation of a TypeScript action.:rocket:
Wait for an ECS service rolling update to complete and output the outcome.
This action only supports [ECS rolling updates](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/deployment-type-ecs.html).

This template includes compilation support, tests, a validation workflow, publishing, and versioning guidance.
## Requirements

If you are new, there's also a simpler introduction. See the [Hello World JavaScript Action](https://github.com/actions/hello-world-javascript-action)
This actions requires that the `aws-cli` is already configured. The official AWS action could be useful that achieve this configuration. Please refer to: [aws-actions/configure-aws-credentials](https://github.com/aws-actions/configure-aws-credentials) for further details.

## Create an action from this template
See [action.yml](./action.yml) for the list of `inputs` and `outputs`.

Click the `Use this Template` and provide the new repo details for your action

## Code in Main

> First, you'll need to have a reasonably modern version of `node` handy. This won't work with versions older than 9, for instance.
Install the dependencies
```bash
$ yarn install
```

Build the typescript and package it for distribution
```bash
$ yarn build && yarn package
```

Run the tests :heavy_check_mark:
```bash
$ yarn test

PASS ./index.test.js
✓ throws invalid number (3ms)
wait 500 ms (504ms)
test runs (95ms)

...
```

## Change action.yml

The action.yml contains defines the inputs and output for your action.

Update the action.yml with your name, description, inputs and outputs for your action.

See the [documentation](https://help.github.com/en/articles/metadata-syntax-for-github-actions)

## Change the Code

Most toolkit and CI/CD operations involve async operations so the action is run in an async function.

```javascript
import * as core from '@actions/core';
...

async function run() {
try {
...
}
catch (error) {
core.setFailed(error.message);
}
}

run()
```

See the [toolkit documentation](https://github.com/actions/toolkit/blob/master/README.md#packages) for the various packages.

## Publish to a distribution branch

Actions are run from GitHub repos so we will checkin the packed dist folder.

Then run [ncc](https://github.com/zeit/ncc) and push the results:
```bash
$ yarn package
$ git add dist
$ git commit -a -m "prod dependencies"
$ git push origin releases/v1
```

Note: We recommend using the `--license` option for ncc, which will create a license file for all of the production node modules used in your project.

Your action is now published! :rocket:

See the [versioning documentation](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md)

## Validate

You can now validate the action by referencing `./` in a workflow in your repo (see [test.yml](.github/workflows/test.yml))
## Example usage

```yaml
uses: ./
with:
milliseconds: 1000
deploy-ecs-service-task-definition:
name: Deploy <SERVICE> service
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v2

- name: Configure AWS credentials
uses: aws-actions/configure-aws-credentials@v1
with:
aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }}
aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }}
aws-region: ${{ env.AWS_REGION }}

- name: Deploy service to Amazon ECS
id: deploy-task-definition
uses: aws-actions/amazon-ecs-deploy-task-definition@v1
with:
cluster: ${{ env.CLUSTER_NAME }}
service: ${{ env.SERVICE_NAME }}
task-definition: ${{ env.TASK_DEFINITION }}
wait-for-service-stability: false

- name: Wait for ${{ env.SERVICE_NAME }} service deployment to complete
id: wait-for-ecs-service-deployment
uses: agendrix/wait-for-ecs-service-deployment-action@<VERSION>
with:
cluster: ${{ env.CLUSTER_NAME }}
service: ${{ env.SERVICE_NAME }}
task-definition-arn: steps.deploy-task-definition.outputs.task-definition-arn
deployment-timeout-minutes: ${{ env.SERVICE_DEPLOYMENT_TIMEOUT }}
```
See the [actions tab](https://github.com/actions/typescript-action/actions) for runs of this action! :rocket:
## Usage:
After testing you can [create a v1 tag](https://github.com/actions/toolkit/blob/master/docs/action-versioning.md) to reference the stable and latest V1 action
## Helpers
If you created helpers that you consider could benefit to other actions, please consider contributing by adding them to the `helpers` folder.
182 changes: 157 additions & 25 deletions __tests__/main.test.ts
Original file line number Diff line number Diff line change
@@ -1,28 +1,160 @@
import { wait } from "../src/wait";
import * as process from "process";
import * as cp from "child_process";
import * as path from "path";

test("throws invalid number", async () => {
const input = parseInt("foo", 10);
await expect(wait(input)).rejects.toThrow("milliseconds not a number");
});
import { DeploymentOutcome, DeploymentStatus, RolloutState } from "../src/ecs/types";
import fetchDeployments from "../src/ecs/fetchDeployments";
import waitForDeploymentOutcome from "../src/ecs/waitForDeploymentOutcome";
import isServiceStable from "../src/ecs/isServiceStable";

test("wait 500 ms", async () => {
const start = new Date();
await wait(500);
const end = new Date();
var delta = Math.abs(end.getTime() - start.getTime());
expect(delta).toBeGreaterThan(450);
});
jest.mock("../src/ecs/fetchDeployments");
let mockedFetchDeployments = fetchDeployments as jest.Mock;

jest.mock("../src/ecs/isServiceStable");
const mockedIsServiceStable = isServiceStable as jest.Mock;
mockedIsServiceStable.mockReturnValue(true);

const TASK_DEFINITION_ARN = "arn:aws:ecs:region:<account-number>:task-definition/family:123";
const CLUSTER = "cluster";
const SERVICE = "service";

describe("waitForDeploymentOutcome", () => {
afterEach(() => mockedFetchDeployments.mockRestore());

test("It must return 'success' if deployment completes and service is stable", async () => {
const deployment = {
id: "1",
status: DeploymentStatus.PRIMARY,
taskDefinitionArn: TASK_DEFINITION_ARN,
rolloutState: RolloutState.IN_PROGRESS
};
const initialDeploymentsState = [deployment];
const finalDeploymentsState = [{ ...deployment, rolloutState: RolloutState.COMPLETED }];
mockedFetchDeployments.mockReturnValueOnce(initialDeploymentsState).mockReturnValueOnce(finalDeploymentsState);
expect(
await waitForDeploymentOutcome(
CLUSTER,
SERVICE,
TASK_DEFINITION_ARN,
setTimeout(() => {}),
1
)
).toBe(DeploymentOutcome.SUCCESS);
mockedFetchDeployments.mockRestore();
});

test("It must handle two ongoing deployments with same task definition (force new deployment)", async () => {
const deploymentA = {
id: "1",
status: DeploymentStatus.PRIMARY,
taskDefinitionArn: TASK_DEFINITION_ARN,
rolloutState: RolloutState.IN_PROGRESS
};
const initialDeploymentsState = [{ ...deploymentA }];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(initialDeploymentsState);

deploymentA.status = DeploymentStatus.ACTIVE;
const deploymentB = {
id: "2",
status: DeploymentStatus.PRIMARY,
taskDefinitionArn: TASK_DEFINITION_ARN,
rolloutState: RolloutState.IN_PROGRESS
};
const middleDeploymentState = [{ ...deploymentA }, { ...deploymentB }];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(middleDeploymentState);

deploymentB.rolloutState = RolloutState.COMPLETED;
const finalDeploymentsState = [{ ...deploymentA }, { ...deploymentB }];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(finalDeploymentsState);

expect(
await waitForDeploymentOutcome(
CLUSTER,
SERVICE,
TASK_DEFINITION_ARN,
setTimeout(() => {}),
1
)
).toBe(DeploymentOutcome.SUCCESS);
});

test("It must restart timeout if a new deployment with same task definition is registered (force new deployment)", async () => {
const deploymentA = {
id: "1",
status: DeploymentStatus.PRIMARY,
taskDefinitionArn: TASK_DEFINITION_ARN,
rolloutState: RolloutState.IN_PROGRESS
};
const initialDeploymentsState = [{ ...deploymentA }];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(initialDeploymentsState);

deploymentA.status = DeploymentStatus.ACTIVE;
const deploymentB = {
id: "2",
status: DeploymentStatus.PRIMARY,
taskDefinitionArn: TASK_DEFINITION_ARN,
rolloutState: RolloutState.IN_PROGRESS
};
const middleDeploymentState = [{ ...deploymentA }, { ...deploymentB }];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(middleDeploymentState);

deploymentB.rolloutState = RolloutState.COMPLETED;
const finalDeploymentsState = [{ ...deploymentA }, { ...deploymentB }];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(finalDeploymentsState);

const timeout = setTimeout(() => {}, 1000);
timeout.refresh = jest.fn();
await waitForDeploymentOutcome(CLUSTER, SERVICE, TASK_DEFINITION_ARN, timeout, 1);

expect(timeout.refresh).toHaveBeenCalled();
});

test("It must return 'skipped' if another deployment is enqueued", async () => {
const deploymentA = {
id: "1",
status: DeploymentStatus.PRIMARY,
taskDefinitionArn: TASK_DEFINITION_ARN,
rolloutState: RolloutState.IN_PROGRESS
};
const initialDeploymentsState = [{ ...deploymentA }];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(initialDeploymentsState);

deploymentA.status = DeploymentStatus.ACTIVE;
const deploymentB = {
id: "2",
status: DeploymentStatus.PRIMARY,
taskDefinitionArn: TASK_DEFINITION_ARN.slice(0, -1),
rolloutState: RolloutState.IN_PROGRESS
};
const finalDeploymentsState = [{ ...deploymentA }, { ...deploymentB }];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(finalDeploymentsState);

expect(
await waitForDeploymentOutcome(
CLUSTER,
SERVICE,
TASK_DEFINITION_ARN,
setTimeout(() => {}),
1
)
).toBe(DeploymentOutcome.SKIPPED);
});

// shows how the runner will run a javascript action with env / stdout protocol
test("test runs", () => {
process.env["INPUT_MILLISECONDS"] = "500";
const np = process.execPath;
const ip = path.join(__dirname, "..", "lib", "src", "main.js");
const options: cp.ExecFileSyncOptions = {
env: process.env
};
console.log(cp.execFileSync(np, [ip], options).toString());
test("It must timeout service deployment never becomes stable", async () => {
const deployments = [
{
id: "1",
status: DeploymentStatus.PRIMARY,
taskDefinitionArn: TASK_DEFINITION_ARN,
rolloutState: RolloutState.IN_PROGRESS
}
];
mockedFetchDeployments = mockedFetchDeployments.mockReturnValueOnce(deployments);
expect(async () => {
const deploymentTimeout = setTimeout(() => {
throw new Error("timeout");
}, 1000);
try {
await waitForDeploymentOutcome(CLUSTER, SERVICE, TASK_DEFINITION_ARN, deploymentTimeout);
} catch (e) {
expect(e).toMatch("timeout");
}
});
});
});
Loading

0 comments on commit 9baf074

Please sign in to comment.