check / check (push) Waiting to run
A plain docker build, as upaas runs it, passed no VERSION build arg and had no .git, so every such image stamped "unknown". .dockerignore now sends .git without its config, which can carry a credential, and every tracked file (an excluded one would read as deleted and mark the version -dirty). The VERSION build arg loses its "unknown" default, so script/version derives the version inside the build; a given VERSION still takes precedence. The builder stage installs git, trusts the copied checkout whoever owns its files, and fails when its context carries .git and the version still comes out "unknown". The CI fingerprint is now the commit being checked. Model: opus-5-5
346 lines
9.3 KiB
Go
346 lines
9.3 KiB
Go
package versionscript_test
|
|
|
|
import (
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/require"
|
|
)
|
|
|
|
const (
|
|
repoRoot = "../.."
|
|
scriptPath = "../../script/version"
|
|
makefilePath = "../../Makefile"
|
|
dockerfilePath = "../../Dockerfile"
|
|
|
|
// unknown is what a tree with no git metadata and no $VERSION must
|
|
// report: a source tarball has no way to know its version, and the
|
|
// one thing it must not do is name a tag it may not be at.
|
|
unknown = "unknown"
|
|
|
|
// scriptMode keeps the copied script runnable; dirMode and fileMode
|
|
// are the ordinary permissions for the throwaway tree around it.
|
|
scriptMode = 0o755
|
|
dirMode = 0o750
|
|
fileMode = 0o600
|
|
)
|
|
|
|
// checkout is a throwaway working tree carrying a copy of the script
|
|
// under test at the same path it lives at in this repository, since the
|
|
// script resolves the checkout root from its own location.
|
|
type checkout struct {
|
|
dir string
|
|
head string
|
|
}
|
|
|
|
func TestVersion_CleanTagReportsExactlyTheTag(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
require.Equal(t, "v1.2.3", c.version(t))
|
|
}
|
|
|
|
func TestVersion_CommitAfterTagCarriesDistanceAndSHA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
head := c.commit(t, "after the tag")
|
|
|
|
got := c.version(t)
|
|
|
|
require.NotEqual(t, "v1.2.3", got,
|
|
"a commit past the tag must not claim to be the tag")
|
|
require.True(t, strings.HasPrefix(got, "v1.2.3-1-g"),
|
|
"want describe form v1.2.3-1-g<sha>, got %q", got)
|
|
require.True(t, strings.HasPrefix(head, strings.TrimPrefix(
|
|
got, "v1.2.3-1-g")),
|
|
"%q must carry the abbreviated head SHA of %q", got, head)
|
|
}
|
|
|
|
func TestVersion_UntaggedHistoryReportsShortSHA(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
|
|
got := c.version(t)
|
|
|
|
require.NotEmpty(t, got)
|
|
require.NotEqual(t, unknown, got)
|
|
require.True(t, strings.HasPrefix(c.head, got),
|
|
"%q must be an abbreviation of head %q", got, c.head)
|
|
}
|
|
|
|
func TestVersion_UncommittedChangesAreMarkedDirty(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(c.dir, "tracked.txt"), []byte("edited\n"), fileMode,
|
|
))
|
|
|
|
require.Equal(t, "v1.2.3-dirty", c.version(t))
|
|
}
|
|
|
|
// A source tarball, or any build context without .git, still has to
|
|
// build. It reports "unknown" rather than failing or naming a tag.
|
|
func TestVersion_NoGitMetadataReportsUnknown(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
require.NoError(t, os.RemoveAll(filepath.Join(c.dir, ".git")))
|
|
|
|
require.Equal(t, unknown, c.version(t))
|
|
}
|
|
|
|
// An unpacked tarball can sit inside an unrelated working copy. The
|
|
// enclosing repository's version is not this tree's version.
|
|
func TestVersion_EnclosingRepositoryIsNotUsed(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
outer := newCheckout(t)
|
|
outer.git(t, "tag", "v9.9.9")
|
|
|
|
inner := filepath.Join(outer.dir, "unpacked")
|
|
require.NoError(t, os.MkdirAll(filepath.Join(inner, "script"), dirMode))
|
|
copyScript(t, inner)
|
|
|
|
require.Equal(t, unknown, runScript(t, inner, nil))
|
|
}
|
|
|
|
// An explicit VERSION, such as the Dockerfile's build arg, wins over
|
|
// anything derivable.
|
|
func TestVersion_EnvironmentOverrideWins(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
require.Equal(t, "v4.5.6",
|
|
runScript(t, c.dir, []string{"VERSION=v4.5.6"}))
|
|
}
|
|
|
|
// An empty VERSION is treated as unset rather than stamping an empty
|
|
// string: a caller exporting VERSION= must not produce a binary
|
|
// reporting "".
|
|
func TestVersion_EmptyOverrideFallsBackToGit(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
require.Equal(t, "v1.2.3", runScript(t, c.dir, []string{"VERSION="}))
|
|
}
|
|
|
|
// Two builds of the same commit must produce a byte-identical binary,
|
|
// which they cannot if the stamped value moves between invocations.
|
|
func TestVersion_IsStableAcrossInvocations(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
c := newCheckout(t)
|
|
c.git(t, "tag", "v1.2.3")
|
|
|
|
first := c.version(t)
|
|
second := c.version(t)
|
|
|
|
require.Equal(t, first, second)
|
|
}
|
|
|
|
// The build target is the only place composing linker flags. If a
|
|
// future edit drops either half, the binary silently reports "dev"
|
|
// again (the defect this package exists for) or fails to link on
|
|
// Alpine.
|
|
func TestMakefile_BuildComposesVersionAndExtraFlags(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
makefile := read(t, makefilePath)
|
|
|
|
require.Contains(t, makefile, "-X main.version=$(VERSION)")
|
|
require.Contains(t, makefile, "$(GO_LDFLAGS)")
|
|
require.Contains(t, makefile, "VERSION ?= $(shell script/version)")
|
|
}
|
|
|
|
// A caller can define VERSION as the empty string -- `make build
|
|
// VERSION=`, or the Dockerfile's `make build VERSION="$VERSION"` when no
|
|
// VERSION build arg was given. script/version's own guard does not cover
|
|
// that: the value never passes through the script. Stamping "" would
|
|
// leave the binary reporting no version and the footer on "dev", which
|
|
// is the defect this package exists for.
|
|
func TestMakefile_EmptyOverrideResolvesLikeAnUnsetOne(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
// A plain assignment would be ignored here: a command-line
|
|
// definition outranks it, and that is the case being corrected.
|
|
require.Contains(t, read(t, makefilePath), "override VERSION :=")
|
|
|
|
requireMake(t)
|
|
|
|
derived := makeVersion(t)
|
|
require.NotEmpty(t, derived)
|
|
|
|
require.Equal(t, derived, makeVersion(t, "VERSION="),
|
|
"an empty VERSION must resolve the way an unset one does")
|
|
require.Equal(t, "v9.9.9", makeVersion(t, "VERSION=v9.9.9"),
|
|
"the empty guard must not clobber a real override")
|
|
}
|
|
|
|
// makeVersion runs this repository's `version` target, which prints the
|
|
// value `make build` would stamp, with the given command-line
|
|
// definitions.
|
|
func makeVersion(t *testing.T, defs ...string) string {
|
|
t.Helper()
|
|
|
|
//nolint:gosec // fixed argv, arguments are test constants
|
|
cmd := exec.CommandContext(t.Context(), "make",
|
|
append([]string{"--no-print-directory", "version"}, defs...)...)
|
|
cmd.Dir = repoRoot
|
|
|
|
// Only the command-line definitions may decide the outcome: an
|
|
// inherited VERSION would change what an unset one resolves to, and
|
|
// an inherited MAKEFLAGS carries the parent's jobserver.
|
|
cmd.Env = append(os.Environ(), "VERSION=", "MAKEFLAGS=", "MAKELEVEL=")
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, string(out))
|
|
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func requireMake(t *testing.T) {
|
|
t.Helper()
|
|
|
|
_, err := exec.LookPath("make")
|
|
if err != nil {
|
|
t.Skipf("make is not installed: %v", err)
|
|
}
|
|
}
|
|
|
|
// Every compile in the image goes through the build target, so the
|
|
// static relink cannot replace the flags that carry the stamp.
|
|
func TestDockerfile_BuildsThroughTheMakeTarget(t *testing.T) {
|
|
t.Parallel()
|
|
|
|
dockerfile := read(t, dockerfilePath)
|
|
|
|
require.NotContains(t, dockerfile, "go build",
|
|
"a raw go build bypasses the Makefile's -X flag")
|
|
require.Contains(t, dockerfile, "ARG VERSION")
|
|
require.Contains(t, dockerfile,
|
|
`make build VERSION="$VERSION" GO_LDFLAGS='-extldflags "-static"'`)
|
|
}
|
|
|
|
func read(t *testing.T, path string) string {
|
|
t.Helper()
|
|
|
|
//nolint:gosec // repo-local build file under test, fixed path
|
|
b, err := os.ReadFile(path)
|
|
require.NoError(t, err)
|
|
|
|
return string(b)
|
|
}
|
|
|
|
// version runs the script in this checkout with no overrides.
|
|
func (c checkout) version(t *testing.T) string {
|
|
t.Helper()
|
|
|
|
return runScript(t, c.dir, nil)
|
|
}
|
|
|
|
func runScript(t *testing.T, dir string, env []string) string {
|
|
t.Helper()
|
|
|
|
//nolint:gosec // fixed argv, repo-local script under test
|
|
cmd := exec.CommandContext(t.Context(), "sh",
|
|
filepath.Join(dir, "script", "version"))
|
|
cmd.Dir = dir
|
|
|
|
cmd.Env = append(os.Environ(), env...)
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, string(out))
|
|
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func (c checkout) git(t *testing.T, args ...string) string {
|
|
t.Helper()
|
|
|
|
//nolint:gosec // fixed argv, arguments are test constants
|
|
cmd := exec.CommandContext(t.Context(), "git", args...)
|
|
cmd.Dir = c.dir
|
|
|
|
out, err := cmd.CombinedOutput()
|
|
require.NoError(t, err, string(out))
|
|
|
|
return strings.TrimSpace(string(out))
|
|
}
|
|
|
|
func (c checkout) commit(t *testing.T, message string) string {
|
|
t.Helper()
|
|
|
|
c.git(t,
|
|
"-c", "user.email=ci@example.invalid",
|
|
"-c", "user.name=ci",
|
|
"-c", "commit.gpgsign=false",
|
|
"commit", "-q", "--allow-empty", "-m", message,
|
|
)
|
|
|
|
return c.git(t, "rev-parse", "HEAD")
|
|
}
|
|
|
|
// newCheckout builds a one-commit repository with a tracked file, so a
|
|
// later edit to that file makes the tree dirty, and with a copy of the
|
|
// script at the path it occupies in this repository.
|
|
func newCheckout(t *testing.T) checkout {
|
|
t.Helper()
|
|
|
|
requireGit(t)
|
|
|
|
dir := t.TempDir()
|
|
c := checkout{dir: dir}
|
|
|
|
require.NoError(t, os.MkdirAll(filepath.Join(dir, "script"), dirMode))
|
|
copyScript(t, dir)
|
|
|
|
require.NoError(t, os.WriteFile(
|
|
filepath.Join(dir, "tracked.txt"), []byte("original\n"), fileMode,
|
|
))
|
|
|
|
c.git(t, "init", "-q", "-b", "main")
|
|
c.git(t, "add", "tracked.txt")
|
|
c.head = c.commit(t, "initial")
|
|
|
|
return c
|
|
}
|
|
|
|
func copyScript(t *testing.T, dir string) {
|
|
t.Helper()
|
|
|
|
body, err := os.ReadFile(scriptPath)
|
|
require.NoError(t, err)
|
|
|
|
//nolint:gosec // the copy has to stay executable to be run
|
|
err = os.WriteFile(
|
|
filepath.Join(dir, "script", "version"), body, scriptMode,
|
|
)
|
|
require.NoError(t, err)
|
|
}
|
|
|
|
func requireGit(t *testing.T) {
|
|
t.Helper()
|
|
|
|
for _, tool := range []string{"sh", "git"} {
|
|
_, err := exec.LookPath(tool)
|
|
if err != nil {
|
|
t.Skipf("%s is not installed: %v", tool, err)
|
|
}
|
|
}
|
|
}
|