feat: enhance deployment process with improved error handling and logging

This commit is contained in:
2026-05-07 16:34:21 -04:00
parent ab0da30e78
commit e3ac72906d
6 changed files with 332 additions and 132 deletions

View File

@@ -1,11 +1,11 @@
import { appendFileSync, mkdtempSync, mkdirSync, writeFileSync } from "node:fs";
import { tmpdir } from "node:os";
import { appendFileSync, existsSync, mkdirSync, writeFileSync } from "node:fs";
import { join } from "node:path";
import { consola } from "consola";
import { execa } from "execa";
import type { WPopContext } from "./context";
import type { DeployEnv } from "./env";
import { run } from "./run";
import { createTempDir } from "./tempdir";
export type SshConfig = {
host: string;
@@ -14,8 +14,12 @@ export type SshConfig = {
privateKey?: string;
};
export type PreparedSsh = SshConfig & {
export type PreparedSsh = {
host: string;
port: number;
user: string;
identityFile?: string;
knownHostsFile: string;
};
export function createSshConfig(env: DeployEnv): SshConfig {
@@ -27,29 +31,43 @@ export function createSshConfig(env: DeployEnv): SshConfig {
};
}
export async function prepareSsh(context: WPopContext, config: SshConfig): Promise<PreparedSsh> {
export async function prepareSsh(
context: WPopContext,
config: SshConfig,
cacheDir: string,
): Promise<PreparedSsh> {
const knownHostsFile = ensureKnownHostsFile(context, cacheDir);
const prepared: PreparedSsh = {
host: config.host,
port: config.port,
user: config.user,
knownHostsFile,
};
if (config.privateKey) {
prepared.identityFile = writePrivateKey(context, config.privateKey);
}
await addKnownHost(context, prepared);
await ensureKnownHost(context, prepared);
await verifySshAuth(context, prepared);
return prepared;
}
export function sshTarget(config: SshConfig): string {
export function sshTarget(config: PreparedSsh): string {
return `${config.user}@${config.host}`;
}
export function sshArgs(config: PreparedSsh): string[] {
const args = ["-p", String(config.port)];
const args = [
"-p",
String(config.port),
"-o",
`UserKnownHostsFile=${config.knownHostsFile}`,
"-o",
"GlobalKnownHostsFile=/dev/null",
];
if (config.identityFile) {
args.push("-i", config.identityFile);
@@ -62,34 +80,66 @@ export function rsyncSshShell(config: PreparedSsh): string {
return ["ssh", ...sshArgs(config)].join(" ");
}
function writePrivateKey(context: WPopContext, privateKey: string): string | undefined {
consola.info("Using SSH private key from SSH_PRIVATE_KEY");
function ensureKnownHostsFile(context: WPopContext, cacheDir: string): string {
const path = join(cacheDir, "known_hosts");
if (context.dryRun) {
return "/tmp/wpop-ssh-key";
return path;
}
mkdirSync(cacheDir, { recursive: true });
if (!existsSync(path)) {
writeFileSync(path, "", { mode: 0o600 });
}
return path;
}
function writePrivateKey(context: WPopContext, privateKey: string): string {
consola.info("Using SSH private key from SSH_PRIVATE_KEY");
const { path: keyDir, cleanup } = createTempDir(context, "wpop-ssh");
if (context.dryRun) {
return join(keyDir, "id_ed25519");
}
const keyDir = mkdtempSync(join(tmpdir(), "wpop-ssh-"));
const keyPath = join(keyDir, "id_ed25519");
writeFileSync(keyPath, privateKey.endsWith("\n") ? privateKey : `${privateKey}\n`, {
mode: 0o600,
});
process.once("exit", cleanup);
return keyPath;
}
async function addKnownHost(context: WPopContext, config: PreparedSsh): Promise<void> {
consola.info(`Scanning SSH host key for ${config.host}`);
async function ensureKnownHost(context: WPopContext, config: PreparedSsh): Promise<void> {
if (context.dryRun) {
consola.info(`Ensuring SSH host key for ${config.host} is known`);
await run(context, "ssh-keyscan", ["-p", String(config.port), "-H", config.host]);
return;
}
if (await hostInKnownHosts(config)) {
consola.debug(`Host ${config.host}:${config.port} already in known_hosts`);
return;
}
consola.info(`Scanning SSH host key for ${config.host}`);
const { stdout } = await execa("ssh-keyscan", ["-p", String(config.port), "-H", config.host]);
const sshDir = join(process.env.HOME ?? context.cwd, ".ssh");
mkdirSync(sshDir, { mode: 0o700, recursive: true });
appendFileSync(join(sshDir, "known_hosts"), `${stdout}\n`);
appendFileSync(config.knownHostsFile, `${stdout}\n`);
}
async function hostInKnownHosts(config: PreparedSsh): Promise<boolean> {
if (!existsSync(config.knownHostsFile)) {
return false;
}
const lookup = config.port === 22 ? config.host : `[${config.host}]:${config.port}`;
const result = await execa("ssh-keygen", ["-F", lookup, "-f", config.knownHostsFile], {
reject: false,
});
return result.exitCode === 0;
}
async function verifySshAuth(context: WPopContext, config: PreparedSsh): Promise<void> {