solace-cnt-scripts

Developer guide

[!WARNING] Not a supported Solace product. solace-util was 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.

Build

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.

Dev script tasks

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

Gates

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.

Goldens

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.

The env-file generator page

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:

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.

Tests

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.

Toolchain pin

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.

Releases

.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

Repository layout

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.