Upgrade ReleaseKit and simplify release tooling
This commit is contained in:
@@ -3,23 +3,4 @@ set -euo pipefail
|
||||
|
||||
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||
cd "$script_dir/.."
|
||||
|
||||
releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit
|
||||
bootstrap_dir=$(mktemp -d "${TMPDIR:-/tmp}/releasekit-bootstrap.XXXXXX")
|
||||
trap 'rm -rf "$bootstrap_dir"' EXIT
|
||||
cp go.mod "$bootstrap_dir/go.mod"
|
||||
if [[ -f go.sum ]]; then
|
||||
cp go.sum "$bootstrap_dir/go.sum"
|
||||
else
|
||||
: > "$bootstrap_dir/go.sum"
|
||||
fi
|
||||
cp "$bootstrap_dir/go.mod" "$bootstrap_dir/releasekit.mod"
|
||||
cp "$bootstrap_dir/go.sum" "$bootstrap_dir/releasekit.sum"
|
||||
|
||||
mkdir -p bin
|
||||
GOWORK=off GOTOOLCHAIN=local go list -mod=mod -modfile="$bootstrap_dir/releasekit.mod" -m "$releasekit_module" >/dev/null
|
||||
GOWORK=off GOTOOLCHAIN=local go build -mod=mod -modfile="$bootstrap_dir/releasekit.mod" \
|
||||
-o ./bin/releasekit "$releasekit_module/cmd/releasekit"
|
||||
rm -rf "$bootstrap_dir"
|
||||
trap - EXIT
|
||||
exec ./bin/releasekit "$@"
|
||||
GOWORK=off GOTOOLCHAIN=local exec go tool releasekit "$@"
|
||||
|
||||
@@ -2,64 +2,30 @@ package scripts
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"go/version"
|
||||
"maps"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/mod/modfile"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
const goStub = `#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
case ${1:?missing Go command} in
|
||||
list)
|
||||
log=$RELEASEKIT_LIST_LOG
|
||||
printf '%s\0' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$log"
|
||||
modfile=
|
||||
for argument in "$@"; do
|
||||
case $argument in -modfile=*) modfile=${argument#-modfile=} ;; esac
|
||||
done
|
||||
[[ -n $modfile && -f $modfile && -f ${modfile%.mod}.sum ]]
|
||||
cp "$modfile" "$RELEASEKIT_MODULE_LOG"
|
||||
cp "${modfile%.mod}.sum" "$RELEASEKIT_SUM_LOG"
|
||||
if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then
|
||||
exit "$RELEASEKIT_BOOTSTRAP_STATUS"
|
||||
fi
|
||||
printf '%s\n' v0.1.0
|
||||
;;
|
||||
build)
|
||||
printf '%s\0' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$RELEASEKIT_BUILD_LOG"
|
||||
if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then
|
||||
exit "$RELEASEKIT_BOOTSTRAP_STATUS"
|
||||
fi
|
||||
output=
|
||||
while (($#)); do
|
||||
case $1 in
|
||||
-o)
|
||||
shift
|
||||
output=${1:?missing output path}
|
||||
;;
|
||||
-o=*) output=${1#-o=} ;;
|
||||
esac
|
||||
shift
|
||||
done
|
||||
[[ -n $output ]]
|
||||
cat > "$output" <<'RELEASEKIT_TOOL'
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
printf '%s\0' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$RELEASEKIT_GO_LOG"
|
||||
if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then
|
||||
exit "$RELEASEKIT_BOOTSTRAP_STATUS"
|
||||
fi
|
||||
[[ $1 == tool && $2 == releasekit ]]
|
||||
shift 2
|
||||
printf '%s\0' "$PWD" "$@" > "$RELEASEKIT_ARGS_LOG"
|
||||
exit "${RELEASEKIT_TOOL_STATUS:-0}"
|
||||
RELEASEKIT_TOOL
|
||||
chmod +x "$output"
|
||||
;;
|
||||
*) exit 64 ;;
|
||||
esac
|
||||
`
|
||||
|
||||
func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) {
|
||||
@@ -71,10 +37,7 @@ func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) {
|
||||
cmd.Dir = layout.caller
|
||||
cmd.Env = layout.environment(map[string]string{
|
||||
"RELEASEKIT_ARGS_LOG": layout.argsLog,
|
||||
"RELEASEKIT_BUILD_LOG": layout.buildLog,
|
||||
"RELEASEKIT_LIST_LOG": layout.listLog,
|
||||
"RELEASEKIT_MODULE_LOG": layout.moduleLog,
|
||||
"RELEASEKIT_SUM_LOG": layout.sumLog,
|
||||
"RELEASEKIT_GO_LOG": layout.goLog,
|
||||
"RELEASEKIT_TOOL_STATUS": strconv.Itoa(toolStatus),
|
||||
})
|
||||
output, err := cmd.CombinedOutput()
|
||||
@@ -84,58 +47,13 @@ func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) {
|
||||
if len(output) != 0 {
|
||||
t.Fatalf("unexpected output: %q", output)
|
||||
}
|
||||
|
||||
listInvocation := readNULArguments(t, layout.listLog)
|
||||
buildInvocation := readNULArguments(t, layout.buildLog)
|
||||
for name, invocation := range map[string][]string{"list": listInvocation, "build": buildInvocation} {
|
||||
if len(invocation) < 4 || invocation[0] != layout.repo {
|
||||
t.Fatalf("%s invocation=%q, want repository %q", name, invocation, layout.repo)
|
||||
}
|
||||
if invocation[1] != "GOWORK=off" || invocation[2] != "GOTOOLCHAIN=local" {
|
||||
t.Fatalf("%s environment=%q", name, invocation[1:3])
|
||||
}
|
||||
wantGo := append([]string{layout.repo, "GOWORK=off", "GOTOOLCHAIN=local", "tool", "releasekit"}, args...)
|
||||
if got := readNULArguments(t, layout.goLog); !reflect.DeepEqual(got, wantGo) {
|
||||
t.Fatalf("Go invocation=%q, want %q", got, wantGo)
|
||||
}
|
||||
listArgs := listInvocation[3:]
|
||||
buildArgs := buildInvocation[3:]
|
||||
modfile := argumentValue(listArgs, "-modfile=")
|
||||
if listArgs[0] != "list" || !slices.Contains(listArgs, "-mod=mod") || !slices.Contains(listArgs, "-m") || !slices.Contains(listArgs, "gitea.maximumdirect.net/eric/ReleaseKit") {
|
||||
t.Fatalf("unexpected module-selection arguments: %q", listArgs)
|
||||
}
|
||||
if modfile == "" || argumentValue(buildArgs, "-modfile=") != modfile || !slices.Contains(buildArgs, "-mod=mod") {
|
||||
t.Fatalf("list/build did not share alternate module metadata: list=%q build=%q", listArgs, buildArgs)
|
||||
}
|
||||
if buildArgs[0] != "build" || !slices.Contains(buildArgs, "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit") {
|
||||
t.Fatalf("unexpected build arguments: %q", buildArgs)
|
||||
}
|
||||
if relative, err := filepath.Rel(layout.repo, modfile); err != nil || relative == "." || (!strings.HasPrefix(relative, ".."+string(filepath.Separator)) && relative != "..") {
|
||||
t.Fatalf("alternate module file %q is not outside repository %q: relative=%q error=%v", modfile, layout.repo, relative, err)
|
||||
}
|
||||
if module := readText(t, layout.moduleLog); !strings.Contains(module, "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0") {
|
||||
t.Fatalf("temporary module file does not retain selected ReleaseKit version: %q", module)
|
||||
}
|
||||
if sum := readText(t, layout.sumLog); !strings.Contains(sum, "fixture checksum") {
|
||||
t.Fatalf("temporary sum file did not copy source checksums: %q", sum)
|
||||
}
|
||||
if sourceModule := readText(t, filepath.Join(layout.repo, "go.mod")); !strings.Contains(sourceModule, "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0") {
|
||||
t.Fatalf("source go.mod changed: %q", sourceModule)
|
||||
}
|
||||
if sourceSum := readText(t, filepath.Join(layout.repo, "go.sum")); sourceSum != "fixture checksum\n" {
|
||||
t.Fatalf("source go.sum changed: %q", sourceSum)
|
||||
}
|
||||
if _, err := os.Stat(modfile); !os.IsNotExist(err) {
|
||||
t.Fatalf("temporary module file remains after wrapper: %v", err)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(filepath.Dir(modfile), "releasekit.sum")); !os.IsNotExist(err) {
|
||||
t.Fatalf("temporary sum file remains after wrapper: %v", err)
|
||||
}
|
||||
if info, err := os.Stat(filepath.Join(layout.repo, "bin", "releasekit")); err != nil || !info.Mode().IsRegular() {
|
||||
t.Fatalf("ReleaseKit executable was not built in bin: %v", err)
|
||||
}
|
||||
|
||||
toolInvocation := readNULArguments(t, layout.argsLog)
|
||||
wantToolInvocation := append([]string{layout.repo}, args...)
|
||||
if !reflect.DeepEqual(toolInvocation, wantToolInvocation) {
|
||||
t.Fatalf("tool invocation=%q, want %q", toolInvocation, wantToolInvocation)
|
||||
wantTool := append([]string{layout.repo}, args...)
|
||||
if got := readNULArguments(t, layout.argsLog); !reflect.DeepEqual(got, wantTool) {
|
||||
t.Fatalf("tool invocation=%q, want %q", got, wantTool)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -166,8 +84,22 @@ func TestDeliveryWorkflowUsesModuleSelectedReleaseKit(t *testing.T) {
|
||||
if err := yaml.Unmarshal(data, &workflow); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(workflow.Matrix.Include) != 1 || workflow.Matrix.Include[0]["GO_IMAGE"] != "golang:1.25.5" {
|
||||
t.Fatalf("workflow Go image = %v, want existing Go 1.25.5 image", workflow.Matrix.Include)
|
||||
goMod, err := modfile.Parse("go.mod", []byte(readText(t, "../go.mod")), nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if goMod.Go == nil {
|
||||
t.Fatal("module does not declare its Go minimum")
|
||||
}
|
||||
if len(workflow.Matrix.Include) != 1 {
|
||||
t.Fatalf("workflow Go matrix = %v", workflow.Matrix.Include)
|
||||
}
|
||||
image := workflow.Matrix.Include[0]["GO_IMAGE"]
|
||||
_, tag, _ := strings.Cut(image, ":")
|
||||
patch, _, _ := strings.Cut(tag, "-")
|
||||
imageVersion := "go" + patch
|
||||
if !version.IsValid(imageVersion) || version.Compare(imageVersion, "go"+goMod.Go.Version) < 0 {
|
||||
t.Fatalf("workflow Go image %q cannot satisfy module minimum %s", image, goMod.Go.Version)
|
||||
}
|
||||
if len(workflow.When) != 1 || workflow.When[0].Event != "tag" || workflow.When[0].Ref != "refs/tags/v*" {
|
||||
t.Fatalf("workflow trigger = %+v", workflow.When)
|
||||
@@ -183,41 +115,30 @@ func TestDeliveryWorkflowUsesModuleSelectedReleaseKit(t *testing.T) {
|
||||
}
|
||||
command := workflow.Steps[0].Commands[0]
|
||||
for _, fragment := range []string{
|
||||
"git fetch origin \"refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}\"",
|
||||
"git checkout --detach \"$${tag_commit}\"",
|
||||
"git status --porcelain --untracked-files=all",
|
||||
"mktemp -d \"$${CI_WORKSPACE%/*}/.releasekit-bootstrap.XXXXXX\"",
|
||||
"trap 'rm -rf \"$${bootstrap_dir}\"' EXIT",
|
||||
"cp go.mod \"$${bootstrap_dir}/go.mod\"",
|
||||
"cp go.sum \"$${bootstrap_dir}/go.sum\"",
|
||||
"export GOWORK=off",
|
||||
"export GOTOOLCHAIN=local",
|
||||
"releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit",
|
||||
"go list -mod=mod -modfile=",
|
||||
"-m \"$${releasekit_module}\"",
|
||||
"go build -mod=mod -modfile=",
|
||||
"-o \"$${bootstrap_dir}/releasekit\" \"$${releasekit_module}/cmd/releasekit\"",
|
||||
"\"$${bootstrap_dir}/releasekit\" ci-deliver",
|
||||
"--repo \"$${CI_WORKSPACE}\"",
|
||||
"--commit \"$${CI_COMMIT_SHA}\"",
|
||||
"git fetch origin", "refs/tags/$${CI_COMMIT_TAG}",
|
||||
"git checkout --detach", "git status --porcelain",
|
||||
"go list", "-modfile=", "-m", "go build", "$${releasekit_module}/cmd/releasekit",
|
||||
"ci-deliver", "--repo \"$${CI_WORKSPACE}\"", "--commit \"$${CI_COMMIT_SHA}\"",
|
||||
"\"$${CI_COMMIT_TAG}\"",
|
||||
} {
|
||||
if !strings.Contains(command, fragment) {
|
||||
t.Fatalf("workflow command is missing %q: %s", fragment, command)
|
||||
}
|
||||
}
|
||||
if strings.Index(command, "git checkout --detach") > strings.Index(command, "go list -mod=mod") || strings.Index(command, "git status --porcelain") > strings.Index(command, "go list -mod=mod") {
|
||||
if strings.Index(command, "git checkout --detach") > strings.Index(command, "go list") || strings.Index(command, "git status --porcelain") > strings.Index(command, "go list") {
|
||||
t.Fatalf("workflow resolves the tool before verifying the tagged source: %s", command)
|
||||
}
|
||||
if strings.Contains(command, "RELEASEKIT_VERSION") || strings.Contains(command, "cmd/releasekit@") {
|
||||
t.Fatalf("workflow retains a separate tool version: %s", command)
|
||||
}
|
||||
goMod, err := os.ReadFile("../go.mod")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
toolSelected := false
|
||||
for _, tool := range goMod.Tool {
|
||||
if tool.Path == "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit" {
|
||||
toolSelected = true
|
||||
}
|
||||
}
|
||||
if !strings.Contains(string(goMod), "gitea.maximumdirect.net/eric/ReleaseKit v0.1.0") {
|
||||
t.Fatalf("go.mod does not retain selected supported ReleaseKit version: %s", goMod)
|
||||
if !toolSelected {
|
||||
t.Fatal("module does not register the ReleaseKit CLI tool")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -227,10 +148,7 @@ func TestReleaseWrapperStopsWhenBootstrapFails(t *testing.T) {
|
||||
cmd.Dir = layout.caller
|
||||
cmd.Env = layout.environment(map[string]string{
|
||||
"RELEASEKIT_ARGS_LOG": layout.argsLog,
|
||||
"RELEASEKIT_BUILD_LOG": layout.buildLog,
|
||||
"RELEASEKIT_LIST_LOG": layout.listLog,
|
||||
"RELEASEKIT_MODULE_LOG": layout.moduleLog,
|
||||
"RELEASEKIT_SUM_LOG": layout.sumLog,
|
||||
"RELEASEKIT_GO_LOG": layout.goLog,
|
||||
"RELEASEKIT_BOOTSTRAP_STATUS": "42",
|
||||
})
|
||||
output, err := cmd.CombinedOutput()
|
||||
@@ -240,17 +158,6 @@ func TestReleaseWrapperStopsWhenBootstrapFails(t *testing.T) {
|
||||
if _, err := os.Stat(layout.argsLog); !os.IsNotExist(err) {
|
||||
t.Fatalf("ReleaseKit executed after failed bootstrap; stat error=%v", err)
|
||||
}
|
||||
if _, err := os.Stat(layout.buildLog); !os.IsNotExist(err) {
|
||||
t.Fatalf("ReleaseKit build ran after module selection failed; stat error=%v", err)
|
||||
}
|
||||
listInvocation := readNULArguments(t, layout.listLog)
|
||||
modfile := argumentValue(listInvocation[3:], "-modfile=")
|
||||
if modfile == "" {
|
||||
t.Fatalf("module selection did not use temporary metadata: %q", listInvocation)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Dir(modfile)); !os.IsNotExist(err) {
|
||||
t.Fatalf("temporary bootstrap directory remains after failed selection: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReleaseKitProfileDeclaresProductAndDeliverySettings(t *testing.T) {
|
||||
@@ -327,38 +234,28 @@ func TestReleaseKitTargetMembership(t *testing.T) {
|
||||
}
|
||||
|
||||
type releaseTestLayout struct {
|
||||
repo string
|
||||
caller string
|
||||
wrapper string
|
||||
argsLog string
|
||||
buildLog string
|
||||
listLog string
|
||||
moduleLog string
|
||||
sumLog string
|
||||
path string
|
||||
repo string
|
||||
caller string
|
||||
wrapper string
|
||||
argsLog string
|
||||
goLog string
|
||||
path string
|
||||
}
|
||||
|
||||
func newReleaseTestLayout(t *testing.T) releaseTestLayout {
|
||||
t.Helper()
|
||||
root := t.TempDir()
|
||||
layout := releaseTestLayout{
|
||||
repo: filepath.Join(root, "repository"),
|
||||
caller: filepath.Join(root, "caller"),
|
||||
argsLog: filepath.Join(root, "tool-arguments"),
|
||||
buildLog: filepath.Join(root, "build-arguments"),
|
||||
listLog: filepath.Join(root, "list-arguments"),
|
||||
moduleLog: filepath.Join(root, "temporary-go.mod"),
|
||||
sumLog: filepath.Join(root, "temporary-go.sum"),
|
||||
path: filepath.Join(root, "bin"),
|
||||
repo: filepath.Join(root, "repository"),
|
||||
caller: filepath.Join(root, "caller"),
|
||||
argsLog: filepath.Join(root, "tool-arguments"),
|
||||
goLog: filepath.Join(root, "go-arguments"),
|
||||
path: filepath.Join(root, "bin"),
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(layout.repo, "scripts"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(layout.caller, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(layout.path, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
for _, directory := range []string{filepath.Join(layout.repo, "scripts"), layout.caller, layout.path} {
|
||||
if err := os.MkdirAll(directory, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
wrapper, err := os.ReadFile("release.sh")
|
||||
if err != nil {
|
||||
@@ -367,8 +264,6 @@ func newReleaseTestLayout(t *testing.T) releaseTestLayout {
|
||||
layout.wrapper = filepath.Join(layout.repo, "scripts", "release.sh")
|
||||
writeReleaseTestFile(t, layout.wrapper, wrapper, 0755)
|
||||
writeReleaseTestFile(t, filepath.Join(layout.path, "go"), []byte(goStub), 0755)
|
||||
writeReleaseTestFile(t, filepath.Join(layout.repo, "go.mod"), []byte("module example.com/audita-fixture\n\ngo 1.25.5\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.1.0\n"), 0644)
|
||||
writeReleaseTestFile(t, filepath.Join(layout.repo, "go.sum"), []byte("fixture checksum\n"), 0644)
|
||||
return layout
|
||||
}
|
||||
|
||||
@@ -381,15 +276,6 @@ func readText(t *testing.T, path string) string {
|
||||
return string(data)
|
||||
}
|
||||
|
||||
func argumentValue(arguments []string, prefix string) string {
|
||||
for _, argument := range arguments {
|
||||
if strings.HasPrefix(argument, prefix) {
|
||||
return strings.TrimPrefix(argument, prefix)
|
||||
}
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func (layout releaseTestLayout) environment(values map[string]string) []string {
|
||||
filtered := make([]string, 0, len(os.Environ())+len(values)+1)
|
||||
for _, value := range os.Environ() {
|
||||
|
||||
Reference in New Issue
Block a user