├── .editorconfig ├── .eslintrc.yml ├── .github ├── FUNDING.yml ├── actions │ └── cache-npm │ │ └── action.yml └── workflows │ ├── build.yml │ ├── reusable-verify.yml │ ├── verify-on-container-alpine.yml │ ├── verify-on-container-centos.yml │ ├── verify-on-container-ubuntu.yml │ ├── verify-on-macos.yml │ ├── verify-on-ubuntu.yml │ └── verify-on-windows.yml ├── .gitignore ├── .markdownlint.yml ├── .npmrc ├── CHANGELOG.md ├── LICENSE ├── README.md ├── action.yml ├── dist ├── main.js ├── main.js.map ├── post.js └── post.js.map ├── package-lock.json ├── package.json ├── scripts ├── prepare-release.sh ├── rebuild.sh └── update-dependencies.sh ├── src ├── common.ts ├── main.ts └── post.ts ├── ssh-key-action.code-workspace └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # http://editorconfig.org/ 2 | # https://github.com/editorconfig/editorconfig/wiki/EditorConfig-Properties 3 | root = true 4 | 5 | 6 | # recommended settings; DO NOT CHANGE! 7 | [*] 8 | end_of_line = lf 9 | charset = utf-8 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | 13 | 14 | # common settings 15 | [*] 16 | indent_style = tab 17 | indent_size = 4 18 | tab_width = 4 19 | max_line_length = off 20 | 21 | quote_type = double 22 | curly_bracket_next_line = true 23 | spaces_around_operators = true 24 | spaces_around_brackets = none 25 | indent_brace_style = allman 26 | 27 | 28 | # JavaScript/TypeScript 29 | [*.{js,ts}] 30 | indent_style = space 31 | curly_bracket_next_line = false 32 | indent_brace_style = K&R 33 | 34 | 35 | # JSON/YAML 36 | [*.{json,babelrc,code-workspace,yml,yaml}] 37 | indent_style = space 38 | indent_size = 2 39 | tab_width = 2 40 | 41 | 42 | # markdown 43 | [*.md] 44 | indent_style = space 45 | -------------------------------------------------------------------------------- /.eslintrc.yml: -------------------------------------------------------------------------------- 1 | # https://eslint.org/ 2 | env: # https://eslint.org/docs/user-guide/configuring#specifying-environments 3 | es6: true 4 | node: true 5 | extends: 6 | - eslint:recommended 7 | - plugin:@typescript-eslint/eslint-recommended 8 | - plugin:@typescript-eslint/recommended 9 | plugins: 10 | - "@typescript-eslint" 11 | parser: "@typescript-eslint/parser" 12 | parserOptions: 13 | sourceType: module 14 | project: ./tsconfig.json 15 | rules: # https://eslint.org/docs/rules/ 16 | accessor-pairs: error 17 | array-bracket-newline: 18 | - error 19 | - consistent 20 | array-bracket-spacing: 21 | - error 22 | - never 23 | array-callback-return: error 24 | arrow-body-style: 'off' 25 | arrow-parens: 26 | - error 27 | - always 28 | arrow-spacing: 29 | - error 30 | - after: true 31 | before: true 32 | block-scoped-var: error 33 | block-spacing: error 34 | brace-style: 'off' # see "@typescript-eslint/brace-style" 35 | callback-return: error 36 | capitalized-comments: 'off' 37 | class-methods-use-this: error 38 | comma-dangle: 39 | - error 40 | - always-multiline 41 | comma-spacing: 42 | - error 43 | - after: true 44 | before: false 45 | comma-style: 46 | - error 47 | - last 48 | complexity: 49 | - error 50 | computed-property-spacing: 51 | - error 52 | - never 53 | consistent-return: 'off' 54 | consistent-this: error 55 | curly: error 56 | default-case: 'off' 57 | dot-location: 58 | - error 59 | - property 60 | dot-notation: 61 | - error 62 | - allowPattern: "^_" 63 | eol-last: error 64 | eqeqeq: error 65 | for-direction: error 66 | func-call-spacing: error 67 | func-name-matching: error 68 | func-names: 69 | - error 70 | - never 71 | func-style: 72 | - error 73 | - declaration 74 | function-paren-newline: 75 | - error 76 | - multiline-arguments 77 | generator-star-spacing: error 78 | getter-return: error 79 | global-require: error 80 | guard-for-in: error 81 | handle-callback-err: error 82 | id-blacklist: error 83 | id-length: error 84 | id-match: error 85 | implicit-arrow-linebreak: 86 | - error 87 | - below 88 | indent: 89 | - error 90 | - 4 91 | - SwitchCase: 1 92 | indent-legacy: 'off' 93 | init-declarations: error 94 | jsx-quotes: error 95 | key-spacing: error 96 | keyword-spacing: 97 | - error 98 | line-comment-position: 'off' 99 | linebreak-style: 100 | - error 101 | - unix 102 | lines-around-comment: 'off' 103 | lines-around-directive: error 104 | lines-between-class-members: 105 | - error 106 | - always 107 | - exceptAfterSingleLine: true 108 | max-depth: error 109 | max-len: 'off' 110 | max-lines: 'off' 111 | max-nested-callbacks: error 112 | max-params: 'off' 113 | max-statements: 'off' 114 | max-statements-per-line: error 115 | multiline-comment-style: 116 | - error 117 | - separate-lines 118 | multiline-ternary: error 119 | new-cap: error 120 | new-parens: error 121 | newline-after-var: 'off' 122 | newline-before-return: 'off' 123 | no-alert: error 124 | no-array-constructor: error 125 | no-await-in-loop: error 126 | no-bitwise: error 127 | no-buffer-constructor: error 128 | no-caller: error 129 | no-catch-shadow: error 130 | no-confusing-arrow: error 131 | no-continue: 'off' 132 | no-div-regex: error 133 | no-duplicate-imports: error 134 | no-else-return: 'off' 135 | no-empty-function: 'off' 136 | no-eq-null: error 137 | no-eval: error 138 | no-extend-native: error 139 | no-extra-bind: error 140 | no-extra-label: error 141 | no-extra-parens: error 142 | no-extra-semi: error 143 | no-floating-decimal: error 144 | no-implicit-coercion: error 145 | no-implicit-globals: error 146 | no-implied-eval: error 147 | no-inline-comments: 'off' 148 | no-invalid-this: error 149 | no-iterator: error 150 | no-label-var: error 151 | no-labels: error 152 | no-lonely-if: error 153 | no-loop-func: error 154 | no-magic-numbers: 'off' 155 | no-mixed-operators: error 156 | no-mixed-requires: error 157 | no-multi-assign: error 158 | no-multi-spaces: error 159 | no-multi-str: error 160 | no-multiple-empty-lines: 161 | - error 162 | - max: 1 163 | no-native-reassign: error 164 | no-negated-condition: 'off' 165 | no-negated-in-lhs: error 166 | no-nested-ternary: error 167 | no-new: error 168 | no-new-func: error 169 | no-new-object: error 170 | no-new-require: error 171 | no-new-wrappers: error 172 | no-octal-escape: error 173 | no-param-reassign: 'off' 174 | no-path-concat: error 175 | no-plusplus: 'off' 176 | no-process-env: 'off' 177 | no-process-exit: error 178 | no-proto: error 179 | no-prototype-builtins: 'off' 180 | no-restricted-globals: error 181 | no-restricted-imports: error 182 | no-restricted-modules: error 183 | no-restricted-properties: error 184 | no-restricted-syntax: error 185 | no-return-assign: error 186 | no-return-await: error 187 | no-script-url: error 188 | no-self-compare: error 189 | no-sequences: error 190 | no-shadow: error 191 | no-shadow-restricted-names: error 192 | no-spaced-func: error 193 | no-sync: 'off' 194 | no-tabs: 'off' 195 | no-template-curly-in-string: error 196 | no-ternary: error 197 | no-throw-literal: error 198 | no-trailing-spaces: error 199 | no-undef-init: error 200 | no-undefined: 'off' 201 | no-underscore-dangle: 'off' 202 | no-unmodified-loop-condition: error 203 | no-unneeded-ternary: error 204 | no-unused-expressions: error 205 | no-use-before-define: 'off' 206 | no-useless-call: error 207 | no-useless-computed-key: error 208 | no-useless-concat: error 209 | no-useless-constructor: error 210 | no-useless-rename: error 211 | no-useless-return: error 212 | no-var: error 213 | no-void: error 214 | no-warning-comments: error 215 | no-whitespace-before-property: error 216 | no-with: error 217 | nonblock-statement-body-position: error 218 | object-curly-newline: 'off' 219 | object-curly-spacing: 220 | - error 221 | - never 222 | object-property-newline: error 223 | object-shorthand: 'off' 224 | one-var: 'off' 225 | one-var-declaration-per-line: error 226 | operator-assignment: error 227 | operator-linebreak: error 228 | padded-blocks: 'off' 229 | padding-line-between-statements: error 230 | prefer-arrow-callback: error 231 | prefer-const: error 232 | prefer-destructuring: error 233 | prefer-numeric-literals: error 234 | prefer-promise-reject-errors: error 235 | prefer-reflect: error 236 | prefer-rest-params: error 237 | prefer-spread: error 238 | prefer-template: error 239 | quote-props: 'off' 240 | quotes: 'off' 241 | radix: error 242 | require-await: error 243 | require-jsdoc: error 244 | rest-spread-spacing: 245 | - error 246 | - never 247 | semi: 'off' # see "@typescript-eslint/semi" 248 | semi-spacing: error 249 | semi-style: 250 | - error 251 | - last 252 | sort-imports: 253 | - error 254 | - ignoreCase: false 255 | ignoreDeclarationSort: true 256 | sort-keys: 'off' 257 | sort-vars: error 258 | space-before-blocks: error 259 | space-before-function-paren: 'off' 260 | space-in-parens: 261 | - error 262 | - never 263 | space-infix-ops: error 264 | space-unary-ops: error 265 | spaced-comment: 266 | - error 267 | - always 268 | strict: error 269 | switch-colon-spacing: error 270 | symbol-description: error 271 | template-curly-spacing: 272 | - error 273 | - never 274 | template-tag-spacing: error 275 | unicode-bom: 276 | - error 277 | - never 278 | valid-jsdoc: 279 | - error 280 | - prefer: 281 | arg: param 282 | argument: param 283 | return: returns 284 | yield: yields 285 | exception: throws 286 | requireReturn: false 287 | requireParamType: false 288 | requireReturnType: false 289 | vars-on-top: error 290 | wrap-iife: error 291 | wrap-regex: error 292 | yield-star-spacing: error 293 | yoda: 294 | - error 295 | - never 296 | 297 | # @typescript-eslint plugin 298 | "@typescript-eslint/ban-ts-ignore": 'off' 299 | "@typescript-eslint/brace-style": 300 | - error 301 | - 1tbs 302 | "@typescript-eslint/member-delimiter-style": 303 | - error 304 | - multiline: 305 | delimiter: semi 306 | requireLast: true 307 | singleline: 308 | delimiter: semi 309 | requireLast: true 310 | "@typescript-eslint/no-empty-interface": 'off' 311 | "@typescript-eslint/no-floating-promises": error 312 | "@typescript-eslint/no-use-before-define": 313 | - error 314 | - functions: false 315 | "@typescript-eslint/semi": error 316 | "@typescript-eslint/strict-boolean-expressions": 317 | - error 318 | - allowString: false 319 | allowNumber: false 320 | allowNullableObject: false 321 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | github: 2 | - shimataro 3 | custom: 4 | - "https://www.paypal.me/shimataro" 5 | -------------------------------------------------------------------------------- /.github/actions/cache-npm/action.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/creating-actions/creating-a-composite-action 2 | name: Cache NPM 3 | description: Composite action (cache NPM) 4 | 5 | runs: 6 | using: composite 7 | steps: 8 | # https://github.com/actions/cache/blob/master/examples.md#node---npm 9 | # https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-an-output-parameter 10 | - name: Get NPM cache directory 11 | run: echo "NPM_CACHE_DIRECTORY=$(npm config get cache)" >> ${{ github.env }} # use "github.env" instead of "github.output"; if use "github.output", "Warning: Input required and not supplied: path" is displayed on post-process 12 | shell: pwsh # use PowerShell; Bash doesn't work on Windows because the value of "github.env" is like "D:\a\_temp\_runner_file_commands\set_env_XXX". 13 | 14 | - name: Get Node.js version 15 | run: echo "NODEJS_VERSION=$(node -v)" >> ${{ github.env }} 16 | shell: pwsh 17 | 18 | - name: Cache NPM modules 19 | uses: actions/cache@v3 20 | with: 21 | path: ${{ env.NPM_CACHE_DIRECTORY }} 22 | key: npm-${{ runner.os }}-${{ runner.arch }}-${{ env.NODEJS_VERSION }}-${{ hashFiles('package-lock.json') }} 23 | restore-keys: | 24 | npm-${{ runner.os }}-${{ runner.arch }}-${{ env.NODEJS_VERSION }}- 25 | npm-${{ runner.os }}-${{ runner.arch }} 26 | -------------------------------------------------------------------------------- /.github/workflows/build.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 2 | 3 | name: Build 4 | 5 | on: 6 | - push 7 | 8 | jobs: 9 | build: 10 | name: Build 11 | runs-on: ${{ matrix.os }} 12 | strategy: 13 | matrix: 14 | os: 15 | - windows-2019 16 | - windows-2022 17 | - macos-11 18 | - macos-12 19 | - ubuntu-20.04 20 | - ubuntu-22.04 21 | nodejs: 22 | - 20 23 | fail-fast: false 24 | steps: 25 | - name: Turn off auto-crlf 26 | run: git config --global core.autocrlf false 27 | - name: Checkout source codes 28 | uses: actions/checkout@v4 29 | - name: Install Node.js 30 | uses: actions/setup-node@v4 31 | with: 32 | node-version: ${{ matrix.nodejs }} 33 | - name: Cache NPM 34 | uses: ./.github/actions/cache-npm 35 | - name: Install dependencies 36 | run: npm ci 37 | - name: Build 38 | run: npm run build 39 | - name: Exit if differ (forgot to commit dist dir?) 40 | run: git diff --exit-code --quiet 41 | - name: Verify 42 | run: npm run verify 43 | -------------------------------------------------------------------------------- /.github/workflows/reusable-verify.yml: -------------------------------------------------------------------------------- 1 | # https://docs.github.com/en/actions/using-workflows/reusing-workflows 2 | name: Reusable workflow (verify) 3 | 4 | on: 5 | workflow_call: 6 | inputs: 7 | os: 8 | required: true 9 | type: string 10 | description: host OS that CI 'runs-on' 11 | docker_image: 12 | required: false 13 | type: string 14 | default: "" 15 | description: Docker image name 16 | package_installation_command: 17 | required: false 18 | type: string 19 | default: "" 20 | description: package installation command 21 | secrets: 22 | SSH_KEY_PEM: 23 | required: true 24 | description: SSH private key (PEM format) 25 | SSH_KEY_PKCS8: 26 | required: true 27 | description: SSH private key (PKCS8 format) 28 | SSH_KEY_RFC4716: 29 | required: true 30 | description: SSH private key (RFC4716 format) 31 | 32 | jobs: 33 | ssh-pem: 34 | name: Connect to github.com (PEM format) 35 | runs-on: ${{ inputs.os }} 36 | container: ${{ inputs.docker_image }} 37 | steps: 38 | - name: Install packages 39 | run: ${{ inputs.package_installation_command }} 40 | if: ${{ inputs.package_installation_command != '' }} 41 | - name: Checkout source codes 42 | uses: actions/checkout@v3 43 | - name: Install SSH key 44 | uses: ./. 45 | with: 46 | key: ${{ secrets.SSH_KEY_PEM }} 47 | known_hosts: unnecessary 48 | - name: git clone through SSH 49 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 50 | 51 | ssh-pem-bitbucket: 52 | name: Connect to bitbucket.org (PEM format) 53 | runs-on: ${{ inputs.os }} 54 | container: ${{ inputs.docker_image }} 55 | steps: 56 | - name: Install packages 57 | run: ${{ inputs.package_installation_command }} 58 | if: ${{ inputs.package_installation_command != '' }} 59 | - name: Checkout source codes 60 | uses: actions/checkout@v3 61 | - name: Install SSH key 62 | uses: ./. 63 | with: 64 | key: ${{ secrets.SSH_KEY_PEM }} 65 | known_hosts: | 66 | bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M= 67 | bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE= 68 | bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO 69 | - name: git clone through SSH 70 | run: git clone git@bitbucket.org:shimataro999/ssh-test.git tmp 71 | 72 | ssh-pkcs8: 73 | name: Connect to github.com (PKCS8 format) 74 | runs-on: ${{ inputs.os }} 75 | container: ${{ inputs.docker_image }} 76 | steps: 77 | - name: Install packages 78 | run: ${{ inputs.package_installation_command }} 79 | if: ${{ inputs.package_installation_command != '' }} 80 | - name: Checkout source codes 81 | uses: actions/checkout@v3 82 | - name: Install SSH key 83 | uses: ./. 84 | with: 85 | key: ${{ secrets.SSH_KEY_PKCS8 }} 86 | known_hosts: unnecessary 87 | - name: git clone through SSH 88 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 89 | 90 | ssh-pkcs8-bitbucket: 91 | name: Connect to bitbucket.org (PKCS8 format) 92 | runs-on: ${{ inputs.os }} 93 | container: ${{ inputs.docker_image }} 94 | steps: 95 | - name: Install packages 96 | run: ${{ inputs.package_installation_command }} 97 | if: ${{ inputs.package_installation_command != '' }} 98 | - name: Checkout source codes 99 | uses: actions/checkout@v3 100 | - name: Install SSH key 101 | uses: ./. 102 | with: 103 | key: ${{ secrets.SSH_KEY_PKCS8 }} 104 | known_hosts: | 105 | bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M= 106 | bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE= 107 | bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO 108 | - name: git clone through SSH 109 | run: git clone git@bitbucket.org:shimataro999/ssh-test.git tmp 110 | 111 | ssh-rfc4716: 112 | name: Connect to github.com (RFC4716 format) 113 | runs-on: ${{ inputs.os }} 114 | container: ${{ inputs.docker_image }} 115 | steps: 116 | - name: Install packages 117 | run: ${{ inputs.package_installation_command }} 118 | if: ${{ inputs.package_installation_command != '' }} 119 | - name: Checkout source codes 120 | uses: actions/checkout@v3 121 | - name: Install SSH key 122 | uses: ./. 123 | with: 124 | key: ${{ secrets.SSH_KEY_RFC4716 }} 125 | known_hosts: unnecessary 126 | - name: git clone through SSH 127 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 128 | 129 | ssh-rfc4716-bitbucket: 130 | name: Connect to bitbucket.org (RFC4716 format) 131 | runs-on: ${{ inputs.os }} 132 | container: ${{ inputs.docker_image }} 133 | steps: 134 | - name: Install packages 135 | run: ${{ inputs.package_installation_command }} 136 | if: ${{ inputs.package_installation_command != '' }} 137 | - name: Checkout source codes 138 | uses: actions/checkout@v3 139 | - name: Install SSH key 140 | uses: ./. 141 | with: 142 | key: ${{ secrets.SSH_KEY_RFC4716 }} 143 | known_hosts: | 144 | bitbucket.org ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQDQeJzhupRu0u0cdegZIa8e86EG2qOCsIsD1Xw0xSeiPDlCr7kq97NLmMbpKTX6Esc30NuoqEEHCuc7yWtwp8dI76EEEB1VqY9QJq6vk+aySyboD5QF61I/1WeTwu+deCbgKMGbUijeXhtfbxSxm6JwGrXrhBdofTsbKRUsrN1WoNgUa8uqN1Vx6WAJw1JHPhglEGGHea6QICwJOAr/6mrui/oB7pkaWKHj3z7d1IC4KWLtY47elvjbaTlkN04Kc/5LFEirorGYVbt15kAUlqGM65pk6ZBxtaO3+30LVlORZkxOh+LKL/BvbZ/iRNhItLqNyieoQj/uh/7Iv4uyH/cV/0b4WDSd3DptigWq84lJubb9t/DnZlrJazxyDCulTmKdOR7vs9gMTo+uoIrPSb8ScTtvw65+odKAlBj59dhnVp9zd7QUojOpXlL62Aw56U4oO+FALuevvMjiWeavKhJqlR7i5n9srYcrNV7ttmDw7kf/97P5zauIhxcjX+xHv4M= 145 | bitbucket.org ecdsa-sha2-nistp256 AAAAE2VjZHNhLXNoYTItbmlzdHAyNTYAAAAIbmlzdHAyNTYAAABBBPIQmuzMBuKdWeF4+a2sjSSpBK0iqitSQ+5BM9KhpexuGt20JpTVM7u5BDZngncgrqDMbWdxMWWOGtZ9UgbqgZE= 146 | bitbucket.org ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIIazEu89wgQZ4bqs3d63QSMzYVa0MuJ2e2gKTKqu+UUO 147 | - name: git clone through SSH 148 | run: git clone git@bitbucket.org:shimataro999/ssh-test.git tmp 149 | 150 | key_if_exists_replace-key_exists: 151 | name: if_key_exists=replace / key exists 152 | runs-on: ${{ inputs.os }} 153 | container: ${{ inputs.docker_image }} 154 | steps: 155 | - name: Install packages 156 | run: ${{ inputs.package_installation_command }} 157 | if: ${{ inputs.package_installation_command != '' }} 158 | - name: Checkout source codes 159 | uses: actions/checkout@v3 160 | - name: Install SSH key (dummy) 161 | uses: ./. 162 | with: 163 | key: "dummy" # replaced 164 | known_hosts: unnecessary 165 | - name: Install SSH key (replaces existing key) 166 | uses: ./. 167 | with: 168 | key: ${{ secrets.SSH_KEY_PEM }} 169 | known_hosts: unnecessary 170 | if_key_exists: replace 171 | - name: git clone through SSH 172 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 173 | 174 | key_if_exists_replace-key_doesnt_exist: 175 | name: if_key_exists=replace / key doesn't exist 176 | runs-on: ${{ inputs.os }} 177 | container: ${{ inputs.docker_image }} 178 | steps: 179 | - name: Install packages 180 | run: ${{ inputs.package_installation_command }} 181 | if: ${{ inputs.package_installation_command != '' }} 182 | - name: Checkout source codes 183 | uses: actions/checkout@v3 184 | - name: Install SSH key 185 | uses: ./. 186 | with: 187 | key: ${{ secrets.SSH_KEY_PEM }} 188 | known_hosts: unnecessary 189 | if_key_exists: replace 190 | - name: git clone through SSH 191 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 192 | 193 | key_if_exists_ignore-key_exists: 194 | name: if_key_exists=ignore / key exists 195 | runs-on: ${{ inputs.os }} 196 | container: ${{ inputs.docker_image }} 197 | steps: 198 | - name: Install packages 199 | run: ${{ inputs.package_installation_command }} 200 | if: ${{ inputs.package_installation_command != '' }} 201 | - name: Checkout source codes 202 | uses: actions/checkout@v3 203 | - name: Install SSH key 204 | uses: ./. 205 | with: 206 | key: ${{ secrets.SSH_KEY_PEM }} 207 | known_hosts: unnecessary 208 | - name: Install SSH key (does nothing) 209 | uses: ./. 210 | with: 211 | key: "dummy" # ignored 212 | known_hosts: unnecessary 213 | if_key_exists: ignore 214 | - name: git clone through SSH 215 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 216 | 217 | key_if_exists_ignore-key_doesnt_exist: 218 | name: if_key_exists=ignore / key doesn't exist 219 | runs-on: ${{ inputs.os }} 220 | container: ${{ inputs.docker_image }} 221 | steps: 222 | - name: Install packages 223 | run: ${{ inputs.package_installation_command }} 224 | if: ${{ inputs.package_installation_command != '' }} 225 | - name: Checkout source codes 226 | uses: actions/checkout@v3 227 | - name: Install SSH key 228 | uses: ./. 229 | with: 230 | key: ${{ secrets.SSH_KEY_PEM }} 231 | known_hosts: unnecessary 232 | if_key_exists: ignore 233 | - name: git clone through SSH 234 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 235 | 236 | key_if_exists_fail-key_exists: 237 | name: if_key_exists=fail / key exists 238 | runs-on: ${{ inputs.os }} 239 | container: ${{ inputs.docker_image }} 240 | steps: 241 | - name: Install packages 242 | run: ${{ inputs.package_installation_command }} 243 | if: ${{ inputs.package_installation_command != '' }} 244 | - name: Checkout source codes 245 | uses: actions/checkout@v3 246 | - name: Install SSH key 247 | uses: ./. 248 | with: 249 | key: ${{ secrets.SSH_KEY_PEM }} 250 | known_hosts: unnecessary 251 | - name: Install SSH key (fails) 252 | uses: ./. 253 | with: 254 | key: "dummy" # fails 255 | known_hosts: unnecessary 256 | if_key_exists: fail 257 | continue-on-error: true 258 | - name: git clone through SSH 259 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 260 | 261 | key_if_exists_fail-key_doesnt_exist: 262 | name: if_key_exists=fail / key doesn't exist 263 | runs-on: ${{ inputs.os }} 264 | container: ${{ inputs.docker_image }} 265 | steps: 266 | - name: Install packages 267 | run: ${{ inputs.package_installation_command }} 268 | if: ${{ inputs.package_installation_command != '' }} 269 | - name: Checkout source codes 270 | uses: actions/checkout@v3 271 | - name: Install SSH key 272 | uses: ./. 273 | with: 274 | key: ${{ secrets.SSH_KEY_PEM }} 275 | known_hosts: unnecessary 276 | if_key_exists: fail 277 | - name: git clone through SSH 278 | run: git clone git@github.com:shimataro/ssh-key-action.git tmp 279 | -------------------------------------------------------------------------------- /.github/workflows/verify-on-container-alpine.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 2 | name: Docker container (Alpine Linux) 3 | 4 | on: 5 | - push 6 | 7 | jobs: 8 | verify: 9 | name: Verify 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - ubuntu-20.04 15 | - ubuntu-22.04 16 | docker_image: 17 | - alpine:3.10 18 | - alpine:3.11 19 | - alpine:3.12 20 | - alpine:3.13 21 | uses: "./.github/workflows/reusable-verify.yml" 22 | with: 23 | os: ${{ matrix.os }} 24 | docker_image: ${{ matrix.docker_image }} 25 | package_installation_command: apk add openssh-client git 26 | secrets: inherit 27 | -------------------------------------------------------------------------------- /.github/workflows/verify-on-container-centos.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 2 | name: Docker container (CentOS) 3 | 4 | on: 5 | - push 6 | 7 | jobs: 8 | verify: 9 | name: Verify 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - ubuntu-20.04 15 | - ubuntu-22.04 16 | docker_image: 17 | - quay.io/centos/centos:stream8 18 | uses: "./.github/workflows/reusable-verify.yml" 19 | with: 20 | os: ${{ matrix.os }} 21 | docker_image: ${{ matrix.docker_image }} 22 | package_installation_command: yum install -y git openssh-clients 23 | secrets: inherit 24 | -------------------------------------------------------------------------------- /.github/workflows/verify-on-container-ubuntu.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 2 | name: Docker container (Ubuntu) 3 | 4 | on: 5 | - push 6 | 7 | jobs: 8 | verify: 9 | name: Verify 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - ubuntu-20.04 15 | - ubuntu-22.04 16 | docker_image: 17 | - ubuntu:20.04 18 | - ubuntu:22.04 19 | uses: "./.github/workflows/reusable-verify.yml" 20 | with: 21 | os: ${{ matrix.os }} 22 | docker_image: ${{ matrix.docker_image }} 23 | package_installation_command: | 24 | apt update 25 | apt install -y openssh-client git 26 | secrets: inherit 27 | -------------------------------------------------------------------------------- /.github/workflows/verify-on-macos.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 2 | name: macOS 3 | 4 | on: 5 | - push 6 | 7 | jobs: 8 | verify: 9 | name: Verify 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - macos-11 15 | - macos-12 16 | uses: "./.github/workflows/reusable-verify.yml" 17 | with: 18 | os: ${{ matrix.os }} 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.github/workflows/verify-on-ubuntu.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 2 | name: Ubuntu 3 | 4 | on: 5 | - push 6 | 7 | jobs: 8 | verify: 9 | name: Verify 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - ubuntu-20.04 15 | - ubuntu-22.04 16 | uses: "./.github/workflows/reusable-verify.yml" 17 | with: 18 | os: ${{ matrix.os }} 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.github/workflows/verify-on-windows.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/workflow-syntax-for-github-actions 2 | name: Windows 3 | 4 | on: 5 | - push 6 | 7 | jobs: 8 | verify: 9 | name: Verify 10 | strategy: 11 | fail-fast: false 12 | matrix: 13 | os: 14 | - windows-2019 15 | - windows-2022 16 | uses: "./.github/workflows/reusable-verify.yml" 17 | with: 18 | os: ${{ matrix.os }} 19 | secrets: inherit 20 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # temporary files 2 | *~ 3 | *.bak 4 | *.log 5 | *.swp 6 | *.tmp 7 | 8 | # image caches 9 | .DS_Store 10 | Thumbs.db 11 | 12 | # patch/merge 13 | *.orig 14 | *.rej 15 | 16 | # IDE 17 | /.idea/ 18 | 19 | # Node.js modules 20 | /node_modules/ 21 | -------------------------------------------------------------------------------- /.markdownlint.yml: -------------------------------------------------------------------------------- 1 | MD007: # ul-indent 2 | indent: 4 3 | MD013: # line-length 4 | line_length: 1024 5 | MD024: # no-duplicate-heading 6 | siblings_only: true 7 | MD026: false # no-trailing-punctuation 8 | MD029: # ol-prefix 9 | style: one 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | #also=dev 2 | engine-strict=true 3 | heading="🍣" 4 | save-prefix="" 5 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | All notable changes to this project will be documented in this file. 4 | 5 | The format is based on [Keep a Changelog](http://keepachangelog.com/en/1.0.0/) 6 | and this project adheres to [Semantic Versioning](http://semver.org/spec/v2.0.0.html). 7 | 8 | ## [Unreleased] 9 | 10 | ## [2.7.0] - 2024-02-11 11 | 12 | ### Others 13 | 14 | * Update to Node.js v20 (thanks [@princemaple](https://github.com/princemaple)) 15 | * drop old containers; Ubuntu 16.04, and CentOS 7 16 | 17 | ## [2.6.1] - 2023-10-13 18 | 19 | ### Fixed 20 | 21 | * JSON parse error on exit, if `if_key_exists`=`fail` and key exists 22 | 23 | ## [2.6.0] - 2023-10-11 24 | 25 | ### Others 26 | 27 | * back up and restore files when exist (thanks [@bambeusz](https://github.com/bambeusz)) 28 | * remove `macos-10.15` and `ubuntu-18.04` virtual environment 29 | 30 | ## [2.5.1] - 2023-03-25 31 | 32 | ### Hotfix 33 | 34 | * update github.com key: (thanks [@phlax](https://github.com/phlax)) 35 | 36 | ## [2.5.0] - 2022-12-24 37 | 38 | ### Added 39 | 40 | * remove SSH directory at the end of workflow 41 | 42 | ## [2.4.0] - 2022-11-03 43 | 44 | ### Added 45 | 46 | * always set server key of `github.com` to `known_hosts` 47 | 48 | ### Fixed 49 | 50 | * usage of `rsync` in README 51 | 52 | ### Others 53 | 54 | * add `windows-2022`, and `macos-11` (thanks [@ViacheslavKudinov](https://github.com/ViacheslavKudinov)) 55 | * add `macos-12`, `ubuntu-22.04`, and `CentOS 8 Stream (Docker container)` 56 | * drop `ubuntu-16.04`, and `CentOS 8 (Docker container)` 57 | * [update Node.js version to 16](https://github.blog/changelog/2022-09-22-github-actions-all-actions-will-begin-running-on-node16-instead-of-node12/) (thanks [@duddu](https://github.com/duddu)) 58 | 59 | ## [2.3.1] - 2021-08-01 60 | 61 | ### Security 62 | 63 | * Fix [CVE-2021-33502](https://github.com/advisories/GHSA-px4h-xg32-q955) 64 | 65 | ### Others 66 | 67 | * add `windows-2016` virtual environment 68 | * [remove `ubuntu-16.04` virtual environment](https://github.blog/changelog/2021-04-29-github-actions-ubuntu-16-04-lts-virtual-environment-will-be-removed-on-september-20-2021/) 69 | 70 | ## [2.3.0] - 2021-03-21 71 | 72 | ### Added 73 | 74 | * `if_key_exists` parameter 75 | * `known_hosts: unnecessary` 76 | * Support Alpine Linux Docker container 77 | 78 | ## [2.2.0] - 2021-02-27 79 | 80 | ### Added 81 | 82 | * Support Ubuntu/CentOS Docker container (thanks [@kujaomega](https://github.com/kujaomega)) 83 | * Support PKCS8/RFC4716 formats (thanks [@tats-u](https://github.com/tats-u)) 84 | 85 | ### Changed 86 | 87 | * Bundle dependencies (thanks [@tats-u](https://github.com/tats-u)) 88 | 89 | ### Fixed 90 | 91 | * comments in README (thanks [@KimSoungRyoul](https://github.com/KimSoungRyoul)) 92 | 93 | ## [2.1.0] - 2020-08-15 94 | 95 | ### Changed 96 | 97 | * Append LF to `known_hosts` / `config` (thanks [@jacktuck](https://github.com/jacktuck)) 98 | 99 | ### Fixed 100 | 101 | * Typo (thanks [@psbss](https://github.com/psbss)) 102 | 103 | ## [2.0.3] - 2020-06-06 104 | 105 | ### Added 106 | 107 | * Ubuntu 20.04 108 | 109 | ### Changed 110 | 111 | * Add short note on how to convert OPENSSH to PEM format by [@shadow1runner](https://github.com/shadow1runner) 112 | 113 | ## [2.0.2] - 2020-04-12 114 | 115 | ### Security 116 | 117 | * update [minimist](https://www.npmjs.com/package/minimist) to 1.2.5 ([CVE-2020-7598](https://github.com/advisories/GHSA-vh95-rmgr-6w4m)) 118 | 119 | ## [2.0.1] - 2020-03-14 120 | 121 | ### Security 122 | 123 | * update [acorn](https://www.npmjs.com/package/acorn) to 7.1.1 ([GHSA-7fhm-mqm4-2wp7](https://github.com/advisories/GHSA-7fhm-mqm4-2wp7)) 124 | 125 | ## [2.0.0] - 2020-02-08 126 | 127 | ### Changed 128 | 129 | * rename `private-key` to `key` 130 | * rename `known-hosts` to `known_hosts` 131 | * make `known_hosts` required 132 | 133 | ## [1.6.5] - 2020-02-08 134 | 135 | ### Others 136 | 137 | * update version of [Checkout](https://github.com/marketplace/actions/checkout) action 138 | 139 | ## [1.6.4] - 2020-01-27 140 | 141 | ### Fixed 142 | 143 | * `node_modules/.bin` error (thanks [@george3447](https://github.com/george3447)) 144 | 145 | ## [1.6.3] - 2020-01-27 146 | 147 | ### Others 148 | 149 | * add Q&A 150 | 151 | ## [1.6.2] - 2020-01-25 152 | 153 | ### Others 154 | 155 | * some updates 156 | 157 | ## [1.6.1] - 2020-01-19 158 | 159 | ### Fixed 160 | 161 | * Some bugfixes 162 | 163 | ## [1.6.0] - 2020-01-18 164 | 165 | ### Changed 166 | 167 | * `public-key` is no longer necessarily 168 | 169 | ## [1.5.0] - 2019/12/30 170 | 171 | ### Changed 172 | 173 | * Append contents of `config` and `known_hosts` when called multiple times. 174 | 175 | ## [1.4.0] - 2019/12/22 176 | 177 | ### Added 178 | 179 | * `config` option 180 | 181 | ## [1.3.0] - 2019/09/29 182 | 183 | ### Added 184 | 185 | * `known-hosts` option 186 | 187 | ## [1.2.0] - 2019/09/22 188 | 189 | ### Fixed 190 | 191 | * CI trigger 192 | * example code in [README](README.md) 193 | 194 | ### Others 195 | 196 | * Install only `dependencies` packages. 197 | 198 | ## [1.1.0] - 2019/09/19 199 | 200 | ### Others 201 | 202 | * Support Visual Studio Code officially. 203 | * Use GitHub Actions for build test. 204 | 205 | ## [1.0.0] - 2019/09/18 206 | 207 | * First release. 208 | 209 | [Unreleased]: https://github.com/shimataro/ssh-key-action/compare/v2.7.0...HEAD 210 | [2.7.0]: https://github.com/shimataro/ssh-key-action/compare/v2.6.1...v2.7.0 211 | [2.6.1]: https://github.com/shimataro/ssh-key-action/compare/v2.6.0...v2.6.1 212 | [2.6.0]: https://github.com/shimataro/ssh-key-action/compare/v2.5.1...v2.6.0 213 | [2.5.1]: https://github.com/shimataro/ssh-key-action/compare/v2.5.0...v2.5.1 214 | [2.5.0]: https://github.com/shimataro/ssh-key-action/compare/v2.4.0...v2.5.0 215 | [2.4.0]: https://github.com/shimataro/ssh-key-action/compare/v2.3.1...v2.4.0 216 | [2.3.1]: https://github.com/shimataro/ssh-key-action/compare/v2.3.0...v2.3.1 217 | [2.3.0]: https://github.com/shimataro/ssh-key-action/compare/v2.2.0...v2.3.0 218 | [2.2.0]: https://github.com/shimataro/ssh-key-action/compare/v2.1.0...v2.2.0 219 | [2.1.0]: https://github.com/shimataro/ssh-key-action/compare/v2.0.3...v2.1.0 220 | [2.0.3]: https://github.com/shimataro/ssh-key-action/compare/v2.0.2...v2.0.3 221 | [2.0.2]: https://github.com/shimataro/ssh-key-action/compare/v2.0.1...v2.0.2 222 | [2.0.1]: https://github.com/shimataro/ssh-key-action/compare/v2.0.0...v2.0.1 223 | [2.0.0]: https://github.com/shimataro/ssh-key-action/compare/v1.6.5...v2.0.0 224 | [1.6.5]: https://github.com/shimataro/ssh-key-action/compare/v1.6.4...v1.6.5 225 | [1.6.4]: https://github.com/shimataro/ssh-key-action/compare/v1.6.3...v1.6.4 226 | [1.6.3]: https://github.com/shimataro/ssh-key-action/compare/v1.6.2...v1.6.3 227 | [1.6.2]: https://github.com/shimataro/ssh-key-action/compare/v1.6.1...v1.6.2 228 | [1.6.1]: https://github.com/shimataro/ssh-key-action/compare/v1.6.0...v1.6.1 229 | [1.6.0]: https://github.com/shimataro/ssh-key-action/compare/v1.5.0...v1.6.0 230 | [1.5.0]: https://github.com/shimataro/ssh-key-action/compare/v1.4.0...v1.5.0 231 | [1.4.0]: https://github.com/shimataro/ssh-key-action/compare/v1.3.0...v1.4.0 232 | [1.3.0]: https://github.com/shimataro/ssh-key-action/compare/v1.2.0...v1.3.0 233 | [1.2.0]: https://github.com/shimataro/ssh-key-action/compare/v1.1.0...v1.2.0 234 | [1.1.0]: https://github.com/shimataro/ssh-key-action/compare/v1.0.0...v1.1.0 235 | [1.0.0]: https://github.com/shimataro/ssh-key-action/compare/8deacc95b1ee5732107e56baa4c8aac4c386ef7e...v1.0.0 236 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 shimataro 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 | # Install SSH Key 2 | 3 | [![Build][image-build]][link-build] 4 | [![Windows][image-verify-windows]][link-verify-windows] 5 | [![macOS][image-verify-macos]][link-verify-macos] 6 | [![Ubuntu][image-verify-ubuntu]][link-verify-ubuntu] 7 | [![Docker container (Ubuntu)][image-verify-docker-container-ubuntu]][link-verify-docker-container-ubuntu] 8 | [![Docker container (CentOS)][image-verify-docker-container-centos]][link-verify-docker-container-centos] 9 | [![Docker container (Alpine Linux)][image-verify-docker-container-alpine]][link-verify-docker-container-alpine] 10 | [![Release][image-release]][link-release] 11 | [![License][image-license]][link-license] 12 | [![Stars][image-stars]][link-stars] 13 | 14 | This action installs SSH key in `~/.ssh`. 15 | 16 | Useful for SCP, SFTP, and `rsync` over SSH in deployment script. 17 | 18 | tested on: 19 | 20 | * [all available virtual machines](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners#supported-runners-and-hardware-resources) (Windows Server 2022/2019, macOS Monterey/Big Sur, and Ubuntu 22.04/20.04) 21 | * [Docker container (Ubuntu)](https://hub.docker.com/_/ubuntu) / requires `openssh-client` package; `apt install -y openssh-client` 22 | * [Docker container (CentOS)](https://quay.io/repository/centos/centos) / requires `openssh-clients` package; `yum install -y openssh-clients` 23 | * [Docker container (Alpine Linux)](https://hub.docker.com/_/alpine) / requires `openssh-client` package; `apk add openssh-client` 24 | 25 | ## Usage 26 | 27 | Add your SSH key to your product secrets by clicking `Settings` - `Secrets` - `Add a new secret` beforehand. 28 | 29 | PEM(RSA), PKCS8, and RFC4716(OpenSSH) formats are OK. 30 | 31 | ```yaml 32 | runs-on: ubuntu-latest 33 | steps: 34 | - name: Install SSH key 35 | uses: shimataro/ssh-key-action@v2 36 | with: 37 | key: ${{ secrets.SSH_KEY }} 38 | name: id_rsa # optional 39 | known_hosts: ${{ secrets.KNOWN_HOSTS }} 40 | config: ${{ secrets.CONFIG }} # ssh_config; optional 41 | if_key_exists: fail # replace / ignore / fail; optional (defaults to fail) 42 | - name: rsync over SSH 43 | run: rsync -r ./foo/ user@remote:bar/ 44 | ``` 45 | 46 | See [Workflow syntax for GitHub Actions](https://help.github.com/en/articles/workflow-syntax-for-github-actions) for details. 47 | 48 | **NOTE:** 49 | 50 | * Server key of `github.com` will be always set to `known_hosts`. 51 | * SSH keys will be removed at the end of workflow. 52 | 53 | ### Install multiple keys 54 | 55 | If you want to install multiple keys, call this action multiple times. 56 | It is useful for port forwarding. 57 | 58 | **NOTE:** When this action is called multiple times, **the contents of `known_hosts` and `config` will be appended**. `key` must be saved as different name, by using `name` option. 59 | 60 | ```yaml 61 | runs-on: ubuntu-latest 62 | steps: 63 | - name: Install SSH key of bastion 64 | uses: shimataro/ssh-key-action@v2 65 | with: 66 | key: ${{ secrets.SSH_KEY_OF_BASTION }} 67 | name: id_rsa-bastion 68 | known_hosts: ${{ secrets.KNOWN_HOSTS_OF_BASTION }} 69 | config: | 70 | Host bastion 71 | HostName xxx.xxx.xxx.xxx 72 | User user-of-bastion 73 | IdentityFile ~/.ssh/id_rsa-bastion 74 | - name: Install SSH key of target 75 | uses: shimataro/ssh-key-action@v2 76 | with: 77 | key: ${{ secrets.SSH_KEY_OF_TARGET }} 78 | name: id_rsa-target 79 | known_hosts: ${{ secrets.KNOWN_HOSTS_OF_TARGET }} # will be appended to existing .ssh/known_hosts 80 | config: | # will be appended to existing .ssh/config 81 | Host target 82 | HostName yyy.yyy.yyy.yyy 83 | User user-of-target 84 | IdentityFile ~/.ssh/id_rsa-target 85 | ProxyCommand ssh -W %h:%p bastion 86 | - name: SCP via port-forwarding 87 | run: scp ./foo/ target:bar/ 88 | ``` 89 | 90 | ## Q&A 91 | 92 | ### SSH failed even though key has been installed. 93 | 94 | Check below: 95 | 96 | * `Host key verification failed.`: 97 | * Set `known_hosts` parameter correctly (use `ssh-keyscan` command). 98 | 99 | ### I want to replace/ignore key if exists. 100 | 101 | Use `if_key_exists` parameter. 102 | 103 | * `replace`: replaces key 104 | * `ignore`: does nothing 105 | * `fail`: fails (default) 106 | 107 | ### How do I use encrypted SSH key? 108 | 109 | This action doesn't support encrypted key directly. 110 | Here are some solutions: 111 | 112 | * decrypting key beforehand: best bet, and works on any VM 113 | * `sshpass` command: next best bet, but not supported on Windows 114 | * `expect` command: be careful not to expose passphrase to console 115 | * `SSH_ASKPASS` environment variable: might be troublesome 116 | 117 | ### Which one is the best way for transferring files, "direct SCP/SFTP/rsync" or "SCP/SFTP/rsync via bastion"? 118 | 119 | I recommend **rsync via bastion**. 120 | 121 | ```bash 122 | rsync -r -e "ssh bastion ssh" ./foo/ target:bar/ 123 | ``` 124 | 125 | It has some advantages over other methods: 126 | 127 | * "Rsync via bastion" doesn't require to update workflow files and `secrets` even if it is necessary to transfer files to multiple servers. 128 | * Other methods require to update `known_hosts` if servers have changed. 129 | * Rsync: 130 | * is fastest of all. 131 | * does **NOT** break files even if disconnected during transferring. 132 | * can remove files that don't exist on server. 133 | * SCP is [deprecated by OpenSSH](https://www.openssh.com/txt/release-8.0) due to outdated and inflexible protocol. 134 | * Using bastion is more secure because: 135 | * it is not necessarily to expose SSH port on servers to public. 136 | * Address filtering is less effective. 137 | * Because Azure address range is [very wide](https://help.github.com/en/actions/automating-your-workflow-with-github-actions/virtual-environments-for-github-hosted-runners#ip-addresses-of-github-hosted-runners). 138 | * And will be updated continuously. 139 | * if security incident ―e.g., private key leaked― occurs, it's OK just to remove `authorized_keys` on bastion. 140 | 141 | ### I want to omit `known_hosts`. 142 | 143 | First of all, you have to understand that it is NOT secure to SSH with no `known_hosts` and using `StrictHostKeyChecking=no` option. 144 | 145 | Why do you want to omit it? 146 | If the reason is **"I'm not understanding about the function of `known_hosts`"** or **"It's bother to fetch server key"**, you should not omit. 147 | If **"It is hard to prefetch server key because the server will be created dynamically"**, you can use bastion server. 148 | 149 | **"`known_hosts` is unnecessary because I'm using secure method for SSH, such as SSHFP and signed server key."** — OK, here is a special value to omit `known_hosts`. 150 | You should use it ONLY IF you are using secure methods... 151 | It is `known_hosts: unnecessary`. 152 | 153 | ## License 154 | 155 | The scripts and documentation in this project are released under the [MIT License](LICENSE) 156 | 157 | ## Changelog 158 | 159 | See [CHANGELOG.md](CHANGELOG.md). 160 | 161 | [image-build]: https://github.com/shimataro/ssh-key-action/workflows/Build/badge.svg?event=push&branch=v2 162 | [link-build]: https://github.com/shimataro/ssh-key-action/actions/workflows/build.yml 163 | [image-verify-windows]: https://github.com/shimataro/ssh-key-action/workflows/Windows/badge.svg?event=push&branch=v2 164 | [link-verify-windows]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-windows.yml 165 | [image-verify-macos]: https://github.com/shimataro/ssh-key-action/workflows/macOS/badge.svg?event=push&branch=v2 166 | [link-verify-macos]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-macos.yml 167 | [image-verify-ubuntu]: https://github.com/shimataro/ssh-key-action/workflows/Ubuntu/badge.svg?event=push&branch=v2 168 | [link-verify-ubuntu]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-ubuntu.yml 169 | [image-verify-docker-container-ubuntu]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-container-ubuntu.yml/badge.svg?event=push&branch=v2 170 | [link-verify-docker-container-ubuntu]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-container-ubuntu.yml 171 | [image-verify-docker-container-centos]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-container-centos.yml/badge.svg?event=push&branch=v2 172 | [link-verify-docker-container-centos]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-container-centos.yml 173 | [image-verify-docker-container-alpine]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-container-alpine.yml/badge.svg?event=push&branch=v2 174 | [link-verify-docker-container-alpine]: https://github.com/shimataro/ssh-key-action/actions/workflows/verify-on-container-alpine.yml 175 | [image-release]: https://img.shields.io/github/release/shimataro/ssh-key-action.svg 176 | [link-release]: https://github.com/shimataro/ssh-key-action/releases 177 | [image-license]: https://img.shields.io/github/license/shimataro/ssh-key-action.svg 178 | [link-license]: ./LICENSE 179 | [image-stars]: https://img.shields.io/github/stars/shimataro/ssh-key-action.svg 180 | [link-stars]: https://github.com/shimataro/ssh-key-action/stargazers 181 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | # https://help.github.com/en/articles/metadata-syntax-for-github-actions 2 | name: "Install SSH Key" 3 | description: "Install SSH key in ~/.ssh" 4 | author: "shimataro" 5 | branding: 6 | icon: "terminal" 7 | color: "gray-dark" 8 | inputs: 9 | key: 10 | description: "SSH private key" 11 | required: true 12 | name: 13 | description: "SSH key file name (default: id_rsa)" 14 | required: false 15 | default: "id_rsa" 16 | known_hosts: 17 | description: "public keys of SSH servers" 18 | required: true 19 | default: "" 20 | config: 21 | description: "SSH config" 22 | required: false 23 | default: "" 24 | if_key_exists: 25 | description: "replace / ignore / fail" 26 | required: false 27 | default: "fail" 28 | runs: 29 | using: "node20" 30 | main: "./dist/main.js" 31 | post: "./dist/post.js" 32 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "install-ssh-key", 3 | "version": "2.7.0", 4 | "private": true, 5 | "description": "Install SSH key in .ssh", 6 | "main": "./dist/main.js", 7 | "engines": { 8 | "node": ">=8.0.0", 9 | "npm": ">=5.7.0" 10 | }, 11 | "scripts": { 12 | "build": "esbuild ./src/main.ts ./src/post.ts --bundle --platform=node --minify --sourcemap --outdir=./dist", 13 | "check-updates": "ncu", 14 | "lint": "run-p lint:*", 15 | "lint:es": "eslint ./src --ext .ts", 16 | "lint:ts": "tsc --noEmit ./src/main.ts ./src/post.ts", 17 | "lint:md": "markdownlint . --ignore node_modules --ignore examples", 18 | "lint:yaml": "yamllint **/{,.}*.{yml,yaml} --ignore=node_modules/**", 19 | "verify": "run-p lint" 20 | }, 21 | "repository": { 22 | "type": "git", 23 | "url": "git+https://github.com/shimataro/ssh-key-action.git" 24 | }, 25 | "keywords": [ 26 | "actions", 27 | "github actions", 28 | "rsync", 29 | "ssh", 30 | "ssh key" 31 | ], 32 | "author": "shimataro", 33 | "license": "MIT", 34 | "devDependencies": { 35 | "@actions/core": "1.10.1", 36 | "@types/node": "20.11.17", 37 | "@typescript-eslint/eslint-plugin": "6.21.0", 38 | "@typescript-eslint/parser": "6.21.0", 39 | "esbuild": "0.20.0", 40 | "eslint": "8.56.0", 41 | "markdownlint-cli": "0.39.0", 42 | "npm-check-updates": "16.14.15", 43 | "npm-run-all": "4.1.5", 44 | "typescript": "5.3.3", 45 | "yaml-lint": "1.7.0" 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /scripts/prepare-release.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # requires following programs: 3 | # - git; I believe you have already installed. 4 | # - sed; Both GNU sed and POSIX sed will work. 5 | set -eu 6 | 7 | GITHUB_BASE="https://github.com" 8 | GITHUB_USER="shimataro" 9 | GITHUB_REPO="ssh-key-action" 10 | 11 | UPSTREAM="origin" 12 | 13 | COLOR_ERROR="\033[1;41m" 14 | COLOR_SECTION="\033[1;34m" 15 | COLOR_COMMAND_NAME="\033[1;34m" 16 | COLOR_OPTION="\033[4;36m" 17 | COLOR_COMMAND="\033[4m" 18 | COLOR_FILE="\033[1;34m" 19 | COLOR_BRANCH="\033[1;31m" 20 | COLOR_INPUT="\033[1;31m" 21 | COLOR_SELECT="\033[1;32m" 22 | COLOR_RESET="\033[m" 23 | 24 | URL_PRODUCT="${GITHUB_BASE}/${GITHUB_USER}/${GITHUB_REPO}" 25 | URL_REPOSITORY="${URL_PRODUCT}.git" 26 | URL_COMPARE="${URL_PRODUCT}/compare" 27 | URL_RELEASE="${URL_PRODUCT}/releases/new" 28 | 29 | function main() { 30 | cd $(dirname ${0})/.. 31 | 32 | if [[ $# -lt 1 ]]; then 33 | usage 34 | fi 35 | 36 | local NEW_VERSION=$1 37 | local CURRENT_VERSION=$( 38 | node -e 'console.log(JSON.parse(require("fs").readFileSync("package.json")).version)' 39 | ) 40 | local TAG="v${NEW_VERSION}" 41 | local BRANCH="release/v${NEW_VERSION}" 42 | local BASE_BRANCH="${TAG%%.*}" 43 | 44 | check_version_format ${NEW_VERSION} 45 | check_current_branch ${BASE_BRANCH} 46 | 47 | create_branch ${BRANCH} ${BASE_BRANCH} 48 | update_package_version ${NEW_VERSION} 49 | update_changelog ${NEW_VERSION} 50 | verify_package 51 | commit_changes ${NEW_VERSION} 52 | finish ${NEW_VERSION} ${CURRENT_VERSION} ${BRANCH} ${BASE_BRANCH} ${TAG} 53 | } 54 | 55 | function usage() { 56 | local COMMAND=$(basename ${0}) 57 | 58 | echo -e "${COLOR_SECTION}NAME${COLOR_RESET} 59 | ${COMMAND} - Prepare for new release 60 | 61 | ${COLOR_SECTION}SYNOPSIS${COLOR_RESET} 62 | ${COLOR_COMMAND_NAME}${COMMAND}${COLOR_RESET} <${COLOR_OPTION}new-version${COLOR_RESET}> 63 | 64 | ${COLOR_SECTION}DESCRIPTION${COLOR_RESET} 65 | This command will... 66 | - create a new branch for release 67 | - update ${COLOR_FILE}CHANGELOG.md${COLOR_RESET} 68 | - update package version in ${COLOR_FILE}package.json${COLOR_RESET} 69 | - update dependencies version in ${COLOR_FILE}package.json${COLOR_RESET} 70 | - verify 71 | - ...and commit! 72 | 73 | ${COLOR_OPTION}new-version${COLOR_RESET} must follow \"Semantic Versioning\" . 74 | " 75 | exit 1 76 | } 77 | 78 | function check_version_format() { 79 | if [[ ${1} =~ ^(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*) ]]; then 80 | return 81 | fi 82 | 83 | echo -e "${COLOR_ERROR}ERROR:${COLOR_RESET} Follow \"Semantic Versioning\" for new version. 84 | " >&2 85 | exit 2 86 | } 87 | 88 | function check_current_branch() { 89 | local BASE_BRANCH=$1 90 | local CURRENT_BRANCH=$(git rev-parse --abbrev-ref HEAD) 91 | if [ ${CURRENT_BRANCH} = ${BASE_BRANCH} ]; then 92 | return 93 | fi 94 | 95 | echo -e "${COLOR_ERROR}ERROR:${COLOR_RESET} Work on ${COLOR_BRANCH}${BASE_BRANCH}${COLOR_RESET} branch 96 | ${COLOR_COMMAND}git checkout ${BASE_BRANCH}${COLOR_RESET} 97 | " >&2 98 | exit 2 99 | } 100 | 101 | function create_branch() { 102 | local BRANCH=$1 103 | local BASE_BRANCH=$2 104 | 105 | git checkout -b ${BRANCH} ${BASE_BRANCH} 106 | } 107 | 108 | function update_package_version() { 109 | local VERSION=$1 110 | 111 | npm version --no-git-tag-version ${VERSION} 112 | } 113 | 114 | function update_changelog() { 115 | local VERSION=$1 116 | local DATE=`date "+%Y-%m-%d"` 117 | local KEYWORD="Unreleased" 118 | 119 | sed -i".bak" -r \ 120 | -e "s/^((##[[:space:]]+)\[${KEYWORD}\])$/\1\n\n\2[${VERSION}] - ${DATE}/" \ 121 | -e "s/^(\[${KEYWORD}\](.*))(v.*)\.\.\.HEAD$/\1v${VERSION}...HEAD\n[${VERSION}]\2\3...v${VERSION}/" \ 122 | CHANGELOG.md 123 | } 124 | 125 | function verify_package() { 126 | npm run verify 127 | } 128 | 129 | function commit_changes() { 130 | local VERSION=$1 131 | 132 | git add CHANGELOG.md package.json package-lock.json 133 | git commit -m "version ${VERSION}" 134 | } 135 | 136 | function finish() { 137 | local NEW_VERSION="${1}" 138 | local CURRENT_VERSION="${2}" 139 | local BRANCH="${3}" 140 | local BASE_BRANCH="${4}" 141 | local TAG="${5}" 142 | 143 | local TITLE="${GITHUB_REPO} ${NEW_VERSION} released" 144 | local CHANGELOG=$( 145 | git diff v${CURRENT_VERSION} -- CHANGELOG.md | 146 | sed -r -e '/^[^+]/d' -e 's/^\+(.*)$/\1/' -e '/^## /d' -e '/^\+/d' -e '/^\[/d' | 147 | urlencode | 148 | replace_lf 149 | ) 150 | local PRERELEASE=0 151 | if [[ ${NEW_VERSION} == "0."* ]]; then 152 | # < 1.0.0 153 | PRERELEASE=1 154 | fi 155 | if [[ ${NEW_VERSION} =~ -[0-9a-zA-Z] ]]; then 156 | # -alpha, -pre, -rc, etc... 157 | PRERELEASE=1 158 | fi 159 | 160 | echo -e " 161 | Branch ${COLOR_BRANCH}${BRANCH}${COLOR_RESET} has been created. 162 | Remaining processes are... 163 | 164 | 1. Make sure all changes are correct 165 | ${COLOR_COMMAND}git diff ${BASE_BRANCH} ${BRANCH}${COLOR_RESET} 166 | 2. Push to remote ${UPSTREAM} 167 | ${COLOR_COMMAND}git push --set-upstream ${UPSTREAM} ${BRANCH}${COLOR_RESET} 168 | 3. Create a pull-request: ${COLOR_BRANCH}${BRANCH}${COLOR_RESET} to ${COLOR_BRANCH}${BASE_BRANCH}${COLOR_RESET} 169 | ${URL_COMPARE}/${BASE_BRANCH}...${BRANCH}?expand=1 170 | select ${COLOR_SELECT}Squash and merge${COLOR_RESET} 171 | 4. Create a new release 172 | ${URL_RELEASE}?tag=${TAG}&target=${BASE_BRANCH}&title=$(urlencode <<<"${TITLE}")&body=${CHANGELOG}&prerelease=${PRERELEASE} 173 | Tag version: ${COLOR_INPUT}${TAG}${COLOR_RESET} 174 | Target: ${COLOR_INPUT}${BASE_BRANCH}${COLOR_RESET} 175 | Release title: ${COLOR_INPUT}${TITLE}${COLOR_RESET} 176 | Description this release: (copy and paste CHANGELOG.md) 177 | 5. Post processing 178 | ${COLOR_COMMAND}git checkout ${BASE_BRANCH}${COLOR_RESET} 179 | ${COLOR_COMMAND}git pull${COLOR_RESET} 180 | ${COLOR_COMMAND}git fetch -p${COLOR_RESET} 181 | ${COLOR_COMMAND}git branch -D ${BRANCH}${COLOR_RESET} 182 | 183 | That's all! 184 | " 185 | } 186 | 187 | function urlencode() { 188 | # https://developer.mozilla.org/en-US/docs/Glossary/percent-encoding 189 | sed -r \ 190 | -e 's/%/%25/g' \ 191 | -e 's/\$/%24/g' -e 's/\(/%28/g' -e 's/\)/%29/g' -e 's/\*/%2A/g' -e 's/\+/%2B/g' -e 's/\//%2F/g' -e 's/\?/%3F/g' -e 's/\[/%5B/g' -e 's/\]/%5D/g' \ 192 | -e 's/!/%21/g' -e 's/#/%23/g' -e 's/&/%26/g' -e "s/'/%27/g" -e 's/,/%2C/g' -e 's/:/%3A/g' -e 's/;/%3B/g' -e 's/=/%3D/g' -e 's/@/%40/g' \ 193 | -e 's/ /+/g' 194 | } 195 | 196 | function replace_lf() { 197 | sed -r \ 198 | -e ':a' -e 'N' -e '$!ba' \ 199 | -e 's/^\n+//' -e 's/\n+$//' -e 's/\n/%0A/g' 200 | } 201 | 202 | main "$@" 203 | -------------------------------------------------------------------------------- /scripts/rebuild.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | set -eu 3 | 4 | npm ci 5 | npm run build 6 | npm run verify 7 | 8 | rm -rf node_modules 9 | npm ci --only=production 10 | git add dist 11 | -------------------------------------------------------------------------------- /scripts/update-dependencies.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # update dependencies 3 | set -eu 4 | 5 | DATE=$(date +"%Y%m%d") 6 | BASE_BRANCH=$(git rev-parse --abbrev-ref HEAD) 7 | TARGET_BRANCH=feature/update-dependencies-${DATE} 8 | 9 | COLOR_SUCCESS="\033[1;32m" 10 | COLOR_ERROR="\033[1;41m" 11 | COLOR_RESET="\033[m" 12 | 13 | cd $(dirname ${0})/.. 14 | 15 | # create target branch 16 | if [[ ! ${BASE_BRANCH} =~ ^v[0-9]+$ ]]; then 17 | echo -e "${COLOR_ERROR}Error:${COLOR_RESET} Base branch must match 'v*'; got '${BASE_BRANCH}'." 18 | exit 1 19 | fi 20 | git checkout -b ${TARGET_BRANCH} 21 | 22 | # check updates 23 | npm ci 24 | npm run check-updates -- -u 25 | 26 | # re-install packages 27 | rm -rf package-lock.json node_modules 28 | npm i 29 | npm dedupe 30 | 31 | # test 32 | npm run build 33 | npm run verify 34 | 35 | # commit 36 | git add package.json package-lock.json dist 37 | git commit -m "update dependencies" 38 | 39 | # finished! 40 | echo -e " 41 | ${COLOR_SUCCESS}🎉All dependencies are updated successfully.🎉${COLOR_RESET} 42 | 43 | Push changes and merge into '${BASE_BRANCH}' branch. 44 | 45 | git push --set-upstream origin ${TARGET_BRANCH} 46 | " 47 | -------------------------------------------------------------------------------- /src/common.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as os from "os"; 3 | import * as path from "path"; 4 | 5 | import * as core from "@actions/core"; 6 | 7 | /** state name of backup suffix */ 8 | const STATE_BACKUP_SUFFIX = "backup-suffix"; 9 | const STATE_CREATED_FILES = "created-files"; 10 | 11 | /** 12 | * create backup suffix name 13 | * @param dirName directory to back up 14 | * @returns backup suffix; empty string if directory does not exist 15 | */ 16 | export function createBackupSuffix(dirName: string): string { 17 | if (!fs.existsSync(dirName)) { 18 | return ""; 19 | } 20 | 21 | const backupSuffix = `.bak-${Date.now()}`; 22 | core.saveState(STATE_BACKUP_SUFFIX, backupSuffix); 23 | return backupSuffix; 24 | } 25 | 26 | /** 27 | * get backup suffix name 28 | * @returns backup suffix (if not, empty string) 29 | */ 30 | export function getBackupSuffix(): string { 31 | return core.getState(STATE_BACKUP_SUFFIX); 32 | } 33 | 34 | /** 35 | * save created file names 36 | * @param fileNames array of file names 37 | */ 38 | export function saveCreatedFileNames(fileNames: string[]): void { 39 | const json = JSON.stringify(fileNames); 40 | core.saveState(STATE_CREATED_FILES, json); 41 | } 42 | 43 | /** 44 | * save created file names 45 | * @returns saved array of file names 46 | */ 47 | export function loadCreatedFileNames(): string[] { 48 | const json = core.getState(STATE_CREATED_FILES); 49 | if (json === "") { 50 | return []; 51 | } 52 | 53 | return JSON.parse(json) as string[]; 54 | } 55 | 56 | /** 57 | * get SSH directory 58 | * @returns SSH directory name 59 | */ 60 | export function getSshDirectory(): string { 61 | return path.resolve(getHomeDirectory(), ".ssh"); 62 | } 63 | 64 | /** 65 | * get home directory 66 | * @returns home directory name 67 | */ 68 | function getHomeDirectory(): string { 69 | const homedir = os.homedir(); 70 | if (homedir === "/github/home") { 71 | // Docker container 72 | return "/root"; 73 | } 74 | 75 | return homedir; 76 | } 77 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | import * as core from "@actions/core"; 5 | 6 | import * as common from "./common"; 7 | 8 | /** file creation info */ 9 | interface FileInfo { 10 | /** file name */ 11 | name: string; 12 | /** file contents */ 13 | contents: string; 14 | /** creation options */ 15 | options: fs.WriteFileOptions; 16 | /** file must not exist when creating */ 17 | mustNotExist: boolean; 18 | } 19 | 20 | /** default known_hosts */ 21 | const KNOWN_HOSTS = [ 22 | "github.com ssh-rsa AAAAB3NzaC1yc2EAAAADAQABAAABgQCj7ndNxQowgcQnjshcLrqPEiiphnt+VTTvDP6mHBL9j1aNUkY4Ue1gvwnGLVlOhGeYrnZaMgRK6+PKCUXaDbC7qtbW8gIkhL7aGCsOr/C56SJMy/BCZfxd1nWzAOxSDPgVsmerOBYfNqltV9/hWCqBywINIR+5dIg6JTJ72pcEpEjcYgXkE2YEFXV1JHnsKgbLWNlhScqb2UmyRkQyytRLtL+38TGxkxCflmO+5Z8CSSNY7GidjMIZ7Q4zMjA2n1nGrlTDkzwDCsw+wqFPGQA179cnfGWOWRVruj16z6XyvxvjJwbz0wQZ75XK5tKSb7FNyeIEs4TT4jk+S4dhPeAUC5y+bDYirYgM4GC7uEnztnZyaVWQ7B381AK4Qdrwt51ZqExKbQpTUNn+EjqoTwvqNj4kqx5QUCI0ThS/YkOxJCXmPUWZbhjpCg56i+2aB6CmK2JGhn57K5mj0MNdBXA4/WnwH6XoPWJzK5Nyu2zB3nAZp+S5hpQs+p1vN1/wsjk=", 23 | ]; 24 | 25 | try { 26 | main(); 27 | } catch (err) { 28 | if (err instanceof Error) { 29 | core.setFailed(err); 30 | } 31 | } 32 | 33 | /** 34 | * main function 35 | */ 36 | export function main(): void { 37 | const sshDirName = common.getSshDirectory(); 38 | 39 | // create ".ssh" directory 40 | const backupSuffix = common.createBackupSuffix(sshDirName); 41 | if (backupSuffix === "") { 42 | createDirectory(sshDirName); 43 | console.log(`✅SSH directory "${sshDirName}" has been created successfully.`); 44 | } 45 | 46 | // files to be created 47 | const files = buildFilesToCreate(sshDirName); 48 | 49 | // back up & create files 50 | const createdFileNames: string[] = []; 51 | const backedUpFileNames: string[] = []; 52 | for (const file of files) { 53 | const pathName = path.join(sshDirName, file.name); 54 | if (backup(pathName, backupSuffix, file.mustNotExist)) { 55 | backedUpFileNames.push(file.name); 56 | } 57 | 58 | fs.writeFileSync(pathName, file.contents, file.options); 59 | createdFileNames.push(file.name); 60 | } 61 | common.saveCreatedFileNames(createdFileNames); 62 | 63 | if (createdFileNames.length > 0) { 64 | console.log(`✅Following files have been created in "${sshDirName}" successfully; ${createdFileNames.join(", ")}`); 65 | } 66 | if (backedUpFileNames.length > 0) { 67 | console.log(`✅Following files have been backed up in suffix "${backupSuffix}" successfully; ${backedUpFileNames.join(", ")}`); 68 | } 69 | } 70 | 71 | /** 72 | * build files to create 73 | * @param dirName directory name in where files will be created 74 | * @returns files 75 | */ 76 | function buildFilesToCreate(dirName: string): FileInfo[] { 77 | // parameters 78 | const key = core.getInput("key", { 79 | required: true, 80 | }); 81 | const name = core.getInput("name"); 82 | const knownHosts = core.getInput("known_hosts", { 83 | required: true, 84 | }); 85 | const config = core.getInput("config"); 86 | const ifKeyExists = core.getInput("if_key_exists"); 87 | 88 | // files to be created 89 | const files: FileInfo[] = [ 90 | { 91 | name: "known_hosts", 92 | contents: insertLf(buildKnownHostsArray(knownHosts).join("\n"), true, true), 93 | options: { 94 | mode: 0o644, 95 | flag: "a", 96 | }, 97 | mustNotExist: false, 98 | }, 99 | ]; 100 | if (shouldCreateKeyFile(path.join(dirName, name), ifKeyExists)) { 101 | files.push({ 102 | name: name, 103 | contents: insertLf(key, false, true), 104 | options: { 105 | mode: 0o400, 106 | flag: "wx", 107 | }, 108 | mustNotExist: true, 109 | }); 110 | } 111 | if (config !== "") { 112 | files.push({ 113 | name: "config", 114 | contents: insertLf(config, true, true), 115 | options: { 116 | mode: 0o644, 117 | flag: "a", 118 | }, 119 | mustNotExist: false, 120 | }); 121 | } 122 | 123 | return files; 124 | } 125 | 126 | /** 127 | * create directory 128 | * @param dirName directory name to remove 129 | */ 130 | function createDirectory(dirName: string): void { 131 | fs.mkdirSync(dirName, { 132 | recursive: true, 133 | mode: 0o700, 134 | }); 135 | } 136 | 137 | /** 138 | * back up file 139 | * @param fileName file to back up 140 | * @param backupSuffix suffix 141 | * @param removeOrig remove original file 142 | * @returns is file backed up? 143 | */ 144 | function backup(fileName: string, backupSuffix: string, removeOrig: boolean): boolean { 145 | if (backupSuffix === "") { 146 | return false; 147 | } 148 | if (!fs.existsSync(fileName)) { 149 | return false; 150 | } 151 | 152 | // move -> copy (in order to keep permissions when restore) 153 | const fileNameBak = `${fileName}${backupSuffix}`; 154 | fs.renameSync(fileName, fileNameBak); 155 | if (!removeOrig) { 156 | fs.copyFileSync(fileNameBak, fileName); 157 | } 158 | 159 | return true; 160 | } 161 | 162 | /** 163 | * prepend/append LF to value if not empty 164 | * @param value the value to insert LF 165 | * @param prepend true to prepend 166 | * @param append true to append 167 | * @returns new value 168 | */ 169 | function insertLf(value: string, prepend: boolean, append: boolean): string { 170 | let affectedValue = value; 171 | 172 | if (value.length === 0) { 173 | // do nothing if empty 174 | return ""; 175 | } 176 | if (prepend && !affectedValue.startsWith("\n")) { 177 | affectedValue = `\n${affectedValue}`; 178 | } 179 | if (append && !affectedValue.endsWith("\n")) { 180 | affectedValue = `${affectedValue}\n`; 181 | } 182 | 183 | return affectedValue; 184 | } 185 | 186 | /** 187 | * should create SSH key file? 188 | * @param keyFilePath path of key file 189 | * @param ifKeyExists action if SSH key exists 190 | * @returns Yes/No 191 | */ 192 | function shouldCreateKeyFile(keyFilePath: string, ifKeyExists: string): boolean { 193 | if (!fs.existsSync(keyFilePath)) { 194 | // should create if file does not exist 195 | return true; 196 | } 197 | 198 | switch (ifKeyExists) { 199 | case "replace": 200 | // should create if replace (existing file will be backed up when creating) 201 | return true; 202 | 203 | case "ignore": 204 | // should NOT create if ignore 205 | return false; 206 | 207 | default: 208 | // error otherwise 209 | throw new Error(`SSH key is already installed. Set "if_key_exists" to "replace" or "ignore" in order to avoid this error.`); 210 | } 211 | } 212 | 213 | /** 214 | * build array of known_hosts 215 | * @param knownHosts known_hosts 216 | * @returns array of known_hosts 217 | */ 218 | function buildKnownHostsArray(knownHosts: string): string[] { 219 | if (knownHosts === "unnecessary") { 220 | return KNOWN_HOSTS; 221 | } 222 | return KNOWN_HOSTS.concat(knownHosts); 223 | } 224 | -------------------------------------------------------------------------------- /src/post.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | import * as core from "@actions/core"; 5 | 6 | import * as common from "./common"; 7 | 8 | try { 9 | post(); 10 | } catch (err) { 11 | if (err instanceof Error) { 12 | core.setFailed(err); 13 | } 14 | } 15 | 16 | /** 17 | * cleanup function 18 | */ 19 | export function post(): void { 20 | const sshDirName = common.getSshDirectory(); 21 | const backupSuffix = common.getBackupSuffix(); 22 | if (backupSuffix === "") { 23 | // remove ".ssh" directory if suffix is not set 24 | removeDirectory(sshDirName); 25 | console.log(`✅SSH directory "${sshDirName}" has been removed successfully.`); 26 | } else { 27 | // remove created files and restore from backup 28 | const removedFileNames = removeCreatedFiles(sshDirName); 29 | if (removedFileNames.length > 0) { 30 | console.log(`✅Following files have been removed successfully; ${removedFileNames.join(", ")}`); 31 | } 32 | 33 | const restoredFileNames = restoreFiles(sshDirName, backupSuffix); 34 | if (restoredFileNames.length > 0) { 35 | console.log(`✅Following files in suffix "${backupSuffix}" have been restored successfully; ${restoredFileNames.join(", ")}`); 36 | } 37 | } 38 | } 39 | 40 | /** 41 | * remove directory 42 | * @param dirName directory name to remove 43 | */ 44 | function removeDirectory(dirName: string): void { 45 | fs.rmSync(dirName, { 46 | recursive: true, 47 | force: true, 48 | }); 49 | } 50 | 51 | /** 52 | * remove created files in main phase 53 | * @param dirName directory name 54 | * @returns removed file names 55 | */ 56 | function removeCreatedFiles(dirName: string): string[] { 57 | const createdFileNames = common.loadCreatedFileNames(); 58 | for (const fileName of createdFileNames) { 59 | const pathName = path.join(dirName, fileName); 60 | 61 | fs.rmSync(pathName); 62 | } 63 | return createdFileNames; 64 | } 65 | 66 | /** 67 | * restore files from backups 68 | * @param dirName directory name 69 | * @param backupSuffix suffix of backup directory 70 | * @returns restored file names 71 | */ 72 | function restoreFiles(dirName: string, backupSuffix: string): string[] { 73 | const restoredFileNames: string[] = []; 74 | const entries = fs.readdirSync(dirName) 75 | .filter((entry) => { 76 | // skip if not a backed-up file 77 | return entry.endsWith(backupSuffix); 78 | }); 79 | 80 | for (const entry of entries) { 81 | const entryOrg = entry.substring(0, entry.length - backupSuffix.length); 82 | const pathNameOrg = path.join(dirName, entryOrg); 83 | const pathNameBak = path.join(dirName, entry); 84 | 85 | fs.renameSync(pathNameBak, pathNameOrg); 86 | restoredFileNames.push(entryOrg); 87 | } 88 | return restoredFileNames; 89 | } 90 | -------------------------------------------------------------------------------- /ssh-key-action.code-workspace: -------------------------------------------------------------------------------- 1 | { 2 | "folders": [ 3 | { 4 | "path": "." 5 | } 6 | ], 7 | "settings": { 8 | "eslint.enable": true, 9 | "json.format.enable": true, 10 | "typescript.format.enable": true, 11 | "typescript.format.insertSpaceAfterCommaDelimiter": true, 12 | "typescript.format.insertSpaceAfterConstructor": false, 13 | "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": false, 14 | "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": false, 15 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": false, 16 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": false, 17 | "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": false, 18 | "typescript.format.insertSpaceAfterSemicolonInForStatements": true, 19 | "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": true, 20 | "typescript.format.insertSpaceBeforeFunctionParenthesis": false, 21 | "typescript.format.placeOpenBraceOnNewLineForFunctions": true, 22 | "typescript.format.placeOpenBraceOnNewLineForControlBlocks": true, 23 | "typescript.validate.enable": true 24 | }, 25 | "extensions": { 26 | "recommendations": [ 27 | "EditorConfig.EditorConfig", 28 | "eg2.vscode-npm-script" 29 | ] 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es6", 4 | "module": "commonjs", 5 | "moduleResolution": "node", 6 | "lib": [ 7 | "es6" 8 | ], 9 | "sourceMap": true, 10 | "outDir": "./dist", 11 | "rootDir": "./src", 12 | 13 | "strict": true, 14 | "noImplicitAny": true, 15 | "strictNullChecks": true, 16 | "strictFunctionTypes": true, 17 | "strictBindCallApply": true, 18 | "strictPropertyInitialization": true, 19 | "noImplicitThis": true, 20 | "alwaysStrict": true, 21 | 22 | "noUnusedLocals": true, 23 | "noUnusedParameters": true, 24 | "noImplicitReturns": true, 25 | "noFallthroughCasesInSwitch": true, 26 | 27 | "esModuleInterop": true 28 | }, 29 | "exclude": ["node_modules", "**/*.test.ts"] 30 | } 31 | --------------------------------------------------------------------------------