189 lines
7.5 KiB
Markdown
189 lines
7.5 KiB
Markdown
# InboxIntel — Git Workflow & Environment Strategy
|
||
|
||
The single source of truth for how we branch, commit, version, and promote code
|
||
across environments. Optimised for a **solo developer on Windows 11 with a future
|
||
Linux production server**. Kept deliberately lightweight — every rule here earns
|
||
its place.
|
||
|
||
---
|
||
|
||
## 1. Branch strategy
|
||
|
||
A trimmed **GitHub Flow + a long-lived `develop`** model. Two permanent branches,
|
||
short-lived branches off `develop`.
|
||
|
||
| Branch | Lives for | Purpose | Deploys to |
|
||
|-----------------|-----------|------------------------------------------------------|------------|
|
||
| `main` | forever | Always releasable. Every commit is tagged & shippable | production |
|
||
| `develop` | forever | Integration branch. What staging runs | staging |
|
||
| `feature/*` | hours–days| One feature or refactor | dev (local)|
|
||
| `fix/*` | hours | Non-urgent bug fix | dev (local)|
|
||
| `hotfix/*` | minutes–hrs| Urgent prod fix, branched from `main` | prod (fast)|
|
||
| `release/x.y.0` | optional | Only if a release needs stabilisation before tagging | staging |
|
||
|
||
**Why this shape (not full GitFlow):** a solo dev doesn't need GitFlow's ceremony
|
||
(separate release managers, parallel release trains). But keeping `develop`
|
||
separate from `main` gives one thing that pure trunk-based can't: a **staging
|
||
environment that always mirrors `develop`** while `main` stays clean and
|
||
tag-perfect for production. `release/*` exists only when you want to freeze
|
||
features and stabilise — skip it for routine work.
|
||
|
||
### Normal flow
|
||
|
||
```
|
||
main ──────●────────────────────●────────── (tagged: v1.2.0, v1.3.0)
|
||
\ /
|
||
develop ──●──●──●──●──●──●──●──●──────────── (staging)
|
||
\ / \ /
|
||
feature/x ●─● \ /
|
||
fix/y ●───●
|
||
```
|
||
|
||
1. `git switch develop && git pull`
|
||
2. `git switch -c feature/sender-policy`
|
||
3. Commit in small conventional commits.
|
||
4. Push, open a PR **into `develop`** (Gitea). CI must be green.
|
||
5. Squash-merge. Delete the branch.
|
||
6. When `develop` is ready to ship → PR `develop → main`, tag, deploy.
|
||
|
||
---
|
||
|
||
## 2. Commit conventions — Conventional Commits
|
||
|
||
Format: `type(scope): short imperative summary`
|
||
|
||
**Types:** `feat`, `fix`, `docs`, `refactor`, `test`, `chore`, `ci`, `perf`,
|
||
`build`, `style`, `security`.
|
||
|
||
Rules:
|
||
- Summary ≤ 72 chars, imperative mood ("add", not "added").
|
||
- One logical change per commit.
|
||
- Body explains **why**, not what (the diff shows what).
|
||
- Breaking change → add `!` (`feat!:`) and a `BREAKING CHANGE:` footer.
|
||
|
||
Examples (matching this repo's history):
|
||
```
|
||
feat(ui): F1+F2 — component primitives + rebuilt app shell
|
||
fix(security): systemic IDOR safeguard via EF global query filters
|
||
ci: add Gitea Actions build + test pipeline
|
||
```
|
||
|
||
Why: conventional commits drive **automatic semver bumps** and a generated
|
||
CHANGELOG, and make `git log` scannable. `feat` → minor, `fix` → patch,
|
||
`BREAKING CHANGE` → major.
|
||
|
||
---
|
||
|
||
## 3. Pull request / merge flow (Gitea)
|
||
|
||
Even solo, PRs are worth it: they run CI, give a diff review checkpoint, and build
|
||
a paper trail.
|
||
|
||
- **Target:** `feature/*` and `fix/*` → `develop`. `develop`/`hotfix/*` → `main`.
|
||
- **Gate:** the `CI` workflow (backend build+test, frontend build) must pass.
|
||
- **Merge style:** **squash-merge** feature branches (one clean commit on
|
||
`develop`). **Merge commit** for `develop → main` (preserves the integration
|
||
history and makes the release boundary visible).
|
||
- **Branch protection (Gitea → Settings → Branches):** protect `main` and
|
||
`develop`; require status checks to pass; disallow force-push.
|
||
|
||
---
|
||
|
||
## 4. Versioning — Semantic Versioning (`MAJOR.MINOR.PATCH`)
|
||
|
||
- **MAJOR** — breaking API/behaviour change.
|
||
- **MINOR** — backward-compatible feature.
|
||
- **PATCH** — backward-compatible fix.
|
||
|
||
The current version lives in [`VERSION`](../VERSION) and is the one place bumped
|
||
per release. Pre-1.0 while scaffolding: stay on `0.x` (minor = features, patch =
|
||
fixes; anything may change).
|
||
|
||
---
|
||
|
||
## 5. Tagging & releases
|
||
|
||
Tags are cut **only on `main`**, annotated, prefixed `v`:
|
||
|
||
```bash
|
||
git switch main && git pull
|
||
# bump VERSION + CHANGELOG in a release commit, then:
|
||
git tag -a v1.3.0 -m "v1.3.0 — sender policy + staging overlay"
|
||
git push origin main --tags
|
||
```
|
||
|
||
- Tag == the exact commit deployed to production == the rollback target.
|
||
- The tag message summarises the release; details live in `CHANGELOG.md`.
|
||
|
||
---
|
||
|
||
## 6. Hotfix process
|
||
|
||
For a bug already in production:
|
||
|
||
```bash
|
||
git switch main && git pull
|
||
git switch -c hotfix/oauth-callback-500
|
||
# fix + test
|
||
git commit -m "fix(security): guard null OAuth state on callback"
|
||
# PR hotfix/* -> main, CI green, merge
|
||
git switch main && git pull
|
||
git tag -a v1.3.1 -m "v1.3.1 hotfix — OAuth callback" && git push origin main --tags
|
||
# deploy the tag, THEN back-merge so develop doesn't lose the fix:
|
||
git switch develop && git merge main && git push
|
||
```
|
||
|
||
The **back-merge to `develop`** is the step people forget — without it the next
|
||
release silently reintroduces the bug.
|
||
|
||
---
|
||
|
||
## 7. Environments
|
||
|
||
| Env | Host | Runtime | Config source | Purpose |
|
||
|-------------|-------------------------|--------------------------------------|--------------------------|--------------------|
|
||
| Development | Windows 11 (native) | `dotnet run` + `vite` (hot reload) | `appsettings.Development.json` + user-secrets | fast iteration, debugging |
|
||
| Staging | Windows 11 (Docker) | Linux containers, `ASPNETCORE_ENVIRONMENT=Production` | `deploy/.env.staging` + `docker-compose.staging.yml` | production rehearsal |
|
||
| Production | Linux server (Docker) | identical Linux containers | `deploy/.env` on the server | live |
|
||
|
||
**Parity principle:** dev is fast (native, Windows) and *not* production-shaped —
|
||
that's fine, it's for inner-loop speed. **Staging is the parity gate**: it runs the
|
||
*same Linux images* Docker builds for production, so "works in staging" genuinely
|
||
predicts "works in prod". The only staging↔prod differences are the dev banner,
|
||
capped sync, ports, and volume namespace — see `docker-compose.staging.yml`.
|
||
|
||
### Commands
|
||
|
||
```powershell
|
||
# DEV (native, hot reload) — two terminals
|
||
dotnet run --project src/InboxIntel.Api # http://localhost:5080
|
||
cd frontend; npm run dev # http://localhost:5173
|
||
|
||
# STAGING (production-shaped, on Windows via Docker)
|
||
./deploy/up.ps1 -Staging # http://localhost:18081
|
||
./deploy/down.ps1 -Staging
|
||
|
||
# PRODUCTION-shaped run locally (smoke test the real config)
|
||
./deploy/up.ps1 # http://localhost:8081
|
||
```
|
||
|
||
Dev and staging use **different ports and different volumes**, so they can run at
|
||
the same time and never share a database.
|
||
|
||
---
|
||
|
||
## 8. Git hooks
|
||
|
||
Installed via `core.hooksPath` (run once per clone):
|
||
|
||
```powershell
|
||
./scripts/install-hooks.ps1
|
||
```
|
||
|
||
- **pre-commit** (fast): blocks committed `.env`/secrets, verifies `dotnet format`.
|
||
- **pre-push** (thorough): `dotnet build -c Release` + `dotnet test` + frontend
|
||
build — the same gate CI runs, caught before the push.
|
||
|
||
Emergency bypass: `--no-verify`. CI still enforces the gate server-side, so a
|
||
bypassed push can still be rejected by branch protection.
|