Skip to content

CI: provision from manifest.scm via Guix#1

Merged
polyedre merged 15 commits into
mainfrom
ci-guix
Jul 3, 2026
Merged

CI: provision from manifest.scm via Guix#1
polyedre merged 15 commits into
mainfrom
ci-guix

Conversation

@polyedre

Copy link
Copy Markdown
Owner

What

Adds GitHub Actions CI and fixes the dependency provisioning so it can actually build.

  • CI workflow (.github/workflows/ci.yml) — on every push to main and every PR: install Guix, then run make build test test-examples inside guix shell -m manifest.scm.
  • manifest.scm — now declares the real dependencies: guile, guile-json (the (json) module), guile-libyaml (the (yaml) module), jq. It's the single source of truth; CI and dev provision from it identically.
  • test/examples.sh + make test-examples — render each standalone example through its own renderer and assert it exits 0 (smoke, not golden).
  • README — documents the Guile-library deps and the guix shell -m manifest.scm install path; FAQ no longer says "no CI yet".

Why Guix and not apt

The first apt-based attempt went red twice: (hexol k8s)/cmdb import (json) (guile-json) and (hexol ansible) imports (yaml) (guile-libyaml). guile-libyaml is FFI-based (nyacc + guile-bytestructures + libyaml) and isn't packaged for Ubuntu, so building it from apt is a fragile multi-package chain. hexol is already a Guix project (.envrc runs use_guix), so provisioning CI from manifest.scm is reproducible and matches the dev environment.

Verification

Locally, guix shell -m manifest.scm -- make build test test-examples is fully green (kernel/surface/construct/k8s/cmdb tests + all six example renders).

Note: main is currently red — the earlier apt-based CI commits landed there before we switched to PRs. Merging this fixes it.

🤖 Generated with Claude Code

polyedre added 15 commits June 20, 2026 10:58
The apt-based CI couldn't satisfy the build: (hexol k8s)/cmdb need
(json) from guile-json and (hexol ansible) needs (yaml) from
guile-libyaml, which isn't packaged for Ubuntu. hexol is a Guix
project anyway, so CI now installs Guix and runs the suite inside
`guix shell -m manifest.scm`, making the manifest the single source
of truth.

- manifest.scm: add guile-json + guile-libyaml (the real deps).
- ci.yml: install Guix, run make build/test/test-examples in the
  guix shell.
- README: document the Guile-library dependencies and the
  `guix shell -m manifest.scm` install path.

Verified locally: `guix shell -m manifest.scm -- make build test
test-examples` is fully green.
guix-install.sh blocks on a yes/no prompt that piped input can't
satisfy. Switch to the manual binary-tarball install from the Guix
manual: extract, create build users, start the daemon, authorize the
official substitute key, then run the suite in guix shell — all one
step so the backgrounded daemon survives.
examples/terraform.scm reads ~/.ssh/id_*.pub at render time; the
runner has none, so generate an ephemeral key (used only as rendered
data, never to connect).
examples/secrets.scm ships an inline (secrets-store …) sealed to an age
recipient plus its throwaway identity, so `hexol render -o yaml -i
examples/secrets.scm` decrypts and substitutes real plaintext on a fresh
clone (the secrets it guards are fake). A `fetch-age-key` hook documents
where the decryption key comes from and how to override it.

`hexol secret` now loads the inventory before shelling out to sops, so the
inventory's own key provisioning — e.g. (setenv "SOPS_AGE_KEY" …) — is
honored by the management verbs too, not just `render`.

Wire the example into the render smoke-test (it degrades to placeholders
where sops is absent, so CI stays green) and point the README at it.
Replace the file-writing terraform-outputter with two read-only CLI
actions in (hexol apply):

- terraform-output-reporter: `hexol output` mirrors `terraform output` —
  bare prints all outputs, a NAME positional prints that one with -raw
  (so `hexol output kubeconfig > deploy/kubeconfig` lands the file),
  --json for machine-readable. No files written by hexol itself.
- terraform-validator: `hexol validate` writes the rendered config,
  `tofu init -backend=false` then `tofu validate` — checks exactly what
  apply would push, touching no infra/state/creds.

