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 aconfig.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 | 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-amd64is tag-triggered and blocking, soiiirisd-vX.Y.Z-windows-amd64.zip+ its.sha256are part of every release. - The MSI ships too, since v0.11.0.
build-windows-msiandrelease-binary-windows-msiare tag-triggered and blocking, soiiirisd-vX.Y.Z-windows-amd64.msi+ its.sha256are part of every release. They wereallow_failureup 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-publishstill links defensively. Its Windows-job dependencies are declaredneeds: optional: true(they are tag-only jobs, absent from other pipelines), andgitlab-release.shuploads + 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:
- Cut the patch release with the revert. Document the
issue in
CHANGELOG.mdunder the new version. - Communicate (whatever channels apply) that operators should upgrade to the patch.
- 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:
- Confirm
[Unreleased]inCHANGELOG.mdaccurately reflects what's shipped since the last release. Edits to[Unreleased]go in normal feature/fix MRs, not at release time. - Pick a version per Versioning.
- Run
scripts/cut-release.sh vX.Y.Z. The script: - Validates the version format, working tree, and current branch.
- Rewrites
CHANGELOG.md: renames[Unreleased]to[vX.Y.Z] - YYYY-MM-DD; inserts an empty[Unreleased]section above. - Commits on
release/vX.Y.Z, pushes, opens an MR via theglab-mrflow. - Review and merge the release MR.
- Tag the merge commit with a signed annotated tag:
The one-line message is what appears as the supplemental
summary on the GitLab Release; the body comes from
CHANGELOG.md. - CI runs the release pipeline. Verify the GitLab Release
entry, asset links, and
:latesttag in the registry. - 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 stablevX.Y.Ztags 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.