The GitLab Mirror¶
The one control that cannot run in CI
Every CI gate in this documentation — pinning, scanning, the coverage floor and the rulesets that require them — runs on GitHub Actions. The applications are also mirrored by hand to the open-regels.nl GitLab instance, and the mirror is outside all of them. It stays outside: each merge leaves it behind until someone pushes.
What changed on 12 September 2026 is that something now notices. A release-time check,
scripts/check-mirror.sh, runs in all three repositories from /bump-release, in the
step that lands the release through a pull request — step 6 in the CPSV Editor, 7 in the
Linked Data Explorer, 8 in the RONL Business API — and compares each remote-tracking ref
with the mirror's. It closes the observation half
of the problem, not the drift. It cannot run in CI, and that is a property of the mirror
rather than a shortcoming of the check: the gitlab remote lives in .git/config, not
in any tracked file, so a runner has no such remote, no credential for it and no route to
it. It also
never pushes — it prints the exact command and stops, because writing to a shared remote
is a decision for a person.
Its output separates the two cases that a commit count cannot, and it prints the remote-tracking form rather than the local branch, which drifts. All four of its paths — match, behind, diverged, missing — were exercised against a scratch bare repository rather than assumed.
On 12 September 2026, by git ls-remote against both remotes:
| Repository | acc |
main |
|---|---|---|
| CPSV Editor | ✅ f7fe80f on both |
✅ f5bae6a on both |
| Linked Data Explorer | ✅ 1babd54 on both |
✅ be6bc54 on both |
| RONL Business API | ✅ 28e1a9e on both |
✅ 311d732 on both |
Two were checked again on 19 September 2026, after their promotions that day, and both
were in sync: the Linked Data Explorer with acc at 379cbab and main at ec4792f,
the CPSV Editor with acc at a37bace and main at 2723db1.
The check itself was re-read from all three repositories on 27 September 2026 and is
unchanged since it landed. On the same day, by git ls-remote against both remotes, all
three mirrors were in sync after that week's promotions: the CPSV Editor at a7fe76f /
7d154ba, the Linked Data Explorer at 0143ea2 / 4148c9a, the RONL Business API at
3c44b9e / 2443adc (acc / main). A tick is still synced at the last check.
Two of the three release procedures run a second check straight after this one — the
CPSV Editor's since v2026.09.6 and the RONL Business API's since v2026.09.11.
npm run check-previews lists the Static Web Apps preview environments Azure actually has
against the pull requests GitHub has open, and prints the exact
az staticwebapp environment delete command for each orphan. It exists for the same
reason as the mirror check: GitHub starts no workflow for a pull request with a merge
conflict, so no close job ever runs for it, and in the CPSV Editor eight previews on
acceptance and production had outlived their pull requests unnoticed. Like check-mirror
it runs on a workstation and never deletes anything itself. It hardcodes no app: the CPSV
Editor's derives each from its deploy workflow's file name, and the RONL Business API's —
whose workflow names carry no hostname, and whose apps span two subscriptions — finds
them by repositoryUrl across every subscription the login can read. The Linked Data
Explorer has no such check.
A tick is synced at the last check, not kept in sync. The RONL Business API's mirror
had never been audited before 12 September, and both branches turned out to be strict
ancestors — acc eight commits behind and main one hundred and eighty-four — so two
fast-forwards reconciled it. It then drifted three more times the same day, as each
promotion pull request merged, which is the behaviour the check exists to surface.
Behind is not the same as diverged¶
One command separates the two cases before anything is pushed:
An ancestor means a fast-forward, and reconciliation is one push. Anything else means the mirror holds commits GitHub has never seen. Commit counts alone do not tell the two apart — "253 behind" and "18 ahead and 306 behind" both read as stale. And push the remote-tracking ref, not the local branch, which drifts:
What the CPSV Editor's divergence turned out to be¶
The CPSV Editor's GitLab main had not moved since 4 March 2026 while GitHub moved on,
and it carried 18 commits GitHub had never seen. By the repository's own record,
seventeen were cross-remote merges with no content of their own. The trees disagreed by
more than that: files existed on GitLab and on no GitHub branch at all — most of them
Create React App leftovers the Vite migration had removed on purpose, and two example
TTLs that existed nowhere on GitHub, neither on main nor on acc. They had been
committed with a CI-skip marker, which is how they came to be on one remote and not the
other without anything noticing.
Compare trees, not commit counts. Eighteen commits ahead was almost entirely noise;
filtering git diff --name-status origin/main gitlab/main to additions is what found the
two files that mattered. Check each result against every branch on the other remote,
not just the matching one.
The reconciliation, in the order that keeps content safe on both remotes:
- Land the missing content on GitHub — v2026.09.3 recovered the two files by cherry-picking the commit that restores them, not by merging a branch based on the stale remote.
- Push
accto the mirror first, so the files exist on GitLab outside the branch about to be overwritten. - Archive the ref being replaced —
archive/gitlab-main-2026-09-09still holds15a7d17on the mirror, so the operation is reversible. - Reset with
--force-with-lease=main:<old-sha>, naming the SHA, so the push refuses if anything moved underneath. Before it, confirm every file about to disappear has a successor.
A skip marker in a commit message switches every gate off
GitHub Actions honours [skip ci], [ci skip], [no ci], [skip actions] and
[actions skip] anywhere in a commit message, including in prose that only
discusses them. That is how the two files bypassed every check, and it is a signal
that something skipped review rather than a convenience for documentation — use
paths-ignore to express "this change does not need a deploy" without switching
the gates off. See Code Standards for why the symptom is
silence rather than red.