check / check (push) Successful in 12m47s
Linting and testing become the lint and test phases of the Dockerfile, and the build stage depends on both. Dockerfile.lint, CHECK_EPOCH and the tests that checked them are removed. Every docker build in script/ passes --no-cache, and script/cibuild runs script/bootstrap first. A host without Go gets the go.mod version from script/install-go in .tool/go, which bootstrap, the Makefile, fmt, fmt-check, precommit and release add to PATH; fmt-check skips .tool. The image takes its version from the VERSION build arg or git describe, dev without .git. This repo's own entries follow the canonical content in .gitignore and .editorconfig. The golangci-lint v2.14.0 findings are fixed. The rules in CLAUDE.md move into AGENTS.md. IsDevVersion counts "unknown". Model: opus-5-5
150 lines
5.0 KiB
Go
150 lines
5.0 KiB
Go
package main_test
|
|
|
|
import (
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/assert"
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
// This file guards the version stamping of the product image (issue
|
|
// #75). The failure it protects against is silent: the image still
|
|
// builds and runs, but `vaultik version` inside it reports "commit:
|
|
// unknown" or a version of "dev", so an operator cannot tell which
|
|
// source produced a given backup. The build takes the version as a
|
|
// build arg, which script/docker computes on the host, and otherwise
|
|
// derives it from the .git in its context; the commit and its date
|
|
// always come from that .git.
|
|
//
|
|
// These are parses of the committed files, because shelling out to
|
|
// docker would nest a build inside `make test`. That `vaultik version`
|
|
// in the built image really prints the host's version is verified by
|
|
// hand.
|
|
|
|
// The files under guard, relative to the repository root.
|
|
const (
|
|
productDockerfile = "Dockerfile"
|
|
dockerScript = "script/docker"
|
|
)
|
|
|
|
// TestProductDockerfileTakesVersionAsBuildArg fails unless the build
|
|
// declares ARG VERSION, with no default, and stamps it into the binary
|
|
// whenever it is given, ahead of the value derived in the container.
|
|
func TestProductDockerfileTakesVersionAsBuildArg(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
found := instructions(t, productDockerfile)
|
|
|
|
require.Contains(t, found, "ARG VERSION",
|
|
"%s must declare `ARG VERSION`, with no default, so the host can"+
|
|
" pass it in", productDockerfile)
|
|
|
|
assertLdflagReferences(t, found, "VERSION")
|
|
}
|
|
|
|
// TestProductDockerfileDerivesVersionFromGit fails unless a build given
|
|
// no VERSION, such as a plain `docker build .` of a clone, takes it from
|
|
// `git describe` of the .git in its context, stamps "dev" when the
|
|
// context has no .git, and fails rather than stamp "dev" when that .git
|
|
// yields no version. The commit and its date come from the same .git,
|
|
// and the build fails rather than stamp them "unknown" when it is
|
|
// present.
|
|
func TestProductDockerfileDerivesVersionFromGit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
found := instructions(t, productDockerfile)
|
|
|
|
buildAt := indexContaining(found, "go build")
|
|
require.GreaterOrEqual(t, buildAt, 0, "%s must build", productDockerfile)
|
|
|
|
assert.Contains(t, found[buildAt], "git describe --tags --always || echo dev",
|
|
"%s must derive the version from git when no VERSION is given,"+
|
|
" and stamp dev when the context has no .git", productDockerfile)
|
|
assert.Contains(t, found[buildAt], "[ -e .git ]",
|
|
"%s must fail when the context carries .git but yields no version",
|
|
productDockerfile)
|
|
assert.Contains(t, found[buildAt], "git rev-parse HEAD",
|
|
"%s must stamp the commit from git", productDockerfile)
|
|
assert.Contains(t, found[buildAt], "git show -s --format=%cs HEAD",
|
|
"%s must stamp the commit date from git", productDockerfile)
|
|
assert.Contains(t, found[buildAt],
|
|
`[ "$commit" = unknown ] || [ "$commit_date" = unknown ]`,
|
|
"%s must fail when the context carries .git but yields no commit"+
|
|
" or date", productDockerfile)
|
|
}
|
|
|
|
// TestDockerScriptComputesVersionOnTheHost fails unless script/docker
|
|
// passes the version it derives where .git exists as a build arg.
|
|
func TestDockerScriptComputesVersionOnTheHost(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
script := readRepoFile(t, dockerScript)
|
|
|
|
assert.Contains(t, script, "--build-arg VERSION=",
|
|
"%s must pass --build-arg VERSION to the build", dockerScript)
|
|
}
|
|
|
|
// assertLdflagReferences fails unless the build instruction uses the
|
|
// named ARG whenever it is given (a ${arg:- reference), so a value
|
|
// passed in is not overridden by one derived inside the container.
|
|
func assertLdflagReferences(t *testing.T, found []string, arg string) {
|
|
t.Helper()
|
|
|
|
for _, instruction := range found {
|
|
if strings.HasPrefix(instruction, "RUN ") &&
|
|
strings.Contains(instruction, "go build") &&
|
|
strings.Contains(instruction, "${"+arg+":-") {
|
|
return
|
|
}
|
|
}
|
|
|
|
assert.Fail(t, "version arg is declared but never stamped",
|
|
"the go build in %s must use ${%s:-...}, or the arg is passed and"+
|
|
" discarded", productDockerfile, arg)
|
|
}
|
|
|
|
// instructions returns the Dockerfile's instructions, one per element,
|
|
// with comments and blank lines dropped and continuation lines joined,
|
|
// so a multi-line RUN is one string.
|
|
func instructions(t *testing.T, name string) []string {
|
|
t.Helper()
|
|
|
|
var (
|
|
out []string
|
|
continued string
|
|
isContinued bool
|
|
)
|
|
|
|
for line := range strings.SplitSeq(readRepoFile(t, name), "\n") {
|
|
trimmed := strings.TrimSpace(line)
|
|
if !isContinued && (trimmed == "" || strings.HasPrefix(trimmed, "#")) {
|
|
continue
|
|
}
|
|
|
|
isContinued = strings.HasSuffix(trimmed, `\`)
|
|
continued += strings.TrimSuffix(trimmed, `\`)
|
|
|
|
if isContinued {
|
|
continue
|
|
}
|
|
|
|
out = append(out, strings.Join(strings.Fields(continued), " "))
|
|
continued = ""
|
|
}
|
|
|
|
return out
|
|
}
|
|
|
|
// indexContaining returns the position of the first instruction
|
|
// containing want; -1 if there is none.
|
|
func indexContaining(found []string, want string) int {
|
|
for i, instruction := range found {
|
|
if strings.Contains(instruction, want) {
|
|
return i
|
|
}
|
|
}
|
|
|
|
return -1
|
|
}
|