Skip to content

About

The xberg-io toolchain baseline for mise: one set of tool and language versions for every repository, locally and in CI.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Repository files navigation

toolchains

The xberg-io toolchain baseline for mise: one set of tool and language versions that every repository installs locally and in CI.

Contents

Path Purpose
baseline.toml Canonical versions, in mise [tools] syntax. Renovate bumps them here first.
scripts/toolchains.py check reports drift from the baseline and the task sets; sync rewrites a repository's pins and shared tasks. Standard library only, Python 3.11+.
task-sets/ Named sets of mise tasks that sync writes into a repository (see Task sets).
default.json Renovate preset for consumers: turns off upstream bumps for baseline tools, groups repo-owned ones.

Consuming repositories

A repository commits:

  • mise.toml with the tools it needs, using the baseline's exact key, version and table options (node = "26.12.1", "github:Goldziher/poly" = "0.29.0"). An alias of a baseline tool (nodejs, core:node, aqua:…) is drift;
  • mise.lock, generated by mise lock, covering linux-x64, linux-arm64, macos-arm64 and windows-x64;
  • rust-toolchain.toml for Rust, which stays authoritative for components and targets. Declaring rust in mise.toml is an error; the file's channel is checked against the baseline's rust;
  • toolchains.toml recording the baseline tag it follows, and any override:
baseline = "v1.0.0"

[overrides.node]
version = "22.20.0"
reason = "engines floor smoke test runs on the oldest supported Node"

[overrides."github:Goldziher/poly"]
version = "0.28.4"
reason = "example: hold the previous release until a regression fix ships"

Only mise.toml is read; profile files such as mise.ci.toml are not checked. A task's own tools pin of a baseline tool in mise.toml (tools = { node = "26.13.0" } under [tasks.setup], or a [tasks.setup.tools] table) must state the override, else the baseline version, too; sync moves it, and fails without writing for any other spelling.

Tools the baseline does not list (for example wasm-bindgen-cli, which must match Cargo.lock) are owned by the repository. Renovate updates them when the repository extends github>xberg-io/toolchains.

ai-rulez is not pinned anywhere: tasks run npx -y ai-rulez@latest, which needs the baseline's node. governance:hooks:install allows the poly hooks' npm channel first, so they run npx -y ai-rulez@latest too, and falls back to an ai-rulez on PATH.

Moving to a new baseline

git clone --depth 1 --branch v1.1.0 https://github.com/xberg-io/toolchains /tmp/toolchains
python3 /tmp/toolchains/scripts/toolchains.py sync --baseline /tmp/toolchains/baseline.toml --ref v1.1.0
mise lock
python3 /tmp/toolchains/scripts/toolchains.py check --baseline /tmp/toolchains/baseline.toml

sync also moves the version files mise does not read (see the table below), so a baseline bump is one sync; it prints Dockerfile tags that still disagree, since it does not rewrite them.

mise lock after sync is required, not optional: shared tasks can pin their own tools (governance:check and the plugin tasks run on a task-level node = "26.13.0"), and a lock without that version fails --locked CI with "No lockfile URL found for node@26.13.0". With MISE_TASK_RUN_AUTO_INSTALL=false and a stale lock, the task falls back to whatever node is on PATH without a warning. check exits 1 when a committed mise.lock lacks a version a selected task pins.

check exits 1 on drift and 2 when nothing could be checked: a missing mise.toml, or neither a declared tool nor a version file covered by the baseline. It lists the repository-owned tools it skipped. A pin of ai-rulez under any backend (github:Goldziher/ai-rulez, npm:ai-rulez) is drift: use npx -y ai-rulez@latest instead. It also exits 1 when the repository root still holds go-task files, since the org runs its tasks with mise: a Taskfile.yml, Taskfile.yaml, Taskfile.dist.yml or Taskfile.dist.yaml in any letter case, or a .task/ directory. In a git checkout only tracked files count, so an ignored .task/ cache passes; elsewhere the files on disk are checked.

Version files mise does not read must agree with the baseline as well, or with the tool's override in toolchains.toml. In a git checkout the tracked files count, outside node_modules, vendor, vendored, third_party and third-party directories; a checkout git cannot read (for example one it reports as dubious ownership) is an error, exit 2, never a walk of the disk. A directory without .git, such as a git archive export, is walked on disk, skipping those directories and .venv, target, dist and build too. check lists the files it compared and the ones it could not, and exits 1 when one disagrees:

