Branch Strategy, Repository Structure & CI/CD Flow
:::note Superseded model
This page was rewritten (2026-09-04) for the trunk-based + Kargo model. The earlier
dev/staging/prod branch-per-environment flow (long-lived branches, dev-<sha> /
staging-<sha> image tags, CI-driven overlay bumps) is retired. Environments are now Kustomize
overlays reconciled from main, promoted by Kargo β see
Kargo β Multi-Stage Promotion.
:::
All ~26 repositories in andrelair-platform follow one trunk-based standard: a long-lived main,
short-lived feature branches, and no branch-per-environment. Environments are deploy targets
(namespaces/overlays), not git branches.
Environments are not git branchesβ
The platform runs exactly two environments (dev + prod). Neither is driven by a long-lived
branch β that's the GitOps anti-pattern (Git is the control plane; environments are overlays, not
branches you merge between).
| Thing | What it is | Fed by |
|---|---|---|
dev environment | <svc>-dev namespace + minicloud-1/dev overlay (permanent) | Kargo auto-promotes the main build into it, then verifies it |
prod environment | <svc>-prod namespace + minicloud-1/prod overlay (permanent) | Kargo opens a CODEOWNERS-gated PR promoting the dev-verified Freight |
main branch | the trunk β the only deploy trigger | a merge builds :<sha> β Kargo takes over |
dev branch (optional) | a working/integration branch | nothing β pushing it no longer builds or deploys |
| feature branches | feat/β¦ fix/β¦ docs/β¦ chore/β¦ β PR β main | short-lived; auto-deleted on merge (delete_branch_on_merge=true on all 26 repos) |
staging was removed platform-wide (environment retired for the 2-env standard; leftover branches
purged 2026-09-04). Do not recreate it.
Repository structure β polyrepo + one GitOps repoβ
Delivery is polyrepo: each service owns its source repo (code + Dockerfile + CI); a single
minicloud-gitops repo holds all deploy configuration (app-of-apps). Source and deploy config
are separated β the opposite of co-locating Helm charts in the manifest repo.
<service-repo>/ # one per microservice (polyrepo)
βββ <source>, Dockerfile
βββ .github/workflows/ci.yml # build β test β scan β sign β push (build-only)
βββ .github/workflows/release.yml # release-please + SemVer dual-tag
minicloud-gitops/ # the single GitOps repo (deploy config only)
βββ apps/ # ArgoCD Application manifests
β βββ platform/ # ~43 infra apps
β βββ workloads/ # <svc>-dev.yaml, <svc>-prod.yaml
βββ manifests/argocd-project/ # the AppProject (sourceRepos/destinations/whitelist)
βββ services/<svc>/ # our services β Kustomize, NOT Helm charts
β βββ base/ # deployment/service/β¦ (no ns, no image tag)
β βββ minicloud-1/{dev,prod}/ # env overlays: image tag + patches (β per-env values)
β βββ kargo/ # Kargo Project + Warehouse + Stages + ProjectConfig + verify
βββ helm-values/minicloud-1/ # values for THIRD-PARTY charts only
βββ manifests/ # platform infra (network policies, quotas, ai, β¦)
How this maps to the reference "microservice-helmcharts" layoutβ
A common tutorial layout centralises everything in the manifest repo (argocd/application/{env}/,
env/{env}/<svc>/values.yaml, kargo/<svc>-config/, service-charts/<svc>/). We have every one of
those building blocks β organised differently, on purpose:
| Reference layout | Here | Why |
|---|---|---|
argocd/application/{dev,staging,prod}/ | apps/{platform,workloads}/<svc>-{dev,prod}.yaml | split by type, not env; 2-env |
AppProject (craftisia-project.yaml, destinations: "*") | manifests/argocd-project/00-project.yaml | explicit allowlist, no "*" (hardened) |
env/{env}/<svc>/values.yaml (Helm values per env) | services/<svc>/minicloud-1/{dev,prod} (Kustomize overlays) | Kustomize for our services; Helm reserved for third-party charts |
service-charts/<svc>/ (Helm chart in the manifest repo) | chart/source in the service's own polyrepo | true polyrepo β deploy config β app source |
kargo/<svc>-config/ (top-level) | services/<svc>/kargo/ (co-located with the service) | everything for a service in one dir |
.github/workflows/docker-ci.yml (in the manifest repo) | CI in each service polyrepo | consistent with polyrepo |
projectconfig.yaml (promotion policies) | services/<svc>/kargo/projectconfig.yaml | identical concept |
dev / staging / prod stages | dev + prod stages (sources.stages: [dev] gate) | 2-env standard |
Kustomize-vs-Helm is a deliberate tool choice, not a gap: minicloud-1/dev/kustomization.yaml
(image tag + patches) does exactly what the reference's env/dev/<svc>/values.yaml does. We do not
restructure the working 91-app GitOps repo to match a tutorial's Helm layout β that would be churn for
zero benefit.
Branch protection β enforced via GitHub Rulesetsβ
Enforced through repository-level rulesets (works on the free plan). One main-protection ruleset
per repo:
| Rule | Value |
|---|---|
| Pull request required | Yes (self-merge allowed; CODEOWNERS review required on protected paths in minicloud-gitops) |
| Signed commits required | Yes β GPG key FD6D39D681DEFA34 (Kargo PRs are squash-merged so GitHub signs the squash commit) |
| Force push | Blocked |
| Auto-delete head branch on merge | On (feature branches are short-lived by construction) |
Direct push to main without a PR is rejected at the API level; the only exception is admin bypass
(org owner), used only in emergencies, and it leaves a visible audit trail. There is no
staging-protection ruleset anymore.
Why per-repo, not org-level: org-level rulesets need the GitHub Team plan; the org is on free. Per-repo rulesets carry identical content, applied via the API in batch.
CI/CD flow (per service)β
feature branch ββPRβββΊ main (signed, PR-gated)
β
βΌ CI (build-only): test β Trivy CRITICAL β build β cosign sign β SBOM (syft)
β dual-push: Harbor :<sha> (dev registry) + ghcr :<sha> (prod, durable)
β on a release-please release: crane adds the vX.Y.Z SemVer alias to the same digest
βΌ
Kargo ββauto-promoteβββΊ dev environment ββverify (smoke AnalysisRun)βββΊ
β
βΌ dev-verified Freight ββCODEOWNERS PRβββΊ prod environment (Argo CD syncs)
- CI is build-only (
push: [main]). It no longer bumps overlays or smokes deploys β Kargo owns promotion + dev verification. See Kargo β Multi-Stage Promotion. - Image tags: the git SHA is the canonical promotion identifier (Warehouses watch it, Freight =
a commit, overlays pin it). A SemVer alias (
vX.Y.Z) is added to the same digest on a release-please release (dual-tagging β SHA for machines, SemVer for humans).
Supply chain (on main)β
| Control | Applied |
|---|---|
| Trivy CRITICAL scan | Yes |
| Cosign keyless sign (Sigstore/Fulcio) | Yes β Harbor and ghcr digests |
| SBOM (CycloneDX via syft) + attached as OCI referrer | Yes |
| GPG-signed GitOps commit | Yes (Kargo squash-merge β signed squash) |
Setting up a new repo (checklist)β
# 1. Create the repo, push initial code to main.
# 2. Enable auto-delete of merged branches:
gh api -X PATCH repos/andrelair-platform/<REPO> -F delete_branch_on_merge=true
# 3. Apply the main-protection ruleset (signed commits + PR + no force-push):
gh api repos/andrelair-platform/<REPO>/rulesets -X POST --input /tmp/main-ruleset.json
# 4. CI trigger = main only:
# on: { push: { branches: [main] }, pull_request: { branches: [main] } }
# 5. If the service warrants Kargo (build it + dev AND prod + env-agnostic + immutable prod tag),
# add services/<svc>/kargo/ β see the Kargo guide "new service checklist".
No dev/staging branches to create. An optional dev integration branch can exist but deploys
nothing.
ACPR / DORA alignmentβ
- Every merge to
mainproduces a[CHANGE]issue (change-record.yml) β who/when/what/risk β satisfying DORA Art.9 ICT change management. - Incident issue template covers DORA Art.19 major-incident classification.
- Separate
dev/prodenvironments (namespaces/overlays) satisfy ACPR 2021-R-01 Β§4.2 environment segregation β segregation is by environment, not by branch. - GPG-signed commits + cosign-signed images + SBOM provide non-repudiation and supply-chain provenance for every production change.
- Blocked force-push on
mainkeeps history immutable.