Give delivery facade ownership of builds
This commit is contained in:
@@ -602,7 +602,7 @@ func main(){ if len(os.Args)>1 && os.Args[1]=="version" {fmt.Println("fixture",b
|
|||||||
}
|
}
|
||||||
buildGit(t, repository, "restore", "cmd/app/main.go")
|
buildGit(t, repository, "restore", "cmd/app/main.go")
|
||||||
buildGit(t, repository, "checkout", "--detach", "v1.2.3")
|
buildGit(t, repository, "checkout", "--detach", "v1.2.3")
|
||||||
delivered, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{RepositoryDir: repository, Product: "Fixture", Version: "v1.2.3", Build: &result})
|
delivered, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{RepositoryDir: repository, Product: "Fixture", Version: "v1.2.3", Prebuilt: &result})
|
||||||
if err == nil || !strings.Contains(err.Error(), "build source") || delivered.State != releasekit.DeliveryNotAttempted {
|
if err == nil || !strings.Contains(err.Error(), "build source") || delivered.State != releasekit.DeliveryNotAttempted {
|
||||||
t.Fatalf("dirty source delivered: %#v %v", delivered, err)
|
t.Fatalf("dirty source delivered: %#v %v", delivered, err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -225,7 +225,7 @@ func runDelivery(ctx context.Context, invocation cli.Invocation, stdout, stderr
|
|||||||
Destination: releasekit.GiteaDestination{
|
Destination: releasekit.GiteaDestination{
|
||||||
BaseURL: delivery.BaseURL, Owner: delivery.Owner, Repository: delivery.Repository, Token: token,
|
BaseURL: delivery.BaseURL, Owner: delivery.Owner, Repository: delivery.Repository, Token: token,
|
||||||
},
|
},
|
||||||
Build: build,
|
Prebuilt: build,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintf(stderr, "delivery state=%s version=%s commit=%s url=%s: %v\n", result.State, result.Version, result.CandidateCommit, result.ReleaseURL, err)
|
fmt.Fprintf(stderr, "delivery state=%s version=%s commit=%s url=%s: %v\n", result.State, result.Version, result.CandidateCommit, result.ReleaseURL, err)
|
||||||
|
|||||||
@@ -31,10 +31,10 @@ func TestReleaseWrapperPreservesNativeExitStatuses(t *testing.T) {
|
|||||||
}
|
}
|
||||||
updatedSources := map[string][]byte{}
|
updatedSources := map[string][]byte{}
|
||||||
for _, path := range []string{
|
for _, path := range []string{
|
||||||
"check.go", "publish.go", "internal/candidate/check.go", "internal/publish/publish.go",
|
"check.go", "publish.go", "delivery.go", "internal/candidate/check.go", "internal/delivery/delivery.go", "internal/publish/publish.go",
|
||||||
"build_test.go", "publish_test.go", "internal/candidate/check_test.go",
|
"build_test.go", "publish_test.go", "internal/candidate/check_test.go",
|
||||||
"internal/gitea/upload.go", "internal/gitea/upload_test.go",
|
"internal/gitea/upload.go", "internal/gitea/upload_test.go",
|
||||||
"cmd/releasekit/main_test.go", "cmd/releasekit/release_wrapper_test.go",
|
"delivery_test.go", "cmd/releasekit/main_test.go", "cmd/releasekit/release_wrapper_test.go",
|
||||||
"cmd/releasekit/internal/cli/config_test.go", "examples/releasekit.json",
|
"cmd/releasekit/internal/cli/config_test.go", "examples/releasekit.json",
|
||||||
} {
|
} {
|
||||||
contents, err := os.ReadFile(filepath.Join(root, path))
|
contents, err := os.ReadFile(filepath.Join(root, path))
|
||||||
|
|||||||
30
delivery.go
30
delivery.go
@@ -2,6 +2,7 @@ package releasekit
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
|
"io"
|
||||||
|
|
||||||
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build"
|
||||||
internaldelivery "gitea.maximumdirect.net/eric/ReleaseKit/internal/delivery"
|
internaldelivery "gitea.maximumdirect.net/eric/ReleaseKit/internal/delivery"
|
||||||
@@ -39,13 +40,21 @@ type DeliveryAsset struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// DeliveryRequest identifies a local tagged checkout and its explicit Gitea
|
// DeliveryRequest identifies a local tagged checkout and its explicit Gitea
|
||||||
// destination. Build is nil for page-only delivery.
|
// destination. Build selects a managed build; Prebuilt supplies caller-owned
|
||||||
|
// build output instead. They cannot both be set. Either may be nil for
|
||||||
|
// page-only delivery when both are nil. Build with no targets, or an empty
|
||||||
|
// Prebuilt result, also performs page-only delivery.
|
||||||
type DeliveryRequest struct {
|
type DeliveryRequest struct {
|
||||||
RepositoryDir string
|
RepositoryDir string
|
||||||
Product string
|
Product string
|
||||||
Version string
|
Version string
|
||||||
Destination GiteaDestination
|
Destination GiteaDestination
|
||||||
Build *BuildResult
|
// Build selects binaries to build in temporary storage outside the checkout.
|
||||||
|
Build *BuildSpec
|
||||||
|
// Prebuilt supplies an existing build whose files remain caller-owned.
|
||||||
|
Prebuilt *BuildResult
|
||||||
|
// Output receives progress from a managed build.
|
||||||
|
Output io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeliveryResult reports hosted content and the state reached by this
|
// DeliveryResult reports hosted content and the state reached by this
|
||||||
@@ -70,23 +79,28 @@ type DeliveryResult struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Deliver verifies the selected local tag and hosted source, then creates or
|
// Deliver verifies the selected local tag and hosted source, then creates or
|
||||||
// finds its release page and adds any missing declared build assets.
|
// finds its release page and adds any missing declared build assets. Build
|
||||||
|
// requests are compiled into temporary storage that Deliver removes on return.
|
||||||
|
// Prebuilt artifacts remain caller-owned and are not removed. Both paths
|
||||||
|
// validate source identity and artifact contents before hosted writes.
|
||||||
func Deliver(ctx context.Context, request DeliveryRequest) (DeliveryResult, error) {
|
func Deliver(ctx context.Context, request DeliveryRequest) (DeliveryResult, error) {
|
||||||
result := DeliveryResult{Version: request.Version, State: DeliveryNotAttempted}
|
result := DeliveryResult{Version: request.Version, State: DeliveryNotAttempted}
|
||||||
var build *internalbuild.Result
|
|
||||||
if request.Build != nil {
|
|
||||||
build = internalBuildResult(*request.Build)
|
|
||||||
}
|
|
||||||
destination := gitea.Destination{
|
destination := gitea.Destination{
|
||||||
BaseURL: request.Destination.BaseURL, Owner: request.Destination.Owner,
|
BaseURL: request.Destination.BaseURL, Owner: request.Destination.Owner,
|
||||||
Repository: request.Destination.Repository, Token: request.Destination.Token,
|
Repository: request.Destination.Repository, Token: request.Destination.Token,
|
||||||
}
|
}
|
||||||
|
var prebuilt *internalbuild.Result
|
||||||
|
if request.Prebuilt != nil {
|
||||||
|
prebuilt = internalBuildResult(*request.Prebuilt)
|
||||||
|
}
|
||||||
internalResult, err := internaldelivery.Deliver(ctx, internaldelivery.Request{
|
internalResult, err := internaldelivery.Deliver(ctx, internaldelivery.Request{
|
||||||
RepositoryDir: request.RepositoryDir,
|
RepositoryDir: request.RepositoryDir,
|
||||||
Product: request.Product,
|
Product: request.Product,
|
||||||
Version: request.Version,
|
Version: request.Version,
|
||||||
Destination: destination,
|
Destination: destination,
|
||||||
Build: build,
|
Build: internalBuildSpec(request.Build),
|
||||||
|
Prebuilt: prebuilt,
|
||||||
|
Output: request.Output,
|
||||||
})
|
})
|
||||||
result.CandidateCommit = internalResult.CandidateCommit
|
result.CandidateCommit = internalResult.CandidateCommit
|
||||||
result.ReleaseURL = internalResult.ReleaseURL
|
result.ReleaseURL = internalResult.ReleaseURL
|
||||||
|
|||||||
490
delivery_test.go
Normal file
490
delivery_test.go
Normal file
@@ -0,0 +1,490 @@
|
|||||||
|
package releasekit_test
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"net/http/httptest"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"testing"
|
||||||
|
|
||||||
|
releasekit "gitea.maximumdirect.net/eric/ReleaseKit"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
temporaryRoot := t.TempDir()
|
||||||
|
t.Setenv("TMPDIR", temporaryRoot)
|
||||||
|
server, remote := newFacadeDeliveryServer(t, repository)
|
||||||
|
defer server.Close()
|
||||||
|
var progress strings.Builder
|
||||||
|
|
||||||
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Fixture",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Destination: facadeDeliveryDestination(server.URL),
|
||||||
|
Build: &releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "fixture",
|
||||||
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||||
|
},
|
||||||
|
Output: &progress,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Deliver() error = %v; result = %#v", err, result)
|
||||||
|
}
|
||||||
|
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != remote.commit {
|
||||||
|
t.Fatalf("delivery result = %#v", result)
|
||||||
|
}
|
||||||
|
if len(result.Uploaded) != 2 || len(remote.assets) != 2 {
|
||||||
|
t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(remote.assets))
|
||||||
|
}
|
||||||
|
if !strings.Contains(progress.String(), "checking fixture version output") {
|
||||||
|
t.Fatalf("build progress = %q", progress.String())
|
||||||
|
}
|
||||||
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
||||||
|
t.Fatalf("managed build output remains: entries=%v error=%v", entries, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverPrebuiltArtifactsRemainCallerOwned(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
outputDir := filepath.Join(t.TempDir(), "caller-owned-assets")
|
||||||
|
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: outputDir,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("Build() error = %v", err)
|
||||||
|
}
|
||||||
|
server, _ := 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 || result.State != releasekit.DeliveryComplete {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
for _, artifact := range prebuilt.Artifacts {
|
||||||
|
if _, err := os.Stat(artifact.Path); err != nil {
|
||||||
|
t.Errorf("caller-owned artifact %q was removed: %v", artifact.Path, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) {
|
||||||
|
for _, test := range []struct {
|
||||||
|
name string
|
||||||
|
build *releasekit.BuildSpec
|
||||||
|
prebuilt *releasekit.BuildResult
|
||||||
|
}{
|
||||||
|
{name: "no build"},
|
||||||
|
{name: "empty target set", build: &releasekit.BuildSpec{}},
|
||||||
|
{name: "empty prebuilt result", prebuilt: &releasekit.BuildResult{}},
|
||||||
|
} {
|
||||||
|
t.Run(test.name, func(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
temporaryRoot := t.TempDir()
|
||||||
|
t.Setenv("TMPDIR", temporaryRoot)
|
||||||
|
server, remote := newFacadeDeliveryServer(t, repository)
|
||||||
|
defer server.Close()
|
||||||
|
var progress strings.Builder
|
||||||
|
|
||||||
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Fixture",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Destination: facadeDeliveryDestination(server.URL),
|
||||||
|
Build: test.build,
|
||||||
|
Prebuilt: test.prebuilt,
|
||||||
|
Output: &progress,
|
||||||
|
})
|
||||||
|
if err != nil || result.State != releasekit.DeliveryComplete {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v", result, err)
|
||||||
|
}
|
||||||
|
if len(remote.assets) != 0 || len(result.Uploaded) != 0 || len(result.Pending) != 0 {
|
||||||
|
t.Fatalf("page-only delivery included assets: result=%#v assets=%v", result, mapKeys(remote.assets))
|
||||||
|
}
|
||||||
|
if progress.Len() != 0 {
|
||||||
|
t.Fatalf("page-only delivery emitted build progress: %q", progress.String())
|
||||||
|
}
|
||||||
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
||||||
|
t.Fatalf("page-only delivery allocated temporary output: entries=%v error=%v", entries, err)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverRejectsDirtyTaggedSourceBeforeBuildOrHostedWrites(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
writeBuildFixture(t, repository, "local-change.txt", "dirty source")
|
||||||
|
temporaryRoot := t.TempDir()
|
||||||
|
t.Setenv("TMPDIR", temporaryRoot)
|
||||||
|
server, remote := newFacadeDeliveryServer(t, repository)
|
||||||
|
defer server.Close()
|
||||||
|
var progress strings.Builder
|
||||||
|
|
||||||
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Fixture",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Destination: facadeDeliveryDestination(server.URL),
|
||||||
|
Build: &releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "fixture",
|
||||||
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||||
|
},
|
||||||
|
Output: &progress,
|
||||||
|
})
|
||||||
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v; want preflight failure for known commit", result, err)
|
||||||
|
}
|
||||||
|
if progress.Len() != 0 {
|
||||||
|
t.Fatalf("build started before source eligibility passed: %q", progress.String())
|
||||||
|
}
|
||||||
|
if remote.requests != 0 {
|
||||||
|
t.Fatalf("hosted requests = %d, want none", remote.requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverRejectsCheckoutAwayFromTagBeforeBuildOrHostedWrites(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
writeBuildFixture(t, repository, "later-change.txt", "new commit")
|
||||||
|
buildGit(t, repository, "add", "--all")
|
||||||
|
buildGit(t, repository, "commit", "-m", "advance checkout after release tag")
|
||||||
|
temporaryRoot := t.TempDir()
|
||||||
|
t.Setenv("TMPDIR", temporaryRoot)
|
||||||
|
server, remote := newFacadeDeliveryServer(t, repository)
|
||||||
|
defer server.Close()
|
||||||
|
var progress strings.Builder
|
||||||
|
|
||||||
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Fixture",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Destination: facadeDeliveryDestination(server.URL),
|
||||||
|
Build: &releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "fixture",
|
||||||
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||||
|
},
|
||||||
|
Output: &progress,
|
||||||
|
})
|
||||||
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit == remote.commit {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v; want tag/HEAD mismatch", result, err)
|
||||||
|
}
|
||||||
|
if progress.Len() != 0 {
|
||||||
|
t.Fatalf("build started before tag/HEAD agreement: %q", progress.String())
|
||||||
|
}
|
||||||
|
if remote.requests != 0 {
|
||||||
|
t.Fatalf("hosted requests = %d, want none", remote.requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverRejectsInvalidCommittedNoteBeforeBuildOrHostedWrites(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumerWithNote(t, "# Different Product v1.2.3\n\nWrong product.\n")
|
||||||
|
temporaryRoot := t.TempDir()
|
||||||
|
t.Setenv("TMPDIR", temporaryRoot)
|
||||||
|
server, remote := newFacadeDeliveryServer(t, repository)
|
||||||
|
defer server.Close()
|
||||||
|
var progress strings.Builder
|
||||||
|
|
||||||
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Fixture",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Destination: facadeDeliveryDestination(server.URL),
|
||||||
|
Build: &releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "fixture",
|
||||||
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||||
|
},
|
||||||
|
Output: &progress,
|
||||||
|
})
|
||||||
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v; want committed-note preflight failure", result, err)
|
||||||
|
}
|
||||||
|
if progress.Len() != 0 {
|
||||||
|
t.Fatalf("build started before committed-note validation: %q", progress.String())
|
||||||
|
}
|
||||||
|
if remote.requests != 0 {
|
||||||
|
t.Fatalf("hosted requests = %d, want none", remote.requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverManagedBuildRejectsCheckoutMutationBeforeHostedWrites(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
temporaryRoot := t.TempDir()
|
||||||
|
t.Setenv("TMPDIR", temporaryRoot)
|
||||||
|
server, remote := newFacadeDeliveryServer(t, repository)
|
||||||
|
defer server.Close()
|
||||||
|
writer := &sourceChangeWriter{change: func() {
|
||||||
|
source, err := os.ReadFile(filepath.Join(repository, "cmd/app/main.go"))
|
||||||
|
if err != nil {
|
||||||
|
t.Errorf("read application source: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
writeBuildFixture(t, repository, "cmd/app/main.go", string(source)+"\n// changed during delivery build\n")
|
||||||
|
}}
|
||||||
|
|
||||||
|
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Fixture",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Destination: facadeDeliveryDestination(server.URL),
|
||||||
|
Build: &releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "fixture",
|
||||||
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||||
|
},
|
||||||
|
Output: writer,
|
||||||
|
})
|
||||||
|
if err == nil || !strings.Contains(err.Error(), "build source") || result.State != releasekit.DeliveryNotAttempted {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v; want build-source eligibility failure", result, err)
|
||||||
|
}
|
||||||
|
if remote.requests != 0 {
|
||||||
|
t.Fatalf("hosted requests = %d, want none", remote.requests)
|
||||||
|
}
|
||||||
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
||||||
|
t.Fatalf("ineligible managed build output remains: entries=%v error=%v", entries, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
temporaryRoot := t.TempDir()
|
||||||
|
t.Setenv("TMPDIR", temporaryRoot)
|
||||||
|
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),
|
||||||
|
Build: &releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/missing",
|
||||||
|
BinaryName: "fixture",
|
||||||
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||||
|
},
|
||||||
|
})
|
||||||
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v; want managed build failure before hosted writes", result, err)
|
||||||
|
}
|
||||||
|
if remote.requests != 0 {
|
||||||
|
t.Fatalf("hosted requests = %d, want none", remote.requests)
|
||||||
|
}
|
||||||
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
||||||
|
t.Fatalf("failed managed build output remains: entries=%v error=%v", entries, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverManagedBuildCancellationCleansTemporaryOutput(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
temporaryRoot := t.TempDir()
|
||||||
|
t.Setenv("TMPDIR", temporaryRoot)
|
||||||
|
server, remote := newFacadeDeliveryServer(t, repository)
|
||||||
|
defer server.Close()
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
defer cancel()
|
||||||
|
writer := deliveryCancelWriter{cancel: cancel}
|
||||||
|
|
||||||
|
result, err := releasekit.Deliver(ctx, releasekit.DeliveryRequest{
|
||||||
|
RepositoryDir: repository,
|
||||||
|
Product: "Fixture",
|
||||||
|
Version: "v1.2.3",
|
||||||
|
Destination: facadeDeliveryDestination(server.URL),
|
||||||
|
Build: &releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/app",
|
||||||
|
BinaryName: "fixture",
|
||||||
|
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
|
||||||
|
},
|
||||||
|
Output: writer,
|
||||||
|
})
|
||||||
|
if !errors.Is(err, context.Canceled) || result.State != releasekit.DeliveryNotAttempted {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v; want cancellation before hosted writes", result, err)
|
||||||
|
}
|
||||||
|
if remote.requests != 0 {
|
||||||
|
t.Fatalf("hosted requests = %d, want none", remote.requests)
|
||||||
|
}
|
||||||
|
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
|
||||||
|
t.Fatalf("canceled managed build output remains: entries=%v error=%v", entries, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestDeliverRejectsBothBuildSourcesBeforeHostedWrites(t *testing.T) {
|
||||||
|
repository := newTaggedDeliveryConsumer(t)
|
||||||
|
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),
|
||||||
|
Build: &releasekit.BuildSpec{},
|
||||||
|
Prebuilt: &releasekit.BuildResult{},
|
||||||
|
})
|
||||||
|
if err == nil || result.State != releasekit.DeliveryNotAttempted || !strings.Contains(err.Error(), "both") {
|
||||||
|
t.Fatalf("Deliver() = %#v, %v; want invalid build-source combination", result, err)
|
||||||
|
}
|
||||||
|
if remote.requests != 0 {
|
||||||
|
t.Fatalf("hosted requests = %d, want none", remote.requests)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTaggedDeliveryConsumer(t *testing.T) string {
|
||||||
|
return newTaggedDeliveryConsumerWithNote(t, "# Fixture v1.2.3\n\nDelivery fixture.\n")
|
||||||
|
}
|
||||||
|
|
||||||
|
func newTaggedDeliveryConsumerWithNote(t *testing.T, note string) string {
|
||||||
|
t.Helper()
|
||||||
|
repository := newBuildConsumer(t, `package main
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo"
|
||||||
|
)
|
||||||
|
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("fixture", buildinfo.Version()) } }
|
||||||
|
`)
|
||||||
|
writeBuildFixture(t, repository, "docs/releases/v1.2.3.md", note)
|
||||||
|
buildGit(t, repository, "add", "--all")
|
||||||
|
buildGit(t, repository, "commit", "-m", "add release note")
|
||||||
|
buildGit(t, repository, "tag", "v1.2.3")
|
||||||
|
return repository
|
||||||
|
}
|
||||||
|
|
||||||
|
func facadeDeliveryDestination(baseURL string) releasekit.GiteaDestination {
|
||||||
|
return releasekit.GiteaDestination{BaseURL: baseURL, Owner: "owner", Repository: "example", Token: "fixture-token"}
|
||||||
|
}
|
||||||
|
|
||||||
|
type deliveryCancelWriter struct{ cancel context.CancelFunc }
|
||||||
|
|
||||||
|
func (writer deliveryCancelWriter) Write(content []byte) (int, error) {
|
||||||
|
writer.cancel()
|
||||||
|
return len(content), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
type facadeDeliveryServer struct {
|
||||||
|
t *testing.T
|
||||||
|
commit string
|
||||||
|
page bool
|
||||||
|
requests int
|
||||||
|
assets map[string][]byte
|
||||||
|
url string
|
||||||
|
}
|
||||||
|
|
||||||
|
func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) {
|
||||||
|
t.Helper()
|
||||||
|
state := &facadeDeliveryServer{t: t, commit: buildGit(t, repository, "rev-parse", "HEAD"), assets: make(map[string][]byte)}
|
||||||
|
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP))
|
||||||
|
state.url = server.URL
|
||||||
|
return server, state
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Request) {
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/api/") {
|
||||||
|
state.requests++
|
||||||
|
if r.Header.Get("Authorization") != "token fixture-token" {
|
||||||
|
state.t.Errorf("authorization = %q", r.Header.Get("Authorization"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
switch r.URL.Path {
|
||||||
|
case "/api/v1/repos/owner/example/tags/v1.2.3":
|
||||||
|
_, _ = fmt.Fprintf(w, `{"name":"v1.2.3","commit":{"sha":%q}}`, state.commit)
|
||||||
|
case "/api/v1/repos/owner/example/releases/tags/v1.2.3":
|
||||||
|
if !state.page {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.writeRelease(w)
|
||||||
|
case "/api/v1/repos/owner/example/releases":
|
||||||
|
var payload struct {
|
||||||
|
Tag string `json:"tag_name"`
|
||||||
|
Target string `json:"target_commitish"`
|
||||||
|
Name string `json:"name"`
|
||||||
|
Body string `json:"body"`
|
||||||
|
}
|
||||||
|
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
|
||||||
|
state.t.Errorf("decode release payload: %v", err)
|
||||||
|
http.Error(w, "invalid payload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if payload.Tag != "v1.2.3" || payload.Target != state.commit || payload.Name != "Fixture v1.2.3" {
|
||||||
|
state.t.Errorf("release payload = %#v", payload)
|
||||||
|
}
|
||||||
|
state.page = true
|
||||||
|
state.writeRelease(w)
|
||||||
|
case "/api/v1/repos/owner/example/releases/10/assets":
|
||||||
|
if r.Method == http.MethodGet {
|
||||||
|
list := make([]map[string]any, 0, len(state.assets))
|
||||||
|
for name, content := range state.assets {
|
||||||
|
list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": state.url + "/files/" + name})
|
||||||
|
}
|
||||||
|
_ = json.NewEncoder(w).Encode(list)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
name := r.URL.Query().Get("name")
|
||||||
|
file, _, err := r.FormFile("attachment")
|
||||||
|
if err != nil {
|
||||||
|
state.t.Errorf("read upload %q: %v", name, err)
|
||||||
|
http.Error(w, "invalid multipart upload", http.StatusBadRequest)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
content, err := io.ReadAll(file)
|
||||||
|
_ = file.Close()
|
||||||
|
if err != nil {
|
||||||
|
state.t.Errorf("read upload %q: %v", name, err)
|
||||||
|
}
|
||||||
|
state.assets[name] = content
|
||||||
|
w.WriteHeader(http.StatusCreated)
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{"id": len(state.assets), "name": name, "size": len(content)})
|
||||||
|
default:
|
||||||
|
if strings.HasPrefix(r.URL.Path, "/files/") {
|
||||||
|
name := strings.TrimPrefix(r.URL.Path, "/files/")
|
||||||
|
content, ok := state.assets[name]
|
||||||
|
if !ok {
|
||||||
|
http.NotFound(w, r)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_, _ = w.Write(content)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
state.t.Errorf("unexpected delivery request %s %s", r.Method, r.URL)
|
||||||
|
http.NotFound(w, r)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (state *facadeDeliveryServer) writeRelease(w http.ResponseWriter) {
|
||||||
|
w.Header().Set("Content-Type", "application/json")
|
||||||
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
|
"id": 10, "html_url": state.url + "/releases/10", "tag_name": "v1.2.3",
|
||||||
|
"target_commitish": state.commit, "name": "Fixture v1.2.3",
|
||||||
|
"body": "# Fixture v1.2.3\n\nDelivery fixture.\n", "draft": false, "prerelease": false,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func mapKeys(values map[string][]byte) []string {
|
||||||
|
keys := make([]string, 0, len(values))
|
||||||
|
for key := range values {
|
||||||
|
keys = append(keys, key)
|
||||||
|
}
|
||||||
|
return keys
|
||||||
|
}
|
||||||
@@ -62,13 +62,20 @@ The command does not run candidate checks or publish tags.
|
|||||||
|
|
||||||
## Go API
|
## Go API
|
||||||
|
|
||||||
Pass the Gitea credential explicitly. A `BuildResult` may be supplied for a
|
Pass the Gitea credential explicitly. Set `Build` to a `BuildSpec` to compile
|
||||||
binary-producing release; it must identify the same tagged repository commit
|
selected targets as part of delivery. The facade verifies the local tag, clean
|
||||||
and selected version, its `SourceVerified` field must confirm the completed
|
checkout, and committed release note before compiling. Managed output is stored
|
||||||
build's clean, unchanged source observations, and its local artifacts must still
|
outside the checkout and removed when delivery returns. `Output` receives build
|
||||||
match their digests. Cleaning the checkout after a dirty build does not make
|
progress. With no build, or with an empty target set, delivery is page-only.
|
||||||
that build eligible for delivery.
|
|
||||||
Leave `Build` nil for a source-only release.
|
To deliver binaries built separately, set `Prebuilt` to the `BuildResult`.
|
||||||
|
ReleaseKit checks that it identifies the selected repository, tagged commit, and
|
||||||
|
version, that its source was verified clean and unchanged during compilation,
|
||||||
|
and that the current files still match their recorded digests. The files remain
|
||||||
|
caller-owned, so callers can retain the original bytes for retries and are
|
||||||
|
responsible for cleanup. With neither artifact source, an empty managed target
|
||||||
|
set, or an empty prebuilt result, delivery is page-only. Do not set both `Build`
|
||||||
|
and `Prebuilt` in one request.
|
||||||
|
|
||||||
```go
|
```go
|
||||||
result, err := releasekit.Deliver(ctx, releasekit.DeliveryRequest{
|
result, err := releasekit.Deliver(ctx, releasekit.DeliveryRequest{
|
||||||
@@ -81,6 +88,11 @@ result, err := releasekit.Deliver(ctx, releasekit.DeliveryRequest{
|
|||||||
Repository: "example",
|
Repository: "example",
|
||||||
Token: token,
|
Token: token,
|
||||||
},
|
},
|
||||||
|
Build: &releasekit.BuildSpec{
|
||||||
|
MainPackage: "./cmd/example",
|
||||||
|
BinaryName: "example",
|
||||||
|
Targets: []releasekit.Target{{OS: "linux", Arch: "amd64"}},
|
||||||
|
},
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
// Keep the returned state and any completed asset identities for recovery.
|
// Keep the returned state and any completed asset identities for recovery.
|
||||||
|
|||||||
@@ -38,7 +38,7 @@ these implemented packages:
|
|||||||
| `internal/artifact` | Shared build-target normalization, artifact declarations, filename rules, and checksum-manifest naming. |
|
| `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-source and release-note checks, managed builds in disposable storage outside the checkout, prebuilt artifact validation, manifest assembly, and hosted delivery coordination. |
|
||||||
| `internal/git` | Git command boundary, push-destination resolution, exact ref inspection, and tag operations, including captured-endpoint verification during recovery. |
|
| `internal/git` | Git command boundary, push-destination resolution, exact ref inspection, and tag operations, including captured-endpoint verification during recovery. |
|
||||||
| `internal/gitea` | Gitea API transport, hosted release pages, asset inspection, manifests, and uploads. |
|
| `internal/gitea` | Gitea API transport, hosted release pages, asset inspection, manifests, and uploads. |
|
||||||
| `internal/gotool` | Effective Go configuration discovery and shared native-validation and static-build environments; see [Go command environments](gotool.md). |
|
| `internal/gotool` | Effective Go configuration discovery and shared native-validation and static-build environments; see [Go command environments](gotool.md). |
|
||||||
@@ -69,7 +69,7 @@ not a separate release matrix:
|
|||||||
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
| Authoritative endpoint and exact annotated tag publication | `releasekit.Publish`, `internal/publish`, `internal/git` | `TestPublishPushesOnlyTheCapturedAnnotatedTag`, `TestResolveUsesPushURLAndQueriesWithoutMutation` |
|
||||||
| Publication partial and uncertain recovery | `internal/publish`, `internal/git` | `TestPublishRejectedPushKeepsLocalTagAndReportsNotPublished`, `TestPublishLostAcknowledgmentConfirmsExactRemoteObject`, `TestPublishLostAcknowledgmentWithChangedEndpointRemainsUnknown`, `TestPublishUnavailableReconciliationRemainsUnknownAndPreservesPushError` |
|
| 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` |
|
| Cross-builds, version smoke checks, empty target sets, and partial outputs | `releasekit.Build`, `internal/build`, `buildinfo` | `TestBuildCreatesSelectedTargetsAndInjectsVersion`, `TestBuildWithNoTargetsDoesNotInvokeCompiler`, `TestBuildReturnsPartialArtifactsAfterTargetFailure` |
|
||||||
| Page-only and binary tagged-source delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlyCreatesAndReusesRelease`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
|
| Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` |
|
||||||
| Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` |
|
| 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`, `TestWoodpeckerDeliveryCommandSequences`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout` |
|
| CLI profiles and prepared Woodpecker sequences | `cmd/releasekit`, `cmd/releasekit/internal/cli`, `examples/woodpecker/` | `TestLoadSettingsRejectsInvalidProfiles`, `TestWoodpeckerDeliveryCommandSequences`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout` |
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
# ReleaseKit audit implementation plan
|
# ReleaseKit audit implementation plan
|
||||||
|
|
||||||
Status: Stages 1–5 complete; Stage 6 next.
|
Status: Stages 1–6 complete; Stage 7 next.
|
||||||
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.
|
||||||
@@ -329,6 +329,17 @@ on success and failure. Extend existing mutation/cancellation coverage where
|
|||||||
needed. Preserve prebuilt retry, digest conflict, note conflict, and source
|
needed. Preserve prebuilt retry, digest conflict, note conflict, and source
|
||||||
eligibility coverage. Confirm the two facades' dependency boundaries.
|
eligibility coverage. Confirm the two facades' dependency boundaries.
|
||||||
|
|
||||||
|
**Completed:** `Deliver` now owns optional managed builds, page-only requests,
|
||||||
|
and explicit prebuilt artifact delivery behind the public facade. Managed output
|
||||||
|
uses temporary storage outside the checkout and is cleaned after success,
|
||||||
|
failure, and cancellation; source and note checks precede compilation, and
|
||||||
|
eligibility is rechecked before hosted writes. Prebuilt files remain
|
||||||
|
caller-owned. Consumer and internal package guides describe these paths.
|
||||||
|
Facade-level consumer and HTTP tests cover managed upload, page-only delivery,
|
||||||
|
preflight and build failures, checkout mutation, cleanup, cancellation, and
|
||||||
|
prebuilt ownership. The full offline suite, API boundary check, and
|
||||||
|
documentation whitespace checks pass.
|
||||||
|
|
||||||
## Stage 7 — Route ordinary CLI delivery through the facade
|
## Stage 7 — Route ordinary CLI delivery through the facade
|
||||||
|
|
||||||
**Covers:** the CLI portion of F5. **Primary owners:**
|
**Covers:** the CLI portion of F5. **Primary owners:**
|
||||||
|
|||||||
@@ -4,6 +4,8 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -11,6 +13,7 @@ import (
|
|||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea"
|
||||||
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir"
|
||||||
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -29,7 +32,9 @@ type Request struct {
|
|||||||
Product string
|
Product string
|
||||||
Version string
|
Version string
|
||||||
Destination gitea.Destination
|
Destination gitea.Destination
|
||||||
Build *internalbuild.Result
|
Build *internalbuild.Spec
|
||||||
|
Prebuilt *internalbuild.Result
|
||||||
|
Output io.Writer
|
||||||
}
|
}
|
||||||
|
|
||||||
type Result struct {
|
type Result struct {
|
||||||
@@ -51,6 +56,9 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
|||||||
if strings.TrimSpace(request.Product) == "" {
|
if strings.TrimSpace(request.Product) == "" {
|
||||||
return result, errors.New("product is required")
|
return result, errors.New("product is required")
|
||||||
}
|
}
|
||||||
|
if request.Build != nil && request.Prebuilt != nil {
|
||||||
|
return result, errors.New("delivery cannot use both a managed build and prebuilt artifacts")
|
||||||
|
}
|
||||||
repositoryDir, err := filepath.Abs(request.RepositoryDir)
|
repositoryDir, err := filepath.Abs(request.RepositoryDir)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return result, fmt.Errorf("resolve repository directory: %w", err)
|
return result, fmt.Errorf("resolve repository directory: %w", err)
|
||||||
@@ -66,21 +74,6 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
|||||||
if source.Head != source.TagCommit {
|
if source.Head != source.TagCommit {
|
||||||
return result, fmt.Errorf("checkout HEAD %s does not match tag %s commit %s", source.Head, request.Version, source.TagCommit)
|
return result, fmt.Errorf("checkout HEAD %s does not match tag %s commit %s", source.Head, request.Version, source.TagCommit)
|
||||||
}
|
}
|
||||||
var assets []gitea.ExpectedAsset
|
|
||||||
if request.Build != nil {
|
|
||||||
assets, err = declaredBuildAssets(*request.Build)
|
|
||||||
if err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := validateBuild(repositoryDir, source.TagCommit, request.Version, request.Build, assets); err != nil {
|
|
||||||
return result, err
|
|
||||||
}
|
|
||||||
if len(assets) > 0 {
|
|
||||||
if err := gitea.VerifyLocalAssets(assets); err != nil {
|
|
||||||
return result, fmt.Errorf("verify build assets: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
notePath := "docs/releases/" + request.Version + ".md"
|
notePath := "docs/releases/" + request.Version + ".md"
|
||||||
note, err := git.ReadCommitFile(ctx, repositoryDir, source.TagCommit, notePath)
|
note, err := git.ReadCommitFile(ctx, repositoryDir, source.TagCommit, notePath)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -90,6 +83,53 @@ func Deliver(ctx context.Context, request Request) (Result, error) {
|
|||||||
return result, fmt.Errorf("release note %q: %w", notePath, err)
|
return result, fmt.Errorf("release note %q: %w", notePath, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
build := request.Prebuilt
|
||||||
|
if request.Build != nil && len(request.Build.Targets) != 0 {
|
||||||
|
temporaryDir, err := tempdir.Outside(repositoryDir, "releasekit-delivery-build-*")
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("create temporary delivery build directory: %w", err)
|
||||||
|
}
|
||||||
|
defer os.RemoveAll(temporaryDir)
|
||||||
|
built, err := internalbuild.Build(ctx, internalbuild.Request{
|
||||||
|
RepositoryDir: repositoryDir,
|
||||||
|
Version: request.Version,
|
||||||
|
Spec: *request.Build,
|
||||||
|
OutputDir: filepath.Join(temporaryDir, "assets"),
|
||||||
|
Output: request.Output,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("build delivery assets: %w", err)
|
||||||
|
}
|
||||||
|
build = &built
|
||||||
|
}
|
||||||
|
var assets []gitea.ExpectedAsset
|
||||||
|
if build != nil {
|
||||||
|
assets, err = declaredBuildAssets(*build)
|
||||||
|
if err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := validateBuild(repositoryDir, source.TagCommit, request.Version, build, assets); err != nil {
|
||||||
|
return result, err
|
||||||
|
}
|
||||||
|
if len(assets) > 0 {
|
||||||
|
if err := gitea.VerifyLocalAssets(assets); err != nil {
|
||||||
|
return result, fmt.Errorf("verify build assets: %w", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if build != nil && len(build.Targets) != 0 {
|
||||||
|
current, err := git.InspectTaggedSource(ctx, repositoryDir, request.Version)
|
||||||
|
if err != nil {
|
||||||
|
return result, fmt.Errorf("recheck tagged source after build: %w", err)
|
||||||
|
}
|
||||||
|
if current.Status != "" {
|
||||||
|
return result, fmt.Errorf("tagged checkout changed during build:\n%s", current.Status)
|
||||||
|
}
|
||||||
|
if current.Head != source.TagCommit || current.TagCommit != source.TagCommit {
|
||||||
|
return result, errors.New("tagged source changed during build")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
releaseResult, err := (gitea.Client{}).EnsureRelease(ctx, gitea.Request{
|
releaseResult, err := (gitea.Client{}).EnsureRelease(ctx, gitea.Request{
|
||||||
Destination: request.Destination,
|
Destination: request.Destination,
|
||||||
Version: request.Version,
|
Version: request.Version,
|
||||||
|
|||||||
Reference in New Issue
Block a user