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.Y → 0.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.X → 2.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 — on hold Generic package registry, linked from the Release Not published while Windows CI is on hold (see Windows artifacts). When enabled: iiirisd.exe + bundled MSYS2 UCRT64 DLLs + compliance bundle.
Windows/amd64 MSI — on hold Generic package registry, linked from the Release Not published while Windows CI is on hold. When enabled: WiX v5 installer; per-user or all-users layout; all-users installs can register the Windows service (INSTALLSERVICE=1).
Windows artifact SHA256s — on hold Generic package registry, linked from the Release .sha256 files for whichever Windows artifacts the release produced — none while on hold.
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

Windows CI is on hold. build-windows-amd64, build-windows-msi, and the release-binary-windows-* re-export jobs are absent from every pipeline unless it sets RUN_WINDOWS=1 — release tags included. A release therefore publishes no Windows zip, MSI, or checksum; v0.12.2 is the last release that carries them. Re-enabling is a v1.0.0 blocker with explicit exit criteria in ../ROADMAP.md "1.0 checklist".

  • The hold is absent, not manual. Each Windows job's first rule is if: $RUN_WINDOWS != "1" → when: never. A manual job would be present but unplayed, which does not satisfy an optional needs: and would hold release-publish in skipped.
  • Opt-in is per pipeline. Set RUN_WINDOWS=1 via "Run pipeline" → Variables or git push -o ci.variable=RUN_WINDOWS=1; the workflow rules admit such a pipeline on any ref. On a tag with the variable set, the Windows jobs are tag-triggered and blocking again, so a failing installer fails that release.
  • Removing the hold is one rule per Windows job in .gitlab-ci.yml, once the ROADMAP exit criteria are met. It restores the pre-hold contract: zip and MSI on every release, both blocking.
  • release-publish still links defensively. Its Windows-job dependencies are declared needs: optional: true (the jobs are absent from every pipeline without RUN_WINDOWS=1), 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 Release entry exists and carries its asset links — check the entry itself, not just the pipeline: asset links are what operators consume. While Windows CI is on hold, no Windows zip or MSI is expected; see Windows artifacts.

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.