Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
18 changes: 18 additions & 0 deletions .github/workflows/test.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,24 @@ on:
pull_request:

jobs:
renderer:
name: Template renderer tests
runs-on: ubuntu-latest
Comment thread
coderabbitai[bot] marked this conversation as resolved.
permissions:
contents: read
steps:
- name: Checkout code
uses: actions/checkout@v4

- name: Set up Go
uses: actions/setup-go@v5
with:
Comment on lines +16 to +21
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot May 24, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Find unpinned uses (not @<40-hex-sha>) and checkout steps missing persist-credentials.
rg -n --iglob '.github/workflows/*.y*ml' 'uses:\s*[^[:space:]]+@([^[:space:]]+)$'
rg -n --iglob '.github/workflows/*.y*ml' 'uses:\s*actions/checkout@'
rg -n --iglob '.github/workflows/*.y*ml' 'persist-credentials:\s*false'

Repository: OneBusAway/docker

Length of output: 508


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Workflow files =="
ls -la .github/workflows || true
fd -e yml -e yaml .github/workflows -a || true

echo
echo "== test.yaml (relevant section) =="
if [ -f .github/workflows/test.yaml ]; then
  nl -ba .github/workflows/test.yaml | sed -n '1,120p'
else
  echo "MISSING: .github/workflows/test.yaml"
  # Try to find similarly named file(s)
  fd -a "test.*y*ml" .github/workflows || true
fi

echo
echo "== Search for checkout/setup-go/persist-credentials =="
# Use a broad search path that will actually match existing files
rg -n --hidden --glob '.github/workflows/*.{yml,yaml}' \
  'uses:\s*actions/(checkout|setup-go)@' .github/workflows || true

rg -n --hidden --glob '.github/workflows/*.{yml,yaml}' \
  'persist-credentials:\s*false' .github/workflows || true

rg -n --hidden --glob '.github/workflows/*.{yml,yaml}' \
  'uses:\s*[^[:space:]]+@v[0-9]+' .github/workflows || true

Repository: OneBusAway/docker

Length of output: 785


🏁 Script executed:

#!/bin/bash
# If .github/workflows doesn't exist, list repository top-level quickly (non-destructive)
ls -la .github || true
ls -la . || true

Repository: OneBusAway/docker

Length of output: 1680


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== test.yaml lines 1-80 =="
sed -n '1,80p' .github/workflows/test.yaml | nl -ba 2>/dev/null || sed -n '1,80p' .github/workflows/test.yaml

echo
echo "== test.yaml lines 10-40 (focus) =="
sed -n '10,40p' .github/workflows/test.yaml

echo
echo "== docker.yaml lines 1-120 =="
sed -n '1,120p' .github/workflows/docker.yaml

echo
echo "== rg: actions/checkout and persist-credentials =="
rg -n '.github/workflows/.*' --no-heading 'uses:\s*actions/checkout@' .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null || true
rg -n 'persist-credentials:\s*false' .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null || true

echo
echo "== rg: actions/setup-go =="
rg -n 'uses:\s*actions/setup-go@' .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null || true

