Report only fully hashed build artifacts
This commit is contained in:
5
build.go
5
build.go
@@ -145,10 +145,7 @@ func internalBuildSpec(spec *BuildSpec) *internalbuild.Spec {
|
|||||||
func internalBuildResult(result BuildResult) *internalbuild.Result {
|
func internalBuildResult(result BuildResult) *internalbuild.Result {
|
||||||
input := internalBuildSpec(&BuildSpec{MainPackage: result.MainPackage, BinaryName: result.BinaryName, Targets: result.Targets})
|
input := internalBuildSpec(&BuildSpec{MainPackage: result.MainPackage, BinaryName: result.BinaryName, Targets: result.Targets})
|
||||||
settings := artifact.NormalizeSettings(input.MainPackage, input.BinaryName, input.Targets)
|
settings := artifact.NormalizeSettings(input.MainPackage, input.BinaryName, input.Targets)
|
||||||
converted := &internalbuild.Result{RepositoryDir: result.RepositoryDir, ModuleDir: result.ModuleDir, ModulePath: result.ModulePath, Tag: result.Tag, CandidateCommit: result.CandidateCommit, SourceVerified: result.SourceVerified, Settings: settings, Version: result.Version, MainPackage: settings.MainPackage, BinaryName: settings.BinaryName, OutputDir: result.OutputDir, Compiler: result.Compiler}
|
converted := &internalbuild.Result{RepositoryDir: result.RepositoryDir, ModuleDir: result.ModuleDir, ModulePath: result.ModulePath, Tag: result.Tag, CandidateCommit: result.CandidateCommit, SourceVerified: result.SourceVerified, Settings: settings, Version: result.Version, OutputDir: result.OutputDir, Compiler: result.Compiler}
|
||||||
for _, target := range settings.Targets {
|
|
||||||
converted.Targets = append(converted.Targets, internalbuild.Target{OS: target.OS, Arch: target.Arch})
|
|
||||||
}
|
|
||||||
for _, artifact := range result.Artifacts {
|
for _, artifact := range result.Artifacts {
|
||||||
converted.Artifacts = append(converted.Artifacts, internalbuild.Artifact{Path: artifact.Path, Filename: artifact.Filename, Target: internalbuild.Target{OS: artifact.Target.OS, Arch: artifact.Target.Arch}, SHA256: artifact.SHA256})
|
converted.Artifacts = append(converted.Artifacts, internalbuild.Artifact{Path: artifact.Path, Filename: artifact.Filename, Target: internalbuild.Target{OS: artifact.Target.OS, Arch: artifact.Target.Arch}, SHA256: artifact.SHA256})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -521,6 +521,61 @@ func TestBuildReportsMissingMainPackage(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildRetainsOnlyHashedArtifactsAfterReadFailure(t *testing.T) {
|
||||||
|
repository := newBuildConsumer(t, "package main\nfunc main() {}\n")
|
||||||
|
realGo, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
bin := t.TempDir()
|
||||||
|
count := filepath.Join(t.TempDir(), "count")
|
||||||
|
script := `#!/bin/sh
|
||||||
|
if [ "$1" != build ]; then exec "$RELEASEKIT_REAL_GO" "$@"; fi
|
||||||
|
output=
|
||||||
|
previous=
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$previous" = -o ]; then output=$arg; fi
|
||||||
|
previous=$arg
|
||||||
|
done
|
||||||
|
"$RELEASEKIT_REAL_GO" "$@" || exit $?
|
||||||
|
if [ -f "$RELEASEKIT_BUILD_COUNT" ]; then
|
||||||
|
rm "$output"
|
||||||
|
mkdir "$output"
|
||||||
|
else
|
||||||
|
: > "$RELEASEKIT_BUILD_COUNT"
|
||||||
|
fi
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(bin, "go"), []byte(script), 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
t.Setenv("RELEASEKIT_REAL_GO", realGo)
|
||||||
|
t.Setenv("RELEASEKIT_BUILD_COUNT", count)
|
||||||
|
secondArch := "arm64"
|
||||||
|
if runtime.GOARCH == secondArch {
|
||||||
|
secondArch = "amd64"
|
||||||
|
}
|
||||||
|
result, err := releasekit.Build(context.Background(), releasekit.BuildRequest{
|
||||||
|
RepositoryDir: repository, Version: "v1.2.3", OutputDir: t.TempDir(),
|
||||||
|
Spec: releasekit.BuildSpec{MainPackage: "./cmd/app", BinaryName: "fixture", Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}, {OS: runtime.GOOS, Arch: secondArch}}},
|
||||||
|
})
|
||||||
|
var pathErr *os.PathError
|
||||||
|
if !errors.As(err, &pathErr) || !strings.Contains(err.Error(), "hash build output") || len(result.Artifacts) != 1 || len(result.Targets) != 2 || result.MainPackage != "./cmd/app" || result.BinaryName != "fixture" || result.CandidateCommit == "" || result.SourceVerified {
|
||||||
|
t.Fatalf("partial build = %#v, error = %v", result, err)
|
||||||
|
}
|
||||||
|
contents, readErr := os.ReadFile(result.Artifacts[0].Path)
|
||||||
|
if readErr != nil {
|
||||||
|
t.Fatal(readErr)
|
||||||
|
}
|
||||||
|
digest := sha256.Sum256(contents)
|
||||||
|
if result.Artifacts[0].SHA256 != hex.EncodeToString(digest[:]) {
|
||||||
|
t.Fatalf("completed artifact digest = %q", result.Artifacts[0].SHA256)
|
||||||
|
}
|
||||||
|
if info, statErr := os.Stat(pathErr.Path); statErr != nil || !info.IsDir() {
|
||||||
|
t.Fatalf("failed output was not retained: %v", statErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildReportsIncorrectVersionOutputAndRetainsBinary(t *testing.T) {
|
func TestBuildReportsIncorrectVersionOutputAndRetainsBinary(t *testing.T) {
|
||||||
repository := newBuildConsumer(t, `package main
|
repository := newBuildConsumer(t, `package main
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
"crypto/sha256"
|
"crypto/sha256"
|
||||||
"encoding/hex"
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
"io"
|
||||||
"net/http"
|
"net/http"
|
||||||
@@ -588,6 +589,65 @@ func main() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestBuildCommandReportsOnlyHashedArtifactsAfterReadFailure(t *testing.T) {
|
||||||
|
repository := newCLIBuildRepository(t, "package main\nfunc main() {}\n")
|
||||||
|
secondArch := "arm64"
|
||||||
|
if runtime.GOARCH == secondArch {
|
||||||
|
secondArch = "amd64"
|
||||||
|
}
|
||||||
|
settings := cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: "./cmd/app", BinaryName: "fixture", Targets: []string{runtime.GOOS + "/" + runtime.GOARCH, runtime.GOOS + "/" + secondArch}}}
|
||||||
|
profile, err := json.Marshal(settings)
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
writeRepositoryFile(t, repository, "releasekit.json", string(profile))
|
||||||
|
realGo, err := exec.LookPath("go")
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
bin := t.TempDir()
|
||||||
|
script := `#!/bin/sh
|
||||||
|
if [ "$1" != build ]; then exec "$RELEASEKIT_REAL_GO" "$@"; fi
|
||||||
|
output=
|
||||||
|
previous=
|
||||||
|
for arg in "$@"; do
|
||||||
|
if [ "$previous" = -o ]; then output=$arg; fi
|
||||||
|
previous=$arg
|
||||||
|
done
|
||||||
|
"$RELEASEKIT_REAL_GO" "$@" || exit $?
|
||||||
|
if [ -f "$RELEASEKIT_BUILD_COUNT" ]; then
|
||||||
|
rm "$output"
|
||||||
|
mkdir "$output"
|
||||||
|
else
|
||||||
|
: > "$RELEASEKIT_BUILD_COUNT"
|
||||||
|
fi
|
||||||
|
`
|
||||||
|
if err := os.WriteFile(filepath.Join(bin, "go"), []byte(script), 0o700); err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
t.Setenv("PATH", bin+string(os.PathListSeparator)+os.Getenv("PATH"))
|
||||||
|
t.Setenv("RELEASEKIT_REAL_GO", realGo)
|
||||||
|
t.Setenv("RELEASEKIT_BUILD_COUNT", filepath.Join(t.TempDir(), "count"))
|
||||||
|
var stdout, stderr bytes.Buffer
|
||||||
|
status := run(context.Background(), []string{"build", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
|
||||||
|
first := fmt.Sprintf("fixture-v1.2.3-%s-%s", runtime.GOOS, runtime.GOARCH)
|
||||||
|
second := fmt.Sprintf("fixture-v1.2.3-%s-%s", runtime.GOOS, secondArch)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
first, second = first+".exe", second+".exe"
|
||||||
|
}
|
||||||
|
content, err := os.ReadFile(filepath.Join(repository, "dist", first))
|
||||||
|
if err != nil {
|
||||||
|
t.Fatal(err)
|
||||||
|
}
|
||||||
|
digest := sha256.Sum256(content)
|
||||||
|
if status != 1 || stdout.Len() != 0 || !strings.Contains(stderr.String(), fmt.Sprintf("built %s %x", first, digest)) || strings.Contains(stderr.String(), "built "+second) || !strings.Contains(stderr.String(), "hash build output") || !strings.Contains(stderr.String(), second) {
|
||||||
|
t.Fatalf("status = %d, stdout = %q, stderr = %q", status, stdout.String(), stderr.String())
|
||||||
|
}
|
||||||
|
if _, err := os.Stat(filepath.Join(repository, "dist", cli.BuildReceiptName)); !errors.Is(err, os.ErrNotExist) {
|
||||||
|
t.Fatalf("partial build left receipt: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestBuildCommandLeavesNoReceiptAfterSmokeFailure(t *testing.T) {
|
func TestBuildCommandLeavesNoReceiptAfterSmokeFailure(t *testing.T) {
|
||||||
repository := newCLIBuildRepository(t, `package main
|
repository := newCLIBuildRepository(t, `package main
|
||||||
import (
|
import (
|
||||||
|
|||||||
@@ -43,7 +43,7 @@ the agent policy; an intermediate compilation dependency is recorded accurately.
|
|||||||
| 5 | A03, A18: piped lifetime and queued cancellation | Complete — process and Git lifecycle tests passed under the race detector; public preparation cancellation passed. |
|
| 5 | A03, A18: piped lifetime and queued cancellation | Complete — process and Git lifecycle tests passed under the race detector; public preparation cancellation passed. |
|
||||||
| 6 | A04 | Complete — Git display, endpoint change, public mismatch, CLI, publication, and Git failure regressions passed. |
|
| 6 | A04 | Complete — Git display, endpoint change, public mismatch, CLI, publication, and Git failure regressions passed. |
|
||||||
| 7 | A05 | Complete — real-repository inspection failures preserve the local ref, available object ID, and Git cause without a push. |
|
| 7 | A05 | Complete — real-repository inspection failures preserve the local ref, available object ID, and Git cause without a push. |
|
||||||
| 8 | A06, A14 | Pending |
|
| 8 | A06, A14 | Complete — public build and prebuilt-delivery checks and CLI receipt/partial-artifact regressions passed. |
|
||||||
| 9 | A16 | Pending |
|
| 9 | A16 | Pending |
|
||||||
| 10 | A07: allocation and temporary metadata | Pending |
|
| 10 | A07: allocation and temporary metadata | Pending |
|
||||||
| 11 | A07: build and delivery outcomes | Pending |
|
| 11 | A07: build and delivery outcomes | Pending |
|
||||||
|
|||||||
@@ -50,10 +50,7 @@ type Result struct {
|
|||||||
SourceVerified bool
|
SourceVerified bool
|
||||||
Settings artifact.Settings
|
Settings artifact.Settings
|
||||||
Version string
|
Version string
|
||||||
MainPackage string
|
|
||||||
BinaryName string
|
|
||||||
OutputDir string
|
OutputDir string
|
||||||
Targets []Target
|
|
||||||
Artifacts []Artifact
|
Artifacts []Artifact
|
||||||
Compiler string
|
Compiler string
|
||||||
}
|
}
|
||||||
@@ -68,9 +65,6 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
|||||||
Tag: identity.Tag,
|
Tag: identity.Tag,
|
||||||
Settings: settings,
|
Settings: settings,
|
||||||
Version: identity.Version,
|
Version: identity.Version,
|
||||||
MainPackage: settings.MainPackage,
|
|
||||||
BinaryName: settings.BinaryName,
|
|
||||||
Targets: settings.Targets,
|
|
||||||
}
|
}
|
||||||
if !versioncontract.ValidStableVersion(identity.Version) {
|
if !versioncontract.ValidStableVersion(identity.Version) {
|
||||||
return result, fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, identity.Version)
|
return result, fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, identity.Version)
|
||||||
@@ -78,14 +72,14 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
|||||||
if identity.RepositoryDir == "" || identity.ModuleDir == "" || identity.ModulePath == "" {
|
if identity.RepositoryDir == "" || identity.ModuleDir == "" || identity.ModulePath == "" {
|
||||||
return result, errors.New("build requires a resolved module identity")
|
return result, errors.New("build requires a resolved module identity")
|
||||||
}
|
}
|
||||||
if len(result.Targets) != 0 {
|
if len(settings.Targets) != 0 {
|
||||||
if settings.MainPackage == "" {
|
if settings.MainPackage == "" {
|
||||||
return result, errors.New("build requires a main package when targets are selected")
|
return result, errors.New("build requires a main package when targets are selected")
|
||||||
}
|
}
|
||||||
if !artifact.ValidComponent(settings.BinaryName) {
|
if !artifact.ValidComponent(settings.BinaryName) {
|
||||||
return result, fmt.Errorf("unsafe binary name %q", settings.BinaryName)
|
return result, fmt.Errorf("unsafe binary name %q", settings.BinaryName)
|
||||||
}
|
}
|
||||||
for _, target := range result.Targets {
|
for _, target := range settings.Targets {
|
||||||
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
||||||
return result, fmt.Errorf("unsafe target %q/%q", target.OS, target.Arch)
|
return result, fmt.Errorf("unsafe target %q/%q", target.OS, target.Arch)
|
||||||
}
|
}
|
||||||
@@ -114,7 +108,7 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
|||||||
return result, fmt.Errorf("inspect build source: %w", err)
|
return result, fmt.Errorf("inspect build source: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result.Targets) == 0 {
|
if len(settings.Targets) == 0 {
|
||||||
result.OutputDir = artifact.OutputDirectory(moduleDir, request.OutputDir)
|
result.OutputDir = artifact.OutputDirectory(moduleDir, request.OutputDir)
|
||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
@@ -145,7 +139,7 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
|||||||
goHost := goTool.HostTarget()
|
goHost := goTool.HostTarget()
|
||||||
host := Target{OS: goHost.OS, Arch: goHost.Arch}
|
host := Target{OS: goHost.OS, Arch: goHost.Arch}
|
||||||
|
|
||||||
for index, target := range result.Targets {
|
for index, target := range settings.Targets {
|
||||||
if err := ctx.Err(); err != nil {
|
if err := ctx.Err(); err != nil {
|
||||||
return result, fmt.Errorf("build canceled: %w", err)
|
return result, fmt.Errorf("build canceled: %w", err)
|
||||||
}
|
}
|
||||||
@@ -158,10 +152,10 @@ func BuildResolved(ctx context.Context, request Request) (Result, error) {
|
|||||||
return result, fmt.Errorf("build target %s/%s: %w", target.OS, target.Arch, err)
|
return result, fmt.Errorf("build target %s/%s: %w", target.OS, target.Arch, err)
|
||||||
}
|
}
|
||||||
artifact := Artifact{Path: path, Filename: filename, Target: target}
|
artifact := Artifact{Path: path, Filename: filename, Target: target}
|
||||||
result.Artifacts = append(result.Artifacts, artifact)
|
if err := hashArtifact(&artifact); err != nil {
|
||||||
if err := hashArtifact(&result.Artifacts[len(result.Artifacts)-1]); err != nil {
|
|
||||||
return result, fmt.Errorf("hash build output %q: %w", path, err)
|
return result, fmt.Errorf("hash build output %q: %w", path, err)
|
||||||
}
|
}
|
||||||
|
result.Artifacts = append(result.Artifacts, artifact)
|
||||||
}
|
}
|
||||||
|
|
||||||
var smokePath string
|
var smokePath string
|
||||||
|
|||||||
Reference in New Issue
Block a user