Make the tagged-release path work on Gitea (closes #65)
All checks were successful
check / check (pull_request) Successful in 2m24s
All checks were successful
check / check (pull_request) Successful in 2m24s
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. The release workflow installs its own Go toolchain, pinned. goreleaser is not a compiler: it shells out to go for the before: hook and for all four cross-compiles, and nothing else in this repo puts a toolchain on the runner, since check.yml does all of its work inside the digest-pinned Dockerfile images. Without that step a tag either fails at the before-hook or, worse, ships binaries built by whatever unpinned Go the runner happens to carry -- the one unpinned thing in a release path whose every other input is hash-pinned, in a repo whose policy admits no exceptions and whose own script/release refuses a goreleaser that is not the pinned build. actions/setup-go is pinned by commit sha like the checkout above it, and reads its version from go.mod rather than restating it. An unobtainable version can no longer produce a binary at all. $(shell) discards exit status, so a missing or broken script/version left VERSION empty and the build went ahead and stamped nothing; the Makefile now stops with an error instead. IsDevVersion("") became true as the second line of defence, 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 a build that cannot be shown to be a release is not one. This is the same defect class as the notice that went silent above, one layer down. 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.
This commit is contained in:
@@ -114,6 +114,14 @@ main() {
|
||||
# sqlite3 CLI: the test suite shells out to it (VACUUM).
|
||||
if missing sqlite3; then pkg_install sqlite sqlite3 sqlite sqlite; fi
|
||||
|
||||
# goreleaser, at the version pinned by script/install-goreleaser and
|
||||
# verified against a hardcoded sha256. Package managers are not used
|
||||
# for it: they ship whatever version they happen to carry, and the
|
||||
# tool that builds a release has to be a known one. The install is
|
||||
# its own script because the release workflow needs goreleaser
|
||||
# without needing the Docker requirement below.
|
||||
"$ROOT/script/install-goreleaser"
|
||||
|
||||
go mod download
|
||||
|
||||
# Last, so that everything installable is installed before the one
|
||||
|
||||
144
script/install-goreleaser
Executable file
144
script/install-goreleaser
Executable file
@@ -0,0 +1,144 @@
|
||||
#!/bin/sh
|
||||
# script/install-goreleaser: install the pinned goreleaser into the
|
||||
# repo-local tool directory. Our own extension to
|
||||
# scripts-to-rule-them-all. Idempotent: exits immediately when the
|
||||
# pinned version is already available.
|
||||
#
|
||||
# script/bootstrap calls this, and so does .gitea/workflows/release.yml.
|
||||
# It is a separate script rather than an inline block in bootstrap
|
||||
# because bootstrap deliberately hard-fails on a machine without a
|
||||
# usable Docker daemon (Docker gates script/lint, and therefore
|
||||
# script/check), while the release runner needs goreleaser and does not
|
||||
# need Docker. One script, two callers, no duplicated pin.
|
||||
#
|
||||
# The install is a specific GitHub release archive verified against the
|
||||
# sha256 hardcoded below, per REPO_POLICIES.md: no `curl | sh`, no
|
||||
# `@latest`, no version tag that a server can move. Bumping goreleaser
|
||||
# means editing GORELEASER_VERSION *and* the four checksums, which are
|
||||
# taken from the checksums.txt published with that release.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
# goreleaser v2.17.1, 2026-08-05. Checksums are from
|
||||
# https://github.com/goreleaser/goreleaser/releases/download/v2.17.1/checksums.txt
|
||||
GORELEASER_VERSION="2.17.1"
|
||||
SHA256_LINUX_X86_64="a99bbc7ae0d8d897b07c4c497a9b62f222558804715ef219d1af05a7e417bc80"
|
||||
SHA256_LINUX_ARM64="702f03769ac8bcb0e47839c82243cc614ae995633599a98c63062e13ea85f829"
|
||||
SHA256_DARWIN_X86_64="a92a68c61a6833ff67748f532cbebc7b8e49ba30de062ab463b221211ee6368f"
|
||||
SHA256_DARWIN_ARM64="b65624885c25da9a677b7ad11cf86a02123cc5a56af66f6b4ebb574658eada2e"
|
||||
|
||||
TOOLBIN="$ROOT/.tool/bin"
|
||||
|
||||
# Print the version of the goreleaser at $1, or nothing if it is not
|
||||
# usable. `goreleaser --version` prints a multi-line banner; the version
|
||||
# is on the line beginning "GitVersion:".
|
||||
goreleaser_version() {
|
||||
[ -x "$1" ] || return 0
|
||||
"$1" --version 2>/dev/null |
|
||||
sed -n 's/^ *GitVersion: *//p' |
|
||||
head -n 1
|
||||
}
|
||||
|
||||
verify_sha256() {
|
||||
file="$1"
|
||||
want="$2"
|
||||
if command -v sha256sum >/dev/null 2>&1; then
|
||||
got="$(sha256sum "$file" | cut -d' ' -f1)"
|
||||
elif command -v shasum >/dev/null 2>&1; then
|
||||
got="$(shasum -a 256 "$file" | cut -d' ' -f1)"
|
||||
else
|
||||
echo "install-goreleaser: no sha256sum or shasum available" >&2
|
||||
return 1
|
||||
fi
|
||||
if [ "$got" != "$want" ]; then
|
||||
echo "install-goreleaser: checksum mismatch for $file" >&2
|
||||
echo " expected: $want" >&2
|
||||
echo " actual: $got" >&2
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
# Already have it, either on PATH or from a previous run? Then stop.
|
||||
# An arbitrary PATH goreleaser is NOT accepted: the config uses
|
||||
# version-2 schema features, and the whole point of pinning is that
|
||||
# a release is cut by a known build of a known tool.
|
||||
if [ "$(goreleaser_version "$(command -v goreleaser || true)")" \
|
||||
= "$GORELEASER_VERSION" ]; then
|
||||
echo "goreleaser $GORELEASER_VERSION already on PATH"
|
||||
return 0
|
||||
fi
|
||||
if [ "$(goreleaser_version "$TOOLBIN/goreleaser")" \
|
||||
= "$GORELEASER_VERSION" ]; then
|
||||
echo "goreleaser $GORELEASER_VERSION already installed in .tool/bin"
|
||||
return 0
|
||||
fi
|
||||
|
||||
os="$(uname -s)"
|
||||
arch="$(uname -m)"
|
||||
case "$os" in
|
||||
Linux) ;;
|
||||
Darwin) ;;
|
||||
*)
|
||||
echo "install-goreleaser: unsupported OS $os" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
case "$arch" in
|
||||
x86_64 | amd64) arch="x86_64" ;;
|
||||
arm64 | aarch64) arch="arm64" ;;
|
||||
*)
|
||||
echo "install-goreleaser: unsupported architecture $arch" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
case "${os}_${arch}" in
|
||||
Linux_x86_64) sum="$SHA256_LINUX_X86_64" ;;
|
||||
Linux_arm64) sum="$SHA256_LINUX_ARM64" ;;
|
||||
Darwin_x86_64) sum="$SHA256_DARWIN_X86_64" ;;
|
||||
Darwin_arm64) sum="$SHA256_DARWIN_ARM64" ;;
|
||||
*)
|
||||
echo "install-goreleaser: no pinned checksum for ${os}_${arch}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
|
||||
archive="goreleaser_${os}_${arch}.tar.gz"
|
||||
url="https://github.com/goreleaser/goreleaser/releases/download/v${GORELEASER_VERSION}/${archive}"
|
||||
|
||||
if ! command -v curl >/dev/null 2>&1; then
|
||||
echo "install-goreleaser: curl is required" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
tmp="$(mktemp -d)"
|
||||
# shellcheck disable=SC2064 # expand $tmp now, not at trap time
|
||||
trap "rm -rf '$tmp'" EXIT INT TERM
|
||||
|
||||
echo "installing goreleaser $GORELEASER_VERSION for ${os}_${arch}"
|
||||
curl -fsSL --retry 3 -o "$tmp/$archive" "$url"
|
||||
verify_sha256 "$tmp/$archive" "$sum"
|
||||
|
||||
tar -xzf "$tmp/$archive" -C "$tmp" goreleaser
|
||||
mkdir -p "$TOOLBIN"
|
||||
# Move into place via a temp name in the destination directory so a
|
||||
# concurrent run never observes a half-written binary.
|
||||
mv "$tmp/goreleaser" "$TOOLBIN/.goreleaser.$$"
|
||||
chmod 0755 "$TOOLBIN/.goreleaser.$$"
|
||||
mv "$TOOLBIN/.goreleaser.$$" "$TOOLBIN/goreleaser"
|
||||
|
||||
installed="$(goreleaser_version "$TOOLBIN/goreleaser")"
|
||||
if [ "$installed" != "$GORELEASER_VERSION" ]; then
|
||||
echo "install-goreleaser: installed binary reports '$installed'," \
|
||||
"expected '$GORELEASER_VERSION'" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo "goreleaser $GORELEASER_VERSION installed to .tool/bin"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
92
script/release
Executable file
92
script/release
Executable file
@@ -0,0 +1,92 @@
|
||||
#!/bin/sh
|
||||
# script/release: build and publish the release artifacts with the
|
||||
# pinned goreleaser. Our own extension to scripts-to-rule-them-all.
|
||||
#
|
||||
# Normally invoked by a tag push through .gitea/workflows/release.yml,
|
||||
# not by hand: a release cut from a workstation is a release nobody can
|
||||
# reproduce. Any arguments are passed through to `goreleaser release`,
|
||||
# which is how script/release-snapshot adds --snapshot.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
ROOT="$(cd "$SCRIPT_DIR/.." && pwd -P)"
|
||||
|
||||
# Keep in sync with script/install-goreleaser, which owns the pin.
|
||||
GORELEASER_VERSION="2.17.1"
|
||||
|
||||
goreleaser_version() {
|
||||
[ -x "$1" ] || return 0
|
||||
"$1" --version 2>/dev/null |
|
||||
sed -n 's/^ *GitVersion: *//p' |
|
||||
head -n 1
|
||||
}
|
||||
|
||||
# Resolve the goreleaser to run, on the same rule script/lint uses for
|
||||
# golangci-lint: a binary on PATH is accepted only when it is exactly
|
||||
# the pinned version, because a differently versioned tool would
|
||||
# produce a differently built release from the same tag. Anything else
|
||||
# comes from .tool/bin, and a missing one is a loud failure naming the
|
||||
# script that installs it rather than a silent fallback.
|
||||
resolve_goreleaser() {
|
||||
path_bin="$(command -v goreleaser || true)"
|
||||
if [ -n "$path_bin" ] &&
|
||||
[ "$(goreleaser_version "$path_bin")" = "$GORELEASER_VERSION" ]; then
|
||||
echo "$path_bin"
|
||||
return 0
|
||||
fi
|
||||
if [ "$(goreleaser_version "$ROOT/.tool/bin/goreleaser")" \
|
||||
= "$GORELEASER_VERSION" ]; then
|
||||
echo "$ROOT/.tool/bin/goreleaser"
|
||||
return 0
|
||||
fi
|
||||
return 1
|
||||
}
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
if ! bin="$(resolve_goreleaser)"; then
|
||||
cat >&2 <<EOF
|
||||
release: goreleaser $GORELEASER_VERSION is not available.
|
||||
|
||||
Run script/bootstrap (or script/install-goreleaser directly) to install
|
||||
it. A goreleaser already on PATH is used only when it reports exactly
|
||||
$GORELEASER_VERSION; any other version is refused rather than used,
|
||||
because the released binaries must come from a known build of a known
|
||||
tool.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
|
||||
snapshot=0
|
||||
for arg in "$@"; do
|
||||
[ "$arg" = "--snapshot" ] && snapshot=1
|
||||
done
|
||||
|
||||
if [ "$snapshot" -eq 0 ]; then
|
||||
# Publishing needs a Gitea token. Check it here so the failure
|
||||
# names the secret, rather than after several minutes of
|
||||
# cross-compiling.
|
||||
if [ -z "${GITEA_TOKEN:-}" ]; then
|
||||
cat >&2 <<'EOF'
|
||||
release: GITEA_TOKEN is not set.
|
||||
|
||||
Publishing needs a Gitea API token with write access to this
|
||||
repository's releases. In CI it comes from the RELEASE_TOKEN repository
|
||||
secret (see .gitea/workflows/release.yml and the Releasing section of
|
||||
README.md). To build without publishing, use script/release-snapshot.
|
||||
EOF
|
||||
exit 1
|
||||
fi
|
||||
# goreleaser picks its forge from whichever token variable is
|
||||
# set and refuses to run when it finds more than one. A CI
|
||||
# runner may export a GITHUB_TOKEN of its own; this repo lives
|
||||
# on Gitea and releases only there, so an unrelated token must
|
||||
# not be allowed to decide where the artifacts are published.
|
||||
unset GITHUB_TOKEN GITLAB_TOKEN
|
||||
fi
|
||||
|
||||
exec "$bin" release --clean "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
18
script/release-snapshot
Executable file
18
script/release-snapshot
Executable file
@@ -0,0 +1,18 @@
|
||||
#!/bin/sh
|
||||
# script/release-snapshot: build the full set of release artifacts
|
||||
# without publishing or tagging anything. Our own extension to
|
||||
# scripts-to-rule-them-all.
|
||||
#
|
||||
# This is the dry run for script/release: same goreleaser, same config,
|
||||
# same cross-compile matrix and checksums, into ./dist. The version it
|
||||
# stamps is the honest dev-<shortcommit> string rather than an invented
|
||||
# release number, so a snapshot binary cannot be mistaken for one.
|
||||
set -eu
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd -P)"
|
||||
|
||||
main() {
|
||||
exec "$SCRIPT_DIR/release" --snapshot "$@"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
73
script/version
Executable file
73
script/version
Executable file
@@ -0,0 +1,73 @@
|
||||
#!/bin/sh
|
||||
# script/version: output the version string to bake into the binary.
|
||||
# Our own extension to scripts-to-rule-them-all, and the single source
|
||||
# of truth for the version: the Makefile's LDFLAGS call this rather
|
||||
# than carrying a hardcoded constant, which is what used to make every
|
||||
# local build claim to be 1.0.0-rc.1 regardless of git state.
|
||||
#
|
||||
# The rules, in order:
|
||||
#
|
||||
# HEAD is exactly on an annotated or lightweight tag
|
||||
# -> that tag, with a leading "v" stripped
|
||||
# anything else
|
||||
# -> "dev-<12 chars of HEAD>"
|
||||
# not a git checkout at all (release tarball, `go install`)
|
||||
# -> "dev"
|
||||
#
|
||||
# Either of the first two gains a "-dirty" suffix when tracked files
|
||||
# have uncommitted changes, because a modified checkout of v1.0.0 is
|
||||
# not v1.0.0. Untracked files are ignored, matching `git describe
|
||||
# --dirty`: a stray scratch file does not change what was compiled.
|
||||
#
|
||||
# The "v" is stripped so that a `make` build and a goreleaser build of
|
||||
# the same tagged commit report the *same* string: goreleaser's
|
||||
# {{ .Version }} is the tag without the prefix, and the release archive
|
||||
# names are built from it. A tag named `v1.0.0` therefore produces
|
||||
# `vaultik 1.0.0`, matching `vaultik_1.0.0_linux_amd64.tar.gz`.
|
||||
#
|
||||
# Nothing here ever invents a version number. An untagged build says so
|
||||
# and names the commit it was built from; it does not round up to the
|
||||
# nearest plausible release.
|
||||
set -eu
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
||||
|
||||
# Length of the commit prefix in a dev version. Matches
|
||||
# globals.ShortCommit, so `vaultik version` shows the same 12 chars in
|
||||
# its version line and its commit line.
|
||||
SHORT_LEN=12
|
||||
|
||||
main() {
|
||||
cd "$ROOT"
|
||||
|
||||
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
||||
echo "dev"
|
||||
return 0
|
||||
fi
|
||||
|
||||
dirty=""
|
||||
if [ -n "$(git status --porcelain --untracked-files=no 2>/dev/null)" ]; then
|
||||
dirty="-dirty"
|
||||
fi
|
||||
|
||||
# --exact-match so a *descendant* of a tag is not reported as that
|
||||
# tag. Plain `git describe --tags` would call a commit 40 patches
|
||||
# past v1.0.0 "v1.0.0-40-gabc1234", and the leading token of that is
|
||||
# a released version the build is not.
|
||||
tag="$(git describe --tags --exact-match HEAD 2>/dev/null || true)"
|
||||
if [ -n "$tag" ]; then
|
||||
echo "${tag#v}${dirty}"
|
||||
return 0
|
||||
fi
|
||||
|
||||
sha="$(git rev-parse "--short=$SHORT_LEN" HEAD 2>/dev/null || true)"
|
||||
if [ -z "$sha" ]; then
|
||||
# A repo with no commits at all.
|
||||
echo "dev"
|
||||
return 0
|
||||
fi
|
||||
|
||||
echo "dev-${sha}${dirty}"
|
||||
}
|
||||
|
||||
main "$@"
|
||||
Reference in New Issue
Block a user