CI, stacks and merging
CI that mirrors precommit, a manifest for required checks, and short stacks
CI runs the same commands
The Test workflow in tickets has four jobs: checks, test,
browser-contracts and specs. The checks job is the precommit
list, one named step per gate, each running the same deno task as
local precommit inside the same devenv profile. A failure in CI can
always be reproduced locally with the command on the failing step.
There is no separate "CI version" of a check. When CI and local disagree, the cause is the environment, and the environment is pinned.
The gate itself is reviewable
GitHub's required status checks live in repository settings, where no pull request can see or change them. That makes them easy to weaken by accident: a renamed job silently stops being required.
The repository keeps a manifest,
scripts/required-checks/checks.json:
["checks", "specs", "test"]
deno task check:required-checks reads the live ruleset with gh and
fails when it does not match the manifest. Changing the gate now
shows up in a diff.
Trial-merge before "ready"
A pull request can pass CI on its own branch and still break main
when merged, because main moved. deno task check:merged-tree
merges the head into the base in a throwaway worktree, runs typecheck
and the suite on the merged tree, and removes the worktree. A pull
request is presented as ready only when both the head CI and this
check pass.
Stacked pull requests
Large features land as a stack: a chain of small pull requests where
each targets the one below it, managed with the gh stack
extension. The rules in AGENTS.md:
- Keep a stack at three to seven pull requests. Split a larger job into several stacks.
- Every layer must be independently green, reviewable, and useful on its own. Delete the implementation a layer replaces in that same layer.
- Review and merge from the bottom up, and rebase the stack after a lower layer changes.
- Run targeted mutation tests while a layer sits above another open layer. Run the full branch mutation gate once it reaches the bottom.
"Useful on its own" connects back to the behaviour contract: each layer is a vertical slice with a production caller, never "add the types" followed by "use the types".
Long-running and scheduled work
Not everything belongs on the merge path:
| Workflow | When | Why not a merge gate |
|---|---|---|
| Payment sandbox e2e | Nightly, 03:17 UTC | Third-party sandboxes are slow and sometimes down |
| Email sandbox e2e | Nightly, 03:47 UTC | Same, for email providers |
| Production suite | Nightly, 04:23 UTC | Backup drills, races and bundle smoke tests are slow |
| Mutation testing | Manual, per module | Far too slow for every push |
| Specification evidence | Manual | Screenshots for each story, keyed by its stable ids |
| Biome fix | Manual | Opens a pull request with lint fixes for a branch |
Finish by rewriting the description
Make the finished PR name and describe what was actually built, in plain words.
Agents write the description at the start, from the plan, and never
update it. By merge time it describes a different change. The final
description must name the behaviour added or replaced, the
production caller, the old path deleted, the line counts, the tests
and mutation commands run, and any change to the pure list or the
declared machines (the contract:diff output goes here).