Skip to content

feat(preview): add the PR-preview deploy workflow (SEA-2027) - #365

Open
rigel-mintaka wants to merge 4 commits into
mainfrom
seal-sea2027-p1-preview-deploy
Open

feat(preview): add the PR-preview deploy workflow (SEA-2027)#365
rigel-mintaka wants to merge 4 commits into
mainfrom
seal-sea2027-p1-preview-deploy

Conversation

@rigel-mintaka

Copy link
Copy Markdown
Contributor

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

@linear-code

linear-code Bot commented Aug 16, 2026

Copy link
Copy Markdown

SEA-2027

rigel-mintaka and others added 3 commits August 16, 2026 11:39
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
rigel-mintaka force-pushed the seal-sea2027-p1-preview-deploy branch from 0a75b31 to 5f68a35 Compare August 16, 2026 15:46
…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>
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