Short `-n` node names silently returned nothing: `node-lookup -R -n ausyd1nxvm2120` found nothing while `-n ausyd1nxvm2120.main.unkin.net` worked, because the PuppetDB `certname` filter needs a FQDN. This auto-qualifies a dotless name before the lookup.
## Changes
- Add `qualifyNode()` pure helper: a dotless name gets `.<domain>` appended; a name already containing a dot (any domain, incl. `*.k8s.syd1.au.unkin.net`) is left unchanged; a single trailing dot is stripped first; empty input is preserved (same "no node" behavior as today).
- Apply normalization to the `-n` value and to stdin-sourced node names in `run()`, so both entry points behave consistently.
- Make the domain configurable: config key `domain`, `NODE_LOOKUP_DOMAIN` env var, and `--domain` flag, all defaulting to `main.unkin.net`.
- Surface `domain` in `config show` / `config init` output and document the new env var/flag/behavior in AGENTS.md.
- Add table-driven `qualifyNode` tests (short name appended, FQDN unchanged, multi-label other-domain FQDN unchanged, trailing-dot handling, empty input, custom domain) and a `NODE_LOOKUP_DOMAIN` env-override test.
Companion tools `pburl`/`pblastreport` take already-qualified hostnames (typically piped from `node-lookup`) via `puppet.ReadHosts` and do not share the `-n` code path, so they are intentionally left out to keep this PR atomic.
## Validation
- `gofmt -l .` clean, `go vet ./...` clean
- `go test -race ./...` pass
- `make build` builds all three binaries
Reviewed-on: #19
Co-authored-by: unkin-agent <unkin-agent@unkin.net>
Co-committed-by: unkin-agent <unkin-agent@unkin.net>
## Why
The `v0.5.4` release step failed with `Error: open node-lookup-linux-amd64: no such file or directory`, so the Gitea releases page carries **none** of the binaries. Root cause: at v0.5.4 the build loop's shell variables (`${name}`, `${pkg}`, `${osarch%/*}`) were unescaped, so Woodpecker substituted them to empty at YAML-parse time and no cross-compiled binaries were produced. The same blanking left the release `--note` empty (`git log "..v0.5.4"`).
The build-loop escaping was already fixed in #16. This PR fixes the two remaining release-step defects and enriches the release assets.
## Changes
- Replace the `git describe --tags --abbrev=0 HEAD^` changelog anchor with a previous-tag scan that skips tags on the current commit and picks the newest semver **ancestor** tag. Several tags point at the same commit (v0.5.3 and v0.5.4 both on `f296056`), so `describe HEAD^` jumps the range back to v0.5.1; the scan correctly selects v0.5.2.
- Attach the packaged RPM (from `dist/`) and a generated `sha256sums.txt` alongside the 12 cross-compiled binaries.
- Build the asset list once and checksum exactly what is uploaded.
Keeps `serviceAccountName: default` and the k8s resource requests/limits on the release step unchanged.
## Validation
- `check-yaml` / `trailing-whitespace` pre-commit hooks pass.
- Local dry-run of the exact release-step shell logic (with `$$`→`$`) from the worktree: `PREV_TAG=v0.5.2`, non-empty notes, and all 13 assets (12 binaries + RPM) plus `sha256sums.txt` resolve on disk.
---------
Co-authored-by: Ben Vincent <neotheo@gmail.com>
Reviewed-on: #18
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
## Why
The **v0.5.3** release pipeline failed at the asset-upload step with `open node-lookup-linux-amd64: no such file or directory`, and release notes came out empty. Woodpecker substitutes `${...}` expressions in `commands` **at parse time**, treating them as pipeline variables. The build loop's shell parameter expansions (`${name}`, `${osarch%/*}`, `${pkg}`) and the release step's `${PREV_TAG}`/`${NOTES}` were blanked before the shell ran, so the per-os/arch binaries were written to garbled names and never matched the upload list.
The RPM itself was unaffected (it's built via `make build`, internal to the Makefile) and `node-lookup-0.5.3-1.x86_64.rpm` published to `rpm-internal` correctly — only the Gitea release binary assets and notes were broken.
## Changes
- Escape all shell variables and command substitutions in the build loop and the release-notes block as `$$`, matching the existing `upload-rpm` step's convention. Genuine Woodpecker vars (`${CI_COMMIT_TAG}`, `${CI_REPO}`) stay single-`$`.
## Follow-up
The v0.5.3 Gitea release assets are being backfilled manually; this fix ensures v0.5.4+ produce them automatically.
Reviewed-on: #16
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
## Why
`node-lookup` output is handy for pivoting to Puppetboard, but there was no quick way to turn a list of hosts into Puppetboard node-page URLs, or to see when each host last ran Puppet. These two small tools close that gap and ship in the **same RPM** so they're available wherever `node-lookup` is.
## Changes
- Add **`pburl`**: reads hostnames from args or piped `node-lookup` output (first field of each line, de-duped) and prints `<host> <puppetboard-node-page-url>`.
- Add **`pblastreport`**: prints `<host>\t<last-report-time>\t<url>` using `report_timestamp` from the PuppetDB v4 `nodes` endpoint. Supports `--relative`/`-r` (relative age) and `--timezone`/`-z <IANA>` (default: local timezone).
- Add **`internal/puppet`** package shared by both tools: config load, PuppetDB `nodes` query, Puppetboard URL construction (`<base>/node/<certname>`), and no-TTY-safe stdin host reading.
- Add **`puppetboard_url`** config key (env `NODE_LOOKUP_PUPPETBOARD_URL`, default `https://puppetboard.k8s.syd1.au.unkin.net`) to the shared config so `config init`/`config show` scaffold it for the whole tool family. `node-lookup`'s own query behaviour is unchanged.
- Build all three binaries individually (each is its own `main` package — a single `go build ./...` can't emit multiple mains) and generate per-binary bash/zsh/fish completions in the Makefile, `build-rpm.sh`, and nfpm spec.
- Cross-compile and attach all three tools per os/arch in the release pipeline; extend `.gitignore`; `go mod tidy` promotes cobra/yaml to direct deps.
- Document the tools, config key, and env var in `AGENTS.md`.
## Testing
- `go test -race ./...` passes (new tests cover config precedence, `nodes` endpoint derivation, host-page URLs, `LookupNode`, stdin host parsing, and the report-time formatting incl. timezone/relative/edge cases).
- Built the RPM locally and confirmed it installs all 3 binaries + 9 completion files.
- Smoke-tested both tools end-to-end against a mock PuppetDB (timezone conversion, relative time, and error handling all correct).
No cross-repo changes needed: the release reuses the existing `default` ServiceAccount and the artifactapi `rpm-internal` upload.
Reviewed-on: #15
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
Two related query-ergonomics fixes surfaced while using the tool interactively.
## 1. `-pm <value>` failed with `unknown command "k8s"`
pflag does not attach a space-separated value to a string flag (`-m`) grouped with a bool flag (`-p`), so `k8s` was left as a stray positional. Only `-pm=k8s` or the un-grouped `-p -m k8s` worked.
- Allow one positional argument (`cobra.MaximumNArgs(1)`) and fall back to it for the match value when `-m` is empty (`matchValue()`). `-pm/-im/-ipm <value>` and a bare `-p <value>` now all work; `-m` still wins when both are given.
## 2. `-F a,b` (multiple facts) returned `{}`
`-jF ipaddress,enc_role` queried a single fact literally named `ipaddress,enc_role`.
- Split `-F` on commas (`splitFactNames`) and match any of them via an `or` over `["=","name",<n>]` clauses (`nameFilter`); a single name keeps the plain `=` form.
- Key JSON output by each result's real fact name so all requested facts appear under the host.
Both paths have unit tests; verified live: `node-lookup -R -pm externaldns | node-lookup -jF ipaddress,enc_role` returns both facts per host. AGENTS.md updated.
Reviewed-on: #14
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
## Why
The unit tests stopped compiling after the `--pm` → `-p`/`-i` match-modifier refactor was left uncommitted, there was no RPM/completions distribution story, and invoking the tool without a TTY against an empty pipe silently returned nothing. This makes the project releasable and safe to run from agents/CI.
## Changes
- Make stdin handling robust: replace the fragile `!isTerminal` check with `stdinReader()`, which only reads node names when stdin is a real pipe/redirect carrying data. Terminals, `/dev/null`, and empty/closed pipes now fall through to a normal query, so running without a TTY behaves like an interactive run.
- Repair and expand `main_test.go` to match the current `buildQuery`/`run` signatures; add coverage for the match modifiers, all output modes, config precedence, and the new `stdinReader` logic. `httptest` stubs PuppetDB (no live deps).
- Add nfpm packaging (`packaging/nfpm.yaml`, `scripts/build-rpm.sh`): installs the binary to `/usr/bin/node-lookup` and bundles generated bash/zsh/fish completions under the standard system paths.
- Rework the Makefile to build into `dist/` and add `completions`/`rpm` targets.
- Split PR CI into `build`, `test`, and `pre-commit` workflows and extend `release` to build the RPM and `PUT` it to the artifactapi `rpm-internal` repo. Every step sets a `serviceAccount` and k8s resources.
The project directory has also been relocated under `prodenv`.
Reviewed-on: #13
Co-authored-by: Ben Vincent <ben@unkin.net>
Co-committed-by: Ben Vincent <ben@unkin.net>
Passing tag both as argument and --tag flag causes ambiguous args error.
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Reviewed-on: #12
Change to only tagging with the makefile.
Change the workflow to create the release based on the tag.
Build the binary for multiple os/archs
Reviewed-on: #11
droneci user lacks write access; switch to token-based auth header.
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Reviewed-on: #7
tea creates the release before the pipeline runs; POST was failing with
conflict, leaving RELEASE_ID empty and skipping the asset upload.
Now GETs the release by tag, PATCHes its body, then uploads the binary.
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Reviewed-on: #6
Capture and print the full Gitea API response before parsing the release
ID, and fail explicitly if the ID is empty so the root cause is visible
in CI logs instead of silently producing a malformed asset upload URL.
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>
Reviewed-on: #5
-a requires -n and prints all PuppetDB facts for the specified node
as 'fact_name value' sorted alphabetically by fact name, useful for
discovering available facts to query against.
Query PuppetDB for node facts via CLI. Replaces the original Python script.
- XDG config (~/.config/node-lookup/config.yaml) with env var overrides
- All flags from original tool preserved (-n, -F, -R, -m, --pm, -1, -2, -C, -A, -j)
- config init / config show subcommands
- Unit tests (23), Makefile, GoReleaser config, pre-commit hooks
💘 Generated with Crush
Assisted-by: Claude Sonnet 4.6 via Crush <crush@charm.land>