engineering-handbook/git-workflow.md
Adam Moussa cfb50ff1fc
docs(git-workflow): make ci-on-merge the sanctioned deploy path
Pipeline deploy on merge to main (or workflow_dispatch where a repo
configures one) is now the only sanctioned path. Local deploy-then-merge
drops to a documented legacy exception that requires a data-loss-prone
stateful change plus advance sign-off, recorded in the PR notes.

Also correct the hook install path for the repo's move under
repositories/seahaven/, and document that core.hooksPath is a single
directory rather than a search path, so a repo-local setting silently
shadows the machine-global security pre-push unless the repo hook is a
shim that re-execs it.
2026-07-28 19:42:33 -04:00

8.2 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/<key>-<description> Adding new functionality or enhancing existing features
bug/<key>-<description> Fixing a non-urgent defect found during development or testing
hotfix/<key>-<description> Fixing a production issue that needs immediate attention
chore/<key>-<description> Routine maintenance, cleanup, dependency bumps, or tooling
docs/<key>-<description> Documentation-only changes
refactor/<key>-<description> Restructuring code without changing behavior
release/<key>-<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.

bug vs hotfix: Use bug/ 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: When the work tracks a Jira issue, put the key after the prefix: feature/PROJ-123-add-receipt-parser (substitute the real project key, e.g. the infra or software-development project). This is what wires the branch, commits, and PR into the issue's development panel. See Linking to Jira below. Work with no Jira issue (one-off scripts, trivial fixes) omits the key and uses a plain description.

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. PROJ-123) wherever it appears and surfaces the branch, commits, PR, and deployment status in that issue's development panel automatically. To make that happen, mention the key in at least one of:

  • the branch name (feature/PROJ-123-add-receipt-parser)
  • the PR title ([PROJ-123] Add receipt parser)
  • a commit message (the Refs: trailer, see commit-messages.md)

Mentioning it in the branch name covers all three at once, so that is the minimum bar.

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

Recommended hooks live in hooks/ — copy them into .git/hooks/ when setting up a project.

Hook Purpose
pre-push Runs npm ci to catch lock file drift before it breaks CI

Install for a Node.js project:

cp ~/Documents/repositories/seahaven/engineering-handbook/hooks/pre-push .git/hooks/pre-push
chmod +x .git/hooks/pre-push

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"
[ -x "$GLOBAL" ] && exec "$GLOBAL" "$@"   # passes args and stdin, preserves the exit code
exit 0                                    # nothing to delegate to

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.