Git Workflow
Last reviewed: March 8, 2025 — workflow procedures are up-to-date.
The main branch is protected. No agent — Cursor, OpenClaw, or human — pushes directly to main. All changes require a feature branch and a pull request with at least one approving review.
This page documents the unified git workflow that both Cursor (local IDE) and OpenClaw (autonomous K8s agents) follow.
Overview
flowchart LR
subgraph workflow ["Git Workflow"]
direction TB
Issue["1. Create GitHub Issue\n(labeled + milestoned)"]
Branch["2. Create Feature Branch\nfrom latest main"]
Changes["3. Make Changes\n+ update docs"]
Validate["4. Pre-Merge Validation\n(Helm, YAML, compatibility)"]
PR["5. Push + Create PR\n(labeled + milestoned)"]
Review["6. Review + Merge"]
Verify["7. Post-Merge Verification\n(ArgoCD sync, pod health)"]
end
Issue --> Branch --> Changes --> Validate --> PR --> Review --> Verify
Branch Protection Rules
| Rule | Enforcement |
|---|---|
Direct pushes to main |
Blocked |
Force pushes to main |
Blocked |
| PR required for merge | Yes — at least 1 approving review |
| Linear history | Required |
| Branch deletion after merge | Recommended |
Cursor (Local IDE) Workflow
Cursor operates interactively on the local filesystem. It follows the same protected-branch rules but without the OpenClaw agent footprint conventions.
Step-by-step
- Start from latest main:
git checkout main && git pull origin main
git checkout -b <type>/<short-description>
-
Make changes to manifests, config, code, and docs.
-
Commit with conventional messages:
git add <files>
git commit -m "<type>: <description>"
- Keep branch fresh — before every push:
git fetch origin main
git merge origin/main --no-edit
- Push and create a PR:
git push -u origin HEAD
gh pr create --title "<type>: <description>" --body "<summary>"
- After merge, verify and clean up:
# ALWAYS verify the PR was actually merged before deleting the branch
gh pr view <number> --json state,mergedAt --jq '"state: \(.state), merged: \(.mergedAt // "NOT MERGED")"'
# Only proceed if state is MERGED:
git checkout main && git pull origin main
git branch -d <branch-name>
git push origin --delete <branch-name>
Never delete a branch without confirming merge
If a PR is closed without merging, the commits exist only on that branch. Deleting it loses the work and requires recovery from git reflog. Always check gh pr view first.
Branch naming
| Prefix | Use for |
|---|---|
feat/ |
New features, services, resources |
fix/ |
Bug fixes, misconfigurations |
chore/ |
Maintenance, dependency updates, cleanup |
docs/ |
Documentation-only changes |
refactor/ |
Restructuring without behavior change |
security/ |
Security hardening, vulnerability fixes |
Commit message format
<type>: <description>
Examples:
feat: add incident response skill and pre-merge validationfix: correct Helm value path for Authentik securityContextdocs: update networking table with new Tailscale port
OpenClaw Agent Workflow
OpenClaw agents follow the same branch protection rules with additional traceability requirements. Every action must be attributable to the specific agent that performed it.
Step-by-step
- Workspace setup (once per session):
cd /data/workspaces/<agent-id>
gh repo clone holdennguyen/homelab homelab 2>/dev/null || \
(cd homelab && git checkout main && git pull origin main)
cd homelab
git config user.name "<agent-id>[bot]"
git config user.email "<agent-id>@openclaw.homelab"
- Create a labeled GitHub issue assigned to the current milestone:
gh issue create \
--title "<type>: <description>" \
--body "<details>
---
Agent: <agent-id> | OpenClaw Homelab" \
--assignee holdennguyen \
--label "agent:<agent-id>,type:<type>,area:<area>,priority:<priority>" \
--milestone "<current-milestone>" \
--repo holdennguyen/homelab
- Create a branch from latest main:
git checkout main && git pull origin main
git checkout -b <agent-id>/<type>/<issue-number>-<short-description>
-
Make changes to manifests, config, docs.
-
Commit with issue reference and agent tag:
git add <files>
git commit -m "<type>: <description> (#<issue-number>) [<agent-id>]"
- Keep branch fresh — before every push:
git fetch origin main
git merge origin/main --no-edit
- Push and create a labeled PR assigned to the same milestone:
git push -u origin HEAD
gh pr create \
--title "<type>: <description>" \
--assignee holdennguyen \
--label "agent:<agent-id>,type:<type>,area:<area>,priority:<priority>" \
--milestone "<current-milestone>" \
--body "Closes #<issue-number>
## Summary
- <what changed and why>
## Test plan
- [ ] ArgoCD syncs successfully
- [ ] Service health verified
- [ ] Documentation updated
---
Agent: <agent-id> | OpenClaw Homelab"
- Report the PR URL back to the orchestrator or user.
Agent footprint
Every OpenClaw agent action is traceable via mandatory conventions:
| Artifact | Format | Example |
|---|---|---|
| Git commit author | <agent-id>[bot] <<agent-id>@openclaw.homelab> |
devops-sre[bot] <devops-sre@openclaw.homelab> |
| Commit message | <type>: <desc> (#<issue>) [<agent-id>] |
feat: add redis (#42) [devops-sre] |
| Branch name | <agent-id>/<type>/<issue>-<desc> |
devops-sre/feat/42-redis-caching |
| Issue/PR labels | agent:<agent-id> |
agent:devops-sre |
| Issue/PR body | Footer: Agent: <id> \| OpenClaw Homelab |
— |
Agent git identities
| Agent | user.name |
user.email |
|---|---|---|
homelab-admin |
homelab-admin[bot] |
homelab-admin@openclaw.homelab |
devops-sre |
devops-sre[bot] |
devops-sre@openclaw.homelab |
software-engineer |
software-engineer[bot] |
software-engineer@openclaw.homelab |
security-analyst |
security-analyst[bot] |
security-analyst@openclaw.homelab |
qa-tester |
qa-tester[bot] |
qa-tester@openclaw.homelab |
GitHub Labels
Every issue and PR MUST be labeled. Labels are the tracking and filtering mechanism for all agents.
| Category | Labels | Rule |
|---|---|---|
| Agent | agent:homelab-admin, agent:devops-sre, agent:software-engineer, agent:security-analyst, agent:qa-tester |
Exactly one (OpenClaw only) |
| Type | type:feat, type:fix, type:chore, type:docs, type:refactor, type:security |
Exactly one |
| Area | area:k8s, area:terraform, area:argocd, area:secrets, area:monitoring, area:networking, area:openclaw, area:auth |
One or more |
| Priority | priority:critical, priority:high, priority:medium, priority:low |
Exactly one |
| Semver | semver:breaking |
Only when a change has breaking impact regardless of type |
| Status | status:reverted |
Applied to PRs that were merged then reverted |
Branch Freshness
Feature branches MUST stay current with main. Stale branches cause merge conflicts and block ArgoCD sync after merge.
Before every push:
git fetch origin main
git merge origin/main --no-edit
If the merge has conflicts:
- Do NOT force-push or reset
- Resolve conflicts in every affected file
git add <resolved-files> && git merge --continue- If conflicts are too complex, report to the orchestrator (or user) with the list of conflicting files
When to run this:
- Before your first commit on a new branch (right after
git checkout -b) - Before every
git push - When
mainhas been updated while your branch/PR is open
Pre-Merge Validation
Run these checks before merging any PR that modifies cluster resources.
Manifest validation
- [ ] YAML is valid:
kubectl apply --dry-run=client -f <file> - [ ] Labels follow
app.kubernetes.io/*conventions - [ ] Namespace exists or
CreateNamespace=trueis set - [ ] No secrets or credentials in the diff
Helm chart value verification
Before changing any Helm valuesObject in an ArgoCD Application CR:
# Verify the key exists in the chart
helm show values <repo>/<chart> --version <version> | grep -A5 "<key>"
# Confirm the value renders into the output
helm template <release> <repo>/<chart> --version <version> \
--set <key>=<value> | grep -A10 "<expected-output>"
Charts silently ignore unknown keys
If a key doesn't appear in helm show values, the chart will accept it without error but it will have no effect on the rendered manifests. This was the root cause of the PR #11 incident where controller.securityContext (External Secrets) and infisical.securityContext (Infisical) were silently ignored.
Service compatibility
- [ ] Container image supports proposed
securityContext(check for s6-overlay, tini, or similar init systems) - [ ] Volume permissions match
fsGroup/runAsUser - [ ] Upstream chart docs confirm the value path
Cross-service impact
- [ ] Changes don't break sync wave dependencies
- [ ] Shared resources (ClusterRoles, CRDs) are not removed or renamed
- [ ] ExternalSecrets still reference valid keys
Documentation Freshness Checks
Every documentation file is mapped to its implementation sources in .doc-manifest.yml. A CI workflow and CLI tool use git history to detect when docs fall behind.
How it works
flowchart LR
Manifest[".doc-manifest.yml\n(doc → source mappings)"]
Script["scripts/doc-freshness.py"]
Git["git log\n(commit timestamps)"]
Manifest --> Script
Git --> Script
Script -->|"source newer than doc"| Stale["STALE\n(X commits behind)"]
Script -->|"doc newer or equal"| Fresh["OK"]
For each entry in the manifest, the script compares the last commit that touched the doc vs the last commit that touched any file in its source directories (excluding the doc itself). If the source is newer, the doc is stale and the report shows how many commits behind it is.
CI workflow (doc-freshness)
The doc-freshness GitHub Actions workflow runs on every PR to main:
- Examines which files the PR changes
- Checks those files against the manifest to find mapped docs
- If a mapped doc was not updated in the PR, it posts a warning comment
The check is advisory only — it does not block merge. If the docs genuinely don't need updating (e.g., a comment change in a source file), the warning is safe to ignore.
Local usage
python scripts/doc-freshness.py # Full freshness table
python scripts/doc-freshness.py --stale # Only stale docs
python scripts/doc-freshness.py --check-pr # Files changed on current branch vs origin/main
python scripts/doc-freshness.py --json # Machine-readable JSON
python scripts/doc-freshness.py --markdown # Markdown table for PR comments
--check-pr requires a feature branch
The --check-pr flag compares HEAD against origin/main. If you run it on main after a merge, there's no diff and it reports "all up-to-date." Always run it from a feature branch before pushing.
Interpreting the report
Document Status Doc Source Behind
─────────────────────────────────── ──────── ────────── ────────── ──────────
✗ docs/networking.md STALE 331be14f f591ad60 25 commits
| Column | Meaning |
|---|---|
| Status | ok = doc is current, STALE = sources are newer, MISSING = doc file doesn't exist |
| Doc | Short hash of the last commit that touched the doc |
| Source | Short hash of the last commit that touched any source (excluding the doc itself) |
| Behind | Number of source commits since the doc was last updated |
Adding a new entry
When creating a new service or documentation file, add an entry to .doc-manifest.yml:
- doc: k8s/apps/my-service/README.md
sources:
- k8s/apps/my-service/
For cross-cutting docs that cover multiple areas, list all relevant source directories.
Post-Merge Verification
After every merge to main, verify the deployment succeeded:
# 1. ArgoCD application health (wait ~3 minutes for sync)
kubectl get applications -n argocd
# 2. Pod health across all namespaces
kubectl get pods -A | grep -v Running | grep -v Completed
# 3. ExternalSecrets synced
kubectl get externalsecrets -A
# 4. Service endpoints reachable
curl -sf http://localhost:30400/api/health # Grafana
curl -sf http://localhost:30600/api/v3/root/config/ # Authentik
curl -sf http://localhost:30789/health # OpenClaw
# 5. No error events
kubectl get events -A --sort-by='.lastTimestamp' --field-selector type!=Normal | tail -10
If any check fails, initiate rollback. See Rollback Procedures.
Rollback Procedures
When a merge to main causes service degradation, roll back via git — ArgoCD auto-syncs the revert.
Standard rollback (git revert)
# Revert a merge commit
git revert <bad-commit-sha> -m 1 --no-edit
git push origin main
Multi-commit rollback (file restore)
# Restore files to a known-good commit
git checkout <known-good-sha> -- path/to/file1.yaml path/to/file2.yaml
git commit -m "revert: restore files to pre-<incident> state"
git push origin main
ArgoCD recovery
If ArgoCD is stuck after a rollback:
# Cancel stuck operation
kubectl patch application <app> -n argocd \
--type json -p '[{"op":"remove","path":"/operation"}]'
# Force-delete crashing pods
kubectl delete pod <pod> -n <namespace> --force --grace-period=0
# Force hard refresh on all applications
for app in $(kubectl get applications -n argocd -o jsonpath='{.items[*].metadata.name}'); do
kubectl patch application "$app" -n argocd \
--type merge -p '{"metadata":{"annotations":{"argocd.argoproj.io/refresh":"hard"}}}'
done
Post-incident cleanup
After the cluster is recovered:
- Reopen the auto-closed issue (feature was not delivered)
- Label the reverted PR with
status:reverted - Assign the issue to a future milestone for re-implementation
- Create per-service sub-issues for safer re-implementation
- Post a post-incident report on the PR with timeline, root cause, and action items
Full procedures are documented in skills/incident-response/SKILL.md.
Semantic Versioning & Releases
The repository follows Semantic Versioning 2.0.0 (vMAJOR.MINOR.PATCH).
Version bump rules
| Condition | Bump | Example |
|---|---|---|
Any PR has semver:breaking |
MAJOR | Terraform state migration, removed service |
At least one type:feat (no breaking) |
MINOR | New service, new agent, new capability |
| Only fixes, chores, docs, refactors, security | PATCH | Bug fix, dependency update, doc improvement |
Milestones
GitHub Milestones group issues and PRs into planned releases:
- Named with the target version (e.g.,
v1.1.0) - Every issue and PR MUST be assigned to a milestone
homelab-admincreates milestones and adjusts versions if breaking changes appear- If no open milestone exists, ask the orchestrator (or user) to create one
Release process
Owned by homelab-admin or the user — sub-agents never create tags or releases:
- Verify all issues in the milestone are closed
- Check for
status:revertedPRs (merge + revert = net zero, exclude from changelog) - Determine the version from the highest-impact non-reverted PR
- Pre-release checklist:
- All ArgoCD applications are
Synced+Healthy - Run
python scripts/doc-freshness.pyand resolve stale entries - Review root
README.md— architecture diagram, repository structure, deployed services table, documentation index, Quick Start, and Future Plans must reflect the current state - Run doc freshness again after any updates
- All ArgoCD applications are
- Create a git tag and GitHub Release:
gh release create "v<version>" --target main --generate-notes --latest - Close the milestone and create the next one
Milestone reassessment
When incidents, reverts, or scope changes alter a milestone's planned work, the release manager must reassess before cutting the release.
When to reassess:
- A PR in the milestone was merged then reverted
- Sibling PRs from the same batch were closed without merge
- Planned features were deferred to a future milestone
- Orphaned merged PRs (no milestone) are discovered
Procedure:
-
Triage sibling PRs — unreviewed PRs created in the same batch as a reverted PR share the same quality risks. Close them and rewrite with proper pre-merge validation.
-
Move deferred work — parent issues of closed PRs go to the next milestone with fresh per-service sub-issues.
-
Assign orphaned merged PRs — any merged PR without a milestone must be assigned:
gh pr list --repo holdennguyen/homelab --state merged --json number,title,milestone \
--jq '.[] | select(.milestone == null) | "\(.number) | \(.title)"'
- Update milestone description — explain the scope change:
gh api repos/holdennguyen/homelab/milestones/<number> --method PATCH \
-f description="<updated scope and rationale>"
-
Reassess version bump — if the only
type:featPRs were reverted, the effective bump may change (e.g., MINOR → PATCH). -
Release what's shipped — if the milestone has 0 open issues, cut the release with what's already merged. Don't hold a milestone open waiting for deferred work.
Release what you have, not what you planned
A milestone that lost its flagship feature to a revert is still releasable if it contains other merged work (infrastructure, docs, tooling). Rescope the description, adjust the version if needed, and ship it. Deferred features go to the next milestone.
What NOT to Do
- Never push directly to
main - Never force-push to
main - Never delete a branch without verifying the PR was merged (
gh pr view <number> --json state,mergedAt) - Never commit secrets, API keys, or credentials
- Never bundle unrelated changes in one PR
- Never assume a Helm value key exists — always verify with
helm show values - Never apply
securityContextchanges without verifying image compatibility - Never skip documentation updates in implementation PRs
- Never create an issue or PR without labels (OpenClaw agents)
- Never omit the agent footprint from any artifact (OpenClaw agents)
- Never rely on
kubectl rollout undoas a permanent fix — ArgoCD will overwrite it
Quick Reference
Cursor
git checkout main && git pull origin main
git checkout -b feat/my-feature
# ... make changes ...
git add . && git commit -m "feat: my feature"
git fetch origin main && git merge origin/main --no-edit
git push -u origin HEAD
gh pr create --title "feat: my feature" --body "Summary of changes"
# After merge — verify BEFORE deleting:
gh pr view <number> --json state --jq '.state' # must say MERGED
git checkout main && git pull origin main
git branch -d feat/my-feature
git push origin --delete feat/my-feature
OpenClaw agent
# Setup
cd /data/workspaces/<agent-id>
gh repo clone holdennguyen/homelab homelab 2>/dev/null || (cd homelab && git checkout main && git pull origin main)
cd homelab
git config user.name "<agent-id>[bot]"
git config user.email "<agent-id>@openclaw.homelab"
# Issue + branch
gh issue create --title "<type>: <desc>" --label "agent:<id>,type:<t>,area:<a>,priority:<p>" --milestone "<ms>" ...
git checkout main && git pull origin main
git checkout -b <agent-id>/<type>/<issue>-<desc>
# Changes + commit + push + PR
git add <files>
git commit -m "<type>: <desc> (#<issue>) [<agent-id>]"
git fetch origin main && git merge origin/main --no-edit
git push -u origin HEAD
gh pr create --title "<type>: <desc>" --label "agent:<id>,..." --milestone "<ms>" ...