Multi-Repo Workflow¶
Cloud-Native DCS is split across two repositories:
| Repo | What's there | Visibility |
|---|---|---|
cloud-native-dcs |
CRDs, operators, gateway, CLI, Helm chart source, docs | Private (license model TBD, #278) |
cndcs-deploy-demo |
One verified production-quality example deployment: Terraform, Flux GitOps, bootstrap scripts | Private (license model TBD, #278) |
All CNDCS repos are private by default. Going public is a deliberate future decision gated on the license-model resolution in #278.
The Helm chart source lives in this repo. The deploy-reference repo only consumes
a pinned chart version published to oci://ghcr.io/cloud-native-dcs/charts/cloud-native-dcs.
Tracking issue: #265.
Sibling checkout layout¶
Both repos assume sibling checkouts under a common parent directory. The
canonical path is ~/cloud-native-dcs/, revised there on 2026-08-10
(cndcs-claude-harness#41, and portable/docs/machine-onboarding.md § 1 for
the reasoning):
~/cloud-native-dcs/cloud-native-dcs/— this repo~/cloud-native-dcs/cndcs-deploy-demo/— deployment example
If you clone with a different layout, override the sibling path when running the sync target:
make sync-deploy-ref DEPLOY_REF_DIR=/path/to/cndcs-deploy-demo
Bumping the chart-version pin¶
After cutting a release of this repo (which publishes a new chart version to ghcr), sync the pin in the deploy-reference repo:
# from this repo's root, with the sibling clone alongside:
make sync-deploy-ref
What it does (see scripts/sync-deploy-ref.sh):
- Reads the chart version from
deploy/helm/cloud-native-dcs/Chart.yaml. - Refuses to run if
../cndcs-deploy-demo/has uncommitted changes. - Checks out
mainin the deploy repo and reconciles it with the origin. Amainthat is behind fast-forwards, and amainthat has diverged is refused for a human to sort out. - Updates every
flux/**/dcs-release.yamlwhosechart:iscloud-native-dcs, replacing theversion:line in the chart spec. - Commits the bump on
mainand pushes it.
The run finishes the flow, and that is the point of it. Pass --no-push to
commit on main without pushing, when you want to read the bump before it
lands:
scripts/sync-deploy-ref.sh ../cndcs-deploy-demo --no-push
git -C ../cndcs-deploy-demo log -1 -p
git -C ../cndcs-deploy-demo push origin main
The script opened a bump-chart-<version> branch and stopped there until
2026-09-08, leaving the push and a pull request for the solo-dev workflow to
finish by hand. Nobody finished it. Four bumps ended up stacked on each other
as unmerged branches. Meanwhile the deploy repo's main went on pinning chart
0.2.0, one of the 24 charts withdrawn from ghcr on 2026-08-03 as
uninstallable. For five weeks the reference deployment pointed at a chart
helm pull returns 404 for, and every release produced another branch. A pull
request there has the same person as author and merger, which is a review that
never happens standing in front of a pin that must not go stale. So the founder
ruled that the script lands on main and pushes (da2e869a8, with the
fast-forward guard that ruling needed corrected under #2098).
The deploy repo's CI (.github/workflows/version-pin-drift.yml) fails if the
pinned version isn't actually published to ghcr, catching the gap before Flux
reconciles a non-existent chart.
Where do plant CRs live?¶
A frequent question after the split: when a customer authors Phase, ControlModule,
Recipe, Unit, etc. manifests, where do those YAML files belong?
| Bucket | What's in it | Lives in |
|---|---|---|
| Syntax reference | Small, self-contained "here's what a Phase looks like" snippets, one CRD per file or directory. Referenced from docs. Evolves in lockstep with CRD schema changes. |
cloud-native-dcs/examples/ |
| Working customer | A complete, GitOps-reconciled plant: Enterprise → Site → Area → ProcessCell → Unit → ControlModule → Phase → Recipe. Mirrors what a real customer's deploy repo would look like. |
cndcs-deploy-demo/flux/clusters/<env>/dcs/ |
The mental model: examples/ is syntax reference, and cndcs-deploy-demo is a
working customer. A real pharma customer forks/copies the deploy-reference shape and drops their
own plant CRs in, occasionally cribbing from examples/ for syntax.
Rules¶
- Never put a
kind: Batchmanifest in a Flux-reconciled directory. Applying aBatchtriggers immediate execution. Batches are per-run operational artifacts. They are not declarative desired state. Keep them out of GitOps. Create them via thedcsCLI, the gateway UI, or a controlled CronJob. If you need to ship example batch manifests, they belong inexamples/with a clear "do not bulk-apply" note. - CRDs reconcile before plant CRs. Any Flux Kustomization that applies plant CRs must
dependsOnthedcs-releaseKustomization (or whatever path installs the chart and its CRDs). - Beware prune cascade when restructuring. Renaming or moving a Kustomization path
under
prune: truecascade-deletes everything that Kustomization owns. See the cutover post-mortem in the GitOps Runbook. - Cluster-scoped vs namespaced.
EnterpriseandSiteare cluster-scoped — scope them carefully when there are multiple environments sharing a cluster. Most other plant resources land insite-<sitename>namespaces created by the Site controller. - Lint the plant CRs in CI, before Flux applies them.
dcs lint <dir>runs every offline check over the tree: that each device address names the I/O module carrying the point, that each function block network is one the runtime can load, and that each ControlModule compiles against the template it names, and that each declared write posture is honoured by what that deployment actually drives. It reads the documents and nothing else, so it runs on a pull request with no cluster and no gateway. This is not a hypothetical: the golden-path site bound three ControlModule ports to simulationaddressMapkeys for months, and the driver answered each of them with0.0at qualityGood(#1749). See CLI Reference.
Reference deployment: dual-mode¶
The hosted reference deployment runs two parallel provisioning flows on the same cluster, with no overlap in site-name space:
- Multi-tenant
autoProvision— each authenticated user gets a per-tenantSitenameddemo-<username>, provisioned at first login from the bundledplant-01scenario. That is the same reference plant the documentation describes, so an evaluator finds the units the Quickstart names on their own tenant. Each tenant's site is minted at runtime. Nothing in Git declares it. - Flux-reconciled
golden-pathsite — a single, dedicated site reconciled fromcndcs-deploy-demo/flux/clusters/demo/dcs/sites/golden-path/. This is the worked example of the customer GitOps pattern that real customers fork.
The two coexist because they serve different jobs. autoProvision is a tenant
onboarding shortcut for the hosted reference deployment, and the golden-path overlay
is how the platform itself is operated. Site names never collide (demo-<username> vs
golden-path), and the cluster-scoped ControlModuleTemplates used by
golden-path are gp-*-prefixed so they don't fight the tenants for ownership.
A single-tenant customer cluster would simply omit the autoProvision side: set
gateway.demo.autoProvision: false, leave gateway.demo.scenarioDir empty, and
reconcile their plant via Flux exactly the way golden-path demonstrates.
When to cross-cut both repos¶
| Change | Touches | How |
|---|---|---|
| New chart values default | this repo | normal PR; bump chart version: in Chart.yaml; cut release; make sync-deploy-ref |
| Reference deployment value override | deploy-reference | direct PR in deploy-reference; pin stays |
| New CRD field used by the reference deployment | both | this repo first → release → make sync-deploy-ref → deploy-reference PR adds value |
| Run docs gain a new section | this repo (requirements) + deploy-reference (worked example) | this repo describes "you must…"; deploy-reference paths are linked from docs/deploy-your-own.md. See #264. |
Out of scope¶
- No chart split. The chart stays in this repo as product code. The deploy repo never forks it.
- No multi-environment Flux. One reference cluster, one example. Forks pile on their own envs.
- No paid tier and no "enterprise edition" split. The product license model is undecided (see #278). When a license model is chosen it applies uniformly across both repos. The deploy reference is a worked example. It is not a separately-licensed tier.