Skip to content

sandbox apply: values documents and a built-in template (ENG-1187) - #361

Draft
joehorsnell wants to merge 5 commits into
joe/ENG-1187/sandbox-apply-dry-runfrom
joe/ENG-1187/sandbox-values-and-dry-run
Draft

sandbox apply: values documents and a built-in template (ENG-1187)#361
joehorsnell wants to merge 5 commits into
joe/ENG-1187/sandbox-apply-dry-runfrom
joe/ENG-1187/sandbox-values-and-dry-run

Conversation

@joehorsnell

@joehorsnell joehorsnell commented Aug 18, 2026

Copy link
Copy Markdown
Contributor

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-run
on 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. -f accepts one, and it says what to fork rather than how a spec
is shaped:

cluster: prod-eks
defaults:
  namespace: hotrod
  imageTemplate: ghcr.io/acme/{workload}:{short-sha}
forks:
  - workload: route
  - workload: frontend
    env:
      LOG_LEVEL: debug

A document with a top-level spec is a sandbox spec and takes the existing path
untouched. Unknown fields are rejected, so envs: fails rather than being silently
dropped.

Rendering through a template embedded in the binary, using the same @{var} engine
that renders a template of your own. signadot sandbox template show prints it; save it,
edit it, pass it to -f, and you have taken over the structure. That is the upgrade path
off 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 completes
the 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 the
fork 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.go and context.go reimplemented per provider, and the second
provider 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 | sh installs and four jq reads of the apply response, and it deleted both of its
templates once a default-route-group input existed.

That run also found the label bug. The apiserver reserves the whole signadot/ prefix and
accepts only signadot/github-repo and signadot/github-pull-request under it, requiring
them 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

  1. Two surfaces or one? The values schema and @{var} templates coexist, bridged by
    template show. Should they converge, and in which direction?
  2. Is the built-in template the right seam? It is the mechanism and the
    documentation of the templateless path, and a compatibility surface the moment someone
    saves a copy.
  3. Does the schema cover enough for the templateless path to be the default for new
    customers? One dog-fooding pass added defaultRouteGroup and description.
  4. How much local validation should mirror the API? The label rules are here because
    the API's message arrives late and opaquely, but every mirrored rule can drift.

Test plan

  • go test ./internal/...
  • Golden fixtures for values and spec documents, --set, --patch, CI-derived and
    authored names, and long-name hashing
  • TestRenderedSpecIsAFixedPoint
  • End to end against staging via signadot#7315: two sandboxes created and Ready
  • Docs page for the values schema and built-in template, before any announcement

joehorsnell and others added 4 commits August 18, 2026 20:06
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
joehorsnell force-pushed the joe/ENG-1187/sandbox-values-and-dry-run branch from 2487501 to f554d81 Compare August 18, 2026 19:15
@joehorsnell
joehorsnell changed the base branch from main to joe/ENG-1187/sandbox-apply-dry-run August 18, 2026 19:15
@joehorsnell joehorsnell changed the title sandbox apply: values documents, a built-in template and --dry-run (ENG-1187) sandbox apply: values documents and a built-in template (ENG-1187) Aug 18, 2026
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>
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