Map CLI profiles to selected modules
This commit is contained in:
@@ -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,
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
},
|
||||
|
||||
26
docs/cli.md
26
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 `<repository>/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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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` |
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user