Skip to content

Commit d86ee7d

Browse files
committed
implemented unit tests
1 parent 4669e8a commit d86ee7d

File tree

7 files changed

+428
-18
lines changed

7 files changed

+428
-18
lines changed

.idea/aws.xml

Lines changed: 14 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/inspectionProfiles/Project_Default.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/vcs.xml

Lines changed: 6 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

.idea/workspace.xml

Lines changed: 47 additions & 13 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

action.yml

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,30 @@ inputs:
4040
force-new-deployment:
4141
description: 'Whether to force a new deployment of the service. Valid value is "true". Will default to not force a new deployment.'
4242
required: false
43+
44+
# New inputs for running a single task outside of a service
45+
run-task:
46+
description: 'Whether to run the task outside of an ECS service. Task will run before the service is updated if both are provided. Will default to not run.'
47+
required: false
48+
run-task-container-overrides:
49+
description: 'A JSON array of container override objects which should applied when running a task outside of a service. Warning: Do not expose this field to untrusted inputs. More details: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerOverride.html'
50+
required: false
51+
run-task-security-groups:
52+
description: 'A comma-separated list of security group IDs to assign to a task when run outside of a service. Will default to none.'
53+
required: false
54+
run-task-subnets:
55+
description: 'A comma-separated list of subnet IDs to assign to a task when run outside of a service. Will default to none.'
56+
required: false
57+
run-task-launch-type:
58+
description: "ECS launch type for tasks run outside of a service. Valid values are 'FARGATE' or 'EC2'. Will default to 'FARGATE'."
59+
required: false
60+
run-task-started-by:
61+
description: "A name to use for the startedBy tag when running a task outside of a service. Will default to 'GitHub-Actions'."
62+
required: false
63+
wait-for-task-stopped:
64+
description: 'Whether to wait for the task to stop when running it outside of a service. Will default to not wait.'
65+
required: false
66+
#back to normal inputs
4367
outputs:
4468
task-definition-arn:
4569
description: 'The ARN of the registered ECS task definition'

index.js

Lines changed: 120 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
const path = require('path');
22
const core = require('@actions/core');
33
const { CodeDeploy, waitUntilDeploymentSuccessful } = require('@aws-sdk/client-codedeploy');
4-
const { ECS, waitUntilServicesStable } = require('@aws-sdk/client-ecs');
4+
const { ECS, waitUntilServicesStable, waitUntilTasksStopped } = require('@aws-sdk/client-ecs');
55
const yaml = require('yaml');
66
const fs = require('fs');
77
const crypto = require('crypto');
@@ -21,6 +21,112 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
2121
'registeredBy'
2222
];
2323

