Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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
Expand Down
85 changes: 38 additions & 47 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -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');
}
Expand All @@ -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
Expand Down Expand Up @@ -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]

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we still trim() here?

});

// 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');
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we want to handle the case where service is in DRAINING state here? Prolly an error if a service is DRAINING

https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_Service.html#ECS-Type-Service-status

} else {
core.info('Service does not exist, will create new service');
}
} catch (error) {
if (error.name === 'ServiceNotFoundException' || error.name === 'ClusterNotFoundException') {

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Doesn't look like ServiceNotFoundException is ever thrown: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_DescribeServices.html

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
Expand Down Expand Up @@ -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;
Expand Down
30 changes: 18 additions & 12 deletions index.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 '';
});

Expand All @@ -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 '';
});

Expand All @@ -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 '';
});

Expand All @@ -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 '';
});
Expand Down Expand Up @@ -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 '';
Expand Down Expand Up @@ -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 '';
Expand All @@ -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();
});
});

Expand All @@ -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 '';
});

Expand Down Expand Up @@ -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 '';
});
Expand All @@ -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 '';
});
Expand All @@ -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 '';
Expand All @@ -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';
Expand Down Expand Up @@ -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 '';
});

Expand All @@ -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 '';
});

Expand All @@ -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 '';
Expand Down