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 (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||||
)
|
)
|
||||||
|
|
||||||
// Target identifies one explicit Go operating-system and architecture pair.
|
// 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.
|
// empty target set records source identity without running the Go toolchain.
|
||||||
// Errors return partial build facts in the result.
|
// Errors return partial build facts in the result.
|
||||||
func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
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)
|
repositoryDir, err := git.RepositoryRoot(ctx, request.RepositoryDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return public, err
|
return failure(err)
|
||||||
}
|
}
|
||||||
|
result.RepositoryDir = repositoryDir
|
||||||
identity, err := releaseidentity.FromWorkingTree(ctx, repositoryDir, request.ModuleDir, request.Version)
|
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 {
|
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,
|
Identity: identity,
|
||||||
Spec: *internalBuildSpec(&request.Spec),
|
Spec: spec,
|
||||||
OutputDir: request.OutputDir,
|
OutputDir: request.OutputDir,
|
||||||
Output: request.Output,
|
Output: request.Output,
|
||||||
})
|
})
|
||||||
public = BuildResult{
|
return publicBuildResult(result), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func publicBuildResult(result internalbuild.Result) BuildResult {
|
||||||
|
public := BuildResult{
|
||||||
RepositoryDir: result.RepositoryDir,
|
RepositoryDir: result.RepositoryDir,
|
||||||
ModuleDir: result.ModuleDir,
|
ModuleDir: result.ModuleDir,
|
||||||
ModulePath: result.ModulePath,
|
ModulePath: result.ModulePath,
|
||||||
@@ -112,7 +128,7 @@ func Build(ctx context.Context, request BuildRequest) (BuildResult, error) {
|
|||||||
SHA256: artifact.SHA256,
|
SHA256: artifact.SHA256,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return public, err
|
return public
|
||||||
}
|
}
|
||||||
|
|
||||||
func internalBuildSpec(spec *BuildSpec) *internalbuild.Spec {
|
func internalBuildSpec(spec *BuildSpec) *internalbuild.Spec {
|
||||||
|
|||||||
@@ -18,6 +18,29 @@ import (
|
|||||||
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
|
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) {
|
func TestBuildCreatesSelectedTargetsAndInjectsVersion(t *testing.T) {
|
||||||
repository := newBuildConsumer(t, `package main
|
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")
|
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 := ""
|
parsedRemote := ""
|
||||||
if remote != nil {
|
if remote != nil {
|
||||||
parsedRemote = *remote
|
parsedRemote = *remote
|
||||||
|
|||||||
@@ -23,3 +23,12 @@ func TestNativeCheckFromChildUsesRootProfileAndSuite(t *testing.T) {
|
|||||||
t.Fatalf("child invocation error = %v, output = %s", err, output)
|
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)
|
## 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
|
**Evidence:** [public Build](../../build.go) now calls working-tree identity
|
||||||
resolution before the internal builder. Invalid stable versions are returned by
|
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.
|
// working tree. The selected module must contain its own go.mod.
|
||||||
func FromWorkingTree(ctx context.Context, repositoryDir, moduleDir, version string) (Identity, error) {
|
func FromWorkingTree(ctx context.Context, repositoryDir, moduleDir, version string) (Identity, error) {
|
||||||
selection, err := ResolveSelection(repositoryDir, moduleDir)
|
selection, err := ResolveSelection(repositoryDir, moduleDir)
|
||||||
|
identity := Identity{Selection: selection, Version: version}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return Identity{}, err
|
return identity, err
|
||||||
}
|
}
|
||||||
if err := ctx.Err(); err != nil {
|
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")
|
declarationPath := filepath.Join(selection.ModuleDir, "go.mod")
|
||||||
declaration, err := os.ReadFile(declarationPath)
|
declaration, err := os.ReadFile(declarationPath)
|
||||||
if err != nil {
|
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)
|
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
|
// Derive applies Go module path-major rules and derives the standard full tag
|
||||||
// and module-local release-note path from a declaration and selection.
|
// and module-local release-note path from a declaration and selection.
|
||||||
func Derive(selection Selection, declaration []byte, version string) (Identity, error) {
|
func Derive(selection Selection, declaration []byte, version string) (Identity, error) {
|
||||||
|
identity := Identity{Selection: selection, Version: version}
|
||||||
if selection.RepositoryDir == "" || selection.ModuleDir == "" || selection.ModuleSubdir == "" {
|
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)
|
modulePath := modfile.ModulePath(declaration)
|
||||||
if modulePath == "" {
|
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 {
|
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)
|
_, pathMajor, ok := module.SplitPathVersion(modulePath)
|
||||||
if !ok {
|
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) {
|
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 {
|
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
|
tagPrefix := selection.ModuleSubdir
|
||||||
|
|||||||
Reference in New Issue
Block a user