Skip to content

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.

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" in package.json, for a Node project;
  • otherwise the first line of a VERSION file.

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.

  1. 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.
  2. Its owners approve it, and a Maintainer merges it (or sets auto-merge), fast-forward, once the review bot’s statuses pass.
  3. 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 (as inkylabs_bot; GitLab refuses its deploy key a protected tag), and starts the tag’s pipeline itself, as inkylabs_bot. A project’s CI skips the pipeline GitLab starts for the new tag (a push pipeline 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 to master starts no pipeline: the merged commit is the one the merge request built and tested.
  4. The tag’s pipeline deploys. Its deploy job runs only for a v* tag pipeline that inkylabs_bot started. The bot sets INKYLABS_PREVIOUS_RELEASE to 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.yml shows how). It’s unset when there’s no such release, and then every job runs.

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:

Terminal window
inkylabs-mr --ship # patch: 1.2.29 → 1.2.30
inkylabs-mr --ship minor # 1.2.29 → 1.3.0 (or major)
inkylabs-mr --ship --no-watch # stop once auto-merge is set
inkylabs-mr --watch # pick up watching again, later

inkylabs-mr --ship bumps the version on master into the branch’s last commit (adding “; release ” to its subject), opens the merge request as 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 --watch again 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).

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.

.gitlab-ci.yml
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 (or VERSION), matching the latest tag, so merging it releases nothing;
  • run inkylabs-review-bot-enroll <project>, which protects v* tags for maintainers (so for inkylabs_bot, which creates them);
  • add the workflow rule that keeps a push to master from starting a pipeline (Verified commits).

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.

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.

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.