echo
echo "== rg: any uses pinned to full commit SHAs? (40 hex) =="
rg -n 'uses:\s*(actions/checkout|actions/setup-go)@[0-9a-f]{40}' .github/workflows/*.yml .github/workflows/*.yaml 2>/dev/null || true

Repository: OneBusAway/docker

Length of output: 6470


🏁 Script executed:

#!/bin/bash
# fallback: print with awk line numbers if nl missing
awk 'NR>=1 && NR<=80 {printf "%5d  %s\n", NR, $0}' .github/workflows/test.yaml | head -n 90

Repository: OneBusAway/docker

Length of output: 2283


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== Any persist-credentials settings in workflows =="
rg -n 'persist-credentials:' .github/workflows/*.yml .github/workflows/*.yaml || true

echo
echo "== All actions/checkout@ usages (with nearby context) =="
rg -n --context 2 'uses:\s*actions/checkout@' .github/workflows/*.yml .github/workflows/*.yaml

echo
echo "== All actions/setup-go@ usages (with nearby context) =="
rg -n --context 2 'uses:\s*actions/setup-go@' .github/workflows/*.yml .github/workflows/*.yaml

Repository: OneBusAway/docker

Length of output: 2082


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo "== workflow yaml files =="
ls -la .github/workflows

echo
echo "== persist-credentials settings (yaml only) =="
rg -n 'persist-credentials:' .github/workflows/*.yaml || true

echo
echo "== actions/checkout@ usages (yaml only) =="
rg -n 'uses:\s*actions/checkout@' .github/workflows/*.yaml

echo
echo "== actions/setup-go@ usages (yaml only) =="
rg -n 'uses:\s*actions/setup-go@' .github/workflows/*.yaml || true

echo
echo "== checkout steps: show 'with:' block if present nearby (test.yaml) =="
rg -n --context 6 'uses:\s*actions/checkout@' .github/workflows/test.yaml

Repository: OneBusAway/docker

Length of output: 2248


Harden GitHub Actions: pin action SHAs and disable checkout credential persistence.

.github/workflows/test.yaml uses tag-based actions/checkout@v4 and actions/setup-go@v5, and none of the checkout steps set persist-credentials: false (same unpinned checkout pattern exists in .github/workflows/docker.yaml).

Suggested patch
       - name: Checkout code
-        uses: actions/checkout@v4
+        uses: actions/checkout@<full-commit-sha>
+        with:
+          persist-credentials: false

       - name: Set up Go
-        uses: actions/setup-go@v5
+        uses: actions/setup-go@<full-commit-sha>
         with:
           go-version: "1.22"
🧰 Tools
🪛 zizmor (1.25.2)

[warning] 14-15: credential persistence through GitHub Actions artifacts (artipacked): does not set persist-credentials: false

(artipacked)


[error] 15-15: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)


[error] 18-18: unpinned action reference (unpinned-uses): action is not pinned to a hash (required by blanket policy)

(unpinned-uses)

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In @.github/workflows/test.yaml around lines 14 - 19, Replace tag-based GitHub
Actions and enable non-persistent checkout: change uses: actions/checkout@v4 and
uses: actions/setup-go@v5 to pinned commit SHAs for those actions, and add
persist-credentials: false to the checkout step (the actions/checkout usage) to
disable credential persistence; apply the same changes to the other workflow
referenced (the actions/checkout step in the docker workflow). Ensure you update
the specific occurrences of "actions/checkout" and "actions/setup-go" in the
workflows so they reference commit SHAs rather than version tags and add the
persist-credentials: false field to each checkout step.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Deferring this one. No workflow in this repo pins actions to commit SHAs or sets persist-credentials: falsedocker.yaml has several tag-pinned actions and there is no dependabot config. Applying it to only the new renderer job would create inconsistency without closing the actual exposure, so this is better handled as a separate repo-wide CI-hardening pass (ideally dependabot-managed) rather than scope-creeping this feature PR.

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@aaronbrethorst, that's a reasonable call — partial hardening of only the new job without addressing the rest of the repo would give a false sense of security without actually reducing the attack surface. A dependabot-managed, repo-wide pass is the right approach.

Would you like me to open a follow-up GitHub issue to track the repo-wide CI hardening work (pinning all action SHAs + persist-credentials: false + adding a Dependabot config for Actions)?

go-version: "1.22"

- name: Run renderer tests
working-directory: oba/config/template_renderer
run: go test ./...

image:
name: Build Docker Image
runs-on: ubuntu-latest
Expand Down
1 change: 1 addition & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@ You can find the latest published Docker images on Docker Hub:
* `GTFS_URL` - The URL to the GTFS feed you want to use.
* GTFS-RT Support (Optional)
* `TZ` - The timezone for the server. Ensure that the server's timezone matches the timezone specified in your static GTFS `agency.txt` file. The timezone format is the IANA standard, and [a full list of timezones can be found on Wikipedia](https://en.wikipedia.org/wiki/List_of_tz_database_time_zones#List).
* `GTFS_RT_FEEDS` - Preferred for configuring one OR many GTFS-RT feeds. A JSON array of feed objects; each object may contain `tripUpdatesUrl`, `vehiclePositionsUrl`, `alertsUrl`, `refreshInterval`, `agencyIds` (array), `feedApiKey`, and `feedApiValue`. Example: `[{"tripUpdatesUrl":"https://a/trips","agencyIds":["unitrans"]},{"vehiclePositionsUrl":"https://b/veh","agencyIds":["kcm"]}]`. When set, it takes precedence over the single-feed variables below.
* `ALERTS_URL` - Service Alerts URL for GTFS-RT.
* `TRIP_UPDATES_URL` - Trip Updates URL for GTFS-RT.
* `VEHICLE_POSITIONS_URL` - Vehicle Positions URL for GTFS-RT.
Expand Down
112 changes: 112 additions & 0 deletions deployment-examples/multiple-rt-feeds/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,112 @@
# Multiple GTFS-RT Feeds Example

This example runs a single OneBusAway server that serves **two transit agencies'
realtime data at once** — King County Metro and Pierce Transit — using the
`GTFS_RT_FEEDS` environment variable.

OneBusAway supports many `GtfsRealtimeSource` beans (one per realtime feed).
`GTFS_RT_FEEDS` exposes that: it takes a JSON array of feed
objects and renders one bean per feed. It supersedes the legacy single-feed
variables (`TRIP_UPDATES_URL`, `VEHICLE_POSITIONS_URL`, `ALERTS_URL`,
`AGENCY_ID`, …), which still work for a single feed when `GTFS_RT_FEEDS` is unset.

## What's configured

| Agency | `agency_id` | Static GTFS | Realtime |
|---|---|---|---|
| King County Metro | `1` | consolidated feed (below) | raw GTFS-RT protobufs from `kcm-alerts-realtime-prod` |
| Pierce Transit | `3` | consolidated feed (below) | relayed through the Puget Sound OneBusAway GTFS-RT endpoints |

**Static data** comes from the consolidated Puget Sound GTFS feed
(`https://gtfs.sound.obaweb.org/prod/gtfs_puget_sound_consolidated.zip`), which
already contains both agencies (and several others). Because it's a single zip,
the normal single-feed bundle builder handles it without any changes.

**Realtime data** is configured with two feeds in `GTFS_RT_FEEDS`. Each feed's
`agencyIds` tells OneBusAway which bundle agency the feed's entities belong to,
so the two protobuf streams line up with agencies `1` and `3` in the bundle:

```json
[
{
"tripUpdatesUrl": "https://s3.amazonaws.com/kcm-alerts-realtime-prod/tripupdates.pb",
"vehiclePositionsUrl": "https://s3.amazonaws.com/kcm-alerts-realtime-prod/vehiclepositions.pb",
"alertsUrl": "https://s3.amazonaws.com/kcm-alerts-realtime-prod/alerts.pb",
"refreshInterval": "30",
"agencyIds": ["1"]
},
{
"tripUpdatesUrl": "https://api.pugetsound.onebusaway.org/api/gtfs_realtime/trip-updates-for-agency/3.pb?key=org.onebusaway.iphone",
"vehiclePositionsUrl": "https://api.pugetsound.onebusaway.org/api/gtfs_realtime/vehicle-positions-for-agency/3.pb?key=org.onebusaway.iphone",
"alertsUrl": "https://api.pugetsound.onebusaway.org/api/gtfs_realtime/alerts-for-agency/3.pb?key=org.onebusaway.iphone",
"refreshInterval": "30",
"agencyIds": ["3"]
}
]
```

> **Note:** `oba_app` is built from this repository's `../../oba` directory, not
> the published `opentransitsoftwarefoundation/onebusaway-api-webapp:2.7.1-latest`
> image, because `GTFS_RT_FEEDS` support is not in a published release yet. Once a
> 2.7.1 image that understands `GTFS_RT_FEEDS` is published, an immutable
> deployment can bake these env vars into a `Dockerfile` that is `FROM` that
> image instead — see [`../immutable`](../immutable) for that pattern.

## Running it

From this directory:

```bash
docker compose up -d --build
```

This:

1. Runs `oba_bundler` once to build the consolidated bundle into a shared volume
(this downloads ~38 MB of GTFS and takes a few minutes).
2. Starts PostgreSQL.
3. Starts `oba_app` after the bundle finishes, configured with both realtime feeds.

Watch the bundle build and app startup:

```bash
docker compose logs -f oba_bundler # bundle build
docker compose logs -f oba_app # config render + Tomcat startup
```

The API is available at <http://localhost:8080> once `oba_app` is up.

## Validating

Run the repository's validation script (from the repo root) against the running server:

```bash
./bin/validate.sh
```

Then confirm **realtime** is flowing for **both** agencies. The most direct
check is live vehicle positions — each count should be non-zero during service
hours, and it proves each feed's entities mapped onto the right bundle agency:

```bash
# King County Metro (agency 1) — live vehicles from its realtime feed
curl -s "http://localhost:8080/api/where/vehicles-for-agency/1.json?key=test" | jq '.data.list | length'

# Pierce Transit (agency 3) — live vehicles from its realtime feed
curl -s "http://localhost:8080/api/where/vehicles-for-agency/3.json?key=test" | jq '.data.list | length'
```

To see the realtime status of a specific trip, pull an active trip from a
vehicle and inspect its status — look for `"predicted": true`:

```bash
TRIP=$(curl -s "http://localhost:8080/api/where/vehicles-for-agency/1.json?key=test" | jq -r '.data.list[0].tripId')
curl -s "http://localhost:8080/api/where/trip-details/$TRIP.json?key=test" \
| jq '.data.entry.status | {predicted, scheduleDeviation, vehicleId, lastUpdateTime}'
```

## Tearing down

```bash
docker compose down -v # -v also removes the bundle and database volumes
```
67 changes: 67 additions & 0 deletions deployment-examples/multiple-rt-feeds/docker-compose.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
# Multiple GTFS-RT feeds example: King County Metro + Pierce Transit.
#
# Demonstrates the GTFS_RT_FEEDS env var, which configures one OR many GTFS-RT
# realtime feeds (one GtfsRealtimeSource bean per feed). Static data comes from
# the consolidated Puget Sound GTFS feed, which already contains both agencies
# (agency_id 1 = King County Metro, agency_id 3 = Pierce Transit), so the
# single-feed bundle builder handles it as-is.
#
# NOTE: oba_app is built from this repo's ../../oba directory rather than the
# published opentransitsoftwarefoundation/onebusaway-api-webapp:2.7.1-latest
# image, because GTFS_RT_FEEDS support is not in a published release yet. Once a
# 2.7.1 image that understands GTFS_RT_FEEDS is published, an immutable
# deployment can instead bake these env vars into a Dockerfile FROM that image
# (see ../immutable for that pattern).

services:
# One-shot job: builds the consolidated bundle into the shared volume, then exits.
oba_bundler:
build:
context: ../../bundler
volumes:
- oba-bundle:/bundle
environment:
- GTFS_URL=https://gtfs.sound.obaweb.org/prod/gtfs_puget_sound_consolidated.zip

oba_database_pg:
image: postgres:16
environment:
POSTGRES_USER: oba_user
POSTGRES_PASSWORD: oba_password
POSTGRES_DB: oba_database
# No host port mapping: oba_app reaches the database over the compose
# network, so there's no need to expose 5432 (and it avoids colliding with
# a Postgres already running on the host).
volumes:
- pg-data:/var/lib/postgresql/data
restart: always

oba_app:
depends_on:
oba_database_pg:
condition: service_started
# Wait for the bundle build to finish before starting the app.
oba_bundler:
condition: service_completed_successfully
build:
context: ../../oba
environment:
- JDBC_URL=jdbc:postgresql://oba_database_pg:5432/oba_database
- JDBC_DRIVER=org.postgresql.Driver
- JDBC_USER=oba_user
- JDBC_PASSWORD=oba_password
- TEST_API_KEY=test # For testing only; remove in production.
- TZ=America/Los_Angeles
# Two realtime feeds, one per agency. KCM publishes raw GTFS-RT protobufs;
# Pierce is relayed through the Puget Sound OneBusAway GTFS-RT endpoints.
# agencyIds maps each feed's entities onto the matching bundle agency.
- 'GTFS_RT_FEEDS=[{"tripUpdatesUrl":"https://s3.amazonaws.com/kcm-alerts-realtime-prod/tripupdates.pb","vehiclePositionsUrl":"https://s3.amazonaws.com/kcm-alerts-realtime-prod/vehiclepositions.pb","alertsUrl":"https://s3.amazonaws.com/kcm-alerts-realtime-prod/alerts.pb","refreshInterval":"30","agencyIds":["1"]},{"tripUpdatesUrl":"https://api.pugetsound.onebusaway.org/api/gtfs_realtime/trip-updates-for-agency/3.pb?key=org.onebusaway.iphone","vehiclePositionsUrl":"https://api.pugetsound.onebusaway.org/api/gtfs_realtime/vehicle-positions-for-agency/3.pb?key=org.onebusaway.iphone","alertsUrl":"https://api.pugetsound.onebusaway.org/api/gtfs_realtime/alerts-for-agency/3.pb?key=org.onebusaway.iphone","refreshInterval":"30","agencyIds":["3"]}]'
volumes:
- oba-bundle:/bundle
ports:
- "8080:8080"
- "1234:1234" # JMX exporter port

volumes:
oba-bundle:
pg-data:
Loading
Loading