Skip to content

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

  1. Reads the chart version from deploy/helm/cloud-native-dcs/Chart.yaml.
  2. Refuses to run if ../cndcs-deploy-demo/ has uncommitted changes.
  3. Checks out main in the deploy repo and reconciles it with the origin. A main that is behind fast-forwards, and a main that has diverged is refused for a human to sort out.
  4. Updates every flux/**/dcs-release.yaml whose chart: is cloud-native-dcs, replacing the version: line in the chart spec.
  5. Commits the bump on main and 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: Batch manifest in a Flux-reconciled directory. Applying a Batch triggers immediate execution. Batches are per-run operational artifacts. They are not declarative desired state. Keep them out of GitOps. Create them via the dcs CLI, the gateway UI, or a controlled CronJob. If you need to ship example batch manifests, they belong in examples/ with a clear "do not bulk-apply" note.
  • CRDs reconcile before plant CRs. Any Flux Kustomization that applies plant CRs must dependsOn the dcs-release Kustomization (or whatever path installs the chart and its CRDs).
  • Beware prune cascade when restructuring. Renaming or moving a Kustomization path under prune: true cascade-deletes everything that Kustomization owns. See the cutover post-mortem in the GitOps Runbook.
  • Cluster-scoped vs namespaced. Enterprise and Site are cluster-scoped — scope them carefully when there are multiple environments sharing a cluster. Most other plant resources land in site-<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 simulation addressMap keys for months, and the driver answered each of them with 0.0 at quality Good (#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:

  1. Multi-tenant autoProvision — each authenticated user gets a per-tenant Site named demo-<username>, provisioned at first login from the bundled plant-01 scenario. 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.
  2. Flux-reconciled golden-path site — a single, dedicated site reconciled from cndcs-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.