Suppress the startup banner under --json (closes #106) #109
Reference in New Issue
Block a user
Delete Branch "fix-json-banner"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Closes issue #106.
The fix
Entrywrites the banner to stdout before cobra parses anything, andthe scan deciding whether to write it knew
--quiet,-qand--cronbut not
--json, so every--jsondocument arrived behind two lines ofprose and a blank line.
--jsonand--json=now join that list.The design question, answered
Extend the raw-argv scan. Do not move the banner after parsing.
The banner is printed before parsing deliberately, and
root.gosayswhy: it must still appear when cobra rejects the arguments
(
requires at least 2 arg(s)) and on--help. After parsing there isno single place that runs on all of those paths, so "after parsing"
means reimplementing the banner in several handlers, or accepting that
it vanishes from the failure paths where a human most wants to know
which build they just ran. That is a real regression traded for a
cosmetic gain.
On whether the persistent/subcommand distinction matters here: it does
not, because the precedent already crosses it.
--cronis in thesuppression list and is also a subcommand flag — it exists only on
snapshot create(internal/cli/snapshot.go:137), not on the rootcommand. So matching
--jsonanywhere in the vector adds anotherinstance of an imprecision the code already accepts, rather than a new
kind of one.
The two error directions are also not symmetric. A false positive loses
a decorative banner. A false negative corrupts a document on stdout and
breaks a pipeline. A scan that cannot be exact should err toward
suppression, and this one does — which is also why
--json=falsesuppresses the banner, exactly as
--quiet=falsealready does. Makingthe value parse honest for
--jsonalone would make it inconsistentwith its two neighbours; the doc comment states the bias instead.
Tests
Three, in
internal/cli, whereTestListSnapshots_JSONStdoutIsOnlyTheDocumentcannot reach:TestEntryJSONStdoutIsExactlyOneDocumentrunsEntryitself with areal argument vector over the process's real stdout file descriptor,
through cobra and the fx graph to the document, and asserts the
capture decodes as one JSON value with nothing after it. It is
hermetic because
file://storage is exempt from the S3 credentialchecks and
snapshot listtreats a destination store with nometadata/as an empty list rather than a failure.TestJSONInvocationStdoutIsExactlyOneDocumentcovers the argumentvectors of all five
--jsoncommands, plus--jsonbefore thesubcommand,
--json=true, and a flag-with-value ahead of it.TestBannerStillPrintedWithoutSuppressingFlagasserts the banner isstill printed without a suppressing flag, so the first two cannot be
satisfied by deleting the banner.
Verified to fail without the fix by reverting
bannerSuppressedInArgs,not by assumption: all three fail, the end-to-end one included, with
Should be empty, but was vaultik dev by Jeffrey Paul ... starting up at ....AGENTS.mdpolicy 9Re-worded to key on the log stream rather than stdout. The operative
rule — structured output when the log stream is not a terminal — is
unchanged; only the stream it names was wrong after
issue #82 moved the
logger and its format test to stderr.
The two smaller observations
bytesAttrKey: fixed rather than commented, being three lines.Keys reaching
writeAttrare group-qualified, sobyteslogged underan open group arrived as
transfer.bytesand fell back to a barenumber. The match is now made on the final dot-separated segment,
tested grouped and ungrouped.
Vaultik.Stderr: kept. The comment still reads true as astatement of intent, and now also says outright that nothing writes to
it today, so the next reader does not go hunting for a writer that
does not exist.
listEnv.stderrinsnapshot_list_test.gowasgenuinely dead — assigned, never read, since those tests began
capturing the process's stderr — and is removed.
prune --jsonis still broken, for a different reasonFiled as
issue #108, not fixed
here.
pruneLocalSnapshots(internal/vaultik/snapshot.go:845-878)writes three lines of prose to stdout with no
--jsonawareness at all,unlike every other writer on a
--jsonpath. It reproduces identicallyon
mainbefore this change,-qnever suppressed it either, and it isa different writer on a different code path — the same reason #106 was
filed out of #82 rather than fixed inside it.
Verification
End to end against a real config with a
file://destination store anda real snapshot, with no other flags and no
-q:| jq .snapshot list --jsonsnapshot verify <id> --jsonsnapshot remove <id> --jsonremote info --jsonprune --jsonscript/cibuildexit 0: freshCHECK_EPOCHper invocation,fmt-check,lintandtestlayers all executed (10 CACHED layers, all dependencyand module layers), 15
okpackages, 0(cached), 134s wall..golangci.ymlunchanged, sha256021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb.No tags created; origin still has zero.
Review: PASS
Independent review of
PR #109 at
4de3e41against issue #106. No
blocking defects.
Verified
Definition of done met (4 of 5
--jsoncommands, with the fifthcorrectly excluded — see below); CI green on
4de3e41; head containsc16ef47, fast-forwardable;make fmt-checkclean; container lint0 issues.executed fresh (60.3s, freshCHECK_EPOCH); noClaude/Anthropic references or trailers anywhere in the diff or the
commit message; commit title ends
(closes #106);.golangci.ymlsha256 matches, and
Dockerfile,script/,REPO_POLICIES.md,.gitea/,Makefileare byte-identical tomain; origin still has 0tags; inclusive terminology clean; no existing test weakened; nothing
live deleted (
listEnv.stderrconfirmed write-only before removal).Fail-without-fix reproduced independently, not taken on trust. With
--json/--json=reverted inbannerSuppressedInArgsandisBytesAttrneutered,
make testfails exactly and only:TestEntryJSONStdoutIsExactlyOneDocument(with the real banner textahead of
[]on the captured fd),TestJSONInvocationStdoutIsExactlyOneDocument,TestBannerSuppressedInArgs, andTestTTYHandlerByteFormattingSurvivesGrouping/grouped(
transfer.bytes=1536instead oftransfer.bytes=1.5 KB). Thegroupedsubtest fails whileungroupedpasses, which is the rightdiscrimination.
TestBannerStillPrintedWithoutSuppressingFlagkeepspassing under the mutation, confirming it is a genuine anti-deletion
guard rather than a second copy of the same assertion.
End to end, built binary, real config,
file://store, real snapshot,no
-qand no other flags:snapshot list --json,remote info --json,snapshot verify <id> --jsonandsnapshot remove <id> --jsonall pipe throughjq -c .at exit 0.prune --jsonfails, withNo stale local snapshots found.ahead of theobject — matching
issue #108 exactly.
internal/vaultik/snapshot.gois untouched by this diff, so that ispre-existing by construction, and the PR body correctly claims four of
five rather than all five.
Argv-scan attack surface. No shorthand exists for
--jsonon any ofthe five registrations (all
BoolVar), so there is no-jhole; no envor config binding sets it.
--json=falsesuppresses (verified: bannergone, table printed) — the accepted false positive, consistent with
--quiet=falseand documented in the doc comment.--config --json ...is likewise a false positive only.
snapshot list -- --jsonprints thebanner and produces no JSON document (verified), so honouring the
terminator is correct. Design decision to keep the banner pre-parse is
sound:
--helpverified to still carry it, and--cronis confirmedsubcommand-only at
internal/cli/snapshot.go:137, so this is anotherinstance of an accepted imprecision rather than a new kind. The error
asymmetry argument holds.
AGENTS.mdpolicy 9's operative rule (jsonl when the log stream is nota terminal) is preserved verbatim; only the stream name changed. No
//nolintwas added forgoconst— the two directives added(
testpackage,gochecknoglobals) are both well-precedented onmain,and the hoisted command-word constants are sensible.
Non-blocking
One fail-open exists, and it is pre-existing.
--configconsumes
--as its value in pflag, butbannerSuppressedInArgstreats the same
--as the terminator and returns false. Verifiedagainst the built binary from a directory containing a config file
literally named
--:vaultik --config -- snapshot list --jsonemits the two banner lines,the blank line, and then
[]. It requires a config file named--,the identical hole already exists for
--quietonmain, andhonouring the terminator is deliberate and tested
(
entry_banner_test.go:152). Recording it because it is the onebanner-on-a-
--json-run case that exists; not a reason to hold thisPR.
README.md:142is inaccurate in the way this PR argues against."The startup banner is the other thing that writes to stdout" reads
as the last such writer, but
pruneLocalSnapshotswrites threeprose lines to stdout on a
--jsonpath — the PR's ownissue #108. The
commit message carries the same claim ("this was the last writer that
could put something on stdout the caller did not ask for"), though it
is qualified later in the same message. The actionable sentence that
follows it —
snapshot list --json | jq .works on its own — is true,which is why this is a nit rather than a defect. A clause such as "the
banner was the last one on the
snapshot listpath" would carry thePR's own standard for a docs file that describes the code.
entry_banner_test.go:119uses a bare"restore"while everyother command word in the file is hoisted to a constant — the exact
thing the constant block's comment and
AGENTS.mdpolicy 10 ask for.entry_banner_test.go:101-104is tautological: the test writesplaceholderJSONDocumentinto the buffer itself and then asserts thebuffer holds one JSON document. The load-bearing assertion is the
require.Emptyabove it (confirmed by the mutation run). Harmless,but it reads as more coverage than it is.
cibuild
script/cibuildexit 0, 2m43s wall. FreshCHECK_EPOCHforced thefmt-check,lint,testandbuildlayers to execute (14 CACHEDlayers, all dependency/module/base layers). 15
okpackages, 0(cached)markers —script/testpasses-count=1, so no result-cachehit is possible. Lint reported
0 issues.in-container, with only theknown
gomodguarddeprecation warning.