Branches, CI rounds and releases#
Owner rule for every repository of the family (2026-10-15). It replaces the
2026-09-24 rule that every platform lane runs on every push. Which repository
follows it today: ci-build-triggers.md.
The rule#
developis the default branch. All work is committed there.Every push to
developruns the Linux x64 lane (linux-x64.yml: the lint gates, the build, the tests, the docs), and so does every pull request. No other platform lane starts on its own. Path-filtered helpers such assubmodule-pins.ymlkeep their filters.Every other lane runs on request. Linux arm64, Windows x64 and arm64, Android, web and whatever else a repository builds run when a commit message asks for them with
[ci <lane>], or when someone starts them by hand.mainis the release branch. It moves only by a fast-forward to adevelopcommit whose full round, every lane, is green.Every push to
mainmakes a release, and only when every lane is green.release.ymlruns all lanes once more on that commit and publishesv<version>only if every one of them passes.
Asking for a lane in a commit message#
Write [ci <word>] anywhere in the message. A lane’s name is its workflow file name
without .yml, and a lane <os>-<arch> answers to each part of its name:
Token |
Runs |
|---|---|
|
every lane: the full round |
|
|
|
|
|
that lane alone |
|
that lane |
|
several words in one token: both Windows lanes and Android |
fix(plugin): link order on arm64 [ci arm64]
feat(windows): DirectML by default [ci windows]
chore(release): 2.1.0+3 [ci all]
Every commit of a push counts, not only the newest: a token in the second of five commits still runs its lanes.
Case does not matter.
[ci-windows]or(ci windows)is not a token.A token anywhere in the message counts, prose included. A body that says “the earlier
[ci all]round was green” asks for every lane: OmniAccelerANT’s f827b20 started all five that way when it meant only[ci web]. To mention a token, leave out its brackets.A word that names no lane is a warning in the run’s log, not an error.
[skip ci]is GitHub’s own and still skips every workflow, Linux x64 included.
The on-request.yml workflow reads the tokens through the hub’s
requested-lanes action and calls
each lane it picks. A push without any [ci in its messages costs nothing: the
reading job is skipped, so no runner starts.
Running a lane by hand#
Every lane workflow has workflow_dispatch. In the browser: Actions → the workflow →
Run workflow, with a branch. From a terminal:
gh workflow run windows-x64.yml --ref develop # one lane
gh workflow run on-request.yml --ref develop -f lanes="windows web" # several
gh workflow run on-request.yml --ref develop -f lanes=all # the full round
--ref takes any branch, so a pull request’s branch can be tried on Windows the
same way. The run uses that branch’s newest commit at the moment it starts.
Merging into main#
Only the owner moves main. An agent prepares the merge and stops there, unless the owner
tells it in that turn to push; it then checks step 2 itself and pushes exactly that commit
(OmniAccelerANT’s 2.1.0, 2026-10-15).
Prepare the release on
develop. Raise the version in the repository’s manifest and turn the CHANGELOG’s## Unreleasedinto## <version> — <date>, with a new empty## Unreleasedabove it. Put[ci all]in that commit’s message.Wait for the full round on that commit. Every run must be green, and the
on-request.ymlrun must have run every lane rather than skipped one:sha=$(git rev-parse develop) gh run list --commit "$sha" --json name,conclusion --jq '.[] | "\(.conclusion) \(.name)"' gh run view <on-request run id> --json jobs --jq '.jobs[] | "\(.conclusion) \(.name)"'
skippedis notsuccesshere:github-cli-pipeline-monitoring.md.Fast-forward
mainto exactly that commit.git push origin "$sha":main
git refuses this when it is not a fast-forward. Then
mainholds a commitdeveloplacks: mergemainintodevelopfirst and start again at step 2.
The release#
A push to main starts release.yml:
Version check, about a minute. The version must have a CHANGELOG section and no release yet; otherwise the run stops here. Every merge into
mainis a release, so every merge carries a new version.Every lane, called as reusable workflows in this one run.
The release job, which
needs:every lane. It runs only when all of them succeeded. It tags the commitv<version>and publishes the GitHub release with the packages the lanes built in this run, aSHA256SUMS, and the version’s CHANGELOG section as notes.
A red lane means no tag and no release. A flaky failure is rerun with gh run rerun <id> --failed, and the release job follows once the rerun is green. A real failure is
fixed on develop and merged again.
Both the version check and the release job run the hub’s
linux/scripts/06-packaging/publish-github-release.sh: --version-file names pubspec.yaml,
Cargo.toml or a VERSION.txt (read by version_util.sh --file-version, which never falls
back to a branch name), --changelog the file whose ## <version> or ## [<version>] section
becomes the notes, --name the release title, then --check or --assets <dir>. It refuses a
version that is released already, a version without a section, notes over GitHub’s 125000
characters, a GitHub that does not answer and an empty directory
(test-publish-github-release.sh, 34 assertions).
How a repository implements it#
File |
Triggers |
|---|---|
|
|
every other lane ( |
|
|
|
|
|
A calling job looks the same in both:
windows-x64:
needs: request
if: contains(needs.request.outputs.lanes, ' windows-x64 ')
# The union of what the lane's own jobs ask for; less fails the run at start.
permissions:
contents: read
security-events: write
uses: ./.github/workflows/windows-x64.yml
secrets: inherit
Permissions. A called workflow’s jobs get at most what the calling job grants. Grant less than a lane’s job declares and GitHub rejects the whole run before any job starts: The nested job ‘…’ is requesting ‘…’, but is only allowed ‘…’.
Concurrency:
group: <lane>-${{ github.ref }}-${{ github.workflow }}withcancel-in-progress: true. Inside a called workflowgithub.workflowis the caller’s name, so a standalone run,on-request.yml’s call andrelease.yml’s call each get a group of their own, and a newer run from the same caller on the same branch replaces an older one. A skipped calling job never starts the lane, so an ordinary push cancels no round someone asked for.Not the lane name alone.
linux-x64-${{ github.ref }}is the group every push todevelopstarts the lane in, and a dry run ofrelease.ymlondevelopcalls the lane in it too, so each cancels the other: OmniAccelerANT’s first dry run, dispatched a minute after a push, cancelled that push’s Linux x64 run (1914a28, 2026-10-16).Not
${{ github.workflow }}alone. Every laneon-request.ymlcalls would share one group and cancel or queue behind each other; OxidANT’s lanes did until they moved (2026-10-15).
A dry run. OmniAccelerANT’s
release.ymltakes adry-runinput: dispatched ondevelop, it runs every lane and collects the release, lists its files in the run’s summary and publishes nothing, which is the full round before a merge as well.Artifacts.
release.ymlholds every lane’s uploads in one run, so artifact names must differ across lanes.Pushes, not tags, start a release. The tag is the release job’s output. Nothing listens for
push: tags.OmniAccelerANT is the reference: its
on-request.ymlandrelease.yml. Its release job fetches only the hub submodule (git submodule update --init --depth 1 third_party/ANTfrastructure), not the others, to run the script.
Why this shape#
Linux x64 on every push, the rest on request. Every push used to start every lane, the Windows ones near an hour each, and the newest push cancelled the round in flight. On 2026-10-12 that cancelled 77 of OmniAccelerANT’s 142 push runs. The Linux x64 lane catches most breakage, and the platform lanes run when a change is about them or a release is near.
One
on-request.yml, not anif:in every lane. A lane triggered on every push with its jobs skipped still enters its concurrency group, and its empty run cancels the requested round in flight. A skipped calling job starts nothing. The token parsing lives in one tested script instead of a copied expression in every lane.The release round runs again on
main. The packages are then built from the tagged commit in the same run, and “all green” holds by construction (needs:). The price is one more full round per release, on runners that are free for public repositories.No branch ruleset with required checks. GitHub counts a job skipped by its
if:as passed for a required check, so a commit whose full round nobody asked for would pass it.The release workflow does not move
mainitself.GITHUB_TOKENcannot be given theworkflowspermission, and GitHub refuses its pushes that bring in changes under.github/workflows/, which nearly every release has.