Skip to content

Commit e87445e

Browse files
committed
"Added support for running ECS tasks outside of a service, updated ECS service update logic, and made minor changes to CodeDeploy deployment description and task exit code handling."
1 parent a4c9569 commit e87445e

File tree

1 file changed

+138
-7
lines changed

1 file changed

+138
-7
lines changed

dist/index.js

Lines changed: 138 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,10 +7,11 @@
77
const path = __nccwpck_require__(1017);
88
const core = __nccwpck_require__(2186);
99
const { CodeDeploy, waitUntilDeploymentSuccessful } = __nccwpck_require__(6692);
10-
const { ECS, waitUntilServicesStable } = __nccwpck_require__(8209);
10+
const { ECS, waitUntilServicesStable, waitUntilTasksStopped } = __nccwpck_require__(8209);
1111
const yaml = __nccwpck_require__(4083);
1212
const fs = __nccwpck_require__(7147);
1313
const crypto = __nccwpck_require__(6113);
14+
const { default: cluster } = __nccwpck_require__(5001);
1415

1516
const MAX_WAIT_MINUTES = 360; // 6 hours
1617
const WAIT_DEFAULT_DELAY_SEC = 15;
@@ -27,15 +28,124 @@ const IGNORED_TASK_DEFINITION_ATTRIBUTES = [
2728
'registeredBy'
2829
];
2930

