74 lines
2.8 KiB
TypeScript
74 lines
2.8 KiB
TypeScript
/**
|
|
* Deliberate boot refusals and whether they should page Sentry.
|
|
*
|
|
* Some startup preconditions the server must not repair itself: an
|
|
* unmigrated schema when auto-apply is off (the operator's migration
|
|
* runner owns schema), or an unmet database contract for authenticated
|
|
* public deployments. The server logs the refusal and exits nonzero so
|
|
* whatever supervises the deployment can act.
|
|
*
|
|
* In supervised managed-cloud deployments (`PAPERCLIP_CLOUD_API_ORIGIN`
|
|
* set), two of these refusals are a routine provisioning phase rather
|
|
* than an incident: a freshly created stack's app container boots
|
|
* before the harness has migrated the empty database or finished
|
|
* applying its committed configuration, crash-loops briefly, and is
|
|
* restarted by the harness once the precondition holds. Reporting every
|
|
* such boot to Sentry buries real errors under hundreds of expected
|
|
* events per fleet build batch, so the crash handler skips the capture
|
|
* for exactly this class — the refusal still logs and still exits
|
|
* nonzero. Everywhere else (self-hosted, local dev) reporting is
|
|
* unchanged.
|
|
*/
|
|
|
|
export type StartupRefusalKind =
|
|
| "schema-not-yet-migrated"
|
|
| "database-contract-unmet";
|
|
|
|
/**
|
|
* A boot refusal whose remedy belongs to the deployment's supervisor.
|
|
* Only refusals that are *expected transients* under managed-cloud
|
|
* provisioning use this class; refusals that always indicate operator
|
|
* error (schema drift, a malformed DATABASE_URL) stay plain `Error`s.
|
|
*/
|
|
export class StartupRefusalError extends Error {
|
|
readonly kind: StartupRefusalKind;
|
|
|
|
constructor(kind: StartupRefusalKind, message: string) {
|
|
super(message);
|
|
this.name = "StartupRefusalError";
|
|
this.kind = kind;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Chooses the error class for a pending-migrations refusal. A database
|
|
* with zero applied migrations AND zero tables is not stale — it has
|
|
* never been migrated at all, which under a supervisor means "not yet"
|
|
* rather than "drifted". Any applied history, or any pre-existing
|
|
* tables beside an empty or wiped migration journal, makes pending
|
|
* migrations a drift signal that must keep reporting.
|
|
*/
|
|
export function migrationRefusalError(
|
|
state: { appliedMigrations: string[]; tableCount: number },
|
|
message: string,
|
|
): Error {
|
|
const neverMigrated = state.appliedMigrations.length === 0 && state.tableCount === 0;
|
|
return neverMigrated
|
|
? new StartupRefusalError("schema-not-yet-migrated", message)
|
|
: new Error(message);
|
|
}
|
|
|
|
/**
|
|
* Whether a startup failure should be captured to Sentry. Everything
|
|
* reports except a supervised-transient refusal in a managed-cloud
|
|
* deployment.
|
|
*/
|
|
export function shouldReportStartupFailure(
|
|
error: unknown,
|
|
env: NodeJS.ProcessEnv = process.env,
|
|
): boolean {
|
|
if (!(error instanceof StartupRefusalError)) return true;
|
|
const cloudOrigin = env.PAPERCLIP_CLOUD_API_ORIGIN?.trim();
|
|
return !cloudOrigin;
|
|
}
|