Skip to content

CI runner topology

What it does

Every iiiris CI job runs on a GitLab SaaS hosted runner. No self-hosted runner is required to build, test, validate, or release the project. Linux jobs run in a project-built container image; Windows jobs run on GitLab's hosted Windows pool with a per-job MSYS2 toolchain.

Surface

Runner tags

Tag Used by
saas-linux-medium-amd64 default: — every Linux job unless overridden
saas-linux-medium-arm64 release-binary-smoke-arm64, release-binary-linux-arm64, release-image-arm64
saas-windows-medium-amd64 build-windows-amd64, build-windows-msi

Images

Image Built by Consumed by
$CI_REGISTRY_IMAGE/iiiris-ci:latest ci-image default: — all plain Linux jobs
$CI_REGISTRY_IMAGE/iiiris-ci:$CI_COMMIT_SHA ci-image traceability only
$CI_REGISTRY_IMAGE/iiiris-ci:e2e ci-image iiif-viewer-e2e
python:3.13 upstream pages
docker:27 + docker:27-dind upstream the .dind job base

iiiris-ci:latest is multi-arch (linux/amd64,linux/arm64), so amd64 and arm64 jobs pull the same tag. iiiris-ci:e2e is amd64-only — the one job that uses it has no arm64 counterpart.

Job shapes

Three shapes, no others:

  1. Plain container job — inherits default:; the script runs directly in iiiris-ci:latest. No docker run wrapper: the repo is already the working directory.
  2. .dind jobimage: docker:27 + services: [docker:27-dind] for jobs that build or push container images (ci-image, docker-build*, release-image*).
  3. Windows jobtags: [saas-windows-medium-amd64], PowerShell script, toolchain installed per job.

Contracts

  • No self-hosted runner is referenced. .gitlab-ci.yml contains no tags: [iiiris]. Any reintroduction is a regression.
  • ci-image is gated, not per-pipeline. It runs only on changes to deploy/ci/** or .gitlab-ci.yml, or on release tags; otherwise it is absent from the pipeline (when: never). Dependent jobs declare needs: [{job: ci-image, optional: true}] so a pipeline that skips it consumes the previously published :latest.
  • A hand-forced rebuild is a separate job, ci-image-rebuild, and nothing may needs: it. The two must not be merged back into one job with a when: manual fallback rule. optional: true lets a dependent run when the needed job is absent; it does not when the job is present but unplayed. A single job with a manual fallback therefore sat in every feature-MR pipeline holding lint, test and all four conformance jobs in skipped — while the pipeline reported success. Any change that puts a manual ci-image back in the dependency graph reintroduces a green check that means "nothing ran".
  • dind runs plain-TCP, not TLS. .dind sets --tls=false, DOCKER_HOST: tcp://docker:2375, DOCKER_TLS_CERTDIR: "". docker buildx create fails with TLS data loaded from the environment ("could not create a builder instance with TLS data loaded from environment"); plain TCP is what makes buildx usable inside dind.
  • ci-image layers are cached in the registry. --cache-to/ --cache-from type=registry,ref=…:buildcache,mode=max (and :e2ecache for the e2e image). A no-op rebuild reuses layers instead of recompiling the libvips-linked toolchain.
  • arm64 builds are native, not emulated. release-binary-*-arm64 runs on an arm64 runner with CGO_ENABLED=1; release-image-arm64 builds on an arm64 runner. Only ci-image and release-image-debian use QEMU (tonistiigi/binfmt) to produce a multi-arch manifest in one job. release-image fuses the two per-arch tags with docker buildx imagetools create — it does not rebuild.
  • The e2e browser is upstream's headless-shell, not Debian's chromium. Debian bookworm's chromium (150.0.7871.46) SIGTRAPs on startup inside a container regardless of --no-sandbox, --disable-dev-shm-usage, --no-zygote, --single-process, or seccomp=unconfined; it dies before printing the DevTools endpoint chromedp waits for. deploy/ci/Dockerfile.e2e copies /headless-shell out of chromedp/headless-shell and puts it on PATH, where chromedp's exec-path probe finds it ahead of any other browser name.
  • Windows jobs must kill their stragglers. GitLab's Windows PowerShell executor will not finish a job while any process still holds the job's stdout handle. build-windows-amd64 terminates iiirisd plus everything running out of the MSYS2 tree (pacman-key leaves gpg-agent/dirmngr daemons behind). Without this the job burns to its 1 h timeout after a fully successful build.
  • Windows cadence is gated. The MSYS2 + Go toolchain install is a ~15–20 min cold start, so build-windows-amd64 runs on release tags and on MRs touching Windows-relevant paths — not every push. build-windows-msi and the release re-export jobs are tag-only.

Out of scope

  • Daemonless image builders (kaniko / buildah). Would avoid dind's privileged mode but rewrites every docker build/buildx invocation and complicates the multi-arch release build. dind works on SaaS runners; revisit only if policy blocks it.
  • Self-hosted runners. Retired. The iiiris-tagged runners may stay registered as a temporary rollback path but are not referenced by CI.
  • Ephemeral-EC2 Windows builds. Retired in favour of the hosted Windows pool. deploy/aws, deploy/packer, and scripts/aws remain in the repo but are no longer wired into CI.
  • macOS and Windows-ARM runners. Not offered by the SaaS pools. macOS users build locally (brew install vips && make build).
  • GitLab cache: for the Go module/build cache. Cold go build on a hosted runner is tolerable today; add it if test/lint wall-clock becomes the bottleneck.