Static release target: StaticRelease binding, carry, atomic flip, control record v5 #2

Merged
eureka merged 6 commits from eureka-self/static-release into main 2026-11-13 16:50:20 +00:00
Owner

Follow-ups idunn-deploy-provenance:follow_up:chat-mind-20261113T125202436Z-t35 and chat-s6737-6737; cut_spec idunn-deploy-provenance:cut_spec:cut-static-release-forward.r1. Forward half of Cut A; the idunn rollback verb is the next cut.

  • [static_release] target declaration and StaticRelease workload binding (no unit, no service): a release tree sealed and installed by the existing machinery, a per-site config file carried as a copy at flip time (outside the sealed digest, sha recorded in the flip evidence), an atomic current flip in Starting behind the same brake authorization as a service. A failed flip before the rename ends FailedBeforeFencing with current untouched; after the rename nothing may abort (retry, last_error visible in idunn status).
  • Control record v5 (idunn.deployment_transaction.v5, new key 37 flip) with the v4 legacy layout, lift on read and history, and the boot migration. Forward-only: an Idunn that predates v5 cannot read a migrated store. Pinned bytes test updated deliberately; v4 fixture kept.
  • Docs: docs/deployment-authority.md, docs/deploy-provenance-target.md.

Tests: 14 static-release tests plus pinned/lift/migration tests (v4 store on a temp copy boots to v5; v5 untouched); full lib suite 555 passed / 107 failed, the 107 being the root-only tests that fail identically on base (uid 985 host). Soul: first pass found v5 uncommitted, a post-rename abort hole and surviving mutations; all fixed; delta pass accepted dc86c63; final round (be98521) is a test and docs only.

Not verified: root install and harden of a static release, real docker materialize for a static target, any run against a live control.cc. Known gap: unbounded retry after a flip intent on a permanent fault (follow-up: bounded retry with an operator-decides state). Deploy nothing.

🤖 Generated with Claude Code

