Redrob Code is a fork of opencode, and that single fact decides the branching model. A fork's most consequential operation is not a feature — it is absorbing upstream, which arrives as a large, conflict-prone merge that can leave the tree broken for a while. Everything below exists so that merge never lands somewhere people are cloning from.
| Branch | What it is |
|---|---|
develop |
Default. Integration. Every feature, fix and upstream sync lands here first. |
main |
The released state. Only ever advanced by a release pull request from develop. |
feat/<slug> |
One change, opened as a pull request into develop. Also fix/, docs/, chore/ and the rest of the organization type list, when they describe the change better. |
sync/upstream-<tag> |
An upstream absorption, opened as a pull request into develop. |
hotfix/<slug> |
A fix for something already released, into main and develop. |
release/<version> |
Only when a release needs stabilising before it ships. Usually skipped. |
develop is the default branch on purpose. Pull requests target it without anyone
having to retarget them, and the people reading the repository's default branch are
contributors. End users install a release artifact; they do not clone.
We do not use master, and our branches are deliberately not named after upstream's
(dev, main). When you add an upstream remote, upstream/dev and origin/develop
should not read as the same thing, because they are not.
release/* is in the table for completeness and is usually a waste of a branch. Reach
for it only when a release genuinely needs to stabilise while develop keeps moving.
The full type list is the redrob-labs organization's, checked by the shared gitflow workflow on
every pull request: feat fix chore docs test refactor perf ci build sync hotfix release. The
caller in .github/workflows/gitflow.yml is a verbatim copy of the organization template and
carries no list of its own.
This is the part gitflow has no opinion about, and the part that matters most here.
We follow upstream tags, not its dev branch. A tag is a point upstream themselves
decided was coherent; dev is whatever was pushed an hour ago.
# once
git remote add upstream https://github.com/anomalyco/opencode.git
# each sync
git fetch upstream --tags
git switch develop && git pull
git switch -c sync/upstream-v1.18.32
git merge v1.18.32The upstream-sync workflow does exactly this and pushes the branch for you; run it from
the Actions tab and it prints a link that opens the pull request. When the merge conflicts
it says which paths conflicted and stops, because resolving a fork's conflicts is a
judgement call about which side is right, and a machine that guesses at that produces a
tree nobody can review.
Neither workflow opens a pull request itself. Doing so needs "Allow GitHub Actions to create and approve pull requests", and that single setting grants both — in a public repository, a workflow able to approve pull requests can approve its own, which is worth more than not clicking a link.
Three things to know before your first sync:
The merge base is real, so most of upstream arrives for free. Our own history sits directly on an upstream tag. A three-way merge takes upstream's version of every file we never touched, and every region of a file our changes do not overlap. You resolve only genuine disagreements — measured at 40 files on the first sync, against 229 total conflicts of which the rest were "upstream changed something we deliberately deleted".
Conflicts cluster on rebranding. Where upstream edits a line we renamed, the merge cannot know which side wins. Keep ours, take their surrounding change.
Some opencode strings are load-bearing. Do not "finish the rebrand" while resolving
a conflict. The legacy .opencode config directory is read on purpose so existing
checkouts keep working, and @gitlab/opencode-gitlab-auth, opencode-gitlab-auth and
opencode-poe-auth are real published packages that are not ours to rename. See
NOTICE.md.
Resolve, push, and open the pull request into develop. Never merge upstream straight
into main.
Versions are ours and start at 0.1.0. They are not derived from the upstream version the
tree is built on: that is recorded in UPSTREAM_VERSION at the repository root, and the
release notes read it so every release still states what it is based on. The old
-redrob.N suffix was retired because it made every release a semver prerelease, which
sorts below the release it precedes. See docs/VERSIONING.md.
Run the release workflow from the Actions tab against develop with a bump of patch,
minor or major. It computes the next version from the newest v* tag, builds every
platform, signs and notarizes the macOS binaries, signs the Windows binaries and installer
through Azure Artifact Signing, attaches build provenance, pushes the annotated tag, and
publishes the release. It then prints a link to open the promotion pull request from
develop into main.
main moves through a reviewed pull request rather than an automated push. It is what
people believe is released, and it is the one branch where an unreviewed push would be
hard to notice.
Pass dry_run: true to build, sign and notarize while tagging and publishing nothing. That
is the way to check the signing credentials are actually visible to this repository --
organization secrets are not automatically visible to a new public repository.
The last two jobs are the organization's, not this repository's. Windows signing is the
shared composite action redrob-labs/.github/actions/sign-windows, and the release is the
shared reusable workflow redrob-labs/.github/.github/workflows/release-publish.yml, which
collects every release-* artifact, writes SHA256SUMS.txt and redrob-launcher.json,
and publishes v<version> as latest (as a prerelease when the version carries a -). This
workflow passes it the tag and the commit explicitly, because the tag is created by the
tag job here rather than by a push. The release notes are GitHub's generated notes; the
upstream version the build is based on is in the tag message and in UPSTREAM_VERSION.
What stays this repository's: version resolution, the Bun build, macOS notarization, the
Linux archives, the Redrob Launcher asset copies (launcher-assets.ts), the VERSION
marker install.sh reads, and build provenance.
Windows signing is Azure Artifact Signing authenticated with the job's OpenID Connect
token: the sign-windows job runs in the release environment with id-token: write,
and azure/login exchanges the token for an Azure credential right before the shared
action runs signtool with the Artifact Signing dlib. The login comes after the build and
immediately before each signing step because the OIDC token lives minutes. The inputs are
the redrob-labs organization secrets AZURE_CLIENT_ID, AZURE_TENANT_ID,
AZURE_SUBSCRIPTION_ID (plain IDs for the login) and ARTIFACT_SIGNING_ENDPOINT,
ARTIFACT_SIGNING_ACCOUNT, ARTIFACT_SIGNING_CERT_PROFILE (the account to sign with); no
certificate file and no client secret exist anywhere. The three ARTIFACT_SIGNING_* names
alone decide whether signing happens -- all unset means an unsigned build with a warning,
some set is an error. Every signed file is read back with signtool verify /pa and must be
Valid with the signer CN=McKinley Rice, Inc.; the log prints <file> signed by CN=....
A failed signature fails the job and so the release.
Branch from main, not develop, so the fix does not drag unreleased work with it.
Open the pull request into main, release from there, then merge main back into
develop so the fix is not lost on the next release.
That last step is checked, because it is the one that gets skipped. The shared gitflow
workflow (gitflow / main is contained in develop) runs on every push to main and, while
main holds a commit develop does not, opens the back-merge pull request itself as
sync/back-merge-<sha>. A promotion pull request from develop leaves the two identical
and the check says nothing; a hotfix opens the back-merge until it is brought back.
The invariant it enforces, which you can read yourself at any time:
git rev-list --count origin/develop..origin/main # 0, outside a release windowIt was not 0 for a month. #13 fixed bun install --frozen-lockfile on main, the
back-merge never happened, and the default branch could not install with the bun version
packageManager pins. The rule above was already written; nothing checked it, and a rule
nothing checks is a preference.