rab.yml reference¶
rab.yml sits at the repository root. It has an optional release:
mapping and a required packages: list. release.py check validates all of
it against the repository's manifests; the gate runs the same check on the
tagged commit and refuses a release that fails it.
Its release-kit 1.x name, release-kit.yml, is still read until rab 3.0.0,
with a warning, when there is no rab.yml; both together are refused.
install renames it (see Migrating from release-kit 1.x).
release: # optional
name_from: onto-cli
tag: "release-{release}"
branch: main
packages:
- name: onto-cli
registry: crates.io
manifest: crates/onto-cli/Cargo.toml
version: "0.1.0"
tag: "{name}-v{version}"
paths: ["crates/onto-cli/**", "Cargo.lock"]
release:¶
| Field | Default | Meaning |
|---|---|---|
name_from |
the last package's name | The package whose version names the release. It must be a listed package's name. If two packages share that name (a crate and a PyPI package), the first one listed is used. |
tag |
"release-{release}" |
The release tag's format. {release} is the release's name. The template's trigger matches release-*; change it if you change this. |
branch |
main |
The default branch. Every tag must point at a commit on it. |
packages:¶
A list, in publication order. Each package comes after the listed packages
of the same registry that it depends on. check reads the dependencies
from the manifests:
Cargo.toml:[dependencies],[build-dependencies]and[target.*.dependencies](a renamed dependency counts by itspackage =name).[dev-dependencies]do not count.pyproject.toml:[project] dependencies(or Poetry's).package.json:dependenciesandpeerDependencies.
| Field | Required | Meaning |
|---|---|---|
name |
yes | The registry's name for the package. For crates.io, PyPI and npm it must equal the manifest's name. For vscode it is <publisher>.<name> from package.json. For github it is free. |
registry |
yes | github, crates.io, pypi, npm or vscode. |
manifest |
yes | The manifest that holds the version: a Cargo.toml, pyproject.toml or package.json, as a path from the repository root. |
version |
yes | The version, quoted. It must equal the manifest's (release.py sync copies it). |
tag |
no | The record tag's format. Default "{name}-v{version}". |
paths |
no | Glob patterns (fnmatch, from the repository root) for the files that make up the package. Default: everything under the manifest's directory (** if the manifest is at the root). |
artifacts |
for github, pypi, vscode |
What to build. See Artifacts. |
notes |
no, github only |
A bash command whose output is the release's notes. Default: GitHub generates them. See GitHub release options. |
checksums |
no, github only |
The name of one file, e.g. SHA256SUMS, listing the SHA-256 of every asset, uploaded with them. |
attest |
no, github only |
true attests the assets' build provenance. |
homebrew |
no, github only |
{tap, formula, name}: a Homebrew formula, proposed to a tap. See Homebrew. |
chocolatey |
no, github only |
{package, test}: a Chocolatey package. See Chocolatey. |
open-vsx |
no, vscode only |
true publishes to Open VSX too, and the version counts as published only once both the Marketplace and Open VSX have it. Needs the OVSX_PAT secret. See VS Code Marketplace and Open VSX. |
Manifests¶
Cargo.toml:[package] nameandversion.version.workspace = truereads[workspace.package] versionfrom the rootCargo.toml.pyproject.toml:[project](PEP 621), or[tool.poetry]. Withdynamic = ["version"]and a[tool.maturin]table, the version is the crate's, as maturin writes it: theCargo.tomlthatmanifest-pathnames (else the one besidepyproject.toml), with a SemVer pre-release in PEP 440 form (0.2.0-rc.1→0.2.0rc1).package.json:nameandversion; forvscode, the name is<publisher>.<name>.
Versions¶
PyPI versions follow PEP 440 (1.2.0, 1.2.0rc1, 1.2.0.post1,
1.2.0.dev3). Every other registry's follow SemVer (1.2.0, 1.2.0-rc.1).
A version with a - makes a GitHub release a pre-release.
Quote versions. YAML reads version: 0.10 as the number 0.1, so rab
refuses any version that is not a string:
onto: quote the version (version: "0.1"); YAML reads an unquoted one as a number
release.py sync rewrites each package's version: line from its manifest,
quoted, and keeps comments and layout. It needs exactly one version: line
per package, so do not put a version: key anywhere else in the file.
paths: what a package ships¶
paths decides when a release would be split (rule 4). If a package's
version is already tagged on another commit, and any file matching its
paths differs between that commit and the one being released, the release
is refused: the same version would name different code.
The default, the manifest's directory, suits a crate whose files all live
there. Widen it when the package also ships files from elsewhere, such as a
workspace Cargo.lock or a shared README.md:
paths: ["crates/onto-cli/**", "Cargo.lock", "README.md"]
Record tags and collisions¶
Each package's record tag must be distinct from every other package's and
from the release tag; check refuses duplicates.
check compares the tags of the versions in the file now. A collision can
also come from history. Suppose crates.io has the crate rs-rich and PyPI has
the Python package rs-rich, both tagged with the default {name}-v{version}.
When the crate was at 0.0.10, the kit pushed the tag rs-rich-v0.0.10.
When the Python package reaches 0.0.10, its tag would be rs-rich-v0.0.10
too, already on another commit.
rab catches this. A package tag that already exists on the remote, at
a commit other than the one being released, must be that package's own
record: an annotated tag whose message starts with
<name> <version> (<registry>), which is what release.py tag writes. If it
is not, tag and the gate refuse:
these tags already exist for something else:
rs-rich-v0.0.10 at 4f2a9c1d7e3b: not rs-rich 0.0.10 (pypi)'s record
Give the package a tag format of its own in rab.yml (for example
`tag: "python-v{version}"` for a PyPI package named like a crate), or bump its version.
The fix is a tag format of its own:
- name: rs-rich
registry: pypi
manifest: crates/rich-py/pyproject.toml
version: "0.0.10"
tag: "python-v{version}"
The check covers packages that are not yet published. A lightweight tag, or one made by other tooling with a different first line, is also "not its own".
A tag that points at a commit with no rab.yml (nor release-kit 1.x's
release-kit.yml), made before the repository adopted the kit, is refused
with an explanation:
rs-rich-v0.0.9 is on 1a2b3c4d5e6f, which has no rab.yml (nor release-kit.yml):
the tag predates the kit. Bump the package's version, or delete the stale tag if nothing was
published from it.
Artifacts¶
crates.io and npm publish from source and take no artifacts (check refuses
them). GitHub Releases, PyPI and the VS Code Marketplace need at least one.
Each artifact is a mapping with a kind. The release run builds only the
selected packages' artifacts, each artifact (and target) in its own job, on
the runner it needs.
kind |
Builds | Registries | Required | Optional |
|---|---|---|---|---|
rust-binary |
<bin>-v<version>-<target>.tar.gz (.zip on Windows), holding the binary and the include files |
github |
bin |
crate, targets, include, test, prepare |
python |
an sdist and a wheel, with python -m build |
pypi, github |
dir, test, prepare |
|
maturin |
a wheel per target with maturin, plus an sdist | pypi, github |
targets, sdist, test, prepare |
|
vsix |
<name>-<version>.vsix, with vsce package |
vscode, github |
dir, test, prepare |
|
command |
whatever run makes, collected by the outputs glob |
github, pypi, vscode |
run, outputs |
os, test, prepare |
ci-artifact |
nothing: promotes the files of one CI artifact, unchanged | github, pypi, vscode |
artifact, sha256, files |
test |
Any other field is refused (rust-binary does not take dir).
rust-binary¶
artifacts:
- kind: rust-binary
bin: onto # the [[bin]] to build
crate: onto-cli # default: the manifest's package name
include: [README.md, LICENSE]
targets: [x86_64-unknown-linux-gnu, aarch64-apple-darwin]
Each target's job runs
cargo build --release --locked -p <crate> --bin <bin> --target <target>
and archives the binary as <bin>-v<version>-<target>/<bin> (.exe on
Windows) with each include file beside it. include paths are from the
repository root; inside the archive each file keeps only its file name. On
Unix the binary is mode 0755.
python¶
- kind: python
dir: python # default: the manifest's directory
Runs python -m pip install build and python -m build --outdir dist <dir>:
an sdist and a pure wheel. One job on ubuntu-latest.
maturin¶
- kind: maturin
targets: [x86_64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-pc-windows-msvc]
sdist: true # default true
Builds from the Cargo.toml beside the pyproject.toml with
PyO3/maturin-action
(--release --locked, manylinux: auto), one job per target. When
pyproject.toml names the crate with [tool.maturin] manifest-path, maturin
runs in the pyproject.toml's directory and finds the crate itself (a
bindings = "bin" package at the root of a workspace, for example). With
sdist: true an extra job, <slug>-sdist, builds the sdist on
ubuntu-latest.
vsix¶
- kind: vsix
dir: editors/vscode # default: the manifest's directory
Runs npm ci in dir (or npm install without a package-lock.json), then
npx @vscode/vsce package, writing <name>-<version>.vsix, where <name> is
the part of the package name after <publisher>.. Node 24, on ubuntu-latest.
command¶
- kind: command
run: ./scripts/package.sh "$VERSION" "$OUT"
outputs: "target/package/*.tar.gz"
os: ubuntu-latest # default
run runs in bash (on Windows runners, Git's bash) from the repository root,
with these variables set:
| Variable | Value |
|---|---|
VERSION |
the package's version |
PACKAGE |
the package's name |
OUT |
the absolute path of the output directory (dist) |
Then every file matching outputs (a glob from the repository root) is copied
into OUT, unless it is already there. Files run writes straight into
$OUT are collected too. The build action installs nothing extra for this
kind: use what the runner has, or install it in run.
ci-artifact¶
- kind: ci-artifact
artifact: 13290975305 # the GitHub Actions artifact's id
sha256: 401bc5a6f09a149774a57d33f427aebdfbaee86bc939945b8368b19a8306fabb
files: "dist/*.whl" # the archive members to publish
Publishes the exact files a CI run in this repository already built and tested, instead of building them again: for a project whose release must ship the reviewed bytes (a wheel with staged components, a VSIX that passed an acceptance gate). The job downloads the artifact's archive and refuses, before anything is published, unless:
- the archive's SHA-256 is
sha256, and GitHub records the same digest; - the artifact has not expired;
- the tagged commit is the commit that built it, or descends from it and
changes nothing but
rab.yml. This is rule 4. The artifact must exist before its id can be pinned, so the pin can never be in the commit that built it: the commit that records the pin is allowed, any other change is not. A CI run on a release-candidate branch qualifies once that branch is merged tomainwith nothing else changed; a run on other files, or on a commit the tag does not descend from, does not; - every member matching
files(a glob over the archive's paths) carries exactly the package's version in its name (PEP 440 form for PyPI, optionally asv<version>):pkg-11.0,pkg-1.0.1andpkg-1.0rc1are not1.0. At least one member must match.
The usual order is: the CI run builds the artifact on the release branch; a
commit then pins its artifact and sha256 in rab.yml; that commit
(or main after the release branch merges) is tagged.
The matching members are copied into dist under their own names, and the
artifact's test (if any) runs on them as for any other kind. Only this kind's
build step is given a token, with actions: read; the build workflow and the
template's build job grant that permission. An artifact lives for its
repository's retention period (90 days by default), so pin it close to the
release.
Targets and runners¶
targets (for rust-binary and maturin) is a list. Each entry is a Rust
target triple, or {target: ..., runner: ...} for a runner the kit does not
know. The default is the first five below.
| Target | Runner |
|---|---|
x86_64-unknown-linux-gnu |
ubuntu-latest |
aarch64-unknown-linux-gnu |
ubuntu-24.04-arm |
aarch64-apple-darwin |
macos-latest |
x86_64-apple-darwin |
macos-latest (cross-compiled; GitHub retired the Intel runners) |
x86_64-pc-windows-msvc |
windows-latest |
x86_64-unknown-linux-musl |
ubuntu-latest (not in the default list) |
aarch64-unknown-linux-musl |
ubuntu-24.04-arm (not in the default list) |
aarch64-pc-windows-msvc |
windows-11-arm (not in the default list) |
targets:
- x86_64-unknown-linux-gnu
- {target: x86_64-unknown-linux-musl, runner: ubuntu-latest}
A target with no known runner and none given is refused by check.
For a rust-binary musl target, the build action installs musl-tools, so
crates with C code (ring, zstd, ...) link statically. A maturin musl wheel
is built in maturin-action's rust-musl-cross container; to build the
aarch64 one cross-compiled on x86_64, give the runner:
{target: aarch64-unknown-linux-musl, runner: ubuntu-latest}.
prepare: before the build¶
Every kind takes an optional prepare: a bash command run in the build job
before anything is built (for maturin, before maturin-action), from the
repository root, with VERSION, PACKAGE and TARGET set. Use it for
files the build ships but the repository does not hold, such as generated
licence notices:
- kind: maturin
prepare: cargo install --locked cargo-about && cargo about generate about.hbs -o THIRD-PARTY-LICENSES.html
test: check what was built¶
Every kind takes an optional test. It runs in the build job, right after the
build, on the files just built. If it fails, the build fails, and nothing that
needs the build is published.
A test is a bash command, run from the repository root with these variables:
| Variable | Value |
|---|---|
DIST |
the absolute path of the output directory |
FILES |
the files this job built, as absolute paths separated by spaces (no .sha256 files) |
VERSION |
the package's version |
PACKAGE |
the package's name |
TARGET |
the job's target, or empty for kinds without targets |
- kind: rust-binary
bin: onto
targets: [x86_64-unknown-linux-gnu, aarch64-unknown-linux-gnu]
test: |
tar -xzf "$FILES" -C "$RUNNER_TEMP"
"$RUNNER_TEMP/onto-v$VERSION-$TARGET/onto" --version | grep -F "$VERSION"
For rust-binary and maturin, a mapping limits the test to some targets.
A cross-compiled build cannot run on its runner (an x86_64-apple-darwin
wheel built on Apple Silicon, for example):
- kind: maturin
targets: [x86_64-unknown-linux-gnu, aarch64-apple-darwin, x86_64-apple-darwin]
test:
run: pip install "$DIST"/*.whl && python -c "import onto"
targets: [x86_64-unknown-linux-gnu, aarch64-apple-darwin]
A mapping without targets runs after every job, like a string. Kinds that
are not built per target (python, vsix, command) take a string, or a
mapping without targets; check refuses targets there. The sdist job of a
maturin artifact runs no test.
What each job produces¶
Every build job uploads its output directory as a workflow artifact named
release--<slug>--<job id>. The slug is the package name with every run of
characters other than letters, digits, ., _ and - replaced by -
(@org/pkg becomes org-pkg). Job ids are listed under
plan. For a github package, every file gets a
<file>.sha256 beside it, in sha256sum format.
GitHub release options¶
A github package can shape its release:
- name: onto
registry: github
manifest: crates/onto-cli/Cargo.toml
version: "0.2.0"
tag: "v{version}"
notes: python3 scripts/changelog-section.py "$VERSION"
checksums: SHA256SUMS
attest: true
artifacts:
- kind: rust-binary
bin: onto
notesruns in thepublish-githubjob, from the tagged commit's checkout, withVERSION,PACKAGEandTAGset. What it prints becomes the release's notes. If it fails or prints nothing, nothing is published. Withoutnotes, GitHub generates the notes.checksumswrites one file, insha256sumformat, over every asset (not the.sha256files), and uploads it with them. The publish action checks it against the downloaded assets as it checks each.sha256.attest: trueattests every asset's build provenance (including the checksums file) withactions/attest-build-provenance, before the upload. Thepublish-githubjob needsid-token: writeandattestations: write; the template grants both.
What check refuses¶
- an unknown registry, or a package listed twice for the same registry;
- a version that is not SemVer (PEP 440 for PyPI), or that differs from the
manifest (
run release.py sync); - a crates.io, PyPI, npm or vscode package whose manifest names another package;
- two packages with the same record tag, or a record tag equal to the release tag;
- a package listed before a package of the same registry it depends on;
- artifacts on crates.io or npm packages, no artifacts on the others, an
unknown kind, a kind its registry cannot publish, a missing or unknown
field, a malformed
test, or a target with no runner; - for a crates.io package, anything that would stop
cargo publish:publish = false, agitorpathdependency without a version, a dependency not on crates.io, a name someone else owns, or a file the package does not carry. See Publishing to crates.io. This part runs only incheck(andpreflight), not intagor the gate.
Before any of that, the file must load. These stop check at once: an empty packages:, a package missing name,
registry, manifest or version, an unquoted version, artifacts that are
not a list of mappings, or a name_from that names no package.