gitaiworkflow

Git Branching Strategy for AI-Assisted Development — Complete Visual Guide

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 integration branch
  • 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/*, or integration — 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.

StrategyCore IdeaBest FitWhy (Not) Here
GitFlowLong-lived develop, formal release/hotfix branches, scheduled mergesShrink-wrapped software, versioned on-prem releasesToo much ceremony for continuous AI PR volume — develop adds a layer without adding a security gate
GitHub Flowmain + short-lived feature branches, deploy on mergeSmall teams, single deployable, fully trusted contributorsToo loose — no supervised space to gate AI-generated code before it’s one merge from production
Trunk-Based DevEveryone commits to trunk; feature flags hide incomplete workHigh-maturity teams with strong test coverage and flag infrastructureThe 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 FlowTrunk-Based Development, plus one supervised integration branch as a review-and-test bufferMulti-surface teams (UI/backend/frontend/mobile) mixing AI agents and humansSelected — 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 integrationrelease/*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

cherry-pick only main Production Only release/vX.Y UAT · Regression · Security integration AI + Human Playground ai/<feature> AI Agent Owned feature/<jira> Human Feature Work bugfix/<jira> Human Bug Fix hotfix/<jira> Emergency Patch experiment/<topic> No Guarantees

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).

PatternExample
feature/JIRA-userfeature/ABC-145-hellodk
bugfix/JIRA-userbugfix/ABC-240-hellodk
hotfix/JIRA-userhotfix/ABC-520-hellodk
ai/JIRA-agentai/ABC-300-agent
release/vX.Yrelease/v1.4
experiment/topicexperiment/new-ui — exempt, no ticket required

Conventional Commits

PrefixExample
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 GateTool Options
SASTOSSSemgrep, CodeQL   EnterpriseSonarQube
Code CoverageEnterpriseSonarQube   OSScoverage.py / JaCoCo / Istanbul
SCA / Dependency (OSA)OSSOWASP Dependency-Check   EnterpriseCheckmarx OSA
IaC / Config ScanOSStflint   EnterpriseCheckmarx
License ScanEnterpriseCheckmarx
Container / Image ScanOSSClair   EnterprisePrisma Cloud
SBOMEnterpriseCheckmarx
Image SigningOSSCosign (Sigstore)
Image RegistryOSSQuay (self-hosted)   EnterpriseRed Hat Quay, Quay.io
K8s Admission EnforcementOSSKyverno, 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.

HookTypical JobMaps to Pipeline Stage
pre-install / pre-upgradeDB schema migration, config validationRuns before Deploy Dev / Deploy QA / Production — a failure here aborts the release, nothing new is applied
post-install / post-upgradeSmoke-test Job, Slack/webhook notificationSmoke
testhelm test Job, run on demand post-deployIntegration / Post-Deploy
pre-rollback / post-rollbackState snapshot, post-rollback verificationFires automatically on helm rollback — the “Fast Rollback” path from Tab 1
pre-delete / post-deleteBackup 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 Approval in 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 TagBumpExample
Any BREAKING CHANGE: footer, or a type!Major — resets MINOR + PATCH1.1.99 → 2.0.0
At least one feat:, no breaking changeMinor — resets PATCH1.1.99 → 1.2.0
Only fix: / perf: / security:Patch1.1.99 → 1.1.100
Only docs: / build: / ci: / test: / refactor: / otel: / ai:No release cut1.1.99 stays 1.1.99

Commit to signed, versioned tag — animated

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.

PrefixChangelog 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.

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-commitprepare-commit-msgcommit-msgpost-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.

Rulemainrelease/*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

github-ci-cd Hooks & Policy Source pre-commit repo: Client-Side Hooks Reusable Workflows workflow_call — CI-Side Org Rulesets Mandates Adoption Application Repositories N Repos, One Config Repo You Don't Own No Push Access, No Admin enforcement shifts to a boundary you DO control Artifact Registry Refuse Unsigned Images Cluster Admission Kyverno / Gatekeeper Deployment Gateway Verify SLSA Provenance

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.

SituationReal Lever
Different team, same GitHub orgOrg-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 existsNot 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.