Skip to content

Commit ede1821

Browse files
authored
feat: Add support for maxAttempts to allow retrying after failure (#750)
* Add maxAttempts in API calls * Define parameter and update readme * Package * Cleanup docs * Refactor for readability * Adjust tests for max-retries * Fix tests * remove duplicate mock setup in test * package
1 parent 9182efe commit ede1821

File tree

5 files changed

+84
-37
lines changed

5 files changed

+84
-37
lines changed

README.md

Lines changed: 35 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -228,7 +228,7 @@ Running a service requires the following minimum set of permissions:
228228
]
229229
}
230230
```
231-
231+
232232
Running a one-off/stand-alone task requires the following minimum set of permissions:
233233
```json
234234
{
@@ -342,7 +342,7 @@ In the following example, the service would not be updated until the ad-hoc task
342342
wait-for-task-stopped: true
343343
```
344344

345-
Overrides and VPC networking options are available as well. See [action.yml](action.yml) for more details. The `FARGATE`
345+
Overrides and VPC networking options are available as well. See [action.yml](action.yml) for more details. The `FARGATE`
346346
launch type requires `awsvpc` network mode in your task definition and you must specify a network configuration.
347347

348348
### Tags
@@ -369,21 +369,44 @@ To tag your tasks:
369369

370370
## Preserving Empty Values with keep-null-value-keys
371371

372-
By default, this action removes empty string, array, and object values from the ECS task definition before registering it. If you want to preserve empty values for specific keys, use the `keep-null-value-keys` input. This is a comma-separated list of key names. When specified, any empty value for those keys will be kept in the registered task definition.
372+
By default, this action removes empty string, array, and object values from the ECS task definition before registering it. This behavior aligns with ECS defaults but can be problematic when you explicitly want to override a non-null default value with an empty or null value.
373+
374+
To preserve empty values for specific keys, use the keep-null-value-keys input. This is a comma-separated list of key names. When specified, empty values for those keys will be retained in the registered task definition.
375+
376+
This is particularly useful in cases where ECS or a previous task definition applies a default value and you want to explicitly unset it.
373377

374378
**Example:**
375379

376-
```yaml
377-
- name: Deploy to Amazon ECS
378-
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
379-
with:
380-
task-definition: task-definition.json
381-
service: my-service
382-
cluster: my-cluster
383-
keep-null-value-keys: tag,command,placementConstraints
380+
```
381+
- name: Deploy to Amazon ECS
382+
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
383+
with:
384+
task-definition: task-definition.json
385+
service: my-service
386+
cluster: my-cluster
387+
keep-null-value-keys: tag,command,placementConstraints
388+
wait-for-service-stability: true
389+
```
390+
391+
## Retries
392+
393+
To automatically retry a failed task definition deployment, use the max-retries input. This controls how many times the action will attempt to register and deploy the task definition before failing.
394+
395+
- Default: 3
396+
- Minimum: 0 (no retries)
397+
398+
```
399+
- name: Deploy to Amazon ECS
400+
uses: aws-actions/amazon-ecs-deploy-task-definition@v2
401+
with:
402+
task-definition: task-definition.json
403+
service: my-service
404+
cluster: my-cluster
405+
max-retries: 5
406+
wait-for-service-stability: true
384407
```
385408
386-
This is useful for cases where a default value is non-null and you want to override the value and set it to null.
409+
Retries apply to transient failures during task definition registration or service update, such as eventual consistency issues or temporary AWS API errors.
387410
388411
## Troubleshooting
389412

action.yml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -90,6 +90,8 @@ inputs:
9090
required: false
9191
keep-null-value-keys:
9292
description: 'A comma-separated list of keys whose empty values (empty string, array, or object) should be preserved in the task definition. By default, empty values are removed.'
93+
max-retries:
94+
description: 'The maximum number of retry attempts for AWS API calls. Defaults to 3.'
9395
required: false
9496
outputs:
9597
task-definition-arn:

dist/index.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -474,17 +474,11 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
474474

475475
async function run() {
476476
try {
477-
const ecs = new ECS({
478-
customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions'
479-
});
480-
const codedeploy = new CodeDeploy({
481-
customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions'
482-
});
483-
484477
// Get inputs
485478
const taskDefinitionFile = core.getInput('task-definition', { required: true });
486479
const service = core.getInput('service', { required: false });
487480
const cluster = core.getInput('cluster', { required: false });
481+
const maxRetries = parseInt(core.getInput('max-retries', { required: false })) || 3;
488482
const waitForService = core.getInput('wait-for-service-stability', { required: false });
489483
let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30;
490484

@@ -513,6 +507,17 @@ async function run() {
513507
if (keepNullValueKeysInput) {
514508
keepNullValueKeys = keepNullValueKeysInput.split(',').map(k => k.trim()).filter(Boolean);
515509
}
510+
const ecs = new ECS({
511+
customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions',
512+
maxAttempts: maxRetries,
513+
retryMode: 'standard'
514+
});
515+
516+
const codedeploy = new CodeDeploy({
517+
customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions',
518+
maxAttempts: maxRetries,
519+
retryMode: 'standard'
520+
});
516521

517522
// Register the task definition
518523
core.debug('Registering the task definition');

index.js

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -468,17 +468,11 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task
468468

469469
async function run() {
470470
try {
471-
const ecs = new ECS({
472-
customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions'
473-
});
474-
const codedeploy = new CodeDeploy({
475-
customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions'
476-
});
477-
478471
// Get inputs
479472
const taskDefinitionFile = core.getInput('task-definition', { required: true });
480473
const service = core.getInput('service', { required: false });
481474
const cluster = core.getInput('cluster', { required: false });
475+
const maxRetries = parseInt(core.getInput('max-retries', { required: false })) || 3;
482476
const waitForService = core.getInput('wait-for-service-stability', { required: false });
483477
let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30;
484478

@@ -507,6 +501,17 @@ async function run() {
507501
if (keepNullValueKeysInput) {
508502
keepNullValueKeys = keepNullValueKeysInput.split(',').map(k => k.trim()).filter(Boolean);
509503
}
504+
const ecs = new ECS({
505+
customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions',
506+
maxAttempts: maxRetries,
507+
retryMode: 'standard'
508+
});
509+
510+
const codedeploy = new CodeDeploy({
511+
customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions',
512+
maxAttempts: maxRetries,
513+
retryMode: 'standard'
514+
});
510515

511516
// Register the task definition
512517
core.debug('Registering the task definition');

index.test.js

Lines changed: 23 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -57,8 +57,9 @@ describe('Deploy to ECS', () => {
5757
core.getInput = jest
5858
.fn()
5959
.mockReturnValueOnce('task-definition.json') // task-definition
60-
.mockReturnValueOnce('service-456') // service
61-
.mockReturnValueOnce('cluster-789'); // cluster
60+
.mockReturnValueOnce('service-456') // service
61+
.mockReturnValueOnce('cluster-789') // cluster
62+
.mockReturnValueOnce('3'); // max-retries
6263

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

@@ -95,7 +96,7 @@ describe('Deploy to ECS', () => {
9596
mockEcsDescribeServices.mockImplementation(
9697
() => Promise.resolve({
9798
failures: [],
98-
services: [{
99+
services: [{
99100
status: 'ACTIVE'
100101
}]
101102
})
@@ -628,6 +629,7 @@ describe('Deploy to ECS', () => {
628629
.mockReturnValueOnce('task-definition.json') // task-definition
629630
.mockReturnValueOnce('service-456') // service
630631
.mockReturnValueOnce('cluster-789') // cluster
632+
.mockReturnValueOnce('3') // max-retries
631633
.mockReturnValueOnce('TRUE'); // wait-for-service-stability
632634

633635
mockEcsDescribeServices.mockImplementation(
@@ -705,6 +707,7 @@ describe('Deploy to ECS', () => {
705707
.mockReturnValueOnce('task-definition.json') // task-definition
706708
.mockReturnValueOnce('service-456') // service
707709
.mockReturnValueOnce('cluster-789') // cluster
710+
.mockReturnValueOnce('3') // max-retries
708711
.mockReturnValueOnce('TRUE') // wait-for-service-stability
709712
.mockReturnValueOnce('60'); // wait-for-minutes
710713

@@ -781,6 +784,7 @@ describe('Deploy to ECS', () => {
781784
.mockReturnValueOnce('task-definition.json') // task-definition
782785
.mockReturnValueOnce('service-456') // service
783786
.mockReturnValueOnce('cluster-789') // cluster
787+
.mockReturnValueOnce('3') // max-retries
784788
.mockReturnValueOnce('TRUE') // wait-for-service-stability
785789
.mockReturnValueOnce('1000'); // wait-for-minutes
786790

@@ -1076,6 +1080,7 @@ describe('Deploy to ECS', () => {
10761080
.mockReturnValueOnce('task-definition.json') // task-definition
10771081
.mockReturnValueOnce('service-456') // service
10781082
.mockReturnValueOnce('cluster-789') // cluster
1083+
.mockReturnValueOnce('3') // max-retries
10791084
.mockReturnValueOnce('TRUE') // wait-for-service-stability
10801085
.mockReturnValueOnce(''); // desired count
10811086

@@ -1117,6 +1122,7 @@ describe('Deploy to ECS', () => {
11171122
.mockReturnValueOnce('task-definition.json') // task-definition
11181123
.mockReturnValueOnce('service-456') // service
11191124
.mockReturnValueOnce('cluster-789') // cluster
1125+
.mockReturnValueOnce('3') // max-retries
11201126
.mockReturnValueOnce('TRUE') // wait-for-service-stability
11211127
.mockReturnValueOnce('60') // wait-for-minutes
11221128
.mockReturnValueOnce(''); // desired count
@@ -1159,6 +1165,7 @@ describe('Deploy to ECS', () => {
11591165
.mockReturnValueOnce('task-definition.json') // task-definition
11601166
.mockReturnValueOnce('service-456') // service
11611167
.mockReturnValueOnce('cluster-789') // cluster
1168+
.mockReturnValueOnce('3') // max-retries
11621169
.mockReturnValueOnce('TRUE') // wait-for-service-stability
11631170
.mockReturnValueOnce('1000') // wait-for-minutes
11641171
.mockReturnValueOnce('abc'); // desired count is NaN
@@ -1201,6 +1208,7 @@ describe('Deploy to ECS', () => {
12011208
.mockReturnValueOnce('task-definition.json') // task-definition
12021209
.mockReturnValueOnce('service-456') // service
12031210
.mockReturnValueOnce('cluster-789') // cluster
1211+
.mockReturnValueOnce('3') // max-retries
12041212
.mockReturnValueOnce('false') // wait-for-service-stability
12051213
.mockReturnValueOnce('') // wait-for-minutes
12061214
.mockReturnValueOnce('true') // force-new-deployment
@@ -1403,7 +1411,7 @@ describe('Deploy to ECS', () => {
14031411
// Empty string for all other inputs
14041412
return '';
14051413
});
1406-
1414+
14071415
await run();
14081416
expect(core.setFailed).toHaveBeenCalledTimes(0);
14091417

@@ -1496,7 +1504,7 @@ describe('Deploy to ECS', () => {
14961504
volumeConfigurations: []
14971505
});
14981506
});
1499-
1507+
15001508
test('run task with setting true to enableECSManagedTags', async () => {
15011509
core.getInput = jest
15021510
.fn(input => {
@@ -1527,7 +1535,7 @@ describe('Deploy to ECS', () => {
15271535
volumeConfigurations: []
15281536
});
15291537
});
1530-
1538+
15311539
test('run task with setting false to enableECSManagedTags', async () => {
15321540
core.getInput = jest
15331541
.fn(input => {
@@ -1565,6 +1573,7 @@ describe('Deploy to ECS', () => {
15651573
.mockReturnValueOnce('task-definition.json') // task-definition
15661574
.mockReturnValueOnce('') // service
15671575
.mockReturnValueOnce('somecluster') // cluster
1576+
.mockReturnValueOnce('3') // max-retries
15681577
.mockReturnValueOnce('') // wait-for-service-stability
15691578
.mockReturnValueOnce('') // wait-for-minutes
15701579
.mockReturnValueOnce('') // force-new-deployment
@@ -1617,7 +1626,7 @@ describe('Deploy to ECS', () => {
16171626
if (input === 'run-task-managed-ebs-volume') return '{}';
16181627
return '';
16191628
});
1620-
1629+
16211630
mockRunTask.mockImplementation(
16221631
() => Promise.resolve({
16231632
failures: [{
@@ -1732,12 +1741,13 @@ describe('Deploy to ECS', () => {
17321741
.mockReturnValueOnce('task-definition.json') // task-definition
17331742
.mockReturnValueOnce('service-456') // service
17341743
.mockReturnValueOnce('cluster-789') // cluster
1744+
.mockReturnValueOnce('3') // max-retries
17351745
.mockReturnValueOnce('false') // wait-for-service-stability
17361746
.mockReturnValueOnce('') // wait-for-minutes
17371747
.mockReturnValueOnce('') // force-new-deployment
17381748
.mockReturnValueOnce('') // desired-count
17391749
.mockReturnValueOnce('') // enable-ecs-managed-tags
1740-
.mockReturnValueOnce('SERVICE'); // propagate-tags
1750+
.mockReturnValueOnce('SERVICE'); // propagate-tags
17411751

17421752
await run();
17431753
expect(core.setFailed).toHaveBeenCalledTimes(0);
@@ -1758,13 +1768,14 @@ describe('Deploy to ECS', () => {
17581768
volumeConfigurations: []
17591769
});
17601770
});
1761-
1771+
17621772
test('update service with setting true to enableECSManagedTags', async () => {
17631773
core.getInput = jest
17641774
.fn()
17651775
.mockReturnValueOnce('task-definition.json') // task-definition
17661776
.mockReturnValueOnce('service-456') // service
17671777
.mockReturnValueOnce('cluster-789') // cluster
1778+
.mockReturnValueOnce('3') // max-retries
17681779
.mockReturnValueOnce('false') // wait-for-service-stability
17691780
.mockReturnValueOnce('') // wait-for-minutes
17701781
.mockReturnValueOnce('') // force-new-deployment
@@ -1791,13 +1802,14 @@ describe('Deploy to ECS', () => {
17911802
volumeConfigurations: []
17921803
});
17931804
});
1794-
1805+
17951806
test('update service with setting false to enableECSManagedTags', async () => {
17961807
core.getInput = jest
17971808
.fn()
17981809
.mockReturnValueOnce('task-definition.json') // task-definition
17991810
.mockReturnValueOnce('service-456') // service
18001811
.mockReturnValueOnce('cluster-789') // cluster
1812+
.mockReturnValueOnce('3') // max-retries
18011813
.mockReturnValueOnce('false') // wait-for-service-stability
18021814
.mockReturnValueOnce('') // wait-for-minutes
18031815
.mockReturnValueOnce('') // force-new-deployment
@@ -2051,4 +2063,4 @@ describe('Deploy to ECS', () => {
20512063
}]
20522064
});
20532065
});
2054-
});
2066+
});

0 commit comments

Comments
 (0)