Present validation errors consistently in the CLI
This commit is contained in:
@@ -40,28 +40,12 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
|||||||
fmt.Fprintf(stderr, "load profile: %v\n", err)
|
fmt.Fprintf(stderr, "load profile: %v\n", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
request := releasekit.ReleaseRequest{
|
request := candidateRequest(invocation, settings, stderr)
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if invocation.Command == cli.PublishAction {
|
if invocation.Command == cli.PublishAction {
|
||||||
fmt.Fprintf(stderr, "validating and publishing %s\n", invocation.Version)
|
fmt.Fprintf(stderr, "validating and publishing %s\n", invocation.Version)
|
||||||
result, err := releasekit.Publish(ctx, request)
|
result, err := releasekit.Publish(ctx, request)
|
||||||
if err != nil {
|
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) {
|
if errors.Is(err, releasekit.ErrInvalidVersion) {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
@@ -75,7 +59,7 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
|||||||
}
|
}
|
||||||
candidate, err := releasekit.Check(ctx, request)
|
candidate, err := releasekit.Check(ctx, request)
|
||||||
if err != nil {
|
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) {
|
if errors.Is(err, releasekit.ErrInvalidVersion) {
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
@@ -149,6 +133,35 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
|||||||
return 2
|
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 {
|
func runDelivery(ctx context.Context, invocation cli.Invocation, stdout, stderr io.Writer) int {
|
||||||
settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath)
|
settings, err := cli.LoadSettings(invocation.Repository, invocation.ConfigPath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -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) {
|
func TestPublishCommandPublishesConfiguredCandidate(t *testing.T) {
|
||||||
repository := newCLIRepository(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")
|
writeRepositoryFile(t, repository, "docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\n## Compatibility\n\nNo action required.\n")
|
||||||
|
|||||||
@@ -30,8 +30,9 @@ publish a tag.
|
|||||||
|
|
||||||
`check` writes errors and diagnostics to standard error. It returns status 0
|
`check` writes errors and diagnostics to standard error. It returns status 0
|
||||||
after local validation, 1 after a candidate validation failure, and 2 for usage,
|
after local validation, 1 after a candidate validation failure, and 2 for usage,
|
||||||
profile, or invalid-version errors. Unsupported argument sequences print usage
|
profile, or invalid-version errors. Shared validation failures identify the
|
||||||
to standard error.
|
failed check and retain useful findings and cause context in one diagnostic.
|
||||||
|
Unsupported argument sequences print usage to standard error.
|
||||||
|
|
||||||
Publish a source tag:
|
Publish a source tag:
|
||||||
|
|
||||||
@@ -49,6 +50,8 @@ progress and failed publication state, including the captured publication
|
|||||||
endpoint for recovery, are written to standard error. A signal
|
endpoint for recovery, are written to standard error. A signal
|
||||||
cancels the active operation; if it arrives during a push, the library's
|
cancels the active operation; if it arrives during a push, the library's
|
||||||
bounded reconciliation determines whether the exact tag was accepted.
|
bounded reconciliation determines whether the exact tag was accepted.
|
||||||
|
Shared validation failures use the same check and finding presentation as
|
||||||
|
`check`, alongside publication state and phase.
|
||||||
|
|
||||||
The command returns status 0 for confirmed publication, 1 for a known
|
The command returns status 0 for confirmed publication, 1 for a known
|
||||||
publication failure or conflict, 2 for usage, profile, or invalid-version
|
publication failure or conflict, 2 for usage, profile, or invalid-version
|
||||||
|
|||||||
@@ -76,7 +76,7 @@ not a separate release matrix:
|
|||||||
| Capability | Implementation owner | Existing evidence |
|
| Capability | Implementation owner | Existing evidence |
|
||||||
| --- | --- | --- |
|
| --- | --- | --- |
|
||||||
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
| Runtime version lookup and injected application version | `buildinfo`, `internal/runtimeinfo`, `internal/versioncontract`, `internal/build` | `TestVersionInConsumerExecutable`, `TestVersion` |
|
||||||
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate` |
|
| Candidate capture, shared validation, public diagnostics, additional checks, and rechecks | `releasekit.Check`, `releasekit.Publish`, `internal/candidate`, `internal/validation`, `cmd/releasekit/internal/cli` | `TestCheckCapturesCandidateAndCallsAdditionalChecksOnce`, `TestCheckReturnsPublicValidationError`, `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishFailedStandardSuiteSkipsAdditionalChecksAndTagCreation`, `TestPublishAdditionalCheckFailurePreservesCauseAndPreventsTagCreation`, `TestCheckCommandBindsConfiguredChecksToCapturedCandidate`, `TestPublishCommandPublishesConfiguredCandidate`, `TestCandidateCommandsPresentValidationDiagnosticsOnce` |
|
||||||
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
| Shared Go settings for validation and builds | `internal/gotool`, `internal/process`, `internal/candidate`, `internal/build` | `TestDefaultValidationDoesNotInheritGOFLAGSRunFilter`, `TestNativeAndStaticCommandsUseOwnedBaseline`, `TestPersistentSettingsPreserveCachesAndNativeCTools`, `TestBuildIgnoresAmbientAndPersistentArchitectureTuning` |
|
||||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# Validation model implementation plan
|
# Validation model implementation plan
|
||||||
|
|
||||||
Status: In progress; shared execution, committed-file inspection, destination resolution, and facade diagnostics are complete.
|
Status: In progress; shared execution, candidate inspection, facade diagnostics, and CLI mapping are complete.
|
||||||
|
|
||||||
This plan implements the enabling work in [model.md](model.md), guided by
|
This plan implements the enabling work in [model.md](model.md), guided by
|
||||||
[ADR 0003](../adr/0003-declarative-validation.md). It is written for a Luna
|
[ADR 0003](../adr/0003-declarative-validation.md). It is written for a Luna
|
||||||
@@ -311,6 +311,10 @@ framework. Run `go run ./internal/apicheck`.
|
|||||||
|
|
||||||
## Stage 6: Establish equivalent profile mapping and CLI presentation
|
## Stage 6: Establish equivalent profile mapping and CLI presentation
|
||||||
|
|
||||||
|
**Completion:** Complete. Check and publication share profile conversion, and
|
||||||
|
the CLI presents typed validation errors through one diagnostic path while
|
||||||
|
preserving existing status, publication-state, and phase reporting.
|
||||||
|
|
||||||
**Outcome:** Profile conversion has one owner and the CLI reports structured
|
**Outcome:** Profile conversion has one owner and the CLI reports structured
|
||||||
validation failures consistently while retaining current exit classification.
|
validation failures consistently while retaining current exit classification.
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user