Compass unified release lane (release-please + folded image publishing)
Status: Draft
Linear:
- TBD (spawning agent stamps the issue at PR time)
Design record. Citations name paths in
compass= RigelBuild/compass (this repo, main at authoring, except where a citation names the unmerged PR #711 workspace). Line numbers drift as code evolves.Supersedes (partially):
compass-release-bundling.md— Fork 3 (two-lane split, §192-235), Fork 4 (v*-tag controls, §275-315), OQ-3 (binary-only trigger breadth, §481-491), and OQ-7’s bump mechanism clause (“manual now, release-please at GA”, §499-503). Every OTHER ruling of that record stays frozen and live — notably OQ-7’s pre-GA0.MINOR.PATCHscheme,v1.0.0at GA, the ONE-whole-product-version attach-check architecture, and the post-1.0 MAJOR rule (§495-516). It also proposes retiring.github/workflows/publish-agent-image.yml, whose design recordcompass-agent-image-publishis compass-managed’s zone — the zone owner has ACCEPTED the retire and will write their own superseded-by amendment (see §Cross-lane coordination).
Problem / Intent
Section titled “Problem / Intent”Matt ruled two changes to the frozen release-bundling design (2026-08-25):
- Cadence = release-please, NOW. One whole-product semver, cut by a
standing release-please “Release PR” that accumulates merged conventional
commits; merging it (Matt’s act) cuts
vX.Y.Zwith a batched conventional-commit CHANGELOG — the Claude-Code/OMP shape. This overrides OQ-7’s “manual tag push now, automation at GA” staging and replaces the Fork 3(c) two-lane split (per-pushbuild-<sha12>prerelease + manualv*semver) with ONE lane cutting real releases. - A release builds/publishes the IMAGE, not a digest pointer.
vX.Y.Zmust exist as a pullableghcr.io/rigelbuild/compass-agent:vX.Y.Ztag alongside the four Go binaries — one version across the whole product. Realization: fold image publishing into the unified release lane and retire the separatepublish-agent-image.ymlFILE while relocating its DUTY.
This record designs that unified lane and the release-please integration for a
go+nix+bun monorepo, under the hard constraint that per-push :git-<sha12> +
:latest image publishing is PRESERVED (verified consumers below).
Approach
Section titled “Approach”A1 — one workflow, three duties, two triggers
Section titled “A1 — one workflow, three duties, two triggers”ONE workflow file (.github/workflows/release.yml, replacing both the PR #711
draft and publish-agent-image.yml) carries three jobs on TWO triggers —
never release-only, per the agent-image zone owner’s steering:
release-pr(trigger: everypush: branches: [main], unfiltered) — runsgoogleapis/release-please-actionauthenticated via a scoped GitHub App token (§Release-PR CI credential), creating/updating the standing Release PR. When the Release PR merges, the same action run on that merge commit creates thevX.Y.Ztag + GitHub Release and emitsreleases_created: true+tag_nameas job outputs. Permissions (job-level):contents: write+pull-requests: write, nothing else.publish-image(trigger: the samepush: main, self-gated by an in-job changed-paths check) — the RELOCATED per-push duty: builds the image spec and publishes:git-<sha12>+:latestviaagent-image/publish.shVERBATIM (no args = the default two-tag set,publish.sh:48-56), then runs the two-copy coherence verify copied frompublish-agent-image.yml:181-212. Permissions:contents: read+packages: write. Concurrency grouppublish-agent-image,queue: max+cancel-in-progress: false.release-assets+release-image(gated onneeds.release-pr.outputs.releases_created == 'true') — build the four Go binaries stamped with the released version, upload assets to the release-please-created Release, and mint:vX.Y.Zon GHCR by digest re-tag (§A4).release-assetsrunscontents: writeonly;release-imagerunscontents: read+packages: writeonly.
Why same-workflow output gating for the release jobs: Releases and tags
created by the workflow token do NOT trigger other workflows (GitHub’s
recursion guard), so an on: push: tags: ['v*'] or on: release follow-up
workflow would silently never fire. Gating release-assets/release-image on
the release-please action’s outputs inside the same run sidesteps that entirely
— no follow-up event, no credential needed for the DOWNSTREAM half. (The
UPSTREAM half — CI on the release-please-opened Release PR — is a separate
concern the recursion guard ALSO bites; it is solved by a scoped GitHub App
token, §Release-PR CI credential + §A5, not by this gating.)
Why an in-job path gate for publish-image, not on.push.paths: the
release-pr job must see EVERY main push (a paths filter would drop commits
from the changelog and stall the Release PR), and on.push.paths is
workflow-level. The per-push image job therefore self-gates with a push-event
before/after changed-path check over the closure set
publish-agent-image.yml:49-64 names (agent-image/**,
packages/compass-agent/**, package.json, bun.lock, the workflow file
retargeted to release.yml, tools/toolchain/versions/bun.nix), defined ONCE
as a workflow-level env and shared with T3’s resolver cross-check. This is a
git-diff over the push range, NOT ci.yml’s moon-affected query (ci.yml:1814,
a related but distinct technique), and it MUST force-publish on the
workflow_dispatch path (dispatch has no push range to diff, and §A4’s
remediation depends on dispatch always publishing).
A2 — release-please strategy + the version source of truth
Section titled “A2 — release-please strategy + the version source of truth”release-please runs release-type: simple (root component, no monorepo
manifest fan-out): the product versions as ONE unit — eight go/cmd/* binaries
carry an identical var version = "0.1.0" fallback stamped at build via
-ldflags "-X main.version=<v>" (e.g. go/cmd/compass-stack/main.go:37-40),
and compass-stack feeds it to Deps.ExpectedVersion (main.go:310) so
client/server are architecturally same-version (OQ-7’s frozen clause, intact).
The bumped source of truth is a root version.txt (the simple strategy’s
native target), plus .release-please-manifest.json +
release-please-config.json. Build-time flow of the ONE version:
- Release builds read it:
-X main.version=$(cat version.txt)— the exactldflagsmechanism PR #711’s build step already uses (ws-release-rail/.github/workflows/release.yml:167), with the cleanX.Y.Z(no+g<sha>suffix; the release tag IS the identity). - The image tag uses the same value:
:v$(cat version.txt)at the release sha equalstag_name. - Dev/bundle builds keep stamping locally (
app-bundle/build.sh’s<version>+g<shortsha>shape, now readingversion.txtas the base instead of the hardcoded0.1.0). - The eight Go
var versionfallback constants are NOT bumped by release-please (kept out of the blast radius; a stale fallback is only ever visible in an unstampedgo run). Annotating them withx-release-please-versiongeneric-updater markers is a non-load-bearing option (OQ-N2).
A3 — what a release contains
Section titled “A3 — what a release contains”Salvaged from PR #711 (the superseded implementation — reference, not kept
as-is): the nix langs→PATH bootstrap, the CGO-free 4-binary build
(compass/compass-server/compass-runner linux-amd64 + compass
darwin-arm64, -trimpath), SHA256SUMS, and the release-notes generator
(tools/release-notes/index.ts — pure assemble() core, GHCR image identity,
nix-outputs manifest, bun:test). Dropped from #711: the build-<sha12>
prerelease identity, the create-or-update prerelease step, and the
image-recorded-absent degradation as a steady state (a semver release
hard-requires its :vX.Y.Z image; §A4). The release body = release-please’s
conventional-commit CHANGELOG section + the generator’s asset/image/nix-outputs
appendix (gh release edit-appended after upload).
A4 — the :vX.Y.Z image: digest re-tag, never a second build
Section titled “A4 — the :vX.Y.Z image: digest re-tag, never a second build”The image build is the heavy ~90m nix closure; the design MUST NOT build it
twice. The release sha is the Release PR’s merge commit, which touches only
version.txt + CHANGELOG.md + manifest — none of which is in the image
closure path set — so the image content at the release sha is byte-identical
to the image already published for the last closure-affecting main sha.
Chosen: re-tag the newest already-published image at-or-before the release
sha. The release-image job (fetch-depth: 0):
- Resolves the source tag by walking first-parent ancestors of the release
sha, newest-first, and taking the first whose
ghcr.io/rigelbuild/compass-agent:git-<sha12>resolves on GHCR (skopeo inspect). That tag’s tree provably contains every closure change at-or-before it, so it is byte-identical to the release sha’s image content — WITHOUT a second identity key. (A path-basedgit log ... -- <paths>resolver was rejected: the per-push lane keys its tag on the PUSH HEAD sha, not the last closure-touching commit, so a multi-commit push landing a closure change below its tip would make a path resolver assert a tag that was never published — a wedged release the dispatch remediation cannot satisfy.) - Hard-fails with a remediation pointer if no ancestor tag resolves within a bounded walk (workflow_dispatch the per-push publish on that sha, then re-run). A missing source image NEVER silently triggers a rebuild here.
skopeo copys that tag’s digest-resolved manifest to:vX.Y.Z— a cheap registry-side manifest write, no nix build at all.- Verifies
:vX.Y.Zresolves and shares the source’s config digest (the same verify shape as the two-copy check,publish-agent-image.yml:204-211).
The job runs inside the SAME publish-agent-image concurrency group as the
per-push job under queue: max (NOT the default single pending slot), so a
:vX.Y.Z mint serializes behind an in-flight :latest move WITHOUT being
cancelled by a later per-push entrant claiming the one pending slot — a
non-superseding release mint must never be silently dropped. :latest stays
owned and moved EXCLUSIVELY by the per-push job — the release lane never
touches it (the frozen Fork 3 rationale for dropping latest from the release
invocation, compass-release-bundling.md:220-232, carries over unchanged).
Rebuild-at-release (running publish.sh git-<sha12> v<X.Y.Z> on the release
sha) was weighed and rejected: it costs the full closure build for a
guaranteed-identical artifact, doubles the cache-miss surface, and — because
publish.sh builds before tagging — would put a second builder in a lane the
frozen record deliberately kept single-builder.
A5 — controls on the new human act (Fork-4-equivalent)
Section titled “A5 — controls on the new human act (Fork-4-equivalent)”Fork 4’s hard controls guarded the old human act (pushing a v* tag). The
human act is now MERGING the Release PR; the controls re-express as:
- The
v*tag ruleset stays, allowlist repointed. Fork 4’s GitHub tag ruleset restrictingv*creation is kept but now permits ONLY the scoped GitHub App identity (release-please is the only tag minter) with repo-admin bypass for break-glass — so a leaked or over-scoped credential still cannot mint a release-triggering tag, and neither can a human by habit. - Main-only by construction + guard. The release jobs run only off the
push: mainevent (if: github.ref == 'refs/heads/main', thepublish-agent-image.yml:87guard, kept on every job), and the tag release-please cuts names the Release PR’s merge commit ON main — ancestry is by construction, not a post-hocmerge-basecheck. - The merge gate IS the review gate. The Release PR is an ordinary PR through branch protection: required checks + Matt’s merge. Its CI runs because release-please opens it via a scoped GitHub App token (§Release-PR CI credential — a bare workflow-token PR gets no CI under the recursion guard); the token is installation-scoped and per-run, NOT a standing PAT.
- Releases serialize without dropping. The image jobs share the
publish-agent-imagegroup underqueue: max+cancel-in-progress: false(§A4), so a release cut is never half-superseded mid-upload nor silently cancelled by a later per-push entrant.
A6 — security posture (copied verbatim where reused)
Section titled “A6 — security posture (copied verbatim where reused)”Wherever the unified lane reuses the frozen lanes’ steps, the posture copies VERBATIM:
cachix/install-nix-actionwith the two caches named inline inextra_nix_config— NOTaccept-flake-config(publish-agent-image.yml:103-115/ #711release.yml:84-96— the attacker-substituter rationale travels with the block).- NO
pull_requesttrigger, ever — fork-PR code never runs with a write token. - The main-ref dispatch guard on every job (
publish-agent-image.yml:84-87). REGISTRY_AUTH_FILEpinning + GHCR login via env-passed actor (publish-agent-image.yml:150-173).- The skopeo bootstrap from
tools/toolchain/skopeo-nix2container-env.nix(publish-agent-image.yml:117-148). - Least privilege PER JOB:
packages: writeexists ONLY on the two image jobs;contents: writeONLY on the release-please + asset-upload jobs; the ci.yml gate keepscontents: read.
Cross-lane coordination (resolved, accepted by owner)
Section titled “Cross-lane coordination (resolved, accepted by owner)”publish-agent-image.yml and its record
docs/designs/infra/ci/compass-agent-image-publish/ are compass-managed’s
zone. The zone owner has ACCEPTED the fold-and-retire (steering relayed
2026-08-25): they review this record’s PR and write their own superseded-by
amendment on their record pointing at this one as part of landing it. The
retire itself (deleting the workflow file) lands in the implementation PR with
their review; this record only carries the proposal + the acceptance.
Hard constraint — per-push publishing preserved (the load-bearing fold rule)
Section titled “Hard constraint — per-push publishing preserved (the load-bearing fold rule)”“Retire the old lane” = retire the FILE, RELOCATE the DUTY. Per-push
:git-<sha12> + :latest publishing continues on every qualifying main push
because verified consumers break otherwise:
.github/workflows/ci.yml:1831-1832— the dogfood e2e legpodman pull ghcr.io/rigelbuild/compass-agent:lateston non-image-affecting PRs (freshness consumer;:latestis Matt-ruled always-fresh,ci.yml:1810-1812).app-bundle/SMOKE.md:59-62— the native app’scompass-stackpulls:latestfrom GHCR at first run (DL-112: the bundle ships NO image).docs/designs/infra/ci/compass-agent-image-publish/design.md:33-43—:git-<short-sha>is IMMUTABLE and is “the pin compass-stack bakes into the native app binary”.
release-please only acts at release time, so the unified lane’s publish-image
job is what keeps these consumers fed between releases.
Alternatives considered
Section titled “Alternatives considered”Cadence (Matt ruled: release-please now)
Section titled “Cadence (Matt ruled: release-please now)”- release-please NOW (chosen, Matt 2026-08-25): batched conventional-commit changelog, one human act (merge the Release PR), the bump derived from commit types the repo already writes. Brings OQ-7’s planned GA end-state forward instead of building the interim twice.
- Per-push auto-patch (every main push bumps PATCH): release noise — a
release per commit is the
build-<sha12>prerelease lane wearing semver clothes; no batching, no human gate, and PATCH stops meaning anything. - The deferred two-lane split (frozen Fork 3(c):
build-<sha12>prerelease per push + manualv*semver): builds a prerelease rail that release-please obsoletes at GA anyway, and keeps the tag-push human act Fork 4 had to harden with a ruleset + ancestry guard. Superseded.
:vX.Y.Z image realization
Section titled “:vX.Y.Z image realization”- Digest re-tag of the per-push image (chosen, §A4): one builder, cheap
manifest copy, provable coherence with the
:git-<sha12>pin. - Rebuild at the release sha: ~90m closure rebuild for a byte-identical artifact; a second builder in a deliberately single-builder lane. Rejected.
Workflow topology
Section titled “Workflow topology”- One workflow, jobs gated on release-please outputs (chosen, §A1):
avoids the
GITHUB_TOKEN-created-tag/release-doesn’t-trigger-workflows trap for the DOWNSTREAM release jobs with no extra credential (the App token, below, is a separate concern — CI on the Release PR, not event propagation). - Two workflows (release-pr + on-release build): would need the release EVENT to fire a second workflow; same-run output gating removes that need entirely, so this buys nothing. Rejected.
- Fold into ci.yml: rejected for the same reasons both frozen lanes
document in their headers (least privilege, concurrency polarity, hot-path
latency;
publish-agent-image.yml:3-28).
Release-PR CI credential (Matt ruled: GitHub App token, 2026-08-25)
Section titled “Release-PR CI credential (Matt ruled: GitHub App token, 2026-08-25)”The design-critic caught that a GITHUB_TOKEN-opened Release PR gets no
pull_request CI (GitHub’s recursion guard), so branch protection cannot gate
it. Three options were put to Matt:
- GitHub App installation token (chosen): release-please authenticates with a scoped (contents + pull-requests), installation-scoped, per-run App token, so the Release PR receives CI and the merge gate is real. A standing App to provision (T6), but the token itself is short-lived and narrowly scoped — materially safer than the standing fine-grained PAT Fork 4 rejected.
- Don’t require CI on the Release PR (gate on merge alone): the PR diff is
machine-generated (
version.txt+CHANGELOG+ manifest) and its tree already passed full CI when the underlying commits merged. Credential-free, but rests the gate entirely on the human merge. Not chosen. actions: write+workflow_dispatchci.yml on the release branch: keepsGITHUB_TOKENbut a dispatched run does not report as the PR’spull_requestrequired checks, so it may not satisfy branch protection cleanly. Weakest; not chosen.
Global Constraints
Section titled “Global Constraints”- ONE version. A release stamps one
vX.Y.Zfromversion.txtacross all four released binaries (-X main.version) AND the image tag; the attach-check (go/cmd/compass-stack/main.go:38-40,310) enforces same-version client/server at runtime (OQ-7 clause, intact). - PRESERVE per-push publishing.
:git-<sha12>+:latestpublish on every closure-affecting main push, exactly as today. Consumers:ci.yml:1831-1832(dogfood:latestpull),app-bundle/SMOKE.md:59-62(first-run:latestpull, DL-112), the baked:git-<sha>binary pin (compass-agent-image-publish/design.md:33-43). Never release-only. - The fold regresses NOTHING (zone-owner conditions, all four carried):
- (a) image publishes SERIALIZE — concurrency group
publish-agent-imageunderqueue: max+cancel-in-progress: false(publish-agent-image.yml:76-78, extended withqueue: max); an in-flight:latestmove is never half-superseded, and the non-superseding:vX.Y.Zrelease mint shares the group WITHOUT being cancelled by a later per-push entrant claiming the single default pending slot (barecancel-in-progress: falsealone would drop it); - (b) least privilege
contents: read+packages: writeon image jobs, NOpull_requesttrigger, main-ref guard (publish-agent-image.yml:68-70,87); - (c)
:git-<sha>immutability = the config-digest refuse-overwrite guard (publish.shguard_immutable) AND the two-copy:git/:latestconfig-digest coherence verify (publish-agent-image.yml:181-212) — both carried; - (d) off the hot path — no release/publish job is a required check; a publish flake never reds the merge gate.
- (a) image publishes SERIALIZE — concurrency group
- Reuse
agent-image/publish.shverbatim — the positional-tag seam (publish.sh:48-56) is the only push mechanism; no new one. - Security posture verbatim where copied: caches named inline in
extra_nix_config(neveraccept-flake-config); nopull_requesttrigger; main-ref guard; skopeo from the pinned helper; pinnedREGISTRY_AUTH_FILE. - Public image, public releases. The image stays public (Matt-ruled);
release assets stay anonymously downloadable (the config-publish consumer
is anonymous,
compass-release-bundling.md:264-267). - Least privilege per job.
packages: writeONLY onpublish-image+release-image;contents: writeONLY onrelease-pr+release-assets; ci.yml’s gate stayscontents: read. - Pinned toolchains from nix, never
setup-go— thelangsbootstrap (#711release.yml:98-117) is the only PATH mechanism.
T1 — release-please configuration
Section titled “T1 — release-please configuration”Add release-please-config.json (release-type: simple, root component,
include-component-in-tag: false so tags are bare vX.Y.Z,
bump-minor-pre-major: true + bump-patch-for-minor-pre-major: true for the
OQ-N1 pre-1.0 damping Matt ruled), .release-please-manifest.json seeded at
the current 0.1.0, and version.txt at 0.1.0. Conventional-commit
subjects are already repo convention (rule://commit-conventions), so no
commit-style migration is needed.
- Interfaces: produces
version.txt(consumed by T3’s build step andapp-bundle/build.sh),release-please-config.json+.release-please-manifest.json(consumed by T2’s action).
T2 — the unified workflow: release-pr job + per-push publish-image job
Section titled “T2 — the unified workflow: release-pr job + per-push publish-image job”Rewrite .github/workflows/release.yml (superseding the PR #711 draft):
trigger on: push: branches: [main] (NO paths filter — release-pr must see
every commit) + workflow_dispatch; workflow-level permissions: {} with
per-job grants.
release-pr:googleapis/release-please-action(SHA-pinned) authenticated viaactions/create-github-app-token(App id + private key from secrets) — NOTGITHUB_TOKEN(§A5); job permissionscontents: write+pull-requests: write; outputsreleases_created,tag_name,sha.publish-image: job permissionscontents: read+packages: write; concurrency grouppublish-agent-imageunderqueue: max+cancel-in-progress: false;if: github.ref == 'refs/heads/main'; in-job changed-path gate over the closure set. The closure path set is defined ONCE (a workflow-levelenv) and consumed by BOTH this gate and T3’s resolver cross-check, with the old self-reference (publish-agent-image.yml) retargeted torelease.yml. NOTE the gate is a push-event before/after diff, NOT ci.yml’s moon-affected query (ci.yml:1814— a related but distinct technique), and it MUST force-publish on theworkflow_dispatchpath (dispatch has no paths to diff; the §A4 remediation depends on dispatch always publishing). Other steps copied verbatim frompublish-agent-image.yml(nix install w/ inline caches, skopeo bootstrap, auth-file pin, GHCR login,./publish.shno-args, tag-resolve + coherence verify).- Interfaces: consumes
agent-image/publish.sh(unchanged),tools/toolchain/skopeo-nix2container-env.nix; produces GHCR:git-<sha12>+:latest.
T3 — release jobs: binaries + assets, image re-tag
Section titled “T3 — release jobs: binaries + assets, image re-tag”Same workflow, two jobs gated on
needs.release-pr.outputs.releases_created == 'true':
release-assets(contents: write): checkout at the release sha; nixlangsbootstrap; build the 4-binary set with-X main.version=$(cat version.txt)and asset namescompass_vX.Y.Z_linux-amd64etc. (#711’s build step re-based frombuild-<sha12>to the semver tag);SHA256SUMS; runtools/release-notes/index.tsfor the asset/image/nix-outputs appendix;gh release upload "$TAG"+ append the appendix to the release-please-generated notes viagh release edit --notes-file.release-image(contents: read+packages: write; concurrency grouppublish-agent-imageunderqueue: max+cancel-in-progress: false;fetch-depth: 0): resolve the source tag by walking first-parent ancestors newest-first and taking the first whose:git-<sha12>resolves on GHCR (§A4), hard-fail w/ remediation if none in a bounded walk,skopeo copyto:vX.Y.Z, verify config-digest equality with the source tag. Never touches:latest; never builds.- Interfaces: consumes
version.txt,tools/release-notes/index.ts(assemble()signature unchanged;taginput nowvX.Y.Z), release-please outputs; produces GHCR:vX.Y.Z.
T4 — retire publish-agent-image.yml (cross-lane, owner-accepted)
Section titled “T4 — retire publish-agent-image.yml (cross-lane, owner-accepted)”Two-PR staging (the same-PR precondition was self-contradictory: the new
workflow has no pull_request trigger, so publish-image cannot produce a
green per-push run inside the PR that would delete the old file). Instead:
- Land the unified
release.ymlFIRST (T2/T3). The oldpublish-agent-image.ymlstays live in parallel — this overlap is SAFE: both share thepublish-agent-imageconcurrency group (groups span workflows repo-wide, so they serialize, never double-run) andpublish.shis idempotent on a matching digest (guard_immutable,publish.sh:95-124), so a redundant publish is a no-op, not a conflict. - Observe ONE green per-push
publish-imagerun on a real closure-affecting main push (proves the relocated duty works end-to-end). - THEN delete
publish-agent-image.ymlin a follow-up PR, reviewed by compass-managed (the zone owner), who write the superseded-by amendment on their record.
- Interfaces: removes
.github/workflows/publish-agent-image.yml; consumes compass-managed’s PR review + their record amendment.
T5 — tag ruleset repoint + local-stamp retarget + release-notes retarget
Section titled “T5 — tag ruleset repoint + local-stamp retarget + release-notes retarget”Update the Fork 4 v* tag ruleset allowlist to permit the App-token path
(repo-admin bypass retained — release-please is the only tag minter). Retarget
app-bundle/build.sh:42’s hardcoded 0.1.0+g<sha> base to read version.txt
(the §A2 promise, previously unowned by any task — after the first real bump a
hardcoded base would silently report stale, and the SMOKE version-containment
assert (build.sh:98-99) would keep passing against the wrong base). Adjust
tools/release-notes/index.ts edge for the semver tag shape and the
required-image posture (a release with no resolvable image is a FAILURE, not a
degradation — flip the null-image arm’s use at release time; the pure
assemble() core keeps the nullable type for the dry-run path). Update
docs/architecture/build-and-ci.md’s lane description.
- Interfaces: GitHub ruleset (settings surface, Matt applies — no IaC rail
for repo rulesets today, OQ-N3);
app-bundle/build.sh(base version fromversion.txt);tools/release-notes/index.ts(classifyImageResult+main()edge).
T6 — provision the scoped GitHub App (release-please identity)
Section titled “T6 — provision the scoped GitHub App (release-please identity)”Create (or reuse an org) GitHub App with contents: write +
pull_requests: write on this repo, install it, and store its app-id +
private key as Actions secrets consumed by T2’s release-pr via
actions/create-github-app-token. Settings/secret surface today (OQ-N3);
a prerequisite for release-pr to open a CI-gated Release PR.
- Interfaces: produces the App-token secrets consumed by T2; no code artifact.
Ledger delta (applied same-PR by the spawning agent)
Section titled “Ledger delta (applied same-PR by the spawning agent)”In docs/designs/DECISIONS.md (§Infrastructure & CI), append (next-free id
verified at PR time = DL-298; main advanced through 283-297 while drafting):
- DL-298: One whole-product release lane driven by release-please
(
release-type: simple, rootversion.txtsource of truth): a standing Release PR batches conventional commits; merging it (Matt’s act) cutsvX.Y.Z+ CHANGELOG, builds the 4-binary asset set stamped fromversion.txt, and mints the:vX.Y.Zimage tag — superseding the release-bundling record’s Fork 3 two-lane split, Fork 4v*-controls, OQ-3 binary-only trigger, and OQ-7’s manual-now/automation-at-GA staging (its other clauses stay Active) →platform/compass-unified-release-lane.md. - DL-299: The
:vX.Y.Zagent image is minted by digest re-tag (skopeo copy) of the already-published per-push:git-<sha12>artifact — never a second closure build;:lateststays owned exclusively by the per-push publish job; the mint shares thepublish-agent-imageconcurrency group →platform/compass-unified-release-lane.md. - DL-300:
publish-agent-image.ymlretires as a FILE with its DUTY relocated into the unified lane’s per-pushpublish-imagejob (verbatimpublish.sh, same paths gate, same serialize/immutability/coherence/ least-privilege/off-hot-path posture) — accepted by the agent-image zone owner, who amend their record with a superseded-by pointer →platform/compass-unified-release-lane.md. - DL-301: release-please authenticates with a scoped GitHub App
installation token (contents + pull-requests, installation-scoped, per-run)
so the Release PR receives
pull_requestCI and branch protection is a real merge gate — a bareGITHUB_TOKEN-opened PR gets none under the recursion guard (Matt-ruled 2026-08-25) →platform/compass-unified-release-lane.md.
Flips (the bundling record’s decisions live in its prose, not as existing DL rows — none found for Fork 3/OQ-3/OQ-7 in the ledger at authoring — so the flip lands as record-header edits, not row-status flips):
docs/designs/platform/compass-release-bundling.mdStatus:line gains: “Fork 3, Fork 4, OQ-3, and OQ-7’s bump-mechanism clause superseded bycompass-unified-release-lane.md(Matt, 2026-08-25); all other rulings remain Active.”docs/designs/infra/ci/compass-agent-image-publish/design.mdStatus:amendment is written by compass-managed (their zone, accepted).
- T1 — release-please config + manifest +
version.txtseed - T2 — unified
release.yml:release-prjob + per-pushpublish-imagejob (posture verbatim) - T3 — release-gated
release-assets+release-image(digest re-tag) jobs - T4 — retire
publish-agent-image.yml(two-PR staging; compass-managed review + amendment) - T5 —
v*ruleset repoint +app-bundle/build.shversion.txt retarget + release-notes retarget + build-and-ci.md update - T6 — provision the scoped GitHub App + store credentials as Actions
secrets (prerequisite for T2’s
release-pr; OQ-N3) - Ledger delta: DL-298/299/300/301 + bundling-record
Status:amendment (spawning agent, same PR)
Open Questions
Section titled “Open Questions”- Release-PR CI credential — DECIDED (Matt, 2026-08-25): GitHub App
installation token. A
GITHUB_TOKEN-opened Release PR gets nopull_requestCI (recursion guard), hollowing the merge gate; release-please authenticates with a scoped App token instead so CI fires and branch protection gates the merge (§A1, §A5(3), DL-301, T6). - OQ-N1 — DECIDED (Matt, 2026-08-25): standard pre-1.0 damping. Under the
intact
0.MINOR.PATCHscheme (OQ-7):feat:→ MINOR,fix:→ PATCH, breaking-change footer → MINOR (not 1.0.0) while 0.x (bump-minor-pre-major+bump-patch-for-minor-pre-major);v1.0.0is a deliberate manualrelease-as: 1.0.0at GA. Configured in T1. - OQ-N2 (non-load-bearing): stamp the eight
var versionfallbacks? Whether release-please’s generic updater should also bump thevar version = "0.1.0"constants acrossgo/cmd/*(viax-release-please-versionannotations) so unstampedgo runbuilds report the current version. Cosmetic; recommendation: no (keep the blast radius atversion.txt), revisit if a stale fallback ever confuses a triage. - OQ-N3 (non-load-bearing, process): manual settings surfaces (tag ruleset + GitHub App). Repo rulesets and the release-please GitHub App have no IaC rail in this repo today; T5/T6 hand Matt the exact ruleset edit and App provisioning as runbook steps (rule://no-human-clicks escape hatch). Accept, or scope a follow-up to bring both under Pulumi’s github provider?
- OQ-N4 (non-load-bearing): release cadence floor/ceiling. ASSUMED: merge-when-Matt-chooses — the Release PR accumulates until Matt merges it, releases happen exactly then. A cadence floor/ceiling (auto-merge after N days, or a staleness reminder) is an additive follow-up, NOT part of this lane’s contract — the mechanism is identical under any answer, so this defers cleanly and the merge ratifies the assumption.
- Cross-lane retire — RESOLVED, accepted by owner. compass-managed keep their record and write the superseded-by amendment themselves while reviewing/landing this PR (steering 2026-08-25). Recorded here for the review trail, not as an open fork.