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
55 changes: 55 additions & 0 deletions dist/index.js
Original file line number Diff line number Diff line change
Expand Up @@ -36630,6 +36630,59 @@ var import_build = /* @__PURE__ */ __toESM((/* @__PURE__ */ __commonJSMin(((expo
})))(), 1);
var { VERSION, YAML, argv, dotenv, echo, expBackoff, fetch, fs, glob, globby, minimist, nothrow, parseArgv, question, quiet, retry, sleep, spinner, stdin, tempdir, tempfile, tmpdir, tmpfile, updateArgv, version, versions, $, Fail, ProcessOutput, ProcessPromise, bus, cd, chalk, defaults, kill, log, os: os$1, path, ps, quote, quotePowerShell, resolveDefaults, syncProcessCwd, useBash, usePowerShell, usePwsh, which, within } = globalThis.Deno ? globalThis.require("./index.cjs") : import_build;
//#endregion
//#region src/host-key-verification.ts
var HOST_KEY_VERIFICATION_FAILED = /host key verification failed/i;
/** Deployer wraps remote output as `[host] < …`. */
var DEPLOYER_HOST_KEY_LINE = /^\[([^\]]+)\]\s*<.*host key verification failed/i;
var OPENSSH_HOST_KEY_CHANGED = /\bhost key for ([^\s]+) has changed/i;
var OPENSSH_HOST_KEY_UNKNOWN = /No \S+ host key is known for (\S+)/i;
var OPENSSH_THE_HOST_KEY = /The \S+ host key for ([^\s]+) has changed/i;
function isHostKeyVerificationFailure(output) {
return HOST_KEY_VERIFICATION_FAILED.test(output);
}
function parseHostsFromHostKeyFailure(output) {
const hosts = /* @__PURE__ */ new Set();
for (const line of output.split(/\r?\n/)) {
const deployer = DEPLOYER_HOST_KEY_LINE.exec(line);
if (deployer) {
hosts.add(deployer[1]);
continue;
}
for (const pattern of [
OPENSSH_HOST_KEY_CHANGED,
OPENSSH_HOST_KEY_UNKNOWN,
OPENSSH_THE_HOST_KEY
]) {
const match = pattern.exec(line);
if (match) hosts.add(match[1].replace(/\.$/, ""));
}
}
return [...hosts];
}
function formatHostKeyVerificationGuidance(hosts, knownHostsConfigured) {
const hostList = hosts.length > 0 ? hosts.join(", ") : "(not found in the log — check hosts in your Deployer recipe)";
const scanExample = hosts.length === 1 ? ` ssh-keyscan -t rsa,ecdsa,ed25519 ${hosts[0]}` : " ssh-keyscan -t rsa,ecdsa,ed25519 YOUR_DEPLOY_HOST";
const knownHostsHint = knownHostsConfigured ? "The known-hosts input is set, but this host is missing or its key no longer matches the server (for example after reinstall or key rotation)." : "When known-hosts is empty this action disables StrictHostKeyChecking; if you still see this, check ssh-config or other SSH settings.";
return [
"SSH host key verification failed for the remote deployment server.",
`Remote host(s): ${hostList}`,
"",
"This refers to the remote server SSH host key (server identity), not your deploy private-key secret.",
"It is unrelated to github.com unless your Deployer recipe connects to GitHub over SSH.",
knownHostsHint,
"",
"Update the action known-hosts input with a current key, for example:",
scanExample,
"",
"https://github.com/deployphp/action/issues/61"
].join("\n");
}
function commandOutput(err) {
if (err !== null && typeof err === "object" && "stdall" in err && typeof err.stdall === "string") return err.stdall;
if (err instanceof Error) return err.message;
return String(err);
}
//#endregion
//#region src/index.ts
$.verbose = true;
(async function main() {
Expand Down Expand Up @@ -36728,6 +36781,8 @@ async function dep() {
try {
await $`${phpBin} ${bin} ${cmd} ${recipeArgs} --no-interaction ${ansi} ${verbosityArgs} ${options}`;
} catch (err) {
const output = commandOutput(err);
if (isHostKeyVerificationFailure(output)) error(formatHostKeyVerificationGuidance(parseHostsFromHostKeyFailure(output), getInput("known-hosts") !== ""));
setFailed(`Failed: dep ${cmd}`);
}
}
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,8 @@
"build": "vite build",
"typecheck": "tsc --noEmit",
"format": "prettier --write .",
"format:check": "prettier --check ."
"format:check": "prettier --check .",
"test": "node --experimental-strip-types --test src/**/*.test.ts"
},
"dependencies": {
"@actions/core": "^3.0.0",
Expand Down
67 changes: 67 additions & 0 deletions src/host-key-verification.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
import assert from 'node:assert/strict'
import { describe, it } from 'node:test'
import {
formatHostKeyVerificationGuidance,
isHostKeyVerificationFailure,
parseHostsFromHostKeyFailure,
} from './host-key-verification.ts'

const deployerLog = `
✈︎ Deploying deploy-test on 1.2.3.4
[23.94.156.6] > echo $0
[23.94.156.6] < ssh multiplexing initialization
[23.94.156.6] < Host key verification failed.
`.trim()

describe('isHostKeyVerificationFailure', () => {
it('detects deployer host key failures', () => {
assert.equal(isHostKeyVerificationFailure(deployerLog), true)
})

it('ignores unrelated output', () => {
assert.equal(isHostKeyVerificationFailure('deploy finished'), false)
})
})

describe('parseHostsFromHostKeyFailure', () => {
it('parses deployer bracket host lines', () => {
assert.deepEqual(parseHostsFromHostKeyFailure(deployerLog), ['23.94.156.6'])
})

it('parses OpenSSH host key changed messages', () => {
const output = `RSA host key for deploy.example.com has changed and you have requested strict checking.
Host key verification failed.`
assert.deepEqual(parseHostsFromHostKeyFailure(output), [
'deploy.example.com',
])
})

it('parses unknown host key messages', () => {
const output = `No ED25519 host key is known for staging.example.net.
Host key verification failed.`
assert.deepEqual(parseHostsFromHostKeyFailure(output), [
'staging.example.net',
])
})

it('deduplicates repeated hosts', () => {
const output = `[10.0.0.1] < Host key verification failed.
[10.0.0.1] < Host key verification failed.`
assert.deepEqual(parseHostsFromHostKeyFailure(output), ['10.0.0.1'])
})
})

describe('formatHostKeyVerificationGuidance', () => {
it('names the remote host and ssh-keyscan command', () => {
const message = formatHostKeyVerificationGuidance(['1.2.3.4'], true)
assert.match(message, /Remote host\(s\): 1\.2\.3\.4/)
assert.match(message, /ssh-keyscan -t rsa,ecdsa,ed25519 1\.2\.3\.4/)
assert.match(message, /not your deploy private-key secret/)
assert.match(message, /unrelated to github\.com/)
})

it('mentions missing known-hosts match when configured', () => {
const message = formatHostKeyVerificationGuidance([], true)
assert.match(message, /known-hosts input is set/)
})
})
86 changes: 86 additions & 0 deletions src/host-key-verification.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,86 @@
const HOST_KEY_VERIFICATION_FAILED = /host key verification failed/i

/** Deployer wraps remote output as `[host] < …`. */
const DEPLOYER_HOST_KEY_LINE =
/^\[([^\]]+)\]\s*<.*host key verification failed/i

const OPENSSH_HOST_KEY_CHANGED = /\bhost key for ([^\s]+) has changed/i
const OPENSSH_HOST_KEY_UNKNOWN = /No \S+ host key is known for (\S+)/i
const OPENSSH_THE_HOST_KEY = /The \S+ host key for ([^\s]+) has changed/i

export function isHostKeyVerificationFailure(output: string): boolean {
return HOST_KEY_VERIFICATION_FAILED.test(output)
}

export function parseHostsFromHostKeyFailure(output: string): string[] {
const hosts = new Set<string>()

for (const line of output.split(/\r?\n/)) {
const deployer = DEPLOYER_HOST_KEY_LINE.exec(line)
if (deployer) {
hosts.add(deployer[1])
continue
}

for (const pattern of [
OPENSSH_HOST_KEY_CHANGED,
OPENSSH_HOST_KEY_UNKNOWN,
OPENSSH_THE_HOST_KEY,
]) {
const match = pattern.exec(line)
if (match) {
hosts.add(match[1].replace(/\.$/, ''))
}
}
}

return [...hosts]
}

export function formatHostKeyVerificationGuidance(
hosts: string[],
knownHostsConfigured: boolean,
): string {
const hostList =
hosts.length > 0
? hosts.join(', ')
: '(not found in the log — check hosts in your Deployer recipe)'

const scanExample =
hosts.length === 1
? ` ssh-keyscan -t rsa,ecdsa,ed25519 ${hosts[0]}`
: ' ssh-keyscan -t rsa,ecdsa,ed25519 YOUR_DEPLOY_HOST'

const knownHostsHint = knownHostsConfigured
? 'The known-hosts input is set, but this host is missing or its key no longer matches the server (for example after reinstall or key rotation).'
: 'When known-hosts is empty this action disables StrictHostKeyChecking; if you still see this, check ssh-config or other SSH settings.'

return [
'SSH host key verification failed for the remote deployment server.',
`Remote host(s): ${hostList}`,
'',
'This refers to the remote server SSH host key (server identity), not your deploy private-key secret.',
'It is unrelated to github.com unless your Deployer recipe connects to GitHub over SSH.',
knownHostsHint,
'',
'Update the action known-hosts input with a current key, for example:',
scanExample,
'',
'https://github.com/deployphp/action/issues/61',
].join('\n')
}

export function commandOutput(err: unknown): string {
if (
err !== null &&
typeof err === 'object' &&
'stdall' in err &&
typeof (err as { stdall: unknown }).stdall === 'string'
) {
return (err as { stdall: string }).stdall
}
if (err instanceof Error) {
return err.message
}
return String(err)
}
16 changes: 16 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,11 @@
import * as core from '@actions/core'
import { $, fs, cd } from 'zx'
import {
commandOutput,
formatHostKeyVerificationGuidance,
isHostKeyVerificationFailure,
parseHostsFromHostKeyFailure,
} from './host-key-verification.js'

$.verbose = true

Expand Down Expand Up @@ -164,6 +170,16 @@ async function dep(): Promise<void> {
try {
await $`${phpBin} ${bin} ${cmd} ${recipeArgs} --no-interaction ${ansi} ${verbosityArgs} ${options}`
} catch (err) {
const output = commandOutput(err)
if (isHostKeyVerificationFailure(output)) {
const hosts = parseHostsFromHostKeyFailure(output)
core.error(
formatHostKeyVerificationGuidance(
hosts,
core.getInput('known-hosts') !== '',
),
)
}
core.setFailed(`Failed: dep ${cmd}`)
}
}
2 changes: 1 addition & 1 deletion tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -15,5 +15,5 @@
"lib": ["ES2023"]
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
"exclude": ["node_modules", "dist", "src/**/*.test.ts"]
}