Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
77 changes: 34 additions & 43 deletions .github/workflows/demo.yml
Original file line number Diff line number Diff line change
@@ -1,46 +1,37 @@
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 [email protected]:mpdude/test-1.git test-1-git
git clone ssh://[email protected]/mpdude/test-1.git test-1-git-ssh
git clone https://github.com/mpdude/test-2.git test-2-http
git clone [email protected]:mpdude/test-2.git test-2-git
git clone ssh://[email protected]/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 [email protected]:mpdude/test-1.git test-1-git
git clone ssh://[email protected]/mpdude/test-1.git test-1-git-ssh
git clone https://github.com/mpdude/test-2.git test-2-http
git clone [email protected]:mpdude/test-2.git test-2-git
git clone ssh://[email protected]/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
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What is this for, given the next step is "Setup node"? Does that next step not work locally and becomes a "no-op" if nodejs is already installed and at the right version?

Copy link
Member Author

@elvinristi elvinristi Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this is "hack" for local container when using with act tool ( only executed when if: ${{ env.ACT }}) - got same error as others with node missing. Some "known" issue with the tool when using following combo:

    runs-on: ubuntu-latest
    container:
      image: ubuntu:latest

This is error without this:
image

via nektos/act#917 (comment)

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Aha... so the action to setup node uses node to run :) - how nice.
But yeah, I noticed in an open source project i contributed to that Github's image "ubuntu-latest" is actually different compared to the "ubuntu:latest" and has more things installed already.

Btw, why do you use (overwrite) "ubuntu:latest" as the image and not let act pick the right image for "ubuntu-latest"? act has an image ghcr.io/catthehacker/ubuntu:act-latest - maybe that one contains node already like github's runner image?

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this was taken from parent fork, to test as container. will see if I have possibility to dig more next week

- name: Setup node
uses: actions/setup-node@v3
with:
node-version: '16.x'
- name: Setup key
uses: ./
with:
ssh-private-keys: |
[
{
"name": "[email protected]:vaimo/vsf2_ext-lru-cache-driver.git",
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could this use yaml instead of text? (i.e. consistent with the rest of the file and validated by the yaml parser, before the action even starts running?)

Copy link
Member Author

@elvinristi elvinristi Oct 27, 2022

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

github action doesn't support it. only accepts text as input

from node_modules/@actions/core/lib/core.d.ts :

/**
 * Gets the value of an input.
 * Unless trimWhitespace is set to false in InputOptions, the value is also trimmed.
 * Returns an empty string if the value is not defined.
 *
 * @param     name     name of the input to get
 * @param     options  optional. See InputOptions.
 * @returns   string
 */
export declare function getInput(name: string, options?: InputOptions): string;
/**
 * Gets the values of an multiline input.  Each value is also trimmed.
 *
 * @param     name     name of the input to get
 * @param     options  optional. See InputOptions.
 * @returns   string[]
 *
 */
export declare function getMultilineInput(name: string, options?: InputOptions): string[];
/**
 * Gets the input value of the boolean type in the YAML 1.2 "core schema" specification.
 * Support boolean input list: `true | True | TRUE | false | False | FALSE` .
 * The return value is also in boolean type.
 * ref: https://yaml.org/spec/1.2/spec.html#id2804923
 *
 * @param     name     name of the input to get
 * @param     options  optional. See InputOptions.
 * @returns   boolean
 */
export declare function getBooleanInput(name: string, options?: InputOptions): boolean;

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok. The format feels quite verbose to me. Would the follow perhaps be easier/nicer (use repo name as key)?

  ssh-private-keys: |
    {
      "[email protected]:vaimo/vsf2_ext-lru-cache-driver.git": "${{ secrets.TEST_1_DEPLOY_KEY }}",
      "[email protected]:mpdude/test-2.git": "${{ secrets.TEST_2_DEPLOY_KEY }}"
    }

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Will think of that, doesn't hurt to refactor it to that way 👍🏻

"key": "${{ secrets.TEST_1_DEPLOY_KEY }}"
},
{
"name": "[email protected]:mpdude/test-2.git",
"key": "${{ secrets.TEST_2_DEPLOY_KEY }}"
}
]
- run: |
git clone https://github.com/vaimo/vsf2_ext-lru-cache-driver.git test-1-http
git clone [email protected]:vaimo/vsf2_ext-lru-cache-driver.git test-1-git
git clone ssh://[email protected]/vaimo/vsf2_ext-lru-cache-driver.git test-1-git-ssh
29 changes: 23 additions & 6 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,8 +39,14 @@ jobs:
# Make sure the @v0.6.0 matches the current version of the
# action
- uses: webfactory/[email protected]
with:
ssh-private-key: ${{ secrets.SSH_PRIVATE_KEY }}
with:
ssh-private-keys: |
[
{
"name": "[email protected]:vendor/repo-1.git",
"key": "${{ 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.
Expand All @@ -55,10 +61,21 @@ You can set up different keys as different secrets and pass them all to the acti
# ... contens as before
- uses: webfactory/[email protected]
with:
ssh-private-key: |
${{ secrets.FIRST_KEY }}
${{ secrets.NEXT_KEY }}
${{ secrets.ANOTHER_KEY }}
ssh-private-keys: |
[
{
"name": "[email protected]:vendor/repo-1.git",
"key": "${{ secrets.FIRST_KEY }}"
},
{
"name": "[email protected]:vendor/repo-2.git",
"key": "${{ secrets.NEXT_KEY }}"
},
{
"name": "[email protected]:vendor/repo-3.git",
"key": "${{ secrets.ANOTHER_KEY }}"
}
]
```

The `ssh-agent` will load all of the keys and try each one in order when establishing SSH connections.
Expand Down
7 changes: 4 additions & 3 deletions action.yml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
71 changes: 42 additions & 29 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -322,18 +322,20 @@ 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 privateKeys = core.getInput('ssh-private-keys');
const logPublicKey = core.getBooleanInput('log-public-key', {default: true});

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.");
if (!privateKeys) {
core.setFailed("The ssh-private-keys Array{name, key} argument is empty. Maybe the secret has not been configured, or you are using a wrong secret name in your workflow file.");

return;
}

const privateKeysData = JSON.parse(privateKeys.replaceAll("\n", ""))

const homeSsh = homePath + '/.ssh';

console.log(`Adding GitHub.com keys to ${homeSsh}/known_hosts`);
Expand All @@ -359,47 +361,58 @@ try {
}
});

console.log("Adding private key(s) to agent");

privateKey.split(/(?=-----BEGIN)/).forEach(function(key) {
child_process.execFileSync(sshAddCmd, ['-'], { input: key.trim() + "\n" });
});
console.log("Adding private key(s) to agent and Configuring deployment key(s)");

console.log("Key(s) added:");
privateKeysData.forEach(async ({ name, key }) => {
const repoName = name.trim();
let privateKey = key.trim();

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')

fs.writeFileSync(`${homeSsh}/key-${sha256}`, key + "\n", { mode: '600' });
const parts = repoName.match(/\bgithub\.com[:/]([_.a-z0-9-]+\/[_.a-z0-9-]+)/i);

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 "[email protected]:${ownerAndRepo}"`);
child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "ssh://[email protected]/${ownerAndRepo}"`);
if (!parts) {
if (logPublicKey) {
console.log(`Comment for name '${repoName}' does not match GitHub URL pattern. Not treating it as a GitHub deploy key.`);
}

const sshConfig = `\nHost key-${sha256}.github.com\n`
return;
}

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 "[email protected]:${ownerAndRepo}"`);
child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "ssh://[email protected]/${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') {
Expand Down
71 changes: 42 additions & 29 deletions index.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,18 +2,20 @@ 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 privateKeys = core.getInput('ssh-private-keys');
const logPublicKey = core.getBooleanInput('log-public-key', {default: true});

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.");
if (!privateKeys) {
core.setFailed("The ssh-private-keys Array{name, key} argument is empty. Maybe the secret has not been configured, or you are using a wrong secret name in your workflow file.");

return;
}

