What to build next, in order. A step is done when its result was observed (a push read back with another tool, a mount in a cluster), not only when a test passes; see AGENTS.md. Phases 3 and 5 are outlines, and each needs its own design page before it is built.
- The format is format-v2.md, and its known answers (§11) are the acceptance test. Change a byte only with a new layout version.
- One public Go package,
pkg/settingsimage, for everything a producer needs: render, build, provenance, push. The CLI is a thin layer over it. Other projects import it, so its API is the one thing to design with care.internal/is the CLI's plumbing. - Dependencies:
oras.land/oras-go/v2,github.com/opencontainers/image-spec,github.com/opencontainers/go-digest, andgo.yaml.in/yaml/v3(its nodes show duplicate keys). No client-go, no Git library: the CLI shells out togit. - Layout v2 says what each file is.
resources/holds Kubernetes resources, read and checked;raw/holds anything, never read. ConfigMaps also become files unless annotated; a Secret must be SOPS-encrypted. Only a closed list of known files (a README, a licence, Git and SOPS settings) may sit beside the two folders. No index file: a reader listsresources/. - The Action is composite and builds the CLI from its own source, which is right for every ref, at 10–20 s.
- The repository is public, and changes are committed straight to
main.
- Put anything per-commit or per-run in the image: no time, no commit, no tool version. That breaks "same folder, same digest", and so every consumer's rollout.
- Change a byte of the format without a new layout version.
- Decrypt anything, hold a key, or log a credential or a settings value.
- Interpolate
${{ }}into arun:script in a workflow or inaction.yml; pass it throughenv:.
Each was observed as described; the commit messages have the details.
- Phase 1, the format package (2026-11-02): render (a directory or a tar archive), build,
provenance, tags and push. Both known answers match; images read back with
craneandorasfromregistry:2and zot (native referrers, no fallback tag). - 2a–2c, the CLI, the Action and its self-test (2026-11-02):
buildandpushas cli.md; a compositeaction.yml; a self-test workflow against a service registry (every run) and ghcr.io (main). - 2d, releases carry binaries (v0.1.0, 2026-11-02): reproducible binaries for linux/amd64,
linux/arm64 and darwin/arm64, a cosign-signed checksums file, SLSA provenance (from 2026-11-03,
when the repository went public), immutable releases, and a
v0tag that moves. A draft left by a failed run is finished withworkflow_dispatchandtag: vX.Y.Z. - Phase 4, SOPS-encrypted Secrets (2026-11-02): plaintext refused, ciphertext carried byte for
byte;
task test-mountdecrypts the mounted file with the test key. - 2f, known top-level files are skipped (2026-11-03): mounted in k3d; an edit to the README
alone gives
changed: false. - 2g, any registry (2026-11-03):
--registry-token-stdinand the Action'sregistry-token/oidc-audience, sent as Bearer to the registry's own origin only; exit 4 on 401/403;github.tokento ghcr.io only. Pushed to zot with bearer auth, read back withcrane; the Action's script run against zot with a fake OIDC endpoint. - 2h, a timestamp tag that never moves (2026-11-03): a re-run of an old commit moves
mainback, but the highest timestamp tag stays on the newest commit. - Hardening the package API (2026-11-03):
Build(Tree)checks every path itself ("/a"used to loop forever,"../a"went into the layer); a file is read only up to the size limit; an empty directory besideresources/andraw/, or an archive's directory entry at a file's path, is refused. - 2i, provenance names no author by default (2026-11-03): one image digest in all three modes;
oras pullof the referrer shows no author, a name, or both.
- 2e, the conformance corpus.
testdata/conformance/<case>/holds aninput/folder, the output ofsettings-image build --oci-layout, and anexpected.json(digest, tree, provenance or refusal). Cases: both known answers, one for every refusal, a nested tree, abinaryDataConfigMap, an annotated ConfigMap, araw/file, and a folder with skipped top-level files. A Go test rebuilds every case and fails on any byte of drift; readers in other languages test against this directory at a release tag.- Observe: the test passes, and fails when one input byte is changed.
- Flux, in a cluster. In k3d with Flux: two commits give two timestamp tags; image automation commits the newer digest into the Deployment and the pod mounts it; re-running the older workflow run changes nothing in the cluster or in Git. Then the README's Flux section loses its "not yet observed" note.
- 2g, the parts not observed yet: a real GitHub OIDC token from a workflow, against a zot that trusts GitHub's issuer; zot with a password file and bearer login on at once.
- The first release with no manual step (the next release-please PR):
gh attestation verifypasses on a downloaded binary, andgh release verifyconfirms the release is immutable. - A production example of consuming a SOPS-encrypted Secret: who decrypts, and where the key lives.
- The Action downloads a released binary with a
versioninput, instead of building it (github-action.md §3). Only when the 10–20 s build matters. - List the Action on the Marketplace (github-action.md §6):
a manual step on the next release's draft; the release workflow waits for it when the
releaseenvironment has a required reviewer.
Phase 2 is done when a workflow in another repository uses ConfigButler/settings-image@v0
and an image appears on a registry for each commit that touches its folder. Measure the
commit-to-image time; expect 30–60 s, mostly runner start-up.
The same package, run continuously in a cluster: a custom resource that names a Flux
GitRepository and a folder, and publishes each fetched revision that changes the folder. That
takes seconds instead of a runner's minute, and pushes to an in-cluster registry. The design page
must answer:
- the API group and kind;
- registry credentials (Flux's
OCIRepositoryauth fields are the model); - commit metadata (a depth-1 fetch of its own, or the
GitRepository's); - the hole it needs in Flux's NetworkPolicy, to download artifacts from source-controller;
- digest parity with the CLI for the same folder. Flux's source-controller drops some files by default (its ignore patterns), so a folder can differ between a checkout and a Flux artifact.
A reader library in another language pins a release of this repository and tests against
testdata/conformance/. A reader refuses an image whose ai.configbutler.settings-image.layout
it does not know. The first is a .NET library, in its own repository.
- A guard that refuses to move the branch tag to a commit that is not a descendant of the current one. It needs the current tag's provenance read back, and must handle reverts and force-pushes. The timestamp tag covers the need first.