Skip to content

Build rules

How anything is built, pinned and turned into an image.

Why the pipeline is shaped this way: container images. What each task does: mise tasks.

The build path

BUILD-1: Build logic MUST live in mise/scripts/, once

  • You MUST put every build step in a script under mise/scripts/.
  • A task body in mise/includes/*.toml MUST be a single run = or a depends list.
  • DO NOT put logic in a task body. It cannot be linted and cannot be called from a Docker stage.

Enforcement: Review only.

BUILD-2: Every caller MUST invoke the same task

  • A workflow, a Docker stage and an engineer MUST reach a build step through mise run //<project>:<task>.
  • A workflow MUST supply credentials, a runner and a trigger only.
  • DO NOT inline docker build, go build, uv sync or docker push in a workflow.

Enforcement: Review only.

BUILD-25: A project is an instance of its archetype

  • A project MUST differ from the others of its kind only in its directory, its name and the identity it declares. A Go service is just another Go service.
  • A workflow MUST work on every project, or on a list of projects it is given.
  • DO NOT add a workflow, task or file for one project.
  • If one project needs a step, then add it to the archetype for every project of that kind.

Enforcement: Review only.

BUILD-3: A service Dockerfile MUST run exactly one Mise task

You MUST use //<project>:install for Go, //<service>:sync-venv for Python.

DO NOT spell the build out. DO NOT add extra glue that makes the Docker build drift from a local or CI build.

Enforcement: Review only.

BUILD-4: Use the shared task names

These are the following: build, install, test-fast, test-integration, test-all, test, lint, format, clean, proto, sync-venv, build-image, push-image.

  • You MAY add a task the includes do not define.
  • DO NOT redefine one they do. The include wins silently.

Enforcement: Review only. A wildcard sweep cannot see a shadowed body.

BUILD-5: lint checks, format mutates

  • lint MUST NOT modify the working tree.
  • Only format MAY modify it.

Enforcement: Review only.

BUILD-6: A new project MUST be a Mise config root

  • You MUST give it a mise.toml including {{vars.repo_root}}/mise/includes/<flavour>.toml. Mise renders variables there but not in [env].
  • DO NOT use a relative include path. It breaks the moment a project moves.
  • You MUST list it in [monorepo].config_roots.
  • DO NOT glob the root list. A listed root with no config file warns on every invocation.

An unlisted project is matched by no wildcard.

Enforcement: Review only. See enforcement gaps.

BUILD-7: No new Makefiles

DO NOT add a Makefile or a target. make is being retired.

The remaining Makefiles are deleted as their callers move to Mise.

Enforcement: Review only.

BUILD-8: Publishing is never a side effect of building

  • build and install MUST produce artefacts locally and touch no registry.
  • Publishing MUST be a separate named task. These are the following: push-image and its equivalents.
  • DO NOT run a publishing task outside CI without a reason.

Enforcement: the task definitions. Review only for new tasks.

BUILD-9: mise/tasks/ MUST stay empty

  • DO NOT add a visible file there.
  • DO NOT chmod +x anything in it.
  • DO NOT delete .tombstone.md. It keeps the directory in git.

Anything visible in it becomes a task of the repository root and matches every '//...:<task>' wildcard.

Enforcement: Review only. See Mise.

Pinning

BUILD-10: Every external input MUST be pinned

DO NOT let a build resolve a version at build time.

Input Pinned by
Base images, ours and upstream <image>:<tag>@sha256:<digest>. The digest pulls, the tag is for the reader
Third-party GitHub Actions full commit SHA, version in a trailing comment
Tools mise.toml [tools] plus mise.lock
Python interpreter the repository-global .python-version
Dependencies uv.lock, go.sum

Enforcement: //infra/docker:check-image-pins (pre-commit) and //infra/docker:check-pinned-tags (check-image-pins.yml) for image digests. Review only for the rest.

BUILD-11: A build MUST NOT fetch an installer script

  • DO NOT pipe curl … | sh in a build path.
  • You MUST install from a release artefact at a pinned version.
  • You MUST make the source URL overridable by build argument.

Enforcement: Review only.

BUILD-12: Our own base digests are repinned by one task

  • You MUST use //infra/docker:repin-base-images.
  • DO NOT hand-edit a FROM digest. The rest fall behind.

Enforcement: //infra/docker:check-image-pins (pre-commit).

BUILD-13: A stale pin MUST fail the build

  • You MUST keep both cmp fuses in both service Dockerfiles.
  • DO NOT bypass them.

What they compare, and why there are two: container images.

Enforcement: the cmp in both service Dockerfiles, plus //infra/docker/go-service:check-base-image (check-image-pins.yml).

BUILD-14: A pin MUST have something that moves it

  • Renovate bumps the upstream FROM digest. That push triggers build-base-images.yml.
  • The repin pull request MUST come from a GitHub App. One opened with GITHUB_TOKEN triggers no checks.
  • DO NOT add a nightly rebuild. OS patches arrive as a digest bump.

Enforcement: build-base-images.yml and Renovate.

BUILD-15: :latest MUST NOT appear in deploy configuration

  • Terraform variables, Kustomize overlays, Ansible variables and inventory files MUST name an immutable version or a digest.
  • DO NOT use a floating tag. It cannot be rolled back.

The platform registries push no :latest at all.

Enforcement: Review only.

Images

BUILD-16: One shared Dockerfile per language family

  • Go MUST build from infra/docker/go-service/Dockerfile, Python from infra/docker/python-service/.
  • A service MUST declare only its identity. These are the following: DOCKER_PROJECT_DIR, DOCKER_IMAGE_NAME, DOCKER_BINARY or DOCKER_PACKAGE.
  • DO NOT write a service-specific Dockerfile without an approved exception.

There is no approved exception at present. The per-service Dockerfiles that predate the shared ones are deleted as their services move across.

Enforcement: Review only.

BUILD-17: The two families MUST stay symmetric

  • Go and Python images MUST share the builder, the pin mechanism, the labels, the entrypoint shape and the runtime user.
  • A change to one family MUST land in the other.
  • DO NOT add a check that holds one family's setting equal to another's. Close the difference instead (principle 7).

Enforcement: Review only.

BUILD-18: Every image MUST carry OCI provenance labels

  • You MUST set org.opencontainers.image.title, .version and .revision from the BUILD_VERSION and BUILD_COMMIT build arguments.
  • DO NOT set org.opencontainers.image.created. A timestamp makes identical inputs produce different digests.

Enforcement: the shared Dockerfiles.

BUILD-19: Provenance arguments are declared last

  • You MUST declare ARG BUILD_COMMIT and the LABEL reading it after the expensive layers.
  • DO order layers least- to most-volatile throughout.

Enforcement: the shared Dockerfiles.

BUILD-20: Runtime stages MUST run as the non-root user 65532

  • You MUST use the numeric id, so Kubernetes runAsNonRoot can verify it.
  • A service image MUST NOT override it.
  • If a service needs a low port or volume ownership, then resolve that in the deployment.

Enforcement: the runtime base images.

BUILD-21: The container process MUST keep the service's real name

  • You MUST carry the name in an environment variable and run it with sh -c … exec.
  • DO NOT use an exec-form ENTRYPOINT. It cannot expand a build argument.

Enforcement: the shared Dockerfiles.

BUILD-22: Mutable configuration MUST NOT be baked into an image

  • Anything that differs between environments, or changes without a code change, MUST arrive at runtime by environment variable or mount.
  • You MAY bake an immutable asset. These are the following: a template, an embedded certificate, seed data.
  • Put a baked asset under <service>/docker/rootfs/.

Enforcement: Review only.

BUILD-23: CGO_ENABLED=0 for Go, unconditionally

  • DO NOT enable cgo. The same builder produces the bin/amd64 binaries Ansible copies onto Ubuntu hosts.
  • If you change this, then move the builder base to glibc first and check in-cluster name resolution.

Enforcement: mise/scripts/go-build.bash.

BUILD-24: Registries are immutable, one repository per image

  • You MUST create a registry with immutable_tags = true, in the europe multi-region, one repository per image.
  • DO NOT use a registry build cache. The :cache tag cannot move after the first build.
  • DO NOT prune by age alone. Cleanup MUST NOT delete a version any environment still pins.

Enforcement: the platform_artifact_registries Terraform module.