Keep a repository current with the law
Pinning a release is the easy half. The hard half is the day a new one is published and somebody has to answer: does moving to it change a duty one of our controls discharges?
This guide builds that answer locally, command by command, and then hands it to
CI. Everything here runs against fixreg, the fixture corpus, so you can follow
it end to end today.
The pin is controls.yaml
Section titled “The pin is controls.yaml”There is deliberately no separate pin file. A control mapping already has to state which corpus its atom ids were written against, so that field is the pin, and a bump rewrites that one line.
version: 1organisation: Fixture Bank plcrelease: fixreg@2025.02controls: - atom: FIXREG-Art5.3-Ob1 control: CTRL-INC-014 owner: incident-response@fixturebank.example evidence: https://grc.fixturebank.example/controls/CTRL-INC-014
- atom: FIXREG-Art5.2-Ob1 status: not_applicable rationale: >- The regulated entity provides no critical services from its own balance sheet; the register is maintained centrally by FixtureCo Services GmbH under the intra-group outsourcing agreement recorded in GRC-2291. Reassess if any critical service is onboarded to this entity.Two dispositions, and the difference between them is the whole design:
- Mapped — an atom a named control discharges, with an owner to tell and evidence to point at. When that atom changes substantively, the owner has to look.
- Waived —
status: not_applicableplus a written rationale. The decision was made once and recorded; a check that re-raised it every release would train its readers to ignore it.
An atom in neither list is a third thing: a gap. Nothing fails over it, because there is no control to review and no owner to tell — but it is reported, every time, in both commands below.
That mapping is fixtures/consumers/fixture-bank/controls.yaml in the engine
repository, minus its comments, and it is what every output on this page was
produced from. Copy it and follow along:
cp openregs/fixtures/consumers/fixture-bank/controls.yaml .1. What binds you, and what you have covered
Section titled “1. What binds you, and what you have covered”openregs coverage answers the mapping question in the direction a regulator
would ask it: for every obligation that binds this entity on this date, is it
mapped, waived, or neither?
openregs coverage \ --map controls.yaml \ --entity-profile small_operator \ --release fixreg@2025.04 \ --as-of 2025-06-01 \ --root path/to/openregscoverage: fixreg@2025.04 as of 2025-06-01, profile small_operator (Fixture Bank plc) 2 applicable: 1 mapped, 0 waived, 1 unmapped gap FIXREG-Art5a-Ob1 art_5a designate a compliance contact note FIXREG-Art5.2-Ob1 derogation: art_9 disapplies art_5__para_2 for small_operator, and small_operator is one of those; the duty is therefore not applicable to small_operator on 2025-06-01 note FIXREG-Art5.3-Ob1 inherited: the duty names operator and small_operator is a kind of operator, so it binds this entity; nothing in force on 2025-06-01 disapplies it note FIXREG-Art5a-Ob1 inherited: …Long lines are wrapped above; each note prints on one line. Read them, not just
the counts. Two different mechanisms are on display:
Article 5(3) binds this entity because a small operator is an operator and the
duty is inherited down the taxonomy; Article 5(2) does not bind it, because
Article 9 disapplies that paragraph for small operators — and the derogation is
named rather than assumed. Run the same command with --entity-profile operator
and the counts change to 3 applicable: 1 mapped, 1 waived, 1 unmapped, because
the derogation no longer applies and the waiver starts doing work.
Mapped plus waived plus unmapped equals applicable, by construction. The one gap
here is FIXREG-Art5a-Ob1 — the compliance-contact duty inserted by the amending
act, which nobody has written a control for yet.
--json <path> and --html <path> write the report to a file. The HTML is one
self-contained file that fetches no stylesheet, script or font, so opening it
makes no network request — which is what makes it safe to hand to an auditor.
2. What a release move would cost you
Section titled “2. What a release move would cost you”openregs impact intersects the diff between two releases with your mapping.
openregs impact \ --diff fixreg@2025.02..fixreg@2025.04 \ --map controls.yaml \ --root path/to/openregsimpact: fixreg@2025.02..fixreg@2025.04 (2025.04.diff.json) against controls.yaml — Fixture Bank plc 2 changed atom(s): 1 impacted (1 substantive, 0 editorial), 0 waived, 1 unmapped IMPACTED substantive changed FIXREG-Art5.3-Ob1 CTRL-INC-014 (incident-response@fixturebank.example) Fixture Regulation (EU) 2024/1, Article 5(3) WARNING substantive added FIXREG-Art5a-Ob1 no control, no waiver Fixture Regulation (EU) 2024/1, Article 5aimpact: FAIL — 1 mapped atom(s) changed substantively (FIXREG-Art5.3-Ob1); this release move needs the control owner before it mergesExit code 1. (Long lines are wrapped above; each row prints on one.) The full report goes to stdout as JSON — ticket-ready, carrying for each impacted atom the before and after text, the amending instruction, and the basis for the classification:
"amendment": { "basis": "no recital of the amending act declares this change meaning-preserving", "classification": "substantive", "instruction": "/akn/eu/act/reg/2025/7/eng@2025-03-01/!main#art_1__point_1", "operation": "substitution"}That classification is the release’s own, read from the diff manifest it shipped and never recomputed here. Whether an amendment was editorial rests on the amending act’s recitals, which your checkout does not contain.
The exit-code contract
Section titled “The exit-code contract”| What changed | Exit code |
|---|---|
| an atom mapped to a control, substantively | 1 — a human owns this before it merges |
| an atom mapped to a control, editorially | 0 — the words moved, the duty did not |
| an atom you waived | 0 — decided and recorded already |
| an atom nobody mapped | 0 — a warning, and a coverage gap |
Nothing in the report is a function of the clock: the same manifest and the same mapping produce byte-identical output, so the check is reproducible and a diff in it means something changed.
3. Prepare the bump
Section titled “3. Prepare the bump”openregs bump does the whole move on a branch: rewrites the pin, runs the
impact report for exactly that move, and writes the pull request body beside it.
openregs bump \ --repo . \ --to fixreg@2025.04 \ --from path/to/openregs/fixtures/registry \ --trust path/to/openregs/config/trust.yaml \ --no-pushpull: fixreg@2025.04 from …/fixtures/registry/fixreg/2025.04 staged 21 file(s), 246770 byte(s) -> …/pulled/.pull-fixreg-2025.04.partialverify: fixreg@2025.04 — …/openregs-bump-corpus-xbn2o4y4/pulled/.pull-fixreg-2025.04.partial signature valid 8 blob signature(s) under ecdsa-p256-sha256 key 24f0b959505940ce ... verified 5 of 5 check(s) passed unpack -> …/openregs-bump-corpus-xbn2o4y4/pulled/fixreg/2025.04bump: controls.yaml fixreg@2025.02 -> fixreg@2025.04 on branch openregs/bump-fixreg-2025.04 (5c3b9d1e0dec, not pushed (--no-push))impact: FAIL — 1 mapped atom(s) changed substantively (FIXREG-Art5.3-Ob1); this release move needs the control owner before it mergesbump: FAIL — open a pull request from openregs/bump-fixreg-2025.04 into main wrote .openregs/branch.txt wrote .openregs/impact.json wrote .openregs/pull-request-title.txt wrote .openregs/pull-request.md wrote .openregs/verdict.txtExit code 1 — openregs impact’s contract unchanged, which is what makes the CI
check red exactly when the bump needs a control owner.
Three things worth noticing:
- The release was pulled and verified before a byte of it was read.
--fromtakes the same registry pathopenregs pulltakes and runs the same five checks over a staged copy. Without--from, the release is read from a checkout given by--corpus. - Your working tree was never touched. The branch is cut in a temporary clone
and pushed to the repository’s
origin;--no-pushstops short of the push, which is the dry run. - The five output files land in the workspace, not on the branch. The bump
commit contains the mapping and nothing else. Add
.openregs/to your.gitignoreso a later job cannot commit them by accident.
Drop --no-push and the branch goes to origin. Either way the repository needs
one: the remote is resolved before the branch is cut, so a repository without an
origin and without --remote is refused even on the dry run, because there is
nowhere to open the bump:
bump: . has no remote named 'origin', so there is nowhere to open the bump.Pass --remote <url>: a pull request is a branch on a remoteWhat the body says
Section titled “What the body says”.openregs/pull-request.md is written for the person who has to make the
decision, not for the person who wrote the tool:
## Impact: FAIL — 1 mapped atom(s) changed substantively
### Impacted controls
| Control | Owner | Atom | Classification | Citation || --- | --- | --- | --- | --- || CTRL-INC-014 | incident-response@fixturebank.example | `FIXREG-Art5.3-Ob1` | **substantive** | … Article 5(3) |
#### `FIXREG-Art5.3-Ob1` — changed, substantive
- basis: amendment-substantive- before: Operators must report to the authority any incident causing losses above EUR 5000. (effective 2024-06-01)- after: Operators must report to the authority any incident causing losses above EUR 10000. (effective 2025-03-01)The owner of CTRL-INC-014 reads that and does one of two things: pushes the
control update onto the bump branch so the release move and the control change
merge together, or replaces the mapping entry with a waiver and a written
rationale in the same pull request. What nobody should do is disable the check —
it is red on precisely the days it is worth something.
4. Put it in CI
Section titled “4. Put it in CI”The commands above are what the shipped integrations run. Both live in the engine repository and are documented there:
- GitHub Actions — a composite action that bumps the pin, posts the impact table as the pull request body, and concludes failure on a substantive change to a mapped atom. Pin it to a commit sha; a tag is a pointer its owner can move.
- GitLab CI — the same integration as a
.gitlab-ci.ymljob, running the same command with the same exit code, opening the merge request with git push options instead of a forge CLI.
A consuming repository installs nothing: the action runs the CLI out of its own
checkout, so the code, the releases and config/trust.yaml all arrive together
when the runner materialises it. That is also why --trust never appears in the
CI examples and does here — locally, you have to say which roster you accept.
Make the check required by branch protection. Everything above is machinery for producing an honest red; a red nobody has to clear is decoration.
Limits, honestly
Section titled “Limits, honestly”fixregis a fixture, not law. Fixture Regulation (EU) 2024/1 does not exist; it is the corpus the engine is developed against. The workflow on this page is real, the regulation it operates on is not, and a control mapped to a FIXREG atom discharges nothing.- The registry can be a URL, but nothing is published at one.
--fromreads a directory or an https registry, andopenregs release assetslays a verified release out as the flat asset tree such a registry serves. What is missing is no longer the tag —fixreg@2025.04exists, built and signed on real runners — but its publication: the publish stage failed on a missing credential, so no assets were attached and nothing is hosted. The CI examples’registry:input still has nothing real to point at. --rootwants a checkout, not a pulled release.impactandcoverageread releases from an engine checkout’sregimes/<regime>/releases/<tag>/layout. A directory produced byopenregs pull --intois laid out differently and will be reported as not built. Usebump --from <registry>, which pulls and verifies into its own temporary corpus, or point--rootat a checkout.gh pr createhas never run here. The engine’s own test suite exercises these commands offline against local bare remotes, which proves the branch, the pin and the verdict — not that a forge accepts them.
Where to go next
Section titled “Where to go next”- CLI reference — every flag of
impact,coverageandbump, generated from the engine’s own parser. - Reviewing atom proposals — where the atom ids your mapping cites come from.
- Point an agent at the law — the other integration: ask the same corpus questions instead of diffing it.