31+
//Code to run task outside of a service aka (also known as) a one-off task
32+
async function runTask(ecs,clusterName, taskDefArn, waitForMinutes) {
33+
core.info('Running task')
34+
35+
const waitForTask = core.getInput('wait-for-task-stopped', { required: false }) || 'false';
36+
const startedBy = core.getInput('run-task-started-by', { required: false }) || 'GitHub-Actions';
37+
const launchType = core.getInput('run-task-launch-type', { required: false })|| 'FARGATE';
38+
const subnetIds = core.getInput('run-task-subnets', { required: false }) || '';
39+
const securityGroupIds = core.getInput('run-task-security-groups', { required: false }) || '';
40+
const containerOverrides = JSON.parse(core.getInput('run-task-container-overrides', { required: false }) || '[]');
41+
42+
let awsvpcConfiguration = {}
43+
44+
45+
if (subnetIds != "") {
46+
awsvpcConfiguration["subnets"] = subnetIds.split(',')
47+
}
48+
49+
if (securityGroupIds != "") {
50+
awsvpcConfiguration["securityGroups"] = securityGroupIds.split(',')
51+
}
52+
53+
const runTaskResponse = await ecs.runTask({
54+
startedBy: startedBy,
55+
cluster: clusterName,
56+
taskDefinition: taskDefArn,
57+
overrides: {
58+
containerOverrides: containerOverrides
59+
},
60+
launchType: launchType,
61+
networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? {} : { awsvpcConfiguration: awsvpcConfiguration },
62+
63+
});
64+
65+
66+
core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`)
67+
68+
const taskArns = runTaskResponse.tasks.map(task => task.taskArn);
69+
core.setOutput('run-task-arn', taskArns);
70+
71+
const region = await ecs.config.region();
72+
const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com';
73+
74+
core.info(`Task running: https://${consoleHostname}/ecs/home?region=${region}#/clusters/${clusterName}/tasks`);
75+
76+
77+
if (runTaskResponse.failures && runTaskResponse.failures.length > 0) {
78+
const failure = runTaskResponse.failures[0];
79+
throw new Error(`${failure.arn} is ${failure.reason}`);
80+
}
81+
82+
// Wait for task to end
83+
if (waitForTask && waitForTask.toLowerCase() === "true") {
84+
await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes)
85+
await tasksExitCode(ecs, clusterName, taskArns)
86+
} else {
87+
core.debug('Not waiting for the task to stop');
88+
}
89+
}
90+
91+
// Poll tasks until they enter a stopped state
92+
async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) {
93+
if (waitForMinutes > MAX_WAIT_MINUTES) {
94+
waitForMinutes = MAX_WAIT_MINUTES;
95+
}
96+
97+
core.info(`Waiting for tasks to stop. Will wait for ${waitForMinutes} minutes`);
98+
99+
const waitTaskResponse = await waitUntilTasksStopped({
100+
client: ecs,
101+
minDelay: WAIT_DEFAULT_DELAY_SEC,
102+
maxWaitTime: waitForMinutes * 60,
103+
}, {
104+
cluster: clusterName,
105+
tasks: taskArns,
106+
});
107+
108+
core.debug(`Run task response ${JSON.stringify(waitTaskResponse)}`);
109+
core.info('All tasks have stopped.');
110+
}
111+
112+
// Check a task's exit code and fail the job on error
113+
async function tasksExitCode(ecs, clusterName, taskArns) {
114+
const describeResponse = await ecs.describeTasks({
115+
cluster: clusterName,
116+
tasks: taskArns
117+
});
118+
119+
const containers = [].concat(...describeResponse.tasks.map(task => task.containers))
120+
const exitCodes = containers.map(container => container.exitCode)
121+
const reasons = containers.map(container => container.reason)
122+
123+
const failuresIdx = [];
124+
125+
exitCodes.filter((exitCode, index) => {
126+
if (exitCode !== 0) {
127+
failuresIdx.push(index)
128+
}
129+
})
130+
131+
const failures = reasons.filter((_, index) => failuresIdx.indexOf(index) !== -1)
132+
if (failures.length > 0) {
133+
throw new Error(`Run task failed: ${JSON.stringify(failures)}`);
134+
}
135+
}
136+
137+
30138
// Deploy to a service that uses the 'ECS' deployment controller
31139
async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount) {
32140
core.debug('Updating the service');
141+
33142
let params = {
34143
cluster: clusterName,
35144
service: service,
36145
taskDefinition: taskDefArn,
37-
forceNewDeployment: forceNewDeployment
146+
forceNewDeployment: forceNewDeployment,
38147
};
148+
39149
// Add the desiredCount property only if it is defined and a number.
40150
if (!isNaN(desiredCount) && desiredCount !== undefined) {
41151
params.desiredCount = desiredCount;
@@ -231,7 +341,7 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
231341
};
232342
// If it hasn't been set then we don't even want to pass it to the api call to maintain previous behaviour.
233343
if (codeDeployDescription) {
234-
deploymentParams.description = codeDeployDescription
344+
deploymentParams.description = (codeDeployDescription.length <= 512) ? codeDeployDescription : `${codeDeployDescription.substring(0,511)}…`;
235345
}
236346
if (codeDeployConfig) {
237347
deploymentParams.deploymentConfigName = codeDeployConfig
@@ -279,15 +389,15 @@ async function run() {
279389
const cluster = core.getInput('cluster', { required: false });
280390
const waitForService = core.getInput('wait-for-service-stability', { required: false });
281391
let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30;
392+
282393
if (waitForMinutes > MAX_WAIT_MINUTES) {
283394
waitForMinutes = MAX_WAIT_MINUTES;
284395
}
285396

286397
const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false';
287398
const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true';
288399
const desiredCount = parseInt((core.getInput('desired-count', {required: false})));
289-
290-
400+
291401
// Register the task definition
292402
core.debug('Registering the task definition');
293403
const taskDefPath = path.isAbsolute(taskDefinitionFile) ?
@@ -306,11 +416,21 @@ async function run() {
306416
}
307417
const taskDefArn = registerResponse.taskDefinition.taskDefinitionArn;
308418
core.setOutput('task-definition-arn', taskDefArn);
419+
420+
// Run the task outside of the service
421+
const clusterName = cluster ? cluster : 'default';
422+
const shouldRunTaskInput = core.getInput('run-task', { required: false }) || 'false';
423+
const shouldRunTask = shouldRunTaskInput.toLowerCase() === 'true';
424+
core.debug(`shouldRunTask: ${shouldRunTask}`);
425+
426+
//run task
427+
if (shouldRunTask) {
428+
core.debug("Running one-off task...");
429+
await runTask(ecs, clusterName, taskDefArn, waitForMinutes);
430+
}
309431

310432
// Update the service with the new task definition
311433
if (service) {
312-
const clusterName = cluster ? cluster : 'default';
313-
314434
// Determine the deployment controller
315435
const describeResponse = await ecs.describeServices({
316436
services: [service],
@@ -329,9 +449,12 @@ async function run() {
329449

330450
if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') {
331451
// Service uses the 'ECS' deployment controller, so we can call UpdateService
452+
core.debug('Updating service...');
332453
await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount);
454+
333455
} else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') {
334456
// Service uses CodeDeploy, so we should start a CodeDeploy deployment
457+
core.debug('Deploying service in the default cluster');
335458
await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes);
336459
} else {
337460
throw new Error(`Unsupported deployment controller: ${serviceResponse.deploymentController.type}`);
@@ -61742,6 +61865,14 @@ module.exports = require("child_process");
6174261865

6174361866
/***/ }),
6174461867

61868+
/***/ 5001:
61869+
/***/ ((module) => {
61870+
61871+
"use strict";
61872+
module.exports = require("cluster");
61873+
61874+
/***/ }),
61875+
6174561876
/***/ 6206:
6174661877
/***/ ((module) => {
6174761878

0 commit comments

Comments
 (0)