From 94aedacf05b07302b4685dd5ed9abee9e213715b Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Mon, 2 Nov 2026 03:14:01 +0000 Subject: [PATCH] Verify releases use original Git objects --- build_test.go | 35 ++++++++++++++++++++++++ delivery_test.go | 25 +++++++++++++++++ docs/roadmap/implementation.md | 2 +- internal/git/source_test.go | 49 ++++++++++++++++++++++++++++++++++ publish_test.go | 31 +++++++++++++++++++++ 5 files changed, 141 insertions(+), 1 deletion(-) diff --git a/build_test.go b/build_test.go index adf3ca0..1e6df58 100644 --- a/build_test.go +++ b/build_test.go @@ -18,6 +18,41 @@ import ( releasekit "go.maximumdirect.net/releasekit" ) +func TestBuildReplacementDerivedSourceIsNotEligibleForDelivery(t *testing.T) { + repository := newTaggedDeliveryConsumer(t) + original := buildGit(t, repository, "rev-parse", "HEAD") + mainPath := filepath.Join(repository, "cmd/app/main.go") + main, err := os.ReadFile(mainPath) + if err != nil { + t.Fatal(err) + } + writeBuildFixture(t, repository, "cmd/app/main.go", string(main)+"\n// replacement source\n") + buildGit(t, repository, "add", "cmd/app/main.go") + buildGit(t, repository, "commit", "-m", "Replacement source") + replacement := buildGit(t, repository, "rev-parse", "HEAD") + buildGit(t, repository, "reset", "--hard", original) + buildGit(t, repository, "replace", original, replacement) + buildGit(t, repository, "reset", "--hard", original) + 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}}}, + }) + if err != nil || result.SourceVerified || len(result.Artifacts) != 1 || result.CandidateCommit != original { + t.Fatalf("replacement build = %#v, error = %v", result, err) + } + buildGit(t, repository, "replace", "-d", original) + buildGit(t, repository, "reset", "--hard", original) + server, remote := newFacadeDeliveryServer(t, repository) + defer server.Close() + delivery, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{ + RepositoryDir: repository, Product: "Fixture", Version: "v1.2.3", + Destination: facadeDeliveryDestination(server.URL), Prebuilt: &result, + }) + if err == nil || delivery.State != releasekit.DeliveryNotAttempted || remote.snapshot().writes != 0 { + t.Fatalf("delivery of replacement build = %#v, error = %v", delivery, err) + } +} + 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{ diff --git a/delivery_test.go b/delivery_test.go index 287ebee..0a7aec7 100644 --- a/delivery_test.go +++ b/delivery_test.go @@ -402,6 +402,29 @@ func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) { } } +func TestDeliverUsesOriginalNoteWithBlobReplacement(t *testing.T) { + repository := newTaggedDeliveryConsumer(t) + original := buildGit(t, repository, "rev-parse", "HEAD:docs/releases/v1.2.3.md") + replacementPath := filepath.Join(t.TempDir(), "note.md") + if err := os.WriteFile(replacementPath, []byte("# Fixture v1.2.3\n\nReplacement note.\n"), 0o600); err != nil { + t.Fatal(err) + } + replacement := buildGit(t, repository, "hash-object", "-w", replacementPath) + buildGit(t, repository, "replace", original, replacement) + server, remote := newFacadeDeliveryServer(t, repository) + defer server.Close() + result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{ + RepositoryDir: repository, Product: "Fixture", Version: "v1.2.3", + Destination: facadeDeliveryDestination(server.URL), + }) + if err != nil || result.State != releasekit.DeliveryComplete { + t.Fatalf("delivery = %#v, error = %v", result, err) + } + if got := remote.snapshot().createdBody; got != "# Fixture v1.2.3\n\nDelivery fixture.\n" { + t.Fatalf("delivered note = %q", got) + } +} + func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) { repository := newTwoModuleDeliveryConsumer(t) server, remote := newFacadeDeliveryServer(t, repository) @@ -817,6 +840,7 @@ type facadeDeliveryData struct { tag string version string noteBody string + createdBody string pages map[string]bool requests int writes int @@ -876,6 +900,7 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ if payload.Tag != data.tag || payload.Target != data.commit || payload.Name != "Fixture "+data.version { data.t.Errorf("release payload = %#v", payload) } + data.createdBody = payload.Body data.pages[data.tag] = true state.writeRelease(w) case "/api/v1/repos/owner/example/releases/10/assets": diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 23a37a4..b07a1dc 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -38,7 +38,7 @@ the agent policy; an intermediate compilation dependency is recorded accurately. | --- | --- | --- | | 1 | A01; publication assertion portion of A19 | Complete — regression failed before the fix and passed afterward; focused publication and reconciliation tests passed. | | 2 | A02, A12: common Git mechanics | Complete — focused Git, candidate, identity, and public build/publication/delivery tests passed; A02 regression evidence follows in Stage 3. | -| 3 | A02: source identity regression evidence | Pending | +| 3 | A02: source identity regression evidence | Complete — commit, tree, and blob replacement regressions and public publication, build, and delivery cases passed. | | 4 | A03: buffered command teardown | Pending | | 5 | A03, A18: piped lifetime and queued cancellation | Pending | | 6 | A04 | Pending | diff --git a/internal/git/source_test.go b/internal/git/source_test.go index a5b8d1b..a08bd35 100644 --- a/internal/git/source_test.go +++ b/internal/git/source_test.go @@ -142,3 +142,52 @@ func TestInspectTaggedSourceReportsDirtyCheckoutAndReadCommitFile(t *testing.T) t.Fatalf("dirty status = %q", source.Status) } } + +func TestCommittedReadsIgnoreReplacementObjects(t *testing.T) { + for _, kind := range []string{"commit", "tree", "blob"} { + t.Run(kind, func(t *testing.T) { + fixture := newRemoteFixture(t) + original := map[string]string{ + "commit": fixture.commit, + "tree": gitText(t, fixture.work, "rev-parse", "HEAD^{tree}"), + "blob": gitText(t, fixture.work, "rev-parse", "HEAD:source.txt"), + } + writeRepoFile(t, fixture.work, "source.txt", "replacement") + gitRun(t, fixture.work, "add", "source.txt") + gitRun(t, fixture.work, "commit", "-m", "Replacement") + replacement := map[string]string{ + "commit": gitText(t, fixture.work, "rev-parse", "HEAD"), + "tree": gitText(t, fixture.work, "rev-parse", "HEAD^{tree}"), + "blob": gitText(t, fixture.work, "rev-parse", "HEAD:source.txt"), + } + gitRun(t, fixture.work, "reset", "--hard", fixture.commit) + gitRun(t, fixture.work, "replace", original[kind], replacement[kind]) + + content, err := ReadCommitFile(t.Context(), fixture.work, fixture.commit, "source.txt") + if err != nil || string(content) != "candidate" { + t.Fatalf("committed content = %q, error = %v", content, err) + } + entries, err := ReadTree(t.Context(), fixture.work, fixture.commit) + if err != nil { + t.Fatal(err) + } + var object string + for _, entry := range entries { + if entry.Path == "source.txt" { + object = entry.ObjectID + } + } + if object != original["blob"] { + t.Fatalf("tree blob = %q, want %q", object, original["blob"]) + } + reader := NewBlobReader(t.Context(), fixture.work) + content, err = reader.Read(t.Context(), object) + if err != nil || string(content) != "candidate" { + t.Fatalf("batch content = %q, error = %v", content, err) + } + if err := reader.Close(); err != nil { + t.Fatal(err) + } + }) + } +} diff --git a/publish_test.go b/publish_test.go index daf4372..9c7862d 100644 --- a/publish_test.go +++ b/publish_test.go @@ -93,6 +93,7 @@ func TestCandidateOperationsDiscoverGoOnlyWhenLaterWorkNeedsIt(t *testing.T) { func TestPublishPushesOnlyTheCapturedAnnotatedTag(t *testing.T) { repo := newPublishFixture(t) gitRun(t, repo.work, "config", "push.recurseSubmodules", "only") + gitRun(t, repo.work, "replace", repo.unrelatedCommit, repo.commit) suiteRuns := filepath.Join(t.TempDir(), "suite-runs") t.Setenv("RELEASEKIT_FIXTURE_SUITE_RUNS", suiteRuns) refsBefore := gitText(t, repo.bare, "show-ref", "--heads", "--tags") @@ -162,6 +163,36 @@ func TestPublishPushesOnlyTheCapturedAnnotatedTag(t *testing.T) { } } +func TestPublishRejectsReplacementDerivedCheckout(t *testing.T) { + repo := newPublishFixture(t) + writeFile(t, repo.work, "broken.go", "package fixture\nfunc broken(\n") + gitRun(t, repo.work, "add", "broken.go") + gitRun(t, repo.work, "commit", "-m", "Original source does not compile") + original := gitText(t, repo.work, "rev-parse", "HEAD") + gitRun(t, repo.work, "-c", "remote.origin.mirror=false", "push", "origin", "HEAD:refs/heads/main") + writeFile(t, repo.work, "broken.go", "package fixture\nfunc valid() {}\n") + writeFile(t, repo.work, "docs/releases/v1.2.3.md", "# Sample App v1.2.3\n\nReplacement note.\n") + gitRun(t, repo.work, "add", "broken.go", "docs/releases/v1.2.3.md") + gitRun(t, repo.work, "commit", "-m", "Replacement source") + replacement := gitText(t, repo.work, "rev-parse", "HEAD") + gitRun(t, repo.work, "reset", "--hard", original) + gitRun(t, repo.work, "replace", original, replacement) + gitRun(t, repo.work, "reset", "--hard", original) + contents, err := os.ReadFile(filepath.Join(repo.work, "broken.go")) + if err != nil || !strings.Contains(string(contents), "func valid") { + t.Fatalf("replacement checkout = %q, error = %v", contents, err) + } + result, err := releasekit.Publish(context.Background(), releasekit.ReleaseRequest{ + RepositoryDir: repo.work, Product: "Sample App", Version: "v1.2.3", + }) + if err == nil || !strings.Contains(err.Error(), "clean") || result.State != releasekit.PublicationNotAttempted { + t.Fatalf("publication = %#v, error = %v; want dirty source rejection", result, err) + } + if err := exec.Command("git", "-C", repo.bare, "show-ref", "--verify", "--quiet", "refs/tags/v1.2.3").Run(); err == nil { + t.Fatal("replacement-derived source was published") + } +} + func TestPublishUsesSeparateFullTagsForModulesAtOneCommit(t *testing.T) { repo := newPublishFixture(t) firstDir, secondDir := "integrations/first", "integrations/second"