2026-10-06 18:59:29 +00:00
|
|
|
package releasekit_test
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"errors"
|
2026-11-02 11:45:42 +00:00
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2026-10-06 18:59:29 +00:00
|
|
|
"os"
|
|
|
|
|
"os/exec"
|
|
|
|
|
"path/filepath"
|
2026-11-02 03:16:36 +00:00
|
|
|
"strconv"
|
2026-10-10 03:40:10 +00:00
|
|
|
"strings"
|
2026-11-02 11:45:42 +00:00
|
|
|
"sync/atomic"
|
2026-10-06 18:59:29 +00:00
|
|
|
"testing"
|
2026-11-02 03:16:36 +00:00
|
|
|
"time"
|
2026-10-06 18:59:29 +00:00
|
|
|
|
2026-11-01 14:37:41 +00:00
|
|
|
releasekit "go.maximumdirect.net/releasekit"
|
2026-10-06 18:59:29 +00:00
|
|
|
)
|
|
|
|
|
|
2026-11-02 11:45:42 +00:00
|
|
|
func TestPrepareTaggedCheckoutRedactsRemoteCredentials(t *testing.T) {
|
|
|
|
|
t.Setenv("NO_PROXY", "*")
|
|
|
|
|
t.Setenv("no_proxy", "*")
|
|
|
|
|
t.Setenv("GIT_CONFIG_GLOBAL", os.DevNull)
|
|
|
|
|
t.Setenv("GIT_CONFIG_NOSYSTEM", "1")
|
|
|
|
|
t.Setenv("GIT_TERMINAL_PROMPT", "0")
|
|
|
|
|
repo := newPublishFixture(t)
|
|
|
|
|
var authenticated atomic.Bool
|
|
|
|
|
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
user, password, ok := r.BasicAuth()
|
|
|
|
|
if !ok {
|
|
|
|
|
w.Header().Set("WWW-Authenticate", `Basic realm="fixture"`)
|
|
|
|
|
w.WriteHeader(http.StatusUnauthorized)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if user != "review-user" || password != "review-password" {
|
|
|
|
|
t.Error("fetch did not preserve the configured credentials")
|
|
|
|
|
}
|
|
|
|
|
authenticated.Store(true)
|
|
|
|
|
w.WriteHeader(http.StatusServiceUnavailable)
|
|
|
|
|
}))
|
|
|
|
|
defer server.Close()
|
|
|
|
|
remote := strings.Replace(server.URL, "http://", "http://review-user:review-password@", 1) + "/repo.git"
|
|
|
|
|
err := releasekit.PrepareTaggedCheckout(t.Context(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repo.work, Tag: "v1.2.3", Remote: remote, ExpectedCommit: repo.commit,
|
|
|
|
|
})
|
|
|
|
|
var exit *exec.ExitError
|
|
|
|
|
if !errors.As(err, &exit) || !authenticated.Load() || !strings.Contains(err.Error(), "v1.2.3") || !strings.Contains(err.Error(), "repo.git") {
|
|
|
|
|
t.Fatalf("preparation failure = %v, authenticated = %t", err, authenticated.Load())
|
|
|
|
|
}
|
|
|
|
|
if strings.Contains(err.Error(), "review-user") || strings.Contains(err.Error(), "review-password") {
|
|
|
|
|
t.Fatalf("preparation diagnostic exposes credentials: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-02 03:16:36 +00:00
|
|
|
func TestPrepareBuildDependenciesCancellationReleasesInheritedOutput(t *testing.T) {
|
|
|
|
|
directory := t.TempDir()
|
|
|
|
|
ready := filepath.Join(t.TempDir(), "descendant")
|
|
|
|
|
gitPath := filepath.Join(t.TempDir(), "git")
|
|
|
|
|
if err := os.WriteFile(gitPath, []byte("#!/bin/sh\nsleep 60 &\nprintf '%s' \"$!\" > \"$RELEASEKIT_PREPARE_DESCENDANT\"\nwait\n"), 0o700); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Setenv("PATH", filepath.Dir(gitPath)+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
|
t.Setenv("RELEASEKIT_PREPARE_DESCENDANT", ready)
|
|
|
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
|
|
|
defer cancel()
|
|
|
|
|
result := make(chan error, 1)
|
|
|
|
|
go func() {
|
|
|
|
|
result <- releasekit.PrepareBuildDependencies(ctx, releasekit.DependencyPreparationRequest{RepositoryDir: directory})
|
|
|
|
|
}()
|
|
|
|
|
deadline := time.After(10 * time.Second)
|
|
|
|
|
var pid int
|
|
|
|
|
for pid == 0 {
|
|
|
|
|
if data, err := os.ReadFile(ready); err == nil {
|
|
|
|
|
pid, _ = strconv.Atoi(string(data))
|
|
|
|
|
}
|
|
|
|
|
select {
|
|
|
|
|
case <-deadline:
|
|
|
|
|
t.Fatal("preparation descendant did not start")
|
|
|
|
|
default:
|
|
|
|
|
time.Sleep(10 * time.Millisecond)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
t.Cleanup(func() {
|
|
|
|
|
if descendant, err := os.FindProcess(pid); err == nil {
|
|
|
|
|
_ = descendant.Kill()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
cancel()
|
|
|
|
|
select {
|
|
|
|
|
case err := <-result:
|
|
|
|
|
if !errors.Is(err, context.Canceled) {
|
|
|
|
|
t.Fatalf("preparation error = %v, want cancellation", err)
|
|
|
|
|
}
|
|
|
|
|
case <-time.After(10 * time.Second):
|
|
|
|
|
t.Fatal("preparation did not return after cancellation")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 18:59:29 +00:00
|
|
|
func TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches(t *testing.T) {
|
|
|
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
|
|
|
tagCommit := publishPreparationTag(t, repository)
|
|
|
|
|
advancePreparationCheckout(t, repository, "later local commit")
|
|
|
|
|
|
|
|
|
|
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository,
|
2026-10-10 03:40:10 +00:00
|
|
|
Tag: "v1.2.3",
|
2026-10-06 18:59:29 +00:00
|
|
|
ExpectedCommit: tagCommit,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != tagCommit {
|
|
|
|
|
t.Fatalf("prepared HEAD = %s, want %s", head, tagCommit)
|
|
|
|
|
}
|
|
|
|
|
if branch := buildGit(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch != "HEAD" {
|
|
|
|
|
t.Fatalf("prepared checkout branch = %q, want detached HEAD", branch)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 03:40:10 +00:00
|
|
|
func TestPrepareTaggedCheckoutAcceptsNestedFullTag(t *testing.T) {
|
|
|
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
|
|
|
tag := "integrations/ckconfig/v1.2.3"
|
|
|
|
|
tagCommit := publishPreparationTagNamed(t, repository, tag)
|
|
|
|
|
advancePreparationCheckout(t, repository, "later local commit")
|
|
|
|
|
|
|
|
|
|
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository, Tag: tag, ExpectedCommit: tagCommit,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != tagCommit {
|
|
|
|
|
t.Fatalf("prepared HEAD = %s, want %s", head, tagCommit)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 18:59:29 +00:00
|
|
|
func TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout(t *testing.T) {
|
|
|
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
|
|
|
tagCommit := publishPreparationTag(t, repository)
|
|
|
|
|
currentCommit := advancePreparationCheckout(t, repository, "later local commit")
|
|
|
|
|
remote := buildGit(t, repository, "remote", "get-url", "origin")
|
|
|
|
|
buildGit(t, repository, "remote", "add", "candidate", remote)
|
|
|
|
|
|
|
|
|
|
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository,
|
2026-10-10 03:40:10 +00:00
|
|
|
Tag: "v1.2.3",
|
2026-10-06 18:59:29 +00:00
|
|
|
Remote: "candidate",
|
|
|
|
|
ExpectedCommit: currentCommit,
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("PrepareTaggedCheckout() succeeded for a tag that resolves to another commit")
|
|
|
|
|
}
|
|
|
|
|
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
|
|
|
|
|
t.Fatalf("wrong-tag rejection changed HEAD to %s, want %s (tag commit %s)", head, currentCommit, tagCommit)
|
|
|
|
|
}
|
|
|
|
|
if branch := buildGit(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch == "HEAD" {
|
|
|
|
|
t.Fatal("wrong-tag rejection detached the checkout")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPrepareTaggedCheckoutReportsLocalCheckoutConflict(t *testing.T) {
|
|
|
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
|
|
|
tagCommit := publishPreparationTag(t, repository)
|
|
|
|
|
advancePreparationCheckout(t, repository, "later committed content")
|
|
|
|
|
path := filepath.Join(repository, "cmd/app/main.go")
|
|
|
|
|
if err := os.WriteFile(path, []byte("uncommitted conflicting content"), 0o600); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
currentCommit := buildGit(t, repository, "rev-parse", "HEAD")
|
|
|
|
|
|
|
|
|
|
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository,
|
2026-10-10 03:40:10 +00:00
|
|
|
Tag: "v1.2.3",
|
2026-10-06 18:59:29 +00:00
|
|
|
ExpectedCommit: tagCommit,
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("PrepareTaggedCheckout() succeeded despite a conflicting local edit")
|
|
|
|
|
}
|
|
|
|
|
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
|
|
|
|
|
t.Fatalf("conflicting checkout changed HEAD to %s, want %s", head, currentCommit)
|
|
|
|
|
}
|
|
|
|
|
contents, err := os.ReadFile(path)
|
|
|
|
|
if err != nil || string(contents) != "uncommitted conflicting content" {
|
|
|
|
|
t.Fatalf("conflicting local file = %q, %v", contents, err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPrepareTaggedCheckoutReportsConflictingLocalTag(t *testing.T) {
|
|
|
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
|
|
|
publishPreparationTag(t, repository)
|
|
|
|
|
currentCommit := advancePreparationCheckout(t, repository, "later local commit")
|
|
|
|
|
buildGit(t, repository, "tag", "v1.2.3")
|
|
|
|
|
|
|
|
|
|
err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository,
|
2026-10-10 03:40:10 +00:00
|
|
|
Tag: "v1.2.3",
|
2026-10-06 18:59:29 +00:00
|
|
|
ExpectedCommit: buildGit(t, repository, "rev-parse", "HEAD~1"),
|
|
|
|
|
})
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatal("PrepareTaggedCheckout() replaced a conflicting local tag")
|
|
|
|
|
}
|
|
|
|
|
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != currentCommit {
|
|
|
|
|
t.Fatalf("conflicting tag fetch changed HEAD to %s, want %s", head, currentCommit)
|
|
|
|
|
}
|
|
|
|
|
if tagCommit := buildGit(t, repository, "rev-parse", "refs/tags/v1.2.3^{commit}"); tagCommit != currentCommit {
|
|
|
|
|
t.Fatalf("conflicting local tag changed to %s, want %s", tagCommit, currentCommit)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPrepareTaggedCheckoutValidatesInputsBeforeFetching(t *testing.T) {
|
|
|
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
|
|
|
publishPreparationTag(t, repository)
|
|
|
|
|
for _, request := range []releasekit.TaggedCheckoutRequest{
|
2026-10-10 03:40:10 +00:00
|
|
|
{RepositoryDir: repository, Tag: "not-a-version", ExpectedCommit: buildGit(t, repository, "rev-parse", "HEAD")},
|
|
|
|
|
{RepositoryDir: repository, Tag: "v1.2.3"},
|
|
|
|
|
{RepositoryDir: repository, Tag: "v1.2.3", ExpectedCommit: "not-an-object-id"},
|
2026-10-06 18:59:29 +00:00
|
|
|
} {
|
|
|
|
|
err := releasekit.PrepareTaggedCheckout(context.Background(), request)
|
|
|
|
|
if err == nil {
|
|
|
|
|
t.Fatalf("PrepareTaggedCheckout(%#v) succeeded", request)
|
|
|
|
|
}
|
|
|
|
|
if _, err := buildGitOutput(repository, "show-ref", "--verify", "refs/tags/v1.2.3"); err == nil {
|
|
|
|
|
t.Fatal("input validation fetched the tag before rejecting the request")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
2026-10-10 03:40:10 +00:00
|
|
|
RepositoryDir: repository, Tag: "invalid", ExpectedCommit: "bad",
|
2026-10-06 18:59:29 +00:00
|
|
|
}); !errors.Is(err, releasekit.ErrInvalidVersion) {
|
|
|
|
|
t.Fatalf("invalid-version error = %v, want ErrInvalidVersion", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 03:40:10 +00:00
|
|
|
func TestPrepareTaggedCheckoutVerifyOnlyIsReadOnly(t *testing.T) {
|
|
|
|
|
for _, test := range []struct {
|
|
|
|
|
name string
|
|
|
|
|
wrong bool
|
|
|
|
|
mutate func(t *testing.T, repository string)
|
|
|
|
|
}{
|
|
|
|
|
{name: "wrong expected commit", wrong: true},
|
|
|
|
|
{name: "dirty checkout", mutate: func(t *testing.T, repository string) {
|
|
|
|
|
if err := os.WriteFile(filepath.Join(repository, "dirty.txt"), []byte("dirty"), 0o600); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
}},
|
|
|
|
|
{name: "missing tag", mutate: func(t *testing.T, repository string) {
|
|
|
|
|
buildGit(t, repository, "tag", "-d", "v1.2.3")
|
|
|
|
|
}},
|
|
|
|
|
{name: "moved tag", mutate: func(t *testing.T, repository string) {
|
|
|
|
|
advancePreparationCheckout(t, repository, "moved tagged commit")
|
|
|
|
|
buildGit(t, repository, "tag", "--force", "v1.2.3")
|
|
|
|
|
}},
|
|
|
|
|
} {
|
|
|
|
|
t.Run(test.name, func(t *testing.T) {
|
|
|
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
|
|
|
tagCommit := publishPreparationTag(t, repository)
|
|
|
|
|
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository, Tag: "v1.2.3", ExpectedCommit: tagCommit,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if test.mutate != nil {
|
|
|
|
|
test.mutate(t, repository)
|
|
|
|
|
}
|
|
|
|
|
commit := tagCommit
|
|
|
|
|
if test.wrong {
|
|
|
|
|
commit = strings.Repeat("0", len(tagCommit))
|
|
|
|
|
}
|
|
|
|
|
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository, Tag: "v1.2.3", Remote: "missing-remote",
|
|
|
|
|
ExpectedCommit: commit, VerifyOnly: true,
|
|
|
|
|
}); err == nil {
|
|
|
|
|
t.Fatal("VerifyOnly accepted an inconsistent prepared checkout")
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
repository := newTaggedDeliveryConsumer(t)
|
|
|
|
|
tagCommit := publishPreparationTag(t, repository)
|
|
|
|
|
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository, Tag: "v1.2.3", ExpectedCommit: tagCommit,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := releasekit.PrepareTaggedCheckout(context.Background(), releasekit.TaggedCheckoutRequest{
|
|
|
|
|
RepositoryDir: repository, Tag: "v1.2.3", Remote: "missing-remote",
|
|
|
|
|
ExpectedCommit: tagCommit, VerifyOnly: true,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatalf("VerifyOnly rejected a prepared checkout: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if head := buildGit(t, repository, "rev-parse", "HEAD"); head != tagCommit {
|
|
|
|
|
t.Fatalf("VerifyOnly changed HEAD to %s, want %s", head, tagCommit)
|
|
|
|
|
}
|
|
|
|
|
if branch := buildGit(t, repository, "rev-parse", "--abbrev-ref", "HEAD"); branch != "HEAD" {
|
|
|
|
|
t.Fatalf("VerifyOnly changed checkout branch to %q", branch)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 18:59:29 +00:00
|
|
|
func TestPrepareBuildDependenciesRequiresRepositoryDirectory(t *testing.T) {
|
2026-10-10 03:30:34 +00:00
|
|
|
if err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{RepositoryDir: " "}); err == nil {
|
2026-10-06 18:59:29 +00:00
|
|
|
t.Fatal("PrepareBuildDependencies() accepted an empty repository directory")
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-10 03:30:34 +00:00
|
|
|
func TestPrepareBuildDependenciesUsesSelectedModuleDirectory(t *testing.T) {
|
|
|
|
|
repository := t.TempDir()
|
|
|
|
|
moduleDir := filepath.Join(repository, "integrations", "ckconfig")
|
|
|
|
|
if err := os.MkdirAll(moduleDir, 0o700); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2026-10-10 15:13:00 +00:00
|
|
|
if err := os.Mkdir(filepath.Join(moduleDir, "deep"), 0o700); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.Symlink("integrations/ckconfig/deep", filepath.Join(repository, "linked")); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
2026-10-10 03:30:34 +00:00
|
|
|
toolDir := t.TempDir()
|
|
|
|
|
marker := filepath.Join(t.TempDir(), "go-directory")
|
|
|
|
|
tool := filepath.Join(toolDir, "go")
|
|
|
|
|
if err := os.WriteFile(tool, []byte("#!/bin/sh\nprintf '%s' \"$PWD\" > \"$PREPARE_DIR_MARKER\"\n"), 0o700); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Setenv("PATH", toolDir+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
|
t.Setenv("PREPARE_DIR_MARKER", marker)
|
2026-10-10 15:13:00 +00:00
|
|
|
for _, selected := range []string{"integrations/ckconfig", "linked/.."} {
|
|
|
|
|
if err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{
|
|
|
|
|
RepositoryDir: repository,
|
|
|
|
|
ModuleDir: selected,
|
|
|
|
|
}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
got, err := os.ReadFile(marker)
|
|
|
|
|
if err != nil || string(got) != moduleDir {
|
|
|
|
|
t.Fatalf("go mod download directory for %q = %q, error=%v, want %q", selected, got, err, moduleDir)
|
|
|
|
|
}
|
2026-10-10 03:30:34 +00:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-01 21:12:05 +00:00
|
|
|
func TestPrepareBuildDependenciesSelectsFromCheckoutRoot(t *testing.T) {
|
|
|
|
|
repo := t.TempDir()
|
|
|
|
|
buildGit(t, repo, "init")
|
|
|
|
|
nested := filepath.Join(repo, "nested")
|
|
|
|
|
child := filepath.Join(nested, "child")
|
|
|
|
|
if err := os.MkdirAll(child, 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.Symlink("nested/child", filepath.Join(repo, "link")); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
bin := t.TempDir()
|
|
|
|
|
marker := filepath.Join(t.TempDir(), "directory")
|
|
|
|
|
if err := os.WriteFile(filepath.Join(bin, "go"), []byte("#!/bin/sh\nprintf '%s' \"$PWD\" > \"$PREPARE_DIR_MARKER\"\n"), 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
|
t.Setenv("PREPARE_DIR_MARKER", marker)
|
|
|
|
|
for _, tc := range []struct{ dir, module, want string }{
|
|
|
|
|
{repo, "", repo}, {child, "", repo}, {repo, "nested", nested},
|
|
|
|
|
{child, "nested", nested}, {child, "link/..", nested},
|
|
|
|
|
} {
|
|
|
|
|
err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{RepositoryDir: tc.dir, ModuleDir: tc.module})
|
|
|
|
|
got, readErr := os.ReadFile(marker)
|
|
|
|
|
if err != nil || readErr != nil || string(got) != tc.want {
|
|
|
|
|
t.Fatalf("selection %q, %q = %q, error %v, read %v; want %q", tc.dir, tc.module, got, err, readErr, tc.want)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestPrepareBuildDependenciesWithoutGit(t *testing.T) {
|
|
|
|
|
repo := t.TempDir()
|
|
|
|
|
buildGit(t, repo, "init")
|
|
|
|
|
standalone := t.TempDir()
|
|
|
|
|
bin := t.TempDir()
|
|
|
|
|
marker := filepath.Join(t.TempDir(), "go-ran")
|
|
|
|
|
if err := os.WriteFile(filepath.Join(bin, "go"), []byte("#!/bin/sh\nprintf ran > \"$PREPARE_DIR_MARKER\"\n"), 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Setenv("PATH", bin)
|
|
|
|
|
t.Setenv("PREPARE_DIR_MARKER", marker)
|
|
|
|
|
if err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{RepositoryDir: standalone}); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(marker); err != nil {
|
|
|
|
|
t.Fatalf("Go did not run: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.Remove(marker); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{RepositoryDir: repo})
|
|
|
|
|
if !errors.Is(err, exec.ErrNotFound) {
|
|
|
|
|
t.Fatalf("checkout error = %v, want missing Git", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(marker); !os.IsNotExist(err) {
|
|
|
|
|
t.Fatalf("Go ran after discovery failure: %v", err)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-11-01 21:53:40 +00:00
|
|
|
func TestPrepareBuildDependenciesRejectsDamagedCheckout(t *testing.T) {
|
|
|
|
|
repo := t.TempDir()
|
|
|
|
|
child := filepath.Join(repo, "child")
|
|
|
|
|
writeBuildFixture(t, repo, "go.mod", "module example.com/root\n\ngo 1.26.0\n")
|
|
|
|
|
writeBuildFixture(t, repo, "child/go.mod", "module example.com/child\n\ngo 1.26.0\n")
|
|
|
|
|
buildGit(t, repo, "init", "-b", "main")
|
|
|
|
|
buildGit(t, repo, "config", "user.name", "Test")
|
|
|
|
|
buildGit(t, repo, "config", "user.email", "test@example.invalid")
|
|
|
|
|
buildGit(t, repo, "add", ".")
|
|
|
|
|
buildGit(t, repo, "commit", "-m", "create module fixture")
|
|
|
|
|
bin := t.TempDir()
|
|
|
|
|
marker := filepath.Join(t.TempDir(), "go-directory")
|
|
|
|
|
if err := os.WriteFile(filepath.Join(bin, "go"), []byte("#!/bin/sh\nprintf '%s' \"$PWD\" > \"$PREPARE_DIR_MARKER\"\n"), 0o755); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
|
|
|
|
t.Setenv("PREPARE_DIR_MARKER", marker)
|
|
|
|
|
request := releasekit.DependencyPreparationRequest{RepositoryDir: child}
|
|
|
|
|
if err := releasekit.PrepareBuildDependencies(t.Context(), request); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if got, err := os.ReadFile(marker); err != nil || string(got) != repo {
|
|
|
|
|
t.Fatalf("healthy checkout selected %q, error %v; want %q", got, err, repo)
|
|
|
|
|
}
|
|
|
|
|
if err := os.Remove(marker); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
if err := os.Rename(filepath.Join(repo, ".git", "HEAD"), filepath.Join(t.TempDir(), "saved-head")); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
err := releasekit.PrepareBuildDependencies(t.Context(), request)
|
|
|
|
|
var exitErr *exec.ExitError
|
|
|
|
|
if !errors.As(err, &exitErr) || exitErr.ExitCode() != 128 {
|
|
|
|
|
t.Fatalf("damaged checkout error = %v; want original Git failure", err)
|
|
|
|
|
}
|
|
|
|
|
if _, err := os.Stat(marker); !os.IsNotExist(err) {
|
|
|
|
|
t.Fatalf("Go ran after damaged checkout discovery: %v", err)
|
|
|
|
|
}
|
|
|
|
|
request.RepositoryDir = t.TempDir()
|
|
|
|
|
if err := releasekit.PrepareBuildDependencies(t.Context(), request); err != nil {
|
|
|
|
|
t.Fatalf("standalone preparation: %v", err)
|
|
|
|
|
}
|
|
|
|
|
if got, err := os.ReadFile(marker); err != nil || string(got) != request.RepositoryDir {
|
|
|
|
|
t.Fatalf("standalone selected %q, error %v; want %q", got, err, request.RepositoryDir)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-10-06 18:59:29 +00:00
|
|
|
func publishPreparationTag(t *testing.T, repository string) string {
|
|
|
|
|
t.Helper()
|
2026-10-10 03:40:10 +00:00
|
|
|
return publishPreparationTagNamed(t, repository, "v1.2.3")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func publishPreparationTagNamed(t *testing.T, repository, tag string) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
ref := "refs/tags/" + tag
|
|
|
|
|
if _, err := buildGitOutput(repository, "show-ref", "--verify", "--quiet", ref); err != nil {
|
|
|
|
|
buildGit(t, repository, "tag", tag)
|
|
|
|
|
}
|
2026-10-06 18:59:29 +00:00
|
|
|
remoteDir := t.TempDir()
|
|
|
|
|
remote := filepath.Join(remoteDir, "origin.git")
|
|
|
|
|
buildGit(t, remoteDir, "init", "--bare", remote)
|
|
|
|
|
buildGit(t, repository, "remote", "add", "origin", remote)
|
2026-10-10 03:40:10 +00:00
|
|
|
commit := buildGit(t, repository, "rev-parse", ref+"^{commit}")
|
|
|
|
|
buildGit(t, repository, "push", "origin", ref)
|
|
|
|
|
buildGit(t, repository, "tag", "-d", tag)
|
2026-10-06 18:59:29 +00:00
|
|
|
return commit
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func advancePreparationCheckout(t *testing.T, repository, source string) string {
|
|
|
|
|
t.Helper()
|
|
|
|
|
path := filepath.Join(repository, "cmd/app/main.go")
|
|
|
|
|
if err := os.WriteFile(path, []byte("package main\n// "+source+"\nfunc main() {}\n"), 0o600); err != nil {
|
|
|
|
|
t.Fatal(err)
|
|
|
|
|
}
|
|
|
|
|
buildGit(t, repository, "add", "cmd/app/main.go")
|
|
|
|
|
buildGit(t, repository, "commit", "-m", source)
|
|
|
|
|
return buildGit(t, repository, "rev-parse", "HEAD")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func buildGitOutput(repository string, args ...string) ([]byte, error) {
|
|
|
|
|
command := exec.Command("git", args...)
|
|
|
|
|
command.Dir = repository
|
|
|
|
|
return command.Output()
|
|
|
|
|
}
|