From bffe300632ab757700caa652f903af22fe304dc6 Mon Sep 17 00:00:00 2001 From: Satej Sawant Date: Mon, 24 Nov 2025 17:06:06 -0500 Subject: [PATCH 1/5] Initialize project structure with package.json and dependencies --- .gitignore | 5 +++++ package.json | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 39 insertions(+) create mode 100644 .gitignore create mode 100644 package.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..7e3789d --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +node_modules/ +dist/ +coverage/ +*.log +.DS_Store diff --git a/package.json b/package.json new file mode 100644 index 0000000..6b93085 --- /dev/null +++ b/package.json @@ -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" + } +} From 763c2c5cc8816a1290e47bae336bbc11154d23dc Mon Sep 17 00:00:00 2001 From: Satej Sawant Date: Mon, 24 Nov 2025 17:10:49 -0500 Subject: [PATCH 2/5] feat: add action.yml metadata file with all CreateExpressGatewayServiceCommand inputs --- action.yml | 102 +++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 102 insertions(+) create mode 100644 action.yml diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..8bdcaec --- /dev/null +++ b/action.yml @@ -0,0 +1,102 @@ +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: + description: 'The name of the ECS Express service to update. Used to construct the service ARN for checking if the service exists. If not provided, a new service will be created.' + required: false + service-name: + description: 'The desired name for a new ECS Express service (serviceName in SDK). Only used when creating a new service. If not provided, AWS will generate a unique name.' + 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 + + # 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). If not specified, Express Mode creates security groups automatically.' + 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 + + # Deployment behavior + wait-for-service-stability: + description: 'Whether to wait for the ECS service to reach stable state after deployment. Valid values are "true" or "false".' + required: false + default: 'true' + wait-for-minutes: + description: 'How long to wait for service stability, in minutes. Default is 30 minutes, maximum is 360 minutes (6 hours).' + required: false + default: '30' + +outputs: + service-arn: + description: 'The ARN of the deployed Express service' + endpoint: + description: 'The endpoint URL of the service (from the Application Load Balancer)' + status: + description: 'The status of the service (ACTIVE, DRAINING, or INACTIVE)' + +runs: + using: 'node20' + main: 'dist/index.js' From 05bf3c2095b549583851a846b7701890955a9818 Mon Sep 17 00:00:00 2001 From: Satej Sawant Date: Mon, 24 Nov 2025 17:15:14 -0500 Subject: [PATCH 3/5] feat: add required input validation for image, execution-role-arn, and infrastructure-role-arn --- index.js | 45 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 45 insertions(+) create mode 100644 index.js diff --git a/index.js b/index.js new file mode 100644 index 0000000..6b5362e --- /dev/null +++ b/index.js @@ -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(); +} From 74e5148ca5f799ec7e86cb133ea096d692d0477e Mon Sep 17 00:00:00 2001 From: Satej Sawant Date: Tue, 25 Nov 2025 09:56:18 -0500 Subject: [PATCH 4/5] Add .kiro and .vscode to .gitignore --- .gitignore | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.gitignore b/.gitignore index 7e3789d..bc8a6fe 100644 --- a/.gitignore +++ b/.gitignore @@ -3,3 +3,5 @@ dist/ coverage/ *.log .DS_Store +.kiro/ +.vscode/ From 5e39f893db1fee7361fc18e310ea2d8034add03e Mon Sep 17 00:00:00 2001 From: Satej Sawant Date: Tue, 25 Nov 2025 10:47:38 -0500 Subject: [PATCH 5/5] Add complete primaryContainer parameters, rename service to service-name, and add unit tests --- action.yml | 28 +++++++--------- index.test.js | 91 +++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 102 insertions(+), 17 deletions(-) create mode 100644 index.test.js diff --git a/action.yml b/action.yml index 8bdcaec..e224b7f 100644 --- a/action.yml +++ b/action.yml @@ -16,11 +16,8 @@ inputs: required: true # Service identification - service: - description: 'The name of the ECS Express service to update. Used to construct the service ARN for checking if the service exists. If not provided, a new service will be created.' - required: false service-name: - description: 'The desired name for a new ECS Express service (serviceName in SDK). Only used when creating a new service. If not provided, AWS will generate a unique 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." @@ -40,6 +37,15 @@ inputs: 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: @@ -57,7 +63,7 @@ inputs: 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). If not specified, Express Mode creates security groups automatically.' + description: 'Comma-separated list of security group IDs for the service (securityGroups in networkConfiguration).' required: false # Health check configuration @@ -79,23 +85,11 @@ inputs: description: 'The target value for the auto-scaling metric (autoScalingTargetValue in scalingTarget). For example, 60 for 60% CPU utilization.' required: false - # Deployment behavior - wait-for-service-stability: - description: 'Whether to wait for the ECS service to reach stable state after deployment. Valid values are "true" or "false".' - required: false - default: 'true' - wait-for-minutes: - description: 'How long to wait for service stability, in minutes. Default is 30 minutes, maximum is 360 minutes (6 hours).' - required: false - default: '30' - outputs: service-arn: description: 'The ARN of the deployed Express service' endpoint: description: 'The endpoint URL of the service (from the Application Load Balancer)' - status: - description: 'The status of the service (ACTIVE, DRAINING, or INACTIVE)' runs: using: 'node20' diff --git a/index.test.js b/index.test.js new file mode 100644 index 0000000..1acba6f --- /dev/null +++ b/index.test.js @@ -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'); + }); + }); +});