How Do I Validate a GTFS Feed in CI?
Run gtfs-guru -i feed.zip -o report --fail-on error as a step in
your pipeline. It exits with code 2 when the feed has an error, which fails the job. On GitHub you can
use the abasis-ltd/gtfs.guru/action@v1 action instead, which also uploads SARIF annotations
to code scanning.
The CLI is a single binary with no Java runtime to install, so a validation step adds little to a pipeline. It reports the same notice codes as the browser validator.
Install the CLI
On Linux and macOS runners, the installer script downloads a release binary to
~/.local/bin:
curl -fsSL https://raw.githubusercontent.com/abasis-ltd/gtfs.guru/main/scripts/install.sh | bash
Set GTFS_GURU_VERSION (for example v1.0.0) to pin a release, or
INSTALL_DIR to install elsewhere. If the runner has a Rust toolchain,
cargo install gtfs-guru works too. Prebuilt archives for Linux, macOS and Windows are attached
to each GitHub release.
Fail the build with --fail-on
gtfs-guru -i feed.zip -o report --fail-on error
Without --fail-on, a completed validation exits 0 even when the feed has errors, so set it in
CI. The levels are none (the default), error and warning. Exit
codes:
0: validation completed and the feed met the threshold1: the run itself failed (bad arguments, unreadable input, I/O error)2: the feed did not meet--fail-on
Add --date YYYY-MM-DD to validate against a fixed day, which keeps date-dependent notices
such as expired_calendar stable between runs, and
--country_code for region-specific rules.
SARIF, JSON and HTML output
Every run writes report.json, report.html and
system_errors.json into the -o directory. Two options matter for CI:
# SARIF for code-scanning tools, written into the -o directory
gtfs-guru -i feed.zip -o report --sarif report.sarif.json --fail-on error
# JSON report on stdout, no files written
gtfs-guru -i feed.zip --stdout | jq '.summary'
Each notice in the JSON report carries its code, severity, and the file, row and field it refers to. The codes match the notice code reference, so a failing job's log can point straight at a page like foreign_key_violation.
GitHub Action
The repository ships a composite action. It installs a checksum-verified binary, runs it, uploads SARIF to code scanning, writes a summary table to the job, and fails the job on a bad feed:
jobs:
validate:
runs-on: ubuntu-latest
permissions:
contents: read
security-events: write # so SARIF reaches the Security tab
steps:
- uses: actions/checkout@v4
- uses: abasis-ltd/gtfs.guru/action@v1
with:
feed: feed.zip
fail-on: error
Use url: instead of feed: to validate a published feed, and pair it with an
on: schedule trigger to catch a live feed going stale. The action sets errors,
warnings, valid and passed outputs for later steps. The
action README lists every input and output. Uploading SARIF from a private
repository needs GitHub Advanced Security; without it the upload is skipped and the pass or fail result
is unaffected.
GitLab CI and other runners
Any runner that can execute a shell script can use the CLI directly. For GitLab:
validate:
image: ubuntu:22.04
before_script:
- apt-get update && apt-get install -y ca-certificates curl
- curl -fsSL https://raw.githubusercontent.com/abasis-ltd/gtfs.guru/main/scripts/install.sh | bash
- export PATH="$HOME/.local/bin:$PATH"
script:
- gtfs-guru -i feed.zip -o out --fail-on error
Status badge
--badge writes a shields.io endpoint file describing the run. Publish that file anywhere
reachable and reference it from your README:
gtfs-guru -i feed.zip -o report --fail-on none --badge badge/gtfs.json

The badge reads valid, 0 errors, N warnings or N errors.
--badge-svg writes a self-contained SVG instead, and --badge-label changes the
left-hand text. Use --fail-on none in the badge step, because a job that stops on the first
error never gets to write the badge.
Diff two releases of a feed
Before publishing a new feed version, compare it with the one currently live:
gtfs-guru diff old.zip new.zip --markdown diff.md --fail-on-new-errors
The diff covers agencies, routes, added, removed and moved stops, trip counts per route, frequency windows,
and changes in notice counts. With --fail-on-new-errors it exits 2 when the new feed adds
error occurrences, so a release can be blocked on regressions while known errors are still being fixed.
--json diff.json writes a machine-readable version, and --no-validation skips
the rules for a faster structural comparison.
FAQ
Why does my CI job pass even though the report shows errors?
The CLI defaults to --fail-on none. Add --fail-on error (or
warning) so it exits with code 2. The GitHub Action defaults to fail-on: error
already.
Does the CI result match what I see on gtfs.guru?
Yes, for the same feed, date and options. The CLI and the browser app run the same Rust validator.
Validating on different days can change date-dependent notices, which is why pinning
--date helps in tests.
Can I validate in Python instead of a shell step?
Yes. pip install gtfs-guru gives you the same validator as a Python module. See
how to validate GTFS in Python.
Related Questions
• How do I validate my GTFS feed?
• Why was my GTFS feed rejected?
• Which GTFS revision and validator release does GTFS Guru follow?
• Every CLI flag (docs/usage.md)
Want to see a report before wiring it into CI? Validate a feed in your browser.