24+
//Code to run task outside of a service aka (also known as) a one-off task
25+
26+
async function runTask(ecs, clusterName, taskDefArn, waitForMinutes) {
27+
core.info('Running task')
28+
29+
const waitForTask = core.getInput('wait-for-task-stopped', { required: false }) || 'false';
30+
const startedBy = core.getInput('run-task-started-by', { required: false }) || 'GitHub-Actions';
31+
const launchType = core.getInput('run-task-launch-type', { required: false }) || 'FARGATE';
32+
const subnetIds = core.getInput('run-task-subnets', { required: false }) || '';
33+
const securityGroupIds = core.getInput('run-task-security-groups', { required: false }) || '';
34+
const containerOverrides = JSON.parse(core.getInput('run-task-container-overrides', { required: false }) || '[]');
35+
let awsvpcConfiguration = {}
36+
37+
38+
if (subnetIds != "") {
39+
awsvpcConfiguration["subnets"] = subnetIds.split(',')
40+
}
41+
42+
if (securityGroupIds != "") {
43+
awsvpcConfiguration["securityGroups"] = securityGroupIds.split(',')
44+
}
45+
46+
const runTaskResponse = await ecs.runTask({
47+
startedBy: startedBy,
48+
cluster: clusterName,
49+
taskDefinition: taskDefArn,
50+
overrides: {
51+
containerOverrides: containerOverrides
52+
},
53+
launchType: launchType,
54+
networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? {} : { awsvpcConfiguration: awsvpcConfiguration }
55+
});
56+
57+
core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`)
58+
59+
const taskArns = runTaskResponse.tasks.map(task => task.taskArn);
60+
core.setOutput('run-task-arn', taskArns);
61+
62+
const region = await ecs.config.region();
63+
const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
64+
65+
core.info(`Task running: https://${consoleHostname}/ecs/home?region=${region}#/clusters/${clusterName}/tasks`);
66+
67+
68+
if (runTaskResponse.failures && runTaskResponse.failures.length > 0) {
69+
const failure = runTaskResponse.failures[0];
70+
throw new Error(`${failure.arn} is ${failure.reason}`);
71+
}
72+
73+
// Wait for task to end
74+
if (waitForTask && waitForTask.toLowerCase() === "true") {
75+
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes)
76+
await tasksExitCode(ecs, clusterName, taskArns)
77+
} else {
78+
core.debug('Not waiting for the task to stop');
79+
}
80+
}
81+
82+
// Poll tasks until they enter a stopped state
83+
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
84+
if (waitForMinutes > MAX_WAIT_MINUTES) {
85+
waitForMinutes = MAX_WAIT_MINUTES;
86+
}
87+
88+
core.info(`Waiting for tasks to stop. Will wait for ${waitForMinutes} minutes`);
89+
90+
const waitTaskResponse = await waitUntilTasksStopped({
91+
client: ecs,
92+
minDelay: WAIT_DEFAULT_DELAY_SEC,
93+
maxWaitTime: waitForMinutes * 60,
94+
}, {
95+
cluster: clusterName,
96+
tasks: taskArns,
97+
});
98+
99+
core.debug(`Run task response ${JSON.stringify(waitTaskResponse)}`);
100+
core.info('All tasks have stopped.');
101+
}
102+
103+
// Check a task's exit code and fail the job on error
104+
async function tasksExitCode(ecs, clusterName, taskArns) {
105+
const describeResponse = await ecs.describeTasks({
106+
cluster: clusterName,
107+
tasks: taskArns
108+
});
109+
110+
const containers = [].concat(...describeResponse.tasks.map(task => task.containers))
111+
const exitCodes = containers.map(container => container.exitCode)
112+
const reasons = containers.map(container => container.reason)
113+
114+
const failuresIdx = [];
115+
116+
exitCodes.filter((exitCode, index) => {
117+
if (exitCode !== 0) {
118+
failuresIdx.push(index)
119+
}
120+
})
121+
122+
const failures = reasons.filter((_, index) => failuresIdx.indexOf(index) !== -1)
123+
if (failures.length > 0) {
124+
throw new Error(`Run task failed: ${JSON.stringify(failures)}`);
125+
}
126+
}
127+
128+
129+
24130
// Deploy to a service that uses the 'ECS' deployment controller
25131
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount) {
26132
core.debug('Updating the service');
@@ -225,7 +331,7 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
225331
};
226332
// If it hasn't been set then we don't even want to pass it to the api call to maintain previous behaviour.
227333
if (codeDeployDescription) {
228-
deploymentParams.description = codeDeployDescription
334+
deploymentParams.description = (codeDeployDescription.length <= 512) ? codeDeployDescription : `${codeDeployDescription.substring(0,511)}…`;
229335
}
230336
if (codeDeployConfig) {
231337
deploymentParams.deploymentConfigName = codeDeployConfig
@@ -301,9 +407,20 @@ async function run() {
301407
const taskDefArn = registerResponse.taskDefinition.taskDefinitionArn;
302408
core.setOutput('task-definition-arn', taskDefArn);
303409

410+
411+
// Run the task outside of the service
412+
const clusterName = cluster ? cluster : 'default';
413+
const shouldRunTaskInput = core.getInput('run-task', { required: false }) || 'false';
414+
const shouldRunTask = shouldRunTaskInput.toLowerCase() === 'true';
415+
core.debug(`shouldRunTask: ${shouldRunTask}`);
416+
if (shouldRunTask) {
417+
core.debug("Running ad-hoc task...");
418+
await runTask(ecs, clusterName, taskDefArn, waitForMinutes);
419+
}
420+
304421
// Update the service with the new task definition
305422
if (service) {
306-
const clusterName = cluster ? cluster : 'default';
423+
//const clusterName = cluster ? cluster : 'default';
307424

308425
// Determine the deployment controller
309426
const describeResponse = await ecs.describeServices({

0 commit comments

Comments
 (0)