engineering-handbook/git-workflow.md

8.4 KiB

Git Workflow

Branching

  • Never commit directly to main
  • Delete branches after merge
  • Use the appropriate branch prefix for the type of work:
Prefix Use when
feature/<description> Adding new functionality or enhancing existing features
fix/<description> Fixing a non-urgent defect found during development or testing
hotfix/<description> Fixing a production issue that needs immediate attention
chore/<description> Routine maintenance, cleanup, dependency bumps, or tooling
docs/<description> Documentation-only changes
refactor/<description> Restructuring code without changing behavior
release/<description> Preparing a release (version bump, tag, changelog)

The prefixes mirror the commit types in commit-messages.md, so the branch and its commits speak the same vocabulary. Use kebab-case for the description (e.g. feature/add-receipt-parser).

fix vs hotfix: Use fix/ for defects caught before they affect production (failing tests, broken dev flows, issues found in review). Use hotfix/ only when production is impacted and the fix needs to bypass normal review cadence.

Jira key: The Jira key does not go in the branch name. It goes in the PR title as a suffix (e.g. feat(parser): add receipt parser (DEV-123)). See pull-requests.md and Linking to Jira below.

Commits

  • Commit each logical change individually with a descriptive message
  • See commit-messages.md for formatting rules
  • Keep the working tree clean: commit or stash before switching context

Linking to Jira

Work is tracked in Jira (seahaven.atlassian.net). Jira is the source of truth for work; GitHub is the source of truth for code. We do not duplicate issues between the two — we link them.

The org-level GitHub for Jira app is already installed across all repos. It detects the Jira issue key (e.g. DEV-123) wherever it appears and surfaces the branch, commits, PR, and deployment status in that issue's development panel automatically. To make that happen:

  • Required: include the key at the end of the PR title in parentheses — feat(parser): add receipt parser (DEV-123). See pull-requests.md for the full format.
  • Recommended: include the key in the Refs: trailer of each commit (see commit-messages.md).

Branch names do not carry Jira keys. The required linkage point is the PR title.

Smart Commit commands

The integration also accepts inline commands in commit messages to act on the issue without opening Jira:

Command Effect
PROJ-123 #comment <text> Add a comment to the issue
PROJ-123 #time 2h <text> Log work
PROJ-123 #done (or another transition name) Transition the issue

Transition names are case-insensitive and match the issue's workflow (#in-progress, #done). Use these sparingly; the link itself is the main goal, not driving the whole workflow from commits.

Deploying

CI-on-merge is the only sanctioned deploy path. A change reaches an environment by merging its PR into main and letting the repo's pipeline deploy, or through a workflow_dispatch run of that same pipeline where the repo configures one. Nobody deploys from a workstation.

The standard flow for changes that deploy to AWS:

  1. Create a feature branch (feature/add-receipt-parser)
  2. Develop and commit locally
  3. Open a PR via gh pr create
  4. Get the review, and confirm CI is green (gh pr checks)
  5. Merge the PR on GitHub
  6. The pipeline deploys on the merge to main, or on a workflow_dispatch run where the repo is set up that way
  7. Verify the deployed environment; if it is wrong, roll forward with a follow-up PR

A local deploy puts code into an environment that no reviewed commit describes, and its result depends on whoever ran it having the right credentials and a clean working tree. The pipeline deploys a known commit with the repo's own OIDC role every time. See cicd.md.

Legacy exception: deploy-then-merge

Merging the feature branch into main locally, deploying by hand (sam deploy, cdk deploy), and only then merging the PR on GitHub is a legacy pattern. It is not a default and it is not available on request. Both of these must hold:

  • The change is data-loss-prone and stateful. A resource replacement that would recreate a table or bucket, a one-way schema or data migration, or a stack operation that has to be watched and aborted mid-flight, where an automated deploy or rollback could destroy data.
  • Adam has signed off on that specific deploy in advance.

Write the justification into the PR description under ## Notes: what the stateful risk is, and where the sign-off happened. An exception that is not written down is not an exception, so use the pipeline.

Push Discipline

  • Push regularly; don't accumulate unpushed commits
  • Never force push main
  • Never amend or rewrite commits already pushed to main
  • If you need to add context to a past change, use PR descriptions or issue comments

Versioning

Use Semantic Versioning (SemVer) as the standard: MAJOR.MINOR.PATCH.

Increment When
MAJOR Breaking changes to the public API or contract (removed endpoints, changed request/response shapes, incompatible config changes)
MINOR New functionality that is backwards-compatible (new endpoints, new optional fields, new features)
PATCH Backwards-compatible bug fixes, dependency updates, documentation corrections

When to version

Not every project needs versioning. Apply SemVer when the project has consumers that depend on its interface:

Version Don't version
Published packages (npm, PyPI) Internal SAM stacks with no external consumers
Public APIs with external consumers One-off scripts and migration tools
Shared libraries used across repos Internal tools used only by the team that builds them
CLIs distributed to users

How to tag

Create annotated tags on main after the PR is merged:

git tag -a v1.2.0 -m "v1.2.0"
git push origin v1.2.0

Start at v0.1.0 for new projects. Move to v1.0.0 when the interface is stable and has external consumers.

Git Hooks

The global security pre-push hook at ~/.config/git/hooks/pre-push is the only pre-push hook in active use. It runs the workstation security scanner before every push and is the deterministic gate for catching secrets and high-severity findings locally. Linting (ruff, tsc/eslint) runs in CI, not in pre-push.

The hooks/ directory ships a pre-push shim for use by repos that set their own core.hooksPath. That shim re-execs the global hook rather than replacing it. It is not an npm-ci runner and is not a general-purpose pre-push template.

core.hooksPath shadows, it does not merge

core.hooksPath names a single directory, not a search path. Setting it in a repo (for example to a tracked .githooks/) makes git run hooks only from there and ignore the machine-global hooks directory completely. Where that global directory holds a mandatory hook, such as the workstation security pre-push, a repo-local core.hooksPath silently disables it: no error, no output, no gate.

A repo that sets its own core.hooksPath must ship a pre-push that re-execs the global hook instead of replacing it:

#!/usr/bin/env bash
GLOBAL="${SH_GLOBAL_HOOKS:-$HOME/.config/git/hooks}/pre-push"
if [ ! -x "$GLOBAL" ]; then
  echo "pre-push: global security hook is missing or not executable: $GLOBAL" >&2
  exit 1
fi
exec "$GLOBAL" "$@"   # passes args and stdin, preserves the exit code

Two consequences:

  • An installer that sets core.hooksPath must verify the shim delegates before it touches the config, and refuse to install if the delegation is missing or broken.
  • A tracked hooks directory only exists on branches that contain it, so the gate is off on every branch that predates it. Land the hooks directory on all long-lived branches before installing.

Check what is actually in effect with git config core.hooksPath and git config --local core.hooksPath, then confirm the repo's pre-push still execs the global one.

Cleaning Up

After a PR is merged:

git checkout main
git pull
git branch -d feature/add-receipt-parser

Don't let stale local branches or untracked project directories accumulate.