Regenerate local artifacts and invalidate stale receipts
This commit is contained in:
2
build.go
2
build.go
@@ -54,7 +54,7 @@ type BuildResult struct {
|
||||
}
|
||||
|
||||
// Build compiles the selected targets with Version injected through buildinfo.
|
||||
// It uses the prepared local Go environment, preserves existing output files,
|
||||
// It uses the prepared local Go environment, regenerates selected output files while preserving unrelated files,
|
||||
// and verifies the version command on the native host before succeeding. An
|
||||
// empty target set records source identity without running the Go toolchain.
|
||||
// Errors return partial build facts in the result.
|
||||
|
||||
@@ -152,38 +152,45 @@ func main() {}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildRejectsUnsafeNameAndPreservesExistingOutput(t *testing.T) {
|
||||
repository := newBuildConsumer(t, "package main\nfunc main() {}\n")
|
||||
outputDir := filepath.Join(t.TempDir(), "artifacts")
|
||||
if err := os.MkdirAll(outputDir, 0o755); err != nil {
|
||||
func TestBuildRegeneratesOwnedOutputsAndPreservesUnrelatedFiles(t *testing.T) {
|
||||
const source = `package main
|
||||
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo")
|
||||
func main(){fmt.Println("fixture",buildinfo.Version())}
|
||||
`
|
||||
repository := newBuildConsumer(t, source)
|
||||
outputDir := t.TempDir()
|
||||
unrelated := filepath.Join(outputDir, "keep.txt")
|
||||
if err := os.WriteFile(unrelated, []byte("keep this file"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
ownedPath := filepath.Join(outputDir, "fixture-v1.2.3-linux-amd64")
|
||||
if err := os.WriteFile(ownedPath, []byte("keep this file"), 0o600); err != nil {
|
||||
request := releasekit.BuildRequest{RepositoryDir: repository, Version: "v1.2.3", Spec: releasekit.BuildSpec{MainPackage: "./cmd/app", BinaryName: "fixture", Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}}, OutputDir: outputDir}
|
||||
unsafe := request
|
||||
unsafe.Spec.BinaryName = "../unsafe"
|
||||
if _, err := releasekit.Build(context.Background(), unsafe); err == nil {
|
||||
t.Fatal("unsafe binary name accepted")
|
||||
}
|
||||
first, err := releasekit.Build(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
request := releasekit.BuildRequest{
|
||||
RepositoryDir: repository,
|
||||
Version: "v1.2.3",
|
||||
Spec: releasekit.BuildSpec{
|
||||
MainPackage: "./cmd/app",
|
||||
BinaryName: "fixture",
|
||||
Targets: []releasekit.Target{{OS: "linux", Arch: "amd64"}},
|
||||
},
|
||||
OutputDir: outputDir,
|
||||
repeated, err := releasekit.Build(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
unsafeRequest := request
|
||||
unsafeRequest.Spec.BinaryName = "../unsafe"
|
||||
if result, err := releasekit.Build(context.Background(), unsafeRequest); err == nil || !strings.Contains(err.Error(), "unsafe binary name") {
|
||||
t.Fatalf("Build() result = %#v, error = %v; expected unsafe-name failure", result, err)
|
||||
if repeated.Artifacts[0].SHA256 != first.Artifacts[0].SHA256 {
|
||||
t.Fatal("identical build changed output")
|
||||
}
|
||||
request.Spec.BinaryName = "fixture"
|
||||
if _, err := releasekit.Build(context.Background(), request); err == nil || !strings.Contains(err.Error(), "already exists") {
|
||||
t.Fatalf("Build() error = %v, want existing-output failure", err)
|
||||
writeBuildFixture(t, repository, "cmd/app/main.go", source+"\nfunc init(){if len([]byte(buildinfo.Version()))==0 {panic(\"changed build\")}}\n")
|
||||
changed, err := releasekit.Build(context.Background(), request)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
contents, err := os.ReadFile(ownedPath)
|
||||
if err != nil || string(contents) != "keep this file" {
|
||||
t.Fatalf("caller output = %q, error = %v", contents, err)
|
||||
if changed.Artifacts[0].SHA256 == first.Artifacts[0].SHA256 {
|
||||
t.Fatal("changed source did not regenerate binary")
|
||||
}
|
||||
content, err := os.ReadFile(unrelated)
|
||||
if err != nil || string(content) != "keep this file" {
|
||||
t.Fatalf("unrelated file=%q %v", content, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -266,3 +266,15 @@ func validSHA256(value string) bool {
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
// RemoveBuildReceipt invalidates prior CLI coordination data after a rebuild.
|
||||
func RemoveBuildReceipt(outputDir string) error {
|
||||
err := os.Remove(filepath.Join(outputDir, BuildReceiptName))
|
||||
if errors.Is(err, os.ErrNotExist) {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
return fmt.Errorf("invalidate prior build receipt: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -100,6 +100,12 @@ func run(ctx context.Context, args []string, stdout, stderr io.Writer) int {
|
||||
OutputDir: invocation.OutputDir,
|
||||
Output: stderr,
|
||||
})
|
||||
if result.OutputDir != "" && len(result.Targets) != 0 {
|
||||
if receiptErr := cli.RemoveBuildReceipt(result.OutputDir); receiptErr != nil {
|
||||
fmt.Fprintf(stderr, "%v\n", receiptErr)
|
||||
return 1
|
||||
}
|
||||
}
|
||||
if err != nil {
|
||||
fmt.Fprintf(stderr, "build failed at %s: %v\n", result.CandidateCommit, err)
|
||||
for _, artifact := range result.Artifacts {
|
||||
|
||||
@@ -737,3 +737,28 @@ func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestFailedRebuildInvalidatesPreviousReceipt(t *testing.T) {
|
||||
const source = `package main
|
||||
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo")
|
||||
func main(){fmt.Println("fixture",buildinfo.Version())}
|
||||
`
|
||||
repository := newCLIBuildRepository(t, source)
|
||||
var stdout, stderr bytes.Buffer
|
||||
args := []string{"build", "--repo", repository, "v1.2.3"}
|
||||
for attempt := 0; attempt < 2; attempt++ {
|
||||
if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
|
||||
t.Fatalf("build %d: status=%d %s", attempt, status, stderr.String())
|
||||
}
|
||||
}
|
||||
path := filepath.Join(repository, "cmd/app/main.go")
|
||||
if err := os.WriteFile(path, []byte("package main\nfunc main(){}\n"), 0600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if status := run(context.Background(), args, &stdout, &stderr); status != 1 {
|
||||
t.Fatalf("failed rebuild status=%d", status)
|
||||
}
|
||||
if _, err := os.Stat(filepath.Join(repository, "dist", cli.BuildReceiptName)); !os.IsNotExist(err) {
|
||||
t.Fatalf("stale receipt remains: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -60,10 +60,11 @@ go run ./cmd/releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSIO
|
||||
targets with the requested version injected, checks the native `version`
|
||||
invocation, and writes binaries plus the private `.releasekit-build.json`
|
||||
receipt into `dist` by default. `--output` selects a different directory;
|
||||
relative paths are resolved from the repository. Existing files with a selected
|
||||
artifact name are preserved and cause an error. A complete nonempty build
|
||||
relative paths are resolved from the repository. Selected artifact filenames
|
||||
are regenerated while unrelated files are preserved. A complete nonempty build
|
||||
writes a receipt only after compilation and smoke checking succeed. On partial
|
||||
failure, standard error lists completed outputs and no receipt is written. An
|
||||
failure, standard error lists completed outputs and prior receipts in that
|
||||
output directory are invalidated; no new receipt is written. An
|
||||
empty target list returns the source commit without building or writing a
|
||||
receipt.
|
||||
|
||||
|
||||
@@ -46,9 +46,8 @@ source and the same HEAD before and after compilation; binary delivery requires
|
||||
that fact. This observation is not a source snapshot or attestation guarantee.
|
||||
|
||||
The default output directory is `<repository>/dist`; a relative `OutputDir` is
|
||||
resolved from the repository. Existing files with a selected artifact name are
|
||||
preserved and cause an error. Other files in the output directory are left in
|
||||
place. A failed later build or native version check returns the binaries already
|
||||
resolved from the repository. Selected artifact filenames are regenerated. Other files in the output
|
||||
directory are left in place. A failed later build or native version check returns the binaries already
|
||||
generated in `BuildResult.Artifacts`.
|
||||
|
||||
Builds use the prepared local Go toolchain, readonly module mode, disabled
|
||||
|
||||
@@ -163,6 +163,8 @@ write outcomes. Both affected suites pass.
|
||||
|
||||
### R4 — Medium: Local artifact regeneration is prohibited
|
||||
|
||||
Status: Resolved.
|
||||
|
||||
Category: Inadvertently strengthened prohibition.
|
||||
|
||||
**Evidence:** [Build](../../internal/build/build.go), around line 114, refuses
|
||||
@@ -188,6 +190,12 @@ machinery.
|
||||
a changed local build can regenerate its owned names; unrelated files survive;
|
||||
a partial rebuild does not authorize incomplete assets through a stale receipt.
|
||||
|
||||
**Resolution:** Builds regenerate selected local artifact names and preserve
|
||||
unrelated output files. The CLI invalidates prior receipts once a rebuild has
|
||||
reached its output directory, including failed rebuilds. API regressions cover
|
||||
repeated and changed-source builds; CLI coverage proves a failed smoke check
|
||||
removes the previous receipt. Affected suites pass.
|
||||
|
||||
### R5 — Medium: The root facade still owns release mechanics
|
||||
|
||||
Category: Architectural ownership gap and maintainability.
|
||||
|
||||
@@ -114,14 +114,6 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
||||
if err := os.MkdirAll(result.OutputDir, 0o755); err != nil {
|
||||
return result, fmt.Errorf("create build output directory %q: %w", result.OutputDir, err)
|
||||
}
|
||||
for _, filename := range filenames {
|
||||
path := filepath.Join(result.OutputDir, filename)
|
||||
if _, err := os.Lstat(path); err == nil {
|
||||
return result, fmt.Errorf("build output %q already exists; preserving caller-owned file", path)
|
||||
} else if !errors.Is(err, os.ErrNotExist) {
|
||||
return result, fmt.Errorf("inspect build output %q: %w", path, err)
|
||||
}
|
||||
}
|
||||
|
||||
compiler, err := runGo(ctx, repositoryDir, nil, "version")
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user