()
| 500 | |
| 501 | /** Hook entry point — called by ep.json on createServer. */ |
| 502 | export const expressCreateServer = async (): Promise<void> => { |
| 503 | detectedMethod = await detectInstallMethod({ |
| 504 | override: settings.updates.installMethod, |
| 505 | rootDir: settings.root, |
| 506 | }); |
| 507 | logger.info(`updater: install method = ${detectedMethod}, tier = ${settings.updates.tier}`); |
| 508 | |
| 509 | // Tier 2: if the previous boot left the state in pending-verification, arm |
| 510 | // the health-check timer (or force rollback when bootCount has climbed past |
| 511 | // the crash-loop threshold). This must run BEFORE polling starts so the |
| 512 | // rollback can fire even if the version checker is misconfigured. |
| 513 | const state = await getCurrentState(); |
| 514 | pendingVerification = checkPendingVerification(state, getRollbackDeps()); |
| 515 | |
| 516 | // Boot-time failure notification. If a previous run produced a failure |
| 517 | // outcome whose admin email we haven't already sent (lastFailureKey |
| 518 | // dedupe), fire it now. Covers: |
| 519 | // - health-check timeout rollback on the previous boot |
| 520 | // - crash-loop forced rollback (detected on a later boot) |
| 521 | // - preflight-failed where we never got to send (e.g. process kill) |
| 522 | // - rollback-failed terminal that the operator hasn't acknowledged |
| 523 | // Fire-and-forget — the rest of boot must proceed regardless. |
| 524 | const failureOutcome = state.lastResult?.outcome === 'rolled-back' ? 'rolled-back' |
| 525 | : state.lastResult?.outcome === 'rollback-failed' ? 'rollback-failed' |
| 526 | : state.lastResult?.outcome === 'preflight-failed' ? 'preflight-failed' |
| 527 | : null; |
| 528 | if (failureOutcome && state.lastResult) { |
| 529 | void notifyApplyFailure({ |
| 530 | outcome: failureOutcome, |
| 531 | targetTag: state.lastResult.targetTag, |
| 532 | reason: state.lastResult.reason ?? failureOutcome, |
| 533 | }); |
| 534 | } |
| 535 | |
| 536 | // Tier 3: instantiate the scheduler unless updates are entirely disabled. |
| 537 | // The runner is purely in-memory — the persisted state file is the source |
| 538 | // of truth for "is something scheduled." On `tier: "off"` we explicitly |
| 539 | // clear any previously-persisted scheduled state to idle so a stale |
| 540 | // schedule from a prior boot can't auto-fire after the operator opted |
| 541 | // out (Qodo #1). |
| 542 | if (settings.updates.tier === 'off') { |
| 543 | if (state.execution.status === 'scheduled') { |
| 544 | logger.info( |
| 545 | `updater: discarding pending Tier 3 schedule for ${state.execution.targetTag} ` + |
| 546 | `because updates.tier="off"`); |
| 547 | state.execution = {status: 'idle'}; |
| 548 | await saveState(stateFilePath(), state); |
| 549 | } |
| 550 | } else { |
| 551 | scheduler = createSchedulerRunner({ |
| 552 | now: () => new Date(), |
| 553 | setTimer: (cb, ms) => setTimeout(cb, ms), |
| 554 | clearTimer: clearTimeout, |
| 555 | triggerApply: schedulerTriggerApply, |
| 556 | }); |
| 557 | if (state.execution.status === 'scheduled') { |
| 558 | logger.info(`updater: rehydrating Tier 3 schedule for ${state.execution.targetTag} at ${state.execution.scheduledFor}`); |
| 559 | scheduler.arm({ |
nothing calls this directly
no test coverage detected