How Do I Validate a GTFS Feed in CI?

By Igor Akimov, maintainer of GTFS Guru

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:

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
![GTFS](https://img.shields.io/endpoint?url=https://example.org/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.