sandbox apply: values documents and a built-in template (ENG-1187) - #361
Draft
joehorsnell wants to merge 5 commits into
Draft
sandbox apply: values documents and a built-in template (ENG-1187)#361joehorsnell wants to merge 5 commits into
joehorsnell wants to merge 5 commits into
Conversation
6 tasks
Adopting Signadot in CI means authoring a @{var} spec template per repository,
and there is no way to see the spec a template produces without applying it.
Neither gap can be closed in a template: the fork list has a length that depends
on the input, and the engine cannot iterate.
So `sandbox apply -f` now also accepts a values document — a flat statement of
which workloads to fork and with what — which is compiled and rendered through a
template embedded in the binary. `sandbox template show` prints that template,
so saving it and editing it is the upgrade path when the values schema stops
fitting.
Alongside it, --dry-run=client renders and validates locally and prints the spec
instead of applying it, before authentication so it needs no credentials.
Rendering is a fixed point, which lets a caller render in one step and apply
exactly those bytes in the next. --dry-run=server is accepted and reports that
it is unavailable; it needs validateOnly on the apiserver.
--patch, --name, --ttl, --ci-context and --default-labels cover what a CI job
knows and a file does not, including the deterministic sandbox name and the
signadot/* labels the GitHub App uses to delete a sandbox on PR close.
This replaces a plan to extract the templating and validation into a shared
libspec module for a GitHub Action to depend on. Keeping the logic here means
one implementation, no second repository to release, and every CLI user gets the
templateless path rather than only Action users. Design notes in
docs/design/sandbox-values-and-dry-run.md.
Co-authored-by: Cursor <cursoragent@cursor.com>
Co-authored-by: Cursor <cursoragent@cursor.com>
Dog-fooding the Action against staging turned up a 400: the API reserves the signadot/ prefix and accepts only github-repo and github-pull-request under it, so the signadot/usage label we added was enough to have the whole spec refused. Drop it. The remaining two are a pair the API wants set together or not at all, so they are stamped as a set, which also stops a build with no pull request from sending the repo key on its own. Both rules are now checked during rendering, so --dry-run=client names the offending key instead of the sandbox being refused after submission. Co-authored-by: Cursor <cursoragent@cursor.com>
Dog-fooding found the label constraint the hard way, and the design doc should say why the pair is stamped atomically. The porting note is there because moving this work into the CLI was justified by other CI systems being cheap to add, and that claim deserves the detail behind it. Co-authored-by: Cursor <cursoragent@cursor.com>
joehorsnell
force-pushed
the
joe/ENG-1187/sandbox-values-and-dry-run
branch
from
August 18, 2026 19:15
2487501 to
f554d81
Compare
joehorsnell
changed the base branch from
main
to
joe/ENG-1187/sandbox-apply-dry-run
August 18, 2026 19:15
The dry run lands on its own, so the design notes should say which half is which, and the case for keeping the CLI at just that half belongs alongside the other alternatives rather than only in the review thread. Co-authored-by: Cursor <cursoragent@cursor.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 decision this asks for
Should the CLI know how to build a sandbox spec from a flat description of what to fork,
so that adopting PR-validation sandboxes does not require authoring a spec template?
This is the proposal, dog-fooded end to end, but it is a draft for design feedback,
not code review. The alternative — the CLI stays minimal and every CI integration
carries this logic itself — is written up and costed in the
design overview
and its Notion mirror.
Stacked on #362, which adds
--dry-runon its own and stands independently of whatever is decided here. Review that one first;
it is small and uncontroversial. This PR's diff is only the values work.
What it adds
A values document.
-faccepts one, and it says what to fork rather than how a specis shaped:
A document with a top-level
specis a sandbox spec and takes the existing pathuntouched. Unknown fields are rejected, so
envs:fails rather than being silentlydropped.
Rendering through a template embedded in the binary, using the same
@{var}enginethat renders a template of your own.
signadot sandbox template showprints it; save it,edit it, pass it to
-f, and you have taken over the structure. That is the upgrade pathoff the values schema when it stops fitting.
The hybrid is deliberate. The document's shape lives in a template, where it is
inspectable. The parts a template cannot compute — the fork list, whose length depends on
its input, plus resolved images, merged env and labels — are computed in Go and injected
as
@{var[yaml]}, which embeds a parsed structure rather than a string.Flags for what CI knows and a file does not:
--name,--ttl,--patch,--ci-context auto|github|none,--default-labels. Context detection is what completesthe templateless path: it derives a deterministic sandbox name from repo and PR, so every
push updates one sandbox in place, and stamps the labels the Signadot GitHub App uses for
teardown.
Why it might belong here rather than in each integration
The
@{var}engine is pure substitution — no iteration — so something has to build thefork list. That something can live in the CLI once, or in every CI integration.
In the CLI: one implementation, and every CLI user gets the templateless path, not just
Actions users. A CircleCI orb is then YAML that writes a values document, which matters
because an orb cannot reuse TypeScript.
In each integration: the CLI keeps a smaller surface and commits to no values schema
before we know the Action is the right shape. The cost is roughly the 550 lines in
internal/render/compile.goandcontext.goreimplemented per provider, and the secondprovider either repeats it or moves it here anyway.
Dog-fooded
signadot#7315 drives this from CI and is
green: both sandbox jobs render a values document, apply the rendered spec, reach Ready,
and run contract and RBAC suites behind it. It replaced two spec templates, two
curl | shinstalls and fourjqreads of the apply response, and it deleted both of itstemplates once a
default-route-groupinput existed.That run also found the label bug. The apiserver reserves the whole
signadot/prefix andaccepts only
signadot/github-repoandsignadot/github-pull-requestunder it, requiringthem set together or not at all; a third built-in label had the whole spec refused with a
400. Both rules are now checked while rendering.Open questions
@{var}templates coexist, bridged bytemplate show. Should they converge, and in which direction?documentation of the templateless path, and a compatibility surface the moment someone
saves a copy.
customers? One dog-fooding pass added
defaultRouteGroupanddescription.the API's message arrives late and opaquely, but every mirrored rule can drift.
Test plan
go test ./internal/...--set,--patch, CI-derived andauthored names, and long-name hashing
TestRenderedSpecIsAFixedPoint