Consolidate artifact declarations
This commit is contained in:
@@ -11,6 +11,7 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -149,7 +150,7 @@ func validateBuildReceipt(receipt BuildReceipt, verifyFiles bool) error {
|
|||||||
if !validCommitID(receipt.CandidateCommit) {
|
if !validCommitID(receipt.CandidateCommit) {
|
||||||
return errors.New("build receipt has invalid candidate commit")
|
return errors.New("build receipt has invalid candidate commit")
|
||||||
}
|
}
|
||||||
if receipt.MainPackage == "" || !safeReceiptComponent(receipt.BinaryName) || receipt.Compiler == "" {
|
if receipt.MainPackage == "" || !artifact.ValidComponent(receipt.BinaryName) || receipt.Compiler == "" {
|
||||||
return errors.New("build receipt is missing build identity")
|
return errors.New("build receipt is missing build identity")
|
||||||
}
|
}
|
||||||
if len(receipt.Targets) == 0 || len(receipt.Artifacts) != len(receipt.Targets) {
|
if len(receipt.Targets) == 0 || len(receipt.Artifacts) != len(receipt.Targets) {
|
||||||
@@ -157,21 +158,29 @@ func validateBuildReceipt(receipt BuildReceipt, verifyFiles bool) error {
|
|||||||
}
|
}
|
||||||
seenTargets := make(map[ReceiptTarget]bool, len(receipt.Targets))
|
seenTargets := make(map[ReceiptTarget]bool, len(receipt.Targets))
|
||||||
seenArtifacts := make(map[ReceiptTarget]bool, len(receipt.Artifacts))
|
seenArtifacts := make(map[ReceiptTarget]bool, len(receipt.Artifacts))
|
||||||
|
targets := make([]artifact.Target, 0, len(receipt.Targets))
|
||||||
for _, target := range receipt.Targets {
|
for _, target := range receipt.Targets {
|
||||||
if !safeReceiptComponent(target.OS) || !safeReceiptComponent(target.Arch) {
|
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
||||||
return fmt.Errorf("build receipt has unsafe target %q/%q", target.OS, target.Arch)
|
return fmt.Errorf("build receipt has unsafe target %q/%q", target.OS, target.Arch)
|
||||||
}
|
}
|
||||||
if seenTargets[target] {
|
if seenTargets[target] {
|
||||||
return fmt.Errorf("build receipt repeats target %s/%s", target.OS, target.Arch)
|
return fmt.Errorf("build receipt repeats target %s/%s", target.OS, target.Arch)
|
||||||
}
|
}
|
||||||
seenTargets[target] = true
|
seenTargets[target] = true
|
||||||
|
targets = append(targets, artifact.Target{OS: target.OS, Arch: target.Arch})
|
||||||
|
}
|
||||||
|
declarations := artifact.ExpectedBuild(receipt.BinaryName, receipt.Version, targets)
|
||||||
|
wantByTarget := make(map[ReceiptTarget]string, len(declarations))
|
||||||
|
for _, declaration := range declarations {
|
||||||
|
target := ReceiptTarget{OS: declaration.Target.OS, Arch: declaration.Target.Arch}
|
||||||
|
wantByTarget[target] = declaration.Filename
|
||||||
}
|
}
|
||||||
for _, artifact := range receipt.Artifacts {
|
for _, artifact := range receipt.Artifacts {
|
||||||
if !seenTargets[artifact.Target] || seenArtifacts[artifact.Target] {
|
if !seenTargets[artifact.Target] || seenArtifacts[artifact.Target] {
|
||||||
return fmt.Errorf("build receipt has an unexpected artifact target %s/%s", artifact.Target.OS, artifact.Target.Arch)
|
return fmt.Errorf("build receipt has an unexpected artifact target %s/%s", artifact.Target.OS, artifact.Target.Arch)
|
||||||
}
|
}
|
||||||
seenArtifacts[artifact.Target] = true
|
seenArtifacts[artifact.Target] = true
|
||||||
wantName := receiptArtifactFilename(receipt.BinaryName, receipt.Version, artifact.Target)
|
wantName := wantByTarget[artifact.Target]
|
||||||
if artifact.Filename != wantName || filepath.Base(artifact.Filename) != artifact.Filename {
|
if artifact.Filename != wantName || filepath.Base(artifact.Filename) != artifact.Filename {
|
||||||
return fmt.Errorf("build receipt has invalid artifact filename %q", artifact.Filename)
|
return fmt.Errorf("build receipt has invalid artifact filename %q", artifact.Filename)
|
||||||
}
|
}
|
||||||
@@ -215,34 +224,22 @@ func verifyReceiptArtifact(artifact ReceiptArtifact) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func normalizeReceiptTargets(targets []string) []ReceiptTarget {
|
func normalizeReceiptTargets(targets []string) []ReceiptTarget {
|
||||||
seen := make(map[ReceiptTarget]bool, len(targets))
|
parsed := make([]artifact.Target, 0, len(targets))
|
||||||
result := make([]ReceiptTarget, 0, len(targets))
|
|
||||||
for _, value := range targets {
|
for _, value := range targets {
|
||||||
goos, goarch, ok := strings.Cut(value, "/")
|
goos, goarch, ok := strings.Cut(value, "/")
|
||||||
if !ok {
|
if !ok {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
target := ReceiptTarget{OS: goos, Arch: goarch}
|
parsed = append(parsed, artifact.Target{OS: goos, Arch: goarch})
|
||||||
if !seen[target] {
|
}
|
||||||
seen[target] = true
|
normalized := artifact.NormalizeTargets(parsed)
|
||||||
result = append(result, target)
|
result := make([]ReceiptTarget, 0, len(normalized))
|
||||||
}
|
for _, target := range normalized {
|
||||||
|
result = append(result, ReceiptTarget{OS: target.OS, Arch: target.Arch})
|
||||||
}
|
}
|
||||||
return result
|
return result
|
||||||
}
|
}
|
||||||
|
|
||||||
func receiptArtifactFilename(binaryName, version string, target ReceiptTarget) string {
|
|
||||||
filename := fmt.Sprintf("%s-%s-%s-%s", binaryName, version, target.OS, target.Arch)
|
|
||||||
if target.OS == "windows" {
|
|
||||||
filename += ".exe"
|
|
||||||
}
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
func safeReceiptComponent(value string) bool {
|
|
||||||
return value != "" && !strings.ContainsAny(value, "/\\\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func validCommitID(value string) bool {
|
func validCommitID(value string) bool {
|
||||||
if len(value) != 40 && len(value) != 64 {
|
if len(value) != 40 && len(value) != 64 {
|
||||||
return false
|
return false
|
||||||
|
|||||||
@@ -35,6 +35,7 @@ these implemented packages:
|
|||||||
| `cmd/releasekit` | Executable entry point, command routing, output, and exit status. |
|
| `cmd/releasekit` | Executable entry point, command routing, output, and exit status. |
|
||||||
| `cmd/releasekit/internal/cli` | Command parsing, strict JSON profile loading, configured checks, and private build receipts. |
|
| `cmd/releasekit/internal/cli` | Command parsing, strict JSON profile loading, configured checks, and private build receipts. |
|
||||||
| `internal/apicheck` | Compiled-export and import-boundary checks run during development. |
|
| `internal/apicheck` | Compiled-export and import-boundary checks run during development. |
|
||||||
|
| `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/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/candidate` | Stable-version, note, repository-state, candidate capture, and post-validation checks. |
|
||||||
| `internal/delivery` | Tagged-checkout verification, artifact inventory/hash validation, manifest assembly, and coordination of hosted delivery results. |
|
| `internal/delivery` | Tagged-checkout verification, artifact inventory/hash validation, manifest assembly, and coordination of hosted delivery results. |
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ReleaseKit audit implementation plan
|
# ReleaseKit audit implementation plan
|
||||||
|
|
||||||
Status: Stages 1–3 complete; Stage 4 not started.
|
Status: Stages 1–4 complete; Stage 5 not started.
|
||||||
Baseline: `e4be7fa` (2026-10-06). The [audit](audit.md) records the evidence,
|
Baseline: `e4be7fa` (2026-10-06). The [audit](audit.md) records the evidence,
|
||||||
product goals, and selected direction. This document owns implementation order
|
product goals, and selected direction. This document owns implementation order
|
||||||
and completion criteria for that work.
|
and completion criteria for that work.
|
||||||
@@ -236,6 +236,14 @@ and manifests. Add only missing coverage for disagreement between these
|
|||||||
boundaries. The API/dependency checker confirms that the shared internal owner
|
boundaries. The API/dependency checker confirms that the shared internal owner
|
||||||
has not leaked through exported signatures or runtime dependencies.
|
has not leaked through exported signatures or runtime dependencies.
|
||||||
|
|
||||||
|
**Completed:** `internal/artifact` now owns target normalization, filename
|
||||||
|
components, binary declarations, and checksum-manifest naming. The builder,
|
||||||
|
receipt adapter, and delivery validation share those rules while retaining
|
||||||
|
their build, receipt-path/file, and hosted-content responsibilities. Existing
|
||||||
|
coverage plus a mismatched delivery declaration check preserve cross-boundary
|
||||||
|
behavior. Focused tests, the full offline suite, API boundary checks, and
|
||||||
|
documentation whitespace checks pass.
|
||||||
|
|
||||||
## Stage 5 — Make delivery outcomes disjoint and actionable
|
## Stage 5 — Make delivery outcomes disjoint and actionable
|
||||||
|
|
||||||
**Covers:** F7. **Primary owners:** `internal/gitea/upload.go`, delivery result
|
**Covers:** F7. **Primary owners:** `internal/gitea/upload.go`, delivery result
|
||||||
|
|||||||
62
internal/artifact/artifact.go
Normal file
62
internal/artifact/artifact.go
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
// Package artifact owns shared build artifact identity rules.
|
||||||
|
package artifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ManifestFilename = "SHA256SUMS"
|
||||||
|
|
||||||
|
type Target struct {
|
||||||
|
OS string
|
||||||
|
Arch string
|
||||||
|
}
|
||||||
|
|
||||||
|
type Declaration struct {
|
||||||
|
Target Target
|
||||||
|
Filename string
|
||||||
|
}
|
||||||
|
|
||||||
|
// NormalizeTargets removes duplicate targets while preserving their first
|
||||||
|
// occurrence.
|
||||||
|
func NormalizeTargets(targets []Target) []Target {
|
||||||
|
seen := make(map[Target]struct{}, len(targets))
|
||||||
|
result := make([]Target, 0, len(targets))
|
||||||
|
for _, target := range targets {
|
||||||
|
if _, ok := seen[target]; ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[target] = struct{}{}
|
||||||
|
result = append(result, target)
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
// ValidComponent reports whether value is a nonempty filename component under
|
||||||
|
// the build and receipt contracts.
|
||||||
|
func ValidComponent(value string) bool {
|
||||||
|
return value != "" && !strings.ContainsAny(value, "/\\\r\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filename returns the declared binary filename for one target.
|
||||||
|
func Filename(binaryName, version string, target Target) string {
|
||||||
|
filename := fmt.Sprintf("%s-%s-%s-%s", binaryName, version, target.OS, target.Arch)
|
||||||
|
if target.OS == "windows" {
|
||||||
|
filename += ".exe"
|
||||||
|
}
|
||||||
|
return filename
|
||||||
|
}
|
||||||
|
|
||||||
|
// ExpectedBuild returns artifact declarations in target order. Callers retain
|
||||||
|
// responsibility for validating their boundary-specific build identity.
|
||||||
|
func ExpectedBuild(binaryName, version string, targets []Target) []Declaration {
|
||||||
|
declarations := make([]Declaration, 0, len(targets))
|
||||||
|
for _, target := range targets {
|
||||||
|
declarations = append(declarations, Declaration{
|
||||||
|
Target: target,
|
||||||
|
Filename: Filename(binaryName, version, target),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return declarations
|
||||||
|
}
|
||||||
42
internal/artifact/artifact_test.go
Normal file
42
internal/artifact/artifact_test.go
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
package artifact
|
||||||
|
|
||||||
|
import (
|
||||||
|
"reflect"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestNormalizeTargetsPreservesOrderAndFirstOccurrence(t *testing.T) {
|
||||||
|
targets := []Target{
|
||||||
|
{OS: "linux", Arch: "amd64"},
|
||||||
|
{OS: "windows", Arch: "arm64"},
|
||||||
|
{OS: "linux", Arch: "amd64"},
|
||||||
|
}
|
||||||
|
want := []Target{{OS: "linux", Arch: "amd64"}, {OS: "windows", Arch: "arm64"}}
|
||||||
|
if got := NormalizeTargets(targets); !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("NormalizeTargets() = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestExpectedBuildUsesSharedNamesInTargetOrder(t *testing.T) {
|
||||||
|
targets := []Target{{OS: "windows", Arch: "arm64"}, {OS: "linux", Arch: "amd64"}}
|
||||||
|
want := []Declaration{
|
||||||
|
{Target: targets[0], Filename: "app-v1.2.3-windows-arm64.exe"},
|
||||||
|
{Target: targets[1], Filename: "app-v1.2.3-linux-amd64"},
|
||||||
|
}
|
||||||
|
if got := ExpectedBuild("app", "v1.2.3", targets); !reflect.DeepEqual(got, want) {
|
||||||
|
t.Fatalf("ExpectedBuild() = %#v, want %#v", got, want)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestValidComponentPreservesBuildAndReceiptRules(t *testing.T) {
|
||||||
|
for _, value := range []string{"", "path/name", `path\name`, "line\nbreak", "line\rbreak"} {
|
||||||
|
if ValidComponent(value) {
|
||||||
|
t.Errorf("ValidComponent(%q) = true, want false", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, value := range []string{"app", "..", "nul\x00byte"} {
|
||||||
|
if !ValidComponent(value) {
|
||||||
|
t.Errorf("ValidComponent(%q) = false, want true", value)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -13,15 +13,13 @@ import (
|
|||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||||
)
|
)
|
||||||
|
|
||||||
type Target struct {
|
type Target = artifact.Target
|
||||||
OS string
|
|
||||||
Arch string
|
|
||||||
}
|
|
||||||
|
|
||||||
type Spec struct {
|
type Spec struct {
|
||||||
MainPackage string
|
MainPackage string
|
||||||
@@ -62,7 +60,7 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
Version: request.Version,
|
Version: request.Version,
|
||||||
MainPackage: request.Spec.MainPackage,
|
MainPackage: request.Spec.MainPackage,
|
||||||
BinaryName: request.Spec.BinaryName,
|
BinaryName: request.Spec.BinaryName,
|
||||||
Targets: normalizeTargets(request.Spec.Targets),
|
Targets: artifact.NormalizeTargets(request.Spec.Targets),
|
||||||
}
|
}
|
||||||
if !versioncontract.ValidStableVersion(request.Version) {
|
if !versioncontract.ValidStableVersion(request.Version) {
|
||||||
return result, fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, request.Version)
|
return result, fmt.Errorf("%w: %q", candidate.ErrInvalidVersion, request.Version)
|
||||||
@@ -74,11 +72,11 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
if request.Spec.MainPackage == "" {
|
if request.Spec.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 !safeComponent(request.Spec.BinaryName) {
|
if !artifact.ValidComponent(request.Spec.BinaryName) {
|
||||||
return result, fmt.Errorf("unsafe binary name %q", request.Spec.BinaryName)
|
return result, fmt.Errorf("unsafe binary name %q", request.Spec.BinaryName)
|
||||||
}
|
}
|
||||||
for _, target := range result.Targets {
|
for _, target := range result.Targets {
|
||||||
if !safeComponent(target.OS) || !safeComponent(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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -107,9 +105,10 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
result.OutputDir = resolvedOutputDir(repositoryDir, request.OutputDir)
|
result.OutputDir = resolvedOutputDir(repositoryDir, request.OutputDir)
|
||||||
filenames := make([]string, 0, len(result.Targets))
|
declarations := artifact.ExpectedBuild(request.Spec.BinaryName, request.Version, result.Targets)
|
||||||
for _, target := range result.Targets {
|
filenames := make([]string, len(declarations))
|
||||||
filenames = append(filenames, artifactFilename(request.Spec.BinaryName, request.Version, target))
|
for i, declaration := range declarations {
|
||||||
|
filenames[i] = declaration.Filename
|
||||||
}
|
}
|
||||||
if err := os.MkdirAll(result.OutputDir, 0o755); err != nil {
|
if err := os.MkdirAll(result.OutputDir, 0o755); err != nil {
|
||||||
return result, fmt.Errorf("create build output directory %q: %w", result.OutputDir, err)
|
return result, fmt.Errorf("create build output directory %q: %w", result.OutputDir, err)
|
||||||
@@ -188,23 +187,6 @@ func Build(ctx context.Context, request Request) (Result, error) {
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeTargets(targets []Target) []Target {
|
|
||||||
seen := make(map[Target]struct{}, len(targets))
|
|
||||||
result := make([]Target, 0, len(targets))
|
|
||||||
for _, target := range targets {
|
|
||||||
if _, ok := seen[target]; ok {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
seen[target] = struct{}{}
|
|
||||||
result = append(result, target)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
func safeComponent(value string) bool {
|
|
||||||
return value != "" && !strings.ContainsAny(value, "/\\\r\n")
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolvedOutputDir(repositoryDir, outputDir string) string {
|
func resolvedOutputDir(repositoryDir, outputDir string) string {
|
||||||
if outputDir == "" {
|
if outputDir == "" {
|
||||||
outputDir = filepath.Join(repositoryDir, "dist")
|
outputDir = filepath.Join(repositoryDir, "dist")
|
||||||
@@ -218,14 +200,6 @@ func resolvedOutputDir(repositoryDir, outputDir string) string {
|
|||||||
return resolved
|
return resolved
|
||||||
}
|
}
|
||||||
|
|
||||||
func artifactFilename(binaryName, version string, target Target) string {
|
|
||||||
filename := fmt.Sprintf("%s-%s-%s-%s", binaryName, version, target.OS, target.Arch)
|
|
||||||
if target.OS == "windows" {
|
|
||||||
filename += ".exe"
|
|
||||||
}
|
|
||||||
return filename
|
|
||||||
}
|
|
||||||
|
|
||||||
func gitCommit(ctx context.Context, repositoryDir string) (string, error) {
|
func gitCommit(ctx context.Context, repositoryDir string) (string, error) {
|
||||||
command := exec.CommandContext(ctx, "git", "-C", repositoryDir, "rev-parse", "--verify", "HEAD^{commit}")
|
command := exec.CommandContext(ctx, "git", "-C", repositoryDir, "rev-parse", "--verify", "HEAD^{commit}")
|
||||||
output, err := command.Output()
|
output, err := command.Output()
|
||||||
|
|||||||
@@ -7,8 +7,8 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||||
)
|
)
|
||||||
@@ -23,30 +23,26 @@ func declaredBuildAssets(result internalbuild.Result) ([]gitea.ExpectedAsset, er
|
|||||||
if len(result.Artifacts) != len(result.Targets) {
|
if len(result.Artifacts) != len(result.Targets) {
|
||||||
return nil, fmt.Errorf("build result must contain one artifact for each selected target")
|
return nil, fmt.Errorf("build result must contain one artifact for each selected target")
|
||||||
}
|
}
|
||||||
wantByTarget := make(map[string]string, len(result.Targets))
|
declarations := artifact.ExpectedBuild(result.BinaryName, result.Version, result.Targets)
|
||||||
for _, target := range result.Targets {
|
wantByTarget := make(map[artifact.Target]string, len(declarations))
|
||||||
if target.OS == "" || target.Arch == "" || strings.ContainsAny(target.OS+target.Arch, "/\\\r\n") {
|
for _, declaration := range declarations {
|
||||||
|
target := declaration.Target
|
||||||
|
if !artifact.ValidComponent(target.OS) || !artifact.ValidComponent(target.Arch) {
|
||||||
return nil, fmt.Errorf("build result contains an invalid target")
|
return nil, fmt.Errorf("build result contains an invalid target")
|
||||||
}
|
}
|
||||||
key := target.OS + "/" + target.Arch
|
if _, exists := wantByTarget[target]; exists {
|
||||||
if _, exists := wantByTarget[key]; exists {
|
return nil, fmt.Errorf("build result repeats target %q", target.OS+"/"+target.Arch)
|
||||||
return nil, fmt.Errorf("build result repeats target %q", key)
|
|
||||||
}
|
}
|
||||||
name := result.BinaryName + "-" + result.Version + "-" + target.OS + "-" + target.Arch
|
wantByTarget[target] = declaration.Filename
|
||||||
if target.OS == "windows" {
|
|
||||||
name += ".exe"
|
|
||||||
}
|
|
||||||
wantByTarget[key] = name
|
|
||||||
}
|
}
|
||||||
assets := make([]gitea.ExpectedAsset, 0, len(result.Artifacts)+1)
|
assets := make([]gitea.ExpectedAsset, 0, len(result.Artifacts)+1)
|
||||||
seenTargets := make(map[string]bool, len(result.Artifacts))
|
seenTargets := make(map[artifact.Target]bool, len(result.Artifacts))
|
||||||
for _, artifact := range result.Artifacts {
|
for _, artifact := range result.Artifacts {
|
||||||
key := artifact.Target.OS + "/" + artifact.Target.Arch
|
wantName, ok := wantByTarget[artifact.Target]
|
||||||
wantName, ok := wantByTarget[key]
|
if !ok || seenTargets[artifact.Target] || artifact.Filename != wantName {
|
||||||
if !ok || seenTargets[key] || artifact.Filename != wantName {
|
|
||||||
return nil, fmt.Errorf("build artifact %q does not match a selected target and its declared name", artifact.Filename)
|
return nil, fmt.Errorf("build artifact %q does not match a selected target and its declared name", artifact.Filename)
|
||||||
}
|
}
|
||||||
seenTargets[key] = true
|
seenTargets[artifact.Target] = true
|
||||||
if filepath.Base(artifact.Path) != artifact.Filename {
|
if filepath.Base(artifact.Path) != artifact.Filename {
|
||||||
return nil, fmt.Errorf("build artifact path %q does not end with filename %q", artifact.Path, artifact.Filename)
|
return nil, fmt.Errorf("build artifact path %q does not end with filename %q", artifact.Path, artifact.Filename)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,6 +61,23 @@ func TestDeclaredBuildAssetsRejectsTamperedArtifact(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDeclaredBuildAssetsRejectsArtifactDeclarationMismatch(t *testing.T) {
|
||||||
|
path := filepath.Join(t.TempDir(), "app-v1.2.3-linux-arm64")
|
||||||
|
contents := []byte("built application")
|
||||||
|
if err := os.WriteFile(path, contents, 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(contents),
|
||||||
|
}},
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "does not match a selected target") {
|
||||||
|
t.Fatalf("declaredBuildAssets() error = %v, want declaration mismatch", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestDeclaredBuildAssetsKeepsSourceOnlyBuildManifestFree(t *testing.T) {
|
func TestDeclaredBuildAssetsKeepsSourceOnlyBuildManifestFree(t *testing.T) {
|
||||||
assets, err := declaredBuildAssets(internalbuild.Result{})
|
assets, err := declaredBuildAssets(internalbuild.Result{})
|
||||||
if err != nil || len(assets) != 0 {
|
if err != nil || len(assets) != 0 {
|
||||||
|
|||||||
@@ -13,9 +13,11 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact"
|
||||||
)
|
)
|
||||||
|
|
||||||
const ManifestFilename = "SHA256SUMS"
|
const ManifestFilename = artifact.ManifestFilename
|
||||||
|
|
||||||
type AssetState string
|
type AssetState string
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user