CI integration
gapline is designed to be dropped into a pipeline: a single static binary, JSON output, stable exit codes. This guide shows how to wire it into common CI platforms and into a local pre-commit hook.
Install gapline in CI
Section titled “Install gapline in CI”The official install script works inside any Linux runner. Pin a version so upgrades are explicit:
curl -fsSL https://gapline.dev/install.sh | sh -s -- --version 1.0.1echo "$HOME/.local/bin" >> "$GITHUB_PATH" # or the equivalent for your CIIf your runner caches binaries between builds, restore ~/.local/bin/gapline from cache before re-running the install.
Platform recipes
Section titled “Platform recipes”name: Validate GTFS
on: [push, pull_request]
jobs: validate: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4
- name: Install gapline run: | curl -fsSL https://gapline.dev/install.sh | sh -s -- --version 1.0.1 echo "$HOME/.local/bin" >> "$GITHUB_PATH"
- name: Validate feed run: gapline validate -f data/gtfs.zip --format json -o report.json
- name: Upload report if: always() uses: actions/upload-artifact@v4 with: name: gtfs-report path: report.jsonvalidate exits non-zero on ERROR-level findings, which fails the job automatically. if: always() ensures the report is uploaded even on a red build.
validate_gtfs: image: ubuntu:24.04 before_script: - apt-get update && apt-get install -y curl - curl -fsSL https://gapline.dev/install.sh | sh -s -- --version 1.0.1 - export PATH="$HOME/.local/bin:$PATH" script: - gapline validate -f data/gtfs.zip --format json -o report.json artifacts: when: always paths: - report.json expire_in: 30 days#!/usr/bin/env bashset -euo pipefail
if ! command -v gapline >/dev/null; then curl -fsSL https://gapline.dev/install.sh | sh -s -- --version 1.0.1 export PATH="$HOME/.local/bin:$PATH"fi
gapline validate -f data/gtfs.zip --format json -o report.jsonWorks in Jenkins, Buildkite, CircleCI, Woodpecker, and anything else that can run a shell script.
Parsing the JSON report
Section titled “Parsing the JSON report”Use jq to turn the report into a human summary, a metric for a dashboard, or a PR comment.
# Top-line counts.jq '.summary' report.json
# Most frequent rules.jq -r '.errors | group_by(.rule_id) | map({rule: .[0].rule_id, count: length}) | sort_by(-.count) | .[:10] | .[] | "\(.count)\t\(.rule)"' report.jsonPost a comment on a GitHub PR
Section titled “Post a comment on a GitHub PR”summary=$(jq -r '"- \(.summary.error_count) errors\n- \(.summary.warning_count) warnings\n- \(.summary.info_count) infos"' report.json)gh pr comment "$PR_NUMBER" --body "### GTFS validation\n$summary"Gate on severity
Section titled “Gate on severity”The default behavior — fail on any ERROR — is usually what you want. Tighten the gate in stricter contexts:
# Also fail on warnings.gapline validate -f data/gtfs.zip --min-severity warning --format json -o report.jsonSee concepts / severities for the policy.
Pre-commit hook
Section titled “Pre-commit hook”A local hook keeps obviously-broken feeds out of the repo:
#!/usr/bin/env bashset -e
if git diff --cached --name-only | grep -q '^data/gtfs'; then echo "Running gapline validate on staged feed…" gapline validate -f data/gtfs.zip --min-severity errorfiMake it executable:
chmod +x .git/hooks/pre-commitFor a team-wide version that ships with the repo, use pre-commit, lefthook, or husky.
Cache the feed
Section titled “Cache the feed”If the feed is large or pulled from a slow source, cache it between runs keyed on its hash:
- name: Restore feed cache uses: actions/cache@v4 with: path: data/gtfs.zip key: gtfs-${{ hashFiles('data/gtfs.zip.sha256') }}Drop the hash file in the repo; update it when you refresh the feed. Downloading again only happens when the hash changes.
See also
Section titled “See also”gapline validate— every flag and exit code.- Output formats — the JSON schema you will be parsing.
- Concepts / Exit codes — how to branch on success vs config vs I/O errors.
- Guides / Validating feeds — the manual workflow that informs the CI version.