Update system hardening: breaking-release gate + release-mode rollback + health budgets + success archival

Four fixes building on the prior upgrade-path work. All observed on
marcelle across today's v2.9.2 → v2.9.5 cycles and addressed here.

- Fix 1 (breaking-release gate). upgrade-check.sh now parses the first
  line of each Gitea release body for `BREAKING: <reason>` and threads
  `breaking`/`breakingReason` through status.json into the API status
  response. Admin UI renders a red Alert with a typed-tag confirmation
  input and gates the Start Upgrade button. auto-upgrade.service.ts
  refuses to apply breaking releases, logging a skip and holding off
  until the operator confirms manually.

- Fix 2 (release-mode rollback). print_rollback_help and the --rollback
  flow both used `git checkout`, which silently fails in release
  installs (no .git). Added INSTALL_MODE branches: release mode
  downloads the prior tarball from Gitea using a new VERSION.rollback
  marker seeded at Phase 3 start. Source mode retains the existing
  git-based flow.

- Fix 3 (Phase 7 health budgets). admin verify_service_health budget
  30s → 90s (matches the admin container's start_period from commit
  47704667). Gancio + MkDocs switched from one-shot to the existing
  verify_service_health retry wrapper. Cuts the cry-wolf
  "services may still be starting" warning from every upgrade result.

- Fix 4 (symmetric success archival). Bash archive_failure_to_history
  already logs failures on exit; added a matching archive_success_to_
  history called after write_result on the success path. API-side
  archiveResult now dedupes on completedAt so double-recording (bash
  + post-restart handler) can't land twice in history.json.

Release the bundle as v2.9.6.

Bunker Admin
This commit is contained in:
2026-04-15 16:57:13 -06:00
parent 47704667b1
commit ac901c9e53
6 changed files with 234 additions and 75 deletions

View File

@@ -36,6 +36,8 @@ export interface UpgradeStatus {
date: string;
author: string;
}>;
breaking?: boolean;
breakingReason?: string;
checkedAt: string;
error: string | null;
}
@@ -206,10 +208,16 @@ function clearStaleProgress(): void {
}
}
/** Archive a completed upgrade result to the persistent history file. */
/** Archive a completed upgrade result to the persistent history file.
* Dedupes on completedAt so the bash-side success archival (upgrade.sh
* archive_success_to_history) and this API-side call can't double-record. */
function archiveResult(result: UpgradeResult): void {
try {
const history = readJsonFile<UpgradeResult[]>(HISTORY_FILE) || [];
if (history[0]?.completedAt === result.completedAt) {
logger.info('Skipping archive — most recent history entry has same completedAt');
return;
}
history.unshift(result);
// Trim to max entries
if (history.length > MAX_HISTORY_ENTRIES) {

View File

@@ -122,6 +122,17 @@ class AutoUpgradeService {
return;
}
// Refuse to auto-apply releases flagged `BREAKING:` in their Gitea body.
// The admin UI gate ensures manual confirmation — auto-upgrade holds off
// and keeps checking, so the block clears once the operator upgrades.
if (status.breaking) {
logger.warn(
`Auto-upgrade: refusing to apply breaking release (${status.remoteCommit}): ${status.breakingReason || '(no reason given)'}. Manual confirmation required via admin UI.`,
);
upgradeService.clearTriggeredBy();
return;
}
logger.info(`Auto-upgrade: ${status.commitsBehind} commits behind, triggering upgrade`);
// Read settings for pullServices and registry options