├── .github └── workflows │ ├── tests-gh-app.yml │ └── tests-ssh.yml ├── .gitignore ├── LICENSE ├── README.md ├── action.yml ├── dist ├── cleanup │ └── index.js └── index.js ├── package-lock.json ├── package.json └── src ├── action-parser.js ├── cleanup.js ├── github-app-setup.js ├── index.js └── ssh-setup.js /.github/workflows/tests-gh-app.yml: -------------------------------------------------------------------------------- 1 | name: GitHub App tests 2 | 3 | on: 4 | - push 5 | - pull_request_target 6 | 7 | env: 8 | PRIVATE_CUSTOM_ACTIONS: '["daspn/private-custom-action-2@master"]' 9 | 10 | jobs: 11 | app-key-base64: 12 | name: GitHub app private checkout with a base64 key encoded 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Private actions checkout 22 | uses: ./ 23 | with: 24 | actions_list: ${{ env.PRIVATE_CUSTOM_ACTIONS }} 25 | checkout_base_path: ./.github/actions 26 | app_id: ${{ secrets.APP_ID }} 27 | app_private_key: ${{ secrets.APP_PRIVATE_KEY_BASE64 }} 28 | 29 | - name: Say hello 30 | uses: ./.github/actions/private-custom-action-2 31 | with: 32 | who_to_greet: World 33 | 34 | app-key-plain: 35 | name: GitHub app private checkout with a plain key encoded 36 | runs-on: ${{ matrix.os }} 37 | strategy: 38 | matrix: 39 | os: [ubuntu-latest, macos-latest] 40 | 41 | steps: 42 | - uses: actions/checkout@v2 43 | 44 | - name: Private actions checkout 45 | uses: ./ 46 | with: 47 | actions_list: ${{ env.PRIVATE_CUSTOM_ACTIONS }} 48 | checkout_base_path: ./.github/actions 49 | app_id: ${{ secrets.APP_ID }} 50 | app_private_key: ${{ secrets.APP_PRIVATE_KEY_PLAIN }} 51 | 52 | - name: Say hello 53 | uses: ./.github/actions/private-custom-action-2 54 | with: 55 | who_to_greet: World 56 | -------------------------------------------------------------------------------- /.github/workflows/tests-ssh.yml: -------------------------------------------------------------------------------- 1 | name: SSH Tests 2 | 3 | on: 4 | - push 5 | - pull_request_target 6 | 7 | env: 8 | PRIVATE_CUSTOM_ACTIONS: '["daspn/private-custom-action-2@master"]' 9 | 10 | jobs: 11 | ssh-key-provided: 12 | name: SSH Private Key Provided Test 13 | runs-on: ${{ matrix.os }} 14 | strategy: 15 | matrix: 16 | os: [ubuntu-latest, macos-latest] 17 | 18 | steps: 19 | - uses: actions/checkout@v2 20 | 21 | - name: Private actions checkout 22 | uses: ./ 23 | with: 24 | actions_list: ${{ env.PRIVATE_CUSTOM_ACTIONS }} 25 | checkout_base_path: ./.github/actions 26 | ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} 27 | 28 | - name: Say hello 29 | uses: ./.github/actions/private-custom-action-2 30 | with: 31 | who_to_greet: World 32 | 33 | no-ssh-key-provided: 34 | name: No SSH Private Key Provided Test 35 | runs-on: ${{ matrix.os }} 36 | strategy: 37 | matrix: 38 | os: [ubuntu-latest, macos-latest] 39 | 40 | steps: 41 | - uses: actions/checkout@v2 42 | 43 | - uses: webfactory/ssh-agent@v0.5.2 44 | with: 45 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 46 | 47 | - name: Private actions checkout 48 | uses: ./ 49 | with: 50 | actions_list: ${{ env.PRIVATE_CUSTOM_ACTIONS }} 51 | checkout_base_path: ./.github/actions 52 | 53 | - name: Say hello 54 | uses: ./.github/actions/private-custom-action-2 55 | with: 56 | who_to_greet: World 57 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 daspn 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 | [![SSH Tests](https://github.com/daspn/private-actions-checkout/actions/workflows/tests-ssh.yml/badge.svg)](https://github.com/daspn/private-actions-checkout/actions/workflows/tests-ssh.yml) 2 | [![GitHub App tests](https://github.com/daspn/private-actions-checkout/actions/workflows/tests-gh-app.yml/badge.svg)](https://github.com/daspn/private-actions-checkout/actions/workflows/tests-gh-app.yml) 3 | 4 | # private-actions-checkout 5 | 6 | Simplifies using custom private actions (and promotes code reuse) by looping through a list of repositories and checking 7 | them out into the job's workspace. Supports using GitHub Apps or multiple SSH keys. 8 | 9 | Optionally configures git to allow subsequent steps to provide authenticated access private repos. 10 | 11 | This action is tested on `ubuntu-latest` and `macos-latest`. No Windows support yet. 12 | 13 | ## Usage 14 | 15 | ### Pre-requisites 16 | Create a workflow `.yml` file in your repositories `.github/workflows` directory. An [example workflow](#example-workflows) is available below. For more information, reference the GitHub Help Documentation for [Creating a workflow file](https://help.github.com/en/articles/configuring-a-workflow#creating-a-workflow-file). 17 | 18 | ### Supported authentications 19 | To checkout private repositories we need a supported authentication accepted by the Git protocol. This action supports 20 | GitHub Apps and SSH keys. 21 | 22 | For enterprise environments we recommend using a GitHub app per organization with 1 installation as it doesn't require having a machine account. 23 | 24 | ### Inputs 25 | 26 | * `actions_list` - **OPTIONAL**: List of private actions to checkout. Must be a JSON array and each entry must match the format owner/repo@ref. May be an empty array if no actions are required. 27 | * `checkout_base_path` - **OPTIONAL**: Where to checkout the custom actions. It uses `./.github/actions` as default path 28 | * `return_app_token` - **OPTIONAL**: If set to `true` then an output variable called `app-token` will be set that can be used for basic auth to github by subsequent steps (only works with Github Apps as the authentication method) 29 | * `configure_git` - **OPTIONAL**: If set to `true` then `git config` is executed to grant subsequent steps access to other private repos using the ssh or Github App token. 30 | 31 | If you want to use **GitHub Apps** (recommended): 32 | * `app_id`: the GitHub App id obtained when you [create a GitHub app](https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app) 33 | * `app_private_key`: the [GitHub App private key](https://docs.github.com/en/free-pro-team@latest/developers/apps/authenticating-with-github-apps#generating-a-private-key) generated for an app with permissions on the repositories 34 | 35 | > We support the key being plain and base64 encoded. To encode the private key you can use the following command: `cat key.pem | base64 | tr -d \\n && echo` 36 | 37 | If you want to use **SSH keys**: 38 | * `ssh_private_key` - **OPTIONAL**: If provided, configures the `ssh-agent` with the given private key. If not provided the code assumes that valid SSH credentials are available to the `git` executable. If `configure_git` is enabled then the agent will be left running until the end of the job. 39 | 40 | ## GitHub app requisites 41 | If you want to use this action with a GitHub app you will need to setup some permissions. 42 | Follow the [create GitHub app guide](https://docs.github.com/en/free-pro-team@latest/developers/apps/creating-a-github-app) and check: 43 | - **Repository permissions** 44 | - Contents: read 45 | 46 | Once it is created, [install the GitHub app](https://docs.github.com/en/free-pro-team@latest/developers/apps/installing-github-apps) in your account or organization and grant access to the repositories that contain the actions you want to 47 | checkout (or all if you are not concerned the app has wide access in the account or org). 48 | 49 | ## Example workflows 50 | ### GitHub app 51 | ```yaml 52 | name: 'Example workflow' 53 | 54 | on: push 55 | 56 | jobs: 57 | example: 58 | runs-on: ubuntu-18.04 59 | 60 | steps: 61 | - uses: actions/checkout@v2 62 | 63 | - name: Private actions checkout 64 | uses: daspn/private-actions-checkout@v2 65 | with: 66 | actions_list: '["githubuser/my-private-action-1@v1", "githubuser/my-private-action-2@v1"]' 67 | checkout_base_path: ./.github/actions 68 | app_id: ${{ secrets.APP_ID }} 69 | app_private_key: ${{ secrets.APP_PRIVATE_KEY }} 70 | 71 | - name: Validation 72 | run: | 73 | ls -lR ./.github/actions 74 | 75 | # the custom private action will be available on the job's workspace 76 | - name: 'Using custom private action 1' 77 | uses: ./.github/actions/my-private-action-1 78 | with: 79 | some_arg: test 80 | 81 | - name: 'Using custom private action 2' 82 | uses: ./.github/actions/my-private-action-2 83 | ``` 84 | 85 | ### SSH 86 | **Single SSH key:** 87 | ```yaml 88 | name: 'Example workflow' 89 | 90 | on: push 91 | 92 | jobs: 93 | example: 94 | runs-on: ubuntu-18.04 95 | 96 | steps: 97 | - uses: actions/checkout@v2 98 | 99 | - name: Private actions checkout 100 | uses: daspn/private-actions-checkout@v2 101 | with: 102 | actions_list: '["githubuser/my-private-action-1@v1", "githubuser/my-private-action-2@v1"]' 103 | checkout_base_path: ./.github/actions 104 | ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY }} 105 | 106 | - name: Validation 107 | run: | 108 | ls -lR ./.github/actions 109 | 110 | # the custom private action will be available on the job's workspace 111 | - name: 'Using custom private action 1' 112 | uses: ./.github/actions/my-private-action-1 113 | with: 114 | some_arg: test 115 | 116 | - name: 'Using custom private action 2' 117 | uses: ./.github/actions/my-private-action-2 118 | ``` 119 | 120 | **Multiple SSH keys workflow example:** 121 | ```yaml 122 | name: 'Multiple SSH Keys workflow example' 123 | 124 | on: push 125 | 126 | jobs: 127 | example: 128 | runs-on: ubuntu-18.04 129 | 130 | steps: 131 | - uses: actions/checkout@v2 132 | 133 | - name: Checking out private actions from github_user 134 | uses: daspn/private-actions-checkout@v2 135 | with: 136 | actions_list: '["github_user/my-private-action-1@v1", "github_user/my-private-action-2@v1"]' 137 | checkout_base_path: ./.github/actions 138 | ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY_1 }} 139 | 140 | - name: Checking out private actions from another_github_user 141 | uses: daspn/private-actions-checkout@v2 142 | with: 143 | actions_list: '["another_github_user/my-private-action-3@v1", "another_github_user/my-private-action-4@v1"]' 144 | checkout_base_path: ./.github/actions 145 | ssh_private_key: ${{ secrets.SSH_PRIVATE_KEY_2 }} 146 | 147 | - name: Validation 148 | run: | 149 | ls -lR ./.github/actions 150 | 151 | # the custom private action will be available on the job's workspace 152 | - name: 'Using custom private action 1' 153 | uses: ./.github/actions/my-private-action-1 154 | with: 155 | some_arg: test 156 | 157 | - name: 'Using custom private action 4' 158 | uses: ./.github/actions/my-private-action-4 159 | ``` 160 | 161 | **No SSH Key example workflow:** 162 | ```yaml 163 | name: 'No SSH Key example workflow' 164 | 165 | on: push 166 | 167 | jobs: 168 | example: 169 | runs-on: ubuntu-18.04 170 | 171 | steps: 172 | - uses: actions/checkout@v2 173 | 174 | # setting up the SSH agent using a third party action 175 | - uses: webfactory/ssh-agent@v0.2.0 176 | with: 177 | ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} 178 | 179 | # as no SSH key is provided the action will assume valid SSH credentials are available 180 | - name: Private actions checkout 181 | uses: daspn/private-actions-checkout@v2 182 | with: 183 | actions_list: '["githubuser/my-private-action-1@v1", "githubuser/my-private-action-2@v1"]' 184 | checkout_base_path: ./.github/actions 185 | 186 | - name: Validation 187 | run: | 188 | ls -lR ./.github/actions 189 | 190 | # the custom private action will be available on the job's workspace 191 | - name: 'Using custom private action 1' 192 | uses: ./.github/actions/my-private-action-1 193 | with: 194 | some_arg: test 195 | 196 | - name: 'Using custom private action 2' 197 | uses: ./.github/actions/my-private-action-2 198 | ``` 199 | 200 | GitHub App authorizing a Go application to fetch other private dependencies: 201 | ```yaml 202 | name: 'Example workflow' 203 | 204 | on: push 205 | 206 | jobs: 207 | example: 208 | runs-on: ubuntu-18.04 209 | 210 | steps: 211 | - uses: actions/checkout@v2 212 | 213 | - name: Private actions checkout 214 | uses: daspn/private-actions-checkout@v2 215 | with: 216 | app_id: ${{ secrets.APP_ID }} 217 | app_private_key: ${{ secrets.APP_PRIVATE_KEY }} 218 | configure_git: true 219 | 220 | - name: Set up Go 221 | uses: actions/setup-go@v2 222 | with: 223 | go-version: 1.15 224 | 225 | # Go build will be able to access other private repos authorized to the app 226 | - name: Build 227 | run: go build -v . 228 | ``` 229 | 230 | 231 | ### How to build 232 | 233 | #### Local environment setup 234 | 235 | To build this code, `Node.js 12.x` is [required](https://nodejs.org/en/download/current/). 236 | 237 | After installing `Node.js 12.x`, install the NPM package `zeit/ncc` by running: 238 | 239 | ```shell 240 | npm i -g @zeit/ncc 241 | ``` 242 | 243 | ## Building the code 244 | 245 | ```shell 246 | npm i 247 | npm run build 248 | ``` 249 | 250 | This will update the `dist/index.js` and `dist/cleanup/index.js` files. 251 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: "Private Actions Checkout" 2 | description: "Enables using private actions on a workflow" 3 | inputs: 4 | actions_list: 5 | description: List of private actions to checkout. Must be a JSON array and each entry must mutch the format owner/repo@ref 6 | required: false 7 | default: '[]' 8 | checkout_base_path: 9 | description: Where to checkout the custom actions 10 | required: true 11 | default: ./.github/actions 12 | ssh_private_key: 13 | description: If provided, configures the ssh-agent with the given private key. 14 | required: false 15 | app_id: 16 | description: The app id to authenticate with a GitHub app 17 | required: false 18 | app_private_key: 19 | description: The app private key to authenticate with a GitHub app 20 | required: false 21 | return_app_token: 22 | description: Sets app-token as an output if set to "true" 23 | required: false 24 | configure_git: 25 | description: Configures git to continue to use the ssh key or app token after the action exits if set to "true" 26 | required: false 27 | outputs: 28 | app-token: 29 | description: transient token generated if a Github app was supplied 30 | runs: 31 | using: "node20" 32 | main: "dist/index.js" 33 | post: "dist/cleanup/index.js" 34 | branding: 35 | icon: 'download-cloud' 36 | color: 'yellow' 37 | -------------------------------------------------------------------------------- /dist/cleanup/index.js: -------------------------------------------------------------------------------- 1 | module.exports = 2 | /******/ (() => { // webpackBootstrap 3 | /******/ var __webpack_modules__ = ({ 4 | 5 | /***/ 351: 6 | /***/ (function(__unused_webpack_module, exports, __webpack_require__) { 7 | 8 | "use strict"; 9 | 10 | var __importStar = (this && this.__importStar) || function (mod) { 11 | if (mod && mod.__esModule) return mod; 12 | var result = {}; 13 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 14 | result["default"] = mod; 15 | return result; 16 | }; 17 | Object.defineProperty(exports, "__esModule", ({ value: true })); 18 | const os = __importStar(__webpack_require__(87)); 19 | const utils_1 = __webpack_require__(278); 20 | /** 21 | * Commands 22 | * 23 | * Command Format: 24 | * ::name key=value,key=value::message 25 | * 26 | * Examples: 27 | * ::warning::This is the message 28 | * ::set-env name=MY_VAR::some value 29 | */ 30 | function issueCommand(command, properties, message) { 31 | const cmd = new Command(command, properties, message); 32 | process.stdout.write(cmd.toString() + os.EOL); 33 | } 34 | exports.issueCommand = issueCommand; 35 | function issue(name, message = '') { 36 | issueCommand(name, {}, message); 37 | } 38 | exports.issue = issue; 39 | const CMD_STRING = '::'; 40 | class Command { 41 | constructor(command, properties, message) { 42 | if (!command) { 43 | command = 'missing.command'; 44 | } 45 | this.command = command; 46 | this.properties = properties; 47 | this.message = message; 48 | } 49 | toString() { 50 | let cmdStr = CMD_STRING + this.command; 51 | if (this.properties && Object.keys(this.properties).length > 0) { 52 | cmdStr += ' '; 53 | let first = true; 54 | for (const key in this.properties) { 55 | if (this.properties.hasOwnProperty(key)) { 56 | const val = this.properties[key]; 57 | if (val) { 58 | if (first) { 59 | first = false; 60 | } 61 | else { 62 | cmdStr += ','; 63 | } 64 | cmdStr += `${key}=${escapeProperty(val)}`; 65 | } 66 | } 67 | } 68 | } 69 | cmdStr += `${CMD_STRING}${escapeData(this.message)}`; 70 | return cmdStr; 71 | } 72 | } 73 | function escapeData(s) { 74 | return utils_1.toCommandValue(s) 75 | .replace(/%/g, '%25') 76 | .replace(/\r/g, '%0D') 77 | .replace(/\n/g, '%0A'); 78 | } 79 | function escapeProperty(s) { 80 | return utils_1.toCommandValue(s) 81 | .replace(/%/g, '%25') 82 | .replace(/\r/g, '%0D') 83 | .replace(/\n/g, '%0A') 84 | .replace(/:/g, '%3A') 85 | .replace(/,/g, '%2C'); 86 | } 87 | //# sourceMappingURL=command.js.map 88 | 89 | /***/ }), 90 | 91 | /***/ 186: 92 | /***/ (function(__unused_webpack_module, exports, __webpack_require__) { 93 | 94 | "use strict"; 95 | 96 | var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) { 97 | function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); } 98 | return new (P || (P = Promise))(function (resolve, reject) { 99 | function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } } 100 | function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } } 101 | function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); } 102 | step((generator = generator.apply(thisArg, _arguments || [])).next()); 103 | }); 104 | }; 105 | var __importStar = (this && this.__importStar) || function (mod) { 106 | if (mod && mod.__esModule) return mod; 107 | var result = {}; 108 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 109 | result["default"] = mod; 110 | return result; 111 | }; 112 | Object.defineProperty(exports, "__esModule", ({ value: true })); 113 | const command_1 = __webpack_require__(351); 114 | const file_command_1 = __webpack_require__(717); 115 | const utils_1 = __webpack_require__(278); 116 | const os = __importStar(__webpack_require__(87)); 117 | const path = __importStar(__webpack_require__(622)); 118 | /** 119 | * The code to exit an action 120 | */ 121 | var ExitCode; 122 | (function (ExitCode) { 123 | /** 124 | * A code indicating that the action was successful 125 | */ 126 | ExitCode[ExitCode["Success"] = 0] = "Success"; 127 | /** 128 | * A code indicating that the action was a failure 129 | */ 130 | ExitCode[ExitCode["Failure"] = 1] = "Failure"; 131 | })(ExitCode = exports.ExitCode || (exports.ExitCode = {})); 132 | //----------------------------------------------------------------------- 133 | // Variables 134 | //----------------------------------------------------------------------- 135 | /** 136 | * Sets env variable for this action and future actions in the job 137 | * @param name the name of the variable to set 138 | * @param val the value of the variable. Non-string values will be converted to a string via JSON.stringify 139 | */ 140 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 141 | function exportVariable(name, val) { 142 | const convertedVal = utils_1.toCommandValue(val); 143 | process.env[name] = convertedVal; 144 | const filePath = process.env['GITHUB_ENV'] || ''; 145 | if (filePath) { 146 | const delimiter = '_GitHubActionsFileCommandDelimeter_'; 147 | const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; 148 | file_command_1.issueCommand('ENV', commandValue); 149 | } 150 | else { 151 | command_1.issueCommand('set-env', { name }, convertedVal); 152 | } 153 | } 154 | exports.exportVariable = exportVariable; 155 | /** 156 | * Registers a secret which will get masked from logs 157 | * @param secret value of the secret 158 | */ 159 | function setSecret(secret) { 160 | command_1.issueCommand('add-mask', {}, secret); 161 | } 162 | exports.setSecret = setSecret; 163 | /** 164 | * Prepends inputPath to the PATH (for this action and future actions) 165 | * @param inputPath 166 | */ 167 | function addPath(inputPath) { 168 | const filePath = process.env['GITHUB_PATH'] || ''; 169 | if (filePath) { 170 | file_command_1.issueCommand('PATH', inputPath); 171 | } 172 | else { 173 | command_1.issueCommand('add-path', {}, inputPath); 174 | } 175 | process.env['PATH'] = `${inputPath}${path.delimiter}${process.env['PATH']}`; 176 | } 177 | exports.addPath = addPath; 178 | /** 179 | * Gets the value of an input. The value is also trimmed. 180 | * 181 | * @param name name of the input to get 182 | * @param options optional. See InputOptions. 183 | * @returns string 184 | */ 185 | function getInput(name, options) { 186 | const val = process.env[`INPUT_${name.replace(/ /g, '_').toUpperCase()}`] || ''; 187 | if (options && options.required && !val) { 188 | throw new Error(`Input required and not supplied: ${name}`); 189 | } 190 | return val.trim(); 191 | } 192 | exports.getInput = getInput; 193 | /** 194 | * Sets the value of an output. 195 | * 196 | * @param name name of the output to set 197 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 198 | */ 199 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 200 | function setOutput(name, value) { 201 | command_1.issueCommand('set-output', { name }, value); 202 | } 203 | exports.setOutput = setOutput; 204 | /** 205 | * Enables or disables the echoing of commands into stdout for the rest of the step. 206 | * Echoing is disabled by default if ACTIONS_STEP_DEBUG is not set. 207 | * 208 | */ 209 | function setCommandEcho(enabled) { 210 | command_1.issue('echo', enabled ? 'on' : 'off'); 211 | } 212 | exports.setCommandEcho = setCommandEcho; 213 | //----------------------------------------------------------------------- 214 | // Results 215 | //----------------------------------------------------------------------- 216 | /** 217 | * Sets the action status to failed. 218 | * When the action exits it will be with an exit code of 1 219 | * @param message add error issue message 220 | */ 221 | function setFailed(message) { 222 | process.exitCode = ExitCode.Failure; 223 | error(message); 224 | } 225 | exports.setFailed = setFailed; 226 | //----------------------------------------------------------------------- 227 | // Logging Commands 228 | //----------------------------------------------------------------------- 229 | /** 230 | * Gets whether Actions Step Debug is on or not 231 | */ 232 | function isDebug() { 233 | return process.env['RUNNER_DEBUG'] === '1'; 234 | } 235 | exports.isDebug = isDebug; 236 | /** 237 | * Writes debug message to user log 238 | * @param message debug message 239 | */ 240 | function debug(message) { 241 | command_1.issueCommand('debug', {}, message); 242 | } 243 | exports.debug = debug; 244 | /** 245 | * Adds an error issue 246 | * @param message error issue message. Errors will be converted to string via toString() 247 | */ 248 | function error(message) { 249 | command_1.issue('error', message instanceof Error ? message.toString() : message); 250 | } 251 | exports.error = error; 252 | /** 253 | * Adds an warning issue 254 | * @param message warning issue message. Errors will be converted to string via toString() 255 | */ 256 | function warning(message) { 257 | command_1.issue('warning', message instanceof Error ? message.toString() : message); 258 | } 259 | exports.warning = warning; 260 | /** 261 | * Writes info to log with console.log. 262 | * @param message info message 263 | */ 264 | function info(message) { 265 | process.stdout.write(message + os.EOL); 266 | } 267 | exports.info = info; 268 | /** 269 | * Begin an output group. 270 | * 271 | * Output until the next `groupEnd` will be foldable in this group 272 | * 273 | * @param name The name of the output group 274 | */ 275 | function startGroup(name) { 276 | command_1.issue('group', name); 277 | } 278 | exports.startGroup = startGroup; 279 | /** 280 | * End an output group. 281 | */ 282 | function endGroup() { 283 | command_1.issue('endgroup'); 284 | } 285 | exports.endGroup = endGroup; 286 | /** 287 | * Wrap an asynchronous function call in a group. 288 | * 289 | * Returns the same type as the function itself. 290 | * 291 | * @param name The name of the group 292 | * @param fn The function to wrap in the group 293 | */ 294 | function group(name, fn) { 295 | return __awaiter(this, void 0, void 0, function* () { 296 | startGroup(name); 297 | let result; 298 | try { 299 | result = yield fn(); 300 | } 301 | finally { 302 | endGroup(); 303 | } 304 | return result; 305 | }); 306 | } 307 | exports.group = group; 308 | //----------------------------------------------------------------------- 309 | // Wrapper action state 310 | //----------------------------------------------------------------------- 311 | /** 312 | * Saves state for current action, the state can only be retrieved by this action's post job execution. 313 | * 314 | * @param name name of the state to store 315 | * @param value value to store. Non-string values will be converted to a string via JSON.stringify 316 | */ 317 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 318 | function saveState(name, value) { 319 | command_1.issueCommand('save-state', { name }, value); 320 | } 321 | exports.saveState = saveState; 322 | /** 323 | * Gets the value of an state set by this action's main execution. 324 | * 325 | * @param name name of the state to get 326 | * @returns string 327 | */ 328 | function getState(name) { 329 | return process.env[`STATE_${name}`] || ''; 330 | } 331 | exports.getState = getState; 332 | //# sourceMappingURL=core.js.map 333 | 334 | /***/ }), 335 | 336 | /***/ 717: 337 | /***/ (function(__unused_webpack_module, exports, __webpack_require__) { 338 | 339 | "use strict"; 340 | 341 | // For internal use, subject to change. 342 | var __importStar = (this && this.__importStar) || function (mod) { 343 | if (mod && mod.__esModule) return mod; 344 | var result = {}; 345 | if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k]; 346 | result["default"] = mod; 347 | return result; 348 | }; 349 | Object.defineProperty(exports, "__esModule", ({ value: true })); 350 | // We use any as a valid input type 351 | /* eslint-disable @typescript-eslint/no-explicit-any */ 352 | const fs = __importStar(__webpack_require__(747)); 353 | const os = __importStar(__webpack_require__(87)); 354 | const utils_1 = __webpack_require__(278); 355 | function issueCommand(command, message) { 356 | const filePath = process.env[`GITHUB_${command}`]; 357 | if (!filePath) { 358 | throw new Error(`Unable to find environment variable for file command ${command}`); 359 | } 360 | if (!fs.existsSync(filePath)) { 361 | throw new Error(`Missing file at path: ${filePath}`); 362 | } 363 | fs.appendFileSync(filePath, `${utils_1.toCommandValue(message)}${os.EOL}`, { 364 | encoding: 'utf8' 365 | }); 366 | } 367 | exports.issueCommand = issueCommand; 368 | //# sourceMappingURL=file-command.js.map 369 | 370 | /***/ }), 371 | 372 | /***/ 278: 373 | /***/ ((__unused_webpack_module, exports) => { 374 | 375 | "use strict"; 376 | 377 | // We use any as a valid input type 378 | /* eslint-disable @typescript-eslint/no-explicit-any */ 379 | Object.defineProperty(exports, "__esModule", ({ value: true })); 380 | /** 381 | * Sanitizes an input into a string so it can be passed into issueCommand safely 382 | * @param input input to sanitize into a string 383 | */ 384 | function toCommandValue(input) { 385 | if (input === null || input === undefined) { 386 | return ''; 387 | } 388 | else if (typeof input === 'string' || input instanceof String) { 389 | return input; 390 | } 391 | return JSON.stringify(input); 392 | } 393 | exports.toCommandValue = toCommandValue; 394 | //# sourceMappingURL=utils.js.map 395 | 396 | /***/ }), 397 | 398 | /***/ 759: 399 | /***/ ((module) => { 400 | 401 | const repoRegex = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(@([A-Za-z0-9_.-]+))?$/ 402 | const convertActionToCloneCommand = (cloneDir, action, cloneUrlBuilder) => { 403 | const match = repoRegex.exec(action) 404 | // Validate the format 405 | if (!match) throw new Error(`The action ${action} does not match with the (org|owner)/repo[@branch|@tag] format`) 406 | 407 | const params = { 408 | owner: match[1], 409 | repo: match[2], 410 | ref: match[4] 411 | } 412 | // Validate owner and repo 413 | if (!params.owner || !params.repo) throw new Error(`The action ${action} doesn't seem to contain a valid owner or repo`) 414 | 415 | const defaultCommand = 'git clone --depth=1' 416 | const branchCommand = params.ref ? `--single-branch --branch ${params.ref}` : '' 417 | const cloneUrl = cloneUrlBuilder(params) 418 | return `${defaultCommand} ${branchCommand} ${cloneUrl} ${cloneDir}/${params.repo}` 419 | } 420 | 421 | module.exports = { 422 | convertActionToCloneCommand 423 | } 424 | 425 | 426 | /***/ }), 427 | 428 | /***/ 18: 429 | /***/ ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => { 430 | 431 | const { 432 | error, 433 | getInput, 434 | setFailed 435 | } = __webpack_require__(186) 436 | const { 437 | cleanupSSH 438 | } = __webpack_require__(368) 439 | 440 | const run = async () => { 441 | try { 442 | const appId = getInput('app_id') 443 | const configGit = getInput('configure_git') === 'true' 444 | 445 | if (!appId && configGit) { 446 | cleanupSSH() 447 | } 448 | } catch (e) { 449 | error(e) 450 | setFailed(e.message) 451 | } 452 | } 453 | 454 | run() 455 | 456 | 457 | /***/ }), 458 | 459 | /***/ 368: 460 | /***/ ((module, __unused_webpack_exports, __webpack_require__) => { 461 | 462 | const { execFileSync, execSync } = __webpack_require__(129) 463 | const fs = __webpack_require__(747) 464 | const { 465 | info, 466 | exportVariable 467 | } = __webpack_require__(186) 468 | 469 | const { convertActionToCloneCommand } = __webpack_require__(759) 470 | const sshHomePath = `${process.env.HOME}/.ssh` 471 | const sshHomeSetup = () => { 472 | info('SSH > Creating SSH home folder') 473 | fs.mkdirSync(sshHomePath, { recursive: true }) 474 | info('SSH > Configuring known_hosts file') 475 | execSync(`ssh-keyscan -H github.com >> ${sshHomePath}/known_hosts`) 476 | } 477 | 478 | const sshAgentStart = (exportEnv) => { 479 | info('SSH > Starting the SSH agent') 480 | const sshAgentOutput = execFileSync('ssh-agent') 481 | const lines = sshAgentOutput.toString().split('\n') 482 | for (const lineNumber in lines) { 483 | const matches = /^(SSH_AUTH_SOCK|SSH_AGENT_PID)=(.*); export \1/.exec(lines[lineNumber]) 484 | if (matches && matches.length > 0) { 485 | process.env[matches[1]] = matches[2] 486 | if (exportEnv) { 487 | exportVariable(matches[1], matches[2]) 488 | info(`SSH > Export ${matches[1]} = ${matches[2]}`) 489 | } 490 | } 491 | } 492 | } 493 | 494 | const addPrivateKey = (privateKey) => { 495 | info('SSH > Adding the private key') 496 | privateKey.split(/(?=-----BEGIN)/).forEach(function (key) { 497 | execSync('ssh-add -', { input: key.trim() + '\n' }) 498 | }) 499 | execSync('ssh-add -l', { stdio: 'inherit' }) 500 | } 501 | 502 | const sshSetup = (privateKey, exportEnv) => { 503 | sshHomeSetup() 504 | sshAgentStart(exportEnv) 505 | addPrivateKey(privateKey) 506 | } 507 | 508 | const configureSSHGit = () => { 509 | const command = 'git config --global url."ssh://git@github.com/".insteadOf "https://github.com/"' 510 | info(`App > ${command}`) 511 | execSync(command) 512 | } 513 | 514 | const cloneWithSSH = (basePath, action) => { 515 | const command = convertActionToCloneCommand(basePath, action, (params) => 516 | `git@github.com:${params.owner}/${params.repo}.git` 517 | ) 518 | info(`SSH > ${command}`) 519 | execSync(command) 520 | } 521 | 522 | const cleanupSSH = () => { 523 | if (process.env.SSH_AGENT_PID) { 524 | info('SSH > Killing the ssh-agent') 525 | /* eslint no-template-curly-in-string: "off" */ 526 | execSync('kill ${SSH_AGENT_PID}', { stdio: 'inherit' }) 527 | } 528 | } 529 | 530 | module.exports = { 531 | sshSetup, 532 | cloneWithSSH, 533 | cleanupSSH, 534 | configureSSHGit 535 | } 536 | 537 | 538 | /***/ }), 539 | 540 | /***/ 129: 541 | /***/ ((module) => { 542 | 543 | "use strict"; 544 | module.exports = require("child_process"); 545 | 546 | /***/ }), 547 | 548 | /***/ 747: 549 | /***/ ((module) => { 550 | 551 | "use strict"; 552 | module.exports = require("fs"); 553 | 554 | /***/ }), 555 | 556 | /***/ 87: 557 | /***/ ((module) => { 558 | 559 | "use strict"; 560 | module.exports = require("os"); 561 | 562 | /***/ }), 563 | 564 | /***/ 622: 565 | /***/ ((module) => { 566 | 567 | "use strict"; 568 | module.exports = require("path"); 569 | 570 | /***/ }) 571 | 572 | /******/ }); 573 | /************************************************************************/ 574 | /******/ // The module cache 575 | /******/ var __webpack_module_cache__ = {}; 576 | /******/ 577 | /******/ // The require function 578 | /******/ function __webpack_require__(moduleId) { 579 | /******/ // Check if module is in cache 580 | /******/ if(__webpack_module_cache__[moduleId]) { 581 | /******/ return __webpack_module_cache__[moduleId].exports; 582 | /******/ } 583 | /******/ // Create a new module (and put it into the cache) 584 | /******/ var module = __webpack_module_cache__[moduleId] = { 585 | /******/ // no module.id needed 586 | /******/ // no module.loaded needed 587 | /******/ exports: {} 588 | /******/ }; 589 | /******/ 590 | /******/ // Execute the module function 591 | /******/ var threw = true; 592 | /******/ try { 593 | /******/ __webpack_modules__[moduleId].call(module.exports, module, module.exports, __webpack_require__); 594 | /******/ threw = false; 595 | /******/ } finally { 596 | /******/ if(threw) delete __webpack_module_cache__[moduleId]; 597 | /******/ } 598 | /******/ 599 | /******/ // Return the exports of the module 600 | /******/ return module.exports; 601 | /******/ } 602 | /******/ 603 | /************************************************************************/ 604 | /******/ /* webpack/runtime/compat */ 605 | /******/ 606 | /******/ __webpack_require__.ab = __dirname + "/";/************************************************************************/ 607 | /******/ // module exports must be returned from runtime so entry inlining is disabled 608 | /******/ // startup 609 | /******/ // Load entry module and return exports 610 | /******/ return __webpack_require__(18); 611 | /******/ })() 612 | ; -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "private-actions-checkout", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "format": "standard --fix", 8 | "build": "ncc build src/index.js && ncc build src/cleanup.js -o dist/cleanup" 9 | }, 10 | "keywords": [], 11 | "author": "", 12 | "license": "ISC", 13 | "dependencies": { 14 | "@actions/core": "^1.10.0", 15 | "@actions/github": "^6.0.0", 16 | "@octokit/auth-app": "^7.1.5", 17 | "https-proxy-agent": "^5.0.1", 18 | "is-base64": "^1.1.0" 19 | }, 20 | "devDependencies": { 21 | "@vercel/ncc": "^0.24.0", 22 | "standard": "^16.0.0" 23 | }, 24 | "standard": { 25 | "ignore": [ 26 | "/dist/*.js" 27 | ] 28 | }, 29 | "volta": { 30 | "node": "16.16.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /src/action-parser.js: -------------------------------------------------------------------------------- 1 | const repoRegex = /^([A-Za-z0-9_.-]+)\/([A-Za-z0-9_.-]+)(@([A-Za-z0-9_.-]+))?$/ 2 | const convertActionToCloneCommand = (cloneDir, action, cloneUrlBuilder) => { 3 | const match = repoRegex.exec(action) 4 | // Validate the format 5 | if (!match) throw new Error(`The action ${action} does not match with the (org|owner)/repo[@branch|@tag] format`) 6 | 7 | const params = { 8 | owner: match[1], 9 | repo: match[2], 10 | ref: match[4] 11 | } 12 | // Validate owner and repo 13 | if (!params.owner || !params.repo) throw new Error(`The action ${action} doesn't seem to contain a valid owner or repo`) 14 | 15 | const defaultCommand = 'git clone --depth=1' 16 | const branchCommand = params.ref ? `--single-branch --branch ${params.ref}` : '' 17 | const cloneUrl = cloneUrlBuilder(params) 18 | return `${defaultCommand} ${branchCommand} ${cloneUrl} ${cloneDir}/${params.repo}` 19 | } 20 | 21 | module.exports = { 22 | convertActionToCloneCommand 23 | } 24 | -------------------------------------------------------------------------------- /src/cleanup.js: -------------------------------------------------------------------------------- 1 | const { 2 | error, 3 | getInput, 4 | setFailed 5 | } = require('@actions/core') 6 | const { 7 | cleanupSSH 8 | } = require('./ssh-setup') 9 | 10 | const run = async () => { 11 | try { 12 | const appId = getInput('app_id') 13 | const configGit = getInput('configure_git') === 'true' 14 | 15 | if (!appId && configGit) { 16 | cleanupSSH() 17 | } 18 | } catch (e) { 19 | error(e) 20 | setFailed(e.message) 21 | } 22 | } 23 | 24 | run() 25 | -------------------------------------------------------------------------------- /src/github-app-setup.js: -------------------------------------------------------------------------------- 1 | const { 2 | error, 3 | info, 4 | setFailed, 5 | setSecret 6 | } = require('@actions/core') 7 | const { getOctokit } = require('@actions/github') 8 | const isBase64 = require('is-base64') 9 | const { execSync } = require('child_process') 10 | const { convertActionToCloneCommand } = require('./action-parser') 11 | const { createAppAuth } = require("@octokit/auth-app"); 12 | const { request } = require("@octokit/request"); 13 | const { HttpsProxyAgent } = require('https-proxy-agent'); 14 | 15 | // Specify custom HTTP agent that obeys proxy env variable 16 | function getHttpsProxyAgent() { 17 | const proxy = process.env["HTTPS_PROXY"] || process.env["https_proxy"]; 18 | 19 | if (!proxy) return undefined; 20 | 21 | return new HttpsProxyAgent(proxy); 22 | } 23 | 24 | // Code based on https://github.com/tibdex/github-app-token 25 | async function obtainAppToken (id, privateKeyInput) { 26 | try { 27 | // Get the parameters and throw if they are not found 28 | const privateKey = isBase64(privateKeyInput) 29 | ? Buffer.from(privateKeyInput, 'base64').toString('utf8') 30 | : privateKeyInput 31 | info(`App > Base 64 detected on private key: ${isBase64(privateKeyInput)}`) 32 | 33 | // Instantiate custom request object with custom agent 34 | const customRequest = request.defaults({ 35 | request: { 36 | agent: getHttpsProxyAgent() 37 | } 38 | }); 39 | 40 | // Obtain JWT 41 | const auth = createAppAuth({ 42 | appId: id, 43 | privateKey, 44 | request: customRequest 45 | }); 46 | 47 | const appAuthentication = await auth({ type: "app" }); 48 | const jwt = appAuthentication.token; 49 | if(!jwt) { 50 | error('App > Cannot get app JWT'); 51 | return; 52 | } 53 | 54 | // Obtain installation id 55 | const octokit = getOctokit(jwt); 56 | const installations = await octokit.apps.listInstallations(); 57 | if(installations.data.length !== 1) { 58 | error(`App > Only 1 installation is allowed for this app. We detected it has ${installations.data.length} installations`); 59 | return; 60 | } 61 | const { 62 | id: installationId 63 | } = installations.data[0]; 64 | info(`App > Installation: ${installationId}`); 65 | 66 | // Obtain token 67 | const installationAuth = createAppAuth({ 68 | appId: id, 69 | privateKey, 70 | installationId, 71 | request: customRequest 72 | }); 73 | const installationAuthentication = await installationAuth({ type: "installation" }); 74 | const token = installationAuthentication.token; 75 | if(!token) { 76 | error('App > Cannot get app token'); 77 | return; 78 | } 79 | 80 | setSecret(token); 81 | info('App > GitHub app token generated successfully'); 82 | return token; 83 | } catch (exception) { 84 | error(exception) 85 | setFailed(exception.message) 86 | } 87 | } 88 | 89 | const configureAppGit = (token) => { 90 | const command = `git config --global url."https://x-access-token:${token}@github.com/".insteadOf "https://github.com/"` 91 | info(`App > ${command}`) 92 | execSync(command) 93 | } 94 | 95 | const cloneWithApp = (basePath, action, token) => { 96 | const command = convertActionToCloneCommand(basePath, action, (params) => 97 | `https://x-access-token:${token}@github.com/${params.owner}/${params.repo}.git` 98 | ) 99 | info(`App > ${command}`) 100 | execSync(command) 101 | } 102 | 103 | module.exports = { 104 | obtainAppToken, 105 | cloneWithApp, 106 | configureAppGit 107 | } 108 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | const { 2 | error, 3 | getInput, 4 | info, 5 | setFailed, 6 | setOutput 7 | } = require('@actions/core') 8 | const { 9 | sshSetup, 10 | cloneWithSSH, 11 | cleanupSSH, 12 | configureSSHGit 13 | } = require('./ssh-setup') 14 | const { 15 | obtainAppToken, 16 | cloneWithApp, 17 | configureAppGit 18 | } = require('./github-app-setup') 19 | 20 | const hasValue = (input) => { 21 | return input.trim().length !== 0 22 | } 23 | 24 | const CLONE_STRATEGY_SSH = 'ssh' 25 | const CLONE_STRATEGY_APP = 'app' 26 | 27 | const run = async () => { 28 | try { 29 | const sshPrivateKey = getInput('ssh_private_key') 30 | const actionsList = JSON.parse(getInput('actions_list')) 31 | const basePath = getInput('checkout_base_path') 32 | const appId = getInput('app_id') 33 | const privateKey = getInput('app_private_key') 34 | const returnAppToken = getInput('return_app_token') === 'true' 35 | const configGit = getInput('configure_git') === 'true' 36 | 37 | let cloneStrategy 38 | let appToken 39 | 40 | // If appId exist we will go ahead and use the GitHub App 41 | if (hasValue(appId) && hasValue(privateKey)) { 42 | cloneStrategy = CLONE_STRATEGY_APP 43 | info('App > Cloning using GitHub App strategy') 44 | appToken = await obtainAppToken(appId, privateKey) 45 | if (!appToken) { 46 | setFailed('App > App token generation failed. Workflow can not continue') 47 | return 48 | } 49 | if (returnAppToken) { 50 | info('App > Returning app-token') 51 | setOutput('app-token', appToken) 52 | } 53 | } else if (hasValue(sshPrivateKey)) { 54 | cloneStrategy = CLONE_STRATEGY_SSH 55 | info('SSH > Cloning using SSH strategy') 56 | info('SSH > Setting up the SSH agent with the provided private key') 57 | sshSetup(sshPrivateKey, configGit) 58 | } else { 59 | cloneStrategy = CLONE_STRATEGY_SSH 60 | info('SSH > Cloning using SSH strategy') 61 | info('SSH > No private key provided. Assuming valid SSH credentials are available') 62 | } 63 | 64 | // Proceed with the clones 65 | actionsList.forEach((action) => { 66 | if (cloneStrategy === CLONE_STRATEGY_APP) { 67 | cloneWithApp(basePath, action, appToken) 68 | } else if (cloneStrategy === CLONE_STRATEGY_SSH) { 69 | cloneWithSSH(basePath, action) 70 | } 71 | }) 72 | 73 | if (configGit) { 74 | if (cloneStrategy === CLONE_STRATEGY_APP) { 75 | configureAppGit(appToken) 76 | } else { 77 | configureSSHGit() 78 | } 79 | } else { 80 | // Cleanup 81 | if (cloneStrategy === CLONE_STRATEGY_SSH && hasValue(sshPrivateKey)) { 82 | cleanupSSH() 83 | } 84 | } 85 | } catch (e) { 86 | error(e) 87 | setFailed(e.message) 88 | } 89 | } 90 | 91 | run() 92 | -------------------------------------------------------------------------------- /src/ssh-setup.js: -------------------------------------------------------------------------------- 1 | const { execFileSync, execSync } = require('child_process') 2 | const fs = require('fs') 3 | const { 4 | info, 5 | exportVariable 6 | } = require('@actions/core') 7 | 8 | const { convertActionToCloneCommand } = require('./action-parser') 9 | const sshHomePath = `${process.env.HOME}/.ssh` 10 | const sshHomeSetup = () => { 11 | info('SSH > Creating SSH home folder') 12 | fs.mkdirSync(sshHomePath, { recursive: true }) 13 | info('SSH > Configuring known_hosts file') 14 | execSync(`ssh-keyscan -H github.com >> ${sshHomePath}/known_hosts`) 15 | } 16 | 17 | const sshAgentStart = (exportEnv) => { 18 | info('SSH > Starting the SSH agent') 19 | const sshAgentOutput = execFileSync('ssh-agent') 20 | const lines = sshAgentOutput.toString().split('\n') 21 | for (const lineNumber in lines) { 22 | const matches = /^(SSH_AUTH_SOCK|SSH_AGENT_PID)=(.*); export \1/.exec(lines[lineNumber]) 23 | if (matches && matches.length > 0) { 24 | process.env[matches[1]] = matches[2] 25 | if (exportEnv) { 26 | exportVariable(matches[1], matches[2]) 27 | info(`SSH > Export ${matches[1]} = ${matches[2]}`) 28 | } 29 | } 30 | } 31 | } 32 | 33 | const addPrivateKey = (privateKey) => { 34 | info('SSH > Adding the private key') 35 | privateKey.split(/(?=-----BEGIN)/).forEach(function (key) { 36 | execSync('ssh-add -', { input: key.trim() + '\n' }) 37 | }) 38 | execSync('ssh-add -l', { stdio: 'inherit' }) 39 | } 40 | 41 | const sshSetup = (privateKey, exportEnv) => { 42 | sshHomeSetup() 43 | sshAgentStart(exportEnv) 44 | addPrivateKey(privateKey) 45 | } 46 | 47 | const configureSSHGit = () => { 48 | const command = 'git config --global url."ssh://git@github.com/".insteadOf "https://github.com/"' 49 | info(`App > ${command}`) 50 | execSync(command) 51 | } 52 | 53 | const cloneWithSSH = (basePath, action) => { 54 | const command = convertActionToCloneCommand(basePath, action, (params) => 55 | `git@github.com:${params.owner}/${params.repo}.git` 56 | ) 57 | info(`SSH > ${command}`) 58 | execSync(command) 59 | } 60 | 61 | const cleanupSSH = () => { 62 | if (process.env.SSH_AGENT_PID) { 63 | info('SSH > Killing the ssh-agent') 64 | /* eslint no-template-curly-in-string: "off" */ 65 | execSync('kill ${SSH_AGENT_PID}', { stdio: 'inherit' }) 66 | } 67 | } 68 | 69 | module.exports = { 70 | sshSetup, 71 | cloneWithSSH, 72 | cleanupSSH, 73 | configureSSHGit 74 | } 75 | --------------------------------------------------------------------------------