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

CI runs in two tiers. The first tier gates today, on a repository with no Rust code: it covers the workflow files, the shell scripts, any Dockerfile, and the committed guards. The second tier is the Rust set, written and gated behind a job that looks for a root Cargo.toml, so it activates by itself when the workspace lands. One conclusion job reads every result and is the single required status check on main.

Tier 1, running now

CheckCommandWhat it protects
Workflow securityzizmor --min-severity=low .github/every uses: pinned to a commit SHA, no credential-persisting checkout, no context spliced into a shell, and the Dependabot cooldown window
Workflow correctnessactionlintexpressions that cannot evaluate, a needs: naming no job, unknown runner labels
Shellshellcheck --severity=style over every tracked shell programshellcheck’s lowest floor, so every finding gates
Containershadolint over every tracked Dockerfilecontainer-recipe defects; no Dockerfile exists yet, so it reports that and passes
Comment stylescripts/checks/comment-style.sh --allline comments only, // TODO(#NNNN): naming its issue, // NOTE: as a citation plus one sentence
Version driftscripts/checks/versions.shevery file that repeats a pin agrees with docs/VERSIONS.md, and no first-party file claims a licence other than BUSL-1.1
Favicon syncscripts/checks/favicon-sync.shthe book theme favicons stay byte-identical to the brand mark they are copies of
The bookmdbook build website/booka page that does not build fails the pull request

Run the same commands locally before you push. A finding costs a local run rather than a CI round trip.

Tier 2, gated on the workspace

cargo fmt --all --check, cargo clippy --workspace --all-targets --all-features -- -D warnings, cargo nextest run --workspace --locked with cargo test --doc --locked (CI runs both one package at a time through cargo hack, so the generated giants never compile side by side on the hosted runner), cargo doc under RUSTDOCFLAGS=-D warnings, cargo deny check, an MSRV check, and dependency review on pull requests. Every cargo lane runs --locked, so CI fails on lockfile drift rather than on registry drift.

The end-to-end lane, and running it locally

One more job, e2e, starts real servers in containers: a PostgreSQL for the OMOP CDM catalogue test and the reference openEHR CDR with its own database for the ITS-REST client test. It is gated on the environment variable FERROBRIDGE_E2E, and every container-backed test returns without touching Docker unless that variable is 1, so your ordinary test run stays offline. To run the lane yourself, start Docker and set the variable:

FERROBRIDGE_E2E=1 cargo nextest run --locked -p omop-cdm -p ferrobridge-server -p ferrobridge-term -p ferrobridge-testkit

The first run pulls the images, which are pinned by digest in docs/VERSIONS.md; later runs start in a few seconds. Each test owns its containers and stops them when it ends.

The conformance pass lists

Neither FHIRconnect nor OMOCL has an external conformance suite, so the corpus tests are the instrument. Eleven corpora are measured, and each keeps a committed pass list under conformance/<corpus>/pass-list.txt: one passing case id per line, then a total line with the corpus size. The HL7 v2 messages count twice: the vendored sets are the hl7v2 corpus and the sets fetched at build time the hl7v2-smoke corpus. HL7’s examples package of each FHIR version is fetched at build time too, and five corpora read it: one per version through the fhir-types model (fhir-r4, fhir-r4b, fhir-r5, fhir-r6) and the R4 package through the facade (fhir-r4-facade).

CorpusA case passes when
FHIRconnect mapping librarythe file parses; it validates against the published schemas (modules/ROOT/attachments/, schema/schema.adoc) or is in the pinned rejection set; it validates against FerroBRIDGE’s strict schemas; it loads into the library set with no refusal naming it; and a program the suites compile against a template carrying its archetype reaches it (types-of-mapping-files/context-mappings.adoc)
OMOCL mapping librarythe file parses, validates against the authored schema, passes the rules of one file, and loads into the library set with every Include resolved
FHIR round-trip lawsPutGet and GetPut both run on the chain and each declares exactly the set its reviewed snapshot pins
FHIRconnect REST API (draft)the wire contract reads every in parameter and part the FSH operation definition declares, and the operation answers only the out parameters it declares, each min = 1 one present (rest-api.adoc, draft)
HL7 v2 message corpora, HL7 v2 smoke corporathe message is framed, decoded and parsed with no refusal, and the Bundle the map writes decodes as R4 and opens with its MessageHeader (bdl-12); an acknowledgment passes when it parses
FHIR R4, FHIR R4B, FHIR R5, FHIR R6the example decodes through the strict JSON codec of its version, re-encodes to the same document, and goes through the XML codec and back unchanged, compared as the lexical document model (members by name, numbers in their written text); a refusal or the first difference is the failure reason
FHIR R4 facadefor each context that maps the example’s type (the suite’s and the KDS diagnosis project), the create answers 201 and the read 200; the read decodes as an R4 instance; no element the example carries comes back with another value (PutGet); and a second create and read of the answer gives it back unchanged. The identity, meta and the subject are the facade’s and leave the comparison. An example of a type no context maps is counted by type under set_aside in the result file, never a case

A case the list records that no longer passes fails the corpus test itself, so a regression fails CI. When your change makes a case pass, or the corpus changes size, the conformance job fails until you record it:

scripts/checks/conformance.sh --update

Commit the rewritten lists and badges with the change. Without a flag the script compares and reports without failing on new passes; --check is what CI runs. It needs cargo-nextest and jq. Never edit a list by hand, and never remove a case from one to make CI green.

The badges and the README block

The script writes one shields.io endpoint badge per file under conformance/badges/. A badge’s message is the passing and the total count (68 / 120), and its colour follows the passing share: red under a quarter, orange under a half, yellow under three quarters, green from three quarters, and brightgreen when every case passes.

  • <corpus>.json: one per corpus, counted from its pass list.
  • hl7v2-family-<family>.json: one per HL7 v2 message family with at least one case in either HL7 v2 corpus, ordered by family code, labelled with the family and counted over both corpora. A family the face maps none of shows red with its passing count at zero. The corpus test records each case’s family, MSH-9.1 or else the code before _ in MSH-9.3, in the result file.
  • hl7v2-version-<version>.json: one per MSH-12 version a case of either HL7 v2 corpus declares, labelled v2.5.1 and so on.

The script also generates the README block between <!-- badges:begin --> and <!-- badges:end --> from those files, in a fixed order, so never edit it by hand: the build badges, a blank line, then one row per standard (FHIRconnect 1.0.0 with the mapping library and the draft REST API, OMOCL 1.0.0, FHIR with the four model corpora, the round-trip laws and the R4 facade, and HL7 v2 with its two corpora, its family badges and its version badges). Every badge links to its pass list, and the family and version badges link to the hl7v2 list. --update rewrites the block and removes a family or version badge no case counts any more; --check fails when a badge file or the block disagrees with what --update would write.

Vendored inputs and the build-time fetch

Every external corpus enters through a committed script under scripts/vendor/, which reads its pin from docs/VERSIONS.md and writes a PROVENANCE.md beside what it fetches. The HL7 FHIR packages, the v2-to-FHIR implementation guide among them, go to tools/fhir-codegen/vendor/<package>/ through scripts/vendor/fhir-packages.sh, and the specification corpora go to docs/specs/. scripts/checks/versions.sh reads every provenance file back against its pin.

The FHIR examples packages are CC0, but at about 600 MB unpacked they would more than double the committed FHIR packages, so scripts/vendor/fhir-packages.sh --build-time fetches them into tools/ferrobridge-testkit/vendor/<package>/package/, which .gitignore refuses except for each committed PROVENANCE.md. The script checks the file count and tree digest the pin records, keeps a tree already at both, and --cache-key names the key CI restores them under. Run it before the FHIR corpus tests; without the packages those tests say they skipped.

The HL7 v2 definitions are the one input whose terms do not permit redistribution, so they are never committed. scripts/vendor/v2ig.sh fetches them at build time into tools/fhir-codegen/vendor/hl7-v2ig/, which .gitignore refuses except for its committed PROVENANCE.md. The script checks the file count and tree digest the pin records on every run. Run it before the codegen drift check, as the codegen-drift job does:

scripts/vendor/v2ig.sh
cargo run --locked -p fhir-codegen -- emit --check

To move the pin, change the commit in docs/VERSIONS.md, run scripts/vendor/v2ig.sh --stamp, and copy the file count and digest it prints into the same row.

The sqlx query metadata

omop-cdm checks its SQL at compile time from the metadata committed under crates/omop-cdm/.sqlx/, and the sqlx-offline job fails when that metadata is stale. After you change a query in omop-cdm, start Docker, install sqlx-cli at the version docs/VERSIONS.md pins, and regenerate:

scripts/checks/sqlx-offline.sh

Commit the rewritten .sqlx/ files with the query change. With DATABASE_URL set in your environment the sqlx macros connect to that database instead of reading the metadata, so leave it unset for an ordinary build.

Workflow security rules

Every workflow follows the same four rules, and the analysers above check them:

  • Every uses: is pinned to a full commit SHA with a trailing version comment.
  • permissions: {} at workflow level, with the minimum granted per job.
  • persist-credentials: false on every checkout that does not push with git.
  • No ${{ }} interpolation inside a run: block; context travels through env:.

Advisory analysers

CodeQL scans the workflow files, because a workflow that holds a token is code. SonarQube Cloud runs a multi-language sweep over shell, YAML, and JSON. OpenSSF Scorecard scores the repository’s security posture. All three are advisory: they gate no merge, and a finding never outranks a specification citation or a project rule. A wrong finding is recorded on the tracker rather than suppressed quietly.

Building this book

$ cargo install mdbook mdbook-toc mdbook-mermaid
$ mdbook serve website/book

Use the versions pinned in docs/VERSIONS.md, which are the ones CI installs. The site as GitHub Pages serves it, the landing page at the root and the book under /docs/, is assembled by scripts/site/assemble.sh _site. That script renders the roadmap block from the open milestones when gh is authenticated, and leaves the block empty without failing when it is not.