Git Branching Strategy for AI-Assisted Development — Complete Visual Guide
Integration-Gate Flow · CI/CD Pipelines · Security · OpenTelemetry · Policy as Code — for Human + AI Agent Teams
Integration-Gate Flow
A Trunk-Based Development variant built for teams where AI agents and humans both open pull requests. It keeps TBD’s short-lived-branch discipline and adds exactly the one thing standard TBD lacks: a supervised integration branch that gates every AI-generated change — with nightly integration, performance, DAST, chaos, and mutation tests — before it’s release-eligible.
TL;DR
- One branching model, not four: Integration-Gate Flow = Trunk-Based Development + one gated
integrationbranch - AI agents get their own
ai/*branches — can never merge without AI Review + Human Review + Security Review - Same 9 PR-pipeline stages and 11 security gates for every surface: backend, web, and mobile
- No direct commits to
main,release/*, orintegration— ever, by anyone or anything - Semver and changelogs are computed from Conventional Commits, never hand-written
- Local git hooks are fast feedback only — every check has a required CI status-check twin that can’t be bypassed
- Monorepo, not polyrepo — one security/OTel/policy config to keep current instead of four
8Branch Types
9PR Pipeline Stages
11Security Gates
6Animated Pipelines
18AI Guardrails
1
Branch Architecture
Eight branch types, why Integration-Gate Flow beats GitFlow/GitHub Flow/Trunk-Based, monorepo vs. polyrepo.
8 Branch Types
2
Pipelines
Six animated pipelines: PR review, security gates, backend release, mobile release, semver, end-to-end.
6 Pipelines · 11 Security Gates
3
Hooks · Webhooks · Governance
Client/server hooks with bypass reporting, AI guardrails, OpenTelemetry, branch protection, policies.
18 AI Guardrails
4
Centralized Governance
One hooks/policy repo, every application repo consumes it — code, Terraform, Helm — plus the answer for repos you don’t administer.
4 Enforcement Layers
Branch Architecture
Branching Strategies Compared
Four real options, not two. GitFlow is too heavy for a codebase where AI agents open PRs continuously; GitHub Flow is too loose once AI-generated code needs a supervised gate before production. The strategy below, Integration-Gate Flow, is a Trunk-Based Development variant — it keeps TBD’s short-lived-branch discipline and adds exactly the one thing standard TBD lacks: a supervised integration branch that gates AI output before it’s release-eligible.
| Strategy | Core Idea | Best Fit | Why (Not) Here |
|---|---|---|---|
| GitFlow | Long-lived develop, formal release/hotfix branches, scheduled merges | Shrink-wrapped software, versioned on-prem releases | Too much ceremony for continuous AI PR volume — develop adds a layer without adding a security gate |
| GitHub Flow | main + short-lived feature branches, deploy on merge | Small teams, single deployable, fully trusted contributors | Too loose — no supervised space to gate AI-generated code before it’s one merge from production |
| Trunk-Based Dev | Everyone commits to trunk; feature flags hide incomplete work | High-maturity teams with strong test coverage and flag infrastructure | The direct ancestor of Integration-Gate Flow below — but leans on flags instead of a reviewed branch to contain AI output, and has no dedicated slot for nightly integration/DAST/chaos runs |
| Integration-Gate Flow | Trunk-Based Development, plus one supervised integration branch as a review-and-test buffer | Multi-surface teams (UI/backend/frontend/mobile) mixing AI agents and humans | Selected — the minimum structure that gives AI output a gated holding area with nightly integration/perf/DAST/chaos/mutation tests, without GitFlow’s develop/release ceremony |
Three properties Integration-Gate Flow specifically buys for AI-assisted development:
Multiple AI Agents + Humans
Every AI agent owns its own ai/<feature> branch. Agents never touch main, release/*, or integration directly — they can only propose a PR.
Release Traceability
Every artifact that reaches main is squash-merged, signed, tagged, and shipped with an SBOM and attestation — any production commit maps 1:1 to a PR, a set of scans, and a build.
Fast Rollback
Squash-only history on main plus immutable, signed, tagged release images means rollback is kubectl rollout undo or re-deploying the previous signed tag — never a revert-and-pray.
The Branch Model
Eight branch types, one direction of promotion. Features, AI-generated code, and bug fixes flow up through integration → release/* → main. hotfix/* is the one branch allowed to move sideways into all three protected branches at once. experiment/* never merges — it exists purely to be cherry-picked from.
Branch topology — animated
Promotion Path (Feature → Integration → Release → Main)
Hotfix — Created From Main, Synced To All Three
Experiment — Cherry-Pick Only, Never Merged
main
Production only. The single source of truth for what is running live.
**Merges from:**release/*, hotfix/*
**Protection:**Signed commits, CODEOWNERS, no force-push, no merge commits, squash only
**On every merge:**Tag release · attach SBOM · attach attestations
release/vX.Y
Cut from integration for UAT, regression, and security validation.
**Merges from:**integration, hotfix/*
**Merges to:**main
**Rule:**No new features — bug fixes only
integration
The AI playground. Every UI, backend, frontend, mobile, and AI-generated PR lands here first.
**Merges from:**feature/*, ai/*, bugfix/*, hotfix/*
**Nightly:**Integration tests · performance · DAST · chaos · mutation tests
ai/
Owned by AI agents — e.g. ai/refactor-payment, ai/new-ui, ai/improve-auth, ai/graphql, ai/optimize-api.
**Rule:**Cannot merge directly — requires AI Review + Human Review + Security Review + passing tests
feature/
One feature per branch. Deleted immediately after merge.
**Merges to:**integration
bugfix/
Standard bug fix tied to a ticket, same review path as feature/*.
**Merges to:**integration
hotfix/
Emergency production patch — the one branch allowed to move sideways.
**Created from:**main
**Merges into:**main, release/*, integration
experiment/
Spikes and prototypes — e.g. experiment/langgraph, experiment/new-cache, experiment/ollama.
**Rule:**No guarantees. Never merged directly. Cherry-pick only.
Never allowed: direct commits to main, release/*, or integration — under any circumstances, by any human or agent. Every change enters through a pull request.
Repository Layout
One repo, one graph, clear ownership per surface. Security, telemetry, and infra live as first-class top-level directories — not bolted on inside each app.
repo/
├── ui/ # design system + shared components
├── backend/ # services, APIs, workers
├── frontend/ # web application
├── mobile/ # iOS + Android apps
├── shared/ # cross-surface types, utils, constants
├── terraform/ # cloud infrastructure as code
├── helm/ # kubernetes helm charts
├── k8s/ # raw manifests, kustomize overlays
├── .github/ # workflows, CODEOWNERS, PR/issue templates
├── otel/ # collector config, dashboards, alert rules
├── security/ # OPA/Kyverno policies, scan configs
├── docs/ # architecture docs, ADRs
├── scripts/ # unified CLI entry point
├── api/ # OpenAPI specs
└── protobuf/ # gRPC/proto contracts
Monorepo vs. Polyrepo
The layout above is a monorepo — ui/, backend/, frontend/, and mobile/ all live in one repository, one integration branch, one set of branch-protection rules. That’s a deliberate choice, not a default.
Monorepo — Selected
Buys: one PR can touch shared/, backend/, and frontend/ atomically — no cross-repo version pinning when an API contract changes. One CODEOWNERS file, one security pipeline, one OTel/policy config to keep current instead of four. An AI agent refactoring an API and its three callers does it in a single reviewable PR.
Costs: CI must be scoped by path filters (see the Terraform/Helm hooks) or every PR triggers every pipeline. Requires a build system that understands partial builds (Nx/Turborepo/Bazel-style) once the repo gets large.
Polyrepo — Rejected Here
Buys: hard isolation — a mobile team can’t accidentally break the backend build. Independent release cadences and independent access control per repo out of the box.
Costs: a cross-surface change becomes N coordinated PRs across N repos, each needing its own review and its own security pipeline run. For a small set of AI agents already producing a high PR volume, that multiplies the exact review load this branch model exists to control — and CODEOWNERS, OTel config, and policy rules must be kept in sync by hand across every repo.
Why monorepo wins here: the AI-guardrail and security-gate machinery in this guide has to be configured and kept current exactly once. Polyrepo would mean replicating branch protection, CODEOWNERS, the security pipeline, and the OTel requirements across every surface’s repo — more places for the gate to silently drift out of sync, which is the one failure mode Integration-Gate Flow is built to prevent.
Branch Naming & Commit Convention
Naming carries information the tooling depends on — the pre-push hook, the CI pipeline, and the changelog generator all parse these patterns.
Branch Naming
Ticket + owner, not a free-text description — git blame → branch → owner is then a one-step lookup, and two people never collide on the same ticket. Enforced by the pre-commit hook and its CI twin (Tab 3).
| Pattern | Example |
|---|---|
| feature/JIRA-user | feature/ABC-145-hellodk |
| bugfix/JIRA-user | bugfix/ABC-240-hellodk |
| hotfix/JIRA-user | hotfix/ABC-520-hellodk |
| ai/JIRA-agent | ai/ABC-300-agent |
| release/vX.Y | release/v1.4 |
| experiment/topic | experiment/new-ui — exempt, no ticket required |
Conventional Commits
| Prefix | Example |
|---|---|
| feat: | feat(auth): add OAuth login |
| fix: | fix(cart): correct tax rounding |
| perf: | perf(api): cache user lookups |
| security: | security(api): validate JWT |
| refactor: | refactor(ui): extract Button |
| docs: | docs(readme): update setup |
| build: | build(deps): bump pnpm lock |
| ci: | ci(workflow): add SBOM step |
| test: | test(payments): add contract test |
| otel: | otel(api): add payment span |
| ai: | ai(ui): generated dashboard |
Pipelines
Pipeline Overview
Six pipelines, one per moment in a change’s life — from a PR being opened to a signed tag running in production. They used to be split across tabs; they’re grouped here in the order a change actually passes through them: PR review → security gates → backend release → mobile release → semantic versioning → the end-to-end picture.
1 · The PR Pipeline
Every change — human or AI — travels the same nine-stage path before it can land on integration. Nothing skips a stage; there is no “trusted author” bypass, including for AI agents.
Developer / agent to merge — animated
2 · Security Pipeline
Eleven gates between a commit and a running, monitored container. Any one of them failing blocks the deploy — there is no override that isn’t logged and reviewed.
Secrets to deploy — animated
Security Tooling — Open Source vs. Enterprise
Every gate has a default. Where the org licenses a commercial platform, it slots into the same gate rather than adding a new one. SCA below covers open-source component analysis (dependency/license risk, sometimes shortened “OSA”).
| Security Gate | Tool Options |
|---|---|
| SAST | OSSSemgrep, CodeQL EnterpriseSonarQube |
| Code Coverage | EnterpriseSonarQube OSScoverage.py / JaCoCo / Istanbul |
| SCA / Dependency (OSA) | OSSOWASP Dependency-Check EnterpriseCheckmarx OSA |
| IaC / Config Scan | OSStflint EnterpriseCheckmarx |
| License Scan | EnterpriseCheckmarx |
| Container / Image Scan | OSSClair EnterprisePrisma Cloud |
| SBOM | EnterpriseCheckmarx |
| Image Signing | OSSCosign (Sigstore) |
| Image Registry | OSSQuay (self-hosted) EnterpriseRed Hat Quay, Quay.io |
| K8s Admission Enforcement | OSSKyverno, OPA / Conftest |
| Runtime Protection (Backend) | EnterprisePrisma Cloud (Defender) |
| Runtime Protection (Mobile / RASP) | EnterpriseAppdome, Guardsquare, Promon |
3 · Backend & Web Release Pipeline
Seventeen stages from a merged PR to a verified production rollout, for every surface that ships as a container — backend services and the containerized web frontend. Approval is the one manual human gate in the entire chain — everything before and after it is automated.
Merge to production, with post-deployment verification — animated
Helm Release Hooks — Not the Same Thing as helm-ci
The helm-ci check in Tab 3 lints the chart at PR time. Helm’s own release hooks are a separate, native mechanism — Kubernetes Jobs annotated helm.sh/hook: ... that run at deploy time, when helm install/upgrade actually executes against the cluster. They slot directly into the stages already shown above.
| Hook | Typical Job | Maps to Pipeline Stage |
|---|---|---|
| pre-install / pre-upgrade | DB schema migration, config validation | Runs before Deploy Dev / Deploy QA / Production — a failure here aborts the release, nothing new is applied |
| post-install / post-upgrade | Smoke-test Job, Slack/webhook notification | Smoke |
| test | helm test Job, run on demand post-deploy | Integration / Post-Deploy |
| pre-rollback / post-rollback | State snapshot, post-rollback verification | Fires automatically on helm rollback — the “Fast Rollback” path from Tab 1 |
| pre-delete / post-delete | Backup Job, external resource cleanup (DNS, LB) | Decommissioning — outside the normal promotion path |
Failure policy: if a pre-* hook Job fails, Helm halts before touching any release resources — the same fail-closed guarantee the rest of this pipeline relies on. Hook ordering within a phase is controlled by helm.sh/hook-weight; cleanup of one-shot hook Jobs is controlled by helm.sh/hook-delete-policy.
Working chart, not just a table: helm/example-service implements all 5 hook points above with the annotations that matter in production (hook-weight, hook-delete-policy, restartPolicy: Never, backoffLimit/activeDeadlineSeconds) — helm lint and helm template clean. Its README covers 11 edge cases the table above can’t: post-hook failures don’t roll back an already-live release (--atomic fixes this), test never runs automatically, why the delete policy is deliberately not hook-failed, idempotency requirements, and more.
4 · Mobile Release Pipeline — Android & iOS
The “frontend” delivery pipeline for the two app-store surfaces. It shares the same PR-review and CI gates as everything else, but diverges after that — there’s no Kubernetes canary for a mobile app; there’s a device farm, a store review queue, an in-app runtime guard, and a phased rollout instead.
Merge to app store, with real-device testing and in-app protection — animated
Build & Test
- Android built via Gradle, iOS via Xcode / Fastlane
- Unit + UI tests run per platform (Espresso, XCUITest)
- Real-device regression on HeadSpin’s device farm — actual hardware, not just emulators/simulators
- Beta build pushed to TestFlight (iOS) and the Play Console internal/closed track (Android)
Ship & Protect
- QA sign-off is the human gate — same role as
Approvalin the backend pipeline - Submitted to App Store Connect and the Google Play Console
- Store review queue is outside our control — build time budgets for it
- RASP (Runtime Application Self-Protection) SDK embedded at build time — guards the live app against tampering, rooting/jailbreak, and hooking frameworks post-install
- Phased/staged rollout percentage in both stores before 100%
5 · Semantic Versioning & Automated Release Notes
The version number and the changelog are both computed, not written by hand. Both are derived from the same input the commit-msg hook already enforces: Conventional Commits.
Semver does not roll over at 99. MAJOR, MINOR, and PATCH are each an independent, uncapped integer — there is no odometer digit limit. 1.1.99 followed by another fix: commit is 1.1.100, not 1.2.0. A segment only resets to zero when a higher segment increments: a feat: resets PATCH; a breaking change resets MINOR and PATCH.
Bump Rule — Highest-Precedence Commit Since the Last Tag Wins
| Commits Since Last Tag | Bump | Example |
|---|---|---|
| Any BREAKING CHANGE: footer, or a type! | Major — resets MINOR + PATCH | 1.1.99 → 2.0.0 |
| At least one feat:, no breaking change | Minor — resets PATCH | 1.1.99 → 1.2.0 |
| Only fix: / perf: / security: | Patch | 1.1.99 → 1.1.100 |
| Only docs: / build: / ci: / test: / refactor: / otel: / ai: | No release cut | 1.1.99 stays 1.1.99 |
Commit to signed, versioned tag — animated
Release-PR Model — Recommended
release-please maintains a standing, auto-updated “Release PR” that accumulates the version bump and changelog as commits land on release/*. Nothing is tagged until that PR is merged.
This fits the model directly — the Release PR is the “Approval” human gate already in the release pipeline. Merging it is what triggers tag, sign, and attest.
Continuous Model — Alternative
semantic-release computes the bump and publishes a tag on every merge to release/*, with no pause for review. Faster, but removes the human checkpoint — only appropriate if release/* is already fully covered by the required reviews in the branch-protection table.
Changelog Section Mapping
Only feat, fix, and perf exist in the default Angular commit preset most release tools ship with. security, otel, and ai are all custom to this convention and need an explicit type map in .release-please-config.json / .releaserc — without it, commits using those three prefixes are silently dropped from the notes.
| Prefix | Changelog Section |
|---|---|
| feat: | Features |
| fix: | Bug Fixes |
| perf: | Performance |
| security: | Security |
| ai: | AI-Generated Changes |
| otel: | Observability |
| refactor: / docs: / build: / ci: / test: | Hidden from notes (internal only) |
Enforcement lever: there is no separate release-notes check to pass or fail — commit-msg is the enforcement point. A commit that doesn’t match Conventional Commits never lands, so a malformed or missing changelog entry is structurally impossible, not just discouraged.
6 · Recommended End-to-End Flow
Every pipeline above, in one path — from a developer or an AI agent opening a branch, to a verified rollout on main.
Developer / AI agent to production — animated
Integration-Gate Flow scales across UI, backend, frontend, mobile, and AI-assisted development while enforcing security, observability, provenance, and release governance at every stage — the same nine PR-pipeline stages and eleven security gates apply regardless of which surface the change touches.
Hooks · Webhooks · Governance
Git Hooks
Two groups: hooks that run on the laptop or agent sandbox before code ever leaves it, and hooks that run server-side in CI once a PR is open. git commit runs pre-commit → prepare-commit-msg → commit-msg → post-commit in that order; git push is a separate command that later runs pre-push.
This isn’t just documentation — it’s running. Every hook below is a real, tested script in github.com/hellodk/github-ci-cd: .pre-commit-config.yaml + scripts/ for the client-side hooks, .github/workflows/ for their CI-side twins. pip install pre-commit && pre-commit install activates them in a clone.
Local hooks are fast feedback, not the enforcement boundary. Every local check below has a required-status-check twin in CI. Skipping a local hook — deliberately with --no-verify, or because a tool isn’t installed — just means finding out at the PR instead of on the laptop. Nothing reaches main without the CI-side check passing regardless of what ran locally.
Client-Side Hooks — Run Locally
pre-commit
**Purpose:**Formatting, linting, secret scan, IaC scan, Helm lint, Terraform validate, unit tests, license check, SBOM generation, branch naming convention — the cheapest place to catch a problem, before it becomes a CI minute.
**If not applicable:**Each file-scoped sub-check no-ops rather than failing — e.g. no .tf staged skips terraform fmt/validate entirely. Branch naming is the exception: like commit-msg, it always applies — every commit happens on some branch, checked against the naming patterns in Tab 1.
Bypass reporting:git commit --no-verify skips this hook silently on the laptop, but every one of these checks re-runs as a required CI status check on the PR — a bypassed commit fails later, not never.
pre-commitgitleakstrufflehog terraform fmtterraform validatetflint checkmarxhelm lintyamllint eslintruffgolangci-lint spotlessktlintswiftlint
pre-commit — ticket traceability
**Purpose:**Warns when a newly added HACK/WORKAROUND/XXX line has no ticket ID on it. The branch name and commit trailer already give git blame a ticket for free on every line — this exists only for lines where that isn’t enough, because a skimming reader needs the explanation right there.
**If not applicable:**No marker words in the diff — nothing to warn about, silent pass.
Bypass reporting:Not applicable — deliberately non-blocking (exit 0 always). Requiring a ticket comment on every changed line would be impractical and just get ignored.
prepare-commit-msg
**Purpose:**Pre-fills a Jira: ABC-145 trailer from the branch name before the editor opens, so the ticket is present without the author retyping it.
**If not applicable:**Skips entirely for git commit -m, merges, squashes, and --template — anywhere the message is already fully decided, mutating it would be surprising.
Bypass reporting:Not applicable — it only pre-fills a starting point; commit-msg is what actually enforces the ticket is present.
commit-msg
**Purpose:**Conventional Commits format, blank line before the body (required for changelog parsing), correct BREAKING CHANGE: footer format, branch↔commit ticket cross-check, Signed-off-by trailer, no forbidden words, 72-char subject limit.
**If not applicable:**Never — every commit has a message, so this hook always runs in full.
**Bypass reporting:**Same --no-verify path skips it locally; CI independently re-validates every commit in the PR and the PR title itself (squash-merge commits the title, not the individual commits) before merge is allowed.
post-commit
**Purpose:**Draft changelog entry appended locally, desktop/Slack notification to the author, SBOM cache refresh, ticket-status ping (e.g. move JIRA to “In Progress”).
**If not applicable:**Never skipped by design, but non-blocking — if a sub-step fails (e.g. Slack API down), the commit still stands.
Bypass reporting:Not applicable — this hook never gates, so there’s nothing to bypass. A failed step is written to a local .git/hooks.log, nothing more.
pre-push
**Purpose:**Unit tests, API contract tests, OpenAPI validation, proto validation, dependency check, SAST — the last local check before code leaves the machine.
**If not applicable:**Contract/proto validation skips if no .proto or api/*.yaml files changed in the pushed commits.
Bypass reporting:git push --no-verify skips it locally; the identical test/contract/SAST suite re-runs as a required CI check before the PR can merge.
Server-Side Hooks — Run in CI
terraform/** → terraform-ci
**Purpose:**terraform fmt -check, terraform validate, tflint, Checkmarx policy scan, terraform plan posted as a PR comment, Infracost estimate posted as a PR comment, nightly drift detection against live state.
**If not applicable:**PR doesn’t touch terraform/** — the job still runs and reports a “skipped, no changes” success, not an absent check, so this required status check never gets stuck pending.
**Bypass reporting:**Nothing to bypass server-side — every run (pass, fail, or skipped) is a GitHub check run, permanently visible in the PR’s Checks tab and Actions history.
helm/** → helm-ci
**Purpose:**helm lint, helm template + kubeconform schema validation, Conftest/OPA policy test against rendered manifests, Chart.yaml version bump check, kube-score / Prisma Cloud config scan on rendered output.
**If not applicable:**PR doesn’t touch helm/** — same skip-as-success pattern as terraform-ci.
**Bypass reporting:**Same — logged as a GitHub check run, nothing hidden or silently absent.
--no-verify cannot be blocked from a hook — by any repo, not just this one. When that flag is passed, git skips invoking the hook script entirely; there’s no code path for a hook to detect or refuse it. Anything claiming to “prevent” a local bypass from inside pre-commit/commit-msg/pre-push itself is describing something that can’t work. The one detection layer that’s still useful: pre-commit and pre-push emit a heartbeat to the OTel Collector when they do run — a push whose commit SHA never sent one gets a non-blocking “likely bypassed locally” PR comment. Informational only.
What actually prevents a bypass from reaching main: server-side branch protection — live on this repo’s main, not just documented. Direct pushes are rejected by GitHub itself before any hook question even arises; 6 required status checks (branch naming, every commit message, PR title, pre-commit re-run, terraform-ci, helm-ci) must pass; enforce_admins is on, so the repo owner isn’t exempt either. Verify it yourself: gh api repos/hellodk/github-ci-cd/branches/main/protection.
GitHub Webhooks
Every branch-protected event fans out to the systems that need to react to it — build, deploy, alert, or record.
Webhook Events
pushpull_requestmerge_group releasetagbranch protection deploymentcheck_runworkflow_run repository_dispatch
Trigger Targets
JenkinsArgo Security PlatformSlack ServiceNowSplunk OpenTelemetry Collector
Pull Request Checks — Mandatory
-
Minimum 2 approvals
-
CODEOWNERS sign-off
-
AI review completed
-
Security review completed
-
Architecture review (critical modules only)
-
Passing CI
-
Coverage threshold met (SonarQube)
-
SBOM generated
-
SLSA provenance attached
-
Signed commits, branch up to date
AI Guardrails
Every AI-generated PR is automatically checked for the failure modes specific to LLM-written code, on top of the standard security pipeline. Grouped by what actually catches each one.
Code Quality — caught by lint / dead-code analysis
Hallucinated Imports Dead Code Duplicate Code Unused Variables
Injection & Access Control — caught by SAST
Prompt Injection SQL Injection XSS SSRF Command Injection Unsafe Deserialization Missing Auth Missing Validation
Data & Secrets — caught by secret scan / DLP
PII Leakage Logging Secrets Hardcoded Credentials
Coverage Gaps — caught by PR checklist review
Missing Telemetry Missing Unit Tests Missing Documentation
OpenTelemetry Requirements
No service ships without traces, metrics, and logs wired in from the first commit — retrofitting observability after an incident is too late.
Trace
Every request gets a span tree from edge to database. Trace IDs propagate through every hop, including AI/LLM calls.
Metrics
Request rate, error rate, latency, and saturation exported in Prometheus format from every service.
Logs
Structured JSON logs carrying the active trace_id so a log line and a span are always one click apart.
Minimum Required Spans
HTTP RequestDatabase RedisKafka AuthenticationAuthorization CacheExternal API QueueFilesystem
Every PR Validates
- Trace propagation across service boundaries
- Correlation IDs present in logs
- Sampling configuration is set, not default
- Resource attributes populated
- OTel semantic conventions followed
Branch Protection Rules
The same rule set, scoped by branch. main carries one extra requirement — tagging — that doesn’t apply upstream. Provenance is delivered as an attestation, so anywhere provenance is required, attestations are required too.
| Rule | main | release/* | integration |
|---|---|---|---|
| No force push | ✓ | ✓ | ✓ |
| Linear / squash-only history | ✓ | ✓ | — |
| Signed, verified commits | ✓ | ✓ | ✓ |
| Required reviews (CODEOWNERS) | ✓ | ✓ | ✓ |
| Required status checks | ✓ | ✓ | ✓ |
| Conversation resolved | ✓ | ✓ | ✓ |
| No stale reviews | ✓ | ✓ | ✓ |
| Branch up to date before merge | ✓ | ✓ | ✓ |
| Passing security scans | ✓ | ✓ | ✓ |
| SLSA provenance required | ✓ | ✓ | — |
| Signed container image required | ✓ | ✓ | — |
| SBOM upload required | ✓ | ✓ | — |
| Tag every release | ✓ | — | — |
| Attestations attached | ✓ | ✓ | — |
Repository Policies — Enforced via OPA / Kyverno
-
No plaintext secrets
-
No large binaries
-
No direct main commits
-
Mandatory CODEOWNERS
-
Mandatory telemetry
-
Mandatory tests
-
Mandatory documentation updates
-
Mandatory changelog
-
Mandatory semantic versioning
-
Mandatory signed commits
-
Mandatory Dependabot resolution
-
Mandatory branch naming
-
Mandatory Conventional Commits
Centralized Governance
Why Centralize
Every hook, workflow, and policy in this guide currently lives inside one repo. The moment a second application repo needs the same branch-naming check, the same security pipeline, the same Helm hook conventions, you have a choice: copy-paste it (and now there are two places that drift out of sync the first time one gets updated), or centralize it — one source of truth, versioned, that every repo consumes rather than reimplements. At 50 application repos, copy-paste isn’t a maintenance cost, it’s a governance failure waiting to happen.
One hooks/policy repo, three distribution mechanisms — animated
This Repo → Distribution Mechanism
Mechanism → Every Application Repo
No Repo Access → Boundary Enforcement Instead
Client-Side — pre-commit’s repo: field
pre-commit was built for exactly this. .pre-commit-config.yaml’s repo: field accepts any git URL — point it at this repo instead of pre-commit/pre-commit-hooks, pin a rev:, and every consuming repo gets the same branch-naming, commit-msg, and ticket-traceability checks with zero duplicated script logic.
CI-Side — Reusable Workflows
pr-checks.yml, terraform-ci.yml, and helm-ci.yml become workflow_call targets here. Every application repo’s own workflow shrinks to a two-line wrapper: uses: hellodk/github-ci-cd/.github/workflows/pr-checks.yml@v1. One version bump here updates every consumer’s next run.
Org-Side — Repository Rulesets
GitHub Rulesets defined once at the organization level, targeting ~ALL repos matching a pattern, make the reusable workflow’s status checks mandatory rather than opt-in — this is what turns “a repo could adopt this” into “every repo must.” Requires org-owner rights, not per-repo admin.
Git Hooks — Centralizing pre-commit
The mechanism already exists in every repo that has a .pre-commit-config.yaml — it just usually points at public hook repos. Point it at yours instead.
# in any application repo's .pre-commit-config.yaml
repos:
- repo: git@github.com:hellodk/github-ci-cd.git
rev: v1.4.0 # pinned — never floating
hooks:
- id: branch-name-check
- id: commit-msg-check
- id: ticket-traceability-check
Bootstrapping new repos: a company-wide init.templateDir (git config --global init.templateDir ~/.git-templates) seeds this config into every git init/git clone automatically. Catching skips even then: point pre-commit.ci at the same config so it re-runs server-side on every PR — the same “local hooks aren’t the enforcement boundary” principle from Tab 3, applied org-wide instead of repo-wide.
Terraform — Centralizing Modules & Policy
Two separate things get centralized: the good pattern itself (as a module teams consume) and the policy that rejects the bad pattern (as something the run pipeline evaluates, not something each repo’s CI has to remember to check).
Shared Module Registry
App teams stop hand-writing for_each+map S3 buckets from scratch — they consume the vetted version:
module "buckets" {
source = "git::https://github.com/hellodk/github-ci-cd.git//terraform/modules/s3-bucket?ref=v1.2.0"
}
Policy Moves to the Runner
If every terraform apply routes through one shared platform — Atlantis, Terraform Cloud, Spacelift — Sentinel or OPA/Conftest policies defined once there apply to every workspace automatically. The enforcement point becomes the platform, not each repo’s willingness to wire up terraform-ci.yml correctly.
Helm — Centralizing Charts & Admission
The strongest lever here isn’t the repo at all — it’s the cluster. A manifest gets admitted or rejected based on what it is, not which repo’s CI produced it.
Shared Chart Registry
Publish helm/example-service’s pattern to an OCI registry (GHCR, Harbor, ChartMuseum). App repos helm install a versioned central chart instead of authoring their own release hooks from first principles — the hook edge cases in Tab 2 get solved once.
Cluster Admission — the Real Backstop
Kyverno/OPA Gatekeeper admission policies (already the Admission stage of Tab 2’s Security Pipeline) don’t care which repo a manifest came from. A workload without the required labels, resource limits, or signed image gets rejected at kubectl apply regardless of source — this is the one control that works even when nothing upstream cooperated.
When You Don’t Control the Repo
This is the real question, and it has a real answer — but not the one “add a hook” implies. You cannot install a git hook into a repo you have no push access to, and you cannot configure branch protection on a repo you’re not an admin of. Full stop. The lever that’s actually available shifts depending on how “not controlled” the repo is.
| Situation | Real Lever |
|---|---|
| Different team, same GitHub org | Org-level Repository Rulesets / Required Workflows — an org owner can mandate these without the individual repo owner opting in. This is the one case where you get repo-level enforcement without repo-level cooperation. |
| Genuinely external (vendor, OSS dependency) | You cannot touch their repo at all. Move enforcement to a boundary you control instead: the artifact registry (refuse unsigned/unscanned images), cluster admission (Kyverno/Gatekeeper reject non-compliant manifests regardless of origin), or a deployment gateway that verifies SLSA provenance/attestation before promoting anything. |
| Vendor relationship, contractual leverage exists | Not a technical control at all — security requirements written into the procurement contract (SOC2 evidence, scan reports, signed artifacts as a delivery condition) is the actual enforcement mechanism when there’s no git access to lean on. |
The pattern underneath all three: when you can’t push a hook upstream into someone else’s process, you verify the output at the boundary where their work meets yours — a registry pull, a cluster admission, a deployment gate. Same principle as Tab 3’s “local hooks aren’t the enforcement boundary,” just with the boundary moved from “your laptop vs. CI” to “their repo vs. your cluster.”
Enjoyed this post?
Get the next one in your inbox — only when I ship something worth reading.
Newsletter form not configured.
Or follow on Substack for the newsletter.
Comments via GitHub Discussions
Comments not configured. Set GISCUS env vars to enable.