Source code
Where it lives
Section titled “Where it lives”Every Inky Labs repository is a project in the
inkylabs group on GitLab.com. Its
subgroup inkylabs/forks
is to hold a shared fork of each project (not yet created). An older
subgroup, inkylabs/staging (“A staging area for changes”), predates these
rules and is not a fork group. GitLab also runs CI (Builds and CI)
and can serve developer-facing sites through GitLab Pages
(Domains); most of Inky Labs’
sites are on Vercel today.
The group is on GitLab’s free tier, and CI jobs run on the group’s own runners (Runners).
The primary branch is master
Section titled “The primary branch is master”Every Inky Labs repository’s primary branch is master. Not main, even
though GitHub and newer git init default to it. One name everywhere means
scripts, CI rules, branch protection and these docs never have to guess.
- It is the group default, so a project created in GitLab starts on
master. A repository created locally and pushed may not. Check withgit symbolic-ref --short HEADbefore the first push, or start it withgit init -b master. - Nobody pushes to it, owners included. It changes only by merging a merge request, and nobody can force-push. A Maintainer merges, once the review bot’s statuses say the owners approved and the tests passed.
Inky Labs’ projects predate these rules, and not all follow them yet:
edit-server, kid-years and remark-fancyquote are on main, and a
project’s master takes pushes until it is set up as
below.
inkylabs-audit reports any project whose default branch
isn’t master, or whose master takes pushes.
Changes arrive as merge requests from forks
Section titled “Changes arrive as merge requests from forks”Every change is a merge request from the project’s fork in
inkylabs/forks. The project itself
keeps only master; branches live in the fork, which every member can push
to. One fork per project, shared by everyone, with the project’s own name
(inkylabs/forks/qev).
git clone git@gitlab.com:inkylabs/<project>.git # clone the project, not the forkcd <project>git switch -c <topic> # …commit…git fetch origin && git rebase origin/master # merges are fast-forward: start from current masterinkylabs-mr # push to the fork (made the first time), open the merge request(inkylabs-mr is in this handbook’s tools/.) To release
the change as well, inkylabs-mr --ship puts the version bump in it, sets
auto-merge and watches it deploy (Releases).
What happens next:
- It is built and tested once. On a trusted workstation,
inkylabs-mrdoes it before it pushes: it runsbazel test //..., uploads the results to the shared cache and records the commit as verified. Otherwise the review bot starts the fork’s pipeline, which does the same (CI ininkylabs/forksis trusted to write the cache). A push to a fork starts no pipeline by itself, so a commit already verified is never built again. - Its owners approve it, and a Maintainer merges it, fast-forward, with GitLab’s merge button or Set to auto-merge, once the review bot’s statuses pass. The merged commit is exactly the commit that was built and tested, so its cache keys are the same.
masterstarts no pipeline. There’s nothing for it to do: the merged commit is the one that was built and tested.- Releases are merge requests too: a reviewed version bump, which the review bot turns into the tag, and the tag’s pipeline deploys (Releases).
- The fork’s
mastercatches up: the review bot fast-forwards it, so a branch pushed later doesn’t carry a backlog ofmaster’s commits up with it.
Why it’s set up this way:
- The project stays clean: no branches, ever. Abandoned work is in the fork, where it bothers nobody, and the source branch is deleted when its merge request merges.
- The forks belong to Inky Labs. A personal fork leaves with its owner, and
its CI runs on the owner’s account; a fork in
inkylabs/forksruns on Inky Labs’, with Inky Labs’ cache. - Building once. A heavy change is built in its fork, and
masterand every developer’s machine reuse it. Only the fork pipeline writes; anyone outside Inky Labs (a fork somewhere else) can’t.
How it’s enforced
Section titled “How it’s enforced”| Where | Branch | Push | Merge | Force-push |
|---|---|---|---|---|
inkylabs/<project> |
master |
No one | Maintainers, once the statuses pass | no |
inkylabs/<project> |
* (anything else) |
No one | No one | no |
inkylabs/forks/<project> |
master |
Maintainers (CI’s sync, as inkylabs_bot) |
Maintainers | no |
inkylabs/forks/<project> |
other branches | Developers and up | — | yes, your own branch |
Each project also merges fast-forward only, with squashing turned off,
deletes the source branch on merge, and requires the pipeline to pass, never
counting a skipped one and never rebasing at merge time (automatic rebase,
which would merge a commit no pipeline tested). Both halves of “fast-forward only” matter: a merge commit
or a squash would each put a new commit on master, not the one the fork
built and tested. The forks are set the same way, so nothing lands on them
differently either.
GitLab deletes a merge request’s branch from the fork only when someone
merges it in GitLab; a branch merged any other way stays. The audit warns on any branch left in a fork that
is already merged into its master; an owner clears them all with
glab api -X DELETE "projects/inkylabs%2Fforks%2F<project>/repository/merged_branches",
which leaves protected and unmerged branches alone.
For a new project, an owner sets this up once:
P="projects/inkylabs%2F<project>"glab api -X DELETE "$P/protected_branches/master"glab api -X POST "$P/protected_branches" -f name=master -F push_access_level=0 -F merge_access_level=40 -F allow_force_push=falseglab api -X POST "$P/protected_branches" -f name='*' -F push_access_level=0 -F merge_access_level=0glab api -X PUT "$P" -f merge_method=ff -f squash_option=never -F remove_source_branch_after_merge=true \ -F only_allow_merge_if_pipeline_succeeds=true -F allow_merge_on_skipped_pipeline=false -F automatic_rebase_enabled=falseThen, once its CODEOWNERS has merged, hand it to the review bot: a merge
request adds the line <project> review to
tools/review-bot/projects.txt,
and once it is released
inkylabs-review-bot-enroll <project> adds
the bot’s deploy key and webhooks and checks the settings above.
Enrolling also lets the bot’s key push the fork’s master, which the bot
keeps in step after every merge.
Exceptions
Section titled “Exceptions”An exception lets a tool push branches to the project itself, for a tool that can only work that way (a CMS that saves drafts as branches, say). It needs all three:
- An owner agrees, and adds a protected-branch rule for exactly that pattern, letting the role that needs it push.
- The project’s
AGENTS.mdexplains it: the pattern, what creates those branches, and who cleans them up. - It’s removed when the need goes.
A staging branch is the other kind: a long-lived branch that reviewed
changes merge into, deployed to a staging site of its own rather than to
production (SVRBC’s svrbc.org has one, next, at next.svrbc.org, while
master stays svrbc.org; Inky Labs has none). It follows the same three
rules, and is protected exactly as master is; projects.txt names it
(<project> next=review), and the bot
treats it like master, by that branch’s own CODEOWNERS. Changes
reach it the usual way, inkylabs-mr --target <branch>; what is ready for
production moves on to master by a merge request from the staging branch
to master.
inkylabs-audit fails a project with no * rule, and
warns about an exception its AGENTS.md doesn’t mention and about any branch
other than master that no exception covers.
Owners
Section titled “Owners”Every project has a CODEOWNERS file saying who must approve its
changes. It lives at .gitlab/CODEOWNERS, in
GitLab’s format,
and its first rule, *, names the project’s owners: whoever must approve any
change. Narrower rules after it give parts of the project their own owners
(the last matching rule wins):
# .gitlab/CODEOWNERS* @cco3/content/ @cco3 @a-content-editorAn owner is a person, by GitLab username. A change to CODEOWNERS is
reviewed like any other, by the owners of *. This handbook’s is
* @cco3: one owner for the whole project.
The review bot
Section titled “The review bot”Not yet running for Inky Labs (its setup is in
infra/review-bot).
It is SVRBC’s bot, called the merge bot there until 2026-10-08.
The rule is that a merge request needs approval from the owners of every path it changes. GitLab Free (Inky Labs’ plan) records approvals but never requires them; that is Premium. So people merge, and the bot tells GitLab whether they may: it posts two commit statuses, and Pipelines must succeed holds the merge button until they, and the tests, pass.
- Approve with GitLab’s Approve button. That is all an owner does. An author who owns what they changed approves it by not marking it a draft: GitLab won’t let authors press Approve, and the merge is their go-ahead. Draft is how to say “not yet”. Approvals given after the latest change count; a rebase that leaves the changes alone keeps them.
review-bot/approvedispendinguntil every changed path has its owners’ approval (its description says whose), thensuccess;failedfor aCODEOWNERSthe bot can’t read. The rules come from the target branch’sCODEOWNERS, never the merge request’s, so a merge request can’t change the rules it is judged by. The bot reads a strict subset of GitLab’s syntax (@usernameowners, sections such as[Docs][2]for two approvals and^[Optional]) and refuses anything else with a message, rather than guessing. A path no line covers blocks it.- The tests: a commit verified on a trusted
machine, with no pipeline of its own, gets
review-bot/verified,success. Otherwise the bot starts the fork’s pipeline, whose jobs speak for themselves. It never reruns a failed one: push a fix, or retry it in GitLab. A pipeline that did run always counts, verified or not, since a project’s pipeline may do more than the marker vouches for. An external pipeline, the kind GitLab makes for a status posted through its API, never counts as tests. - Merge with the button, or Set to auto-merge. Auto-merge is the
merge queue. Merges are fast-forward, so each merge leaves the others
behind, and GitLab cancels their auto-merge; the bot rebases the first
approved one in the queue (never with
skip_ci, which would leave GitLab judging a skipped pipeline), sets auto-merge on it again, and GitLab merges it once its statuses pass; then the next. The rest wait unrebased, so a batch of n merge requests costs n pipelines, not one per merge for each. GitLab won’t set auto-merge on one already behind: click Rebase first. One that fails or conflicts leaves the queue, and the bot says why. - It keeps one comment on the merge request, edited in place, saying what it is waiting for and where it is in the queue.
- After a merge it releases and syncs the fork: a version bump gets its
v<version>tag, created through the API, and the bot starts the tag’s pipeline (Releases); the fork’smasteris fast-forwarded with its key to match. A push event starts it; each 15-minute sweep finishes either if a run didn’t.
A guardrail, not enforcement. It stops mistakes: merging before the owners approve or the tests pass. It doesn’t stop someone determined: anyone with Developer can post a status named like the bot’s (the bot names such a status in its comment and posts its own again), and a Maintainer can turn Pipelines must succeed off. Developers are trusted already; getting around the bot on purpose is a breach of these rules, not a workaround.
It acts within seconds of an approval, a push or a pipeline: webhooks start
it, and a run every 15 minutes catches anything they missed. The code is
tools/review-bot/,
and it runs as an AWS Lambda function (review-bot).
The projects it looks after are listed in tools/review-bot/projects.txt,
with any branch besides master it looks after (<branch>=review, a staging
branch). inkylabs-audit fails
a project with no CODEOWNERS or no * rule, and checks the settings
review mode relies on (merge-checks).
At SVRBC it was first a merge bot: master took pushes only from its
deploy key, nobody merged, and the bot merged by pushing once the owners
approved. That mode is gone.
Breaking glass. A Maintainer can merge past a stuck status by turning
Pipelines must succeed off. Do it only when the bot is down and a fix can’t
wait; put it back with inkylabs-review-bot-enroll, and expect the audit to
flag the project until then.
Getting access
Section titled “Getting access”You can’t request access from the group page; that button is turned off. (Not yet for Inky Labs: it is still on.) Ask an owner to add you. New members start as Developer, which is enough to contribute: push branches to the forks and open merge requests.
| Role | Can |
|---|---|
| Developer | Push branches to inkylabs/forks, open merge requests and issues, create projects |
| Maintainer | Also change project settings, and merge once the review bot’s statuses pass |
| Owner | Also manage members, subgroups and group settings, and grant exceptions |
Nobody, owners included, pushes to a project’s master.
Clone over SSH once your key is on your GitLab profile:
git clone git@gitlab.com:inkylabs/<project>.gitglab, GitLab’s CLI, is the easiest way
to script against the API. The examples in this handbook use it. Log in once
with glab auth login.
New projects
Section titled “New projects”-
Create it in the
inkylabsgroup, not under your own account, so it never has to be transferred. -
Public by default. Most Inky Labs projects are public, and this handbook is written on that assumption. Make a project private only when its contents are private, such as personal information or unpublished material. Even in a private project, credentials still go in CI/CD variables, never in the repository.
-
Name a project that publishes a site after its domain (
inkylabs.com,lofi.inkylabs.com). Name anything else in lowercase kebab case (edit-server,remark-verses). A few older projects predate this (qev, which publishes qev.inkylabs.com); renaming one breaks every clone and link, so it isn’t worth doing just for this. -
No
inkylabs-prefix inside the group: the group already says whose it is (inkylabs/review-bot, notinkylabs/inkylabs-review-bot). Keep the prefix where names are global or shared: AWS resources (Naming), commands onPATH(inkylabs-mr), and local clones in a flat~/projects/(~/projects/inkylabs-review-bot). -
Add an
AGENTS.mdthat points back to this handbook. See Agents files. -
A new site starts from the template:
inkylabs-new-sitewrites its repository, and its AWS resources and DNS records here, step by step. -
Give it a GitLab avatar of its own (Settings → General → Project avatar), so lists, merge requests and notifications tell the projects apart at a glance. A project with a site uses the site’s logo; others build on this handbook’s mark, an ink drop (
#1f2a44) between angle brackets (#3b5bdb), insrc/assets/logo-light.svg: a placeholder until Inky Labs has a logo of its own. Put it on a solid background: a dark logo on a transparent one disappears in GitLab’s dark theme. -
Name the same logo in a
t3.jsonat the repository’s root, so T3 Code shows it beside the project as GitLab does. PointiconPathat the committed image the avatar comes from (a site’spublic/favicon.svg):{"$schema": "https://t3.codes/schema/t3.json","iconPath": "public/favicon.svg"}The image must stand alone: an SVG that draws another file through
<image href>(a site’sicons/maskable.svg) shows up blank.
Retired projects
Section titled “Retired projects”A project nobody works on any more is archived, not left failing the
handbook’s rules (Settings → General → Advanced → Archive project). An
archived project is read-only: no pushes, merge requests or pipelines, though
it stays visible and can be cloned, and a site it already deployed keeps
serving. inkylabs-audit and
inkylabs-review-bot-enroll skip it.
Delete its fork in inkylabs/forks when you archive it: work pushed there
could never merge. inkylabs-audit fails a fork whose
project is archived or gone (no-stray-forks).
Unarchive it to work on it again (and inkylabs-mr makes it a new fork); it
then has to meet the rules like any other project. None is archived yet.
Shared CI templates
Section titled “Shared CI templates”CI configuration that projects include rather than copy lives in this
handbook’s ci/
(Builds and CI). SVRBC also keeps
svrbc/ci-templates, whose
npm-publish.yml publishes to npm with trusted publishing (OIDC), so there
is no NPM_TOKEN to store. Inky Labs has no such project yet.
Open questions
Section titled “Open questions”- Require two-factor authentication for group members. It is currently off.