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

164 lines
8.2 KiB
Markdown

# 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](commit-messages.md#types), 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](#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](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](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](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](https://semver.org/) (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:
```bash
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/`](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:
```bash
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:
```bash
#!/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:
```bash
git checkout main
git pull
git branch -d feature/add-receipt-parser
```
Don't let stale local branches or untracked project directories accumulate.