From 5aba4c6c87e2ac53c36dc34306137e330f68ee0c Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 1 Nov 2026 21:12:05 +0000 Subject: [PATCH] Anchor dependency preparation to checkout root --- docs/consumers/delivery.md | 6 +++- docs/roadmap/implementation.md | 7 ++-- preparation.go | 19 ++++++++--- preparation_test.go | 59 ++++++++++++++++++++++++++++++++++ 4 files changed, 83 insertions(+), 8 deletions(-) diff --git a/docs/consumers/delivery.md b/docs/consumers/delivery.md index 607d09a..88ce19f 100644 --- a/docs/consumers/delivery.md +++ b/docs/consumers/delivery.md @@ -41,7 +41,11 @@ For binary profiles, call `PrepareBuildDependencies` with a `DependencyPreparationRequest` selecting the repository and module after checkout preparation and before delivery. It runs `go mod download` in that module with workspace discovery disabled and the available compatible local Go -toolchain. +toolchain. Inside a Git checkout, `RepositoryDir` can name a subdirectory; +`ModuleDir` is interpreted relative to the containing checkout root, and an +empty selection chooses that root. Previously, a supplied checkout subdirectory +acted as the selection base. Outside a checkout, the supplied directory remains +the base for `ModuleDir`. The operation may use the network and updates the Go module cache while preserving the caller's proxy, private-module, credential, and cache settings. If checksums are missing, Go may update `go.sum`. For binary CI delivery, prepare diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 8d77ebb..5fd2351 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -110,8 +110,7 @@ and CLI publication tests. Use the offline environment specified in Stage 10. **Status:** complete. `go test ./internal/git` passes offline, including real checkout, worktree, alias, standalone, missing-Git, diagnostic, and failure -cases. The public Go-only preparation composition check is deferred to Stage 3, -where preparation adopts discovery. +cases. The public Go-only preparation composition check passed in Stage 3. **Files:** `internal/git/source.go` or a focused companion file, Git tests, and `internal/process/process.go` only if an existing invocation capability proves @@ -172,7 +171,9 @@ checkout, discovery fails with the missing-Git cause before Go runs. ## Stage 3: Apply checkout-root selection to dependency preparation -**Status:** pending. **Depends on:** Stage 2. +**Status:** complete. Public preparation and CLI `TestCIDeliver` checks pass +offline, including checkout-relative, symlink, standalone, and missing-Git +selection. **Depends on:** Stage 2. **Files:** `preparation.go`, `preparation_test.go`, and `docs/consumers/delivery.md`'s preparation guidance. diff --git a/preparation.go b/preparation.go index 5b3639e..9802004 100644 --- a/preparation.go +++ b/preparation.go @@ -6,6 +6,7 @@ import ( "fmt" "strings" + "go.maximumdirect.net/releasekit/internal/git" internalpreparation "go.maximumdirect.net/releasekit/internal/preparation" "go.maximumdirect.net/releasekit/internal/releaseidentity" ) @@ -37,11 +38,13 @@ func PrepareTaggedCheckout(ctx context.Context, request TaggedCheckoutRequest) e } // DependencyPreparationRequest selects the repository module whose dependencies -// should be prepared. +// should be prepared. Within a checkout, RepositoryDir may name any directory +// in it; ModuleDir is resolved from the checkout root. Outside a checkout, +// ModuleDir is resolved from RepositoryDir. type DependencyPreparationRequest struct { RepositoryDir string - // ModuleDir selects a repository-relative module directory; empty selects - // the repository root. + // ModuleDir selects a directory relative to the checkout root or standalone + // RepositoryDir; empty selects that base. ModuleDir string } @@ -55,7 +58,15 @@ func PrepareBuildDependencies(ctx context.Context, request DependencyPreparation if strings.TrimSpace(request.RepositoryDir) == "" { return errors.New("repository directory is required") } - selection, err := releaseidentity.ResolveSelection(request.RepositoryDir, request.ModuleDir) + base := request.RepositoryDir + root, found, err := git.DiscoverRepositoryRoot(ctx, base) + if err != nil { + return fmt.Errorf("discover dependency preparation repository: %w", err) + } + if found { + base = root + } + selection, err := releaseidentity.ResolveSelection(base, request.ModuleDir) if err != nil { return fmt.Errorf("resolve dependency preparation module: %w", err) } diff --git a/preparation_test.go b/preparation_test.go index 0a080cf..ef13ffc 100644 --- a/preparation_test.go +++ b/preparation_test.go @@ -249,6 +249,65 @@ func TestPrepareBuildDependenciesUsesSelectedModuleDirectory(t *testing.T) { } } +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) + } +} + func publishPreparationTag(t *testing.T, repository string) string { t.Helper() return publishPreparationTagNamed(t, repository, "v1.2.3")