# shdctl workflows

> Run shdctl in your CI pipeline with ArgoCD, or use it for generate-only, air-gapped, or direct Helm deployments

## Recommended workflow: CI + ArgoCD

This is the recommended way to run shdctl: in a CI job, with ArgoCD reconciling the rendered charts from a Git repo. It is [Mode A](./architecture.md#mode-a--recommended-\(ci\)) of the architecture.

### One-time setup

* **Create the deployment namespace** (`configuration.kubernetes.namespace`). Nothing in the release creates it, and every step below that touches the cluster needs it.
* **Install ArgoCD so that it reconciles Applications in that namespace.** Every application lands there, in ArgoCD's `default` project — shdctl applies the top-level one and ArgoCD creates the rest from Git. Install ArgoCD into the deployment namespace, or configure it to watch that namespace (ArgoCD's "Applications in any namespace", which needs the namespace in both its `application.namespaces` setting and the `default` project's `sourceNamespaces`). Give it read access to the Git repo the pipeline commits the rendered charts to.
* **Point `deployment.argocd` in your manifest at that repo** (the [manifest reference](./manifest.md)). `repoURL` and `destinationServer` are required; `targetRevision` is the branch the pipeline pushes to; `pathPrefix` is the directory, relative to the repository root, the charts are committed under. The pipeline below runs at the root of your repository checkout and renders into `generated-charts/`, so set `pathPrefix: generated-charts` — or change both together.
* **Build `secrets.import.yaml` once, with `--persist`,** so that it carries every value shdctl generates as well as the ones you supply. `secret generate` reads the release's secrets schema, so pull the release first:

  ```sh
  shdctl release pull --clean-output
  shdctl secret generate --import my-values.yaml --use-defaults --persist secrets.import.yaml
  ```

  Check it for `TBD` before you store it — a required value missing from `my-values.yaml` is saved as that placeholder, and later runs reuse it without warning again. Store that file in your CI secret store, base64-encoded. A value it does not carry is generated afresh on every pipeline run, and deploying a regenerated password over the one a running service holds breaks that service. When an upgrade adds a secret, run `shdctl secret generate --use-defaults --persist secrets.import.yaml` against the new release — it reads the file back and generates only the new values — and store the result.
* **Provision a kubeconfig for the CI runner with namespace-scoped RBAC**: `get`, `create` and `patch` on `Secret` and on `argoproj.io/Application` in the deployment namespace. `kubectl apply` reads each object before creating or patching it, which is why `get` is needed.
* **Step 0 of the pipeline reads cluster-scoped objects that role cannot.** Either grant the CI identity these read-only rules through a ClusterRole, or run `shdctl cluster check` yourself before the first deploy and leave it out of the pipeline:

  ```yaml
  rules:
    - apiGroups: [""]
      resources: ["nodes"]
      verbs: ["list"]
    - apiGroups: [""]
      resources: ["namespaces"]
      verbs: ["get"]
    - apiGroups: ["storage.k8s.io"]
      resources: ["storageclasses"]
      verbs: ["list"]
    - apiGroups: ["apiregistration.k8s.io"]
      resources: ["apiservices"]
      verbs: ["get"]
    - apiGroups: ["apiextensions.k8s.io"]
      resources: ["customresourcedefinitions"]
      verbs: ["get"]
  ```

  Add `list` on `pods` for the ArgoCD controller check, which is skipped without it, and `list` on `nodepools` in `karpenter.sh` if Karpenter provides your transformation nodes on demand.
* **Registry credentials for the runner**: a Unity registry account for `release pull` and, air-gapped, the source side of `artifact sync`; push access to your own registry for its target side.

### Pipeline steps

```sh
# 0. Validate the manifest, then that the cluster meets the prerequisites (read-only)
shdctl manifest validate
shdctl cluster check

# 1. Pull the release from Unity's registry (read-only)
echo "$UNITY_REGISTRY_PASSWORD" | shdctl configure set --username "$UNITY_REGISTRY_USERNAME" --password-stdin
shdctl release pull --clean-output

# 2. (Air-gapped only) Mirror artifacts to your registry, then confirm the result.
#    Run `docker login` for Unity's registry and yours first.
shdctl artifact sync preflight
shdctl artifact sync images --skip-existing --cleanup
shdctl artifact sync oras
shdctl artifact sync verify

# 3. Render ArgoCD app-of-apps charts into generated-charts/, the directory pathPrefix names
shdctl release generate --format argocd --clean-output

# 4. Render Kubernetes Secret manifests (secrets.import.yaml decoded from your CI secret store)
shdctl secret generate --import secrets.import.yaml --use-defaults --output secrets.yaml

# 5. Commit charts to your Git repo
git add generated-charts/
git commit -m "Deploy release ${RELEASE_VERSION}"
git push

# 6. Apply Secrets to the cluster (namespace-scoped kubeconfig; every Secret already names its namespace)
shdctl secret deploy --file secrets.yaml

# 7. Apply the bootstrap Application (namespace-scoped kubeconfig)
shdctl release deploy --format argocd

# ArgoCD picks up the changes from Git from here on.
```

For the credentials shdctl reads and the verbs it calls in your cluster, refer to [What shdctl touches](./architecture.md#what-shdctl-touches).

### Helm alternative

If you don't run ArgoCD, you can use `--format helm` in the same CI pipeline. Replace steps 3, 5, and 7 with `shdctl release generate --clean-output` and `shdctl release deploy --format helm`; skip the `git push` and the ArgoCD items of the one-time setup. The CI runner then needs a cluster-wide kubeconfig (it runs `helm upgrade --install` directly), which is a weaker trust boundary than the ArgoCD path. ArgoCD is recommended for that reason.

Run `shdctl release deploy --format helm --dry-run` as its own step before the real deploy: it prints every chart's `helm` command, wave by wave, which is the record to read when a deploy stops partway. With `--concurrency` above 1 (or `deployment.helm.concurrency`), it prints the sequential plan, which leaves out the dependency steps a parallel deploy runs. `--retries 1` on the real deploy rides out the first-install race where a chart's CRDs are not yet visible to the next chart.

## Other ways to use shdctl

The recommended workflow above (Mode A) is one of several. The required step is always `shdctl release generate` (and usually `shdctl secret generate` alongside it); choose what you do with the output.

### Generate-only — bring your own CD (Mode C)

The minimum viable use of shdctl. shdctl renders charts and secrets from your manifest and the extracted release package; your existing CD tooling — `helm`, `kubectl`, Flux, a custom pipeline — handles everything downstream. shdctl never holds a kubeconfig and never touches your cluster.

```sh
shdctl release pull --clean-output
shdctl release generate --clean-output
shdctl secret generate --import secrets.import.yaml --use-defaults --output secrets.yaml
# Hand off generated-charts/ and secrets.yaml to your existing pipeline.
```

Refer to the [Mode C diagram](./architecture.md#mode-c--generate-only-\(bring-your-own-cd\)) for the trust boundary.

### Air-gapped (Mode B)

Mirror Unity's release artifacts into your registry once from a bastion, then run any of the modes above with no internet access at deploy time — for a Helm deploy, from a host with no Helm chart repository registered ([What shdctl touches](./architecture.md#what-shdctl-touches)).

```sh
# On the bastion (one-time per release):
shdctl release pull --clean-output
shdctl artifact sync preflight                         # proves the credentials
shdctl artifact sync images --skip-existing --cleanup  # only what your features require
shdctl artifact sync oras
shdctl artifact sync verify                            # reports anything still missing
# Then continue with generate / deploy from inside your network.
```

The mirror covers what your manifest's enabled features require, so re-run `shdctl artifact sync images` after changing a feature toggle, or mirror the full set with `--all`. `shdctl artifact sync verify` compares a registry against a manifest and exits non-zero when a required image is absent; run it before every deploy from a mirrored registry. Note the order: `preflight` proves your credentials and `verify` reports content, and a `verify` run against a registry you cannot authenticate to reports everything as missing. [Verify the target registry](./commands/artifact.md#verify-the-target-registry) explains how to read a report.

Refer to the [Mode B diagram](./architecture.md#mode-b--air-gapped-mirror).

### Direct Helm deploy

shdctl runs `helm upgrade --install` directly. Suited to operators who already manage Helm releases by hand and have a kubeconfig with cluster-wide deploy rights. No separate architecture diagram — this is a thin wrapper around `helm upgrade --install`; review the Helm flags in [Generate and deploy charts](./commands/release.md#generate-and-deploy-charts).

```sh
shdctl release pull --clean-output
shdctl release generate --clean-output
shdctl release deploy --format helm
```

Pass `--dry-run` first to print every `helm` command, wave by wave, without contacting the cluster. The deploy does not create the namespace: it has to exist first.

For faster deployments, set `--concurrency N` on `shdctl release deploy` (or `deployment.helm.concurrency: N` in your manifest). Charts within the same wave deploy in parallel, largest first; waves themselves remain sequential. Per-chart helm output is buffered and printed when each chart finishes (`==> [chart-name] done in 12.3s`), so line ordering will not match sequential mode. A summary line at the end of every wave and at the end of deploy reports total / succeeded / failed counts and elapsed time. Helm format only. [Generate and deploy charts](./commands/release.md#generate-and-deploy-charts) lists every flag.

### Minimum requirement

Whichever mode you pick, `shdctl release generate` is the one command that must run. It is what turns your `manifest.yaml` into Helm chart values for the rest of the pipeline.
