Skip to content

Serve immutable snapshots and reseal on SIGHUP (PE shape)#10

Merged
miharp merged 1 commit into
mainfrom
feat/reseal-trigger-and-snapshot
Jul 24, 2026
Merged

Serve immutable snapshots and reseal on SIGHUP (PE shape)#10
miharp merged 1 commit into
mainfrom
feat/reseal-trigger-and-snapshot

Conversation

@miharp

@miharp miharp commented Jul 24, 2026

Copy link
Copy Markdown
Owner

Adopts Puppet Enterprise's Code Manager shape for the two coupled problems in the publisher: nothing resealed after startup, and artifacts were streamed from live staging, so a fetch overlapping an r10k deploy could serve a half-written tree whose bytes no longer matched the advertised code_id.

Reading PE's own implementation (the .clj source ships in puppet-server-release.jar) showed both are one idea: after r10k finishes, Code Manager commits the staging tree into an immutable git snapshot and serves that, and the commit is an explicit post-deploy trigger, not a filesystem watch. This mirrors that, adapted to codavox observing rather than owning the deploy.

Snapshot (closes the live-staging race)

Reseal now materializes each current version as a deterministic .tar.gz under <state>/artifacts — temp file + rename, so it publishes atomically — and serving streams that file. Bytes and code_id can no longer drift: the bytes are a snapshot from when the tree was quiescent, not whatever staging holds at request time.

  • Superseded artifacts are reaped on the next reseal. Safe: an in-flight download holds an open descriptor, so unlinking the file it streams is fine on Unix.
  • Only the current version is kept on disk — same "compilers retain old versions themselves" policy as before.

Trigger (closes the "nothing reseals after startup" gap)

SIGHUP reseals the running publisher, wired to r10k's postrun:

# r10k.yaml
postrun: ['/bin/sh', '-c', 'systemctl reload codavox-publish']

The signal fires only after r10k returns, so the tree is quiescent and no reseal observes a half-written deploy. This is deliberately not a filesystem watch: a watch would have to reconstruct the "deploy finished" signal postrun already gives us, and codavox doesn't own the deploy, so the explicit trigger is both simpler and the correct coupling. It's PE's commit-content!-after-r10k, across a process boundary.

SIGTERM/interrupt now drain in-flight downloads via graceful shutdown (Server.Serve(ctx) replaces the blocking ListenAndServeTLS).

API / signature changes

  • NewStore(stagingDir, artifactDir) — the publisher now needs a writable data dir. The publish command derives it from --state; tests that run the real publisher pass --state <tempdir>.
  • --state now holds both the provenance log and materialized artifacts.

Tests

  • TestArtifactIsSnapshotNotLiveStaging — mutate staging after sealing, without a reseal; the served artifact still extracts to the sealed code_id and carries the snapshot's bytes, not the mid-deploy write. This is the whole point of the change.
  • TestResealReapsSupersededArtifact — a new code_id reaps the old artifact and materializes the new one.
  • TestSIGHUPTriggersReseal — deploy into a running publisher, SIGHUP, and a compiler converges to the new version with no restart.

Verified: go test -race ./..., golangci-lint 0 issues, gofmt/vet/markdownlint clean.

Docs: publishing.md — artifact endpoint, Resealing (SIGHUP + postrun), and the --state purpose.

🤖 Generated with Claude Code

The publisher only sealed at startup, so an r10k deploy went unseen until a
restart, and it streamed artifacts from the live staging directory — so a
fetch overlapping an r10k deploy could serve a half-written tree whose bytes no
longer matched the advertised code_id. The agent's verify-by-reseal caught it,
but by rejection every poll rather than convergence.

PE's Code Manager solves both with one idea: after r10k finishes it commits the
staging tree into an immutable git snapshot and serves that, never the live
working dir; the commit is an explicit post-deploy trigger, not a filesystem
watch. This adopts the same shape, adapted to codavox not owning the deploy.

Snapshot: Reseal now materializes each current version as a deterministic
.tar.gz under <state>/artifacts, written to a temp file and renamed into place,
and serving streams that file. Bytes and code_id can no longer drift, because
the bytes are a snapshot from when the tree was quiescent rather than whatever
staging holds at request time. Superseded artifacts are reaped on the next
reseal; an in-flight download holds an open descriptor, so unlinking the file
it streams is safe on Unix. Only the current version is kept on disk — the same
'compilers retain old versions themselves' policy as before.

Trigger: SIGHUP reseals the running publisher, wired to r10k's postrun hook
(systemctl reload). The signal fires only after r10k returns, so the tree is
quiescent and no reseal observes a half-written deploy. A watch mode would have
to reconstruct that 'deploy finished' signal postrun already gives us, and
codavox does not own the deploy, so the explicit trigger is both simpler and
the correct coupling. SIGTERM/interrupt now drain in-flight downloads via
graceful shutdown (Server.Serve(ctx) replaces the blocking ListenAndServeTLS).

NewStore takes an artifact directory; the publish command derives it from
--state. Tests that run the real publisher pass --state a temp dir, since the
publisher now needs a writable data directory.

Tests: artifact is the seal-time snapshot even after staging is mutated without
a reseal; superseded artifacts are reaped; SIGHUP makes a running publisher
advertise new code and a compiler converge without a restart.

Signed-off-by: Michael Harp <mike@mikeharp.com>
@miharp
miharp merged commit 37827f2 into main Jul 24, 2026
7 checks passed
@miharp
miharp deleted the feat/reseal-trigger-and-snapshot branch July 24, 2026 11:44
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