├── .dockerignore ├── .editorconfig ├── .eslintignore ├── .eslintrc.json ├── .gitattributes ├── .github ├── CODEOWNERS ├── CONTRIBUTING.md ├── FUNDING.yml ├── ISSUE_TEMPLATE │ ├── bug.yml │ ├── config.yml │ └── feature.yml ├── SUPPORT.md ├── dependabot.yml ├── ghaction-import-gpg.png ├── labels.yml └── workflows │ ├── ci.yml │ ├── labels.yml │ ├── test.yml │ └── validate.yml ├── .gitignore ├── .prettierignore ├── .prettierrc.json ├── .yarn └── plugins │ └── @yarnpkg │ └── plugin-interactive-tools.cjs ├── .yarnrc.yml ├── LICENSE ├── README.md ├── __tests__ ├── fixtures │ ├── gpg.conf │ ├── test-key-base64.pgp │ ├── test-key-gpg-output.txt │ ├── test-key.pass │ ├── test-key.pgp │ ├── test-subkey-base64.pgp │ ├── test-subkey-gpg-output.txt │ ├── test-subkey.pass │ └── test-subkey.pgp ├── gpg.test.ts └── openpgp.test.ts ├── action.yml ├── codecov.yml ├── dev.Dockerfile ├── dist ├── index.js ├── index.js.map ├── licenses.txt └── sourcemap-register.js ├── docker-bake.hcl ├── jest.config.ts ├── package.json ├── src ├── addressparser.d.ts ├── context.ts ├── git.ts ├── gpg.ts ├── main.ts ├── openpgp.ts └── state-helper.ts ├── tsconfig.json └── yarn.lock /.dockerignore: -------------------------------------------------------------------------------- 1 | /coverage 2 | 3 | # Dependency directories 4 | node_modules/ 5 | jspm_packages/ 6 | 7 | # yarn v2 8 | .yarn/cache 9 | .yarn/unplugged 10 | .yarn/build-state.yml 11 | .yarn/install-state.gz 12 | .pnp.* 13 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # This file is for unifying the coding style for different editors and IDEs. 2 | # More information at http://editorconfig.org 3 | 4 | root = true 5 | 6 | [*] 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | 14 | [*.md] 15 | trim_trailing_whitespace = false 16 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | /dist/** 2 | /coverage/** 3 | /node_modules/** 4 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended", 11 | "plugin:jest/recommended", 12 | "plugin:prettier/recommended" 13 | ], 14 | "parser": "@typescript-eslint/parser", 15 | "parserOptions": { 16 | "ecmaVersion": "latest", 17 | "sourceType": "module" 18 | }, 19 | "plugins": [ 20 | "@typescript-eslint", 21 | "jest", 22 | "prettier" 23 | ] 24 | } 25 | -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | /.yarn/releases/** binary 2 | /.yarn/plugins/** binary 3 | /dist/** linguist-generated=true 4 | /lib/** linguist-generated=true 5 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @crazy-max 2 | -------------------------------------------------------------------------------- /.github/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Contributing 2 | 3 | Hi there! We're thrilled that you'd like to contribute to this project. Your help is essential for keeping it great. 4 | 5 | Contributions to this project are [released](https://docs.github.com/en/github/site-policy/github-terms-of-service#6-contributions-under-repository-license) 6 | to the public under the [project's open source license](LICENSE). 7 | 8 | ## Submitting a pull request 9 | 10 | 1. [Fork](https://github.com/crazy-max/ghaction-import-gpg/fork) and clone the repository 11 | 2. Configure and install the dependencies: `yarn install` 12 | 3. Create a new branch: `git checkout -b my-branch-name` 13 | 4. Make your changes 14 | 5. Make sure the tests pass: `docker buildx bake test` 15 | 6. Format code and build javascript artifacts: `docker buildx bake pre-checkin` 16 | 7. Validate all code has correctly formatted and built: `docker buildx bake validate` 17 | 8. Push to your fork and [submit a pull request](https://github.com/crazy-max/ghaction-import-gpg/compare) 18 | 9. Pat your self on the back and wait for your pull request to be reviewed and merged. 19 | 20 | Here are a few things you can do that will increase the likelihood of your pull request being accepted: 21 | 22 | - Write tests. 23 | - Make sure the `README.md` and any other relevant **documentation are kept up-to-date**. 24 | - We try to follow [SemVer v2.0.0](https://semver.org/). Randomly breaking public APIs is not an option. 25 | - Keep your change as focused as possible. If there are multiple changes you would like to make that are not dependent upon each other, consider submitting them as **separate pull requests**. 26 | - Write a [good commit message](http://tbaggery.com/2008/04/19/a-note-about-git-commit-messages.html). 27 | 28 | ## Resources 29 | 30 | - [How to Contribute to Open Source](https://opensource.guide/how-to-contribute/) 31 | - [Using Pull Requests](https://docs.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-requests) 32 | - [GitHub Help](https://docs.github.com/en) 33 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: crazy-max 2 | custom: https://www.paypal.me/crazyws 3 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/bug.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema 2 | name: Bug Report 3 | description: Report a bug 4 | labels: 5 | - kind/bug 6 | - status/triage 7 | 8 | body: 9 | - type: checkboxes 10 | attributes: 11 | label: Support guidelines 12 | description: Please read the support guidelines before proceeding. 13 | options: 14 | - label: I've read the [support guidelines](https://github.com/crazy-max/ghaction-import-gpg/blob/master/.github/SUPPORT.md) 15 | required: true 16 | 17 | - type: checkboxes 18 | attributes: 19 | label: I've found a bug and checked that ... 20 | description: | 21 | Make sure that your request fulfills all of the following requirements. If one requirement cannot be satisfied, explain in detail why. 22 | options: 23 | - label: ... the documentation does not mention anything about my problem 24 | - label: ... there are no open or closed issues that are related to my problem 25 | 26 | - type: textarea 27 | attributes: 28 | label: Description 29 | description: | 30 | Please provide a brief description of the bug in 1-2 sentences. 31 | validations: 32 | required: true 33 | 34 | - type: textarea 35 | attributes: 36 | label: Expected behaviour 37 | description: | 38 | Please describe precisely what you'd expect to happen. 39 | validations: 40 | required: true 41 | 42 | - type: textarea 43 | attributes: 44 | label: Actual behaviour 45 | description: | 46 | Please describe precisely what is actually happening. 47 | validations: 48 | required: true 49 | 50 | - type: textarea 51 | attributes: 52 | label: Steps to reproduce 53 | description: | 54 | Please describe the steps to reproduce the bug. 55 | placeholder: | 56 | 1. ... 57 | 2. ... 58 | 3. ... 59 | validations: 60 | required: true 61 | 62 | - type: input 63 | attributes: 64 | label: Repository URL 65 | description: > 66 | Enter the URL of the repository where you are experiencing the 67 | issue. If your repository is private, provide a link to a minimal 68 | repository that reproduces the issue. 69 | validations: 70 | required: true 71 | 72 | - type: input 73 | attributes: 74 | label: Workflow run URL 75 | description: > 76 | Enter the URL of the GitHub Action workflow run that fails (e.g. 77 | `https://github.com///actions/runs/`) 78 | 79 | - type: textarea 80 | attributes: 81 | label: YAML workflow 82 | description: | 83 | Provide the YAML of the workflow that's causing the issue. 84 | Make sure to remove any sensitive information. 85 | render: yaml 86 | validations: 87 | required: true 88 | 89 | - type: textarea 90 | attributes: 91 | label: Workflow logs 92 | description: > 93 | [Attach](https://docs.github.com/en/get-started/writing-on-github/working-with-advanced-formatting/attaching-files) 94 | the [log file of your workflow run](https://docs.github.com/en/actions/managing-workflow-runs/using-workflow-run-logs#downloading-logs) 95 | and make sure to remove any sensitive information. 96 | 97 | - type: textarea 98 | attributes: 99 | label: Additional info 100 | description: | 101 | Please provide any additional information that seem useful. 102 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/config.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/configuring-issue-templates-for-your-repository#configuring-the-template-chooser 2 | blank_issues_enabled: true 3 | contact_links: 4 | - name: Questions and Discussions 5 | url: https://github.com/crazy-max/ghaction-import-gpg/discussions/new 6 | about: Use Github Discussions to ask questions and/or open discussion topics. 7 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/communities/using-templates-to-encourage-useful-issues-and-pull-requests/syntax-for-githubs-form-schema 2 | name: Feature request 3 | description: Missing functionality? Come tell us about it! 4 | labels: 5 | - kind/enhancement 6 | - status/triage 7 | 8 | body: 9 | - type: textarea 10 | id: description 11 | attributes: 12 | label: Description 13 | description: What is the feature you want to see? 14 | validations: 15 | required: true 16 | -------------------------------------------------------------------------------- /.github/SUPPORT.md: -------------------------------------------------------------------------------- 1 | # Support [![](https://isitmaintained.com/badge/resolution/crazy-max/ghaction-import-gpg.svg)](https://isitmaintained.com/project/crazy-max/ghaction-import-gpg) 2 | 3 | ## Reporting an issue 4 | 5 | Please do a search in [open issues](https://github.com/crazy-max/ghaction-import-gpg/issues?utf8=%E2%9C%93&q=) to see if the issue or feature request has already been filed. 6 | 7 | If you find your issue already exists, make relevant comments and add your [reaction](https://github.com/blog/2119-add-reactions-to-pull-requests-issues-and-comments). Use a reaction in place of a "+1" comment. 8 | 9 | :+1: - upvote 10 | 11 | :-1: - downvote 12 | 13 | If you cannot find an existing issue that describes your bug or feature, submit an issue using the guidelines below. 14 | 15 | ## Writing good bug reports and feature requests 16 | 17 | File a single issue per problem and feature request. 18 | 19 | * Do not enumerate multiple bugs or feature requests in the same issue. 20 | * Do not add your issue as a comment to an existing issue unless it's for the identical input. Many issues look similar, but have different causes. 21 | 22 | The more information you can provide, the more likely someone will be successful reproducing the issue and finding a fix. 23 | 24 | You are now ready to [create a new issue](https://github.com/crazy-max/ghaction-import-gpg/issues/new/choose)! 25 | 26 | ## Closure policy 27 | 28 | * Issues that don't have the information requested above (when applicable) will be closed immediately and the poster directed to the support guidelines. 29 | * Issues that go a week without a response from original poster are subject to closure at my discretion. 30 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "github-actions" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | labels: 8 | - "kind/dependencies" 9 | - "bot" 10 | - package-ecosystem: "npm" 11 | directory: "/" 12 | schedule: 13 | interval: "daily" 14 | versioning-strategy: "increase" 15 | allow: 16 | - dependency-type: "production" 17 | labels: 18 | - "kind/dependencies" 19 | - "bot" 20 | -------------------------------------------------------------------------------- /.github/ghaction-import-gpg.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/crazy-max/ghaction-import-gpg/e89d40939c28e39f97cf32126055eeae86ba74ec/.github/ghaction-import-gpg.png -------------------------------------------------------------------------------- /.github/labels.yml: -------------------------------------------------------------------------------- 1 | ## more info https://github.com/crazy-max/ghaction-github-labeler 2 | - 3 | name: "bot" 4 | color: "69cde9" 5 | description: "" 6 | - 7 | name: "good first issue" 8 | color: "7057ff" 9 | description: "" 10 | - 11 | name: "help wanted" 12 | color: "4caf50" 13 | description: "" 14 | - 15 | name: "area/ci" 16 | color: "ed9ca9" 17 | description: "" 18 | - 19 | name: "kind/bug" 20 | color: "b60205" 21 | description: "" 22 | - 23 | name: "kind/dependencies" 24 | color: "0366d6" 25 | description: "" 26 | - 27 | name: "kind/docs" 28 | color: "c5def5" 29 | description: "" 30 | - 31 | name: "kind/duplicate" 32 | color: "cccccc" 33 | description: "" 34 | - 35 | name: "kind/enhancement" 36 | color: "0054ca" 37 | description: "" 38 | - 39 | name: "kind/invalid" 40 | color: "e6e6e6" 41 | description: "" 42 | - 43 | name: "kind/upstream" 44 | color: "fbca04" 45 | description: "" 46 | - 47 | name: "kind/wontfix" 48 | color: "ffffff" 49 | description: "" 50 | - 51 | name: "status/automerge" 52 | color: "8f4fbc" 53 | description: "" 54 | - 55 | name: "status/needs-investigation" 56 | color: "e6625b" 57 | description: "" 58 | - 59 | name: "status/needs-more-info" 60 | color: "795548" 61 | description: "" 62 | - 63 | name: "status/stale" 64 | color: "237da0" 65 | description: "" 66 | - 67 | name: "status/triage" 68 | color: "dde4b7" 69 | description: "" 70 | -------------------------------------------------------------------------------- /.github/workflows/ci.yml: -------------------------------------------------------------------------------- 1 | name: ci 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 8 | permissions: 9 | contents: read 10 | 11 | on: 12 | schedule: 13 | - cron: '0 10 * * *' 14 | push: 15 | branches: 16 | - 'master' 17 | - 'releases/v*' 18 | tags: 19 | - 'v*' 20 | pull_request: 21 | branches: 22 | - 'master' 23 | - 'releases/v*' 24 | 25 | jobs: 26 | gpg: 27 | runs-on: ubuntu-latest 28 | steps: 29 | - 30 | name: GPG conf 31 | run: | 32 | cat ~/.gnupg/gpg.conf || true 33 | 34 | armored: 35 | runs-on: ${{ matrix.os }} 36 | strategy: 37 | fail-fast: false 38 | matrix: 39 | key: 40 | - test-key 41 | - test-subkey 42 | global: 43 | - false 44 | - true 45 | os: 46 | - ubuntu-latest 47 | - macos-latest 48 | - windows-latest 49 | include: 50 | - key: test-subkey 51 | fingerprint: C17D11ADF199F12A30A0910F1F80449BE0B08CB8 52 | steps: 53 | - 54 | name: Checkout 55 | uses: actions/checkout@v4 56 | - 57 | name: GPG conf 58 | uses: actions/github-script@v7 59 | with: 60 | script: | 61 | const fs = require('fs'); 62 | const gnupgfolder = `${require('os').homedir()}/.gnupg`; 63 | if (!fs.existsSync(gnupgfolder)){ 64 | fs.mkdirSync(gnupgfolder); 65 | } 66 | fs.chmodSync(gnupgfolder, '0700'); 67 | fs.copyFile('__tests__/fixtures/gpg.conf', `${gnupgfolder}/gpg.conf`, (err) => { 68 | if (err) throw err; 69 | }); 70 | - 71 | name: Get test key and passphrase 72 | uses: actions/github-script@v7 73 | id: test 74 | with: 75 | script: | 76 | const fs = require('fs'); 77 | core.setOutput('pgp', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}.pgp', {encoding: 'utf8'})); 78 | core.setOutput('passphrase', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}.pass', {encoding: 'utf8'})); 79 | - 80 | name: Import GPG 81 | uses: ./ 82 | with: 83 | gpg_private_key: ${{ steps.test.outputs.pgp }} 84 | passphrase: ${{ steps.test.outputs.passphrase }} 85 | trust_level: 5 86 | git_config_global: ${{ matrix.global }} 87 | git_user_signingkey: true 88 | git_commit_gpgsign: true 89 | git_tag_gpgsign: true 90 | git_push_gpgsign: if-asked 91 | fingerprint: ${{ matrix.fingerprint }} 92 | - 93 | name: List keys 94 | run: | 95 | gpg -K 96 | shell: bash 97 | 98 | base64: 99 | runs-on: ${{ matrix.os }} 100 | strategy: 101 | fail-fast: false 102 | matrix: 103 | key: 104 | - test-key 105 | - test-subkey 106 | os: 107 | - ubuntu-latest 108 | - macos-latest 109 | - windows-latest 110 | include: 111 | - key: test-subkey 112 | fingerprint: C17D11ADF199F12A30A0910F1F80449BE0B08CB8 113 | steps: 114 | - 115 | name: Checkout 116 | uses: actions/checkout@v4 117 | - 118 | name: Get test key and passphrase 119 | uses: actions/github-script@v7 120 | id: test 121 | with: 122 | script: | 123 | const fs = require('fs'); 124 | core.setOutput('pgp-base64', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}-base64.pgp', {encoding: 'utf8'})); 125 | core.setOutput('passphrase', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}.pass', {encoding: 'utf8'})); 126 | - 127 | name: Import GPG 128 | uses: ./ 129 | with: 130 | gpg_private_key: ${{ steps.test.outputs.pgp-base64 }} 131 | passphrase: ${{ steps.test.outputs.passphrase }} 132 | git_user_signingkey: true 133 | git_commit_gpgsign: true 134 | git_tag_gpgsign: true 135 | git_push_gpgsign: if-asked 136 | fingerprint: ${{ matrix.fingerprint }} 137 | 138 | trust: 139 | runs-on: ${{ matrix.os }} 140 | strategy: 141 | fail-fast: false 142 | matrix: 143 | key: 144 | - test-key 145 | level: 146 | - '' 147 | - 5 148 | - 4 149 | - 3 150 | - 2 151 | - 1 152 | os: 153 | - ubuntu-latest 154 | - macos-latest 155 | - windows-latest 156 | steps: 157 | - 158 | name: Checkout 159 | uses: actions/checkout@v4 160 | - 161 | name: GPG conf 162 | uses: actions/github-script@v7 163 | with: 164 | script: | 165 | const fs = require('fs'); 166 | const gnupgfolder = `${require('os').homedir()}/.gnupg`; 167 | if (!fs.existsSync(gnupgfolder)){ 168 | fs.mkdirSync(gnupgfolder); 169 | } 170 | fs.chmodSync(gnupgfolder, '0700'); 171 | fs.copyFile('__tests__/fixtures/gpg.conf', `${gnupgfolder}/gpg.conf`, (err) => { 172 | if (err) throw err; 173 | }); 174 | - 175 | name: Get test key and passphrase 176 | uses: actions/github-script@v7 177 | id: test 178 | with: 179 | script: | 180 | const fs = require('fs'); 181 | core.setOutput('pgp', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}.pgp', {encoding: 'utf8'})); 182 | core.setOutput('passphrase', fs.readFileSync('__tests__/fixtures/${{ matrix.key }}.pass', {encoding: 'utf8'})); 183 | - 184 | name: Import GPG 185 | id: import_gpg 186 | uses: ./ 187 | with: 188 | gpg_private_key: ${{ steps.test.outputs.pgp }} 189 | passphrase: ${{ steps.test.outputs.passphrase }} 190 | trust_level: ${{ matrix.level }} 191 | - 192 | name: List trust values 193 | run: | 194 | gpg --export-ownertrust 195 | shell: bash 196 | -------------------------------------------------------------------------------- /.github/workflows/labels.yml: -------------------------------------------------------------------------------- 1 | name: labels 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 8 | permissions: 9 | contents: read 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'master' 15 | paths: 16 | - '.github/labels.yml' 17 | - '.github/workflows/labels.yml' 18 | pull_request: 19 | paths: 20 | - '.github/labels.yml' 21 | - '.github/workflows/labels.yml' 22 | 23 | jobs: 24 | labeler: 25 | runs-on: ubuntu-latest 26 | permissions: 27 | # same as global permissions 28 | contents: read 29 | # required to update labels 30 | issues: write 31 | steps: 32 | - 33 | name: Checkout 34 | uses: actions/checkout@v4 35 | - 36 | name: Run Labeler 37 | uses: crazy-max/ghaction-github-labeler@v5 38 | with: 39 | dry-run: ${{ github.event_name == 'pull_request' }} 40 | -------------------------------------------------------------------------------- /.github/workflows/test.yml: -------------------------------------------------------------------------------- 1 | name: test 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 8 | permissions: 9 | contents: read 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'master' 15 | - 'releases/v*' 16 | pull_request: 17 | 18 | jobs: 19 | test: 20 | runs-on: ubuntu-latest 21 | steps: 22 | - 23 | name: Checkout 24 | uses: actions/checkout@v4 25 | - 26 | name: Test 27 | uses: docker/bake-action@v6 28 | with: 29 | source: . 30 | targets: test 31 | - 32 | name: Upload coverage 33 | uses: codecov/codecov-action@v5 34 | with: 35 | files: ./coverage/clover.xml 36 | token: ${{ secrets.CODECOV_TOKEN }} 37 | -------------------------------------------------------------------------------- /.github/workflows/validate.yml: -------------------------------------------------------------------------------- 1 | name: validate 2 | 3 | concurrency: 4 | group: ${{ github.workflow }}-${{ github.ref }} 5 | cancel-in-progress: true 6 | 7 | # https://docs.github.com/en/actions/using-workflows/workflow-syntax-for-github-actions#permissions 8 | permissions: 9 | contents: read 10 | 11 | on: 12 | push: 13 | branches: 14 | - 'master' 15 | - 'releases/v*' 16 | pull_request: 17 | 18 | jobs: 19 | prepare: 20 | runs-on: ubuntu-latest 21 | outputs: 22 | targets: ${{ steps.generate.outputs.targets }} 23 | steps: 24 | - 25 | name: Checkout 26 | uses: actions/checkout@v4 27 | - 28 | name: List targets 29 | id: generate 30 | uses: docker/bake-action/subaction/list-targets@v6 31 | with: 32 | target: validate 33 | 34 | validate: 35 | runs-on: ubuntu-latest 36 | needs: 37 | - prepare 38 | strategy: 39 | fail-fast: false 40 | matrix: 41 | target: ${{ fromJson(needs.prepare.outputs.targets) }} 42 | steps: 43 | - 44 | name: Validate 45 | uses: docker/bake-action@v6 46 | with: 47 | targets: ${{ matrix.target }} 48 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # https://raw.githubusercontent.com/github/gitignore/main/Node.gitignore 2 | 3 | # Logs 4 | logs 5 | *.log 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | lerna-debug.log* 10 | .pnpm-debug.log* 11 | 12 | # Diagnostic reports (https://nodejs.org/api/report.html) 13 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 14 | 15 | # Runtime data 16 | pids 17 | *.pid 18 | *.seed 19 | *.pid.lock 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # Dependency directories 26 | node_modules/ 27 | jspm_packages/ 28 | 29 | # TypeScript cache 30 | *.tsbuildinfo 31 | 32 | # Optional npm cache directory 33 | .npm 34 | 35 | # Optional eslint cache 36 | .eslintcache 37 | 38 | # Yarn Integrity file 39 | .yarn-integrity 40 | 41 | # dotenv environment variable files 42 | .env 43 | .env.development.local 44 | .env.test.local 45 | .env.production.local 46 | .env.local 47 | 48 | # yarn v2 49 | .yarn/cache 50 | .yarn/unplugged 51 | .yarn/build-state.yml 52 | .yarn/install-state.gz 53 | .pnp.* 54 | -------------------------------------------------------------------------------- /.prettierignore: -------------------------------------------------------------------------------- 1 | # Dependency directories 2 | node_modules/ 3 | jspm_packages/ 4 | 5 | # yarn v2 6 | .yarn/ 7 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "printWidth": 240, 3 | "tabWidth": 2, 4 | "useTabs": false, 5 | "semi": true, 6 | "singleQuote": true, 7 | "trailingComma": "none", 8 | "bracketSpacing": false, 9 | "arrowParens": "avoid", 10 | "parser": "typescript" 11 | } 12 | -------------------------------------------------------------------------------- /.yarnrc.yml: -------------------------------------------------------------------------------- 1 | logFilters: 2 | - code: YN0013 3 | level: discard 4 | - code: YN0019 5 | level: discard 6 | - code: YN0076 7 | level: discard 8 | 9 | nodeLinker: node-modules 10 | 11 | plugins: 12 | - path: .yarn/plugins/@yarnpkg/plugin-interactive-tools.cjs 13 | spec: "@yarnpkg/plugin-interactive-tools" 14 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020-2025 CrazyMax 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 | [![GitHub release](https://img.shields.io/github/release/crazy-max/ghaction-import-gpg.svg?style=flat-square)](https://github.com/crazy-max/ghaction-import-gpg/releases/latest) 2 | [![GitHub marketplace](https://img.shields.io/badge/marketplace-import--gpg-blue?logo=github&style=flat-square)](https://github.com/marketplace/actions/import-gpg) 3 | [![Test workflow](https://img.shields.io/github/actions/workflow/status/crazy-max/ghaction-import-gpg/test.yml?branch=master&label=test&logo=github&style=flat-square)](https://github.com/crazy-max/ghaction-import-gpg/actions?workflow=test) 4 | [![Codecov](https://img.shields.io/codecov/c/github/crazy-max/ghaction-import-gpg?logo=codecov&style=flat-square)](https://codecov.io/gh/crazy-max/ghaction-import-gpg) 5 | [![Become a sponsor](https://img.shields.io/badge/sponsor-crazy--max-181717.svg?logo=github&style=flat-square)](https://github.com/sponsors/crazy-max) 6 | [![Paypal Donate](https://img.shields.io/badge/donate-paypal-00457c.svg?logo=paypal&style=flat-square)](https://www.paypal.me/crazyws) 7 | 8 | ## About 9 | 10 | GitHub Action to easily import a GPG key. 11 | 12 | ![Import GPG](.github/ghaction-import-gpg.png) 13 | 14 | ___ 15 | 16 | * [Features](#features) 17 | * [Prerequisites](#prerequisites) 18 | * [Usage](#usage) 19 | * [Workflow](#workflow) 20 | * [Sign commits](#sign-commits) 21 | * [Use a subkey](#use-a-subkey) 22 | * [Set key's trust level](#set-keys-trust-level) 23 | * [Customizing](#customizing) 24 | * [inputs](#inputs) 25 | * [outputs](#outputs) 26 | * [Contributing](#contributing) 27 | * [License](#license) 28 | 29 | ## Features 30 | 31 | * Works on Linux, macOS and Windows [virtual environments](https://help.github.com/en/articles/virtual-environments-for-github-actions#supported-virtual-environments-and-hardware-resources) 32 | * Allow seeding the internal cache of `gpg-agent` with provided passphrase 33 | * Signing-only subkeys support 34 | * Purge imported GPG key, cache information and kill agent from runner 35 | * (Git) Enable signing for Git commits, tags and pushes 36 | * (Git) Configure and check committer info against GPG key 37 | 38 | ## Prerequisites 39 | 40 | First, [generate a GPG key](https://docs.github.com/en/github/authenticating-to-github/generating-a-new-gpg-key) and 41 | export the GPG private key as an ASCII armored version to your clipboard: 42 | 43 | ```shell 44 | # macOS 45 | gpg --armor --export-secret-key joe@foo.bar | pbcopy 46 | 47 | # Ubuntu (assuming GNU base64) 48 | gpg --armor --export-secret-key joe@foo.bar -w0 | xclip 49 | 50 | # Arch 51 | gpg --armor --export-secret-key joe@foo.bar | xclip -selection clipboard -i 52 | 53 | # FreeBSD (assuming BSD base64) 54 | gpg --armor --export-secret-key joe@foo.bar | xclip 55 | ``` 56 | 57 | Paste your clipboard as a [`secret`](https://help.github.com/en/actions/configuring-and-managing-workflows/creating-and-storing-encrypted-secrets) 58 | named `GPG_PRIVATE_KEY` for example. Create another secret with the 59 | `PASSPHRASE` if applicable. 60 | 61 | ## Usage 62 | 63 | ### Workflow 64 | 65 | ```yaml 66 | name: import-gpg 67 | 68 | on: 69 | push: 70 | branches: master 71 | 72 | jobs: 73 | import-gpg: 74 | runs-on: ubuntu-latest 75 | steps: 76 | - 77 | name: Checkout 78 | uses: actions/checkout@v4 79 | - 80 | name: Import GPG key 81 | uses: crazy-max/ghaction-import-gpg@v6 82 | with: 83 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 84 | passphrase: ${{ secrets.PASSPHRASE }} 85 | - 86 | name: List keys 87 | run: gpg -K 88 | ``` 89 | 90 | ### Sign commits 91 | 92 | ```yaml 93 | name: import-gpg 94 | 95 | on: 96 | push: 97 | branches: master 98 | 99 | jobs: 100 | sign-commit: 101 | runs-on: ubuntu-latest 102 | steps: 103 | - 104 | name: Checkout 105 | uses: actions/checkout@v4 106 | - 107 | name: Import GPG key 108 | uses: crazy-max/ghaction-import-gpg@v6 109 | with: 110 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 111 | passphrase: ${{ secrets.PASSPHRASE }} 112 | git_user_signingkey: true 113 | git_commit_gpgsign: true 114 | - 115 | name: Sign commit and push changes 116 | run: | 117 | echo foo > bar.txt 118 | git add . 119 | git commit -S -m "This commit is signed!" 120 | git push 121 | ``` 122 | 123 | ### Use a subkey 124 | 125 | With the input `fingerprint`, you can specify which one of the subkeys in a GPG 126 | key you want to use for signing. 127 | 128 | ```yaml 129 | name: import-gpg 130 | 131 | on: 132 | push: 133 | branches: master 134 | 135 | jobs: 136 | import-gpg: 137 | runs-on: ubuntu-latest 138 | steps: 139 | - 140 | name: Checkout 141 | uses: actions/checkout@v4 142 | - 143 | name: Import GPG key 144 | uses: crazy-max/ghaction-import-gpg@v6 145 | with: 146 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 147 | passphrase: ${{ secrets.PASSPHRASE }} 148 | fingerprint: "C17D11ADF199F12A30A0910F1F80449BE0B08CB8" 149 | - 150 | name: List keys 151 | run: gpg -K 152 | ``` 153 | 154 | For example, given this GPG key with a signing subkey: 155 | 156 | ``` 157 | pub ed25519 2021-09-24 [C] 158 | 87F257B89CE462100BEC0FFE6071D218380FDCC8 159 | Keygrip = F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092 160 | uid [ unknown] Joe Bar 161 | sub ed25519 2021-09-24 [S] 162 | C17D11ADF199F12A30A0910F1F80449BE0B08CB8 163 | Keygrip = DEE0FC98F441519CA5DE5D79773CB29009695FEB 164 | ``` 165 | 166 | You can use the subkey with signing capability whose fingerprint is `C17D11ADF199F12A30A0910F1F80449BE0B08CB8`. 167 | 168 | ### Set key's trust level 169 | 170 | With the `trust_level` input, you can specify the trust level of the GPG key. 171 | 172 | Valid values are: 173 | * `1`: unknown 174 | * `2`: never 175 | * `3`: marginal 176 | * `4`: full 177 | * `5`: ultimate 178 | 179 | ```yaml 180 | name: import-gpg 181 | 182 | on: 183 | push: 184 | branches: master 185 | 186 | jobs: 187 | import-gpg: 188 | runs-on: ubuntu-latest 189 | steps: 190 | - 191 | name: Checkout 192 | uses: actions/checkout@v4 193 | - 194 | name: Import GPG key 195 | uses: crazy-max/ghaction-import-gpg@v6 196 | with: 197 | gpg_private_key: ${{ secrets.GPG_PRIVATE_KEY }} 198 | passphrase: ${{ secrets.PASSPHRASE }} 199 | trust_level: 5 200 | ``` 201 | 202 | ## Customizing 203 | 204 | ### inputs 205 | 206 | The following inputs can be used as `step.with` keys: 207 | 208 | | Name | Type | Description | 209 | |-----------------------|--------|--------------------------------------------------------------------------------------------| 210 | | `gpg_private_key` | String | GPG private key exported as an ASCII armored version or its base64 encoding (**required**) | 211 | | `passphrase` | String | Passphrase of the GPG private key | 212 | | `trust_level` | String | Set key's trust level | 213 | | `git_config_global` | Bool | Set Git config global (default `false`) | 214 | | `git_user_signingkey` | Bool | Set GPG signing keyID for this Git repository (default `false`) | 215 | | `git_commit_gpgsign` | Bool | Sign all commits automatically. (default `false`) | 216 | | `git_tag_gpgsign` | Bool | Sign all tags automatically. (default `false`) | 217 | | `git_push_gpgsign` | String | Sign all pushes automatically. (default `if-asked`) | 218 | | `git_committer_name` | String | Set commit author's name (defaults to the name associated with the GPG key) | 219 | | `git_committer_email` | String | Set commit author's email (defaults to the email address associated with the GPG key) | 220 | | `workdir` | String | Working directory (below repository root) (default `.`) | 221 | | `fingerprint` | String | Specific fingerprint to use (subkey) | 222 | 223 | > [!NOTE] 224 | > `git_user_signingkey` needs to be enabled for `git_commit_gpgsign`, `git_tag_gpgsign`, 225 | > `git_push_gpgsign`, `git_committer_name`, `git_committer_email` inputs. 226 | 227 | ### outputs 228 | 229 | The following outputs are available: 230 | 231 | | Name | Type | Description | 232 | |---------------|--------|---------------------------------------------------------------------------------------------------------------------------------| 233 | | `fingerprint` | String | Fingerprint of the GPG key (recommended as [user ID](https://www.gnupg.org/documentation/manuals/gnupg/Specify-a-User-ID.html)) | 234 | | `keyid` | String | Low 64 bits of the X.509 certificate SHA-1 fingerprint | 235 | | `name` | String | Name associated with the GPG key | 236 | | `email` | String | Email address associated with the GPG key | 237 | 238 | ## Contributing 239 | 240 | Want to contribute? Awesome! The most basic way to show your support is to star 241 | the project, or to raise issues. You can also support this project by [**becoming a sponsor on GitHub**](https://github.com/sponsors/crazy-max) 242 | or by making a [PayPal donation](https://www.paypal.me/crazyws) to ensure this 243 | journey continues indefinitely! 244 | 245 | Thanks again for your support, it is much appreciated! :pray: 246 | 247 | ## License 248 | 249 | MIT. See `LICENSE` for more details. 250 | -------------------------------------------------------------------------------- /__tests__/fixtures/gpg.conf: -------------------------------------------------------------------------------- 1 | ################################################################################ 2 | # GnuPG Options 3 | 4 | # (OpenPGP-Configuration-Options) 5 | # Assume that command line arguments are given as UTF8 strings. 6 | utf8-strings 7 | 8 | # (OpenPGP-Protocol-Options) 9 | # Set the list of personal digest/cipher/compression preferences. This allows 10 | # the user to safely override the algorithm chosen by the recipient key 11 | # preferences, as GPG will only select an algorithm that is usable by all 12 | # recipients. 13 | personal-digest-preferences SHA512 SHA384 SHA256 SHA224 14 | personal-cipher-preferences AES256 AES192 AES CAST5 CAMELLIA192 BLOWFISH TWOFISH CAMELLIA128 3DES 15 | personal-compress-preferences ZLIB BZIP2 ZIP 16 | 17 | # Set the list of default preferences to string. This preference list is used 18 | # for new keys and becomes the default for "setpref" in the edit menu. 19 | default-preference-list SHA512 SHA384 SHA256 SHA224 AES256 AES192 AES CAST5 ZLIB BZIP2 ZIP Uncompressed 20 | 21 | # (OpenPGP-Esoteric-Options) 22 | # Use name as the message digest algorithm used when signing a key. Running the 23 | # program with the command --version yields a list of supported algorithms. Be 24 | # aware that if you choose an algorithm that GnuPG supports but other OpenPGP 25 | # implementations do not, then some users will not be able to use the key 26 | # signatures you make, or quite possibly your entire key. 27 | # 28 | # SHA-1 is the only algorithm specified for OpenPGP V4. By changing the 29 | # cert-digest-algo, the OpenPGP V4 specification is not met but with even 30 | # GnuPG 1.4.10 (release 2009) supporting SHA-2 algorithm, this should be safe. 31 | # Source: https://tools.ietf.org/html/rfc4880#section-12.2 32 | cert-digest-algo SHA512 33 | digest-algo SHA256 34 | 35 | # Selects how passphrases for symmetric encryption are mangled. 3 (the default) 36 | # iterates the whole process a number of times (see --s2k-count). 37 | s2k-mode 3 38 | 39 | # (OpenPGP-Protocol-Options) 40 | # Use name as the cipher algorithm for symmetric encryption with a passphrase 41 | # if --personal-cipher-preferences and --cipher-algo are not given. The 42 | # default is AES-128. 43 | s2k-cipher-algo AES256 44 | 45 | # (OpenPGP-Protocol-Options) 46 | # Use name as the digest algorithm used to mangle the passphrases for symmetric 47 | # encryption. The default is SHA-1. 48 | s2k-digest-algo SHA512 49 | 50 | # (OpenPGP-Protocol-Options) 51 | # Specify how many times the passphrases mangling for symmetric encryption is 52 | # repeated. This value may range between 1024 and 65011712 inclusive. The 53 | # default is inquired from gpg-agent. Note that not all values in the 54 | # 1024-65011712 range are legal and if an illegal value is selected, GnuPG will 55 | # round up to the nearest legal value. This option is only meaningful if 56 | # --s2k-mode is set to the default of 3. 57 | s2k-count 1015808 58 | 59 | ################################################################################ 60 | # GnuPG View Options 61 | 62 | # Select how to display key IDs. "long" is the more accurate (but less 63 | # convenient) 16-character key ID. Add an "0x" to include an "0x" at the 64 | # beginning of the key ID. 65 | keyid-format 0xlong 66 | 67 | # List all keys with their fingerprints. This is the same output as --list-keys 68 | # but with the additional output of a line with the fingerprint. If this 69 | # command is given twice, the fingerprints of all secondary keys are listed too. 70 | with-fingerprint 71 | with-fingerprint 72 | -------------------------------------------------------------------------------- /__tests__/fixtures/test-key-base64.pgp: -------------------------------------------------------------------------------- 1 | LS0tLS1CRUdJTiBQR1AgUFJJVkFURSBLRVkgQkxPQ0stLS0tLQoKbFFkR0JGNnR6YUFCRUFDakZiWDdQRkVHNnZEUE4yTVB5eFlXNy8zby9zb25PUmo0SFhVRmpGeHhKeGt0SjN4MwpOMWF5SFBKMWxxSWVvaVk3alZicTBaZEVWR2tkM1lzS0c5Wk1kWmt6R3pZNlBRUEMvK004T256T2lPUHdVZFdjCitUZGhoMTE1THZWejBNTUtZaWFiNlNuOWNneGo5T24zTENRS3Bqdk1EcFBvOVR0ZjZ2MkdRSXc4aDJBQ3ZkelEKNzFMdElFTFMvSStkTGJmWml3cFV1MmZoUVQxM0VKa0VuWU1PWXdNNWpOVWQ2NlA5aXRVYzdNck9XamtpY3JLUApvRjFkUWFDTSt0dUt1eHZEOFdMZGl3VTV4NjBOb0drSkhIVWVoS1FYbDJkVnpqcHFFcUhLRUJKdDl0Zko5bHBFCllJaXNnd0I4bzNwZXMwZmdDZWhqVzJ6STk1L285K2F5SjZubDRnNSttU3ZXUlhFdTY2aDcxbndNMFl1dnF1azgKM21lN3FoWWZEckRkQ3djeFM1QlMxaHdha1RnVVFMRDk5RlpqYngxajhzcTk2STY1TzBHUmR5VTJQUjhLSWp3dQpKcmtUSDRabEt4SzNGUWdoVWhGb0E1R2tpRGIrZUNsbVJNU25pNXFnKzgxVDRYQ2htVWtFcHJBM2VXQ0hMK01hCnhSTk54TFMrcjZoSDlIRzVKQnhwVjNpYVRJOUhIcG5RS2hFZWFMWHFzVVREWmxpTjloUDdZd284YnBVQjhqMmQKb1dZd0RWNGRQeU1LcjZGYjhSRENoMnE1Z0pHYlZwOHcvTm1tQlRlTCtJUDJmRmdnSmtSZnl1bXYzVWw3eDY2TAp0QkZRNHJZbzRKVVVyR3dlU1RuZUc2UkVJZ3hINjZoSXJObDZWby9EMVp5a25UZTFkTU91L0JUa2tRQVJBUUFCCi9nY0RBcXJhOEtPK2gzYmZ5dTkwdnhUTDFybzR4L3g5aWw3VkJjV2xJUjRjQlA3SW1neHYrVDRod1BJdThQMXgKbE9seExOV2VnRk9WMGlkb1R5MW8zVkxMQmV2L0YrSWxzcFg0QSsyWEVJZGRSNm5abktGaTBMdjJMNFRLZ0U5RQpWSkpUc3ptdmlESVJMTUxOOWRXekRmQThoajV0UjVJbm90OTJDSFJGNDE0QVMyMkpIdmxoYkZTTFFuanFzTitDCm4xY1FwTk9KaGt4c1NmWnN4am5GYS83MHkvdTh2MG84bXp5TFptazlIcHpSSEd6b3o4SWZwTHA4T1RxQlI5dTYKenpvS0x5MTZ6Wk81NU9LYmo3aDh1Vlp2RFVxOWw4aURJQ3BxV01kWnFCSklsNTZNQmV4WUtnWXhoM1lPLzh2MgpvWGxpKzhYdWFxNVFMaUNOM3lUN0liS29ZenBsbkZmYUp3RmlNaDdSMWlQTFhhWUFaMHFkUmlqbGJ0c2VUSzFtCm9ITmt3VWJ4Vnpqa2g0TGZFOFVwbU13Wm41WmpXbmkzMjMwU29pWHVLeTBPSGtHdndHdldXQUwxbUV1b1l1VUkKbUZNY0g1TW5peFA4b1FZWktEajJJUi95RWVPcGRVNkIvdHIzVGsxTmlkTGY3cFVNcUc3RmYxTlU2ZEFVZUJwYQo5eGFoSVRNakh2cmhnTUlTWTRJWVplcDVjRW5WdzhsUVRwVUp0Vy9lUE16ckZodTNzQTdvTmRqOWpvVy9WTWZ6Ckg3TUh3d2F2dElDc1lxb3FWM2xualg0RUM5ZFc2bzhQVFVnMnU5NTZkbXRLN0tBeVVLLyt3MmFMTkdUMjhDaE4KamhSWUh2SHpCOUt3NWFzcUkvbFRNNDllcXNsQnFZUU1UVGpkQnBoa1l1U1pRek5NZjI5MWovWm1vTGhEMUExYQpTOHRVbk55Z0tWNEQxY0pZZ1NYZnpoRm9VOGliLzBTUG8rS3FRK0N6R1Mrd3hYZzZXTkJBNndlcFRqcG5WVngzCjRKQURQOElKY0RDM1AwaXdBcmVXalN5MTVGMWN2ZW1GRkIwU0xOVWt5Wkd6c3h0S3piTTErOGtobDY4K2VhekMKTHpSajByeGZJRjV6bldqWDFRRmhLeENrNmVGMElXRFkwK2IzREJrbUNoTUU5WURYSjNUdGhjcUE3SmdjWDRKSQpNNC93ZHFoZ2VySllPbWoraTJRME0rQnUwMmljT0pZTXdUTU1zRFZsN1hHSGthQ3VSZ1o1NGVaQVVIN0pGd1VtCjFDdDN0Y2FxaVRNbXowbmdIVnFCVGF1emdxS0R2endkVnFkZmcwNUgzNjRuSk1heS8zb21SNkdheUliNUN3U28KeGROVndHM215UFByYWRUOU1QMDltRHI0eXMyemNuUW1Da3ZUVkJGNmNNWjFFaDZQUVE4Q3lRV3YwemthQm5xagpKck0xaFJwZ1c0WmxSb3NTSWpDYWFKam9sTjVRRGNYQk05VGJXOXd3K1pZc3Rhek4yYlYxWlE3QkVqbEhRUGExCkJoek1zdnFrYkVUSHNJcERORjUyZ1pLbjNROWVJWDA1QmVhZHpwSFViNS9YT2hlSUhWSWRoU2FUbGdsL3FRVzUKaFFnUEdTelNWNktoWEVZN2FldlRkdk9ncSsrV2lFTGtqZnoyZjJsUUZlc1RqRm9RV0V2eFZEVW1MeEh0RWhhTgpET3VoNEgzbVg1T3BuM3BMUW1xV1ZoSlRiRmR4K2c1cVFkME5DVzRtRGFURldUUkxGTFpRc1NKeERTZWc5eHJZCmdtYWlpOE5oTVpSd3F1QURXKzZpVTZLZnJhQmhuZ2k3SFJ6NFRmcVByOW1hL0tVWTQ2NGNxaW0xZm53WGVqeXgKanNiNVlIUjlSNjZpK0Y2UC95c0Y1dytRdVZkRHQxZm5mOUdMYXkwcjZxeHBBOGZ0MnZHUGNEczQ4MDZIdWorNwpBcTVWZUphTmtDdWgzR1IzeFZuQ0ZBei83QXRrTzZ4S3VabThCM3E5MDRVdU1kU21raFdiYW9iSXVGL0IyQjZTCmVhd0lYUUhFT3BsSzNpYzI2ZDhDa2Y0Z2JqZU9SZkVMY01BRWk1bkdYcFRUaENkbXhRQXBDTHhBWVluVGZRVDEKeGhsRHdUOXhQRWFibzk4bUl3SkpzQVU1VnNURFlXK3FmbzRxSXg4Z1lvU0tjOVh1M3lWaDNuKzlrNDNHY201Vgo5bHZLMXNsaWpmK1R6T0RadC9qc21rRjhtUGpYeVA1S09JK3hRcC9tNFB4VzNwcDU3WXJZai9SbndnYSs4REtYCmpNc1c3bUxBQVovZStQWTZ6L3MzeDFLcmZrK0JiNVBoNG1JMHpqdzV3ZVFkdHlFVG9SZ3ZlZGEwR0VwdlpTQlUKWlhOMFpYSWdQR3B2WlVCbWIyOHVZbUZ5UG9rQ05nUVFBUWdBSUFVQ1hxM05vQVlMQ1FjSUF3SUVGUWdLQWdRVwpBZ0VBQWhrQkFoc0RBaDRCQUFvSkVIMkZIcmN0YzcyZ3h0UVAvQXVsYUNsSWNuL2tEdDQzbWhZbnlMZ2xQZmJvCkFxUGxVMjZjaFhvbEJnMFdvMGZyRlkzYUlzNVNyY1dFZjhhUjRYTHdDRkd5aTN2eWEwQ1V4amdoTjV0WkJZcW8KdnN3YlQwMHpQM29oeHhsSkZDUlJSOWJjN09aWENnVGRkdGZWZjZFS3JVQXpJa2JXeUFoYUpud0p5LzFVR3BTdwpTRU8vS3Bhc3RyVktmM3N2MXdxT2VGUTRERnlqYU5kYSt4djNkVldTOGRiN0tvZ3FKaVBGWlhyUUszRktWSXhTCmZ4UlNtS2FZTjcvL2QreHdWQUVZKytScm5ML284QjJrVjZONjhjQ3BRV0pFTHlZbkp6aXM5TEJjV2QvM3dpWWgKZWZUeVkrZVBLVWpjQitrRVpueUpmTGM3QzJobGwyZTdVSjBmeHYrazh2SFJlUmhyTldtR1JYc2pOUnhpdzNVMApoZnZ4RC9DOG55cUFiZVRIcDRYRFg3OFRjM1hDeXNBcUlZYm9JTCtSeWV3RE1qakxqNXZ6VVlBZFVkdHlOYUQ3CkM2TTJSNnBOMUdBdDUyQ0ptQy9aNkY3VzdHRkdvWU9kRWtWZE1RRHNqQ3dTY3lFVU5sR2o5WmFndzVNMkVnU2UKNmdhSGdNZ1R6c016Q2M0VzZXVjVSY1M1NWNmRE5PWHR4UHNNSlR0NEZtWHJqbDExcHJCenBNZnBVNWE5enhEWgpvaTU0Wlo4VlBFNmpzVDRMenczc25pM2M4M3dtMjhBck0yMEF6WjF2aDdmazNTZmQwdTRZYXo3czlKbEVtNStECjM0dEV5bGkyOCtRakNRYzE4RWZRVWlKcWlZRUpSeEpYSjNlc3ZNSGZZaTQ1cFYvRWg1RGdSVzEzMDVmVUpWLzYKK3JHcGcwTmVqc0hvWmRaUG5RZEdCRjZ0emFBQkVBQzRtVlhUa1ZrNktkZmE0cjV6bHpzb0lyUjI3bGFVbE1rYgpPQk10K2Fva3FTK0JFYm1Ubk1nNnhJQW1jVVQ1dXZHQWM4Uy9XaHJQb1lmYzE1ZlRVeUhJejhaYkRvQWcwTE82CjBJbzRWa0F2TkpORW5zU1Y5VmRMQmgvWFlsYzRLNDlKcUt5V1RMNC9GSkZBR2JzbUhZM2IrUVU5MEFTNkZZUnYKS2VCQW9peWVicmp4MHZtemI4RThoM3h0aFZMTitBZk1sUjFpY2tZNjJ6dm5wa2JuY1NNWS9za3VyMUQyS2ZiRgozc0ZwcnR5MnBFdGpGY3lCNSsxOGwySXl5SEdPbEVVdzFQWmRPQVY0L015aDFFWlJnWUJQczgwbFlUSkFMQ1ZGCklkT2FrSDMzV0pDSW10TlpCMEFiRFRBQkcrSnRNalFHc2NPYTBxemYxWS83dGxoZ0NyeW5CQmRhSUpUeDk1VEQKMjFCVUhjSE91NXlUSVM2VWx5c3hma3Y2MTErQmlPS0hnZHE3RFZHUDc4VnV6QTdiQ2psUDErdkhxSXQzY25JYQp0MnRFeXVaL1hGNHVjMy9pNGcwdVA5cjdBbXRFVDdaNlNLRUNXanBWditVRWdMeDVDditxbCtMU0tZUU12VTlhCmkzQjFGOWZhdG4zRlNMVllyTDRhUnh1NFRTdzlQT2IwL2xnRE5tTjNsR1FPc2pHQ1pQaWJrSGpnUEVWeEt1aXEKOU9pMzgvVlRRMFpLQW1Id0JUcTFXVFpJclByQ1cwL1lNUTZ5SUpadWx3UTlZeDFjZ3pZekVmZzA0ZlBYbFhNaQp2a3ZOcEtiWUlJQ3pxajAvRFZ6dHo5d2dwVzZtbmQwQTJWWDJkcWJNTTBmSlVDSEE2cGo4QXZYWTRSKzlRNHJqCmVXUks5eWNJblFBUkFRQUIvZ2NEQXBqdDdiaVJPMFBFeXJyQWlVd0RNc0pMNC9DVk11MTFxVVdFUGpLZTJHcmgKWlRXM04rbTNuZUtQUlVMdStMVXRuZFVjRWRWV1VDb0R6QUo3TXdpaFp0VjV2S1NULzVTY2QyaW5vbk9hSnFvQQpuUzN3bkVNTi9TYzkzSEFaaVpuRngzTktqUVZOQ3didUVzNDVtWGtrY2pMbTJpYWRyVEw4Zkw0YWNzdTVJc3ZECkxiRHdWT1BlTm5IS2w2SHIyMGUzOWZLMEZ1SkV5SDQ5Sk02VTNCMS84Mzg1c0pCOCtFMjQraHZTRjgxYU1kZGgKTmU0QmMzWllpWWFLeGUxcXVQTktDMENRaEFaaVQ3THNNZmtJblhyMGhZMUkra0lTTlhFSjFkUFlPRVdpdjBaZQpqRDVQdXBuMzRva0tORWVCQ3grZEs4Qm1VQ2k2SmdzN01jVUE3aE4wRC9ZVVMrKzVmdVI1NVVRcTJqOFVpMHRTClA4R0RyODZ1cEgzUGdFTDBTVGg5ZllmSjdUZXN4dXJ3b25XamxtbVQ2Mk15bDRQcitSbXBTNlBYT25odGNBRG0KZUdMcHpoVHZlRmo0SkJMTXB5WUhnQlRxY3MxMnpmcHJBVE9wc0kvODlrbVFvR0NacEc2K0FiZlNIcU5OUGR5MgplcVVDQmhPWmxJSWRhMXovY2V4bVUzZi9nQnF5ZmxGZjhma3ZtbE80QXZJOGFNSDNPcGdIZFduemgrQUI1MXhqCmttZEQvb1dlbDl2N0R6NEhvWlVmd0ZhTFowZkUzUDl2b0Q4ZStzQ3dxUXdWcVJZNEwvQk9ZUEQ1bm9WT0tnT2oKQUJOS3U1dUtyb2JqNnJGVWk2RFRVQ2pGR2Ntb0YxU2MwNnhGTmFhZ1VOZ2dSYm1sQy9kejIyUldkRFVZdjVyYQpONlR4SURrR0MwY0s2dWp5SzBuZXMzRE4wYUhqZ3dXdU1YRFlrTjNVY2tpZWJJNEN2L2VGOWp2VUtPU2lJY3kxClJ0eGRhelpTNGRZZzJMQk1lSktWa1BpNWVsc055dzI4MTJuRVkzZHUvbkVrUVlYZllnV09GMjdPUitnNFk5WXcKMUJpcUoxVFRqYlFuZC9raE9DcnJiekRIMW13MDArMVhWc1Q2d2pPYnVZcXF4UFBTODdVcnFtTWY2T2RvWWZQbQp6RU9uTkxCbnNKNVZRTTNBM3BjVDQwUmZkQnJaUk84TGpHaHpLVHJleXEzQytqejBSTGE1SE5FOEdnT2hHeWNrCk1FNGgrUmhYbEU4S0dNK3RUbzZQQTFOSlNyRXQrOGtaenhqUDRySUVuMGFWdGhDa05YSzEyaW51WHRuSG0wYW8KaUxVbFFPc2ZQRkVuemwwVFVQZDcrejdqL3dCK1hpS1UvQXlFVXVCMG12ZHhkS3RxWHZhamFoT3loTGp6SFFoegpabk5sZ0FOR3RpcWNTb0pta0o4eUF2aHJ0UVg1MWZRTGZ0eGJBclJXMVJZay81bCtHeTNhelIrZ1VDMTdNNkpOCmpyVVl4bjB6bEF4REdGSDdnQUNIVU9Od1Zla2N1RWZmSHpndTJsazdNeU8xWStsUG53YWJxakcwZVdXSHVVMDAKaHNrSmxYeWhqN0RlUjEyYndqWWt5eWpHNjJHdk9IMDJnM09NdlVnTkdIK0szMjFEejUzOWNzQ2gveHd0ZzdXdApVM1lBcGhVN2h0UTFkUERmazFJUnM3RFFvMkwrWlRFNTd2bUw1bTBsNmZUYXRhRVdCUFVYa3lnZlFGVUpPTTZRCnlZNzZVRVp3dzFPU0R1ak5lWTE3MU5TVHpYQ1ZrVWVBZEFNWGdqYUhYV0xLMlFVUVVvWGJZWC9LcjdWdnQ5RnUKSmg2ZUdqanA3ZFNqUTkrRFc4Q0FCOHZ4ZDkzZ3NRUUdXWWptR3U4a2hrRW14Nk9kWmhtU2JEYmU5MTVMUVRiOQpzUGhrMnM1L1N6c3ZyNVcySkoyMzIxSkk2S1hCSk1adlBDNWpFQldtUnpPWWtSZDJ2bG9mdCtDU01mWEYrWmZkCm5ZdGM2UjNkdmI5dmNqbythOXdGdGZjb0RzTzBNYVBTTSs5R0IyNU1hbWRhdG1HWDZpTE95OVJlMVVBQndVaS8KVmhUV05rUDV1enF4MHNEd0hFSWEycllPd3hwSVpEd3dqTTNvT0FTQ1cxRERCUTBCSTlLTmpmSWVMM3VieDJtUwoyeDhoRlU5cVNLNHVtb0ROYnpPcUdQU2xrZGJpUGNOakYyWmNTTjFxUVppWWR3TEw1ZHc2QVBOeUJWanhUTjFKCmdrQ2RKL0h3QVkrcjkzTGJsNWc4Z3o4ZDB2SkV5Zm4vLzM0c245dSt0b1NUdzU1R2NHOUtzMWtTS0llRE5oMGgKTWlQbTNIbUpBaDhFR0FFSUFBa0ZBbDZ0emFBQ0d3d0FDZ2tRZllVZXR5MXp2YUJWOWhBQWdsaVgzNnBYSjU5ZwozSTkvNFI2OGUvZkdnMEZNTTZEKzAxeUNlaUtBcE9ZUnJKMGNZS243SVREWW1IaGxHR3BCQWllOTBVc3FYMTJoCmhkTFA3TG9ReDdzalR5elF0NkptcEE4a3JJd2kyT043RktCa2RZYjhJWXg0bUUvNXZLbllUNC9TRm53VG1uWlkKK20rTnpLMlUvcW1ocThKeU84Z296ZEFLSlVjZ3o0OUlWdjJJajB0UTRxYVBieVB3UXhJRHlLblQ3NThuSmhCMQpqVHFvK29XdEVSOHEzb2t6SWxxY0FycW41ckRhTkp4K0RSWUw0RS9JZGR5SFFBaVVXVWthOHVzSVVxZVc1cmV1CnpvUFVFMkNDZk9KU0dBcmtxSFFRcU14MFdFempRVHdBUGFIclFiZXJhNFNiaVYvbzRDTENWL3U1cDFRbmlnK1EKaVVzYWttbEQyOTl0Ly8xMjVMSVFFYTVxemQ5aFJDN3UxdUpTN1ZkVzhlR0lFY1owL1hUL3NyK3oyM3owa3BaSApEM2RYUFgwQndNNElQOXh1MzFDTmcxMHgwckt3amJ4eThWYXNrRkVlbHBxcHUrZ3BBbnhxTWQxZXZwZVVIY09kCnI1UmdQZ2tORmZiYTlOYnhmN3VFWCtIT21zT00ra2R0U21kR0l2c0JaalZuVzMxbm5vRE1wNDlqRzRPeW5qckgKY1J1b005c3hkcjZVRHFiMjJDWjMvZTBZTjRVYVpNM1lEV01WYVAvUUJWZ3ZJRmNkQnlxTldlenBkOVQ0WlVJSQpNWmxhVjF1Um5IZzZCL3pUemhJZE1NODBBWHo2VXY2a3c0UytMdDdIbGJybk1UN3VLTHV2ekg3Y2xlMGhjSVVhClBlamdYTzB1SVJvbFlRM3N6MnRNR2h4MU1mQnFINjQ9Cj1XYndCCi0tLS0tRU5EIFBHUCBQUklWQVRFIEtFWSBCTE9DSy0tLS0t -------------------------------------------------------------------------------- /__tests__/fixtures/test-key-gpg-output.txt: -------------------------------------------------------------------------------- 1 | tru::1:1645715610:1661267528:3:1:5 2 | pub:-:4096:1:7D851EB72D73BDA0:1588448672:::-:::scESC::::::23::0: 3 | fpr:::::::::27571A53B86AF0C799B38BA77D851EB72D73BDA0: 4 | grp:::::::::3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627: 5 | uid:-::::1588448672::C1B25336F8F0F0F22BAF57137BE493ADEDA8CCAA::Joe Tester ::::::::::0: 6 | sub:-:4096:1:D523BD50DD70B0BA:1588448672::::::e::::::23: 7 | fpr:::::::::5A282E1460C0BC419615D34DD523BD50DD70B0BA: 8 | grp:::::::::BA83FC8947213477F28ADC019F6564A956456163: 9 | -------------------------------------------------------------------------------- /__tests__/fixtures/test-key.pass: -------------------------------------------------------------------------------- 1 | with stupid passphrase -------------------------------------------------------------------------------- /__tests__/fixtures/test-key.pgp: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | 3 | 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 104 | PejgXO0uIRolYQ3sz2tMGhx1MfBqH64= 105 | =WbwB 106 | -----END PGP PRIVATE KEY BLOCK----- -------------------------------------------------------------------------------- /__tests__/fixtures/test-subkey-base64.pgp: -------------------------------------------------------------------------------- 1 | LS0tLS1CRUdJTiBQR1AgUFJJVkFURSBLRVkgQkxPQ0stLS0tLQoKbElZRVlVNU0yaFlKS3dZQkJBSGFSdzhCQVFkQXNSbDlDUEtaaDB4MC9FRDFveDJwTmJ6R1J1TlpvRlVSN0JsYgpOUUdabzB6K0J3TUN1dVdvaTR5WTQ0YkhNU1AwMjBLRmUvOHhpWHJwby9LandiMXJaa1g3dW1laWZBRFh6L1JiCmJuMXdKMENGQ09TOHl4R3laL3NCYlk1OGZEL0gvMFU2TFdiSmRHSG1mZ0RXYTl0OEFQK09NTFFWU205bElFSmgKY2lBOGFtOWxRR0poY2k1bWIyOCtpSkFFRXhZS0FEZ1dJUVNIOGxlNG5PUmlFQXZzRC81Z2NkSVlPQS9jeUFVQwpZVTVNMmdJYkFRVUxDUWdIQXdVVkNna0lDd1VXQWdNQkFBSWVBUUlYZ0FBS0NSQmdjZElZT0EvY3lGd1VBUUN0CmRQdzU3MDh0Z296NkNqcEFMbzBjQ2NtZ2xuVHdGWlBYTm1DaGdPZUIzQUVBdkdNV2lrYy9iaG9waVRGUzNLVWkKR042a1o5ZUlhaTRYeDloTjRSZTlEd1NjaGdSaFRrMURGZ2tyQmdFRUFkcEhEd0VCQjBDVUtPdVVMYlNqZVF4QwpHNmY4VkhNWHRUbnc4MkF2TmlwM01rY3RNZEZmbC80SEF3SlBPM1loUVJkWU44Y1A1cVhvOFcwazFPZEJaTEJyCmN5cm5ra2tYVk91cjh1SlExV2tMb2FMSnZ3VmN1MlplSFlWdmcramNFSmVlTVF0ME43OWVOUUs5VVMzeEQ5ak4Kc2JZbTVrUkNHWldpaU84RUdCWUtBQ0FXSVFTSDhsZTRuT1JpRUF2c0QvNWdjZElZT0EvY3lBVUNZVTVOUXdJYgpBZ0NCQ1JCZ2NkSVlPQS9jeUhZZ0JCa1dDZ0FkRmlFRXdYMFJyZkdaOFNvd29KRVBINEJFbStDd2pMZ0ZBbUZPClRVTUFDZ2tRSDRCRW0rQ3dqTGlJT1FFQTZjazVCbXMwYzBvbHV4Ly9BeUprMlpINWl5WW11WmpaVTJNOEhtcEoKa1BJQkFPVWJsQmlwZURpc0dqQ0VmTE1SN1czcFBYTTMyY0ZOWVdwOW1SNzJ6SWdOcEdvQS8zM1grRG55VHhtTgpYeUlpZFFtK0J3TFBZOXRTUlMvL0dCbVg4eHdDUWpWS0FRRG54V0VyaVk4clBQOTFUblhtR0VjL05LeFZVcHJoCjVRTndjMHNBTjVGRUJ3PT0KPTExQjQKLS0tLS1FTkQgUEdQIFBSSVZBVEUgS0VZIEJMT0NLLS0tLS0K -------------------------------------------------------------------------------- /__tests__/fixtures/test-subkey-gpg-output.txt: -------------------------------------------------------------------------------- 1 | tru::1:1645715610:1661267528:3:1:5 2 | pub:-:256:22:6071D218380FDCC8:1632521434:::-:::cSC:::::ed25519:::0: 3 | fpr:::::::::87F257B89CE462100BEC0FFE6071D218380FDCC8: 4 | grp:::::::::F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092: 5 | uid:-::::1632521434::019F22ECD701BC0F6AFE686ABD2B010B812B828E::Joe Bar ::::::::::0: 6 | sub:-:256:22:1F80449BE0B08CB8:1632521539::::::s:::::ed25519:: 7 | fpr:::::::::C17D11ADF199F12A30A0910F1F80449BE0B08CB8: 8 | grp:::::::::DEE0FC98F441519CA5DE5D79773CB29009695FEB: 9 | -------------------------------------------------------------------------------- /__tests__/fixtures/test-subkey.pass: -------------------------------------------------------------------------------- 1 | with another passphrase -------------------------------------------------------------------------------- /__tests__/fixtures/test-subkey.pgp: -------------------------------------------------------------------------------- 1 | -----BEGIN PGP PRIVATE KEY BLOCK----- 2 | 3 | lIYEYU5M2hYJKwYBBAHaRw8BAQdAsRl9CPKZh0x0/ED1ox2pNbzGRuNZoFUR7Blb 4 | NQGZo0z+BwMCuuWoi4yY44bHMSP020KFe/8xiXrpo/Kjwb1rZkX7umeifADXz/Rb 5 | bn1wJ0CFCOS8yxGyZ/sBbY58fD/H/0U6LWbJdGHmfgDWa9t8AP+OMLQVSm9lIEJh 6 | ciA8am9lQGJhci5mb28+iJAEExYKADgWIQSH8le4nORiEAvsD/5gcdIYOA/cyAUC 7 | YU5M2gIbAQULCQgHAwUVCgkICwUWAgMBAAIeAQIXgAAKCRBgcdIYOA/cyFwUAQCt 8 | dPw5708tgoz6CjpALo0cCcmglnTwFZPXNmChgOeB3AEAvGMWikc/bhopiTFS3KUi 9 | GN6kZ9eIai4Xx9hN4Re9DwSchgRhTk1DFgkrBgEEAdpHDwEBB0CUKOuULbSjeQxC 10 | G6f8VHMXtTnw82AvNip3MkctMdFfl/4HAwJPO3YhQRdYN8cP5qXo8W0k1OdBZLBr 11 | cyrnkkkXVOur8uJQ1WkLoaLJvwVcu2ZeHYVvg+jcEJeeMQt0N79eNQK9US3xD9jN 12 | sbYm5kRCGZWiiO8EGBYKACAWIQSH8le4nORiEAvsD/5gcdIYOA/cyAUCYU5NQwIb 13 | AgCBCRBgcdIYOA/cyHYgBBkWCgAdFiEEwX0RrfGZ8SowoJEPH4BEm+CwjLgFAmFO 14 | TUMACgkQH4BEm+CwjLiIOQEA6ck5Bms0c0olux//AyJk2ZH5iyYmuZjZU2M8HmpJ 15 | kPIBAOUblBipeDisGjCEfLMR7W3pPXM32cFNYWp9mR72zIgNpGoA/33X+DnyTxmN 16 | XyIidQm+BwLPY9tSRS//GBmX8xwCQjVKAQDnxWEriY8rPP91TnXmGEc/NKxVUprh 17 | 5QNwc0sAN5FEBw== 18 | =11B4 19 | -----END PGP PRIVATE KEY BLOCK----- 20 | -------------------------------------------------------------------------------- /__tests__/gpg.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from '@jest/globals'; 2 | import * as fs from 'fs'; 3 | import * as gpg from '../src/gpg'; 4 | import {parseKeygripFromGpgColonsOutput} from '../src/gpg'; 5 | 6 | const userInfos = [ 7 | { 8 | key: 'test-key', 9 | pgp: fs.readFileSync('__tests__/fixtures/test-key.pgp', { 10 | encoding: 'utf8', 11 | flag: 'r' 12 | }), 13 | pgp_base64: fs.readFileSync('__tests__/fixtures/test-key-base64.pgp', { 14 | encoding: 'utf8', 15 | flag: 'r' 16 | }), 17 | passphrase: fs.readFileSync('__tests__/fixtures/test-key.pass', { 18 | encoding: 'utf8', 19 | flag: 'r' 20 | }), 21 | name: 'Joe Tester', 22 | email: 'joe@foo.bar', 23 | keyID: '7D851EB72D73BDA0', 24 | primary_key_fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', 25 | fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', 26 | fingerprints: ['27571A53B86AF0C799B38BA77D851EB72D73BDA0', '5A282E1460C0BC419615D34DD523BD50DD70B0BA'], 27 | keygrips: ['3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627', 'BA83FC8947213477F28ADC019F6564A956456163'] 28 | }, 29 | { 30 | key: 'test-subkey', 31 | pgp: fs.readFileSync('__tests__/fixtures/test-subkey.pgp', { 32 | encoding: 'utf8', 33 | flag: 'r' 34 | }), 35 | pgp_base64: fs.readFileSync('__tests__/fixtures/test-subkey-base64.pgp', { 36 | encoding: 'utf8', 37 | flag: 'r' 38 | }), 39 | passphrase: fs.readFileSync('__tests__/fixtures/test-subkey.pass', { 40 | encoding: 'utf8', 41 | flag: 'r' 42 | }), 43 | name: 'Joe Bar', 44 | email: 'joe@bar.foo', 45 | keyID: '6071D218380FDCC8', 46 | primary_key_fingerprint: '87F257B89CE462100BEC0FFE6071D218380FDCC8', 47 | fingerprint: 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8', 48 | fingerprints: ['87F257B89CE462100BEC0FFE6071D218380FDCC8', 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8'], 49 | keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB'] 50 | } 51 | ]; 52 | 53 | describe('getVersion', () => { 54 | it('returns GnuPG and libgcrypt version', async () => { 55 | await gpg.getVersion().then(version => { 56 | expect(version.gnupg).not.toEqual(''); 57 | expect(version.libgcrypt).not.toEqual(''); 58 | }); 59 | }); 60 | }); 61 | 62 | describe('getDirs', () => { 63 | it('returns GnuPG dirs', async () => { 64 | await gpg.getDirs().then(dirs => { 65 | expect(dirs.libdir).not.toEqual(''); 66 | expect(dirs.datadir).not.toEqual(''); 67 | expect(dirs.homedir).not.toEqual(''); 68 | }); 69 | }); 70 | }); 71 | 72 | describe('configureAgent', () => { 73 | // eslint-disable-next-line jest/expect-expect 74 | it('configures GnuPG agent', async () => { 75 | await gpg.configureAgent(await gpg.getHome(), gpg.agentConfig); 76 | }); 77 | }); 78 | 79 | for (const userInfo of userInfos) { 80 | // eslint-disable-next-line jest/valid-title 81 | describe(userInfo.key, () => { 82 | describe('importKey', () => { 83 | it('imports key (as armored string) to GnuPG', async () => { 84 | await gpg.importKey(userInfo.pgp).then(output => { 85 | expect(output).not.toEqual(''); 86 | }); 87 | }); 88 | it('imports key (as base64 string) to GnuPG', async () => { 89 | await gpg.importKey(userInfo.pgp_base64).then(output => { 90 | expect(output).not.toEqual(''); 91 | }); 92 | }); 93 | }); 94 | 95 | describe('getKeygrips', () => { 96 | it('returns the keygrips', async () => { 97 | await gpg.importKey(userInfo.pgp); 98 | await gpg.getKeygrips(userInfo.fingerprint).then(keygrips => { 99 | expect(keygrips.length).toEqual(userInfo.keygrips.length); 100 | for (let i = 0; i < keygrips.length; i++) { 101 | expect(keygrips[i]).toEqual(userInfo.keygrips[i]); 102 | } 103 | }); 104 | }); 105 | }); 106 | 107 | describe('getKeygrip', () => { 108 | it('returns the keygrip for a given fingerprint', async () => { 109 | await gpg.importKey(userInfo.pgp); 110 | for (const {idx, fingerprint} of userInfo.fingerprints.map((fingerprint, idx) => ({idx, fingerprint}))) { 111 | await gpg.getKeygrip(fingerprint).then(keygrip => { 112 | expect(keygrip.length).toEqual(userInfo.keygrips[idx].length); 113 | expect(keygrip).toEqual(userInfo.keygrips[idx]); 114 | }); 115 | } 116 | }); 117 | }); 118 | 119 | describe('presetPassphrase', () => { 120 | it('presets passphrase', async () => { 121 | await gpg.importKey(userInfo.pgp); 122 | await gpg.configureAgent(await gpg.getHome(), gpg.agentConfig); 123 | for (const keygrip of await gpg.getKeygrips(userInfo.fingerprint)) { 124 | await gpg.presetPassphrase(keygrip, userInfo.passphrase).then(output => { 125 | expect(output).not.toEqual(''); 126 | }); 127 | } 128 | }); 129 | }); 130 | 131 | describe('setTrustLevel', () => { 132 | it('set trust level', async () => { 133 | await gpg.importKey(userInfo.pgp); 134 | await gpg.configureAgent(await gpg.getHome(), gpg.agentConfig); 135 | expect(() => { 136 | gpg.setTrustLevel(userInfo.keyID, '5'); 137 | }).not.toThrow(); 138 | }); 139 | }); 140 | 141 | describe('deleteKey', () => { 142 | // eslint-disable-next-line jest/expect-expect 143 | it('removes key from GnuPG', async () => { 144 | await gpg.importKey(userInfo.pgp); 145 | await gpg.deleteKey(userInfo.primary_key_fingerprint); 146 | }); 147 | }); 148 | }); 149 | } 150 | 151 | describe('killAgent', () => { 152 | // eslint-disable-next-line jest/expect-expect 153 | it('kills GnuPG agent', async () => { 154 | await gpg.killAgent(); 155 | }); 156 | }); 157 | 158 | describe('parseKeygripFromGpgColonsOutput', () => { 159 | it('returns the keygrip of a given fingerprint from a GPG command output using the option: --with-colons', async () => { 160 | const outputUsingTestKey = fs.readFileSync('__tests__/fixtures/test-key-gpg-output.txt', { 161 | encoding: 'utf8', 162 | flag: 'r' 163 | }); 164 | 165 | const keygripPrimaryTestKey = parseKeygripFromGpgColonsOutput(outputUsingTestKey, '27571A53B86AF0C799B38BA77D851EB72D73BDA0'); 166 | expect(keygripPrimaryTestKey).toBe('3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627'); 167 | 168 | const keygripSubkeyTestKey = parseKeygripFromGpgColonsOutput(outputUsingTestKey, '5A282E1460C0BC419615D34DD523BD50DD70B0BA'); 169 | expect(keygripSubkeyTestKey).toBe('BA83FC8947213477F28ADC019F6564A956456163'); 170 | 171 | const outputUsingTestSubkey = fs.readFileSync('__tests__/fixtures/test-subkey-gpg-output.txt', { 172 | encoding: 'utf8', 173 | flag: 'r' 174 | }); 175 | 176 | const keygripPrimaryTestSubkey = parseKeygripFromGpgColonsOutput(outputUsingTestSubkey, '87F257B89CE462100BEC0FFE6071D218380FDCC8'); 177 | expect(keygripPrimaryTestSubkey).toBe('F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092'); 178 | 179 | const keygripSubkeyTestSubkey = parseKeygripFromGpgColonsOutput(outputUsingTestSubkey, 'C17D11ADF199F12A30A0910F1F80449BE0B08CB8'); 180 | expect(keygripSubkeyTestSubkey).toBe('DEE0FC98F441519CA5DE5D79773CB29009695FEB'); 181 | }); 182 | }); 183 | -------------------------------------------------------------------------------- /__tests__/openpgp.test.ts: -------------------------------------------------------------------------------- 1 | import {describe, expect, it} from '@jest/globals'; 2 | import * as fs from 'fs'; 3 | import * as openpgp from '../src/openpgp'; 4 | 5 | const userInfos = [ 6 | { 7 | key: 'test-key', 8 | pgp: fs.readFileSync('__tests__/fixtures/test-key.pgp', { 9 | encoding: 'utf8', 10 | flag: 'r' 11 | }), 12 | pgp_base64: fs.readFileSync('__tests__/fixtures/test-key-base64.pgp', { 13 | encoding: 'utf8', 14 | flag: 'r' 15 | }), 16 | passphrase: fs.readFileSync('__tests__/fixtures/test-key.pass', { 17 | encoding: 'utf8', 18 | flag: 'r' 19 | }), 20 | name: 'Joe Tester', 21 | email: 'joe@foo.bar', 22 | keyID: '7D851EB72D73BDA0', 23 | fingerprint: '27571A53B86AF0C799B38BA77D851EB72D73BDA0', 24 | keygrip: '3E2D1142AA59E08E16B7E2C64BA6DDC773B1A627' 25 | }, 26 | { 27 | key: 'test-subkey', 28 | pgp: fs.readFileSync('__tests__/fixtures/test-subkey.pgp', { 29 | encoding: 'utf8', 30 | flag: 'r' 31 | }), 32 | pgp_base64: fs.readFileSync('__tests__/fixtures/test-subkey-base64.pgp', { 33 | encoding: 'utf8', 34 | flag: 'r' 35 | }), 36 | passphrase: fs.readFileSync('__tests__/fixtures/test-subkey.pass', { 37 | encoding: 'utf8', 38 | flag: 'r' 39 | }), 40 | name: 'Joe Bar', 41 | email: 'joe@bar.foo', 42 | keyID: '6071D218380FDCC8', 43 | fingerprint: '87F257B89CE462100BEC0FFE6071D218380FDCC8', 44 | keygrips: ['F5C3ABFAAB36B427FD98C4EDD0387E08EA1E8092', 'DEE0FC98F441519CA5DE5D79773CB29009695FEB'] 45 | } 46 | ]; 47 | 48 | for (const userInfo of userInfos) { 49 | // eslint-disable-next-line jest/valid-title 50 | describe(userInfo.key, () => { 51 | describe('readPrivateKey', () => { 52 | it('returns a PGP private key from an armored string', async () => { 53 | await openpgp.readPrivateKey(userInfo.pgp).then(privateKey => { 54 | expect(privateKey.keyID).toEqual(userInfo.keyID); 55 | expect(privateKey.name).toEqual(userInfo.name); 56 | expect(privateKey.email).toEqual(userInfo.email); 57 | expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); 58 | }); 59 | }); 60 | it('returns a PGP private key from a base64 armored string', async () => { 61 | await openpgp.readPrivateKey(userInfo.pgp_base64).then(privateKey => { 62 | expect(privateKey.keyID).toEqual(userInfo.keyID); 63 | expect(privateKey.name).toEqual(userInfo.name); 64 | expect(privateKey.email).toEqual(userInfo.email); 65 | expect(privateKey.fingerprint).toEqual(userInfo.fingerprint); 66 | }); 67 | }); 68 | }); 69 | 70 | describe('generateKeyPair', () => { 71 | it('generates a PGP key pair', async () => { 72 | await openpgp.generateKeyPair(userInfo.name, userInfo.email, userInfo.passphrase).then(keyPair => { 73 | expect(keyPair).not.toBeUndefined(); 74 | expect(keyPair.publicKey).not.toBeUndefined(); 75 | expect(keyPair.privateKey).not.toBeUndefined(); 76 | }); 77 | }, 30000); 78 | }); 79 | 80 | describe('isArmored', () => { 81 | it('returns true for armored key string', async () => { 82 | await openpgp.isArmored(userInfo.pgp).then(armored => { 83 | expect(armored).toEqual(true); 84 | }); 85 | }); 86 | it('returns false for base64 key string', async () => { 87 | await openpgp.isArmored(userInfo.pgp_base64).then(armored => { 88 | expect(armored).toEqual(false); 89 | }); 90 | }); 91 | }); 92 | }); 93 | } 94 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/metadata-syntax-for-github-actions 2 | name: 'Import GPG' 3 | description: 'GitHub Action to easily import a GPG key' 4 | author: 'crazy-max' 5 | branding: 6 | color: 'yellow' 7 | icon: 'lock' 8 | 9 | inputs: 10 | gpg_private_key: 11 | description: 'GPG private key exported as an ASCII armored version or its base64 encoding' 12 | required: true 13 | passphrase: 14 | description: 'Passphrase of the GPG private key' 15 | required: false 16 | trust_level: 17 | description: "Set key's trust level" 18 | required: false 19 | git_config_global: 20 | description: 'Set Git config global' 21 | default: 'false' 22 | required: false 23 | git_user_signingkey: 24 | description: 'Set GPG signing keyID for this Git repository' 25 | default: 'false' 26 | required: false 27 | git_commit_gpgsign: 28 | description: 'Sign all commits automatically' 29 | default: 'false' 30 | required: false 31 | git_tag_gpgsign: 32 | description: 'Sign all tags automatically' 33 | default: 'false' 34 | required: false 35 | git_push_gpgsign: 36 | description: 'Sign all pushes automatically' 37 | default: 'if-asked' 38 | required: false 39 | git_committer_name: 40 | description: 'Commit author''s name' 41 | required: false 42 | git_committer_email: 43 | description: 'Commit author''s email' 44 | required: false 45 | workdir: 46 | description: 'Working directory (below repository root)' 47 | default: '.' 48 | required: false 49 | fingerprint: 50 | description: 'Specific fingerprint to use (subkey)' 51 | required: false 52 | 53 | outputs: 54 | fingerprint: 55 | description: 'Fingerprint of the GPG key (recommended as user ID)' 56 | keyid: 57 | description: 'Low 64 bits of the X.509 certificate SHA-1 fingerprint' 58 | name: 59 | description: 'Name associated with the GPG key' 60 | email: 61 | description: 'Email address associated with the GPG key' 62 | 63 | runs: 64 | using: 'node20' 65 | main: 'dist/index.js' 66 | post: 'dist/index.js' 67 | -------------------------------------------------------------------------------- /codecov.yml: -------------------------------------------------------------------------------- 1 | comment: false 2 | 3 | coverage: 4 | status: 5 | project: # settings affecting project coverage 6 | default: 7 | target: auto # auto % coverage target 8 | threshold: 5% # allow for 5% reduction of coverage without failing 9 | patch: off 10 | 11 | github_checks: 12 | annotations: false 13 | -------------------------------------------------------------------------------- /dev.Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax=docker/dockerfile:1 2 | 3 | ARG NODE_VERSION=20 4 | 5 | FROM node:${NODE_VERSION}-alpine AS base 6 | RUN apk add --no-cache cpio findutils git 7 | WORKDIR /src 8 | RUN --mount=type=bind,target=.,rw \ 9 | --mount=type=cache,target=/src/.yarn/cache <&2 'ERROR: Vendor result differs. Please vendor your package with "docker buildx bake vendor"' 31 | git status --porcelain -- yarn.lock 32 | exit 1 33 | fi 34 | EOT 35 | 36 | FROM deps AS build 37 | RUN --mount=type=bind,target=.,rw \ 38 | --mount=type=cache,target=/src/.yarn/cache \ 39 | --mount=type=cache,target=/src/node_modules \ 40 | yarn run build && mkdir /out && cp -Rf dist /out/ 41 | 42 | FROM scratch AS build-update 43 | COPY --from=build /out / 44 | 45 | FROM build AS build-validate 46 | RUN --mount=type=bind,target=.,rw <&2 'ERROR: Build result differs. Please build first with "docker buildx bake build"' 52 | git status --porcelain -- dist 53 | exit 1 54 | fi 55 | EOT 56 | 57 | FROM deps AS format 58 | RUN --mount=type=bind,target=.,rw \ 59 | --mount=type=cache,target=/src/.yarn/cache \ 60 | --mount=type=cache,target=/src/node_modules \ 61 | yarn run format \ 62 | && mkdir /out && find . -name '*.ts' -not -path './node_modules/*' -not -path './.yarn/*' | cpio -pdm /out 63 | 64 | FROM scratch AS format-update 65 | COPY --from=format /out / 66 | 67 | FROM deps AS lint 68 | RUN --mount=type=bind,target=.,rw \ 69 | --mount=type=cache,target=/src/.yarn/cache \ 70 | --mount=type=cache,target=/src/node_modules \ 71 | yarn run lint 72 | 73 | FROM deps AS test 74 | RUN apk add --no-cache gnupg 75 | ENV RUNNER_TEMP=/tmp/github_runner 76 | ENV RUNNER_TOOL_CACHE=/tmp/github_tool_cache 77 | RUN --mount=type=bind,target=.,rw \ 78 | --mount=type=cache,target=/src/.yarn/cache \ 79 | --mount=type=cache,target=/src/node_modules \ 80 | yarn run test --coverage --coverageDirectory=/tmp/coverage 81 | 82 | FROM scratch AS test-coverage 83 | COPY --from=test /tmp/coverage / 84 | -------------------------------------------------------------------------------- /dist/licenses.txt: -------------------------------------------------------------------------------- 1 | @actions/core 2 | MIT 3 | The MIT License (MIT) 4 | 5 | Copyright 2019 GitHub 6 | 7 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 8 | 9 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 10 | 11 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 12 | 13 | @actions/exec 14 | MIT 15 | The MIT License (MIT) 16 | 17 | Copyright 2019 GitHub 18 | 19 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 20 | 21 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 22 | 23 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 24 | 25 | @actions/http-client 26 | MIT 27 | Actions Http Client for Node.js 28 | 29 | Copyright (c) GitHub, Inc. 30 | 31 | All rights reserved. 32 | 33 | MIT License 34 | 35 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and 36 | associated documentation files (the "Software"), to deal in the Software without restriction, 37 | including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, 38 | and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, 39 | subject to the following conditions: 40 | 41 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 42 | 43 | THE SOFTWARE IS PROVIDED *AS IS*, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT 44 | LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN 45 | NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, 46 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE 47 | SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 48 | 49 | 50 | @actions/io 51 | MIT 52 | 53 | addressparser 54 | MIT 55 | Copyright (c) 2014-2016 Andris Reinman 56 | 57 | Permission is hereby granted, free of charge, to any person obtaining a copy 58 | of this software and associated documentation files (the "Software"), to deal 59 | in the Software without restriction, including without limitation the rights 60 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 61 | copies of the Software, and to permit persons to whom the Software is 62 | furnished to do so, subject to the following conditions: 63 | 64 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 65 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 66 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 67 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 68 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 69 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 70 | SOFTWARE. 71 | 72 | 73 | openpgp 74 | LGPL-3.0+ 75 | GNU LESSER GENERAL PUBLIC LICENSE 76 | Version 3, 29 June 2007 77 | 78 | Copyright (C) 2007 Free Software Foundation, Inc. 79 | Everyone is permitted to copy and distribute verbatim copies 80 | of this license document, but changing it is not allowed. 81 | 82 | 83 | This version of the GNU Lesser General Public License incorporates 84 | the terms and conditions of version 3 of the GNU General Public 85 | License, supplemented by the additional permissions listed below. 86 | 87 | 0. Additional Definitions. 88 | 89 | As used herein, "this License" refers to version 3 of the GNU Lesser 90 | General Public License, and the "GNU GPL" refers to version 3 of the GNU 91 | General Public License. 92 | 93 | "The Library" refers to a covered work governed by this License, 94 | other than an Application or a Combined Work as defined below. 95 | 96 | An "Application" is any work that makes use of an interface provided 97 | by the Library, but which is not otherwise based on the Library. 98 | Defining a subclass of a class defined by the Library is deemed a mode 99 | of using an interface provided by the Library. 100 | 101 | A "Combined Work" is a work produced by combining or linking an 102 | Application with the Library. The particular version of the Library 103 | with which the Combined Work was made is also called the "Linked 104 | Version". 105 | 106 | The "Minimal Corresponding Source" for a Combined Work means the 107 | Corresponding Source for the Combined Work, excluding any source code 108 | for portions of the Combined Work that, considered in isolation, are 109 | based on the Application, and not on the Linked Version. 110 | 111 | The "Corresponding Application Code" for a Combined Work means the 112 | object code and/or source code for the Application, including any data 113 | and utility programs needed for reproducing the Combined Work from the 114 | Application, but excluding the System Libraries of the Combined Work. 115 | 116 | 1. Exception to Section 3 of the GNU GPL. 117 | 118 | You may convey a covered work under sections 3 and 4 of this License 119 | without being bound by section 3 of the GNU GPL. 120 | 121 | 2. Conveying Modified Versions. 122 | 123 | If you modify a copy of the Library, and, in your modifications, a 124 | facility refers to a function or data to be supplied by an Application 125 | that uses the facility (other than as an argument passed when the 126 | facility is invoked), then you may convey a copy of the modified 127 | version: 128 | 129 | a) under this License, provided that you make a good faith effort to 130 | ensure that, in the event an Application does not supply the 131 | function or data, the facility still operates, and performs 132 | whatever part of its purpose remains meaningful, or 133 | 134 | b) under the GNU GPL, with none of the additional permissions of 135 | this License applicable to that copy. 136 | 137 | 3. Object Code Incorporating Material from Library Header Files. 138 | 139 | The object code form of an Application may incorporate material from 140 | a header file that is part of the Library. You may convey such object 141 | code under terms of your choice, provided that, if the incorporated 142 | material is not limited to numerical parameters, data structure 143 | layouts and accessors, or small macros, inline functions and templates 144 | (ten or fewer lines in length), you do both of the following: 145 | 146 | a) Give prominent notice with each copy of the object code that the 147 | Library is used in it and that the Library and its use are 148 | covered by this License. 149 | 150 | b) Accompany the object code with a copy of the GNU GPL and this license 151 | document. 152 | 153 | 4. Combined Works. 154 | 155 | You may convey a Combined Work under terms of your choice that, 156 | taken together, effectively do not restrict modification of the 157 | portions of the Library contained in the Combined Work and reverse 158 | engineering for debugging such modifications, if you also do each of 159 | the following: 160 | 161 | a) Give prominent notice with each copy of the Combined Work that 162 | the Library is used in it and that the Library and its use are 163 | covered by this License. 164 | 165 | b) Accompany the Combined Work with a copy of the GNU GPL and this license 166 | document. 167 | 168 | c) For a Combined Work that displays copyright notices during 169 | execution, include the copyright notice for the Library among 170 | these notices, as well as a reference directing the user to the 171 | copies of the GNU GPL and this license document. 172 | 173 | d) Do one of the following: 174 | 175 | 0) Convey the Minimal Corresponding Source under the terms of this 176 | License, and the Corresponding Application Code in a form 177 | suitable for, and under terms that permit, the user to 178 | recombine or relink the Application with a modified version of 179 | the Linked Version to produce a modified Combined Work, in the 180 | manner specified by section 6 of the GNU GPL for conveying 181 | Corresponding Source. 182 | 183 | 1) Use a suitable shared library mechanism for linking with the 184 | Library. A suitable mechanism is one that (a) uses at run time 185 | a copy of the Library already present on the user's computer 186 | system, and (b) will operate properly with a modified version 187 | of the Library that is interface-compatible with the Linked 188 | Version. 189 | 190 | e) Provide Installation Information, but only if you would otherwise 191 | be required to provide such information under section 6 of the 192 | GNU GPL, and only to the extent that such information is 193 | necessary to install and execute a modified version of the 194 | Combined Work produced by recombining or relinking the 195 | Application with a modified version of the Linked Version. (If 196 | you use option 4d0, the Installation Information must accompany 197 | the Minimal Corresponding Source and Corresponding Application 198 | Code. If you use option 4d1, you must provide the Installation 199 | Information in the manner specified by section 6 of the GNU GPL 200 | for conveying Corresponding Source.) 201 | 202 | 5. Combined Libraries. 203 | 204 | You may place library facilities that are a work based on the 205 | Library side by side in a single library together with other library 206 | facilities that are not Applications and are not covered by this 207 | License, and convey such a combined library under terms of your 208 | choice, if you do both of the following: 209 | 210 | a) Accompany the combined library with a copy of the same work based 211 | on the Library, uncombined with any other library facilities, 212 | conveyed under the terms of this License. 213 | 214 | b) Give prominent notice with the combined library that part of it 215 | is a work based on the Library, and explaining where to find the 216 | accompanying uncombined form of the same work. 217 | 218 | 6. Revised Versions of the GNU Lesser General Public License. 219 | 220 | The Free Software Foundation may publish revised and/or new versions 221 | of the GNU Lesser General Public License from time to time. Such new 222 | versions will be similar in spirit to the present version, but may 223 | differ in detail to address new problems or concerns. 224 | 225 | Each version is given a distinguishing version number. If the 226 | Library as you received it specifies that a certain numbered version 227 | of the GNU Lesser General Public License "or any later version" 228 | applies to it, you have the option of following the terms and 229 | conditions either of that published version or of any later version 230 | published by the Free Software Foundation. If the Library as you 231 | received it does not specify a version number of the GNU Lesser 232 | General Public License, you may choose any version of the GNU Lesser 233 | General Public License ever published by the Free Software Foundation. 234 | 235 | If the Library as you received it specifies that a proxy can decide 236 | whether future versions of the GNU Lesser General Public License shall 237 | apply, that proxy's public statement of acceptance of any version is 238 | permanent authorization for you to choose that version for the 239 | Library. 240 | 241 | 242 | tunnel 243 | MIT 244 | The MIT License (MIT) 245 | 246 | Copyright (c) 2012 Koichi Kobayashi 247 | 248 | Permission is hereby granted, free of charge, to any person obtaining a copy 249 | of this software and associated documentation files (the "Software"), to deal 250 | in the Software without restriction, including without limitation the rights 251 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 252 | copies of the Software, and to permit persons to whom the Software is 253 | furnished to do so, subject to the following conditions: 254 | 255 | The above copyright notice and this permission notice shall be included in 256 | all copies or substantial portions of the Software. 257 | 258 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 259 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 260 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 261 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 262 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 263 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN 264 | THE SOFTWARE. 265 | -------------------------------------------------------------------------------- /dist/sourcemap-register.js: -------------------------------------------------------------------------------- 1 | (()=>{var e={650:e=>{var r=Object.prototype.toString;var n=typeof Buffer.alloc==="function"&&typeof Buffer.allocUnsafe==="function"&&typeof Buffer.from==="function";function isArrayBuffer(e){return r.call(e).slice(8,-1)==="ArrayBuffer"}function fromArrayBuffer(e,r,t){r>>>=0;var o=e.byteLength-r;if(o<0){throw new RangeError("'offset' is out of bounds")}if(t===undefined){t=o}else{t>>>=0;if(t>o){throw new RangeError("'length' is out of bounds")}}return n?Buffer.from(e.slice(r,r+t)):new Buffer(new Uint8Array(e.slice(r,r+t)))}function fromString(e,r){if(typeof r!=="string"||r===""){r="utf8"}if(!Buffer.isEncoding(r)){throw new TypeError('"encoding" must be a valid string encoding')}return n?Buffer.from(e,r):new Buffer(e,r)}function bufferFrom(e,r,t){if(typeof e==="number"){throw new TypeError('"value" argument must not be a number')}if(isArrayBuffer(e)){return fromArrayBuffer(e,r,t)}if(typeof e==="string"){return fromString(e,r)}return n?Buffer.from(e):new Buffer(e)}e.exports=bufferFrom},274:(e,r,n)=>{var t=n(339);var o=Object.prototype.hasOwnProperty;var i=typeof Map!=="undefined";function ArraySet(){this._array=[];this._set=i?new Map:Object.create(null)}ArraySet.fromArray=function ArraySet_fromArray(e,r){var n=new ArraySet;for(var t=0,o=e.length;t=0){return r}}else{var n=t.toSetString(e);if(o.call(this._set,n)){return this._set[n]}}throw new Error('"'+e+'" is not in the set.')};ArraySet.prototype.at=function ArraySet_at(e){if(e>=0&&e{var t=n(190);var o=5;var i=1<>1;return r?-n:n}r.encode=function base64VLQ_encode(e){var r="";var n;var i=toVLQSigned(e);do{n=i&a;i>>>=o;if(i>0){n|=u}r+=t.encode(n)}while(i>0);return r};r.decode=function base64VLQ_decode(e,r,n){var i=e.length;var s=0;var l=0;var c,p;do{if(r>=i){throw new Error("Expected more digits in base 64 VLQ value.")}p=t.decode(e.charCodeAt(r++));if(p===-1){throw new Error("Invalid base64 digit: "+e.charAt(r-1))}c=!!(p&u);p&=a;s=s+(p<{var n="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".split("");r.encode=function(e){if(0<=e&&e{r.GREATEST_LOWER_BOUND=1;r.LEAST_UPPER_BOUND=2;function recursiveSearch(e,n,t,o,i,a){var u=Math.floor((n-e)/2)+e;var s=i(t,o[u],true);if(s===0){return u}else if(s>0){if(n-u>1){return recursiveSearch(u,n,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return n1){return recursiveSearch(e,u,t,o,i,a)}if(a==r.LEAST_UPPER_BOUND){return u}else{return e<0?-1:e}}}r.search=function search(e,n,t,o){if(n.length===0){return-1}var i=recursiveSearch(-1,n.length,e,n,t,o||r.GREATEST_LOWER_BOUND);if(i<0){return-1}while(i-1>=0){if(t(n[i],n[i-1],true)!==0){break}--i}return i}},680:(e,r,n)=>{var t=n(339);function generatedPositionAfter(e,r){var n=e.generatedLine;var o=r.generatedLine;var i=e.generatedColumn;var a=r.generatedColumn;return o>n||o==n&&a>=i||t.compareByGeneratedPositionsInflated(e,r)<=0}function MappingList(){this._array=[];this._sorted=true;this._last={generatedLine:-1,generatedColumn:0}}MappingList.prototype.unsortedForEach=function MappingList_forEach(e,r){this._array.forEach(e,r)};MappingList.prototype.add=function MappingList_add(e){if(generatedPositionAfter(this._last,e)){this._last=e;this._array.push(e)}else{this._sorted=false;this._array.push(e)}};MappingList.prototype.toArray=function MappingList_toArray(){if(!this._sorted){this._array.sort(t.compareByGeneratedPositionsInflated);this._sorted=true}return this._array};r.H=MappingList},758:(e,r)=>{function swap(e,r,n){var t=e[r];e[r]=e[n];e[n]=t}function randomIntInRange(e,r){return Math.round(e+Math.random()*(r-e))}function doQuickSort(e,r,n,t){if(n{var t;var o=n(339);var i=n(345);var a=n(274).I;var u=n(449);var s=n(758).U;function SourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}return n.sections!=null?new IndexedSourceMapConsumer(n,r):new BasicSourceMapConsumer(n,r)}SourceMapConsumer.fromSourceMap=function(e,r){return BasicSourceMapConsumer.fromSourceMap(e,r)};SourceMapConsumer.prototype._version=3;SourceMapConsumer.prototype.__generatedMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_generatedMappings",{configurable:true,enumerable:true,get:function(){if(!this.__generatedMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__generatedMappings}});SourceMapConsumer.prototype.__originalMappings=null;Object.defineProperty(SourceMapConsumer.prototype,"_originalMappings",{configurable:true,enumerable:true,get:function(){if(!this.__originalMappings){this._parseMappings(this._mappings,this.sourceRoot)}return this.__originalMappings}});SourceMapConsumer.prototype._charIsMappingSeparator=function SourceMapConsumer_charIsMappingSeparator(e,r){var n=e.charAt(r);return n===";"||n===","};SourceMapConsumer.prototype._parseMappings=function SourceMapConsumer_parseMappings(e,r){throw new Error("Subclasses must implement _parseMappings")};SourceMapConsumer.GENERATED_ORDER=1;SourceMapConsumer.ORIGINAL_ORDER=2;SourceMapConsumer.GREATEST_LOWER_BOUND=1;SourceMapConsumer.LEAST_UPPER_BOUND=2;SourceMapConsumer.prototype.eachMapping=function SourceMapConsumer_eachMapping(e,r,n){var t=r||null;var i=n||SourceMapConsumer.GENERATED_ORDER;var a;switch(i){case SourceMapConsumer.GENERATED_ORDER:a=this._generatedMappings;break;case SourceMapConsumer.ORIGINAL_ORDER:a=this._originalMappings;break;default:throw new Error("Unknown order of iteration.")}var u=this.sourceRoot;a.map((function(e){var r=e.source===null?null:this._sources.at(e.source);r=o.computeSourceURL(u,r,this._sourceMapURL);return{source:r,generatedLine:e.generatedLine,generatedColumn:e.generatedColumn,originalLine:e.originalLine,originalColumn:e.originalColumn,name:e.name===null?null:this._names.at(e.name)}}),this).forEach(e,t)};SourceMapConsumer.prototype.allGeneratedPositionsFor=function SourceMapConsumer_allGeneratedPositionsFor(e){var r=o.getArg(e,"line");var n={source:o.getArg(e,"source"),originalLine:r,originalColumn:o.getArg(e,"column",0)};n.source=this._findSourceIndex(n.source);if(n.source<0){return[]}var t=[];var a=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,i.LEAST_UPPER_BOUND);if(a>=0){var u=this._originalMappings[a];if(e.column===undefined){var s=u.originalLine;while(u&&u.originalLine===s){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}else{var l=u.originalColumn;while(u&&u.originalLine===r&&u.originalColumn==l){t.push({line:o.getArg(u,"generatedLine",null),column:o.getArg(u,"generatedColumn",null),lastColumn:o.getArg(u,"lastGeneratedColumn",null)});u=this._originalMappings[++a]}}}return t};r.SourceMapConsumer=SourceMapConsumer;function BasicSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sources");var u=o.getArg(n,"names",[]);var s=o.getArg(n,"sourceRoot",null);var l=o.getArg(n,"sourcesContent",null);var c=o.getArg(n,"mappings");var p=o.getArg(n,"file",null);if(t!=this._version){throw new Error("Unsupported version: "+t)}if(s){s=o.normalize(s)}i=i.map(String).map(o.normalize).map((function(e){return s&&o.isAbsolute(s)&&o.isAbsolute(e)?o.relative(s,e):e}));this._names=a.fromArray(u.map(String),true);this._sources=a.fromArray(i,true);this._absoluteSources=this._sources.toArray().map((function(e){return o.computeSourceURL(s,e,r)}));this.sourceRoot=s;this.sourcesContent=l;this._mappings=c;this._sourceMapURL=r;this.file=p}BasicSourceMapConsumer.prototype=Object.create(SourceMapConsumer.prototype);BasicSourceMapConsumer.prototype.consumer=SourceMapConsumer;BasicSourceMapConsumer.prototype._findSourceIndex=function(e){var r=e;if(this.sourceRoot!=null){r=o.relative(this.sourceRoot,r)}if(this._sources.has(r)){return this._sources.indexOf(r)}var n;for(n=0;n1){v.source=l+_[1];l+=_[1];v.originalLine=i+_[2];i=v.originalLine;v.originalLine+=1;v.originalColumn=a+_[3];a=v.originalColumn;if(_.length>4){v.name=c+_[4];c+=_[4]}}m.push(v);if(typeof v.originalLine==="number"){d.push(v)}}}s(m,o.compareByGeneratedPositionsDeflated);this.__generatedMappings=m;s(d,o.compareByOriginalPositions);this.__originalMappings=d};BasicSourceMapConsumer.prototype._findMapping=function SourceMapConsumer_findMapping(e,r,n,t,o,a){if(e[n]<=0){throw new TypeError("Line must be greater than or equal to 1, got "+e[n])}if(e[t]<0){throw new TypeError("Column must be greater than or equal to 0, got "+e[t])}return i.search(e,r,o,a)};BasicSourceMapConsumer.prototype.computeColumnSpans=function SourceMapConsumer_computeColumnSpans(){for(var e=0;e=0){var t=this._generatedMappings[n];if(t.generatedLine===r.generatedLine){var i=o.getArg(t,"source",null);if(i!==null){i=this._sources.at(i);i=o.computeSourceURL(this.sourceRoot,i,this._sourceMapURL)}var a=o.getArg(t,"name",null);if(a!==null){a=this._names.at(a)}return{source:i,line:o.getArg(t,"originalLine",null),column:o.getArg(t,"originalColumn",null),name:a}}}return{source:null,line:null,column:null,name:null}};BasicSourceMapConsumer.prototype.hasContentsOfAllSources=function BasicSourceMapConsumer_hasContentsOfAllSources(){if(!this.sourcesContent){return false}return this.sourcesContent.length>=this._sources.size()&&!this.sourcesContent.some((function(e){return e==null}))};BasicSourceMapConsumer.prototype.sourceContentFor=function SourceMapConsumer_sourceContentFor(e,r){if(!this.sourcesContent){return null}var n=this._findSourceIndex(e);if(n>=0){return this.sourcesContent[n]}var t=e;if(this.sourceRoot!=null){t=o.relative(this.sourceRoot,t)}var i;if(this.sourceRoot!=null&&(i=o.urlParse(this.sourceRoot))){var a=t.replace(/^file:\/\//,"");if(i.scheme=="file"&&this._sources.has(a)){return this.sourcesContent[this._sources.indexOf(a)]}if((!i.path||i.path=="/")&&this._sources.has("/"+t)){return this.sourcesContent[this._sources.indexOf("/"+t)]}}if(r){return null}else{throw new Error('"'+t+'" is not in the SourceMap.')}};BasicSourceMapConsumer.prototype.generatedPositionFor=function SourceMapConsumer_generatedPositionFor(e){var r=o.getArg(e,"source");r=this._findSourceIndex(r);if(r<0){return{line:null,column:null,lastColumn:null}}var n={source:r,originalLine:o.getArg(e,"line"),originalColumn:o.getArg(e,"column")};var t=this._findMapping(n,this._originalMappings,"originalLine","originalColumn",o.compareByOriginalPositions,o.getArg(e,"bias",SourceMapConsumer.GREATEST_LOWER_BOUND));if(t>=0){var i=this._originalMappings[t];if(i.source===n.source){return{line:o.getArg(i,"generatedLine",null),column:o.getArg(i,"generatedColumn",null),lastColumn:o.getArg(i,"lastGeneratedColumn",null)}}}return{line:null,column:null,lastColumn:null}};t=BasicSourceMapConsumer;function IndexedSourceMapConsumer(e,r){var n=e;if(typeof e==="string"){n=o.parseSourceMapInput(e)}var t=o.getArg(n,"version");var i=o.getArg(n,"sections");if(t!=this._version){throw new Error("Unsupported version: "+t)}this._sources=new a;this._names=new a;var u={line:-1,column:0};this._sections=i.map((function(e){if(e.url){throw new Error("Support for url field in sections not implemented.")}var n=o.getArg(e,"offset");var t=o.getArg(n,"line");var i=o.getArg(n,"column");if(t{var t=n(449);var o=n(339);var i=n(274).I;var a=n(680).H;function SourceMapGenerator(e){if(!e){e={}}this._file=o.getArg(e,"file",null);this._sourceRoot=o.getArg(e,"sourceRoot",null);this._skipValidation=o.getArg(e,"skipValidation",false);this._sources=new i;this._names=new i;this._mappings=new a;this._sourcesContents=null}SourceMapGenerator.prototype._version=3;SourceMapGenerator.fromSourceMap=function SourceMapGenerator_fromSourceMap(e){var r=e.sourceRoot;var n=new SourceMapGenerator({file:e.file,sourceRoot:r});e.eachMapping((function(e){var t={generated:{line:e.generatedLine,column:e.generatedColumn}};if(e.source!=null){t.source=e.source;if(r!=null){t.source=o.relative(r,t.source)}t.original={line:e.originalLine,column:e.originalColumn};if(e.name!=null){t.name=e.name}}n.addMapping(t)}));e.sources.forEach((function(t){var i=t;if(r!==null){i=o.relative(r,t)}if(!n._sources.has(i)){n._sources.add(i)}var a=e.sourceContentFor(t);if(a!=null){n.setSourceContent(t,a)}}));return n};SourceMapGenerator.prototype.addMapping=function SourceMapGenerator_addMapping(e){var r=o.getArg(e,"generated");var n=o.getArg(e,"original",null);var t=o.getArg(e,"source",null);var i=o.getArg(e,"name",null);if(!this._skipValidation){this._validateMapping(r,n,t,i)}if(t!=null){t=String(t);if(!this._sources.has(t)){this._sources.add(t)}}if(i!=null){i=String(i);if(!this._names.has(i)){this._names.add(i)}}this._mappings.add({generatedLine:r.line,generatedColumn:r.column,originalLine:n!=null&&n.line,originalColumn:n!=null&&n.column,source:t,name:i})};SourceMapGenerator.prototype.setSourceContent=function SourceMapGenerator_setSourceContent(e,r){var n=e;if(this._sourceRoot!=null){n=o.relative(this._sourceRoot,n)}if(r!=null){if(!this._sourcesContents){this._sourcesContents=Object.create(null)}this._sourcesContents[o.toSetString(n)]=r}else if(this._sourcesContents){delete this._sourcesContents[o.toSetString(n)];if(Object.keys(this._sourcesContents).length===0){this._sourcesContents=null}}};SourceMapGenerator.prototype.applySourceMap=function SourceMapGenerator_applySourceMap(e,r,n){var t=r;if(r==null){if(e.file==null){throw new Error("SourceMapGenerator.prototype.applySourceMap requires either an explicit source file, "+'or the source map\'s "file" property. Both were omitted.')}t=e.file}var a=this._sourceRoot;if(a!=null){t=o.relative(a,t)}var u=new i;var s=new i;this._mappings.unsortedForEach((function(r){if(r.source===t&&r.originalLine!=null){var i=e.originalPositionFor({line:r.originalLine,column:r.originalColumn});if(i.source!=null){r.source=i.source;if(n!=null){r.source=o.join(n,r.source)}if(a!=null){r.source=o.relative(a,r.source)}r.originalLine=i.line;r.originalColumn=i.column;if(i.name!=null){r.name=i.name}}}var l=r.source;if(l!=null&&!u.has(l)){u.add(l)}var c=r.name;if(c!=null&&!s.has(c)){s.add(c)}}),this);this._sources=u;this._names=s;e.sources.forEach((function(r){var t=e.sourceContentFor(r);if(t!=null){if(n!=null){r=o.join(n,r)}if(a!=null){r=o.relative(a,r)}this.setSourceContent(r,t)}}),this)};SourceMapGenerator.prototype._validateMapping=function SourceMapGenerator_validateMapping(e,r,n,t){if(r&&typeof r.line!=="number"&&typeof r.column!=="number"){throw new Error("original.line and original.column are not numbers -- you probably meant to omit "+"the original mapping entirely and only map the generated position. If so, pass "+"null for the original mapping instead of an object with empty or null values.")}if(e&&"line"in e&&"column"in e&&e.line>0&&e.column>=0&&!r&&!n&&!t){return}else if(e&&"line"in e&&"column"in e&&r&&"line"in r&&"column"in r&&e.line>0&&e.column>=0&&r.line>0&&r.column>=0&&n){return}else{throw new Error("Invalid mapping: "+JSON.stringify({generated:e,source:n,original:r,name:t}))}};SourceMapGenerator.prototype._serializeMappings=function SourceMapGenerator_serializeMappings(){var e=0;var r=1;var n=0;var i=0;var a=0;var u=0;var s="";var l;var c;var p;var f;var g=this._mappings.toArray();for(var h=0,d=g.length;h0){if(!o.compareByGeneratedPositionsInflated(c,g[h-1])){continue}l+=","}}l+=t.encode(c.generatedColumn-e);e=c.generatedColumn;if(c.source!=null){f=this._sources.indexOf(c.source);l+=t.encode(f-u);u=f;l+=t.encode(c.originalLine-1-i);i=c.originalLine-1;l+=t.encode(c.originalColumn-n);n=c.originalColumn;if(c.name!=null){p=this._names.indexOf(c.name);l+=t.encode(p-a);a=p}}s+=l}return s};SourceMapGenerator.prototype._generateSourcesContent=function SourceMapGenerator_generateSourcesContent(e,r){return e.map((function(e){if(!this._sourcesContents){return null}if(r!=null){e=o.relative(r,e)}var n=o.toSetString(e);return Object.prototype.hasOwnProperty.call(this._sourcesContents,n)?this._sourcesContents[n]:null}),this)};SourceMapGenerator.prototype.toJSON=function SourceMapGenerator_toJSON(){var e={version:this._version,sources:this._sources.toArray(),names:this._names.toArray(),mappings:this._serializeMappings()};if(this._file!=null){e.file=this._file}if(this._sourceRoot!=null){e.sourceRoot=this._sourceRoot}if(this._sourcesContents){e.sourcesContent=this._generateSourcesContent(e.sources,e.sourceRoot)}return e};SourceMapGenerator.prototype.toString=function SourceMapGenerator_toString(){return JSON.stringify(this.toJSON())};r.h=SourceMapGenerator},351:(e,r,n)=>{var t;var o=n(591).h;var i=n(339);var a=/(\r?\n)/;var u=10;var s="$$$isSourceNode$$$";function SourceNode(e,r,n,t,o){this.children=[];this.sourceContents={};this.line=e==null?null:e;this.column=r==null?null:r;this.source=n==null?null:n;this.name=o==null?null:o;this[s]=true;if(t!=null)this.add(t)}SourceNode.fromStringWithSourceMap=function SourceNode_fromStringWithSourceMap(e,r,n){var t=new SourceNode;var o=e.split(a);var u=0;var shiftNextLine=function(){var e=getNextLine();var r=getNextLine()||"";return e+r;function getNextLine(){return u=0;r--){this.prepend(e[r])}}else if(e[s]||typeof e==="string"){this.children.unshift(e)}else{throw new TypeError("Expected a SourceNode, string, or an array of SourceNodes and strings. Got "+e)}return this};SourceNode.prototype.walk=function SourceNode_walk(e){var r;for(var n=0,t=this.children.length;n0){r=[];for(n=0;n{function getArg(e,r,n){if(r in e){return e[r]}else if(arguments.length===3){return n}else{throw new Error('"'+r+'" is a required argument.')}}r.getArg=getArg;var n=/^(?:([\w+\-.]+):)?\/\/(?:(\w+:\w+)@)?([\w.-]*)(?::(\d+))?(.*)$/;var t=/^data:.+\,.+$/;function urlParse(e){var r=e.match(n);if(!r){return null}return{scheme:r[1],auth:r[2],host:r[3],port:r[4],path:r[5]}}r.urlParse=urlParse;function urlGenerate(e){var r="";if(e.scheme){r+=e.scheme+":"}r+="//";if(e.auth){r+=e.auth+"@"}if(e.host){r+=e.host}if(e.port){r+=":"+e.port}if(e.path){r+=e.path}return r}r.urlGenerate=urlGenerate;function normalize(e){var n=e;var t=urlParse(e);if(t){if(!t.path){return e}n=t.path}var o=r.isAbsolute(n);var i=n.split(/\/+/);for(var a,u=0,s=i.length-1;s>=0;s--){a=i[s];if(a==="."){i.splice(s,1)}else if(a===".."){u++}else if(u>0){if(a===""){i.splice(s+1,u);u=0}else{i.splice(s,2);u--}}}n=i.join("/");if(n===""){n=o?"/":"."}if(t){t.path=n;return urlGenerate(t)}return n}r.normalize=normalize;function join(e,r){if(e===""){e="."}if(r===""){r="."}var n=urlParse(r);var o=urlParse(e);if(o){e=o.path||"/"}if(n&&!n.scheme){if(o){n.scheme=o.scheme}return urlGenerate(n)}if(n||r.match(t)){return r}if(o&&!o.host&&!o.path){o.host=r;return urlGenerate(o)}var i=r.charAt(0)==="/"?r:normalize(e.replace(/\/+$/,"")+"/"+r);if(o){o.path=i;return urlGenerate(o)}return i}r.join=join;r.isAbsolute=function(e){return e.charAt(0)==="/"||n.test(e)};function relative(e,r){if(e===""){e="."}e=e.replace(/\/$/,"");var n=0;while(r.indexOf(e+"/")!==0){var t=e.lastIndexOf("/");if(t<0){return r}e=e.slice(0,t);if(e.match(/^([^\/]+:\/)?\/*$/)){return r}++n}return Array(n+1).join("../")+r.substr(e.length+1)}r.relative=relative;var o=function(){var e=Object.create(null);return!("__proto__"in e)}();function identity(e){return e}function toSetString(e){if(isProtoString(e)){return"$"+e}return e}r.toSetString=o?identity:toSetString;function fromSetString(e){if(isProtoString(e)){return e.slice(1)}return e}r.fromSetString=o?identity:fromSetString;function isProtoString(e){if(!e){return false}var r=e.length;if(r<9){return false}if(e.charCodeAt(r-1)!==95||e.charCodeAt(r-2)!==95||e.charCodeAt(r-3)!==111||e.charCodeAt(r-4)!==116||e.charCodeAt(r-5)!==111||e.charCodeAt(r-6)!==114||e.charCodeAt(r-7)!==112||e.charCodeAt(r-8)!==95||e.charCodeAt(r-9)!==95){return false}for(var n=r-10;n>=0;n--){if(e.charCodeAt(n)!==36){return false}}return true}function compareByOriginalPositions(e,r,n){var t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0||n){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0){return t}t=e.generatedLine-r.generatedLine;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByOriginalPositions=compareByOriginalPositions;function compareByGeneratedPositionsDeflated(e,r,n){var t=e.generatedLine-r.generatedLine;if(t!==0){return t}t=e.generatedColumn-r.generatedColumn;if(t!==0||n){return t}t=strcmp(e.source,r.source);if(t!==0){return t}t=e.originalLine-r.originalLine;if(t!==0){return t}t=e.originalColumn-r.originalColumn;if(t!==0){return t}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsDeflated=compareByGeneratedPositionsDeflated;function strcmp(e,r){if(e===r){return 0}if(e===null){return 1}if(r===null){return-1}if(e>r){return 1}return-1}function compareByGeneratedPositionsInflated(e,r){var n=e.generatedLine-r.generatedLine;if(n!==0){return n}n=e.generatedColumn-r.generatedColumn;if(n!==0){return n}n=strcmp(e.source,r.source);if(n!==0){return n}n=e.originalLine-r.originalLine;if(n!==0){return n}n=e.originalColumn-r.originalColumn;if(n!==0){return n}return strcmp(e.name,r.name)}r.compareByGeneratedPositionsInflated=compareByGeneratedPositionsInflated;function parseSourceMapInput(e){return JSON.parse(e.replace(/^\)]}'[^\n]*\n/,""))}r.parseSourceMapInput=parseSourceMapInput;function computeSourceURL(e,r,n){r=r||"";if(e){if(e[e.length-1]!=="/"&&r[0]!=="/"){e+="/"}r=e+r}if(n){var t=urlParse(n);if(!t){throw new Error("sourceMapURL could not be parsed")}if(t.path){var o=t.path.lastIndexOf("/");if(o>=0){t.path=t.path.substring(0,o+1)}}r=join(urlGenerate(t),r)}return normalize(r)}r.computeSourceURL=computeSourceURL},997:(e,r,n)=>{n(591).h;r.SourceMapConsumer=n(952).SourceMapConsumer;n(351)},284:(e,r,n)=>{e=n.nmd(e);var t=n(997).SourceMapConsumer;var o=n(17);var i;try{i=n(147);if(!i.existsSync||!i.readFileSync){i=null}}catch(e){}var a=n(650);function dynamicRequire(e,r){return e.require(r)}var u=false;var s=false;var l=false;var c="auto";var p={};var f={};var g=/^data:application\/json[^,]+base64,/;var h=[];var d=[];function isInBrowser(){if(c==="browser")return true;if(c==="node")return false;return typeof window!=="undefined"&&typeof XMLHttpRequest==="function"&&!(window.require&&window.module&&window.process&&window.process.type==="renderer")}function hasGlobalProcessEventEmitter(){return typeof process==="object"&&process!==null&&typeof process.on==="function"}function globalProcessVersion(){if(typeof process==="object"&&process!==null){return process.version}else{return""}}function globalProcessStderr(){if(typeof process==="object"&&process!==null){return process.stderr}}function globalProcessExit(e){if(typeof process==="object"&&process!==null&&typeof process.exit==="function"){return process.exit(e)}}function handlerExec(e){return function(r){for(var n=0;n"}var n=this.getLineNumber();if(n!=null){r+=":"+n;var t=this.getColumnNumber();if(t){r+=":"+t}}}var o="";var i=this.getFunctionName();var a=true;var u=this.isConstructor();var s=!(this.isToplevel()||u);if(s){var l=this.getTypeName();if(l==="[object Object]"){l="null"}var c=this.getMethodName();if(i){if(l&&i.indexOf(l)!=0){o+=l+"."}o+=i;if(c&&i.indexOf("."+c)!=i.length-c.length-1){o+=" [as "+c+"]"}}else{o+=l+"."+(c||"")}}else if(u){o+="new "+(i||"")}else if(i){o+=i}else{o+=r;a=false}if(a){o+=" ("+r+")"}return o}function cloneCallSite(e){var r={};Object.getOwnPropertyNames(Object.getPrototypeOf(e)).forEach((function(n){r[n]=/^(?:is|get)/.test(n)?function(){return e[n].call(e)}:e[n]}));r.toString=CallSiteToString;return r}function wrapCallSite(e,r){if(r===undefined){r={nextPosition:null,curPosition:null}}if(e.isNative()){r.curPosition=null;return e}var n=e.getFileName()||e.getScriptNameOrSourceURL();if(n){var t=e.getLineNumber();var o=e.getColumnNumber()-1;var i=/^v(10\.1[6-9]|10\.[2-9][0-9]|10\.[0-9]{3,}|1[2-9]\d*|[2-9]\d|\d{3,}|11\.11)/;var a=i.test(globalProcessVersion())?0:62;if(t===1&&o>a&&!isInBrowser()&&!e.isEval()){o-=a}var u=mapSourcePosition({source:n,line:t,column:o});r.curPosition=u;e=cloneCallSite(e);var s=e.getFunctionName;e.getFunctionName=function(){if(r.nextPosition==null){return s()}return r.nextPosition.name||s()};e.getFileName=function(){return u.source};e.getLineNumber=function(){return u.line};e.getColumnNumber=function(){return u.column+1};e.getScriptNameOrSourceURL=function(){return u.source};return e}var l=e.isEval()&&e.getEvalOrigin();if(l){l=mapEvalOrigin(l);e=cloneCallSite(e);e.getEvalOrigin=function(){return l};return e}return e}function prepareStackTrace(e,r){if(l){p={};f={}}var n=e.name||"Error";var t=e.message||"";var o=n+": "+t;var i={nextPosition:null,curPosition:null};var a=[];for(var u=r.length-1;u>=0;u--){a.push("\n at "+wrapCallSite(r[u],i));i.nextPosition=i.curPosition}i.curPosition=i.nextPosition=null;return o+a.reverse().join("")}function getErrorSource(e){var r=/\n at [^(]+ \((.*):(\d+):(\d+)\)/.exec(e.stack);if(r){var n=r[1];var t=+r[2];var o=+r[3];var a=p[n];if(!a&&i&&i.existsSync(n)){try{a=i.readFileSync(n,"utf8")}catch(e){a=""}}if(a){var u=a.split(/(?:\r\n|\r|\n)/)[t-1];if(u){return n+":"+t+"\n"+u+"\n"+new Array(o).join(" ")+"^"}}}return null}function printErrorAndExit(e){var r=getErrorSource(e);var n=globalProcessStderr();if(n&&n._handle&&n._handle.setBlocking){n._handle.setBlocking(true)}if(r){console.error();console.error(r)}console.error(e.stack);globalProcessExit(1)}function shimEmitUncaughtException(){var e=process.emit;process.emit=function(r){if(r==="uncaughtException"){var n=arguments[1]&&arguments[1].stack;var t=this.listeners(r).length>0;if(n&&!t){return printErrorAndExit(arguments[1])}}return e.apply(this,arguments)}}var S=h.slice(0);var _=d.slice(0);r.wrapCallSite=wrapCallSite;r.getErrorSource=getErrorSource;r.mapSourcePosition=mapSourcePosition;r.retrieveSourceMap=v;r.install=function(r){r=r||{};if(r.environment){c=r.environment;if(["node","browser","auto"].indexOf(c)===-1){throw new Error("environment "+c+" was unknown. Available options are {auto, browser, node}")}}if(r.retrieveFile){if(r.overrideRetrieveFile){h.length=0}h.unshift(r.retrieveFile)}if(r.retrieveSourceMap){if(r.overrideRetrieveSourceMap){d.length=0}d.unshift(r.retrieveSourceMap)}if(r.hookRequire&&!isInBrowser()){var n=dynamicRequire(e,"module");var t=n.prototype._compile;if(!t.__sourceMapSupport){n.prototype._compile=function(e,r){p[r]=e;f[r]=undefined;return t.call(this,e,r)};n.prototype._compile.__sourceMapSupport=true}}if(!l){l="emptyCacheBetweenOperations"in r?r.emptyCacheBetweenOperations:false}if(!u){u=true;Error.prepareStackTrace=prepareStackTrace}if(!s){var o="handleUncaughtExceptions"in r?r.handleUncaughtExceptions:true;try{var i=dynamicRequire(e,"worker_threads");if(i.isMainThread===false){o=false}}catch(e){}if(o&&hasGlobalProcessEventEmitter()){s=true;shimEmitUncaughtException()}}};r.resetRetrieveHandlers=function(){h.length=0;d.length=0;h=S.slice(0);d=_.slice(0);v=handlerExec(d);m=handlerExec(h)}},147:e=>{"use strict";e.exports=require("fs")},17:e=>{"use strict";e.exports=require("path")}};var r={};function __webpack_require__(n){var t=r[n];if(t!==undefined){return t.exports}var o=r[n]={id:n,loaded:false,exports:{}};var i=true;try{e[n](o,o.exports,__webpack_require__);i=false}finally{if(i)delete r[n]}o.loaded=true;return o.exports}(()=>{__webpack_require__.nmd=e=>{e.paths=[];if(!e.children)e.children=[];return e}})();if(typeof __webpack_require__!=="undefined")__webpack_require__.ab=__dirname+"/";var n={};(()=>{__webpack_require__(284).install()})();module.exports=n})(); -------------------------------------------------------------------------------- /docker-bake.hcl: -------------------------------------------------------------------------------- 1 | target "_common" { 2 | args = { 3 | BUILDKIT_CONTEXT_KEEP_GIT_DIR = 1 4 | } 5 | } 6 | 7 | group "default" { 8 | targets = ["build"] 9 | } 10 | 11 | group "pre-checkin" { 12 | targets = ["vendor", "format", "build"] 13 | } 14 | 15 | group "validate" { 16 | targets = ["lint", "build-validate", "vendor-validate"] 17 | } 18 | 19 | target "build" { 20 | dockerfile = "dev.Dockerfile" 21 | target = "build-update" 22 | output = ["."] 23 | } 24 | 25 | target "build-validate" { 26 | inherits = ["_common"] 27 | dockerfile = "dev.Dockerfile" 28 | target = "build-validate" 29 | output = ["type=cacheonly"] 30 | } 31 | 32 | target "format" { 33 | dockerfile = "dev.Dockerfile" 34 | target = "format-update" 35 | output = ["."] 36 | } 37 | 38 | target "lint" { 39 | dockerfile = "dev.Dockerfile" 40 | target = "lint" 41 | output = ["type=cacheonly"] 42 | } 43 | 44 | target "vendor" { 45 | dockerfile = "dev.Dockerfile" 46 | target = "vendor-update" 47 | output = ["."] 48 | } 49 | 50 | target "vendor-validate" { 51 | inherits = ["_common"] 52 | dockerfile = "dev.Dockerfile" 53 | target = "vendor-validate" 54 | output = ["type=cacheonly"] 55 | } 56 | 57 | target "test" { 58 | dockerfile = "dev.Dockerfile" 59 | target = "test-coverage" 60 | output = ["./coverage"] 61 | } 62 | -------------------------------------------------------------------------------- /jest.config.ts: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | clearMocks: true, 3 | testEnvironment: 'node', 4 | moduleFileExtensions: ['js', 'ts'], 5 | testMatch: ['**/*.test.ts'], 6 | transform: { 7 | '^.+\\.ts$': 'ts-jest' 8 | }, 9 | collectCoverageFrom: ['src/**/{!(main.ts),}.ts'], 10 | coveragePathIgnorePatterns: ['dist/', 'node_modules/', '__tests__/'], 11 | verbose: true 12 | }; 13 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "import-gpg", 3 | "description": "GitHub Action to easily import a GPG key", 4 | "main": "src/main.ts", 5 | "scripts": { 6 | "build": "ncc build src/main.ts --source-map --minify --license licenses.txt", 7 | "lint": "yarn run prettier && yarn run eslint", 8 | "format": "yarn run prettier:fix && yarn run eslint:fix", 9 | "eslint": "eslint --max-warnings=0 .", 10 | "eslint:fix": "eslint --fix .", 11 | "prettier": "prettier --check \"./**/*.ts\"", 12 | "prettier:fix": "prettier --write \"./**/*.ts\"", 13 | "test": "jest", 14 | "all": "yarn run build && yarn run format && yarn test" 15 | }, 16 | "repository": { 17 | "type": "git", 18 | "url": "git+https://github.com/crazy-max/ghaction-import-gpg.git" 19 | }, 20 | "keywords": [ 21 | "actions", 22 | "gpg", 23 | "signing", 24 | "git" 25 | ], 26 | "author": "CrazyMax", 27 | "license": "MIT", 28 | "packageManager": "yarn@3.6.3", 29 | "dependencies": { 30 | "@actions/core": "^1.11.1", 31 | "@actions/exec": "^1.1.1", 32 | "addressparser": "^1.0.1", 33 | "openpgp": "^6.1.0" 34 | }, 35 | "devDependencies": { 36 | "@types/node": "^20.6.0", 37 | "@typescript-eslint/eslint-plugin": "^6.6.0", 38 | "@typescript-eslint/parser": "^6.6.0", 39 | "@vercel/ncc": "^0.38.0", 40 | "eslint": "^8.49.0", 41 | "eslint-config-prettier": "^9.0.0", 42 | "eslint-plugin-jest": "^27.2.3", 43 | "eslint-plugin-prettier": "^5.0.0", 44 | "jest": "^29.6.4", 45 | "prettier": "^3.0.3", 46 | "ts-jest": "^29.1.1", 47 | "ts-node": "^10.9.1", 48 | "typescript": "^5.2.2" 49 | } 50 | } 51 | -------------------------------------------------------------------------------- /src/addressparser.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace addressparser { 2 | interface Address { 3 | name: string; 4 | address: string; 5 | } 6 | } 7 | 8 | /** 9 | * Parses structured e-mail addresses from an address field 10 | * 11 | * Example: 12 | * 13 | * 'Name ' 14 | * 15 | * will be converted to 16 | * 17 | * [{name: 'Name', address: 'address@domain'}] 18 | * 19 | * @param str Address field 20 | * @return An array of address objects 21 | */ 22 | declare function addressparser(address: string): addressparser.Address[]; 23 | 24 | export = addressparser; 25 | -------------------------------------------------------------------------------- /src/context.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | 3 | export interface Inputs { 4 | gpgPrivateKey: string; 5 | passphrase: string; 6 | trustLevel: string; 7 | gitConfigGlobal: boolean; 8 | gitUserSigningkey: boolean; 9 | gitCommitGpgsign: boolean; 10 | gitTagGpgsign: boolean; 11 | gitPushGpgsign: string; 12 | gitCommitterName: string; 13 | gitCommitterEmail: string; 14 | workdir: string; 15 | fingerprint: string; 16 | } 17 | 18 | export async function getInputs(): Promise { 19 | return { 20 | gpgPrivateKey: core.getInput('gpg_private_key', {required: true}), 21 | passphrase: core.getInput('passphrase'), 22 | trustLevel: core.getInput('trust_level'), 23 | gitConfigGlobal: core.getBooleanInput('git_config_global'), 24 | gitUserSigningkey: core.getBooleanInput('git_user_signingkey'), 25 | gitCommitGpgsign: core.getBooleanInput('git_commit_gpgsign'), 26 | gitTagGpgsign: core.getBooleanInput('git_tag_gpgsign'), 27 | gitPushGpgsign: core.getInput('git_push_gpgsign') || 'if-asked', 28 | gitCommitterName: core.getInput('git_committer_name'), 29 | gitCommitterEmail: core.getInput('git_committer_email'), 30 | workdir: core.getInput('workdir') || '.', 31 | fingerprint: core.getInput('fingerprint') 32 | }; 33 | } 34 | -------------------------------------------------------------------------------- /src/git.ts: -------------------------------------------------------------------------------- 1 | import * as exec from '@actions/exec'; 2 | 3 | const git = async (args: string[] = []): Promise => { 4 | return await exec 5 | .getExecOutput(`git`, args, { 6 | ignoreReturnCode: true, 7 | silent: true 8 | }) 9 | .then(res => { 10 | if (res.stderr.length > 0 && res.exitCode != 0) { 11 | throw new Error(res.stderr); 12 | } 13 | return res.stdout.trim(); 14 | }); 15 | }; 16 | 17 | export async function setConfig(key: string, value: string, global: boolean): Promise { 18 | const args: Array = ['config']; 19 | if (global) { 20 | args.push('--global'); 21 | } 22 | args.push(key, value); 23 | await git(args); 24 | } 25 | -------------------------------------------------------------------------------- /src/gpg.ts: -------------------------------------------------------------------------------- 1 | import * as exec from '@actions/exec'; 2 | import * as fs from 'fs'; 3 | import * as path from 'path'; 4 | import * as os from 'os'; 5 | import * as openpgp from './openpgp'; 6 | 7 | export const agentConfig = `default-cache-ttl 21600 8 | max-cache-ttl 31536000 9 | allow-preset-passphrase`; 10 | 11 | export interface Version { 12 | gnupg: string; 13 | libgcrypt: string; 14 | } 15 | 16 | export interface Dirs { 17 | libdir: string; 18 | libexecdir: string; 19 | datadir: string; 20 | homedir: string; 21 | } 22 | 23 | const gpgConnectAgent = async (command: string): Promise => { 24 | return await exec 25 | .getExecOutput(`gpg-connect-agent "${command}" /bye`, [], { 26 | ignoreReturnCode: true, 27 | silent: true 28 | }) 29 | .then(res => { 30 | if (res.stderr.length > 0 && res.exitCode != 0) { 31 | throw new Error(res.stderr); 32 | } 33 | for (const line of res.stdout.replace(/\r/g, '').trim().split(/\n/g)) { 34 | if (line.startsWith('ERR')) { 35 | throw new Error(line); 36 | } 37 | } 38 | return res.stdout.trim(); 39 | }); 40 | }; 41 | 42 | export const getHome = async (): Promise => { 43 | let homedir = ''; 44 | if (process.env.GNUPGHOME) { 45 | homedir = process.env.GNUPGHOME; 46 | } else if (os.platform() == 'win32' && !process.env.HOME && process.env.USERPROFILE) { 47 | homedir = path.join(process.env.USERPROFILE, '.gnupg'); 48 | } else if (process.env.HOME) { 49 | homedir = path.join(process.env.HOME, '.gnupg'); 50 | } else { 51 | homedir = (await getDirs()).homedir; 52 | } 53 | if (homedir.length == 0) { 54 | throw new Error('Unable to determine GnuPG home directory'); 55 | } 56 | if (!fs.existsSync(homedir)) { 57 | fs.mkdirSync(homedir, {recursive: true}); 58 | } 59 | return homedir; 60 | }; 61 | 62 | export const getVersion = async (): Promise => { 63 | return await exec 64 | .getExecOutput('gpg', ['--version'], { 65 | ignoreReturnCode: true, 66 | silent: true 67 | }) 68 | .then(res => { 69 | if (res.stderr.length > 0 && res.exitCode != 0) { 70 | throw new Error(res.stderr); 71 | } 72 | 73 | let gnupgVersion = ''; 74 | let libgcryptVersion = ''; 75 | 76 | for (const line of res.stdout.replace(/\r/g, '').trim().split(/\n/g)) { 77 | if (line.startsWith('gpg (GnuPG) ')) { 78 | gnupgVersion = line.substr('gpg (GnuPG) '.length).trim(); 79 | } else if (line.startsWith('gpg (GnuPG/MacGPG2) ')) { 80 | gnupgVersion = line.substr('gpg (GnuPG/MacGPG2) '.length).trim(); 81 | } else if (line.startsWith('libgcrypt ')) { 82 | libgcryptVersion = line.substr('libgcrypt '.length).trim(); 83 | } 84 | } 85 | 86 | return { 87 | gnupg: gnupgVersion, 88 | libgcrypt: libgcryptVersion 89 | }; 90 | }); 91 | }; 92 | 93 | export const getDirs = async (): Promise => { 94 | return await exec 95 | .getExecOutput('gpgconf', ['--list-dirs'], { 96 | ignoreReturnCode: true, 97 | silent: true 98 | }) 99 | .then(res => { 100 | if (res.stderr.length > 0 && res.exitCode != 0) { 101 | throw new Error(res.stderr); 102 | } 103 | 104 | let libdir = ''; 105 | let libexecdir = ''; 106 | let datadir = ''; 107 | let homedir = ''; 108 | 109 | for (const line of res.stdout.replace(/\r/g, '').trim().split(/\n/g)) { 110 | if (line.startsWith('libdir:')) { 111 | libdir = line.substr('libdir:'.length).replace('%3a', ':').trim(); 112 | } else if (line.startsWith('libexecdir:')) { 113 | libexecdir = line.substr('libexecdir:'.length).replace('%3a', ':').trim(); 114 | } else if (line.startsWith('datadir:')) { 115 | datadir = line.substr('datadir:'.length).replace('%3a', ':').trim(); 116 | } else if (line.startsWith('homedir:')) { 117 | homedir = line.substr('homedir:'.length).replace('%3a', ':').trim(); 118 | } 119 | } 120 | 121 | return { 122 | libdir: libdir, 123 | libexecdir: libexecdir, 124 | datadir: datadir, 125 | homedir: homedir 126 | }; 127 | }); 128 | }; 129 | 130 | export const importKey = async (key: string): Promise => { 131 | const keyFolder: string = fs.mkdtempSync(path.join(os.tmpdir(), 'ghaction-import-gpg-')); 132 | const keyPath = `${keyFolder}/key.pgp`; 133 | fs.writeFileSync(keyPath, (await openpgp.isArmored(key)) ? key : Buffer.from(key, 'base64').toString(), {mode: 0o600}); 134 | 135 | return await exec 136 | .getExecOutput('gpg', ['--import', '--batch', '--yes', keyPath], { 137 | ignoreReturnCode: true, 138 | silent: true 139 | }) 140 | .then(res => { 141 | if (res.stderr.length > 0 && res.exitCode != 0) { 142 | throw new Error(res.stderr); 143 | } 144 | if (res.stderr != '') { 145 | return res.stderr.trim(); 146 | } 147 | return res.stdout.trim(); 148 | }) 149 | .finally(() => { 150 | fs.unlinkSync(keyPath); 151 | }); 152 | }; 153 | 154 | export const getKeygrips = async (fingerprint: string): Promise> => { 155 | return await exec 156 | .getExecOutput('gpg', ['--batch', '--with-colons', '--with-keygrip', '--list-secret-keys', fingerprint], { 157 | ignoreReturnCode: true, 158 | silent: true 159 | }) 160 | .then(res => { 161 | const keygrips: Array = []; 162 | for (const line of res.stdout.replace(/\r/g, '').trim().split(/\n/g)) { 163 | if (line.startsWith('grp')) { 164 | keygrips.push(line.replace(/(grp|:)/g, '').trim()); 165 | } 166 | } 167 | return keygrips; 168 | }); 169 | }; 170 | 171 | export const parseKeygripFromGpgColonsOutput = (output: string, fingerprint: string): string => { 172 | let keygrip = ''; 173 | let fingerPrintFound = false; 174 | const lines = output.replace(/\r/g, '').trim().split(/\n/g); 175 | 176 | for (const line of lines) { 177 | if (line.startsWith(`fpr:`) && line.includes(`:${fingerprint}:`)) { 178 | // We reach the record with the matching fingerprint. 179 | // The next keygrip record is the keygrip for this fingerprint. 180 | fingerPrintFound = true; 181 | continue; 182 | } 183 | 184 | if (line.startsWith('grp:') && fingerPrintFound) { 185 | keygrip = line.replace(/(grp|:)/g, '').trim(); 186 | break; 187 | } 188 | } 189 | 190 | return keygrip; 191 | }; 192 | 193 | export const getKeygrip = async (fingerprint: string): Promise => { 194 | return await exec 195 | .getExecOutput('gpg', ['--batch', '--with-colons', '--with-keygrip', '--list-secret-keys', fingerprint], { 196 | ignoreReturnCode: true, 197 | silent: true 198 | }) 199 | .then(res => { 200 | return parseKeygripFromGpgColonsOutput(res.stdout, fingerprint); 201 | }); 202 | }; 203 | 204 | export const configureAgent = async (homedir: string, config: string): Promise => { 205 | const gpgAgentConf = path.join(homedir, 'gpg-agent.conf'); 206 | await fs.writeFile(gpgAgentConf, config, function (err) { 207 | if (err) throw err; 208 | }); 209 | await gpgConnectAgent('RELOADAGENT'); 210 | }; 211 | 212 | export const presetPassphrase = async (keygrip: string, passphrase: string): Promise => { 213 | const hexPassphrase: string = Buffer.from(passphrase, 'utf8').toString('hex').toUpperCase(); 214 | await gpgConnectAgent(`PRESET_PASSPHRASE ${keygrip} -1 ${hexPassphrase}`); 215 | return await gpgConnectAgent(`KEYINFO ${keygrip}`); 216 | }; 217 | 218 | export const setTrustLevel = async (keyID: string, trust: string): Promise => { 219 | await exec 220 | .getExecOutput('gpg', ['--batch', '--no-tty', '--command-fd', '0', '--edit-key', keyID], { 221 | ignoreReturnCode: true, 222 | silent: true, 223 | input: Buffer.from(`trust\n${trust}\ny\nquit\n`) 224 | }) 225 | .then(res => { 226 | if (res.stderr.length > 0 && res.exitCode != 0) { 227 | throw new Error(res.stderr); 228 | } 229 | }); 230 | }; 231 | 232 | export const deleteKey = async (fingerprint: string): Promise => { 233 | await exec 234 | .getExecOutput('gpg', ['--batch', '--yes', '--delete-secret-keys', fingerprint], { 235 | ignoreReturnCode: true, 236 | silent: true 237 | }) 238 | .then(res => { 239 | if (res.stderr.length > 0 && res.exitCode != 0) { 240 | throw new Error(res.stderr); 241 | } 242 | }); 243 | await exec 244 | .getExecOutput('gpg', ['--batch', '--yes', '--delete-keys', fingerprint], { 245 | ignoreReturnCode: true, 246 | silent: true 247 | }) 248 | .then(res => { 249 | if (res.stderr.length > 0 && res.exitCode != 0) { 250 | throw new Error(res.stderr); 251 | } 252 | }); 253 | }; 254 | 255 | export const killAgent = async (): Promise => { 256 | await gpgConnectAgent('KILLAGENT'); 257 | }; 258 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | import * as context from './context'; 3 | import * as git from './git'; 4 | import * as gpg from './gpg'; 5 | import * as openpgp from './openpgp'; 6 | import * as stateHelper from './state-helper'; 7 | 8 | async function run(): Promise { 9 | try { 10 | const inputs: context.Inputs = await context.getInputs(); 11 | 12 | if (inputs.workdir && inputs.workdir !== '.') { 13 | core.info(`Using ${inputs.workdir} as working directory...`); 14 | process.chdir(inputs.workdir); 15 | } 16 | 17 | const version = await gpg.getVersion(); 18 | const dirs = await gpg.getDirs(); 19 | await core.group(`GnuPG info`, async () => { 20 | core.info(`Version : ${version.gnupg} (libgcrypt ${version.libgcrypt})`); 21 | core.info(`Libdir : ${dirs.libdir}`); 22 | core.info(`Libexecdir : ${dirs.libexecdir}`); 23 | core.info(`Datadir : ${dirs.datadir}`); 24 | core.info(`Homedir : ${dirs.homedir}`); 25 | }); 26 | 27 | const privateKey = await openpgp.readPrivateKey(inputs.gpgPrivateKey); 28 | await core.group(`GPG private key info`, async () => { 29 | core.info(`Fingerprint : ${privateKey.fingerprint}`); 30 | core.info(`KeyID : ${privateKey.keyID}`); 31 | core.info(`Name : ${privateKey.name}`); 32 | core.info(`Email : ${privateKey.email}`); 33 | core.info(`CreationTime : ${privateKey.creationTime}`); 34 | }); 35 | 36 | stateHelper.setFingerprint(privateKey.fingerprint); 37 | 38 | let fingerprint = privateKey.fingerprint; 39 | if (inputs.fingerprint) { 40 | fingerprint = inputs.fingerprint; 41 | } 42 | 43 | await core.group(`Fingerprint to use`, async () => { 44 | core.info(fingerprint); 45 | }); 46 | 47 | await core.group(`Importing GPG private key`, async () => { 48 | await gpg.importKey(inputs.gpgPrivateKey).then(stdout => { 49 | core.info(stdout); 50 | }); 51 | }); 52 | 53 | if (inputs.passphrase) { 54 | await core.group(`Configuring GnuPG agent`, async () => { 55 | const gpgHome = await gpg.getHome(); 56 | core.info(`GnuPG home: ${gpgHome}`); 57 | await gpg.configureAgent(gpgHome, gpg.agentConfig); 58 | }); 59 | if (!inputs.fingerprint) { 60 | // Set the passphrase for all subkeys 61 | await core.group(`Getting keygrips`, async () => { 62 | for (const keygrip of await gpg.getKeygrips(fingerprint)) { 63 | core.info(`Presetting passphrase for ${keygrip}`); 64 | await gpg.presetPassphrase(keygrip, inputs.passphrase).then(stdout => { 65 | core.debug(stdout); 66 | }); 67 | } 68 | }); 69 | } else { 70 | // Set the passphrase only for the subkey specified in the input `fingerprint` 71 | await core.group(`Getting keygrip for fingerprint`, async () => { 72 | const keygrip = await gpg.getKeygrip(fingerprint); 73 | core.info(`Presetting passphrase for key ${fingerprint} with keygrip ${keygrip}`); 74 | await gpg.presetPassphrase(keygrip, inputs.passphrase).then(stdout => { 75 | core.debug(stdout); 76 | }); 77 | }); 78 | } 79 | } 80 | 81 | if (inputs.trustLevel) { 82 | await core.group(`Setting key's trust level`, async () => { 83 | await gpg.setTrustLevel(privateKey.keyID, inputs.trustLevel).then(() => { 84 | core.info(`Trust level set to ${inputs.trustLevel} for ${privateKey.keyID}`); 85 | }); 86 | }); 87 | } 88 | 89 | await core.group(`Setting outputs`, async () => { 90 | core.info(`fingerprint=${fingerprint}`); 91 | core.setOutput('fingerprint', fingerprint); 92 | core.info(`keyid=${privateKey.keyID}`); 93 | core.setOutput('keyid', privateKey.keyID); 94 | core.info(`name=${privateKey.name}`); 95 | core.setOutput('name', privateKey.name); 96 | core.info(`email=${privateKey.email}`); 97 | core.setOutput('email', privateKey.email); 98 | }); 99 | 100 | if (inputs.gitUserSigningkey) { 101 | core.info('Setting GPG signing keyID for this Git repository'); 102 | await git.setConfig('user.signingkey', privateKey.keyID, inputs.gitConfigGlobal); 103 | 104 | const userEmail = inputs.gitCommitterEmail || privateKey.email; 105 | const userName = inputs.gitCommitterName || privateKey.name; 106 | 107 | if (userEmail != privateKey.email) { 108 | core.setFailed(`Committer email "${inputs.gitCommitterEmail}" (name: "${inputs.gitCommitterName}") does not match GPG private key email "${privateKey.email}" (name: "${privateKey.name}")`); 109 | return; 110 | } 111 | 112 | core.info(`Configuring Git committer (${userName} <${userEmail}>)`); 113 | await git.setConfig('user.name', userName, inputs.gitConfigGlobal); 114 | await git.setConfig('user.email', userEmail, inputs.gitConfigGlobal); 115 | 116 | if (inputs.gitCommitGpgsign) { 117 | core.info('Sign all commits automatically'); 118 | await git.setConfig('commit.gpgsign', 'true', inputs.gitConfigGlobal); 119 | } 120 | if (inputs.gitTagGpgsign) { 121 | core.info('Sign all tags automatically'); 122 | await git.setConfig('tag.gpgsign', 'true', inputs.gitConfigGlobal); 123 | } 124 | if (inputs.gitPushGpgsign) { 125 | core.info('Sign all pushes automatically'); 126 | await git.setConfig('push.gpgsign', inputs.gitPushGpgsign, inputs.gitConfigGlobal); 127 | } 128 | } 129 | } catch (error) { 130 | core.setFailed(error.message); 131 | } 132 | } 133 | 134 | async function cleanup(): Promise { 135 | if (stateHelper.fingerprint.length <= 0) { 136 | core.debug('Primary key fingerprint is not defined. Skipping cleanup.'); 137 | return; 138 | } 139 | try { 140 | core.info(`Removing key ${stateHelper.fingerprint}`); 141 | await gpg.deleteKey(stateHelper.fingerprint); 142 | 143 | core.info('Killing GnuPG agent'); 144 | await gpg.killAgent(); 145 | } catch (error) { 146 | core.warning(error.message); 147 | } 148 | } 149 | 150 | if (!stateHelper.IsPost) { 151 | run(); 152 | } else { 153 | cleanup(); 154 | } 155 | -------------------------------------------------------------------------------- /src/openpgp.ts: -------------------------------------------------------------------------------- 1 | import * as openpgp from 'openpgp'; 2 | import addressparser from 'addressparser'; 3 | 4 | export interface PrivateKey { 5 | fingerprint: string; 6 | keyID: string; 7 | name: string; 8 | email: string; 9 | creationTime: Date; 10 | } 11 | 12 | export interface KeyPair { 13 | publicKey: string; 14 | privateKey: string; 15 | } 16 | 17 | export const readPrivateKey = async (key: string): Promise => { 18 | const privateKey = await openpgp.readKey({ 19 | armoredKey: (await isArmored(key)) ? key : Buffer.from(key, 'base64').toString() 20 | }); 21 | 22 | const address = await privateKey.getPrimaryUser().then(primaryUser => { 23 | return addressparser(primaryUser.user.userID?.userID)[0]; 24 | }); 25 | 26 | return { 27 | fingerprint: privateKey.getFingerprint().toUpperCase(), 28 | keyID: privateKey.getKeyID().toHex().toUpperCase(), 29 | name: address.name, 30 | email: address.address, 31 | creationTime: privateKey.getCreationTime() 32 | }; 33 | }; 34 | 35 | export const generateKeyPair = async (name: string, email: string, passphrase: string, type?: 'ecc' | 'rsa'): Promise => { 36 | const keyPair = await openpgp.generateKey({ 37 | userIDs: [{name: name, email: email}], 38 | passphrase: passphrase, 39 | type: type 40 | }); 41 | 42 | return { 43 | publicKey: keyPair.publicKey.replace(/\r\n/g, '\n').trim(), 44 | privateKey: keyPair.privateKey.replace(/\r\n/g, '\n').trim() 45 | }; 46 | }; 47 | 48 | export const isArmored = async (text: string): Promise => { 49 | return text.trimLeft().startsWith('---'); 50 | }; 51 | -------------------------------------------------------------------------------- /src/state-helper.ts: -------------------------------------------------------------------------------- 1 | import * as core from '@actions/core'; 2 | 3 | export const IsPost = !!process.env['STATE_isPost']; 4 | export const fingerprint = process.env['STATE_fingerprint'] || ''; 5 | 6 | export function setFingerprint(fingerprint: string) { 7 | core.saveState('fingerprint', fingerprint); 8 | } 9 | 10 | if (!IsPost) { 11 | core.saveState('isPost', 'true'); 12 | } 13 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "target": "es6", 5 | "module": "commonjs", 6 | "newLine": "lf", 7 | "outDir": "./lib", 8 | "rootDir": "./src", 9 | "forceConsistentCasingInFileNames": true, 10 | "noImplicitAny": false, 11 | "resolveJsonModule": true, 12 | "useUnknownInCatchVariables": false, 13 | }, 14 | "exclude": [ 15 | "node_modules", 16 | "**/*.test.ts", 17 | "jest.config.ts" 18 | ] 19 | } 20 | --------------------------------------------------------------------------------