Present validation errors consistently in the CLI

This commit is contained in:
2026-10-07 18:36:37 +00:00
parent 8bac90a9a8
commit 1ebd45c975
5 changed files with 95 additions and 23 deletions

View File

@@ -40,28 +40,12 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
fmt.Fprintf(stderr, "load profile: %v\n", err)
return 2
}
request := releasekit.ReleaseRequest{
RepositoryDir: invocation.Repository,
Product: settings.Product,
Version: invocation.Version,
Remote: settings.Remote,
Build: configuredBuildSpec(settings.Build),
Output: stderr,
}
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,
}, stderr, stderr)
}
}
request := candidateRequest(invocation, settings, stderr)
if invocation.Command == cli.PublishAction {
fmt.Fprintf(stderr, "validating and publishing %s\n", 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: %v\n", result.State, result.Phase, result.Version, result.CandidateCommit, result.TagObject, result.Endpoint, err)
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))
if errors.Is(err, releasekit.ErrInvalidVersion) {
return 2
}
@@ -75,7 +59,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
}
candidate, err := releasekit.Check(ctx, request)
if err != nil {
fmt.Fprintf(stderr, "check failed: %v\n", err)
fmt.Fprintf(stderr, "check failed: %s\n", candidateFailureText(err))
if errors.Is(err, releasekit.ErrInvalidVersion) {
return 2
}
@@ -149,6 +133,35 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
return 2
}
func candidateRequest(invocation cli.Invocation, settings cli.Settings, output io.Writer) releasekit.ReleaseRequest {
request := releasekit.ReleaseRequest{
RepositoryDir: invocation.Repository,
Product: settings.Product,
Version: invocation.Version,
Remote: settings.Remote,
Build: configuredBuildSpec(settings.Build),
Output: output,
}
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,
}, output, output)
}
}
return request
}
func candidateFailureText(err error) string {
var validationError *releasekit.ValidationError
if errors.As(err, &validationError) {
return validationError.Error()
}
return err.Error()
}
func runDelivery(ctx context.Context, invocation cli.Invocation, stdout, stderr io.Writer) int {
settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath)
if err != nil {

View File

@@ -97,6 +97,58 @@ func TestCheckCommandBindsConfiguredChecksToCapturedCandidate(t *testing.T) {
}
}
func TestCandidateCommandsPresentValidationDiagnosticsOnce(t *testing.T) {
repository := newCLIRepository(t)
profile, err := json.Marshal(cli.Settings{Product: "Example Product"})
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 candidate commands")
remote := filepath.Join(t.TempDir(), "origin.git")
cliGitRun(t, repository, "init", "--bare", remote)
cliGitRun(t, repository, "remote", "add", "origin", remote)
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main")
goPath, err := exec.LookPath("go")
if err != nil {
t.Fatal(err)
}
goDirectory := t.TempDir()
shim := filepath.Join(goDirectory, "go")
if err := os.WriteFile(shim, []byte("#!/bin/sh\nif [ \"$1\" = test ]; then echo 'fixture Go suite failure' >&2; exit 1; fi\nexec \"$RELEASEKIT_REAL_GO\" \"$@\"\n"), 0o755); err != nil {
t.Fatal(err)
}
t.Setenv("RELEASEKIT_REAL_GO", goPath)
t.Setenv("PATH", goDirectory+string(os.PathListSeparator)+os.Getenv("PATH"))
for _, command := range []struct {
name string
wantStatus int
args []string
}{
{name: "check", wantStatus: 1, args: []string{"check", "--repo", repository, "v1.2.3"}},
{name: "publish", wantStatus: 1, args: []string{"publish", "--repo", repository, "v1.2.3"}},
} {
t.Run(command.name, func(t *testing.T) {
var stdout, stderr bytes.Buffer
if status := run(context.Background(), command.args, &stdout, &stderr); status != command.wantStatus {
t.Fatalf("run() status = %d; stdout=%q stderr=%q", status, stdout.String(), stderr.String())
}
if got := strings.Count(stderr.String(), "go-test validation failed"); got != 1 {
t.Fatalf("validation diagnostic appeared %d times: %q", got, stderr.String())
}
if !strings.Contains(stderr.String(), "fixture Go suite failure") {
t.Fatalf("stderr omitted the validation cause: %q", stderr.String())
}
if command.name == "publish" && !strings.Contains(stderr.String(), "state=not_attempted phase=validation") {
t.Fatalf("publication validation error lost its state: %q", stderr.String())
}
})
}
}
func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) {
repository := newCLIRepository(t)
writeRepositoryFile(t, repository, "docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\n## Compatibility\n\nNo action required.\n")