Skip to content
Open
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
40 changes: 33 additions & 7 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import * as fs$1 from "fs";
import { constants, promises } from "fs";
import * as path$1 from "path";
import * as events from "events";
import { fileURLToPath } from "node:url";
import "child_process";
import "timers";
import * as process$1 from "node:process";
Expand Down Expand Up @@ -36632,25 +36633,49 @@ var { VERSION, YAML, argv, dotenv, echo, expBackoff, fetch, fs, glob, globby, mi
//#endregion
//#region src/index.ts
$.verbose = true;
(async function main() {
var DEFAULT_AUTH_SOCK = "/tmp/ssh-auth.sock";
async function main() {
try {
await ssh();
await dep();
} catch (err) {
setFailed(err instanceof Error ? err.message : String(err));
}
})();
}
async function sshAgentIsReachable(authSock) {
try {
await $({ env: {
...process.env,
SSH_AUTH_SOCK: authSock
} })`ssh-add -l`;
return true;
} catch (err) {
return (err instanceof Error ? err.message : String(err)).includes("The agent has no identities.");
}
}
async function ensureSshAgent(authSock) {
if (await sshAgentIsReachable(authSock)) {
exportVariable("SSH_AUTH_SOCK", authSock);
return;
}
if (fs.existsSync(authSock)) fs.rmSync(authSock, { force: true });
const agentPid = (await $`ssh-agent -a ${authSock}`).stdout.match(/SSH_AGENT_PID=(\d+)/)?.[1];
exportVariable("SSH_AUTH_SOCK", authSock);
if (agentPid !== void 0) exportVariable("SSH_AGENT_PID", agentPid);
}
async function ssh() {
if (getBooleanInput("skip-ssh-setup")) return;
const sshHomeDir = `${process.env["HOME"]}/.ssh`;
if (!fs.existsSync(sshHomeDir)) fs.mkdirSync(sshHomeDir);
const authSock = "/tmp/ssh-auth.sock";
await $`ssh-agent -a ${authSock}`;
exportVariable("SSH_AUTH_SOCK", authSock);
const authSock = process.env["SSH_AUTH_SOCK"] || DEFAULT_AUTH_SOCK;
await ensureSshAgent(authSock);
let privateKey = getInput("private-key");
if (privateKey !== "") {
privateKey = privateKey.replace(/\r/g, "").trim() + "\n";
const p = $`ssh-add -`;
const p = $({ env: {
...process.env,
SSH_AUTH_SOCK: authSock
} })`ssh-add -`;
p.stdin.write(privateKey);
p.stdin.end();
await p;
Expand Down Expand Up @@ -36731,5 +36756,6 @@ async function dep() {
setFailed(`Failed: dep ${cmd}`);
}
}
if (process.argv[1] === fileURLToPath(import.meta.url)) main();
//#endregion
export {};
export { dep, main, ssh };
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"scripts": {
"build": "vite build",
"typecheck": "tsc --noEmit",
"test": "npm run build && node --test tests/*.test.mjs",
"format": "prettier --write .",
"format:check": "prettier --check ."
},
Expand Down
51 changes: 43 additions & 8 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import * as core from '@actions/core'
import { $, fs, cd } from 'zx'
import { fileURLToPath } from 'node:url'

$.verbose = true

Expand All @@ -13,16 +14,47 @@ interface DeployerManifestEntry {
url: string
}

void (async function main(): Promise<void> {
const DEFAULT_AUTH_SOCK = '/tmp/ssh-auth.sock'

export async function main(): Promise<void> {
try {
await ssh()
await dep()
} catch (err) {
core.setFailed(err instanceof Error ? err.message : String(err))
}
})()
}

async function sshAgentIsReachable(authSock: string): Promise<boolean> {
try {
await $({ env: { ...process.env, SSH_AUTH_SOCK: authSock } })`ssh-add -l`
return true
} catch (err) {
const message = err instanceof Error ? err.message : String(err)
return message.includes('The agent has no identities.')
}
}

async function ensureSshAgent(authSock: string): Promise<void> {
if (await sshAgentIsReachable(authSock)) {
core.exportVariable('SSH_AUTH_SOCK', authSock)
return
}

async function ssh(): Promise<void> {
if (fs.existsSync(authSock)) {
fs.rmSync(authSock, { force: true })
}

const result = await $`ssh-agent -a ${authSock}`
const agentPid = result.stdout.match(/SSH_AGENT_PID=(\d+)/)?.[1]

core.exportVariable('SSH_AUTH_SOCK', authSock)
if (agentPid !== undefined) {
core.exportVariable('SSH_AGENT_PID', agentPid)
}
}

export async function ssh(): Promise<void> {
if (core.getBooleanInput('skip-ssh-setup')) {
return
}
Expand All @@ -33,14 +65,13 @@ async function ssh(): Promise<void> {
fs.mkdirSync(sshHomeDir)
}

const authSock = '/tmp/ssh-auth.sock'
await $`ssh-agent -a ${authSock}`
core.exportVariable('SSH_AUTH_SOCK', authSock)
const authSock = process.env['SSH_AUTH_SOCK'] || DEFAULT_AUTH_SOCK
await ensureSshAgent(authSock)

let privateKey = core.getInput('private-key')
if (privateKey !== '') {
privateKey = privateKey.replace(/\r/g, '').trim() + '\n'
const p = $`ssh-add -`
const p = $({ env: { ...process.env, SSH_AUTH_SOCK: authSock } })`ssh-add -`
p.stdin.write(privateKey)
p.stdin.end()
await p
Expand All @@ -62,7 +93,7 @@ async function ssh(): Promise<void> {
}
}

async function dep(): Promise<void> {
export async function dep(): Promise<void> {
let bin = core.getInput('deployer-binary')
const subDirectory = core.getInput('sub-directory').trim()

Expand Down Expand Up @@ -167,3 +198,7 @@ async function dep(): Promise<void> {
core.setFailed(`Failed: dep ${cmd}`)
}
}

if (process.argv[1] === fileURLToPath(import.meta.url)) {
void main()
}
76 changes: 76 additions & 0 deletions tests/ssh.test.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
import test from 'node:test'
import assert from 'node:assert/strict'
import fs from 'node:fs'
import os from 'node:os'
import path from 'node:path'
import { execFileSync, spawnSync } from 'node:child_process'

import { ssh } from '../dist/index.js'

function restoreEnv(snapshot) {
for (const [key, value] of Object.entries(snapshot)) {
if (value === undefined) {
delete process.env[key]
} else {
process.env[key] = value
}
}
}

test('ssh setup reuses an existing agent across repeated action runs', async () => {
const tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'deployphp-action-'))
const homeDir = path.join(tmpDir, 'home')
const keyPath = path.join(tmpDir, 'id_ed25519')
const authSock = path.join(tmpDir, 'ssh-auth.sock')
fs.mkdirSync(homeDir, { recursive: true })
execFileSync('ssh-keygen', ['-t', 'ed25519', '-N', '', '-f', keyPath], {
stdio: 'ignore',
})

const envSnapshot = {
HOME: process.env.HOME,
SSH_AUTH_SOCK: process.env.SSH_AUTH_SOCK,
SSH_AGENT_PID: process.env.SSH_AGENT_PID,
'INPUT_SKIP-SSH-SETUP': process.env['INPUT_SKIP-SSH-SETUP'],
'INPUT_PRIVATE-KEY': process.env['INPUT_PRIVATE-KEY'],
}

process.env.HOME = homeDir
process.env.SSH_AUTH_SOCK = authSock
delete process.env.SSH_AGENT_PID
process.env['INPUT_SKIP-SSH-SETUP'] = 'false'
process.env['INPUT_PRIVATE-KEY'] = fs.readFileSync(keyPath, 'utf8')

try {
await ssh()
let listed = spawnSync('ssh-add', ['-l'], {
env: { ...process.env, SSH_AUTH_SOCK: authSock },
encoding: 'utf8',
})
assert.equal(listed.status, 0, listed.stderr)
const firstPid = process.env.SSH_AGENT_PID
assert.ok(firstPid, 'expected SSH_AGENT_PID to be exported')

await ssh()
listed = spawnSync('ssh-add', ['-l'], {
env: { ...process.env, SSH_AUTH_SOCK: authSock },
encoding: 'utf8',
})
assert.equal(listed.status, 0, listed.stderr)
assert.equal(process.env.SSH_AGENT_PID, firstPid)
assert.ok(fs.existsSync(authSock), 'expected SSH agent socket to remain available')
} finally {
if (process.env.SSH_AGENT_PID) {
spawnSync('ssh-agent', ['-k'], {
env: {
...process.env,
SSH_AUTH_SOCK: authSock,
SSH_AGENT_PID: process.env.SSH_AGENT_PID,
},
stdio: 'ignore',
})
}
restoreEnv(envSnapshot)
fs.rmSync(tmpDir, { recursive: true, force: true })
}
})