Gate prune's local-cleanup prose on --json, and give make build a rule
#111
Reference in New Issue
Block a user
Delete Branch "fix-prune-json-and-build"
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 #108.
Closes issue #110.
Two unrelated defects of the same shape — a command reporting something
it did not do — in one PR because both are small.
#108: the decision, and why
CleanupLocalSnapshotswrote three prose lines to stdout with no--jsonawareness, covering every branch, so no input avoided them andvaultik prune --json | jqfailed even after the banner fix.-qneverhelped either:
printlnStdout/stdoutfwrite straight toVaultik.Stdoutand never consultVaultik.UI, which is whatSetQuietaffects.Taken — thread
opts.JSONthrough.CleanupLocalSnapshotsnowtakes
*PruneOptions, symmetric withPruneBlobs, its sibling phase,and gates each write on
!opts.JSONexactly asprune.go:113-150,RemoveSnapshotandremote infoalready do. One pattern in thepackage rather than two.
Rejected —
log.Info. The logger's default level isslog.LevelWarn(internal/log/log.go:68), so this would not relocatethe lines to stderr; it would delete them from a plain
vaultik prune."Removing stale local record: <id>" narrates the deletion of rows
from the local index, and making that visible only under
--verboseisa behaviour regression rather than a stream fix. Verified in the field:
vaultik prunewith no flags still prints all three lines.Rejected — a count in
PruneBlobsResult. Every field of that structis blob-scoped (
blobs_found,blobs_deleted,blobs_failed,bytes_freed) and it is produced by the phase that runs after thisreconciliation. Adding
stale_recordswould make the document's nameinaccurate and would change a published
--jsonschema as a side effectof a stream-hygiene fix. A prune document covering both phases is a
reasonable thing to want; it is a schema design question and should be
its own issue. Flagging rather than silently adding it, as the issue
asked.
The two events are additionally emitted as
log.Inforecords — thepattern
PruneBlobsalready uses, logging and printing in parallel — sothey survive on stderr under
--verboseeven in--jsonmode.#110:
.PHONYauditbuildwas in.PHONYwith nobuild:rule; a phony target with noprerequisites and no recipe is already satisfied, so
make buildprinted "Nothing to be done" and exited 0 with no binary. Fixed with
build: vaultik, keepingvaultik:as the file rule.Audited all 19 names on that line —
all,bootstrap,setup,check,test,lint,lint-fix,fmt,fmt-check,build,clean,deps,test-coverage,local,install,release,release-snapshot,docker,hooks.buildwas the only onewithout a rule; nothing else needed fixing or removing.
vaultikiscorrectly absent from
.PHONY, being a real file target.Verification
Every new test was confirmed to fail with the fix reverted, not assumed
to. With the gates removed and
build: vaultikdeleted, all three newtest groups fail (
cmd/vaultik,internal/cli,internal/vaultik),including each of the three
CleanupLocalSnapshotsbranches.Both
make builddirections, which item 2 of #110 asks for:rm -f vaultik && make build->EXIT=0, 142829863-byte binaryproduced.
cmd/vaultik->make: *** [Makefile:82: vaultik] Error 1,EXIT=2, no binary. A target that cannot fail would be no betterthan one that cannot build.
vaultik prune --json | jq .against a real config and afile://destination store, no other flags, not
-q, in both branches:jqexit 0;od -cconfirms stdout is the documentand nothing else.
metadata/removed under a live localrecord):
jqexit 0,{"blobs_found":1,"blobs_deleted":1,...}, andthe record is gone from the index afterwards.
--jsonthe human narration is unchanged, all three linesintact.
The other four still pipe cleanly, re-checked on the built binary:
snapshot list --json,snapshot verify <id> --json,snapshot remove <id> --json,remote info --json—jqexit 0for each.
script/cibuild: exit 0, 157s wall,--build-arg CHECK_EPOCHfresh, 16oklines, 0(cached), 10CACHEDlayers, all of them below theARG CHECK_EPOCHline (apk,go mod download, COPY); the three checkRUNs each echoed the fresh epoch, somake fmt-check,make lintandmake testall really executed.make checkgreen locally as well, 0lint issues.
The
okcount is 16, not the 15 of the previous cycle:cmd/vaultikhad no test file before this change and now has one, so itmoves from
? no test filestook.Notes
README.md's stdout/stderr section called the banner "the other thingthat writes to stdout", which #108 contradicted. Rewritten to state
the contract that now holds, naming
pruneexplicitly.flagConfigandprogramNameconstants extracted in the CLI tests:the new argument vector pushed
"--config"and"vaultik"overgoconst's 3-occurrence threshold, which also flagged the pre-existing
literals in
entry_banner_test.goandroot.go..golangci.yml(sha256
021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb),Dockerfile,script/,REPO_POLICIES.md. No tags created; originstill has zero.
Independent review of PR #111 — verdict: PASS
Verified against head
be786fe, basemainf21e7c9(unmoved;merge-treeclean, mergeable).Checked and passing: definition of done met for both issue #108 and issue #110; all three stdout writes gated on every branch and the human path still prints all three lines (real binary, real config,
file://store);prune --json | jq .exits 0 in both branches withod -cshowing stdout is the document alone;rm -f vaultik && make buildexits 0 and produces the binary while a deliberately brokencmd/vaultikexits 2 and produces none; my own.PHONYaudit of all 19 names confirmsbuildwas the only one without a rule andvaultikis correctly absent; CI green onbe786fe;script/cibuildexit 0, 142s wall, 16ok, 0(cached), 14CACHEDlayers all belowARG CHECK_EPOCHwith all three checkRUNs echoing the fresh epoch, container lint 0 issues; the 16thokiscmd/vaultikgaining exactly one test file, not a new package;.golangci.ymlsha256 matches andDockerfile,script/,REPO_POLICIES.md,.gitea/are byte-identical tomain;entry_banner_test.gochanges are constant extraction only, no assertion removed or weakened; no Claude/Anthropic references or attribution trailers in the diff or commit; origin has zero tags and none were created.Negative controls reproduced here rather than taken on report:
build: vaultik—TestPhonyTargetsAllHaveRulesandTestBuildTargetBuildsTheBinaryboth fail, andmake buildreverts toNothing to be done, exit 0, no binary.build:plus an empty recipe —TestBuildTargetBuildsTheBinaryfails, so the empty-recipe respelling of the defect is caught too..PHONY—TestPhonyTargetsAllHaveRulesnames it and fails.!opts.JSONgates — all threeTestCleanupLocalSnapshots_JSONWritesNothingToStdoutsubtests and bothTestEntryPruneJSONStdoutIsExactlyOneDocumentsubtests fail.Non-blocking findings
1. The PR body's claim about the
log.Inforecords is false: they cannot be emitted under--json.The body states the two events "survive on stderr under
--verboseeven in--jsonmode". They do not.internal/cli/prune.go:49setsQuiet: rootFlags.Quiet || opts.JSON, andinternal/log/log.go:56evaluatescfg.Cron || cfg.Quiettoslog.LevelWarnas the first case of the switch, ahead of theDebugandVerbosecases.--jsontherefore pins the level to WARN, and neitherlog.Infoatinternal/vaultik/snapshot.go:866nor the one at:884is reachable in that mode. Measured on the built binary with a stale record present:--verbose pruneemits both records on stderr;--verbose prune --jsonemits neither;--debug prune --jsonemits neither.Why it matters: this is the justification offered for the gating being lossless. Under
--jsonthe removal of local index rows now has no signal anywhere — suppressed from stdout, deliberately absent fromPruneBlobsResult, and below the log level.Two things keep it off the blocking list. The permanent record is accurate: the commit message,
TODO.mdand theCleanupLocalSnapshotsdoc comment all state the--verbosesurvival without the "even in--json" qualifier, which is true for a human-mode run; only the PR body overreaches. And the behaviour is inherited, not introduced —PruneBlobs' ownlog.Infocalls are equally invisible under--jsononmaintoday, so this PR faithfully copies an existing pattern. Acceptable looks like: strike that clause from the body. Whetherprune --jsonshould report a stale-record count at all is the schema question the author already flagged for sneak, correctly out of scope here.2.
Also closes #110.may not survive a squash merge. The landing commit's subject carries(closes #108)per convention and its body carries a bareAlso closes #110.that Gitea would match. But the repo's default merge style is squash, which composes the message from the PR title and body, and the body spells both closers as markdown links (Closes [issue #110](...)) whose keyword match is not certain. No commit inmaincloses two issues, so there is no precedent to lean on. Confirm issue #110 actually closes on merge and close it by hand if it does not.Nits
mainis 74.cmd/vaultik/makefile_test.go:62"Sanity check" is the only occurrence of that term in the tree.Disclosure
Mutation testing was done on a throwaway copy of the head worktree, never on the PR tree; the PR worktree was confirmed clean afterwards, and nothing was committed, pushed or tagged. The host
make lintresult was not used for the verdict — the 0-issue figure above is the containerised run insidescript/cibuild. Thegomodguarddeprecation warning appeared as expected (issue #90).Correcting the PR body before merge, per the review's finding 1.
The body claims the two new
log.Inforecords "survive on stderr under--verboseeven in--jsonmode". That is false.internal/cli/prune.go:49setsQuiet: rootFlags.Quiet || opts.JSON, andinternal/log/log.go:56makescfg.Cron || cfg.Quiet→slog.LevelWarnthe first switch case, ahead of
DebugandVerbose. So--jsonpins the level to WARN and both
log.Infocalls are unreachable there.Measured:
--verbose pruneemits both;--verbose prune --jsonand--debug prune --jsonemit neither.The clause is struck. The permanent record — commit message,
TODO.md,the doc comment — never carried it and is accurate as written.
Merging anyway, for the reasons the reviewer gave: the inaccuracy is
confined to the PR body, and the underlying behaviour is inherited rather
than introduced.
PruneBlobs' ownlog.Infocalls are equally invisibleunder
--jsononmaintoday.But the net effect is worth stating plainly, because it is a real gap
rather than a cosmetic one: under
--json, removing a stale localrecord produces no signal in either stream. Not on stdout (correctly
gated), not on stderr (level-pinned to WARN), and the count is
deliberately absent from
PruneBlobsResult. Local index rows are deletedand a machine consumer has no way to know. Filed as a follow-up.
Also handling the review's finding 2:
Also closes #110.in markdown-linkform may not trip the auto-close parser, so I am putting a plain
Closes #110.in the squash message rather than relying on it.