All checks were successful
check / check (pull_request) Successful in 2m57s
script/cibuild was a bare `docker build .` with no cache control. The
Dockerfile does `COPY . .` and then `RUN make fmt-check` / `RUN make
lint` in the lint stage and `COPY . .` / `RUN make test` in the builder
stage. On an unchanged tree Docker served those RUN layers from cache,
so the checks never executed, and the build still exited 0 -- the exit
code, which is the one signal automation trusts, was wrong, and wrong
in the direction that matters: the longer a branch sits unchanged, the
more likely its "verification" is a replay, which is exactly its state
just before a merge.
The fix is an `ARG CHECK_EPOCH` declared immediately above the check
RUNs, with script/cibuild passing a fresh value on every invocation.
ARG scope is per-stage in Docker, so the lint stage and the builder
stage each declare their own; covering only one would leave half the
gate fake.
Placement is the substance of the change. The ARG sits below the
`apk add`, `COPY go.mod go.sum`, and `go mod download` layers in both
stages, so only the check layers are invalidated: earlier and every
build would be cold, later and the checks would stay cached.
The epoch is assigned to its own variable rather than substituted
inline into the --build-arg:
epoch="$(date +%s)"
docker build --build-arg CHECK_EPOCH="$epoch" .
Under `set -eu` a command substitution that fails inside an argument
does not abort the script. Inline, a failing `date` would leave
CHECK_EPOCH an empty string; an empty string is a constant; and a
constant CHECK_EPOCH is precisely the cached-check false green this
commit exists to eliminate -- so the guard would have carried a silent
path to the defect it guards against. As a bare assignment, `set -e`
aborts before any build starts.
The guarantee is conditional, and README.md and the Dockerfile now say
so instead of claiming the check layers can never be cached. They are
keyed on CHECK_EPOCH, so they re-run for any value not yet built
against this tree -- but a build that omits --build-arg gets the empty
default, and on an unchanged tree every build after the first then
replays them, executes nothing, and exits 0. That state was produced
by measurement rather than reasoned about. Issue #91 tracks the
upstream hardening that would make the missing-arg case fail loudly,
along with the expanded ARG form, a per-invocation epoch, and
script/docker.
The bare unreferenced ARG form is kept deliberately, not because it
matches upstream -- upstream has since settled on expanding the value
into the check command. A declared-but-unreferenced ARG does enter
BuildKit's cache key, which is measured on this host rather than
assumed, and upstream records that repos on the bare form need no
rework. Moving to the expanded form is hardening, tracked in #91.
The script/cibuild header comment claimed the Dockerfile runs
script/check via make check. It does not: it runs make fmt-check and
make lint in the lint stage and make test in the builder stage.
Corrected.
Measurements are recorded once, in the verification comment on PR #89:
a back-to-back script/cibuild pair on an unchanged tree, and the
counterfactual that withholds --build-arg and reproduces the original
false green on its second run. They are deliberately not restated here
or in TODO.md, so there is a single record that cannot disagree with
itself.
.golangci.yml is unchanged (sha256 021cc83f4e6f...643346bcb), as is the
lint-stage FROM line that is the single source of truth for the linter
version, script/lint's pinned-image logic, and
.gitea/workflows/check.yml, whose only step is script/cibuild.
8.5 KiB
8.5 KiB
Workflow
- branch (from
main) - do the work in Next Step
- move Next Step to the top of Completed Steps
- move the top item of Future Steps into Next Step
- commit (
TODO.mdchanges in the same commit as the work) - merge to
mainif the branch is not protected, otherwise open a PR - push
Status
pre-1.0
Next Step
Triage the stale remote branches (issue #71): for each, merge the work or delete the branch.
Completed Steps
- 2026-08-09: Stopped
script/cibuildfrom reporting a green it did not earn (issue #85). A baredocker build .let Docker serve the check layers from the layer cache whenever the tree had not changed: the checks never executed and the build still exited 0. The fix is anARG CHECK_EPOCHdeclared immediately above the checkRUNs in both the lint stage and the builder stage (ARGscope is per-stage, so each declares its own), withscript/cibuildassigningepoch="$(date +%s)"and passing--build-arg CHECK_EPOCH="$epoch". The assignment is separate on purpose: underset -eua command substitution that fails inside an argument does not abort the script, which would leave an empty constantCHECK_EPOCHand restore the very false green being fixed. Placement is the rest of the point — theARGsits below theapk add,COPY go.mod go.sum, andgo mod downloadlayers, so only the checks are invalidated and the dependency layers still cache. The guarantee is conditional on a fresh value rather than absolute: a baredocker build .gets an emptyCHECK_EPOCHand can still serve the check layers from cache, whichREADME.mdand theDockerfilenow say plainly, with issue #91 tracking the upstream hardening (expandedARGform, unset guard, per-invocation epoch,script/docker) that would close it. Verified by re-running the reproduction plus the withheld---build-argcounterfactual; the measurements are recorded once, in the PR #89 verification comment, rather than restated here..golangci.yml, the lint-stageFROMline and its digest,script/lint, and.gitea/workflows/check.ymlare all untouched. - 2026-08-09: Corrected the
Vaultik.UIdoc comment (issue #84). It claimed the cli layer replaces the writer with a discarding one in--cronmode; the actual mechanism isUI.SetQuiet(true)insetupGlobals, which drops Begin/Complete/Info/Notice/Detail/ Progress/Banner but still emits Warning and Error. The--cronline inREADME.mdsaid "Silent unless error", which understated what survives, and now names warnings too. The other--croncomments (internal/log/log.go,internal/cli/snapshot.go,internal/vaultik/snapshot.go) were audited and already accurate. Comments and docs only, no behavior change. - 2026-08-09: Made
snapshot listlist the destination store without the private key (issue #64). The listing is now the union of the local index and a single streamed listing of themetadata/prefix, with noage_secret_keygate — the manifest is unencrypted, so a host holding only the public key can enumerate its own backups and a host that lost its local index can still see them. A remote-only snapshot's hostname and name are deliberately not recovered (they are not recoverable without the private key, and making them so would undo the privacy property tracked in issue #81); such rows are labelled by an abbreviation of their remote key and carry the real timestamp and compressed size from the manifest, with<remote only>in the two columns that require the local index. Local-only snapshots are reported as drift, and the hint now namesvaultik prune, which exists, instead ofvaultik snapshot cleanup, which does not.reportRemoteDriftcollapsed into the merged view. Every remote manifest read in the codebase now goes throughdownloadManifestByKey, so issue #81 has one call site to change. Review rework: snapshot timestamps now normalize to UTC inscanSnapshotRows, the one place they enter the domain, so the merged TIMESTAMP column cannot show local time for a locally tracked row and UTC for a remote-only row on a non-UTC host;GetIncompleteByHostnamewas folded onto that same scanner.--jsonnow reports the unreadable-manifest count and the 1000-row truncation on stderr instead of returning a silently short document (the document's shape is unchanged). The two per-snapshotlog.Warncalls on the listing path now route through the same JSON-aware writer as the existing workaround, so one corrupt manifest can no longer put a log line on stdout ahead of the document and break| jq— still a local workaround pending issue #82. Verified withscript/cibuildand with an uncachedmake check(0 issues., no cached test packages), plus end to end against afile://destination with no secret key present. - 2026-08-09: Closed the gap between
make lintand CI (issue #78).script/lintnow runs the digest-pinnedgolangci-lintimage taken from theDockerfilelint stage, which is the single source of truth for the linter version; the duplicate pin in theMakefiledepstarget and the unpinnedgolangci-lintinstall inscript/bootstrapare gone. Agolangci-lintonPATHis used only when its version is exactly the pinned one (which is how the lint stage runs it inside the container); anything else goes through Docker, and a missing or unreachable Docker daemon is a hard error rather than a silent fallback.make checkis therefore now as trustworthy asscript/cibuild. - 2026-08-09: Finished the lint remediation under the canonical
.golangci.yml(issue #61, which also unblocks issue #59). The remaining findings were fixed behavior-preservingly:wsl_v5whitespace,sqlclosecheck, andprealloc. Thesqlclosechecksites now closesql.Rowsin a deferred closure instead of via theCloseRowshelper, which the linter could not see through. Only therevivepackage-name findings remain suppressed, with per-site//nolintdirectives; the package-rename question behind them is tracked in issue #76. Verified withscript/cibuild, which exits 0 — that is the only trustworthy gate, becausescript/lintruns whatevergolangci-linthappens to be onPATHrather than the pinned v2.12.2 that CI and theDockerfileuse, somake checkcan report green on findings CI still fails. That tooling gap is tracked in issue #78. - 2026-08-09: The earlier next step "reconcile the uncommitted
ARCHITECTURE.mdedits onmain" needed no work: the working tree is clean andARCHITECTURE.mdis committed onmain. - 2026-08-07: Updated golangci-lint to v2.12.2 everywhere it is pinned
(
Dockerfilelint stage,Makefiledeps target), replaced.golangci.ymlwith the canonical config (v2 schema,default: all), and remediated the bulk of the lint findings it surfaced (issue #61): behavior-preserving fixes across every package, 2,990 findings down to 80.make testandmake fmt-checkwere green at that point butmake lintwas still red; the commit message claimingmake checkwas green was wrong. - 2026-08-07: Added the standard
.golangci.ymland.editorconfig(issue #59); lint findings under the new config are tracked in issue #61.script/bootstrapnow installs sqlite3 (needed by tests). - 2026-07-07 Adopted scripts-to-rule-them-all:
script/entrypoints, Makefile shims, README Entrypoints section - 2026-07-02: Consolidated CLI verbs, retired overlapping commands; bound the local index to its backup destination URL.
- 2026-06-28: snapshot rm now removes metadata only and prints the prune command; restore skips chown when running as non-root.
- 2026-06-26: Snapshot IDs hashed at the storage boundary; snapshot list made resilient to bad remote entries.
- 2026-06-24: Collapsed snapshot prune into vaultik prune; restore streams blobs to disk and restores files in blob-locality order; cron output fixes.
- 2026-06-17: Restore overhaul: ReadAt chunk reads from cached blobs, reference-counted blob sweeper, integration tests; new internal/ui output layer, banner, and progress lines.
- 2025-12-18: Added ARCHITECTURE.md and godoc coverage for exported API.
- 2025-07-26: End-to-end integration tests; manifest format refactor; renamed backup to snapshot; afero filesystem abstraction.
- 2025-07-20: Initial design and implementation: cobra + fx CLI skeleton, SQLite index database, UUID blob storage with streaming chunking.
Future Steps
- Define remaining scope for a first tagged release and cut v0.1.0.