In June I moved everything to GitLab and wrote a hundred confident words about why. Last weekend I moved it all back. This is the post that closes the loop.

The June reason

In leaving-github-for-gitlab I wrote the trigger plainly: a visibility scare had me asking why I was still on GitHub at all. And gitlab-superior-for-solo-devs made the case: one URL, one login, integrated CI on every push.

That reasoning was honest. It also had an expiry date I could not see yet.

What expired

Reason (June) Status (October)
Integrated CI on every push Pipeline went local — gates run on this machine, remote CI added nothing
One login, one home Code already lived on GitHub — the submodule, the public tools
Two hosts add safety Two hosts meant two drift sources, not safety

The CI row is the whole story. Once bb gates and the pre-push hook did the real checking, the hosted pipeline became a green badge over work done elsewhere. I kept paying the second-host tax — second login, second token, second drift surface — for a badge.

Prove it, then move it

Nothing switched until both hosts held identical refs. I saved the branch-and-tag list from the old host, pushed branch plus tags to the new private repo, and diffed the two lists until they matched exactly.

Then the stronger proof: a fresh clone from the new host, submodules included, built green with zero gate failures. Only after that did the old remote get renamed to a frozen spare and the new one become origin.

Migration log

Every step below ran once, in order, with output saved. No step ran twice.

Step Command Result
Record git ls-remote origin main + 3 tags saved
Create gh repo create homepage --private PRIVATE, empty
Push git push branch, then tags main + 3 tags live
Prove fresh clone, bb doctor, bb build 0 failures, exit 0
Switch rename remotes, new origin frozen spare kept
Retire archive old repo server-side private, read-only
Rewrites 7 files, CI file deleted committed, pushed

Fewer hops on purpose

Reads were batched into single passes and every green result was accepted immediately — never re-run. The two pushes went out back to back with no inspection round trip between them.

Verification was one pass, not a loop: ref diff, then clone, then build. Three proofs, one direction, zero repeats. Fewer hops is not haste; it is refusing to pay for certainty twice.

The safety switch

The new host notifies Cloudflare on every push, and Cloudflare tries to build. Its build command is exit 1 — it always fails, so it can never deploy anything. The red badge is the design, not an accident.

Real deploys stay on the direct-upload path, and the live stamp check still rules: the served build must equal HEAD, or nothing counts as shipped.

Decisions expire when constraints change — write down the expiry date.