ADR-0126: Pull requests carry the verification; main releases only what changed
- Status: accepted; supersedes in part ADR-0088 (the site's pull-request build and the release trigger's path list) and ADR-0124 (OSV on every pull request); decision 5 superseded by ADR-0129
- Date: 2026-09-29
- Deciders: Mahdi Amirabdollahi
Context
A source pull request took about fifteen minutes. The backend suite ran as one serial Surefire JVM (about ten minutes of test time), and the image build waited for it before starting its own four. After the merge, main ran the same backend and frontend suites again before it released. Then the release published the image, the plugin API to Maven Central and the SDK to npm, all three, even when a change touched only the UI, only ci.yml, or only the Docker Hub description. OSV scanned every pull request, documentation ones included, and nothing on main required a check to pass before a merge.
Decision
We will verify each change once, on its pull request, and publish from main only the artifacts whose inputs changed.
- One required check. Every pull-request job in
ci.ymlruns in parallel, gated by its own path filter. That covers the backend (three Surefire shards, round-robin over the sorted test classes), the frontend, the plugin template end to end, the image build, the site build, OSV when a manifest changed, and actionlint when a workflow changed.ci-okdepends on all of them and fails when any failed or was cancelled. Themainruleset requires onlyci-ok, on a branch that is up to date withmain, so a job skipped by its path filter never blocks a merge. - No re-test on
main. Because the ruleset is strict, the merge commit is the tree CI verified. A push tomaingoes straight toreleasewhen what the image is built from changed. A change toci.ymlalone tests everything on its PR and releases nothing. - Per-artifact publish gates.
publish-apicompares the release tag with the newest version on Maven Central, oversrc/mainandpom.xml.publish-sdkcompares it with the newest version on npm, over the SDK's inputs (web/packages,web/src/sdk,web/src/kernel, the web manifests). Each publishes only on a difference. Comparing against the registry rather than the previous commit means a failed publish is retried by the next release. - The Docker Hub description is its own job, run when
docs/dockerhub.mdchanges. It no longer cuts a release. - The release pushes through a deploy key. The release commit and tag go to
mainover SSH with a deploy key that is the ruleset's only bypass besides the repository admin. The workflow token cannot bypass a required status check. - Pinned actions. Every third-party action is pinned to a commit SHA with its version in a comment. Dependabot updates them weekly.
Consequences
- A source PR takes about as long as its slowest shard, not the sum of backend and image. A merge releases in about five minutes instead of sixteen.
- Strict up-to-date branches mean that after each release commit, an open PR must be updated and re-run before it can merge. With one maintainer and few open PRs, that is cheap. A merge queue would remove it, but merge queues are not available to user-owned repositories.
- A version on Docker Hub may have no matching Maven Central or npm version. That is expected: a plugin builds against the newest API and SDK that exist, and
studio.sincestates the oldest Studio it runs on. - The shards are balanced by class count, not by time. If one lags, weigh them by the Surefire report times.
- Losing the deploy key blocks releases until a new key is added to the repository, the secret and the ruleset.
Alternatives considered
- Keep re-testing on
main. Rejected: it repeats a run that verified the same tree and delays every release by about eleven minutes. - A merge queue. Not available for a repository owned by a user account.
- Parallel Surefire forks in one job. Rejected: timing-sensitive broker tests share the CPU and get flakier, and local runs would change too. Separate runners isolate them.
- Path filters on the publish jobs, against the previous commit. Rejected: a failed publish would never be retried, because the next release's diff no longer contains the change.