Skip to content

Releases

The contract iiiris maintains for releases: what triggers a release, what artifacts ship, what versioning rules apply, and how latest, rollback, and release notes are handled. Companion to the operator-facing walkthroughs in ../deployment.md — this spec captures what the release process guarantees; that file captures how an operator consumes a release.

What a release is

A release is a signed annotated tag matching v\d+\.\d+\.\d+ pushed to the GitLab origin. Pushing such a tag fires the release stage in .gitlab-ci.yml, which produces the artifact set below and a GitLab Release entry. Nothing else is a release:

  • Branch pushes are not releases.
  • Manual pipeline reruns on a tag are not new releases (they may republish the same artifacts; the tag identifies the release, not the pipeline run).
  • A push of an unsigned tag is rejected (see Signing below).

The tag is authoritative. The pipeline that produced its artifacts is just a build of that tag.

Signing

Release tags must be signed (git tag -s vX.Y.Z). Signing is load-bearing because the rollback contract is forward-only — published tags and images are never deleted (see Rollback). A signed tag makes any retroactive rewrite of a release identifier detectable, which is the only defence against an attacker (or a confused maintainer) silently swapping out what vX.Y.Z points at.

The scripts/cut-release.sh helper enforces git tag -s; the spec requires it.

Versioning

Strict Semantic Versioning, currently pre-1.0 (0.X.Y).

Pre-1.0 rules

While the major version is 0, a minor bump (0.X.0) is required for any breaking change to:

  • The IIIF API surface. info.json field names or shapes, the set of supported formats/qualities/extra-features, status code mapping, the route grammar in iiif.v{2,3}.ParsePath.
  • The YAML config schema or IIIRIS_* env vars. Renamed or removed fields, changed defaults that affect existing deployments, new required fields without a config.Default() value.
  • CLI flags / binary invocation. Renamed or removed flags. The surface is currently minimal (-config); the rule applies to anything added later from the time it ships.

A patch bump (0.X.Y0.X.Y+1) covers everything else: bug fixes, internal refactors, dependency updates, performance work, log-format tweaks, and cache-format changes (which are self-healing — see below).

Cache format is not a release surface

The filesystem cache backend's on-disk format is governed by cacheKeyVersion in internal/auth/cachekey.go (see cache-filesystem.md and scaffold.md). When the version bumps, iiirisd detects the legacy state on attach and wipes it automatically; operator-dropped files are preserved. From the operator's perspective the upgrade is seamless — caches re-warm, configuration doesn't change. So cache-format changes are patch bumps, not minor.

When 1.0 happens

1.0 graduates the surfaces above into long-term compatibility guarantees: a major bump (1.X2.X) is required for any breaking change to the IIIF API surface, config schema/env vars, or CLI flags. 1.0 itself is declared in its own brief when:

  • The IIIF Image API surface is stable against both 2.x and 3.0 validators (it is today) and no near-term work is expected to alter it.
  • The YAML config schema has stabilised — no field churn over several minor releases.
  • The CLI flag surface has had a deliberate design pass (more than just -config).

Until then, the pre-1.0 rules apply and operators understand minor bumps may require attention.

Cadence

Ad-hoc. Releases are cut when meaningful work has shipped; there is no fixed schedule. Pre-release channels (-rc, -beta, nightly images) are not in scope — only stable vX.Y.Z tags produce artifacts.

Artifact set

Each release publishes:

Artifact Location Notes
Distroless container, multi-arch ${CI_REGISTRY_IMAGE}:vX.Y.Z + :latest linux/amd64 + linux/arm64. Default for docker pull.
Debian-slim container, multi-arch ${CI_REGISTRY_IMAGE}:vX.Y.Z-debian No :latest-debian. Fallback for shell-in-container debugging.
Linux/amd64 binary Generic package registry, linked from the Release Stripped, version-stamped. Requires libvips on host.
Linux/arm64 binary Generic package registry, linked from the Release Built natively on saas-linux-medium-arm64. Requires libvips on host.
Linux binary SHA256s Generic package registry, linked from the Release .sha256 file alongside each binary.
Windows/amd64 zip Generic package registry, linked from the Release iiirisd.exe + bundled MSYS2 UCRT64 DLLs + compliance bundle. Built on every release tag (see Windows artifacts below).
Windows/amd64 MSI Generic package registry, linked from the Release WiX v5 installer. Per-user or all-users layout; all-users installs can register the Windows service (INSTALLSERVICE=1).
Windows artifact SHA256s Generic package registry, linked from the Release .sha256 files for whichever Windows artifacts the release produced.
GitLab Release entry GitLab Releases UI Description sourced from CHANGELOG.md; tag annotation as one-line supplemental summary; asset links to the package-registry binaries + container images.

The CI registry is the sole publication target. Mirroring to Docker Hub, GHCR, or quay.io is not in scope.

