81 lines
2.9 KiB
TypeScript
81 lines
2.9 KiB
TypeScript
import { createHash } from 'node:crypto';
|
|
import type { DeploySummary } from '$lib/utils/deploy-summary-core';
|
|
|
|
/**
|
|
* Assembles the `details` JSON stored on a stack_deploy schedule_executions row.
|
|
*
|
|
* Deliberately dependency-free (no db.ts, no filesystem) so it can be unit tested
|
|
* under Bun -- see deploy-run-record.ts for the database-touching half that calls
|
|
* this. Importing db.ts here would make this module unimportable under Bun
|
|
* (better-sqlite3, ERR_DLOPEN_FAILED), breaking this file's own test and dragging
|
|
* down every other test file that happens to run in the same process.
|
|
*/
|
|
|
|
export interface DeployRunOptions {
|
|
pull: boolean;
|
|
build: boolean;
|
|
forceRecreate: boolean;
|
|
}
|
|
|
|
export interface BuildRunDetailsInput {
|
|
options: DeployRunOptions;
|
|
summary?: DeploySummary;
|
|
exitCode: number;
|
|
composeHash: string;
|
|
envHash: string;
|
|
logFile: string;
|
|
/** Set when the run's output hit deploy-log-store's total-size budget and was cut off. */
|
|
truncated?: boolean;
|
|
}
|
|
|
|
export interface DeployRunDetails {
|
|
options: DeployRunOptions;
|
|
summary?: DeploySummary;
|
|
exitCode: number;
|
|
composeHash: string;
|
|
envHash: string;
|
|
logFile: string;
|
|
truncated?: boolean;
|
|
}
|
|
|
|
/**
|
|
* The log file name is what ties this schedule_executions row to its log file on
|
|
* disk (deploy-logs/<id>.log). Task 13's reconciler deletes any file whose id has
|
|
* no matching record -- if the name were missing here, a live run's own file would
|
|
* look orphaned and get eaten out from under it.
|
|
*/
|
|
export function buildRunDetails(input: BuildRunDetailsInput): DeployRunDetails {
|
|
if (!input.logFile) {
|
|
throw new Error('buildRunDetails: logFile is required to tie the record to its log file');
|
|
}
|
|
return {
|
|
options: input.options,
|
|
summary: input.summary,
|
|
exitCode: input.exitCode,
|
|
composeHash: input.composeHash,
|
|
envHash: input.envHash,
|
|
logFile: input.logFile,
|
|
...(input.truncated ? { truncated: true } : {})
|
|
};
|
|
}
|
|
|
|
/** SHA-256 of the compose file content used for this deploy, hex-encoded. */
|
|
export function hashComposeContent(content: string): string {
|
|
return createHash('sha256').update(content).digest('hex');
|
|
}
|
|
|
|
/**
|
|
* SHA-256 fingerprint of the effective env for this deploy (every var -- secret and
|
|
* non-secret alike -- that will actually reach the container), hex-encoded. Sorted by
|
|
* key so the same env always hashes to the same value regardless of object key order.
|
|
* This is a change-fingerprint, not a secret store -- the hash alone can't be turned
|
|
* back into the values it was built from, so hashing secret values in is safe even
|
|
* though logging them would not be. Callers merge whatever sources make up their
|
|
* effective env (DB non-secret + secret vars, or a request body's raw/array vars)
|
|
* into one Record before calling this.
|
|
*/
|
|
export function hashEnvFingerprint(vars: Record<string, string>): string {
|
|
const serialized = Object.keys(vars).sort().map((k) => `${k}=${vars[k]}`).join('\n');
|
|
return createHash('sha256').update(serialized).digest('hex');
|
|
}
|