├── .github ├── dependabot.yml ├── PULL_REQUEST_TEMPLATE.md └── workflows │ ├── package.yml │ ├── check.yml │ └── codeql-analysis.yml ├── CODE_OF_CONDUCT.md ├── .eslintrc.json ├── package.json ├── LICENSE ├── .gitignore ├── .mergify.yml ├── action.yml ├── cleanup.js ├── CONTRIBUTING.md ├── CHANGELOG.md ├── cleanup.test.js ├── index.js ├── THIRD-PARTY ├── README.md └── index.test.js /.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/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 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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/workflows/package.yml: -------------------------------------------------------------------------------- 1 | on: 2 | push: 3 | branches: 4 | - main 5 | 6 | name: Package 7 | 8 | jobs: 9 | check: 10 | name: Package distribution file 11 | runs-on: ubuntu-latest 12 | steps: 13 | - name: Checkout 14 | uses: actions/checkout@v3 15 | with: 16 | ref: main 17 | - name: Package 18 | run: | 19 | npm ci 20 | npm test 21 | npm run package 22 | - name: Commit 23 | run: | 24 | git config --global user.name "GitHub Actions" 25 | git add dist/ 26 | git commit -m "chore: Update dist" || echo "No changes to commit" 27 | git push origin HEAD:main 28 | -------------------------------------------------------------------------------- /.github/workflows/check.yml: -------------------------------------------------------------------------------- 1 | on: 2 | [pull_request] 3 | 4 | name: Check 5 | 6 | jobs: 7 | check: 8 | name: Run Unit Tests 9 | runs-on: ubuntu-latest 10 | steps: 11 | - name: Checkout 12 | uses: actions/checkout@v3 13 | - name: Run tests 14 | run: | 15 | npm ci 16 | npm test 17 | 18 | conventional-commits: 19 | name: Semantic Pull Request 20 | runs-on: ubuntu-latest 21 | steps: 22 | - name: validate 23 | uses: actions/github-script@v6 24 | with: 25 | script: | 26 | // See https://gist.github.com/marcojahn/482410b728c31b221b70ea6d2c433f0c#file-conventional-commit-regex-md 27 | const regex = /^(build|chore|ci|docs|feat|fix|perf|refactor|revert|style|test){1}(\([\w\-\.]+\))?(!)?: ([\w ])+([\s\S]*)/g; 28 | const pr = context.payload.pull_request; 29 | const title = pr.title; 30 | if (title.match(regex) == null) { 31 | throw `PR title "${title}"" does not match conventional commits from https://www.conventionalcommits.org/en/v1.0.0/` 32 | } 33 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "aws-actions-amazon-ecr-login", 3 | "version": "1.5.3", 4 | "description": "Logs in the local Docker client to the AWS account's ECR registry", 5 | "main": "index.js", 6 | "scripts": { 7 | "lint": "eslint **.js", 8 | "package": "ncc build index.js -o dist && ncc build cleanup.js -o dist/cleanup", 9 | "test": "eslint **.js && jest --coverage --verbose" 10 | }, 11 | "repository": { 12 | "type": "git", 13 | "url": "git+https://github.com/aws-actions/amazon-ecr-login.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-ecr-login/issues" 25 | }, 26 | "homepage": "https://github.com/aws-actions/amazon-ecr-login#readme", 27 | "dependencies": { 28 | "@actions/core": "^1.10.0", 29 | "@actions/exec": "^1.1.1", 30 | "aws-sdk": "^2.1301.0" 31 | }, 32 | "devDependencies": { 33 | "@zeit/ncc": "^0.22.3", 34 | "eslint": "^8.32.0", 35 | "jest": "^29.4.0" 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /.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 | 67 | # Extra files 68 | .idea 69 | -------------------------------------------------------------------------------- /.mergify.yml: -------------------------------------------------------------------------------- 1 | queue_rules: 2 | - name: default 3 | conditions: 4 | # Conditions to get out of the queue (= merged) 5 | - status-success=Run Unit Tests 6 | - status-success=Semantic Pull Request 7 | 8 | pull_request_rules: 9 | - name: Automatically merge on CI success and review approval 10 | conditions: 11 | - base~=main|integ-tests 12 | - "#approved-reviews-by>=1" 13 | - approved-reviews-by=@aws-actions/aws-ecr 14 | - -approved-reviews-by~=author 15 | - status-success=Run Unit Tests 16 | - status-success=Semantic Pull Request 17 | - label!=work-in-progress 18 | - -title~=(WIP|wip) 19 | - -merged 20 | - -closed 21 | - author!=dependabot[bot] 22 | actions: 23 | queue: 24 | method: squash 25 | name: default 26 | 27 | - name: Automatically approve and merge Dependabot PRs 28 | conditions: 29 | - base=main 30 | - author=dependabot[bot] 31 | - status-success=Run Unit Tests 32 | - status-success=Semantic Pull Request 33 | - -title~=(WIP|wip) 34 | - -label~=(blocked|do-not-merge) 35 | - -merged 36 | - -closed 37 | actions: 38 | review: 39 | type: APPROVE 40 | queue: 41 | method: squash 42 | name: default 43 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: 'Amazon ECR "Login" Action for GitHub Actions' 2 | description: 'Logs in the local Docker client to one or more Amazon ECR Private registries or an Amazon ECR Public registry' 3 | branding: 4 | icon: 'cloud' 5 | color: 'orange' 6 | inputs: 7 | registries: 8 | description: >- 9 | A comma-delimited list of AWS account IDs that are associated with the ECR Private registries. 10 | If you do not specify a registry, the default ECR Private registry is assumed. 11 | If 'public' is given as input to 'registry-type', this input is ignored. 12 | required: false 13 | skip-logout: 14 | description: >- 15 | Whether to skip explicit logout of the registries during post-job cleanup. 16 | Exists for backward compatibility on self-hosted runners. 17 | Not recommended. 18 | Options: ['true', 'false'] 19 | required: false 20 | default: 'false' 21 | registry-type: 22 | description: >- 23 | Which ECR registry type to log into. 24 | Options: [private, public] 25 | required: false 26 | default: private 27 | outputs: 28 | registry: 29 | description: >- 30 | The URI of the ECR Private or ECR Public registry. 31 | If logging into multiple registries on ECR Private, this output will not be set. 32 | runs: 33 | using: 'node16' 34 | main: 'dist/index.js' 35 | post: 'dist/cleanup/index.js' 36 | -------------------------------------------------------------------------------- /cleanup.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const exec = require('@actions/exec'); 3 | 4 | /** 5 | * When the GitHub Actions job is done, logout of ECR Private/Public. 6 | */ 7 | 8 | const STATES = { 9 | registries: 'registries' 10 | }; 11 | 12 | async function cleanup() { 13 | try { 14 | const registriesState = core.getState(STATES.registries); 15 | 16 | if (registriesState) { 17 | const registries = registriesState.split(','); 18 | const failedLogouts = []; 19 | 20 | // Logout of each registry 21 | for (const registry of registries) { 22 | core.info(`Logging out of registry ${registry}`); 23 | 24 | // Execute the docker logout command 25 | let doLogoutStdout = ''; 26 | let doLogoutStderr = ''; 27 | const exitCode = await exec.exec('docker', ['logout', registry], { 28 | silent: true, 29 | ignoreReturnCode: true, 30 | listeners: { 31 | stdout: (data) => { 32 | doLogoutStdout += data.toString(); 33 | }, 34 | stderr: (data) => { 35 | doLogoutStderr += data.toString(); 36 | } 37 | } 38 | }); 39 | if (exitCode !== 0) { 40 | core.debug(doLogoutStdout); 41 | core.error(`Could not logout of registry ${registry}: ${doLogoutStderr}`); 42 | failedLogouts.push(registry); 43 | } 44 | } 45 | 46 | if (failedLogouts.length) { 47 | throw new Error(`Failed to logout: ${failedLogouts.join(',')}`); 48 | } 49 | } 50 | } 51 | catch (error) { 52 | core.setFailed(error.message); 53 | } 54 | } 55 | 56 | module.exports = cleanup; 57 | 58 | /* istanbul ignore next */ 59 | if (require.main === module) { 60 | cleanup(); 61 | } 62 | -------------------------------------------------------------------------------- /.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: [main] 11 | pull_request: 12 | # The branches below must be a subset of the branches above 13 | branches: [main] 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@v3 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@v2 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@v2 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@v2 72 | -------------------------------------------------------------------------------- /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-ecr-login/issues), or [recently closed](https://github.com/aws-actions/amazon-ecr-login/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-ecr-login/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-ecr-login/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 | -------------------------------------------------------------------------------- /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 | ### [1.5.3](https://github.com/aws-actions/amazon-ecr-login/compare/v1.5.2...v1.5.3) (2022-10-29) 6 | 7 | ### [1.5.2](https://github.com/aws-actions/amazon-ecr-login/compare/v1.5.1...v1.5.2) (2022-10-18) 8 | 9 | ### [1.5.1](https://github.com/aws-actions/amazon-ecr-login/compare/v1.5.0...v1.5.1) (2022-08-04) 10 | 11 | 12 | ### Bug Fixes 13 | 14 | * reverted change that masked Docker credentials ([7d073b6](https://github.com/aws-actions/amazon-ecr-login/commit/7d073b66cc2799eb766b698980d716db7e62a8b7)) 15 | 16 | ## [1.5.0](https://github.com/aws-actions/amazon-ecr-login/compare/v1.4.0...v1.5.0) (2022-06-27) 17 | 18 | 19 | ### Features 20 | 21 | * added ECR Public Registry support ([b4f084e](https://github.com/aws-actions/amazon-ecr-login/commit/b4f084e928b56f43c9afa5773b761bbfcd7e83e2)) 22 | 23 | ## [1.4.0](https://github.com/aws-actions/amazon-ecr-login/compare/v1.3.3...v1.4.0) (2022-05-20) 24 | 25 | 26 | ### Features 27 | 28 | * output docker credentials after login ([57206dc](https://github.com/aws-actions/amazon-ecr-login/commit/57206dc28c379a6eebdce44c592109d2e97e031d)) 29 | * support for username and password outputs ([d121236](https://github.com/aws-actions/amazon-ecr-login/commit/d121236bfd0a712a9f4bd93767d696874680bc95)) 30 | 31 | 32 | ### Bug Fixes 33 | 34 | * add-mask to login outputs ([45a78e2](https://github.com/aws-actions/amazon-ecr-login/commit/45a78e2dab5678b27e94cf31545c181e8ca9c044)) 35 | 36 | ### [1.3.3](https://github.com/aws-actions/amazon-ecr-login/compare/v1.3.2...v1.3.3) (2021-02-15) 37 | 38 | ### [1.3.2](https://github.com/aws-actions/amazon-ecr-login/compare/v1.3.1...v1.3.2) (2021-02-01) 39 | 40 | ### [1.3.1](https://github.com/aws-actions/amazon-ecr-login/compare/v1.3.0...v1.3.1) (2020-11-24) 41 | 42 | ## [1.3.0](https://github.com/aws-actions/amazon-ecr-login/compare/v1.2.2...v1.3.0) (2020-10-29) 43 | 44 | 45 | ### Features 46 | 47 | * optional skipping of docker registries logout in post step ([#78](https://github.com/aws-actions/amazon-ecr-login/issues/78)) ([dd3fdee](https://github.com/aws-actions/amazon-ecr-login/commit/dd3fdeeb95577a637ece5e647581680afda16e6f)) 48 | 49 | ### [1.2.2](https://github.com/aws-actions/amazon-ecr-login/compare/v1.2.1...v1.2.2) (2020-10-05) 50 | 51 | ### [1.2.1](https://github.com/aws-actions/amazon-ecr-login/compare/v1.2.0...v1.2.1) (2020-08-25) 52 | 53 | ## [1.2.0](https://github.com/aws-actions/amazon-ecr-login/compare/v1.1.4...v1.2.0) (2020-08-11) 54 | 55 | 56 | ### Features 57 | 58 | * logout docker registries in post step ([#70](https://github.com/aws-actions/amazon-ecr-login/issues/70)) ([6cfbb32](https://github.com/aws-actions/amazon-ecr-login/commit/6cfbb329c3ecc5a7f78c5b7f5a779ad99aa77cea)) 59 | 60 | ### [1.1.4](https://github.com/aws-actions/amazon-ecr-login/compare/v1.1.3...v1.1.4) (2020-07-17) 61 | 62 | ### [1.1.3](https://github.com/aws-actions/amazon-ecr-login/compare/v1.1.2...v1.1.3) (2020-07-14) 63 | 64 | ### [1.1.2](https://github.com/aws-actions/amazon-ecr-login/compare/v1.1.1...v1.1.2) (2020-06-30) 65 | 66 | ### [1.1.1](https://github.com/aws-actions/amazon-ecr-login/compare/v1.1.0...v1.1.1) (2020-06-09) 67 | 68 | ## [1.1.0](https://github.com/aws-actions/amazon-ecr-login/compare/v1.0.7...v1.1.0) (2020-05-27) 69 | 70 | 71 | ### Features 72 | 73 | * output registry URI if single registry ID is provided as input ([#50](https://github.com/aws-actions/amazon-ecr-login/issues/50)) ([cfd96f4](https://github.com/aws-actions/amazon-ecr-login/commit/cfd96f4b0041e43b7473d8e0850bf7fb4471a507)) 74 | 75 | ### [1.0.7](https://github.com/aws-actions/amazon-ecr-login/compare/v1.0.6...v1.0.7) (2020-05-18) 76 | 77 | ### [1.0.6](https://github.com/aws-actions/amazon-ecr-login/compare/v1.0.5...v1.0.6) (2020-05-08) 78 | 79 | ### [1.0.5](https://github.com/aws-actions/amazon-ecr-login/compare/v1.0.4...v1.0.5) (2020-04-02) 80 | 81 | ### [1.0.4](https://github.com/aws-actions/amazon-ecr-login/compare/v1.0.3...v1.0.4) (2020-03-05) 82 | 83 | 84 | ### Bug Fixes 85 | 86 | * Add debugging for registry ID input ([127dcbd](https://github.com/aws-actions/amazon-ecr-login/commit/127dcbdc25a788bc50ed461ba2d597287ec9ae1f)) 87 | 88 | ### [1.0.3](https://github.com/aws-actions/amazon-ecr-login/compare/v1.0.2...v1.0.3) (2020-02-07) 89 | 90 | ### [1.0.2](https://github.com/aws-actions/amazon-ecr-login/compare/v1.0.1...v1.0.2) (2020-02-06) 91 | 92 | 93 | ### Bug Fixes 94 | 95 | * Match package version to current tag version ([e254adb](https://github.com/aws-actions/amazon-ecr-login/commit/e254adbeaeb34c7a2d6bd0a6600f3bbf89fc1e5d)) 96 | -------------------------------------------------------------------------------- /cleanup.test.js: -------------------------------------------------------------------------------- 1 | const cleanup = require('./cleanup.js'); 2 | const core = require('@actions/core'); 3 | const exec = require('@actions/exec'); 4 | 5 | jest.mock('@actions/core'); 6 | jest.mock('@actions/exec'); 7 | 8 | function mockGetState(requestResponse) { 9 | return function (name, options) { // eslint-disable-line no-unused-vars 10 | return requestResponse[name] 11 | } 12 | } 13 | 14 | const ECR_STATES = { 15 | 'registries': '123456789012.dkr.ecr.aws-region-1.amazonaws.com,111111111111.dkr.ecr.aws-region-1.amazonaws.com' 16 | }; 17 | 18 | describe('Logout from ECR', () => { 19 | 20 | beforeEach(() => { 21 | jest.clearAllMocks(); 22 | 23 | core.getState = jest.fn().mockImplementation(mockGetState(ECR_STATES)); 24 | exec.exec.mockReturnValue(0); 25 | }); 26 | 27 | test('logs out docker client for registries in action state', async () => { 28 | await cleanup(); 29 | 30 | expect(core.getState).toHaveBeenCalledWith('registries'); 31 | expect(exec.exec).toHaveBeenNthCalledWith(1, 32 | 'docker', 33 | ['logout', '123456789012.dkr.ecr.aws-region-1.amazonaws.com'], 34 | expect.anything()); 35 | expect(exec.exec).toHaveBeenNthCalledWith(2, 36 | 'docker', 37 | ['logout', '111111111111.dkr.ecr.aws-region-1.amazonaws.com'], 38 | expect.anything()); 39 | expect(exec.exec).toHaveBeenCalledTimes(2); 40 | expect(core.setFailed).toHaveBeenCalledTimes(0); 41 | }); 42 | 43 | test('logs out docker client for public registry in action state', async () => { 44 | const mockStates = { 45 | 'registries': 'public.ecr.aws' 46 | }; 47 | core.getState = jest.fn().mockImplementation(mockGetState(mockStates)); 48 | 49 | await cleanup(); 50 | 51 | expect(core.getState).toHaveBeenCalledWith('registries'); 52 | expect(exec.exec).toHaveBeenNthCalledWith(1, 53 | 'docker', 54 | ['logout', 'public.ecr.aws'], 55 | expect.anything()); 56 | expect(exec.exec).toHaveBeenCalledTimes(1); 57 | expect(core.setFailed).toHaveBeenCalledTimes(0); 58 | }); 59 | 60 | test('handles zero registries', async () => { 61 | const mockStates = { 62 | 'registries' : '' 63 | }; 64 | core.getState = jest.fn().mockImplementation(mockGetState(mockStates)); 65 | 66 | await cleanup(); 67 | 68 | expect(core.getState).toHaveBeenCalledWith('registries'); 69 | expect(exec.exec).toHaveBeenCalledTimes(0); 70 | expect(core.setFailed).toHaveBeenCalledTimes(0); 71 | }); 72 | 73 | test('error is caught by core.setFailed for failed docker logout', async () => { 74 | exec.exec.mockReturnValue(1); 75 | 76 | await cleanup(); 77 | 78 | expect(core.setFailed).toHaveBeenCalled(); 79 | }); 80 | 81 | test('continues to attempt logouts after a failed logout', async () => { 82 | const mockStates = { 83 | 'registries' : '123456789012.dkr.ecr.aws-region-1.amazonaws.com,111111111111.dkr.ecr.aws-region-1.amazonaws.com,222222222222.dkr.ecr.aws-region-1.amazonaws.com' 84 | }; 85 | core.getState = jest.fn().mockImplementation(mockGetState(mockStates)); 86 | exec.exec.mockImplementationOnce((commandLine, args, options) => { 87 | options.listeners.stdout('stdout of '); 88 | options.listeners.stdout('registry 1'); 89 | options.listeners.stderr('stderr of '); 90 | options.listeners.stderr('registry 1'); 91 | return(1); 92 | }).mockImplementationOnce((commandLine, args, options) => { 93 | options.listeners.stdout('stdout of '); 94 | options.listeners.stdout('registry 2'); 95 | options.listeners.stderr('stderr of '); 96 | options.listeners.stderr('registry 2'); 97 | return(1); 98 | }).mockReturnValueOnce(0); 99 | 100 | await cleanup(); 101 | 102 | expect(core.getState).toHaveBeenCalledWith('registries'); 103 | expect(exec.exec).toHaveBeenNthCalledWith(1, 104 | 'docker', 105 | ['logout', '123456789012.dkr.ecr.aws-region-1.amazonaws.com'], 106 | expect.anything()); 107 | expect(exec.exec).toHaveBeenNthCalledWith(2, 108 | 'docker', 109 | ['logout', '111111111111.dkr.ecr.aws-region-1.amazonaws.com'], 110 | expect.anything()); 111 | expect(exec.exec).toHaveBeenNthCalledWith(3, 112 | 'docker', 113 | ['logout', '222222222222.dkr.ecr.aws-region-1.amazonaws.com'], 114 | expect.anything()); 115 | expect(core.error).toHaveBeenNthCalledWith(1, 'Could not logout of registry 123456789012.dkr.ecr.aws-region-1.amazonaws.com: stderr of registry 1'); 116 | expect(core.error).toHaveBeenNthCalledWith(2, 'Could not logout of registry 111111111111.dkr.ecr.aws-region-1.amazonaws.com: stderr of registry 2'); 117 | expect(core.setFailed).toHaveBeenCalledWith('Failed to logout: 123456789012.dkr.ecr.aws-region-1.amazonaws.com,111111111111.dkr.ecr.aws-region-1.amazonaws.com'); 118 | expect(exec.exec).toHaveBeenCalledTimes(3); 119 | expect(core.error).toHaveBeenCalledTimes(2); 120 | expect(core.setFailed).toHaveBeenCalledTimes(1); 121 | }); 122 | }); 123 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | const core = require('@actions/core'); 2 | const exec = require('@actions/exec'); 3 | const aws = require('aws-sdk'); 4 | 5 | const ECR_LOGIN_GITHUB_ACTION_USER_AGENT = 'amazon-ecr-login-for-github-actions'; 6 | const ECR_PUBLIC_REGISTRY_URI = 'public.ecr.aws'; 7 | 8 | const INPUTS = { 9 | skipLogout: 'skip-logout', 10 | registries: 'registries', 11 | registryType: 'registry-type' 12 | }; 13 | 14 | const OUTPUTS = { 15 | registry: 'registry', 16 | dockerUsername: 'docker_username', 17 | dockerPassword: 'docker_password' 18 | }; 19 | 20 | const STATES = { 21 | registries: 'registries' 22 | }; 23 | 24 | const REGISTRY_TYPES = { 25 | private: 'private', 26 | public: 'public' 27 | }; 28 | 29 | 30 | function replaceSpecialCharacters(registryUri) { 31 | return registryUri.replace(/[^a-zA-Z0-9_]+/g, '_'); 32 | } 33 | 34 | async function getEcrAuthTokenWrapper(authTokenRequest) { 35 | const ecr = new aws.ECR({ 36 | customUserAgent: ECR_LOGIN_GITHUB_ACTION_USER_AGENT 37 | }); 38 | const authTokenResponse = await ecr.getAuthorizationToken(authTokenRequest).promise(); 39 | if (!authTokenResponse) { 40 | throw new Error('Amazon ECR authorization token returned no data'); 41 | } else if (!authTokenResponse.authorizationData || !Array.isArray(authTokenResponse.authorizationData)) { 42 | throw new Error('Amazon ECR authorization token is invalid'); 43 | } else if (!authTokenResponse.authorizationData.length) { 44 | throw new Error('Amazon ECR authorization token does not contain any authorization data'); 45 | } 46 | 47 | return authTokenResponse; 48 | } 49 | 50 | async function getEcrPublicAuthTokenWrapper(authTokenRequest) { 51 | const ecrPublic = new aws.ECRPUBLIC({ 52 | customUserAgent: ECR_LOGIN_GITHUB_ACTION_USER_AGENT 53 | }); 54 | const authTokenResponse = await ecrPublic.getAuthorizationToken(authTokenRequest).promise(); 55 | if (!authTokenResponse) { 56 | throw new Error('Amazon ECR Public authorization token returned no data'); 57 | } else if (!authTokenResponse.authorizationData) { 58 | throw new Error('Amazon ECR Public authorization token is invalid'); 59 | } else if (Object.keys(authTokenResponse.authorizationData).length === 0) { 60 | throw new Error('Amazon ECR Public authorization token does not contain any authorization data'); 61 | } 62 | 63 | return { 64 | authorizationData: [ 65 | { 66 | authorizationToken: authTokenResponse.authorizationData.authorizationToken, 67 | proxyEndpoint: ECR_PUBLIC_REGISTRY_URI 68 | } 69 | ] 70 | }; 71 | } 72 | 73 | async function run() { 74 | // Get inputs 75 | const skipLogout = core.getInput(INPUTS.skipLogout, { required: false }).toLowerCase() === 'true'; 76 | const registries = core.getInput(INPUTS.registries, { required: false }); 77 | const registryType = core.getInput(INPUTS.registryType, { required: false }).toLowerCase() || REGISTRY_TYPES.private; 78 | 79 | const registryUriState = []; 80 | 81 | try { 82 | if (registryType !== REGISTRY_TYPES.private && registryType !== REGISTRY_TYPES.public) { 83 | throw new Error(`Invalid input for '${INPUTS.registryType}', possible options are [${REGISTRY_TYPES.private}, ${REGISTRY_TYPES.public}]`); 84 | } 85 | 86 | // Get the ECR/ECR Public authorization token(s) 87 | const authTokenRequest = {}; 88 | if (registryType === REGISTRY_TYPES.private && registries) { 89 | const registryIds = registries.split(','); 90 | core.debug(`Requesting auth token for ${registryIds.length} registries:`); 91 | for (const id of registryIds) { 92 | core.debug(` '${id}'`); 93 | } 94 | authTokenRequest.registryIds = registryIds; 95 | } 96 | const authTokenResponse = registryType === REGISTRY_TYPES.private ? 97 | await getEcrAuthTokenWrapper(authTokenRequest) : 98 | await getEcrPublicAuthTokenWrapper(authTokenRequest); 99 | 100 | // Login to each registry 101 | for (const authData of authTokenResponse.authorizationData) { 102 | const authToken = Buffer.from(authData.authorizationToken, 'base64').toString('utf-8'); 103 | const creds = authToken.split(':', 2); 104 | const proxyEndpoint = authData.proxyEndpoint; 105 | const registryUri = proxyEndpoint.replace(/^https?:\/\//,''); 106 | 107 | core.info(`Logging into registry ${registryUri}`); 108 | 109 | // output the registry URI if this action is doing a single registry login 110 | if (authTokenResponse.authorizationData.length === 1) { 111 | core.setOutput(OUTPUTS.registry, registryUri); 112 | } 113 | 114 | // Execute the docker login command 115 | let doLoginStdout = ''; 116 | let doLoginStderr = ''; 117 | const exitCode = await exec.exec('docker', ['login', '-u', creds[0], '-p', creds[1], proxyEndpoint], { 118 | silent: true, 119 | ignoreReturnCode: true, 120 | listeners: { 121 | stdout: (data) => { 122 | doLoginStdout += data.toString(); 123 | }, 124 | stderr: (data) => { 125 | doLoginStderr += data.toString(); 126 | } 127 | } 128 | }); 129 | if (exitCode !== 0) { 130 | core.debug(doLoginStdout); 131 | throw new Error(`Could not login to registry ${registryUri}: ${doLoginStderr}`); 132 | } 133 | 134 | // Output docker username and password 135 | const secretSuffix = replaceSpecialCharacters(registryUri); 136 | core.setOutput(`${OUTPUTS.dockerUsername}_${secretSuffix}`, creds[0]); 137 | core.setOutput(`${OUTPUTS.dockerPassword}_${secretSuffix}`, creds[1]); 138 | 139 | registryUriState.push(registryUri); 140 | } 141 | } 142 | catch (error) { 143 | core.setFailed(error.message); 144 | } 145 | 146 | // Pass the logged-in registry URIs to the post action for logout 147 | if (registryUriState.length) { 148 | if (!skipLogout) { 149 | core.saveState(STATES.registries, registryUriState.join()); 150 | } 151 | core.debug(`'${INPUTS.skipLogout}' is ${skipLogout} for ${registryUriState.length} registries.`); 152 | } 153 | } 154 | 155 | module.exports = { 156 | run, 157 | replaceSpecialCharacters 158 | }; 159 | 160 | /* istanbul ignore next */ 161 | if (require.main === module) { 162 | run(); 163 | } 164 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ## Amazon ECR "Login" Action for GitHub Actions 2 | 3 | Logs in the local Docker client to one or more Amazon ECR Private registries or an Amazon ECR Public registry. 4 | 5 | **Table of Contents** 6 | 7 | 8 | 9 | - [Example of Usage](#example-of-usage) 10 | - [Credentials and Region](#credentials-and-region) 11 | - [Permissions](#permissions) 12 | - [Troubleshooting](#troubleshooting) 13 | - [License Summary](#license-summary) 14 | - [Security Disclosures](#security-disclosures) 15 | 16 | 17 | 18 | ## Examples of Usage 19 | 20 | ### Building and pushing an image 21 | 22 | #### Before each of the following examples, make sure to include the following: 23 | ```yaml 24 | - name: Checkout repo 25 | uses: actions/checkout@v3 26 | 27 | - name: Configure AWS credentials 28 | uses: aws-actions/configure-aws-credentials@v1 # More information on this action can be found below in the 'AWS Credentials' section 29 | with: 30 | role-to-assume: arn:aws:iam::123456789012:role/my-github-actions-role 31 | aws-region: aws-region-1 32 | ``` 33 | 34 | #### Login to Amazon ECR Private, then build and push a Docker image: 35 | ```yaml 36 | - name: Login to Amazon ECR 37 | id: login-ecr 38 | uses: aws-actions/amazon-ecr-login@v1 39 | 40 | - name: Build, tag, and push docker image to Amazon ECR 41 | env: 42 | REGISTRY: ${{ steps.login-ecr.outputs.registry }} 43 | REPOSITORY: my-ecr-repo 44 | IMAGE_TAG: ${{ github.sha }} 45 | run: | 46 | docker build -t $REGISTRY/$REPOSITORY:$IMAGE_TAG . 47 | docker push $REGISTRY/$REPOSITORY:$IMAGE_TAG 48 | ``` 49 | 50 | #### Login to Amazon ECR Public, then build and push a Docker image: 51 | ```yaml 52 | - name: Login to Amazon ECR Public 53 | id: login-ecr-public 54 | uses: aws-actions/amazon-ecr-login@v1 55 | with: 56 | registry-type: public 57 | 58 | - name: Build, tag, and push docker image to Amazon ECR Public 59 | env: 60 | REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} 61 | REGISTRY_ALIAS: my-ecr-public-registry-alias 62 | REPOSITORY: my-ecr-public-repo 63 | IMAGE_TAG: ${{ github.sha }} 64 | run: | 65 | docker build -t $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG . 66 | docker push $REGISTRY/$REGISTRY_ALIAS/$REPOSITORY:$IMAGE_TAG 67 | ``` 68 | 69 | #### Login to Amazon ECR Private, then package and push a Helm chart: 70 | ```yaml 71 | - name: Login to Amazon ECR 72 | id: login-ecr 73 | uses: aws-actions/amazon-ecr-login@v1 74 | 75 | - name: Package and push helm chart to Amazon ECR 76 | env: 77 | REGISTRY: ${{ steps.login-ecr.outputs.registry }} 78 | REPOSITORY: my-ecr-repo 79 | run: | 80 | helm package $REPOSITORY 81 | helm push $REPOSITORY-0.1.0.tgz oci://$REGISTRY 82 | ``` 83 | 84 | #### Login to Amazon ECR Public, then package and push a Helm chart: 85 | ```yaml 86 | - name: Login to Amazon ECR Public 87 | id: login-ecr-public 88 | uses: aws-actions/amazon-ecr-login@v1 89 | with: 90 | registry-type: public 91 | 92 | - name: Package and push helm chart to Amazon ECR Public 93 | env: 94 | REGISTRY: ${{ steps.login-ecr-public.outputs.registry }} 95 | REGISTRY_ALIAS: my-ecr-public-registry-alias 96 | REPOSITORY: my-ecr-public-repo 97 | run: | 98 | helm package $REPOSITORY 99 | helm push $REPOSITORY-0.1.0.tgz oci://$REGISTRY/$REGISTRY_ALIAS 100 | ``` 101 | 102 | (Helm uses the same credential store as Docker, so Helm can authenticate with the same credentials that you use for Docker) 103 | 104 | #### Login to ECR on multiple AWS accounts 105 | 106 | ```yaml 107 | - name: Configure AWS credentials 108 | uses: aws-actions/configure-aws-credentials@v1 109 | with: 110 | role-to-assume: arn:aws:iam::123456789012:role/my-github-actions-role 111 | aws-region: aws-region-1 112 | 113 | - name: Login to Amazon ECR 114 | id: login-ecr 115 | uses: aws-actions/amazon-ecr-login@v1 116 | with: 117 | registries: "123456789012,998877665544" 118 | ``` 119 | 120 | The repository on account `998877665544` needs to explicitly grant access to role: 121 | `arn:aws:iam::123456789012:role/my-github-actions-role` in order for cross-account access to work 122 | 123 | Please refer to [AWS docs](https://aws.amazon.com/premiumsupport/knowledge-center/secondary-account-access-ecr/) 124 | for details on how to configure ECR policies 125 | 126 | ### Using an image as a service 127 | 128 | Login to Amazon ECR Private, then use the outputted Docker credentials to run your private image as a service in another job 129 | ```yaml 130 | jobs: 131 | login-to-amazon-ecr: 132 | runs-on: ubuntu-latest 133 | steps: 134 | - name: Configure AWS credentials 135 | uses: aws-actions/configure-aws-credentials@v1 136 | with: 137 | role-to-assume: arn:aws:iam::123456789012:role/my-github-actions-role 138 | aws-region: us-east-1 139 | mask-aws-account-id: 'false' 140 | - name: Login to Amazon ECR 141 | id: login-ecr 142 | uses: aws-actions/amazon-ecr-login@v1 143 | outputs: 144 | registry: ${{ steps.login-ecr.outputs.registry }} 145 | docker_username: ${{ steps.login-ecr.outputs.docker_username_123456789012_dkr_ecr_us_east_1_amazonaws_com }} # More information on these outputs can be found below in the 'Docker Credentials' section 146 | docker_password: ${{ steps.login-ecr.outputs.docker_password_123456789012_dkr_ecr_us_east_1_amazonaws_com }} 147 | 148 | run-with-internal-service: 149 | name: Run something with an internal image as a service 150 | needs: login-to-amazon-ecr 151 | runs-on: ubuntu-latest 152 | services: 153 | internal-service: 154 | image: ${{ needs.login-to-amazon-ecr.outputs.registry }}/my-ecr-repo:latest 155 | credentials: 156 | username: ${{ needs.login-to-amazon-ecr.outputs.docker_username }} 157 | password: ${{ needs.login-to-amazon-ecr.outputs.docker_password }} 158 | ports: 159 | - '80:80' 160 | steps: 161 | - name: Run steps in container 162 | run: echo "run steps in container" 163 | ``` 164 | 165 | See [action.yml](action.yml) for the full documentation for this action's inputs and outputs. 166 | 167 | ## Credentials and Region 168 | 169 | ### AWS Credentials 170 | 171 | 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. Use [the `aws-actions/configure-aws-credentials` action](https://github.com/aws-actions/configure-aws-credentials) to configure the GitHub Actions environment with a role using GitHub's OIDC provider and your desired region. 172 | 173 | ```yaml 174 | - name: Configure AWS credentials 175 | uses: aws-actions/configure-aws-credentials@v1 176 | with: 177 | role-to-assume: arn:aws:iam::123456789012:role/my-github-actions-role 178 | aws-region: us-east-1 179 | 180 | - name: Login to Amazon ECR Private 181 | id: login-ecr 182 | uses: aws-actions/amazon-ecr-login@v1 183 | ``` 184 | 185 | We recommend following [Amazon IAM best practices](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html) when using AWS services in GitHub Actions workflows, including: 186 | * [Assume an IAM role](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#bp-workloads-use-roles) to receive temporary credentials. See the [Sample IAM Role CloudFormation Template](https://github.com/aws-actions/configure-aws-credentials#sample-iam-role-cloudformation-template) in the `aws-actions/configure-aws-credentials` action to get an example. 187 | * [Grant least privilege](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#grant-least-privilege) to the IAM role 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 | * [Monitor the activity](https://docs.aws.amazon.com/IAM/latest/UserGuide/best-practices.html#remove-credentials) of the IAM role used in GitHub Actions workflows. 189 | 190 | ### Docker Credentials 191 | The registry URIs for ECR Private and ECR Public are as follows: 192 | - Registry URI for ECR Private: `123456789012.dkr.ecr.aws-region-1.amazonaws.com` 193 | - Registry URI for ECR Public: `public.ecr.aws` 194 | 195 | After logging in, you can access the docker username and password via action outputs using the following format: 196 | 197 | If using ECR Private: 198 | - Docker username output: `docker_username_123456789012_dkr_ecr_aws_region_1_amazonaws_com` 199 | - Docker password output: `docker_password_123456789012_dkr_ecr_aws_region_1_amazonaws_com` 200 | 201 | If using ECR Public: 202 | - Docker username output: `docker_username_public_ecr_aws` 203 | - Docker password output: `docker_password_public_ecr_aws` 204 | 205 | To push Helm charts, you can also login through Docker. By default, Helm can authenticate with the same credentials that you use for Docker. 206 | 207 | ## Permissions 208 | 209 | ### ECR Private 210 | 211 | To see how and where to implement the permissions below, see the [IAM section in the Amazon ECR User Guide](https://docs.aws.amazon.com/AmazonECR/latest/userguide/security-iam.html). 212 | 213 | This action requires the following minimum set of permissions to login to ECR Private: 214 | 215 | ```json 216 | { 217 | "Version": "2012-10-17", 218 | "Statement": [ 219 | { 220 | "Sid": "GetAuthorizationToken", 221 | "Effect": "Allow", 222 | "Action": [ 223 | "ecr:GetAuthorizationToken" 224 | ], 225 | "Resource": "*" 226 | } 227 | ] 228 | } 229 | ``` 230 | 231 | Docker commands in your GitHub Actions workflow, like `docker pull` and `docker push`, may require additional permissions attached to the credentials used by this action. 232 | 233 | The following minimum permissions are required for pulling an image from an ECR Private repository: 234 | 235 | ```json 236 | { 237 | "Version": "2012-10-17", 238 | "Statement": [ 239 | { 240 | "Sid": "AllowPull", 241 | "Effect": "Allow", 242 | "Action": [ 243 | "ecr:BatchGetImage", 244 | "ecr:GetDownloadUrlForLayer" 245 | ], 246 | "Resource": "arn:aws:ecr:us-east-1:123456789012:repository/my-ecr-repo" 247 | } 248 | ] 249 | } 250 | ``` 251 | 252 | The following minimum permissions are required for pushing and pulling images in an ECR Private repository: 253 | 254 | ```json 255 | { 256 | "Version": "2012-10-17", 257 | "Statement": [ 258 | { 259 | "Sid": "AllowPushPull", 260 | "Effect": "Allow", 261 | "Action": [ 262 | "ecr:BatchGetImage", 263 | "ecr:BatchCheckLayerAvailability", 264 | "ecr:CompleteLayerUpload", 265 | "ecr:GetDownloadUrlForLayer", 266 | "ecr:InitiateLayerUpload", 267 | "ecr:PutImage", 268 | "ecr:UploadLayerPart" 269 | ], 270 | "Resource": "arn:aws:ecr:us-east-1:123456789012:repository/my-ecr-repo" 271 | } 272 | ] 273 | } 274 | ``` 275 | 276 | ### ECR Public 277 | 278 | To see how and where to implement the permissions below, see the [IAM section in the Amazon ECR Public User Guide](https://docs.aws.amazon.com/AmazonECR/latest/public/security-iam.html). 279 | 280 | 281 | This action requires the following minimum set of permissions to login to ECR Public: 282 | 283 | ```json 284 | { 285 | "Version": "2012-10-17", 286 | "Statement": [ 287 | { 288 | "Sid": "GetAuthorizationToken", 289 | "Effect": "Allow", 290 | "Action": [ 291 | "ecr-public:GetAuthorizationToken", 292 | "sts:GetServiceBearerToken" 293 | ], 294 | "Resource": "*" 295 | } 296 | ] 297 | } 298 | ``` 299 | 300 | Docker commands in your GitHub Actions workflow, like `docker push`, may require additional permissions attached to the credentials used by this action. There are no permissions needed for pulling images from ECR Public. 301 | 302 | The following minimum permissions are required for pushing an image to an ECR Public repository: 303 | 304 | ```json 305 | { 306 | "Version": "2012-10-17", 307 | "Statement": [ 308 | { 309 | "Sid": "AllowPush", 310 | "Effect": "Allow", 311 | "Action": [ 312 | "ecr-public:BatchCheckLayerAvailability", 313 | "ecr-public:CompleteLayerUpload", 314 | "ecr-public:InitiateLayerUpload", 315 | "ecr-public:PutImage", 316 | "ecr-public:UploadLayerPart" 317 | ], 318 | "Resource": "arn:aws:ecr-public:us-east-1:123456789012:repository/my-ecr-public-repo" 319 | } 320 | ] 321 | } 322 | ``` 323 | 324 | ## Troubleshooting 325 | 326 | ### Configure credentials 327 | 328 | `Inaccessible host: 'api.ecr-public.aws-region-1.amazonaws.com' at port 'undefined'. This service may not be available in the 'aws-region-1' region.` 329 | 330 | - The `AWS_DEFAULT_REGION` environment variable is configured as a region where ECR Public isn't available. 331 | - ECR Public can only be logged into from the `us-east-1` region. In the `aws-actions/configure-aws-credentials` action, the `aws-region` input must be `us-east-1`. 332 | 333 | `GetAuthorizationToken command is only supported in us-east-1.` 334 | 335 | - The `AWS_DEFAULT_REGION` environment variable is configured as `us-west-2`. 336 | - ECR Public can only be logged into from the `us-east-1` region. In the `aws-actions/configure-aws-credentials` action, the `aws-region` input must be `us-east-1`. 337 | 338 | ### Inputs 339 | 340 | `Invalid parameter at 'registryIds' failed to satisfy constraint: 'Member must satisfy constraint: [Member must satisfy regular expression pattern: [0-9]{12}]'` 341 | 342 | - One of the registries you provided in the `registries` input isn't a sequence of 12 digits 343 | - For users providing only a single registry ID in the `registries` input, if the ID begins with a 0, make sure to enclose it in quotes. GitHub Actions will read an input as a number if all of the characters in the input are digits. So if your registry ID begins with a 0, the 0 will be truncated. See issue [#225](https://github.com/aws-actions/amazon-ecr-login/issues/225). 344 | 345 | ## License Summary 346 | 347 | This code is made available under the MIT license. 348 | 349 | ## Security Disclosures 350 | 351 | 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). 352 | -------------------------------------------------------------------------------- /index.test.js: -------------------------------------------------------------------------------- 1 | const {run, replaceSpecialCharacters} = require('./index.js'); 2 | const core = require('@actions/core'); 3 | const exec = require('@actions/exec'); 4 | 5 | jest.mock('@actions/core'); 6 | jest.mock('@actions/exec'); 7 | 8 | function mockGetInput(requestResponse) { 9 | return function (name, options) { // eslint-disable-line no-unused-vars 10 | return requestResponse[name] 11 | } 12 | } 13 | 14 | const ECR_DEFAULT_INPUTS = { 15 | 'registries': '', 16 | 'skip-logout': '', 17 | 'registry-type': '' 18 | }; 19 | 20 | const ECR_PUBLIC_DEFAULT_INPUTS = { 21 | 'registries': '', 22 | 'skip-logout': '', 23 | 'registry-type': 'public' 24 | }; 25 | 26 | const mockEcrGetAuthToken = jest.fn(); 27 | const mockEcrPublicGetAuthToken = jest.fn(); 28 | jest.mock('aws-sdk', () => { 29 | return { 30 | ECR: jest.fn(() => ({ 31 | getAuthorizationToken: mockEcrGetAuthToken 32 | })), 33 | ECRPUBLIC: jest.fn(() => ({ 34 | getAuthorizationToken: mockEcrPublicGetAuthToken 35 | })) 36 | }; 37 | }); 38 | 39 | describe('Login to ECR', () => { 40 | beforeEach(() => { 41 | jest.clearAllMocks(); 42 | 43 | core.getInput = jest.fn().mockImplementation(mockGetInput(ECR_DEFAULT_INPUTS)); 44 | 45 | mockEcrGetAuthToken.mockImplementation(() => { 46 | return { 47 | promise() { 48 | return Promise.resolve({ 49 | authorizationData: [ 50 | { 51 | authorizationToken: Buffer.from('hello:world').toString('base64'), 52 | proxyEndpoint: 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com' 53 | } 54 | ] 55 | }); 56 | } 57 | }; 58 | }); 59 | 60 | exec.exec.mockReturnValue(0); 61 | }); 62 | 63 | test('gets auth token from ECR and logins the Docker client for the default registry', async () => { 64 | await run(); 65 | 66 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({}); 67 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'registry', '123456789012.dkr.ecr.aws-region-1.amazonaws.com'); 68 | expect(exec.exec).toHaveBeenNthCalledWith(1, 69 | 'docker', 70 | ['login', '-u', 'hello', '-p', 'world', 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'], 71 | expect.anything()); 72 | expect(core.saveState).toHaveBeenNthCalledWith(1, 'registries', '123456789012.dkr.ecr.aws-region-1.amazonaws.com'); 73 | expect(exec.exec).toHaveBeenCalledTimes(1); 74 | expect(core.setOutput).toHaveBeenCalledTimes(3); 75 | expect(core.saveState).toHaveBeenCalledTimes(1); 76 | }); 77 | 78 | test('gets auth token from ECR and logins the Docker client for each provided registry', async () => { 79 | const mockInputs = { 80 | 'registries' : '123456789012,111111111111', 81 | 'skip-logout': '', 82 | 'registry-type': '' 83 | }; 84 | core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs)); 85 | mockEcrGetAuthToken.mockImplementation(() => { 86 | return { 87 | promise() { 88 | return Promise.resolve({ 89 | authorizationData: [ 90 | { 91 | authorizationToken: Buffer.from('hello:world').toString('base64'), 92 | proxyEndpoint: 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com' 93 | }, 94 | { 95 | authorizationToken: Buffer.from('foo:bar').toString('base64'), 96 | proxyEndpoint: 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com' 97 | } 98 | ] 99 | }); 100 | } 101 | }; 102 | }); 103 | 104 | await run(); 105 | 106 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({ 107 | registryIds: ['123456789012','111111111111'] 108 | }); 109 | expect(exec.exec).toHaveBeenNthCalledWith(1, 110 | 'docker', 111 | ['login', '-u', 'hello', '-p', 'world', 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'], 112 | expect.anything()); 113 | expect(exec.exec).toHaveBeenNthCalledWith(2, 114 | 'docker', 115 | ['login', '-u', 'foo', '-p', 'bar', 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com'], 116 | expect.anything()); 117 | expect(core.saveState).toHaveBeenNthCalledWith(1, 'registries', '123456789012.dkr.ecr.aws-region-1.amazonaws.com,111111111111.dkr.ecr.aws-region-1.amazonaws.com'); 118 | expect(exec.exec).toHaveBeenCalledTimes(2); 119 | expect(core.setOutput).toHaveBeenCalledTimes(4); 120 | expect(core.saveState).toHaveBeenCalledTimes(1); 121 | }); 122 | 123 | test('outputs the registry ID if a single registry is provided in the input', async () => { 124 | const mockInputs = { 125 | 'registries' : '111111111111', 126 | 'skip-logout': '', 127 | 'registry-type': '' 128 | }; 129 | core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs)); 130 | mockEcrGetAuthToken.mockImplementation(() => { 131 | return { 132 | promise() { 133 | return Promise.resolve({ 134 | authorizationData: [ 135 | { 136 | authorizationToken: Buffer.from('foo:bar').toString('base64'), 137 | proxyEndpoint: 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com' 138 | } 139 | ] 140 | }); 141 | } 142 | }; 143 | }); 144 | 145 | await run(); 146 | 147 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({ 148 | registryIds: ['111111111111'] 149 | }); 150 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'registry', '111111111111.dkr.ecr.aws-region-1.amazonaws.com'); 151 | expect(exec.exec).toHaveBeenCalledTimes(1); 152 | expect(exec.exec).toHaveBeenNthCalledWith(1, 153 | 'docker', 154 | ['login', '-u', 'foo', '-p', 'bar', 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com'], 155 | expect.anything()); 156 | expect(core.saveState).toHaveBeenCalledWith('registries', '111111111111.dkr.ecr.aws-region-1.amazonaws.com'); 157 | expect(exec.exec).toHaveBeenCalledTimes(1); 158 | expect(core.setOutput).toHaveBeenCalledTimes(3); 159 | expect(core.saveState).toHaveBeenCalledTimes(1); 160 | }); 161 | 162 | test('error is caught by core.setFailed for failed docker login', async () => { 163 | exec.exec.mockReturnValue(1); 164 | 165 | await run(); 166 | 167 | expect(core.setFailed).toHaveBeenCalled(); 168 | expect(core.setOutput).toHaveBeenCalledWith('registry', '123456789012.dkr.ecr.aws-region-1.amazonaws.com'); 169 | expect(core.saveState).toHaveBeenCalledTimes(0); 170 | }); 171 | 172 | test('logged-in registries are saved as state even if the action fails', async () => { 173 | const mockInputs = { 174 | 'registries' : '123456789012,111111111111', 175 | 'skip-logout': '', 176 | 'registry-type': '' 177 | }; 178 | core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs)); 179 | mockEcrGetAuthToken.mockImplementation(() => { 180 | return { 181 | promise() { 182 | return Promise.resolve({ 183 | authorizationData: [ 184 | { 185 | authorizationToken: Buffer.from('hello:world').toString('base64'), 186 | proxyEndpoint: 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com' 187 | }, 188 | { 189 | authorizationToken: Buffer.from('foo:bar').toString('base64'), 190 | proxyEndpoint: 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com' 191 | } 192 | ] 193 | }); 194 | } 195 | }; 196 | }); 197 | exec.exec.mockImplementation((commandLine, args, options) => { 198 | options.listeners.stdout('Hello World '); 199 | options.listeners.stdout('on stdout\n'); 200 | options.listeners.stderr('Some fancy error '); 201 | options.listeners.stderr('from docker login stderr'); 202 | return(1); 203 | }).mockReturnValueOnce(0); 204 | 205 | await run(); 206 | 207 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({ 208 | registryIds: ['123456789012','111111111111'] 209 | }); 210 | expect(exec.exec).toHaveBeenNthCalledWith(1, 211 | 'docker', 212 | ['login', '-u', 'hello', '-p', 'world', 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'], 213 | expect.anything()); 214 | expect(exec.exec).toHaveBeenNthCalledWith(2, 215 | 'docker', 216 | ['login', '-u', 'foo', '-p', 'bar', 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com'], 217 | expect.anything()); 218 | expect(core.saveState).toHaveBeenCalledWith('registries', '123456789012.dkr.ecr.aws-region-1.amazonaws.com'); 219 | expect(core.setFailed).toHaveBeenCalledWith('Could not login to registry 111111111111.dkr.ecr.aws-region-1.amazonaws.com: Some fancy error from docker login stderr'); 220 | expect(exec.exec).toHaveBeenCalledTimes(2); 221 | expect(core.setOutput).toHaveBeenCalledTimes(2); 222 | expect(core.saveState).toHaveBeenCalledTimes(1); 223 | }); 224 | 225 | test(`throws error when getAuthorizationToken does return an empty authorization data`, async () => { 226 | mockEcrGetAuthToken.mockImplementation(() => { 227 | return { 228 | promise() { 229 | return Promise.resolve({ 230 | authorizationData: [] 231 | }); 232 | } 233 | }; 234 | }); 235 | 236 | await run(); 237 | 238 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({}); 239 | expect(core.setFailed).toHaveBeenCalledWith('Amazon ECR authorization token does not contain any authorization data'); 240 | expect(exec.exec).toHaveBeenCalledTimes(0); 241 | expect(core.setOutput).toHaveBeenCalledTimes(0); 242 | expect(core.saveState).toHaveBeenCalledTimes(0); 243 | expect(core.setFailed).toHaveBeenCalledTimes(1); 244 | }); 245 | 246 | test(`throws error when getAuthorizationToken does not contain authorization data`, async () => { 247 | mockEcrGetAuthToken.mockImplementation(() => { 248 | return { 249 | promise() { 250 | return Promise.resolve({ 251 | foo: 'bar' 252 | }); 253 | } 254 | }; 255 | }); 256 | 257 | await run(); 258 | 259 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({}); 260 | expect(core.setFailed).toHaveBeenCalledWith('Amazon ECR authorization token is invalid'); 261 | expect(exec.exec).toHaveBeenCalledTimes(0); 262 | expect(core.setOutput).toHaveBeenCalledTimes(0); 263 | expect(core.saveState).toHaveBeenCalledTimes(0); 264 | expect(core.setFailed).toHaveBeenCalledTimes(1); 265 | }); 266 | 267 | test(`throws error when getAuthorizationToken does not return data`, async () => { 268 | mockEcrGetAuthToken.mockImplementation(() => { 269 | return { 270 | promise() { 271 | // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ECR.html#getAuthorizationToken-property 272 | // data (Object) — the de-serialized data returned from the request. Set to null if a request error occurs. 273 | return Promise.resolve(null); 274 | } 275 | }; 276 | }); 277 | 278 | await run(); 279 | 280 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({}); 281 | expect(core.setFailed).toHaveBeenCalledWith('Amazon ECR authorization token returned no data'); 282 | expect(exec.exec).toHaveBeenCalledTimes(0); 283 | expect(core.setOutput).toHaveBeenCalledTimes(0); 284 | expect(core.saveState).toHaveBeenCalledTimes(0); 285 | expect(core.setFailed).toHaveBeenCalledTimes(1); 286 | }); 287 | 288 | test('error is caught by core.setFailed for ECR call', async () => { 289 | mockEcrGetAuthToken.mockImplementation(() => { 290 | throw new Error(); 291 | }); 292 | 293 | await run(); 294 | 295 | expect(core.setOutput).toHaveBeenCalledTimes(0); 296 | expect(core.saveState).toHaveBeenCalledTimes(0); 297 | expect(core.setFailed).toHaveBeenCalled(); 298 | }); 299 | 300 | test('skips logout when specified and logging into default registry', async () => { 301 | const mockInputs = { 302 | 'registries' : '', 303 | 'skip-logout': 'true', 304 | 'registry-type': '' 305 | }; 306 | core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs)); 307 | 308 | await run(); 309 | 310 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({}); 311 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'registry', '123456789012.dkr.ecr.aws-region-1.amazonaws.com'); 312 | expect(exec.exec).toHaveBeenNthCalledWith(1, 313 | 'docker', 314 | ['login', '-u', 'hello', '-p', 'world', 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com'], 315 | expect.anything()); 316 | expect(core.saveState).toHaveBeenCalledTimes(0); 317 | }); 318 | 319 | test('skips logout when specified and logging into multiple registries', async () => { 320 | const mockInputs = { 321 | 'registries' : '123456789012,111111111111', 322 | 'skip-logout': 'true', 323 | 'registry-type': '' 324 | }; 325 | core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs)); 326 | mockEcrGetAuthToken.mockImplementation(() => { 327 | return { 328 | promise() { 329 | return Promise.resolve({ 330 | authorizationData: [ 331 | { 332 | authorizationToken: Buffer.from('hello:world').toString('base64'), 333 | proxyEndpoint: 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com' 334 | }, 335 | { 336 | authorizationToken: Buffer.from('foo:bar').toString('base64'), 337 | proxyEndpoint: 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com' 338 | } 339 | ] 340 | }); 341 | } 342 | }; 343 | }); 344 | 345 | await run(); 346 | 347 | expect(mockEcrGetAuthToken).toHaveBeenCalledWith({ 348 | registryIds: ['123456789012','111111111111'] 349 | }); 350 | expect(exec.exec).toHaveBeenCalledTimes(2); 351 | expect(core.saveState).toHaveBeenCalledTimes(0); 352 | }); 353 | 354 | test('replaces special characters', () => { 355 | expect(replaceSpecialCharacters('111111111111.dkr.ecr.aws-region-1.amazonaws.com')).toBe('111111111111_dkr_ecr_aws_region_1_amazonaws_com') 356 | expect(replaceSpecialCharacters('229236603350.dkr.ecr.us-east-1.amazonaws.com')).toBe('229236603350_dkr_ecr_us_east_1_amazonaws_com') 357 | }); 358 | 359 | test('sets the Actions outputs to the docker credentials', async () => { 360 | const mockInputs = { 361 | 'registries' : '123456789012,111111111111', 362 | 'skip-logout': 'true', 363 | 'registry-type': '' 364 | }; 365 | core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs)); 366 | mockEcrGetAuthToken.mockImplementation(() => { 367 | return { 368 | promise() { 369 | return Promise.resolve({ 370 | authorizationData: [ 371 | { 372 | authorizationToken: Buffer.from('hello:world').toString('base64'), 373 | proxyEndpoint: 'https://123456789012.dkr.ecr.aws-region-1.amazonaws.com' 374 | }, 375 | { 376 | authorizationToken: Buffer.from('foo:bar').toString('base64'), 377 | proxyEndpoint: 'https://111111111111.dkr.ecr.aws-region-1.amazonaws.com' 378 | } 379 | ] 380 | }); 381 | } 382 | }; 383 | }); 384 | 385 | await run(); 386 | 387 | expect(core.setOutput).toHaveBeenCalledTimes(4); 388 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'docker_username_123456789012_dkr_ecr_aws_region_1_amazonaws_com', 'hello'); 389 | expect(core.setOutput).toHaveBeenNthCalledWith(2, 'docker_password_123456789012_dkr_ecr_aws_region_1_amazonaws_com', 'world'); 390 | expect(core.setOutput).toHaveBeenNthCalledWith(3, 'docker_username_111111111111_dkr_ecr_aws_region_1_amazonaws_com', 'foo'); 391 | expect(core.setOutput).toHaveBeenNthCalledWith(4, 'docker_password_111111111111_dkr_ecr_aws_region_1_amazonaws_com', 'bar'); 392 | }); 393 | }); 394 | 395 | describe('Login to ECR Public', () => { 396 | beforeEach(() => { 397 | jest.clearAllMocks(); 398 | 399 | core.getInput = jest.fn().mockImplementation(mockGetInput(ECR_PUBLIC_DEFAULT_INPUTS)); 400 | 401 | mockEcrPublicGetAuthToken.mockImplementation(() => { 402 | return { 403 | promise() { 404 | return Promise.resolve({ 405 | authorizationData: { 406 | authorizationToken: Buffer.from('hello:world').toString('base64') 407 | } 408 | }); 409 | } 410 | }; 411 | }); 412 | 413 | exec.exec.mockReturnValue(0); 414 | }); 415 | 416 | test('gets auth token from ECR Public and logins the Docker client for the default registry', async () => { 417 | await run(); 418 | 419 | expect(mockEcrPublicGetAuthToken).toHaveBeenCalledWith({}); 420 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'registry', 'public.ecr.aws'); 421 | expect(exec.exec).toHaveBeenNthCalledWith(1, 422 | 'docker', 423 | ['login', '-u', 'hello', '-p', 'world', 'public.ecr.aws'], 424 | expect.anything()); 425 | expect(core.saveState).toHaveBeenNthCalledWith(1, 'registries', 'public.ecr.aws'); 426 | expect(exec.exec).toHaveBeenCalledTimes(1); 427 | expect(core.setOutput).toHaveBeenCalledTimes(3); 428 | expect(core.saveState).toHaveBeenCalledTimes(1); 429 | }); 430 | 431 | test('error is caught by core.setFailed for invalid registry-type input', async () => { 432 | const mockInputs = { 433 | 'registries' : '', 434 | 'skip-logout': '', 435 | 'registry-type': 'invalid' 436 | }; 437 | core.getInput = jest.fn().mockImplementation(mockGetInput(mockInputs)); 438 | 439 | await run(); 440 | 441 | expect(core.setFailed).toHaveBeenCalledWith(`Invalid input for 'registry-type', possible options are [private, public]`); 442 | expect(core.saveState).toHaveBeenCalledTimes(0); 443 | }); 444 | 445 | test('outputs the registry URI', async () => { 446 | await run(); 447 | 448 | expect(mockEcrPublicGetAuthToken).toHaveBeenCalledWith({}) 449 | expect(core.setOutput).toHaveBeenNthCalledWith(1, 'registry', 'public.ecr.aws'); 450 | expect(exec.exec).toHaveBeenCalledTimes(1); 451 | expect(exec.exec).toHaveBeenNthCalledWith(1, 452 | 'docker', 453 | ['login', '-u', 'hello', '-p', 'world', 'public.ecr.aws'], 454 | expect.anything()); 455 | expect(core.saveState).toHaveBeenCalledWith('registries', 'public.ecr.aws'); 456 | expect(exec.exec).toHaveBeenCalledTimes(1); 457 | expect(core.setOutput).toHaveBeenCalledTimes(3); 458 | expect(core.saveState).toHaveBeenCalledTimes(1); 459 | }); 460 | 461 | test(`throws error when getAuthorizationToken does return an empty authorization data`, async () => { 462 | mockEcrPublicGetAuthToken.mockImplementation(() => { 463 | return { 464 | promise() { 465 | return Promise.resolve({ 466 | authorizationData: {} 467 | }); 468 | } 469 | }; 470 | }); 471 | 472 | await run(); 473 | 474 | expect(mockEcrPublicGetAuthToken).toHaveBeenCalledWith({}); 475 | expect(core.setFailed).toHaveBeenCalledWith('Amazon ECR Public authorization token does not contain any authorization data'); 476 | expect(exec.exec).toHaveBeenCalledTimes(0); 477 | expect(core.setOutput).toHaveBeenCalledTimes(0); 478 | expect(core.saveState).toHaveBeenCalledTimes(0); 479 | expect(core.setFailed).toHaveBeenCalledTimes(1); 480 | }); 481 | 482 | test(`throws error when getAuthorizationToken does not contain authorization data`, async () => { 483 | mockEcrPublicGetAuthToken.mockImplementation(() => { 484 | return { 485 | promise() { 486 | return Promise.resolve({ 487 | hello: 'world' 488 | }); 489 | } 490 | }; 491 | }); 492 | 493 | await run(); 494 | 495 | expect(mockEcrPublicGetAuthToken).toHaveBeenCalledWith({}); 496 | expect(core.setFailed).toHaveBeenCalledWith('Amazon ECR Public authorization token is invalid'); 497 | expect(exec.exec).toHaveBeenCalledTimes(0); 498 | expect(core.setOutput).toHaveBeenCalledTimes(0); 499 | expect(core.saveState).toHaveBeenCalledTimes(0); 500 | expect(core.setFailed).toHaveBeenCalledTimes(1); 501 | }); 502 | 503 | test(`throws error when getAuthorizationToken does not return data`, async () => { 504 | mockEcrPublicGetAuthToken.mockImplementation(() => { 505 | return { 506 | promise() { 507 | // https://docs.aws.amazon.com/AWSJavaScriptSDK/latest/AWS/ECR.html#getAuthorizationToken-property 508 | // data (Object) — the de-serialized data returned from the request. Set to null if a request error occurs. 509 | return Promise.resolve(null); 510 | } 511 | }; 512 | }); 513 | 514 | await run(); 515 | 516 | expect(mockEcrPublicGetAuthToken).toHaveBeenCalledWith({}); 517 | expect(core.setFailed).toHaveBeenCalledWith('Amazon ECR Public authorization token returned no data'); 518 | expect(exec.exec).toHaveBeenCalledTimes(0); 519 | expect(core.setOutput).toHaveBeenCalledTimes(0); 520 | expect(core.saveState).toHaveBeenCalledTimes(0); 521 | expect(core.setFailed).toHaveBeenCalledTimes(1); 522 | }); 523 | 524 | test('error is caught by core.setFailed for ECR call', async () => { 525 | mockEcrPublicGetAuthToken.mockImplementation(() => { 526 | throw new Error(); 527 | }); 528 | 529 | await run(); 530 | 531 | expect(core.setOutput).toHaveBeenCalledTimes(0); 532 | expect(core.saveState).toHaveBeenCalledTimes(0); 533 | expect(core.setFailed).toHaveBeenCalled(); 534 | }); 535 | 536 | test('sets the Actions outputs to the docker credentials', async () => { 537 | await run(); 538 | 539 | expect(core.setOutput).toHaveBeenCalledTimes(3); 540 | expect(core.setOutput).toHaveBeenNthCalledWith(2, 'docker_username_public_ecr_aws', 'hello'); 541 | expect(core.setOutput).toHaveBeenNthCalledWith(3, 'docker_password_public_ecr_aws', 'world'); 542 | }); 543 | }); 544 | --------------------------------------------------------------------------------