Found while verifying issue #106 end to end.
Filing rather than fixing drive-by, for the same reason #106 was filed
out of #82: this is a different writer on a different code path, and it
is not the banner.
Problem
pruneLocalSnapshots (internal/vaultik/snapshot.go:845-878) is the
only stdout writer on a --json path with no --json awareness at all.
Every other one is gated — Prune's blob phase checks opts.JSON
before each of its five writes (internal/vaultik/prune.go:113-150), RemoveSnapshot returns early on opts.JSON
(internal/vaultik/snapshot.go:1042), remote info wraps its whole
human report in if !jsonOutput (internal/vaultik/info.go:216). This
function does not, and Prune calls it before the blob phase, so its
line lands ahead of the document:
$ vaultik --config /tmp/demo/config.yml prune --json
No stale local snapshots found.
{
"blobs_found": 1,
"blobs_deleted": 1,
"bytes_freed": 246
}
$ vaultik --config /tmp/demo/config.yml prune --json | jq .
jq: parse error: Invalid numeric literal at line 1, column 3
Three writes are affected, covering every branch of the function, so
there is no input that avoids it:
v.stdoutf("Removing stale local record: %s\n", id) (line 858)
v.printlnStdout("No stale local snapshots found.") (line 873)
v.stdoutf("Removed %d stale local snapshot record(s).\n", removed)
(line 875)
It reproduces identically on main before the #106 change and after
it. #106 removes the three banner lines; these remain.
-q does not suppress it either, so it is not covered by the
workaround #106 retired. printlnStdout and stdoutf write straight
to v.Stdout and never consult v.UI, which is what SetQuiet
affects. vaultik prune -q --json | jq fails today for this reason.
prune is the only affected command: snapshot list --json, snapshot verify --json, snapshot remove --json and remote info --json all pipe into jq cleanly once the banner is
gone. Verified against a real file:// destination store with a real
snapshot.
Expected
prune --json emits its PruneBlobsResult and nothing else, the same
contract issue #82
established for the logger and #106 for the banner.
Notes
The narrow fix is to thread opts.JSON into pruneLocalSnapshots the
way the surrounding code already does. Worth considering instead
whether these three lines want to be log.Info records — they are
progress narration, not output the caller asked for, and the logger is
already on stderr. That choice is what this issue needs decided; the
count of stale records is not currently in PruneBlobsResult, so if it
should survive into the document that is a third option.
Found while verifying
[issue #106](https://git.eeqj.de/sneak/vaultik/issues/106) end to end.
Filing rather than fixing drive-by, for the same reason #106 was filed
out of #82: this is a different writer on a different code path, and it
is not the banner.
## Problem
`pruneLocalSnapshots` (`internal/vaultik/snapshot.go:845-878`) is the
only stdout writer on a `--json` path with no `--json` awareness at all.
Every other one is gated — `Prune`'s blob phase checks `opts.JSON`
before each of its five writes (`internal/vaultik/prune.go:113-150`),
`RemoveSnapshot` returns early on `opts.JSON`
(`internal/vaultik/snapshot.go:1042`), `remote info` wraps its whole
human report in `if !jsonOutput` (`internal/vaultik/info.go:216`). This
function does not, and `Prune` calls it before the blob phase, so its
line lands ahead of the document:
```
$ vaultik --config /tmp/demo/config.yml prune --json
No stale local snapshots found.
{
"blobs_found": 1,
"blobs_deleted": 1,
"bytes_freed": 246
}
```
```
$ vaultik --config /tmp/demo/config.yml prune --json | jq .
jq: parse error: Invalid numeric literal at line 1, column 3
```
Three writes are affected, covering every branch of the function, so
there is no input that avoids it:
- `v.stdoutf("Removing stale local record: %s\n", id)` (line 858)
- `v.printlnStdout("No stale local snapshots found.")` (line 873)
- `v.stdoutf("Removed %d stale local snapshot record(s).\n", removed)`
(line 875)
## Not the same defect as #106, and not fixed by it
- It reproduces identically on `main` before the #106 change and after
it. #106 removes the three banner lines; these remain.
- `-q` does not suppress it either, so it is not covered by the
workaround #106 retired. `printlnStdout` and `stdoutf` write straight
to `v.Stdout` and never consult `v.UI`, which is what `SetQuiet`
affects. `vaultik prune -q --json | jq` fails today for this reason.
- `prune` is the only affected command: `snapshot list --json`,
`snapshot verify --json`, `snapshot remove --json` and
`remote info --json` all pipe into `jq` cleanly once the banner is
gone. Verified against a real `file://` destination store with a real
snapshot.
## Expected
`prune --json` emits its `PruneBlobsResult` and nothing else, the same
contract [issue #82](https://git.eeqj.de/sneak/vaultik/issues/82)
established for the logger and #106 for the banner.
## Notes
The narrow fix is to thread `opts.JSON` into `pruneLocalSnapshots` the
way the surrounding code already does. Worth considering instead
whether these three lines want to be `log.Info` records — they are
progress narration, not output the caller asked for, and the logger is
already on stderr. That choice is what this issue needs decided; the
count of stale records is not currently in `PruneBlobsResult`, so if it
should survive into the document that is a third option.
Implementing this together with issue #110 as one PR
against main; they are unrelated in the code but both small.
The decision this issue asks for
Of the three options in the description I am taking the first — thread opts.JSON into the function — and explicitly rejecting the other two.
Not log.Info. The logger's default level is slog.LevelWarn
(internal/log/log.go:68), so moving these three lines to log.Info
would not relocate them 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; a user who runs prune with no
flags should see that it happened, and making it visible only under --verbose is a behaviour regression rather than a stream fix.
Not PruneBlobsResult. Every field of that struct is blob-scoped
(blobs_found, blobs_deleted, blobs_failed, bytes_freed) and it
is the result of the blob phase, which runs after this reconciliation.
Adding a stale-record count to it would make the document's name
inaccurate and would change a published --json schema as a side
effect of a stream-hygiene fix. If the count should be
machine-readable, the right shape is a prune document covering both
phases, and that is a separate design question, not this patch.
So: CleanupLocalSnapshots takes opts *PruneOptions, symmetric with
its sibling phase PruneBlobs(opts), and each of the three writes is
gated on !opts.JSON exactly as prune.go:113-150 already gates its
own. The two informational events additionally get log.Info records,
mirroring PruneBlobs, which already logs and prints in parallel.
Tests
Failing-before, at two layers:
internal/vaultik: CleanupLocalSnapshots with JSON: true leaves
stdout empty in both branches (stale records present, and none), and
with JSON: false still emits all three lines — so the guard cannot
be satisfied by deleting the output.
internal/cli: prune --json run end to end through Entry, cobra
and fx over the process's real stdout descriptor against a
hermetic file:// store, asserting stdout holds exactly one JSON
document. Both branches, by seeding the index database with a
snapshot that has no remote manifest. This is the assertion the
issue makes — vaultik prune --json | jq . with no other flags —
with the pipe replaced by a decoder.
Plus a manual end-to-end run of the built binary against a real config
and file:// destination store, in both branches, and a re-check that
the other four --json commands still pipe cleanly.
README.md:142 will be adjusted: it currently describes the banner as
"the other thing that writes to stdout", which this issue contradicts.
## Plan
Implementing this together with
[issue #110](https://git.eeqj.de/sneak/vaultik/issues/110) as one PR
against `main`; they are unrelated in the code but both small.
### The decision this issue asks for
Of the three options in the description I am taking the first — thread
`opts.JSON` into the function — and explicitly rejecting the other two.
**Not `log.Info`.** The logger's default level is `slog.LevelWarn`
(`internal/log/log.go:68`), so moving these three lines to `log.Info`
would not relocate them 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; a user who runs `prune` with no
flags should see that it happened, and making it visible only under
`--verbose` is a behaviour regression rather than a stream fix.
**Not `PruneBlobsResult`.** Every field of that struct is blob-scoped
(`blobs_found`, `blobs_deleted`, `blobs_failed`, `bytes_freed`) and it
is the result of the blob phase, which runs after this reconciliation.
Adding a stale-record count to it would make the document's name
inaccurate and would change a published `--json` schema as a side
effect of a stream-hygiene fix. If the count should be
machine-readable, the right shape is a prune document covering both
phases, and that is a separate design question, not this patch.
So: `CleanupLocalSnapshots` takes `opts *PruneOptions`, symmetric with
its sibling phase `PruneBlobs(opts)`, and each of the three writes is
gated on `!opts.JSON` exactly as `prune.go:113-150` already gates its
own. The two informational events additionally get `log.Info` records,
mirroring `PruneBlobs`, which already logs and prints in parallel.
### Tests
Failing-before, at two layers:
1. `internal/vaultik`: `CleanupLocalSnapshots` with `JSON: true` leaves
stdout empty in both branches (stale records present, and none), and
with `JSON: false` still emits all three lines — so the guard cannot
be satisfied by deleting the output.
2. `internal/cli`: `prune --json` run end to end through `Entry`, cobra
and fx over the process's real stdout descriptor against a
hermetic `file://` store, asserting stdout holds exactly one JSON
document. Both branches, by seeding the index database with a
snapshot that has no remote manifest. This is the assertion the
issue makes — `vaultik prune --json | jq .` with no other flags —
with the pipe replaced by a decoder.
Plus a manual end-to-end run of the built binary against a real config
and `file://` destination store, in both branches, and a re-check that
the other four `--json` commands still pipe cleanly.
`README.md:142` will be adjusted: it currently describes the banner as
"the other thing that writes to stdout", which this issue contradicts.
Implemented in PR #111, on branch fix-prune-json-and-build.
What was built
CleanupLocalSnapshots now takes *PruneOptions and gates all three
writes on !opts.JSON, matching PruneBlobs — its sibling phase, which
already takes the same struct — and RemoveSnapshot and remote info.
The two informational events are additionally emitted as log.Info
records, the pattern PruneBlobs already uses, so they still exist on
stderr for anyone running --verbose.
The decision the issue asked for, restated with the reasons in full in
the PR body: the log.Info-only option was rejected because the
logger's default level is slog.LevelWarn, so it would not move those
lines to stderr, it would delete them from a plain vaultik prune —
narrating the deletion of local index rows only under --verbose is a
behaviour regression, not a stream fix. The PruneBlobsResult option
was rejected because every field of that struct is blob-scoped and it is
produced by the phase that runs after this reconciliation; adding a
stale-record count would change a published --json schema as a side
effect. Per the issue's request, that is flagged rather than silently
added — if the count should be machine-readable, the right shape is a
prune document covering both phases, which is its own design question.
How it was verified
Against a real config and a file:// destination store with a real
snapshot, using the binary from make build, with no other flags and
no -q:
No stale records: vaultik prune --json | jq . exits 0. od -c on
the raw stdout shows the document and nothing else.
Stale record present (destination metadata/ removed under a live
local record): vaultik prune --json | jq . exits 0, {"blobs_found": 1, "blobs_deleted": 1, "bytes_freed": 229}, and the
record is gone from the index afterwards.
Without --json, all three lines are still printed — confirmed on the
same store: "Removing stale local record: ...", "Removed 1 stale local
snapshot record(s).", and "No stale local snapshots found." on the
other branch.
The other four commands still pipe cleanly on the same binary: snapshot list --json, snapshot verify <id> --json, snapshot remove <id> --json, remote info --json, jq exit 0
for each.
Tests, each confirmed to fail with the fix reverted rather than assumed
to: unit coverage of CleanupLocalSnapshots asserting stdout stays
empty under --json in all three branches (stale records, none, empty
index) and that the human output is retained without it, so the guard
cannot be satisfied by deleting the lines; plus an end-to-end prune --json through Entry, cobra and fx over the process's real
stdout descriptor against a hermetic file:// store, asserting exactly
one JSON document, in both branches.
make check green; script/cibuild exit 0 with the check layers
confirmed to have executed rather than replayed from cache.
Implemented in
[PR #111](https://git.eeqj.de/sneak/vaultik/pulls/111), on branch
`fix-prune-json-and-build`.
## What was built
`CleanupLocalSnapshots` now takes `*PruneOptions` and gates all three
writes on `!opts.JSON`, matching `PruneBlobs` — its sibling phase, which
already takes the same struct — and `RemoveSnapshot` and `remote info`.
The two informational events are additionally emitted as `log.Info`
records, the pattern `PruneBlobs` already uses, so they still exist on
stderr for anyone running `--verbose`.
The decision the issue asked for, restated with the reasons in full in
the PR body: the `log.Info`-only option was rejected because the
logger's default level is `slog.LevelWarn`, so it would not move those
lines to stderr, it would delete them from a plain `vaultik prune` —
narrating the deletion of local index rows only under `--verbose` is a
behaviour regression, not a stream fix. The `PruneBlobsResult` option
was rejected because every field of that struct is blob-scoped and it is
produced by the phase that runs after this reconciliation; adding a
stale-record count would change a published `--json` schema as a side
effect. Per the issue's request, that is flagged rather than silently
added — if the count should be machine-readable, the right shape is a
prune document covering both phases, which is its own design question.
## How it was verified
Against a real config and a `file://` destination store with a real
snapshot, using the binary from `make build`, **with no other flags and
no `-q`**:
- No stale records: `vaultik prune --json | jq .` exits 0. `od -c` on
the raw stdout shows the document and nothing else.
- Stale record present (destination `metadata/` removed under a live
local record): `vaultik prune --json | jq .` exits 0,
`{"blobs_found": 1, "blobs_deleted": 1, "bytes_freed": 229}`, and the
record is gone from the index afterwards.
- Without `--json`, all three lines are still printed — confirmed on the
same store: "Removing stale local record: ...", "Removed 1 stale local
snapshot record(s).", and "No stale local snapshots found." on the
other branch.
- The other four commands still pipe cleanly on the same binary:
`snapshot list --json`, `snapshot verify <id> --json`,
`snapshot remove <id> --json`, `remote info --json`, `jq` exit 0
for each.
Tests, each confirmed to fail with the fix reverted rather than assumed
to: unit coverage of `CleanupLocalSnapshots` asserting stdout stays
empty under `--json` in all three branches (stale records, none, empty
index) and that the human output is retained without it, so the guard
cannot be satisfied by deleting the lines; plus an end-to-end
`prune --json` through `Entry`, cobra and fx over the process's real
stdout descriptor against a hermetic `file://` store, asserting exactly
one JSON document, in both branches.
`make check` green; `script/cibuild` exit 0 with the check layers
confirmed to have executed rather than replayed from cache.
Blocking a user prevents them from interacting with repositories, such as opening or commenting on pull requests or issues. Learn more about blocking a user.
Found while verifying
issue #106 end to end.
Filing rather than fixing drive-by, for the same reason #106 was filed
out of #82: this is a different writer on a different code path, and it
is not the banner.
Problem
pruneLocalSnapshots(internal/vaultik/snapshot.go:845-878) is theonly stdout writer on a
--jsonpath with no--jsonawareness at all.Every other one is gated —
Prune's blob phase checksopts.JSONbefore each of its five writes (
internal/vaultik/prune.go:113-150),RemoveSnapshotreturns early onopts.JSON(
internal/vaultik/snapshot.go:1042),remote infowraps its wholehuman report in
if !jsonOutput(internal/vaultik/info.go:216). Thisfunction does not, and
Prunecalls it before the blob phase, so itsline lands ahead of the document:
Three writes are affected, covering every branch of the function, so
there is no input that avoids it:
v.stdoutf("Removing stale local record: %s\n", id)(line 858)v.printlnStdout("No stale local snapshots found.")(line 873)v.stdoutf("Removed %d stale local snapshot record(s).\n", removed)(line 875)
Not the same defect as #106, and not fixed by it
mainbefore the #106 change and afterit. #106 removes the three banner lines; these remain.
-qdoes not suppress it either, so it is not covered by theworkaround #106 retired.
printlnStdoutandstdoutfwrite straightto
v.Stdoutand never consultv.UI, which is whatSetQuietaffects.
vaultik prune -q --json | jqfails today for this reason.pruneis the only affected command:snapshot list --json,snapshot verify --json,snapshot remove --jsonandremote info --jsonall pipe intojqcleanly once the banner isgone. Verified against a real
file://destination store with a realsnapshot.
Expected
prune --jsonemits itsPruneBlobsResultand nothing else, the samecontract issue #82
established for the logger and #106 for the banner.
Notes
The narrow fix is to thread
opts.JSONintopruneLocalSnapshotstheway the surrounding code already does. Worth considering instead
whether these three lines want to be
log.Inforecords — they areprogress narration, not output the caller asked for, and the logger is
already on stderr. That choice is what this issue needs decided; the
count of stale records is not currently in
PruneBlobsResult, so if itshould survive into the document that is a third option.
Plan
Implementing this together with
issue #110 as one PR
against
main; they are unrelated in the code but both small.The decision this issue asks for
Of the three options in the description I am taking the first — thread
opts.JSONinto the function — and explicitly rejecting the other two.Not
log.Info. The logger's default level isslog.LevelWarn(
internal/log/log.go:68), so moving these three lines tolog.Infowould not relocate them to stderr, it would delete them from a plain
vaultik prune. "Removing stale local record: <id>" narrates thedeletion of rows from the local index; a user who runs
prunewith noflags should see that it happened, and making it visible only under
--verboseis a behaviour regression rather than a stream fix.Not
PruneBlobsResult. Every field of that struct is blob-scoped(
blobs_found,blobs_deleted,blobs_failed,bytes_freed) and itis the result of the blob phase, which runs after this reconciliation.
Adding a stale-record count to it would make the document's name
inaccurate and would change a published
--jsonschema as a sideeffect of a stream-hygiene fix. If the count should be
machine-readable, the right shape is a prune document covering both
phases, and that is a separate design question, not this patch.
So:
CleanupLocalSnapshotstakesopts *PruneOptions, symmetric withits sibling phase
PruneBlobs(opts), and each of the three writes isgated on
!opts.JSONexactly asprune.go:113-150already gates itsown. The two informational events additionally get
log.Inforecords,mirroring
PruneBlobs, which already logs and prints in parallel.Tests
Failing-before, at two layers:
internal/vaultik:CleanupLocalSnapshotswithJSON: trueleavesstdout empty in both branches (stale records present, and none), and
with
JSON: falsestill emits all three lines — so the guard cannotbe satisfied by deleting the output.
internal/cli:prune --jsonrun end to end throughEntry, cobraand fx over the process's real stdout descriptor against a
hermetic
file://store, asserting stdout holds exactly one JSONdocument. Both branches, by seeding the index database with a
snapshot that has no remote manifest. This is the assertion the
issue makes —
vaultik prune --json | jq .with no other flags —with the pipe replaced by a decoder.
Plus a manual end-to-end run of the built binary against a real config
and
file://destination store, in both branches, and a re-check thatthe other four
--jsoncommands still pipe cleanly.README.md:142will be adjusted: it currently describes the banner as"the other thing that writes to stdout", which this issue contradicts.
Implemented in
PR #111, on branch
fix-prune-json-and-build.What was built
CleanupLocalSnapshotsnow takes*PruneOptionsand gates all threewrites on
!opts.JSON, matchingPruneBlobs— its sibling phase, whichalready takes the same struct — and
RemoveSnapshotandremote info.The two informational events are additionally emitted as
log.Inforecords, the pattern
PruneBlobsalready uses, so they still exist onstderr for anyone running
--verbose.The decision the issue asked for, restated with the reasons in full in
the PR body: the
log.Info-only option was rejected because thelogger's default level is
slog.LevelWarn, so it would not move thoselines to stderr, it would delete them from a plain
vaultik prune—narrating the deletion of local index rows only under
--verboseis abehaviour regression, not a stream fix. The
PruneBlobsResultoptionwas rejected because every field of that struct is blob-scoped and it is
produced by the phase that runs after this reconciliation; adding a
stale-record count would change a published
--jsonschema as a sideeffect. Per the issue's request, that is flagged rather than silently
added — if the count should be machine-readable, the right shape is a
prune document covering both phases, which is its own design question.
How it was verified
Against a real config and a
file://destination store with a realsnapshot, using the binary from
make build, with no other flags andno
-q:vaultik prune --json | jq .exits 0.od -conthe raw stdout shows the document and nothing else.
metadata/removed under a livelocal record):
vaultik prune --json | jq .exits 0,{"blobs_found": 1, "blobs_deleted": 1, "bytes_freed": 229}, and therecord is gone from the index afterwards.
--json, all three lines are still printed — confirmed on thesame store: "Removing stale local record: ...", "Removed 1 stale local
snapshot record(s).", and "No stale local snapshots found." on the
other branch.
snapshot list --json,snapshot verify <id> --json,snapshot remove <id> --json,remote info --json,jqexit 0for each.
Tests, each confirmed to fail with the fix reverted rather than assumed
to: unit coverage of
CleanupLocalSnapshotsasserting stdout staysempty under
--jsonin all three branches (stale records, none, emptyindex) and that the human output is retained without it, so the guard
cannot be satisfied by deleting the lines; plus an end-to-end
prune --jsonthroughEntry, cobra and fx over the process's realstdout descriptor against a hermetic
file://store, asserting exactlyone JSON document, in both branches.
make checkgreen;script/cibuildexit 0 with the check layersconfirmed to have executed rather than replayed from cache.