Downloadable binaries are public. Release binaries are uploaded to the project's generic package registry (packages/generic/iiiris/<tag>/…), not attached as job artifacts. Generic packages on a public project are anonymously downloadable independent of builds_access_level, so release downloads are public even while CI logs/artifacts stay members-only. The Release is created — and its asset links attached — via the Releases API in scripts/gitlab-release.sh, not the GitLab release: keyword (which the runner executes via release-cli, absent from our images and runner-version dependent — the cause of v0.5.0's release-publish failure).

latest semantics

:latest always points at the newest stable vX.Y.Z. The release pipeline re-tags :latest on every release; operators on :latest follow the project's stable line. There is no "pinned 0.x" alternative — pre-1.0 minor bumps may require operator attention, which is the cost of pre-1.0.

Windows artifacts

build-windows-amd64, build-windows-msi, and the release-binary-windows-* re-export jobs run on GitLab's SaaS Windows pool on every release tag — no CI variable gates them. (The RUN_WINDOWS=1 gate belonged to the retired ephemeral-EC2 orchestrator; see ci-runners.md.)

  • The zip ships. build-windows-amd64 is tag-triggered and blocking, so iiirisd-vX.Y.Z-windows-amd64.zip + its .sha256 are part of every release.
  • The MSI ships too, since v0.11.0. build-windows-msi and release-binary-windows-msi are tag-triggered and blocking, so iiirisd-vX.Y.Z-windows-amd64.msi + its .sha256 are part of every release. They were allow_failure up to and including v0.10.0, which is how a broken installer went three releases without anyone noticing it produced no file at all; a failing installer now fails the release.
  • release-publish still links defensively. Its Windows-job dependencies are declared needs: optional: true (they are tag-only jobs, absent from other pipelines), and gitlab-release.sh uploads + links a Windows asset only when its file is present — so a Release entry never carries a dangling link.

Release notes

The GitLab Release description is sourced from CHANGELOG.md: the body of the ## [vX.Y.Z] - YYYY-MM-DD section is extracted (heading line stripped) by scripts/gitlab-release.sh and posted as the Release description. The tag annotation contributes a one-line supplemental summary rendered above the CHANGELOG content.

This makes CHANGELOG.md the single source of truth for release content. Tag annotations are a hook, not the body.

Rollback

Forward-only. A bad release is fixed by cutting a new patch release with the revert (or fix). :latest re-points forward on the next release.

Published tags and container images are never deleted. Deployments that pinned a bad version need a clean upgrade path, not a 404. The operational impact of a registry delete (every deployment pulling the bad tag fails) is worse than the impact of the bad release itself in nearly all scenarios.

If a release is severe enough that it must not be used at all, the appropriate response is:

  1. Cut the patch release with the revert. Document the issue in CHANGELOG.md under the new version.
  2. Communicate (whatever channels apply) that operators should upgrade to the patch.
  3. Optionally, add a note to the bad release's GitLab Release entry pointing at the patch.

How to cut a release

The maintainer flow, end to end:

  1. Confirm [Unreleased] in CHANGELOG.md accurately reflects what's shipped since the last release. Edits to [Unreleased] go in normal feature/fix MRs, not at release time.
  2. Pick a version per Versioning.
  3. Run scripts/cut-release.sh vX.Y.Z. The script:
  4. Validates the version format, working tree, and current branch.
  5. Rewrites CHANGELOG.md: renames [Unreleased] to [vX.Y.Z] - YYYY-MM-DD; inserts an empty [Unreleased] section above.
  6. Commits on release/vX.Y.Z, pushes, opens an MR via the glab-mr flow.
  7. Review and merge the release MR.
  8. Tag the merge commit with a signed annotated tag:
    git checkout main && git pull
    git tag -s vX.Y.Z -m "one-line summary"
    git push --tags
    
    The one-line message is what appears as the supplemental summary on the GitLab Release; the body comes from CHANGELOG.md.
  9. CI runs the release pipeline. Verify the GitLab Release entry, asset links, and :latest tag in the registry.
  10. Confirm the Windows assets on the Release entry. Both the zip and the MSI are expected on every release — see Windows artifacts. Check the entry itself, not just the pipeline: asset links are what operators consume.

Out of scope (deliberately)

  • Image signing (cosign), SBOM generation, provenance attestations. Supply-chain hygiene worth doing, but adds key-management and tooling surface that's better layered in a follow-up brief once this baseline is stable.
  • Mirroring to Docker Hub / GHCR / quay.io. GitLab Container Registry is the sole publication target.
  • Pre-release channels (-rc, -beta, nightlies). Only stable vX.Y.Z tags produce artifacts.
  • Auto-tagging from CI. The human runs cut-release.sh, reviews the MR, merges, and tags. Pushing a tag is the explicit "ship this" gesture; CI does not infer it.
  • Multi-registry mirroring or operator-facing release metadata feeds (RSS, JSON manifest). Operators consume the GitLab Releases UI directly.