From cb050aaa3a64d715ca3ca99798f03aaca3734c20 Mon Sep 17 00:00:00 2001 From: Elvin Risti Date: Wed, 26 Oct 2022 17:50:29 +0300 Subject: [PATCH 1/2] feat: replace ssh-private-key with ssh-private-keys and expected input is json with array if {name, key} entries. --- .github/workflows/demo.yml | 71 ++++++++---------- README.md | 29 ++++---- action.yml | 7 +- dist/cleanup.js | 57 +++++++++------ dist/index.js | 143 +++++++++++++++++++++++-------------- index.js | 86 +++++++++++++--------- package.json | 2 +- yarn.lock | 40 +++++------ 8 files changed, 247 insertions(+), 188 deletions(-) diff --git a/.github/workflows/demo.yml b/.github/workflows/demo.yml index 1cfd2a1..383c5fc 100644 --- a/.github/workflows/demo.yml +++ b/.github/workflows/demo.yml @@ -1,46 +1,31 @@ on: [ push, pull_request ] jobs: - deployment_keys_demo: - strategy: - fail-fast: false - matrix: - os: [ ubuntu-latest, macOS-latest, windows-latest ] - runs-on: ${{ matrix.os }} - steps: - - uses: actions/checkout@v3 - - name: Setup key - uses: ./ - with: - ssh-private-key: | - ${{ secrets.MPDUDE_TEST_1_DEPLOY_KEY }} - ${{ secrets.MPDUDE_TEST_2_DEPLOY_KEY }} - - run: | - git clone https://github.com/mpdude/test-1.git test-1-http - git clone git@github.com:mpdude/test-1.git test-1-git - git clone ssh://git@github.com/mpdude/test-1.git test-1-git-ssh - git clone https://github.com/mpdude/test-2.git test-2-http - git clone git@github.com:mpdude/test-2.git test-2-git - git clone ssh://git@github.com/mpdude/test-2.git test-2-git-ssh - - docker_demo: - runs-on: ubuntu-latest - container: - image: ubuntu:latest - steps: - - uses: actions/checkout@v3 - - run: apt update && apt install -y openssh-client git - - name: Setup key - uses: ./ - with: - ssh-private-key: | - ${{ secrets.MPDUDE_TEST_1_DEPLOY_KEY }} - ${{ secrets.MPDUDE_TEST_2_DEPLOY_KEY }} - - run: | - git clone https://github.com/mpdude/test-1.git test-1-http - git clone git@github.com:mpdude/test-1.git test-1-git - git clone ssh://git@github.com/mpdude/test-1.git test-1-git-ssh - git clone https://github.com/mpdude/test-2.git test-2-http - git clone git@github.com:mpdude/test-2.git test-2-git - git clone ssh://git@github.com/mpdude/test-2.git test-2-git-ssh - + vmo_deployment_keys_demo: + runs-on: ubuntu-latest + container: + image: ubuntu:latest + steps: + - uses: actions/checkout@v3 + - run: apt update && apt install -y openssh-client git curl + - if: ${{ env.ACT }} + name: Hack container for local development + run: | + curl -fsSL https://deb.nodesource.com/setup_16.x | bash -s + apt-get install -y nodejs + - name: Setup node + uses: actions/setup-node@v3 + with: + node-version: '16.x' + - name: Setup key + uses: ./ + with: + ssh-private-keys: | + { + "git@github.com:vaimo/vsf2_ext-lru-cache-driver.git": "${{ secrets.TEST_1_DEPLOY_KEY }}", + "git@github.com:mpdude/test-2.git": "${{ secrets.TEST_2_DEPLOY_KEY }}" + } + - run: | + git clone https://github.com/vaimo/vsf2_ext-lru-cache-driver.git test-1-http + git clone git@github.com:vaimo/vsf2_ext-lru-cache-driver.git test-1-git + git clone ssh://git@github.com/vaimo/vsf2_ext-lru-cache-driver.git test-1-git-ssh diff --git a/README.md b/README.md index 63d86fd..62d6536 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ GitHub Actions only have access to the repository they run for. So, in order to * In your repository, go to the *Settings > Secrets* menu and create a new secret. In this example, we'll call it `SSH_PRIVATE_KEY`. * Put the contents of the *private* SSH key file into the contents field.
* This key should start with `-----BEGIN ... PRIVATE KEY-----`, consist of many lines and ends with `-----END ... PRIVATE KEY-----`. -5. In your workflow definition file, add the following step. Preferably this would be rather on top, near the `actions/checkout@v2` line. +5. In your workflow definition file, add the following step. Preferably this would be rather on top, near the `actions/checkout@v3` line. ```yaml # .github/workflows/my-workflow.yml @@ -35,12 +35,13 @@ jobs: my_job: ... steps: - - actions/checkout@v2 - # Make sure the @v0.6.0 matches the current version of the - # action - - uses: webfactory/ssh-agent@v0.6.0 - with: - ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }} + - actions/checkout@v3 + - uses: vaimo/webfactory-ssh-agent-github@feature/json-private-keys + with: + ssh-private-keys: | + { + "git@github.com:vendor/repo-1.git": "${{ secrets.SSH_PRIVATE_KEY }}" + } - ... other steps ``` 5. If, for some reason, you need to change the location of the SSH agent socket, you can use the `ssh-auth-sock` input to provide a path. @@ -53,12 +54,14 @@ You can set up different keys as different secrets and pass them all to the acti ```yaml # ... contens as before - - uses: webfactory/ssh-agent@v0.6.0 + - uses: vaimo/webfactory-ssh-agent-github@feature/json-private-keys with: - ssh-private-key: | - ${{ secrets.FIRST_KEY }} - ${{ secrets.NEXT_KEY }} - ${{ secrets.ANOTHER_KEY }} + ssh-private-keys: | + { + "git@github.com:vendor/repo-1.git": "${{ secrets.FIRST_KEY }}", + "git@github.com:vendor/repo-2.git": "${{ secrets.NEXT_KEY }}", + "git@github.com:vendor/repo-3.git": "${{ secrets.ANOTHER_KEY }}", + } ``` The `ssh-agent` will load all of the keys and try each one in order when establishing SSH connections. @@ -80,7 +83,7 @@ To support picking the right key in this use case, this action scans _key commen The following inputs can be used to control the action's behavior: -* `ssh-private-key`: Required. Use this to provide the key(s) to load as GitHub Actions secrets. +* `ssh-private-keys`: Required. Use this to provide the key(s) to load as GitHub Actions secrets. * `ssh-auth-sock`: Can be used to control where the SSH agent socket will be placed. Ultimately affects the `$SSH_AUTH_SOCK` environment variable. * `log-public-key`: Set this to `false` if you want to suppress logging of _public_ key information. To simplify debugging and since it contains public key information only, this is turned on by default. diff --git a/action.yml b/action.yml index ec3dfd9..85e3404 100644 --- a/action.yml +++ b/action.yml @@ -1,9 +1,10 @@ -name: 'webfactory/ssh-agent' +name: 'vaimo/webfactory-ssh-agent' description: 'Run `ssh-agent` and load an SSH key to access other private repositories' inputs: - ssh-private-key: - description: 'Private SSH key to register in the SSH agent' + ssh-private-keys: + description: 'Private SSH key(s) to register in the SSH agent' required: true + default: 'GitHub' ssh-auth-sock: description: 'Where to place the SSH Agent auth socket' log-public-key: diff --git a/dist/cleanup.js b/dist/cleanup.js index 8af40c8..ef26d17 100644 --- a/dist/cleanup.js +++ b/dist/cleanup.js @@ -292,13 +292,14 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.issueCommand = void 0; +exports.prepareKeyValueMessage = exports.issueFileCommand = void 0; // We use any as a valid input type /* eslint-disable @typescript-eslint/no-explicit-any */ const fs = __importStar(__webpack_require__(747)); const os = __importStar(__webpack_require__(87)); +const uuid_1 = __webpack_require__(62); const utils_1 = __webpack_require__(82); -function issueCommand(command, message) { +function issueFileCommand(command, message) { const filePath = process.env[`GITHUB_${command}`]; if (!filePath) { throw new Error(`Unable to find environment variable for file command ${command}`); @@ -310,7 +311,22 @@ function issueCommand(command, message) { encoding: 'utf8' }); } -exports.issueCommand = issueCommand; +exports.issueFileCommand = issueFileCommand; +function prepareKeyValueMessage(key, value) { + const delimiter = `ghadelimiter_${uuid_1.v4()}`; + const convertedValue = utils_1.toCommandValue(value); + // These should realistically never happen, but just in case someone finds a + // way to exploit uuid generation let's not allow keys or values that contain + // the delimiter. + if (key.includes(delimiter)) { + throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); + } + if (convertedValue.includes(delimiter)) { + throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); + } + return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; +} +exports.prepareKeyValueMessage = prepareKeyValueMessage; //# sourceMappingURL=file-command.js.map /***/ }), @@ -1668,7 +1684,6 @@ const file_command_1 = __webpack_require__(102); const utils_1 = __webpack_require__(82); const os = __importStar(__webpack_require__(87)); const path = __importStar(__webpack_require__(622)); -const uuid_1 = __webpack_require__(62); const oidc_utils_1 = __webpack_require__(742); /** * The code to exit an action @@ -1698,20 +1713,9 @@ function exportVariable(name, val) { process.env[name] = convertedVal; const filePath = process.env['GITHUB_ENV'] || ''; if (filePath) { - const delimiter = `ghadelimiter_${uuid_1.v4()}`; - // These should realistically never happen, but just in case someone finds a way to exploit uuid generation let's not allow keys or values that contain the delimiter. - if (name.includes(delimiter)) { - throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); - } - if (convertedVal.includes(delimiter)) { - throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); - } - const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; - file_command_1.issueCommand('ENV', commandValue); - } - else { - command_1.issueCommand('set-env', { name }, convertedVal); + return file_command_1.issueFileCommand('ENV', file_command_1.prepareKeyValueMessage(name, val)); } + command_1.issueCommand('set-env', { name }, convertedVal); } exports.exportVariable = exportVariable; /** @@ -1729,7 +1733,7 @@ exports.setSecret = setSecret; function addPath(inputPath) { const filePath = process.env['GITHUB_PATH'] || ''; if (filePath) { - file_command_1.issueCommand('PATH', inputPath); + file_command_1.issueFileCommand('PATH', inputPath); } else { command_1.issueCommand('add-path', {}, inputPath); @@ -1769,7 +1773,10 @@ function getMultilineInput(name, options) { const inputs = getInput(name, options) .split('\n') .filter(x => x !== ''); - return inputs; + if (options && options.trimWhitespace === false) { + return inputs; + } + return inputs.map(input => input.trim()); } exports.getMultilineInput = getMultilineInput; /** @@ -1802,8 +1809,12 @@ exports.getBooleanInput = getBooleanInput; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function setOutput(name, value) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('OUTPUT', file_command_1.prepareKeyValueMessage(name, value)); + } process.stdout.write(os.EOL); - command_1.issueCommand('set-output', { name }, value); + command_1.issueCommand('set-output', { name }, utils_1.toCommandValue(value)); } exports.setOutput = setOutput; /** @@ -1932,7 +1943,11 @@ exports.group = group; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function saveState(name, value) { - command_1.issueCommand('save-state', { name }, value); + const filePath = process.env['GITHUB_STATE'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('STATE', file_command_1.prepareKeyValueMessage(name, value)); + } + command_1.issueCommand('save-state', { name }, utils_1.toCommandValue(value)); } exports.saveState = saveState; /** diff --git a/dist/index.js b/dist/index.js index 3039a0b..d92e88c 100644 --- a/dist/index.js +++ b/dist/index.js @@ -292,13 +292,14 @@ var __importStar = (this && this.__importStar) || function (mod) { return result; }; Object.defineProperty(exports, "__esModule", { value: true }); -exports.issueCommand = void 0; +exports.prepareKeyValueMessage = exports.issueFileCommand = void 0; // We use any as a valid input type /* eslint-disable @typescript-eslint/no-explicit-any */ const fs = __importStar(__webpack_require__(747)); const os = __importStar(__webpack_require__(87)); +const uuid_1 = __webpack_require__(62); const utils_1 = __webpack_require__(82); -function issueCommand(command, message) { +function issueFileCommand(command, message) { const filePath = process.env[`GITHUB_${command}`]; if (!filePath) { throw new Error(`Unable to find environment variable for file command ${command}`); @@ -310,7 +311,22 @@ function issueCommand(command, message) { encoding: 'utf8' }); } -exports.issueCommand = issueCommand; +exports.issueFileCommand = issueFileCommand; +function prepareKeyValueMessage(key, value) { + const delimiter = `ghadelimiter_${uuid_1.v4()}`; + const convertedValue = utils_1.toCommandValue(value); + // These should realistically never happen, but just in case someone finds a + // way to exploit uuid generation let's not allow keys or values that contain + // the delimiter. + if (key.includes(delimiter)) { + throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); + } + if (convertedValue.includes(delimiter)) { + throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); + } + return `${key}<<${delimiter}${os.EOL}${convertedValue}${os.EOL}${delimiter}`; +} +exports.prepareKeyValueMessage = prepareKeyValueMessage; //# sourceMappingURL=file-command.js.map /***/ }), @@ -322,18 +338,19 @@ const core = __webpack_require__(470); const child_process = __webpack_require__(129); const fs = __webpack_require__(747); const crypto = __webpack_require__(417); -const { homePath, sshAgentCmd, sshAddCmd, gitCmd } = __webpack_require__(972); +const { homePath, sshAgentCmd, sshAddCmd, sshKeyGenCmd, gitCmd } = __webpack_require__(972); try { - const privateKey = core.getInput('ssh-private-key'); - const logPublicKey = core.getBooleanInput('log-public-key', {default: true}); + const privateKeys = core.getInput('ssh-private-keys', { required: true }); + const logPublicKey = core.getBooleanInput('log-public-key'); - if (!privateKey) { - core.setFailed("The ssh-private-key argument is empty. Maybe the secret has not been configured, or you are using a wrong secret name in your workflow file."); - - return; + if (!privateKeys) { + throw new Error("The ssh-private-keys {name: key} argument is empty. Maybe the secret has not been configured, or you are using a wrong secret name in your workflow file.") } + console.debug(privateKeys.replaceAll("\n", "")) + const privateKeysData = JSON.parse(privateKeys.replaceAll("\n", "")) + const homeSsh = homePath + '/.ssh'; console.log(`Adding GitHub.com keys to ${homeSsh}/known_hosts`); @@ -349,7 +366,7 @@ try { const sshAgentArgs = (authSock && authSock.length > 0) ? ['-a', authSock] : []; // Extract auth socket path and agent pid and set them as job variables - child_process.execFileSync(sshAgentCmd, sshAgentArgs).toString().split("\n").forEach(function(line) { + child_process.execFileSync(sshAgentCmd, sshAgentArgs).toString().split("\n").forEach((line) => { const matches = /^(SSH_AUTH_SOCK|SSH_AGENT_PID)=(.*); export \1/.exec(line); if (matches && matches.length > 0) { @@ -359,54 +376,73 @@ try { } }); - console.log("Adding private key(s) to agent"); + console.log("Adding private key(s) to agent and Configuring deployment key(s)"); - privateKey.split(/(?=-----BEGIN)/).forEach(function(key) { - child_process.execFileSync(sshAddCmd, ['-'], { input: key.trim() + "\n" }); - }); + Object.keys(privateKeysData).forEach(async (name) => { + const repoName = name.trim(); + let privateKey = privateKeysData[name].trim(); - console.log("Key(s) added:"); + if (!privateKey) { + throw new Error('privateKey is empty for "' + name + '"') + } - child_process.execFileSync(sshAddCmd, ['-l'], { stdio: 'inherit' }); + privateKey = privateKey.replace(/(KEY-----)(...)/, '$1\n$2') + .replace(/(...)(-----END )/, '$1\n$2') + "\n" - console.log('Configuring deployment key(s)'); + child_process.execFileSync(sshAddCmd, ['-'], { input: privateKey }); - child_process.execFileSync(sshAddCmd, ['-L']).toString().trim().split(/\r?\n/).forEach(function(key) { - const parts = key.match(/\bgithub\.com[:/]([_.a-z0-9-]+\/[_.a-z0-9-]+)/i); + const sha256 = crypto.createHash('sha256').update(privateKey).digest('hex'); + const filename = `${homeSsh}/key-${sha256}` - if (!parts) { - if (logPublicKey) { - console.log(`Comment for (public) key '${key}' does not match GitHub URL pattern. Not treating it as a GitHub deploy key.`); + await fs.writeFile(filename, privateKey, { }, (err) => { + if (err) { + console.log(err) + return } - return; - } - const sha256 = crypto.createHash('sha256').update(key).digest('hex'); - const ownerAndRepo = parts[1].replace(/\.git$/, ''); + fs.chmodSync(filename, '600') + + const parts = repoName.match(/\bgithub\.com[:/]([_.a-z0-9-]+\/[_.a-z0-9-]+)/i); + + if (!parts) { + if (logPublicKey) { + console.log(`Comment for name '${repoName}' does not match GitHub URL pattern. Not treating it as a GitHub deploy key.`); + } + + return; + } - fs.writeFileSync(`${homeSsh}/key-${sha256}`, key + "\n", { mode: '600' }); + const ownerAndRepo = parts[1].replace(/\.git$/, ''); - child_process.execSync(`${gitCmd} config --global --replace-all url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "https://github.com/${ownerAndRepo}"`); - child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "git@github.com:${ownerAndRepo}"`); - child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "ssh://git@github.com/${ownerAndRepo}"`); + child_process.execSync(`${gitCmd} config --global --replace-all url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "https://github.com/${ownerAndRepo}"`); + child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "git@github.com:${ownerAndRepo}"`); + child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "ssh://git@github.com/${ownerAndRepo}"`); - const sshConfig = `\nHost key-${sha256}.github.com\n` + const sshConfig = `\nHost key-${sha256}.github.com\n` + ` HostName github.com\n` - + ` IdentityFile ${homeSsh}/key-${sha256}\n` + + ` IdentityFile ${filename}\n` + ` IdentitiesOnly yes\n`; - fs.appendFileSync(`${homeSsh}/config`, sshConfig); + fs.appendFileSync(`${homeSsh}/config`, sshConfig); - console.log(`Added deploy-key mapping: Use identity '${homeSsh}/key-${sha256}' for GitHub repository ${ownerAndRepo}`); + console.log(`Added deploy-key mapping: Use identity '${homeSsh}/key-${sha256}' for GitHub repository ${ownerAndRepo}`); + }) }); + console.log("Key(s) added:"); + + child_process.execFileSync(sshAddCmd, ['-l'], { stdio: 'inherit' }); } catch (error) { - if (error.code == 'ENOENT') { + if (error.code === 'ENOENT') { console.log(`The '${error.path}' executable could not be found. Please make sure it is on your PATH and/or the necessary packages are installed.`); console.log(`PATH is set to: ${process.env.PATH}`); } + if (error.constructor && error.constructor.name === 'SyntaxError') { + console.error(`JSON parsing error on "ssh-private-keys" value: ${error.message}`); + } + core.setFailed(error.message); } @@ -1747,7 +1783,6 @@ const file_command_1 = __webpack_require__(102); const utils_1 = __webpack_require__(82); const os = __importStar(__webpack_require__(87)); const path = __importStar(__webpack_require__(622)); -const uuid_1 = __webpack_require__(62); const oidc_utils_1 = __webpack_require__(742); /** * The code to exit an action @@ -1777,20 +1812,9 @@ function exportVariable(name, val) { process.env[name] = convertedVal; const filePath = process.env['GITHUB_ENV'] || ''; if (filePath) { - const delimiter = `ghadelimiter_${uuid_1.v4()}`; - // These should realistically never happen, but just in case someone finds a way to exploit uuid generation let's not allow keys or values that contain the delimiter. - if (name.includes(delimiter)) { - throw new Error(`Unexpected input: name should not contain the delimiter "${delimiter}"`); - } - if (convertedVal.includes(delimiter)) { - throw new Error(`Unexpected input: value should not contain the delimiter "${delimiter}"`); - } - const commandValue = `${name}<<${delimiter}${os.EOL}${convertedVal}${os.EOL}${delimiter}`; - file_command_1.issueCommand('ENV', commandValue); - } - else { - command_1.issueCommand('set-env', { name }, convertedVal); + return file_command_1.issueFileCommand('ENV', file_command_1.prepareKeyValueMessage(name, val)); } + command_1.issueCommand('set-env', { name }, convertedVal); } exports.exportVariable = exportVariable; /** @@ -1808,7 +1832,7 @@ exports.setSecret = setSecret; function addPath(inputPath) { const filePath = process.env['GITHUB_PATH'] || ''; if (filePath) { - file_command_1.issueCommand('PATH', inputPath); + file_command_1.issueFileCommand('PATH', inputPath); } else { command_1.issueCommand('add-path', {}, inputPath); @@ -1848,7 +1872,10 @@ function getMultilineInput(name, options) { const inputs = getInput(name, options) .split('\n') .filter(x => x !== ''); - return inputs; + if (options && options.trimWhitespace === false) { + return inputs; + } + return inputs.map(input => input.trim()); } exports.getMultilineInput = getMultilineInput; /** @@ -1881,8 +1908,12 @@ exports.getBooleanInput = getBooleanInput; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function setOutput(name, value) { + const filePath = process.env['GITHUB_OUTPUT'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('OUTPUT', file_command_1.prepareKeyValueMessage(name, value)); + } process.stdout.write(os.EOL); - command_1.issueCommand('set-output', { name }, value); + command_1.issueCommand('set-output', { name }, utils_1.toCommandValue(value)); } exports.setOutput = setOutput; /** @@ -2011,7 +2042,11 @@ exports.group = group; */ // eslint-disable-next-line @typescript-eslint/no-explicit-any function saveState(name, value) { - command_1.issueCommand('save-state', { name }, value); + const filePath = process.env['GITHUB_STATE'] || ''; + if (filePath) { + return file_command_1.issueFileCommand('STATE', file_command_1.prepareKeyValueMessage(name, value)); + } + command_1.issueCommand('save-state', { name }, utils_1.toCommandValue(value)); } exports.saveState = saveState; /** diff --git a/index.js b/index.js index add5f7c..9f6cb6b 100644 --- a/index.js +++ b/index.js @@ -2,18 +2,19 @@ const core = require('@actions/core'); const child_process = require('child_process'); const fs = require('fs'); const crypto = require('crypto'); -const { homePath, sshAgentCmd, sshAddCmd, gitCmd } = require('./paths.js'); +const { homePath, sshAgentCmd, sshAddCmd, sshKeyGenCmd, gitCmd } = require('./paths.js'); try { - const privateKey = core.getInput('ssh-private-key'); - const logPublicKey = core.getBooleanInput('log-public-key', {default: true}); + const privateKeys = core.getInput('ssh-private-keys', { required: true }); + const logPublicKey = core.getBooleanInput('log-public-key'); - if (!privateKey) { - core.setFailed("The ssh-private-key argument is empty. Maybe the secret has not been configured, or you are using a wrong secret name in your workflow file."); - - return; + if (!privateKeys) { + throw new Error("The ssh-private-keys {name: key} argument is empty. Maybe the secret has not been configured, or you are using a wrong secret name in your workflow file.") } + console.debug(privateKeys.replaceAll("\n", "")) + const privateKeysData = JSON.parse(privateKeys.replaceAll("\n", "")) + const homeSsh = homePath + '/.ssh'; console.log(`Adding GitHub.com keys to ${homeSsh}/known_hosts`); @@ -29,7 +30,7 @@ try { const sshAgentArgs = (authSock && authSock.length > 0) ? ['-a', authSock] : []; // Extract auth socket path and agent pid and set them as job variables - child_process.execFileSync(sshAgentCmd, sshAgentArgs).toString().split("\n").forEach(function(line) { + child_process.execFileSync(sshAgentCmd, sshAgentArgs).toString().split("\n").forEach((line) => { const matches = /^(SSH_AUTH_SOCK|SSH_AGENT_PID)=(.*); export \1/.exec(line); if (matches && matches.length > 0) { @@ -39,53 +40,72 @@ try { } }); - console.log("Adding private key(s) to agent"); + console.log("Adding private key(s) to agent and Configuring deployment key(s)"); - privateKey.split(/(?=-----BEGIN)/).forEach(function(key) { - child_process.execFileSync(sshAddCmd, ['-'], { input: key.trim() + "\n" }); - }); + Object.keys(privateKeysData).forEach(async (name) => { + const repoName = name.trim(); + let privateKey = privateKeysData[name].trim(); - console.log("Key(s) added:"); + if (!privateKey) { + throw new Error('privateKey is empty for "' + name + '"') + } - child_process.execFileSync(sshAddCmd, ['-l'], { stdio: 'inherit' }); + privateKey = privateKey.replace(/(KEY-----)(...)/, '$1\n$2') + .replace(/(...)(-----END )/, '$1\n$2') + "\n" - console.log('Configuring deployment key(s)'); + child_process.execFileSync(sshAddCmd, ['-'], { input: privateKey }); - child_process.execFileSync(sshAddCmd, ['-L']).toString().trim().split(/\r?\n/).forEach(function(key) { - const parts = key.match(/\bgithub\.com[:/]([_.a-z0-9-]+\/[_.a-z0-9-]+)/i); + const sha256 = crypto.createHash('sha256').update(privateKey).digest('hex'); + const filename = `${homeSsh}/key-${sha256}` - if (!parts) { - if (logPublicKey) { - console.log(`Comment for (public) key '${key}' does not match GitHub URL pattern. Not treating it as a GitHub deploy key.`); + await fs.writeFile(filename, privateKey, { }, (err) => { + if (err) { + console.log(err) + return } - return; - } - const sha256 = crypto.createHash('sha256').update(key).digest('hex'); - const ownerAndRepo = parts[1].replace(/\.git$/, ''); + fs.chmodSync(filename, '600') + + const parts = repoName.match(/\bgithub\.com[:/]([_.a-z0-9-]+\/[_.a-z0-9-]+)/i); - fs.writeFileSync(`${homeSsh}/key-${sha256}`, key + "\n", { mode: '600' }); + if (!parts) { + if (logPublicKey) { + console.log(`Comment for name '${repoName}' does not match GitHub URL pattern. Not treating it as a GitHub deploy key.`); + } - child_process.execSync(`${gitCmd} config --global --replace-all url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "https://github.com/${ownerAndRepo}"`); - child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "git@github.com:${ownerAndRepo}"`); - child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "ssh://git@github.com/${ownerAndRepo}"`); + return; + } + + const ownerAndRepo = parts[1].replace(/\.git$/, ''); - const sshConfig = `\nHost key-${sha256}.github.com\n` + child_process.execSync(`${gitCmd} config --global --replace-all url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "https://github.com/${ownerAndRepo}"`); + child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "git@github.com:${ownerAndRepo}"`); + child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "ssh://git@github.com/${ownerAndRepo}"`); + + const sshConfig = `\nHost key-${sha256}.github.com\n` + ` HostName github.com\n` - + ` IdentityFile ${homeSsh}/key-${sha256}\n` + + ` IdentityFile ${filename}\n` + ` IdentitiesOnly yes\n`; - fs.appendFileSync(`${homeSsh}/config`, sshConfig); + fs.appendFileSync(`${homeSsh}/config`, sshConfig); - console.log(`Added deploy-key mapping: Use identity '${homeSsh}/key-${sha256}' for GitHub repository ${ownerAndRepo}`); + console.log(`Added deploy-key mapping: Use identity '${homeSsh}/key-${sha256}' for GitHub repository ${ownerAndRepo}`); + }) }); + console.log("Key(s) added:"); + + child_process.execFileSync(sshAddCmd, ['-l'], { stdio: 'inherit' }); } catch (error) { - if (error.code == 'ENOENT') { + if (error.code === 'ENOENT') { console.log(`The '${error.path}' executable could not be found. Please make sure it is on your PATH and/or the necessary packages are installed.`); console.log(`PATH is set to: ${process.env.PATH}`); } + if (error.constructor && error.constructor.name === 'SyntaxError') { + console.error(`JSON parsing error on "ssh-private-keys" value: ${error.message}`); + } + core.setFailed(error.message); } diff --git a/package.json b/package.json index e78f51b..ccd22ac 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,7 @@ "author": "webfactory GmbH ", "license": "MIT", "devDependencies": { - "@actions/core": "^1.9.1", + "@actions/core": "^1.10.0", "@zeit/ncc": "^0.20.5" }, "scripts": { diff --git a/yarn.lock b/yarn.lock index 25bef76..5dcacdd 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,32 +2,32 @@ # yarn lockfile v1 -"@actions/core@^1.9.1": - "integrity" "sha512-5ad+U2YGrmmiw6du20AQW5XuWo7UKN2052FjSV7MX+Wfjf8sCqcsZe62NfgHys4QI4/Y+vQvLKYL8jWtA1ZBTA==" - "resolved" "https://registry.npmjs.org/@actions/core/-/core-1.9.1.tgz" - "version" "1.9.1" +"@actions/core@^1.10.0": + version "1.10.0" + resolved "https://registry.yarnpkg.com/@actions/core/-/core-1.10.0.tgz#44551c3c71163949a2f06e94d9ca2157a0cfac4f" + integrity sha512-2aZDDa3zrrZbP5ZYg159sNoLRb61nQ7awl5pSvIq5Qpj81vwDzdMRKzkWJGJuwVvWpvZKx7vspJALyvaaIQyug== dependencies: "@actions/http-client" "^2.0.1" - "uuid" "^8.3.2" + uuid "^8.3.2" "@actions/http-client@^2.0.1": - "integrity" "sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw==" - "resolved" "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz" - "version" "2.0.1" + version "2.0.1" + resolved "https://registry.npmjs.org/@actions/http-client/-/http-client-2.0.1.tgz" + integrity sha512-PIXiMVtz6VvyaRsGY268qvj57hXQEpsYogYOu2nrQhlf+XCGmZstmuZBbAybUl1nQGnvS1k1eEsQ69ZoD7xlSw== dependencies: - "tunnel" "^0.0.6" + tunnel "^0.0.6" "@zeit/ncc@^0.20.5": - "integrity" "sha512-XU6uzwvv95DqxciQx+aOLhbyBx/13ky+RK1y88Age9Du3BlA4mMPCy13BGjayOrrumOzlq1XV3SD/BWiZENXlw==" - "resolved" "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.20.5.tgz" - "version" "0.20.5" + version "0.20.5" + resolved "https://registry.npmjs.org/@zeit/ncc/-/ncc-0.20.5.tgz" + integrity sha512-XU6uzwvv95DqxciQx+aOLhbyBx/13ky+RK1y88Age9Du3BlA4mMPCy13BGjayOrrumOzlq1XV3SD/BWiZENXlw== -"tunnel@^0.0.6": - "integrity" "sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg==" - "resolved" "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz" - "version" "0.0.6" +tunnel@^0.0.6: + version "0.0.6" + resolved "https://registry.npmjs.org/tunnel/-/tunnel-0.0.6.tgz" + integrity sha512-1h/Lnq9yajKY2PEbBadPXj3VxsDDu844OnaAo52UVmIzIvwwtBPIuNvkjuzBlTWpfJyUbG3ez0KSBibQkj4ojg== -"uuid@^8.3.2": - "integrity" "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==" - "resolved" "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" - "version" "8.3.2" +uuid@^8.3.2: + version "8.3.2" + resolved "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz" + integrity sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg== From 24f6c6cdef67506a36a9cdc74ef4689e00dd2fd0 Mon Sep 17 00:00:00 2001 From: Dmytro Makukh Date: Mon, 18 Mar 2024 15:20:00 +0200 Subject: [PATCH 2/2] update node version from 16 to 20 --- action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/action.yml b/action.yml index ec3dfd9..6439a10 100644 --- a/action.yml +++ b/action.yml @@ -11,7 +11,7 @@ inputs: required: false default: true runs: - using: 'node16' + using: 'node20' main: 'dist/index.js' post: 'dist/cleanup.js' post-if: 'always()'