Keyboard shortcuts

Press ← or → to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Checks and gates

Every check is a committed script or a pinned tool you can run yourself. The design of the workflows is in docs/ci-cd.md; this page is the short version.

The required check

A pull request merges into main only when conclusion passes, the one job of ci.yml that passes when every other job in it passed or was skipped.

main also requires signed commits and a branch that is up to date with it.

The two tiers of ci.yml

The first tier runs on every change, because it needs no Rust build (the manifest check installs the toolchain only to read the manifests):

CheckWhat it guards
zizmorworkflow security
actionlintworkflow correctness
shellcheckevery first-party shell script
hadolintevery first-party Dockerfile
kubeconformthe Kubernetes package under deploy/kubernetes/, its base and its example overlay rendered by kustomize, in strict mode
observabilitythe Grafana dashboard and the Prometheus rule file under deploy/observability/: promtool check rules, and every metric they name is one the gateway exports (scripts/checks/observability.sh)
comment-stylethe comment budgets of the Rust sources
file-lengthno hand-written Rust file or book page over 1000 lines; a listed file may shrink and never grow
duplicationthe duplicated lines of the hand-written Rust, measured by jscpd at 50 tokens over 5 lines, may fall and never rise; a rise names both places of each new block
versionsevery repeated pin agrees with docs/VERSIONS.md
favicon-syncthe book’s favicons match the brand mark
conformance-matrixthe conformance matrix agrees with the specification and with the tests that claim each point, and the README conformance badges agree with the matrix and the AQL golden pass list
e2e-placementevery test that checks the FERROFED_E2E gate lives in its crate’s e2e test module, and the CI end-to-end job still sets the gate and selects test(/^e2e::/)
aql-spliceno Rust source outside the tests builds AQL text with format!, concat!, push_str or +: a query is a template openehr-query parses, its values bound or sent as query parameters
obligationsthe obligations checklist agrees with the vendored specification, the conformance matrix and the tests it names, and a re-pin fails until every changed page is reclassified
technical-documentationevery element of Annex III of Regulation (EU) 2025/327 has a row in the technical documentation index whose file and heading exist, and a version whose harmonised components differ from the previous release has its review rows (Art 30(2))
annex-iievery point of Annex II of Regulation (EU) 2025/327 has a row in the Annex II checklist whose phrase is in that point’s text, whose evidence pages, headings and tests exist, and whose gap names its issue, with the page rendered from the checklist
chapter-iiievery obligation of Chapter III of Regulation (EU) 2025/327 on the manufacturer, and every item of Annex IV, has a row in the conformity record whose phrase is in that provision’s text, whose Art 105 date is right, whose evidence exists and whose gap names its issue, and no row claims more than the rest of the record supports
site-linksevery internal link and anchor of the assembled site (the landing page and this book) and of the README, checked offline by lychee, so a page or an anchor that does not exist fails the change
tracker-helpersthe self-tests of the scripts/gh tracker helpers
crate-version-guard self-testthe crate-version guard judges only what a pull request changes, against a stub repository
manifestsevery Cargo manifest, of the workspace and of fuzz/, parses, read by cargo metadata without compiling, so a manifest a merge broke fails here first; the one first-tier check that installs the pinned toolchain

The second tier is the Rust lane: formatting, the fuzz crate’s lockfile, clippy, the tests, the end-to-end suite against two containerised nodes, the release compose files (no compose file builds the image, the release compose.yaml renders, and its example ferrofed.toml and registry.toml pass ferrofed config check, and so does the example Kubernetes ConfigMap with synthetic secrets), rustdoc, cargo deny, the MSRV build, every feature of each published crate on its own, the packaging dry run, the crate-version guard and dependency review. A detect job gates it on the root Cargo.toml, which exists, so the tier runs on every change; the crate-version guard and dependency review run on pull requests only.

The conformance report job reads the JUnit reports of the test run and the end-to-end run, and writes the report section 16.4 of the specification asks for: every test track of section 16.3 and every conformance point of section 17 as pass, fail, not run, deferred or open, with the issue and the reason of each row that is not scored. A point section 17 scores against a node or the federation operator is reported in a class of its own, never as a gateway pass (section 16.2). Its result comes from the node profile checks, which the end-to-end run executes against each harness CDR product, and the report lists what each check observed. The report is in the job summary and in the conformance-report artifact. The job fails when a covered track or point did not pass, and a node or operator point never fails it. Locally, with Docker running, bash scripts/conformance/report.sh --run runs the whole workspace with the FERROFED_E2E gate set and writes target/conformance/report.md, report.tsv and node-profile.tsv.

The fhir-validator (EEHRxF documents) job runs HL7’s FHIR Validator over every patient summary document the tests write, offline, against the vendored HL7 Europe packages, and fails on an error. The tests write their documents only when FERROFED_EMIT_DIR names a directory. The job’s results, the validator’s output with every issue listed, are the eehrxf-fhir-validation artifact. Locally, with Java 17 or later, set FERROFED_EMIT_DIR for a test run and run scripts/checks/fhir-validator.sh over that directory with the version and checksum the job sets.

The Docs workflow builds this book and the landing page on every pull request, and publishes them from main. A book that does not build fails it. The required link check is the site-links job of ci.yml, because the Docs build is no required check.

No check fails a page that cites a closed issue as remaining work. Whether an issue is closed is the tracker’s state, and a list of closed issues committed here would be wrong the moment the next one closes, so an offline check has nothing true to compare with. A check that asks the tracker could see the state, but a page also cites closed issues on purpose, as the record of what shipped, and no rule can tell that use from a claim of remaining work. A page names a closed issue with “(now closed)” or in the past tense, and review holds it to that.

Running them locally

bash scripts/checks/versions.sh --self-test
bash scripts/checks/versions.sh
bash scripts/checks/comment-style.sh --all
bash scripts/checks/file-length.sh --self-test
bash scripts/checks/file-length.sh
bash scripts/checks/duplication.sh --self-test
bash scripts/checks/duplication.sh
bash scripts/checks/favicon-sync.sh
bash scripts/checks/conformance-matrix.sh
bash scripts/conformance/report.sh --self-test
bash scripts/checks/e2e-placement.sh --self-test
bash scripts/checks/e2e-placement.sh
bash scripts/checks/aql-splice.sh --self-test
bash scripts/checks/aql-splice.sh
bash scripts/checks/obligations.sh --self-test
bash scripts/checks/obligations.sh
bash scripts/checks/technical-documentation.sh --self-test
bash scripts/checks/technical-documentation.sh
bash scripts/checks/annex-ii.sh --self-test
bash scripts/checks/annex-ii.sh
bash scripts/checks/chapter-iii.sh --self-test
bash scripts/checks/chapter-iii.sh
bash scripts/checks/site-links.sh --self-test
bash scripts/checks/site-links.sh
bash scripts/checks/crate-version-guard.sh --self-test
bash scripts/checks/manifests.sh --self-test
bash scripts/checks/manifests.sh
for helper in fields labels migrate-fields rel; do bash "scripts/gh/$helper.sh" --self-test; done
find scripts .claude/hooks -name '*.sh' -exec shellcheck --severity=style {} +
actionlint
zizmor --min-severity=low .github/
scripts/site/assemble.sh _site

Advisory analyzers

CodeQL, OpenSSF Scorecard and SonarQube Cloud run too. Their findings are read and weighed, but they are advisory and never a reason on their own to change code.