Skip to content
Draft
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
17 changes: 14 additions & 3 deletions .github/workflows/docker.yml
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,11 @@ jobs:
# drift from the actual build. Image name is lower-cased for GHCR.
- name: Compute image tags
id: tags
env:
# `latest` (and release semantics) only apply to actual releases:
# a pushed `v*` tag or a published release. Plain merges to main build
# a 0.0.0-SNAPSHOT dev image and must never move `latest`.
IS_RELEASE: ${{ startsWith(github.ref, 'refs/tags/v') || github.event_name == 'release' }}
run: |
set -euo pipefail
PROFILE_ARG=""
Expand All @@ -55,12 +60,16 @@ jobs:
API_VERSION=$(mvn -q -N $PROFILE_ARG help:evaluate -Dexpression=project.version -DforceStdout)
VALIDATOR_VERSION=$(mvn -q -N $PROFILE_ARG help:evaluate -Dexpression=gtfs-validator.version -DforceStdout)
IMAGE=$(echo "${REGISTRY}/${IMAGE_NAME}" | tr '[:upper:]' '[:lower:]')
# Fork-aware OCI source URL (lower-cased) so the package links to the
# repo it was built from, not a hardcoded upstream.
REPO_LC=$(echo "${GITHUB_REPOSITORY}" | tr '[:upper:]' '[:lower:]')
echo "image_source=https://github.com/${REPO_LC}.git" >> "$GITHUB_OUTPUT"
# Self-documenting tag: <apiVersion>-validator<validatorCoreVersion>.
# The `validator` infix scopes the trailing version to the validator
# core, not the API (the two versions evolve independently).
DUAL_TAG="${IMAGE}:${API_VERSION}-validator${VALIDATOR_VERSION}"
TAGS="${DUAL_TAG}"
if [ "${{ matrix.latest }}" = "true" ]; then
if [ "${{ matrix.latest }}" = "true" ] && [ "$IS_RELEASE" = "true" ]; then
TAGS="${TAGS}"$'\n'"${IMAGE}:latest"
fi
{
Expand All @@ -75,8 +84,9 @@ jobs:
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v4
# Only authenticate and push for events that are not pull requests.
# Pull requests still build the image to validate the Dockerfile.
# Authenticate and push on all non-PR events: merges to main publish API
# snapshot images (version 0.0.0-SNAPSHOT), and `v*` tags / releases publish
# versioned images. Pull requests build only, to validate the Dockerfile.
- name: Log in to the Container registry
if: github.event_name != 'pull_request'
uses: docker/login-action@v4
Expand All @@ -90,6 +100,7 @@ jobs:
context: .
build-args: |
MAVEN_PROFILES=${{ matrix.maven_profiles }}
IMAGE_SOURCE=${{ steps.tags.outputs.image_source }}
platforms: linux/amd64,linux/arm64
push: ${{ github.event_name != 'pull_request' }}
tags: ${{ steps.tags.outputs.tags }}
Expand Down
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ replay_pid*
target/
HELP.md

# maven-git-versioning-extension generated pom (build artifact)
.git-versioned-pom.xml

# IDE
.idea/
*.iml
Expand Down
13 changes: 13 additions & 0 deletions .mvn/extensions.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Derives the Maven project version from git at build time so it always
matches the release tag, without ever editing pom.xml. See the rules in
.mvn/maven-git-versioning-extension.xml. -->
<extensions xmlns="http://maven.apache.org/EXTENSIONS/1.2.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/EXTENSIONS/1.2.0 https://maven.apache.org/xsd/core-extensions-1.2.0.xsd">
<extension>
<groupId>me.qoomon</groupId>
<artifactId>maven-git-versioning-extension</artifactId>
<version>9.12.1</version>
</extension>
</extensions>
32 changes: 32 additions & 0 deletions .mvn/maven-git-versioning-extension.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- Git versioning rules for me.qoomon:maven-git-versioning-extension.

The release tag is the single source of truth: a git tag `vX.Y.Z` produces
project version `X.Y.Z`. Any other build (commits on main, PRs, or local
checkouts without a release tag) produces `0.0.0-SNAPSHOT`. pom.xml keeps
the placeholder version `0.0.0-SNAPSHOT` and is never edited for a release.

The API version stays independent of the validator core version. -->
<configuration xmlns="https://github.com/qoomon/maven-git-versioning-extension"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="https://github.com/qoomon/maven-git-versioning-extension https://qoomon.github.io/maven-git-versioning-extension/configuration-9.4.0.xsd">

<refs>
<!-- Release tags like v1.0.0 -> version 1.0.0 -->
<ref type="tag">
<pattern><![CDATA[v(?<version>[0-9]+\.[0-9]+\.[0-9]+)]]></pattern>
<version>${ref.version}</version>
</ref>

<!-- Any branch (main, PRs, features) -> fixed dev SNAPSHOT version -->
<ref type="branch">
<pattern>.+</pattern>
<version>0.0.0-SNAPSHOT</version>
</ref>
</refs>

<!-- Fallback for detached HEAD / commit builds with no matching ref -->
<rev>
<version>0.0.0-SNAPSHOT</version>
</rev>
</configuration>
8 changes: 8 additions & 0 deletions Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,14 @@ RUN mvn -B -q ${MAVEN_PROFILES:+-P}${MAVEN_PROFILES} clean package -DskipTests

# --- Runtime stage -----------------------------------------------------------
FROM eclipse-temurin:17-jre

# Link the published GHCR package to its source repository so it appears in the
# repo's Packages section (and inherits the repo for permissions/visibility).
# Overridable so forks publish under their own repo: CI passes the lower-cased
# ${{ github.repository }}. The default keeps local builds labelled sensibly.
ARG IMAGE_SOURCE="https://github.com/mobilitydata/gtfs-validator-api.git"
LABEL org.opencontainers.image.source="${IMAGE_SOURCE}"

RUN groupadd -r spring && useradd -r -g spring spring

WORKDIR /app
Expand Down
126 changes: 114 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -17,10 +17,17 @@ The API project version is **independent** of the validator core version:

| Version | Where | Current |
|---------|-------|---------|
| API project version | `pom.xml` | `1.0.0` |
| API project version | git tag `vX.Y.Z` (derived at build time) | see releases |
| Validator core (dependency) | `gtfs-validator.version` property | `8.0.1` |
| OpenAPI spec version | `docs/GTFSValidatorAPI.yaml` `info.version` | `2.0.0` |

The API project version is **derived from git** by the
[`maven-git-versioning-extension`](https://github.com/qoomon/maven-git-versioning-extension)
(configured in `.mvn/`): a release tag `vX.Y.Z` yields version `X.Y.Z`, while any
other build (commits on `main`, PRs, local checkouts) yields `0.0.0-SNAPSHOT`. The
`<version>` in `pom.xml` is only a placeholder and is never edited for a release —
see [Releasing](#releasing).

The validator core version is reported at runtime by `GET /v2/metadata`.

## Endpoints
Expand Down Expand Up @@ -132,36 +139,131 @@ docker build --build-arg MAVEN_PROFILES=snapshot -t gtfs-validator-api:snapshot
### Pre-built image

CI publishes multi-arch images (`linux/amd64`, `linux/arm64`) to the GitHub
Container Registry on every push to the default branch and on version tags. Two
variants are published:
Container Registry. The image tag encodes both the API version (derived from the
git tag) and the validator core version. Two variants are published per build:

| Variant | Tags | Validator core |
|---------|------|----------------|
| Stable | `latest`, `1.0.0-validator8.0.1` | stable release |
| Snapshot | `1.0.0-validator8.0.2-SNAPSHOT` | pre-release SNAPSHOT |
| Variant | Example release tag | Example main-merge tag | Validator core |
|---------|--------------------|------------------------|----------------|
| Stable (`stable-core`) | `1.0.0-validator8.0.1` (+ `latest`) | `0.0.0-SNAPSHOT-validator8.0.1` | stable release |
| Snapshot (`snapshot-core`) | `1.0.0-validator8.0.2-SNAPSHOT` | `0.0.0-SNAPSHOT-validator8.0.2-SNAPSHOT` | pre-release SNAPSHOT |

The tag format is `<apiVersion>-validator<validatorCoreVersion>`: the `validator`
infix scopes the trailing version to the validator **core**, not the API (the two
versions evolve independently). `latest` always points at the stable variant; the
snapshot image is never tagged `latest`.
versions evolve independently). Both variants are published on every merge to
`main` (as API snapshots, version `0.0.0-SNAPSHOT`) and on every release (versioned
`X.Y.Z`); the stable variant additionally gets `latest` on releases only. The
snapshot variant is never tagged `latest`. See [Releasing](#releasing) for how
versions are produced.

#### Using the published images

The images live at `ghcr.io/mobilitydata/gtfs-validator-api`. They are public, so
no login is required to pull. Browse all available tags on the
[package page](https://github.com/MobilityData/gtfs-validator-api/pkgs/container/gtfs-validator-api).

**Stable** — recommended for normal use; built against a released validator core:

```bash
# Stable
# `latest` always points at the most recent release
docker pull ghcr.io/mobilitydata/gtfs-validator-api:latest
docker run --rm -p 8080:8080 ghcr.io/mobilitydata/gtfs-validator-api:latest

# Snapshot (built against the validator core SNAPSHOT)
# …or pin an exact, immutable version
docker run --rm -p 8080:8080 \
ghcr.io/mobilitydata/gtfs-validator-api:1.0.0-validator8.0.1
```

**Snapshot** — for trying the latest validator core before it is released; built
against a `-SNAPSHOT` of the validator. The tag is re-published as the upstream
snapshot moves, so re-pull to get the newest build:

```bash
docker pull ghcr.io/mobilitydata/gtfs-validator-api:1.0.0-validator8.0.2-SNAPSHOT
docker run --rm -p 8080:8080 \
ghcr.io/mobilitydata/gtfs-validator-api:1.0.0-validator8.0.2-SNAPSHOT
```

Once a container is running, the API is available on port `8080` regardless of
which image you chose:

```bash
# Confirm which validator core the running image uses
curl http://localhost:8080/v2/metadata

# Validate a feed
curl -X POST http://localhost:8080/v2/validate-upload \
-F "file=@feed.zip" -F "countryCode=CA" -H "Accept: application/json"
```

Pass JVM options via `JAVA_OPTS`, e.g. `-e JAVA_OPTS="-Xmx4g"`, and activate the
structured-logging profile with `-e SPRING_PROFILES_ACTIVE=json`. If you have
pinned the package to **private** visibility, authenticate first:

```bash
echo "$GITHUB_TOKEN" | docker login ghcr.io -u <username> --password-stdin
```

## Releasing

The API version is **derived from the git tag** at build time by the
[`maven-git-versioning-extension`](https://github.com/qoomon/maven-git-versioning-extension)
(see `.mvn/`); `pom.xml` keeps the placeholder `0.0.0-SNAPSHOT` and is **never**
edited by hand for a release. To cut a release and publish images:

1. Make sure `main` is green and the desired changes are merged.
2. Create and push a semver tag prefixed with `v`:

```bash
git tag v1.0.0
git push origin v1.0.0
```

(Or publish a GitHub Release with that tag — either triggers the same flow.)
3. The `docker.yml` workflow then builds both variants and **publishes**:
- stable: `ghcr.io/<owner>/gtfs-validator-api:1.0.0-validator<core>` **and** `:latest`
- snapshot: `ghcr.io/<owner>/gtfs-validator-api:1.0.0-validator<coreSnapshot>`

Notes:
- Every merge to `main` publishes both variants as **API snapshots** (version
`0.0.0-SNAPSHOT`, e.g. `0.0.0-SNAPSHOT-validator8.0.1`), without moving `latest`.
- A release (`v*` tag / GitHub Release) publishes both variants **versioned**
(`X.Y.Z-…`); the stable variant also updates `latest`.
- Pull requests build both variants to validate the Dockerfile but publish nothing.
- The tag must match `v` + semver (e.g. `v1.2.3`); other tags don't set the version.

### What gets published when

Image tags follow `<apiVersion>-validator<validatorCoreVersion>`. The **API version**
comes from git (a release tag, or `0.0.0-SNAPSHOT` otherwise) and the **validator
core version** comes from the build variant. These are independent: a `main` build
is an *API snapshot*, which is not the same thing as the validator-core snapshot.

**On merge to `main`** the API version resolves to `0.0.0-SNAPSHOT`, so both variants
publish as API snapshots (and `latest` is not moved):

| Variant | Tag |
|---------|-----|
| stable-core | `0.0.0-SNAPSHOT-validator8.0.1` |
| snapshot-core | `0.0.0-SNAPSHOT-validator8.0.2-SNAPSHOT` |

**On release** (`vX.Y.Z` tag / GitHub Release) the API version resolves to `X.Y.Z`:

| Variant | Tag |
|---------|-----|
| stable-core | `X.Y.Z-validator8.0.1` **+ `latest`** |
| snapshot-core | `X.Y.Z-validator8.0.2-SNAPSHOT` |

**On pull requests** both variants build (to validate the Dockerfile) but nothing is
published.

## Continuous integration

GitHub Actions workflows live in `.github/workflows/`:

| Workflow | Trigger | Purpose |
|----------|---------|---------|
| `build.yml` | push / PR to `main`/`master`, manual | `mvn clean verify` — OpenAPI generation, compile, integration tests and the Spotless code-style check; uploads the built jar. |
| `docker.yml` | push / PR / tag `v*` / release, manual | Builds two image variants (stable and validator-SNAPSHOT) via a matrix, multi-arch. On non-PR events it pushes to `ghcr.io/<owner>/gtfs-validator-api`; stable gets `latest` + `<api>-validator<core>`, snapshot gets `<api>-validator<core>` only. PRs build both but push neither. |
| `docker.yml` | push / PR / tag `v*` / release, manual | Builds two image variants (stable and validator-SNAPSHOT) via a matrix, multi-arch. On non-PR events it pushes to `ghcr.io/<owner>/gtfs-validator-api` tagged `<api>-validator<core>`: `main` merges publish API snapshots (`0.0.0-SNAPSHOT-…`), releases publish versioned images with `latest` on the stable variant. PRs build both but push neither. See [Releasing](#releasing). |

## Configuration

Expand Down
8 changes: 6 additions & 2 deletions pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,12 @@

<groupId>org.mobilitydata.gtfs-validator</groupId>
<artifactId>gtfs-validator-api</artifactId>
<!-- API project version is independent of the validator core version. -->
<version>1.0.0</version>
<!-- API project version is independent of the validator core version.
This is a placeholder: the real version is derived from git at build
time by maven-git-versioning-extension (see .mvn/). A `vX.Y.Z` tag
yields version X.Y.Z; any other build yields 0.0.0-SNAPSHOT. This file
is never edited for a release. -->
<version>0.0.0-SNAPSHOT</version>
<packaging>jar</packaging>

<name>gtfs-validator-api</name>
Expand Down
Loading