You need Go (the version in go.mod, or later) and just.
Nothing else: the linters run at pinned versions through go run, and are never added to
go.mod.
just check # gofmt, go vet, the tests with coverage: what every change must pass
just ci # all of CI's checks: the above, staticcheck and govulncheck
just fmt # format every Go file
just test-one ./internal/core/network # one package's tests, verbosely
just coverage # what the tests leave uncovered, as a page to open
just record-json # record the JSON shapes again, after a change that is meant
just run graph whoami # build and run ldo-go from the working tree
just install # put the working tree's ldo-go on PATH, in ~/go/bin
just dist 0.1.0 # the binaries for every platform a release carries, in dist/
just secrets # the secret scan, uncommitted changes included (needs gitleaks)Run just for the full list. See CONTRIBUTING.md before opening a pull
request, and AI.md for the conventions, which hold for people and AI coding
assistants alike.
cmd/ldo-go/ main: signals, then cli.Execute
internal/core/ vendor-neutral: errors, config, brand, the HTTP client, the network
rules and probe, the token store, polling, inputs and workbooks,
row filters, logging, the YAML and JSON writers, Markdown, render,
colour, sorting, text files, the browser
internal/microsoft/ the shared Microsoft layer (clouds, profiles, tokens, API clients),
with identity/ (every sign-in) and azcli/ (the Azure CLI) as helpers
internal/microsoft/*/ one package per API: entra, xdr, graph, azure, pim, logicapps, ...
internal/servicenow/ the ServiceNow layer (config, OAuth, the Table API), with instance/
internal/atlassian/ the Atlassian layer (config, API tokens), with jira/ and confluence/
internal/terraform/ modules as files (the HCL splitter, the tools), with sort/ and docs/
internal/cli/ the command: parses, calls a client, renders
internal/fakes/ the test fakes, one package per concern
internal/project/ tests of the project itself: layering, docs, dashes
internal/project/layering_test.go holds the layering: core imports nothing of ours but
core, a feature package only core and its vendor's layer (a composite, devices, also
the features it combines), and only cli prints or exits. It also fails for a package with
no tests.
Every package has its tests beside it. They never touch the network, a real az or a real
clock, so an hour-long watch runs in microseconds:
| Fake | Stands in for |
|---|---|
httpfake |
every HTTP call: a transport that answers from routes, and records each request |
azfake |
the Azure CLI: its accounts, and any command's answer |
clockfake |
the clock and sleeps, so a watch moves on at once |
tokenfake |
a token provider, with the claims a test gives |
graphfake, xdrfake, armfake |
Graph's, Defender's and Resource Manager's records |
snowfake |
a ServiceNow instance: the Table API, OAuth's grants and PKCE, and a person approving a sign-in |
atlassianfake |
a Jira and Confluence site, paged as the real ones page |
terraformfake |
a module on disk, and terraform fmt and terraform-docs |
certfake |
certificate authorities and the certificates they sign, for the TLS tests |
workbookfake |
Excel workbooks built by hand |
CLI tests run through the harness in internal/cli/harness_test.go: newHarness(t, handler), then h.ok(args...) for a command that must succeed, h.fails(code, args...)
for one that must not, contains to check output, and usageError for a usage message. It
runs in UTC, and each run starts afresh, as a new process would.
internal/cli/json_output_test.go runs every command with -o json and compares the shape
of what it writes (each key, and the kind of value under it) with the record in
internal/cli/testdata/json-output.json. Scripts read that JSON, so a key renamed or
dropped fails there. A change that is meant goes in the changelog, then into the record with
just record-json. A new command needs a case there.
The docs are tested too: every ldo-go example in the README, AI.md and docs/ runs with
--help, every just example is a recipe, every relative link and heading it names is
there, and no file holds an em or en dash.
The Python ldo is the reference, and a difference is a bug unless
How it differs says why. Where the Python has a golden output (KQL, YAML,
detection exports, analyzer reports, Message Center tasks, Logic App rewrites), the Go test
compares with it byte for byte, from testdata/. For the rest, run both side by side, with
the same config and inputs:
diff <(ldo-go xdr timeline web01 --since 1h --show-query) <(ldo xdr timeline web01 --since 1h --show-query)
diff <(ldo-go terraform sort ./module-a --check -o csv) <(ldo terraform sort ./module-b --check -o csv)and compare the JSON shapes: internal/cli/testdata/json-output.json against the Python's
tests/project/json_output.json.
The fakes cannot know everything a real tenant returns, so before a release, run the build
against one. ldo-go self-test (hidden from --help) runs every read-only command against
names you give, throws their output away, and reports what happened to each:
ldo-go self-test --device web01.corp.example --user ana@corp.example --group "Linux servers" --report self-test.json
ldo-go self-test --device web01.corp.example --only xdr --only devices
ldo-go self-test --device web01.corp.example --all
ldo-go self-test --workspace law-soc --vault kv-app --only logs --only keyvault --snow --atlassianEach case that needs a name (--device, --user, --group, --workspace, --vault,
--snow, --atlassian) runs only when it is given, and a note counts the ones left out. It
reaches only what you name: give --vault a vault you can read.
| Result | Means |
|---|---|
| ok | it worked |
| attention | it exited 3, having found something, as designed |
| refused | it stopped with an error it explained: usually a permission or scope the sign-in lacks |
| usage | it rejected its arguments: a bug in the test or the command |
| CRASH | it panicked: a bug, with the lines of ldo-go it came through |
It exits 1 on a crash or a usage error. Nothing it runs changes anything, and it prints no
token; use a profile that is already signed in, since a sign-in cannot be answered while it
runs. Run the Python ldo self-test with the same names, and compare the two reports'
results: every command should end the same way in both. Rename anything from the tenant
before sharing a report.
| Workflow | What it does |
|---|---|
ci.yml |
gitleaks over every commit; just ci on Linux (gofmt, go vet, the tests with the coverage floor, staticcheck, govulncheck); the tests on macOS and Windows; the binaries for every platform, smoke tested and kept as the run's dist artifact |
release.yml |
on a v* tag: ci.yml as the gate, then the GitHub release of the binaries it built, with SHA256SUMS and signed build provenance |
codeql.yml |
CodeQL on the Go code and the workflows, on every change and weekly |
dependabot.yml |
weekly updates to the Go modules and the actions |
The third-party action (setup-just) is pinned to a commit, and gitleaks to a checksum.
On GitHub, turn on private vulnerability reporting (Settings, Security), Dependabot alerts,
and protect main so the ci.yml jobs must pass before a merge.
- Turn
## UnreleasedinCHANGELOG.mdinto## <version>(a new, empty## Unreleasedabove it), merge tomain, and let CI pass. - Run
ldo-go self-testin a real tenant, from thedistartifact of that CI run orjust build. just release <version>: it checks the tree is clean and in step withorigin/main, that the changelog has the version and the tag is new, then tags and pushes.
The release workflow runs CI again as its gate, checks the tag, the changelog and the
binaries' own --version agree, and only then creates the release: the six binaries (Linux,
macOS and Windows, for amd64 and arm64), SHA256SUMS, and the changelog's section as its
notes. The binaries carry signed build provenance, which anyone can check:
gh attestation verify ldo-go-linux-amd64 --repo libre-devops/ldo-go-cli
sha256sum --check --ignore-missing SHA256SUMSA pre-release is the same with a version such as 0.2.0-rc.1: it is marked as one, so it
never becomes the latest. The version comes from the tag, set into the binaries at build
time; go install github.com/libre-devops/ldo-go-cli/cmd/ldo-go@v0.1.0 knows it from the
module instead. Never move or reuse a released tag.