├── .eslintrc.json ├── .github ├── ISSUE_TEMPLATE │ └── new-feature-template.md ├── PULL_REQUEST_TEMPLATE.md ├── dependabot.yml └── workflows │ ├── check.yml │ ├── codeql-analysis.yml │ ├── notifications.yml │ └── package.yml ├── .gitignore ├── .mergify.yml ├── CHANGELOG.md ├── CODE_OF_CONDUCT.md ├── CONTRIBUTING.md ├── LICENSE ├── README.md ├── THIRD-PARTY ├── action.yml ├── dist └── index.js ├── eslint.config.mjs ├── index.js ├── index.test.js ├── package-lock.json └── package.json /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "commonjs": true, 4 | "es6": true, 5 | "node": true, 6 | "jest": true 7 | }, 8 | "extends": "eslint:recommended", 9 | "globals": { 10 | "Atomics": "readonly", 11 | "SharedArrayBuffer": "readonly" 12 | }, 13 | "parserOptions": { 14 | "ecmaVersion": 2018 15 | }, 16 | "rules": { 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/new-feature-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: New Feature Template 3 | about: Template for a new Feature Request 4 | title: "[New Feature]:" 5 | labels: feature-request 6 | assignees: '' 7 | 8 | --- 9 | 10 | ## Feature Description 11 | A clear and concise description of the feature you're proposing. 12 | 13 | ## Problem Statement 14 | Describe the problem this feature would solve 15 | 16 | ## Alternatives Considered/ Work-arounds 17 | - Describe any alternative solutions or features you've considered. 18 | - If you are already circumventing this problem through any code-changes or processes, please mention those 19 | 20 | ## Additional Context 21 | Add any other context, screenshots, or examples about the feature request here. 22 | 23 | ## Willingness to Contribute 24 | Are you willing to submit a Pull Request for this feature? [Yes/No] 25 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | *Issue #, if available:* 2 | 3 | *Description of changes:* 4 | 5 | 6 | By submitting this pull request, I confirm that you can use, modify, copy, and redistribute this contribution, under the terms of your choice. 7 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: npm 4 | directory: "/" 5 | schedule: 6 | interval: weekly 7 | day: tuesday 8 | open-pull-requests-limit: 10 9 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: [pull_request] 2 | 3 | name: Check 4 | 5 | jobs: 6 | check: 7 | name: Run Unit Tests 8 | runs-on: ubuntu-latest 9 | steps: 10 | - name: Checkout 11 | uses: actions/checkout@v4 12 | - name: Run tests 13 | run: | 14 | npm ci 15 | npm test 16 | 17 | conventional-commits: 18 | name: Semantic Pull Request 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: validate 22 | uses: actions/github-script@v7 23 | with: 24 | script: | 25 | // See https://gist.github.com/marcojahn/482410b728c31b221b70ea6d2c433f0c#file-conventional-commit-regex-md 26 | const regex = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)/g; 27 | const pr = context.payload.pull_request; 28 | const title = pr.title; 29 | if (title.match(regex) == null) { 30 | throw `PR title "${title}"" does not match conventional commits from https://www.conventionalcommits.org/en/v1.0.0/` 31 | } 32 | -------------------------------------------------------------------------------- /.github/workflows/codeql-analysis.yml: -------------------------------------------------------------------------------- 1 | # For most projects, this workflow file will not need changing; you simply need 2 | # to commit it to your repository. 3 | # 4 | # You may wish to alter this file to override the set of languages analyzed, 5 | # or to provide custom queries or build logic. 6 | name: "CodeQL" 7 | 8 | on: 9 | push: 10 | branches: [master] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [master] 14 | schedule: 15 | - cron: '0 0 * * 2' 16 | 17 | jobs: 18 | analyze: 19 | name: Analyze 20 | runs-on: ubuntu-latest 21 | 22 | strategy: 23 | fail-fast: false 24 | matrix: 25 | # Override automatic language detection by changing the below list 26 | # Supported options are ['csharp', 'cpp', 'go', 'java', 'javascript', 'python'] 27 | language: ['javascript'] 28 | # Learn more... 29 | # https://docs.github.com/en/github/finding-security-vulnerabilities-and-errors-in-your-code/configuring-code-scanning#overriding-automatic-language-detection 30 | 31 | steps: 32 | - name: Checkout repository 33 | uses: actions/checkout@v4 34 | with: 35 | # We must fetch at least the immediate parents so that if this is 36 | # a pull request then we can checkout the head. 37 | fetch-depth: 2 38 | 39 | # If this run was triggered by a pull request event, then checkout 40 | # the head of the pull request instead of the merge commit. 41 | - run: git checkout HEAD^2 42 | if: ${{ github.event_name == 'pull_request' }} 43 | 44 | # Initializes the CodeQL tools for scanning. 45 | - name: Initialize CodeQL 46 | uses: github/codeql-action/init@v3 47 | with: 48 | languages: ${{ matrix.language }} 49 | # If you wish to specify custom queries, you can do so here or in a config file. 50 | # By default, queries listed here will override any specified in a config file. 51 | # Prefix the list here with "+" to use these queries and those in the config file. 52 | # queries: ./path/to/local/query, your-org/your-repo/queries@main 53 | 54 | # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). 55 | # If this step fails, then you should remove it and run the build manually (see below) 56 | - name: Autobuild 57 | uses: github/codeql-action/autobuild@v3 58 | 59 | # ℹ️ Command-line programs to run using the OS shell. 60 | # 📚 https://git.io/JvXDl 61 | 62 | # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines 63 | # and modify them (or add more) to build your code if your project 64 | # uses a compiled language 65 | 66 | #- run: | 67 | # make bootstrap 68 | # make release 69 | 70 | - name: Perform CodeQL Analysis 71 | uses: github/codeql-action/analyze@v3 72 | -------------------------------------------------------------------------------- /.github/workflows/notifications.yml: -------------------------------------------------------------------------------- 1 | name: Send Notifications to Slack 2 | on: 3 | pull_request: 4 | types: [opened, reopened] 5 | issues: 6 | types: [opened] 7 | issue_comment: 8 | types: [created] 9 | 10 | jobs: 11 | issue-notifications: 12 | name: Send Notifications 13 | runs-on: ubuntu-latest 14 | 15 | steps: 16 | - uses: actions/github-script@v7 17 | id: sanitize-title 18 | with: 19 | script: | 20 | const isPR = !!context.payload.pull_request; 21 | const isIssue = !!context.payload.issue; 22 | const item = isPR ? context.payload.pull_request : isIssue ? context.payload.issue : context.payload.issue_comment.issue; 23 | 24 | // Sanitization functions 25 | const sanitizeTitle = (title) => { 26 | return title 27 | // Remove potential markdown formatting 28 | .replace(/[*_~`]/g, '') 29 | // Remove potential HTML tags 30 | .replace(/<[^>]*>/g, '') 31 | // Remove multiple spaces 32 | .replace(/\s{2,}/g, ' ') 33 | // Trim whitespace 34 | .trim() 35 | // Enforce max length of 100 36 | .substring(0, 100); 37 | }; 38 | 39 | // Escape special characters for Slack 40 | const escapeForSlack = (text) => { 41 | return text 42 | .replace(/"/g, '"') 43 | .replace(/&/g, '&') 44 | .replace(//g, '>') 47 | .replace(/&lt;/g, '<') 48 | .replace(/&gt;/g, '>'); 49 | }; 50 | 51 | const sanitizedTitle = escapeForSlack(sanitizeTitle(item.title)); 52 | console.log('Sanitized Title: ', sanitizedTitle); 53 | core.setOutput('safe-title', sanitizedTitle); 54 | - name: Send notifications on Pull Request 55 | if: ${{ github.event_name == 'pull_request'}} 56 | id: slack_PR 57 | uses: slackapi/slack-github-action@v1.26.0 58 | with: 59 | payload: | 60 | { 61 | "Notification Type": "Pull Request", 62 | "Notification URL":"${{ github.event.pull_request.html_url }}", 63 | "GitHub Repo": "${{ github.repository }}", 64 | "Notification Title": "${{ steps.sanitize-title.outputs.safe-title }}" 65 | } 66 | env: 67 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 68 | - name: Send notification on new issues 69 | if: ${{github.event_name == 'issues'}} 70 | id: slack_issue 71 | uses: slackapi/slack-github-action@v1.26.0 72 | with: 73 | payload: | 74 | { 75 | "Notification Type": "Issue", 76 | "Notification URL":"${{ github.event.issue.html_url }}", 77 | "GitHub Repo": "${{ github.repository }}", 78 | "Notification Title": "${{ steps.sanitize-title.outputs.safe-title }}" 79 | } 80 | env: 81 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 82 | - name: Send notification on Issues and Pull Requests Comments 83 | if: ${{github.event_name == 'issue_comment'}} 84 | id: slack_issue_comment 85 | uses: slackapi/slack-github-action@v1.26.0 86 | with: 87 | payload: | 88 | { 89 | "Notification Type": "Issue comment", 90 | "Notification URL":"${{ github.event.comment.html_url }}", 91 | "GitHub Repo": "${{ github.repository }}", 92 | "Notification Title": "${{ steps.sanitize-title.outputs.safe-title }}" 93 | } 94 | env: 95 | SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} 96 | -------------------------------------------------------------------------------- /.github/workflows/package.yml: -------------------------------------------------------------------------------- 1 | name: Package 2 | 3 | # When a pull request is opened/reopened or when the head branch of the pull request is updated. 4 | on: 5 | pull_request: 6 | 7 | 8 | jobs: 9 | build: 10 | name: Package distribution file 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: write 14 | steps: 15 | - name: Init a git repo 16 | uses: actions/checkout@v4 17 | - name: Checkout PR 18 | env: 19 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 20 | run: gh pr checkout ${{ github.event.pull_request.number }} 21 | - name: Package 22 | run: | 23 | npm ci 24 | npm test 25 | npm run package 26 | - name: Commit to PR 27 | if: github.actor == 'dependabot[bot]' 28 | env: 29 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 30 | run: | 31 | git config --global user.name "GitHub Actions" 32 | git add dist/ 33 | git commit -m "chore: Update dist" || echo "No changes to commit" 34 | git push 35 | - name: Check git diff 36 | if: github.actor != 'dependabot[bot]' 37 | run: | 38 | git diff --exit-code dist/index.js 39 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | 3 | # Editors 4 | .vscode 5 | 6 | # Logs 7 | logs 8 | *.log 9 | npm-debug.log* 10 | yarn-debug.log* 11 | yarn-error.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (http://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Other Dependency directories 41 | jspm_packages/ 42 | 43 | # TypeScript v1 declaration files 44 | typings/ 45 | 46 | # Optional npm cache directory 47 | .npm 48 | 49 | # Optional eslint cache 50 | .eslintcache 51 | 52 | # Optional REPL history 53 | .node_repl_history 54 | 55 | # Output of 'npm pack' 56 | *.tgz 57 | 58 | # Yarn Integrity file 59 | .yarn-integrity 60 | 61 | # dotenv environment variables file 62 | .env 63 | 64 | # next.js build output 65 | .next 66 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | merge_conditions: 4 | # Conditions to get out of the queue (= merged) 5 | - status-success=Run Unit Tests 6 | - status-success=Semantic Pull Request 7 | - status-success=Analyze (javascript) 8 | merge_method: squash 9 | 10 | pull_request_rules: 11 | - name: Automatically merge on CI success and review approval 12 | conditions: 13 | - base~=master|integ-tests 14 | - "#approved-reviews-by>=1" 15 | - approved-reviews-by=@aws-actions/aws-ecs-devx 16 | - -approved-reviews-by~=author 17 | - status-success=Run Unit Tests 18 | - status-success=Semantic Pull Request 19 | - status-success=Analyze (javascript) 20 | - label!=work-in-progress 21 | - -title~=(WIP|wip) 22 | - -merged 23 | - -closed 24 | - author!=dependabot[bot] 25 | actions: 26 | queue: 27 | name: default 28 | 29 | - name: Automatically approve and merge Dependabot PRs 30 | conditions: 31 | - base=master 32 | - author=dependabot[bot] 33 | - status-success=Run Unit Tests 34 | - status-success=Semantic Pull Request 35 | - status-success=Analyze (javascript) 36 | - -title~=(WIP|wip) 37 | - -label~=(blocked|do-not-merge) 38 | - -merged 39 | - -closed 40 | actions: 41 | review: 42 | type: APPROVE 43 | queue: 44 | name: default -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. See [standard-version](https://github.com/conventional-changelog/standard-version) for commit guidelines. 4 | 5 | ### [2.3.2](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v2.3.1...v2.3.2) (2025-04-14) 6 | 7 | ### [2.3.1](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v2.3.0...v2.3.1) (2025-03-17) 8 | 9 | 10 | ### Bug Fixes 11 | 12 | * propagate run-task-arn to outputs ([#740](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/740)) ([ba4c50f](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/ba4c50ff72022c29eca99b2b348bca524e6c1b0f)) 13 | * set propagateTags to null if unset ([6d07985](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/6d079859a0107705ccbf1ede83cc3516807b1ecb)) 14 | 15 | ## [2.3.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v2.2.0...v2.3.0) (2025-01-30) 16 | 17 | 18 | ### Features 19 | 20 | * Add support for 'VolumeConfigurations' property on both UpdateService and RunTask API call ([#721](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/721)) ([0bad458](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/0bad458c6aa901707e510cd05b797b05da075633)) 21 | 22 | ## [2.2.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v2.1.2...v2.2.0) (2024-12-06) 23 | 24 | 25 | ### Features 26 | 27 | * add run-task-capacity-provider-strategy input ([#661](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/661)) ([6ebedf4](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/6ebedf489a59397e203a34a9cb7f85c8e303142c)) 28 | 29 | 30 | ### Bug Fixes 31 | 32 | * when no input enableECSManagedTagsInput, not include it to request params ([#669](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/669)) ([e4558ed](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/e4558ed83a830c66b168104c883a31784769e99c)), closes [#682](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/682) [#683](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/683) [#681](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/681) [#679](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/679) 33 | 34 | ### [2.1.2](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v2.1.1...v2.1.2) (2024-10-24) 35 | 36 | ### [2.1.1](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v2.1.0...v2.1.1) (2024-10-03) 37 | 38 | ## [2.1.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v2.0.0...v2.1.0) (2024-09-05) 39 | 40 | 41 | ### Features 42 | 43 | * Enable AWS managed tags ([#622](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/622)) ([5ae7be6](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/5ae7be6fcfec491494b3dbe937800837321d81d9)) 44 | * Tags for services and ad-hoc run ([#629](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/629)) ([1b137d4](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/1b137d48136614359c0c3a573120ab771daa6320)) 45 | 46 | 47 | ### Bug Fixes 48 | 49 | * set networkConfiguration to null when using bridge network mode ([#617](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/617)) ([0a1e247](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/0a1e24711a61a2279b2bf40c6877fdbfd117997e)) 50 | 51 | ## [2.0.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.6.0...v2.0.0) (2024-08-06) 52 | 53 | 54 | ### ⚠ BREAKING CHANGES 55 | 56 | * AWS SDK v3 upgrade contains some backward incompatible changes. 57 | 58 | ### Features 59 | 60 | * add ad-hoc task runs ([#304](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/304)) ([b3a528e](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/b3a528eb690c86037acd19fd6a2a86337f4e3657)) 61 | * add new parameters and tests to one-off task feature ([#593](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/593)) ([67393b6](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/67393b6dddfbf0653b20b162dcdd0d3821366bc4)) 62 | * Add CodeDeploy deployment config name parameter ([4b15394](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/4b153949000fb656721f5a776216cb7e446d9f98)) 63 | 64 | ### Bug Fixes 65 | 66 | * Link to events v2 url ([#588](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/588)) ([1a69dae](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/1a69daea10712415b65b5c90f8c41b1b6b556ab5)) 67 | * pass maxWaitTime in seconds ([b5c6c3f](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/b5c6c3fcbdf37b6f40a448364f91bfa3f824e3d0)) 68 | * waiter options ([a15de3c](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/a15de3cf6c410374c35333dbbf96b183206ac0b7)) 69 | 70 | ## [1.6.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.5.0...v1.6.0) (2024-07-18) 71 | 72 | ### Please note that this is a backward incompatible release with the upgrade to AWS SDK v3. We recommend using v2 of this Github action which includes the SDK upgrade, and update your task definition parameters to adhere to the specification defined in AWS documentation [here](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/task_definition_parameters.html). 73 | ### Features 74 | 75 | * Add CodeDeploy deployment config name parameter ([4b15394](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/4b153949000fb656721f5a776216cb7e446d9f98)) 76 | 77 | 78 | ### Bug Fixes 79 | 80 | * pass maxWaitTime in seconds ([b5c6c3f](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/b5c6c3fcbdf37b6f40a448364f91bfa3f824e3d0)) 81 | * waiter options ([a15de3c](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/a15de3cf6c410374c35333dbbf96b183206ac0b7)) 82 | 83 | ## [1.5.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.11...v1.5.0) (2024-05-07) 84 | 85 | 86 | ### Features 87 | 88 | * Add desired tasks ([#505](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/505)) ([e5f78d3](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/e5f78d3088b0f4f96faca249870440a0001deaa3)) 89 | 90 | ### [1.4.11](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.10...v1.4.11) (2023-01-04) 91 | 92 | ### [1.4.10](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.9...v1.4.10) (2022-09-30) 93 | 94 | 95 | ### Bug Fixes 96 | 97 | * support new 'ECS' deployment type rather than relying on a null value ([#387](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/387)) ([b74b034](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/b74b034038701c2a78e7715e68f28b8fd49a14c7)) 98 | * Use correct host for China region console ([#309](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/309)) ([bfe35b5](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/bfe35b582b00dd351d71abc7af67f91e493c0802)) 99 | 100 | ### [1.4.9](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.8...v1.4.9) (2022-01-18) 101 | 102 | 103 | ### Bug Fixes 104 | 105 | * Strict Mode Deprecation ([ec3c2b2](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/ec3c2b2d3e7138039c827953d14cccbedc99ae23)) 106 | 107 | ### [1.4.8](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.7...v1.4.8) (2021-11-23) 108 | 109 | ### [1.4.7](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.6...v1.4.7) (2021-07-13) 110 | 111 | 112 | ### Bug Fixes 113 | 114 | * Container Definition Environment variables are being removed when empty ([#224](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/224)) ([632a7fa](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/632a7fad2a714a363ed824224a88254c429236d5)) 115 | 116 | ### [1.4.6](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.5...v1.4.6) (2021-06-02) 117 | 118 | 119 | ### Bug Fixes 120 | 121 | * Cannot read property 'length' of undefined ([#202](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/202)) ([8009d7d](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/8009d7da6ac76c5f49983585decef599d9916042)) 122 | 123 | ### [1.4.5](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.4...v1.4.5) (2021-05-10) 124 | 125 | ### [1.4.4](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.3...v1.4.4) (2021-02-23) 126 | 127 | ### [1.4.3](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.2...v1.4.3) (2021-02-08) 128 | 129 | 130 | ### Bug Fixes 131 | 132 | * allow empty values in proxyConfiguration.properties ([#168](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/168)) ([3963f7f](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/3963f7f3050f9c64b285d6a437b3d447a73131f3)), closes [#163](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/163) 133 | * enable forceNewDeployment for ECS Task that is broken per [#157](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/157) ([#159](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/159)) ([4b6d445](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/4b6d44541b0b3e5871a0eb4265d8c35a35cbb215)) 134 | 135 | ### [1.4.2](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.1...v1.4.2) (2021-01-26) 136 | 137 | 138 | ### Bug Fixes 139 | 140 | * ignore additional fields from task definition input ([#165](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/165)) ([7727942](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/77279428f4b2e987d6c03366891893fb8161c1e4)) 141 | 142 | ### [1.4.1](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.4.0...v1.4.1) (2020-12-22) 143 | 144 | 145 | ### Bug Fixes 146 | 147 | * forceNewDeployment input to take a boolean ([#150](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/150)) ([06f69cf](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/06f69cf0d8243e21900f315a65772f40e9b508a2)) 148 | * forceNewDeployment to be a boolean ([#140](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/140)) ([9407da9](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/9407da9865a8d6b2d45c8239daeaff7203b49d45)) 149 | 150 | ## [1.4.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.10...v1.4.0) (2020-10-29) 151 | 152 | 153 | ### Features 154 | 155 | * allow forceNewDeployment ([#116](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/116)) ([f2d330f](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/f2d330fcd84477fa5332a7f18acb483c21e31bee)) 156 | 157 | ### [1.3.10](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.9...v1.3.10) (2020-09-29) 158 | 159 | ### [1.3.9](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.8...v1.3.9) (2020-08-25) 160 | 161 | ### [1.3.8](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.7...v1.3.8) (2020-08-11) 162 | 163 | ### [1.3.7](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.6...v1.3.7) (2020-07-17) 164 | 165 | ### [1.3.6](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.5...v1.3.6) (2020-07-14) 166 | 167 | ### [1.3.5](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.4...v1.3.5) (2020-06-30) 168 | 169 | ### [1.3.4](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.3...v1.3.4) (2020-06-09) 170 | 171 | ### [1.3.3](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.2...v1.3.3) (2020-05-27) 172 | 173 | ### [1.3.2](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.1...v1.3.2) (2020-05-18) 174 | 175 | ### [1.3.1](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.3.0...v1.3.1) (2020-05-08) 176 | 177 | 178 | ### Bug Fixes 179 | 180 | * clean null values out of arrays ([#63](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/63)) ([6b1f3e4](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/6b1f3e4e8c4e9b191fbf70a5c79418b7eaa995a9)) 181 | 182 | ## [1.3.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.2.0...v1.3.0) (2020-04-22) 183 | 184 | 185 | ### Features 186 | 187 | * Add more debugging, including link to the ECS or CodeDeploy console ([#56](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/56)) ([f0b3966](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/f0b3966cfef41a73fc35f3001025fb9290b3673b)) 188 | 189 | ## [1.2.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.1.0...v1.2.0) (2020-04-02) 190 | 191 | 192 | ### Features 193 | 194 | * clean empty arrays and objects from the task def file ([#52](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/52)) ([e64c8a6](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/e64c8a6fd7cb8f40b6487fc0acd0a357cc1eaffd)) 195 | 196 | ## [1.1.0](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.0.3...v1.1.0) (2020-03-05) 197 | 198 | 199 | ### Features 200 | 201 | * add option to specify number of minutes to wait for deployment to complete ([#37](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/37)) ([27c64c3](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/27c64c3fabb355c8a4311a02eaf507f684adc033)), closes [#33](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/33) 202 | 203 | ### [1.0.3](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.0.2...v1.0.3) (2020-02-06) 204 | 205 | ### [1.0.2](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/compare/v1.0.1...v1.0.2) (2020-02-06) 206 | 207 | 208 | ### Bug Fixes 209 | 210 | * Ignore task definition fields that are Describe outputs, but not Register inputs ([70d7e5a](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/70d7e5a70a160768b612a0d0db2820fb24259958)), closes [#22](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues/22) 211 | * Match package version to current tag version ([2c12fa8](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/2c12fa8bf9f89ea322d319c83cfcf8f3175bfbb1)) 212 | * Reduce error debugging ([7a9b7f7](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/commit/7a9b7f71e4f9b87151c1b4e3bde474db2eee1595)) 213 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | ## Code of Conduct 2 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 3 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 4 | opensource-codeofconduct@amazon.com with any additional questions or comments. 5 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing Guidelines 2 | 3 | Thank you for your interest in contributing to our project. Whether it's a bug report, new feature, correction, or additional 4 | documentation, we greatly value feedback and contributions from our community. 5 | 6 | Please read through this document before submitting any issues or pull requests to ensure we have all the necessary 7 | information to effectively respond to your bug report or contribution. 8 | 9 | 10 | ## Reporting Bugs/Feature Requests 11 | 12 | We welcome you to use the GitHub issue tracker to report bugs or suggest features. 13 | 14 | When filing an issue, please check [existing open](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues), or [recently closed](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues?utf8=%E2%9C%93&q=is%3Aissue%20is%3Aclosed%20), issues to make sure somebody else hasn't already 15 | reported the issue. Please try to include as much information as you can. Details like these are incredibly useful: 16 | 17 | * A reproducible test case or series of steps 18 | * The version of our code being used 19 | * Any modifications you've made relevant to the bug 20 | * Anything unusual about your environment or deployment 21 | 22 | 23 | ## Contributing via Pull Requests 24 | Contributions via pull requests are much appreciated. Before sending us a pull request, please ensure that: 25 | 26 | 1. You are working against the latest source on the *master* branch. 27 | 2. You check existing open, and recently merged, pull requests to make sure someone else hasn't addressed the problem already. 28 | 3. You open an issue to discuss any significant work - we would hate for your time to be wasted. 29 | 30 | To send us a pull request, please: 31 | 32 | 1. Fork the repository. 33 | 2. Modify the source; please focus on the specific change you are contributing. If you also reformat all the code, it will be hard for us to focus on your change. 34 | 3. Ensure local tests pass. 35 | 4. Commit to your fork using clear commit messages. 36 | 5. Send us a pull request, answering any default questions in the pull request interface. 37 | 6. Pay attention to any automated CI failures reported in the pull request, and stay involved in the conversation. 38 | 39 | GitHub provides additional document on [forking a repository](https://help.github.com/articles/fork-a-repo/) and 40 | [creating a pull request](https://help.github.com/articles/creating-a-pull-request/). 41 | 42 | 43 | ## Finding contributions to work on 44 | Looking at the existing issues is a great way to find something to contribute on. As our projects, by default, use the default GitHub issue labels (enhancement/bug/duplicate/help wanted/invalid/question/wontfix), looking at any ['help wanted'](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/labels/help%20wanted) issues is a great place to start. 45 | 46 | 47 | ## Code of Conduct 48 | This project has adopted the [Amazon Open Source Code of Conduct](https://aws.github.io/code-of-conduct). 49 | For more information see the [Code of Conduct FAQ](https://aws.github.io/code-of-conduct-faq) or contact 50 | opensource-codeofconduct@amazon.com with any additional questions or comments. 51 | 52 | 53 | ## Security issue notifications 54 | If you discover a potential security issue in this project we ask that you notify AWS/Amazon Security via our [vulnerability reporting page](http://aws.amazon.com/security/vulnerability-reporting/). Please do **not** create a public github issue. 55 | 56 | 57 | ## Licensing 58 | 59 | See the [LICENSE](https://github.com/aws-actions/amazon-ecs-deploy-task-definition/blob/master/LICENSE) file for our project's licensing. We will ask you to confirm the licensing of your contribution. 60 | 61 | We may ask you to sign a [Contributor License Agreement (CLA)](http://en.wikipedia.org/wiki/Contributor_License_Agreement) for larger changes. 62 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright 2019 Amazon.com, Inc. or its affiliates. 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Amazon ECS "Deploy Task Definition" Action for GitHub Actions 2 | 3 | Registers an Amazon ECS task definition and deploys it to an ECS service. 4 | 5 | **Table of Contents** 6 | 7 | 8 | 9 | - [Usage](#usage) 10 | + [Task definition file](#task-definition-file) 11 | + [Task definition container image values](#task-definition-container-image-values) 12 | - [Credentials and Region](#credentials-and-region) 13 | - [Permissions](#permissions) 14 | - [AWS CodeDeploy Support](#aws-codedeploy-support) 15 | - [Troubleshooting](#troubleshooting) 16 | - [License Summary](#license-summary) 17 | - [Security Disclosures](#security-disclosures) 18 | 19 | 20 | 21 | ## Usage 22 | 23 | ```yaml 24 | - name: Deploy to Amazon ECS 25 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 26 | with: 27 | task-definition: task-definition.json 28 | service: my-service 29 | cluster: my-cluster 30 | wait-for-service-stability: true 31 | ``` 32 | 33 | See [action.yml](action.yml) for the full documentation for this action's inputs and outputs. 34 | In most cases when running a one-off task, subnet ID's, subnet groups, and assign public IP will be required. 35 | Assign public IP will only be applied when a subnet or security group is defined. 36 | 37 | ### Task definition file 38 | 39 | It is highly recommended to treat the task definition "as code" by checking it into your git repository as a JSON file. Changes to any task definition attributes like container images, environment variables, CPU, and memory can be deployed with this GitHub action by editing your task definition file and pushing a new git commit. 40 | 41 | An existing task definition can be downloaded to a JSON file with the following command. Account IDs can be removed from the file by removing the `taskDefinitionArn` attribute, and updating the `executionRoleArn` and `taskRoleArn` attribute values to contain role names instead of role ARNs. 42 | ```sh 43 | aws ecs describe-task-definition \ 44 | --task-definition my-task-definition-family \ 45 | --query taskDefinition > task-definition.json 46 | ``` 47 | 48 | Alternatively, you can start a new task definition file from scratch with the following command. In the generated file, fill in your attribute values and remove any attributes not needed for your application. 49 | ```sh 50 | aws ecs register-task-definition \ 51 | --generate-cli-skeleton > task-definition.json 52 | ``` 53 | 54 | If you do not wish to store your task definition as a file in your git repository, your GitHub Actions workflow can download the existing task definition. 55 | ```yaml 56 | - name: Download task definition 57 | run: | 58 | aws ecs describe-task-definition --task-definition my-task-definition-family --query taskDefinition > task-definition.json 59 | ``` 60 | 61 | ### Task definition container image values 62 | 63 | It is highly recommended that each time your GitHub Actions workflow runs and builds a new container image for deployment, a new container image ID is generated. For example, use the commit ID as the new image's tag, instead of updating the 'latest' tag with the new image. Using a unique container image ID for each deployment allows rolling back to a previous container image. 64 | 65 | The task definition file can be updated prior to deployment with the new container image ID using [the `aws-actions/amazon-ecs-render-task-definition` action](https://github.com/aws-actions/amazon-ecs-render-task-definition). The following example builds a new container image tagged with the commit ID, inserts the new image ID as the image for the `my-container` container in the task definition file, and then deploys the rendered task definition file to ECS: 66 | 67 | ```yaml 68 | - name: Configure AWS credentials 69 | uses: aws-actions/configure-aws-credentials@v1 70 | with: 71 | aws-access-key-id: ${{ secrets.AWS_ACCESS_KEY_ID }} 72 | aws-secret-access-key: ${{ secrets.AWS_SECRET_ACCESS_KEY }} 73 | aws-region: us-east-2 74 | 75 | - name: Login to Amazon ECR 76 | id: login-ecr 77 | uses: aws-actions/amazon-ecr-login@v1 78 | 79 | - name: Build, tag, and push image to Amazon ECR 80 | id: build-image 81 | env: 82 | ECR_REGISTRY: ${{ steps.login-ecr.outputs.registry }} 83 | ECR_REPOSITORY: my-ecr-repo 84 | IMAGE_TAG: ${{ github.sha }} 85 | run: | 86 | docker build -t $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG . 87 | docker push $ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG 88 | echo "image=$ECR_REGISTRY/$ECR_REPOSITORY:$IMAGE_TAG" >> $GITHUB_OUTPUT 89 | 90 | - name: Fill in the new image ID in the Amazon ECS task definition 91 | id: task-def 92 | uses: aws-actions/amazon-ecs-render-task-definition@v1 93 | with: 94 | task-definition: task-definition.json 95 | container-name: my-container 96 | image: ${{ steps.build-image.outputs.image }} 97 | 98 | - name: Deploy Amazon ECS task definition 99 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 100 | with: 101 | task-definition: ${{ steps.task-def.outputs.task-definition }} 102 | service: my-service 103 | cluster: my-cluster 104 | wait-for-service-stability: true 105 | ``` 106 | 107 | ### Tags 108 | 109 | To turn on [Amazon ECS-managed tags](https://docs.aws.amazon.com/AmazonECS/latest/developerguide/ecs-using-tags.html#managed-tags) `aws:ecs:serviceName` and `aws:ecs:clusterName` for the tasks in the service or the standalone tasks by setting `enable-ecs-managed-tags`: 110 | 111 | ```yaml 112 | - name: Deploy Amazon ECS task definition 113 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 114 | with: 115 | task-definition: task-definition.json 116 | service: my-service 117 | cluster: my-cluster 118 | wait-for-service-stability: true 119 | enable-ecs-managed-tags: true 120 | ``` 121 | 122 | You can propagate your custom tags from your existing service using `propagate-tags`: 123 | 124 | ```yaml 125 | - name: Deploy Amazon ECS task definition 126 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 127 | with: 128 | task-definition: task-definition.json 129 | service: my-service 130 | cluster: my-cluster 131 | wait-for-service-stability: true 132 | propagate-tags: SERVICE 133 | ``` 134 | 135 | ### EBS Volume Configuration 136 | This action supports configuring Amazon EBS volumes for both services and standalone tasks. 137 | 138 | For Services (Update Service): 139 | 140 | ```yaml 141 | - name: Deploy to Amazon ECS with EBS Volume 142 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 143 | with: 144 | task-definition: task-definition.json 145 | service: my-service 146 | cluster: my-cluster 147 | wait-for-service-stability: true 148 | service-managed-ebs-volume-name: "ebs1" 149 | service-managed-ebs-volume: '{"sizeInGiB": 30, "volumeType": "gp3", "encrypted": true, "roleArn":"arn:aws:iam:::role/ebs-role"}' 150 | ``` 151 | 152 | Note: Your task definition must include a volume that is configuredAtLaunch: 153 | 154 | ```json 155 | ... 156 | "volumes": [ 157 | { 158 | "name": "ebs1", 159 | "configuredAtLaunch": true 160 | } 161 | ], 162 | ... 163 | ``` 164 | 165 | For Standalone Tasks (RunTask): 166 | 167 | ```yaml 168 | - name: Deploy to Amazon ECS 169 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 170 | with: 171 | task-definition: task-definition.json 172 | cluster: my-cluster 173 | run-task: true 174 | run-task-launch-type: EC2 175 | run-task-managed-ebs-volume-name: "ebs1" 176 | run-task-managed-ebs-volume: '{"filesystemType":"xfs", "roleArn":"arn:aws:iam:::role/github-actions-setup-stack-EBSRole-YwVmgS4g7gQE", "encrypted":false, "sizeInGiB":30}' 177 | ``` 178 | 179 | ## Credentials and Region 180 | 181 | This action relies on the [default behavior of the AWS SDK for Javascript](https://docs.aws.amazon.com/sdk-for-javascript/v2/developer-guide/setting-credentials-node.html) to determine AWS credentials and region. 182 | Use [the `aws-actions/configure-aws-credentials` action](https://github.com/aws-actions/configure-aws-credentials) to configure the GitHub Actions environment with environment variables containing AWS credentials and your desired region. 183 | 184 | We recommend following [Amazon IAM best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) for the AWS credentials used in GitHub Actions workflows, including: 185 | * Do not store credentials in your repository's code. You may use [GitHub Actions secrets](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/creating-and-using-encrypted-secrets) to store credentials and redact credentials from GitHub Actions workflow logs. 186 | * [Create an individual IAM user](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#create-iam-users) with an access key for use in GitHub Actions workflows, preferably one per repository. Do not use the AWS account root user access key. 187 | * [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) to the credentials used in GitHub Actions workflows. Grant only the permissions required to perform the actions in your GitHub Actions workflows. See the Permissions section below for the permissions required by this action. 188 | * [Rotate the credentials](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#rotate-credentials) used in GitHub Actions workflows regularly. 189 | * [Monitor the activity](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#keep-a-log) of the credentials used in GitHub Actions workflows. 190 | 191 | ## Permissions 192 | 193 | Running a service requires the following minimum set of permissions: 194 | ```json 195 | { 196 | "Version":"2012-10-17", 197 | "Statement":[ 198 | { 199 | "Sid":"RegisterTaskDefinition", 200 | "Effect":"Allow", 201 | "Action":[ 202 | "ecs:RegisterTaskDefinition" 203 | ], 204 | "Resource":"*" 205 | }, 206 | { 207 | "Sid":"PassRolesInTaskDefinition", 208 | "Effect":"Allow", 209 | "Action":[ 210 | "iam:PassRole" 211 | ], 212 | "Resource":[ 213 | "arn:aws:iam:::role/", 214 | "arn:aws:iam:::role/" 215 | ] 216 | }, 217 | { 218 | "Sid":"DeployService", 219 | "Effect":"Allow", 220 | "Action":[ 221 | "ecs:UpdateService", 222 | "ecs:DescribeServices" 223 | ], 224 | "Resource":[ 225 | "arn:aws:ecs:::service//" 226 | ] 227 | } 228 | ] 229 | } 230 | ``` 231 | 232 | Running a one-off/stand-alone task requires the following minimum set of permissions: 233 | ```json 234 | { 235 | "Version": "2012-10-17", 236 | "Statement":[ 237 | { 238 | "Sid": "VisualEditor0", 239 | "Effect": "Allow", 240 | "Action":[ 241 | "ecs:RunTask", 242 | "ecs:RegisterTaskDefinition", 243 | "ecs:DescribeTasks" 244 | ], 245 | "Resource": "*" 246 | }, 247 | { 248 | "Sid": "PassRolesInTaskDefinition", 249 | "Effect":"Allow", 250 | "Action":[ 251 | "iam:PassRole" 252 | ], 253 | "Resource":[ 254 | "arn:aws:iam:::role/", 255 | "arn:aws:iam:::role/" 256 | ] 257 | } 258 | ] 259 | } 260 | ``` 261 | Note: the policy above assumes the account has opted in to the ECS long ARN format. 262 | 263 | ## AWS CodeDeploy Support 264 | 265 | For ECS services that uses the `CODE_DEPLOY` deployment controller, additional configuration is needed for this action: 266 | 267 | ```yaml 268 | - name: Deploy to Amazon ECS 269 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 270 | with: 271 | task-definition: task-definition.json 272 | service: my-service 273 | cluster: my-cluster 274 | wait-for-service-stability: true 275 | codedeploy-appspec: appspec.json 276 | codedeploy-application: my-codedeploy-application 277 | codedeploy-deployment-group: my-codedeploy-deployment-group 278 | ``` 279 | 280 | The minimal permissions require access to CodeDeploy: 281 | 282 | ```json 283 | { 284 | "Version":"2012-10-17", 285 | "Statement":[ 286 | { 287 | "Sid":"RegisterTaskDefinition", 288 | "Effect":"Allow", 289 | "Action":[ 290 | "ecs:RegisterTaskDefinition" 291 | ], 292 | "Resource":"*" 293 | }, 294 | { 295 | "Sid":"PassRolesInTaskDefinition", 296 | "Effect":"Allow", 297 | "Action":[ 298 | "iam:PassRole" 299 | ], 300 | "Resource":[ 301 | "arn:aws:iam:::role/", 302 | "arn:aws:iam:::role/" 303 | ] 304 | }, 305 | { 306 | "Sid":"DeployService", 307 | "Effect":"Allow", 308 | "Action":[ 309 | "ecs:DescribeServices", 310 | "codedeploy:GetDeploymentGroup", 311 | "codedeploy:CreateDeployment", 312 | "codedeploy:GetDeployment", 313 | "codedeploy:GetDeploymentConfig", 314 | "codedeploy:RegisterApplicationRevision" 315 | ], 316 | "Resource":[ 317 | "arn:aws:ecs:::service//", 318 | "arn:aws:codedeploy:::deploymentgroup:/", 319 | "arn:aws:codedeploy:::deploymentconfig:*", 320 | "arn:aws:codedeploy:::application:" 321 | ] 322 | } 323 | ] 324 | } 325 | ``` 326 | 327 | ## Running Tasks 328 | 329 | For services which need an initialization task, such as database migrations, or ECS tasks that are run without a service, additional configuration can be added to trigger an ad-hoc task run. When combined with GitHub Action's `on: schedule` triggers, runs can also be scheduled without EventBridge. 330 | 331 | In the following example, the service would not be updated until the ad-hoc task exits successfully. 332 | 333 | ```yaml 334 | - name: Deploy to Amazon ECS 335 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 336 | with: 337 | task-definition: task-definition.json 338 | service: my-service 339 | cluster: my-cluster 340 | wait-for-service-stability: true 341 | run-task: true 342 | wait-for-task-stopped: true 343 | ``` 344 | 345 | Overrides and VPC networking options are available as well. See [action.yml](action.yml) for more details. The `FARGATE` 346 | launch type requires `awsvpc` network mode in your task definition and you must specify a network configuration. 347 | 348 | ### Tags 349 | 350 | To tag your tasks: 351 | 352 | * to turn on Amazon ECS-managed tags (`aws:ecs:clusterName`), use `enable-ecs-managed-tags` 353 | * for custom tags, use `run-task-tags` 354 | 355 | ```yaml 356 | - name: Deploy to Amazon ECS 357 | uses: aws-actions/amazon-ecs-deploy-task-definition@v2 358 | with: 359 | task-definition: task-definition.json 360 | service: my-service 361 | cluster: my-cluster 362 | wait-for-service-stability: true 363 | run-task: true 364 | enable-ecs-managed-tags: true 365 | run-task-tags: '[{"key": "project", "value": "myproject"}]' 366 | wait-for-task-stopped: true 367 | ``` 368 | 369 | ## Troubleshooting 370 | 371 | This action emits debug logs to help troubleshoot deployment failures. To see the debug logs, create a secret named `ACTIONS_STEP_DEBUG` with value `true` in your repository. 372 | 373 | ## License Summary 374 | 375 | This code is made available under the MIT license. 376 | 377 | ## Security Disclosures 378 | 379 | If you would like to report a potential security issue in this project, please do not create a GitHub issue. Instead, please follow the instructions [here](https://aws.amazon.com/security/vulnerability-reporting/) or [email AWS security directly](mailto:aws-security@amazon.com). 380 | 381 | -------------------------------------------------------------------------------- /THIRD-PARTY: -------------------------------------------------------------------------------- 1 | ** AWS SDK for JavaScript; version 2.562.0 -- https://github.com/aws/aws-sdk-js 2 | Copyright 2012-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. 3 | 4 | Apache License 5 | 6 | Version 2.0, January 2004 7 | 8 | http://www.apache.org/licenses/ TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND 9 | DISTRIBUTION 10 | 11 | 1. Definitions. 12 | 13 | "License" shall mean the terms and conditions for use, reproduction, and 14 | distribution as defined by Sections 1 through 9 of this document. 15 | 16 | "Licensor" shall mean the copyright owner or entity authorized by the 17 | copyright owner that is granting the License. 18 | 19 | "Legal Entity" shall mean the union of the acting entity and all other 20 | entities that control, are controlled by, or are under common control 21 | with that entity. For the purposes of this definition, "control" means 22 | (i) the power, direct or indirect, to cause the direction or management 23 | of such entity, whether by contract or otherwise, or (ii) ownership of 24 | fifty percent (50%) or more of the outstanding shares, or (iii) 25 | beneficial ownership of such entity. 26 | 27 | "You" (or "Your") shall mean an individual or Legal Entity exercising 28 | permissions granted by this License. 29 | 30 | "Source" form shall mean the preferred form for making modifications, 31 | including but not limited to software source code, documentation source, 32 | and configuration files. 33 | 34 | "Object" form shall mean any form resulting from mechanical 35 | transformation or translation of a Source form, including but not limited 36 | to compiled object code, generated documentation, and conversions to 37 | other media types. 38 | 39 | "Work" shall mean the work of authorship, whether in Source or Object 40 | form, made available under the License, as indicated by a copyright 41 | notice that is included in or attached to the work (an example is 42 | provided in the Appendix below). 43 | 44 | "Derivative Works" shall mean any work, whether in Source or Object form, 45 | that is based on (or derived from) the Work and for which the editorial 46 | revisions, annotations, elaborations, or other modifications represent, 47 | as a whole, an original work of authorship. For the purposes of this 48 | License, Derivative Works shall not include works that remain separable 49 | from, or merely link (or bind by name) to the interfaces of, the Work and 50 | Derivative Works thereof. 51 | 52 | "Contribution" shall mean any work of authorship, including the original 53 | version of the Work and any modifications or additions to that Work or 54 | Derivative Works thereof, that is intentionally submitted to Licensor for 55 | inclusion in the Work by the copyright owner or by an individual or Legal 56 | Entity authorized to submit on behalf of the copyright owner. For the 57 | purposes of this definition, "submitted" means any form of electronic, 58 | verbal, or written communication sent to the Licensor or its 59 | representatives, including but not limited to communication on electronic 60 | mailing lists, source code control systems, and issue tracking systems 61 | that are managed by, or on behalf of, the Licensor for the purpose of 62 | discussing and improving the Work, but excluding communication that is 63 | conspicuously marked or otherwise designated in writing by the copyright 64 | owner as "Not a Contribution." 65 | 66 | "Contributor" shall mean Licensor and any individual or Legal Entity on 67 | behalf of whom a Contribution has been received by Licensor and 68 | subsequently incorporated within the Work. 69 | 70 | 2. Grant of Copyright License. Subject to the terms and conditions of this 71 | License, each Contributor hereby grants to You a perpetual, worldwide, 72 | non-exclusive, no-charge, royalty-free, irrevocable copyright license to 73 | reproduce, prepare Derivative Works of, publicly display, publicly perform, 74 | sublicense, and distribute the Work and such Derivative Works in Source or 75 | Object form. 76 | 77 | 3. Grant of Patent License. Subject to the terms and conditions of this 78 | License, each Contributor hereby grants to You a perpetual, worldwide, 79 | non-exclusive, no-charge, royalty-free, irrevocable (except as stated in 80 | this section) patent license to make, have made, use, offer to sell, sell, 81 | import, and otherwise transfer the Work, where such license applies only to 82 | those patent claims licensable by such Contributor that are necessarily 83 | infringed by their Contribution(s) alone or by combination of their 84 | Contribution(s) with the Work to which such Contribution(s) was submitted. 85 | If You institute patent litigation against any entity (including a 86 | cross-claim or counterclaim in a lawsuit) alleging that the Work or a 87 | Contribution incorporated within the Work constitutes direct or contributory 88 | patent infringement, then any patent licenses granted to You under this 89 | License for that Work shall terminate as of the date such litigation is 90 | filed. 91 | 92 | 4. Redistribution. You may reproduce and distribute copies of the Work or 93 | Derivative Works thereof in any medium, with or without modifications, and 94 | in Source or Object form, provided that You meet the following conditions: 95 | 96 | (a) You must give any other recipients of the Work or Derivative Works a 97 | copy of this License; and 98 | 99 | (b) You must cause any modified files to carry prominent notices stating 100 | that You changed the files; and 101 | 102 | (c) You must retain, in the Source form of any Derivative Works that You 103 | distribute, all copyright, patent, trademark, and attribution notices 104 | from the Source form of the Work, excluding those notices that do not 105 | pertain to any part of the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must include 109 | a readable copy of the attribution notices contained within such NOTICE 110 | file, excluding those notices that do not pertain to any part of the 111 | Derivative Works, in at least one of the following places: within a 112 | NOTICE text file distributed as part of the Derivative Works; within the 113 | Source form or documentation, if provided along with the Derivative 114 | Works; or, within a display generated by the Derivative Works, if and 115 | wherever such third-party notices normally appear. The contents of the 116 | NOTICE file are for informational purposes only and do not modify the 117 | License. You may add Your own attribution notices within Derivative Works 118 | that You distribute, alongside or as an addendum to the NOTICE text from 119 | the Work, provided that such additional attribution notices cannot be 120 | construed as modifying the License. 121 | 122 | You may add Your own copyright statement to Your modifications and may 123 | provide additional or different license terms and conditions for use, 124 | reproduction, or distribution of Your modifications, or for any such 125 | Derivative Works as a whole, provided Your use, reproduction, and 126 | distribution of the Work otherwise complies with the conditions stated in 127 | this License. 128 | 129 | 5. Submission of Contributions. Unless You explicitly state otherwise, any 130 | Contribution intentionally submitted for inclusion in the Work by You to the 131 | Licensor shall be under the terms and conditions of this License, without 132 | any additional terms or conditions. Notwithstanding the above, nothing 133 | herein shall supersede or modify the terms of any separate license agreement 134 | you may have executed with Licensor regarding such Contributions. 135 | 136 | 6. Trademarks. This License does not grant permission to use the trade 137 | names, trademarks, service marks, or product names of the Licensor, except 138 | as required for reasonable and customary use in describing the origin of the 139 | Work and reproducing the content of the NOTICE file. 140 | 141 | 7. Disclaimer of Warranty. Unless required by applicable law or agreed to in 142 | writing, Licensor provides the Work (and each Contributor provides its 143 | Contributions) on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY 144 | KIND, either express or implied, including, without limitation, any 145 | warranties or conditions of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or 146 | FITNESS FOR A PARTICULAR PURPOSE. You are solely responsible for determining 147 | the appropriateness of using or redistributing the Work and assume any risks 148 | associated with Your exercise of permissions under this License. 149 | 150 | 8. Limitation of Liability. In no event and under no legal theory, whether 151 | in tort (including negligence), contract, or otherwise, unless required by 152 | applicable law (such as deliberate and grossly negligent acts) or agreed to 153 | in writing, shall any Contributor be liable to You for damages, including 154 | any direct, indirect, special, incidental, or consequential damages of any 155 | character arising as a result of this License or out of the use or inability 156 | to use the Work (including but not limited to damages for loss of goodwill, 157 | work stoppage, computer failure or malfunction, or any and all other 158 | commercial damages or losses), even if such Contributor has been advised of 159 | the possibility of such damages. 160 | 161 | 9. Accepting Warranty or Additional Liability. While redistributing the Work 162 | or Derivative Works thereof, You may choose to offer, and charge a fee for, 163 | acceptance of support, warranty, indemnity, or other liability obligations 164 | and/or rights consistent with this License. However, in accepting such 165 | obligations, You may act only on Your own behalf and on Your sole 166 | responsibility, not on behalf of any other Contributor, and only if You 167 | agree to indemnify, defend, and hold each Contributor harmless for any 168 | liability incurred by, or claims asserted against, such Contributor by 169 | reason of your accepting any such warranty or additional liability. END OF 170 | TERMS AND CONDITIONS 171 | 172 | APPENDIX: How to apply the Apache License to your work. 173 | 174 | To apply the Apache License to your work, attach the following boilerplate 175 | notice, with the fields enclosed by brackets "[]" replaced with your own 176 | identifying information. (Don't include the brackets!) The text should be 177 | enclosed in the appropriate comment syntax for the file format. We also 178 | recommend that a file or class name and description of purpose be included on 179 | the same "printed page" as the copyright notice for easier identification 180 | within third-party archives. 181 | 182 | Copyright [yyyy] [name of copyright owner] 183 | 184 | Licensed under the Apache License, Version 2.0 (the "License"); 185 | 186 | you may not use this file except in compliance with the License. 187 | 188 | You may obtain a copy of the License at 189 | 190 | http://www.apache.org/licenses/LICENSE-2.0 191 | 192 | Unless required by applicable law or agreed to in writing, software 193 | 194 | distributed under the License is distributed on an "AS IS" BASIS, 195 | 196 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 197 | 198 | See the License for the specific language governing permissions and 199 | 200 | limitations under the License. 201 | 202 | * For AWS SDK for JavaScript see also this required NOTICE: 203 | Copyright 2012-2018 Amazon.com, Inc. or its affiliates. All Rights 204 | Reserved. 205 | 206 | ------ 207 | 208 | ** GitHub Actions Toolkit; version 1.2.0 -- https://github.com/actions/toolkit 209 | Copyright 2019 GitHub 210 | 211 | MIT License 212 | 213 | Copyright (c) 214 | 215 | Permission is hereby granted, free of charge, to any person obtaining a copy of 216 | this software and associated documentation files (the "Software"), to deal in 217 | the Software without restriction, including without limitation the rights to 218 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies 219 | of the Software, and to permit persons to whom the Software is furnished to do 220 | so, subject to the following conditions: 221 | 222 | The above copyright notice and this permission notice shall be included in all 223 | copies or substantial portions of the Software. 224 | 225 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 226 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 227 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 228 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 229 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 230 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 231 | SOFTWARE. 232 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Amazon ECS "Deploy Task Definition" Action for GitHub Actions' 2 | description: 'Registers an Amazon ECS task definition, and deploys it to an ECS service' 3 | branding: 4 | icon: 'cloud' 5 | color: 'orange' 6 | inputs: 7 | task-definition: 8 | description: 'The path to the ECS task definition file to register.' 9 | required: true 10 | desired-count: 11 | description: 'The number of instantiations of the task to place and keep running in your service.' 12 | required: false 13 | service: 14 | description: 'The name of the ECS service to deploy to. If no service is given, the action will not deploy the task, but only register the task definition.' 15 | required: false 16 | cluster: 17 | description: "The name of the ECS service's cluster. Will default to the 'default' cluster." 18 | required: false 19 | wait-for-service-stability: 20 | description: 'Whether to wait for the ECS service to reach stable state after deploying the new task definition. Valid value is "true". Will default to not waiting.' 21 | required: false 22 | wait-for-minutes: 23 | description: 'How long to wait for the ECS service to reach stable state, in minutes (default: 30 minutes, max: 6 hours). For CodeDeploy deployments, any wait time configured in the CodeDeploy deployment group will be added to this value.' 24 | required: false 25 | codedeploy-appspec: 26 | description: "The path to the AWS CodeDeploy AppSpec file, if the ECS service uses the CODE_DEPLOY deployment controller. Will default to 'appspec.yaml'." 27 | required: false 28 | codedeploy-application: 29 | description: "The name of the AWS CodeDeploy application, if the ECS service uses the CODE_DEPLOY deployment controller. Will default to 'AppECS-{cluster}-{service}'." 30 | required: false 31 | codedeploy-deployment-group: 32 | description: "The name of the AWS CodeDeploy deployment group, if the ECS service uses the CODE_DEPLOY deployment controller. Will default to 'DgpECS-{cluster}-{service}'." 33 | required: false 34 | codedeploy-deployment-description: 35 | description: "A description of the deployment, if the ECS service uses the CODE_DEPLOY deployment controller. NOTE: This will be truncated to 512 characters if necessary." 36 | required: false 37 | codedeploy-deployment-config: 38 | description: "The name of the AWS CodeDeploy deployment configuration, if the ECS service uses the CODE_DEPLOY deployment controller. If not specified, the value configured in the deployment group or `CodeDeployDefault.OneAtATime` is used as the default." 39 | required: false 40 | force-new-deployment: 41 | description: 'Whether to force a new deployment of the service. Valid value is "true". Will default to not force a new deployment.' 42 | required: false 43 | service-managed-ebs-volume-name: 44 | description: "The name of the volume, to be manage in the ECS service. This value must match the volume name from the Volume object in the task definition, that was configuredAtLaunch." 45 | required: false 46 | service-managed-ebs-volume: 47 | description: "A JSON object defining the configuration settings for the EBS Service volume that was ConfiguredAtLaunch. You can configure size, volumeType, IOPS, throughput, snapshot and encryption in ServiceManagedEBSVolumeConfiguration. Currently, the only supported volume type is an Amazon EBS volume." 48 | required: false 49 | run-task: 50 | description: 'A boolean indicating whether to run a stand-alone task in a ECS cluster. Task will run before the service is updated if both are provided. Default value is false .' 51 | required: false 52 | run-task-container-overrides: 53 | description: 'A JSON array of container override objects which should applied when running a task outside of a service. Warning: Do not expose this field to untrusted inputs. More details: https://docs.aws.amazon.com/AmazonECS/latest/APIReference/API_ContainerOverride.html' 54 | required: false 55 | run-task-security-groups: 56 | description: 'A comma-separated list of security group IDs to assign to a task when run outside of a service. Will default to none.' 57 | required: false 58 | run-task-subnets: 59 | description: 'A comma-separated list of subnet IDs to assign to a task when run outside of a service. Will default to none.' 60 | required: false 61 | run-task-assign-public-IP: 62 | description: "Whether the task's elastic network interface receives a public IP address. The default value is DISABLED but will only be applied if run-task-subnets or run-task-security-groups are also set." 63 | required: false 64 | run-task-capacity-provider-strategy: 65 | description: 'A JSON array of capacity provider strategy items which should applied when running a task outside of a service. Will default to none.' 66 | required: false 67 | run-task-launch-type: 68 | description: "ECS launch type for tasks run outside of a service. Valid values are 'FARGATE' or 'EC2'. Will default to 'FARGATE'. Will only be applied if run-task-capacity-provider-strategy is not set." 69 | required: false 70 | run-task-started-by: 71 | description: "A name to use for the startedBy tag when running a task outside of a service. Will default to 'GitHub-Actions'." 72 | required: false 73 | run-task-tags: 74 | description: 'A JSON array of tags.' 75 | required: false 76 | run-task-managed-ebs-volume-name: 77 | description: "The name of the volume. This value must match the volume name from the Volume object in the task definition, that was configuredAtLaunch." 78 | required: false 79 | run-task-managed-ebs-volume: 80 | description: "A JSON object defining the configuration settings for the Amazon EBS task volume that was configuredAtLaunch. These settings are used to create each Amazon EBS volume, with one volume created for each task in the service. The Amazon EBS volumes are visible in your account in the Amazon EC2 console once they are created." 81 | required: false 82 | wait-for-task-stopped: 83 | description: 'Whether to wait for the task to stop when running it outside of a service. Will default to not wait.' 84 | required: false 85 | enable-ecs-managed-tags: 86 | description: "Determines whether to turn on Amazon ECS managed tags 'aws:ecs:serviceName' and 'aws:ecs:clusterName' for the tasks in the service." 87 | required: false 88 | propagate-tags: 89 | description: "Determines to propagate the tags from the 'SERVICE' to the task." 90 | required: false 91 | outputs: 92 | task-definition-arn: 93 | description: 'The ARN of the registered ECS task definition.' 94 | codedeploy-deployment-id: 95 | description: 'The deployment ID of the CodeDeploy deployment (if the ECS service uses the CODE_DEPLOY deployment controller).' 96 | run-task-arn: 97 | description: 'The ARN(s) of the task(s) that were started by the run-task option. Output is in an array JSON format.' 98 | 99 | runs: 100 | using: 'node20' 101 | main: 'dist/index.js' 102 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from "globals"; 2 | 3 | 4 | export default [ 5 | {files: ["**/*.js"], languageOptions: {sourceType: "commonjs"}}, 6 | {languageOptions: { globals: globals.browser }}, 7 | ]; -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | const core = require('@actions/core'); 3 | const { CodeDeploy, waitUntilDeploymentSuccessful } = require('@aws-sdk/client-codedeploy'); 4 | const { ECS, waitUntilServicesStable, waitUntilTasksStopped } = require('@aws-sdk/client-ecs'); 5 | const yaml = require('yaml'); 6 | const fs = require('fs'); 7 | const crypto = require('crypto'); 8 | const MAX_WAIT_MINUTES = 360; // 6 hours 9 | const WAIT_DEFAULT_DELAY_SEC = 15; 10 | 11 | // Attributes that are returned by DescribeTaskDefinition, but are not valid RegisterTaskDefinition inputs 12 | const IGNORED_TASK_DEFINITION_ATTRIBUTES = [ 13 | 'compatibilities', 14 | 'taskDefinitionArn', 15 | 'requiresAttributes', 16 | 'revision', 17 | 'status', 18 | 'registeredAt', 19 | 'deregisteredAt', 20 | 'registeredBy' 21 | ]; 22 | 23 | // Method to run a stand-alone task with desired inputs 24 | async function runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags) { 25 | core.info('Running task') 26 | 27 | const waitForTask = core.getInput('wait-for-task-stopped', { required: false }) || 'false'; 28 | const startedBy = core.getInput('run-task-started-by', { required: false }) || 'GitHub-Actions'; 29 | const launchType = core.getInput('run-task-launch-type', { required: false }) || 'FARGATE'; 30 | const subnetIds = core.getInput('run-task-subnets', { required: false }) || ''; 31 | const securityGroupIds = core.getInput('run-task-security-groups', { required: false }) || ''; 32 | const containerOverrides = JSON.parse(core.getInput('run-task-container-overrides', { required: false }) || '[]'); 33 | const assignPublicIP = core.getInput('run-task-assign-public-IP', { required: false }) || 'DISABLED'; 34 | const tags = JSON.parse(core.getInput('run-task-tags', { required: false }) || '[]'); 35 | const capacityProviderStrategy = JSON.parse(core.getInput('run-task-capacity-provider-strategy', { required: false }) || '[]'); 36 | const runTaskManagedEBSVolumeName = core.getInput('run-task-managed-ebs-volume-name', { required: false }) || ''; 37 | const runTaskManagedEBSVolume = core.getInput('run-task-managed-ebs-volume', { required: false }) || '{}'; 38 | 39 | let awsvpcConfiguration = {} 40 | 41 | if (subnetIds != "") { 42 | awsvpcConfiguration["subnets"] = subnetIds.split(',') 43 | } 44 | 45 | if (securityGroupIds != "") { 46 | awsvpcConfiguration["securityGroups"] = securityGroupIds.split(',') 47 | } 48 | 49 | if(assignPublicIP != "" && (subnetIds != "" || securityGroupIds != "")){ 50 | awsvpcConfiguration["assignPublicIp"] = assignPublicIP 51 | } 52 | let volumeConfigurations = []; 53 | let taskManagedEBSVolumeObject; 54 | 55 | if (runTaskManagedEBSVolumeName != '') { 56 | if (runTaskManagedEBSVolume != '{}') { 57 | taskManagedEBSVolumeObject = convertToManagedEbsVolumeObject(runTaskManagedEBSVolume); 58 | volumeConfigurations = [{ 59 | name: runTaskManagedEBSVolumeName, 60 | managedEBSVolume: taskManagedEBSVolumeObject 61 | }]; 62 | } else { 63 | core.warning(`run-task-managed-ebs-volume-name provided without run-task-managed-ebs-volume value. VolumeConfigurations property will not be included in the RunTask API call`); 64 | } 65 | } 66 | 67 | const runTaskResponse = await ecs.runTask({ 68 | startedBy: startedBy, 69 | cluster: clusterName, 70 | taskDefinition: taskDefArn, 71 | overrides: { 72 | containerOverrides: containerOverrides 73 | }, 74 | capacityProviderStrategy: capacityProviderStrategy.length === 0 ? null : capacityProviderStrategy, 75 | launchType: capacityProviderStrategy.length === 0 ? launchType : null, 76 | networkConfiguration: Object.keys(awsvpcConfiguration).length === 0 ? null : { awsvpcConfiguration: awsvpcConfiguration }, 77 | enableECSManagedTags: enableECSManagedTags, 78 | tags: tags, 79 | volumeConfigurations: volumeConfigurations 80 | }); 81 | 82 | core.debug(`Run task response ${JSON.stringify(runTaskResponse)}`) 83 | 84 | const taskArns = runTaskResponse.tasks.map(task => task.taskArn); 85 | core.setOutput('run-task-arn', taskArns); 86 | 87 | const region = await ecs.config.region(); 88 | const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com'; 89 | 90 | core.info(`Task running: https://${consoleHostname}/ecs/home?region=${region}#/clusters/${clusterName}/tasks`); 91 | 92 | if (runTaskResponse.failures && runTaskResponse.failures.length > 0) { 93 | const failure = runTaskResponse.failures[0]; 94 | throw new Error(`${failure.arn} is ${failure.reason}`); 95 | } 96 | 97 | // Wait for task to end 98 | if (waitForTask && waitForTask.toLowerCase() === "true") { 99 | await waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) 100 | await tasksExitCode(ecs, clusterName, taskArns) 101 | } else { 102 | core.debug('Not waiting for the task to stop'); 103 | } 104 | } 105 | 106 | function convertToManagedEbsVolumeObject(managedEbsVolume) { 107 | managedEbsVolumeObject = {} 108 | const ebsVolumeObject = JSON.parse(managedEbsVolume); 109 | if ('roleArn' in ebsVolumeObject){ // required property 110 | managedEbsVolumeObject.roleArn = ebsVolumeObject.roleArn; 111 | core.debug(`Found RoleArn ${ebsVolumeObject['roleArn']}`); 112 | } else { 113 | throw new Error('managed-ebs-volume must provide "role-arn" to associate with the EBS volume') 114 | } 115 | 116 | if ('encrypted' in ebsVolumeObject) { 117 | managedEbsVolumeObject.encrypted = ebsVolumeObject.encrypted; 118 | } 119 | if ('filesystemType' in ebsVolumeObject) { 120 | managedEbsVolumeObject.filesystemType = ebsVolumeObject.filesystemType; 121 | } 122 | if ('iops' in ebsVolumeObject) { 123 | managedEbsVolumeObject.iops = ebsVolumeObject.iops; 124 | } 125 | if ('kmsKeyId' in ebsVolumeObject) { 126 | managedEbsVolumeObject.kmsKeyId = ebsVolumeObject.kmsKeyId; 127 | } 128 | if ('sizeInGiB' in ebsVolumeObject) { 129 | managedEbsVolumeObject.sizeInGiB = ebsVolumeObject.sizeInGiB; 130 | } 131 | if ('snapshotId' in ebsVolumeObject) { 132 | managedEbsVolumeObject.snapshotId = ebsVolumeObject.snapshotId; 133 | } 134 | if ('tagSpecifications' in ebsVolumeObject) { 135 | managedEbsVolumeObject.tagSpecifications = ebsVolumeObject.tagSpecifications; 136 | } 137 | if (('throughput' in ebsVolumeObject) && (('volumeType' in ebsVolumeObject) && (ebsVolumeObject.volumeType == 'gp3'))){ 138 | managedEbsVolumeObject.throughput = ebsVolumeObject.throughput; 139 | } 140 | if ('volumeType' in ebsVolumeObject) { 141 | managedEbsVolumeObject.volumeType = ebsVolumeObject.volumeType; 142 | } 143 | core.debug(`Created managedEbsVolumeObject: ${JSON.stringify(managedEbsVolumeObject)}`); 144 | return managedEbsVolumeObject; 145 | } 146 | 147 | // Poll tasks until they enter a stopped state 148 | async function waitForTasksStopped(ecs, clusterName, taskArns, waitForMinutes) { 149 | if (waitForMinutes > MAX_WAIT_MINUTES) { 150 | waitForMinutes = MAX_WAIT_MINUTES; 151 | } 152 | 153 | core.info(`Waiting for tasks to stop. Will wait for ${waitForMinutes} minutes`); 154 | 155 | const waitTaskResponse = await waitUntilTasksStopped({ 156 | client: ecs, 157 | minDelay: WAIT_DEFAULT_DELAY_SEC, 158 | maxWaitTime: waitForMinutes * 60, 159 | }, { 160 | cluster: clusterName, 161 | tasks: taskArns, 162 | }); 163 | 164 | core.debug(`Run task response ${JSON.stringify(waitTaskResponse)}`); 165 | core.info('All tasks have stopped.'); 166 | } 167 | 168 | // Check a task's exit code and fail the job on error 169 | async function tasksExitCode(ecs, clusterName, taskArns) { 170 | const describeResponse = await ecs.describeTasks({ 171 | cluster: clusterName, 172 | tasks: taskArns 173 | }); 174 | 175 | const containers = [].concat(...describeResponse.tasks.map(task => task.containers)) 176 | const exitCodes = containers.map(container => container.exitCode) 177 | const reasons = containers.map(container => container.reason) 178 | 179 | const failuresIdx = []; 180 | 181 | exitCodes.filter((exitCode, index) => { 182 | if (exitCode !== 0) { 183 | failuresIdx.push(index) 184 | } 185 | }) 186 | 187 | const failures = reasons.filter((_, index) => failuresIdx.indexOf(index) !== -1) 188 | if (failures.length > 0) { 189 | throw new Error(`Run task failed: ${JSON.stringify(failures)}`); 190 | } 191 | } 192 | 193 | // Deploy to a service that uses the 'ECS' deployment controller 194 | async function updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags) { 195 | core.debug('Updating the service'); 196 | 197 | const serviceManagedEBSVolumeName = core.getInput('service-managed-ebs-volume-name', { required: false }) || ''; 198 | const serviceManagedEBSVolume = core.getInput('service-managed-ebs-volume', { required: false }) || '{}'; 199 | 200 | let volumeConfigurations = []; 201 | let serviceManagedEbsVolumeObject; 202 | 203 | if (serviceManagedEBSVolumeName != '') { 204 | if (serviceManagedEBSVolume != '{}') { 205 | serviceManagedEbsVolumeObject = convertToManagedEbsVolumeObject(serviceManagedEBSVolume); 206 | volumeConfigurations = [{ 207 | name: serviceManagedEBSVolumeName, 208 | managedEBSVolume: serviceManagedEbsVolumeObject 209 | }]; 210 | } else { 211 | core.warning('service-managed-ebs-volume-name provided without service-managed-ebs-volume value. VolumeConfigurations property will not be included in the UpdateService API call'); 212 | } 213 | } 214 | 215 | let params = { 216 | cluster: clusterName, 217 | service: service, 218 | taskDefinition: taskDefArn, 219 | forceNewDeployment: forceNewDeployment, 220 | enableECSManagedTags: enableECSManagedTags, 221 | propagateTags: propagateTags, 222 | volumeConfigurations: volumeConfigurations 223 | }; 224 | 225 | // Add the desiredCount property only if it is defined and a number. 226 | if (!isNaN(desiredCount) && desiredCount !== undefined) { 227 | params.desiredCount = desiredCount; 228 | } 229 | await ecs.updateService(params); 230 | 231 | const region = await ecs.config.region(); 232 | const consoleHostname = region.startsWith('cn') ? 'console.amazonaws.cn' : 'console.aws.amazon.com'; 233 | 234 | core.info(`Deployment started. Watch this deployment's progress in the Amazon ECS console: https://${region}.${consoleHostname}/ecs/v2/clusters/${clusterName}/services/${service}/events?region=${region}`); 235 | 236 | // Wait for service stability 237 | if (waitForService && waitForService.toLowerCase() === 'true') { 238 | core.debug(`Waiting for the service to become stable. Will wait for ${waitForMinutes} minutes`); 239 | await waitUntilServicesStable({ 240 | client: ecs, 241 | minDelay: WAIT_DEFAULT_DELAY_SEC, 242 | maxWaitTime: waitForMinutes * 60 243 | }, { 244 | services: [service], 245 | cluster: clusterName 246 | }); 247 | } else { 248 | core.debug('Not waiting for the service to become stable'); 249 | } 250 | } 251 | 252 | // Find value in a CodeDeploy AppSpec file with a case-insensitive key 253 | function findAppSpecValue(obj, keyName) { 254 | return obj[findAppSpecKey(obj, keyName)]; 255 | } 256 | 257 | function findAppSpecKey(obj, keyName) { 258 | if (!obj) { 259 | throw new Error(`AppSpec file must include property '${keyName}'`); 260 | } 261 | 262 | const keyToMatch = keyName.toLowerCase(); 263 | 264 | for (var key in obj) { 265 | if (key.toLowerCase() == keyToMatch) { 266 | return key; 267 | } 268 | } 269 | 270 | throw new Error(`AppSpec file must include property '${keyName}'`); 271 | } 272 | 273 | function isEmptyValue(value) { 274 | if (value === null || value === undefined || value === '') { 275 | return true; 276 | } 277 | 278 | if (Array.isArray(value)) { 279 | for (var element of value) { 280 | if (!isEmptyValue(element)) { 281 | // the array has at least one non-empty element 282 | return false; 283 | } 284 | } 285 | // the array has no non-empty elements 286 | return true; 287 | } 288 | 289 | if (typeof value === 'object') { 290 | for (var childValue of Object.values(value)) { 291 | if (!isEmptyValue(childValue)) { 292 | // the object has at least one non-empty property 293 | return false; 294 | } 295 | } 296 | // the object has no non-empty property 297 | return true; 298 | } 299 | 300 | return false; 301 | } 302 | 303 | function emptyValueReplacer(_, value) { 304 | if (isEmptyValue(value)) { 305 | return undefined; 306 | } 307 | 308 | if (Array.isArray(value)) { 309 | return value.filter(e => !isEmptyValue(e)); 310 | } 311 | 312 | return value; 313 | } 314 | 315 | function cleanNullKeys(obj) { 316 | return JSON.parse(JSON.stringify(obj, emptyValueReplacer)); 317 | } 318 | 319 | function removeIgnoredAttributes(taskDef) { 320 | for (var attribute of IGNORED_TASK_DEFINITION_ATTRIBUTES) { 321 | if (taskDef[attribute]) { 322 | core.warning(`Ignoring property '${attribute}' in the task definition file. ` + 323 | 'This property is returned by the Amazon ECS DescribeTaskDefinition API and may be shown in the ECS console, ' + 324 | 'but it is not a valid field when registering a new task definition. ' + 325 | 'This field can be safely removed from your task definition file.'); 326 | delete taskDef[attribute]; 327 | } 328 | } 329 | 330 | return taskDef; 331 | } 332 | 333 | function maintainValidObjects(taskDef) { 334 | if (validateProxyConfigurations(taskDef)) { 335 | taskDef.proxyConfiguration.properties.forEach((property, index, arr) => { 336 | if (!('value' in property)) { 337 | arr[index].value = ''; 338 | } 339 | if (!('name' in property)) { 340 | arr[index].name = ''; 341 | } 342 | }); 343 | } 344 | 345 | if(taskDef && taskDef.containerDefinitions){ 346 | taskDef.containerDefinitions.forEach((container) => { 347 | if(container.environment){ 348 | container.environment.forEach((property, index, arr) => { 349 | if (!('value' in property)) { 350 | arr[index].value = ''; 351 | } 352 | }); 353 | } 354 | }); 355 | } 356 | return taskDef; 357 | } 358 | 359 | function validateProxyConfigurations(taskDef){ 360 | return 'proxyConfiguration' in taskDef && taskDef.proxyConfiguration.type && taskDef.proxyConfiguration.type == 'APPMESH' && taskDef.proxyConfiguration.properties && taskDef.proxyConfiguration.properties.length > 0; 361 | } 362 | 363 | // Deploy to a service that uses the 'CODE_DEPLOY' deployment controller 364 | async function createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes) { 365 | core.debug('Updating AppSpec file with new task definition ARN'); 366 | 367 | let codeDeployAppSpecFile = core.getInput('codedeploy-appspec', { required : false }); 368 | codeDeployAppSpecFile = codeDeployAppSpecFile ? codeDeployAppSpecFile : 'appspec.yaml'; 369 | 370 | let codeDeployApp = core.getInput('codedeploy-application', { required: false }); 371 | codeDeployApp = codeDeployApp ? codeDeployApp : `AppECS-${clusterName}-${service}`; 372 | 373 | let codeDeployGroup = core.getInput('codedeploy-deployment-group', { required: false }); 374 | codeDeployGroup = codeDeployGroup ? codeDeployGroup : `DgpECS-${clusterName}-${service}`; 375 | 376 | let codeDeployDescription = core.getInput('codedeploy-deployment-description', { required: false }); 377 | 378 | let codeDeployConfig = core.getInput('codedeploy-deployment-config', { required: false }); 379 | 380 | let deploymentGroupDetails = await codedeploy.getDeploymentGroup({ 381 | applicationName: codeDeployApp, 382 | deploymentGroupName: codeDeployGroup 383 | }); 384 | deploymentGroupDetails = deploymentGroupDetails.deploymentGroupInfo; 385 | 386 | // Insert the task def ARN into the appspec file 387 | const appSpecPath = path.isAbsolute(codeDeployAppSpecFile) ? 388 | codeDeployAppSpecFile : 389 | path.join(process.env.GITHUB_WORKSPACE, codeDeployAppSpecFile); 390 | const fileContents = fs.readFileSync(appSpecPath, 'utf8'); 391 | const appSpecContents = yaml.parse(fileContents); 392 | 393 | for (var resource of findAppSpecValue(appSpecContents, 'resources')) { 394 | for (var name in resource) { 395 | const resourceContents = resource[name]; 396 | const properties = findAppSpecValue(resourceContents, 'properties'); 397 | const taskDefKey = findAppSpecKey(properties, 'taskDefinition'); 398 | properties[taskDefKey] = taskDefArn; 399 | } 400 | } 401 | 402 | const appSpecString = JSON.stringify(appSpecContents); 403 | const appSpecHash = crypto.createHash('sha256').update(appSpecString).digest('hex'); 404 | 405 | // Start the deployment with the updated appspec contents 406 | core.debug('Starting CodeDeploy deployment'); 407 | let deploymentParams = { 408 | applicationName: codeDeployApp, 409 | deploymentGroupName: codeDeployGroup, 410 | revision: { 411 | revisionType: 'AppSpecContent', 412 | appSpecContent: { 413 | content: appSpecString, 414 | sha256: appSpecHash 415 | } 416 | } 417 | }; 418 | 419 | // If it hasn't been set then we don't even want to pass it to the api call to maintain previous behaviour. 420 | if (codeDeployDescription) { 421 | // CodeDeploy Deployment Descriptions have a max length of 512 characters, so truncate if necessary 422 | deploymentParams.description = (codeDeployDescription.length <= 512) ? codeDeployDescription : `${codeDeployDescription.substring(0,511)}…`; 423 | } 424 | if (codeDeployConfig) { 425 | deploymentParams.deploymentConfigName = codeDeployConfig 426 | } 427 | const createDeployResponse = await codedeploy.createDeployment(deploymentParams); 428 | core.setOutput('codedeploy-deployment-id', createDeployResponse.deploymentId); 429 | 430 | const region = await codedeploy.config.region(); 431 | core.info(`Deployment started. Watch this deployment's progress in the AWS CodeDeploy console: https://console.aws.amazon.com/codesuite/codedeploy/deployments/${createDeployResponse.deploymentId}?region=${region}`); 432 | 433 | // Wait for deployment to complete 434 | if (waitForService && waitForService.toLowerCase() === 'true') { 435 | // Determine wait time 436 | const deployReadyWaitMin = deploymentGroupDetails.blueGreenDeploymentConfiguration.deploymentReadyOption.waitTimeInMinutes; 437 | const terminationWaitMin = deploymentGroupDetails.blueGreenDeploymentConfiguration.terminateBlueInstancesOnDeploymentSuccess.terminationWaitTimeInMinutes; 438 | let totalWaitMin = deployReadyWaitMin + terminationWaitMin + waitForMinutes; 439 | if (totalWaitMin > MAX_WAIT_MINUTES) { 440 | totalWaitMin = MAX_WAIT_MINUTES; 441 | } 442 | core.debug(`Waiting for the deployment to complete. Will wait for ${totalWaitMin} minutes`); 443 | await waitUntilDeploymentSuccessful({ 444 | client: codedeploy, 445 | minDelay: WAIT_DEFAULT_DELAY_SEC, 446 | maxWaitTime: totalWaitMin * 60 447 | }, { 448 | deploymentId: createDeployResponse.deploymentId 449 | }); 450 | } else { 451 | core.debug('Not waiting for the deployment to complete'); 452 | } 453 | } 454 | 455 | async function run() { 456 | try { 457 | const ecs = new ECS({ 458 | customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions' 459 | }); 460 | const codedeploy = new CodeDeploy({ 461 | customUserAgent: 'amazon-ecs-deploy-task-definition-for-github-actions' 462 | }); 463 | 464 | // Get inputs 465 | const taskDefinitionFile = core.getInput('task-definition', { required: true }); 466 | const service = core.getInput('service', { required: false }); 467 | const cluster = core.getInput('cluster', { required: false }); 468 | const waitForService = core.getInput('wait-for-service-stability', { required: false }); 469 | let waitForMinutes = parseInt(core.getInput('wait-for-minutes', { required: false })) || 30; 470 | 471 | if (waitForMinutes > MAX_WAIT_MINUTES) { 472 | waitForMinutes = MAX_WAIT_MINUTES; 473 | } 474 | 475 | const forceNewDeployInput = core.getInput('force-new-deployment', { required: false }) || 'false'; 476 | const forceNewDeployment = forceNewDeployInput.toLowerCase() === 'true'; 477 | const desiredCount = parseInt((core.getInput('desired-count', {required: false}))); 478 | const enableECSManagedTagsInput = core.getInput('enable-ecs-managed-tags', { required: false }) || ''; 479 | let enableECSManagedTags = null; 480 | if (enableECSManagedTagsInput !== '') { 481 | enableECSManagedTags = enableECSManagedTagsInput.toLowerCase() === 'true'; 482 | } 483 | const propagateTagsInput = core.getInput('propagate-tags', { required: false }) || ''; 484 | let propagateTags = null; 485 | if (propagateTagsInput !== '') { 486 | propagateTags = propagateTagsInput; 487 | } 488 | 489 | // Register the task definition 490 | core.debug('Registering the task definition'); 491 | const taskDefPath = path.isAbsolute(taskDefinitionFile) ? 492 | taskDefinitionFile : 493 | path.join(process.env.GITHUB_WORKSPACE, taskDefinitionFile); 494 | const fileContents = fs.readFileSync(taskDefPath, 'utf8'); 495 | const taskDefContents = maintainValidObjects(removeIgnoredAttributes(cleanNullKeys(yaml.parse(fileContents)))); 496 | let registerResponse; 497 | try { 498 | registerResponse = await ecs.registerTaskDefinition(taskDefContents); 499 | } catch (error) { 500 | core.setFailed("Failed to register task definition in ECS: " + error.message); 501 | core.debug("Task definition contents:"); 502 | core.debug(JSON.stringify(taskDefContents, undefined, 4)); 503 | throw(error); 504 | } 505 | const taskDefArn = registerResponse.taskDefinition.taskDefinitionArn; 506 | core.setOutput('task-definition-arn', taskDefArn); 507 | 508 | // Run the task outside of the service 509 | const clusterName = cluster ? cluster : 'default'; 510 | const shouldRunTaskInput = core.getInput('run-task', { required: false }) || 'false'; 511 | const shouldRunTask = shouldRunTaskInput.toLowerCase() === 'true'; 512 | core.debug(`shouldRunTask: ${shouldRunTask}`); 513 | if (shouldRunTask) { 514 | core.debug("Running ad-hoc task..."); 515 | await runTask(ecs, clusterName, taskDefArn, waitForMinutes, enableECSManagedTags); 516 | } 517 | 518 | // Update the service with the new task definition 519 | if (service) { 520 | // Determine the deployment controller 521 | const describeResponse = await ecs.describeServices({ 522 | services: [service], 523 | cluster: clusterName 524 | }); 525 | 526 | if (describeResponse.failures && describeResponse.failures.length > 0) { 527 | const failure = describeResponse.failures[0]; 528 | throw new Error(`${failure.arn} is ${failure.reason}`); 529 | } 530 | 531 | const serviceResponse = describeResponse.services[0]; 532 | if (serviceResponse.status != 'ACTIVE') { 533 | throw new Error(`Service is ${serviceResponse.status}`); 534 | } 535 | 536 | if (!serviceResponse.deploymentController || !serviceResponse.deploymentController.type || serviceResponse.deploymentController.type === 'ECS') { 537 | // Service uses the 'ECS' deployment controller, so we can call UpdateService 538 | core.debug('Updating service...'); 539 | await updateEcsService(ecs, clusterName, service, taskDefArn, waitForService, waitForMinutes, forceNewDeployment, desiredCount, enableECSManagedTags, propagateTags); 540 | 541 | } else if (serviceResponse.deploymentController.type === 'CODE_DEPLOY') { 542 | // Service uses CodeDeploy, so we should start a CodeDeploy deployment 543 | core.debug('Deploying service in the default cluster'); 544 | await createCodeDeployDeployment(codedeploy, clusterName, service, taskDefArn, waitForService, waitForMinutes); 545 | } else { 546 | throw new Error(`Unsupported deployment controller: ${serviceResponse.deploymentController.type}`); 547 | } 548 | } else { 549 | core.debug('Service was not specified, no service updated'); 550 | } 551 | } 552 | catch (error) { 553 | core.setFailed(error.message); 554 | core.debug(error.stack); 555 | } 556 | } 557 | 558 | module.exports = run; 559 | 560 | /* istanbul ignore next */ 561 | if (require.main === module) { 562 | run(); 563 | } 564 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const run = require('.'); 2 | const core = require('@actions/core'); 3 | const { CodeDeploy, waitUntilDeploymentSuccessful } = require('@aws-sdk/client-codedeploy'); 4 | const { ECS, waitUntilServicesStable, waitUntilTasksStopped } = require('@aws-sdk/client-ecs'); 5 | const fs = require('fs'); 6 | const path = require('path'); 7 | 8 | jest.mock('@actions/core'); 9 | jest.mock('fs', () => ({ 10 | promises: { access: jest.fn() }, 11 | readFileSync: jest.fn(), 12 | constants: { 13 | O_CREATE: jest.fn() 14 | } 15 | })); 16 | 17 | const mockEcsRegisterTaskDef = jest.fn(); 18 | const mockEcsUpdateService = jest.fn(); 19 | const mockEcsDescribeServices = jest.fn(); 20 | const mockCodeDeployCreateDeployment = jest.fn(); 21 | const mockCodeDeployGetDeploymentGroup = jest.fn(); 22 | const mockRunTask = jest.fn(); 23 | const mockWaitUntilTasksStopped = jest.fn().mockRejectedValue(new Error('failed')); 24 | const mockEcsDescribeTasks = jest.fn(); 25 | const config = { 26 | region: () => Promise.resolve('fake-region'), 27 | }; 28 | 29 | jest.mock('@aws-sdk/client-codedeploy'); 30 | jest.mock('@aws-sdk/client-ecs'); 31 | 32 | const EXPECTED_DEFAULT_WAIT_TIME = 30; 33 | const EXPECTED_CODE_DEPLOY_DEPLOYMENT_READY_WAIT_TIME = 60; 34 | const EXPECTED_CODE_DEPLOY_TERMINATION_WAIT_TIME = 30; 35 | 36 | describe('Deploy to ECS', () => { 37 | 38 | const mockEcsClient = { 39 | config, 40 | registerTaskDefinition: mockEcsRegisterTaskDef, 41 | updateService: mockEcsUpdateService, 42 | describeServices: mockEcsDescribeServices, 43 | describeTasks: mockEcsDescribeTasks, 44 | runTask: mockRunTask, 45 | waitUntilTasksStopped: mockWaitUntilTasksStopped 46 | }; 47 | 48 | const mockCodeDeployClient = { 49 | config, 50 | createDeployment: mockCodeDeployCreateDeployment, 51 | getDeploymentGroup: mockCodeDeployGetDeploymentGroup 52 | }; 53 | 54 | beforeEach(() => { 55 | jest.clearAllMocks(); 56 | 57 | core.getInput = jest 58 | .fn() 59 | .mockReturnValueOnce('task-definition.json') // task-definition 60 | .mockReturnValueOnce('service-456') // service 61 | .mockReturnValueOnce('cluster-789'); // cluster 62 | 63 | process.env = Object.assign(process.env, { GITHUB_WORKSPACE: __dirname }); 64 | 65 | fs.readFileSync.mockImplementation((pathInput, encoding) => { 66 | if (encoding != 'utf8') { 67 | throw new Error(`Wrong encoding ${encoding}`); 68 | } 69 | 70 | if (pathInput == path.join(process.env.GITHUB_WORKSPACE, 'appspec.yaml')) { 71 | return ` 72 | Resources: 73 | - TargetService: 74 | Type: AWS::ECS::Service 75 | Properties: 76 | TaskDefinition: helloworld 77 | LoadBalancerInfo: 78 | ContainerName: web 79 | ContainerPort: 80`; 80 | } 81 | 82 | if (pathInput == path.join(process.env.GITHUB_WORKSPACE, 'task-definition.json')) { 83 | return JSON.stringify({ family: 'task-def-family' }); 84 | } 85 | 86 | throw new Error(`Unknown path ${pathInput}`); 87 | }); 88 | 89 | mockEcsRegisterTaskDef.mockImplementation( 90 | () => Promise.resolve({ taskDefinition: { taskDefinitionArn: 'task:def:arn' } }) 91 | ); 92 | 93 | mockEcsUpdateService.mockImplementation(() => Promise.resolve({})); 94 | 95 | mockEcsDescribeServices.mockImplementation( 96 | () => Promise.resolve({ 97 | failures: [], 98 | services: [{ 99 | status: 'ACTIVE' 100 | }] 101 | }) 102 | ); 103 | 104 | mockCodeDeployCreateDeployment.mockImplementation( 105 | () => Promise.resolve({ deploymentId: 'deployment-1' }) 106 | ); 107 | 108 | mockCodeDeployGetDeploymentGroup.mockImplementation( 109 | () => Promise.resolve({ 110 | deploymentGroupInfo: { 111 | blueGreenDeploymentConfiguration: { 112 | deploymentReadyOption: { 113 | waitTimeInMinutes: EXPECTED_CODE_DEPLOY_DEPLOYMENT_READY_WAIT_TIME 114 | }, 115 | terminateBlueInstancesOnDeploymentSuccess: { 116 | terminationWaitTimeInMinutes: EXPECTED_CODE_DEPLOY_TERMINATION_WAIT_TIME 117 | } 118 | } 119 | } 120 | }) 121 | ); 122 | 123 | mockRunTask.mockImplementation( 124 | () => Promise.resolve({ 125 | failures: [], 126 | tasks: [ 127 | { 128 | containers: [ 129 | { 130 | lastStatus: "RUNNING", 131 | exitCode: 0, 132 | reason: '', 133 | taskArn: "arn:aws:ecs:fake-region:account_id:task/arn" 134 | } 135 | ], 136 | desiredStatus: "RUNNING", 137 | lastStatus: "RUNNING", 138 | taskArn: "arn:aws:ecs:fake-region:account_id:task/arn" 139 | // taskDefinitionArn: "arn:aws:ecs:::task-definition/amazon-ecs-sample:1" 140 | } 141 | ] 142 | })); 143 | 144 | mockEcsDescribeTasks.mockImplementation( 145 | () => Promise.resolve({ 146 | failures: [], 147 | tasks: [ 148 | { 149 | containers: [ 150 | { 151 | lastStatus: "RUNNING", 152 | exitCode: 0, 153 | reason: '', 154 | taskArn: "arn:aws:ecs:fake-region:account_id:task/arn" 155 | } 156 | ], 157 | desiredStatus: "RUNNING", 158 | lastStatus: "RUNNING", 159 | taskArn: "arn:aws:ecs:fake-region:account_id:task/arn" 160 | } 161 | ] 162 | })); 163 | 164 | ECS.mockImplementation(() => mockEcsClient); 165 | 166 | waitUntilTasksStopped.mockImplementation(() => Promise.resolve({})); 167 | 168 | waitUntilServicesStable.mockImplementation(() => Promise.resolve({})); 169 | 170 | CodeDeploy.mockImplementation(() => mockCodeDeployClient); 171 | 172 | waitUntilDeploymentSuccessful.mockImplementation(() => Promise.resolve({})); 173 | }); 174 | 175 | test('registers the task definition contents and updates the service', async () => { 176 | await run(); 177 | expect(core.setFailed).toHaveBeenCalledTimes(0); 178 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 179 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 180 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 181 | cluster: 'cluster-789', 182 | services: ['service-456'] 183 | }); 184 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 185 | cluster: 'cluster-789', 186 | service: 'service-456', 187 | taskDefinition: 'task:def:arn', 188 | forceNewDeployment: false, 189 | enableECSManagedTags: null, 190 | propagateTags: null, 191 | volumeConfigurations: [] 192 | }); 193 | expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); 194 | expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://fake-region.console.aws.amazon.com/ecs/v2/clusters/cluster-789/services/service-456/events?region=fake-region"); 195 | }); 196 | 197 | test('registers the task definition contents and updates the service if deployment controller type is ECS', async () => { 198 | mockEcsDescribeServices.mockImplementation( 199 | () => Promise.resolve({ 200 | failures: [], 201 | services: [{ 202 | status: 'ACTIVE', 203 | deploymentController: { 204 | type: 'ECS' 205 | } 206 | }] 207 | }) 208 | ); 209 | 210 | await run(); 211 | expect(core.setFailed).toHaveBeenCalledTimes(0); 212 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 213 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 214 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 215 | cluster: 'cluster-789', 216 | services: ['service-456'] 217 | }); 218 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 219 | cluster: 'cluster-789', 220 | service: 'service-456', 221 | taskDefinition: 'task:def:arn', 222 | forceNewDeployment: false, 223 | enableECSManagedTags: null, 224 | propagateTags: null, 225 | volumeConfigurations: [] 226 | }); 227 | expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); 228 | expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://fake-region.console.aws.amazon.com/ecs/v2/clusters/cluster-789/services/service-456/events?region=fake-region"); 229 | }); 230 | 231 | test('prints Chinese console domain for cn regions', async () => { 232 | const originalRegion = config.region; 233 | config.region = () => Promise.resolve('cn-fake-region'); 234 | await run(); 235 | 236 | expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the Amazon ECS console: https://cn-fake-region.console.amazonaws.cn/ecs/v2/clusters/cluster-789/services/service-456/events?region=cn-fake-region"); 237 | 238 | // reset 239 | config.region = originalRegion; 240 | }); 241 | 242 | test('cleans null keys out of the task definition contents', async () => { 243 | fs.readFileSync.mockImplementation((pathInput, encoding) => { 244 | if (encoding != 'utf8') { 245 | throw new Error(`Wrong encoding ${encoding}`); 246 | } 247 | 248 | return '{ "ipcMode": null, "family": "task-def-family" }'; 249 | }); 250 | 251 | await run(); 252 | expect(core.setFailed).toHaveBeenCalledTimes(0); 253 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 254 | }); 255 | 256 | test('cleans empty arrays out of the task definition contents', async () => { 257 | fs.readFileSync.mockImplementation((pathInput, encoding) => { 258 | if (encoding != 'utf8') { 259 | throw new Error(`Wrong encoding ${encoding}`); 260 | } 261 | 262 | return '{ "tags": [], "family": "task-def-family" }'; 263 | }); 264 | 265 | await run(); 266 | expect(core.setFailed).toHaveBeenCalledTimes(0); 267 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 268 | }); 269 | 270 | test('cleans empty strings and objects out of the task definition contents', async () => { 271 | fs.readFileSync.mockImplementation((pathInput, encoding) => { 272 | if (encoding != 'utf8') { 273 | throw new Error(`Wrong encoding ${encoding}`); 274 | } 275 | 276 | return ` 277 | { 278 | "memory": "", 279 | "containerDefinitions": [ { 280 | "name": "sample-container", 281 | "logConfiguration": {}, 282 | "repositoryCredentials": { "credentialsParameter": "" }, 283 | "command": [ 284 | "" 285 | ], 286 | "environment": [ 287 | { 288 | "name": "hello", 289 | "value": "world" 290 | }, 291 | { 292 | "name": "test", 293 | "value": "" 294 | }, 295 | { 296 | "name": "", 297 | "value": "" 298 | } 299 | ], 300 | "secretOptions": [ { 301 | "name": "", 302 | "valueFrom": "" 303 | } ], 304 | "cpu": 0, 305 | "essential": false 306 | } ], 307 | "requiresCompatibilities": [ "EC2" ], 308 | "registeredAt": 1611690781, 309 | "family": "task-def-family" 310 | } 311 | `; 312 | }); 313 | 314 | await run(); 315 | expect(core.setFailed).toHaveBeenCalledTimes(0); 316 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { 317 | family: 'task-def-family', 318 | containerDefinitions: [ 319 | { 320 | name: 'sample-container', 321 | cpu: 0, 322 | essential: false, 323 | environment: [{ 324 | name: 'hello', 325 | value: 'world' 326 | }, { 327 | "name": "test", 328 | "value": "" 329 | }] 330 | } 331 | ], 332 | requiresCompatibilities: ['EC2'] 333 | }); 334 | }); 335 | 336 | test('maintains empty keys in proxyConfiguration.properties for APPMESH', async () => { 337 | fs.readFileSync.mockImplementation((pathInput, encoding) => { 338 | if (encoding != 'utf8') { 339 | throw new Error(`Wrong encoding ${encoding}`); 340 | } 341 | 342 | return ` 343 | { 344 | "memory": "", 345 | "containerDefinitions": [ { 346 | "name": "sample-container", 347 | "logConfiguration": {}, 348 | "repositoryCredentials": { "credentialsParameter": "" }, 349 | "command": [ 350 | "" 351 | ], 352 | "environment": [ 353 | { 354 | "name": "hello", 355 | "value": "world" 356 | }, 357 | { 358 | "name": "", 359 | "value": "" 360 | } 361 | ], 362 | "secretOptions": [ { 363 | "name": "", 364 | "valueFrom": "" 365 | } ], 366 | "cpu": 0, 367 | "essential": false 368 | } ], 369 | "requiresCompatibilities": [ "EC2" ], 370 | "registeredAt": 1611690781, 371 | "family": "task-def-family", 372 | "proxyConfiguration": { 373 | "type": "APPMESH", 374 | "containerName": "envoy", 375 | "properties": [ 376 | { 377 | "name": "ProxyIngressPort", 378 | "value": "15000" 379 | }, 380 | { 381 | "name": "AppPorts", 382 | "value": "1234" 383 | }, 384 | { 385 | "name": "EgressIgnoredIPs", 386 | "value": "169.254.170.2,169.254.169.254" 387 | }, 388 | { 389 | "name": "IgnoredGID", 390 | "value": "" 391 | }, 392 | { 393 | "name": "EgressIgnoredPorts", 394 | "value": "" 395 | }, 396 | { 397 | "name": "IgnoredUID", 398 | "value": "1337" 399 | }, 400 | { 401 | "name": "ProxyEgressPort", 402 | "value": "15001" 403 | }, 404 | { 405 | "value": "some-value" 406 | } 407 | ] 408 | } 409 | } 410 | `; 411 | }); 412 | 413 | await run(); 414 | expect(core.setFailed).toHaveBeenCalledTimes(0); 415 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { 416 | family: 'task-def-family', 417 | containerDefinitions: [ 418 | { 419 | name: 'sample-container', 420 | cpu: 0, 421 | essential: false, 422 | environment: [{ 423 | name: 'hello', 424 | value: 'world' 425 | }] 426 | } 427 | ], 428 | requiresCompatibilities: ['EC2'], 429 | proxyConfiguration: { 430 | type: "APPMESH", 431 | containerName: "envoy", 432 | properties: [ 433 | { 434 | name: "ProxyIngressPort", 435 | value: "15000" 436 | }, 437 | { 438 | name: "AppPorts", 439 | value: "1234" 440 | }, 441 | { 442 | name: "EgressIgnoredIPs", 443 | value: "169.254.170.2,169.254.169.254" 444 | }, 445 | { 446 | name: "IgnoredGID", 447 | value: "" 448 | }, 449 | { 450 | name: "EgressIgnoredPorts", 451 | value: "" 452 | }, 453 | { 454 | name: "IgnoredUID", 455 | value: "1337" 456 | }, 457 | { 458 | name: "ProxyEgressPort", 459 | value: "15001" 460 | }, 461 | { 462 | name: "", 463 | value: "some-value" 464 | } 465 | ] 466 | } 467 | }); 468 | }); 469 | 470 | test('cleans invalid keys out of the task definition contents', async () => { 471 | fs.readFileSync.mockImplementation((pathInput, encoding) => { 472 | if (encoding != 'utf8') { 473 | throw new Error(`Wrong encoding ${encoding}`); 474 | } 475 | 476 | return '{ "compatibilities": ["EC2"], "taskDefinitionArn": "arn:aws...:task-def-family:1", "family": "task-def-family", "revision": 1, "status": "ACTIVE" }'; 477 | }); 478 | 479 | await run(); 480 | expect(core.setFailed).toHaveBeenCalledTimes(0); 481 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 482 | }); 483 | 484 | test('registers the task definition contents and creates a CodeDeploy deployment, waits for 30 minutes + deployment group wait time', async () => { 485 | core.getInput = jest 486 | .fn() 487 | .mockReturnValueOnce('task-definition.json') // task-definition 488 | .mockReturnValueOnce('service-456') // service 489 | .mockReturnValueOnce('cluster-789') // cluster 490 | .mockReturnValueOnce('TRUE'); // wait-for-service-stability 491 | 492 | mockEcsDescribeServices.mockImplementation( 493 | () => Promise.resolve({ 494 | failures: [], 495 | services: [{ 496 | status: 'ACTIVE', 497 | deploymentController: { 498 | type: 'CODE_DEPLOY' 499 | } 500 | }] 501 | }) 502 | ); 503 | 504 | await run(); 505 | expect(core.setFailed).toHaveBeenCalledTimes(0); 506 | 507 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 508 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 509 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 510 | cluster: 'cluster-789', 511 | services: ['service-456'] 512 | }); 513 | 514 | expect(mockCodeDeployCreateDeployment).toHaveBeenNthCalledWith(1, { 515 | applicationName: 'AppECS-cluster-789-service-456', 516 | deploymentGroupName: 'DgpECS-cluster-789-service-456', 517 | revision: { 518 | revisionType: 'AppSpecContent', 519 | appSpecContent: { 520 | content: JSON.stringify({ 521 | Resources: [{ 522 | TargetService: { 523 | Type: 'AWS::ECS::Service', 524 | Properties: { 525 | TaskDefinition: 'task:def:arn', 526 | LoadBalancerInfo: { 527 | ContainerName: "web", 528 | ContainerPort: 80 529 | } 530 | } 531 | } 532 | }] 533 | }), 534 | sha256: '0911d1e99f48b492e238d1284d8ddb805382d33e1d1fc74ffadf37d8b7e6d096' 535 | } 536 | } 537 | }); 538 | 539 | expect(waitUntilDeploymentSuccessful).toHaveBeenNthCalledWith( 540 | 1, 541 | { 542 | client: mockCodeDeployClient, 543 | minDelay: 15, 544 | maxWaitTime: ( 545 | EXPECTED_DEFAULT_WAIT_TIME + 546 | EXPECTED_CODE_DEPLOY_TERMINATION_WAIT_TIME + 547 | EXPECTED_CODE_DEPLOY_DEPLOYMENT_READY_WAIT_TIME 548 | ) * 60, 549 | }, 550 | { 551 | deploymentId: 'deployment-1', 552 | } 553 | ); 554 | 555 | expect(mockEcsUpdateService).toHaveBeenCalledTimes(0); 556 | expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); 557 | 558 | expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the AWS CodeDeploy console: https://console.aws.amazon.com/codesuite/codedeploy/deployments/deployment-1?region=fake-region"); 559 | }); 560 | 561 | test('registers the task definition contents and creates a CodeDeploy deployment, waits for 1 hour + deployment group\'s wait time', async () => { 562 | core.getInput = jest 563 | .fn() 564 | .mockReturnValueOnce('task-definition.json') // task-definition 565 | .mockReturnValueOnce('service-456') // service 566 | .mockReturnValueOnce('cluster-789') // cluster 567 | .mockReturnValueOnce('TRUE') // wait-for-service-stability 568 | .mockReturnValueOnce('60'); // wait-for-minutes 569 | 570 | mockEcsDescribeServices.mockImplementation( 571 | () => Promise.resolve({ 572 | failures: [], 573 | services: [{ 574 | status: 'ACTIVE', 575 | deploymentController: { 576 | type: 'CODE_DEPLOY' 577 | } 578 | }] 579 | }) 580 | ); 581 | 582 | await run(); 583 | expect(core.setFailed).toHaveBeenCalledTimes(0); 584 | 585 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 586 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 587 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 588 | cluster: 'cluster-789', 589 | services: ['service-456'] 590 | }); 591 | 592 | expect(mockCodeDeployCreateDeployment).toHaveBeenNthCalledWith(1, { 593 | applicationName: 'AppECS-cluster-789-service-456', 594 | deploymentGroupName: 'DgpECS-cluster-789-service-456', 595 | revision: { 596 | revisionType: 'AppSpecContent', 597 | appSpecContent: { 598 | content: JSON.stringify({ 599 | Resources: [{ 600 | TargetService: { 601 | Type: 'AWS::ECS::Service', 602 | Properties: { 603 | TaskDefinition: 'task:def:arn', 604 | LoadBalancerInfo: { 605 | ContainerName: "web", 606 | ContainerPort: 80 607 | } 608 | } 609 | } 610 | }] 611 | }), 612 | sha256: '0911d1e99f48b492e238d1284d8ddb805382d33e1d1fc74ffadf37d8b7e6d096' 613 | } 614 | } 615 | }); 616 | 617 | expect(waitUntilDeploymentSuccessful).toHaveBeenNthCalledWith( 618 | 1, 619 | { 620 | client: mockCodeDeployClient, 621 | minDelay: 15, 622 | maxWaitTime: ( 623 | 60 + 624 | EXPECTED_CODE_DEPLOY_TERMINATION_WAIT_TIME + 625 | EXPECTED_CODE_DEPLOY_DEPLOYMENT_READY_WAIT_TIME 626 | ) * 60, 627 | }, 628 | { 629 | deploymentId: 'deployment-1', 630 | } 631 | ); 632 | 633 | expect(mockEcsUpdateService).toHaveBeenCalledTimes(0); 634 | expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); 635 | }); 636 | 637 | test('registers the task definition contents and creates a CodeDeploy deployment, waits for max 6 hours', async () => { 638 | core.getInput = jest 639 | .fn() 640 | .mockReturnValueOnce('task-definition.json') // task-definition 641 | .mockReturnValueOnce('service-456') // service 642 | .mockReturnValueOnce('cluster-789') // cluster 643 | .mockReturnValueOnce('TRUE') // wait-for-service-stability 644 | .mockReturnValueOnce('1000'); // wait-for-minutes 645 | 646 | mockEcsDescribeServices.mockImplementation( 647 | () => Promise.resolve({ 648 | failures: [], 649 | services: [{ 650 | status: 'ACTIVE', 651 | deploymentController: { 652 | type: 'CODE_DEPLOY' 653 | } 654 | }] 655 | }) 656 | ); 657 | 658 | await run(); 659 | expect(core.setFailed).toHaveBeenCalledTimes(0); 660 | 661 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 662 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 663 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 664 | cluster: 'cluster-789', 665 | services: ['service-456'] 666 | }); 667 | 668 | expect(mockCodeDeployCreateDeployment).toHaveBeenNthCalledWith(1, { 669 | applicationName: 'AppECS-cluster-789-service-456', 670 | deploymentGroupName: 'DgpECS-cluster-789-service-456', 671 | revision: { 672 | revisionType: 'AppSpecContent', 673 | appSpecContent: { 674 | content: JSON.stringify({ 675 | Resources: [{ 676 | TargetService: { 677 | Type: 'AWS::ECS::Service', 678 | Properties: { 679 | TaskDefinition: 'task:def:arn', 680 | LoadBalancerInfo: { 681 | ContainerName: "web", 682 | ContainerPort: 80 683 | } 684 | } 685 | } 686 | }] 687 | }), 688 | sha256: '0911d1e99f48b492e238d1284d8ddb805382d33e1d1fc74ffadf37d8b7e6d096' 689 | } 690 | } 691 | }); 692 | 693 | expect(waitUntilDeploymentSuccessful).toHaveBeenNthCalledWith( 694 | 1, 695 | { 696 | client: mockCodeDeployClient, 697 | minDelay: 15, 698 | maxWaitTime: 6 * 60 * 60, 699 | }, 700 | { 701 | deploymentId: 'deployment-1', 702 | } 703 | ); 704 | 705 | expect(mockEcsUpdateService).toHaveBeenCalledTimes(0); 706 | expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); 707 | }); 708 | 709 | test('does not wait for a CodeDeploy deployment, parses JSON appspec file', async () => { 710 | core.getInput = jest 711 | .fn() 712 | .mockReturnValueOnce('task-definition.json') // task-definition 713 | .mockReturnValueOnce('service-456') // service 714 | .mockReturnValueOnce('cluster-789') // cluster 715 | .mockReturnValueOnce('false') // wait-for-service-stability 716 | .mockReturnValueOnce('') // wait-for-minutes 717 | .mockReturnValueOnce('') // force-new-deployment 718 | .mockReturnValueOnce('') // run-task 719 | .mockReturnValueOnce('') // desired count 720 | .mockReturnValueOnce('') // enable-ecs-managed-tags 721 | .mockReturnValueOnce('') // propagate-task 722 | .mockReturnValueOnce('/hello/appspec.json') // codedeploy-appspec 723 | .mockReturnValueOnce('MyApplication') // codedeploy-application 724 | .mockReturnValueOnce('MyDeploymentGroup'); // codedeploy-deployment-group 725 | 726 | fs.readFileSync.mockReturnValue(` 727 | { 728 | "Resources": [ 729 | { 730 | "TargetService": { 731 | "Type": "AWS::ECS::Service", 732 | "Properties": { 733 | "TaskDefinition": "helloworld", 734 | "LoadBalancerInfo": { 735 | "ContainerName": "web", 736 | "ContainerPort": 80 737 | } 738 | } 739 | } 740 | } 741 | ] 742 | } 743 | `); 744 | 745 | fs.readFileSync.mockImplementation((pathInput, encoding) => { 746 | if (encoding != 'utf8') { 747 | throw new Error(`Wrong encoding ${encoding}`); 748 | } 749 | 750 | if (pathInput == path.join('/hello/appspec.json')) { 751 | return ` 752 | { 753 | "Resources": [ 754 | { 755 | "TargetService": { 756 | "Type": "AWS::ECS::Service", 757 | "Properties": { 758 | "TaskDefinition": "helloworld", 759 | "LoadBalancerInfo": { 760 | "ContainerName": "web", 761 | "ContainerPort": 80 762 | } 763 | } 764 | } 765 | } 766 | ] 767 | }`; 768 | } 769 | 770 | if (pathInput == path.join(process.env.GITHUB_WORKSPACE, 'task-definition.json')) { 771 | return JSON.stringify({ family: 'task-def-family' }); 772 | } 773 | 774 | throw new Error(`Unknown path ${pathInput}`); 775 | }); 776 | 777 | mockEcsDescribeServices.mockImplementation( 778 | () => Promise.resolve({ 779 | failures: [], 780 | services: [{ 781 | status: 'ACTIVE', 782 | deploymentController: { 783 | type: 'CODE_DEPLOY' 784 | } 785 | }] 786 | }) 787 | ); 788 | 789 | await run(); 790 | expect(core.setFailed).toHaveBeenCalledTimes(0); 791 | 792 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 793 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 794 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 795 | cluster: 'cluster-789', 796 | services: ['service-456'] 797 | }); 798 | 799 | expect(mockCodeDeployCreateDeployment).toHaveBeenNthCalledWith(1, { 800 | applicationName: 'MyApplication', 801 | deploymentGroupName: 'MyDeploymentGroup', 802 | revision: { 803 | revisionType: 'AppSpecContent', 804 | appSpecContent: { 805 | content: JSON.stringify({ 806 | Resources: [{ 807 | TargetService: { 808 | Type: 'AWS::ECS::Service', 809 | Properties: { 810 | TaskDefinition: 'task:def:arn', 811 | LoadBalancerInfo: { 812 | ContainerName: "web", 813 | ContainerPort: 80 814 | } 815 | } 816 | } 817 | }] 818 | }), 819 | sha256: '0911d1e99f48b492e238d1284d8ddb805382d33e1d1fc74ffadf37d8b7e6d096' 820 | } 821 | } 822 | }); 823 | 824 | expect(waitUntilDeploymentSuccessful).toHaveBeenCalledTimes(0); 825 | expect(mockEcsUpdateService).toHaveBeenCalledTimes(0); 826 | expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); 827 | }); 828 | 829 | test('registers the task definition contents and creates a CodeDeploy deployment with custom application, deployment group, description and config', async () => { 830 | core.getInput = jest 831 | .fn(input => { 832 | return { 833 | 'task-definition': 'task-definition.json', 834 | 'service': 'service-456', 835 | 'cluster': 'cluster-789', 836 | 'wait-for-service-stability': 'TRUE', 837 | 'codedeploy-application': 'Custom-Application', 838 | 'codedeploy-deployment-group': 'Custom-Deployment-Group', 839 | 'codedeploy-deployment-description': 'Custom-Deployment', 840 | 'codedeploy-deployment-config': 'CodeDeployDefault.AllAtOnce' 841 | }[input]; 842 | }); 843 | 844 | mockEcsDescribeServices.mockImplementation( 845 | () => Promise.resolve({ 846 | failures: [], 847 | services: [{ 848 | status: 'ACTIVE', 849 | deploymentController: { 850 | type: 'CODE_DEPLOY' 851 | } 852 | }] 853 | }) 854 | ); 855 | 856 | await run(); 857 | expect(core.setFailed).toHaveBeenCalledTimes(0); 858 | 859 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 860 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 861 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 862 | cluster: 'cluster-789', 863 | services: ['service-456'] 864 | }); 865 | 866 | expect(mockCodeDeployCreateDeployment).toHaveBeenNthCalledWith(1, { 867 | applicationName: 'Custom-Application', 868 | deploymentGroupName: 'Custom-Deployment-Group', 869 | deploymentConfigName: 'CodeDeployDefault.AllAtOnce', 870 | description: 'Custom-Deployment', 871 | revision: { 872 | revisionType: 'AppSpecContent', 873 | appSpecContent: { 874 | content: JSON.stringify({ 875 | Resources: [{ 876 | TargetService: { 877 | Type: 'AWS::ECS::Service', 878 | Properties: { 879 | TaskDefinition: 'task:def:arn', 880 | LoadBalancerInfo: { 881 | ContainerName: "web", 882 | ContainerPort: 80 883 | } 884 | } 885 | } 886 | }] 887 | }), 888 | sha256: '0911d1e99f48b492e238d1284d8ddb805382d33e1d1fc74ffadf37d8b7e6d096' 889 | } 890 | } 891 | }); 892 | 893 | expect(waitUntilDeploymentSuccessful).toHaveBeenNthCalledWith( 894 | 1, 895 | { 896 | client: mockCodeDeployClient, 897 | minDelay: 15, 898 | maxWaitTime: ( 899 | EXPECTED_DEFAULT_WAIT_TIME + 900 | EXPECTED_CODE_DEPLOY_TERMINATION_WAIT_TIME + 901 | EXPECTED_CODE_DEPLOY_DEPLOYMENT_READY_WAIT_TIME 902 | ) * 60, 903 | }, 904 | { 905 | deploymentId: 'deployment-1', 906 | } 907 | ); 908 | 909 | expect(mockEcsUpdateService).toHaveBeenCalledTimes(0); 910 | expect(waitUntilServicesStable).toHaveBeenCalledTimes(0); 911 | 912 | expect(core.info).toBeCalledWith("Deployment started. Watch this deployment's progress in the AWS CodeDeploy console: https://console.aws.amazon.com/codesuite/codedeploy/deployments/deployment-1?region=fake-region"); 913 | }); 914 | 915 | test('registers the task definition contents at an absolute path', async () => { 916 | core.getInput = jest.fn().mockReturnValueOnce('/hello/task-definition.json'); 917 | fs.readFileSync.mockImplementation((pathInput, encoding) => { 918 | if (encoding != 'utf8') { 919 | throw new Error(`Wrong encoding ${encoding}`); 920 | } 921 | 922 | if (pathInput == '/hello/task-definition.json') { 923 | return JSON.stringify({ family: 'task-def-family-absolute-path' }); 924 | } 925 | 926 | throw new Error(`Unknown path ${pathInput}`); 927 | }); 928 | 929 | await run(); 930 | expect(core.setFailed).toHaveBeenCalledTimes(0); 931 | 932 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family-absolute-path' }); 933 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 934 | }); 935 | 936 | test('waits for the service to be stable', async () => { 937 | core.getInput = jest 938 | .fn() 939 | .mockReturnValueOnce('task-definition.json') // task-definition 940 | .mockReturnValueOnce('service-456') // service 941 | .mockReturnValueOnce('cluster-789') // cluster 942 | .mockReturnValueOnce('TRUE') // wait-for-service-stability 943 | .mockReturnValueOnce(''); // desired count 944 | 945 | await run(); 946 | expect(core.setFailed).toHaveBeenCalledTimes(0); 947 | 948 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 949 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 950 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 951 | cluster: 'cluster-789', 952 | services: ['service-456'] 953 | }); 954 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 955 | cluster: 'cluster-789', 956 | service: 'service-456', 957 | taskDefinition: 'task:def:arn', 958 | forceNewDeployment: false, 959 | enableECSManagedTags: null, 960 | propagateTags: null, 961 | volumeConfigurations: [] 962 | }); 963 | expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 964 | 1, 965 | { 966 | client: mockEcsClient, 967 | minDelay: 15, 968 | maxWaitTime: EXPECTED_DEFAULT_WAIT_TIME * 60, 969 | }, 970 | { 971 | services: ['service-456'], 972 | cluster: 'cluster-789', 973 | } 974 | ); 975 | }); 976 | 977 | test('waits for the service to be stable for specified minutes', async () => { 978 | core.getInput = jest 979 | .fn() 980 | .mockReturnValueOnce('task-definition.json') // task-definition 981 | .mockReturnValueOnce('service-456') // service 982 | .mockReturnValueOnce('cluster-789') // cluster 983 | .mockReturnValueOnce('TRUE') // wait-for-service-stability 984 | .mockReturnValueOnce('60') // wait-for-minutes 985 | .mockReturnValueOnce(''); // desired count 986 | 987 | await run(); 988 | expect(core.setFailed).toHaveBeenCalledTimes(0); 989 | 990 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 991 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 992 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 993 | cluster: 'cluster-789', 994 | services: ['service-456'] 995 | }); 996 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 997 | cluster: 'cluster-789', 998 | service: 'service-456', 999 | taskDefinition: 'task:def:arn', 1000 | forceNewDeployment: false, 1001 | enableECSManagedTags: null, 1002 | propagateTags: null, 1003 | volumeConfigurations: [] 1004 | }); 1005 | expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 1006 | 1, 1007 | { 1008 | client: mockEcsClient, 1009 | minDelay: 15, 1010 | maxWaitTime: 60 * 60, 1011 | }, 1012 | { 1013 | services: ['service-456'], 1014 | cluster: 'cluster-789', 1015 | } 1016 | ); 1017 | }); 1018 | 1019 | test('waits for the service to be stable for max 6 hours', async () => { 1020 | core.getInput = jest 1021 | .fn() 1022 | .mockReturnValueOnce('task-definition.json') // task-definition 1023 | .mockReturnValueOnce('service-456') // service 1024 | .mockReturnValueOnce('cluster-789') // cluster 1025 | .mockReturnValueOnce('TRUE') // wait-for-service-stability 1026 | .mockReturnValueOnce('1000') // wait-for-minutes 1027 | .mockReturnValueOnce('abc'); // desired count is NaN 1028 | 1029 | await run(); 1030 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1031 | 1032 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1033 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1034 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 1035 | cluster: 'cluster-789', 1036 | services: ['service-456'] 1037 | }); 1038 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 1039 | cluster: 'cluster-789', 1040 | service: 'service-456', 1041 | taskDefinition: 'task:def:arn', 1042 | forceNewDeployment: false, 1043 | enableECSManagedTags: null, 1044 | propagateTags: null, 1045 | volumeConfigurations: [] 1046 | }); 1047 | expect(waitUntilServicesStable).toHaveBeenNthCalledWith( 1048 | 1, 1049 | { 1050 | client: mockEcsClient, 1051 | minDelay: 15, 1052 | maxWaitTime: 6 * 60 * 60, 1053 | }, 1054 | { 1055 | services: ['service-456'], 1056 | cluster: 'cluster-789', 1057 | } 1058 | ); 1059 | }); 1060 | 1061 | test('force new deployment', async () => { 1062 | core.getInput = jest 1063 | .fn() 1064 | .mockReturnValueOnce('task-definition.json') // task-definition 1065 | .mockReturnValueOnce('service-456') // service 1066 | .mockReturnValueOnce('cluster-789') // cluster 1067 | .mockReturnValueOnce('false') // wait-for-service-stability 1068 | .mockReturnValueOnce('') // wait-for-minutes 1069 | .mockReturnValueOnce('true') // force-new-deployment 1070 | .mockReturnValueOnce('4'); // desired count is number 1071 | 1072 | await run(); 1073 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1074 | 1075 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1076 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1077 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 1078 | cluster: 'cluster-789', 1079 | services: ['service-456'] 1080 | }); 1081 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 1082 | cluster: 'cluster-789', 1083 | desiredCount: 4, 1084 | service: 'service-456', 1085 | taskDefinition: 'task:def:arn', 1086 | forceNewDeployment: true, 1087 | enableECSManagedTags: null, 1088 | propagateTags: null, 1089 | volumeConfigurations: [] 1090 | }); 1091 | }); 1092 | 1093 | test('defaults to the default cluster', async () => { 1094 | core.getInput = jest 1095 | .fn() 1096 | .mockReturnValueOnce('task-definition.json') // task-definition 1097 | .mockReturnValueOnce('service-456') // service 1098 | .mockReturnValueOnce(''); // desired count 1099 | 1100 | await run(); 1101 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1102 | 1103 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1104 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1105 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 1106 | cluster: 'default', 1107 | services: ['service-456'] 1108 | }); 1109 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 1110 | cluster: 'default', 1111 | service: 'service-456', 1112 | taskDefinition: 'task:def:arn', 1113 | forceNewDeployment: false, 1114 | enableECSManagedTags: null, 1115 | propagateTags: null, 1116 | volumeConfigurations: [] 1117 | }); 1118 | }); 1119 | 1120 | test('does not update service if none specified', async () => { 1121 | core.getInput = jest 1122 | .fn() 1123 | .mockReturnValueOnce('task-definition.json'); // task-definition 1124 | 1125 | await run(); 1126 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1127 | 1128 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1129 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1130 | expect(mockEcsDescribeServices).toHaveBeenCalledTimes(0); 1131 | expect(mockEcsUpdateService).toHaveBeenCalledTimes(0); 1132 | }); 1133 | 1134 | test('run task', async () => { 1135 | core.getInput = jest 1136 | .fn() 1137 | .mockReturnValueOnce('task-definition.json') // task-definition 1138 | .mockReturnValueOnce('') // service 1139 | .mockReturnValueOnce('') // cluster 1140 | .mockReturnValueOnce('') // wait-for-service-stability 1141 | .mockReturnValueOnce('') // wait-for-minutes 1142 | .mockReturnValueOnce('') // enable-ecs-managed-tags 1143 | .mockReturnValueOnce('') // propagate-tags 1144 | .mockReturnValueOnce('') // force-new-deployment 1145 | .mockReturnValueOnce('') // desired-count 1146 | .mockReturnValueOnce('true'); // run-task 1147 | 1148 | await run(); 1149 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1150 | 1151 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1152 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1153 | expect(mockRunTask).toHaveBeenCalledTimes(1); 1154 | expect(mockRunTask).toHaveBeenNthCalledWith(1,{ 1155 | startedBy: 'GitHub-Actions', 1156 | cluster: 'default', 1157 | capacityProviderStrategy: null, 1158 | launchType: 'FARGATE', 1159 | taskDefinition: 'task:def:arn', 1160 | overrides: {"containerOverrides": []}, 1161 | networkConfiguration: null, 1162 | enableECSManagedTags: null, 1163 | tags: [], 1164 | volumeConfigurations: [] 1165 | }); 1166 | 1167 | expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); 1168 | }); 1169 | 1170 | test('run task with options', async () => { 1171 | core.getInput = jest 1172 | .fn() 1173 | .mockReturnValueOnce('task-definition.json') // task-definition 1174 | .mockReturnValueOnce('') // service 1175 | .mockReturnValueOnce('somecluster') // cluster 1176 | .mockReturnValueOnce('') // wait-for-service-stability 1177 | .mockReturnValueOnce('') // wait-for-minutes 1178 | .mockReturnValueOnce('') // force-new-deployment 1179 | .mockReturnValueOnce('') // desired-count 1180 | .mockReturnValueOnce('false') // enable-ecs-managed-tags 1181 | .mockReturnValueOnce('') // propagate-tags 1182 | .mockReturnValueOnce('true') // run-task 1183 | .mockReturnValueOnce('false') // wait-for-task-stopped 1184 | .mockReturnValueOnce('someJoe') // run-task-started-by 1185 | .mockReturnValueOnce('EC2') // run-task-launch-type 1186 | .mockReturnValueOnce('a,b') // run-task-subnet-ids 1187 | .mockReturnValueOnce('c,d') // run-task-security-group-ids 1188 | .mockReturnValueOnce(JSON.stringify([{ name: 'someapp', command: 'somecmd' }])) // run-task-container-overrides 1189 | .mockReturnValueOnce('') // run-task-assign-public-IP 1190 | .mockReturnValueOnce('[{"key": "project", "value": "myproject"}]'); // run-task-tags 1191 | 1192 | await run(); 1193 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1194 | 1195 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1196 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1197 | expect(mockRunTask).toHaveBeenCalledWith({ 1198 | startedBy: 'someJoe', 1199 | cluster: 'somecluster', 1200 | capacityProviderStrategy: null, 1201 | launchType: 'EC2', 1202 | taskDefinition: 'task:def:arn', 1203 | overrides: { containerOverrides: [{ name: 'someapp', command: 'somecmd' }] }, 1204 | networkConfiguration: { awsvpcConfiguration: { subnets: ['a', 'b'], securityGroups: ['c', 'd'], assignPublicIp: "DISABLED" } }, 1205 | enableECSManagedTags: false, 1206 | tags: [{"key": "project", "value": "myproject"}], 1207 | volumeConfigurations: [] 1208 | }); 1209 | expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); 1210 | }); 1211 | 1212 | test('run task with capacity provider strategy', async () => { 1213 | core.getInput = jest 1214 | .fn() 1215 | .mockReturnValueOnce('task-definition.json') // task-definition 1216 | .mockReturnValueOnce('') // service 1217 | .mockReturnValueOnce('somecluster') // cluster 1218 | .mockReturnValueOnce('') // wait-for-service-stability 1219 | .mockReturnValueOnce('') // wait-for-minutes 1220 | .mockReturnValueOnce('') // force-new-deployment 1221 | .mockReturnValueOnce('') // desired-count 1222 | .mockReturnValueOnce('false') // enable-ecs-managed-tags 1223 | .mockReturnValueOnce('') // propagate-tags 1224 | .mockReturnValueOnce('true') // run-task 1225 | .mockReturnValueOnce('false') // wait-for-task-stopped 1226 | .mockReturnValueOnce('someJoe') // run-task-started-by 1227 | .mockReturnValueOnce('') // run-task-launch-type 1228 | .mockReturnValueOnce('a,b') // run-task-subnet-ids 1229 | .mockReturnValueOnce('c,d') // run-task-security-group-ids 1230 | .mockReturnValueOnce(JSON.stringify([{ name: 'someapp', command: 'somecmd' }])) // run-task-container-overrides 1231 | .mockReturnValueOnce('') // run-task-assign-public-IP 1232 | .mockReturnValueOnce('[{"key": "project", "value": "myproject"}]') // run-task-tags 1233 | .mockReturnValueOnce('[{"capacityProvider":"FARGATE_SPOT","weight":1}]'); // run-task-capacity-provider-strategy 1234 | 1235 | await run(); 1236 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1237 | 1238 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1239 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1240 | expect(mockRunTask).toHaveBeenCalledWith({ 1241 | startedBy: 'someJoe', 1242 | cluster: 'somecluster', 1243 | capacityProviderStrategy: [{"capacityProvider":"FARGATE_SPOT","weight":1}], 1244 | launchType: null, 1245 | taskDefinition: 'task:def:arn', 1246 | overrides: { containerOverrides: [{ name: 'someapp', command: 'somecmd' }] }, 1247 | networkConfiguration: { awsvpcConfiguration: { subnets: ['a', 'b'], securityGroups: ['c', 'd'], assignPublicIp: "DISABLED" } }, 1248 | enableECSManagedTags: false, 1249 | tags: [{"key": "project", "value": "myproject"}], 1250 | volumeConfigurations: [] 1251 | }); 1252 | expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); 1253 | }); 1254 | 1255 | test('run task and service ', async () => { 1256 | core.getInput = jest 1257 | .fn() 1258 | .mockReturnValueOnce('task-definition.json') // task-definition 1259 | .mockReturnValueOnce('service-456') // service 1260 | .mockReturnValueOnce('somecluster') // cluster 1261 | .mockReturnValueOnce('true') // wait-for-service-stability 1262 | .mockReturnValueOnce('') // wait-for-minutes 1263 | .mockReturnValueOnce('') // force-new-deployment 1264 | .mockReturnValueOnce('') // desired-count 1265 | .mockReturnValueOnce('') // enable-ecs-managed-tags 1266 | .mockReturnValueOnce('') // propagate-tags 1267 | .mockReturnValueOnce('true') // run-task 1268 | .mockReturnValueOnce('false') // wait-for-task-stopped 1269 | .mockReturnValueOnce('someJoe') // run-task-started-by 1270 | .mockReturnValueOnce('EC2') // run-task-launch-type 1271 | .mockReturnValueOnce('a,b') // run-task-subnet-ids 1272 | .mockReturnValueOnce('c,d') // run-task-security-group-ids 1273 | .mockReturnValueOnce(JSON.stringify([{ name: 'someapp', command: 'somecmd' }])); // run-task-container-overrides 1274 | 1275 | await run(); 1276 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1277 | 1278 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1279 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1280 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 1281 | cluster: 'somecluster', 1282 | services: ['service-456'] 1283 | }); 1284 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 1285 | cluster: 'somecluster', 1286 | service: 'service-456', 1287 | taskDefinition: 'task:def:arn', 1288 | forceNewDeployment: false, 1289 | enableECSManagedTags: null, 1290 | propagateTags: null, 1291 | volumeConfigurations: [] 1292 | }); 1293 | expect(mockRunTask).toHaveBeenCalledWith({ 1294 | startedBy: 'someJoe', 1295 | cluster: 'somecluster', 1296 | taskDefinition: 'task:def:arn', 1297 | capacityProviderStrategy: null, 1298 | launchType: 'EC2', 1299 | overrides: { containerOverrides: [{ name: 'someapp', command: 'somecmd' }] }, 1300 | networkConfiguration: { awsvpcConfiguration: { subnets: ['a', 'b'], securityGroups: ['c', 'd'], assignPublicIp: "DISABLED" } }, 1301 | enableECSManagedTags: null, 1302 | tags: [], 1303 | volumeConfigurations: [] 1304 | }); 1305 | expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); 1306 | }); 1307 | 1308 | test('run task and wait for it to stop', async () => { 1309 | core.getInput = jest 1310 | .fn() 1311 | .mockReturnValueOnce('task-definition.json') // task-definition 1312 | .mockReturnValueOnce('') // service 1313 | .mockReturnValueOnce('somecluster') // cluster 1314 | .mockReturnValueOnce('') // wait-for-service-stability 1315 | .mockReturnValueOnce('') // wait-for-minutes 1316 | .mockReturnValueOnce('') // force-new-deployment 1317 | .mockReturnValueOnce('') // desired-count 1318 | .mockReturnValueOnce('') // enable-ecs-managed-tags 1319 | .mockReturnValueOnce('') // propagate-tags 1320 | .mockReturnValueOnce('true') // run-task 1321 | .mockReturnValueOnce('true'); // wait-for-task-stopped 1322 | 1323 | await run(); 1324 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1325 | 1326 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1327 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1328 | expect(mockRunTask).toHaveBeenCalledTimes(1); 1329 | expect(core.setOutput).toHaveBeenNthCalledWith(2, 'run-task-arn', ["arn:aws:ecs:fake-region:account_id:task/arn"]); 1330 | expect(waitUntilTasksStopped).toHaveBeenCalledTimes(1); 1331 | }); 1332 | 1333 | test('run task in bridge network mode', async () => { 1334 | core.getInput = jest 1335 | .fn() 1336 | .mockReturnValueOnce('task-definition.json') // task-definition 1337 | .mockReturnValueOnce('service-456') // service 1338 | .mockReturnValueOnce('somecluster') // cluster 1339 | .mockReturnValueOnce('true') // wait-for-service-stability 1340 | .mockReturnValueOnce('') // wait-for-minutes 1341 | .mockReturnValueOnce('') // enable-ecs-managed-tags 1342 | .mockReturnValueOnce('') // force-new-deployment 1343 | .mockReturnValueOnce('') // desired-count 1344 | .mockReturnValueOnce('') // propagate-tags 1345 | .mockReturnValueOnce('true') // run-task 1346 | .mockReturnValueOnce('true') // wait-for-task-stopped 1347 | .mockReturnValueOnce('someJoe') // run-task-started-by 1348 | .mockReturnValueOnce('EC2') // run-task-launch-type 1349 | .mockReturnValueOnce('') // run-task-subnet-ids 1350 | .mockReturnValueOnce('') // run-task-security-group-ids 1351 | .mockReturnValueOnce('') // run-task-container-overrides 1352 | .mockReturnValueOnce('') // run-task-assign-public-IP 1353 | 1354 | await run(); 1355 | expect(mockRunTask).toHaveBeenCalledWith({ 1356 | startedBy: 'someJoe', 1357 | cluster: 'somecluster', 1358 | taskDefinition: 'task:def:arn', 1359 | capacityProviderStrategy: null, 1360 | launchType: 'EC2', 1361 | overrides: { containerOverrides: [] }, 1362 | networkConfiguration: null, 1363 | enableECSManagedTags: null, 1364 | tags: [], 1365 | volumeConfigurations: [] 1366 | }); 1367 | }); 1368 | 1369 | test('run task with setting true to enableECSManagedTags', async () => { 1370 | core.getInput = jest 1371 | .fn() 1372 | .mockReturnValueOnce('task-definition.json') // task-definition 1373 | .mockReturnValueOnce('') // service 1374 | .mockReturnValueOnce('somecluster') // cluster 1375 | .mockReturnValueOnce('') // wait-for-service-stability 1376 | .mockReturnValueOnce('') // wait-for-minutes 1377 | .mockReturnValueOnce('') // force-new-deployment 1378 | .mockReturnValueOnce('') // desired-count 1379 | .mockReturnValueOnce('true') // enable-ecs-managed-tags 1380 | .mockReturnValueOnce('') // propagate-tags 1381 | .mockReturnValueOnce('true'); // run-task 1382 | 1383 | await run(); 1384 | expect(mockRunTask).toHaveBeenCalledWith({ 1385 | startedBy: 'GitHub-Actions', 1386 | cluster: 'somecluster', 1387 | taskDefinition: 'task:def:arn', 1388 | capacityProviderStrategy: null, 1389 | launchType: 'FARGATE', 1390 | overrides: { containerOverrides: [] }, 1391 | networkConfiguration: null, 1392 | enableECSManagedTags: true, 1393 | tags: [], 1394 | volumeConfigurations: [] 1395 | }); 1396 | }); 1397 | 1398 | test('run task with setting false to enableECSManagedTags', async () => { 1399 | core.getInput = jest 1400 | .fn() 1401 | .mockReturnValueOnce('task-definition.json') // task-definition 1402 | .mockReturnValueOnce('') // service 1403 | .mockReturnValueOnce('somecluster') // cluster 1404 | .mockReturnValueOnce('') // wait-for-service-stability 1405 | .mockReturnValueOnce('') // wait-for-minutes 1406 | .mockReturnValueOnce('') // force-new-deployment 1407 | .mockReturnValueOnce('') // desired-count 1408 | .mockReturnValueOnce('false') // enable-ecs-managed-tags 1409 | .mockReturnValueOnce('') // propagate-tags 1410 | .mockReturnValueOnce('true'); // run-task 1411 | 1412 | await run(); 1413 | expect(mockRunTask).toHaveBeenCalledWith({ 1414 | startedBy: 'GitHub-Actions', 1415 | cluster: 'somecluster', 1416 | taskDefinition: 'task:def:arn', 1417 | capacityProviderStrategy: null, 1418 | launchType: 'FARGATE', 1419 | overrides: { containerOverrides: [] }, 1420 | networkConfiguration: null, 1421 | enableECSManagedTags: false, 1422 | tags: [], 1423 | volumeConfigurations: [] 1424 | }); 1425 | }); 1426 | 1427 | test('error is caught if run task fails with (wait-for-task-stopped: true)', async () => { 1428 | core.getInput = jest 1429 | .fn() 1430 | .mockReturnValueOnce('task-definition.json') // task-definition 1431 | .mockReturnValueOnce('') // service 1432 | .mockReturnValueOnce('somecluster') // cluster 1433 | .mockReturnValueOnce('') // wait-for-service-stability 1434 | .mockReturnValueOnce('') // wait-for-minutes 1435 | .mockReturnValueOnce('') // force-new-deployment 1436 | .mockReturnValueOnce('') // desired-count 1437 | .mockReturnValueOnce('') // enable-ecs-managed-tags 1438 | .mockReturnValueOnce('') // propagate-tags 1439 | .mockReturnValueOnce('true') // run-task 1440 | .mockReturnValueOnce('true'); // wait-for-task-stopped 1441 | 1442 | mockRunTask.mockImplementation( 1443 | () => Promise.resolve({ 1444 | failures: [{ 1445 | reason: 'TASK_FAILED', 1446 | arn: "arn:aws:ecs:fake-region:account_id:task/arn" 1447 | }], 1448 | tasks: [ 1449 | { 1450 | containers: [ 1451 | { 1452 | lastStatus: "RUNNING", 1453 | exitCode: 0, 1454 | reason: '', 1455 | taskArn: "arn:aws:ecs:fake-region:account_id:task/arn" 1456 | } 1457 | ], 1458 | desiredStatus: "RUNNING", 1459 | lastStatus: "STOPPED", 1460 | taskArn: "arn:aws:ecs:fake-region:account_id:task/arn" 1461 | } 1462 | ] 1463 | }) 1464 | ); 1465 | 1466 | await run(); 1467 | expect(core.setFailed).toBeCalledWith("arn:aws:ecs:fake-region:account_id:task/arn is TASK_FAILED"); 1468 | }); 1469 | 1470 | test('error is caught if run task fails with (wait-for-task-stopped: false) and with service', async () => { 1471 | core.getInput = jest 1472 | .fn() 1473 | .mockReturnValueOnce('task-definition.json') // task-definition 1474 | .mockReturnValueOnce('') // service 1475 | .mockReturnValueOnce('somecluster') // cluster 1476 | .mockReturnValueOnce('') // wait-for-service-stability 1477 | .mockReturnValueOnce('') // wait-for-minutes 1478 | .mockReturnValueOnce('') // force-new-deployment 1479 | .mockReturnValueOnce('') // desired-count 1480 | .mockReturnValueOnce('') // enable-ecs-managed-tags 1481 | .mockReturnValueOnce('') // propagate-tags 1482 | .mockReturnValueOnce('true') // run-task 1483 | .mockReturnValueOnce('false'); // wait-for-task-stopped 1484 | 1485 | mockRunTask.mockImplementation( 1486 | () => Promise.resolve({ 1487 | failures: [{ 1488 | reason: 'TASK_FAILED', 1489 | arn: "arn:aws:ecs:fake-region:account_id:task/arn" 1490 | }], 1491 | tasks: [ 1492 | { 1493 | containers: [ 1494 | { 1495 | lastStatus: "RUNNING", 1496 | exitCode: 0, 1497 | reason: '', 1498 | taskArn: "arn:aws:ecs:fake-region:account_id:task/arn" 1499 | } 1500 | ], 1501 | desiredStatus: "RUNNING", 1502 | lastStatus: "STOPPED", 1503 | taskArn: "arn:aws:ecs:fake-region:account_id:task/arn" 1504 | } 1505 | ] 1506 | }) 1507 | ); 1508 | 1509 | await run(); 1510 | expect(core.setFailed).toBeCalledWith("arn:aws:ecs:fake-region:account_id:task/arn is TASK_FAILED"); 1511 | }); 1512 | 1513 | test('error caught if AppSpec file is not formatted correctly', async () => { 1514 | mockEcsDescribeServices.mockImplementation( 1515 | () => Promise.resolve({ 1516 | failures: [], 1517 | services: [{ 1518 | status: 'ACTIVE', 1519 | deploymentController: { 1520 | type: 'CODE_DEPLOY' 1521 | } 1522 | }] 1523 | }) 1524 | ); 1525 | fs.readFileSync.mockReturnValue("hello: world"); 1526 | 1527 | await run(); 1528 | 1529 | expect(core.setFailed).toBeCalledWith("AppSpec file must include property 'resources'"); 1530 | }); 1531 | 1532 | test('error is caught if service does not exist', async () => { 1533 | mockEcsDescribeServices.mockImplementation( 1534 | () => Promise.resolve({ 1535 | failures: [{ 1536 | reason: 'MISSING', 1537 | arn: 'hello' 1538 | }], 1539 | services: [] 1540 | }) 1541 | ); 1542 | 1543 | await run(); 1544 | 1545 | expect(core.setFailed).toBeCalledWith('hello is MISSING'); 1546 | }); 1547 | 1548 | test('error is caught if service is inactive', async () => { 1549 | mockEcsDescribeServices.mockImplementation( 1550 | () => Promise.resolve({ 1551 | failures: [], 1552 | services: [{ 1553 | status: 'INACTIVE' 1554 | }] 1555 | }) 1556 | ); 1557 | 1558 | await run(); 1559 | 1560 | expect(core.setFailed).toBeCalledWith('Service is INACTIVE'); 1561 | }); 1562 | 1563 | test('error is caught if service uses external deployment controller', async () => { 1564 | mockEcsDescribeServices.mockImplementation( 1565 | () => Promise.resolve({ 1566 | failures: [], 1567 | services: [{ 1568 | status: 'ACTIVE', 1569 | deploymentController: { 1570 | type: 'EXTERNAL' 1571 | } 1572 | }] 1573 | }) 1574 | ); 1575 | 1576 | await run(); 1577 | 1578 | expect(core.setFailed).toBeCalledWith('Unsupported deployment controller: EXTERNAL'); 1579 | }); 1580 | 1581 | test('error is caught if task def registration fails', async () => { 1582 | mockEcsRegisterTaskDef.mockImplementation(() => { 1583 | throw new Error("Could not parse"); 1584 | }); 1585 | 1586 | await run(); 1587 | 1588 | expect(core.setFailed).toHaveBeenCalledTimes(2); 1589 | expect(core.setFailed).toHaveBeenNthCalledWith(1, 'Failed to register task definition in ECS: Could not parse'); 1590 | expect(core.setFailed).toHaveBeenNthCalledWith(2, 'Could not parse'); 1591 | }); 1592 | 1593 | test('propagate service tags from service', async () => { 1594 | core.getInput = jest 1595 | .fn() 1596 | .mockReturnValueOnce('task-definition.json') // task-definition 1597 | .mockReturnValueOnce('service-456') // service 1598 | .mockReturnValueOnce('cluster-789') // cluster 1599 | .mockReturnValueOnce('false') // wait-for-service-stability 1600 | .mockReturnValueOnce('') // wait-for-minutes 1601 | .mockReturnValueOnce('') // force-new-deployment 1602 | .mockReturnValueOnce('') // desired-count 1603 | .mockReturnValueOnce('') // enable-ecs-managed-tags 1604 | .mockReturnValueOnce('SERVICE'); // propagate-tags 1605 | 1606 | await run(); 1607 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1608 | 1609 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1610 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1611 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 1612 | cluster: 'cluster-789', 1613 | services: ['service-456'] 1614 | }); 1615 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 1616 | cluster: 'cluster-789', 1617 | service: 'service-456', 1618 | taskDefinition: 'task:def:arn', 1619 | forceNewDeployment: false, 1620 | enableECSManagedTags: null, 1621 | propagateTags: 'SERVICE', 1622 | volumeConfigurations: [] 1623 | }); 1624 | }); 1625 | 1626 | test('update service with setting true to enableECSManagedTags', async () => { 1627 | core.getInput = jest 1628 | .fn() 1629 | .mockReturnValueOnce('task-definition.json') // task-definition 1630 | .mockReturnValueOnce('service-456') // service 1631 | .mockReturnValueOnce('cluster-789') // cluster 1632 | .mockReturnValueOnce('false') // wait-for-service-stability 1633 | .mockReturnValueOnce('') // wait-for-minutes 1634 | .mockReturnValueOnce('') // force-new-deployment 1635 | .mockReturnValueOnce('') // desired-count 1636 | .mockReturnValueOnce('true') // enable-ecs-managed-tags 1637 | .mockReturnValueOnce('SERVICE'); // propagate-tags 1638 | 1639 | await run(); 1640 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1641 | 1642 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1643 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1644 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 1645 | cluster: 'cluster-789', 1646 | services: ['service-456'] 1647 | }); 1648 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 1649 | cluster: 'cluster-789', 1650 | service: 'service-456', 1651 | taskDefinition: 'task:def:arn', 1652 | forceNewDeployment: false, 1653 | enableECSManagedTags: true, 1654 | propagateTags: 'SERVICE', 1655 | volumeConfigurations: [] 1656 | }); 1657 | }); 1658 | 1659 | test('update service with setting false to enableECSManagedTags', async () => { 1660 | core.getInput = jest 1661 | .fn() 1662 | .mockReturnValueOnce('task-definition.json') // task-definition 1663 | .mockReturnValueOnce('service-456') // service 1664 | .mockReturnValueOnce('cluster-789') // cluster 1665 | .mockReturnValueOnce('false') // wait-for-service-stability 1666 | .mockReturnValueOnce('') // wait-for-minutes 1667 | .mockReturnValueOnce('') // force-new-deployment 1668 | .mockReturnValueOnce('') // desired-count 1669 | .mockReturnValueOnce('false') // enable-ecs-managed-tags 1670 | .mockReturnValueOnce('SERVICE'); // propagate-tags 1671 | 1672 | await run(); 1673 | expect(core.setFailed).toHaveBeenCalledTimes(0); 1674 | 1675 | expect(mockEcsRegisterTaskDef).toHaveBeenNthCalledWith(1, { family: 'task-def-family' }); 1676 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'task-definition-arn', 'task:def:arn'); 1677 | expect(mockEcsDescribeServices).toHaveBeenNthCalledWith(1, { 1678 | cluster: 'cluster-789', 1679 | services: ['service-456'] 1680 | }); 1681 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 1682 | cluster: 'cluster-789', 1683 | service: 'service-456', 1684 | taskDefinition: 'task:def:arn', 1685 | forceNewDeployment: false, 1686 | enableECSManagedTags: false, 1687 | propagateTags: 'SERVICE', 1688 | volumeConfigurations: [] 1689 | }); 1690 | }); 1691 | 1692 | test('update service with new EBS volume configuration', async () => { 1693 | core.getInput = jest 1694 | .fn() 1695 | .mockImplementation((name) => { 1696 | console.log(`Getting input for: ${name}`); 1697 | const inputs = { 1698 | 'task-definition': 'task-definition.json', 1699 | 'service': 'service-456', 1700 | 'cluster': 'cluster-789', 1701 | 'service-managed-ebs-volume-name': 'ebs1', 1702 | 'service-managed-ebs-volume': JSON.stringify({ 1703 | filesystemType: "xfs", 1704 | roleArn: "arn:aws:iam::123:role/ebs-role", 1705 | encrypted: false, 1706 | sizeInGiB: 30 1707 | }), 1708 | 'run-task': 'false' 1709 | }; 1710 | return inputs[name] || ''; 1711 | }); 1712 | 1713 | await run(); 1714 | 1715 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(1, { 1716 | cluster: 'cluster-789', 1717 | service: 'service-456', 1718 | taskDefinition: 'task:def:arn', 1719 | forceNewDeployment: false, 1720 | enableECSManagedTags: null, 1721 | propagateTags: null, 1722 | volumeConfigurations: [{ 1723 | name: 'ebs1', 1724 | managedEBSVolume: { 1725 | filesystemType: "xfs", 1726 | roleArn: "arn:aws:iam::123:role/ebs-role", 1727 | encrypted: false, 1728 | sizeInGiB: 30 1729 | } 1730 | }] 1731 | }); 1732 | }); 1733 | 1734 | test('update existing EBS volume configuration in an ECS Service', async () => { 1735 | // First create a service with initial EBS configuration 1736 | core.getInput = jest 1737 | .fn() 1738 | .mockImplementation((name) => { 1739 | const inputs = { 1740 | 'task-definition': 'task-definition.json', 1741 | 'service': 'service-456', 1742 | 'cluster': 'cluster-789', 1743 | 'service-managed-ebs-volume-name': 'ebs1', 1744 | 'service-managed-ebs-volume': JSON.stringify({ 1745 | filesystemType: "xfs", 1746 | roleArn: "arn:aws:iam::123:role/ebs-role", 1747 | encrypted: false, 1748 | sizeInGiB: 30 1749 | }), 1750 | 'run-task': 'false' 1751 | }; 1752 | return inputs[name] || ''; 1753 | }); 1754 | 1755 | await run(); 1756 | 1757 | // Then update the service with new EBS configuration 1758 | core.getInput = jest 1759 | .fn() 1760 | .mockImplementation((name) => { 1761 | const inputs = { 1762 | 'task-definition': 'task-definition.json', 1763 | 'service': 'service-456', 1764 | 'cluster': 'cluster-789', 1765 | 'service-managed-ebs-volume-name': 'ebs1', 1766 | 'service-managed-ebs-volume': JSON.stringify({ 1767 | filesystemType: "xfs", 1768 | roleArn: "arn:aws:iam::123:role/ebs-role", 1769 | encrypted: true, // Changed 1770 | sizeInGiB: 50 // Changed 1771 | }), 1772 | 'run-task': 'false' 1773 | }; 1774 | return inputs[name] || ''; 1775 | }); 1776 | 1777 | await run(); 1778 | 1779 | // Verify the second call had the updated configuration 1780 | expect(mockEcsUpdateService).toHaveBeenNthCalledWith(2, { 1781 | cluster: 'cluster-789', 1782 | service: 'service-456', 1783 | taskDefinition: 'task:def:arn', 1784 | forceNewDeployment: false, 1785 | enableECSManagedTags: null, 1786 | propagateTags: null, 1787 | volumeConfigurations: [{ 1788 | name: 'ebs1', 1789 | managedEBSVolume: { 1790 | filesystemType: "xfs", 1791 | roleArn: "arn:aws:iam::123:role/ebs-role", 1792 | encrypted: true, 1793 | sizeInGiB: 50 1794 | } 1795 | }] 1796 | }); 1797 | }); 1798 | 1799 | test('remove service EBS volume configuration', async () => { 1800 | 1801 | // First call - create service with EBS configuration 1802 | core.getInput = jest 1803 | .fn() 1804 | .mockImplementation((name) => { 1805 | const inputs = { 1806 | 'task-definition': 'task-definition.json', 1807 | 'service': 'service-456', 1808 | 'cluster': 'cluster-789', 1809 | 'service-managed-ebs-volume-name': 'ebs1', 1810 | 'service-managed-ebs-volume': JSON.stringify({ 1811 | filesystemType: "xfs", 1812 | roleArn: "arn:aws:iam::123:role/ebs-role", 1813 | encrypted: false, 1814 | sizeInGiB: 30 1815 | }), 1816 | 'run-task': 'false' 1817 | }; 1818 | return inputs[name] || ''; 1819 | }); 1820 | 1821 | await run(); 1822 | 1823 | // Second call - remove EBS configuration 1824 | core.getInput = jest 1825 | .fn() 1826 | .mockImplementation((name) => { 1827 | const inputs = { 1828 | 'task-definition': 'task-definition.json', 1829 | 'service': 'service-456', 1830 | 'cluster': 'cluster-789', 1831 | 'run-task': 'false' 1832 | }; 1833 | return inputs[name] || ''; 1834 | }); 1835 | 1836 | await run(); 1837 | 1838 | // Verify both calls were made correctly 1839 | expect(mockEcsUpdateService).toHaveBeenCalledTimes(2); 1840 | 1841 | // Verify first call had the EBS configuration 1842 | expect(mockEcsUpdateService.mock.calls[0][0]).toEqual({ 1843 | cluster: 'cluster-789', 1844 | service: 'service-456', 1845 | taskDefinition: 'task:def:arn', 1846 | forceNewDeployment: false, 1847 | enableECSManagedTags: null, 1848 | propagateTags: null, 1849 | volumeConfigurations: [{ 1850 | name: 'ebs1', 1851 | managedEBSVolume: { 1852 | filesystemType: "xfs", 1853 | roleArn: "arn:aws:iam::123:role/ebs-role", 1854 | encrypted: false, 1855 | sizeInGiB: 30 1856 | } 1857 | }] 1858 | }); 1859 | 1860 | // Verify second call had empty volume configurations 1861 | expect(mockEcsUpdateService.mock.calls[1][0]).toEqual({ 1862 | cluster: 'cluster-789', 1863 | service: 'service-456', 1864 | taskDefinition: 'task:def:arn', 1865 | forceNewDeployment: false, 1866 | enableECSManagedTags: null, 1867 | propagateTags: null, 1868 | volumeConfigurations: [] 1869 | }); 1870 | }); 1871 | 1872 | test('run task with EBS volume configuration', async () => { 1873 | core.getInput = jest 1874 | .fn() 1875 | .mockImplementation((name) => { 1876 | const inputs = { 1877 | 'task-definition': 'task-definition.json', 1878 | 'service': '', 1879 | 'cluster': 'cluster-789', 1880 | 'run-task': 'true', 1881 | 'run-task-launch-type': 'EC2', 1882 | 'run-task-managed-ebs-volume-name': 'ebs1', 1883 | 'run-task-managed-ebs-volume': JSON.stringify({ 1884 | filesystemType: "xfs", 1885 | roleArn: "arn:aws:iam::123:role/ebs-role", 1886 | encrypted: false, 1887 | sizeInGiB: 30 1888 | }) 1889 | }; 1890 | return inputs[name] || ''; 1891 | }); 1892 | 1893 | await run(); 1894 | 1895 | expect(mockRunTask).toHaveBeenCalledWith({ 1896 | cluster: 'cluster-789', 1897 | taskDefinition: 'task:def:arn', 1898 | startedBy: 'GitHub-Actions', 1899 | capacityProviderStrategy: null, 1900 | launchType: 'EC2', 1901 | enableECSManagedTags: null, 1902 | tags: [], 1903 | overrides: { 1904 | containerOverrides: [] 1905 | }, 1906 | networkConfiguration: null, 1907 | volumeConfigurations: [{ 1908 | name: 'ebs1', 1909 | managedEBSVolume: { 1910 | filesystemType: "xfs", 1911 | roleArn: "arn:aws:iam::123:role/ebs-role", 1912 | encrypted: false, 1913 | sizeInGiB: 30 1914 | } 1915 | }] 1916 | }); 1917 | }); 1918 | }); -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-actions-amazon-ecs-deploy-task-definition", 3 | "version": "2.3.2", 4 | "description": "Registers an Amazon ECS task definition and deploys it to an ECS service.", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint **.js", 8 | "package": "ncc build index.js -o dist", 9 | "test": "eslint **.js && jest --coverage" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/aws-actions/amazon-ecs-deploy-task-definition.git" 14 | }, 15 | "keywords": [ 16 | "AWS", 17 | "GitHub", 18 | "Actions", 19 | "JavaScript" 20 | ], 21 | "author": "AWS", 22 | "license": "MIT", 23 | "bugs": { 24 | "url": "https://github.com/aws-actions/amazon-ecs-deploy-task-definition/issues" 25 | }, 26 | "homepage": "https://github.com/aws-actions/amazon-ecs-deploy-task-definition#readme", 27 | "dependencies": { 28 | "@actions/core": "^1.10.1", 29 | "@aws-sdk/client-codedeploy": "^3.798.0", 30 | "@aws-sdk/client-ecs": "^3.741.0", 31 | "yaml": "^2.7.1" 32 | }, 33 | "devDependencies": { 34 | "@eslint/js": "^9.27.0", 35 | "@vercel/ncc": "^0.38.3", 36 | "eslint": "^9.22.0", 37 | "globals": "^15.14.0", 38 | "jest": "^29.7.0" 39 | }, 40 | "overrides": { 41 | "@smithy/smithy-client": "3.3.6" 42 | } 43 | } 44 | --------------------------------------------------------------------------------