Draft: Gate -nix pushes on asset name parity
What does this MR do?
It adds a gate that stops a -nix image from being pushed when its frontend assets name a file that the Debian assets image does not have.
What breaks without it
- GitLab.com serves
/assetsfrom a bucket, not from the pod. The deployer fills that bucket from the Debiangitlab-assets-eeimage (ADR-0045, "Context and Problem Statement"). - Rails on a Nix pod writes the asset names of its own build into each page (
lib/gitlab/webpack/manifest.rb). - When the bucket lacks one of those names, the browser gets a 404. The staging canary on Nix images served almost blank pages that way (production#23043).
The hardened-builds fixes make the Nix names match upstream's. They are all merged and in the current pin (listed under "Related issues"). Nothing checks that they stay that way. Without the gate, the first sign of a new difference would be 404s on GitLab.com.
What the gate does
Before the first -nix image with the assets is pushed, the gate:
- takes the webpack, Rspack and Sprockets manifests of the Nix assets build;
- streams the same manifests out of the Debian assets image for the same GitLab commit (
crane export, about 230 MB for EE); - fails when the Nix manifests name a file that the Debian image does not have.
Source maps and manifest metadata do not count. Files that only the Debian image has are fine. Each side needs at least one webpack manifest: manifest.json up to 19.5, and only manifest.rspack.json from 19.6 on, because GitLab now builds its production assets with Rspack only (gitlab-org/gitlab!263628 (merged)). Each webpack manifest must be on both sides or on neither. The details are in docs/nix-variant.md, "Asset parity with the Debian variant".
Scope. The gate checks the direction that breaks GitLab.com today: every name a Nix pod writes into a page must be in the bucket. ADR-0045's decision asks for more: the same file names on both sides, and every file upstream's image ships, source maps included, in the Nix output. The gate does not check that reverse direction yet. That is left for a follow-up.
| Piece | Change |
|---|---|
gitlab-assets-{ce,ee} |
Save their manifests and the locked gitlab-rails commit as artifacts. |
build-scripts/nix_assets_parity.sh |
New. Finds the Debian image, streams its manifests and compares. |
gitlab-assets-parity-{ce,ee} |
New jobs. They run next to gitlab-rails-{ce,ee}. |
gitlab-rails-{ce,ee} |
Run the same check during their build, and push only when it passes. |
| webservice, sidekiq, toolbox, workhorse, geo-logcursor | Need the parity job of their edition before they push. |
| rails closure cache | Also uploads when the job fails, but only a completed export. |
Why the rails jobs run the check themselves: each image job builds and pushes in one go. A needs: on the parity job would hold back the rails build, the longest step of the pipeline. So the rails jobs run the check in the background and push nothing until it passes. No build waits for the gate.
Why the rails cache uploads on failure: a failed check fails gitlab-rails-* after its whole build. With the closure in the cache, a retry does not rebuild it. An after_script guard (nix_cache_guard) keeps the cache only when the closure export completed, so a partial export is never uploaded (docs/nix-variant.md, "Where the gate sits").
| Exit | Meaning | Job result |
|---|---|---|
| 0 | Every file the Nix manifests name is in the Debian image. | passes |
| 1 | A file, an entrypoint or a manifest differs. | fails |
| 2 | The gate cannot check: a manifest, the image, a tool or a variable is missing. | fails |
| 3 | Branch pipelines only: upstream built no assets image for the locked commit. | warning |
Which Debian image the gate compares with:
- Tag and auto-deploy pipelines: the image the Debian jobs copy (
GITLAB_ASSETS_TAG). The gate waits up to 30 minutes for it, then fails with exit 2. - Branch pipelines (MRs, master):
GITLAB_ASSETS_TAGnames a branch there, and that tag moves. So the gate takes the image of the commit the Nix registry locked. Upstream builds that image only for some commits, and gitlab-foss images carry gitlab-foss commits, so CE always exits 3 here. - Auto-deploy builds no CE (
.nix-ce-rules), so only tag pipelines check CE.
On a mismatch, the job log lists a few of the differing names. The full lists are in the job artifacts, under artifacts/nix-assets-parity/. On exit 3, no-reference.txt there says why there was nothing to compare with.
How to test
- Locally: run
bats build-scripts/testswith bash 5 andjq. GNU and BusyBoxtarandawkboth work. Then runshellcheckon the file list oflint:build-scripts-shellcheck(.gitlab/ci/tests.gitlab-ci.yml). - In CI: in an MR pipeline, start the manual
nix_pipelinejob. In its child pipeline, expectgitlab-assets-parity-eeto pass andgitlab-assets-parity-ceto warn with exit 3. Thegitlab-rails-{ce,ee}logs show the same check.
Before merging
Nothing outside this MR blocks it any more. Every hardened-builds fix is merged and in the pin. gitlab-org/gitlab!262551 (merged) made the Rspack names independent of the checkout path and, like gitlab-org/gitlab!261541 (merged), is backported (ADR-0045, "What stays intact").
One cost to know about: the parity jobs have no cache, so nix_setup builds the hardened-builds ci-toolchain (Go, cosign, skopeo, the Yarn 4 fetcher) in each of them. With the current pin that took 27m40s in gitlab-assets-parity-ee, out of the job's 50 minute script limit. The check itself took 47 seconds. Other nix jobs pay the same setup today (26m20s in certificates). If it grows, the toolchain needs a cache, or .nix-assets-parity a longer RUNNER_SCRIPT_TIMEOUT.
Related issues
- hardened-builds !234: ADR-0045, "Nix and Debian Images Side by Side on GitLab.com", merged as
docs/adr/ADR-0045-nix-and-debian-images-side-by-side.md. It states the requirement this gate checks. - hardened-builds !230 (closed): the fixes that make the Nix names match upstream's. Its description lists the MRs it was split into.
- gitlab-org/gitlab!262551 (merged): makes the Rspack output independent of the checkout path.
- gitlab-org/gitlab!263628 (merged): builds production assets with Rspack only from 19.6, so neither side has
webpack/manifest.jsonany more. - production#23043: the staging canary that got 404s.
The hardened-builds fixes split out of hardened-builds !230:
| MR | Change | State |
|---|---|---|
| !235 | Drop the obsolete Vue compiler workarounds | merged, in the current pin |
| !236 | Run @gitlab/web-ide's postinstall |
merged, in the current pin |
| !238 | Ship precompressed .gz assets | merged, in the current pin |
| !239 | Build assets with source maps | merged, in the current pin |
| !240 | Install only production JS dependencies | merged, in the current pin |
| !241 | Check dependency install scripts against a list | merged, in the current pin |
| !242 | Compile from GitLab's own yarn.lock | merged, in the current pin |
The current pin is hardened-builds d25abede (ci_files/variables.yml, 2026-10-29).
Test plan
At d286c440, rebased onto master 55409ac0:
- bats, full suite: 189/189 under bash 5.3.20, GNU tar 1.35 and GNU Awk 5.4.1, with 1 skip (the real-data test below, which needs local trees). This MR adds
nix_assets_parity.bats(64 tests) andnix_cache_guard.bats(10 tests). Their fixtures are cut from the real 19.2.1 manifests. They cover a renamed entrypoint, lazy and worker chunks, a missing entrypoint, a webpack manifest on one side only, the Rspack-only manifests of 19.6, Sprockets mismatches, bad input, the exit-3 report, and the runner'sset -eo pipefail. The 7 tests added or changed for 19.6 fail against the previous version of the gate. - shellcheck 0.11.0 on the
lint:build-scripts-shellcheckfile list: clean. - BusyBox
tarandawk(thebats/bats:1.11.0image): 189/189, 1 skip, intest:build-scripts. - Real data, 19.6 EE, outside CI: the manifests that Nix run 3003802259 handed over and fetched for gitlab master 7666d5af. The previous version of the gate exits 2 on them, this one exits 0:
manifest.rspack.json2115 files and 1286 entrypoints, Sprockets 1239 assets, on both sides. - Real data, 19.2.1 EE, outside CI, at 41cf0720, before the 19.6 change: the Nix
gitlab-assets-eeoutput andregistry.gitlab.com/gitlab-org/gitlab/gitlab-assets-ee:v19.2.1-eename the same 3485 webpack files, 589 entrypoints and 650 Sprockets assets. Both trees carrymanifest.json, which the 19.6 change compares as before. Earlier runs also streamed the image live, and renaming the main chunk made the gate fail. - 19.5 with Rspack, outside CI:
assets_manifest_compareon a Nix build of the canary auto-deploy 19.5.202610091422 (f2f37752ef9) againstdev.gitlab.org:5005/gitlab/gitlab-ee/gitlab-assets-ee:f2f37752ef9e6db6db87a8c72844c4c22dfce858:webpack/manifest.json4574 files and 921 entrypoints OK,webpack/manifest.rspack.json1663 files and 921 entrypoints OK, Sprockets 991 assets OK, exit 0. 6 Rspack entrypoints list the same files in another order; the gate compares sets, so that passes. - GitLab CI lint and a green MR pipeline on GitLab.com: 3004361718.
- Nix run 3004364043, at d286c440, building gitlab master 90551fac (19.6.0-prerelease) with the hardened-builds pin d25abede:
-
gitlab-assets-parity-eepasses:manifest.rspack.json2115 files and 1286 entrypoints on both sides, Sprockets 1239 assets, nomanifest.jsonon either side. -
gitlab-assets-parity-cewarns with exit 3: no gitlab-foss image for a gitlab commit. -
gitlab-rails-eeruns the same check during its build, passes and pushes.gitlab-rails-cewarns and pushes.
-
- Nix run 3003802259, at 089e8dd2, before the 19.6 change:
gitlab-rails-eeandgitlab-assets-parity-eefailed closed with exit 2 on gitlab master 7666d5af, because neither side hadwebpack/manifest.json, while every Rspack and Sprockets name matched. That is what the 19.6 change fixes. - Nix run 2943419417, on an earlier version of this MR (075840db), building gitlab master 25876b0b with a hardened-builds pin that had none of the fixes:
-
gitlab-assets-parity-eefails with exit 1, as it should: 1978 webpack files only the Nix build has, andmanifest.rspack.jsononly on the Debian side. -
gitlab-rails-eeruns the check during its build, fails with exit 1 after the closure export, pushes nothing, and still uploads its closure cache. - Not covered by a Nix run since then: a mismatch (covered by bats), and the guard's discard path (a failed build without an export, covered by bats).
-
Checklist
See Definition of done.
For anything in this list which will not be completed, please provide a reason in the MR discussion
Required
- Merge Request Title, and Description are up to date, accurate, and descriptive
- MR targeting the appropriate branch
- MR has a green pipeline on GitLab.com
- When ready for review, MR is labeled "~workflow::ready for review" per the Distribution MR workflow
Expected (please provide an explanation if not completing)
- Test plan indicating conditions for success has been posted and passes (above)
- Documentation created/updated (
docs/nix-variant.md,docs/ci-variables.md,build-scripts/tests/README.md) - Integration tests added to GitLab QA: not applicable, CI-only change
- The impact any change in container size has should be evaluated: none, no image content changes
- New dependencies are managed with GitLab forked renovatebot: none added;
craneandjqcome from the hardened-buildsci-toolchain