Anchor dependency preparation to checkout root

This commit is contained in:
2026-11-01 21:12:05 +00:00
parent b47202a2e9
commit 5aba4c6c87
4 changed files with 83 additions and 8 deletions

View File

@@ -41,7 +41,11 @@ For binary profiles, call `PrepareBuildDependencies` with a
`DependencyPreparationRequest` selecting the repository and module after `DependencyPreparationRequest` selecting the repository and module after
checkout preparation and before delivery. It runs `go mod download` in that checkout preparation and before delivery. It runs `go mod download` in that
module with workspace discovery disabled and the available compatible local Go 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 The operation may use the network and updates the Go module cache while
preserving the caller's proxy, private-module, credential, and cache settings. 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 If checksums are missing, Go may update `go.sum`. For binary CI delivery, prepare

View File

@@ -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 **Status:** complete. `go test ./internal/git` passes offline, including real
checkout, worktree, alias, standalone, missing-Git, diagnostic, and failure checkout, worktree, alias, standalone, missing-Git, diagnostic, and failure
cases. The public Go-only preparation composition check is deferred to Stage 3, cases. The public Go-only preparation composition check passed in Stage 3.
where preparation adopts discovery.
**Files:** `internal/git/source.go` or a focused companion file, Git tests, and **Files:** `internal/git/source.go` or a focused companion file, Git tests, and
`internal/process/process.go` only if an existing invocation capability proves `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 ## 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 **Files:** `preparation.go`, `preparation_test.go`, and
`docs/consumers/delivery.md`'s preparation guidance. `docs/consumers/delivery.md`'s preparation guidance.

View File

@@ -6,6 +6,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"go.maximumdirect.net/releasekit/internal/git"
internalpreparation "go.maximumdirect.net/releasekit/internal/preparation" internalpreparation "go.maximumdirect.net/releasekit/internal/preparation"
"go.maximumdirect.net/releasekit/internal/releaseidentity" "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 // 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 { type DependencyPreparationRequest struct {
RepositoryDir string RepositoryDir string
// ModuleDir selects a repository-relative module directory; empty selects // ModuleDir selects a directory relative to the checkout root or standalone
// the repository root. // RepositoryDir; empty selects that base.
ModuleDir string ModuleDir string
} }
@@ -55,7 +58,15 @@ func PrepareBuildDependencies(ctx context.Context, request DependencyPreparation
if strings.TrimSpace(request.RepositoryDir) == "" { if strings.TrimSpace(request.RepositoryDir) == "" {
return errors.New("repository directory is required") 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 { if err != nil {
return fmt.Errorf("resolve dependency preparation module: %w", err) return fmt.Errorf("resolve dependency preparation module: %w", err)
} }

View File

@@ -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 { func publishPreparationTag(t *testing.T, repository string) string {
t.Helper() t.Helper()
return publishPreparationTagNamed(t, repository, "v1.2.3") return publishPreparationTagNamed(t, repository, "v1.2.3")