feat(preview): add the PR-preview deploy workflow (SEA-2027) - #365
Open
rigel-mintaka wants to merge 4 commits into
Open
feat(preview): add the PR-preview deploy workflow (SEA-2027)#365rigel-mintaka wants to merge 4 commits into
rigel-mintaka wants to merge 4 commits into
Conversation
Deploy a compass PR's full stack to the isolated `preview` env on mattfw, selected by a single-holder `preview` GitHub label, served tailnet-gated, with a GitHub Deployment as the PR-visible state surface (Record B-preview P1, orion docs/designs/platform/compass-pr-preview/design.md). - .github/workflows/compass-preview-deploy.yml: a SEPARATE least-privilege workflow (the publish-agent-image.yml precedent), never a required check. Triggers on pull_request [labeled, synchronize, unlabeled, closed]; pull-requests:write + deployments:write + contents:read confined here; concurrency group compass-preview-deploy, cancel-in-progress:false. The fork guard is two-layer: on:pull_request (not _target) so no secret ever reaches fork code, plus a same-repo head.repo==github.repository gate on the deploy job; a fork claim routes to a strip+comment reject job that never displaces or deploys. Thin body — all logic in the bun/TS tool. - tools/compass-preview-deploy/: the bun/TS deploy tool (construction/ execution split). Pure decide() label-lifecycle state machine (claim/displace+sticky/release+inactive, fork-claim rejection, displaced- loser release race-safety); runOnce() dispatch driving the Deployment record (in_progress -> success/failure, environment_url, inactive-on- release) and the SEA-2014 preview-link payload; the mattfw deploy sequence (checkout, compass-preview.service restart, SEA-2011 compass-ui build against preview's :50161 door with a reviewer bearer minted via IssueToken, tailscale serve at one stable root URL — never funnel). - Deploy reach: the GH-hosted runner joins the tailnet ephemerally as a tagged ACL-scoped node, then ssh's mattfw; the OAuth secret is fork-guarded. Needs an orion-side tailscale ACL tag + ssh grant (flagged, not built here). Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
… (SEA-2027)
The single-holder release lifecycle was dead in the untested real-I/O seam:
- createDeployment built the payload as a bun-shell string literal
`-f payload={"pr":N}`; $ strips the quotes, sending invalid JSON
`{pr:N}`. Build the JSON in JS and interpolate the variable so $ passes
one properly-quoted arg.
- findActiveDeployment only recovered the PR when payload was an object, but
the Deployments API returns payload as a JSON STRING \u2014 so it always
returned null, ctx.activePreviewPr was always null, and decide() never
reached `release` for unlabeled/closed (env never torn down; race guard
dead). Extract a pure exported deploymentPr(payload) that recovers the pr
from a string OR object payload, never throwing, and use it.
Adds deploymentPr unit tests (string/object/malformed/missing/non-numeric/
non-object). 30 tests green.
Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
… best-effort fork-reject (SEA-2027) Review round 1 (ReviewP1) — 0 high, 3 medium, 3 low fixed (the 4th low, ssh host-key pinning, deferred to SEA-2110: it needs mattfw's host key published as a CI secret, an infra action): - createDeployment sent required_contexts as -f required_contexts[]= which gh serializes to ['' ] (a context literally named ''), not []. Send the raw-JSON empty array -F required_contexts:=[] (via the args-array form, dodging bun's quote-stripping) so the deployment is not left pending on a bogus context. - extracted the write-side payload build into an exported pure deploymentPayload(pr) and pinned it to the read side (deploymentPr) with a round-trip test — this is the seam that regressed under bun quote-stripping and had no write-side test. - corrected the output-contract comments: the cross-workflow interface SEA-2014 consumes is the GitHub Deployment record (environment_url), which this tool already drives; GITHUB_OUTPUT does not cross workflows, so the emitted outputs are a same-run breadcrumb, not that interface. - documented the displace path's reliance on GitHub auto_inactive to flip a displaced holder's Deployment inactive; noted a superseded release is not exactly-once (reconciled by the next claim). - made the fork-reject label/comment best-effort explicit (try/catch) and added a test that a throwing label API still exits 0. Spec-impact: none Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
rigel-mintaka
force-pushed
the
seal-sea2027-p1-preview-deploy
branch
from
August 16, 2026 15:46
0a75b31 to
5f68a35
Compare
…key[] (SEA-2027)
Review round 2 (ReviewP1r2) — HIGH: the round-1 required_contexts fix used
`-F required_contexts:=[]`, which is httpie/curl raw-JSON syntax gh does NOT
support. Verified against gh 2.96.0 with a capture server: it sends a bogus
field `{"required_contexts:":"[]"}` with the real field ABSENT, so the
Deployments API defaults to requiring all commit-status contexts and 409s while
the CI gate runs concurrently — createDeployment throws unhandled and the claim
crashes. This is the same seam that regressed in round 1 (`-f key[]=` sent
[""]), now in a third form.
Root cause of the repeat: no test covered realGitHubApi's arg construction (the
fakes bypass it). Fix both the encoding and the gap:
- extract the gh-api argv into a pure exported createDeploymentArgs(repo,ref,pr)
using the ONLY gh form that yields an empty array: `-f required_contexts[]`
(key with [] and NO =/value). Verified end-to-end: gh sends
{"required_contexts":[]}.
- unit-test the builder's encoding (asserts `-f required_contexts[]`, rejects
the two wrong forms, pins the payload + endpoint), so the write side is
regression-pinned, not just the payload round-trip.
Spec-impact: none
Co-authored-by: Matt Wilkinson <matt@sealedsecurity.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The single-holder release lifecycle was dead in the untested real-I/O seam:
-f payload={"pr":N}; $ strips the quotes, sending invalid JSON{pr:N}. Build the JSON in JS and interpolate the variable so $ passesone properly-quoted arg.
the Deployments API returns payload as a JSON STRING \u2014 so it always
returned null, ctx.activePreviewPr was always null, and decide() never
reached
releasefor unlabeled/closed (env never torn down; race guarddead). Extract a pure exported deploymentPr(payload) that recovers the pr
from a string OR object payload, never throwing, and use it.
Adds deploymentPr unit tests (string/object/malformed/missing/non-numeric/
non-object). 30 tests green.
Co-authored-by: Matt Wilkinson matt@sealedsecurity.com