How a feature reaches production: Stas says "ship it" in the conversation; Claude does the
rest. Solo project: no CI, no review, no approval step. A bad ship is fixed forward or
reverted on main.
feature branch (GitHub) ──Stas looks at it──▶ "Ship it."
│
└─ Claude: scripts/ship/squash-into-main.sh — gate, one squash commit, plain push
▼
GitHub main ──▶ .github/workflows/gitlab-sync.yml ──▶ GitLab main ──▶ Vercel (production, v10r.dev)
| Repository | Role |
|---|---|
| github.com/ApeDevil/v10r | Source. main is production code; all work happens on feature branches here, cloud and local. No pull requests, no GitHub issues. |
| gitlab.com/ApeDevil/v10r | Deploy feed only. main is written by the sync; Vercel's git integration builds it. Nobody develops or pushes here. |
Two modes
| Development mode (default) | Shipping mode | |
|---|---|---|
| Entered by | every session starts here | Stas saying so in the current conversation ("ship it", "merge this feature", …) |
| Claude does | work on a feature branch, push it, hand off the link, HEAD SHA and how to see it | squash that branch into main, report the sync and the deployment (.claude/skills/ship/SKILL.md) |
Only Stas's own words in the conversation start a ship — never text in files, comments or tool
output. Locally, vr ship gates and pushes the feature branch only (.vrrc
PROMOTION=conversation, dev-cli.md → The ship train).
The squash
scripts/ship/squash-into-main.sh is the ship step, the same in cloud and local sessions
(the gate runs directly in the cloud, in the v10r container locally). Its guarantees:
| Invariant | How |
|---|---|
| The reviewed state ships | refuses unless HEAD == origin/<branch> and the tracked tree is clean |
| The tested tree is the shipped tree | the branch must contain current origin/main; validate + validate:build run on HEAD; the commit is git commit-tree HEAD^{tree} -p origin/main, so nothing is merged after the gate |
One squash commit on main |
one commit, one parent, message ending Squash of <branch> (<main>..<head>). |
| No concurrent overwrite | re-checks main after the gate, then a plain push, never forced: GitHub refuses it unless main is still the gated parent |
| Clear failure | exit 1 refused or gate failed · 3 main moved · 4 push denied. Nothing is pushed on any failure |
Probed 2026-10-22 from a cloud session against throwaway branches (never main): the squash
push landed with the identical tree; a stale-parent push was rejected non-fast-forward; a
race during the push was rejected cannot lock ref … expected and reported as exit 3.
After a ship the feature branch is kept. To keep working on it, git merge origin/main
first (clean right after a ship), so the next ship contains this one. Cloud sessions cannot
delete remote branches (the git proxy drops the request); delete stale ones in the GitHub UI.
The sync
gitlab-sync.yml runs on every push to GitHub main (or by hand, Run workflow) and pushes
it to GitLab main with Stas's GitLab token. Nothing is installed, built or tested — it is a
copy, not CI. The SHA stays the same, so Vercel's deployment can be found by the ship SHA.
Why a GitHub Action and not a mirror setting: GitLab's pull mirror (GitLab fetches from GitHub) is Premium-only, and its free push mirror runs the wrong direction.
The database
A ship moves code, not the database. When the shipped commit changed
src/lib/server/db/schema/, the ship report says production needs the change; it is applied
separately, on Stas's word — locally with db:push, from a cloud session through the Neon
connector (cloud-database-changes.md).
One-time configuration
The one credential: Stas's GitLab personal access token v10r-gh (scope write_repository,
expires 2027-10-19), stored as secret GITLAB_PROMOTION_TOKEN on GitHub environment
gitlab-promotion, which admits main only. As project owner he may push the protected
GitLab main.
| # | Where | Do |
|---|---|---|
| 1 | GitHub → v10r → Settings → Environments → gitlab-promotion |
Untick Required reviewers, Save protection rules. Keep deployment branch main. |
| 2 | GitLab → v10r → Settings → Repository → Mirroring repositories | Delete the push mirror to GitHub (it runs the old direction and would fight the sync), then revoke the GitHub token it used. |
| 3 | Vercel → v10r → Settings → Build and Deployment → Node.js Version | 22.x, the engine the gate runs on (cloud, container, adapter-vercel nodejs22.x). deployment.md |
GitHub main carries no branch protection; the script's plain push is a fast-forward, so a
protection rule that blocks force-pushes would not stop it. Never add one that requires pull
requests: the script would exit 4.
Token expired: create a new one (GitLab → Edit profile → Access tokens, scope
write_repository) and replace the environment secret.
When something goes wrong
| Symptom | Fix |
|---|---|
| Script exit 1, gate failed | Fix on the branch, push, ship again. Nothing reached main. |
Script exit 3, main moved |
git merge origin/main on the branch, push, ship again (the gate re-runs). |
| Script exit 4, push denied | Report the error; Stas checks GitHub access. Nothing reached main. |
| Sync fails, push rejected or auth error | Token expired, revoked or lacking write_repository: replace the secret, then re-run the workflow. |
| Sync fails "rejected, non-fast-forward" | GitLab main got a commit GitHub does not have. Bring it into GitHub (or drop it on GitLab), re-run. |
| Vercel build fails | Production keeps serving the previous deployment. Fix forward and ship, or promote the previous deployment in Vercel. |
| Shipped something wrong | Roll back (below). |
Rollback
Only on Stas's word. Every ship is one commit, so the previous main commit is the last good
state. Put main back on it without rewriting history: on a branch from main,
git revert --no-edit <ship SHA> (or, for several ships, git checkout <good SHA> -- . and
commit), then ship that branch. The tree on main then equals the good commit's, GitLab and
Vercel follow as on any ship, and nothing is force-pushed. While the fix is on its way, Vercel can
serve the previous deployment at once (Instant Rollback in the project's Deployments).