Preserve build errors and available failure evidence
This commit is contained in:
30
build.go
30
build.go
@@ -2,12 +2,14 @@ package releasekit
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||
)
|
||||
|
||||
// Target identifies one explicit Go operating-system and architecture pair.
|
||||
@@ -71,22 +73,36 @@ type BuildResult struct {
|
||||
// empty target set records source identity without running the Go toolchain.
|
||||
// Errors return partial build facts in the result.
|
||||
func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
||||
public := BuildResult{Version: request.Version}
|
||||
spec := *internalBuildSpec(&request.Spec)
|
||||
result := internalbuild.Result{Version: request.Version}
|
||||
failure := func(err error) (BuildResult, error) {
|
||||
result.Settings = artifact.NormalizeSettings(spec.MainPackage, spec.BinaryName, spec.Targets)
|
||||
return publicBuildResult(result), err
|
||||
}
|
||||
if !versioncontract.ValidStableVersion(request.Version) {
|
||||
return failure(fmt.Errorf("%w: %q", ErrInvalidVersion, request.Version))
|
||||
}
|
||||
repositoryDir, err := git.RepositoryRoot(ctx, request.RepositoryDir)
|
||||
if err != nil {
|
||||
return public, err
|
||||
return failure(err)
|
||||
}
|
||||
result.RepositoryDir = repositoryDir
|
||||
identity, err := releaseidentity.FromWorkingTree(ctx, repositoryDir, request.ModuleDir, request.Version)
|
||||
result.ModuleDir, result.ModulePath, result.Tag = identity.ModuleDir, identity.ModulePath, identity.Tag
|
||||
if err != nil {
|
||||
return public, err
|
||||
return failure(err)
|
||||
}
|
||||
result, err := internalbuild.BuildResolved(ctx, internalbuild.Request{
|
||||
result, err = internalbuild.BuildResolved(ctx, internalbuild.Request{
|
||||
Identity: identity,
|
||||
Spec: *internalBuildSpec(&request.Spec),
|
||||
Spec: spec,
|
||||
OutputDir: request.OutputDir,
|
||||
Output: request.Output,
|
||||
})
|
||||
public = BuildResult{
|
||||
return publicBuildResult(result), err
|
||||
}
|
||||
|
||||
func publicBuildResult(result internalbuild.Result) BuildResult {
|
||||
public := BuildResult{
|
||||
RepositoryDir: result.RepositoryDir,
|
||||
ModuleDir: result.ModuleDir,
|
||||
ModulePath: result.ModulePath,
|
||||
@@ -112,7 +128,7 @@ func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
||||
SHA256: artifact.SHA256,
|
||||
})
|
||||
}
|
||||
return public, err
|
||||
return public
|
||||
}
|
||||
|
||||
func internalBuildSpec(spec *BuildSpec) *internalbuild.Spec {
|
||||
|
||||
@@ -18,6 +18,29 @@ import (
|
||||
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
|
||||
)
|
||||
|
||||
func TestBuildPreservesInvalidVersionAndDeclarationFailureEvidence(t *testing.T) {
|
||||
spec := releasekit.BuildSpec{MainPackage: "./cmd/app", BinaryName: "widget", Targets: []releasekit.Target{{OS: "linux", Arch: "amd64"}, {OS: "linux", Arch: "amd64"}}}
|
||||
invalid, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: filepath.Join(t.TempDir(), "absent"), Version: "bad-version", Spec: spec,
|
||||
})
|
||||
if !errors.Is(err, releasekit.ErrInvalidVersion) || invalid.Version != "bad-version" || invalid.MainPackage != spec.MainPackage || invalid.BinaryName != spec.BinaryName || len(invalid.Targets) != 1 {
|
||||
t.Fatalf("invalid version result = %#v, error = %v", invalid, err)
|
||||
}
|
||||
repo := newPublishFixture(t)
|
||||
writeFile(t, repo.work, "nested/go.mod", "go 1.26.0\n")
|
||||
failed, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||
RepositoryDir: repo.work, ModuleDir: "nested", Version: "v1.2.3", Spec: spec,
|
||||
})
|
||||
if err == nil || failed.RepositoryDir != repo.work || failed.ModuleDir != filepath.Join(repo.work, "nested") || failed.Version != "v1.2.3" || failed.MainPackage != spec.MainPackage || failed.BinaryName != spec.BinaryName || len(failed.Targets) != 1 || failed.SourceVerified {
|
||||
t.Fatalf("declaration failure = %#v, error = %v", failed, err)
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
if _, err := releasekit.Build(ctx, releasekit.BuildRequest{RepositoryDir: repo.work, Version: "v1.2.3", Spec: spec}); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("canceled build error = %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildCreatesSelectedTargetsAndInjectsVersion(t *testing.T) {
|
||||
repository := newBuildConsumer(t, `package main
|
||||
|
||||
|
||||
@@ -105,6 +105,9 @@ func Parse(args []string) (Invocation, error) {
|
||||
return Invocation{}, errors.New("ci-deliver TAG must end in a stable vMAJOR.MINOR.PATCH version")
|
||||
}
|
||||
}
|
||||
if !versioncontract.ValidStableVersion(version) {
|
||||
return Invocation{}, errors.New("VERSION must be a stable vMAJOR.MINOR.PATCH version")
|
||||
}
|
||||
parsedRemote := ""
|
||||
if remote != nil {
|
||||
parsedRemote = *remote
|
||||
|
||||
@@ -23,3 +23,12 @@ func TestNativeCheckFromChildUsesRootProfileAndSuite(t *testing.T) {
|
||||
t.Fatalf("child invocation error = %v, output = %s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNativeBuildClassifiesInvalidVersionBeforeSourceAccess(t *testing.T) {
|
||||
binary := buildWorkflowCLI(t, cliRepositoryRoot(t))
|
||||
command := exec.Command(binary, "build", "--repo", filepath.Join(t.TempDir(), "absent"), "bad-version")
|
||||
output, err := command.CombinedOutput()
|
||||
if exit, ok := err.(*exec.ExitError); !ok || exit.ExitCode() != 2 || !strings.Contains(string(output), "stable vMAJOR.MINOR.PATCH") {
|
||||
t.Fatalf("invalid build error = %v, output = %s", err, output)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -64,7 +64,14 @@ independent worktree behavior.
|
||||
|
||||
## Issue 2: Preserve standalone Build error classification and partial evidence (P2)
|
||||
|
||||
Status: open. Category: regression and incomplete failure evidence.
|
||||
Status: complete. Category: regression and incomplete failure evidence.
|
||||
|
||||
**Completion:** Standalone builds classify invalid versions before source access
|
||||
and retain resolved selection and normalized build settings on identity failures.
|
||||
Working-tree derivation preserves progressive identity evidence. Native CLI
|
||||
validation reports invalid-version status 2 before repository access. Focused
|
||||
API, native CLI, parser, identity, cancellation, and partial-artifact tests pass
|
||||
offline.
|
||||
|
||||
**Evidence:** [public Build](../../build.go) now calls working-tree identity
|
||||
resolution before the internal builder. Invalid stable versions are returned by
|
||||
|
||||
@@ -99,16 +99,17 @@ func FromCommittedSelection(ctx context.Context, selection Selection, version, c
|
||||
// working tree. The selected module must contain its own go.mod.
|
||||
func FromWorkingTree(ctx context.Context, repositoryDir, moduleDir, version string) (Identity, error) {
|
||||
selection, err := ResolveSelection(repositoryDir, moduleDir)
|
||||
identity := Identity{Selection: selection, Version: version}
|
||||
if err != nil {
|
||||
return Identity{}, err
|
||||
return identity, err
|
||||
}
|
||||
if err := ctx.Err(); err != nil {
|
||||
return Identity{}, fmt.Errorf("resolve working-tree release identity: %w", err)
|
||||
return identity, fmt.Errorf("resolve working-tree release identity: %w", err)
|
||||
}
|
||||
declarationPath := filepath.Join(selection.ModuleDir, "go.mod")
|
||||
declaration, err := os.ReadFile(declarationPath)
|
||||
if err != nil {
|
||||
return Identity{}, fmt.Errorf("read selected module declaration %q: %w", declarationPath, err)
|
||||
return identity, fmt.Errorf("read selected module declaration %q: %w", declarationPath, err)
|
||||
}
|
||||
return Derive(selection, declaration, version)
|
||||
}
|
||||
@@ -116,25 +117,27 @@ func FromWorkingTree(ctx context.Context, repositoryDir, moduleDir, version stri
|
||||
// Derive applies Go module path-major rules and derives the standard full tag
|
||||
// and module-local release-note path from a declaration and selection.
|
||||
func Derive(selection Selection, declaration []byte, version string) (Identity, error) {
|
||||
identity := Identity{Selection: selection, Version: version}
|
||||
if selection.RepositoryDir == "" || selection.ModuleDir == "" || selection.ModuleSubdir == "" {
|
||||
return Identity{}, fmt.Errorf("incomplete module selection")
|
||||
return identity, fmt.Errorf("incomplete module selection")
|
||||
}
|
||||
modulePath := modfile.ModulePath(declaration)
|
||||
if modulePath == "" {
|
||||
return Identity{}, fmt.Errorf("selected module declaration has no valid module directive")
|
||||
return identity, fmt.Errorf("selected module declaration has no valid module directive")
|
||||
}
|
||||
identity.ModulePath = modulePath
|
||||
if err := module.CheckPath(modulePath); err != nil {
|
||||
return Identity{}, fmt.Errorf("invalid module path %q: %w", modulePath, err)
|
||||
return identity, fmt.Errorf("invalid module path %q: %w", modulePath, err)
|
||||
}
|
||||
_, pathMajor, ok := module.SplitPathVersion(modulePath)
|
||||
if !ok {
|
||||
return Identity{}, fmt.Errorf("module path %q has an invalid major-version suffix", modulePath)
|
||||
return identity, fmt.Errorf("module path %q has an invalid major-version suffix", modulePath)
|
||||
}
|
||||
if !versioncontract.ValidStableVersion(version) {
|
||||
return Identity{}, fmt.Errorf("invalid stable release version %q", version)
|
||||
return identity, fmt.Errorf("invalid stable release version %q", version)
|
||||
}
|
||||
if err := module.CheckPathMajor(version, pathMajor); err != nil {
|
||||
return Identity{}, fmt.Errorf("version %q is incompatible with module path %q: %w", version, modulePath, err)
|
||||
return identity, fmt.Errorf("version %q is incompatible with module path %q: %w", version, modulePath, err)
|
||||
}
|
||||
|
||||
tagPrefix := selection.ModuleSubdir
|
||||
|
||||
Reference in New Issue
Block a user