From f837871b773fe4063ca2c450e2b8ad5254609acc Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sat, 10 Oct 2026 03:30:34 +0000 Subject: [PATCH] Map CLI profiles to selected modules --- cmd/releasekit/internal/cli/config.go | 37 +++--- cmd/releasekit/internal/cli/config_test.go | 54 ++++++++ cmd/releasekit/main.go | 86 ++++++++++--- cmd/releasekit/main_test.go | 136 +++++++++++++++++++-- docs/cli.md | 26 +++- docs/config.md | 6 + docs/consumers/delivery.md | 8 +- docs/internal/overview.md | 2 +- docs/roadmap/implementation.md | 11 ++ internal/preparation/preparation.go | 8 +- preparation.go | 26 +++- preparation_test.go | 28 ++++- 12 files changed, 368 insertions(+), 60 deletions(-) diff --git a/cmd/releasekit/internal/cli/config.go b/cmd/releasekit/internal/cli/config.go index 2c8ed6c..506198b 100644 --- a/cmd/releasekit/internal/cli/config.go +++ b/cmd/releasekit/internal/cli/config.go @@ -16,7 +16,7 @@ import ( "gitea.maximumdirect.net/eric/ReleaseKit/internal/process" ) -const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION\n releasekit publish [--repo DIR] [--config FILE] VERSION\n releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSION\n releasekit deliver [--repo DIR] [--config FILE] [--assets DIR] VERSION\n releasekit ci-deliver [--repo DIR] [--config FILE] [--remote NAME] --commit OID VERSION" +const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION\n releasekit publish [--repo DIR] [--config FILE] [--expected-module DIR] VERSION\n releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSION\n releasekit deliver [--repo DIR] [--config FILE] [--assets DIR] VERSION\n releasekit ci-deliver [--repo DIR] [--config FILE] [--remote NAME] --commit OID VERSION" type Command string @@ -31,14 +31,15 @@ const ( // Invocation contains parsed command settings owned by the CLI adapter. type Invocation struct { - Command Command - Repository string - ConfigPath string - OutputDir string - AssetsDir string - Remote string - Commit string - Version string + Command Command + Repository string + ConfigPath string + OutputDir string + AssetsDir string + Remote string + Commit string + Version string + ExpectedModule string } // Parse accepts only commands that have been implemented by the executable. @@ -67,6 +68,10 @@ func Parse(args []string) (Invocation, error) { flags.StringVar(&assetsDir, "assets", "", "verified build assets directory") } var remote *string + expectedModule := "" + if action == PublishAction { + flags.StringVar(&expectedModule, "expected-module", "", "expected module directory from a launcher mapping") + } expectedCommit := "" if action == CIDeliveryAction { remote = flags.String("remote", "origin", "Git remote") @@ -88,7 +93,7 @@ func Parse(args []string) (Invocation, error) { if remote != nil { parsedRemote = *remote } - return Invocation{Command: action, Repository: *repository, ConfigPath: *configPath, OutputDir: outputDir, AssetsDir: assetsDir, Remote: parsedRemote, Commit: expectedCommit, Version: flags.Arg(0)}, nil + return Invocation{Command: action, Repository: *repository, ConfigPath: *configPath, OutputDir: outputDir, AssetsDir: assetsDir, Remote: parsedRemote, Commit: expectedCommit, Version: flags.Arg(0), ExpectedModule: expectedModule}, nil default: return Invocation{}, fmt.Errorf("unknown command %q", args[0]) } @@ -116,6 +121,7 @@ type CheckCommand struct { // validation selections, and check commands read from a releasekit.json profile. type Settings struct { Product string `json:"product"` + ModuleDir string `json:"module_dir,omitempty"` Remote string `json:"remote,omitempty"` ExpectedPushURL string `json:"expected_push_url,omitempty"` Checks []CheckCommand `json:"checks,omitempty"` @@ -126,7 +132,10 @@ type Settings struct { // ValidationSettings selects shared candidate checks in a release profile. type ValidationSettings struct { - Go GoValidationSettings `json:"go,omitempty"` + // SharedFiles selects committed files relative to the repository root. + // Nil uses optional defaults; a non-nil empty slice selects no shared files. + SharedFiles []string `json:"shared_files"` + Go GoValidationSettings `json:"go,omitempty"` // GoFormat checks the formatting of committed regular Go source files. // Omitted or false disables the check. GoFormat bool `json:"go_format,omitempty"` @@ -269,9 +278,9 @@ func LoadSettings(repoDir, configPath string) (Settings, error) { return settings, nil } -// RunChecks runs configured argument vectors in repoDir. Each command receives +// RunChecks runs configured argument vectors in dir. Each command receives // its own environment overlay and inherits the remaining process environment. -func RunChecks(ctx context.Context, repoDir string, checks []CheckCommand, ownedEnv map[string]string, stdout, stderr io.Writer) error { +func RunChecks(ctx context.Context, dir string, checks []CheckCommand, ownedEnv map[string]string, stdout, stderr io.Writer) error { for i, check := range checks { environment := make(map[string]string, len(check.Env)+len(ownedEnv)) for key, value := range check.Env { @@ -282,7 +291,7 @@ func RunChecks(ctx context.Context, repoDir string, checks []CheckCommand, owned } if err := process.Run(ctx, process.Invocation{ Args: check.Args, - Dir: repoDir, + Dir: dir, Env: environment, Stdout: stdout, Stderr: stderr, diff --git a/cmd/releasekit/internal/cli/config_test.go b/cmd/releasekit/internal/cli/config_test.go index 7402adc..f2ea4ca 100644 --- a/cmd/releasekit/internal/cli/config_test.go +++ b/cmd/releasekit/internal/cli/config_test.go @@ -33,6 +33,10 @@ func TestParse(t *testing.T) { if publish.Command != PublishAction || publish.Repository != "candidate" || publish.ConfigPath != "releasekit.json" || publish.Version != "v1.2.4" { t.Fatalf("Parse(publish) = %#v", publish) } + publishMapped, err := Parse([]string{"publish", "--repo", "candidate", "--expected-module", "integrations/ckconfig", "v1.2.4"}) + if err != nil || publishMapped.ExpectedModule != "integrations/ckconfig" { + t.Fatalf("Parse(publish --expected-module) = %#v, error = %v", publishMapped, err) + } build, err := Parse([]string{"build", "--repo", "candidate", "--config", "releasekit.json", "--output", "output with spaces", "v1.2.5"}) if err != nil { t.Fatal(err) @@ -166,6 +170,56 @@ func TestLoadSettingsReadsGoValidation(t *testing.T) { } } +func TestLoadSettingsPreservesSharedFilesSelection(t *testing.T) { + repoDir := t.TempDir() + profileDir := filepath.Join(repoDir, "profiles") + if err := os.MkdirAll(profileDir, 0o700); err != nil { + t.Fatal(err) + } + for _, test := range []struct { + name string + profile string + wantNil bool + wantFile string + }{ + {name: "omitted", profile: `{"product":"Example","module_dir":"integrations/ckconfig","validation":{}}`, wantNil: true}, + {name: "null", profile: `{"product":"Example","module_dir":"integrations/ckconfig","validation":{"shared_files":null}}`, wantNil: true}, + {name: "empty", profile: `{"product":"Example","module_dir":"integrations/ckconfig","validation":{"shared_files":[]}}`}, + {name: "selected", profile: `{"product":"Example","module_dir":"integrations/ckconfig","validation":{"shared_files":["../README.md"]}}`, wantFile: "../README.md"}, + } { + t.Run(test.name, func(t *testing.T) { + path := filepath.Join(profileDir, "release.json") + if err := os.WriteFile(path, []byte(test.profile), 0o600); err != nil { + t.Fatal(err) + } + settings, err := LoadSettings(repoDir, filepath.Join("profiles", "release.json")) + if err != nil { + t.Fatal(err) + } + if settings.ModuleDir != "integrations/ckconfig" { + t.Fatalf("module_dir = %q", settings.ModuleDir) + } + if settings.Validation == nil || (settings.Validation.SharedFiles == nil) != test.wantNil { + t.Fatalf("shared_files = %#v, want nil=%t", settings.Validation, test.wantNil) + } + if test.wantFile != "" && (len(settings.Validation.SharedFiles) != 1 || settings.Validation.SharedFiles[0] != test.wantFile) { + t.Fatalf("shared_files = %#v, want %q", settings.Validation.SharedFiles, test.wantFile) + } + encoded, err := json.Marshal(settings) + if err != nil { + t.Fatal(err) + } + var roundTrip Settings + if err := json.Unmarshal(encoded, &roundTrip); err != nil { + t.Fatal(err) + } + if (roundTrip.Validation.SharedFiles == nil) != test.wantNil { + t.Fatalf("serialized shared_files = %#v, want nil=%t; JSON=%s", roundTrip.Validation.SharedFiles, test.wantNil, encoded) + } + }) + } +} + func TestLoadSettingsPreservesMarkdownObjectSelection(t *testing.T) { repoDir := t.TempDir() profilePath := filepath.Join(repoDir, "releasekit.json") diff --git a/cmd/releasekit/main.go b/cmd/releasekit/main.go index 8742674..d33b5b3 100644 --- a/cmd/releasekit/main.go +++ b/cmd/releasekit/main.go @@ -14,6 +14,7 @@ import ( "gitea.maximumdirect.net/eric/ReleaseKit" "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" + "gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" ) func main() { @@ -42,11 +43,15 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { } request := candidateRequest(invocation, settings, stderr) if invocation.Command == cli.PublishAction { + if err := checkExpectedModule(invocation.Repository, invocation.ExpectedModule, settings.ModuleDir); err != nil { + fmt.Fprintf(stderr, "profile module selection: %v\n", err) + return 2 + } request.ExpectedPushURL = settings.ExpectedPushURL - fmt.Fprintf(stderr, "validating and publishing %s\n", invocation.Version) + fmt.Fprintf(stderr, "validating and publishing module %s version %s\n", selectedModuleLabel(settings.ModuleDir), invocation.Version) result, err := releasekit.Publish(ctx, request) if err != nil { - fmt.Fprintf(stderr, "publication state=%s phase=%s version=%s commit=%s tag-object=%s endpoint=%q: %s\n", result.State, result.Phase, result.Version, result.CandidateCommit, result.TagObject, result.Endpoint, candidateFailureText(err)) + fmt.Fprintf(stderr, "publication state=%s phase=%s module=%s module-dir=%s version=%s tag=%s commit=%s tag-object=%s endpoint=%q: %s\n", result.State, result.Phase, result.ModulePath, result.ModuleDir, result.Version, result.Tag, result.CandidateCommit, result.TagObject, result.Endpoint, candidateFailureText(err)) if errors.Is(err, releasekit.ErrInvalidVersion) { return 2 } @@ -55,18 +60,18 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { } return 1 } - fmt.Fprintf(stdout, "published %s at %s (tag object %s)\n", result.Version, result.CandidateCommit, result.TagObject) + fmt.Fprintf(stdout, "published %s as %s at %s (tag object %s)\n", result.Version, result.Tag, result.CandidateCommit, result.TagObject) return 0 } candidate, err := releasekit.Check(ctx, request) if err != nil { - fmt.Fprintf(stderr, "check failed: %s\n", candidateFailureText(err)) + fmt.Fprintf(stderr, "check failed module=%s module-dir=%s version=%s tag=%s commit=%s: %s\n", candidate.ModulePath, candidate.ModuleDir, candidate.Version, candidate.Tag, candidate.Commit, candidateFailureText(err)) if errors.Is(err, releasekit.ErrInvalidVersion) { return 2 } return 1 } - fmt.Fprintf(stdout, "local candidate %s at %s is valid\n", candidate.Version, candidate.Commit) + fmt.Fprintf(stdout, "local candidate module %s version %s tag %s at %s is valid\n", candidate.ModulePath, candidate.Version, candidate.Tag, candidate.Commit) return 0 case cli.BuildAction: settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath) @@ -80,6 +85,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { } result, err := releasekit.Build(ctx, releasekit.BuildRequest{ RepositoryDir: invocation.Repository, + ModuleDir: settings.ModuleDir, Version: invocation.Version, Spec: *configuredBuildSpec(settings.Build), OutputDir: invocation.OutputDir, @@ -92,7 +98,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { } } if err != nil { - fmt.Fprintf(stderr, "build failed at %s: %v\n", result.CandidateCommit, err) + fmt.Fprintf(stderr, "build failed module=%s module-dir=%s version=%s tag=%s commit=%s: %v\n", result.ModulePath, result.ModuleDir, result.Version, result.Tag, result.CandidateCommit, err) for _, artifact := range result.Artifacts { fmt.Fprintf(stderr, "built %s %s\n", artifact.Filename, artifact.SHA256) } @@ -102,7 +108,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { return 1 } if len(result.Targets) == 0 { - fmt.Fprintf(stdout, "no build targets selected for %s at %s\n", result.Version, result.CandidateCommit) + fmt.Fprintf(stdout, "no build targets selected for module %s version %s tag %s at %s\n", result.ModulePath, result.Version, result.Tag, result.CandidateCommit) return 0 } receipt := receiptFromBuild(result) @@ -120,7 +126,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { fmt.Fprintln(stderr, "build receipt does not match the selected source and build settings") return 1 } - fmt.Fprintf(stdout, "built %d artifacts for %s at %s with %s\n", len(result.Artifacts), result.Version, result.CandidateCommit, result.Compiler) + fmt.Fprintf(stdout, "built %d artifacts for module %s version %s tag %s at %s with %s\n", len(result.Artifacts), result.ModulePath, result.Version, result.Tag, result.CandidateCommit, result.Compiler) for _, artifact := range result.Artifacts { fmt.Fprintf(stdout, "%s %s\n", artifact.Filename, artifact.SHA256) } @@ -137,6 +143,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int { func candidateRequest(invocation cli.Invocation, settings cli.Settings, output io.Writer) releasekit.ReleaseRequest { request := releasekit.ReleaseRequest{ RepositoryDir: invocation.Repository, + ModuleDir: settings.ModuleDir, Product: settings.Product, Version: invocation.Version, Remote: settings.Remote, @@ -147,9 +154,13 @@ func candidateRequest(invocation cli.Invocation, settings cli.Settings, output i if len(settings.Checks) != 0 { checks := settings.Checks request.AdditionalChecks = func(ctx context.Context, candidate releasekit.Candidate) error { - return cli.RunChecks(ctx, candidate.RepositoryDir, checks, map[string]string{ - "RELEASEKIT_VERSION": candidate.Version, - "RELEASEKIT_CANDIDATE": candidate.Commit, + return cli.RunChecks(ctx, candidate.ModuleDir, checks, map[string]string{ + "RELEASEKIT_VERSION": candidate.Version, + "RELEASEKIT_CANDIDATE": candidate.Commit, + "RELEASEKIT_REPOSITORY_DIR": candidate.RepositoryDir, + "RELEASEKIT_MODULE_DIR": candidate.ModuleDir, + "RELEASEKIT_MODULE_PATH": candidate.ModulePath, + "RELEASEKIT_TAG": candidate.Tag, }, output, output) } } @@ -161,7 +172,8 @@ func validationSpec(settings *cli.ValidationSettings) releasekit.ValidationSpec return releasekit.ValidationSpec{} } result := releasekit.ValidationSpec{ - GoFormat: settings.GoFormat, + SharedFiles: copyStringsPreservingEmpty(settings.SharedFiles), + GoFormat: settings.GoFormat, ModuleDistribution: releasekit.ModuleDistributionValidationSpec{ WorkspaceFiles: settings.ModuleDistribution.WorkspaceFiles, LocalReplacements: settings.ModuleDistribution.LocalReplacements, @@ -199,6 +211,38 @@ func candidateFailureText(err error) string { return err.Error() } +func checkExpectedModule(repositoryDir, expectedModule, profileModule string) error { + if expectedModule == "" { + return nil + } + expected, err := releaseidentity.ResolveSelection(repositoryDir, expectedModule) + if err != nil { + return fmt.Errorf("normalize expected module %q: %w", expectedModule, err) + } + selected, err := releaseidentity.ResolveSelection(repositoryDir, profileModule) + if err != nil { + return fmt.Errorf("normalize profile module %q: %w", profileModule, err) + } + if expected.ModuleSubdir != selected.ModuleSubdir { + return fmt.Errorf("expected module %q resolves to %q, but the profile selects %q", expectedModule, expected.ModuleSubdir, selected.ModuleSubdir) + } + return nil +} + +func selectedModuleLabel(moduleDir string) string { + if moduleDir == "" { + return "." + } + return moduleDir +} + +func copyStringsPreservingEmpty(values []string) []string { + if values == nil { + return nil + } + return append([]string{}, values...) +} + func runDelivery(ctx context.Context, invocation cli.Invocation, stdout, stderr io.Writer) int { settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath) if err != nil { @@ -233,7 +277,10 @@ func runCIDelivery(ctx context.Context, invocation cli.Invocation, stdout, stder return status } if settings.Build != nil && len(settings.Build.Targets) != 0 { - if err := releasekit.PrepareBuildDependencies(ctx, invocation.Repository); err != nil { + if err := releasekit.PrepareBuildDependencies(ctx, releasekit.DependencyPreparationRequest{ + RepositoryDir: invocation.Repository, + ModuleDir: settings.ModuleDir, + }); err != nil { fmt.Fprintf(stderr, "prepare build dependencies: %v\n", err) return 1 } @@ -301,6 +348,7 @@ func deliverWithDestination(ctx context.Context, invocation cli.Invocation, sett result, err := releasekit.Deliver(ctx, releasekit.DeliveryRequest{ RepositoryDir: invocation.Repository, + ModuleDir: settings.ModuleDir, Product: settings.Product, Version: invocation.Version, Destination: destination, @@ -309,7 +357,7 @@ func deliverWithDestination(ctx context.Context, invocation cli.Invocation, sett Output: stderr, }) if err != nil { - fmt.Fprintf(stderr, "delivery state=%s version=%s commit=%s url=%s: %v\n", result.State, result.Version, result.CandidateCommit, result.ReleaseURL, err) + fmt.Fprintf(stderr, "delivery state=%s module=%s module-dir=%s version=%s tag=%s commit=%s url=%s: %v\n", result.State, result.ModulePath, result.ModuleDir, result.Version, result.Tag, result.CandidateCommit, result.ReleaseURL, err) writeDeliveryAssets(stderr, result) writeDeliveryRecovery(stderr, result) if errors.Is(err, releasekit.ErrInvalidVersion) { @@ -320,7 +368,7 @@ func deliverWithDestination(ctx context.Context, invocation cli.Invocation, sett } return 1 } - fmt.Fprintf(stdout, "delivered %s at %s (state %s)\n", result.Version, result.CandidateCommit, result.State) + fmt.Fprintf(stdout, "delivered module %s version %s tag %s at %s (state %s)\n", result.ModulePath, result.Version, result.Tag, result.CandidateCommit, result.State) if result.ReleaseURL != "" { fmt.Fprintf(stdout, "release: %s\n", result.ReleaseURL) } @@ -342,13 +390,13 @@ func writeDeliveryAssets(output io.Writer, result releasekit.DeliveryResult) { func writeDeliveryRecovery(output io.Writer, result releasekit.DeliveryResult) { switch result.State { case releasekit.DeliveryNotAttempted: - fmt.Fprintln(output, "recovery: fix the local or preflight failure before retrying; no remote write was attempted") + fmt.Fprintf(output, "recovery: fix the local or preflight failure for module %s version %s tag %s before retrying; no remote write was attempted\n", result.ModulePath, result.Version, result.Tag) case releasekit.DeliveryPartial: - fmt.Fprintln(output, "recovery: fix the reported cause, then retry this version") + fmt.Fprintf(output, "recovery: fix the reported cause, then retry module %s version %s tag %s\n", result.ModulePath, result.Version, result.Tag) case releasekit.DeliveryConflict: - fmt.Fprintln(output, "recovery: investigate conflicting hosted content; use a new version if the source or published contents need correction") + fmt.Fprintf(output, "recovery: investigate conflicting hosted content for module %s version %s tag %s; use a new version if the source or published contents need correction\n", result.ModulePath, result.Version, result.Tag) case releasekit.DeliveryUnknown: - fmt.Fprintln(output, "recovery: resolve the remote state before retrying") + fmt.Fprintf(output, "recovery: resolve the remote state for module %s version %s tag %s before retrying\n", result.ModulePath, result.Version, result.Tag) default: return } diff --git a/cmd/releasekit/main_test.go b/cmd/releasekit/main_test.go index b365256..48d2191 100644 --- a/cmd/releasekit/main_test.go +++ b/cmd/releasekit/main_test.go @@ -51,6 +51,13 @@ func TestUnknownCommand(t *testing.T) { func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) { raceSelected := racePrerequisitesAvailable() repository := newCLIRepository(t) + moduleDir := filepath.Join(repository, "integrations", "ckconfig") + writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/ckconfig\n\ngo 1.25.0\n") + writeRepositoryFile(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nNested release note.\n") + writeRepositoryFile(t, repository, "integrations/ckconfig/fixture_test.go", "package ckconfig\nimport \"testing\"\nfunc TestFixture(t *testing.T) {}\n") + writeRepositoryFile(t, repository, "integrations/ckconfig/race_fixture_test.go", "//go:build race\n\npackage ckconfig\n\nimport (\n\t\"os\"\n\t\"testing\"\n)\n\nfunc TestRaceBuildSelection(t *testing.T) {\n\tmarker := os.Getenv(\"RELEASEKIT_RACE_MARKER\")\n\tif marker == \"\" { return }\n\tif err := os.WriteFile(marker, []byte(\"race enabled\"), 0o600); err != nil { t.Fatal(err) }\n}\n") + cliGitRun(t, repository, "add", "integrations/ckconfig") + cliGitRun(t, repository, "commit", "-m", "add nested module candidate") profilePath := filepath.Join(t.TempDir(), "profile with spaces.json") orderPath := filepath.Join(t.TempDir(), "check-order") t.Setenv("RELEASEKIT_CHECK_ORDER_FILE", orderPath) @@ -70,9 +77,10 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) { } } settings := cli.Settings{ - Product: "Example Product", - Remote: "origin", - Checks: []cli.CheckCommand{check("first"), check("second")}, + Product: "Example Product", + ModuleDir: "integrations/ckconfig", + Remote: "origin", + Checks: []cli.CheckCommand{check("first"), check("second")}, } if raceSelected { settings.Validation = &cli.ValidationSettings{Go: cli.GoValidationSettings{Race: true}} @@ -102,7 +110,10 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) { 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 { + if !strings.Contains(stderr.String(), fmt.Sprintf("%s|%s|%s|example.com/ckconfig|integrations/ckconfig/v1.2.3", moduleDir, repository, moduleDir)) { + t.Fatalf("configured checks did not receive the selected module directory and identity: %q", stderr.String()) + } + if want := fmt.Sprintf("local candidate module example.com/ckconfig version v1.2.3 tag integrations/ckconfig/v1.2.3 at %s is valid\n", commit); stdout.String() != want { t.Fatalf("stdout = %q, want %q", stdout.String(), want) } if raceSelected { @@ -154,6 +165,40 @@ func TestCandidateRequestMapsGoValidation(t *testing.T) { } } +func TestCandidateRequestPreservesModuleAndSharedFileSelection(t *testing.T) { + for _, test := range []struct { + name string + shared []string + wantNil bool + wantValues []string + }{ + {name: "nil", wantNil: true}, + {name: "empty", shared: []string{}}, + {name: "selected", shared: []string{"README.md", "docs/policy.md"}, wantValues: []string{"README.md", "docs/policy.md"}}, + } { + t.Run(test.name, func(t *testing.T) { + request := candidateRequest(cli.Invocation{Repository: "repository", Version: "v1.2.3"}, cli.Settings{ + ModuleDir: "integrations/ckconfig", + Validation: &cli.ValidationSettings{SharedFiles: test.shared}, + }, nil) + if request.ModuleDir != "integrations/ckconfig" { + t.Fatalf("module selection = %q", request.ModuleDir) + } + if (request.Validation.SharedFiles == nil) != test.wantNil { + t.Fatalf("mapped shared_files = %#v, want nil=%t", request.Validation.SharedFiles, test.wantNil) + } + if len(request.Validation.SharedFiles) != len(test.wantValues) { + t.Fatalf("mapped shared_files = %#v, want %#v", request.Validation.SharedFiles, test.wantValues) + } + for i, path := range test.wantValues { + if request.Validation.SharedFiles[i] != path { + t.Fatalf("mapped shared_files = %#v, want %#v", request.Validation.SharedFiles, test.wantValues) + } + } + }) + } +} + func TestValidationSpecPreservesMarkdownObjectSelection(t *testing.T) { for _, test := range []struct { name string @@ -172,6 +217,33 @@ func TestValidationSpecPreservesMarkdownObjectSelection(t *testing.T) { } } +func TestPublishExpectedModuleRejectsMismatchBeforeProductCommands(t *testing.T) { + repository := newCLIRepository(t) + writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/ckconfig\n\ngo 1.25.0\n") + marker := filepath.Join(t.TempDir(), "product-command-ran") + profile, err := json.Marshal(cli.Settings{ + Product: "Example Product", + Checks: []cli.CheckCommand{{Args: []string{"sh", "-c", "touch " + marker}}}, + }) + if err != nil { + t.Fatal(err) + } + writeRepositoryFile(t, repository, "releasekit.json", string(profile)) + cliGitRun(t, repository, "add", "releasekit.json", "integrations/ckconfig/go.mod") + cliGitRun(t, repository, "commit", "-m", "configure root release profile") + var stdout, stderr bytes.Buffer + status := run(context.Background(), []string{"publish", "--repo", repository, "--expected-module", "integrations/ckconfig", "v1.2.3"}, &stdout, &stderr) + if status != 2 || !strings.Contains(stderr.String(), "profile module selection") { + t.Fatalf("publish status=%d stdout=%q stderr=%q, want module-selection error", status, stdout.String(), stderr.String()) + } + if _, err := os.Stat(marker); !os.IsNotExist(err) { + t.Fatalf("product command ran despite module mismatch: stat error=%v", err) + } + if err := exec.Command("git", "-C", repository, "show-ref", "--verify", "refs/tags/v1.2.3").Run(); err == nil { + t.Fatal("module mismatch created a release tag") + } +} + func TestCandidateCommandsRunMarkdownProfile(t *testing.T) { repository := newCLIRepository(t) profile, err := json.Marshal(cli.Settings{ @@ -328,10 +400,10 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG" } 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 { + if want := fmt.Sprintf("published v1.2.3 as 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")) { + if !bytes.Contains(stderr.Bytes(), []byte("validating and publishing module . version 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 { @@ -351,6 +423,47 @@ printf '%s %s\n' "$version" "$candidate" >> "$RELEASEKIT_COMPATIBILITY_LOG" } } +func TestBuildCommandUsesProfileModuleSelection(t *testing.T) { + repository := newCLIRepository(t) + writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/ckconfig\n\ngo 1.25.0\n") + profile, err := json.Marshal(cli.Settings{ + Product: "Example Product", + ModuleDir: "integrations/ckconfig", + Build: &cli.BuildSettings{}, + }) + if err != nil { + t.Fatal(err) + } + writeRepositoryFile(t, repository, "releasekit.json", string(profile)) + var stdout, stderr bytes.Buffer + status := run(context.Background(), []string{"build", "--repo", repository, "v1.2.3"}, &stdout, &stderr) + if status != 0 { + t.Fatalf("build status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) + } + if !strings.Contains(stdout.String(), "no build targets selected for module example.com/ckconfig version v1.2.3 tag integrations/ckconfig/v1.2.3") { + t.Fatalf("build did not use profile module selection: %q", stdout.String()) + } +} + +func TestProfileSharedFilesRequireCommittedFilesWithoutEnablingChecks(t *testing.T) { + repository := newCLIRepository(t) + profile, err := json.Marshal(cli.Settings{ + Product: "Example Product", + Validation: &cli.ValidationSettings{SharedFiles: []string{"docs/shared.md"}}, + }) + 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 an explicit shared file") + var stdout, stderr bytes.Buffer + status := run(context.Background(), []string{"check", "--repo", repository, "v1.2.3"}, &stdout, &stderr) + if status != 1 || !strings.Contains(stderr.String(), "shared-files") || !strings.Contains(stderr.String(), "docs/shared.md") { + t.Fatalf("check status=%d stdout=%q stderr=%q, want missing shared-files failure", status, stdout.String(), stderr.String()) + } +} + func TestBuildCommandWritesValidatedReceiptForConfiguredArtifacts(t *testing.T) { repository := newCLIBuildRepository(t, `package main @@ -440,7 +553,12 @@ func TestCandidateCheckHelper(t *testing.T) { os.Exit(9) } } - fmt.Fprintf(os.Stdout, "%s|%s", os.Getenv("RELEASEKIT_VERSION"), os.Getenv("RELEASEKIT_CANDIDATE")) + workdir, err := os.Getwd() + if err != nil { + fmt.Fprintln(os.Stderr, err) + os.Exit(9) + } + fmt.Fprintf(os.Stdout, "%s|%s|%s|%s|%s|%s|%s", os.Getenv("RELEASEKIT_VERSION"), os.Getenv("RELEASEKIT_CANDIDATE"), workdir, os.Getenv("RELEASEKIT_REPOSITORY_DIR"), os.Getenv("RELEASEKIT_MODULE_DIR"), os.Getenv("RELEASEKIT_MODULE_PATH"), os.Getenv("RELEASEKIT_TAG")) os.Exit(0) } @@ -760,7 +878,7 @@ func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *test { name: "partial", status: 1, - recovery: []string{"recovery: fix the reported cause", "retry this version"}, + recovery: []string{"recovery: fix the reported cause", "retry module example.com/cli-candidate version v1.2.3 tag v1.2.3"}, assetObservation: "pending helper-v1.2.3-linux-amd64", }, { @@ -773,7 +891,7 @@ func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *test { name: "unknown", status: 3, - recovery: []string{"resolve the remote state before retrying"}, + recovery: []string{"recovery: resolve the remote state for module example.com/cli-candidate version v1.2.3 tag v1.2.3"}, assetObservation: "uncertain helper-v1.2.3-linux-amd64", missingObservation: "pending SHA256SUMS", }, diff --git a/docs/cli.md b/docs/cli.md index f005ad5..0adb091 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -23,7 +23,7 @@ go run ./cmd/releasekit check [--repo DIR] [--config FILE] VERSION `VERSION` must use stable `vMAJOR.MINOR.PATCH` syntax. Options must precede the version. The repository defaults to the current directory, and the profile defaults to `/releasekit.json`. A successful command prints the -captured local commit and version. It checks the committed release note and +captured module, version, full tag, and commit. It checks the committed release note and local Git state, runs the standard offline Go test suite (with `-race` when `validation.go.race` is true), and runs a separate `go vet -mod=readonly ./...` pass after successful tests when `validation.go.full_vet` is true. It then runs @@ -31,6 +31,15 @@ selected package compilation and committed Go formatting, followed by configured additional checks in profile order. It does not inspect remote availability or publish a tag. +The profile's optional `module_dir` selects a repository-relative physical +module directory; omission selects the root module. Validation and configured +product checks use that module selection. Additional check commands run with +the selected module as their working directory and receive +`RELEASEKIT_VERSION`, `RELEASEKIT_CANDIDATE`, `RELEASEKIT_REPOSITORY_DIR`, +`RELEASEKIT_MODULE_DIR`, `RELEASEKIT_MODULE_PATH`, and `RELEASEKIT_TAG` in their +environment. These values are added to each child environment without changing +the ReleaseKit process environment. + When `validation.go.compile_targets` is selected, `check` compiles all packages for each configured `GOOS/GOARCH` pair after tests and full vet, using static mode with cgo disabled. This check produces no artifacts and does not execute @@ -63,7 +72,7 @@ Unsupported argument sequences print usage to standard error. Publish a source tag: ```sh -go run ./cmd/releasekit publish [--repo DIR] [--config FILE] VERSION +go run ./cmd/releasekit publish [--repo DIR] [--config FILE] [--expected-module DIR] VERSION ``` `publish` reads the same product, remote, validation, and additional-check @@ -75,8 +84,8 @@ tests. Selected package compilation and committed Go formatting follow. The selected file checks then run in order: workspace files, local replacements, file size, and Markdown. Configured checks run afterward in profile order. It then creates an annotated local tag -and pushes only its exact tag object. It prints the confirmed version, -candidate commit, and tag object +and pushes only its exact full tag object. It prints the confirmed semantic +version, full tag, candidate commit, and tag object on success. Validation progress and failed publication state, including the captured publication endpoint for recovery, are written to standard error. A signal @@ -113,6 +122,11 @@ It reports outside-checkout links to standard error unless boundary enforcement is selected; submodule contents remain out of scope. Markdown failures stop publication before product checks or tag creation. +`--expected-module DIR` optionally checks a launcher's selected physical module +against the profile's `module_dir` before candidate work. Both paths are +normalized relative to the repository; the profile remains the source of module +selection. The root selection is `.`. + The optional profile field `expected_push_url` applies only to `publish`. A nonempty value must exactly match the effective push URL resolved by Git; mismatches fail during preflight before Go validation or tag creation. Git URL @@ -136,8 +150,8 @@ go run ./cmd/releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSIO targets with the requested version injected, checks the native `version` invocation, and writes binaries plus the private `.releasekit-build.json` receipt into `dist` by default. `--output` selects a different directory; -relative paths are resolved from the repository. Selected artifact filenames -are regenerated while unrelated files are preserved. A complete nonempty build +relative paths are resolved from the selected module directory. Selected +artifact filenames are regenerated while unrelated files are preserved. A complete nonempty build writes a receipt only after compilation and smoke checking succeed. On partial failure, standard error lists completed outputs and prior receipts in that output directory are invalidated; no new receipt is written. An diff --git a/docs/config.md b/docs/config.md index abb7ab4..1dc4d12 100644 --- a/docs/config.md +++ b/docs/config.md @@ -9,6 +9,7 @@ The current profile fields are: | Field | Required | Behavior | | --- | --- | --- | | `product` | Yes | Product name used by release operations. | +| `module_dir` | No | Repository-relative physical Go module directory; omission selects `.` (the repository root). | | `remote` | No | Selected Git remote; defaults to `origin`. | | `expected_push_url` | No | Exact effective push URL required by `publish`; ignored by local `check`. | | `checks` | No | Ordered additional child commands, each with an argument vector and optional environment map. | @@ -28,6 +29,11 @@ target requires the package and binary name; each target is written as one `GOOS/GOARCH` pair. An omitted or empty target list means no binary work. The optional `validation` object selects shared checks implemented by ReleaseKit. +`validation.shared_files` is a committed file list resolved from the repository +root. Omission or `null` uses the optional defaults for enabled file checks; +`[]` selects no shared files. Each explicit file must exist in the candidate, +even when no file checks are enabled. Paths are literal and have no glob +semantics. This field does not enable any checks. `validation.go.race` enables the race detector for the single standard Go test suite. `validation.go.full_vet` runs a separate native `go vet` pass after that suite succeeds; ReleaseKit disables test-time vet for that suite to avoid diff --git a/docs/consumers/delivery.md b/docs/consumers/delivery.md index 96b369f..d9502f5 100644 --- a/docs/consumers/delivery.md +++ b/docs/consumers/delivery.md @@ -35,9 +35,11 @@ if err != nil { } ``` -For binary profiles, call `PrepareBuildDependencies(ctx, repositoryDir)` after -checkout preparation and before delivery. It runs `go mod download` with -workspace discovery disabled and the available compatible local Go toolchain. +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. 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`; see the diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 6ece0bf..916a941 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -89,7 +89,7 @@ not a separate release matrix: | Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` | | Cross-builds, version smoke checks, module paths, empty target sets, and partial outputs | `releasekit.Build`, `internal/build`, `internal/artifact`, `internal/tempdir`, `buildinfo` | `TestBuildCreatesSelectedTargetsAndInjectsVersion`, `TestBuildUsesNestedWorkingTreeModuleAndModuleRelativeOutput`, `TestBuildResolvesDirtyWorkingTreeDeclaration`, `TestBuildWithNoTargetsDoesNotInvokeCompiler`, `TestBuildReturnsPartialArtifactsAfterTargetFailure`, `TestNormalizeSettingsKeepsBuildInputsAndNormalizesTargets`, `TestOutsideFallsBackWhenTemporaryDirectoryIsInsideWorktree` | | Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverUsesCommittedModuleIdentityForSeparatePages`, `TestDeliverRejectsPrebuiltArtifactsForAnotherModuleBeforeHostedRequests`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites`, `TestEnsureReleaseEscapesSlashContainingTag` | -| Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` | +| Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesUsesSelectedModuleDirectory`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` | | Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` | | CLI profiles and prepared Woodpecker sequences | `cmd/releasekit`, `cmd/releasekit/internal/cli`, `examples/woodpecker/` | `TestLoadSettingsRejectsInvalidProfiles`, `TestWoodpeckerTagDeliveryTemplate`, `TestWoodpeckerTagDeliveryUsesTaggedModuleVersion`, `TestWoodpeckerSourceDeliveryTemplateSelfBootstraps`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout` | diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 20d5a1b..d0860de 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -541,6 +541,17 @@ Missing explicit files fail without enabling checks. Verify nested failure diagnostics and retain a minimal root profile with no new required fields. Run CLI profile, command, preparation, and compiled API checks. +**Completed:** Profiles now map `module_dir` and `validation.shared_files`, +preserving nil versus explicit empty selections through JSON and facade copies. +Candidate commands run from the selected module with repository, module, path, +version, commit, and full-tag context. `publish --expected-module` compares +normalized profile selection before callbacks, and delivery/build map the profile +module through their facades. Dependency preparation now accepts a +repository/module request and runs in that module. Verified nested CLI checks and +build selection, explicit missing shared files, mismatch rejection before +product commands, module-targeted preparation, `go test ./...`, +`go run ./internal/apicheck`, and documentation whitespace checks. + ## Stage 11: Implement full-tag CI delivery and prepared-checkout verification **Outcome:** The CLI can prepare a complete event tag or reuse a verified diff --git a/internal/preparation/preparation.go b/internal/preparation/preparation.go index f78ceb9..4c5cf81 100644 --- a/internal/preparation/preparation.go +++ b/internal/preparation/preparation.go @@ -35,9 +35,9 @@ func PrepareTaggedCheckout(ctx context.Context, request TaggedCheckoutRequest) e return git.PrepareTaggedCheckout(ctx, request.RepositoryDir, request.Version, remote, request.ExpectedCommit) } -func PrepareBuildDependencies(ctx context.Context, repositoryDir string) error { - if strings.TrimSpace(repositoryDir) == "" { - return errors.New("repository directory is required") +func PrepareBuildDependencies(ctx context.Context, moduleDir string) error { + if strings.TrimSpace(moduleDir) == "" { + return errors.New("module directory is required") } - return gotool.PrepareBuildDependencies(ctx, repositoryDir) + return gotool.PrepareBuildDependencies(ctx, moduleDir) } diff --git a/preparation.go b/preparation.go index 4554f25..a777f50 100644 --- a/preparation.go +++ b/preparation.go @@ -2,8 +2,12 @@ package releasekit import ( "context" + "errors" + "fmt" + "strings" internalpreparation "gitea.maximumdirect.net/eric/ReleaseKit/internal/preparation" + "gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" ) // TaggedCheckoutRequest selects a stable tag and the full Git object ID @@ -29,12 +33,28 @@ func PrepareTaggedCheckout(ctx context.Context, request TaggedCheckoutRequest) e }) } -// PrepareBuildDependencies runs `go mod download` in repositoryDir with +// DependencyPreparationRequest selects the repository module whose dependencies +// should be prepared. +type DependencyPreparationRequest struct { + RepositoryDir string + // ModuleDir selects a repository-relative module directory; empty selects + // the repository root. + ModuleDir string +} + +// PrepareBuildDependencies runs `go mod download` in the selected module with // workspace discovery disabled and the locally available compatible Go // toolchain selected. It may access module networks and updates the Go module // cache using the caller's Go proxy, private-module, credential, and cache // settings. If required checksums are missing, the Go command may update // go.sum. Cancellation and Go command failures are returned to the caller. -func PrepareBuildDependencies(ctx context.Context, repositoryDir string) error { - return internalpreparation.PrepareBuildDependencies(ctx, repositoryDir) +func PrepareBuildDependencies(ctx context.Context, request DependencyPreparationRequest) error { + if strings.TrimSpace(request.RepositoryDir) == "" { + return errors.New("repository directory is required") + } + selection, err := releaseidentity.ResolveSelection(request.RepositoryDir, request.ModuleDir) + if err != nil { + return fmt.Errorf("resolve dependency preparation module: %w", err) + } + return internalpreparation.PrepareBuildDependencies(ctx, selection.ModuleDir) } diff --git a/preparation_test.go b/preparation_test.go index 3694d26..5933f4f 100644 --- a/preparation_test.go +++ b/preparation_test.go @@ -128,11 +128,37 @@ func TestPrepareTaggedCheckoutValidatesInputsBeforeFetching(t *testing.T) { } func TestPrepareBuildDependenciesRequiresRepositoryDirectory(t *testing.T) { - if err := releasekit.PrepareBuildDependencies(context.Background(), " "); err == nil { + if err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{RepositoryDir: " "}); err == nil { t.Fatal("PrepareBuildDependencies() accepted an empty repository directory") } } +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) + } + 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) + if err := releasekit.PrepareBuildDependencies(context.Background(), releasekit.DependencyPreparationRequest{ + RepositoryDir: repository, + ModuleDir: "integrations/ckconfig", + }); err != nil { + t.Fatal(err) + } + got, err := os.ReadFile(marker) + if err != nil || string(got) != moduleDir { + t.Fatalf("go mod download directory = %q, error=%v, want %q", got, err, moduleDir) + } +} + func publishPreparationTag(t *testing.T, repository string) string { t.Helper() remoteDir := t.TempDir()