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:
- Create a feature branch (
feature/add-receipt-parser) - Develop and commit locally
- Open a PR via
gh pr create - Get the review, and confirm CI is green (
gh pr checks) - Merge the PR on GitHub
- The pipeline deploys on the merge to
main, or on aworkflow_dispatchrun where the repo is set up that way - 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.hooksPathmust 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.