Preserve native exit codes in the release wrapper
This commit is contained in:
100
cmd/releasekit/release_wrapper_test.go
Normal file
100
cmd/releasekit/release_wrapper_test.go
Normal file
@@ -0,0 +1,100 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestReleaseWrapperPreservesNativeExitStatuses(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("POSIX release entry point")
|
||||
}
|
||||
_, source, _, _ := runtime.Caller(0)
|
||||
root := filepath.Clean(filepath.Join(filepath.Dir(source), "../.."))
|
||||
realGit, err := exec.LookPath("git")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
wrapper, err := os.ReadFile(filepath.Join(root, "scripts/release"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, mode := range []struct {
|
||||
name string
|
||||
status int
|
||||
version string
|
||||
}{{"success", 0, "v1.2.3"}, {"invalid", 2, "invalid"}, {"rejected", 1, "v1.2.3"}, {"unknown", 3, "v1.2.3"}} {
|
||||
t.Run(mode.name, func(t *testing.T) {
|
||||
repository := filepath.Join(t.TempDir(), "source")
|
||||
clone := exec.Command(realGit, "clone", "--local", "--no-hardlinks", root, repository)
|
||||
if output, err := clone.CombinedOutput(); err != nil {
|
||||
t.Fatalf("clone: %v %s", err, output)
|
||||
}
|
||||
cliGitRun(t, repository, "config", "user.name", "ReleaseKit Test")
|
||||
cliGitRun(t, repository, "config", "user.email", "fixture@example.test")
|
||||
cliGitRun(t, repository, "config", "commit.gpgsign", "false")
|
||||
if err := os.WriteFile(filepath.Join(repository, "scripts/release"), wrapper, 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.MkdirAll(filepath.Join(repository, "docs/releases"), 0700); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := os.WriteFile(filepath.Join(repository, "docs/releases/v1.2.3.md"), []byte("# ReleaseKit v1.2.3\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
profile := map[string]any{"product": "ReleaseKit", "checks": []map[string]any{{"args": []string{"sh", "-c", "true"}}}}
|
||||
content, _ := json.Marshal(profile)
|
||||
if err := os.WriteFile(filepath.Join(repository, "releasekit.json"), content, 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
cliGitRun(t, repository, "add", "--all")
|
||||
cliGitRun(t, repository, "commit", "-m", "prepare wrapper fixture")
|
||||
remote := filepath.Join(t.TempDir(), "origin.git")
|
||||
cliGitRun(t, repository, "init", "--bare", remote)
|
||||
cliGitRun(t, repository, "remote", "set-url", "origin", remote)
|
||||
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main")
|
||||
if mode.name == "rejected" {
|
||||
if err := os.WriteFile(filepath.Join(remote, "hooks/pre-receive"), []byte("#!/bin/sh\nexit 1\n"), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if mode.name == "unknown" {
|
||||
bin := t.TempDir()
|
||||
marker := filepath.Join(bin, "push-started")
|
||||
shim := `#!/bin/sh
|
||||
for arg do
|
||||
if [ "$arg" = push ]; then : > "$RELEASEKIT_TEST_PUSH_MARKER"; exit 1; fi
|
||||
done
|
||||
if [ -f "$RELEASEKIT_TEST_PUSH_MARKER" ]; then
|
||||
for arg do if [ "$arg" = ls-remote ]; then exit 128; fi; done
|
||||
fi
|
||||
exec "$RELEASEKIT_TEST_REAL_GIT" "$@"
|
||||
`
|
||||
if err := os.WriteFile(filepath.Join(bin, "git"), []byte(shim), 0755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||
t.Setenv("RELEASEKIT_TEST_REAL_GIT", realGit)
|
||||
t.Setenv("RELEASEKIT_TEST_PUSH_MARKER", marker)
|
||||
}
|
||||
command := exec.Command("./scripts/release", mode.version)
|
||||
command.Dir = repository
|
||||
output, err := command.CombinedOutput()
|
||||
status := 0
|
||||
if err != nil {
|
||||
exit, ok := err.(*exec.ExitError)
|
||||
if !ok {
|
||||
t.Fatal(err)
|
||||
}
|
||||
status = exit.ExitCode()
|
||||
}
|
||||
if status != mode.status {
|
||||
t.Fatalf("status=%d want=%d output=%s", status, mode.status, output)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
# Command line
|
||||
|
||||
ReleaseKit currently provides five commands.
|
||||
ReleaseKit currently provides five commands. The exit statuses below belong
|
||||
to the native executable. `go run` examples are convenient for source execution,
|
||||
but the Go driver maps a nonzero child exit status to 1. Use the built executable
|
||||
or `./scripts/release` for publication when distinct statuses are needed.
|
||||
|
||||
Print the executable version:
|
||||
|
||||
|
||||
@@ -64,7 +64,9 @@ on success. Exit status 0 means publication is confirmed; status 1 means a
|
||||
known failure or conflict; status 2 means invalid input or profile; status 3
|
||||
means the push outcome remains unknown and requires resolution.
|
||||
|
||||
The CLI runs from the source tree being published, so this command also works
|
||||
The wrapper builds the native CLI into ignored `.releasekit/bin`, then invokes
|
||||
it directly to preserve its exit status. The CLI comes from the source tree
|
||||
being published, so this command also works
|
||||
for ReleaseKit's first release before any released ReleaseKit executable
|
||||
exists. It uses the local Go toolchain. Preparation must finish before the
|
||||
offline validation begins.
|
||||
|
||||
@@ -291,6 +291,8 @@ authentication. The Gitea suite passes.
|
||||
|
||||
### R8 — Medium: The release wrapper collapses documented exit codes
|
||||
|
||||
Status: Resolved.
|
||||
|
||||
Category: Executable integration and documentation correctness.
|
||||
|
||||
**Evidence:** [scripts/release](../../scripts/release), line 9, executes
|
||||
@@ -313,6 +315,12 @@ distribution, or external preparation gate.
|
||||
unknown-publication status 3, as well as ordinary success and known failure.
|
||||
Use local fixtures rather than a real publication endpoint.
|
||||
|
||||
**Resolution:** The source wrapper builds its native executable into ignored
|
||||
`.releasekit/bin` and invokes it directly. Documentation distinguishes native
|
||||
exit codes from `go run` driver behavior. Local-repository tests execute the
|
||||
actual wrapper and verify success 0, rejected publication 1, invalid input 2,
|
||||
and unresolved publication 3. The focused wrapper test passes.
|
||||
|
||||
### R9 — High: Manual recovery inspects the fetch endpoint
|
||||
|
||||
Category: Release recovery correctness and documentation gap.
|
||||
|
||||
@@ -6,4 +6,6 @@ if [ "$#" -ne 1 ]; then
|
||||
exit 2
|
||||
fi
|
||||
|
||||
GOTOOLCHAIN=local exec go run ./cmd/releasekit publish "$1"
|
||||
mkdir -p .releasekit/bin
|
||||
GOTOOLCHAIN=local go build -o .releasekit/bin/releasekit ./cmd/releasekit
|
||||
exec .releasekit/bin/releasekit publish "$1"
|
||||
|
||||
Reference in New Issue
Block a user