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
7 changes: 7 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
node_modules/
dist/
coverage/
*.log
.DS_Store
.kiro/
.vscode/
96 changes: 96 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
name: 'Amazon ECS "Deploy Express Service" Action for GitHub Actions'
description: 'Creates or updates an Amazon ECS Express Mode service'
branding:
icon: 'cloud'
color: 'orange'
inputs:
# Required inputs
image:
description: 'The container image URI to deploy (e.g., 123456789012.dkr.ecr.us-east-1.amazonaws.com/my-app:latest)'
required: true
execution-role-arn:
description: 'The ARN of the task execution role that grants the ECS agent permission to pull container images and publish logs'
required: true
infrastructure-role-arn:
description: 'The ARN of the infrastructure role that grants ECS permission to create and manage AWS resources (ALB, target groups, etc.)'
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
default: 'default'

# Primary container configuration
container-port:
description: 'The port number on the container that receives traffic (containerPort in primaryContainer). If not specified, Express Mode will use port 80.'
required: false
environment-variables:
description: 'Environment variables to set in the container (environment in primaryContainer). Provide as JSON array: [{"name":"KEY","value":"VALUE"}]'
required: false
secrets:
description: 'Secrets to inject into the container (secrets in primaryContainer). Provide as JSON array: [{"name":"KEY","valueFrom":"arn:aws:secretsmanager:..."}]'
required: false
command:
description: 'Override the default container command (command in primaryContainer). Provide as JSON array: ["node","server.js"]'
required: false
log-group:
description: 'CloudWatch Logs log group name for container logs (logGroup in awsLogsConfiguration). If not specified, Express Mode creates a log group automatically.'
required: false
log-stream-prefix:
description: 'CloudWatch Logs stream prefix for container logs (logStreamPrefix in awsLogsConfiguration). If not specified, Express Mode uses a default prefix.'
required: false
repository-credentials:
description: 'ARN of the secret containing credentials for private container registry (credentialsParameter in repositoryCredentials). Required for private registries outside ECR.'
required: false

# Resource configuration
cpu:
description: 'The number of CPU units to allocate (256, 512, 1024, 2048, 4096, 8192, 16384). If not specified, Express Mode defaults to 256 (.25 vCPU).'
required: false
memory:
description: 'The amount of memory in MiB to allocate (512, 1024, 2048, 4096, 8192, 16384, 30720, 61440, 122880). If not specified, Express Mode defaults to 512 MiB.'
required: false
task-role-arn:
description: 'The ARN of the IAM role that the container can assume to make AWS API calls (taskRoleArn)'
required: false

# Network configuration
subnets:
description: 'Comma-separated list of subnet IDs for the service (subnets in networkConfiguration). If not specified, Express Mode uses the default VPC.'
required: false
security-groups:
description: 'Comma-separated list of security group IDs for the service (securityGroups in networkConfiguration).'
required: false

# Health check configuration
health-check-path:
description: 'The path for ALB health checks (healthCheckPath). If not specified, Express Mode defaults to /ping.'
required: false

# Auto-scaling configuration
min-task-count:
description: 'Minimum number of tasks for auto-scaling (minTaskCount in scalingTarget). Must be less than or equal to max-task-count.'
required: false
max-task-count:
description: 'Maximum number of tasks for auto-scaling (maxTaskCount in scalingTarget). Must be greater than or equal to min-task-count.'
required: false
auto-scaling-metric:
description: 'The metric to use for auto-scaling (autoScalingMetric in scalingTarget): AVERAGE_CPU, AVERAGE_MEMORY, or REQUEST_COUNT_PER_TARGET'
required: false
auto-scaling-target-value:
description: 'The target value for the auto-scaling metric (autoScalingTargetValue in scalingTarget). For example, 60 for 60% CPU utilization.'
required: false

outputs:
service-arn:
description: 'The ARN of the deployed Express service'
endpoint:
description: 'The endpoint URL of the service (from the Application Load Balancer)'

runs:
using: 'node20'
main: 'dist/index.js'
45 changes: 45 additions & 0 deletions index.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const core = require('@actions/core');
const { ECSClient } = require('@aws-sdk/client-ecs');

