Promotions from reflecting 21 projects' session logs (incl. agent-runtimes 122-log drain). Adds coverage across networking (eBPF VIP/VPN SNAT/VLAN bridge/forward-auth preflight/ingress TLS), kubernetes (CSI hotplug/PodSecurity debug/self-managed GitOps/runtime annotations), CI (dispatch tokens/runner death/base image), git (CI-rebase/shallow reset/PR governance), python (async session pool/httpx redirects/logging), TDD (AsyncMock/xfail lifecycle), api-integration (SDK parse/token-scope 404/schema probing), plus docker, scripting, debugging, security-architecture, secrets, react, octopus. State: .distill-state.json refreshed with current HEADs + 5 newly-tracked projects.
110 lines
10 KiB
Markdown
110 lines
10 KiB
Markdown
# Git & Source Control
|
|
|
|
## Commit Practices
|
|
|
|
- Use meaningful commit messages; prefer small, focused commits over large batches
|
|
- Never commit secrets in plaintext — use SOPS + age or equivalent encryption
|
|
- Enable pre-commit hooks where appropriate (secret detection, linting, formatting)
|
|
- **Commit each implementation phase separately.** For multi-phase milestones, commit at each phase boundary with tests passing. Each commit should be self-contained and independently describable. Phase-by-phase commits surface issues early, keep history bisectable, and make later review and reflection far easier than one large end-of-milestone commit.
|
|
|
|
## Repo Initialization
|
|
|
|
- **Rename default branch immediately after `git init`.** `git init` still creates `master` on many systems despite `main` being the modern default. Run `git branch -m master main` (or configure `init.defaultBranch = main` globally) before first push — otherwise `git push -u origin main` fails with `src refspec main does not match any`. Bake this into any project-bootstrap script.
|
|
|
|
## Pre-Commit Hooks
|
|
|
|
- Block `local_secrets/` and similar directories from being committed
|
|
- Auto-encrypt files matching `.sops.yaml` rules that aren't yet encrypted
|
|
- Enable with `git config core.hooksPath .githooks`
|
|
- Consider secret detection, linting, and formatting hooks
|
|
- **Copy hook scripts into `.git/hooks/`; don't symlink.** A symlinked hook resolves to the working-tree file, which changes on every branch switch — hooks then run stale (or wrong-branch) logic. Use an `install-hooks.sh` that copies the source script into `.git/hooks/`, and re-run it whenever the source changes.
|
|
- **Document the sync-then-commit sequence when hooks require an up-to-date main.** If a pre-commit hook requires local `main` to match `origin/main`, the commit silently blocks when local `main` is even one commit behind. Standard sequence: `git checkout main && git pull --ff-only && git checkout <branch> && git rebase main`.
|
|
|
|
## GitOps Workflow
|
|
|
|
- All infrastructure changes should be tracked in Git
|
|
- No manual changes without corresponding GitOps manifests — anything applied manually (e.g., `kubectl apply`, `helm install`) should immediately get a corresponding tracked manifest
|
|
- For ArgoCD-managed clusters: edit in Git, push, sync — never edit live resources directly
|
|
|
|
## Separate Data Repos from Code Repos for GitOps Controllers
|
|
|
|
When a GitOps controller watches a git repo for declarative state (zone files, policy documents, config blobs), keep that data in a **repo separate from the controller's app code**. Mixing the two means every data change triggers CI builds (container rebuilds, test runs) for no reason, and pollutes the code repo's history with non-code churn.
|
|
|
|
Separation yields:
|
|
- Pure data commits with no CI noise
|
|
- Cleaner webhook targeting per-repo
|
|
- Independent access control for data editors vs code maintainers
|
|
- Simpler rollback semantics — revert a data change without touching the controller image
|
|
|
|
## Remote Conventions
|
|
|
|
- SSH workflows preferred over HTTPS for Git remotes
|
|
- Use SSH config host aliases for multi-user setups (e.g., `gitea.example.com-<user>`)
|
|
- Remote URL format: `git@<host-alias>:<org>/<repo>.git`
|
|
- Optionally push-mirror to GitHub for public visibility
|
|
- **Prefer internal hostnames for self-hosted Git remotes on the local network.** SSH push to a self-hosted Git server through a public/VPS hostname can return `kex_exchange_identification: read: Connection reset by peer`, especially during heavy agent activity — VPS routes often have connection limits or rate limiting that fail silently under load. On the local network, use the internal hostname in SSH aliases for heavy push workflows; reserve the public hostname for external access or HTTPS. Test with `ssh -T <alias>` immediately after configuring.
|
|
|
|
## Cross-User Pushes
|
|
|
|
- **Prefer SSH aliases over temp-URL swaps.** When a repo is owned by user A but your default SSH key authenticates as user B, configure a dedicated SSH host alias (`Host gitea.example.com-userA` with matching private key) and use `git@gitea.example.com-userA:org/repo.git`. Avoid temp-URL-swap (embedding an HTTPS token in the remote URL, pushing, then resetting) — it's clunky, prone to shell-quoting errors, and leaks tokens into reflog/history.
|
|
- **API-created repos need the SSH user as a collaborator.** If a repo is created via API token (user A) but pushes use an SSH alias authenticating as user B, user B has no access by default. Add the SSH-authenticating user as admin collaborator via API before the first push. Applies to Gitea, GitHub, and any platform where API auth and SSH auth use different identities.
|
|
|
|
## Access and Clone Gotchas
|
|
|
|
- **Org repos require explicit collaborator grants.** Don't assume organizational membership implies write access — verify permissions before setting up automation or CI/CD.
|
|
- **Shallow clones break push operations.** `git clone --depth 1` is fine for read-only CI jobs, but pipelines that push artifacts, tags, or mirror to other remotes need full clones.
|
|
- **Reset shallow clones against `HEAD`, not `origin/main`.** A single-branch shallow clone (`--depth N`) creates no remote-tracking refs for other branches, so `origin/main` does not exist unless `main` is the branch being cloned. Init/reset logic that does `git reset --hard origin/main` fails on any other branch. Reset against `HEAD` instead — it is branch-agnostic and works regardless of which branch was shallow-cloned.
|
|
|
|
## Cross-Remote Hygiene for Multi-Remote Projects
|
|
|
|
When a project has divergent remotes (e.g., local Gitea with granular commits + GitHub with squash-merged PRs), histories diverge and naive merges explode.
|
|
|
|
- **Never `git merge <remote>/<branch> --allow-unrelated-histories`** — it produces 50+ mass conflicts across unrelated files.
|
|
- **Import specific files instead:** `git checkout <remote>/<branch> -- <specific-files>` brings those paths in as a normal local change.
|
|
- **Always diff after a bulk checkout.** `git checkout` from a remote silently overwrites locally-modified files with older remote versions with zero warning. Run `git status` and review each touched file before staging; restore with `git checkout HEAD -- <file>` if an unwanted overwrite happened.
|
|
- **After an upstream PR squash-merge, reset — don't rebase — local branches to main.** Rebase against a squashed history leaves phantom commits; a clean `git reset --hard origin/main` on the local branch is correct.
|
|
|
|
## Version Management
|
|
|
|
- Use the latest stable version of dependencies unless pinned for a reason
|
|
- Verify versions from live sources (`helm search repo`, upstream docs, package registries) — don't rely on memory
|
|
- Document the reason in a comment if a version is intentionally pinned below latest
|
|
- Check compatibility matrices before upgrading (e.g., Talos ↔ Kubernetes, framework ↔ runtime)
|
|
|
|
## Placeholder Conventions in Template-Heavy Repos
|
|
|
|
When files contain multiple templating syntaxes (Go templates `{{ .var }}`, CI variables `${{ }}`, Helm `{{ }}`, etc.), use a distinct placeholder convention for your own substitutions that can't be confused with any templating language:
|
|
|
|
- **Double-underscore:** `__CUSTOMER_NAME__`, `__DOMAIN__`
|
|
- **All-caps curly brace (no spaces):** `{{CUSTOMER_NAME}}` (distinct from Go's `{{ .Title }}` with spaces and dots)
|
|
|
|
Choose one convention per repo and document it.
|
|
|
|
## Git Worktrees for Parallel Agent Work
|
|
|
|
When running parallel agents or tasks that modify the same repo:
|
|
- Give each task its own branch and worktree (`git worktree add .worktrees/<task-id> -b <branch>`)
|
|
- Tasks with no dependencies branch from HEAD; tasks with one dependency branch from that dependency's branch
|
|
- Tasks with multiple dependencies get an octopus merge base branch
|
|
- Worktrees share the `.git` object store — fast creation, minimal disk usage
|
|
- Keep containers detached (`docker run -d`, not `--rm`) so logs survive for inspection after exit
|
|
|
|
## Rebase Before Manual Commits to a CI-Auto-Bumped Branch/Deploy Repo
|
|
|
|
When CI auto-commits back to the same branch you push to — `[skip ci]` dependency-sync commits, deploy manifests with an image `newTag` bumped by a build job, ArgoCD `chore: deploy` commits — your push races those bots. A push made just after your fetch is rejected as non-fast-forward (`! [rejected] ... (fetch first)`), and on a deploy repo the running image can silently lag `origin/main` by many builds with no obvious error.
|
|
|
|
- **Standard sequence:** `git pull --rebase origin <branch> && git push origin <branch>`. Rebase (not merge) keeps history linear against the bot commits.
|
|
- **Expect conflicts in bot-managed files** (dependency manifests, version pins, image tags). Resolve by taking the higher/newer value.
|
|
- This race also fires immediately after *you* trigger a dependency-sync that CI auto-commits — pull-rebase before pushing your own follow-up.
|
|
|
|
## Gitea: Use `workflow_dispatch`, Not `repository_dispatch`, for API-Triggered Builds
|
|
|
|
On Gitea (observed 1.25.x), `POST /api/v1/repos/<org>/<repo>/dispatches` (the `repository_dispatch` trigger) returns 404 and never fires the workflow. Trigger builds instead via `POST /api/v1/repos/<org>/<repo>/actions/workflows/<file>.yaml/dispatches` with body `{"ref":"main","inputs":{...}}` (the `workflow_dispatch` trigger).
|
|
|
|
Also don't point a Gitea webhook at the `/dispatches` endpoint to chain builds: Gitea webhooks send the full push-event body (not `{"event_type": ...}`, which the endpoint ignores) and carry no auth header (so the call 401s). Chain builds from an in-CI dispatch step instead.
|
|
|
|
## PR Governance on Protected Branches
|
|
|
|
- **Don't bypass a `required_approvals` gate to unblock automation.** On a branch protected with `required_approvals: N`, do not self-approve a PR through a second bot account/token to let automation merge. That approval gate is a deliberate production-publish guardrail set by the repo owner; bypassing it defeats its intent. Leave the PR open for a human to approve. (This is distinct from a merge-whitelist misconfiguration, which is a config bug to fix — an approval gate is intentional.)
|
|
- **Promote a targeted change with a feature branch, never by merging the whole staging branch.** When a protected `main` requires approvals and you need to publish one targeted change, do not push/merge the entire integration branch (e.g. `staging → main`) — that promotes *all* accumulated changes at once. Push a feature branch containing only the targeted change and open a PR against `main`. Keeps the diff reviewable and prevents accidental bulk promotion.
|