feat(release): persist resumable run state

This commit is contained in:
2026-07-22 18:30:40 +02:00
parent bd715a8473
commit d0dc916837
5 changed files with 2082 additions and 5 deletions

View File

@@ -18,6 +18,7 @@ candidate/publish actions:
- configure target versions per release unit in the web UI
- select the repositories that should advance through checkboxes
- generate dry-run selective release plans for independently versioned packages
- freeze a selective plan as a durable, resumable local release run
- create annotated source release tags for selected clean, version-aligned
repositories and publish each branch/tag pair atomically
- generate signed catalog candidates for the selected release units
@@ -49,6 +50,70 @@ tag. `source_preflight_ready` means that the plan-visible source gates pass; the
non-mutating `Preview Tag + Publish` remains mandatory for remote, manifest, and
immutable-tag checks.
## Durable release runs
The **Durable Run State** card turns the current repository/version selection
into a versioned local run record. The server rebuilds the selective plan and
requires the plan to resolve exactly the requested repositories and target
versions; the browser cannot submit or replace the plan snapshot. The input and
plan are then immutable and covered by a canonical SHA-256 integrity digest.
The complete record also has a checksum so a valid-looking manual edit to its
mutable state fails closed. File permissions remain the authority boundary;
these digests detect accidental or manual corruption, not an attacker who can
replace the private record and recompute its checksums. Changing a target,
channel, or gate input requires a new run.
Run records live below `runtime/release-runs/`. They survive console restarts,
but remain local operator state rather than a signed release artifact or the
system audit log. The directory is restricted to mode `0700` and records to
`0600`. Writes use a cross-process lock, a same-directory temporary file,
`fsync`, atomic replacement, and directory `fsync`. Symbolic-link storage paths,
overly broad record modes, malformed schemas, unknown fields, invalid state
combinations, oversized files, and digest mismatches fail closed. A bad record
is neither rewritten nor automatically quarantined.
The default store belongs to the selected workspace: the console uses that
workspace's meta checkout when it is available, or a stable workspace-digest
namespace below this checkout's runtime directory otherwise. The same digest is
part of the immutable input snapshot. An alternate workspace therefore cannot
silently list or resume another workspace's runs. `run_state_root` remains an
explicit embedding/test override.
Each frozen plan step has an explicit `pending`, `running`, `succeeded`,
`failed`, or `interrupted` state. Plan order remains a prerequisite: a later
step is unavailable until earlier steps have succeeded. Exact attempt and
resume/retry request identifiers are fingerprinted so repeated state commands
are idempotent. The record keeps at most 256 server-generated state events and
128 request fingerprints. Events contain only an enum event type, timestamp,
step identifier, and bounded result code—never commands, process output,
confirmation text, credentials, bearer tokens, or signing material.
An explicit resume after a process restart converts every persisted `running`
step to `interrupted`; it never guesses whether an external effect happened.
An interrupted read-only step can be prepared for retry. An interrupted
mutating step remains unavailable until the operator has reconciled local and
remote state and explicitly records that reconciliation when preparing the
retry. A known failed attempt can likewise be prepared for retry. The UI keeps
the retry control visible and explains why it is disabled when retry is unsafe.
The run API is covered by the same local console token middleware as every
other `/api/` route:
- `POST /api/release-runs` rebuilds and freezes a selective plan.
- `GET /api/release-runs` lists bounded summaries; unreadable entries are
reported as unavailable instead of being silently omitted.
- `GET /api/release-runs/{run_id}` reads and verifies one exact record.
- `POST /api/release-runs/{run_id}/resume` records explicit recovery.
- `POST /api/release-runs/{run_id}/steps/{step_id}/retry` prepares a failed or
safely reconciled interrupted step for another attempt.
This foundation tracks state only. It deliberately does not run a plan step or
infer success from a button click. Existing preview and executor controls stay
separate and continue to require `COMMIT`, `TAG`, `PUBLISH`, `APPLY`, or `PUSH`
at their existing narrow boundaries. Wiring confirmed executors to claim and
finish run steps, plus package/install verification, remains follow-up work;
until then the console must not present a run as execution evidence.
Dashboard collection is also fail closed. Unreadable Core version metadata or a
malformed module contract is returned as a bounded `collection_errors` entry
with a remediation, marks the dashboard blocked, and becomes a structured