Follow-ups `idunn-deploy-provenance:follow_up:chat-mind-20261113T125202436Z-t35` and `chat-s6737-6737`; cut_spec `idunn-deploy-provenance:cut_spec:cut-static-release-forward.r1`. Forward half of Cut A; the `idunn rollback` verb is the next cut. - `[static_release]` target declaration and `StaticRelease` workload binding (no unit, no service): a release tree sealed and installed by the existing machinery, a per-site config file carried as a copy at flip time (outside the sealed digest, sha recorded in the flip evidence), an atomic `current` flip in Starting behind the same brake authorization as a service. A failed flip before the rename ends FailedBeforeFencing with `current` untouched; after the rename nothing may abort (retry, `last_error` visible in `idunn status`). - Control record v5 (`idunn.deployment_transaction.v5`, new key 37 `flip`) with the v4 legacy layout, lift on read and history, and the boot migration. **Forward-only: an Idunn that predates v5 cannot read a migrated store.** Pinned bytes test updated deliberately; v4 fixture kept. - Docs: docs/deployment-authority.md, docs/deploy-provenance-target.md. Tests: 14 static-release tests plus pinned/lift/migration tests (v4 store on a temp copy boots to v5; v5 untouched); full lib suite 555 passed / 107 failed, the 107 being the root-only tests that fail identically on base (uid 985 host). Soul: first pass found v5 uncommitted, a post-rename abort hole and surviving mutations; all fixed; delta pass accepted dc86c63; final round (be98521) is a test and docs only. Not verified: root install and harden of a static release, real docker materialize for a static target, any run against a live `control.cc`. Known gap: unbounded retry after a flip intent on a permanent fault (follow-up: bounded retry with an operator-decides state). Deploy nothing. 🤖 Generated with [Claude Code](https://claude.com/claude-code)
A target may declare [static_release] artifact = "<id>" instead of
[service], and bind driver = "static-release" (release_root, current_link,
one carry source/destination). TargetDeclaration.service becomes Option with
a service() accessor; admission refuses both-or-neither, state/provides/
dependencies/conflicts/route/write lease on a static target, stop-then-start,
retain_releases < 2, a carry destination that is not a single leaf or collides
with an artifact destination, a carry source inside the release root, and a
current link overlapping the release root or an authority path.

Read-only audit of TransactionCompletion::Admitted / admitted_for readers
(cut spec step 2); a static Admitted transaction has no AdmittedGeneration:
- control_plane.rs:1781 (deadline_for_current_activity, Complete): reads the
  variant and drains_incumbent only. Tolerates.
- :1828 (carries_a_deadline): variant only. Tolerates.
- :2086 (is_terminal): variant plus post_commit_cleanup. Tolerates.
- :2550 (validate, Complete): requires Admitted and generation-<tx>; the
  generation record is never read. Tolerates; static gets its own branch.
- :5085 (derived_command_status): variant only. Tolerates.
- :6567 region (command admission; the admitted_for read is at :6587): passes
  an Option incumbent, None for a static target. Tolerates. Anchor drifted by
  ~20 lines; the code matches.
- :8371 (advance_committing): the only writer of Admitted with a generation;
  a static transaction never enters Committing. Unchanged.
Other admitted_for readers (3517 fenced-incumbent check, 4963 render, 5482
incident close, 9571 provider rehydrate, 9654/9712/9777 rebase, continuity,
take_target): all handle None; a static target has no fencing, no incident,
no capability to be a provider.

Fallout: runner_for/workload_for gain a StaticRelease arm (docker runner; the
systemd driver serves install only), and the service-only readers in host.rs,
drivers.rs and deployment_plan.rs go through service()?.

Tests: cargo test -p idunn --lib deployment::tests::static_ (6 passed).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
install_release and validate_installed_release read the release root through
WorkloadBinding::release_root(), so a static release installs through the
same copy, digest and harden path as a systemd one. StaticReleasePort
{ observe_current, carry_and_flip } with FilesystemStaticRelease: reads the
operator's site file (regular file, not a link, 1 MiB cap) and hashes it
before anything moves; writes <release>/<destination> by temp + rename at mode
0444 outside every artifact digest; then renames a fresh symlink over
current_link (a stale flip link a crash left is replaced; a real directory in
the link's place is never replaced). observe_current refuses a link that names
anything but a release under the release root.

Tests on a tempdir stand-in site (fake vite dist with .br/.gz twins, fake site
config): cargo test -p idunn --lib drivers::tests::static_release_ (5 passed).

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
A static release is the same DeploymentTransaction in three phases: Sealing,
Starting (carry and flip, behind the deployment brake) and Complete. It has no
Expected, activation, workload, warming, isolation, fencing, leasing, ready or
routing, and creates no AdmittedGeneration.

DeploymentTransaction gains key 37 `flip` (FlipEvidence: release, previous
release, carry source, carried sha256, time). Adding a key changes the bytes of
every record, and the control store refuses a record that does not re-encode to
what is stored, so this is schema idunn.deployment_transaction.v5. The v4
layout is kept as LegacyDeploymentTransactionV4 and lifted by the existing
mechanism (read_transaction_record, history decode, and the boot migration,
which rewrites v4 records in control.cc to v5). Migration is forward-only: an
Idunn that predates v5 cannot read a migrated control.cc.

Also: is_static() and brake_subject() (the brake names the binding's runtime id
and the sealed release, read by deployment_authorization, durable-authority
validation and status); validate_static; advance_sealing skips readiness_class
and expected_projection for static; advance_starting delegates to
advance_static (intent with the previous release read from current_link,
carry_and_flip then record, observe link == release, then Complete with
Admitted{generation-<tx>} and post_commit_cleanup(SkippedNoIncumbent, source
Pending)); a failure in Starting takes the existing pre-fencing abort;
Engine.static_release Arc<dyn StaticReleasePort>. Docs: a Static release
section in deployment-authority.md and a scope line in
deploy-provenance-target.md.

Tests: control_plane::tests::static_release_ (3), a pinned v4 record lifts and
re-encodes as the pinned v5, a v4 control store copy in a temp dir migrates to
v5 and an already-v5 store is untouched byte for byte, and the pinned-bytes
test now pins v5 with the v4 bytes kept as a legacy fixture. The test runner is
a stand-in over the real seal_artifact behind a cfg(test) Engine seam, because
DockerRunnerDriver workspace ownership requires root.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
d5ea779 named this work but did not contain it: the working-tree edits were never
staged. LegacyDeploymentTransactionV4, the v5 schema constant, the v4 lift in
read_transaction_record and history decode, the boot-migration case, and the
pinned v5 / legacy v4 tests.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
carry_and_flip returns FlipRefused only before the rename (the last fallible
step); the parent-directory sync after it is best-effort. Once a flip intent is
recorded, any error that is not FlipRefused leaves the transaction resumable
instead of taking the pre-fencing abort, and a resumed step that finds current
already naming the release records the carry already in it (new port method
carried_sha256) instead of flipping again. validate refuses a pre-fencing abort
beside a landed flip.

Tests: post-rename port fault and post-landing observe fault both end Admitted
with current on the new release; validation refuses FailedBeforeFencing with a
landed flip; a refused flip over a real directory writes no carry; a populated
v4 record lifts to the exact v5 field by field.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Release ids are per transaction, so redeploying an unchanged tree with a changed
site file yields a new release (the test asserts distinct ids, the first release
keeping its carry, current carrying the new file); previous_release_id can never
equal release_id and no refusal is needed. A flip whose current link was replaced
by a directory after the intent retries visibly: last_error is shown by idunn
status as 'waiting on', cancel is refused, and the docs name the by-hand fix.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
eureka merged commit 9f15cee11f into main 2026-11-13 16:50:20 +00:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
GameCult/Idunn!2
No description provided.