const privateKeysData = JSON.parse(privateKeys.replaceAll("\n", ""))

const homeSsh = homePath + '/.ssh';

console.log(`Adding GitHub.com keys to ${homeSsh}/known_hosts`);
Expand All @@ -39,47 +41,58 @@ 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" });
});
privateKeysData.forEach(async ({ name, key }) => {
const repoName = name.trim();
let privateKey = key.trim();

console.log("Key(s) added:");
privateKey = privateKey.replace(/(KEY-----)(...)/, '$1\n$2')
.replace(/(...)(-----END )/, '$1\n$2') + "\n"

child_process.execFileSync(sshAddCmd, ['-l'], { stdio: 'inherit' });
child_process.execFileSync(sshAddCmd, ['-'], { input: privateKey });

console.log('Configuring deployment key(s)');
const sha256 = crypto.createHash('sha256').update(privateKey).digest('hex');
const filename = `${homeSsh}/key-${sha256}`

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);

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')

fs.writeFileSync(`${homeSsh}/key-${sha256}`, key + "\n", { mode: '600' });
const parts = repoName.match(/\bgithub\.com[:/]([_.a-z0-9-]+\/[_.a-z0-9-]+)/i);

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 "[email protected]:${ownerAndRepo}"`);
child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "ssh://[email protected]/${ownerAndRepo}"`);
if (!parts) {
if (logPublicKey) {
console.log(`Comment for name '${repoName}' does not match GitHub URL pattern. Not treating it as a GitHub deploy key.`);
}

const sshConfig = `\nHost key-${sha256}.github.com\n`
return;
}

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 "[email protected]:${ownerAndRepo}"`);
child_process.execSync(`${gitCmd} config --global --add url."git@key-${sha256}.github.com:${ownerAndRepo}".insteadOf "ssh://[email protected]/${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') {
Expand Down