Skip to content

feat(api): port server-side rollback (#553) onto the ADR 0040 wire - #895

Open
elliotBraem wants to merge 1 commit into
mainfrom
feat/rollback-config-half-port
Open

elliotBraem wants to merge 1 commit into
mainfrom
feat/rollback-config-half-port

Conversation

@elliotBraem

Copy link
Copy Markdown
Contributor

Fly-hosting ticket 06 — port the console rollback config-half from 87d37e45 on feat/reconciler-v1 (#553).

Stacking note: this was branched stacked on PR #892 (fix/deploy-history-reader-wire) per the ticket, but #892 merged (squashed, byte-identical content) before this PR was opened — the stack collapsed and the branch was rebased onto main (the two #892 commits dropped as already-upstream). This PR is now targetable at main directly; retargeting happened because #892 merged, as the ticket planned.

What was ported vs re-implemented

Re-implemented (forced by the wire cutovers): the old rollback-service.ts read history through the pre-cutover helpers (historyUrlForKey, auditManifestKeyPrefix, parseAuditManifest, registryNamespaceForAccount) that no longer exist. The service now derives every key through keyPathOf (runtimeConfig + deployRecord), reads through registryClientShape from everything-dev/registry, and stamps the audit trail as a DeployRecordSchema record at deploys/<ts>.json — so a rollback appears as a /deploys row with the rolledBackFrom badge (the old branch's manifests/ audit-manifest payload would have been invisible on the new wire). The per-check vocabulary is remote everywhere (ADR 0040 §5), not slot.

Ported as-was (decision structure and refusals): unconfigured → refuse; wrong namespace → refuse (own-namespace-only; registry-wired ownership is ticket 09); unknown block height → refuse; identical-to-live → refuse (unreachable live tolerated — the snapshot publish is idempotent); SRI failure → refuse with per-remote checks; pre-cutover snapshot → refuse as deliberately unrestorable (ADR 0040 §4, wording updated; no server-side force — the CLI keeps --force); signing failure → refuse with the cause. Refusals are results (status: "refused"), not thrown errors.

Reused unchanged (shared machinery, new exports only): verifyRollbackSnapshot / buildRollbackPayload from everything-dev/deploy/rollback and submitRegistryWrite / SigningStrategy from everything-dev/auth/near-signer already existed in packages/everything-dev — this PR only adds the subpath exports (+ tsdown entries) so the api consumes exactly what the CLI's rollback flow uses. No verification is re-implemented in the api shell. The atomic write carries the config + deploy record in one __fastdata_kv call.

Also carried over: hosting.rollback { account, gateway } variables + optional ROLLBACK_PRIVATE_KEY secret, POST /hosting/rollback behind session/api-key auth (Effect-native handler, tagged reads), the /deploys rollback button (deploys-rollback-<i>) + ConfirmDialog + toasts + list invalidation, DOM lib for api (+ .github/templates/api — near-signer's near-cli source needs JsonWebKey), near-kit in api deps (catalog).

Changed from the source branch: the changeset is patch on everything-dev only — the old one named api/ui (minor), which the current release rules reject (private workspaces). The rolledBackFrom badge test id moved to deploys-rolled-back-<i> so the ticket's deploys-rollback-<i> belongs to the button (the badge took that name in #892).

Signing recommendation

ROLLBACK_PRIVATE_KEY should be replaced by custody publish (publish.auth = "custody", OutLayer) as the default path, but not in this PR:

  • Today the secret means a long-lived NEAR private key sitting in the runtime's env — the exact standing-credential shape the custody wallet exists to retire (TEE-held keys, one per user + network, fetched per login). Rollback is a low-frequency, high-blast-radius write; it is precisely the action that should ride the custody coordinator rather than a key an operator pasted into an env var.
  • The api shell already has the seams to consume it: submitRegistryWrite accepts a strategy, and the custody path in the deploy train composes mode: "custody" writes with an idempotency key — wiring rollback through OutLayer's /wallet/v1/call is a contained change to buildFastKvRollbackDeps plus a config leg.
  • Kept working either way: the secret path is the fallback in this PR (refuses when absent, never misconfigures silently). Recommendation: flip the default to custody when ticket 09 wires namespace ownership, and deprecate the env secret then — one migration, one PR.

Test evidence

  • pnpm --filter api test: 7 files / 57 tests green, including new rollback-service.test.ts (11: every refusal path incl. verification-read failure, stamping, atomic-write shape via key assertions, signing failure, unreachable-live tolerance) and rollback-route.test.ts (2: 401, not-configured) — in-memory pglite via the committed .env.test.
  • pnpm run typecheck green (all 13 workspaces); pnpm run lint green (no errors; pre-existing warnings untouched).
  • Browser spec extended in tests/regression/browser/specs/deploys.spec.ts: a CI-runnable own-namespace leg (button → ConfirmDialog → outcome toast, trace) and the full publish v2 → roll back → config pointer shows rolledBackFrom leg, gated behind ROLLBACK_SPEC_ACCOUNT/ROLLBACK_SPEC_GATEWAY (needs a disposable namespace + the stack configured with hosting.rollback + ROLLBACK_PRIVATE_KEY); not booted locally — owner/CI step per the ticket. Both legs end with a Playwright trace artifact.
  • Not in scope here: the instance half (restart on the rolled-back pin + drift marker) is ticket 08; registry-wired ownership is ticket 09.

Post-merge cleanup for the owner (recorded in the ticket): origin/feat/reconciler-v1, local rb-553, and feat/rollback-config-half can be deleted once this merges.

The console rollback config-half (fly-hosting ticket 06), re-implemented
from 87d37e4 on feat/reconciler-v1 against the post-cutover registry
wire rather than merged (slot→remote, digest cutover, wire cutover all
postdate it):

- everything-dev gains two user-facing subpaths: ./deploy/rollback
  (verifyRollbackSnapshot pinned-remote SRI gate + buildRollbackPayload
  rolledBackFrom stamp — the CLI's own machinery, unchanged) and
  ./auth/near-signer (submitRegistryWrite + signing strategies).
- api/src/lib/rollback-service.ts: fetch the target snapshot from the
  runtimeConfig history (public FastKV reads through registryClientShape),
  compare against live, verify every pinned remote, stamp rolledBackFrom,
  then one atomic __fastdata_kv write carrying the config + a
  DeployRecordSchema audit record (deploys/<ts>.json — the rollback shows
  up as a /deploys row). Keys derive through keyPathOf; the pre-cutover
  audit-manifest read is gone (the record's registryUrl derives locally).
- Refuse-not-half-execute: unknown block height, identical-to-live, SRI
  failure (per-remote checks returned), pre-cutover snapshot (deliberately
  unrestorable per ADR 0040 §4 — no server-side force, the CLI keeps
  --force), wrong namespace, unconfigured, verification read failure.
- Own namespace only: hosting.rollback { account, gateway } variables +
  optional ROLLBACK_PRIVATE_KEY secret; absent → the route refuses with
  status: refused, not an error. Registry-wired ownership is ticket 09.
- POST /hosting/rollback behind session/api-key auth, Effect-native
  handler; /deploys own-namespace rows gain a rollback button
  (deploys-rollback-<i>) with ConfirmDialog, refusal/success toasts, list
  invalidation; the rolledBackFrom badge moves to deploys-rolled-back-<i>.
- api (+ template) tsconfig gains the DOM lib (near-signer's near-cli
  source needs JsonWebKey); api gains near-kit (catalog).
- Tests: service 11/11 (every refusal path incl. verification-read
  failure, stamping, atomic-write shape, signing failure, unreachable-live
  tolerance), route 2/2 (401 + not-configured); browser spec extended
  with the own-namespace outcome leg plus the owner-gated
  publish-v2→rollback→rolledBackFrom leg (traces).

The instance half (restart on the rolled-back pin + drift marker) is
ticket 08's reconciler work, not here.

This branch has not been deployed

No deployments
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant