398 lines
20 KiB
Markdown
398 lines
20 KiB
Markdown
# GovOPlaN Release Console
|
|
|
|
The release console is a local operator tool for planning and executing
|
|
GovOPlaN releases. It belongs to the `govoplan` meta repository because it works
|
|
across all local checkouts, release scripts, module manifests, migration audits,
|
|
catalog files, Git state, and signing keys.
|
|
|
|
The current implementation has a read-only dashboard plus guarded local
|
|
candidate/publish actions:
|
|
|
|
- inspect repositories from `repositories.json`
|
|
- show dirty, ahead, behind, missing, no-HEAD, and tag state
|
|
- show local package catalog and keyring state
|
|
- optionally run release/dev migration audits
|
|
- propose next actions without executing them
|
|
- compare local catalog/keyring JSON with the published channel and public
|
|
keyring when online checks are enabled
|
|
- 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
|
|
- preview/apply/push reviewed catalog candidates behind explicit confirmations
|
|
|
|
Start it from the meta repository:
|
|
|
|
```sh
|
|
./.venv/bin/python tools/release/release-console.py
|
|
```
|
|
|
|
The server binds to `127.0.0.1` by default and prints a URL containing a local
|
|
API token. Open that URL in a browser on the same machine.
|
|
|
|
The web UI starts with a repository table. Each repository can be checked
|
|
independently and assigned its own target version. Repositories without version
|
|
metadata remain visible so they can be planned as initial releases. `Build Plan`
|
|
shows the dry-run commands for the selected rows, and `Generate Candidate`
|
|
creates a signed catalog candidate that advances only selected repositories that
|
|
already have a catalog entry.
|
|
|
|
`Build Plan` also returns structured release-gate findings for each selected
|
|
repository. The plan names the recommended next action and gives an explicit
|
|
remediation for source-version, lockfile, Core WebUI composition, Git state, and
|
|
worktree findings. A target version that has not yet been applied consistently
|
|
to the selected source tree is therefore explained before the source-tag
|
|
preflight rather than appearing only as an error after the operator tries to
|
|
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.
|
|
|
|
Creation requires a caller-generated `request_id`. Its SHA-256 fingerprint is
|
|
the private, workspace-scoped durable mapping to exactly one run; the raw
|
|
identifier is never persisted. Repeating the same identifier with the same
|
|
immutable inputs returns that run without rebuilding the plan, even if the
|
|
live dashboard has since drifted. Reusing it with different inputs fails
|
|
closed. The browser keeps an uncertain create identifier in session storage,
|
|
replays it after reload, and selects the known run returned by the server. A
|
|
successful create remains shown as saved if only the subsequent list refresh
|
|
fails.
|
|
|
|
Run records survive console restarts, but remain local operator state rather
|
|
than a signed release artifact or the system audit log. The default location is
|
|
`$XDG_STATE_HOME/govoplan/release-console/workspace-<sha256>/release-runs/`, or
|
|
`~/.local/state/...` when `XDG_STATE_HOME` is unset or relative. It is outside
|
|
the source checkout so filesystems without enforceable POSIX modes cannot
|
|
silently weaken the journal. Newly created state directories use mode `0700`
|
|
and records use `0600`. Writes use a cross-process lock, a same-directory
|
|
temporary file, `fsync`, atomic replacement, and directory/parent `fsync`.
|
|
Symbolic-link paths, untrusted owners or writable ancestry, 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 full resolved-workspace SHA-256 is part of both the private storage
|
|
namespace and immutable input snapshot. Every list, read, and state transition
|
|
checks it. An alternate workspace therefore cannot list or resume another
|
|
workspace's runs. This remains true for an embedding/test `run_state_root`
|
|
override: the server always appends
|
|
`workspace-<full-sha256>/release-runs/` rather than treating the override as a
|
|
shared record directory. A corrupt record from one workspace therefore cannot
|
|
leak even its identifier or an integrity error into another workspace.
|
|
|
|
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/reconciliation request identifiers are fingerprinted so delayed
|
|
repetitions remain idempotent for the run's lifetime. These fingerprints are
|
|
never evicted: the store fails closed before accepting more than 2,048 commands
|
|
or 2,048 attempts and asks the operator to create a fresh run. The display
|
|
record keeps at most 256 server-generated state events. 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.
|
|
|
|
Before a step can enter `running`, the store reserves the two command-ledger
|
|
slots needed for worst-case recovery. It also projects the serialized record
|
|
through start, finish/failure, resume, and the required terminal reconciliation
|
|
or read-only retry; the start is rejected unless every required atomic write
|
|
fits the record-size bound. An explicit resume consumes one slot and is
|
|
accepted only while a persisted attempt is actually running. A mutating attempt
|
|
always retains its final `effect_absent` or `effect_succeeded` slot;
|
|
`unresolved` may be recorded at most once for that attempt and only when an
|
|
additional ledger slot and serialized terminal-write capacity are available.
|
|
Read-only interruption and known failure retain the slot and byte capacity
|
|
needed to prepare a retry. Capacity exhaustion is therefore detected before
|
|
starting an effect rather than stranding an ambiguous attempt.
|
|
|
|
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 independently reconciles
|
|
local and remote state, selects `effect_absent`, `effect_succeeded`, or
|
|
`unresolved`, and types `RECONCILE`. `effect_absent` prepares a safe new
|
|
attempt, `effect_succeeded` advances the run without repeating the external
|
|
effect, and `unresolved` keeps the run blocked. Each outcome emits a bounded,
|
|
code-only state event. A known failed attempt can likewise be prepared for
|
|
retry. The UI keeps unavailable controls visible and disabled.
|
|
|
|
The browser likewise retains the request identifier for an uncertain
|
|
resume/retry/reconciliation response and replays it after reload. A successful
|
|
replay selects the returned run state. Transport and server failures retain the
|
|
identifier; a deterministic `4xx` rejection clears it so a stale command cannot
|
|
poison a later attempt.
|
|
|
|
The run API is covered by the same local console token middleware as every
|
|
other `/api/` route:
|
|
|
|
- `POST /api/release-runs` requires `request_id`, then idempotently rebuilds and
|
|
freezes a selective plan only when that creation is not already known.
|
|
- `GET /api/release-runs` lists bounded summaries ordered by verified
|
|
`updated_at`, then `created_at` and run identifier. Unreadable entries are
|
|
appended deterministically when room remains instead of displacing newer
|
|
verified runs.
|
|
- `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
|
|
read-only interrupted step for another attempt.
|
|
- `POST /api/release-runs/{run_id}/steps/{step_id}/reconcile` records a
|
|
confirmed observed outcome for an interrupted mutating step.
|
|
|
|
Run-storage errors are confined to the Durable Run State card; dashboard and
|
|
release-preview collection continue and show the bounded storage remediation.
|
|
|
|
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
|
|
blocker in both full and selective release plans. The console does not silently
|
|
omit a contract or turn these source errors into an HTTP 500.
|
|
|
|
The release-control area above the repository table is read-only and is meant
|
|
to become the central release cockpit. It shows:
|
|
|
|
- local and published channel health
|
|
- catalog/keyring drift
|
|
- signature and trusted-key status
|
|
- published module versions and refs
|
|
- local checkout version drift against the catalog
|
|
- catalog-declared interface compatibility
|
|
|
|
The target-version control can generate the next major, minor, or subversion
|
|
from the current base version. Manual target input accepts explicit versions
|
|
such as `0.2.0` or `0.2.0-alpha1`, but requires the first three version numbers
|
|
to move forward.
|
|
|
|
Plain repository pushes are separate from catalog publication. `Preview Push`
|
|
shows the selected repository push commands. `Push Selected` requires `PUSH` in
|
|
the repository push confirmation field.
|
|
|
|
The source release panel closes the former gap between a release plan and a
|
|
catalog candidate. `Preview Tag + Publish` is non-mutating. `Create Tags`
|
|
requires `TAG`; `Publish Tags` requires `PUBLISH` and atomically pushes the
|
|
selected branch and annotated tag. The gate requires an aligned target version,
|
|
a clean named branch with a HEAD, and a checkout that is not behind. Existing
|
|
local or remote tags must resolve to the selected HEAD and are never moved.
|
|
The local and remote annotated tag objects must also be identical, not merely
|
|
point at the same commit. Before any source tag is created, the console loads
|
|
the cross-repository module registry. This release gate also rejects every
|
|
user-facing workflow documentation topic that has no scope condition, or has
|
|
an alternative condition without `required_scopes` or `any_scopes`.
|
|
|
|
The catalog workflow panel can also operate on the same selected rows:
|
|
|
|
- `Generate` creates a signed candidate below `runtime/release-candidates/`.
|
|
- `Preview` validates a candidate and shows what would be copied into the
|
|
website repository.
|
|
- `Apply + Website Tag` requires `APPLY` in the confirmation field.
|
|
- `Push Website Release` requires `PUSH` in the confirmation field.
|
|
|
|
Source release tags belong to Core or module repositories. Website catalog
|
|
publication creates a separate catalog tag in the website repository; the UI
|
|
names these independently to avoid confusing the two immutable references.
|
|
|
|
Candidate signing is fail-closed: every Core and module source ref in the
|
|
complete post-update catalog must already have the requested annotated tag both
|
|
locally and on the configured source remote. Both tags must be the same
|
|
annotation object with version-aligned tagged package metadata. Selected tags
|
|
must additionally resolve to the selected clean, version-aligned HEAD, and the
|
|
signed `selected_units` record captures the peeled commit and tag-object
|
|
identifiers. Create and publish the source tags before using `Generate`.
|
|
|
|
Catalog publication repeats that check for selected units and also verifies
|
|
every Core and module source ref in the complete candidate, including entries
|
|
preserved from the previous catalog. Historical refs need not be at the current
|
|
worktree HEAD, but their local and remote annotated tags must match and their
|
|
tagged installable package and module-manifest metadata must declare the catalog
|
|
version. New selected releases additionally pass the stricter current-source
|
|
alignment gate, including public runtime version declarations. A candidate
|
|
cannot be applied or published while any referenced source tag is absent or
|
|
inconsistent; the preview and API response list each repository and missing or
|
|
invalid ref so the operator can repair the exact source releases first.
|
|
|
|
Read-only provenance checks derive a same-host HTTPS Git URL from conventional
|
|
SSH remotes when possible, with a non-interactive check of the configured remote
|
|
as fallback. Source tag creation and atomic publication always use the explicit
|
|
configured Git remote.
|
|
|
|
The default signing key is
|
|
`$HOME/.config/govoplan/release-keys/release-key-1.pem` when no signing key is
|
|
entered in the UI.
|
|
|
|
Generate a selective release plan from the terminal:
|
|
|
|
```sh
|
|
./.venv/bin/python tools/release/release-plan.py \
|
|
--repo govoplan-files \
|
|
--target-version 0.1.9 \
|
|
--channel stable \
|
|
--online
|
|
```
|
|
|
|
`--online` compares the local catalog/keyring with the published channel and
|
|
published keyring. Use `--remote-tags` only when the plan also needs to check
|
|
Git remotes for tag existence; that can be slower across the full repository
|
|
set.
|
|
|
|
Build a signed selective catalog candidate:
|
|
|
|
```sh
|
|
KEY_DIR="$HOME/.config/govoplan/release-keys"
|
|
|
|
./.venv/bin/python tools/release/release-catalog.py selective \
|
|
--repo-version govoplan-files=0.1.9 \
|
|
--channel stable \
|
|
--catalog-signing-key "release-key-1=$KEY_DIR/release-key-1.pem"
|
|
```
|
|
|
|
This writes candidate catalog/keyring files below `runtime/release-candidates/`,
|
|
generates the browsable module directory below `modules/`, validates the signed
|
|
candidate with the module installer validator, and reports whether the candidate
|
|
catalog/keyring match the currently published channel. It does not publish to
|
|
the website repository.
|
|
|
|
Regenerate the browsable module directory from an existing catalog/keyring:
|
|
|
|
```sh
|
|
./.venv/bin/python tools/release/release-catalog.py module-directory \
|
|
--catalog ../addideas-govoplan-website/public/catalogs/v1/channels/stable.json \
|
|
--keyring ../addideas-govoplan-website/public/catalogs/v1/keyring.json \
|
|
--output-dir runtime/module-directory-preview \
|
|
--channel stable
|
|
```
|
|
|
|
Preview publication of a reviewed candidate:
|
|
|
|
```sh
|
|
./.venv/bin/python tools/release/release-catalog.py publish-candidate \
|
|
--candidate-dir runtime/release-candidates/stable-YYYYMMDD-HHMMSS \
|
|
--channel stable
|
|
```
|
|
|
|
Apply the reviewed candidate into the website repository without pushing:
|
|
|
|
```sh
|
|
./.venv/bin/python tools/release/release-catalog.py publish-candidate \
|
|
--candidate-dir runtime/release-candidates/stable-YYYYMMDD-HHMMSS \
|
|
--channel stable \
|
|
--apply \
|
|
--commit \
|
|
--tag
|
|
```
|
|
|
|
Push is a separate explicit flag:
|
|
|
|
```sh
|
|
./.venv/bin/python tools/release/release-catalog.py publish-candidate \
|
|
--candidate-dir runtime/release-candidates/stable-YYYYMMDD-HHMMSS \
|
|
--channel stable \
|
|
--apply \
|
|
--commit \
|
|
--tag \
|
|
--push
|
|
```
|
|
|
|
Published channels are expected below the public catalog base URL:
|
|
|
|
- `https://govoplan.add-ideas.de/catalogs/v1/channels/stable.json`
|
|
- `https://govoplan.add-ideas.de/catalogs/v1/keyring.json`
|
|
|
|
The console treats channels as release artifacts. A package can advance without
|
|
forcing every repository to the same tag, but channel publication must preserve
|
|
the unchanged package versions, validate interface compatibility, sign the
|
|
updated catalog, and keep the published keyring healthy.
|
|
|
|
When a selected module exposes a WebUI package, its requested version must also
|
|
match Core's `webui/package.release.json` input and the resolved
|
|
`package-lock.release.json` entry. The source-tag preflight, selective plan, and
|
|
catalog-candidate writer all enforce this composition boundary. Pins for modules
|
|
that are not part of the selective release remain unchanged.
|
|
|
|
Release integration also enforces repository and composition version alignment
|
|
and generates a CycloneDX SBOM from the resolved Python environment and the
|
|
release WebUI lockfile. Catalog publication should attach that immutable SBOM
|
|
and its digest to the corresponding Core/composition release.
|
|
|
|
Addresses and Notifications have current WebUI source contributions but are
|
|
not part of the pinned v0.1.8 WebUI release composition because their v0.1.8
|
|
tags predate those packages. They re-enter the release composition only through
|
|
new, immutable module tags whose backend, manifest, frontend, and lock metadata
|
|
pass the alignment gate.
|
|
|
|
Candidate publication verifies signatures against the already published
|
|
keyring, not against keys supplied only by the candidate. A changed keyring
|
|
must have its canonical SHA-256 embedded in the signed catalog. The public
|
|
module-directory files are regenerated from that verified catalog and keyring
|
|
at publication time; candidate-supplied directory files are never copied as
|
|
authoritative provenance.
|
|
|
|
## Published Module Directory
|
|
|
|
The target release repository is an online, browsable module directory. The
|
|
catalog remains the machine-readable channel entry point, but the published
|
|
space should also expose a tree that operators and installations can inspect:
|
|
|
|
- `/catalogs/v1/channels/<channel>.json` describes the active channel state.
|
|
- `/catalogs/v1/keyring.json` publishes trusted release signing keys.
|
|
- `/catalogs/v1/modules/<module>/<version>/manifest.json` describes one
|
|
published module version, including repository refs, package refs, contracts,
|
|
compatibility windows, signatures, and available artifacts.
|
|
- `/catalogs/v1/modules/<module>/index.json` lists available versions for
|
|
one module.
|
|
- `/catalogs/v1/modules/index.json` lists all published modules.
|
|
|
|
Gitea tags/releases remain the source release anchors. The public GovOPlaN
|
|
catalog directory becomes the installation-facing release repository that points
|
|
to those anchors and carries structured compatibility information.
|
|
|
|
Selective catalog candidates now include this directory tree. Publishing a
|
|
candidate copies the channel catalog, keyring, and `modules/` tree into the
|
|
website repository together.
|
|
|
|
Selective catalog generation can add a module that is not yet represented in
|
|
the source channel. Initial entries are synthesized from the selected
|
|
repository's `[project]` metadata, `govoplan.modules` entry point, and runtime
|
|
`ModuleManifest`; there is no separate hand-maintained initial-entry list. The
|
|
release gate requires the entry point to resolve inside the selected checkout,
|
|
exact package/manifest/frontend version agreement, verified source-tag
|
|
provenance, a non-conflicting module id, and complete required dependency and
|
|
interface closure in the resulting candidate. Classification beyond the
|
|
manifest-derived `official` tag remains an explicit later catalog curation
|
|
step rather than an inferred business/service label.
|
|
|
|
## Direction
|
|
|
|
The console should grow in slices:
|
|
|
|
1. Read-only dashboard and next-action suggestions.
|
|
2. Release plan builder that writes explicit JSON plans.
|
|
3. Dry-run executor that shows exact commands and expected file changes.
|
|
4. Apply executor for commit, tag, push, artifact build, signing, and catalog
|
|
publication.
|
|
5. Compatibility planner for manifest contracts and version ranges.
|
|
6. Install/test workflow that validates a release from tags or catalog entries.
|
|
|
|
All mutation must stay explicit: plan first, dry run second, apply only after a
|
|
clear confirmation.
|