/**
* Main entry point for the GitHub Action
* Creates or updates an Amazon ECS Express Mode service
*/
async function run() {
try {
core.info('Amazon ECS Deploy Express Service action started');

// Read required inputs
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 (!image || image.trim() === '') {
throw new Error('Input required and not supplied: image');
}

if (!executionRoleArn || executionRoleArn.trim() === '') {
throw new Error('Input required and not supplied: execution-role-arn');
}

if (!infrastructureRoleArn || infrastructureRoleArn.trim() === '') {
throw new Error('Input required and not supplied: infrastructure-role-arn');
}

core.info(`Container image: ${image}`);
core.debug(`Execution role ARN: ${executionRoleArn}`);
core.debug(`Infrastructure role ARN: ${infrastructureRoleArn}`);

} catch (error) {
core.setFailed(error.message);
core.debug(error.stack);
}
}

module.exports = run;

// Execute run() if this module is the entry point
if (require.main === module) {
run();
}
91 changes: 91 additions & 0 deletions index.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const core = require('@actions/core');
const { ECSClient } = require('@aws-sdk/client-ecs');
const run = require('./index');

// Mock the dependencies
jest.mock('@actions/core');
jest.mock('@aws-sdk/client-ecs');

describe('Amazon ECS Deploy Express Service Action', () => {
beforeEach(() => {
jest.clearAllMocks();

// Setup default mock implementations
core.getInput = jest.fn();
core.setFailed = jest.fn();
core.setOutput = jest.fn();
core.info = jest.fn();
core.debug = jest.fn();
});

describe('Input Validation', () => {
test('should fail when image input is missing', async () => {
core.getInput.mockImplementation((name) => {
if (name === 'image') return '';
if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/execution';
if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/infrastructure';
return '';
});

await run();

expect(core.setFailed).toHaveBeenCalledWith('Input required and not supplied: image');
});

test('should fail when execution-role-arn input is missing', async () => {
core.getInput.mockImplementation((name) => {
if (name === 'image') return 'nginx:latest';
if (name === 'execution-role-arn') return '';
if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/infrastructure';
return '';
});

await run();

expect(core.setFailed).toHaveBeenCalledWith('Input required and not supplied: execution-role-arn');
});

test('should fail when infrastructure-role-arn input is missing', async () => {
core.getInput.mockImplementation((name) => {
if (name === 'image') return 'nginx:latest';
if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/execution';
if (name === 'infrastructure-role-arn') return '';
return '';
});

await run();

expect(core.setFailed).toHaveBeenCalledWith('Input required and not supplied: infrastructure-role-arn');
});

test('should accept valid required inputs', async () => {
core.getInput.mockImplementation((name) => {
if (name === 'image') return 'nginx:latest';
if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/execution';
if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/infrastructure';
if (name === 'cluster') return 'default';
return '';
});

await run();

expect(core.info).toHaveBeenCalledWith('Amazon ECS Deploy Express Service action started');
expect(core.info).toHaveBeenCalledWith('Container image: nginx:latest');
});
});

describe('Input Trimming', () => {
test('should fail when image input is only whitespace', async () => {
core.getInput.mockImplementation((name) => {
if (name === 'image') return ' ';
if (name === 'execution-role-arn') return 'arn:aws:iam::123456789012:role/execution';
if (name === 'infrastructure-role-arn') return 'arn:aws:iam::123456789012:role/infrastructure';
return '';
});

await run();

expect(core.setFailed).toHaveBeenCalledWith('Input required and not supplied: image');
});
});
});
34 changes: 34 additions & 0 deletions package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
{
"name": "amazon-ecs-deploy-express-service",
"version": "1.0.0",
"description": "GitHub Action to deploy containerized applications to Amazon ECS Express Mode services",
"main": "index.js",
"scripts": {
"package": "ncc build index.js -o dist",
"lint": "eslint **.js",
"test": "eslint **.js && jest --coverage"
},
"repository": {
"type": "git",
"url": "git+https://github.com/aws-actions/amazon-ecs-deploy-express-service.git"
},
"keywords": [
"actions",
"aws",
"ecs",
"express",
"deploy"
],
"author": "AWS",
"license": "MIT",
"dependencies": {
"@actions/core": "^1.10.1",
"@aws-sdk/client-ecs": "^3.939.0"
},
"devDependencies": {
"@eslint/js": "^9.38.0",
"@vercel/ncc": "^0.38.3",
"eslint": "^9.39.1",
"jest": "^29.7.0"
}
}