Factor config-writing into write-terraform-config, shared by the applier
and validator. Wire both actions into examples/homelab.scm.
The footer re-explained every verb's behavior in prose, pushing the
verb/inventory-action list — the part --help is actually used for — far
down. Keep only the notes that don't fit a synopsis line: where the
inventory comes from and the global --color flag.
Inline secrets no longer need a hand-written id. `(hx-secret "ENC[…]")` is
keyed by its path in the resolved state (e.g.
kubernetes_resources.app-secrets.stringData.DB_PASSWORD); `(hx-secret 'id
"ENC")` still pins an explicit, rename-safe id, and `(secret-ref 'id)`
references one. resolve-secret-refs walks the state tracking the path, gathers
each marker's ciphertext keyed by id-or-path, decrypts the shared envelope once,
and substitutes. The inline registry is gone — the marker carries its cipher.

`hexol secret` now folds the inventory (decryption disabled via the
secret-resolution-disabled parameter) and runs the same path walk, so seal and
decrypt agree on keys; ciphertext byte-search still locates each splice site.
Verbs name secrets by path or id.

set/edit/rm/edit-all now seal to the recipients already in the envelope
(sops -e --age/--pgp), so they need no .sops.yaml and never silently switch a
store's recipients; rekey still rotates via the .sops.yaml creation rule. This
makes examples/secrets.scm self-managing in-repo without an age creation rule.

examples/secrets.scm uses the no-symbol form.
cache.scm carried a byte-for-byte copy of kernel's FNV-1a/64 hash and
its u64 constants (its own comment noted it 'mirrors (hexol kernel)'s
addresser'). Export fnv1a-64 from the kernel and import it; cache keeps
only the 3-line hex16 wrapper.
deep-merge-with and its merge-by-key helper (replace / append /
replace-by-key strategies) had no caller outside their own test — every
real merge goes through plain deep-merge. Collapse deep-merge to the
self-contained no-strategy recursion and delete the strategy machinery,
its exports, and its test block.
op:copy / op:move / op:delete and their hx-copy / hx-move / hx-delete
surface macros had no caller in any example, library, or inventory —
only their own tests. Remove the ops, the %copy/%move/%delete macros,
the exports, and the tests. state-delete stays as a state primitive
(symmetric with state-set/state-append).
Every typed constructor paired a %name define* (full #:key list) with a
define-construct re-declaring the same fields, whose #:build only
forwarded them — each field written three times. For the 15 helpers
called by nothing but their own construct, fold the body straight into
#:build and delete the helper. The 7 helpers shared by composites
(%deployment, %ingress, %custom-resource, %namespace, %service-account,
%cluster-role, %cluster-role-binding) stay: they're the runtime-keyword
implementation the macro surface can't forward into (a #:map field can't
splice a runtime alist). No output change — golden renders and the test
suite are byte-identical.
state-diff carried map-alist?, a near-copy of (hexol yaml) object-shape?
differing only by the distinct-key check. Resolved state never has
duplicate keys (state-set/deep-merge always replace), so the two agree
on every input state-diff sees; drop map-alist? and call object-shape?
(already imported, already used by sequence-of-maps?). show/explain
output byte-identical.
show's source s-expression was run through a hand-rolled, string-aware
Scheme tinter (token-end + tint-atom + highlight-sexp, ~75 lines) purely
to color a debug view. pretty-print already lays the form out; print it
indented and uncolored. Tree labels, the hash gutter, and the red/green
state delta keep their color. Non-TTY output (and the golden renders)
unchanged.
The CLI's JSON renderer lived in the CMDB namespace, forcing bin/hexol
to depend on cmdb just for sexp->json-string. Move it to (hexol json),
reusing (hexol yaml) object-shape? instead of a third local copy, and
repoint bin/hexol, cmdb/server, and the build target. Render output
identical.
The event-sourced CMDB (HTTP fact-log server, versioned-library replay,
waved rollout tooling) was a second product beside the inventory engine
that the hexol CLI never invoked — its only tie, the JSON adapter, moved
to (hexol json) in the previous commit. Remove cmdb/, bin/cmdb-server,
bin/sync-inventory, bin/promote, docs/cmdb.md, the two cmdb tests, the
cmdb-only examples/regions.scm, and the Makefile test lines; scrub the
README/model.md references. (This also subsumes the v1/v2 library
duplication finding.)

docs/authoring.md still lists the cmdb layout but is mid-edit in the
working tree, so its scrub is left to that change.
@polyedre
polyedre merged commit fc804d8 into main Jul 3, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant