Rendering
konfig build <env> turns the default export of an env entry file into a directory of YAML files. This page follows that path and names the pieces you can rely on when reading diffs, debugging a failed render, or wiring CI.
From env file to files
- The CLI finds
konfig.jsonby walking up from the working directory and decodes it strictly. It then resolves the entry file for the env:<root>/<envs.<env>.entry>, or<root>/env/<env>.tswhen the env is not listed. From the env name and the--cluster,--k8s-version, and--flag k=vflags it builds the render context. Errors at this step:ConfigNotFound,ConfigParseError,EnvEntryNotFound. - It imports the entry module. The default export must be an Effect: an
AppOfApps.fromModules({ ... })orBundle.fromModules({ ... })program. A missing default export, a non-Effect, or an import failure fails withEnvLoadError { entry, cause }. - It runs that Effect. The result is a list of Applications (or Bundles), each carrying its
manifestsarray. - It renders every manifest. Each manifest’s
renderis called with the context; concurrency is unbounded within an app, four apps at a time. Results are flattened: typed objects, arrays, and raw YAML blobs are all accepted, and raw YAML is split into documents. Values without a stringkindandmetadata.nameare dropped silently. - It assigns file names. Each document becomes one file at
<outDir>/<env>[/<cluster>]/<app>/<Kind>-<name>.yaml, whereoutDiris<root>/<outDir.manifests>fromkonfig.json. The Argo CD Application CR for each app is written to<outDir>/<env>[/<cluster>]/<apps-name>/Application-<app>.yaml;apps-nameis thenamegiven toAppOfApps.fromModules, defaultapps. Bundles emit no CR. - It writes atomically. Files are staged under
<outDir>/<env>[/<cluster>].tmpwith mode0600. The previous live directory is then removed and the staged tree renamed into its place, so a build that dies while rendering never touches the previous output.
validate and diff run steps 1 to 4 in memory and skip the write.
For scripts outside the CLI, @konfig.ts/core exports render(program, { env?, layers?, runMain? }). You pass a function from the render context to an Effect; it builds a context for env (default "prod"), provides the Node services (merged with any layers you pass), scopes the Effect, and runs it with Effect’s Node runtime.
Stable YAML
All output goes through Yaml.serialize, so two renders of the same input are byte-identical and diffs show semantic changes only:
- Root keys are ordered
apiVersion,kind,metadata,spec,status, then everything else alphabetically. Insidemetadata:name,namespace,labels,annotations, then the rest alphabetically. Every other object is sorted alphabetically. nullandundefinedvalues are dropped.- Two-space indent, indented sequences, no line wrapping, plain scalars where possible.
- YAML 1.1 semantics, so strings such as
"yes","no","on","off"are quoted and are not read back as booleans by go-yaml based tools. - CRLF is normalised and the file ends with a single newline.
Yaml.filenameFor({ kind, metadata: { name } }) produces <Kind>-<name>.yaml with . and / in either part replaced by -. It throws if kind or metadata.name is missing; validate or decode first.
Structural diff
konfig diff <env> renders in memory and compares the result against a committed baseline, so you can see what a change would do before writing anything. The baseline is <root>/<diff.baseline>/<env>/ (DiffBaselineMissing when konfig.json has no diff block); files are matched by relative path, and only .yaml files in the baseline are read. The comparison (diffFiles({ left, right }) in @konfig.ts/core) is structural rather than textual:
- Both sides are parsed as multi-document YAML. When a file holds more than one document on either side, documents are keyed by kind, namespace, and name, so reordering documents inside a file is not a change.
- Values are normalised before comparison: key order is irrelevant and
null/undefinedentries are dropped. - Helm bookkeeping is ignored: the
helm.sh/chartlabel,app.kubernetes.io/managed-by: Helm, and themeta.helm.sh/release-name/meta.helm.sh/release-namespaceannotations. - Every value under
data/stringDataof akind: Secretdocument is replaced by<redacted>, so a rotation shows as unchanged and no plaintext reaches the terminal. Adding or removing a key still shows. - Each file (and each document inside a multi-doc file) is reported as
Same,MissingLeft,MissingRight, orChanged.--format summary|detail|jsonpicks the output (defaultsummary); any difference exits non-zero (DiffNonEmpty).
Build cache
konfig build skips the render entirely when nothing that feeds it has changed. Before rendering it computes a SHA-256 hash over the decoded konfig.json, the render-context signature (cluster, k8sVersion, sorted flags), the env entry file, every file under root (skipping node_modules, dist, and .konfig), and any extra cacheInclude files, directories, or globs (relative to the konfig.json directory).
The hash is conservative in one direction only. Touching any file under root invalidates it, so unnecessary re-renders are possible. The reverse is not guaranteed: files outside root that feed the render (a shared contracts package, for example) are only hashed when listed in cacheInclude; without that, a change there can be served a stale hit.
The cache entry lives at <configDir>/.konfig/cache/<env>-<ctx digest>.json and records the input hash, an output hash, the absolute output directory, the file count, and a timestamp (inputHash, outputHash, outDirAbs, fileCount, timestamp). A hit is honoured only if inputHash and outDirAbs match and the on-disk output tree still hashes to outputHash; an out-of-band edit or deletion is a miss. --no-cache skips the check, forces a render, and does not write a new entry. A cache hit prints Cached with the env name, file count, and output directory instead of render timings (cached: true and zero timings in --log json).
Validation levels
konfig validate <env> renders in memory and checks every file structurally, without a cluster. The YAML must parse, and each document must have apiVersion, kind, and a valid metadata.name; labels and annotations must be string maps. Name rules follow Kubernetes:
| Field | Rule |
|---|---|
metadata.name (most kinds) | RFC 1123 subdomain: lowercase alphanumerics, dashes, and dots; max 253 chars |
metadata.name of Namespace and Service | RFC 1123 label: no dots, max 63 chars |
metadata.namespace (when present) | RFC 1123 label, regardless of kind |
Any issue fails with StructuralValidationFailed { env, issueCount }.
Deeper, schema-level validation is left to --strict. It stages the same in-memory render into a scratch directory, runs kubeconform -summary -strict over it, and decides pass or fail from the exit code (KubeconformReportError on non-zero); it no longer depends on a prior konfig build. kubeconform must be on PATH (KubeconformNotFound otherwise). --ignore-missing-schemas is forwarded as -ignore-missing-schemas for CRDs without published schemas, and --k8s-version is forwarded as -kubernetes-version <ver>.
Errors
Every manifest’s render fails with AnyRenderError, a tagged union you can Effect.catchTag on:
| Tag | Raised by |
|---|---|
RenderError | General render failures: secret sources, sops verification, sealed-secrets, missing backend source, and anything a builder cannot type more precisely. Carries message and optional cause. |
EmbedYamlReadError | Manifest.embedYaml({ path }) when the file cannot be read. Carries path and cause. |
BoundaryDecodeError | Schema decode failures at a boundary (boundary({ schema, label? })), for example a malformed SopsSecret document. Carries schema (the label, default "boundary") and cause. |
HelmVersionTooLow | Helm.release({ minVersion }), konfig helm fetch, or konfig crd when the helm binary is older than required or missing. Carries required and found. |
HelmRenderError | Every other Helm.release failure (pull, template, digest); wraps HelmDigestMismatch and process errors as cause. Carries chart, version, cause. |
HelmDigestMismatch | Chart tarball hash differs from digest; carries chart, version, expected, actual. Reaches you as the cause of a HelmRenderError. |
CrdExtractError | konfig crd extract / verify failures for one chart; carries chart and cause. |
Logging flags
konfig build accepts --log text|json (default text) to switch the final report line between prose and a JSON object with env, files, outDir, renderMs, writeMs, totalMs, and cached. In text mode a Rendering env '<env>'... line precedes the render. --verbose wraps the render in an Effect span named konfig.render.<env> so tracing output includes it.