Source code
Where it lives
Section titled “Where it lives”Every SVRBC repository is a project in the
svrbc group on GitLab.com. The one
subgroup is svrbc/forks, which
holds a shared fork of each project. GitLab also runs CI (Builds and CI) and serves the
static sites through GitLab Pages (Domains).
The group is on GitLab’s free tier, and CI jobs run on GitLab.com’s shared runners.
The primary branch is master
Section titled “The primary branch is master”Every SVRBC 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. Nobody merges by hand either: the merge bot does, once the owners have approved.
svrbc-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
svrbc/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
(svrbc/forks/doreancon.org).
git clone git@gitlab.com:svrbc/<project>.git # clone the project, not the forkcd <project>svrbc-fork # once: make sure the fork exists, add it as `fork`git switch -c <topic> # …commit…git fetch origin && git rebase origin/master # merges are fast-forward: start from current mastersvrbc-fork mr # push to the fork, open the merge request(svrbc-fork is in this handbook’s tools/.)
What happens next:
- It is built and tested once. On a trusted workstation,
svrbc-fork mrdoes it before it pushes: it runsbazel test //..., uploads the results to the shared cache and records the commit as verified. Otherwise the merge bot starts the fork’s pipeline, which does the same (CI insvrbc/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 the merge bot merges it, fast-forward. 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 merge bot turns into the tag, and the tag’s pipeline deploys (Releases).
- The fork’s
mastercatches up: the merge 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 SVRBC. A personal fork leaves with its owner, and
its CI runs on the owner’s account; a fork in
svrbc/forksruns on SVRBC’s, with SVRBC’s 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 SVRBC (a fork somewhere else) can’t.
How it’s enforced
Section titled “How it’s enforced”| Where | Branch | Push | Merge | Force-push |
|---|---|---|---|---|
svrbc/<project> |
master |
Only the merge bot’s deploy key | No one | no |
svrbc/<project> |
* (anything else) |
No one | No one | no |
svrbc/forks/<project> |
master |
Maintainers (CI’s sync, as svrbc_bot) |
Maintainers | no |
svrbc/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, where it has CI, requires the
pipeline to pass. 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.
For a new project, an owner sets this up once:
P="projects/svrbc%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=trueThen, once its CODEOWNERS has merged, hand it to the merge bot:
svrbc-merge-bot-enroll <project> adds
the bot’s deploy key and webhooks and changes master to the first row
above, and a merge request adds the project to
tools/merge-bot/projects.txt.
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.org’s next, at next.svrbc.org; master stays
svrbc.org). It follows the same three rules, and is protected exactly as
master is, push only by the merge bot’s deploy key and merge by no one,
which is also what opts it in to the bot: the bot merges into master and
into any branch so protected, by that branch’s own CODEOWNERS. Changes
reach it the usual way, svrbc-fork mr --target <branch>; what is ready for
production moves on to master by a merge request from the staging branch
to master.
svrbc-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 *. doreancon.org’s is
* @cco3: one owner for the whole project.
The merge bot
Section titled “The merge bot”The rule is that a merge request needs approval from the owners of every path it changes. GitLab Free (svrbc’s plan) records approvals but never requires them, and can’t limit merging to named people; both are Premium. So nobody merges: a bot does, once the rule is met.
- Approve with GitLab’s Approve button. That is all an owner does.
- The bot merges when the latest commit’s pipeline has passed, or it has
no pipeline and is verified, and
every changed path has an approval from one of its owners given after the
latest change. GitLab won’t let authors approve their own merge requests,
so an author who owns what they changed adds the
merge-bot::readylabel instead; that counts as their approval. A rebase that leaves the changes alone keeps approvals; any other push resets them. Drafts never merge. - It rebases, as the last step. A merge request that isn’t on top of
masteris rebased by the bot (GitLab’s rebase, which keeps the approvals) once the owners have approved and nothing but its tests is missing, and only when GitLab sees no conflict; a conflict, or a rebase GitLab couldn’t do, is left to a person, and the bot’s comment says so. The rebased commit is new, so it’s tested again: one pipeline, mostly cache hits, which the bot starts. It doesn’t test the stale commit first, and it doesn’t rebase a merge request whose stale commit failed. - The rules come from
master’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 the merge. - It runs a pipeline only when one is needed. When the latest commit isn’t verified and has no pipeline, the bot starts one in the fork. 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 (the handbook’s plans its OpenTofu changes), so a project that still runs a pipeline on every push waits for it as before. Turning those off (Verified commits) is what lets a verified commit skip CI.
- It labels where the tests stand:
tests::passed(verified, or the pipeline passed),tests::testing(a pipeline is starting or running) ortests::failed. One at a time; it removes the others. - It keeps one comment on the merge request, edited in place, saying what it is waiting for.
- It merges by pushing the merge request’s head commit to
masterwith its deploy key (merges are fast-forward, so that push is the merge), and GitLab marks the merge request merged.masteris protected so that only that key can push and nobody can merge. - Then 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. Both are idempotent, and each 15-minute sweep finishes either if a run didn’t.
It acts within seconds of the approval or the pipeline: webhooks start it,
and a run every 15 minutes catches anything they missed. The code is
tools/merge-bot/,
and it runs as an AWS Lambda function (merge-bot).
The projects it looks after are listed in tools/merge-bot/projects.txt.
svrbc-audit fails a project with no CODEOWNERS or
no * rule, and warns about one the bot doesn’t look after yet.
Breaking glass. Only a group Owner can get around the bot, by changing
master’s protection, the same limit Premium has. Do it only when the bot is
down and a fix can’t wait; put the protection back with
svrbc-merge-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. 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 svrbc/forks, open merge requests and issues, create projects |
| Maintainer | Also change project settings. Merging is the merge bot’s, once a project’s owners approve |
| 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:svrbc/<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
svrbcgroup, not under your own account, so it never has to be transferred. - Public by default. Most SVRBC projects are public, and this handbook is written on that assumption. Make a project private only when its contents are private, such as member 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
(
baptistcatechism.org,psalter.svrbc.org). Name anything else in lowercase kebab case (two-by-two,street-tracts). - No
svrbc-prefix inside the group: the group already says whose it is (svrbc/merge-bot, notsvrbc/svrbc-merge-bot). Keep the prefix where names are global or shared: AWS resources (Naming), commands onPATH(svrbc-fork), and local clones in a flat~/projects/(~/projects/svrbc-merge-bot). A few older projects (svrbc-bulletin,svrbc-constitution,svrbc-nuxt-symbolics) predate this; renaming one breaks every clone and link, so it isn’t worth doing just for this. - Add an
AGENTS.mdthat points back to this handbook. See Agents files. - 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 SVRBC’s cross, the circuit-board one in svrbc.org’s
static/logo-transparent.svg(this handbook’s is that cross in angle brackets). Put it on a solid background: a black logo on a transparent one disappears in GitLab’s dark theme.
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. svrbc-audit and
svrbc-merge-bot-enroll skip it.
Delete its fork in svrbc/forks when you archive it: work pushed there
could never merge. svrbc-audit fails a fork whose
project is archived or gone (no-stray-forks).
Unarchive it to work on it again (and svrbc-fork makes it a new fork); it
then has to meet the rules like any other project. Archived so far: svaforum.org, and doreanpress.org (which moved
to GitHub; its 2021 history is only here).
Shared CI templates
Section titled “Shared CI templates”svrbc/ci-templates holds CI
configuration that projects include rather than copy. So far it has
npm-publish.yml, which publishes to npm with trusted publishing (OIDC), so
there is no NPM_TOKEN to store. Each template starts with a comment
explaining how to use it. For this one:
include: - project: svrbc/ci-templates ref: master file: /npm-publish.yml
deploy: extends: .npm-publishThe Bazel-based CI template described in Builds and CI will go there too.
Open questions
Section titled “Open questions”- Require two-factor authentication for group members. It is currently off.