File Compared with Match sync
.nvmrc, .node-version node exact; a leading v is ignored, a bare major (26) or lts/* is drift rewrites, keeping a v
.python-version python exact, first version line rewrites the first line
.ruby-version ruby exact; a ruby- prefix is ignored rewrites, keeping ruby-
.tool-versions each baseline tool it names (nodejs, golang included) exact, first version; system, lts and latest are skipped, java only when temurin-<x.y.z>+<build> no
package.json packageManager pnpm exact; a +sha512.… suffix is ignored, other package managers are skipped rewrites; a +sha512.<hex> suffix is recomputed from the npm registry's dist.integrity for the new release, and sync fails rather than drop it when the registry cannot be read
nested rust-toolchain.toml, any rust-toolchain rust exact, like the root file: 1.99 is drift against 1.99.1; named channels (nightly, nightly-2026-01-01, stable, beta) are listed as not checked; invalid TOML is drift rewrites numeric channels
Dockerfile, Dockerfile.<name>, *.Dockerfile, Containerfile node, python, go, ruby major.minor of official node, python, golang, ruby images no, prints the drift

Exact matching keeps every place that installs a runtime on the version CI locks. That includes .python-version: 3.13 is drift against 3.13.18 on purpose, since pyenv and uv resolve it to whatever patch is installed. The root rust-toolchain.toml is checked with the mise pins and must name the baseline channel exactly; a repository that needs another toolchain for fuzzing or Miri keeps it in a nested file.

A version file that must stay off the baseline on purpose, such as a nested rust-toolchain.toml that pins a crate's MSRV, is listed under file_overrides in toolchains.toml by its repository-relative path, with a reason:

[file_overrides]
".github/toolchains/fetcher-msrv/rust-toolchain.toml" = "the fetcher crate's MSRV job builds on its oldest supported Rust"

sync leaves the file alone and check lists it as not checked. check exits 1 when an entry has no reason, names a path the repository does not have (in a git checkout, a tracked one), or names a file it does not compare as a version file; the root rust-toolchain.toml is overridden with [overrides.rust] instead.

Dockerfile tags are compared on major.minor only, because the official images publish patch tags after the release the baseline already follows: FROM node:26.13-slim and FROM node:26.13.1 pass against 26.13.0; FROM node:26, node:latest and an untagged node are drift. Dockerfile.<name> with a template or document extension (.j2, .jinja, .tmpl, .tpl, .template, .md, .txt) is not a Dockerfile. An ARG default in the same file resolves a ${VAR} tag, and ${VAR:-x} or ${VAR-x} falls back to x; a tag that still names an argument without a value is listed as not checked. Other registries (ghcr.io/…/node) and other images are not compared.

sync writes nothing when any pin cannot be rewritten (for example a [tools.node] subtable) or when table options differ from the baseline; fix those by hand.

Task sets

Tasks that several repositories define with the same body live here as named sets, so a fix lands once and repositories pick it up on the next baseline move. A repository opts in from toolchains.toml:

baseline = "v1.6.0"
task_sets = ["governance", "governance-check", "lint-format"]

[task_overrides]
lint = "runs tflint over terraform/, not poly"

sync (same command as in Moving to a new baseline) then writes, all generated and committed:

  • mise-tasks/shared.toml: the selected sets' tasks, under a header naming the toolchains tag;
  • mise-tasks/.shared/<set>/…: the sets' file tasks, executable, plus .manifest, the list of files sync owns. The directory is hidden because mise registers every executable under an included directory as a task;
  • "mise-tasks" and "mise-tasks/shared.toml" in mise.toml's [task_config] includes, adding the table when it is missing. includes replaces mise's default scan, so sync also keeps any default task directory (.mise-tasks, .mise/tasks, …) the repository has.

Dropping a set deletes the files sync wrote for it, as listed in the manifest; dropping the last one also takes "mise-tasks/shared.toml" out of includes. Sync writes and deletes only regular files under those two paths, never through a symlink, and stops without writing when the manifest names anything else or a shipped path already holds a file sync did not write. Do not edit the generated files: change the set here and tag a release.

A task listed under task_overrides is left out of shared.toml (with its aliases) and stays the repository's own, for a body that must differ. Every other task a selected set provides must not be defined locally: check exits 1 when mise.toml [tasks], a TOML file mise loads (an includes file, or any *.toml under an included directory), or an executable file task defines one of those names or aliases. It also exits 1 on an unknown set; an override without a reason, for a task no selected set provides, or for a task the repository does not define; a missing include; and any generated file that differs from what sync would write, including its exec bit. In a repository that selects a set or has a manifest, every file under mise-tasks/.shared/ must be one sync wrote.

Repository specifics come from the environment with a default, never a repository name: set DOCS_DIR (default docs-site), BATS_LIB_REF or the deps-refresh variables in mise.toml's [env].

Set Tasks Notes
governance governance:hooks:install poly hooks for AI governance
governance-check governance:check npx -y ai-rulez@latest on the baseline node
lint-format lint, format, format:check repository-wide poly
docs-site docs:build, docs:serve DOCS_DIR
docs-snippets docs:snippets:audit, docs:snippets:gaps alef snippets; DOCS_DIR
rust rust:format, rust:clean Cargo workspace
alef alef:regenerate, alef:verify, alef:verify:strict, alef:snippets, e2e:generate alef-generated outputs
alef-bindings alef:build, test:bindings alef language bindings
alef-publish publish:{build,package,prepare,validate} alef release packaging
alef-pin alef:pin:check the alef_version pin in alef.toml
alef-version version:show, version:bump:{major,minor,patch} the repository defines version:sync
plugin plugin:generate, plugin:verify, plugin:skills:test npx -y ai-rulez@latest plugin and public skills scripts
plugin-runtime plugin:launcher:test, plugin:hermes:test, plugin:test MCP launcher, Hermes bridge and OpenCode test scripts
bats-lib test:scripts:lib:check, test:scripts:lib:sync the repository's scripts/task/sync-bats-lib.sh; BATS_LIB_REF in [env]
bats-suite test:scripts Bats suites under scripts/tests/bats
deps-refresh deps (deps <update|upgrade> [lang]) DEPS_LANGS, DEPS_<LANG>_DIR, DEPS_NODE_DIRS, DEPS_RUST_MANIFESTS in [env]

Why these members

A task joins a set when its body is identical in at least two repositories and in at least half of the repositories that define the name, so overrides stay the exception. A set also gives no repository a task it does not define: a member only some of a set's repositories define moves to a set of its own, or stays local when that set would ship no file task and replace fewer than six local definitions. Counts are repositories with that exact body / repositories defining the name, from every repository's default branch on 2026-10-29, after each repository removed the tasks it does not use (remote file tasks count by source file).

Set Members (identical / defining)
governance governance:hooks:install 17/17
governance-check governance:check 15/16; apart from governance because one repository removed it and still installs the hooks
lint-format format 10/13, format:check 8/8, lint 8/14; no lint:check alias, which would hand the name back to the repositories that removed it; check tasks call lint
docs-site docs:build 7/8, docs:serve 7/8
docs-snippets docs:snippets:audit 3/4, docs:snippets:gaps 3/4; apart from docs-site because the docs-only repositories have no alef
rust rust:format 5/5, rust:clean 5/5
alef alef:regenerate 4/4, alef:verify:strict 4/4, alef:verify 4/5, alef:snippets 3/4, e2e:generate 3/5
alef-bindings alef:build 4/4, test:bindings 4/4; apart from alef because one alef repository defines neither
alef-publish publish:* 3/3 each; apart from alef because two alef repositories define none of them
alef-pin alef:pin:check 2/2, a file task
alef-version version:show 5/5, version:bump:* 4/5 each
plugin plugin:generate 6/6, plugin:verify 6/6, plugin:skills:test 6/6
plugin-runtime plugin:launcher:test 3/3, plugin:hermes:test 3/3, plugin:test 2/4; apart from plugin because three plugin repositories do not define the first two
bats-lib test:scripts:lib:check 5/5, test:scripts:lib:sync 5/5, with the BATS_LIB_REF three of them set on the task moved to [env]
bats-suite test:scripts 3/4, a file task; apart from bats-lib because two of its repositories name or split their suites differently
deps-refresh deps 5/5, already one shared file

Removed on 2026-10-29: governance:generate (1/2), docs:lint:prose (2/5), rust:licenses and the versions:sync alias (no repository defines them). Left local although they meet the count, because a set would give them to repositories that do not define them and a set of their own would replace fewer than six definitions: docs:build:strict (2/2), docs:snippets:list (3/3), docs:snippets:list:lang (2/2), alef:docs (2/2), readme:generate (2/2), alef:format (2/2), alef:sync (2/3), version:set (2/4), rust:format:check (3/4).

Left out because the bodies diverge per repository: rust:build, rust:test, rust:doc, rust:lint, docs:lint:links, docs:snippets:validate, docs:snippets:all, alef:generate, version:sync, setup. Also left out: default (mise tasks ls in 9/9, nothing to maintain), build (5/10, a wrapper for the repository's own rust:build) and the per-language elixir:*, go:* and ruby:* tasks (two or three repositories, each tied to its package layout).

Lock gaps

CI locks the whole baseline for all four platforms. These cannot be locked and are allowed by name: dotnet and rust (installed by script, no checksummed artifact), aws-cli, bats and ruby on Windows, cargo-sort on linux-arm64. Jobs on those platforms keep their existing setup action for that tool.

Changing the baseline

Edit baseline.toml, keep default.json's disabled list equal to its keys (a test enforces this), and tag a release. Patch versions follow the newest patch of the line in use; moving a minor or major line is a reviewed change.

About

The xberg-io toolchain baseline for mise: one set of tool and language versions for every repository, locally and in CI.

Topics

Resources

Security policy

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages