yukta, a small command-line program written in Go. It does no workflow work itself. Every command turns your typed words into one request to a running Yukta server, then prints the answer. This page explains how that translation works, what the program guarantees, and where it quietly falls short of the server it talks to.
Primer
People and scripts need to drive a Yukta server without opening the web UI: start a run in a CI job, watch its progress in a terminal, pause a node while debugging, list plugins. The server already offers all of this over HTTP (REST). Typing rawcurl commands with long paths and JSON bodies is slow and error-prone, and a Java client would force every machine to install a JVM. The cli module solves this with one small native program, yukta, that has no runtime to install and starts in milliseconds.
Where the cli sits
The vocabulary in eight cards
One example, end to end
A developer starts a workflow with an input payload, then watches it live. Click a step to see what happens, why, and what can go wrong. (The session must already exist on the server; create it first with ‘yukta session apply’.)Parse command
workflow start s1 build --payload '{"msg":"hi"}'. Cobra finds the start command, checks it received exactly two positional arguments (session id, workflow id) and stores the flags. The global flags --url, --token and --timeout are written straight into the one shared client object.Why. Checking argument counts before any network call means a typo costs nothing. Binding flags directly to the client avoids a separate configuration step that could be skipped.Can go wrong. The URL default comes from YUKTA_SERVER_URL if set, else http://localhost:8080. A forgotten variable silently points the command at the wrong server.Check payload
--payload (inline text) or --payload-file (a path) is read. The two flags cannot be combined. The text must parse as a JSON object, not an array or a bare number.Why. The server declares the payload as a map of names to values. Rejecting a bad payload here gives a clear local message instead of a server validation error after a network round trip.Can go wrong. With --payload-file a missing file is a plain read error, which is good. The other file-or-JSON commands (see the risks) cannot tell a mistyped path from bad JSON.Build request
POST /api/workflow/start with a JSON body holding sessionId, workflowId and the payload. It adds Content-Type and Accept JSON headers and, when a token is set, Authorization: Bearer ....Why. Ids that go into a path are escaped. Without that, a node id such as a/../../sessions would silently change which endpoint is called.Can go wrong. The token is sent to whatever --url says, including plain http://, with no warning. There is also no way to send the server’s Idempotency-Key header, so a timed-out start cannot be retried safely.Send and wait
--timeout 0 disables). The server answers 202 Accepted with an envelope containing the new execution id. Any 2xx counts as success. Any other status becomes the error text API error: status N: body.Why. The limit covers connecting, waiting and reading the whole body. Without it a hung server would hang the shell or CI job forever.Can go wrong. No retries, ever. A 429 (server at its run limit) or a network blip is reported once and the command exits with status 1. The raw error body is shown, not parsed.Print result
data field and prints Workflow started with execution ID: e42, or the same data as indented JSON when -o json is set.Why. JSON output lets a script capture the id (jq -r .executionId) and feed it to the next command.Can go wrong. The server’s response shape is copied by hand into a Go struct. If the server renames executionId, the CLI prints an empty id and still exits 0.Open stream
yukta workflow status-stream s1 e42 opens GET /api/workflow/s1/status/e42/stream?includeHistory=false asking for text/event-stream. It checks the response really has that content type before reading.Why. No timeout is applied, because a stream may legitimately stay open for hours. Ctrl-C and SIGTERM are wired to cancel the request cleanly.Can go wrong. The CLI sends includeHistory=false by default, while the server’s own default is true. Events that happened before you connected are not replayed unless you pass --include-history.Read events
data: are collected, and a blank line ends one event. Each event is decoded as a progress snapshot and printed as indented JSON. The server’s 15-second : ping comments are ignored.Why. Writing the scanner by hand keeps the dependency list to Cobra only. A fuzz test feeds it random input so a bad stream cannot crash it.Can go wrong. An event not followed by a blank line at the end of the connection is dropped. If the connection simply closes, the command ends with exit 0, which looks like success even when the network failed.Ctrl-C or end
Last-Event-ID header is never sent. A dropped connection means you re-run the command and may see events twice or miss some.A command in plain words
Type
Configure
Build
Send
Judge
Decode
Components
Entry & Wiring
main, one client, 5 groups Starts the program: creates one HTTP client, builds the root command, attaches the five command groups and runs Cobra. Any returned error is printed aserror: ... on standard error and the process exits with status 1.
-
maincalls a singleRunfunction. It creates exactly one client object and passes that same object to every command group (plugin, controlbus, logs, workflow, session). Sharing one object is what lets the global flags work: the root command writes--url,--tokenand--timeoutinto the client’s fields, and every command later reads the same fields. - The client is created before the flags are read, but with fields the flag parser fills in. An earlier design replaced the server address in a pre-run hook; the current one binds the flags directly, so there is no moment where a command could see the wrong address.
-
Registration is manual: each group is added with an explicit call. That is simple, but it means a command can be written, tested and documented yet never reach the user. This happened with
session apply, which was built and unit-tested but not registered; it is registered now, and nothing tests the command tree as a whole, so the same can happen again. -
Cobra prints its own
Error: ...and the usage text for any error, and thenmainprintserror: ...again. A failed command therefore shows the message twice with a block of help text between. - The exit code is only ever 0 or 1. A 404, a timeout and a bad flag all look the same to a calling script.
- Guarantees. Every command shares one client. Any error ends the process with status 1 after a message on standard error.
- State. Nothing is held after the process exits. There is no config file, history or cache.
- Ceiling. Not applicable: one short-lived process per command.
- On failure. A failed command prints the message twice and exits 1. A panic would print a Go stack trace and exit 2; none is known.
yukta (binary name), exit status 0 or 1
Root & Global Flags
url, token, timeout, -o Defines the top-levelyukta command and the four options every command inherits, and applies the defaults.
-
--urldefaults to theYUKTA_SERVER_URLenvironment variable, orhttp://localhost:8080when that is unset. The flag always wins over the variable. The default is read when the command tree is built, which is why a test or a script must set the variable before the program starts. -
--tokendefaults toYUKTA_TOKEN. It is sent asAuthorization: Bearer ...on normal requests and on streams. The server accepts it only whenyukta.api-keyis configured; otherwise the server ignores it. Passing the secret as a flag puts it in shell history and in the process list (visible to other users on the machine), so the environment variable is the safer way in our view. -
--timeoutdefaults to 30 seconds and applies only to normal calls.0turns it off. Streams ignore it on purpose, because a status stream may run for hours. -
-o/--outputistableby default orjson. The value is held in a package-level variable that commands read through a getter. Nothing validates it:-o yamlis accepted and treated as table by most commands, and onlysession getrejects it. -
The root command’s short description still reads ‘Code quality gates for AI-driven development’. That describes one use of Yukta, not the product, so
yukta --helpmisdescribes it. - Guarantees. Flag values reach the client before any command body runs. The flag always overrides the environment variable.
- State. None. Defaults come from two environment variables read at start-up.
- Ceiling. No ceiling. The only shared mutable state is the output-format variable, which is why tests that change it must reset it.
-
On failure. A bad duration such as
--timeout abcis rejected by Cobra before anything runs.
--url / YUKTA_SERVER_URL (default http://localhost:8080), --token / YUKTA_TOKEN, --timeout (default 30s, 0 = off), -o, --output table|json (default table)
Command Builders
Cobra groups, Action helper Declares every command, its arguments and flags, calls the matching client function and chooses how to print the result.-
There are five groups.
session(list, get, summaries, workflow get/graph/executions),workflow(start, stop, pause, resume, status, status-stream, history, executions stop/restart, node pause/resume/stop/skip, node step enable/disable/next),plugin(list, get),controlbus(nodes, all-nodes, heartbeat, command) andlogs. Group names follow the server’s resources, so someone who knows the REST API can guess the command. -
Most ‘do something to this id’ commands are built by one generic helper,
Action. A command supplies its usage text, how many arguments it takes, the client call to make and a one-line success message. The helper adds the ‘if-o jsonprint the raw answer, else print the message’ behaviour. This replaced about a dozen near-identical files and means pause, resume, step and stop all behave the same way. -
Commands that take JSON (
controlbus commandandsession apply) accept either a file path or inline JSON. The code checks whether a file with that name exists; if so it reads it, otherwise it treats the argument itself as JSON. Example:yukta controlbus command wf1 node1 cmd.jsno(typo) does not say ‘file not found’. It says ‘invalid JSON’, because the typo was parsed as JSON text. -
workflow starttakes--payloador--payload-file. They exclude each other, and the content must be a JSON object.workflow stoptakes no options and always sends the default stop mode, so the server’s?mode=DRAIN|CANCEL|KILLchoice is unreachable.workflow node stophas--immediateand--reason, but it always sendsreason=, even when empty, which replaces the server’s default reason text. -
None of the list commands accept
--limitor a cursor. The server returns at most 500 items per page by default (1,000 at most) and announces a next page in anX-Next-Cursorheader, which the CLI never reads. An installation with more than 500 sessions is silently shown only the first 500. - Guarantees. Argument counts are checked before any request is sent, and every id is a required positional argument. Payload and JSON arguments are parsed locally before sending.
- State. None. The only shared variable is the chosen output format.
- Ceiling. About forty commands across five groups. Each new server endpoint needs a hand-written command, which does not scale as the API grows.
- On failure. A bad argument count or invalid JSON stops the command before any network call, and exits 1.
yukta session list|get|summaries, yukta session workflow get|graph|executions, yukta workflow start|stop|pause|resume|status|status-stream|history, yukta workflow node ..., yukta workflow executions stop|restart, yukta plugin list|get, yukta controlbus nodes|all-nodes|heartbeat|command, yukta logs [--entries]
Output Formatter
table or JSON to stdout Prints aligned tables and indented JSON, and writes errors to standard error.- A table is written with Go’s tab writer: columns are padded by two spaces. A header row comes first. This is for people reading a terminal.
-
JSON output is indented by two spaces and written to standard output, so it can be piped to
jq. Encoding errors go to standard error and do not stop the program. -
Which output a command gives is decided by the command, not by this component, and the choices are uneven.
session list,plugin list,controlbus nodesprint real tables.workflow statusandhistoryprint plain text lines instead of a table.session summaries,session workflow graph,session workflow executionsandsession workflow getalways print JSON whatever-osays.workflow status-streamalways prints one indented JSON object per event.logsprints each line as the server sent it. -
controlbus heartbeatin table mode walks a Go map, so its rows come out in random order from run to run.controlbus commandsorts its keys first. - Guarantees. Data goes to standard output and errors to standard error, so piping JSON into another tool is safe.
- State. None.
- Ceiling. Whole results are held in memory and printed at the end (except streams), which is fine for hundreds of rows.
- On failure. A JSON encoding failure prints a line to standard error and the command still exits 0.
-o table|json
HTTP Client Core
headers, timeout, errors Builds every request and turns the server’s answer into either bytes or an error. All resource functions and streams go through it.-
Every request carries
Content-Type: application/jsonandAccept: application/json, plus the bearer token when one is set. The path must start with/. The address is joined from the base URL (trailing slashes removed) and the path. -
Normal calls get a time limit: the request context is wrapped with the
--timeoutvalue, and the limit stays active until the whole body has been read. So a server that answers headers quickly but trickles the body is also cut off. The underlying Go HTTP client has no timeout of its own, deliberately, so that streams are not cut. -
Any status outside 200-299 returns the error text
API error: status 404: {body}, with the server’s raw JSON error body included as-is. The CLI does not parse the error envelope (message, error, path), so the user sees JSON text inside an error line. The server’s 202 for a start is accepted because it is a 2xx. -
Path pieces are escaped with Go’s path-escape function through one helper,
apiPath. Example: node ida/bbecomesa%2Fb, so it stays one path segment and cannot be used to reach a different endpoint. Query values are built separately with the standard query encoder. -
There are no retries, no connection reuse tuning, no proxy settings beyond Go’s defaults (the standard
HTTP_PROXYvariables are honoured by Go’s default transport), no TLS options (no way to trust a private CA other than the system store) and no redirect policy beyond Go’s default. - Guarantees. Every id in a path is escaped. A normal call never waits longer than the timeout (0 disables). A non-2xx answer is always an error that contains the status and body.
- State. No state beyond the three settings on the client (base URL, token, timeout).
- Ceiling. One request at a time per command. No pooling concerns for a short-lived process.
-
On failure. Network and timeout errors read
request failed: .... A server error readsAPI error: status N: body. Neither is retried.
--url, --token, --timeout, standard HTTP_PROXY / HTTPS_PROXY / NO_PROXY
Resource Clients
one function per endpoint One Go function per server endpoint (sessions, workflows and executions, plugins, control bus, logs), each with hand-written types for the answers.-
Each function checks its ids are not empty, builds the path, sends the request and decodes only the
datafield of the envelope into a Go type. Example types:WorkflowStartResponse{executionId},WorkflowProgress{executionId, status, tasks[]},PluginDetails. -
Some answers are typed (workflow start, progress, history, plugins). Others are left as
map[string]anyorany(session details, workflow definition, graph, summaries, heartbeat, command reply). The loosely typed ones cannot drift in a way that breaks, but they also give no help to someone reading the code. -
The CLI calls the server’s older, session-qualified paths for most run control:
/api/workflow/{sessionId}/{executionId}/pause,/api/workflow/{sessionId}/status/{executionId}and the node paths. The server marks these as deprecated and offers shorter equivalents such as/api/executions/{executionId}/pause. The older paths have one advantage: with a session in the path the server also checks that the run belongs to that session, so one session’s id cannot steer another session’s run. -
The decoding step treats a missing or renamed field as an empty value rather than an error. If the server renamed
executionId,workflow startwould print an empty id and exit 0. -
No test compares these types or paths to the server’s real API description (the OpenAPI document at
/v3/api-docs). They are checked only against fake servers written by the same author, so a wrong assumption would be repeated in both places. - Guarantees. A call with an empty id fails locally with a clear message, and the response is decoded or an error returned. It never returns half-decoded data.
- State. None.
- Ceiling. Roughly 30 hand-written endpoint functions. Server changes need matching manual edits.
-
On failure. A body that is not valid JSON gives
failed to unmarshal response. A valid body with different field names decodes to empty values.
/api/sessions..., /api/workflow/..., /api/plugins, /api/control/..., /api/sessions/{sid}/executions/{eid}/logs
SSE Streaming
status and log streams Opens long-lived event streams for execution status and logs, and reads them until the user stops or the server ends them.-
A stream request sends
Accept: text/event-streamand the same bearer token. A non-2xx status is read and reported like a normal error. A 2xx answer must have media typetext/event-stream, otherwise the stream is refused. The time limit does not apply, so only Ctrl-C, SIGTERM or the server can end it. -
The scanner reads lines with a buffer that grows to 1 MB. A line longer than that stops the stream with an error.
data:lines in the same event are joined with newlines; a blank line fires the event.event:is remembered but never used.id:,retry:and comment lines (the server’s: pingkeep-alive every 15 seconds) are ignored. -
There is no reconnect. The server supports
Last-Event-ID(event ids count from 0 per stream, and a reconnecting client with that header skips what it already saw), but the CLI never sends it. A dropped connection ends the command. If the network closes cleanly, the command ends with exit 0, which cannot be told apart from ‘the execution finished’. -
Status updates are decoded as progress snapshots and each is printed as indented JSON. Log streams print each event’s data as one line.
logs --entriesreads the structured variant. By the server’s design these end when the execution reaches a final state. -
A fuzz test feeds random bytes to the scanner to prove it never panics. Plain
go testruns only its saved examples; the random exploration needsgo test -fuzz, which nothing in the build runs. - Guarantees. Ctrl-C ends a stream cleanly with status 0. A stream of the wrong content type is never read. Event text is passed through unmodified.
- State. None. The last event id is never stored, so resuming is not possible.
- Ceiling. One connection per command. A single event larger than 1 MB (for example a progress snapshot with very large node metadata) ends the stream with an error.
-
On failure. A malformed progress event ends the stream with
failed to unmarshal progress. A server that stops sending but keeps the connection open is not detected, apart from the server’s own pings, which the CLI does not watch for.
workflow status-stream [--include-history], logs [--entries]
Build & Quality
Makefile, Gradle, release Compiles the program for five platforms and runs all quality checks. The Makefile holds the real logic; Gradle only calls it so the JVM build can include the CLI.-
make buildproduces one static binary (CGO_ENABLED=0, symbols stripped) namedbuild/yukta-<os>-<arch>, with.exeadded on Windows.make build-allbuilds linux/amd64, linux/arm64, darwin/amd64, darwin/arm64 and windows/amd64. Static means one file with nothing to install. -
Tools are pinned by version and run with
go run pkg@version: golangci-lint v2.14.0 (standard linters plus gocritic and revive), gosec v2.29.0 (security patterns) and govulncheck v1.8.0 (known-vulnerable dependencies and standard library). Pinning means the same commit gives the same result next month. The cost is a download on first use. -
make cover-checkalways runs the tests afresh and then fails if any single function is below 100% coverage. Exceptions:cmd/yukta/main.go,internal/testutil/testutil.goandtest/integration_test.goare exempt (0%). Per-function is stricter than a project average: a new untested function cannot hide behind well-tested ones. -
build.gradle.ktsis a 48-line wrapper: each Gradle task such asgoBuild,goLintorgoTestruns one Make target and shows its output. The Gradlechecktask runs all of them. Because it runs bothgoTestand the coverage check (which itself runs the tests), the tests run twice. -
Windows developers need
makeand a POSIX shell (the Makefile uses shell, awk and grep), for example through WSL or Git Bash. The produced Windows binary itself needs nothing. -
On a version tag, the release workflow runs
make build-all, writesSHA256SUMS, attaches a signed build-provenance attestation (a record proving which commit and workflow produced the files) and publishes a GitHub release. CI and release read the Go version fromgo.mod. The binary has no version command, and the build embeds no version string. - Guarantees. Tool versions are pinned. Coverage is regenerated on every check. Release binaries come with checksums and a provenance record.
-
State. Build output goes to
cli/build/;coverage.outis a generated file. Nothing else persists. -
Ceiling. Tests run twice in
check. Tools are downloaded on first use, so a clean machine needs network access to the Go module proxy. -
On failure. Any failing step stops
make check. A missingmakeon Windows stops the Gradle tasks before Go is even called.
make build | build-all | test | cover | cover-check | fmt | vet | lint | sec | vuln | check, ./gradlew :cli:goBuild | goBuildAll | goTest | check, GOOS= / GOARCH= overrides, go.mod: go 1.26, toolchain go1.27.2
Test Harness
httptest, capture, fuzz Lets every command and client function be tested end to end against a real local HTTP server without a real Yukta.-
A helper starts a throwaway local server that returns a fixed status and body and records each request’s method and path-with-query. A test builds the real command, runs it with a list of arguments and checks both what was sent (
POST /api/workflow/s1/e1/pause) and what was printed. - Printing is checked by swapping standard output for a pipe during the call and reading it back. Streams use another helper that serves a fixed block of event text with the right content type.
- Earlier versions had a 612-line hand-written fake client behind several interfaces. It was deleted: fakes tested the fake. Real HTTP exercises the escaping, headers and error handling for free.
-
The file called
test/integration_test.gois not an integration test in the usual sense. It never contacts a server; it checks how the client is constructed (default URL, trailing slash trimming, header setup). Nothing in the repository runs the CLI against a real Yukta server, so the paths and shapes inresare never checked against the actual controllers. - Because the output-format setting is a shared variable, tests that change it must restore it, or later tests in the same package see the wrong format.
- Guarantees. Every function except the three exempt files is fully covered. The paths a command calls are asserted exactly.
- State. Each test starts its own server and cleans it up.
- Ceiling. Test code is about three times the size of main code (the improvements note gives about 8.0k test lines against 2.9k main lines).
- On failure. A test that leaves the output variable changed can break unrelated tests. The fake server returns whatever the test says, so a wrong assumption about the server passes.
go test ./..., make cover-check, go test -fuzz FuzzScanSSE (manual)
Scenarios
Start a run with a payload
One request path from keystroke to printed result, including where an idempotency key would matter.Typed command
yukta workflow start s1 build --payload '{"msg":"hi"}'. Cobra matches the command and checks it got two positional arguments before anything else happens.Flags reach the client
--url, --token and --timeout (or their defaults from the environment) were written into the shared client while the flags were parsed. No command needs to look them up.Payload is validated locally
One request, one answer
POST /api/workflow/start with a 30-second limit. The server replies 202 with an execution id. The CLI cannot send an Idempotency-Key, so if this call timed out, running it again could start a second run.Workflow started with execution ID: e42 is printed, or JSON with -o json. A script can then pass the id to status or logs.Watch a run live
How live status streams work, why they have no time limit, and what Ctrl-C and a dropped connection do.Open the stream
yukta workflow status-stream s1 e42 asks for an event stream. The stream path is built with escaped ids and the same bearer token. The CLI sends includeHistory=false unless told otherwise.No time limit
Events arrive
: ping comment every 15 seconds so proxies do not close an idle connection. The scanner joins lines into events and prints each snapshot as JSON.Ctrl-C
Connection drops instead
Last-Event-ID, so nothing resumes.The server says no
What the user sees when the server refuses a call, and why scripts cannot tell failures apart.A refusal comes back
yukta workflow start s1 build and the server is at its run limit. It answers 429 with a JSON error body.Turned into an error
API error: status 429: {body}. The CLI does not read the body’s message field, so the user sees raw JSON inside a sentence.No retry
Printed twice
Error: ... followed by the full usage text. Then main prints error: ... again. The one useful line is repeated and wrapped in help text, so it is easy to miss in CI logs.The 401 variant
yukta.api-key set and no token was given, every call returns 401 with WWW-Authenticate: Bearer. The CLI reports the same generic API error: status 401, with no hint to set YUKTA_TOKEN.Slow or unreachable server
Time limits, what they cover, and the two places they do not (streams and ambiguous starts).Server does not answer
Refused instantly
connection refused straight away. The message is request failed: ... and exit 1.Slow body
--timeout 120s or 0.Streams are exempt
yukta logs waiting forever. The server’s 15-second pings exist for proxies; the CLI does not watch for them.Ambiguous start
workflow start times out, the run may or may not have started. Without an idempotency key the user cannot safely repeat it. They must check workflow history first, which is capped at the server’s first page.Create a session with apply
A real gap: a documented, tested command that users cannot run, and why no check caught it.Docs promise it
yukta session apply session.json and the help text of yukta session says ‘Apply session configurations’. A developer types it.It is registered now
apply reads a file or inline JSON, validates it, posts to POST /api/sessions and has its own local -o flag. Until the fix, the session group never registered it and Cobra reported an unknown command, even though its own tests passed.No test over the tree
yukta session --help list apply?’ would catch the next command that is written but not registered.Typos and flags
-o flag shadows the global one.Before applying
POST /api/sessions/validate, so a CI job cannot check a definition without saving it.From commit to released binary
How the build proves quality, what it cannot prove, and how binaries reach users.Local check
./gradlew :cli:check. Gradle calls Make for format check, module verify, vet, lint, security scan, vulnerability scan, tests and coverage. Tests run twice because both the test task and the coverage task run them.Pinned tools
go run pkg@version. The same commit gives the same verdict. Pinning previously exposed 32 unchecked errors and 5 security findings that unpinned runs had hidden.Coverage gate
Tag and release
SHA256SUMS, records build provenance and publishes them. Names look like yukta-linux-amd64 and yukta-windows-amd64.exe.Which version is this?
version command and no version string embedded at build time, so bug reports cannot name a release.Capabilities
What the program can do today, grouped by area. ‘Limit’ names the practical edge of each.Sessions
Runs
Nodes
Live
Discovery
Control bus
Access
Output
Packaging
Guarantees and limits
What the CLI promises, what it leaves to the server, and what it does not do. ‘Enforced’ means code checks it; ‘not enforced’ means a user or the server must.Behaviour of a command
Per-call semantics. The CLI keeps no data between runs.Enforced by the CLI vs left to the server
Defaults and limits
Reference
Command to REST endpoint map
What each command calls. sid = session id, eid = execution id, nid = node id, wid = workflow id. Paths marked (legacy) are the session-qualified forms that the server documents as deprecated; the server also offers /api/executions/{eid}/… forms.Server endpoints with no CLI command
Found by comparing the web controllers with the commands. Each is a candidate for a new command or a reason to generate the client.Build and check targets
The Makefile is the source of truth. Gradle task names are listed for people who start from the JVM side.Architecture decisions
Hand-written types and loosely typed answers
Hand-written types and loosely typed answers
data field into small hand-written structs, or a generic map for complex answers such as graphs.Consequences. Little code and no generator dependency. Unknown fields are ignored, which is lenient, but renamed fields become empty without any error.Alternatives considered. Generate from the OpenAPI document: always correct, but large generated code and a build step. Strict decoding that rejects missing fields: catches drift, but breaks on harmless server additions if not tuned.Revisit when. a contract test finds drift, or a second client (for example a Python SDK) appears and could share the generated model.The Makefile is the build; Gradle only wraps it
The Makefile is the build; Gradle only wraps it
./gradlew check) would not cover the CLI.Revisit when. Windows contributors become common, or the Make logic outgrows shell and awk.One generic Action builder and commands grouped by resource
One generic Action builder and commands grouped by resource
Test with a real local HTTP server, not mocks
Test with a real local HTTP server, not mocks
httptest server that records the path and returns canned answers. Enforce 100% coverage per function.Consequences. Escaping, headers and error text are tested for real. The fake server’s answers are still written by the author, so wrong assumptions about the server pass. Coverage cannot see an unregistered command.Alternatives considered. Interface mocks: faster to write but test the mock. A real server in CI: the truest, slow and heavy (needs the JVM image). Recorded responses from the real server: realistic, but need regular refresh.Revisit when. a contract problem reaches users, which would justify the cost of a real-server test.Streams: no timeout, own SSE scanner, no reconnect
Streams: no timeout, own SSE scanner, no reconnect
Pin the quality tools with go run and versions
Pin the quality tools with go run and versions
go run pkg@version in the Makefile (golangci-lint v2.14.0, gosec v2.29.0, govulncheck v1.8.0).Consequences. Repeatable results with nothing to install; the module’s own dependency list stays small. Versions need manual bumps, and a first run downloads the tool.Alternatives considered. A tool directive in go.mod: native, but adds hundreds of indirect requirements to the CLI module. Installing from a Docker image: strong isolation, heavier setup. Unpinned @latest: no upkeep but no repeatability.Revisit when. the manual bumps are forgotten, or Dependabot support for these pins becomes possible.Static binaries per platform with checksums and provenance
Static binaries per platform with checksums and provenance
CGO_ENABLED=0 for five targets, publish SHA256SUMS and a build-provenance attestation on tagged releases.Consequences. No installs, and users can verify what they run. No installer, no package-manager entries, no auto-update, no version command.Alternatives considered. Packages for Homebrew, apt and winget: easier for users, more release work. A container image: simple for CI but awkward for desktops. Source install with go install: needs Go.Revisit when. many users ask for package-manager installs, or a signed release is required.