Adopt ReleaseKit profile and local wrapper
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
# Implementation Plan: ReleaseKit Adoption
|
# Implementation Plan: ReleaseKit Adoption
|
||||||
|
|
||||||
**Last updated:** 2026-10-07
|
**Last updated:** 2026-10-07
|
||||||
**Status:** Stage 1 complete; Stages 2–5 pending.
|
**Status:** Stages 1–2 complete; Stages 3–5 pending.
|
||||||
**Audience:** Luna coding agent.
|
**Audience:** Luna coding agent.
|
||||||
|
|
||||||
## Scope and Execution
|
## Scope and Execution
|
||||||
@@ -98,7 +98,7 @@ Checks passed: `go test ./internal/cli ./cmd/audita`,
|
|||||||
`go build -o ./bin/audita ./cmd/audita`, `./bin/audita version`, and
|
`go build -o ./bin/audita ./cmd/audita`, `./bin/audita version`, and
|
||||||
`git diff --check`.
|
`git diff --check`.
|
||||||
|
|
||||||
## Stage 2 — Introduce the Product Profile and Thin Local Wrapper
|
## Stage 2 — Introduce the Product Profile and Thin Local Wrapper [x]
|
||||||
|
|
||||||
**Primary files:** new `releasekit.json`, `scripts/release.sh`, and
|
**Primary files:** new `releasekit.json`, `scripts/release.sh`, and
|
||||||
`scripts/release_test.go`.
|
`scripts/release_test.go`.
|
||||||
@@ -154,6 +154,12 @@ This prints the tool's version, not Audita's. Check ignored build output and
|
|||||||
`git diff --check`, then commit. No publication or delivery invocation against
|
`git diff --check`, then commit. No publication or delivery invocation against
|
||||||
an external service is needed.
|
an external service is needed.
|
||||||
|
|
||||||
|
**Completed 2026-10-07:** Added the Audita profile and replaced the local
|
||||||
|
publication workflow with a wrapper that builds and execs ReleaseKit. Replaced
|
||||||
|
the publication test matrix with focused wrapper and profile checks. Checks
|
||||||
|
passed: `go test ./scripts`, `bash scripts/release.sh version`, ignored-output
|
||||||
|
verification for `bin/releasekit`, and `git diff --check`.
|
||||||
|
|
||||||
## Stage 3 — Migrate Woodpecker Delivery and Retire the Builder
|
## Stage 3 — Migrate Woodpecker Delivery and Retire the Builder
|
||||||
|
|
||||||
**Primary files:** `.woodpecker/release.yml` and deletion of
|
**Primary files:** `.woodpecker/release.yml` and deletion of
|
||||||
|
|||||||
15
releasekit.json
Normal file
15
releasekit.json
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
{
|
||||||
|
"product": "Audita",
|
||||||
|
"remote": "origin",
|
||||||
|
"build": {
|
||||||
|
"main_package": "./cmd/audita",
|
||||||
|
"binary_name": "audita",
|
||||||
|
"targets": ["linux/amd64", "linux/arm64"]
|
||||||
|
},
|
||||||
|
"delivery": {
|
||||||
|
"base_url": "https://gitea.maximumdirect.net",
|
||||||
|
"owner": "eric",
|
||||||
|
"repository": "audita",
|
||||||
|
"token_env": "RELEASEKIT_GITEA_TOKEN"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,48 +1,9 @@
|
|||||||
#!/usr/bin/env bash
|
#!/usr/bin/env bash
|
||||||
set -euo pipefail
|
set -euo pipefail
|
||||||
|
|
||||||
fail() { echo "Release stopped: $*" >&2; exit 1; }
|
script_dir=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)
|
||||||
if [[ $# != 2 || ( $1 != --check && $1 != --publish ) || ! $2 =~ ^v(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)\.(0|[1-9][0-9]*)$ ]]; then
|
cd "$script_dir/.."
|
||||||
echo 'Usage: scripts/release.sh --check|--publish vMAJOR.MINOR.PATCH' >&2
|
mkdir -p bin
|
||||||
exit 2
|
GOWORK=off GOTOOLCHAIN=local go build -mod=readonly \
|
||||||
fi
|
-o ./bin/releasekit gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit
|
||||||
mode=$1
|
exec ./bin/releasekit "$@"
|
||||||
version=$2
|
|
||||||
cd "$(dirname "$0")/.."
|
|
||||||
[[ $(git symbolic-ref --quiet --short HEAD) == main ]] || fail 'switch to main'
|
|
||||||
[[ -z $(git status --porcelain) ]] || fail 'commit or remove outstanding changes first'
|
|
||||||
candidate=$(git rev-parse HEAD)
|
|
||||||
note="docs/releases/$version.md"
|
|
||||||
[[ -s $note ]] || fail "missing release note: $note"
|
|
||||||
IFS= read -r title < "$note" || true
|
|
||||||
[[ $title == "# Audita $version" ]] || fail "note must begin with # Audita $version"
|
|
||||||
git show-ref --verify --quiet "refs/tags/$version" && fail 'version already exists locally'
|
|
||||||
|
|
||||||
# Check mode is local and never contacts a remote or creates a tag.
|
|
||||||
if [[ $mode == --publish ]]; then
|
|
||||||
existing=$(git ls-remote --tags origin "refs/tags/$version")
|
|
||||||
[[ -z $existing ]] || fail 'version already exists on origin'
|
|
||||||
fi
|
|
||||||
build_dir=$(mktemp -d)
|
|
||||||
trap 'rm -rf "$build_dir"' EXIT
|
|
||||||
GOWORK=off go test -mod=readonly ./...
|
|
||||||
bash scripts/build-release.sh "$version" "$build_dir"
|
|
||||||
[[ $(git rev-parse HEAD) == "$candidate" ]] || fail 'HEAD changed during validation'
|
|
||||||
[[ -z $(git status --porcelain) ]] || fail 'worktree changed during validation'
|
|
||||||
[[ $(git symbolic-ref --quiet --short HEAD) == main ]] || fail 'branch changed during validation'
|
|
||||||
if [[ $mode == --check ]]; then
|
|
||||||
echo "Validated $version at $candidate; nothing published."
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
# Fetch the authoritative branch before checking that this is a forward update.
|
|
||||||
git fetch --no-tags origin refs/heads/main
|
|
||||||
git merge-base --is-ancestor FETCH_HEAD "$candidate" || fail 'candidate does not include origin/main'
|
|
||||||
existing=$(git ls-remote --tags origin "refs/tags/$version")
|
|
||||||
[[ -z $existing ]] || fail 'version already exists on origin'
|
|
||||||
git tag "$version" "$candidate"
|
|
||||||
[[ $(git rev-parse "refs/tags/$version") == "$candidate" ]] || fail 'tag target changed'
|
|
||||||
# Atomic publication prevents a rejected tag/branch update from publishing only half.
|
|
||||||
if ! git push --atomic origin "$candidate:refs/heads/main" "refs/tags/$version:refs/tags/$version"; then
|
|
||||||
fail "push failed or its outcome is uncertain; inspect origin/main and refs/tags/$version before retrying (local tag retained)"
|
|
||||||
fi
|
|
||||||
echo "Released $version at $candidate. Binary publication runs asynchronously; CI is not awaited."
|
|
||||||
|
|||||||
@@ -1,142 +1,274 @@
|
|||||||
package scripts
|
package scripts
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
"os/exec"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
|
"reflect"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Real local Git repositories exercise publication; Go is stubbed at the slow
|
const goStub = `#!/usr/bin/env bash
|
||||||
// build/test boundary so these tests remain offline and cannot recurse.
|
set -euo pipefail
|
||||||
func TestReleaseWorkflow(t *testing.T) {
|
printf '%s\0' "$PWD" "GOWORK=$GOWORK" "GOTOOLCHAIN=$GOTOOLCHAIN" "$@" > "$RELEASEKIT_BUILD_LOG"
|
||||||
for _, scenario := range []string{"check", "publish", "dirty", "tests-fail", "build-fail", "existing-remote-tag", "remote-ahead", "rejected-push", "source-changed", "missing-note", "invalid-version", "existing-local-tag", "wrong-branch", "head-changed"} {
|
if [[ ${RELEASEKIT_BOOTSTRAP_STATUS:-0} != 0 ]]; then
|
||||||
t.Run(scenario, func(t *testing.T) {
|
exit "$RELEASEKIT_BOOTSTRAP_STATUS"
|
||||||
root := t.TempDir()
|
fi
|
||||||
repo, remote := filepath.Join(root, "repo"), filepath.Join(root, "remote.git")
|
output=
|
||||||
if err := os.MkdirAll(repo, 0755); err != nil {
|
while (($#)); do
|
||||||
t.Fatal(err)
|
if [[ $1 == -o ]]; then
|
||||||
|
shift
|
||||||
|
output=${1:?missing output path}
|
||||||
|
fi
|
||||||
|
shift
|
||||||
|
done
|
||||||
|
[[ -n $output ]]
|
||||||
|
cat > "$output" <<'RELEASEKIT_TOOL'
|
||||||
|
#!/usr/bin/env bash
|
||||||
|
set -euo pipefail
|
||||||
|
printf '%s\0' "$PWD" "$@" > "$RELEASEKIT_ARGS_LOG"
|
||||||
|
exit "${RELEASEKIT_TOOL_STATUS:-0}"
|
||||||
|
RELEASEKIT_TOOL
|
||||||
|
chmod +x "$output"
|
||||||
|
`
|
||||||
|
|
||||||
|
func TestReleaseWrapperForwardsNativeArgumentsAndStatus(t *testing.T) {
|
||||||
|
for _, toolStatus := range []string{"0", "3"} {
|
||||||
|
t.Run("status "+toolStatus, func(t *testing.T) {
|
||||||
|
layout := newReleaseTestLayout(t)
|
||||||
|
args := []string{"publish", "v4.2.0", "argument with spaces"}
|
||||||
|
cmd := exec.Command("bash", append([]string{layout.wrapper}, args...)...)
|
||||||
|
cmd.Dir = layout.caller
|
||||||
|
cmd.Env = layout.environment(map[string]string{
|
||||||
|
"RELEASEKIT_ARGS_LOG": layout.argsLog,
|
||||||
|
"RELEASEKIT_BUILD_LOG": layout.buildLog,
|
||||||
|
"RELEASEKIT_TOOL_STATUS": toolStatus,
|
||||||
|
})
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if got := commandStatus(err); got != toolStatusInt(toolStatus) {
|
||||||
|
t.Fatalf("exit=%d, want %s; output=%q, error=%v", got, toolStatus, output, err)
|
||||||
}
|
}
|
||||||
env := append(os.Environ(), "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL="+os.DevNull, "GIT_AUTHOR_NAME=Release Test", "GIT_AUTHOR_EMAIL=test@example.invalid", "GIT_COMMITTER_NAME=Release Test", "GIT_COMMITTER_EMAIL=test@example.invalid", "GIT_TERMINAL_PROMPT=0")
|
if len(output) != 0 {
|
||||||
run := func(dir string, args ...string) string {
|
t.Fatalf("unexpected output: %q", output)
|
||||||
t.Helper()
|
|
||||||
c := exec.Command("git", args...)
|
|
||||||
c.Dir = dir
|
|
||||||
c.Env = env
|
|
||||||
b, e := c.CombinedOutput()
|
|
||||||
if e != nil {
|
|
||||||
t.Fatalf("git %v: %v\n%s", args, e, b)
|
|
||||||
}
|
}
|
||||||
return strings.TrimSpace(string(b))
|
|
||||||
|
buildInvocation := readNULArguments(t, layout.buildLog)
|
||||||
|
if len(buildInvocation) < 4 || buildInvocation[0] != layout.repo {
|
||||||
|
t.Fatalf("build invocation=%q, want repository %q", buildInvocation, layout.repo)
|
||||||
}
|
}
|
||||||
write := func(path, data string, mode os.FileMode) {
|
if buildInvocation[1] != "GOWORK=off" || buildInvocation[2] != "GOTOOLCHAIN=local" {
|
||||||
t.Helper()
|
t.Fatalf("build environment=%q", buildInvocation[1:3])
|
||||||
if e := os.MkdirAll(filepath.Dir(path), 0755); e != nil {
|
|
||||||
t.Fatal(e)
|
|
||||||
}
|
}
|
||||||
if e := os.WriteFile(path, []byte(data), mode); e != nil {
|
buildArgs := buildInvocation[3:]
|
||||||
t.Fatal(e)
|
if buildArgs[0] != "build" || !containsArgument(buildArgs, "-mod=readonly") || !containsArgument(buildArgs, "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit") {
|
||||||
|
t.Fatalf("unexpected build arguments: %q", buildArgs)
|
||||||
}
|
}
|
||||||
|
if !hasAdjacentArguments(buildArgs, "-o", "./bin/releasekit") {
|
||||||
|
t.Fatalf("build output was not selected: %q", buildArgs)
|
||||||
}
|
}
|
||||||
run(root, "init", "--bare", "--initial-branch=main", remote)
|
|
||||||
run(repo, "init", "--initial-branch=main")
|
toolInvocation := readNULArguments(t, layout.argsLog)
|
||||||
for _, name := range []string{"release.sh", "build-release.sh"} {
|
wantToolInvocation := append([]string{layout.repo}, args...)
|
||||||
b, e := os.ReadFile(name)
|
if !reflect.DeepEqual(toolInvocation, wantToolInvocation) {
|
||||||
if e != nil {
|
t.Fatalf("tool invocation=%q, want %q", toolInvocation, wantToolInvocation)
|
||||||
t.Fatal(e)
|
|
||||||
}
|
|
||||||
write(filepath.Join(repo, "scripts", name), string(b), 0755)
|
|
||||||
}
|
|
||||||
write(filepath.Join(repo, "docs/releases/v1.2.3.md"), "# Audita v1.2.3\n\nA test release.\n", 0644)
|
|
||||||
run(repo, "add", ".")
|
|
||||||
run(repo, "commit", "-m", "Initial fixture")
|
|
||||||
run(repo, "remote", "add", "origin", remote)
|
|
||||||
run(repo, "push", "origin", "main")
|
|
||||||
write(filepath.Join(repo, "change.txt"), "candidate\n", 0644)
|
|
||||||
run(repo, "add", ".")
|
|
||||||
run(repo, "commit", "-m", "Candidate fixture")
|
|
||||||
candidate := run(repo, "rev-parse", "HEAD")
|
|
||||||
log := filepath.Join(root, "go.log")
|
|
||||||
stub := "#!/bin/sh\necho \"$* GOOS=$GOOS GOARCH=$GOARCH CGO_ENABLED=$CGO_ENABLED GOWORK=$GOWORK\" >> \"$GO_LOG\"\n"
|
|
||||||
switch scenario {
|
|
||||||
case "existing-local-tag":
|
|
||||||
run(repo, "tag", "v1.2.3")
|
|
||||||
case "wrong-branch":
|
|
||||||
run(repo, "checkout", "-b", "feature")
|
|
||||||
case "head-changed":
|
|
||||||
stub += "if [ \"$1\" = test ]; then echo changed > change.txt; git add change.txt; git commit -m Changed; fi\n"
|
|
||||||
case "dirty":
|
|
||||||
write(filepath.Join(repo, "untracked"), "dirty", 0644)
|
|
||||||
case "tests-fail":
|
|
||||||
stub += "[ \"$1\" != test ] || exit 1\n"
|
|
||||||
case "build-fail":
|
|
||||||
stub += "[ \"$GOARCH\" != arm64 ] || exit 1\n"
|
|
||||||
case "existing-remote-tag":
|
|
||||||
run(remote, "tag", "v1.2.3", "main")
|
|
||||||
case "remote-ahead":
|
|
||||||
other := filepath.Join(root, "other")
|
|
||||||
run(root, "clone", remote, other)
|
|
||||||
write(filepath.Join(other, "other.txt"), "new remote commit", 0644)
|
|
||||||
run(other, "add", ".")
|
|
||||||
run(other, "commit", "-m", "Remote fixture")
|
|
||||||
run(other, "push", "origin", "main")
|
|
||||||
case "rejected-push":
|
|
||||||
write(filepath.Join(remote, "hooks/pre-receive"), "#!/bin/sh\nexit 1\n", 0755)
|
|
||||||
case "source-changed":
|
|
||||||
stub += "[ \"$1\" != test ] || echo changed > change.txt\n"
|
|
||||||
case "missing-note":
|
|
||||||
run(repo, "rm", "docs/releases/v1.2.3.md")
|
|
||||||
run(repo, "commit", "-m", "Remove fixture note")
|
|
||||||
}
|
|
||||||
stub += "exit 0\n"
|
|
||||||
write(filepath.Join(root, "bin/go"), stub, 0755)
|
|
||||||
mode, version := "--publish", "v1.2.3"
|
|
||||||
if scenario == "check" {
|
|
||||||
mode = "--check"
|
|
||||||
run(repo, "remote", "remove", "origin")
|
|
||||||
}
|
|
||||||
if scenario == "invalid-version" {
|
|
||||||
version = "v01.2.3"
|
|
||||||
}
|
|
||||||
c := exec.Command("bash", "scripts/release.sh", mode, version)
|
|
||||||
c.Dir = repo
|
|
||||||
c.Env = append(env, "PATH="+filepath.Join(root, "bin")+":"+os.Getenv("PATH"), "GO_LOG="+log)
|
|
||||||
b, e := c.CombinedOutput()
|
|
||||||
success := scenario == "check" || scenario == "publish"
|
|
||||||
if (e == nil) != success {
|
|
||||||
t.Fatalf("success=%v: %v\n%s", success, e, b)
|
|
||||||
}
|
|
||||||
tags := run(remote, "tag", "--list")
|
|
||||||
if scenario == "publish" {
|
|
||||||
if run(remote, "rev-parse", "refs/tags/v1.2.3") != candidate || run(remote, "rev-parse", "main") != candidate {
|
|
||||||
t.Fatal("wrong source published")
|
|
||||||
}
|
|
||||||
if !strings.Contains(string(b), "CI is not awaited") {
|
|
||||||
t.Fatal("missing asynchronous completion message")
|
|
||||||
}
|
|
||||||
} else if scenario != "existing-remote-tag" && tags != "" {
|
|
||||||
t.Fatalf("unexpected remote tags: %s", tags)
|
|
||||||
}
|
|
||||||
if scenario == "rejected-push" && run(remote, "rev-parse", "main") == candidate {
|
|
||||||
t.Fatal("rejected push published candidate")
|
|
||||||
}
|
|
||||||
if scenario == "check" && run(repo, "tag", "--list") != "" {
|
|
||||||
t.Fatal("check created a tag")
|
|
||||||
}
|
|
||||||
if success {
|
|
||||||
calls, e := os.ReadFile(log)
|
|
||||||
if e != nil {
|
|
||||||
t.Fatal(e)
|
|
||||||
}
|
|
||||||
lines := strings.Split(strings.TrimSpace(string(calls)), "\n")
|
|
||||||
if len(lines) != 3 {
|
|
||||||
t.Fatalf("expected one test and two builds: %s", calls)
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(lines[0], "test -mod=readonly ./...") || !strings.Contains(lines[1], "GOOS=linux GOARCH=amd64 CGO_ENABLED=0 GOWORK=off") || !strings.Contains(lines[2], "GOOS=linux GOARCH=arm64 CGO_ENABLED=0 GOWORK=off") {
|
|
||||||
t.Fatalf("unexpected validation: %s", calls)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestReleaseWrapperStopsWhenBootstrapFails(t *testing.T) {
|
||||||
|
layout := newReleaseTestLayout(t)
|
||||||
|
cmd := exec.Command("bash", layout.wrapper, "publish", "v4.2.0")
|
||||||
|
cmd.Dir = layout.caller
|
||||||
|
cmd.Env = layout.environment(map[string]string{
|
||||||
|
"RELEASEKIT_ARGS_LOG": layout.argsLog,
|
||||||
|
"RELEASEKIT_BUILD_LOG": layout.buildLog,
|
||||||
|
"RELEASEKIT_BOOTSTRAP_STATUS": "42",
|
||||||
|
})
|
||||||
|
output, err := cmd.CombinedOutput()
|
||||||
|
if got := commandStatus(err); got != 42 {
|
||||||
|
t.Fatalf("exit=%d, want 42; output=%q, error=%v", got, output, err)
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(layout.argsLog); !os.IsNotExist(err) {
|
||||||
|
t.Fatalf("ReleaseKit executed after failed bootstrap; stat error=%v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestReleaseKitProfileDeclaresProductAndDeliverySettings(t *testing.T) {
|
||||||
|
data, err := os.ReadFile("../releasekit.json")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
var raw map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(data, &raw); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, exists := raw["checks"]; exists {
|
||||||
|
t.Fatal("profile must not define additional checks")
|
||||||
|
}
|
||||||
|
var deliveryFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(raw["delivery"], &deliveryFields); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if _, exists := deliveryFields["token"]; exists {
|
||||||
|
t.Fatal("profile must not contain a credential value")
|
||||||
|
}
|
||||||
|
var profile struct {
|
||||||
|
Product string `json:"product"`
|
||||||
|
Remote string `json:"remote"`
|
||||||
|
Build struct {
|
||||||
|
MainPackage string `json:"main_package"`
|
||||||
|
BinaryName string `json:"binary_name"`
|
||||||
|
Targets []string `json:"targets"`
|
||||||
|
} `json:"build"`
|
||||||
|
Delivery struct {
|
||||||
|
BaseURL string `json:"base_url"`
|
||||||
|
Owner string `json:"owner"`
|
||||||
|
Repository string `json:"repository"`
|
||||||
|
TokenEnv string `json:"token_env"`
|
||||||
|
} `json:"delivery"`
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(data, &profile); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
if profile.Product != "Audita" || profile.Remote != "origin" {
|
||||||
|
t.Fatalf("product=%q remote=%q", profile.Product, profile.Remote)
|
||||||
|
}
|
||||||
|
if profile.Build.MainPackage != "./cmd/audita" || profile.Build.BinaryName != "audita" {
|
||||||
|
t.Fatalf("build=%+v", profile.Build)
|
||||||
|
}
|
||||||
|
if !sameMembers(profile.Build.Targets, []string{"linux/amd64", "linux/arm64"}) {
|
||||||
|
t.Fatalf("build targets=%q", profile.Build.Targets)
|
||||||
|
}
|
||||||
|
if profile.Delivery.BaseURL != "https://gitea.maximumdirect.net" || profile.Delivery.Owner != "eric" || profile.Delivery.Repository != "audita" || profile.Delivery.TokenEnv != "RELEASEKIT_GITEA_TOKEN" {
|
||||||
|
t.Fatalf("delivery=%+v", profile.Delivery)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type releaseTestLayout struct {
|
||||||
|
repo string
|
||||||
|
caller string
|
||||||
|
wrapper string
|
||||||
|
argsLog string
|
||||||
|
buildLog 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"),
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
wrapper, err := os.ReadFile("release.sh")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
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)
|
||||||
|
return layout
|
||||||
|
}
|
||||||
|
|
||||||
|
func (layout releaseTestLayout) environment(values map[string]string) []string {
|
||||||
|
filtered := make([]string, 0, len(os.Environ())+len(values)+1)
|
||||||
|
for _, value := range os.Environ() {
|
||||||
|
key, _, _ := strings.Cut(value, "=")
|
||||||
|
if key == "PATH" || hasKey(values, key) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
filtered = append(filtered, value)
|
||||||
|
}
|
||||||
|
filtered = append(filtered, "PATH="+layout.path+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
for key, value := range values {
|
||||||
|
filtered = append(filtered, key+"="+value)
|
||||||
|
}
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
|
func writeReleaseTestFile(t *testing.T, path string, data []byte, mode os.FileMode) {
|
||||||
|
t.Helper()
|
||||||
|
if err := os.WriteFile(path, data, mode); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func readNULArguments(t *testing.T, path string) []string {
|
||||||
|
t.Helper()
|
||||||
|
data, err := os.ReadFile(path)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
parts := strings.Split(string(data), "\x00")
|
||||||
|
if len(parts) > 0 && parts[len(parts)-1] == "" {
|
||||||
|
parts = parts[:len(parts)-1]
|
||||||
|
}
|
||||||
|
return parts
|
||||||
|
}
|
||||||
|
|
||||||
|
func commandStatus(err error) int {
|
||||||
|
if err == nil {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
if exitErr, ok := err.(*exec.ExitError); ok {
|
||||||
|
return exitErr.ExitCode()
|
||||||
|
}
|
||||||
|
return -1
|
||||||
|
}
|
||||||
|
|
||||||
|
func toolStatusInt(value string) int {
|
||||||
|
if value == "3" {
|
||||||
|
return 3
|
||||||
|
}
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
func containsArgument(args []string, want string) bool {
|
||||||
|
for _, arg := range args {
|
||||||
|
if arg == want {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasAdjacentArguments(args []string, first, second string) bool {
|
||||||
|
for i := 0; i+1 < len(args); i++ {
|
||||||
|
if args[i] == first && args[i+1] == second {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func sameMembers(got, want []string) bool {
|
||||||
|
if len(got) != len(want) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
for _, value := range got {
|
||||||
|
if !containsArgument(want, value) {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func hasKey(values map[string]string, key string) bool {
|
||||||
|
_, exists := values[key]
|
||||||
|
return exists
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user