Version and bind module build receipts
This commit is contained in:
@@ -2,7 +2,6 @@ package cli
|
||||
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -12,10 +11,12 @@ import (
|
||||
"strings"
|
||||
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity"
|
||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||
)
|
||||
|
||||
const BuildReceiptName = ".releasekit-build.json"
|
||||
const BuildReceiptSchemaVersion = 1
|
||||
|
||||
type ReceiptTarget struct {
|
||||
OS string `json:"os"`
|
||||
@@ -31,6 +32,7 @@ type ReceiptArtifact struct {
|
||||
|
||||
// BuildReceipt is private local coordination data for generated binaries.
|
||||
type BuildReceipt struct {
|
||||
SchemaVersion int `json:"schema_version"`
|
||||
RepositoryDir string `json:"repository_dir"`
|
||||
ModuleDir string `json:"module_dir"`
|
||||
ModulePath string `json:"module_path"`
|
||||
@@ -52,7 +54,7 @@ func WriteBuildReceipt(outputDir string, receipt BuildReceipt) (string, error) {
|
||||
return "", fmt.Errorf("resolve build receipt directory: %w", err)
|
||||
}
|
||||
receipt.OutputDir = resolvedDir
|
||||
if err := validateBuildReceipt(receipt, true); err != nil {
|
||||
if err := validateBuildReceipt(receipt); err != nil {
|
||||
return "", err
|
||||
}
|
||||
contents, err := json.MarshalIndent(receipt, "", " ")
|
||||
@@ -114,7 +116,10 @@ func LoadBuildReceipt(path string) (BuildReceipt, error) {
|
||||
}
|
||||
return BuildReceipt{}, fmt.Errorf("decode build receipt: %w", err)
|
||||
}
|
||||
if err := validateBuildReceipt(receipt, true); err != nil {
|
||||
if err := validateBuildReceipt(receipt); err != nil {
|
||||
if receipt.SchemaVersion != BuildReceiptSchemaVersion {
|
||||
return BuildReceipt{}, fmt.Errorf("%w; regenerate it with the updated build command", err)
|
||||
}
|
||||
return BuildReceipt{}, err
|
||||
}
|
||||
if filepath.Clean(receipt.OutputDir) != filepath.Dir(resolvedPath) {
|
||||
@@ -128,22 +133,26 @@ func (receipt BuildReceipt) MatchesBuild(repositoryDir, commit, version string,
|
||||
if err != nil || filepath.Clean(receipt.RepositoryDir) != filepath.Clean(resolvedRepo) {
|
||||
return false
|
||||
}
|
||||
if receipt.CandidateCommit != commit || receipt.Version != version || receipt.MainPackage != settings.MainPackage || receipt.BinaryName != settings.BinaryName {
|
||||
if receipt.CandidateCommit != commit || receipt.Version != version {
|
||||
return false
|
||||
}
|
||||
want := normalizeReceiptTargets(settings.Targets)
|
||||
if len(want) != len(receipt.Targets) {
|
||||
want := artifact.NormalizeSettings(settings.MainPackage, settings.BinaryName, parseReceiptTargets(settings.Targets))
|
||||
got := artifact.NormalizeSettings(receipt.MainPackage, receipt.BinaryName, parseStoredTargets(receipt.Targets))
|
||||
if want.MainPackage != got.MainPackage || want.BinaryName != got.BinaryName || len(want.Targets) != len(got.Targets) {
|
||||
return false
|
||||
}
|
||||
for i, target := range want {
|
||||
if target != receipt.Targets[i] {
|
||||
for i, target := range want.Targets {
|
||||
if target != got.Targets[i] {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
func validateBuildReceipt(receipt BuildReceipt, verifyFiles bool) error {
|
||||
func validateBuildReceipt(receipt BuildReceipt) error {
|
||||
if receipt.SchemaVersion != BuildReceiptSchemaVersion {
|
||||
return fmt.Errorf("unsupported or missing build receipt schema version %d", receipt.SchemaVersion)
|
||||
}
|
||||
if !versioncontract.ValidStableVersion(receipt.Version) {
|
||||
return fmt.Errorf("build receipt has invalid version %q", receipt.Version)
|
||||
}
|
||||
@@ -153,6 +162,9 @@ func validateBuildReceipt(receipt BuildReceipt, verifyFiles bool) error {
|
||||
if strings.TrimSpace(receipt.ModulePath) == "" || strings.TrimSpace(receipt.Tag) == "" {
|
||||
return errors.New("build receipt is missing module path or full tag")
|
||||
}
|
||||
if err := validateRecordedIdentity(receipt); err != nil {
|
||||
return fmt.Errorf("build receipt has inconsistent module identity: %w", err)
|
||||
}
|
||||
if !validCommitID(receipt.CandidateCommit) {
|
||||
return errors.New("build receipt has invalid candidate commit")
|
||||
}
|
||||
@@ -197,39 +209,38 @@ func validateBuildReceipt(receipt BuildReceipt, verifyFiles bool) error {
|
||||
if !validSHA256(artifact.SHA256) {
|
||||
return fmt.Errorf("build receipt has invalid SHA-256 for %q", artifact.Filename)
|
||||
}
|
||||
if verifyFiles {
|
||||
if err := verifyReceiptArtifact(artifact); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func verifyReceiptArtifact(artifact ReceiptArtifact) error {
|
||||
info, err := os.Lstat(artifact.Path)
|
||||
func validateRecordedIdentity(receipt BuildReceipt) error {
|
||||
repositoryDir := filepath.Clean(receipt.RepositoryDir)
|
||||
moduleDir := filepath.Clean(receipt.ModuleDir)
|
||||
relative, err := filepath.Rel(repositoryDir, moduleDir)
|
||||
if err != nil {
|
||||
return fmt.Errorf("inspect build artifact %q: %w", artifact.Path, err)
|
||||
return fmt.Errorf("relate module directory to repository: %w", err)
|
||||
}
|
||||
if !info.Mode().IsRegular() {
|
||||
return fmt.Errorf("build artifact %q is not a regular file", artifact.Path)
|
||||
if relative == ".." || strings.HasPrefix(relative, ".."+string(filepath.Separator)) || filepath.IsAbs(relative) {
|
||||
return errors.New("module directory is outside repository")
|
||||
}
|
||||
file, err := os.Open(artifact.Path)
|
||||
if relative == "" {
|
||||
relative = "."
|
||||
}
|
||||
identity, err := releaseidentity.Derive(releaseidentity.Selection{
|
||||
RepositoryDir: repositoryDir,
|
||||
ModuleDir: moduleDir,
|
||||
ModuleSubdir: filepath.ToSlash(relative),
|
||||
}, []byte("module "+receipt.ModulePath+"\n"), receipt.Version)
|
||||
if err != nil {
|
||||
return fmt.Errorf("open build artifact %q: %w", artifact.Path, err)
|
||||
return err
|
||||
}
|
||||
defer file.Close()
|
||||
hash := sha256.New()
|
||||
if _, err := io.Copy(hash, file); err != nil {
|
||||
return fmt.Errorf("hash build artifact %q: %w", artifact.Path, err)
|
||||
}
|
||||
if actual := hex.EncodeToString(hash.Sum(nil)); actual != artifact.SHA256 {
|
||||
return fmt.Errorf("build artifact %q hash changed: got %s, expected %s", artifact.Path, actual, artifact.SHA256)
|
||||
if identity.Tag != receipt.Tag {
|
||||
return fmt.Errorf("full tag %q does not match module selection and version; want %q", receipt.Tag, identity.Tag)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func normalizeReceiptTargets(targets []string) []ReceiptTarget {
|
||||
func parseReceiptTargets(targets []string) []artifact.Target {
|
||||
parsed := make([]artifact.Target, 0, len(targets))
|
||||
for _, value := range targets {
|
||||
goos, goarch, ok := strings.Cut(value, "/")
|
||||
@@ -238,12 +249,15 @@ func normalizeReceiptTargets(targets []string) []ReceiptTarget {
|
||||
}
|
||||
parsed = append(parsed, artifact.Target{OS: goos, Arch: goarch})
|
||||
}
|
||||
normalized := artifact.NormalizeTargets(parsed)
|
||||
result := make([]ReceiptTarget, 0, len(normalized))
|
||||
for _, target := range normalized {
|
||||
result = append(result, ReceiptTarget{OS: target.OS, Arch: target.Arch})
|
||||
return parsed
|
||||
}
|
||||
|
||||
func parseStoredTargets(targets []ReceiptTarget) []artifact.Target {
|
||||
parsed := make([]artifact.Target, 0, len(targets))
|
||||
for _, target := range targets {
|
||||
parsed = append(parsed, artifact.Target{OS: target.OS, Arch: target.Arch})
|
||||
}
|
||||
return result
|
||||
return parsed
|
||||
}
|
||||
|
||||
func validCommitID(value string) bool {
|
||||
|
||||
@@ -3,65 +3,86 @@ package cli
|
||||
import (
|
||||
"crypto/sha256"
|
||||
"encoding/hex"
|
||||
"encoding/json"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildReceiptRoundTripBindsFilesAndBuildInputs(t *testing.T) {
|
||||
outputDir := t.TempDir()
|
||||
repositoryDir := t.TempDir()
|
||||
filename := "example-v1.2.3-linux-amd64"
|
||||
artifactPath := filepath.Join(outputDir, filename)
|
||||
contents := []byte("built application")
|
||||
if err := os.WriteFile(artifactPath, contents, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256(contents)
|
||||
receipt := BuildReceipt{
|
||||
RepositoryDir: repositoryDir,
|
||||
ModuleDir: repositoryDir,
|
||||
ModulePath: "example.com/receipt",
|
||||
Tag: "v1.2.3",
|
||||
CandidateCommit: strings.Repeat("a", 40),
|
||||
Version: "v1.2.3",
|
||||
MainPackage: "./cmd/example",
|
||||
BinaryName: "example",
|
||||
Targets: []ReceiptTarget{{OS: "linux", Arch: "amd64"}},
|
||||
Compiler: "go version go1.25.0 linux/amd64",
|
||||
Artifacts: []ReceiptArtifact{{
|
||||
Path: artifactPath,
|
||||
Filename: filename,
|
||||
Target: ReceiptTarget{OS: "linux", Arch: "amd64"},
|
||||
SHA256: hex.EncodeToString(digest[:]),
|
||||
}},
|
||||
}
|
||||
path, err := WriteBuildReceipt(outputDir, receipt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := LoadBuildReceipt(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings := BuildSettings{
|
||||
MainPackage: "./cmd/example",
|
||||
BinaryName: "example",
|
||||
Targets: []string{"linux/amd64", "linux/amd64"},
|
||||
}
|
||||
if !loaded.MatchesBuild(repositoryDir, strings.Repeat("a", 40), "v1.2.3", settings) {
|
||||
t.Fatalf("receipt does not match normalized build settings: %#v", loaded)
|
||||
}
|
||||
settings.BinaryName = "other"
|
||||
if loaded.MatchesBuild(repositoryDir, strings.Repeat("a", 40), "v1.2.3", settings) {
|
||||
t.Fatal("receipt matched changed build settings")
|
||||
}
|
||||
if err := os.WriteFile(artifactPath, []byte("changed artifact"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadBuildReceipt(path); err == nil || !strings.Contains(err.Error(), "hash changed") {
|
||||
t.Fatalf("LoadBuildReceipt() error = %v, want changed-file hash error", err)
|
||||
func TestBuildReceiptRoundTripBindsRootAndNestedModuleIdentity(t *testing.T) {
|
||||
for _, test := range []struct {
|
||||
name string
|
||||
moduleDir string
|
||||
modulePath string
|
||||
tag string
|
||||
}{
|
||||
{name: "root", moduleDir: ".", modulePath: "example.com/receipt", tag: "v1.2.3"},
|
||||
{name: "nested", moduleDir: "integrations/ckconfig", modulePath: "example.com/ckconfig", tag: "integrations/ckconfig/v1.2.3"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
outputDir := t.TempDir()
|
||||
repositoryDir := t.TempDir()
|
||||
moduleDir := repositoryDir
|
||||
if test.moduleDir != "." {
|
||||
moduleDir = filepath.Join(repositoryDir, filepath.FromSlash(test.moduleDir))
|
||||
if err := os.MkdirAll(moduleDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
filename := "example-v1.2.3-linux-amd64"
|
||||
artifactPath := filepath.Join(outputDir, filename)
|
||||
contents := []byte("built application")
|
||||
if err := os.WriteFile(artifactPath, contents, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256(contents)
|
||||
receipt := BuildReceipt{
|
||||
SchemaVersion: BuildReceiptSchemaVersion,
|
||||
RepositoryDir: repositoryDir,
|
||||
ModuleDir: moduleDir,
|
||||
ModulePath: test.modulePath,
|
||||
Tag: test.tag,
|
||||
CandidateCommit: strings.Repeat("a", 40),
|
||||
Version: "v1.2.3",
|
||||
MainPackage: "./cmd/example",
|
||||
BinaryName: "example",
|
||||
Targets: []ReceiptTarget{{OS: "linux", Arch: "amd64"}},
|
||||
Compiler: "go version go1.25.0 linux/amd64",
|
||||
Artifacts: []ReceiptArtifact{{
|
||||
Path: artifactPath, Filename: filename,
|
||||
Target: ReceiptTarget{OS: "linux", Arch: "amd64"}, SHA256: hex.EncodeToString(digest[:]),
|
||||
}},
|
||||
}
|
||||
path, err := WriteBuildReceipt(outputDir, receipt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
loaded, err := LoadBuildReceipt(path)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
settings := BuildSettings{MainPackage: "./cmd/example", BinaryName: "example", Targets: []string{"linux/amd64", "linux/amd64"}}
|
||||
if !loaded.MatchesBuild(repositoryDir, strings.Repeat("a", 40), "v1.2.3", settings) {
|
||||
t.Fatalf("receipt does not match normalized build settings: %#v", loaded)
|
||||
}
|
||||
settings.BinaryName = "other"
|
||||
if loaded.MatchesBuild(repositoryDir, strings.Repeat("a", 40), "v1.2.3", settings) {
|
||||
t.Fatal("receipt matched changed build settings")
|
||||
}
|
||||
if err := os.WriteFile(artifactPath, []byte("changed artifact"), 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadBuildReceipt(path); err != nil {
|
||||
t.Fatalf("receipt loading verified changed artifact bytes: %v", err)
|
||||
}
|
||||
if err := os.Remove(artifactPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadBuildReceipt(path); err != nil {
|
||||
t.Fatalf("receipt loading required artifact bytes: %v", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +92,9 @@ func TestLoadBuildReceiptRejectsUnknownFieldsAndMultipleValues(t *testing.T) {
|
||||
body string
|
||||
want string
|
||||
}{
|
||||
{name: "unknown field", body: `{"unexpected":true}`, want: "unknown field"},
|
||||
{name: "unknown field", body: `{"schema_version":1,"unexpected":true}`, want: "unknown field"},
|
||||
{name: "unversioned receipt", body: `{}`, want: "regenerate it with the updated build command"},
|
||||
{name: "unknown schema", body: `{"schema_version":2}`, want: "regenerate it with the updated build command"},
|
||||
{name: "multiple values", body: `{} {}`, want: "multiple JSON values"},
|
||||
} {
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
@@ -85,3 +108,66 @@ func TestLoadBuildReceiptRejectsUnknownFieldsAndMultipleValues(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestWriteBuildReceiptRejectsInconsistentRecordedIdentity(t *testing.T) {
|
||||
repositoryDir := t.TempDir()
|
||||
moduleDir := filepath.Join(repositoryDir, "integrations", "ckconfig")
|
||||
if err := os.MkdirAll(moduleDir, 0o755); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
outputDir := t.TempDir()
|
||||
receipt := BuildReceipt{
|
||||
SchemaVersion: BuildReceiptSchemaVersion,
|
||||
RepositoryDir: repositoryDir, ModuleDir: moduleDir, ModulePath: "example.com/ckconfig", Tag: "v1.2.3",
|
||||
CandidateCommit: strings.Repeat("a", 40), Version: "v1.2.3", MainPackage: ".", BinaryName: "helper",
|
||||
OutputDir: outputDir, Targets: []ReceiptTarget{{OS: "linux", Arch: "amd64"}}, Compiler: "go version fixture",
|
||||
Artifacts: []ReceiptArtifact{{
|
||||
Path: filepath.Join(outputDir, "helper-v1.2.3-linux-amd64"), Filename: "helper-v1.2.3-linux-amd64",
|
||||
Target: ReceiptTarget{OS: "linux", Arch: "amd64"}, SHA256: strings.Repeat("a", 64),
|
||||
}},
|
||||
}
|
||||
if _, err := WriteBuildReceipt(outputDir, receipt); err == nil || !strings.Contains(err.Error(), "full tag") {
|
||||
t.Fatalf("WriteBuildReceipt() error = %v, want inconsistent full tag", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestLoadBuildReceiptRejectsMismatchedModuleIdentity(t *testing.T) {
|
||||
repositoryDir := t.TempDir()
|
||||
outputDir := t.TempDir()
|
||||
artifactPath := filepath.Join(outputDir, "helper-v1.2.3-linux-amd64")
|
||||
contents := []byte("fixture")
|
||||
if err := os.WriteFile(artifactPath, contents, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
digest := sha256.Sum256(contents)
|
||||
receipt := BuildReceipt{
|
||||
SchemaVersion: BuildReceiptSchemaVersion,
|
||||
RepositoryDir: repositoryDir, ModuleDir: repositoryDir, ModulePath: "example.com/receipt", Tag: "v1.2.3",
|
||||
CandidateCommit: strings.Repeat("a", 40), Version: "v1.2.3", MainPackage: ".", BinaryName: "helper",
|
||||
OutputDir: outputDir, Targets: []ReceiptTarget{{OS: "linux", Arch: "amd64"}}, Compiler: "go version fixture",
|
||||
Artifacts: []ReceiptArtifact{{
|
||||
Path: artifactPath, Filename: filepath.Base(artifactPath),
|
||||
Target: ReceiptTarget{OS: "linux", Arch: "amd64"}, SHA256: hex.EncodeToString(digest[:]),
|
||||
}},
|
||||
}
|
||||
contents, err := json.Marshal(receipt)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var fields map[string]any
|
||||
if err := json.Unmarshal(contents, &fields); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fields["module_dir"] = filepath.Join(repositoryDir, "integrations", "ckconfig")
|
||||
contents, err = json.Marshal(fields)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
path := filepath.Join(outputDir, BuildReceiptName)
|
||||
if err := os.WriteFile(path, contents, 0o600); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := LoadBuildReceipt(path); err == nil || !strings.Contains(err.Error(), "inconsistent module identity") {
|
||||
t.Fatalf("LoadBuildReceipt() error = %v, want inconsistent module identity", err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -394,6 +394,7 @@ func configuredBuildSpec(settings *cli.BuildSettings) *releasekit.BuildSpec {
|
||||
|
||||
func receiptFromBuild(result releasekit.BuildResult) cli.BuildReceipt {
|
||||
receipt := cli.BuildReceipt{
|
||||
SchemaVersion: cli.BuildReceiptSchemaVersion,
|
||||
RepositoryDir: result.RepositoryDir,
|
||||
ModuleDir: result.ModuleDir,
|
||||
ModulePath: result.ModulePath,
|
||||
|
||||
@@ -381,7 +381,7 @@ func main() {
|
||||
BinaryName: "fixture",
|
||||
Targets: []string{fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)},
|
||||
}
|
||||
if !receipt.MatchesBuild(repository, commit, "v1.2.3", settings) || len(receipt.Artifacts) != 1 {
|
||||
if receipt.SchemaVersion != cli.BuildReceiptSchemaVersion || !receipt.MatchesBuild(repository, commit, "v1.2.3", settings) || len(receipt.Artifacts) != 1 {
|
||||
t.Fatalf("build receipt = %#v", receipt)
|
||||
}
|
||||
if !bytes.Contains(stdout.Bytes(), []byte(receipt.Artifacts[0].Filename)) || !bytes.Contains(stdout.Bytes(), []byte(receipt.Artifacts[0].SHA256)) {
|
||||
@@ -881,7 +881,7 @@ func TestDeliverRejectsCheckoutThatDiffersFromLocalTag(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testing.T) {
|
||||
for _, mode := range []string{"valid receipt", "tampered", "stale settings"} {
|
||||
for _, mode := range []string{"valid receipt", "tampered", "missing artifact", "stale settings"} {
|
||||
t.Run(mode, func(t *testing.T) {
|
||||
repository := newCLIRepository(t)
|
||||
state := &deliveryServerState{}
|
||||
@@ -902,6 +902,16 @@ func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testin
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if mode == "missing artifact" {
|
||||
if err := os.Remove(artifactPath); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
if mode == "tampered" || mode == "missing artifact" {
|
||||
if _, err := cli.LoadBuildReceipt(filepath.Join(assetsDir, cli.BuildReceiptName)); err != nil {
|
||||
t.Fatalf("receipt loading performed artifact verification: %v", err)
|
||||
}
|
||||
}
|
||||
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
|
||||
var stdout, stderr bytes.Buffer
|
||||
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr)
|
||||
@@ -917,6 +927,11 @@ func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testin
|
||||
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 || state.assetUploads != 0 {
|
||||
t.Fatalf("%s receipt status=%d requests=%d stdout=%q stderr=%q", mode, status, state.apiRequests, stdout.String(), stderr.String())
|
||||
}
|
||||
if mode == "tampered" || mode == "missing artifact" {
|
||||
if strings.Contains(stderr.String(), "load verified build receipt") {
|
||||
t.Fatalf("%s artifact was rejected while loading its receipt: %q", mode, stderr.String())
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -937,6 +952,7 @@ func writeDeliveryReceipt(t *testing.T, repository string, settings cli.BuildSet
|
||||
digest := sha256.Sum256(content)
|
||||
target := cli.ReceiptTarget{OS: "linux", Arch: "amd64"}
|
||||
receipt := cli.BuildReceipt{
|
||||
SchemaVersion: cli.BuildReceiptSchemaVersion,
|
||||
RepositoryDir: resolvedRepository, ModuleDir: resolvedRepository, ModulePath: "example.com/cli-candidate", Tag: "v1.2.3",
|
||||
CandidateCommit: cliGitOutput(t, repository, "rev-parse", "HEAD"),
|
||||
SourceVerified: true,
|
||||
|
||||
@@ -83,6 +83,8 @@ points inside the source tree.
|
||||
|
||||
For delivery from a tagged checkout, `deliver` builds selected tagged-source
|
||||
targets into temporary storage by default. Use `--assets DIR` to deliver a
|
||||
separate build output; ReleaseKit requires its receipt and verifies the source,
|
||||
build settings, and artifact hashes before the first Gitea write. See the
|
||||
separate build output; ReleaseKit requires the current module-bound receipt and
|
||||
checks its source identity and normalized build settings. It verifies artifact
|
||||
bytes and sizes during delivery before the first Gitea write. Older receipts
|
||||
must be regenerated with the updated `build` command. See the
|
||||
[delivery consumer guide](delivery.md) for page-only delivery and retry states.
|
||||
|
||||
@@ -89,9 +89,11 @@ RELEASEKIT_GITEA_TOKEN="$GITEA_TOKEN" releasekit deliver --assets dist v1.2.3
|
||||
|
||||
The build command does not need delivery credentials. The `--assets DIR`
|
||||
directory must contain the private `.releasekit-build.json` receipt created by
|
||||
`build`. ReleaseKit checks the receipt's repository, commit, version, resolved
|
||||
module directory and path, full tag, build settings, and file hashes before
|
||||
making a remote request; caller-owned files remain available for retries.
|
||||
`build`. ReleaseKit checks its schema, repository, commit, version, resolved
|
||||
module directory and path, full tag, normalized build settings, and artifact
|
||||
declarations. Old receipts must be regenerated with the updated `build`
|
||||
command. Delivery verifies the artifact bytes and sizes before making a hosted
|
||||
request; caller-owned files remain available for retries.
|
||||
Ordinary `deliver` does not download application dependencies or change the
|
||||
checkout.
|
||||
|
||||
|
||||
@@ -499,6 +499,15 @@ changed or missing artifact bytes fails during delivery before hosted requests.
|
||||
Protect verification ownership through an observable read boundary rather than
|
||||
tests tied to private helper choreography. Run receipt and CLI prebuilt tests.
|
||||
|
||||
**Completed:** Replaced the unversioned receipt with schema version 1 and pure
|
||||
recorded-identity checks for module path, physical directory, version, and full
|
||||
tag. Receipt loading now validates structure and normalized settings without
|
||||
opening artifacts; delivery remains responsible for verifying artifact bytes
|
||||
and sizes before hosted requests. Old and unknown schemas require regeneration.
|
||||
Verified root and nested receipts, malformed identity and settings, changed and
|
||||
missing artifacts, `go test ./...`, `go run ./internal/apicheck`, and
|
||||
`git diff --check`.
|
||||
|
||||
## Stage 10: Map profiles and product commands to module operations
|
||||
|
||||
**Outcome:** Local check, publication, and build commands express the new API with
|
||||
|
||||
Reference in New Issue
Block a user