No tag could be cut from this repo at all. Three independent blockers.
goreleaser was configured for GitHub while the repo lives on Gitea:
.goreleaser.yaml had a release: block but no gitea_urls:, so goreleaser
defaulted to the GitHub API and a release would have failed or published
somewhere nobody is looking. It now points at https://git.eeqj.de/api/v1.
The version was a hardcoded Makefile constant, VERSION := 1.0.0-rc.1, so
every local build claimed to be a release candidate that had never been
tagged and did not exist, while git tag -l was empty and internal/globals
defaulted to dev. The version now comes from git, via the new
script/version: the exact tag with a leading v stripped when HEAD is on
one (so a make build and a goreleaser build of the same commit report the
same string, and it matches the archive names), otherwise dev-<12-char
sha>, with -dirty appended in either case when tracked files are
modified. Untracked files are not counted, matching git describe --dirty.
goreleaser's snapshot template gets the same treatment: it was
{{ incpatch .Version }}-next, which manufactures a release number from
the last tag and, with no tags at all, from goreleaser's fabricated
v0.0.0.
That change had one non-obvious consequence. internal/cli/version.go
gated its "this is a development build" notice on the version being
exactly "dev", so as soon as untagged builds carried a commit sha the
notice would have gone silent and an unreleased binary would have read as
a release. The gate is now globals.IsDevVersion, a predicate over a
string rather than a comparison against a global so that it can be
tested, and it is tested at the boundary that matters: dev-<sha> and its
-dirty variant are development builds, 1.0.0-dev and 1.0.0-rc.1 are not.
The command writes to cmd.OutOrStdout() so its output can be asserted on
at all.
Releases now come from CI rather than a workstation: a tag-triggered
.gitea/workflows/release.yml, with fetch-depth: 0 because a shallow
checkout has no tags and would silently mislabel the release, and with
the RELEASE_TOKEN repository secret passed as GITEA_TOKEN (documented in
README.md; the runner's automatic token is deliberately not used, since
it is not guaranteed to carry release write scope). script/release unsets
any GITHUB_TOKEN or GITLAB_TOKEN it finds, because goreleaser picks its
forge from whichever token variable is set and refuses to run when it
sees more than one -- an unrelated runner token must not get to decide
where these artifacts are published.
make release and make release-snapshot were the last two Makefile targets
that were not shims; they now call script/release and
script/release-snapshot, which resolve goreleaser the way script/lint
resolves the linter -- a PATH binary is accepted only at the pinned
version, never as a silent fallback. script/bootstrap installs it from a
sha256-verified GitHub release archive per REPO_POLICIES.md, through a
separate script/install-goreleaser: separate because script/bootstrap
hard-fails without a usable Docker daemon by design, and the release
runner needs goreleaser without needing Docker. dist/ and .tool/ are
gitignored and excluded from the Docker build context.
Verified by running it: make release-snapshot produces the four
linux,darwin x amd64,arm64 archives plus checksums.txt, and the binary
from dist/ reports dev-<sha> with the development-build notice. Tag
handling was exercised in a throwaway repository; no tag was created
here, since that is the owner's call. Signing, SBOM, reproducible builds,
shell completions and a man page remain out of scope.
Closes#80.
script/lint pointed GOLANGCI_LINT_CACHE at a path shared by every
worktree of this repo. Two worktrees have identical Go file contents, so
their cache keys collided and one tree's stored findings replayed for
another, paths included - observed as 231 findings all citing another
session's worktree, with no parallel-runner message to signal it. The
failure is symmetric and only one direction is loud: a clean tree failed
by a dirty sibling gets investigated, a dirty tree passed by a clean
sibling does not.
The cache is now keyed per worktree on a digest of $ROOT, and remains
persistent. Independently of that, script/lint-audit inspects every run's
output and fails the run if any finding cites a path outside the tree
being linted. That guard is the load-bearing part: it converts a silent
unearned green into a hard error regardless of how the cache is keyed. It
is deliberately built so it can never certify a pass, only reject, so it
cannot itself become a gate that reports green.
The native path was gated on version equality alone, which admitted a
locally installed matching binary and bypassed the digest pin. It now
requires VAULTIK_LINT_IN_CONTAINER=1, set only by the Dockerfile lint
stage, in addition to version equality. /.dockerenv was rejected as the
signal because dockerd creates it for `docker run` but not reliably
during a BuildKit `docker build`, which is the case the exception exists
for. A version mismatch inside the container is now a hard error rather
than a fall-through.
This mattered more than the issue supposed: on this host a matching
golangci-lint exists on PATH, so script/lint was taking the native path
and linting against the global cache without ever running the pinned
image. That is the likely root of the observed contamination, and it is
closed here rather than mitigated.
The parallel-runner error is retried rather than reported. It is not a
lint result, and surfacing it as a non-zero exit is indistinguishable to
a caller from real findings; exhausted retries fail saying the tree was
never analysed. Note that a private cache alone does not remove lock
contention - measured with two concurrent runs using separate cache
directories.
script/bootstrap no longer reports success on a machine that cannot run
the gate: docker is now required by lint, check and precommit, so a
missing binary or unreachable daemon is a hard failure naming what will
not work.
script/lint ran bare golangci-lint from PATH while CI and the Dockerfile
pinned v2.12.2 by digest, so make lint and CI could disagree about
findings. That drift ran both directions: it produced two false green
claims during the lint remediation, and on an ambient 2.10.1 it also
reported four gosec findings on a tree CI linted clean.
script/lint now extracts the image reference - tag and digest - from the
Dockerfile lint stage FROM line and runs that exact image under docker.
The Dockerfile FROM line is the single source of truth for the linter
version; the duplicate pins in the Makefile deps target and in
script/bootstrap are removed rather than kept in sync.
A golangci-lint on PATH is used only when its version exactly equals the
pin, which is what makes the in-container lint stage work (the Dockerfile
runs make lint inside the pinned image, where there is no docker daemon).
Any other version, or none, goes through docker. When docker is
unavailable the script fails with an actionable message and never falls
back to a different linter version.
script/lint-fix delegates to script/lint --fix so autofixes come from the
pinned linter too. The container mounts persistent build and module
caches and runs as the invoking uid/gid.
Verified by reinstating the four historical nolint directives that 2.10.1
requires and 2.12.2 reports as unused: the old script passed on that tree
and the new one fails with four nolintlint findings.