Releases
A release is a merge request. Like every other change, it is reviewed before it happens: a project’s release is a reviewed change to its version number, and the tag that deploys it follows from the merge. Nobody tags a release by hand.
Not yet set up for Inky Labs: there is no review bot, inkylabs_bot user or
inkylabs/forks group yet, so no Inky Labs project releases this way. The
process is SVRBC’s (Releases), where
it runs.
Why a version file
Section titled “Why a version file”A tag can’t go through a merge request: it isn’t a change to any file, only a name pointing at a commit. So the decision to release is written into a file the project already keeps, and CI makes the tag from it:
"version"inpackage.json, for a Node project;- otherwise the first line of a
VERSIONfile.
Reviewers then see the release as an ordinary diff:
"version": "1.1.31","version": "1.1.32",Approving and merging that is the release. A CHANGELOG.md entry in the same
merge request, saying what’s shipping, is good practice but not required.
What happens
Section titled “What happens”- Open the release merge request: the version bump, from the project’s fork like any change, often in the change’s own merge request (Source code; Shipping a change). Its pipeline runs the full check.
- Its owners approve it, and a Maintainer merges it (or sets auto-merge), fast-forward, once the review bot’s statuses pass.
- The review bot tags it. Right after the merge, it sees a version with
no
v<version>tag yet, creates the tag on that commit through the API (asinkylabs_bot; GitLab refuses its deploy key a protected tag), and starts the tag’s pipeline itself, asinkylabs_bot. A project’s CI skips the pipeline GitLab starts for the new tag (apushpipeline for a tag), so the bot’s is the only one; in one that doesn’t, the bot sees it and starts none. On a merge that didn’t change the version, the tag already exists and it does nothing. A push tomasterstarts no pipeline: the merged commit is the one the merge request built and tested. - The tag’s pipeline deploys. Its deploy job runs only for a
v*tag pipeline thatinkylabs_botstarted. The bot setsINKYLABS_PREVIOUS_RELEASEto the newest earlier release whose pipeline passed, so a job can run only when its files changed since then (rules: changes: compare_to: refs/tags/$INKYLABS_PREVIOUS_RELEASE;ci/release.ymlshows how). It’s unset when there’s no such release, and then every job runs.
Shipping a change
Section titled “Shipping a change”The version bump can ride in the change’s own merge request, and usually does: a commit such as “Add a dark mode; release 1.2.29” is the change and its release together. An owner who wants a change live in one step ships it:
inkylabs-mr --ship # patch: 1.2.29 → 1.2.30inkylabs-mr --ship minor # 1.2.29 → 1.3.0 (or major)inkylabs-mr --ship --no-watch # stop once auto-merge is setinkylabs-mr --watch # pick up watching again, laterinkylabs-mr --ship bumps the version on master into the
branch’s last commit (adding “; release inkylabs-mr always does, sets auto-merge, and watches it
through each step above: approved, merged, tagged, and the tag’s pipeline
passing. It exits 0 only once that pipeline has passed. Everything between
setting auto-merge and the deploy already happens on its own; --ship only
saves the typing, and says when it’s done.
It finishes on its own only when nobody else has to act:
- The author owns every path it changes. Then not marking it a draft is
their approval (The review bot). When someone
else owns one, it says whose approval it’s waiting for and exits 3; run
inkylabs-mr --watchagain once they have. - The author is a Maintainer, which setting auto-merge takes.
It exits 1, with a link, if a pipeline fails or the review bot takes it out
of the auto-merge queue (two releases racing for the same version conflict
on package.json, for one: rebase and ship again), and 4 if it’s still going
after --timeout minutes (60 by default).
Why the deploy checks who made the tag
Section titled “Why the deploy checks who made the tag”GitLab’s free tier can protect tags only by role: v* can be limited to
maintainers, but not to inkylabs_bot alone. A maintainer could still push a
tag by hand. So every deploy job has the rule from .inkylabs-release, which
runs it only when the tag’s pipeline was started by inkylabs_bot. A tag pushed
by a person deploys nothing; the only way to production is a merged version
bump. The bot creates its tags through the API as inkylabs_bot, a
maintainer, with its token’s Repository Tag: Create.
Setting up a project
Section titled “Setting up a project”include: - project: inkylabs/developers.inkylabs.com file: ci/release.yml
deploy-production: # …the project's own deploy steps… rules: - !reference [.inkylabs-release, rules]Then:
- put the current version in
package.json(orVERSION), matching the latest tag, so merging it releases nothing; - run
inkylabs-review-bot-enroll <project>, which protectsv*tags for maintainers (so forinkylabs_bot, which creates them); - add the workflow rule that keeps a push to
masterfrom starting a pipeline (Verified commits).
A staging branch
Section titled “A staging branch”A staging branch deploys on every merge, not from
tags: its pipeline on the project (never the fork) deploys to the staging
site, whose own deploy role trusts only that protected branch, never the
release tags (production’s role trusts only the tags). Only the review bot can
push to it, so a merge there is as reviewed as a version bump; nothing it
deploys is production.
inkylabs-audit’s release-gate may warn about it, which the project’s
AGENTS.md explains.
Data that changes on its own
Section titled “Data that changes on its own”Keep data that changes on its own out of the release, and the site
publishing only from releases. A site whose numbers change without anyone
changing its code gets them from a scheduled AWS function that writes the
data where the page reads it, beside the site rather than in it. SVRBC’s
analytics.svrbc.org, whose visitor counts come
from PostHog, is the example:
infra/analytics-svrbc-org-data/.
Republishing the site from a GitLab pipeline schedule doesn’t work:
GitLab lets only someone allowed to merge into a protected branch own a
schedule on it, and nobody may merge into an Inky Labs project’s master (only
the review bot pushes). SVRBC tried that first, on 2026-10-08, and GitLab
refused to create the schedule.
The bot’s token
Section titled “The bot’s token”Nothing in CI needs a token of inkylabs_bot: the review bot tags
releases and syncs forks with its deploy key, and starts release pipelines
with its own token, which renews itself (review-bot).
So don’t create a group CI/CD variable holding one. (SVRBC’s earlier
ci/release.yml and ci/fork-sync.yml used such a variable, and retired
it.)
inkylabs-audit warns about a project whose CI deploys
from tags without the release gate, and about tags not protected for
maintainers.