From bb2aca92a85bac6a927969e7be4c4b70d0e04716 Mon Sep 17 00:00:00 2001 From: Satej Sawant Date: Tue, 25 Nov 2025 15:51:28 -0500 Subject: [PATCH 1/2] refactor: move service-name validation earlier Move service-name to the top of required input validations since it's now a required parameter. This provides faster feedback to users if the service name is missing. Changes: - Read and validate service-name first among required inputs - Remove duplicate service-name declarations - Fix test mocks that had duplicate service-name entries - All 17 tests passing --- index.js | 85 +++++++++++++++++++++++---------------------------- index.test.js | 30 ++++++++++-------- 2 files changed, 56 insertions(+), 59 deletions(-) diff --git a/index.js b/index.js index 36dbccd..c590da2 100644 --- a/index.js +++ b/index.js @@ -15,11 +15,16 @@ async function run() { core.info('Amazon ECS Deploy Express Service action started'); // Read required inputs + const serviceName = core.getInput('service-name', { required: false }); const image = core.getInput('image', { required: false }); const executionRoleArn = core.getInput('execution-role-arn', { required: false }); const infrastructureRoleArn = core.getInput('infrastructure-role-arn', { required: false }); // Validate required inputs are not empty + if (!serviceName || serviceName.trim() === '') { + throw new Error('Input required and not supplied: service-name'); + } + if (!image || image.trim() === '') { throw new Error('Input required and not supplied: image'); } @@ -46,7 +51,6 @@ async function run() { core.debug('ECS client created successfully'); // Read optional inputs for service identification - const serviceName = core.getInput('service-name', { required: false }); const clusterName = core.getInput('cluster', { required: false }) || 'default'; // Read optional container configuration inputs @@ -89,57 +93,46 @@ async function run() { const accountId = arnParts[4]; core.debug(`AWS Account ID: ${accountId}`); - // Construct service ARN if service name is provided - let serviceArn = null; - let serviceExists = false; + // Construct service ARN + const serviceArn = `arn:aws:ecs:${region}:${accountId}:service/${clusterName}/${serviceName}`; + core.info(`Constructed service ARN: ${serviceArn}`); - if (serviceName && serviceName.trim() !== '') { - // Construct service ARN: arn:aws:ecs:{region}:{account}:service/{cluster}/{service} - serviceArn = `arn:aws:ecs:${region}:${accountId}:service/${clusterName}/${serviceName}`; - core.info(`Constructed service ARN: ${serviceArn}`); + // Check if service exists using DescribeServices + let serviceExists = false; + try { + core.info('Checking if service exists...'); + const describeCommand = new DescribeServicesCommand({ + cluster: clusterName, + services: [serviceName] + }); - // Check if service exists using DescribeServices - try { - core.info('Checking if service exists...'); - const describeCommand = new DescribeServicesCommand({ - cluster: clusterName, - services: [serviceName] - }); - - const describeResponse = await ecs.send(describeCommand); - - if (describeResponse.services && describeResponse.services.length > 0) { - const service = describeResponse.services[0]; - if (service.status !== 'INACTIVE') { - serviceExists = true; - core.info(`Service exists with status: ${service.status}`); - } else { - core.info('Service exists but is INACTIVE, will create new service'); - } - } else { - core.info('Service does not exist, will create new service'); - } - } catch (error) { - if (error.name === 'ServiceNotFoundException' || error.name === 'ClusterNotFoundException') { - core.info('Service or cluster not found, will create new service'); - serviceExists = false; + const describeResponse = await ecs.send(describeCommand); + + if (describeResponse.services && describeResponse.services.length > 0) { + const service = describeResponse.services[0]; + if (service.status !== 'INACTIVE') { + serviceExists = true; + core.info(`Service exists with status: ${service.status}`); } else { - throw error; + core.info('Service exists but is INACTIVE, will create new service'); } + } else { + core.info('Service does not exist, will create new service'); + } + } catch (error) { + if (error.name === 'ServiceNotFoundException' || error.name === 'ClusterNotFoundException') { + core.info('Service or cluster not found, will create new service'); + serviceExists = false; + } else { + throw error; } - } else { - core.info('No service name provided, will create a new service with AWS-generated name'); } // Log the decision - if (serviceArn) { - if (serviceExists) { - core.info('Will UPDATE existing service'); - } else { - core.info('Will CREATE new service'); - } + if (serviceExists) { + core.info('Will UPDATE existing service'); } else { - core.info('Will CREATE new service with AWS-generated name'); + core.info('Will CREATE new service'); } // Build SDK command input object @@ -233,10 +226,8 @@ async function run() { } } - // Add optional service configuration - if (serviceName && serviceName.trim() !== '') { - serviceConfig.serviceName = serviceName; - } + // Add service configuration + serviceConfig.serviceName = serviceName; if (clusterName && clusterName !== 'default') { serviceConfig.cluster = clusterName; diff --git a/index.test.js b/index.test.js index 1b981c6..36253d7 100644 --- a/index.test.js +++ b/index.test.js @@ -30,6 +30,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return ''; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; return ''; }); @@ -43,6 +44,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return ''; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; return ''; }); @@ -56,6 +58,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return ''; + if (name === 'service-name') return 'test-service'; return ''; }); @@ -69,6 +72,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; if (name === 'cluster') return 'default'; return ''; }); @@ -168,6 +172,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; if (name === 'service-name') return 'my-service'; if (name === 'cluster') return 'default'; return ''; @@ -202,6 +207,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; if (name === 'service-name') return 'my-service'; if (name === 'cluster') return 'default'; return ''; @@ -228,27 +234,19 @@ describe('Amazon ECS Deploy Express Service', () => { expect(mockSend).toHaveBeenCalledTimes(2); }); - test('creates service when no service name provided', async () => { + test('fails when service-name is missing', async () => { core.getInput.mockImplementation((name) => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; + if (name === 'service-name') return ''; return ''; }); - // Only call: CreateExpressGatewayServiceCommand (no service name, so no describe) - mockSend.mockResolvedValueOnce({ - service: { - serviceArn: 'arn:aws:ecs:us-east-1:123456789012:service/default/generated-name' - } - }); - await run(); - expect(core.info).toHaveBeenCalledWith('No service name provided, will create a new service with AWS-generated name'); - expect(core.info).toHaveBeenCalledWith('Will CREATE new service with AWS-generated name'); - expect(core.info).toHaveBeenCalledWith('Creating Express Gateway service...'); - expect(mockSend).toHaveBeenCalledTimes(1); + expect(core.setFailed).toHaveBeenCalled(); }); }); @@ -258,6 +256,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; return ''; }); @@ -285,6 +284,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; if (name === 'container-port') return '8080'; return ''; }); @@ -308,6 +308,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; if (name === 'environment-variables') return '[{"name":"ENV","value":"prod"}]'; return ''; }); @@ -331,6 +332,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; if (name === 'cpu') return '512'; if (name === 'memory') return '1024'; return ''; @@ -355,6 +357,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; if (name === 'min-task-count') return '1'; if (name === 'max-task-count') return '10'; if (name === 'auto-scaling-metric') return 'AVERAGE_CPU'; @@ -383,6 +386,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; return ''; }); @@ -403,6 +407,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; return ''; }); @@ -422,6 +427,7 @@ describe('Amazon ECS Deploy Express Service', () => { if (name === 'image') return '123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest'; if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/ecsTaskExecutionRole'; if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/ecsInfrastructureRole'; + if (name === 'service-name') return 'test-service'; if (name === 'service-name') return 'my-service'; if (name === 'cluster') return 'nonexistent'; return ''; From 35586fa903cbff64ba7b9ccc4218407a12485ed1 Mon Sep 17 00:00:00 2001 From: Satej Sawant Date: Tue, 25 Nov 2025 15:52:25 -0500 Subject: [PATCH 2/2] refactor: move service-name to top of action.yml inputs Move service-name to be the first input in action.yml since it's now a required parameter. This makes the action.yml structure consistent with the validation order in index.js. Changes: - Move service-name from 'Service identification' section to top - Place it as the first required input - Simplify description to match index.js comment - All 17 tests passing --- action.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/action.yml b/action.yml index e224b7f..4b029a2 100644 --- a/action.yml +++ b/action.yml @@ -5,6 +5,9 @@ branding: color: 'orange' inputs: # Required inputs + service-name: + description: 'The name of the ECS Express service. Used for both creating new services and updating existing ones.' + required: true image: description: 'The container image URI to deploy (e.g., 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest)' required: true @@ -16,9 +19,6 @@ inputs: required: true # Service identification - service-name: - description: 'The name of the ECS Express service (serviceName). If the service exists, it will be updated. If not, a new service will be created with this name. If not provided, AWS will generate a unique name for new services.' - required: false cluster: description: "The name of the ECS cluster. Will default to the 'default' cluster." required: false