In this guide (13 items)
Git Workflow & Best Practices
Git Workflow & Best Practices
House standard for using Git: commit hygiene, conventional commits, branching, pull requests, history discipline, and secret safety. These rules are what github-actions.md enforces in CI (commit-lint, semver, branch/tag rulesets) — keep both in sync.
1. Commit philosophy
- Atomic commits. One logical change per commit. A commit should build and pass tests on its own. Don’t mix a refactor with a feature, or formatting with logic.
- Self-explanatory. The message says why, the diff says what. Future you reads the log, not your memory.
- Commit early, commit often — locally. Squash into clean units before the branch is reviewed/merged.
- Never commit secrets, generated artifacts, or large binaries. (§8, §9)
2. Conventional commits
Every commit follows Conventional Commits. This is mechanically enforced by commitlint in CI and drives automatic semantic versioning.
<type>(<scope>): <subject>
<body>
<footer>Allowed types (lower-case, never empty):
| Type | Use | SemVer |
|---|---|---|
feat |
new user-facing capability | minor |
fix |
bug fix | patch |
refactor |
behavior-preserving restructuring | – |
perf |
performance improvement | patch |
test |
add/adjust tests only | – |
docs |
documentation only | – |
style |
formatting, no logic (whitespace, lint) | – |
build |
build system / dependencies | – |
ci |
CI config / pipelines | – |
chore |
tooling, housekeeping | – |
revert |
reverts a previous commit | – |
Rules (matching the enforced commitlint ruleset):
typerequired, lower-case, from the list above.scopeoptional, lower-case (e.g.feat(auth):).subjectrequired, imperative mood (“add”, not “added”/“adds”), no trailing period, ≤ 100 chars; full header ≤ 120 chars.- Body wraps at ~100 cols, explains the why and trade-offs.
- Breaking changes: add
!after type/scope and aBREAKING CHANGE:footer → triggers a major bump.
feat(payment)!: replace synchronous capture with async authorization
Capture now happens via the settlement worker, not inline on the request.
BREAKING CHANGE: POST /payments returns 202 + a status URL instead of 200.Co-Authored-By: ...Good vs bad:
✅ fix(auth): reject tokens whose exp equals now (use <=, not <)✅ refactor(order): extract pricing into PricingService❌ Fixed stuff # no type, vague, past tense❌ feat: Added Feature. # capitalized subject, past tense, trailing period❌ wip # not a change description3. Branching model
Trunk-oriented flow with two long-lived protected branches and short-lived work branches.
main— production; always releasable; tagged on release.develop— integration; what the next release is cut from.- Work branches — short-lived, branched from
develop, one concern each:
| Prefix | For |
|---|---|
feature/<slug> |
new capability |
fix/<slug> |
bug fix |
refactor/<slug> |
restructuring |
chore/<slug> |
tooling/maintenance |
hotfix/<slug> |
urgent prod fix, branched from main |
Rules
- Branch names are validated in CI (
validate-source-branch) — use the prefixes. - Keep branches short-lived (hours/days, not weeks); rebase on
developoften. - One branch = one PR = one concern. Split large work into stacked PRs.
- Delete the branch after merge (CI
delete-branchautomates this).
4. Keeping a branch current
Prefer rebase for local/feature branches to keep history linear; never rebase shared/protected branches.
git fetch origingit rebase origin/develop # replay your commits on top of latest develop# resolve conflicts → git add -p → git rebase --continuegit push --force-with-lease # safe force-push; refuses if remote moved- Use
--force-with-lease, never bare--force(it clobbers others’ pushes). - Don’t rebase a branch someone else is also committing to — merge instead.
- Resolve conflicts per-hunk; re-run tests after a rebase.
5. Pull requests
PRs are the only path into main/develop (enforced by ruleset).
- Small and focused. A reviewable PR is < ~400 changed lines. Stack PRs for big work.
- Description: what + why + how to test; link the issue/ADR. If it’s an architecturally significant decision, attach an ADR.
- Green before review: PR pipeline (lint, build, test, security) must pass.
- Required: 2 approvals, all review threads resolved, approval after the last push, status checks green on the latest commit.
- Merge = squash, producing one conventional commit on the target branch → linear history. The squash commit subject must itself be conventional (it’s what semver reads).
- No force-push to the base; no merge commits (linear history rule).
6. Contributing to a repository (fork-based)
For repos where you lack push access (OSS, cross-team, third-party), contribute via the fork + pull-request model. §5 covers PRs from a branch you can push; this covers PRs from a fork you can’t.
Before you code
- Read the repo’s
CONTRIBUTING.mdand Code of Conduct first — the project’s own process always wins over this guide. Generate both for your own projects from the templates in resources.md. - Discuss first. Open (or claim) an issue describing the change before writing code; get maintainer buy-in on anything non-trivial. An unsolicited large PR with no prior discussion usually gets closed.
Fork & keep it synced — add the canonical repo as upstream; rebase on it
before branching and before every push so your PR stays conflict-free.
# fork on the host, then clone your forkgit clone git@github.com:<you>/<repo>.git && cd <repo>git remote add upstream git@github.com:<ORG>/<repo>.git # canonical repogit fetch upstream
git switch develop # or main, per the repo's base branchgit rebase upstream/develop # never commit directly on your fork's basegit push # fast-forward your fork's basePropose the change — branch off upstream, push to your fork (origin),
open the PR against the canonical repo.
git switch -c feature/<slug> upstream/develop# ... atomic conventional commits (§1, §2) ...git push -u origin feature/<slug> # origin = your fork# open PR: base = <ORG>:develop, head = <you>:feature/<slug>, link the issue- Same bar as §5: focused diff, green pipeline, conventional squash subject; PR
body says what + why + how to test and links the issue (
Closes #123). - DCO / CLA: if the project requires a Developer Certificate of Origin, sign
off every commit —
git commit -sadds aSigned-off-by:trailer (§9). Some orgs require a signed CLA instead; checkCONTRIBUTING.md. - Respond to review promptly; push fixups to the same branch (the PR updates itself) — don’t open a new PR per revision.
- Follow the Code of Conduct in every interaction; assume good faith, credit sources, never paste proprietary or unlicensed code.
7. History discipline
Treat shared history as immutable — the same principle as liquibase.md for migrations.
- Never rewrite published history on
main/develop(no force-push, no amend of pushed commits). Fix forward with arevertor a new commit. - Local, unpushed history is yours to clean up:
git rebase -i, squash, reorder, reword — before opening/updating the PR for review. - Revert a bad commit on a protected branch with
git revert <sha>(creates an inverse commit; preserves history). - Tags are immutable: cut
vX.Y.Z, never move or delete a released tag (tag ruleset blocks update/delete/force).
8. Secrets & sensitive data
- Never commit API keys, tokens, passwords, connection strings, private keys,
.envfiles. CI runs secret scanning (TruffleHog) and will fail the build. - Pre-commit grep before pushing: look for
sk-,ghp_,AKIA,password=,secret=,BEGIN PRIVATE KEY. - If a secret is committed: rotate it immediately (assume it’s compromised),
then purge from history (
git filter-repo) and force-push with coordination — rotation first, scrubbing second. - Keep secrets in the platform’s secret store / CI secrets, referenced by env var (see security.md).
9. .gitignore & repo hygiene
- Commit a
.gitignorecovering build output (build/,target/,dist/,node_modules/), IDE files (.idea/,.vscode/), OS cruft (.DS_Store), and local env (.env,*.local). - Keep
gradle/wrapper/gradle-wrapper.jarand lockfiles (*.lock,pnpm-lock.yaml) — they’re reproducibility inputs, not artifacts. - Large binaries → Git LFS, not the main tree. Better: don’t version build outputs at all; rebuild them in CI.
- One concern per repo; don’t vendor unrelated tooling into product repos.
10. Signing & identity
- Configure a real
user.name/user.emailmatching your account. - Sign commits (GPG/SSH/Sigstore
gitsign) where the org requires verified commits; rulesets can mandate signed commits on protected branches. - Bot/CI commits use the Actions identity and are the only actors allowed to bypass tag protection.
11. Hooks & local enforcement
Catch problems before they reach CI.
- commit-msg hook → run commitlint locally so bad messages fail at commit time, not in the pipeline.
- pre-commit hook → formatter + linter (e.g. Spotless, ktlint, biome) and a secret-scan; keep it fast.
- Manage hooks with a lightweight, zero-runtime-dep tool (e.g. simple-git-hooks / lefthook); commit the config so the whole team shares it.
- Hooks are a convenience, not the source of truth — CI re-runs every check; never rely on local hooks alone.
12. Anti-patterns
| ❌ | ✅ |
|---|---|
git commit -m "fix" / “wip” / “stuff” |
conventional, descriptive subject |
| Mixing feature + refactor + formatting in one commit | atomic commits, one concern |
git push --force to a shared branch |
--force-with-lease, and never on protected branches |
| Amending/rebasing already-pushed shared history | revert forward; rewrite only local commits |
| Long-lived feature branch diverging for weeks | short-lived, rebased on develop often |
Committing .env, keys, node_modules, build/ |
.gitignore + secret store + LFS |
Merge commits into main |
squash merge, linear history |
| Moving/deleting a released tag | tags immutable; cut a new version |
| Giant 2000-line PR | split into small/stacked PRs (< ~400 lines) |
| Relying on local hooks only | CI re-validates everything |
Committing on your fork’s main/develop, or a stale fork |
track upstream; rebase before branching (§6) |
| Opening a fork PR with no prior issue/discussion | issue-first; get maintainer buy-in (§6) |
13. Quick reference
# start workgit switch develop && git pull --ff-onlygit switch -c feature/order-pricing
# commit (conventional)git add -pgit commit -m "feat(order): add tiered pricing calculator"
# stay currentgit fetch origin && git rebase origin/developgit push --force-with-lease
# clean local history before reviewgit rebase -i origin/develop # squash/reword unpushed commits
# undo safely on a shared branchgit revert <bad-sha>
# open PR → squash-merge → branch auto-deleted by CI
# contribute to a repo you can't push to (fork-based, §6)git remote add upstream git@github.com:<ORG>/<repo>.gitgit fetch upstream && git switch -c feature/<slug> upstream/developgit push -u origin feature/<slug> # origin = your fork; PR base = <ORG>:developGit usage + conventional commits + branching that CI enforces. Pairs with github-actions.md (commit-lint, semver, rulesets), adr.md (record significant decisions in the PR), and liquibase.md (same immutability principle for shared history).