Compare commits
2
Commits
8076a183e0
..
next
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
070090124a | ||
|
|
584444b619 |
+62
-2
@@ -1,4 +1,65 @@
|
|||||||
.git
|
# .dockerignore does NOT use .gitignore semantics. Docker matches with
|
||||||
|
# moby/patternmatcher: filepath.Match plus `**`, so `*` does not cross
|
||||||
|
# `/` and an unprefixed pattern is anchored at the context root. Every
|
||||||
|
# depth-independent pattern therefore needs `**/`, or `config/.env` and
|
||||||
|
# `certs/server.key` still ship while this file reads as solved. Only
|
||||||
|
# genuinely root-anchored entries go unprefixed. Never transplant these
|
||||||
|
# into .gitignore, where `**/` is wrong.
|
||||||
|
#
|
||||||
|
# Matching is case-sensitive, so secrets use character ranges rather
|
||||||
|
# than an ALL-CAPS twin, which would still miss `Server.Key`.
|
||||||
|
#
|
||||||
|
# Extend with this repo's own host-built artifacts, written anchored:
|
||||||
|
# `/myapp`, never `**/myapp`, which also matches `cmd/myapp/` and
|
||||||
|
# deletes the package directory from the context.
|
||||||
|
|
||||||
|
# .git is sent without its config. Without a VERSION build argument the
|
||||||
|
# stage that compiles runs `git describe --tags --always` on .git, which
|
||||||
|
# does not need .git/config; that file can hold a credential, such as a
|
||||||
|
# password in a remote URL or the token the CI checkout step stores there.
|
||||||
|
.git/config
|
||||||
|
|
||||||
|
# Agent scratch: one full checkout of the repo per in-flight agent.
|
||||||
|
# Anchored because it occurs once where agents run at the repo root.
|
||||||
|
# KNOWN GAP: a repo running agents in subdirectories still ships
|
||||||
|
# `services/api/.claude/` and must add its own anchored entry.
|
||||||
|
.claude
|
||||||
|
|
||||||
|
# Environment files. `*.env` covers bare `.env` and the `prod.env`
|
||||||
|
# convention. Re-include a committed template with a negation if the
|
||||||
|
# build needs one: `!docs/example.env`.
|
||||||
|
**/*.[eE][nN][vV]
|
||||||
|
**/.[eE][nN][vV].*
|
||||||
|
**/.[eE][nN][vV][rR][cC]
|
||||||
|
|
||||||
|
# Private keys and the bundles carrying them. Public certificates
|
||||||
|
# (*.crt, *.cer) are deliberately absent: they are legitimate inputs.
|
||||||
|
**/*.[pP][eE][mM]
|
||||||
|
**/*.[kK][eE][yY]
|
||||||
|
**/*.[pP]12
|
||||||
|
**/*.[pP][fF][xX]
|
||||||
|
**/[iI][dD]_[rR][sS][aA]
|
||||||
|
**/[iI][dD]_[dD][sS][aA]
|
||||||
|
**/[iI][dD]_[eE][cC][dD][sS][aA]
|
||||||
|
**/[iI][dD]_[eE][dD]25519
|
||||||
|
|
||||||
|
# Dependencies: restored inside the image, never copied in.
|
||||||
|
**/node_modules
|
||||||
|
|
||||||
|
# OS metadata.
|
||||||
|
**/.DS_Store
|
||||||
|
**/Thumbs.db
|
||||||
|
|
||||||
|
# Editor state: never a build input, and it churns COPY.
|
||||||
|
**/*.swp
|
||||||
|
**/*.swo
|
||||||
|
**/*~
|
||||||
|
**/*.bak
|
||||||
|
**/.idea
|
||||||
|
**/.vscode
|
||||||
|
**/*.sublime-*
|
||||||
|
|
||||||
|
# This repo's own entries.
|
||||||
.gitea
|
.gitea
|
||||||
*.md
|
*.md
|
||||||
LICENSE
|
LICENSE
|
||||||
@@ -7,4 +68,3 @@ dist
|
|||||||
.tool
|
.tool
|
||||||
coverage.out
|
coverage.out
|
||||||
coverage.html
|
coverage.html
|
||||||
.DS_Store
|
|
||||||
|
|||||||
+1
-3
@@ -47,9 +47,7 @@ checksum:
|
|||||||
# A snapshot is not a release and must not name itself like one. The
|
# A snapshot is not a release and must not name itself like one. The
|
||||||
# previous `{{ incpatch .Version }}-next` derived a plausible-looking
|
# previous `{{ incpatch .Version }}-next` derived a plausible-looking
|
||||||
# release number from the last tag -- and with no tags in the repo at
|
# release number from the last tag -- and with no tags in the repo at
|
||||||
# all, from goreleaser's fabricated v0.0.0. This produces the same
|
# all, from goreleaser's fabricated v0.0.0.
|
||||||
# string script/version produces for an untagged build, so a snapshot
|
|
||||||
# binary and a `make vaultik` binary of the same clean commit agree.
|
|
||||||
snapshot:
|
snapshot:
|
||||||
version_template: "dev-{{ slice .FullCommit 0 12 }}"
|
version_template: "dev-{{ slice .FullCommit 0 12 }}"
|
||||||
|
|
||||||
|
|||||||
+27
-31
@@ -20,10 +20,10 @@
|
|||||||
# golang:1.26.1-alpine, 2026-03-17
|
# golang:1.26.1-alpine, 2026-03-17
|
||||||
FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder
|
FROM golang:1.26.1-alpine@sha256:2389ebfa5b7f43eeafbd6be0c3700cc46690ef842ad962f6c5bd6be49ed82039 AS builder
|
||||||
|
|
||||||
# Build tooling: make, plus a C toolchain because `go test -race` needs cgo.
|
# Build tooling: make, plus a C toolchain because `go test -race` needs cgo,
|
||||||
# The sqlite driver is pure Go (modernc.org/sqlite), so no sqlite library or
|
# and git, which derives the version below. The sqlite driver is pure Go
|
||||||
# CLI is required.
|
# (modernc.org/sqlite), so no sqlite library or CLI is required.
|
||||||
RUN apk add --no-cache make build-base
|
RUN apk add --no-cache make build-base git
|
||||||
|
|
||||||
WORKDIR /src
|
WORKDIR /src
|
||||||
|
|
||||||
@@ -47,48 +47,44 @@ COPY . .
|
|||||||
# unreferenced-ARG handling staying as it is. It also puts the epoch in
|
# unreferenced-ARG handling staying as it is. It also puts the epoch in
|
||||||
# the build log, where a reader can see the layer was keyed fresh.
|
# the build log, where a reader can see the layer was keyed fresh.
|
||||||
#
|
#
|
||||||
# The guard is what makes a build that omits --build-arg fail instead of
|
# A build that passes no CHECK_EPOCH, such as a plain `docker build .`,
|
||||||
# lie. An unset ARG is an empty string, and an empty string is a
|
# keys these layers on the empty string, so rebuilding an unchanged
|
||||||
# perfectly stable cache key: without the guard the first such build
|
# checkout replays them from cache and runs nothing. Only the scripts'
|
||||||
# runs the checks and every one after it on an unchanged tree replays
|
# builds mean the checks executed.
|
||||||
# these layers from cache, executes nothing, and still exits 0. Failed
|
|
||||||
# steps are never cached, so the guard fails on EVERY invocation rather
|
|
||||||
# than once -- a bare `docker build .` is a loud error, not a quiet
|
|
||||||
# green. Do not give CHECK_EPOCH a default value; a default would
|
|
||||||
# satisfy the guard with a constant and restore the hole.
|
|
||||||
#
|
#
|
||||||
# Everything above this line (apk, go.mod, `go mod download`) is
|
# Everything above this line (apk, go.mod, `go mod download`) is
|
||||||
# deliberately outside the busted range and keeps caching.
|
# deliberately outside the busted range and keeps caching.
|
||||||
ARG CHECK_EPOCH
|
ARG CHECK_EPOCH
|
||||||
RUN [ -n "$CHECK_EPOCH" ] || exit 1
|
|
||||||
RUN echo "check epoch: ${CHECK_EPOCH}" && make fmt-check
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make fmt-check
|
||||||
RUN echo "check epoch: ${CHECK_EPOCH}" && make test
|
RUN echo "check epoch: ${CHECK_EPOCH}" && make test
|
||||||
|
|
||||||
# Version, commit and build date are computed on the host by
|
# Version, commit and build date: the build args when given (script/docker
|
||||||
# script/docker and script/cibuild (where .git exists) and passed in as
|
# and script/cibuild pass the ones they compute on the host), otherwise
|
||||||
# build args. The build context excludes .git (see .dockerignore), so
|
# derived from the .git in the build context. The version is then `git
|
||||||
# the build cannot derive them itself: it used to try, with `git
|
# describe --tags --always`: the tag on a tagged commit, tag-N-gHASH after
|
||||||
# rev-parse` inside this stage, and always got "unknown". VERSION comes
|
# one, the short commit when no tag is reachable. A context that carries
|
||||||
# from script/version, the source of truth shared with the Makefile, so
|
# .git and still yields no version fails the build; one without .git, as
|
||||||
# it carries the same tag / dev-<sha> / -dirty rules and a Docker image
|
# from a source tarball, stamps "dev" and an "unknown" commit and date.
|
||||||
# reports the same string a local build of the same tree would.
|
|
||||||
#
|
|
||||||
# The defaults are the fallback for a bare `docker build .` that passes
|
|
||||||
# none of them: an unset arg would otherwise stamp an empty string and
|
|
||||||
# produce an image that cannot report its own version, commit or date.
|
|
||||||
# They match what an out-of-git build reports elsewhere.
|
|
||||||
#
|
#
|
||||||
# These ARGs sit here, after the checks, rather than at the top of the
|
# These ARGs sit here, after the checks, rather than at the top of the
|
||||||
# stage: every commit changes their values, and a value change
|
# stage: every commit changes their values, and a value change
|
||||||
# invalidates all layers below the ARG. Declared up top they would bust
|
# invalidates all layers below the ARG. Declared up top they would bust
|
||||||
# `go mod download`; here they only rekey this build layer, which the
|
# `go mod download`; here they only rekey this build layer, which the
|
||||||
# COPY of the sources above already rebuilds on any change anyway.
|
# COPY of the sources above already rebuilds on any change anyway.
|
||||||
ARG VERSION=dev
|
ARG VERSION
|
||||||
ARG COMMIT=unknown
|
ARG COMMIT
|
||||||
ARG COMMIT_DATE=unknown
|
ARG COMMIT_DATE
|
||||||
|
|
||||||
# Build (pure Go, no CGO required since we use modernc.org/sqlite)
|
# Build (pure Go, no CGO required since we use modernc.org/sqlite)
|
||||||
RUN CGO_ENABLED=0 go build -ldflags "-X 'sneak.berlin/go/vaultik/internal/globals.Version=${VERSION}' -X 'sneak.berlin/go/vaultik/internal/globals.Commit=${COMMIT}' -X 'sneak.berlin/go/vaultik/internal/globals.CommitDate=${COMMIT_DATE}'" -o /vaultik ./cmd/vaultik
|
RUN version="${VERSION:-$(git describe --tags --always || echo dev)}"; \
|
||||||
|
if [ -e .git ] && { [ -z "$version" ] || [ "$version" = dev ] || \
|
||||||
|
[ "$version" = unknown ]; }; then \
|
||||||
|
echo "the build context carries .git but yields no version" >&2; \
|
||||||
|
exit 1; \
|
||||||
|
fi; \
|
||||||
|
commit="${COMMIT:-$(git rev-parse HEAD || echo unknown)}"; \
|
||||||
|
commit_date="${COMMIT_DATE:-$(git show -s --format=%cs HEAD || echo unknown)}"; \
|
||||||
|
CGO_ENABLED=0 go build -ldflags "-X 'sneak.berlin/go/vaultik/internal/globals.Version=${version}' -X 'sneak.berlin/go/vaultik/internal/globals.Commit=${commit}' -X 'sneak.berlin/go/vaultik/internal/globals.CommitDate=${commit_date}'" -o /vaultik ./cmd/vaultik
|
||||||
|
|
||||||
# Runtime stage
|
# Runtime stage
|
||||||
# alpine:3.21, 2026-02-25
|
# alpine:3.21, 2026-02-25
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
.PHONY: all bootstrap setup check test lint lint-fix fmt fmt-check build clean deps test-coverage local install release release-snapshot docker hooks
|
.PHONY: all bootstrap setup check test lint lint-fix fmt fmt-check build clean deps test-coverage local install release release-snapshot docker hooks
|
||||||
|
|
||||||
# Version number, derived from git by script/version -- the tag when
|
# Version number, derived from git by script/version (`git describe
|
||||||
# HEAD is on one, otherwise dev-<sha>. This used to be a hardcoded
|
# --tags --always --dirty`). This used to be a hardcoded
|
||||||
# constant, which meant every local build claimed to be a release that
|
# constant, which meant every local build claimed to be a release that
|
||||||
# had never been tagged.
|
# had never been tagged.
|
||||||
VERSION := $(shell script/version)
|
VERSION := $(shell script/version)
|
||||||
|
|||||||
@@ -770,8 +770,9 @@ them. We provide:
|
|||||||
* `script/projectname` — print the project name (used for the Docker
|
* `script/projectname` — print the project name (used for the Docker
|
||||||
image tag)
|
image tag)
|
||||||
* `script/version` — print the version string to bake into the binary.
|
* `script/version` — print the version string to bake into the binary.
|
||||||
The `Makefile`'s `LDFLAGS` call this; it is the single source of truth
|
The `Makefile`'s `LDFLAGS` call this, and `script/docker` and
|
||||||
for the version. See [releasing](#releasing) for the rules.
|
`script/cibuild` pass its output to the image build. See
|
||||||
|
[releasing](#releasing) for the rules.
|
||||||
* `script/install-goreleaser` — install the pinned `goreleaser` into
|
* `script/install-goreleaser` — install the pinned `goreleaser` into
|
||||||
`.tool/bin` from a sha256-verified release archive. Idempotent, and
|
`.tool/bin` from a sha256-verified release archive. Idempotent, and
|
||||||
called by `script/bootstrap`; the release workflow calls it directly
|
called by `script/bootstrap`; the release workflow calls it directly
|
||||||
@@ -863,16 +864,18 @@ them. We provide:
|
|||||||
module layers sit above the `ARG` and still cache, so a build is not
|
module layers sit above the `ARG` and still cache, so a build is not
|
||||||
cold.
|
cold.
|
||||||
|
|
||||||
A build that supplies no `CHECK_EPOCH` — a bare `docker build .` or
|
A `docker build -f Dockerfile.lint .` that supplies no `CHECK_EPOCH`
|
||||||
`docker build -f Dockerfile.lint .` — fails rather than lying. An
|
fails rather than lying. An unset `ARG` is an empty string and an
|
||||||
unset `ARG` is an empty string and an empty string is a stable cache
|
empty string is a stable cache key, so without a guard such a build
|
||||||
key, so without a guard such a build would serve every check layer
|
would serve the lint layer from cache, execute nothing, and still exit
|
||||||
from cache, execute nothing, and still exit 0. Each file therefore
|
0. `Dockerfile.lint` therefore asserts the value is non-empty before
|
||||||
asserts the value is non-empty before running anything, and because
|
running anything, and because failed steps are never cached that
|
||||||
failed steps are never cached that assertion fires on every
|
assertion fires on every invocation rather than once. The product
|
||||||
invocation rather than once. Use `script/lint`, `script/docker` or
|
`Dockerfile` has no such guard, because a plain `docker build .` must
|
||||||
`script/cibuild`, which pass the arg; a bare `docker build` is a loud
|
succeed: without `CHECK_EPOCH`, rebuilding an unchanged checkout
|
||||||
error.
|
replays its check layers from cache. Use `script/lint`,
|
||||||
|
`script/docker` or `script/cibuild`, which pass the arg, when the
|
||||||
|
checks must run.
|
||||||
* `script/precommit` — pre-commit gate: `go mod tidy` + `go fmt` (must
|
* `script/precommit` — pre-commit gate: `go mod tidy` + `go fmt` (must
|
||||||
not change files), then `script/check`
|
not change files), then `script/check`
|
||||||
* `script/install-precommit` — install the git pre-commit hook that
|
* `script/install-precommit` — install the git pre-commit hook that
|
||||||
@@ -883,24 +886,33 @@ them. We provide:
|
|||||||
### version numbers
|
### version numbers
|
||||||
|
|
||||||
The version a binary reports comes from git, not from a constant in a
|
The version a binary reports comes from git, not from a constant in a
|
||||||
file. `script/version` decides it, and everything that stamps a binary
|
file. It is `git describe --tags --always --dirty`, which
|
||||||
agrees with it:
|
`script/version` runs for the `Makefile`, `script/docker` and
|
||||||
|
`script/cibuild`:
|
||||||
|
|
||||||
* `HEAD` is exactly on a tag → that tag with a leading `v` stripped, so
|
* `HEAD` is exactly on a tag → that tag, such as `v1.0.0`.
|
||||||
the tag `v1.0.0` produces `vaultik 1.0.0`, matching the archive name
|
* a commit after a tag → `<tag>-<N>-g<short sha>`.
|
||||||
`vaultik_1.0.0_linux_amd64.tar.gz`. `goreleaser` strips the prefix the
|
* no tag reachable → the short commit sha.
|
||||||
same way.
|
* any of these, with uncommitted changes to tracked files → a `-dirty`
|
||||||
* anything else → `dev-<12 chars of the commit sha>`.
|
|
||||||
* either, with uncommitted changes to tracked files → a `-dirty`
|
|
||||||
suffix, because a modified checkout of a tag is not that tag.
|
suffix, because a modified checkout of a tag is not that tag.
|
||||||
|
|
||||||
A build that is not a release never names itself like one. `vaultik
|
A `docker build .` of a clone, with no build arguments, runs the same
|
||||||
version` says so in as many words on a development build, and
|
`git describe` (without `--dirty`) on the `.git` in its build context,
|
||||||
`goreleaser --snapshot` stamps the same `dev-<sha>` string rather than
|
so it stamps the same value for a clean commit; the build fails if the
|
||||||
inventing the next patch number. If `script/version` cannot be run at
|
context carries `.git` and no version comes out. A binary built without
|
||||||
all, `make` stops with an error instead of building an unversioned
|
git metadata reports `dev`.
|
||||||
binary, and a binary that somehow carries an empty version string still
|
|
||||||
reports itself as a development build.
|
`goreleaser` stamps a release binary with the tag minus its leading
|
||||||
|
`v`, so the tag `v1.0.0` produces `vaultik 1.0.0`, matching the archive
|
||||||
|
name `vaultik_1.0.0_linux_amd64.tar.gz`. `goreleaser --snapshot` stamps
|
||||||
|
`dev-<12 chars of the commit sha>` rather than inventing the next patch
|
||||||
|
number. `vaultik version` calls a build a development build when its
|
||||||
|
version is `dev`, `dev-<sha>`, the short commit sha or
|
||||||
|
`<tag>-<N>-g<short sha>`, with or without `-dirty`; only a plain tag,
|
||||||
|
such as `v1.0.0` or `1.0.0`, is a release. If `script/version` cannot
|
||||||
|
be run at all, `make` stops with an error instead of building an
|
||||||
|
unversioned binary, and a binary that somehow carries an empty version
|
||||||
|
string still reports itself as a development build.
|
||||||
|
|
||||||
### cutting a release
|
### cutting a release
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import (
|
|||||||
// This file guards the version stamping of the product image (issue
|
// This file guards the version stamping of the product image (issue
|
||||||
// #75). The failure it protects against is silent: the image still
|
// #75). The failure it protects against is silent: the image still
|
||||||
// builds and runs, but `vaultik version` inside it reports "commit:
|
// builds and runs, but `vaultik version` inside it reports "commit:
|
||||||
// unknown", so an operator cannot tell which source produced a given
|
// unknown" or a version of "dev", so an operator cannot tell which
|
||||||
// backup. .dockerignore excludes .git, so the build cannot derive the
|
// source produced a given backup. The build takes the values as build
|
||||||
// commit itself; the values must be computed on the host and passed in.
|
// args, which script/docker computes on the host, and otherwise derives
|
||||||
|
// them from the .git in its context.
|
||||||
//
|
//
|
||||||
// These are parses of the committed files, for the same reason the lint
|
// These are parses of the committed files, for the same reason the lint
|
||||||
// guards next door are: shelling out to docker would nest a build
|
// guards next door are: shelling out to docker would nest a build
|
||||||
@@ -32,35 +33,41 @@ func versionArgs() []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestProductDockerfileTakesVersionAsBuildArgs fails unless the build
|
// TestProductDockerfileTakesVersionAsBuildArgs fails unless the build
|
||||||
// declares each version arg and stamps it into the binary by ldflag
|
// declares each version arg, with no default, and stamps it into the
|
||||||
// reference, rather than computing it in the container.
|
// binary whenever it is given, ahead of the value derived in the
|
||||||
|
// container.
|
||||||
func TestProductDockerfileTakesVersionAsBuildArgs(t *testing.T) {
|
func TestProductDockerfileTakesVersionAsBuildArgs(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
found := instructions(t, productDockerfile)
|
found := instructions(t, productDockerfile)
|
||||||
|
|
||||||
for _, arg := range versionArgs() {
|
for _, arg := range versionArgs() {
|
||||||
require.GreaterOrEqual(t, indexOf(found, "ARG "+arg), 0,
|
require.Contains(t, found, "ARG "+arg,
|
||||||
"%s must declare `ARG %s` so the host can pass it in",
|
"%s must declare `ARG %s`, with no default, so the host can"+
|
||||||
productDockerfile, arg)
|
" pass it in", productDockerfile, arg)
|
||||||
|
|
||||||
assertLdflagReferences(t, found, arg)
|
assertLdflagReferences(t, found, arg)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestProductDockerfileDoesNotDeriveVersionItself is the anti-regression
|
// TestProductDockerfileDerivesVersionFromGit fails unless a build given
|
||||||
// for the original defect: the container ran `git rev-parse`, but .git
|
// no VERSION, such as a plain `docker build .` of a clone, takes it from
|
||||||
// is not in the build context, so it always resolved to "unknown". No
|
// `git describe` of the .git in its context, and fails rather than
|
||||||
// git command may reach into a build that cannot see the history.
|
// stamp "dev" when that .git yields no version.
|
||||||
func TestProductDockerfileDoesNotDeriveVersionItself(t *testing.T) {
|
func TestProductDockerfileDerivesVersionFromGit(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
text := instructionText(readRepoFile(t, productDockerfile))
|
found := instructions(t, productDockerfile)
|
||||||
|
|
||||||
assert.NotContains(t, text, "git ",
|
buildAt := indexContaining(found, "go build")
|
||||||
"%s must not run git: .git is excluded from the build context, so"+
|
require.GreaterOrEqual(t, buildAt, 0, "%s must build", productDockerfile)
|
||||||
" any value it derives is wrong. Pass version, commit and date"+
|
|
||||||
" in as build args instead.", productDockerfile)
|
assert.Contains(t, found[buildAt], "git describe --tags --always",
|
||||||
|
"%s must derive the version from git when no VERSION is given",
|
||||||
|
productDockerfile)
|
||||||
|
assert.Contains(t, found[buildAt], "[ -e .git ]",
|
||||||
|
"%s must fail when the context carries .git but yields no version",
|
||||||
|
productDockerfile)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestDockerScriptComputesVersionOnTheHost fails unless script/docker
|
// TestDockerScriptComputesVersionOnTheHost fails unless script/docker
|
||||||
@@ -78,25 +85,25 @@ func TestDockerScriptComputesVersionOnTheHost(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
assert.Contains(t, script, "/version",
|
assert.Contains(t, script, "/version",
|
||||||
"%s must take VERSION from script/version, the source of truth"+
|
"%s must take VERSION from script/version, as the Makefile does",
|
||||||
" shared with the Makefile", dockerScript)
|
dockerScript)
|
||||||
}
|
}
|
||||||
|
|
||||||
// assertLdflagReferences fails unless some build instruction stamps the
|
// assertLdflagReferences fails unless the build instruction uses the
|
||||||
// named variable from the ARG (a ${arg} reference), not from a value
|
// named ARG whenever it is given (a ${arg:- reference), so a value
|
||||||
// computed inside the container.
|
// passed in is not overridden by one derived inside the container.
|
||||||
func assertLdflagReferences(t *testing.T, found []string, arg string) {
|
func assertLdflagReferences(t *testing.T, found []string, arg string) {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
|
|
||||||
for _, instruction := range found {
|
for _, instruction := range found {
|
||||||
if strings.HasPrefix(instruction, "RUN ") &&
|
if strings.HasPrefix(instruction, "RUN ") &&
|
||||||
strings.Contains(instruction, "go build") &&
|
strings.Contains(instruction, "go build") &&
|
||||||
strings.Contains(instruction, "${"+arg+"}") {
|
strings.Contains(instruction, "${"+arg+":-") {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
assert.Fail(t, "version arg is declared but never stamped",
|
assert.Fail(t, "version arg is declared but never stamped",
|
||||||
"the go build in %s must reference ${%s} in its ldflags, or the"+
|
"the go build in %s must use ${%s:-...}, or the arg is passed and"+
|
||||||
" arg is passed and discarded", productDockerfile, arg)
|
" discarded", productDockerfile, arg)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,20 +152,21 @@ func TestLintDockerfileVerifiesTheLinterConfig(t *testing.T) {
|
|||||||
assertEpochExpandedInto(t, found, verify)
|
assertEpochExpandedInto(t, found, verify)
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestProductDockerfileCannotBeCachedGreen holds the same line for the
|
// TestProductDockerfileKeysChecksOnTheEpoch holds the same line for the
|
||||||
// checks that remain in the product image build.
|
// checks that remain in the product image build, but without the guard:
|
||||||
func TestProductDockerfileCannotBeCachedGreen(t *testing.T) {
|
// a plain `docker build .` with no build arguments must succeed.
|
||||||
|
func TestProductDockerfileKeysChecksOnTheEpoch(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
found := instructions(t, productDockerfile)
|
found := instructions(t, productDockerfile)
|
||||||
|
|
||||||
argAt := indexOf(found, checkEpochARG)
|
argAt := indexOf(found, checkEpochARG)
|
||||||
require.GreaterOrEqual(t, argAt, 0,
|
require.GreaterOrEqual(t, argAt, 0,
|
||||||
"%s must declare `%s` with no default value",
|
"%s must declare `%s`", productDockerfile, checkEpochARG)
|
||||||
productDockerfile, checkEpochARG)
|
|
||||||
|
|
||||||
assert.GreaterOrEqual(t, indexOf(found, checkEpochGuard), argAt,
|
assert.Equal(t, -1, indexOf(found, checkEpochGuard),
|
||||||
"%s must guard against an empty CHECK_EPOCH", productDockerfile)
|
"%s must not refuse an empty CHECK_EPOCH: a plain `docker build .`"+
|
||||||
|
" must succeed", productDockerfile)
|
||||||
|
|
||||||
assertEpochExpandedInto(t, found[argAt:], "make fmt-check")
|
assertEpochExpandedInto(t, found[argAt:], "make fmt-check")
|
||||||
assertEpochExpandedInto(t, found[argAt:], "make test")
|
assertEpochExpandedInto(t, found[argAt:], "make test")
|
||||||
|
|||||||
+18
-10
@@ -3,6 +3,7 @@
|
|||||||
package globals
|
package globals
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"regexp"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
)
|
)
|
||||||
@@ -10,12 +11,11 @@ import (
|
|||||||
// Appname is the application name, populated from main().
|
// Appname is the application name, populated from main().
|
||||||
var Appname = "vaultik" //nolint:gochecknoglobals // set via -ldflags at build time
|
var Appname = "vaultik" //nolint:gochecknoglobals // set via -ldflags at build time
|
||||||
|
|
||||||
// DevVersion is the version a binary reports when it was not built
|
// DevVersion is the version a binary reports when it was built without
|
||||||
// from a tagged commit. script/version emits either this exact string
|
// git metadata: script/version emits it outside a git checkout, and an
|
||||||
// (outside a git checkout) or this string followed by "-" and the
|
// unstamped `go build` keeps it. goreleaser's snapshot template stamps
|
||||||
// commit it was built from, and goreleaser's snapshot template matches
|
// it followed by "-" and the commit it was built from. It is
|
||||||
// that shape. It is deliberately not a number: a build that is not a
|
// deliberately not a number.
|
||||||
// release must not name itself like one.
|
|
||||||
const DevVersion = "dev"
|
const DevVersion = "dev"
|
||||||
|
|
||||||
// Version is the application version, populated from main().
|
// Version is the application version, populated from main().
|
||||||
@@ -60,9 +60,12 @@ func New() (*Globals, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// IsDevVersion reports whether v names a development build rather than
|
// IsDevVersion reports whether v names a development build rather than
|
||||||
// a release. Both "dev" and "dev-<sha>" (and its "-dirty" variant)
|
// a release. "dev" and goreleaser's snapshot "dev-<sha>" count, and so
|
||||||
// count: a caller that compares against "dev" exactly would treat every
|
// does what `git describe --tags --always --dirty` gives a make or
|
||||||
// commit-stamped development build as a release.
|
// docker build of an untagged commit: the bare short commit, or
|
||||||
|
// tag-N-gHASH on a commit after a tag. Any version ending in "-dirty"
|
||||||
|
// counts, a modified checkout of a tag ("v1.0.0-dirty") included.
|
||||||
|
// A plain tag such as "v1.0.0" or "1.0.0" is a release.
|
||||||
//
|
//
|
||||||
// The empty string counts too. Nothing that knows its version reports
|
// The empty string counts too. Nothing that knows its version reports
|
||||||
// no version, so an empty Version means the stamping failed, and the
|
// no version, so an empty Version means the stamping failed, and the
|
||||||
@@ -71,7 +74,12 @@ func New() (*Globals, error) {
|
|||||||
// case; this is the second line of defence, for a binary linked by
|
// case; this is the second line of defence, for a binary linked by
|
||||||
// something other than the Makefile.
|
// something other than the Makefile.
|
||||||
func IsDevVersion(v string) bool {
|
func IsDevVersion(v string) bool {
|
||||||
return v == "" || v == DevVersion || strings.HasPrefix(v, DevVersion+"-")
|
if v == "" || v == DevVersion || strings.HasPrefix(v, DevVersion+"-") ||
|
||||||
|
strings.HasSuffix(v, "-dirty") {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
return regexp.MustCompile(`^(.+-[0-9]+-g)?[0-9a-f]+$`).MatchString(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
// shortCommitLen is the number of commit-hash characters ShortCommit keeps.
|
// shortCommitLen is the number of commit-hash characters ShortCommit keeps.
|
||||||
|
|||||||
@@ -34,10 +34,11 @@ func TestGlobalsNew(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TestIsDevVersion covers the boundary that matters: everything
|
// TestIsDevVersion covers the boundary that matters: everything
|
||||||
// script/version and goreleaser's snapshot template can emit for an
|
// script/version, a plain docker build and goreleaser's snapshot
|
||||||
// untagged build must be recognised as a development build, and a real
|
// template can emit for an untagged build must be recognised as a
|
||||||
// tag must not be. A plain equality check against "dev" used to decide
|
// development build, and a real tag must not be. A plain equality check
|
||||||
// this, which classified every commit-stamped dev build as a release.
|
// against "dev" used to decide this, which classified every
|
||||||
|
// commit-stamped dev build as a release.
|
||||||
func TestIsDevVersion(t *testing.T) {
|
func TestIsDevVersion(t *testing.T) {
|
||||||
t.Parallel()
|
t.Parallel()
|
||||||
|
|
||||||
@@ -49,8 +50,17 @@ func TestIsDevVersion(t *testing.T) {
|
|||||||
{"dev", true},
|
{"dev", true},
|
||||||
{"dev-b6e4a218a39e", true},
|
{"dev-b6e4a218a39e", true},
|
||||||
{"dev-b6e4a218a39e-dirty", true},
|
{"dev-b6e4a218a39e-dirty", true},
|
||||||
// What a tagged build produces (script/version strips the
|
// What `git describe --tags --always --dirty` produces with no
|
||||||
// leading "v", matching goreleaser's .Version).
|
// tag reachable, and on a commit after a tag.
|
||||||
|
{"877eb2f", true},
|
||||||
|
{"877eb2f-dirty", true},
|
||||||
|
{"v1.0.0-3-g877eb2f", true},
|
||||||
|
{"v1.0.0-3-g877eb2f-dirty", true},
|
||||||
|
{"1.0.0-rc.1-12-g877eb2f", true},
|
||||||
|
// A tagged commit with uncommitted changes is not that tag.
|
||||||
|
{"v1.0.0-dirty", true},
|
||||||
|
// What a tagged build produces (goreleaser's .Version strips
|
||||||
|
// the leading "v"; script/version keeps it).
|
||||||
{"1.0.0", false},
|
{"1.0.0", false},
|
||||||
{"0.1.0", false},
|
{"0.1.0", false},
|
||||||
{"1.0.0-rc.1", false},
|
{"1.0.0-rc.1", false},
|
||||||
|
|||||||
+9
-9
@@ -24,9 +24,11 @@ main() {
|
|||||||
# value is what forces those layers to re-run: without it an
|
# value is what forces those layers to re-run: without it an
|
||||||
# unchanged tree replays them from cache, the checks never execute,
|
# unchanged tree replays them from cache, the checks never execute,
|
||||||
# and the build still exits 0. Each ARG sits immediately above the
|
# and the build still exits 0. Each ARG sits immediately above the
|
||||||
# check RUNs, so dependency and module layers still cache. Both
|
# check RUNs, so dependency and module layers still cache.
|
||||||
# Dockerfiles also refuse to build at all when CHECK_EPOCH is empty,
|
# Dockerfile.lint also refuses to build at all when CHECK_EPOCH is
|
||||||
# so a missing value fails loudly here rather than passing quietly.
|
# empty, so a missing value fails its build loudly rather than passing
|
||||||
|
# quietly; the product Dockerfile does not, because a plain `docker
|
||||||
|
# build .` must succeed.
|
||||||
#
|
#
|
||||||
# The value must be unique per invocation, not per second. `date +%s`
|
# The value must be unique per invocation, not per second. `date +%s`
|
||||||
# is second-granular, so two concurrent invocations in the same
|
# is second-granular, so two concurrent invocations in the same
|
||||||
@@ -57,12 +59,10 @@ main() {
|
|||||||
--build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint .
|
--build-arg CHECK_EPOCH="$epoch" -f Dockerfile.lint .
|
||||||
|
|
||||||
# Version, commit and build date are computed here on the host, the
|
# Version, commit and build date are computed here on the host, the
|
||||||
# same way script/docker does, and passed into the product build so
|
# same way script/docker does, and passed into the product build,
|
||||||
# the CI-built image reports its real source. The build context
|
# where they take precedence over what the build would derive from
|
||||||
# excludes .git (see .dockerignore), so the build cannot derive them
|
# the .git in its context. VERSION comes from script/version, as in
|
||||||
# itself; without these it would stamp the Dockerfile's dev/unknown
|
# the Makefile.
|
||||||
# fallbacks. VERSION comes from script/version, the source of truth
|
|
||||||
# shared with the Makefile.
|
|
||||||
version="$("$ROOT/script/version")"
|
version="$("$ROOT/script/version")"
|
||||||
commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||||
commit_date="$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)"
|
commit_date="$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)"
|
||||||
|
|||||||
+10
-13
@@ -1,7 +1,8 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/docker: build the Docker image tagged with the project name.
|
# script/docker: build the Docker image tagged with the project name.
|
||||||
# Identical in all repos; the tag comes from script/projectname.
|
# The tag comes from script/projectname. Unlike the canonical copy in
|
||||||
# Generic: needs no adaptation.
|
# sneak/prompts, it passes a fresh CHECK_EPOCH instead of --no-cache, and
|
||||||
|
# COMMIT and COMMIT_DATE as well as VERSION.
|
||||||
#
|
#
|
||||||
# This builds the PRODUCT image only, and the product Dockerfile has no
|
# This builds the PRODUCT image only, and the product Dockerfile has no
|
||||||
# lint stage: linting lives in Dockerfile.lint and is run by
|
# lint stage: linting lives in Dockerfile.lint and is run by
|
||||||
@@ -21,19 +22,15 @@ main() {
|
|||||||
# comments there. This script is not the CI gate, but a local build
|
# comments there. This script is not the CI gate, but a local build
|
||||||
# is almost always warm, so without this it would report a green the
|
# is almost always warm, so without this it would report a green the
|
||||||
# tree had not earned and the two entrypoints would disagree about
|
# tree had not earned and the two entrypoints would disagree about
|
||||||
# whether the tree is clean. The Dockerfile now refuses to build
|
# whether the tree is clean.
|
||||||
# without a non-empty value, so this is required, not optional.
|
|
||||||
epoch="$(date +%s%N)$$"
|
epoch="$(date +%s%N)$$"
|
||||||
|
|
||||||
# Version, commit and build date are computed here on the host,
|
# Version, commit and build date are computed here on the host and
|
||||||
# where .git exists, and passed into the build. The build context
|
# passed into the build, where they take precedence over what the
|
||||||
# excludes .git (see .dockerignore), so the container cannot derive
|
# build would derive from the .git in its context. VERSION comes
|
||||||
# them itself -- it used to try and always got "unknown", giving
|
# from script/version, as in the Makefile, so the image reports the
|
||||||
# every image a "commit: unknown" it could not be traced from.
|
# same string, -dirty included, that a local build of the same tree
|
||||||
# VERSION comes from script/version, the source of truth shared with
|
# would.
|
||||||
# the Makefile, so a Docker build reports the same string (tag,
|
|
||||||
# dev-<sha>, or a -dirty variant) that a local build of the same
|
|
||||||
# tree would.
|
|
||||||
version="$("$SCRIPT_DIR/version")"
|
version="$("$SCRIPT_DIR/version")"
|
||||||
commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
commit="$(git rev-parse HEAD 2>/dev/null || echo unknown)"
|
||||||
commit_date="$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)"
|
commit_date="$(git show -s --format=%cs HEAD 2>/dev/null || echo unknown)"
|
||||||
|
|||||||
+16
-60
@@ -1,73 +1,29 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
# script/version: output the version string to bake into the binary.
|
# script/version: output the version string to bake into the binary.
|
||||||
# Our own extension to scripts-to-rule-them-all, and the single source
|
# Our own extension to scripts-to-rule-them-all. The Makefile's LDFLAGS
|
||||||
# of truth for the version: the Makefile's LDFLAGS call this rather
|
# call this rather than carrying a hardcoded constant, which is what used
|
||||||
# than carrying a hardcoded constant, which is what used to make every
|
# to make every local build claim to be 1.0.0-rc.1 regardless of git
|
||||||
# local build claim to be 1.0.0-rc.1 regardless of git state.
|
# state. script/docker and script/cibuild pass its output to the image
|
||||||
|
# build; given no version, the image build runs `git describe --tags
|
||||||
|
# --always` itself, without `--dirty`.
|
||||||
#
|
#
|
||||||
# The rules, in order:
|
# The version is `git describe --tags --always --dirty`: the tag on a
|
||||||
|
# tagged commit, tag-N-gHASH on a commit after one, the short commit
|
||||||
|
# when no tag is reachable, each with a "-dirty" suffix when tracked
|
||||||
|
# files have uncommitted changes. Untracked files are ignored: a stray
|
||||||
|
# scratch file does not change what was compiled. Outside a git checkout
|
||||||
|
# (release tarball, `go install`), or in one with no commits, it is
|
||||||
|
# "dev".
|
||||||
#
|
#
|
||||||
# HEAD is exactly on an annotated or lightweight tag
|
# Nothing here ever invents a version number.
|
||||||
# -> 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
|
set -eu
|
||||||
|
|
||||||
ROOT="$(cd "$(dirname "$0")/.." && pwd -P)"
|
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() {
|
main() {
|
||||||
cd "$ROOT"
|
cd "$ROOT"
|
||||||
|
version="$(git describe --tags --always --dirty 2>/dev/null || true)"
|
||||||
if ! git rev-parse --git-dir >/dev/null 2>&1; then
|
echo "${version:-dev}"
|
||||||
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 "$@"
|
main "$@"
|
||||||
|
|||||||
Reference in New Issue
Block a user