diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..bc8a6fe --- /dev/null +++ b/.gitignore @@ -0,0 +1,7 @@ +node_modules/ +dist/ +coverage/ +*.log +.DS_Store +.kiro/ +.vscode/ diff --git a/action.yml b/action.yml new file mode 100644 index 0000000..e224b7f --- /dev/null +++ b/action.yml @@ -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' 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(); +} 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'); + }); + }); +}); 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" + } +}