diff --git a/README.md b/README.md index 649ae5956..d0b20be0f 100644 --- a/README.md +++ b/README.md @@ -228,7 +228,7 @@ Running a service requires the following minimum set of permissions: ] } ``` - + Running a one-off/stand-alone task requires the following minimum set of permissions: ```json { @@ -342,7 +342,7 @@ In the following example, the service would not be updated until the ad-hoc task wait-for-task-stopped: true ``` -Overrides and VPC networking options are available as well. See [action.yml](action.yml) for more details. The `FARGATE` +Overrides and VPC networking options are available as well. See [action.yml](action.yml) for more details. The `FARGATE` launch type requires `awsvpc` network mode in your task definition and you must specify a network configuration. ### Tags @@ -369,21 +369,44 @@ To tag your tasks: ## Preserving Empty Values with keep-null-value-keys -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. +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. + +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. + +This is particularly useful in cases where ECS or a previous task definition applies a default value and you want to explicitly unset it. **Example:** -```yaml - - name: Deploy to Amazon ECS - uses: aws-actions/amazon-ecs-deploy-task-definition@v2 - with: - task-definition: task-definition.json - service: my-service - cluster: my-cluster - keep-null-value-keys: tag,command,placementConstraints +``` +- name: Deploy to Amazon ECS + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: task-definition.json + service: my-service + cluster: my-cluster + keep-null-value-keys: tag,command,placementConstraints + wait-for-service-stability: true +``` + +## Retries + +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. + +- Default: 3 +- Minimum: 0 (no retries) + +``` +- name: Deploy to Amazon ECS + uses: aws-actions/amazon-ecs-deploy-task-definition@v2 + with: + task-definition: task-definition.json + service: my-service + cluster: my-cluster + max-retries: 5 + wait-for-service-stability: true ``` -This is useful for cases where a default value is non-null and you want to override the value and set it to null. +Retries apply to transient failures during task definition registration or service update, such as eventual consistency issues or temporary AWS API errors. ## Troubleshooting diff --git a/action.yml b/action.yml index 296630b8e..606c4f14e 100644 --- a/action.yml +++ b/action.yml @@ -90,6 +90,8 @@ inputs: required: false keep-null-value-keys: 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.' + max-retries: + description: 'The maximum number of retry attempts for AWS API calls. Defaults to 3.' required: false outputs: task-definition-arn: diff --git a/dist/index.js b/dist/index.js index 6ee35adba..4c0fc3d61 100644 --- a/dist/index.js +++ b/dist/index.js @@ -474,17 +474,11 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task async function run() { try { - const ecs = new ECS({ - customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions' - }); - const codedeploy = new CodeDeploy({ - customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions' - }); - // 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 maxRetries = parseInt(core.getInput('max-retries', { required: false })) || 3; const waitForService = core.getInput('wait-for-service-stability', { required: false }); let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30; @@ -513,6 +507,17 @@ async function run() { if (keepNullValueKeysInput) { keepNullValueKeys = keepNullValueKeysInput.split(',').map(k => k.trim()).filter(Boolean); } + const ecs = new ECS({ + customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions', + maxAttempts: maxRetries, + retryMode: 'standard' + }); + + const codedeploy = new CodeDeploy({ + customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions', + maxAttempts: maxRetries, + retryMode: 'standard' + }); // Register the task definition core.debug('Registering the task definition'); diff --git a/index.js b/index.js index 5210df9f5..2bcf2b415 100644 --- a/index.js +++ b/index.js @@ -468,17 +468,11 @@ async function createCodeDeployDeployment(codedeploy, clusterName, service, task async function run() { try { - const ecs = new ECS({ - customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions' - }); - const codedeploy = new CodeDeploy({ - customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions' - }); - // 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 maxRetries = parseInt(core.getInput('max-retries', { required: false })) || 3; const waitForService = core.getInput('wait-for-service-stability', { required: false }); let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30; @@ -507,6 +501,17 @@ async function run() { if (keepNullValueKeysInput) { keepNullValueKeys = keepNullValueKeysInput.split(',').map(k => k.trim()).filter(Boolean); } + const ecs = new ECS({ + customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions', + maxAttempts: maxRetries, + retryMode: 'standard' + }); + + const codedeploy = new CodeDeploy({ + customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions', + maxAttempts: maxRetries, + retryMode: 'standard' + }); // Register the task definition core.debug('Registering the task definition'); diff --git a/index.test.js b/index.test.js index 5c762719e..5e79c4cd6 100644 --- a/index.test.js +++ b/index.test.js @@ -57,8 +57,9 @@ describe('Deploy to ECS', () => { core.getInput = jest .fn() .mockReturnValueOnce('task-definition.json') // task-definition - .mockReturnValueOnce('service-456') // service - .mockReturnValueOnce('cluster-789'); // cluster + .mockReturnValueOnce('service-456') // service + .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3'); // max-retries process.env = Object.assign(process.env, { GITHUB_WORKSPACE: __dirname }); @@ -95,7 +96,7 @@ describe('Deploy to ECS', () => { mockEcsDescribeServices.mockImplementation( () => Promise.resolve({ failures: [], - services: [{ + services: [{ status: 'ACTIVE' }] }) @@ -628,6 +629,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('TRUE'); // wait-for-service-stability mockEcsDescribeServices.mockImplementation( @@ -705,6 +707,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('TRUE') // wait-for-service-stability .mockReturnValueOnce('60'); // wait-for-minutes @@ -781,6 +784,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('TRUE') // wait-for-service-stability .mockReturnValueOnce('1000'); // wait-for-minutes @@ -1076,6 +1080,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('TRUE') // wait-for-service-stability .mockReturnValueOnce(''); // desired count @@ -1117,6 +1122,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('TRUE') // wait-for-service-stability .mockReturnValueOnce('60') // wait-for-minutes .mockReturnValueOnce(''); // desired count @@ -1159,6 +1165,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('TRUE') // wait-for-service-stability .mockReturnValueOnce('1000') // wait-for-minutes .mockReturnValueOnce('abc'); // desired count is NaN @@ -1201,6 +1208,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('false') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('true') // force-new-deployment @@ -1403,7 +1411,7 @@ describe('Deploy to ECS', () => { // Empty string for all other inputs return ''; }); - + await run(); expect(core.setFailed).toHaveBeenCalledTimes(0); @@ -1496,7 +1504,7 @@ describe('Deploy to ECS', () => { volumeConfigurations: [] }); }); - + test('run task with setting true to enableECSManagedTags', async () => { core.getInput = jest .fn(input => { @@ -1527,7 +1535,7 @@ describe('Deploy to ECS', () => { volumeConfigurations: [] }); }); - + test('run task with setting false to enableECSManagedTags', async () => { core.getInput = jest .fn(input => { @@ -1565,6 +1573,7 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('') // service .mockReturnValueOnce('somecluster') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment @@ -1617,7 +1626,7 @@ describe('Deploy to ECS', () => { if (input === 'run-task-managed-ebs-volume') return '{}'; return ''; }); - + mockRunTask.mockImplementation( () => Promise.resolve({ failures: [{ @@ -1732,12 +1741,13 @@ describe('Deploy to ECS', () => { .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('false') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment .mockReturnValueOnce('') // desired-count .mockReturnValueOnce('') // enable-ecs-managed-tags - .mockReturnValueOnce('SERVICE'); // propagate-tags + .mockReturnValueOnce('SERVICE'); // propagate-tags await run(); expect(core.setFailed).toHaveBeenCalledTimes(0); @@ -1758,13 +1768,14 @@ describe('Deploy to ECS', () => { volumeConfigurations: [] }); }); - + test('update service with setting true to enableECSManagedTags', async () => { core.getInput = jest .fn() .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('false') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment @@ -1791,13 +1802,14 @@ describe('Deploy to ECS', () => { volumeConfigurations: [] }); }); - + test('update service with setting false to enableECSManagedTags', async () => { core.getInput = jest .fn() .mockReturnValueOnce('task-definition.json') // task-definition .mockReturnValueOnce('service-456') // service .mockReturnValueOnce('cluster-789') // cluster + .mockReturnValueOnce('3') // max-retries .mockReturnValueOnce('false') // wait-for-service-stability .mockReturnValueOnce('') // wait-for-minutes .mockReturnValueOnce('') // force-new-deployment @@ -2051,4 +2063,4 @@ describe('Deploy to ECS', () => { }] }); }); -}); \ No newline at end of file +});