1365 lines
56 KiB
Go
1365 lines
56 KiB
Go
package main
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/sha256"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strings"
|
|
"testing"
|
|
|
|
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli"
|
|
)
|
|
|
|
func TestVersionCommand(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
if status := run(context.Background(), []string{"version"}, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("run() status = %d, want 0; stderr = %q", status, stderr.String())
|
|
}
|
|
if want := fmt.Sprintf("releasekit %s\n", buildinfo.Version()); stdout.String() != want {
|
|
t.Fatalf("version output = %q, want %q", stdout.String(), want)
|
|
}
|
|
if stderr.Len() != 0 {
|
|
t.Fatalf("version command wrote to stderr: %q", stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestUnknownCommand(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
if status := run(context.Background(), []string{"unknown"}, &stdout, &stderr); status != 2 {
|
|
t.Fatalf("run() status = %d, want 2", status)
|
|
}
|
|
if stdout.Len() != 0 {
|
|
t.Fatalf("unknown command wrote to stdout: %q", stdout.String())
|
|
}
|
|
if want := "unknown command \"unknown\"\n" + cli.Usage + "\n"; stderr.String() != want {
|
|
t.Fatalf("usage output = %q, want %q", stderr.String(), want)
|
|
}
|
|
}
|
|
|
|
func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
|
|
raceSelected := racePrerequisitesAvailable()
|
|
repository := newCLIRepository(t)
|
|
profilePath := filepath.Join(t.TempDir(), "profile with spaces.json")
|
|
orderPath := filepath.Join(t.TempDir(), "check-order")
|
|
t.Setenv("RELEASEKIT_CHECK_ORDER_FILE", orderPath)
|
|
executable, err := os.Executable()
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
check := func(name string) cli.CheckCommand {
|
|
return cli.CheckCommand{
|
|
Args: []string{executable, "-test.run=TestCandidateCheckHelper", "--", name},
|
|
Env: map[string]string{
|
|
"RELEASEKIT_CHECK_HELPER": "1",
|
|
"RELEASEKIT_CHECK_NAME": name,
|
|
"RELEASEKIT_VERSION": "profile version",
|
|
"RELEASEKIT_CANDIDATE": "profile commit",
|
|
},
|
|
}
|
|
}
|
|
settings := cli.Settings{
|
|
Product: "Example Product",
|
|
Remote: "origin",
|
|
Checks: []cli.CheckCommand{check("first"), check("second")},
|
|
}
|
|
if raceSelected {
|
|
settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}}
|
|
}
|
|
contents, err := json.Marshal(settings)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(profilePath, contents, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
raceMarker := ""
|
|
if raceSelected {
|
|
raceMarker = filepath.Join(t.TempDir(), "race-enabled")
|
|
t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker)
|
|
}
|
|
args := []string{"check", "--repo", repository, "--config", profilePath, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
|
}
|
|
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
if got, err := os.ReadFile(orderPath); err != nil || string(got) != "first\nsecond\n" {
|
|
t.Fatalf("configured check order = %q, error = %v", got, err)
|
|
}
|
|
if got, want := strings.Count(stderr.String(), "v1.2.3|"+commit), 2; got != want {
|
|
t.Fatalf("configured checks received captured candidate %d times, want %d; stderr=%q", got, want, stderr.String())
|
|
}
|
|
if want := fmt.Sprintf("local candidate v1.2.3 at %s is valid\n", commit); stdout.String() != want {
|
|
t.Fatalf("stdout = %q, want %q", stdout.String(), want)
|
|
}
|
|
if raceSelected {
|
|
if _, err := os.Stat(raceMarker); err != nil {
|
|
t.Fatalf("race-enabled profile did not run: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestCandidateRequestMapsGoValidation(t *testing.T) {
|
|
limit := int64(2048)
|
|
request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{
|
|
Product: "Example Product",
|
|
ExpectedPushURL: "publication-only value",
|
|
Validation: &cli.ValidationSettings{
|
|
GoFormat: true,
|
|
Markdown: &cli.MarkdownSettings{EnforceCheckoutBoundary: true},
|
|
Go: cli.GoValidationSettings{Race: true, FullVet: true, CompileTargets: []string{"linux/amd64", "darwin/arm64"}},
|
|
ModuleDistribution: cli.ModuleDistributionSettings{WorkspaceFiles: true, LocalReplacements: true},
|
|
FileSize: &cli.FileSizeSettings{LimitBytes: &limit, Exclusions: []string{"cache"}},
|
|
},
|
|
}, nil)
|
|
if !request.Validation.Go.Race || !request.Validation.Go.FullVet {
|
|
t.Fatalf("candidate request validation = %#v, want Go race and full vet enabled", request.Validation)
|
|
}
|
|
if !request.Validation.GoFormat {
|
|
t.Fatalf("candidate request GoFormat = false, want true")
|
|
}
|
|
if request.Validation.Markdown == nil || !request.Validation.Markdown.EnforceCheckoutBoundary {
|
|
t.Fatalf("candidate request Markdown = %#v, want boundary enforcement enabled", request.Validation.Markdown)
|
|
}
|
|
if !request.Validation.ModuleDistribution.WorkspaceFiles || !request.Validation.ModuleDistribution.LocalReplacements {
|
|
t.Fatalf("candidate request module distribution = %#v, want both checks enabled", request.Validation.ModuleDistribution)
|
|
}
|
|
if request.Validation.FileSize == nil || request.Validation.FileSize.LimitBytes == nil || *request.Validation.FileSize.LimitBytes != limit || len(request.Validation.FileSize.Exclusions) != 1 || request.Validation.FileSize.Exclusions[0] != "cache" {
|
|
t.Fatalf("candidate request file size = %#v, want configured limit and exclusions", request.Validation.FileSize)
|
|
}
|
|
if request.ExpectedPushURL != "" {
|
|
t.Fatalf("candidate request carried publication-only expected push URL %q", request.ExpectedPushURL)
|
|
}
|
|
wantTargets := []releasekit.Target{{OS: "linux", Arch: "amd64"}, {OS: "darwin", Arch: "arm64"}}
|
|
if len(request.Validation.Go.CompileTargets) != len(wantTargets) {
|
|
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.Go.CompileTargets, wantTargets)
|
|
}
|
|
for index := range wantTargets {
|
|
if request.Validation.Go.CompileTargets[index] != wantTargets[index] {
|
|
t.Fatalf("candidate request compile targets = %#v, want %#v", request.Validation.Go.CompileTargets, wantTargets)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestValidationSpecPreservesMarkdownObjectSelection(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
settings *cli.MarkdownSettings
|
|
want bool
|
|
}{
|
|
{name: "omitted"},
|
|
{name: "empty object", settings: &cli.MarkdownSettings{}, want: true},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
got := validationSpec(&cli.ValidationSettings{Markdown: test.settings}).Markdown != nil
|
|
if got != test.want {
|
|
t.Fatalf("Markdown selected = %t, want %t", got, test.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCandidateCommandsRunMarkdownProfile(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
profile, err := json.Marshal(cli.Settings{
|
|
Product: "Example Product",
|
|
Validation: &cli.ValidationSettings{
|
|
Markdown: &cli.MarkdownSettings{},
|
|
},
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeRepositoryFile(t, repository, "releasekit.json", string(profile))
|
|
writeRepositoryFile(t, repository, "guide.md", "[missing](missing.md#top)\n")
|
|
cliGitRun(t, repository, "add", "releasekit.json", "guide.md")
|
|
cliGitRun(t, repository, "commit", "-m", "configure Markdown candidate validation")
|
|
remote := filepath.Join(t.TempDir(), "origin.git")
|
|
cliGitRun(t, repository, "init", "--bare", remote)
|
|
cliGitRun(t, repository, "remote", "add", "origin", remote)
|
|
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main")
|
|
|
|
for _, command := range []string{"check", "publish"} {
|
|
t.Run(command, func(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
args := []string{command, "--repo", repository, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 1 {
|
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q, want validation failure", status, stdout.String(), stderr.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "markdown-links") || !strings.Contains(stderr.String(), "guide.md:1:") || !strings.Contains(stderr.String(), "missing.md#top") {
|
|
t.Fatalf("stderr = %q, want Markdown identity, source location, and target", stderr.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
profile, err := json.Marshal(cli.Settings{Product: "Example Product"})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
writeRepositoryFile(t, repository, "releasekit.json", string(profile))
|
|
cliGitRun(t, repository, "add", "releasekit.json")
|
|
cliGitRun(t, repository, "commit", "-m", "configure candidate commands")
|
|
remote := filepath.Join(t.TempDir(), "origin.git")
|
|
cliGitRun(t, repository, "init", "--bare", remote)
|
|
cliGitRun(t, repository, "remote", "add", "origin", remote)
|
|
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main")
|
|
|
|
goPath, err := exec.LookPath("go")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
goDirectory := t.TempDir()
|
|
shim := filepath.Join(goDirectory, "go")
|
|
if err := os.WriteFile(shim, []byte("#!/bin/sh\nif [ \"$1\" = test ]; then echo 'fixture Go suite failure' >&2; exit 1; fi\nexec \"$RELEASEKIT_REAL_GO\" \"$@\"\n"), 0o755); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("RELEASEKIT_REAL_GO", goPath)
|
|
t.Setenv("PATH", goDirectory+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
for _, command := range []struct {
|
|
name string
|
|
wantStatus int
|
|
args []string
|
|
}{
|
|
{name: "check", wantStatus: 1, args: []string{"check", "--repo", repository, "v1.2.3"}},
|
|
{name: "publish", wantStatus: 1, args: []string{"publish", "--repo", repository, "v1.2.3"}},
|
|
} {
|
|
t.Run(command.name, func(t *testing.T) {
|
|
var stdout, stderr bytes.Buffer
|
|
if status := run(context.Background(), command.args, &stdout, &stderr); status != command.wantStatus {
|
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
|
}
|
|
if got := strings.Count(stderr.String(), "go-test validation failed"); got != 1 {
|
|
t.Fatalf("validation diagnostic appeared %d times: %q", got, stderr.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "fixture Go suite failure") {
|
|
t.Fatalf("stderr omitted the validation cause: %q", stderr.String())
|
|
}
|
|
if command.name == "publish" && !strings.Contains(stderr.String(), "state=not_attempted phase=validation") {
|
|
t.Fatalf("publication validation error lost its state: %q", stderr.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestCandidateFailurePresentationRetainsCallbackContext(t *testing.T) {
|
|
diagnostic := &releasekit.ValidationError{
|
|
Check: "examples",
|
|
Findings: []releasekit.ValidationFinding{{Path: "examples/run/main.go", Message: "unexpected output"}},
|
|
}
|
|
err := fmt.Errorf("validate candidate captured-commit: %w", diagnostic)
|
|
text := candidateFailureText(err)
|
|
for _, part := range []string{"captured-commit", "examples", "examples/run/main.go", "unexpected output"} {
|
|
if !strings.Contains(text, part) {
|
|
t.Errorf("CLI presentation %q omits %q", text, part)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) {
|
|
raceSelected := racePrerequisitesAvailable()
|
|
repository := newCLIRepository(t)
|
|
writeRepositoryFile(t, repository, "docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\n## Compatibility\n\nNo action required.\n")
|
|
writeRepositoryFile(t, repository, "scripts/check-release-note.sh", `#!/bin/sh
|
|
set -eu
|
|
version=$RELEASEKIT_VERSION
|
|
candidate=$RELEASEKIT_CANDIDATE
|
|
test "$(git rev-parse HEAD)" = "$candidate"
|
|
test "$(sed -n '1p' "docs/releases/$version.md")" = "# Example Product $version"
|
|
grep -Fxq '## Compatibility' "docs/releases/$version.md"
|
|
grep -Fxq 'No action required.' "docs/releases/$version.md"
|
|
grep -Fxq 'module example.com/cli-candidate' go.mod
|
|
printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG"
|
|
`)
|
|
cliGitRun(t, repository, "add", "docs/releases/v1.2.3.md", "scripts/check-release-note.sh")
|
|
cliGitRun(t, repository, "commit", "-m", "add compatibility note and check")
|
|
checkLog := filepath.Join(t.TempDir(), "compatibility-check.log")
|
|
remote := filepath.Join(t.TempDir(), "origin.git")
|
|
settings := cli.Settings{
|
|
Product: "Example Product",
|
|
Remote: "origin",
|
|
ExpectedPushURL: remote,
|
|
Checks: []cli.CheckCommand{{
|
|
Args: []string{"sh", "scripts/check-release-note.sh"},
|
|
Env: map[string]string{"RELEASEKIT_COMPATIBILITY_LOG": checkLog},
|
|
}},
|
|
}
|
|
if raceSelected {
|
|
settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}}
|
|
}
|
|
contents, err := json.Marshal(settings)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(filepath.Join(repository, "releasekit.json"), contents, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cliGitRun(t, repository, "add", "releasekit.json")
|
|
cliGitRun(t, repository, "commit", "-m", "configure source release")
|
|
cliGitRun(t, repository, "init", "--bare", remote)
|
|
cliGitRun(t, repository, "remote", "add", "origin", remote)
|
|
cliGitRun(t, repository, "push", "--set-upstream", "origin", "main")
|
|
|
|
var stdout, stderr bytes.Buffer
|
|
raceMarker := ""
|
|
if raceSelected {
|
|
raceMarker = filepath.Join(t.TempDir(), "race-enabled")
|
|
t.Setenv("RELEASEKIT_RACE_MARKER", raceMarker)
|
|
}
|
|
args := []string{"publish", "--repo", repository, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
|
}
|
|
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
tagObject := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3")
|
|
if want := fmt.Sprintf("published v1.2.3 at %s (tag object %s)\n", commit, tagObject); stdout.String() != want {
|
|
t.Fatalf("stdout = %q, want %q", stdout.String(), want)
|
|
}
|
|
if !bytes.Contains(stderr.Bytes(), []byte("validating and publishing v1.2.3")) || !bytes.Contains(stderr.Bytes(), []byte("example.com/cli-candidate")) {
|
|
t.Fatalf("stderr does not show publication progress and validation: %q", stderr.String())
|
|
}
|
|
if got := cliGitOutput(t, remote, "rev-parse", "refs/tags/v1.2.3"); got != tagObject {
|
|
t.Fatalf("remote tag object = %s, want %s", got, tagObject)
|
|
}
|
|
checkResult, err := os.ReadFile(checkLog)
|
|
if err != nil {
|
|
t.Fatalf("read product compatibility check result: %v", err)
|
|
}
|
|
if got, want := strings.Count(string(checkResult), "v1.2.3 "+commit), 1; got != want {
|
|
t.Fatalf("product compatibility check ran %d times, want %d", got, want)
|
|
}
|
|
if raceSelected {
|
|
if _, err := os.Stat(raceMarker); err != nil {
|
|
t.Fatalf("race-enabled profile did not run: %v", err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestBuildCommandWritesValidatedReceiptForConfiguredArtifacts(t *testing.T) {
|
|
repository := newCLIBuildRepository(t, `package main
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
|
|
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
|
)
|
|
|
|
func main() {
|
|
if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixture %s\n", buildinfo.Version()) }
|
|
}
|
|
`)
|
|
var stdout, stderr bytes.Buffer
|
|
args := []string{"build", "--repo", repository, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
|
}
|
|
receiptPath := filepath.Join(repository, "dist", cli.BuildReceiptName)
|
|
receipt, err := cli.LoadBuildReceipt(receiptPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
settings := cli.BuildSettings{
|
|
MainPackage: "./cmd/app",
|
|
BinaryName: "fixture",
|
|
Targets: []string{fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)},
|
|
}
|
|
if !receipt.MatchesBuild(repository, commit, "v1.2.3", settings) || len(receipt.Artifacts) != 1 {
|
|
t.Fatalf("build receipt = %#v", receipt)
|
|
}
|
|
if !bytes.Contains(stdout.Bytes(), []byte(receipt.Artifacts[0].Filename)) || !bytes.Contains(stdout.Bytes(), []byte(receipt.Artifacts[0].SHA256)) {
|
|
t.Fatalf("stdout does not report receipt artifacts: %q", stdout.String())
|
|
}
|
|
if got := cliGitOutput(t, repository, "status", "--porcelain"); got != "" {
|
|
t.Fatalf("build output dirtied candidate: %s", got)
|
|
}
|
|
}
|
|
|
|
func TestBuildCommandLeavesNoReceiptAfterSmokeFailure(t *testing.T) {
|
|
repository := newCLIBuildRepository(t, `package main
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
)
|
|
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("wrong") } }
|
|
`)
|
|
var stdout, stderr bytes.Buffer
|
|
args := []string{"build", "--repo", repository, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 1 {
|
|
t.Fatalf("run() status = %d; stdout=%q stderr=%q, want 1", status, stdout.String(), stderr.String())
|
|
}
|
|
if stdout.Len() != 0 || !bytes.Contains(stderr.Bytes(), []byte("native version output")) {
|
|
t.Fatalf("stdout=%q stderr=%q; want smoke failure without final output", stdout.String(), stderr.String())
|
|
}
|
|
if _, err := os.Stat(filepath.Join(repository, "dist", cli.BuildReceiptName)); !os.IsNotExist(err) {
|
|
t.Fatalf("partial build wrote receipt: %v", err)
|
|
}
|
|
filename := fmt.Sprintf("fixture-v1.2.3-%s-%s", runtime.GOOS, runtime.GOARCH)
|
|
if runtime.GOOS == "windows" {
|
|
filename += ".exe"
|
|
}
|
|
if _, err := os.Stat(filepath.Join(repository, "dist", filename)); err != nil {
|
|
t.Fatalf("completed artifact not reported or retained: %v; stderr=%q", err, stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestCandidateCheckHelper(t *testing.T) {
|
|
if os.Getenv("RELEASEKIT_CHECK_HELPER") != "1" {
|
|
return
|
|
}
|
|
if orderPath := os.Getenv("RELEASEKIT_CHECK_ORDER_FILE"); orderPath != "" {
|
|
file, err := os.OpenFile(orderPath, os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o600)
|
|
if err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(9)
|
|
}
|
|
if _, err := fmt.Fprintln(file, os.Getenv("RELEASEKIT_CHECK_NAME")); err != nil {
|
|
_ = file.Close()
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(9)
|
|
}
|
|
if err := file.Close(); err != nil {
|
|
fmt.Fprintln(os.Stderr, err)
|
|
os.Exit(9)
|
|
}
|
|
}
|
|
fmt.Fprintf(os.Stdout, "%s|%s", os.Getenv("RELEASEKIT_VERSION"), os.Getenv("RELEASEKIT_CANDIDATE"))
|
|
os.Exit(0)
|
|
}
|
|
|
|
func newCLIRepository(t *testing.T) string {
|
|
t.Helper()
|
|
repository := filepath.Join(t.TempDir(), "candidate repository")
|
|
if err := os.Mkdir(repository, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cliGitRun(t, repository, "init", "--initial-branch=main")
|
|
cliGitRun(t, repository, "config", "user.name", "ReleaseKit Test")
|
|
cliGitRun(t, repository, "config", "user.email", "releasekit@example.test")
|
|
cliGitRun(t, repository, "config", "commit.gpgsign", "false")
|
|
write := func(name, value string) {
|
|
t.Helper()
|
|
path := filepath.Join(repository, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(value), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n")
|
|
write("candidate_test.go", "package candidate\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n")
|
|
write("race_candidate_test.go", `//go:build race
|
|
|
|
package candidate
|
|
|
|
import (
|
|
"os"
|
|
"testing"
|
|
)
|
|
|
|
func TestRaceBuildSelection(t *testing.T) {
|
|
marker := os.Getenv("RELEASEKIT_RACE_MARKER")
|
|
if marker == "" {
|
|
return
|
|
}
|
|
if err := os.WriteFile(marker, []byte("race enabled"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
`)
|
|
write("docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nRelease note.\n")
|
|
cliGitRun(t, repository, "add", "--all")
|
|
cliGitRun(t, repository, "commit", "-m", "candidate")
|
|
return repository
|
|
}
|
|
|
|
func racePrerequisitesAvailable() bool {
|
|
supported := runtime.GOOS == "darwin" && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64") ||
|
|
(runtime.GOOS == "freebsd" || runtime.GOOS == "linux") && (runtime.GOARCH == "amd64" || runtime.GOARCH == "arm64" || runtime.GOARCH == "ppc64le") ||
|
|
runtime.GOOS == "windows" && runtime.GOARCH == "amd64"
|
|
if !supported {
|
|
return false
|
|
}
|
|
cgo, err := exec.Command("go", "env", "CGO_ENABLED").Output()
|
|
if err != nil || strings.TrimSpace(string(cgo)) != "1" {
|
|
return false
|
|
}
|
|
cc, err := exec.Command("go", "env", "CC").Output()
|
|
if err != nil || len(strings.Fields(string(cc))) == 0 {
|
|
return false
|
|
}
|
|
if _, err := exec.LookPath(strings.Fields(string(cc))[0]); err != nil {
|
|
return false
|
|
}
|
|
return true
|
|
}
|
|
|
|
func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
settings := cli.Settings{
|
|
Product: "Example Product",
|
|
Validation: &cli.ValidationSettings{Go: cli.GoValidationSettings{CompileTargets: []string{"linux/amd64"}}},
|
|
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
|
}
|
|
writeDeliveryProfile(t, repository, settings)
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3") // Historical lightweight tags are valid delivery inputs.
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
|
if status != 0 {
|
|
t.Fatalf("delivery attempt %d status=%d stdout=%q stderr=%q", attempt+1, status, stdout.String(), stderr.String())
|
|
}
|
|
if !strings.Contains(stdout.String(), "state complete") || !strings.Contains(stdout.String(), "release: "+server.URL+"/releases/10") {
|
|
t.Fatalf("delivery output = %q", stdout.String())
|
|
}
|
|
}
|
|
if state.releaseCreates != 1 || state.assetUploads != 0 || state.assetListReads != 0 {
|
|
t.Fatalf("page-only calls = creates:%d asset uploads:%d asset reads:%d", state.releaseCreates, state.assetUploads, state.assetListReads)
|
|
}
|
|
if state.noteBody != "# Example Product v1.2.3\n\nRelease note.\n" {
|
|
t.Fatalf("hosted note body = %q", state.noteBody)
|
|
}
|
|
}
|
|
|
|
func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) {
|
|
repository := newDeliveryBuildRepository(t)
|
|
temporaryRoot := t.TempDir()
|
|
t.Setenv("TMPDIR", temporaryRoot)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
settings := cli.Settings{
|
|
Product: "Example Product",
|
|
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}},
|
|
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
|
}
|
|
writeDeliveryProfile(t, repository, settings)
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
cliGitRun(t, repository, "checkout", "--detach", "v1.2.3")
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
|
if status != 0 {
|
|
t.Fatalf("delivery attempt %d status=%d stdout=%q stderr=%q", attempt+1, status, stdout.String(), stderr.String())
|
|
}
|
|
if attempt == 1 && (!strings.Contains(stdout.String(), "matching helper-v1.2.3-linux-amd64") || !strings.Contains(stdout.String(), "matching SHA256SUMS")) {
|
|
t.Fatalf("second delivery did not report matching assets: %q", stdout.String())
|
|
}
|
|
}
|
|
if state.releaseCreates != 1 || state.assetUploads != 2 {
|
|
t.Fatalf("delivery calls = creates:%d asset uploads:%d", state.releaseCreates, state.assetUploads)
|
|
}
|
|
if _, ok := state.assets["helper-v1.2.3-linux-amd64"]; !ok {
|
|
t.Fatalf("binary was not uploaded: %#v", state.assets)
|
|
}
|
|
if _, ok := state.assets["SHA256SUMS"]; !ok {
|
|
t.Fatalf("checksum manifest was not uploaded: %#v", state.assets)
|
|
}
|
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
|
t.Fatalf("facade-owned build output remains after delivery: entries=%v error=%v", entries, err)
|
|
}
|
|
}
|
|
|
|
func TestCIDeliverLoadsTaggedProfileAndSkipsUnavailablePageOnlyDependencies(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire example.com/unavailable/module v1.2.3\n")
|
|
cliGitRun(t, repository, "add", "go.mod")
|
|
cliGitRun(t, repository, "commit", "-m", "declare unavailable dependency")
|
|
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{
|
|
Product: "Example Product",
|
|
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
|
})
|
|
expectedCommit := pushTaggedCLIRepository(t, repository)
|
|
state.commit = expectedCommit
|
|
writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{Product: "Later Branch Profile"})
|
|
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
t.Setenv("GOPROXY", "off")
|
|
t.Setenv("GOSUMDB", "off")
|
|
var stdout, stderr bytes.Buffer
|
|
args := []string{"ci-deliver", "--repo", repository, "--config", "profiles/ci-delivery.json", "--commit", expectedCommit, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
|
}
|
|
if state.releaseCreates != 1 || state.apiRequests == 0 {
|
|
t.Fatalf("page delivery did not reach the server: creates=%d requests=%d", state.releaseCreates, state.apiRequests)
|
|
}
|
|
if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != expectedCommit {
|
|
t.Fatalf("prepared HEAD = %s, want %s", got, expectedCommit)
|
|
}
|
|
}
|
|
|
|
func TestCIDeliverRejectsWrongCommitBeforeGoOrHostedRequests(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire example.com/unavailable/module v1.2.3\n")
|
|
cliGitRun(t, repository, "add", "go.mod")
|
|
cliGitRun(t, repository, "commit", "-m", "declare unavailable dependency")
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
writeDeliveryProfile(t, repository, cli.Settings{
|
|
Product: "Example Product",
|
|
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}},
|
|
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
|
})
|
|
expectedCommit := pushTaggedCLIRepository(t, repository)
|
|
state.commit = expectedCommit
|
|
|
|
shimDir := t.TempDir()
|
|
markerPath := filepath.Join(shimDir, "go-invoked")
|
|
goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n"
|
|
if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(goShim), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("RELEASEKIT_GO_MARKER", markerPath)
|
|
t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
wrongCommit := strings.Repeat("0", 40)
|
|
args := []string{"ci-deliver", "--repo", repository, "--commit", wrongCommit, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 1 {
|
|
t.Fatalf("run() status=%d stdout=%q stderr=%q, want preparation failure", status, stdout.String(), stderr.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "resolves to commit") {
|
|
t.Fatalf("stderr=%q, want expected-commit mismatch", stderr.String())
|
|
}
|
|
if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
|
|
t.Fatalf("Go was invoked before commit agreement: %v", err)
|
|
}
|
|
if state.apiRequests != 0 {
|
|
t.Fatalf("hosted API requests = %d, want 0", state.apiRequests)
|
|
}
|
|
}
|
|
|
|
func TestCIDeliverPreparesDependenciesAndBuildsConfiguredTargets(t *testing.T) {
|
|
repository := newDeliveryBuildRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
writeDeliveryProfile(t, repository, cli.Settings{
|
|
Product: "Example Product",
|
|
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)}},
|
|
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
|
})
|
|
expectedCommit := pushTaggedCLIRepository(t, repository)
|
|
state.commit = expectedCommit
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String())
|
|
}
|
|
if state.releaseCreates != 1 || state.assetUploads != 2 {
|
|
t.Fatalf("delivery counts = creates:%d uploads:%d", state.releaseCreates, state.assetUploads)
|
|
}
|
|
if _, ok := state.assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok {
|
|
t.Fatalf("binary not uploaded: %#v", state.assets)
|
|
}
|
|
}
|
|
|
|
func TestCIDeliverValidatesCredentialsBeforePreparingDependencies(t *testing.T) {
|
|
repository := newDeliveryBuildRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
writeDeliveryProfile(t, repository, cli.Settings{
|
|
Product: "Example Product",
|
|
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}},
|
|
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
|
})
|
|
expectedCommit := pushTaggedCLIRepository(t, repository)
|
|
state.commit = expectedCommit
|
|
|
|
shimDir := t.TempDir()
|
|
markerPath := filepath.Join(shimDir, "go-invoked")
|
|
goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n"
|
|
if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(goShim), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("RELEASEKIT_GO_MARKER", markerPath)
|
|
t.Setenv("PATH", shimDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "")
|
|
var stdout, stderr bytes.Buffer
|
|
args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 2 {
|
|
t.Fatalf("run() status=%d stdout=%q stderr=%q, want credential error", status, stdout.String(), stderr.String())
|
|
}
|
|
if !strings.Contains(stderr.String(), "non-empty token") {
|
|
t.Fatalf("stderr=%q, want missing-credential error", stderr.String())
|
|
}
|
|
if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
|
|
t.Fatalf("Go was invoked before credential validation: %v", err)
|
|
}
|
|
if state.apiRequests != 0 {
|
|
t.Fatalf("hosted API requests = %d, want 0", state.apiRequests)
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsInvalidTaggedSourceBeforeManagedBuild(t *testing.T) {
|
|
repository := newDeliveryBuildRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
settings := cli.Settings{
|
|
Product: "Example Product",
|
|
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}},
|
|
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
|
}
|
|
writeDeliveryProfile(t, repository, settings)
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
if err := os.WriteFile(filepath.Join(repository, "uncommitted.txt"), []byte("dirty"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
|
if status != 1 || state.apiRequests != 0 || strings.Contains(stderr.String(), "building ") {
|
|
t.Fatalf("invalid-source delivery status=%d requests=%d stdout=%q stderr=%q", status, state.apiRequests, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
state string
|
|
status int
|
|
recovery []string
|
|
assetObservation string
|
|
missingObservation string
|
|
}{
|
|
{
|
|
name: "partial",
|
|
status: 1,
|
|
recovery: []string{"recovery: fix the reported cause", "retry this version"},
|
|
assetObservation: "pending helper-v1.2.3-linux-amd64",
|
|
},
|
|
{
|
|
name: "conflict",
|
|
status: 1,
|
|
recovery: []string{"investigate conflicting hosted content", "new version", "retain the original build artifacts"},
|
|
assetObservation: "conflicting helper-v1.2.3-linux-amd64",
|
|
missingObservation: "pending SHA256SUMS",
|
|
},
|
|
{
|
|
name: "unknown",
|
|
status: 3,
|
|
recovery: []string{"resolve the remote state before retrying"},
|
|
assetObservation: "uncertain helper-v1.2.3-linux-amd64",
|
|
missingObservation: "pending SHA256SUMS",
|
|
},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
|
|
writeDeliveryProfile(t, repository, cli.Settings{
|
|
Product: "Example Product",
|
|
Build: &build,
|
|
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
|
|
})
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
assetsDir, _ := writeDeliveryReceipt(t, repository, build)
|
|
switch test.name {
|
|
case "partial":
|
|
state.failAssetName = "helper-v1.2.3-linux-amd64"
|
|
case "conflict":
|
|
state.assets["helper-v1.2.3-linux-amd64"] = []byte("different hosted bytes")
|
|
case "unknown":
|
|
state.ambiguousAssetName = "helper-v1.2.3-linux-amd64"
|
|
}
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr)
|
|
if status != test.status || !strings.Contains(stderr.String(), "state="+test.name) {
|
|
t.Fatalf("status=%d output=%q want status=%d state=%s", status, stderr.String(), test.status, test.name)
|
|
}
|
|
for _, phrase := range test.recovery {
|
|
if !strings.Contains(stderr.String(), phrase) {
|
|
t.Errorf("delivery output does not include recovery guidance %q: %s", phrase, stderr.String())
|
|
}
|
|
}
|
|
if !strings.Contains(stderr.String(), test.assetObservation) {
|
|
t.Errorf("delivery output does not include asset observation %q: %s", test.assetObservation, stderr.String())
|
|
}
|
|
if test.missingObservation != "" && !strings.Contains(stderr.String(), test.missingObservation) {
|
|
t.Errorf("delivery output does not include later missing asset %q: %s", test.missingObservation, stderr.String())
|
|
}
|
|
if test.name == "unknown" && strings.Contains(stderr.String(), "pending helper-v1.2.3-linux-amd64") {
|
|
t.Errorf("uncertain asset also appears pending: %s", stderr.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsWrongHostedTagBeforeReleaseWrite(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
state := &deliveryServerState{remoteCommit: strings.Repeat("0", 40)}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
|
if status != 1 || state.releaseCreates != 0 || state.assetUploads != 0 {
|
|
t.Fatalf("wrong hosted tag status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.releaseCreates, state.assetUploads, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestDeliverRequiresCredentialBeforeRemoteRequests(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example", TokenEnv: "RELEASEKIT_TEST_DELIVERY_TOKEN"}})
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
t.Setenv("RELEASEKIT_TEST_DELIVERY_TOKEN", "")
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
|
if status != 2 || state.apiRequests != 0 || !strings.Contains(stderr.String(), "RELEASEKIT_TEST_DELIVERY_TOKEN") {
|
|
t.Fatalf("missing credential status=%d requests=%d stdout=%q stderr=%q", status, state.apiRequests, stdout.String(), stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsCheckoutThatDiffersFromLocalTag(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later commit"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cliGitRun(t, repository, "add", "later.txt")
|
|
cliGitRun(t, repository, "commit", "-m", "advance checkout")
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
|
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 {
|
|
t.Fatalf("mismatched checkout status=%d requests=%d creates=%d stdout=%q stderr=%q", status, state.apiRequests, state.releaseCreates, stdout.String(), stderr.String())
|
|
}
|
|
if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3"); got != state.commit {
|
|
t.Fatalf("delivery moved tag from %s to %s", state.commit, got)
|
|
}
|
|
}
|
|
|
|
func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testing.T) {
|
|
for _, mode := range []string{"valid receipt", "tampered", "stale settings"} {
|
|
t.Run(mode, func(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
current := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
|
|
settings := cli.Settings{Product: "Example Product", Build: ¤t, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
|
|
writeDeliveryProfile(t, repository, settings)
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
receiptSettings := current
|
|
if mode == "stale settings" {
|
|
receiptSettings.BinaryName = "previous"
|
|
}
|
|
assetsDir, artifactPath := writeDeliveryReceipt(t, repository, receiptSettings)
|
|
if mode == "tampered" {
|
|
if err := os.WriteFile(artifactPath, []byte("tampered artifact"), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr)
|
|
if mode == "valid receipt" {
|
|
if status != 0 || state.releaseCreates != 1 || state.assetUploads != 2 {
|
|
t.Fatalf("valid receipt status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.releaseCreates, state.assetUploads, stdout.String(), stderr.String())
|
|
}
|
|
if _, err := os.Stat(artifactPath); err != nil {
|
|
t.Fatalf("prebuilt artifact was not retained: %v", err)
|
|
}
|
|
return
|
|
}
|
|
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 || state.assetUploads != 0 {
|
|
t.Fatalf("%s receipt status=%d requests=%d stdout=%q stderr=%q", mode, status, state.apiRequests, stdout.String(), stderr.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func writeDeliveryReceipt(t *testing.T, repository string, settings cli.BuildSettings) (string, string) {
|
|
t.Helper()
|
|
assetsDir := t.TempDir()
|
|
filename := settings.BinaryName + "-v1.2.3-linux-amd64"
|
|
artifactPath := filepath.Join(assetsDir, filename)
|
|
content := []byte("fixture artifact")
|
|
if err := os.WriteFile(artifactPath, content, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
resolvedRepository, err := filepath.Abs(repository)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
digest := sha256.Sum256(content)
|
|
target := cli.ReceiptTarget{OS: "linux", Arch: "amd64"}
|
|
receipt := cli.BuildReceipt{
|
|
RepositoryDir: resolvedRepository, CandidateCommit: cliGitOutput(t, repository, "rev-parse", "HEAD"),
|
|
SourceVerified: true,
|
|
Version: "v1.2.3", MainPackage: settings.MainPackage, BinaryName: settings.BinaryName,
|
|
OutputDir: assetsDir, Targets: []cli.ReceiptTarget{target}, Compiler: "go version fixture",
|
|
Artifacts: []cli.ReceiptArtifact{{Path: artifactPath, Filename: filename, Target: target, SHA256: hex.EncodeToString(digest[:])}},
|
|
}
|
|
if _, err := cli.WriteBuildReceipt(assetsDir, receipt); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
return assetsDir, artifactPath
|
|
}
|
|
|
|
type deliveryServerState struct {
|
|
malformedCreate bool
|
|
failAssetListing bool
|
|
failAssetDownloads bool
|
|
failAssetName string
|
|
ambiguousAssetName string
|
|
commit string
|
|
product string
|
|
remoteCommit string
|
|
apiRequests int
|
|
pageExists bool
|
|
releaseCreates int
|
|
assetUploads int
|
|
assetListReads int
|
|
noteBody string
|
|
assets map[string][]byte
|
|
}
|
|
|
|
func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server {
|
|
t.Helper()
|
|
if state.assets == nil {
|
|
state.assets = make(map[string][]byte)
|
|
}
|
|
serverURL := ""
|
|
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
|
state.apiRequests++
|
|
if r.Header.Get("Authorization") != "token fixture-token" {
|
|
t.Errorf("authorization = %q", r.Header.Get("Authorization"))
|
|
}
|
|
}
|
|
switch r.URL.Path {
|
|
case "/api/v1/repos/owner/example/tags/v1.2.3":
|
|
if state.commit == "" {
|
|
t.Error("tag source commit was not initialized")
|
|
}
|
|
commit := state.remoteCommit
|
|
if commit == "" {
|
|
commit = state.commit
|
|
}
|
|
_, _ = fmt.Fprintf(w, `{"name":"v1.2.3","commit":{"sha":%q}}`, commit)
|
|
case "/api/v1/repos/owner/example/releases/tags/v1.2.3":
|
|
if !state.pageExists {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
writeDeliveryRelease(w, serverURL, state)
|
|
case "/api/v1/repos/owner/example/releases":
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("release request method = %s", r.Method)
|
|
http.Error(w, "method", 405)
|
|
return
|
|
}
|
|
var payload struct {
|
|
Tag string `json:"tag_name"`
|
|
Target string `json:"target_commitish"`
|
|
Name string `json:"name"`
|
|
Body string `json:"body"`
|
|
}
|
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
|
t.Errorf("decode create payload: %v", err)
|
|
}
|
|
if payload.Tag != "v1.2.3" || payload.Target != state.commit {
|
|
t.Errorf("release source payload = %#v", payload)
|
|
}
|
|
state.pageExists, state.releaseCreates, state.noteBody = true, state.releaseCreates+1, payload.Body
|
|
if state.malformedCreate {
|
|
w.WriteHeader(http.StatusCreated)
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "tag_name": "v1.2.3", "name": "inconsistent response", "body": payload.Body})
|
|
return
|
|
}
|
|
writeDeliveryRelease(w, serverURL, state)
|
|
case "/api/v1/repos/owner/example/releases/10/assets":
|
|
if r.Method == http.MethodGet {
|
|
state.assetListReads++
|
|
if state.failAssetListing {
|
|
http.Error(w, "unavailable", 503)
|
|
return
|
|
}
|
|
list := make([]map[string]any, 0, len(state.assets))
|
|
for name, content := range state.assets {
|
|
list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": serverURL + "/files/" + name})
|
|
}
|
|
_ = json.NewEncoder(w).Encode(list)
|
|
return
|
|
}
|
|
if r.Method != http.MethodPost {
|
|
t.Errorf("asset request method = %s", r.Method)
|
|
http.Error(w, "method", 405)
|
|
return
|
|
}
|
|
name := r.URL.Query().Get("name")
|
|
file, _, err := r.FormFile("attachment")
|
|
if err != nil {
|
|
t.Errorf("read asset upload %q: %v", name, err)
|
|
http.Error(w, "multipart", 400)
|
|
return
|
|
}
|
|
content, err := io.ReadAll(file)
|
|
_ = file.Close()
|
|
if err != nil {
|
|
t.Errorf("read asset bytes: %v", err)
|
|
}
|
|
if _, exists := state.assets[name]; exists {
|
|
t.Errorf("asset %q uploaded more than once", name)
|
|
}
|
|
if name == state.failAssetName {
|
|
http.Error(w, "upload failed", http.StatusBadRequest)
|
|
return
|
|
}
|
|
if name == state.ambiguousAssetName {
|
|
http.Error(w, "upload outcome ambiguous", http.StatusInternalServerError)
|
|
return
|
|
}
|
|
state.assets[name] = content
|
|
state.assetUploads++
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": state.assetUploads, "name": name, "size": len(content)})
|
|
default:
|
|
if strings.HasPrefix(r.URL.Path, "/files/") {
|
|
if state.failAssetDownloads {
|
|
http.Error(w, "unavailable", 503)
|
|
return
|
|
}
|
|
name := strings.TrimPrefix(r.URL.Path, "/files/")
|
|
content, ok := state.assets[name]
|
|
if !ok {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
_, _ = w.Write(content)
|
|
return
|
|
}
|
|
t.Errorf("unexpected delivery request %s %s", r.Method, r.URL.String())
|
|
http.NotFound(w, r)
|
|
}
|
|
})
|
|
server := httptest.NewServer(handler)
|
|
serverURL = server.URL
|
|
return server
|
|
}
|
|
|
|
func writeDeliveryRelease(w http.ResponseWriter, serverURL string, state *deliveryServerState) {
|
|
w.Header().Set("Content-Type", "application/json")
|
|
product := state.product
|
|
if product == "" {
|
|
product = "Example Product"
|
|
}
|
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL + "/releases/10", "tag_name": "v1.2.3", "target_commitish": state.commit, "name": product + " v1.2.3", "body": state.noteBody, "draft": false, "prerelease": false})
|
|
}
|
|
|
|
func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) {
|
|
writeDeliveryProfileAt(t, repository, "releasekit.json", settings)
|
|
}
|
|
|
|
func writeDeliveryProfileAt(t *testing.T, repository, name string, settings cli.Settings) {
|
|
t.Helper()
|
|
contents, err := json.Marshal(settings)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
path := filepath.Join(repository, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, contents, 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cliGitRun(t, repository, "add", name)
|
|
cliGitRun(t, repository, "commit", "-m", "configure delivery")
|
|
}
|
|
|
|
func newDeliveryBuildRepository(t *testing.T) string {
|
|
t.Helper()
|
|
repository := filepath.Join(t.TempDir(), "tagged application")
|
|
if err := os.Mkdir(repository, 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
cliGitRun(t, repository, "init", "--initial-branch=main")
|
|
cliGitRun(t, repository, "config", "user.name", "ReleaseKit Test")
|
|
cliGitRun(t, repository, "config", "user.email", "releasekit@example.test")
|
|
files := map[string]string{
|
|
"go.mod": "module example.com/tagged-app\n\ngo 1.25.0\n",
|
|
"main.go": "package main\nimport \"fmt\"\nfunc main() { fmt.Println(\"helper v1.2.3\") }\n",
|
|
"docs/releases/v1.2.3.md": "# Example Product v1.2.3\n\nRelease note.\n",
|
|
}
|
|
for name, contents := range files {
|
|
path := filepath.Join(repository, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
cliGitRun(t, repository, "add", "--all")
|
|
cliGitRun(t, repository, "commit", "-m", "tagged candidate")
|
|
return repository
|
|
}
|
|
|
|
func newCLIBuildRepository(t *testing.T, mainSource string) string {
|
|
t.Helper()
|
|
repository := newCLIRepository(t)
|
|
root, err := filepath.Abs(filepath.Join("..", ".."))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
module := fmt.Sprintf("module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.0.0\n\nreplace gitea.maximumdirect.net/eric/ReleaseKit => %s\n", filepath.ToSlash(root))
|
|
write := func(name, contents string) {
|
|
t.Helper()
|
|
path := filepath.Join(repository, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
write("go.mod", module)
|
|
write("cmd/app/main.go", mainSource)
|
|
write("releasekit.json", fmt.Sprintf(`{"product":"Example Product","build":{"main_package":"./cmd/app","binary_name":"fixture","targets":[%q]}}`, fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)))
|
|
write(".gitignore", "dist/\n")
|
|
cliGitRun(t, repository, "add", "--all")
|
|
cliGitRun(t, repository, "commit", "-m", "configure build")
|
|
return repository
|
|
}
|
|
|
|
func cliGitRun(t *testing.T, repository string, args ...string) {
|
|
t.Helper()
|
|
commandArgs := append([]string{"git"}, args...)
|
|
command := exec.Command(commandArgs[0], commandArgs[1:]...)
|
|
command.Dir = repository
|
|
output, err := command.CombinedOutput()
|
|
if err != nil {
|
|
t.Fatalf("%v: %v\n%s", commandArgs, err, output)
|
|
}
|
|
}
|
|
|
|
func writeRepositoryFile(t *testing.T, repository, name, contents string) {
|
|
t.Helper()
|
|
path := filepath.Join(repository, filepath.FromSlash(name))
|
|
if err := os.MkdirAll(filepath.Dir(path), 0o700); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(path, []byte(contents), 0o600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func pushTaggedCLIRepository(t *testing.T, repository string) string {
|
|
t.Helper()
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
commit := cliGitOutput(t, repository, "rev-parse", "v1.2.3^{commit}")
|
|
remote := filepath.Join(t.TempDir(), "origin.git")
|
|
cliGitRun(t, repository, "init", "--bare", remote)
|
|
cliGitRun(t, repository, "remote", "add", "origin", remote)
|
|
cliGitRun(t, repository, "push", "origin", "refs/tags/v1.2.3")
|
|
return commit
|
|
}
|
|
|
|
func cliGitOutput(t *testing.T, repository string, args ...string) string {
|
|
t.Helper()
|
|
commandArgs := append([]string{"git"}, args...)
|
|
command := exec.Command(commandArgs[0], commandArgs[1:]...)
|
|
command.Dir = repository
|
|
output, err := command.Output()
|
|
if err != nil {
|
|
t.Fatalf("%v: %v", commandArgs, err)
|
|
}
|
|
return strings.TrimSpace(string(output))
|
|
}
|
|
|
|
func TestDeliverRejectsReceiptBuiltFromDirtySourceAfterCleanup(t *testing.T) {
|
|
repository := newDeliveryBuildRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
settings := cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
|
|
writeDeliveryProfile(t, repository, settings)
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
mainPath := filepath.Join(repository, "main.go")
|
|
original, err := os.ReadFile(mainPath)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.WriteFile(mainPath, append(original, []byte("\n// uncommitted build\n")...), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
assets := t.TempDir()
|
|
var stdout, stderr bytes.Buffer
|
|
if status := run(context.Background(), []string{"build", "--repo", repository, "--output", assets, "v1.2.3"}, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("local build status=%d %s", status, stderr.String())
|
|
}
|
|
receipt, err := cli.LoadBuildReceipt(filepath.Join(assets, cli.BuildReceiptName))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if receipt.SourceVerified {
|
|
t.Fatal("dirty build receipt claims verified source")
|
|
}
|
|
cliGitRun(t, repository, "restore", "main.go")
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
stdout.Reset()
|
|
stderr.Reset()
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr)
|
|
if status != 1 || state.apiRequests != 0 || !strings.Contains(stderr.String(), "build source") {
|
|
t.Fatalf("status=%d requests=%d %s", status, state.apiRequests, stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestRecoveredPageCreatePreservesPartialDeliveryOnInspectionFailure(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
state := &deliveryServerState{malformedCreate: true, failAssetListing: true}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
|
|
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
assets, _ := writeDeliveryReceipt(t, repository, build)
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr)
|
|
if status != 1 || !strings.Contains(stderr.String(), "state=partial") || state.releaseCreates != 1 || state.assetListReads != 1 || state.assetUploads != 0 {
|
|
t.Fatalf("status=%d state=%#v %s", status, state, stderr.String())
|
|
}
|
|
}
|
|
|
|
func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) {
|
|
for _, existingPage := range []bool{false, true} {
|
|
t.Run(fmt.Sprintf("existing-page-%t", existingPage), func(t *testing.T) {
|
|
repository := newCLIRepository(t)
|
|
state := &deliveryServerState{pageExists: existingPage, noteBody: "# Example Product v1.2.3\n\nRelease note.\n", failAssetDownloads: true, assets: map[string][]byte{"helper-v1.2.3-linux-amd64": []byte("fixture artifact")}}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
|
|
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
assets, _ := writeDeliveryReceipt(t, repository, build)
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr)
|
|
expectedState := "partial"
|
|
expectedCreates := 1
|
|
if existingPage {
|
|
expectedState = "not_attempted"
|
|
expectedCreates = 0
|
|
}
|
|
if status != 1 || !strings.Contains(stderr.String(), "state="+expectedState) || state.assetUploads != 0 || state.releaseCreates != expectedCreates {
|
|
t.Fatalf("status=%d creates=%d uploads=%d %s", status, state.releaseCreates, state.assetUploads, stderr.String())
|
|
}
|
|
if existingPage && !strings.Contains(stderr.String(), "recovery: fix the local or preflight failure") {
|
|
t.Fatalf("not-attempted recovery guidance missing: %s", stderr.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestFailedRebuildInvalidatesPreviousReceipt(t *testing.T) {
|
|
const source = `package main
|
|
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo")
|
|
func main(){fmt.Println("fixture",buildinfo.Version())}
|
|
`
|
|
repository := newCLIBuildRepository(t, source)
|
|
var stdout, stderr bytes.Buffer
|
|
args := []string{"build", "--repo", repository, "v1.2.3"}
|
|
for attempt := 0; attempt < 2; attempt++ {
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("build %d: status=%d %s", attempt, status, stderr.String())
|
|
}
|
|
}
|
|
path := filepath.Join(repository, "cmd/app/main.go")
|
|
if err := os.WriteFile(path, []byte("package main\nfunc main(){}\n"), 0600); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if status := run(context.Background(), args, &stdout, &stderr); status != 1 {
|
|
t.Fatalf("failed rebuild status=%d", status)
|
|
}
|
|
if _, err := os.Stat(filepath.Join(repository, "dist", cli.BuildReceiptName)); !os.IsNotExist(err) {
|
|
t.Fatalf("stale receipt remains: %v", err)
|
|
}
|
|
}
|
|
|
|
func TestTemporaryDeliveryBuildDoesNotRequireWritableRepositoryParent(t *testing.T) {
|
|
if runtime.GOOS == "windows" || os.Getuid() == 0 {
|
|
t.Skip("requires ordinary Unix permission checks")
|
|
}
|
|
repository := newDeliveryBuildRepository(t)
|
|
state := &deliveryServerState{}
|
|
server := newDeliveryServer(t, state)
|
|
defer server.Close()
|
|
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
|
|
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD")
|
|
cliGitRun(t, repository, "tag", "v1.2.3")
|
|
parent := t.TempDir()
|
|
moved := filepath.Join(parent, "repo")
|
|
if err := os.Rename(repository, moved); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := os.Chmod(parent, 0555); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
t.Cleanup(func() { os.Chmod(parent, 0700) })
|
|
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
|
var stdout, stderr bytes.Buffer
|
|
if status := run(context.Background(), []string{"deliver", "--repo", moved, "v1.2.3"}, &stdout, &stderr); status != 0 {
|
|
t.Fatalf("status=%d %s", status, stderr.String())
|
|
}
|
|
if status := cliGitOutput(t, moved, "status", "--porcelain"); status != "" {
|
|
t.Fatalf("source dirtied: %s", status)
|
|
}
|
|
}
|