[!WARNING] Not a supported Solace product.
solace-utilwas created by Solace Professional Services and is supported only by Solace Professional Services – not by Solace Support. For help with this tool, contact your Solace Professional Services representative rather than opening a Solace Support case. This notice covers this tool only, not the Solace PubSub+ Event Broker or the EventBroker Operator that it deploys and operates.
Building, testing, and releasing solace-util. For using the tool see
README.md; for the conventions the code follows see
CLAUDE.md.
Building from source needs Go 1.27 or later (Toolchain pin). Quick local build:
go build -o solace-util .
Through the dev scripts, which is what CI and the release pipeline use:
scripts/dev.ps1 build # Windows -- the host's target
./scripts/dev.sh build # Linux/macOS
build emits dist/solace-util-<os>-<arch> (Windows gets a .exe suffix). TARGET_OS and
TARGET_ARCH pick the target; unset means the host. dist cross-compiles all four release
targets – linux/amd64, linux/arm64, darwin/arm64, windows/amd64 – in one go. Both build
with CGO_ENABLED=0 -trimpath -ldflags '-s -w -X solace/internal/cli.version=<version>'.
<version> is git describe --tags --dirty --always: on a tag push HEAD is exactly
the pushed tag, so a release binary reports e.g. v1.2.3, matching the GitHub
release; a local build between tags reports a pseudo-version like
v1.2.3-5-gabc1234-dirty. The quick local go build above stamps nothing and reports
dev. solace-util version prints that value plus the Go toolchain and the OS/arch the
binary was built for.
scripts/dev.ps1 and scripts/dev.sh are behaviourally identical and own every
build/test/scan command. The workflows call task names only, so local runs match CI:
| Task | Does |
|---|---|
tidy |
go mod tidy |
vet |
go vet ./... |
build |
Compile -> dist/solace-util-<os>-<arch>[.exe]. See Build above |
test |
go test -count=1 ./... (race on by default on dev.sh; opt-in on dev.ps1 with SOLACE_RACE=1). A race run defaults GORACE to atexit_sleep_ms=0 when it is unset, since the race detector’s 1s pause before every clean exit – every test binary, and every helper child internal/engine re-runs – was most of the wall time |
regen |
Rewrite the committed goldens – see Goldens. Deliberately outside all/full, since a gate must not rewrite what it compares against |
cov |
Coverage profile -> coverage/coverage.html + .out, prints the total. Never races, on either script: test is the race run, so cov does not repeat it just to collect coverage, and the total does not depend on the cover mode |
scan |
go tool govulncheck -format json (version pinned in go.mod/go.sum), judged by internal/tools/vulnjudge – fatal on a fixable vulnerability this module calls, warns and passes on one with no released fix. Raw stream kept at scripts/logs/scan.json |
dist |
Local convenience: cross-compile all four release targets into dist/. Also accepted as binaries |
graphify |
Refresh graphify-out/. Local only; skipped when CI is set |
all |
build vet test – the fast inner loop; CI runs all scan |
full |
all + cov scan graphify – the pre-tag sweep |
Run the local gate with scripts/dev.ps1 all scan (or ./scripts/dev.sh all scan), and
full before tagging. Per-task logs land in scripts/logs/<task>.log, each closing with a
<timestamp> | <task> | <duration>s | OK|FAILED footer; coverage HTML in
coverage/coverage.html.
Current test coverage is 93.4% as of 2026-10-02, recorded in scripts/logs/cov.log. The
previous total is the local floor: if a change lands lower, either add the missing tests or
say in the change which number moved and why (deleting dead code or a weak test can
legitimately lower it). This is a local mechanism – a CI runner is a fresh checkout with no
prior cov log, so the pipeline cannot catch a coverage regression and is not expected to.
test.md carries the per-package breakdown behind this total – update both in the
same change so they cannot drift apart.
Several committed files are generated and byte-compared by the test suite, so test fails
while any of them is stale. regen is the one task that rewrites them:
| Golden | Generated by |
|---|---|
| commands.md | TestCommandDocs in internal/cli/commanddoc_test.go, from the live cobra tree |
| abbreviation.md | TestAbbreviationDocs in internal/cli/abbrevdoc_test.go, from the three abbrev sets plus pflag’s shorthands. Shares the same -update flag, so one regen rewrites both |
| import.md | TestImportDocs in internal/broker/importdoc_test.go, from the section classification in internal/broker/sections.go |
internal/render/testdata/*.golden |
the render package’s manifest tests |
internal/k8s/testdata/*.golden |
the k8s package’s manifest tests |
internal/convert/testdata/*.golden |
the convert package’s conversion tests |
| ../env/sample.yaml | TestSampleYAMLMatchesTheFullExample in internal/examples/examples_test.go, from internal/examples/assets/full.yaml – the template a bare solace-util examples prints. regen runs this package first, because the render and k8s goldens are rendered from the sample it rewrites |
the GEN block of ../solace-yaml-generator.html |
TestGeneratorPageEmbedsTheCLI in internal/k8s/generatorpage_test.go, from the embedded operator bundle and config.ApplyDefaults. Only the block between its BEGIN GENERATED and END GENERATED lines is rewritten; the rest of the page is hand-written – see The env-file generator page |
regen walks ./internal/examples ./internal/cli ./internal/convert ./internal/render
./internal/k8s ./internal/broker in that order (both scripts’ task_regen/Task-regen).
Only the first position is load-bearing, for the reason in the row above; ./internal/broker
is last because it was added last, not because anything downstream depends on it – unlike
env/sample.yaml, docs/import.md is generated straight from sections.go, so nothing else’s
golden is rendered from it.
Never hand-edit one. A stale-golden failure names the file and the first differing line;
the fix is regen, then review the diff before committing it. Any command, flag, Short
or Long change therefore means regenerating commands.md in the same change.
solace-yaml-generator.html is one self-contained page,
opened from disk in a browser. Its form covers every env-file key; it writes the env file,
lists what config.Load would refuse, and previews the two Kubernetes artifacts:
solace-util operator generate and solace-util broker generate. For anything the form can
express, both previews are meant to be byte-identical to what the CLI renders from the env
file shown. A value given as an *Env reference shows as <value of $NAME>, since only the
CLI reads the variable.
Two halves, kept in step differently:
GEN block. TestGeneratorPageEmbedsTheCLI fails while that block is
stale – a bundle bump, a changed default – and regen rewrites it. The test also fails
when the bundle gains a template action the page’s renderer does not implement, which
regen cannot fix: extend renderOperatorTemplate, then the test’s pageOperatorActions.render.BrokerCR, k8s.GenSecrets/GenBroker and k8s.GenOperator. Nothing in the
suite runs the JavaScript, so a change to any of those Go renderers or to the env-file
schema needs the matching edit in the page, and this cross-check before it ships.The cross-check. Open the page, set up a scenario, download env.yaml, then:
solace-util operator generate -e env.yaml --platform kubernetes > operator.cli.yaml
solace-util broker generate -e env.yaml --platform kubernetes > broker.cli.yaml
diff operator.cli.yaml operator.yaml # the page's Download from each panel
diff broker.cli.yaml broker.yaml
solace-util broker generate -e env.yaml --platform docker # or podman: must load
Both diffs must be empty, using literal secrets and the certificate files picked in the TLS
section. Run it for the form’s defaults (no service ports, so neither CR carries ports:); an
explicit port list with a service port that differs from its container port and a /UDP
entry; HA with TLS files, registry credentials, additional
users, a monitor password and a PSK; existing claims with pod metadata and node and pod
affinity; replication with each kind of via; and a rootless podman file.
test.md catalogues every test in the repo – what each one proves, the per-package fixtures and doubles to reuse, and the injectable seams. Read it before adding a test, and update it in the same change when you add or remove one.
That suite runs with no cluster, no container engine and no broker, which is what keeps it
fast – and also what leaves a handful of comments in the code marked ASSUMED, NOT VERIFIED
or NEEDS VERIFICATION. Each one names what is assumed and what would settle it; they are
checked by hand against real infrastructure, since the behaviour in question belongs to a
container engine or a live broker rather than to this code.
The Go toolchain is pinned by the toolchain line in go.mod, not just the go
line: go 1.27 is a minimum, so a machine with an older Go would otherwise build against
the oldest 1.27 patch and ship its unpatched standard library. Both dev scripts export
GOTOOLCHAIN from that line unless you set it yourself, so an exported GOTOOLCHAIN=local
cannot quietly bypass the pin. scan reports standard library vulnerabilities like any
other – when it does, raise that toolchain line to the release that fixes them and
re-run the gate.
.github/workflows/tag.yml is the only automatic pipeline:
pushing a v* tag runs the gates on Ubuntu and Windows, cross-compiles the targets in the
BUILD_TARGETS repo variable, and creates a GitHub release with the binaries and
SHA256SUMS.txt – only if everything passed.
.github/workflows/ci.yml runs the gates and is reused by the
tag pipeline; it does not run on ordinary pushes or PRs (only on workflow_dispatch and
PRs that touch the workflows or the dev scripts), so full on a clean checkout before
tagging is what catches a file you never committed.
./scripts/dev.sh full # clean checkout, everything green
git tag v0.1.0 && git push origin v0.1.0
| Path | Contents |
|---|---|
main.go |
Entry point (cli.Execute()). |
internal/abbrev |
The rules every approved short form obeys, shared by the command, role and --platform sets. Stdlib-only leaf. |
internal/config |
Env-file schema, loading, defaults, validation, role helpers. |
internal/engine |
External-command runner (real exec; an echo variant used as a test seam; secrets never echoed). |
internal/render |
Templating for the broker CR, operator bundle, compose/run/quadlet artifacts. |
internal/broker |
Platform-agnostic config/verify operations over an injected transport. |
internal/k8s |
Kubernetes cluster/operator operations and the kubectl transport. |
internal/container |
Docker/Podman host operations (Manager) and the node-local <runtime> exec/cp transport. |
internal/convert |
Legacy bash env -> YAML converter behind solace-util convert. |
internal/output |
The one package that owns every stdout/stderr shape (tags, sections, tables). |
internal/cli |
Cobra command tree and handlers. |
internal/tools/vulnjudge |
Dev-only judge the scan task pipes govulncheck JSON through. |
internal/examples |
The embedded env-file templates examples prints, and the generator for env/sample.yaml. |
env/ |
Config files. sample.yaml is generated – edit internal/examples/assets/full.yaml. |
solace-yaml-generator.html |
The browser env-file generator; its GEN block is generated (see The env-file generator page). |
docs/ |
commands.md – generated CLI reference; abbreviation.md – generated glossary of every short form; configuration.md – the env file; operations.md – day-2 procedures; developer.md – this file; test.md – the catalogue of every test. |
scripts/ |
dev.ps1 / dev.sh developer tooling. |
graphify-out/ |
Persistent knowledge graph of the repo. Rebuild with the graphify task. |