MedVertical

BlogFHIR CI/CD

How to Add a FHIR Validation Gate to GitHub Actions

A FHIR validation gate should be boring: run on every pull request, fail clearly, produce machine-readable output, and leave a path from local checks to audit-grade Records evidence.

André Sheydin
André Sheydin
Founder, MedVertical4 min read
fhirvalidationgithub-actionsci-cdnpmdeveloper-tools

Most FHIR teams start validation in the same place: someone adds the HL7 validator to a CI pipeline, points it at a folder of example resources, and calls the build red when the validator reports errors.

That is a good first step. It is also easy to make it too heavy, too noisy, or too detached from the way developers actually work.

A useful FHIR validation gate should have four properties:

  • It runs on every pull request.
  • It validates the files developers are already changing.
  • It prints failures in a format CI systems can consume.
  • It leaves a path to deeper profile, terminology, baseline, and evidence workflows later.

The public @records-fhir/cli package is built for that first layer. It gives FHIR teams a free local validation gate that runs from npm, with JSON and JUnit output for CI. Teams that need configured server validation, baselines, drift detection, or release evidence can use the same CLI against a Records API.

The minimal GitHub Actions gate

Create .github/workflows/fhir-validation.yml:

name: FHIR validationon:  pull_request:    paths:      - "fhir/**"      - "examples/**"      - ".github/workflows/fhir-validation.yml"jobs:  validate-fhir:    runs-on: ubuntu-latest    steps:      - name: Checkout        uses: actions/checkout@v4      - name: Setup Node.js        uses: actions/setup-node@v4        with:          node-version: "20"      - name: Validate local FHIR resources        run: |          npx --yes @records-fhir/cli \            validate-file ./fhir \            --engine \            --format junit > fhir-validation.xml      - name: Upload validation report        if: always()        uses: actions/upload-artifact@v4        with:          name: fhir-validation-report          path: fhir-validation.xml

This workflow does not require a Records account. It runs the CLI from npm, validates the local ./fhir folder, uses the local engine mode, and writes JUnit output so the result can be collected by CI tooling.

If your resources live somewhere else, change ./fhir to the directory that contains your test resources, sample bundles, or generated fixtures.

Why validate-file belongs in CI

FHIR validation is often treated as a certification or integration task: run a validator when a release is almost done, then fix whatever breaks. That is late. By the time CI finds a profile violation, the resource shape may already be baked into mapping code, test fixtures, API contracts, and UI assumptions.

A pull-request gate moves the feedback earlier. It does not prove that production data is valid, but it catches the obvious mistakes before they ship:

  • missing required elements
  • wrong resource types
  • invalid references
  • malformed bundles
  • profile violations in examples or generated fixtures
  • regressions introduced by mapping changes

That is the right scope for CI. Keep the gate narrow, deterministic, and cheap to run.

Make failures readable

For local debugging, plain terminal output is usually enough:

npx --yes @records-fhir/cli validate-file ./fhir --engine

For CI systems, machine-readable output is more useful:

npx --yes @records-fhir/cli validate-file ./fhir --engine --format jsonnpx --yes @records-fhir/cli validate-file ./fhir --engine --format junit

Use JSON when another script will parse the result. Use JUnit when your CI system or reporting layer expects test-style XML artifacts.

The important point is that validation output should survive the failed job. A red build with no artifact is a weak signal. A red build with a structured report is something the team can inspect, archive, and compare.

Pin the package when the gate becomes critical

For early experiments, npx --yes @records-fhir/cli is fine. Once the gate becomes part of release quality, pin the package:

- name: Validate local FHIR resources  run: |    npx --yes @records-fhir/cli@0.1.1 \      validate-file ./fhir \      --engine \      --format junit > fhir-validation.xml

Pinning avoids surprise behavior changes when a new CLI version is published. The same principle applies to profiles, terminology snapshots, and any validator dependency in your quality pipeline: if you want reproducible results, lock the inputs.

Add a Records API gate when local checks are not enough

Local validation is the first layer. It is good for pull requests and fast feedback. It does not replace configured validation against the same profiles, terminology assumptions, environments, and baselines your team uses for release decisions.

For that, run the same CLI against a Records API:

- name: Records validation gate  run: |    npx --yes @records-fhir/cli \      --api-url="${{ secrets.RECORDS_API_URL }}" \      --auth-token="${{ secrets.RECORDS_AUTH_TOKEN }}" \      validate-file ./fhir \      --format junit > records-validation.xml

That moves the workflow from a local file check to a configured Records validation run. The distinction matters:

  • local CLI gate: fast, free, developer-facing
  • Records API gate: configured, team-facing, connected to baselines and evidence

Use the local gate to stop obvious regressions. Use Records when the question becomes: did this release change validation status, drift from baseline, or produce evidence we can show later?

What CI still cannot answer

A GitHub Actions gate answers a narrow question: did this pull request introduce a validation problem in the resources we checked?

It does not answer whether production data is valid right now. It does not catch terminology drift that happens without a commit. It does not inspect legacy resources already stored in a FHIR server. It does not produce a longitudinal evidence trail by itself.

That is not a failure of CI. It is a boundary.

FHIR quality needs layers:

  • local validation while developers build resources
  • pull-request validation before changes merge
  • release validation against configured profiles and baselines
  • continuous validation against real environments
  • evidence that records what changed, when, and under which validation inputs

The GitHub Actions gate is the easiest layer to add. Start there, keep it boring, and make the next layer explicit.

Links

André Sheydin

About the author

André Sheydin

André is the founder of MedVertical and a product and design lead based in Cologne. He has spent more than 25 years shaping digital products, platforms, and design systems across complex domains, including healthcare, pharma, automotive, and SaaS. His work focuses on turning technical requirements into product structures that teams can actually build and operate.

Records

See the validation layer behind the article.

Records deploys adjacent to your FHIR server and validates your data continuously, with profile context, terminology resolution, and reproducible evidence.