Commit Graph
12 Commits
Author SHA1 Message Date
Juan DenisandClaude Fable 5 4af3df1136 fix(nginx): never-cache the SPA entrypoint; warn on stale :3847 panel vhosts
A test box's panel domain kept serving a Jun 25 bundle after the Jul 3
update — the domain's custom vhost still proxied the retired :3847
frontend container, and the old bundle's calls to deleted legacy
marketplace endpoints read as 'Failed to load extensions'. It looked
exactly like a browser/CDN cache problem (it wasn't — cf-cache-status
was DYNAMIC; the origin itself was stale).

- both stock confs: expires -1 on the SPA entrypoint (emits
  Cache-Control: no-cache WITHOUT discarding server-level security
  headers, unlike add_header-in-location) — a cached index.html
  references hashed assets that vanish on the next blue/green switch
- update.sh refresh_config now runs report_stale_panel_vhosts: warns,
  naming the file, when any enabled vhost still proxies :3847;
  observation-only per R1, silent on a missing sites-enabled dir,
  added to the fresh-box loop per policy
- proving tests: entrypoint no-cache asserted for both confs; stale
  vhost flagged (and clean one not); missing-dir silence (42 passed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 23:48:15 -04:00
Juan DenisandClaude Fable 5 7345807b12 test(scripts): chain multi-statement if-subshell assertions (no vacuous passes)
bash suppresses set -e inside any tested-context command list — an
if-condition subshell, or the left side of ||, even with set -Eeuo
pipefail re-armed inside — so multi-statement test bodies only gated
on their LAST statement. Today's cmp-less EL9 CI run proved it: a
byte-identity test passed while its cmp assertions exited 127.

28 blocks across test_update/test_install/test_lib/test_agent_install
converted to explicit && chains so every assertion gates the result.
No test weakened; no vacuous pass was hiding a real script bug (each
newly-gated statement was verified to genuinely succeed).

Verified: Git Bash 171 passed / 0 failed; rockylinux:9 all five
suites 177p/0f; opensuse/leap:15.5 148p/0f (pre-existing python3/tar
skips).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 21:53:14 -04:00
Juan DenisandClaude Fable 5 502ed19ae0 test(scripts): fresh-box observation loop, failing-stub library, exit-status assertions
The July 2 outage function had a unit test — with a populated fixture
only. This encodes the missing net:

- fresh-minimal-box fixture loop: every observation/discovery/snapshot/
  report function in update.sh (17) and install.sh (16) must survive a
  zero-app box under set -Eeuo pipefail; policy comment requires new
  functions to be appended
- scripts/test/stubs.sh: shared failing-stub factories (systemctl exit
  5, docker fail/empty, curl exit 22, GitHub rate-limit JSON)
- assertion sweep: 43 bare capture-then-compare sites now include exit
  status in the assertion
- best-effort contract test: the run block's real APP_BASELINE ->
  report chain exits 0 on a fresh box AND on a genuine regression
- fix(update): the loop immediately caught report_failure's trailing
  '[ -n $UPDATE_LOG ] && printf' returning 1 when UPDATE_LOG is empty
  (same species as the outage) — converted to if, added to the loop

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 18:30:26 -04:00
Juan DenisandClaude Fable 5 4af9e72ad9 fix(update): harden failure semantics — prune, capture purity, rollback re-entry (U1-U14)
- find-based backup pruning; empty/missing BACKUP_DIR is a no-op (U1)
- download_release progress to stderr; stdout is the tarball path only (U2)
- guard bare systemctl stop/start/daemon-reload in forward+rollback paths (U3,U4,U8,U13)
- ROLLING_BACK=1 inside rollback(); never runs twice via the EXIT trap (U5)
- npm preflight for source updates; guarded frontend build pipeline (U6)
- empty-ORIG_ARGS re-exec guard, cleanup_on_exit return 0, df -Pk,
  SIGPIPE guards, cd leaks subshelled, local typo (U7,U9-U12,U14)
- 14 new proving assertions in test_update.sh (35 passed)

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 16:50:53 -04:00
Juan Denis a90c56428f Make update checks tolerant and add empty-dir test
Allow failures when querying GitHub release tag (prevent pipeline from aborting) and add a unit test ensuring discover_app_upstreams tolerates an empty app-locations directory.

Changes:
- scripts/update.sh: make the curl|grep pipeline resilient by appending `|| true` so the command substitution won't fail the script when the GitHub API call returns nothing.
- scripts/test/test_update.sh: add test T16b to verify discover_app_upstreams does not abort under `set -euo pipefail` when the app-locations directory is empty.

These changes prevent hard failures during release detection and add coverage for empty-directory handling.
2026-07-02 15:08:34 -04:00
Juan DenisandClaude Fable 5 1c911b50df feat(extensions): installed plugins survive panel updates (#48)
A panel update deploys a fresh clone/tarball preserving only .env and
the DB, wiping app/plugins/<slug> + src/plugins/<slug> for any
URL/registry/upload-installed extension — the surviving InstalledPlugin
row then flipped to error at next boot. Two-layer fix:

- update.sh: preserve_installed_plugins carries user-installed plugin
  dirs from the old tree into the new one (skipping anything the new
  tree already ships and __pycache__), before the frontend build so
  carried UIs recompile. Covered by harness test T18.
- boot repair pass (plugin_service.repair_missing_plugins, runs before
  load_all_plugins): builtin slugs reinstall from builtin-extensions/,
  url installs re-download from source_url (force + no hot-load so the
  loader registers once), sourceless uploads get a clear re-upload
  error. install_from_url/path grew force/hot_load plumbing.

4 new repair tests (16 total in the pipeline suite); update.sh harness
20 passed.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-02 02:03:33 -04:00
Juan DenisandClaude Opus 4.8 5b0c57ee0a feat(update): verify hosted apps stay reachable across the update
Turns the zero-downtime promise into an actual check. The updater now
snapshots which managed-app upstreams are reachable through nginx
*before* the blue/green switch, re-probes after the graceful reload, and
reports any app that was serving and has since gone dark.

- discover_app_upstreams scans /etc/nginx/serverkit-locations/*.conf for
  the per-app proxy_pass upstreams (host:port).
- probe_app_upstreams / snapshot_app_reachability record up/down (any
  HTTP status = up; a refused/timed-out connection = down).
- report_app_uptime_regressions diffs before-vs-after and warns for any
  up->down app, ignoring apps that were already down. Best-effort: it
  never fails an otherwise-healthy update, just surfaces the regression.
- New "Verifying App Uptime" phase wired into the main flow; baseline
  captured before the backend stop.

Tests: T16 (upstream discovery), T17 (regression detection both ways).
19 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 02:26:44 -04:00
Juan DenisandClaude Opus 4.8 ca66629780 feat(deploy): serve panel frontend from host nginx, retire the container
The panel is now fully off Docker: the React SPA is served as static
files directly by host nginx from frontend/dist, eliminating the
serverkit-frontend container and the double nginx hop (host nginx ->
container nginx -> dist). Docker is now used ONLY for managed app
workloads.

- nginx sites (serverkit.conf + serverkit-insecure.conf): `location /`
  now roots at /opt/serverkit/frontend/dist with a try_files SPA
  fallback and a long-cache /assets/ block, instead of proxying
  127.0.0.1:3847. Root tracks the /opt/serverkit symlink so blue/green
  updates swap assets atomically.
- install.sh: launch_services no longer builds/starts a frontend
  container; adds apply_frontend_root (rewrites the dist root for a
  custom SERVERKIT_DIR) and selinux_label_frontend_dist (httpd_sys_-
  content_t so SELinux hosts can read the bundle from /opt).
- update.sh: refresh_config carries the same root rewrite across
  upgrades; selinux_label_dist relabels the switched-in slot; dropped
  the `docker compose up -d --force-recreate frontend` from both the
  forward and rollback paths.
- docker-compose.yml: frontend service moved behind a `legacy-frontend`
  profile (opt-in escape hatch); default `up -d` starts nothing.
- docs/DEPLOYMENT.md: documents the off-Docker panel model.

Tests: T14 (refresh_config rewrites dist root for custom install dir),
T15 (shipped nginx configs serve static, never proxy :3847). 16 passed.

Note: live nginx -t / SELinux file-serving not validated on a RHEL box
from this dev host; logic is best-effort guarded.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 02:24:22 -04:00
Juan DenisandClaude Opus 4.8 4da684be29 fix(update): zero-downtime nginx on panel update (reload, never stop)
Host nginx is the front door for every managed app (it reverse-proxies
their containers via /etc/nginx/serverkit-locations/*.conf). The updater
used to `systemctl stop nginx` for the whole blue/green switch window,
which blacked out every unrelated hosted site for 10-60s during a panel
update. The slot switch only ever changes nginx config, never anything
nginx serves from the slot, so a stop was never required.

Add reload_nginx_graceful(): validate config (nginx -t) then
`systemctl reload nginx` - keeps listening sockets and in-flight
connections alive, so apps never blink. Starts nginx if it happens to be
down. Wired into both the forward update flow and rollback(); only the
panel backend is now cycled. Dropped the stray frontend-only
`docker compose up -d backend` line.

Tests: T11 reloads-not-stops a running nginx, T12 starts a stopped
nginx, T13 guards against `systemctl stop nginx` reappearing. 13 passed.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-30 02:16:24 -04:00
Juan DenisandClaude Opus 4.8 aa6ee2737f feat(update): Phase 2 — version-aware, dry-run-honest, rollback-safe updater
Updater robustness from the hardening plan §5:

- Version comparison before updating (G3): is_already_current compares the
  local VERSION to the target release tag (release mode) or the local checkout
  HEAD to the remote branch HEAD via `git ls-remote` (branch/main mode). When
  current, both the hybrid and docker flows print "Already up to date" and exit
  0 — unless --force. Offline/indeterminate always proceeds rather than wrongly
  blocking.

- Dry-run fidelity: new run_in_dir reports the working directory a command
  would run in, replacing the `run_or_dry cd X && run_or_dry compose ...` chain
  that printed compose commands with no cwd context.

- Rollback hardening: migrate_database now runs against the NEW slot's DB copy
  (slot-absolute path) instead of the /opt/serverkit symlink that still
  resolves to the live old slot — so a rollback hands the old code a
  byte-for-byte-unchanged database (PostgreSQL/shared DBs migrate directly, as
  before). rollback_docker guards `git reset` on a non-empty SHA with a clear
  warning. Both rollback paths now run a post-rollback health check and say
  plainly when even the rollback is unhealthy.

- Telemetry/logging: halt() always prints the full log path; both summaries
  gained an "old → new", mode (release/branch/main, +docker), and duration
  line, plus the log path.

Tests: test_update.sh gains versions_equal, is_already_current force/offline
short-circuits, and a flask-stub proof that migrate_database targets the new
slot's DB (T10). 10 passed / 2 skipped locally; shellcheck error+warning clean.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-25 00:31:03 -04:00
Juan DenisandClaude Opus 4.8 33c2483f7e feat(update): self-updating bootstrap + concurrent-run lock
Two reliability gaps remained after the failure cascade:

1. Stale-updater trap. The updater that runs is whatever is installed, so an
   old update.sh fails in ways the current code already handles and can't fix
   itself — exactly what left a box stuck. maybe_reexec_latest_updater fetches
   the newest update.sh for the target ref and re-execs into it (guarded by
   SERVERKIT_UPDATER_REEXECED, skipped offline/dry-run/opt-out, only after a
   bash -n check). From this version on, 'serverkit update' is reliable no
   matter how stale the box is.

2. No concurrency guard. acquire_update_lock takes an flock so two updates
   can't clobber the same blue/green slot or race docker compose; the lock
   auto-releases on exit and degrades gracefully where flock/the path is
   unavailable.

Both are covered by scripts/test/test_update.sh (guard no-ops; lock
refuses a held lock, succeeds when free).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:52:09 -04:00
Juan DenisandClaude Opus 4.8 2549d4bbc7 feat(update): failure cascade + make the updater unit-testable
Harden update.sh against the class of failure that left a box silently stuck
on the old version, and make it testable without a real deploy.

Robustness (cascade):
- L5 loud failures: `set -Eeuo pipefail` (the -E/errtrace makes the ERR trap
  fire inside functions too) + a report_failure ERR trap that names the phase,
  exit code, line and command, plus always-on logging to
  /var/log/serverkit/update-*.log. No more silent drop-to-prompt.
- L4 docker-path rollback: snapshot the previous git SHA, :rollback image tags
  and dist.rollback before mutating; if the new backend is not healthy within
  90s, revert to the snapshot and halt (was: warn and continue).
- L6 self-heal: pin COMPOSE_PROJECT_NAME (kills the symlink-rename warning) and
  reclaim a stale inactive blue/green slot left by a prior failed hybrid run.

Testability:
- Parameterize the system dirs (SERVERKIT_NGINX_DIR / _LETSENCRYPT_DIR /
  _SYSTEMD_DIR / _CONFIG_DIR) and add SERVERKIT_NO_LOG, so refresh_config can
  run against fixtures instead of the host /etc.
- Make update.sh source-able via a BASH_SOURCE guard so functions can be
  exercised directly.
- scripts/test/test_update.sh: plain-bash unit tests (no deps, no server) that
  reproduce the missing-serverkit.conf regression and cover the
  detection/slot/report logic.
- .github/workflows/scripts-ci.yml: bash -n + shellcheck + the unit tests on
  changes to scripts/**, install.sh, serverkit.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-24 23:34:52 -04:00