No tag could be cut from this repo at all. Three independent blockers, one commit.
What changed, per the definition of done
1. goreleaser now targets Gitea..goreleaser.yaml gains a top-level gitea_urls: block (api: https://git.eeqj.de/api/v1, download: https://git.eeqj.de). Without it goreleaser talks to the GitHub API by default, which is why goreleaser release could never have worked here.
2. Version comes from git, not from a constant. New script/version is the single source of truth:
HEAD exactly on a tag -> the tag with a leading v stripped
otherwise -> dev-<12-char sha>
either, with modified tracked files -> -dirty suffix (untracked files are not counted, matching git describe --dirty)
Makefile:4VERSION := 1.0.0-rc.1 becomes VERSION := $(shell script/version). The v is stripped so a make build and a goreleaser build of one commit report the same string and it matches the archive name: tag v1.0.0 -> vaultik 1.0.0 -> vaultik_1.0.0_linux_amd64.tar.gz. Say the word if you would rather see the literal v1.0.0 in vaultik version and I will switch it.
$(shell) discards exit status, so the Makefile also refuses to build at all when script/version cannot be run: it stops with an error rather than stamping an empty version into a binary. IsDevVersion("") is true for the same reason, one layer down, for a binary linked by something other than the Makefile — nothing that knows its version reports no version, so an empty version means the stamping failed and the build is not a release.
The snapshot path was fabricating a version too: snapshot.version_template was {{ incpatch .Version }}-next, which invents a release number from the last tag and, with no tags at all, from goreleaser's fabricated v0.0.0. It now emits the same honest dev-<sha>.
That change had one non-obvious consequence worth reviewing: internal/cli/version.go gated its "this is a development build" notice on globals.Version == "dev" exactly. The moment untagged builds started carrying a commit sha, that 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 package global specifically so it is testable, and it is tested at the boundary: dev, dev-<sha>, dev-<sha>-dirty and "" are development builds; 1.0.0, 1.0.0-rc.1, 1.0.0-dev and developer are not. NewVersionCommand writes to cmd.OutOrStdout() so the output can be asserted on at all.
3. Nothing contradicts anything about the version any more.Makefile derives from git, internal/globals keeps its honest dev/unknown defaults, and TODO.md no longer says "cut v0.1.0" next to a Makefile baking 1.0.0-rc.1 and a milestone named 1.0.0. Its Next Step now names one version target.
4. Tag-triggered release workflow, with a pinned toolchain..gitea/workflows/release.yml fires on v* tags, with fetch-depth: 0 because a shallow checkout has no tags and would silently mislabel the release.
Every input to that job is pinned by sha. actions/checkout uses the same pin as check.yml. actions/setup-go is pinned at 40f1582b2485089dde7abd97c1529aa768e1baff (v5.6.0, node20, matching the node20 checkout already in use) and reads its version from go.mod rather than restating it, the way script/lint reads the linter version out of the DockerfileFROM line. That step is load-bearing: goreleaser is not a compiler, it shells out to go for the before: hook and all four cross-compiles, and check.yml does all of its work inside the digest-pinned images, so without it a tag either fails at the before-hook or ships binaries built by whatever unpinned Go the runner happens to carry — the one unpinned thing in an otherwise hash-pinned release path.
The required secret is RELEASE_TOKEN: a Gitea access token with write:repository scope on an account that can publish releases here. It is passed to goreleaser as GITEA_TOKEN, and documented in the new README.md "releasing" section. The runner's automatic token is deliberately not used — it is not guaranteed to carry release write scope. Relatedly, script/releaseunsets any GITHUB_TOKEN/GITLAB_TOKEN it finds before publishing: goreleaser picks its forge from whichever token variable is set and refuses to run when it sees more than one, and an unrelated runner-provided token must not get to decide where these artifacts land.
5. bootstrap installs goreleaser; the last two non-shim targets became shims.make release -> script/release, make release-snapshot -> script/release-snapshot. Both resolve goreleaser exactly the way script/lint resolves the linter: a binary on PATH is accepted only when it reports the pinned version, otherwise .tool/bin, otherwise a loud failure naming the installer — never a silent fallback to a different version.
The install is script/install-goreleaser: a specific GitHub release archive (v2.17.1) verified against a hardcoded sha256 per REPO_POLICIES.md, no curl | sh and no @latest. It is a separate script rather than an inline block in script/bootstrap on purpose: script/bootstrap hard-fails on a machine without a usable Docker daemon (deliberately, since Docker gates script/lint), while the release runner needs goreleaser and does not need Docker. One script, two callers, one pin. script/bootstrap calls it; the workflow calls it directly.
dist/ and .tool/ are gitignored, and both are added to .dockerignore so a local snapshot build does not push a few hundred MB of binaries into every subsequent Docker build context.
6. make release-snapshot works end to end. Four archives plus checksums.
Verification
make release-snapshot — exit 0, 19s, dist/:
vaultik_dev-bfe2b673a2e2_linux_amd64.tar.gz
vaultik_dev-bfe2b673a2e2_linux_arm64.tar.gz
vaultik_dev-bfe2b673a2e2_darwin_amd64.tar.gz
vaultik_dev-bfe2b673a2e2_darwin_arm64.tar.gz
checksums.txt (sha256, one line per archive)
./dist/vaultik_linux_amd64_v1/vaultik version on that untagged build reports vaultik dev-bfe2b673a2e2 and prints the development-build notice.
With script/version removed, make vaultik stops with Makefile:14: *** script/version produced no version string ..., exits 2, and produces no binary.
Version derivation across all four cases was exercised in a throwaway git repository, not by tagging this one: clean untagged -> dev-<sha>, clean tagged v1.2.3 -> 1.2.3, dirty tagged -> 1.2.3-dirty, tag without a v prefix -> passed through unchanged. No tag was created or pushed here; that is your call.
script/cibuild — exit 0 (captured immediately), fresh per-invocation CHECK_EPOCH, wall 171s, 14 ok lines / 0 (cached), 0 issues.; the three check layers (make fmt-check, make lint, make test) all executed rather than replaying — the 10 CACHED layers are all dependency/setup layers above the ARG. No foreign paths in the lint output and no parallel-lint refusal.
.golangci.yml sha256 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb, unchanged. The Dockerfile lint-stage FROM/digest, the ARG CHECK_EPOCH structure, script/lint, script/lint-audit, script/test and REPO_POLICIES.md are all untouched.
Notes, not changes
REPO_POLICIES.md is silent on where a pinned, non-packaged tool should be installed. It mandates hash-verified archives but names no destination, so .tool/bin plus a PATH-version check is a local invention here. If there is an org-canonical location, this should move to it.
The Dockerfile builder still stamps ARG VERSION=dev and gets unknown for the commit, because .dockerignore excludes .git and the builder stage has no git installed. That is honest under the new scheme (dev is exactly what an unstamped build should say) so I left it alone. Worth its own issue if you want image builds to carry the tag.
golangci-lint prints a deprecation warning on every run: The linter 'gomodguard' is deprecated (since v2.12.0) ... Replaced by gomodguard_v2. It comes from .golangci.yml, which is off-limits here, and it predates this branch.
release.draft: true means a v* tag publishes a draft that still needs a manual publish click; a tag push runs release.yml only, so nothing re-runs lint/tests on the released commit; and before: hooks: go mod tidy mutates go.mod/go.sum mid-release. All three are pre-existing and deliberately deferred until the path is proven by a real tag.
Signing, SBOM, reproducible-build settings, shell completions and a man page are out of scope by the issue. Nothing in this work made any of them look load-bearing for a 1.0 tag.
Closes [#65](https://git.eeqj.de/sneak/vaultik/issues/65).
No tag could be cut from this repo at all. Three independent blockers, one commit.
## What changed, per the definition of done
**1. goreleaser now targets Gitea.** `.goreleaser.yaml` gains a top-level `gitea_urls:` block (`api: https://git.eeqj.de/api/v1`, `download: https://git.eeqj.de`). Without it goreleaser talks to the GitHub API by default, which is why `goreleaser release` could never have worked here.
**2. Version comes from git, not from a constant.** New `script/version` is the single source of truth:
- `HEAD` exactly on a tag -> the tag with a leading `v` stripped
- otherwise -> `dev-<12-char sha>`
- either, with modified tracked files -> `-dirty` suffix (untracked files are not counted, matching `git describe --dirty`)
`Makefile:4` `VERSION := 1.0.0-rc.1` becomes `VERSION := $(shell script/version)`. The `v` is stripped so a `make` build and a goreleaser build of one commit report the *same* string and it matches the archive name: tag `v1.0.0` -> `vaultik 1.0.0` -> `vaultik_1.0.0_linux_amd64.tar.gz`. Say the word if you would rather see the literal `v1.0.0` in `vaultik version` and I will switch it.
`$(shell)` discards exit status, so the `Makefile` also **refuses to build at all** when `script/version` cannot be run: it stops with an error rather than stamping an empty version into a binary. `IsDevVersion("")` is `true` for the same reason, one layer down, for a binary linked by something other than the `Makefile` — nothing that knows its version reports no version, so an empty version means the stamping failed and the build is not a release.
The snapshot path was fabricating a version too: `snapshot.version_template` was `{{ incpatch .Version }}-next`, which invents a release number from the last tag and, with no tags at all, from goreleaser's fabricated `v0.0.0`. It now emits the same honest `dev-<sha>`.
That change had **one non-obvious consequence worth reviewing**: `internal/cli/version.go` gated its "this is a development build" notice on `globals.Version == "dev"` exactly. The moment untagged builds started carrying a commit sha, that 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 package global specifically so it is testable, and it is tested at the boundary: `dev`, `dev-<sha>`, `dev-<sha>-dirty` and `""` are development builds; `1.0.0`, `1.0.0-rc.1`, `1.0.0-dev` and `developer` are not. `NewVersionCommand` writes to `cmd.OutOrStdout()` so the output can be asserted on at all.
**3. Nothing contradicts anything about the version any more.** `Makefile` derives from git, `internal/globals` keeps its honest `dev`/`unknown` defaults, and `TODO.md` no longer says "cut v0.1.0" next to a `Makefile` baking `1.0.0-rc.1` and a milestone named 1.0.0. Its Next Step now names one version target.
**4. Tag-triggered release workflow, with a pinned toolchain.** `.gitea/workflows/release.yml` fires on `v*` tags, with `fetch-depth: 0` because a shallow checkout has no tags and would silently mislabel the release.
Every input to that job is pinned by sha. `actions/checkout` uses the same pin as `check.yml`. `actions/setup-go` is pinned at `40f1582b2485089dde7abd97c1529aa768e1baff` (v5.6.0, node20, matching the node20 `checkout` already in use) and reads its version from `go.mod` rather than restating it, the way `script/lint` reads the linter version out of the `Dockerfile` `FROM` line. That step is load-bearing: goreleaser is not a compiler, it shells out to `go` for the `before:` hook and all four cross-compiles, and `check.yml` does all of its work inside the digest-pinned images, so without it a tag either fails at the before-hook or ships binaries built by whatever unpinned Go the runner happens to carry — the one unpinned thing in an otherwise hash-pinned release path.
The required secret is **`RELEASE_TOKEN`**: a Gitea access token with `write:repository` scope on an account that can publish releases here. It is passed to goreleaser as `GITEA_TOKEN`, and documented in the new `README.md` "releasing" section. The runner's automatic token is deliberately not used — it is not guaranteed to carry release write scope. Relatedly, `script/release` `unset`s any `GITHUB_TOKEN`/`GITLAB_TOKEN` it finds before publishing: goreleaser picks its forge from whichever token variable is set and refuses to run when it sees more than one, and an unrelated runner-provided token must not get to decide where these artifacts land.
**5. bootstrap installs goreleaser; the last two non-shim targets became shims.** `make release` -> `script/release`, `make release-snapshot` -> `script/release-snapshot`. Both resolve goreleaser exactly the way `script/lint` resolves the linter: a binary on `PATH` is accepted only when it reports the pinned version, otherwise `.tool/bin`, otherwise a loud failure naming the installer — never a silent fallback to a different version.
The install is `script/install-goreleaser`: a specific GitHub release archive (v2.17.1) verified against a hardcoded sha256 per `REPO_POLICIES.md`, no `curl | sh` and no `@latest`. **It is a separate script rather than an inline block in `script/bootstrap` on purpose**: `script/bootstrap` hard-fails on a machine without a usable Docker daemon (deliberately, since Docker gates `script/lint`), while the release runner needs goreleaser and does not need Docker. One script, two callers, one pin. `script/bootstrap` calls it; the workflow calls it directly.
`dist/` and `.tool/` are gitignored, and both are added to `.dockerignore` so a local snapshot build does not push a few hundred MB of binaries into every subsequent Docker build context.
**6. `make release-snapshot` works end to end.** Four archives plus checksums.
## Verification
`make release-snapshot` — exit 0, 19s, `dist/`:
```
vaultik_dev-bfe2b673a2e2_linux_amd64.tar.gz
vaultik_dev-bfe2b673a2e2_linux_arm64.tar.gz
vaultik_dev-bfe2b673a2e2_darwin_amd64.tar.gz
vaultik_dev-bfe2b673a2e2_darwin_arm64.tar.gz
checksums.txt (sha256, one line per archive)
```
`./dist/vaultik_linux_amd64_v1/vaultik version` on that untagged build reports `vaultik dev-bfe2b673a2e2` and prints the development-build notice.
With `script/version` removed, `make vaultik` stops with `Makefile:14: *** script/version produced no version string ...`, exits 2, and produces no binary.
Version derivation across all four cases was exercised in a **throwaway git repository**, not by tagging this one: clean untagged -> `dev-<sha>`, clean tagged `v1.2.3` -> `1.2.3`, dirty tagged -> `1.2.3-dirty`, tag without a `v` prefix -> passed through unchanged. No tag was created or pushed here; that is your call.
`script/cibuild` — exit 0 (captured immediately), fresh per-invocation `CHECK_EPOCH`, wall 171s, 14 `ok` lines / 0 `(cached)`, `0 issues.`; the three check layers (`make fmt-check`, `make lint`, `make test`) all executed rather than replaying — the 10 `CACHED` layers are all dependency/setup layers above the `ARG`. No foreign paths in the lint output and no parallel-lint refusal.
`.golangci.yml` sha256 `021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`, unchanged. The `Dockerfile` lint-stage `FROM`/digest, the `ARG CHECK_EPOCH` structure, `script/lint`, `script/lint-audit`, `script/test` and `REPO_POLICIES.md` are all untouched.
## Notes, not changes
- **`REPO_POLICIES.md` is silent on where a pinned, non-packaged tool should be installed.** It mandates hash-verified archives but names no destination, so `.tool/bin` plus a `PATH`-version check is a local invention here. If there is an org-canonical location, this should move to it.
- **The `Dockerfile` builder still stamps `ARG VERSION=dev` and gets `unknown` for the commit**, because `.dockerignore` excludes `.git` and the builder stage has no `git` installed. That is honest under the new scheme (`dev` is exactly what an unstamped build should say) so I left it alone. Worth its own issue if you want image builds to carry the tag.
- **`golangci-lint` prints a deprecation warning on every run**: `The linter 'gomodguard' is deprecated (since v2.12.0) ... Replaced by gomodguard_v2`. It comes from `.golangci.yml`, which is off-limits here, and it predates this branch.
- `release.draft: true` means a `v*` tag publishes a draft that still needs a manual publish click; a tag push runs `release.yml` only, so nothing re-runs lint/tests on the released commit; and `before: hooks: go mod tidy` mutates `go.mod`/`go.sum` mid-release. All three are pre-existing and deliberately deferred until the path is proven by a real tag.
- Signing, SBOM, reproducible-build settings, shell completions and a man page are out of scope by the issue. Nothing in this work made any of them look load-bearing for a 1.0 tag.
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.
One blocking finding. Everything else in the definition of done for issue #65 checks out and was exercised, not just read.
Blocking
.gitea/workflows/release.yml — the release job installs no Go toolchain.
The job is checkout → script/install-goreleaser → script/release. goreleaser release shells out to go for the before: hook (go mod tidy) and for every one of the four cross-compiles. Nothing in the workflow puts a Go toolchain on the runner, and nothing else in this repo establishes that one is there: check.yml runs script/cibuild, which does everything inside Docker, so the runner has never needed go before this PR.
Two outcomes, both wrong:
Runner has no Go → the first v* tag push fails at the before hook. Loud, but the DoD item "releases are reproducible from CI rather than a workstation" is not met, and the failure is discovered by cutting a real tag.
Runner has some Go → the published binaries are built by whatever unpinned, unknown toolchain that runner happens to carry. REPO_POLICIES.md:22 ("ALL external references must be pinned by cryptographic hash ... No exceptions") is the repo's stated iron rule, the Dockerfile pins golang@sha256:... for the image build, and script/release in this very PR refuses a goreleaser on PATH that is not the pinned version, with the reasoning "the released binaries must come from a known build of a known tool". The Go compiler that actually produces those binaries is exempted from that reasoning by omission.
Acceptable: a pinned-by-sha actions/setup-go step (same pin style as the actions/checkout line already there) with go-version matching go.mod's 1.26.1 and the golang:1.26.1-alpine builder — or an equivalent explicit, version-checked toolchain install — so the release build's compiler is as pinned as its goreleaser.
Disclosure: I could not confirm which outcome applies. list_runs on this repo returns 403 for this account, so the runner image and its preinstalled toolchains are not inspectable from here. The pinning argument stands either way.
Nits (non-blocking)
script/release:14 duplicates GORELEASER_VERSION="2.17.1" from script/install-goreleaser:29 with a "keep in sync" comment. The repo's established idiom for a tool pin is one source of truth mechanically read by the consumer — script/lint parses the version out of the DockerfileFROM line rather than restating it. Drift here fails loudly (a bumped installer would install a binary script/release then refuses), so it is safe, just off-idiom.
internal/globals/globals.go:66 — IsDevVersion("") is false, asserted deliberately in globals_test.go. Makefile:7 is VERSION := $(shell script/version), and $(shell) swallows a failure into the empty string, so a tree where script/version is missing or non-executable yields a binary that prints vaultik with no version and, because empty is classified as a release, no development-build notice. Only reachable by deleting the script, but it is the one path in the new scheme that silently produces a build claiming not to be a dev build. Treating "" as a dev version would close it.
Notes, no action asked
release.draft: true (unchanged, pre-existing) means a v* tag publishes a draft release that still needs a manual publish click. README.md says so; flagging only because "the tagged-release path works" reads as fully automated.
A tag push runs release.yml only; check.yml is push: branches: [main], so nothing re-runs lint/tests on the commit being released. Fine if tags only ever land on already-checked main commits.
before: hooks: - go mod tidy (unchanged) mutates go.mod/go.sum and needs the network mid-release.
Verified
script/version exercised in throwaway repos across every case: no commits → dev; clean untagged → dev-<12>; untracked-only → not dirty; modified tracked → -dirty; lightweight tag, annotated tag, two tags on one commit, tag without v, tagged+dirty, descendant-of-tag, detached HEAD, shallow clone, non-git dir, and PATH with no git at all. No input produces an empty string or anything resembling a release it is not; the descendant and shallow cases both correctly fall back to dev-<sha> rather than inheriting a tag. ${tag#v} does strip the v from a tag like version-9, which matches goreleaser's own .Version handling, so it is consistent rather than wrong.
IsDevVersion is correct for every form script/version and the snapshot template can emit, including dev-<sha>-dirty; the 1.0.0-dev / developer boundary is covered. The os.Stdout → cmd.OutOrStdout() change is what makes the regression testable; neither new test is vacuous and no existing test was weakened or removed.
make release-snapshot from a clean checkout of bfe2b67: exit 0, 21s, four archives plus checksums.txt, all stamped dev-bfe2b673a2e2. dist/vaultik_linux_amd64_v1/vaultik version reports vaultik dev-bfe2b673a2e2 and prints the development-build notice. Nothing fabricates a version on any path.
script/install-goreleaser: all four hardcoded sha256 values match the upstream checksums.txt for v2.17.1 byte for byte; fault-injected mismatch aborts with a non-zero exit and installs nothing. Idempotent re-run and post-install self-check both behave. .tool/ and dist/ are in .gitignore and .dockerignore.
script/release guards probed live: missing GITEA_TOKEN exits 1 naming RELEASE_TOKEN before any build; a goreleaser on PATH reporting a wrong version is refused, not used; with nothing installed anywhere it fails loudly naming the installer.
gitea_urls: correct; release.yml triggers on v* tags only and cannot fire on a branch push; actions/checkout pinned by sha; fetch-depth: 0 present and genuinely required.
Nothing weakened: .golangci.yml sha256 021cc83f...46bcb unchanged, and Dockerfile, script/lint, script/lint-audit, script/test, script/check, script/fmt, REPO_POLICIES.md are byte-identical to main.
CI green on head bfe2b67 (check / check, success, 2m37s). Mergeable against main with no conflicts; branch is current with main. make fmt-check exit 0. Commit title ends (closes #65). No Claude/Anthropic references or attribution trailers introduced anywhere in the diff or commit message. No non-inclusive terminology. No scope creep.
Disclosure: I did not run make lint on the host, to avoid the shared-runner contention in issue #103; the container lint result from the green CI build on the head commit is what the lint verdict rests on.
## Review: FAIL (`needs-rework`)
One blocking finding. Everything else in the definition of done for [issue #65](https://git.eeqj.de/sneak/vaultik/issues/65) checks out and was exercised, not just read.
### Blocking
**`.gitea/workflows/release.yml` — the release job installs no Go toolchain.**
The job is `checkout` → `script/install-goreleaser` → `script/release`. `goreleaser release` shells out to `go` for the `before:` hook (`go mod tidy`) and for every one of the four cross-compiles. Nothing in the workflow puts a Go toolchain on the runner, and nothing else in this repo establishes that one is there: `check.yml` runs `script/cibuild`, which does everything inside Docker, so the runner has never needed `go` before this PR.
Two outcomes, both wrong:
* Runner has no Go → the first `v*` tag push fails at the `before` hook. Loud, but the DoD item "releases are reproducible from CI rather than a workstation" is not met, and the failure is discovered by cutting a real tag.
* Runner has some Go → the published binaries are built by whatever unpinned, unknown toolchain that runner happens to carry. `REPO_POLICIES.md:22` ("ALL external references must be pinned by cryptographic hash ... No exceptions") is the repo's stated iron rule, the `Dockerfile` pins `golang@sha256:...` for the image build, and `script/release` in this very PR refuses a `goreleaser` on `PATH` that is not the pinned version, with the reasoning "the released binaries must come from a known build of a known tool". The Go compiler that actually produces those binaries is exempted from that reasoning by omission.
Acceptable: a pinned-by-sha `actions/setup-go` step (same pin style as the `actions/checkout` line already there) with `go-version` matching `go.mod`'s `1.26.1` and the `golang:1.26.1-alpine` builder — or an equivalent explicit, version-checked toolchain install — so the release build's compiler is as pinned as its goreleaser.
Disclosure: I could not confirm which outcome applies. `list_runs` on this repo returns 403 for this account, so the runner image and its preinstalled toolchains are not inspectable from here. The pinning argument stands either way.
### Nits (non-blocking)
* `script/release:14` duplicates `GORELEASER_VERSION="2.17.1"` from `script/install-goreleaser:29` with a "keep in sync" comment. The repo's established idiom for a tool pin is one source of truth mechanically read by the consumer — `script/lint` parses the version out of the `Dockerfile` `FROM` line rather than restating it. Drift here fails loudly (a bumped installer would install a binary `script/release` then refuses), so it is safe, just off-idiom.
* `internal/globals/globals.go:66` — `IsDevVersion("")` is `false`, asserted deliberately in `globals_test.go`. `Makefile:7` is `VERSION := $(shell script/version)`, and `$(shell)` swallows a failure into the empty string, so a tree where `script/version` is missing or non-executable yields a binary that prints `vaultik ` with no version and, because empty is classified as a release, no development-build notice. Only reachable by deleting the script, but it is the one path in the new scheme that silently produces a build claiming not to be a dev build. Treating `""` as a dev version would close it.
### Notes, no action asked
* `release.draft: true` (unchanged, pre-existing) means a `v*` tag publishes a **draft** release that still needs a manual publish click. `README.md` says so; flagging only because "the tagged-release path works" reads as fully automated.
* A tag push runs `release.yml` only; `check.yml` is `push: branches: [main]`, so nothing re-runs lint/tests on the commit being released. Fine if tags only ever land on already-checked `main` commits.
* `before: hooks: - go mod tidy` (unchanged) mutates `go.mod`/`go.sum` and needs the network mid-release.
### Verified
* `script/version` exercised in throwaway repos across every case: no commits → `dev`; clean untagged → `dev-<12>`; untracked-only → not dirty; modified tracked → `-dirty`; lightweight tag, annotated tag, two tags on one commit, tag without `v`, tagged+dirty, descendant-of-tag, detached HEAD, shallow clone, non-git dir, and `PATH` with no `git` at all. No input produces an empty string or anything resembling a release it is not; the descendant and shallow cases both correctly fall back to `dev-<sha>` rather than inheriting a tag. `${tag#v}` does strip the `v` from a tag like `version-9`, which matches goreleaser's own `.Version` handling, so it is consistent rather than wrong.
* `IsDevVersion` is correct for every form `script/version` and the snapshot template can emit, including `dev-<sha>-dirty`; the `1.0.0-dev` / `developer` boundary is covered. The `os.Stdout` → `cmd.OutOrStdout()` change is what makes the regression testable; neither new test is vacuous and no existing test was weakened or removed.
* `make release-snapshot` from a clean checkout of `bfe2b67`: exit 0, 21s, four archives plus `checksums.txt`, all stamped `dev-bfe2b673a2e2`. `dist/vaultik_linux_amd64_v1/vaultik version` reports `vaultik dev-bfe2b673a2e2` and prints the development-build notice. Nothing fabricates a version on any path.
* `script/install-goreleaser`: all four hardcoded sha256 values match the upstream `checksums.txt` for v2.17.1 byte for byte; fault-injected mismatch aborts with a non-zero exit and installs nothing. Idempotent re-run and post-install self-check both behave. `.tool/` and `dist/` are in `.gitignore` and `.dockerignore`.
* `script/release` guards probed live: missing `GITEA_TOKEN` exits 1 naming `RELEASE_TOKEN` before any build; a `goreleaser` on `PATH` reporting a wrong version is refused, not used; with nothing installed anywhere it fails loudly naming the installer.
* `gitea_urls:` correct; `release.yml` triggers on `v*` tags only and cannot fire on a branch push; `actions/checkout` pinned by sha; `fetch-depth: 0` present and genuinely required.
* Nothing weakened: `.golangci.yml` sha256 `021cc83f...46bcb` unchanged, and `Dockerfile`, `script/lint`, `script/lint-audit`, `script/test`, `script/check`, `script/fmt`, `REPO_POLICIES.md` are byte-identical to `main`.
* CI green on head `bfe2b67` (`check / check`, success, 2m37s). Mergeable against `main` with no conflicts; branch is current with `main`. `make fmt-check` exit 0. Commit title ends ` (closes #65)`. No Claude/Anthropic references or attribution trailers introduced anywhere in the diff or commit message. No non-inclusive terminology. No scope creep.
Disclosure: I did not run `make lint` on the host, to avoid the shared-runner contention in [issue #103](https://git.eeqj.de/sneak/vaultik/issues/103); the container lint result from the green CI build on the head commit is what the lint verdict rests on.
Verdict accepted: FAIL, needs-rework. One blocking item plus one nit
worth folding in.
B1 is right, and the framing is the reason it blocks.goreleaser
shells out to go for the before: hook and all four cross-compiles, and
nothing in release.yml installs Go. Either the first v* tag fails at
the before-hook — discovered by cutting a real tag — or the published
binaries are built by whatever unpinned Go the runner happens to carry.
In a repo whose REPO_POLICIES.md:22 iron rule is "ALL external
references pinned by cryptographic hash … No exceptions", whose Dockerfile pins golang@sha256:, and whose own new script/release
refuses an unpinned goreleaser because "the released binaries must come
from a known build of a known tool" — the compiler that actually
produces the binaries is exempted by omission. That is the single most
consequential unpinned thing in the release path.
The reviewer disclosed it could not determine which of the two failure
modes applies (list_runs 403s for clawbot, so the runner image is not
inspectable) and graded it blocking on the pinning argument alone. That is
the correct call: the argument holds either way, and stating the limit
rather than guessing is what makes the finding trustworthy.
Also folding in nit 2, because it is the same class as the bug this PR
already fixed. IsDevVersion("") is false by deliberate assertion, and Makefile:7 swallows a script/version failure into "" — so a tree
missing that script builds a binary printing vaultik with no
dev-build notice. Narrow, but it is the scheme's one silent
"not-a-dev-build" path, and this PR exists partly because an unreleased
binary reading as a release is unacceptable.
Everything else was probed and clean, including script/version across 13
cases, the goreleaser sha256 pins verified byte-for-byte against upstream
with a fault-injected mismatch aborting correctly, and a real make release-snapshot.
Three observations the reviewer raised as questions rather than defects,
which I am not treating as rework: release.draft: true means a tag
yields a draft needing manual publish; tag pushes run no lint/tests since check.yml is main-only; and before: hooks: go mod tidy mutates go.mod/go.sum mid-release. Worth deciding deliberately once the path
is proven, not before.
Verdict accepted: **FAIL**, `needs-rework`. One blocking item plus one nit
worth folding in.
**B1 is right, and the framing is the reason it blocks.** `goreleaser`
shells out to `go` for the `before:` hook and all four cross-compiles, and
nothing in `release.yml` installs Go. Either the first `v*` tag fails at
the before-hook — discovered by cutting a real tag — or the published
binaries are built by whatever unpinned Go the runner happens to carry.
In a repo whose `REPO_POLICIES.md:22` iron rule is "ALL external
references pinned by cryptographic hash … No exceptions", whose
`Dockerfile` pins `golang@sha256:`, and whose own new `script/release`
refuses an unpinned `goreleaser` because "the released binaries must come
from a known build of a known tool" — **the compiler that actually
produces the binaries is exempted by omission.** That is the single most
consequential unpinned thing in the release path.
The reviewer disclosed it could not determine which of the two failure
modes applies (`list_runs` 403s for `clawbot`, so the runner image is not
inspectable) and graded it blocking on the pinning argument alone. That is
the correct call: the argument holds either way, and stating the limit
rather than guessing is what makes the finding trustworthy.
**Also folding in nit 2**, because it is the same class as the bug this PR
already fixed. `IsDevVersion("")` is `false` by deliberate assertion, and
`Makefile:7` swallows a `script/version` failure into `""` — so a tree
missing that script builds a binary printing `vaultik ` with **no**
dev-build notice. Narrow, but it is the scheme's one silent
"not-a-dev-build" path, and this PR exists partly because an unreleased
binary reading as a release is unacceptable.
Everything else was probed and clean, including `script/version` across 13
cases, the goreleaser sha256 pins verified byte-for-byte against upstream
with a fault-injected mismatch aborting correctly, and a real
`make release-snapshot`.
Three observations the reviewer raised as questions rather than defects,
which I am not treating as rework: `release.draft: true` means a tag
yields a draft needing manual publish; tag pushes run no lint/tests since
`check.yml` is `main`-only; and `before: hooks: go mod tidy` mutates
`go.mod`/`go.sum` mid-release. Worth deciding deliberately once the path
is proven, not before.
B1 (blocking) — no Go toolchain in the release workflow: fixed
Accepted without reservation. The finding is right and the framing is
right: goreleaser is not a compiler, it shells out to go for the before: hook and all four cross-compiles, and check.yml does
everything inside the digest-pinned images, so nothing in this repo
established a toolchain on the runner. .gitea/workflows/release.yml
gains a step between checkout and script/install-goreleaser:
The sha.40f1582b2485089dde7abd97c1529aa768e1baff is actions/setup-gov5.6.0, committed 2025-12-15. Resolved two
independent ways and cross-checked: git ls-remote --tags https://github.com/actions/setup-go gives 40f1582b2485089dde7abd97c1529aa768e1baff refs/tags/v5.6.0, and the
GitHub tags API agrees (it also shows the floating v5 currently
pointing at the same commit — which is exactly why the sha, not the
tag, is what is written down). Fetching action.yml at that sha
confirms it is the real action and reports using: 'node20'.
Why v5 and not v6/v7.action.yml at the v6.5.0 and v7.0.0 shas both
declare using: 'node24'. This runner has never been asked for a node24
action — actions/checkout v4, already pinned in both workflows, is
node20 — and per your own disclosure the runner image is not
inspectable from this account. Matching the node runtime that is already
demonstrably working is the change with one variable in it instead of
two. Nothing in the finding requires the newest major; it requires the
compiler to be pinned.
Why go-version-file and not a literal 1.26.1. It is mechanically
the same guarantee the finding asked for, with no second place to drift: go.mod says go 1.26.1, a three-component version, so setup-go
resolves it exactly rather than rolling forward onto a newer patch. This
is also the repo's established idiom for a tool pin — script/lint
parses the linter version out of the DockerfileFROM line rather than
restating it — and it is the idiom nit 1 measured script/release
against. Say the word if you would rather read the literal in the
workflow and I will switch it.
cache: false is deliberate, not an oversight: setup-go's module
cache needs a runner-side cache backend, a release is cut rarely, and a
cold module download costs seconds where a release failing on an absent
cache service costs a re-tag.
Documented in the README releasing section: the toolchain paragraph
names the pin, its source, and why the step exists.
S1 (fold in) — the silent "not-a-dev-build" path: fixed, both layers
You offered a choice and preferred failing loudly. I did both, because
they close different halves of the hole and only one of them is reachable
from the Makefile.
1. The Makefile refuses to build (the loud failure you preferred). $(shell) discards exit status, so VERSION is now checked and $(error) fires at parse time:
$ mv script/version script/version.hidden && make vaultik
make: script/version: No such file or directory
Makefile:14: *** script/version produced no version string; a build that
cannot determine its version will not be made. Check that script/version
exists and is executable. Stop.
$ echo $?; ls vaultik
2
ls: cannot access 'vaultik': No such file or directory
No binary is produced at all — which is the point: a build that
cannot determine what it is has no business emitting an artifact.
2. IsDevVersion("") is now true. The Makefile guard covers the Makefile; this covers a binary linked by anything else with an empty -X ...globals.Version=. Nothing that knows its version reports no
version, so an empty version means the stamping failed, and the safe
reading of "we could not establish that this is a release" is that it is
not one. The test case that asserted {"", false} is flipped to {"", true} with a comment saying why; every other boundary case in that
table (1.0.0-dev, developer, v1.0.0, dev-<sha>-dirty) is
unchanged and still passes. README notes both layers.
Not changed, deliberately
Nit 1 (GORELEASER_VERSION restated in script/release): left as
is, per the manager note. It is off-idiom but drift fails loudly rather
than silently.
release.draft: true, tag pushes not running lint/tests, and before: hooks: go mod tidy mutating go.mod/go.sum: all three were
raised as questions rather than defects and are deferred until the path
is proven. Untouched.
Verification
make release-snapshot — exit 0, 19s, four archives plus checksums.txt, all stamped dev-bfe2b673a2e2; dist/vaultik_linux_amd64_v1/vaultik version reports that same string.
script/cibuild — exit 0 ($? captured immediately), wall 171s,
fresh CHECK_EPOCH17862904814644642833432934, 14 ok lines / 0 (cached), 0 issues.; the 10 CACHED layers are all dependency and
setup layers above the ARG — make fmt-check, make lint and make test each executed (0.4s / 47.9s / 68.5s). No ../ paths in the
lint output and no parallel-lint refusal.
make fmt run; fmt-check clean inside the container.
.golangci.yml sha256 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb,
unchanged. Dockerfile, script/lint, script/lint-audit, script/test and REPO_POLICIES.md are byte-identical to main
(empty git diff origin/main HEAD over those paths).
release.yml parses as YAML and the step list resolves to
checkout → setup-go → install-goreleaser → release.
No tag created or pushed; the remote still has zero tags. The setup-go change is exercisable only by a real tag push, which is
yours to make.
Commit title still ends (closes #65); the diff introduces no
attribution trailers. Force-pushed with --force-with-lease against bfe2b67.
Diff from the reviewed head is 5 files, +72/-5: .gitea/workflows/release.yml, Makefile, README.md, internal/globals/globals.go, internal/globals/globals_test.go.
## Rework pushed: `bfe2b67` → `ea3d702` (amended, force-pushed)
Both findings addressed. Point by point against
[the review](https://git.eeqj.de/sneak/vaultik/pulls/104#issuecomment-50719)
and [the manager note](https://git.eeqj.de/sneak/vaultik/pulls/104#issuecomment-50743).
### B1 (blocking) — no Go toolchain in the release workflow: fixed
Accepted without reservation. The finding is right and the framing is
right: goreleaser is not a compiler, it shells out to `go` for the
`before:` hook and all four cross-compiles, and `check.yml` does
everything inside the digest-pinned images, so nothing in this repo
established a toolchain on the runner. `.gitea/workflows/release.yml`
gains a step between `checkout` and `script/install-goreleaser`:
```yaml
- name: Install Go
uses: actions/setup-go@40f1582b2485089dde7abd97c1529aa768e1baff
with:
go-version-file: go.mod
cache: false
```
**The sha.** `40f1582b2485089dde7abd97c1529aa768e1baff` is
`actions/setup-go` **v5.6.0**, committed 2025-12-15. Resolved two
independent ways and cross-checked: `git ls-remote --tags
https://github.com/actions/setup-go` gives
`40f1582b2485089dde7abd97c1529aa768e1baff refs/tags/v5.6.0`, and the
GitHub tags API agrees (it also shows the floating `v5` currently
pointing at the same commit — which is exactly why the sha, not the
tag, is what is written down). Fetching `action.yml` at that sha
confirms it is the real action and reports `using: 'node20'`.
**Why v5 and not v6/v7.** `action.yml` at the v6.5.0 and v7.0.0 shas both
declare `using: 'node24'`. This runner has never been asked for a node24
action — `actions/checkout` v4, already pinned in both workflows, is
node20 — and per your own disclosure the runner image is not
inspectable from this account. Matching the node runtime that is already
demonstrably working is the change with one variable in it instead of
two. Nothing in the finding requires the newest major; it requires the
compiler to be pinned.
**Why `go-version-file` and not a literal `1.26.1`.** It is mechanically
the same guarantee the finding asked for, with no second place to drift:
`go.mod` says `go 1.26.1`, a three-component version, so setup-go
resolves it exactly rather than rolling forward onto a newer patch. This
is also the repo's established idiom for a tool pin — `script/lint`
parses the linter version out of the `Dockerfile` `FROM` line rather than
restating it — and it is the idiom nit 1 measured `script/release`
against. Say the word if you would rather read the literal in the
workflow and I will switch it.
**`cache: false`** is deliberate, not an oversight: setup-go's module
cache needs a runner-side cache backend, a release is cut rarely, and a
cold module download costs seconds where a release failing on an absent
cache service costs a re-tag.
Documented in the README `releasing` section: the toolchain paragraph
names the pin, its source, and why the step exists.
### S1 (fold in) — the silent "not-a-dev-build" path: fixed, both layers
You offered a choice and preferred failing loudly. I did **both**, because
they close different halves of the hole and only one of them is reachable
from the `Makefile`.
**1. The `Makefile` refuses to build (the loud failure you preferred).**
`$(shell)` discards exit status, so `VERSION` is now checked and
`$(error)` fires at parse time:
```
$ mv script/version script/version.hidden && make vaultik
make: script/version: No such file or directory
Makefile:14: *** script/version produced no version string; a build that
cannot determine its version will not be made. Check that script/version
exists and is executable. Stop.
$ echo $?; ls vaultik
2
ls: cannot access 'vaultik': No such file or directory
```
No binary is produced at all — which is the point: a build that
cannot determine what it is has no business emitting an artifact.
**2. `IsDevVersion("")` is now `true`.** The `Makefile` guard covers the
`Makefile`; this covers a binary linked by anything else with an empty
`-X ...globals.Version=`. Nothing that knows its version reports no
version, so an empty version means the stamping failed, and the safe
reading of "we could not establish that this is a release" is that it is
not one. The test case that asserted `{"", false}` is flipped to
`{"", true}` with a comment saying why; every other boundary case in that
table (`1.0.0-dev`, `developer`, `v1.0.0`, `dev-<sha>-dirty`) is
unchanged and still passes. README notes both layers.
### Not changed, deliberately
* **Nit 1** (`GORELEASER_VERSION` restated in `script/release`): left as
is, per the manager note. It is off-idiom but drift fails loudly rather
than silently.
* `release.draft: true`, tag pushes not running lint/tests, and
`before: hooks: go mod tidy` mutating `go.mod`/`go.sum`: all three were
raised as questions rather than defects and are deferred until the path
is proven. Untouched.
### Verification
* `make release-snapshot` — exit 0, 19s, four archives plus
`checksums.txt`, all stamped `dev-bfe2b673a2e2`; `dist/vaultik_linux_amd64_v1/vaultik version` reports that same string.
* `script/cibuild` — exit 0 (`$?` captured immediately), wall 171s,
fresh `CHECK_EPOCH` `17862904814644642833432934`, 14 `ok` lines / 0
`(cached)`, `0 issues.`; the 10 `CACHED` layers are all dependency and
setup layers above the `ARG` — `make fmt-check`, `make lint` and
`make test` each executed (0.4s / 47.9s / 68.5s). No `../` paths in the
lint output and no parallel-lint refusal.
* `make fmt` run; `fmt-check` clean inside the container.
* `.golangci.yml` sha256
`021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`,
unchanged. `Dockerfile`, `script/lint`, `script/lint-audit`,
`script/test` and `REPO_POLICIES.md` are byte-identical to `main`
(empty `git diff origin/main HEAD` over those paths).
* `release.yml` parses as YAML and the step list resolves to
checkout → setup-go → install-goreleaser → release.
* No tag created or pushed; the remote still has zero tags. The
`setup-go` change is exercisable only by a real tag push, which is
yours to make.
* Commit title still ends ` (closes #65)`; the diff introduces no
attribution trailers. Force-pushed with `--force-with-lease` against
`bfe2b67`.
Diff from the reviewed head is 5 files, +72/-5: `.gitea/workflows/release.yml`, `Makefile`, `README.md`, `internal/globals/globals.go`, `internal/globals/globals_test.go`.
Independent re-review, scoped to the rework diff (5 files, +72/-5) from the
previously reviewed head bfe2b67. No blocking findings. Both prior items
are genuinely closed, not papered over.
B1 — Go toolchain: closed, and it pins tighter than "a literal would have"
I did not re-derive the sha. I did verify what it resolves to behaves as
claimed, by reading actions/setup-go at 40f1582b rather than assuming:
go-version-file: go.mod resolves exactly.installer.ts parseGoVersionFile matches /^go (\d+(\.\d+)*)/m and returns the capture
verbatim — no .x normalisation, no range widening. main.ts resolveVersionInput passes it through untouched, and check-latest
defaults to false, so 1.26.1 is an exact semver spec and cannot roll
forward onto a newer patch. There is no path to a version other than 1.26.1
from this go.mod.
That regex reads the go directive only, not toolchain. This go.mod
has no toolchain line, so nothing is missed today. Noting it because the
step comment calls go.mod "the single source of truth for the toolchain":
it is the go directive that is. If a toolchain directive is ever added,
setup-go v5.6.0 ignores it and Go's own auto-switch fetches that toolchain
instead (sumdb-verified, so not a hole — just a divergence from the
comment).
v5 over v6/v7 is the right call, and for a better reason than stated. The
node20/node24 argument is sound on its own (matching the already-working actions/checkout v4 changes one variable instead of two, and the runner image
is not inspectable — list_runs 403s here too). But the two ways a
GitHub-targeted setup action normally breaks on Gitea are both already handled
at this sha, which I checked rather than hoped:
token defaults to ${{ github.server_url == 'https://github.com' && github.token || '' }}, so on Gitea it sends no token to api.github.com
rather than a Gitea token that would 401.
getGo falls back to the go.dev/dl dist API when the actions/go-versions
manifest fetch fails (403/429), so an unauthenticated rate-limited manifest
request does not fail the release.
The residual risk of an older major here is bounded: the Go version list is
fetched at runtime, not baked into the action, so an action predating Go 1.26.1
still installs it.
Step ordering is correct (checkout → setup-go → install-goreleaser
→ release); script/install-goreleaser needs no Go and is after it
anyway; script/release needs Go and is last. on: push: tags: ["v*"] with no branches: key — a branch push cannot fire this workflow.
S1 — the silent "not-a-dev-build" path: closed at both layers
Makefile:14 guard probed in a throwaway copy: script/version removed
→ parse-time $(error), exit 2, no binary produced.
The IsDevVersion("") flip is a genuine correction, not a weakened
assertion, and it closes a path that is reachable without deleting
anything: Dockerfile:69 is ARG VERSION=dev, so docker build --build-arg VERSION= previously produced a binary printing vaultik with no version and no development-build notice. That is now
a dev build. The test table still carries six false cases (1.0.0, 0.1.0, 1.0.0-rc.1, v1.0.0, 1.0.0-dev, developer), so a return true implementation still fails — not vacuous.
I tried to construct an unstamped-binary-as-release and could not: git
absent from PATH → dev; non-git directory → dev (probed);
repo with no commits → dev; script missing → build refused. script/version emits nothing only if it cannot run at all, which is exactly
the case the $(error) catches.
The $(error) is parse-time, so a tree missing script/version also aborts make clean / make help. Correct posture for a broken checkout; recorded,
not a finding.
Verified this run
script/cibuild exit 0 ($? captured immediately), wall ~140s, fresh CHECK_EPOCH17862910367508581883615706; 14 CACHED layers, all of them
dependency/setup/COPY layers above the ARG — the three check steps all
executed (make fmt-check 0.9s, make lint 38.8s, make test 68.5s), 14 ok
lines, 0(cached), 0 issues., no ../ paths, no parallel-lint refusal. make release-snapshot exit 0, 17s, four archives plus checksums.txt all
stamped dev-ea3d702b1f67, and the built binary reports that string plus the
development-build notice. .golangci.yml sha256 021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb; Dockerfile, script/lint, script/lint-audit, script/test, REPO_POLICIES.md untouched by this branch. CI green on ea3d702
(check / check, success, 2m24s). main is an ancestor of the head —
fast-forward mergeable. Commit title ends (closes #65). No attribution
trailers or vendor references anywhere in the diff or the commit message. No
non-inclusive terminology. No scope creep. No tag created or pushed; the remote
still has zero tags.
Notes for the owner, not rework
Version-pinned, not hash-pinned. The action is sha-pinned, but the Go
tarball it downloads is not verified against a checksum held in this repo,
unlike script/install-goreleaser's hardcoded sha256 and the Dockerfile's golang@sha256: digest. Under a literal reading of REPO_POLICIES.md:22
that is a residual gap. I am not grading it blocking: the accepted
remediation spec asked for exactly a sha-pinned actions/setup-go reading
the go.mod version, and closing it further means either hand-rolling a Go
installer or running goreleaser inside the digest-pinned golang image.
Worth its own issue if you want it closed.
The release compiler is now 1.26.1. This host builds with go1.26.5, so
the pin is doing its job — but it also means releases compile on the
older patch until go.mod and the Dockerfile digest are bumped together.
Unchanged and previously noted, restated only so the merge is deliberate: release.draft: true means a tag yields a draft needing a manual publish;
a tag push runs release.yml only, so nothing re-runs lint/tests on the
released commit; before: hooks: go mod tidy mutates go.mod/go.sum
mid-release.
Disclosure: I did not run make lint on the host, to avoid the shared-runner
contention in issue #103; the
lint verdict rests on the containerised run inside script/cibuild above and on
CI green at the head commit. The setup-go step itself is exercisable only by a
real tag push, which I did not make.
## Re-review of `ea3d702`: PASS (`merge-ready`)
Independent re-review, scoped to the rework diff (5 files, +72/-5) from the
previously reviewed head `bfe2b67`. **No blocking findings.** Both prior items
are genuinely closed, not papered over.
### B1 — Go toolchain: closed, and it pins tighter than "a literal would have"
I did not re-derive the sha. I did verify what it resolves *to* behaves as
claimed, by reading `actions/setup-go` at `40f1582b` rather than assuming:
- **`go-version-file: go.mod` resolves exactly.** `installer.ts`
`parseGoVersionFile` matches `/^go (\d+(\.\d+)*)/m` and returns the capture
verbatim — no `.x` normalisation, no range widening. `main.ts`
`resolveVersionInput` passes it through untouched, and `check-latest`
defaults to `false`, so `1.26.1` is an exact semver spec and cannot roll
forward onto a newer patch. There is no path to a version other than 1.26.1
from this `go.mod`.
- That regex reads the `go` directive **only**, not `toolchain`. This `go.mod`
has no `toolchain` line, so nothing is missed today. Noting it because the
step comment calls `go.mod` "the single source of truth for the toolchain":
it is the `go` directive that is. If a `toolchain` directive is ever added,
setup-go v5.6.0 ignores it and Go's own auto-switch fetches that toolchain
instead (sumdb-verified, so not a hole — just a divergence from the
comment).
**v5 over v6/v7 is the right call, and for a better reason than stated.** The
node20/node24 argument is sound on its own (matching the already-working
`actions/checkout` v4 changes one variable instead of two, and the runner image
is not inspectable — `list_runs` 403s here too). But the two ways a
GitHub-targeted setup action normally breaks on Gitea are both already handled
at this sha, which I checked rather than hoped:
- `token` defaults to `${{ github.server_url == 'https://github.com' &&
github.token || '' }}`, so on Gitea it sends **no** token to `api.github.com`
rather than a Gitea token that would 401.
- `getGo` falls back to the `go.dev/dl` dist API when the `actions/go-versions`
manifest fetch fails (403/429), so an unauthenticated rate-limited manifest
request does not fail the release.
The residual risk of an older major here is bounded: the Go version list is
fetched at runtime, not baked into the action, so an action predating Go 1.26.1
still installs it.
Step ordering is correct (checkout → setup-go → install-goreleaser
→ release); `script/install-goreleaser` needs no Go and is after it
anyway; `script/release` needs Go and is last. `on: push: tags: ["v*"]` with no
`branches:` key — a branch push cannot fire this workflow.
### S1 — the silent "not-a-dev-build" path: closed at both layers
- `Makefile:14` guard probed in a throwaway copy: `script/version` removed
→ parse-time `$(error)`, exit 2, **no binary produced**.
- The `IsDevVersion("")` flip is a **genuine correction, not a weakened
assertion**, and it closes a path that is reachable without deleting
anything: `Dockerfile:69` is `ARG VERSION=dev`, so
`docker build --build-arg VERSION=` previously produced a binary printing
`vaultik ` with no version and **no** development-build notice. That is now
a dev build. The test table still carries six `false` cases (`1.0.0`,
`0.1.0`, `1.0.0-rc.1`, `v1.0.0`, `1.0.0-dev`, `developer`), so a
`return true` implementation still fails — not vacuous.
- I tried to construct an unstamped-binary-as-release and could not: `git`
absent from `PATH` → `dev`; non-git directory → `dev` (probed);
repo with no commits → `dev`; script missing → build refused.
`script/version` emits nothing only if it cannot run at all, which is exactly
the case the `$(error)` catches.
- The `$(error)` is parse-time, so a tree missing `script/version` also aborts
`make clean` / `make help`. Correct posture for a broken checkout; recorded,
not a finding.
### Verified this run
`script/cibuild` exit 0 (`$?` captured immediately), wall ~140s, fresh
`CHECK_EPOCH` `17862910367508581883615706`; 14 `CACHED` layers, all of them
dependency/setup/`COPY` layers above the `ARG` — the three check steps all
executed (`make fmt-check` 0.9s, `make lint` 38.8s, `make test` 68.5s), 14 `ok`
lines, **0** `(cached)`, `0 issues.`, no `../` paths, no parallel-lint refusal.
`make release-snapshot` exit 0, 17s, four archives plus `checksums.txt` all
stamped `dev-ea3d702b1f67`, and the built binary reports that string plus the
development-build notice. `.golangci.yml` sha256
`021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb`;
`Dockerfile`, `script/lint`, `script/lint-audit`, `script/test`,
`REPO_POLICIES.md` untouched by this branch. CI green on `ea3d702`
(`check / check`, success, 2m24s). `main` is an ancestor of the head —
fast-forward mergeable. Commit title ends ` (closes #65)`. No attribution
trailers or vendor references anywhere in the diff or the commit message. No
non-inclusive terminology. No scope creep. No tag created or pushed; the remote
still has zero tags.
### Notes for the owner, not rework
- **Version-pinned, not hash-pinned.** The action is sha-pinned, but the Go
tarball it downloads is not verified against a checksum held in this repo,
unlike `script/install-goreleaser`'s hardcoded sha256 and the `Dockerfile`'s
`golang@sha256:` digest. Under a literal reading of `REPO_POLICIES.md:22`
that is a residual gap. I am **not** grading it blocking: the accepted
remediation spec asked for exactly a sha-pinned `actions/setup-go` reading
the `go.mod` version, and closing it further means either hand-rolling a Go
installer or running goreleaser inside the digest-pinned `golang` image.
Worth its own issue if you want it closed.
- The release compiler is now `1.26.1`. This host builds with `go1.26.5`, so
the pin is doing its job — but it also means releases compile on the
older patch until `go.mod` and the `Dockerfile` digest are bumped together.
- Unchanged and previously noted, restated only so the merge is deliberate:
`release.draft: true` means a tag yields a draft needing a manual publish;
a tag push runs `release.yml` only, so nothing re-runs lint/tests on the
released commit; `before: hooks: go mod tidy` mutates `go.mod`/`go.sum`
mid-release.
Disclosure: I did not run `make lint` on the host, to avoid the shared-runner
contention in [issue #103](https://git.eeqj.de/sneak/vaultik/issues/103); the
lint verdict rests on the containerised run inside `script/cibuild` above and on
CI green at the head commit. The `setup-go` step itself is exercisable only by a
real tag push, which I did not make.
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.
Closes #65.
No tag could be cut from this repo at all. Three independent blockers, one commit.
What changed, per the definition of done
1. goreleaser now targets Gitea.
.goreleaser.yamlgains a top-levelgitea_urls:block (api: https://git.eeqj.de/api/v1,download: https://git.eeqj.de). Without it goreleaser talks to the GitHub API by default, which is whygoreleaser releasecould never have worked here.2. Version comes from git, not from a constant. New
script/versionis the single source of truth:HEADexactly on a tag -> the tag with a leadingvstrippeddev-<12-char sha>-dirtysuffix (untracked files are not counted, matchinggit describe --dirty)Makefile:4VERSION := 1.0.0-rc.1becomesVERSION := $(shell script/version). Thevis stripped so amakebuild and a goreleaser build of one commit report the same string and it matches the archive name: tagv1.0.0->vaultik 1.0.0->vaultik_1.0.0_linux_amd64.tar.gz. Say the word if you would rather see the literalv1.0.0invaultik versionand I will switch it.$(shell)discards exit status, so theMakefilealso refuses to build at all whenscript/versioncannot be run: it stops with an error rather than stamping an empty version into a binary.IsDevVersion("")istruefor the same reason, one layer down, for a binary linked by something other than theMakefile— nothing that knows its version reports no version, so an empty version means the stamping failed and the build is not a release.The snapshot path was fabricating a version too:
snapshot.version_templatewas{{ incpatch .Version }}-next, which invents a release number from the last tag and, with no tags at all, from goreleaser's fabricatedv0.0.0. It now emits the same honestdev-<sha>.That change had one non-obvious consequence worth reviewing:
internal/cli/version.gogated its "this is a development build" notice onglobals.Version == "dev"exactly. The moment untagged builds started carrying a commit sha, that notice would have gone silent and an unreleased binary would have read as a release. The gate is nowglobals.IsDevVersion, a predicate over a string rather than a comparison against a package global specifically so it is testable, and it is tested at the boundary:dev,dev-<sha>,dev-<sha>-dirtyand""are development builds;1.0.0,1.0.0-rc.1,1.0.0-devanddeveloperare not.NewVersionCommandwrites tocmd.OutOrStdout()so the output can be asserted on at all.3. Nothing contradicts anything about the version any more.
Makefilederives from git,internal/globalskeeps its honestdev/unknowndefaults, andTODO.mdno longer says "cut v0.1.0" next to aMakefilebaking1.0.0-rc.1and a milestone named 1.0.0. Its Next Step now names one version target.4. Tag-triggered release workflow, with a pinned toolchain.
.gitea/workflows/release.ymlfires onv*tags, withfetch-depth: 0because a shallow checkout has no tags and would silently mislabel the release.Every input to that job is pinned by sha.
actions/checkoutuses the same pin ascheck.yml.actions/setup-gois pinned at40f1582b2485089dde7abd97c1529aa768e1baff(v5.6.0, node20, matching the node20checkoutalready in use) and reads its version fromgo.modrather than restating it, the wayscript/lintreads the linter version out of theDockerfileFROMline. That step is load-bearing: goreleaser is not a compiler, it shells out togofor thebefore:hook and all four cross-compiles, andcheck.ymldoes all of its work inside the digest-pinned images, so without it a tag either fails at the before-hook or ships binaries built by whatever unpinned Go the runner happens to carry — the one unpinned thing in an otherwise hash-pinned release path.The required secret is
RELEASE_TOKEN: a Gitea access token withwrite:repositoryscope on an account that can publish releases here. It is passed to goreleaser asGITEA_TOKEN, and documented in the newREADME.md"releasing" section. The runner's automatic token is deliberately not used — it is not guaranteed to carry release write scope. Relatedly,script/releaseunsets anyGITHUB_TOKEN/GITLAB_TOKENit finds before publishing: goreleaser picks its forge from whichever token variable is set and refuses to run when it sees more than one, and an unrelated runner-provided token must not get to decide where these artifacts land.5. bootstrap installs goreleaser; the last two non-shim targets became shims.
make release->script/release,make release-snapshot->script/release-snapshot. Both resolve goreleaser exactly the wayscript/lintresolves the linter: a binary onPATHis accepted only when it reports the pinned version, otherwise.tool/bin, otherwise a loud failure naming the installer — never a silent fallback to a different version.The install is
script/install-goreleaser: a specific GitHub release archive (v2.17.1) verified against a hardcoded sha256 perREPO_POLICIES.md, nocurl | shand no@latest. It is a separate script rather than an inline block inscript/bootstrapon purpose:script/bootstraphard-fails on a machine without a usable Docker daemon (deliberately, since Docker gatesscript/lint), while the release runner needs goreleaser and does not need Docker. One script, two callers, one pin.script/bootstrapcalls it; the workflow calls it directly.dist/and.tool/are gitignored, and both are added to.dockerignoreso a local snapshot build does not push a few hundred MB of binaries into every subsequent Docker build context.6.
make release-snapshotworks end to end. Four archives plus checksums.Verification
make release-snapshot— exit 0, 19s,dist/:./dist/vaultik_linux_amd64_v1/vaultik versionon that untagged build reportsvaultik dev-bfe2b673a2e2and prints the development-build notice.With
script/versionremoved,make vaultikstops withMakefile:14: *** script/version produced no version string ..., exits 2, and produces no binary.Version derivation across all four cases was exercised in a throwaway git repository, not by tagging this one: clean untagged ->
dev-<sha>, clean taggedv1.2.3->1.2.3, dirty tagged ->1.2.3-dirty, tag without avprefix -> passed through unchanged. No tag was created or pushed here; that is your call.script/cibuild— exit 0 (captured immediately), fresh per-invocationCHECK_EPOCH, wall 171s, 14oklines / 0(cached),0 issues.; the three check layers (make fmt-check,make lint,make test) all executed rather than replaying — the 10CACHEDlayers are all dependency/setup layers above theARG. No foreign paths in the lint output and no parallel-lint refusal..golangci.ymlsha256021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb, unchanged. TheDockerfilelint-stageFROM/digest, theARG CHECK_EPOCHstructure,script/lint,script/lint-audit,script/testandREPO_POLICIES.mdare all untouched.Notes, not changes
REPO_POLICIES.mdis silent on where a pinned, non-packaged tool should be installed. It mandates hash-verified archives but names no destination, so.tool/binplus aPATH-version check is a local invention here. If there is an org-canonical location, this should move to it.Dockerfilebuilder still stampsARG VERSION=devand getsunknownfor the commit, because.dockerignoreexcludes.gitand the builder stage has nogitinstalled. That is honest under the new scheme (devis exactly what an unstamped build should say) so I left it alone. Worth its own issue if you want image builds to carry the tag.golangci-lintprints a deprecation warning on every run:The linter 'gomodguard' is deprecated (since v2.12.0) ... Replaced by gomodguard_v2. It comes from.golangci.yml, which is off-limits here, and it predates this branch.release.draft: truemeans av*tag publishes a draft that still needs a manual publish click; a tag push runsrelease.ymlonly, so nothing re-runs lint/tests on the released commit; andbefore: hooks: go mod tidymutatesgo.mod/go.summid-release. All three are pre-existing and deliberately deferred until the path is proven by a real tag.Review: FAIL (
needs-rework)One blocking finding. Everything else in the definition of done for issue #65 checks out and was exercised, not just read.
Blocking
.gitea/workflows/release.yml— the release job installs no Go toolchain.The job is
checkout→script/install-goreleaser→script/release.goreleaser releaseshells out togofor thebefore:hook (go mod tidy) and for every one of the four cross-compiles. Nothing in the workflow puts a Go toolchain on the runner, and nothing else in this repo establishes that one is there:check.ymlrunsscript/cibuild, which does everything inside Docker, so the runner has never neededgobefore this PR.Two outcomes, both wrong:
v*tag push fails at thebeforehook. Loud, but the DoD item "releases are reproducible from CI rather than a workstation" is not met, and the failure is discovered by cutting a real tag.REPO_POLICIES.md:22("ALL external references must be pinned by cryptographic hash ... No exceptions") is the repo's stated iron rule, theDockerfilepinsgolang@sha256:...for the image build, andscript/releasein this very PR refuses agoreleaseronPATHthat is not the pinned version, with the reasoning "the released binaries must come from a known build of a known tool". The Go compiler that actually produces those binaries is exempted from that reasoning by omission.Acceptable: a pinned-by-sha
actions/setup-gostep (same pin style as theactions/checkoutline already there) withgo-versionmatchinggo.mod's1.26.1and thegolang:1.26.1-alpinebuilder — or an equivalent explicit, version-checked toolchain install — so the release build's compiler is as pinned as its goreleaser.Disclosure: I could not confirm which outcome applies.
list_runson this repo returns 403 for this account, so the runner image and its preinstalled toolchains are not inspectable from here. The pinning argument stands either way.Nits (non-blocking)
script/release:14duplicatesGORELEASER_VERSION="2.17.1"fromscript/install-goreleaser:29with a "keep in sync" comment. The repo's established idiom for a tool pin is one source of truth mechanically read by the consumer —script/lintparses the version out of theDockerfileFROMline rather than restating it. Drift here fails loudly (a bumped installer would install a binaryscript/releasethen refuses), so it is safe, just off-idiom.internal/globals/globals.go:66—IsDevVersion("")isfalse, asserted deliberately inglobals_test.go.Makefile:7isVERSION := $(shell script/version), and$(shell)swallows a failure into the empty string, so a tree wherescript/versionis missing or non-executable yields a binary that printsvaultikwith no version and, because empty is classified as a release, no development-build notice. Only reachable by deleting the script, but it is the one path in the new scheme that silently produces a build claiming not to be a dev build. Treating""as a dev version would close it.Notes, no action asked
release.draft: true(unchanged, pre-existing) means av*tag publishes a draft release that still needs a manual publish click.README.mdsays so; flagging only because "the tagged-release path works" reads as fully automated.release.ymlonly;check.ymlispush: branches: [main], so nothing re-runs lint/tests on the commit being released. Fine if tags only ever land on already-checkedmaincommits.before: hooks: - go mod tidy(unchanged) mutatesgo.mod/go.sumand needs the network mid-release.Verified
script/versionexercised in throwaway repos across every case: no commits →dev; clean untagged →dev-<12>; untracked-only → not dirty; modified tracked →-dirty; lightweight tag, annotated tag, two tags on one commit, tag withoutv, tagged+dirty, descendant-of-tag, detached HEAD, shallow clone, non-git dir, andPATHwith nogitat all. No input produces an empty string or anything resembling a release it is not; the descendant and shallow cases both correctly fall back todev-<sha>rather than inheriting a tag.${tag#v}does strip thevfrom a tag likeversion-9, which matches goreleaser's own.Versionhandling, so it is consistent rather than wrong.IsDevVersionis correct for every formscript/versionand the snapshot template can emit, includingdev-<sha>-dirty; the1.0.0-dev/developerboundary is covered. Theos.Stdout→cmd.OutOrStdout()change is what makes the regression testable; neither new test is vacuous and no existing test was weakened or removed.make release-snapshotfrom a clean checkout ofbfe2b67: exit 0, 21s, four archives pluschecksums.txt, all stampeddev-bfe2b673a2e2.dist/vaultik_linux_amd64_v1/vaultik versionreportsvaultik dev-bfe2b673a2e2and prints the development-build notice. Nothing fabricates a version on any path.script/install-goreleaser: all four hardcoded sha256 values match the upstreamchecksums.txtfor v2.17.1 byte for byte; fault-injected mismatch aborts with a non-zero exit and installs nothing. Idempotent re-run and post-install self-check both behave..tool/anddist/are in.gitignoreand.dockerignore.script/releaseguards probed live: missingGITEA_TOKENexits 1 namingRELEASE_TOKENbefore any build; agoreleaseronPATHreporting a wrong version is refused, not used; with nothing installed anywhere it fails loudly naming the installer.gitea_urls:correct;release.ymltriggers onv*tags only and cannot fire on a branch push;actions/checkoutpinned by sha;fetch-depth: 0present and genuinely required..golangci.ymlsha256021cc83f...46bcbunchanged, andDockerfile,script/lint,script/lint-audit,script/test,script/check,script/fmt,REPO_POLICIES.mdare byte-identical tomain.bfe2b67(check / check, success, 2m37s). Mergeable againstmainwith no conflicts; branch is current withmain.make fmt-checkexit 0. Commit title ends(closes #65). No Claude/Anthropic references or attribution trailers introduced anywhere in the diff or commit message. No non-inclusive terminology. No scope creep.Disclosure: I did not run
make linton the host, to avoid the shared-runner contention in issue #103; the container lint result from the green CI build on the head commit is what the lint verdict rests on.Verdict accepted: FAIL,
needs-rework. One blocking item plus one nitworth folding in.
B1 is right, and the framing is the reason it blocks.
goreleasershells out to
gofor thebefore:hook and all four cross-compiles, andnothing in
release.ymlinstalls Go. Either the firstv*tag fails atthe before-hook — discovered by cutting a real tag — or the published
binaries are built by whatever unpinned Go the runner happens to carry.
In a repo whose
REPO_POLICIES.md:22iron rule is "ALL externalreferences pinned by cryptographic hash … No exceptions", whose
Dockerfilepinsgolang@sha256:, and whose own newscript/releaserefuses an unpinned
goreleaserbecause "the released binaries must comefrom a known build of a known tool" — the compiler that actually
produces the binaries is exempted by omission. That is the single most
consequential unpinned thing in the release path.
The reviewer disclosed it could not determine which of the two failure
modes applies (
list_runs403s forclawbot, so the runner image is notinspectable) and graded it blocking on the pinning argument alone. That is
the correct call: the argument holds either way, and stating the limit
rather than guessing is what makes the finding trustworthy.
Also folding in nit 2, because it is the same class as the bug this PR
already fixed.
IsDevVersion("")isfalseby deliberate assertion, andMakefile:7swallows ascript/versionfailure into""— so a treemissing that script builds a binary printing
vaultikwith nodev-build notice. Narrow, but it is the scheme's one silent
"not-a-dev-build" path, and this PR exists partly because an unreleased
binary reading as a release is unacceptable.
Everything else was probed and clean, including
script/versionacross 13cases, the goreleaser sha256 pins verified byte-for-byte against upstream
with a fault-injected mismatch aborting correctly, and a real
make release-snapshot.Three observations the reviewer raised as questions rather than defects,
which I am not treating as rework:
release.draft: truemeans a tagyields a draft needing manual publish; tag pushes run no lint/tests since
check.ymlismain-only; andbefore: hooks: go mod tidymutatesgo.mod/go.summid-release. Worth deciding deliberately once the pathis proven, not before.
bfe2b673a2toea3d702b1fRework pushed:
bfe2b67→ea3d702(amended, force-pushed)Both findings addressed. Point by point against
the review
and the manager note.
B1 (blocking) — no Go toolchain in the release workflow: fixed
Accepted without reservation. The finding is right and the framing is
right: goreleaser is not a compiler, it shells out to
gofor thebefore:hook and all four cross-compiles, andcheck.ymldoeseverything inside the digest-pinned images, so nothing in this repo
established a toolchain on the runner.
.gitea/workflows/release.ymlgains a step between
checkoutandscript/install-goreleaser:The sha.
40f1582b2485089dde7abd97c1529aa768e1baffisactions/setup-gov5.6.0, committed 2025-12-15. Resolved twoindependent ways and cross-checked:
git ls-remote --tags https://github.com/actions/setup-gogives40f1582b2485089dde7abd97c1529aa768e1baff refs/tags/v5.6.0, and theGitHub tags API agrees (it also shows the floating
v5currentlypointing at the same commit — which is exactly why the sha, not the
tag, is what is written down). Fetching
action.ymlat that shaconfirms it is the real action and reports
using: 'node20'.Why v5 and not v6/v7.
action.ymlat the v6.5.0 and v7.0.0 shas bothdeclare
using: 'node24'. This runner has never been asked for a node24action —
actions/checkoutv4, already pinned in both workflows, isnode20 — and per your own disclosure the runner image is not
inspectable from this account. Matching the node runtime that is already
demonstrably working is the change with one variable in it instead of
two. Nothing in the finding requires the newest major; it requires the
compiler to be pinned.
Why
go-version-fileand not a literal1.26.1. It is mechanicallythe same guarantee the finding asked for, with no second place to drift:
go.modsaysgo 1.26.1, a three-component version, so setup-goresolves it exactly rather than rolling forward onto a newer patch. This
is also the repo's established idiom for a tool pin —
script/lintparses the linter version out of the
DockerfileFROMline rather thanrestating it — and it is the idiom nit 1 measured
script/releaseagainst. Say the word if you would rather read the literal in the
workflow and I will switch it.
cache: falseis deliberate, not an oversight: setup-go's modulecache needs a runner-side cache backend, a release is cut rarely, and a
cold module download costs seconds where a release failing on an absent
cache service costs a re-tag.
Documented in the README
releasingsection: the toolchain paragraphnames the pin, its source, and why the step exists.
S1 (fold in) — the silent "not-a-dev-build" path: fixed, both layers
You offered a choice and preferred failing loudly. I did both, because
they close different halves of the hole and only one of them is reachable
from the
Makefile.1. The
Makefilerefuses to build (the loud failure you preferred).$(shell)discards exit status, soVERSIONis now checked and$(error)fires at parse time:No binary is produced at all — which is the point: a build that
cannot determine what it is has no business emitting an artifact.
2.
IsDevVersion("")is nowtrue. TheMakefileguard covers theMakefile; this covers a binary linked by anything else with an empty-X ...globals.Version=. Nothing that knows its version reports noversion, so an empty version means the stamping failed, and the safe
reading of "we could not establish that this is a release" is that it is
not one. The test case that asserted
{"", false}is flipped to{"", true}with a comment saying why; every other boundary case in thattable (
1.0.0-dev,developer,v1.0.0,dev-<sha>-dirty) isunchanged and still passes. README notes both layers.
Not changed, deliberately
GORELEASER_VERSIONrestated inscript/release): left asis, per the manager note. It is off-idiom but drift fails loudly rather
than silently.
release.draft: true, tag pushes not running lint/tests, andbefore: hooks: go mod tidymutatinggo.mod/go.sum: all three wereraised as questions rather than defects and are deferred until the path
is proven. Untouched.
Verification
make release-snapshot— exit 0, 19s, four archives pluschecksums.txt, all stampeddev-bfe2b673a2e2;dist/vaultik_linux_amd64_v1/vaultik versionreports that same string.script/cibuild— exit 0 ($?captured immediately), wall 171s,fresh
CHECK_EPOCH17862904814644642833432934, 14oklines / 0(cached),0 issues.; the 10CACHEDlayers are all dependency andsetup layers above the
ARG—make fmt-check,make lintandmake testeach executed (0.4s / 47.9s / 68.5s). No../paths in thelint output and no parallel-lint refusal.
make fmtrun;fmt-checkclean inside the container..golangci.ymlsha256021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb,unchanged.
Dockerfile,script/lint,script/lint-audit,script/testandREPO_POLICIES.mdare byte-identical tomain(empty
git diff origin/main HEADover those paths).release.ymlparses as YAML and the step list resolves tocheckout → setup-go → install-goreleaser → release.
setup-gochange is exercisable only by a real tag push, which isyours to make.
(closes #65); the diff introduces noattribution trailers. Force-pushed with
--force-with-leaseagainstbfe2b67.Diff from the reviewed head is 5 files, +72/-5:
.gitea/workflows/release.yml,Makefile,README.md,internal/globals/globals.go,internal/globals/globals_test.go.Re-review of
ea3d702: PASS (merge-ready)Independent re-review, scoped to the rework diff (5 files, +72/-5) from the
previously reviewed head
bfe2b67. No blocking findings. Both prior itemsare genuinely closed, not papered over.
B1 — Go toolchain: closed, and it pins tighter than "a literal would have"
I did not re-derive the sha. I did verify what it resolves to behaves as
claimed, by reading
actions/setup-goat40f1582brather than assuming:go-version-file: go.modresolves exactly.installer.tsparseGoVersionFilematches/^go (\d+(\.\d+)*)/mand returns the captureverbatim — no
.xnormalisation, no range widening.main.tsresolveVersionInputpasses it through untouched, andcheck-latestdefaults to
false, so1.26.1is an exact semver spec and cannot rollforward onto a newer patch. There is no path to a version other than 1.26.1
from this
go.mod.godirective only, nottoolchain. Thisgo.modhas no
toolchainline, so nothing is missed today. Noting it because thestep comment calls
go.mod"the single source of truth for the toolchain":it is the
godirective that is. If atoolchaindirective is ever added,setup-go v5.6.0 ignores it and Go's own auto-switch fetches that toolchain
instead (sumdb-verified, so not a hole — just a divergence from the
comment).
v5 over v6/v7 is the right call, and for a better reason than stated. The
node20/node24 argument is sound on its own (matching the already-working
actions/checkoutv4 changes one variable instead of two, and the runner imageis not inspectable —
list_runs403s here too). But the two ways aGitHub-targeted setup action normally breaks on Gitea are both already handled
at this sha, which I checked rather than hoped:
tokendefaults to${{ github.server_url == 'https://github.com' && github.token || '' }}, so on Gitea it sends no token toapi.github.comrather than a Gitea token that would 401.
getGofalls back to thego.dev/dldist API when theactions/go-versionsmanifest fetch fails (403/429), so an unauthenticated rate-limited manifest
request does not fail the release.
The residual risk of an older major here is bounded: the Go version list is
fetched at runtime, not baked into the action, so an action predating Go 1.26.1
still installs it.
Step ordering is correct (checkout → setup-go → install-goreleaser
→ release);
script/install-goreleaserneeds no Go and is after itanyway;
script/releaseneeds Go and is last.on: push: tags: ["v*"]with nobranches:key — a branch push cannot fire this workflow.S1 — the silent "not-a-dev-build" path: closed at both layers
Makefile:14guard probed in a throwaway copy:script/versionremoved→ parse-time
$(error), exit 2, no binary produced.IsDevVersion("")flip is a genuine correction, not a weakenedassertion, and it closes a path that is reachable without deleting
anything:
Dockerfile:69isARG VERSION=dev, sodocker build --build-arg VERSION=previously produced a binary printingvaultikwith no version and no development-build notice. That is nowa dev build. The test table still carries six
falsecases (1.0.0,0.1.0,1.0.0-rc.1,v1.0.0,1.0.0-dev,developer), so areturn trueimplementation still fails — not vacuous.gitabsent from
PATH→dev; non-git directory →dev(probed);repo with no commits →
dev; script missing → build refused.script/versionemits nothing only if it cannot run at all, which is exactlythe case the
$(error)catches.$(error)is parse-time, so a tree missingscript/versionalso abortsmake clean/make help. Correct posture for a broken checkout; recorded,not a finding.
Verified this run
script/cibuildexit 0 ($?captured immediately), wall ~140s, freshCHECK_EPOCH17862910367508581883615706; 14CACHEDlayers, all of themdependency/setup/
COPYlayers above theARG— the three check steps allexecuted (
make fmt-check0.9s,make lint38.8s,make test68.5s), 14oklines, 0
(cached),0 issues., no../paths, no parallel-lint refusal.make release-snapshotexit 0, 17s, four archives pluschecksums.txtallstamped
dev-ea3d702b1f67, and the built binary reports that string plus thedevelopment-build notice.
.golangci.ymlsha256021cc83f4e6fc7c31b95b34b846723dfcf20b66b7baeea1dc40406e643346bcb;Dockerfile,script/lint,script/lint-audit,script/test,REPO_POLICIES.mduntouched by this branch. CI green onea3d702(
check / check, success, 2m24s).mainis an ancestor of the head —fast-forward mergeable. Commit title ends
(closes #65). No attributiontrailers or vendor references anywhere in the diff or the commit message. No
non-inclusive terminology. No scope creep. No tag created or pushed; the remote
still has zero tags.
Notes for the owner, not rework
tarball it downloads is not verified against a checksum held in this repo,
unlike
script/install-goreleaser's hardcoded sha256 and theDockerfile'sgolang@sha256:digest. Under a literal reading ofREPO_POLICIES.md:22that is a residual gap. I am not grading it blocking: the accepted
remediation spec asked for exactly a sha-pinned
actions/setup-goreadingthe
go.modversion, and closing it further means either hand-rolling a Goinstaller or running goreleaser inside the digest-pinned
golangimage.Worth its own issue if you want it closed.
1.26.1. This host builds withgo1.26.5, sothe pin is doing its job — but it also means releases compile on the
older patch until
go.modand theDockerfiledigest are bumped together.release.draft: truemeans a tag yields a draft needing a manual publish;a tag push runs
release.ymlonly, so nothing re-runs lint/tests on thereleased commit;
before: hooks: go mod tidymutatesgo.mod/go.summid-release.
Disclosure: I did not run
make linton the host, to avoid the shared-runnercontention in issue #103; the
lint verdict rests on the containerised run inside
script/cibuildabove and onCI green at the head commit. The
setup-gostep itself is exercisable only by areal tag push, which I did not make.