ADR-1595: Build and run what the push-only and release-only workflows publish, before they publish¶
- Status: Accepted
- Date: 2026-10-04
- Deciders: lusoris
- Tags:
ci,release,docker,supply-chain,testing
Context¶
Six workflows build artifacts and none of them ran on a pull request: docker-publish-tester.yml and windows-tester-bundle.yml ran on a push to master, macos-tester-bundle.yml only on dispatch, and docker-publish-production.yml, docker-publish-operator-node.yml and supply-chain.yml only when a release is published. A change to a Dockerfile, a toolkit pin, the licence inputs or the vmaf-mcp package therefore reached the release unbuilt, and the local merge train (which restacks and builds CPU and CUDA only) was the only gate before master. A push-triggered run that fails on master is a red master; a release-triggered run that fails is a failed release.
The six differ in cost. The tester image is two architectures plus three GPU toolkit images (up to six hours of runner time). The Windows zips are three builds of up to 150 minutes on Windows runners. The macOS bundle runs on a macOS runner, which bills at ten times a Linux runner. The production and operator / node images are five image builds, three of them GPU toolkits. The supply-chain run is minutes, but half of its jobs need an OIDC token or a registry.
Decision¶
We will verify each workflow before it publishes, sized to what its runner costs, and never publish, sign or attest from a pull-request or scheduled run:
- Tester image and Windows zip: add a
pull_requesttrigger with the path list of the existingpushtrigger. Thevalidatejob resolves the pull request's merge commit as the source withpublish=falseand narrows the build matrix: the amd64 image, and the x64 zip. The arm64 and GPU images and the arm64 and CUDA zips stay with the push run. - macOS bundle: add a weekly
schedulethat builds and verifies master's head, with no publish; no per-pull-request run. - Production, operator / server / node and supply-chain: add one workflow,
release-dry-run.yml, that runs on every pull request but routes in-job (scripts/ci/release-dry-run-plan.sh) so a pull request builds only the group whose inputs it touches, and runs every group weekly. It builds the same Dockerfile targets for linux/amd64 without pushing and runs them throughscripts/ci/release-image-smoke.sh; the GPU toolkit images are built, not loaded; it builds thevmaf-mcpwheel and sdist, generates its SBOMs and checks them withscripts/release/verify-mcp-sbom.sh, whichsupply-chain.ymlnow runs too, so the check that gates a release is the one a pull request exercised. scripts/ci/tests/test_pr_time_verify_workflows.pyholds the result: it runs thevalidatestep of the tester workflows for pull-request, push and schedule events, fails ifrelease-dry-run.ymlgains a credential, a push or an unpinned action, and fails if what the dry run builds differs from what the release builds.
Alternatives considered¶
| Option | Pros | Cons | Why not chosen |
|---|---|---|---|
| Run each workflow's full jobs on every pull request | One implementation, no routing | Hours of Windows, arm64 and GPU runner time per pull request on a queue that is already saturated | Cost out of proportion to what the extra legs find |
| Weekly schedule only, for all six | Cheapest | A change reaches master and a week of other changes before it is found; no signal on the pull request that caused it | Slow feedback for the workflows whose inputs change often (Dockerfiles, build-config.env) |
Make the release workflows reusable (workflow_call) and call them with a dry-run flag | One implementation of every step | Rewrites six release workflows that cannot be run here; a mistake costs a release; the publish steps are interleaved with the build steps | The release path is changed as little as possible: one verifier extracted and shared, the rest mirrored and held in lockstep by a test |
Path-filtered pull_request trigger on release-dry-run.yml | Native, no plan job | Different groups need different paths; ADR-1140 forbids trigger filters on required-context workflows and ADR-1297 moves checks toward required | The in-job plan gives per-group routing and works unchanged if the checks become required |
| Make the new checks required in the aggregator | Blocks a merge on them | Absent-means-pass already covers the routed-out case, but the tester and Windows legs cost hours and the queue is saturated | Left for a decision once the runtime of the amd64 legs is measured on master |
Consequences¶
- Positive: a Dockerfile, toolkit-pin, licence-input or
vmaf-mcpchange is built on its pull request; a bundle that stops building is found within a week; the SBOM check that gates a release has fixtures that refuse each planted defect. - Negative: pull requests that change
build-config.envor the licence inputs now start five image builds and three GPU builds (cache-read only, no write); the arm64 and GPU tester images and the arm64 and CUDA Windows zips are still first built by the merge; the HTTP startup smoke of the server images stays release-only. - Neutral / follow-ups: measure the amd64 legs on master and decide whether to require them in
required-aggregator.yml; a new image target orvmaf-mcpbuild command goes into the dry run and the release workflow together.