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/*.tomlMUST be a singlerun =or adependslist. - 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 syncordocker pushin 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
lintMUST NOT modify the working tree.- Only
formatMAY modify it.
Enforcement: Review only.
BUILD-6: A new project MUST be a Mise config root
- You MUST give it a
mise.tomlincluding{{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
buildandinstallMUST produce artefacts locally and touch no registry.- Publishing MUST be a separate named task. These are the following:
push-imageand 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 +xanything 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 … | shin 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
FROMdigest. 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
cmpfuses 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
FROMdigest. That push triggersbuild-base-images.yml. - The repin pull request MUST come from a GitHub App. One opened with
GITHUB_TOKENtriggers 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 frominfra/docker/python-service/. - A service MUST declare only its identity. These are the following:
DOCKER_PROJECT_DIR,DOCKER_IMAGE_NAME,DOCKER_BINARYorDOCKER_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,.versionand.revisionfrom theBUILD_VERSIONandBUILD_COMMITbuild 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_COMMITand theLABELreading 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
runAsNonRootcan 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/amd64binaries 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 theeuropemulti-region, one repository per image. - DO NOT use a registry build cache. The
:cachetag 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.