From 39196a525c3aa04d014a17cde9e5e25df289199b Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Fri, 9 Oct 2026 01:40:43 +0000 Subject: [PATCH] Use one delivery asset verification pass --- delivery_test.go | 44 ++++++++++++++++++++++++++++++++ docs/internal/overview.md | 4 +-- docs/roadmap/implementation.md | 19 +++++++++++++- internal/delivery/assets.go | 19 -------------- internal/delivery/assets_test.go | 14 ---------- 5 files changed, 64 insertions(+), 36 deletions(-) diff --git a/delivery_test.go b/delivery_test.go index 16c3df7..9c11450 100644 --- a/delivery_test.go +++ b/delivery_test.go @@ -134,6 +134,50 @@ func TestDeliverPrebuiltArtifactsRemainCallerOwned(t *testing.T) { } } +func TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites(t *testing.T) { + repository := newTaggedDeliveryConsumer(t) + prebuilt, err := releasekit.Build(context.Background(), 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: filepath.Join(t.TempDir(), "caller-owned-assets"), + }) + if err != nil { + t.Fatalf("Build() error = %v", err) + } + contents, err := os.ReadFile(prebuilt.Artifacts[0].Path) + if err != nil { + t.Fatal(err) + } + if len(contents) == 0 { + t.Fatal("built artifact is empty") + } + contents[0] ^= 0xff + if err := os.WriteFile(prebuilt.Artifacts[0].Path, contents, 0o600); err != nil { + t.Fatal(err) + } + + 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), + Prebuilt: &prebuilt, + }) + if err == nil || !strings.Contains(err.Error(), "verify build assets") || result.State != releasekit.DeliveryNotAttempted { + t.Fatalf("Deliver() = %#v, %v; want local asset verification failure", result, err) + } + if remote.requests != 0 { + t.Fatalf("hosted requests = %d, want none before local verification", remote.requests) + } +} + func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) { for _, test := range []struct { name string diff --git a/docs/internal/overview.md b/docs/internal/overview.md index ee14ed1..78a4b0c 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -39,7 +39,7 @@ reports these implemented packages: | `internal/artifact` | Shared build-target normalization, artifact declarations, filename rules, and checksum-manifest naming. | | `internal/build` | Candidate-build coordination, selected Go target compilation, version injection, native smoke check, and artifact digests. | | `internal/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. | -| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, prebuilt artifact validation, manifest assembly, and hosted delivery coordination. | +| `internal/delivery` | Tagged-source and release-note checks, managed builds in disposable storage outside the checkout, structural artifact declaration, manifest assembly, one local digest-and-size preflight before hosted requests, and delivery coordination. | | `internal/git` | Git command boundary, committed-tree reads, lazy local-only batched blob reads, push-destination resolution, exact ref inspection, tagged checkout preparation, and tag operations, including captured-endpoint verification during recovery. | | `internal/gitea` | Gitea API transport, hosted release pages, asset inspection, manifests, and uploads. | | `internal/gotool` | Effective Go configuration discovery, shared native-validation and static-build environments, and network-capable dependency preparation; see [Go command environments](gotool.md). | @@ -87,7 +87,7 @@ not a separate release matrix: | Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestPublishRejectsUnexpectedPushURLBeforeValidation`, `TestPublishComparesExpectedURLWithResolvedInsteadOfDestination`, `TestPublishExpectedPushURLUsesPushURLWhenFetchAndPushDiffer`, `TestCheckIgnoresExpectedPushURL`, `TestResolveUsesPushURLAndQueriesWithoutMutation` | | Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` | | Cross-builds, version smoke checks, empty target sets, and partial outputs | `releasekit.Build`, `internal/build`, `buildinfo` | `TestBuildCreatesSelectedTargetsAndInjectsVersion`, `TestBuildWithNoTargetsDoesNotInvokeCompiler`, `TestBuildReturnsPartialArtifactsAfterTargetFailure` | -| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` | +| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` | | Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` | | Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` | | CLI profiles and prepared Woodpecker sequences | `cmd/releasekit`, `cmd/releasekit/internal/cli`, `examples/woodpecker/` | `TestLoadSettingsRejectsInvalidProfiles`, `TestWoodpeckerTagDeliveryTemplate`, `TestWoodpeckerTagDeliveryUsesTaggedModuleVersion`, `TestWoodpeckerSourceDeliveryTemplateSelfBootstraps`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout` | diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index c8a0506..a237c88 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # Computational Efficiency Implementation Plan -**Status:** Stages 1–7 complete; Stages 8–11 pending. +**Status:** Stages 1–8 complete; Stages 9–11 pending. ## Purpose And Baseline @@ -451,6 +451,23 @@ prebuilt delivery; these are explanatory measurements of the chosen flow. Streaming upload, exact remote comparison, and later upload verification remain intact. Update the inventory's delivery mechanics where needed. +**Completion:** Complete. `declaredBuildAssets` now performs target/name, +filename, regular-file, and stat checks, then uses each recorded SHA-256 to +build expected assets and the manifest. The separate local verification in +`Deliver` remains after build identity checks and before hosted requests; it +checks current bytes and sizes for managed and prebuilt artifacts. A same-size +prebuilt mutation now fails at this delivery preflight with zero hosted +requests. Delivery, Gitea, facade, and CLI tests passed. On the recorded Go/Git +and machine setup, three artifact runs measured declarations at 11.3–15.4 µs +(2,296 B/op) and declarations plus one local verification at 121–136 ms +(about 69.7 KB/op). The Stage 2 comparison was 94.3–95.4 ms and 186.7–188.4 ms +respectively, each including the declaration hash pass; the new second result +therefore removes one 32 MiB file-read/hash pass from this workload. Three +facade runs measured fresh uploads at 485–520 ms and matching retries at +214–311 ms. These complete-workflow timings overlap the recorded baselines +within shared-machine variation and remain dominated by the local HTTP fixture, +upload, and remote comparison work. + ## Stage 9: Transfer Completed Go Output Without Copies **Owners:** [gotool.go](../../internal/gotool/gotool.go), validation/build callers, diff --git a/internal/delivery/assets.go b/internal/delivery/assets.go index 91d201e..d5419ef 100644 --- a/internal/delivery/assets.go +++ b/internal/delivery/assets.go @@ -1,10 +1,7 @@ package delivery import ( - "crypto/sha256" - "encoding/hex" "fmt" - "io" "os" "path/filepath" @@ -53,22 +50,6 @@ func declaredBuildAssets(result internalbuild.Result) ([]gitea.ExpectedAsset, er if !info.Mode().IsRegular() { return nil, fmt.Errorf("build artifact %q is not a regular file", artifact.Filename) } - file, err := os.Open(artifact.Path) - if err != nil { - return nil, fmt.Errorf("open build artifact %q: %w", artifact.Filename, err) - } - hash := sha256.New() - _, copyErr := io.Copy(hash, file) - closeErr := file.Close() - if copyErr != nil { - return nil, fmt.Errorf("hash build artifact %q: %w", artifact.Filename, copyErr) - } - if closeErr != nil { - return nil, fmt.Errorf("close build artifact %q: %w", artifact.Filename, closeErr) - } - if artifact.SHA256 != hex.EncodeToString(hash.Sum(nil)) { - return nil, fmt.Errorf("build artifact %q does not match its declared SHA-256 digest", artifact.Filename) - } assets = append(assets, gitea.ExpectedAsset{ Filename: artifact.Filename, SHA256: artifact.SHA256, diff --git a/internal/delivery/assets_test.go b/internal/delivery/assets_test.go index 96ca04c..83634b0 100644 --- a/internal/delivery/assets_test.go +++ b/internal/delivery/assets_test.go @@ -47,20 +47,6 @@ func TestDeclaredBuildAssetsGenerateManifestFromBuildResult(t *testing.T) { } } -func TestDeclaredBuildAssetsRejectsTamperedArtifact(t *testing.T) { - path := filepath.Join(t.TempDir(), "app-v1.2.3-linux-amd64") - if err := os.WriteFile(path, []byte("changed bytes"), 0o600); err != nil { - t.Fatal(err) - } - _, err := declaredBuildAssets(internalbuild.Result{ - Version: "v1.2.3", BinaryName: "app", Targets: []internalbuild.Target{{OS: "linux", Arch: "amd64"}}, - Artifacts: []internalbuild.Artifact{{Path: path, Filename: filepath.Base(path), Target: internalbuild.Target{OS: "linux", Arch: "amd64"}, SHA256: assetDigest([]byte("original bytes"))}}, - }) - if err == nil || !strings.Contains(err.Error(), "SHA-256") { - t.Fatalf("declaredBuildAssets() error = %v, want digest mismatch", err) - } -} - func TestDeclaredBuildAssetsRejectsArtifactDeclarationMismatch(t *testing.T) { path := filepath.Join(t.TempDir(), "app-v1.2.3-linux-arm64") contents := []byte("built application")