The concrete how. Branch model, day-to-day flow, and the release process that every repo I own follows.
This document is the how. For the reasoning behind each decision below, see Branching Strategy.
| Branch | Purpose | Protection |
|---|---|---|
main |
Latest release. Always stable. Never directly committed to. | PR required. CI must pass. No force pushes. No deletions. |
develop |
Integration trunk. Every in-flight change lands here via PR. This is the branch you create features from and PR against. | PR required. CI must pass (Lint, Commit Lint). No force pushes. No deletions. |
feature/*, fix/*, chore/*, docs/*, refactor/* |
Short-lived branches off develop. One logical change each. Deleted after merge. |
None — ephemeral. |
v* tags |
Cut from main at release time. Trigger the release pipeline. |
Ruleset: cannot be created, deleted, or non-ff-moved outside the release flow. |
Separate from the integration trunk above, GitHub's "default branch" repo setting is a UX pointer. It controls what visitors see on the repo landing page, what git clone checks out without a --branch flag, and what the PR-creation UI suggests as the base. It does not affect where work integrates — develop is the integration trunk regardless.
- Before the first release: the setting points at
develop. Nothing has shipped, so the integration state is the only meaningful state. - After the first release: the setting flips to
main. External visitors see the released product, not work-in-flight. Contributors still branch from and PR todevelop— that doesn't change.
The flip happens automatically as part of /publish-release, so a repo's first release also publishes a coherent landing page. The flip is idempotent — every subsequent release re-asserts main as the default, with no effect if it's already there.
| Prefix | Use |
|---|---|
feat: |
New feature or capability |
fix: |
Bug fix |
docs: |
Documentation only |
chore: |
Maintenance, dependencies, housekeeping, releases |
refactor: |
Code change that neither fixes a bug nor adds a feature |
Breaking changes: append ! after the type. E.g. feat!: remove legacy API.
One logical change per PR. Keep commits atomic and the history readable.
Two paths:
New repo from scratch: run /create-repo <name>. Creates from the template, clones locally, applies branch protection, default branch, and tag ruleset in one shot.
Existing repo: run /setup-repo <owner/repo>. Applies the standard branch model, protection, and tag ruleset to a repo that's already there.
Both skills set up:
developbranch created frommainif missing, set as the GitHub default branch (correct for a brand-new repo with no releases yet — see GitHub default branch setting above; the flip tomainhappens at first release)- Branch protection on
develop. Require PR, requireLintandCommit Lintstatus checks - Branch protection on
main. Require PR, requireLint - Tag ruleset on
v*. No creation, deletion, or non-fast-forward
After /setup-repo, customise .github/workflows/validate.yml for the project's language/toolchain, then rerun /setup-repo to update required status check names to match the actual job names.
git checkout develop && git pull
git checkout -b feature/<short-description>Use the prefix that matches the change type. feature/, fix/, chore/, docs/, refactor/.
Conventional commit messages, one logical change per commit:
git add <files>
git commit -m "feat: <short imperative description>"git push -u origin feature/<short-description>
gh pr create --base develop --assignee amchestePR body: a short summary of what changed and why, plus a test plan checklist.
Wait for Lint and Commit Lint (plus any project-specific checks) to pass. Then merge via the GitHub UI. A standard merge is fine for feature branches landing on develop.
GitHub can do this automatically on merge, or delete locally:
git checkout develop && git pull
git branch -d feature/<short-description>Releases are handled by /publish-release <version>, which walks through the four-step ceremony below. The version argument is either an explicit semver (1.2.0, 0.3.0-beta.1) or a bump level (patch, minor, major).
- Working tree clean
- On
develop, up to date withorigin/develop VERSIONfile exists in the repo root
git checkout -b chore/release-v<version>
./scripts/bump-version.sh set <version> # or: patch | minor | major
# Script commits: "chore: release v<version>"
git push -u origin chore/release-v<version>
gh pr create \
--base develop \
--head chore/release-v<version> \
--title "chore: release v<version>" \
--body "Version bump to v<version>. Merge to proceed with the develop→main release."Wait for CI. Merge via the GitHub UI.
This step does not use a GitHub PR. GitHub's merge button squash-merges by default, which drops commit ancestry and causes merge conflicts on every subsequent release. See the philosophy doc for the full story.
git checkout main && git pull
git merge --no-ff origin/develop -m "chore: release v<version>"
git push origin mainThis preserves the full commit graph. main remains a strict ancestor of develop, and the next release cycle starts from clean state.
git tag -a "v<version>" -m "Release v<version>"
git push origin "v<version>"The tag push triggers the release pipeline via the v* tag filter on the release workflow.
gh run list --limit 3Watch the pipeline at https://github.com/amcheste/<repo>/actions. Pre-release versions (containing -, e.g. -beta.1) are published as pre-releases and do not become latest. Stable versions become latest once all pipeline gates pass.
After the tag pushes and the pipeline starts, set main as the GitHub default branch if it isn't already. On the first release this flips the repo's landing page from develop to main. On every subsequent release it's a no-op.
current=$(gh repo view --json defaultBranchRef --jq '.defaultBranchRef.name')
if [ "$current" != "main" ]; then
gh repo edit --default-branch main
fiThis is the moment a repo transitions from "no releases yet, show contributors the integration state" to "has released, show external visitors the shipped product." Contributors continue to branch from and PR to develop either way — only what GitHub renders on the landing page changes.
feature branch ──► develop ──► develop ──► main ──► v1.2.0 tag ──► pipeline ──► default branch
(PR merge) (PR merge) (CLI merge --no-ff) (push tag) (auto-fires) (→ main on
▲ ▲ first release)
│ │
version bump CLI only
PR merged first (NOT a GitHub PR)
- Branching Strategy philosophy. The reasoning behind every decision above.
- Development Tooling Stack. How this workflow runs inside the broader AI-assisted development stack.