13 Commits
v0.4.0 ... main

81 changed files with 898 additions and 554 deletions

View File

@@ -8,6 +8,10 @@ tagged-source delivery API, and the `version`, `check`, `publish`, `build`,
maintainer procedure and prepared Woodpecker workflow for hosted note-page maintainer procedure and prepared Woodpecker workflow for hosted note-page
delivery are documented. ReleaseKit does not declare binary deliverables. delivery are documented. ReleaseKit does not declare binary deliverables.
The Go module is `go.maximumdirect.net/releasekit`. The
[adoption guide](docs/consumers/adoption.md#migrating-from-the-original-module-path)
explains the v0.5.0 migration from the original Gitea module path.
Release operations can select one Go module within a repository. The repository Release operations can select one Go module within a repository. The repository
root remains the default; a selected module's declared import path and physical root remains the default; a selected module's declared import path and physical
directory determine its complete Git tag and module-local release note. See the directory determine its complete Git tag and module-local release note. See the

View File

@@ -5,11 +5,11 @@ import (
"fmt" "fmt"
"io" "io"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact" "go.maximumdirect.net/releasekit/internal/artifact"
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" internalbuild "go.maximumdirect.net/releasekit/internal/build"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/releaseidentity"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
) )
// Target identifies one explicit Go operating-system and architecture pair. // Target identifies one explicit Go operating-system and architecture pair.

View File

@@ -15,7 +15,7 @@ import (
"testing" "testing"
"time" "time"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
) )
func TestBuildPreservesInvalidVersionAndDeclarationFailureEvidence(t *testing.T) { func TestBuildPreservesInvalidVersionAndDeclarationFailureEvidence(t *testing.T) {
@@ -48,7 +48,7 @@ import (
"fmt" "fmt"
"os" "os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { func main() {
@@ -127,7 +127,7 @@ func TestBuildUsesGoHostDespiteAmbientTargetEnvironment(t *testing.T) {
import ( import (
"fmt" "fmt"
"os" "os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { func main() {
@@ -159,7 +159,7 @@ func TestBuildIgnoresAmbientAndPersistentArchitectureTuning(t *testing.T) {
import ( import (
"fmt" "fmt"
"os" "os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixture %s\n", buildinfo.Version()) } } func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixture %s\n", buildinfo.Version()) } }
`) `)
@@ -272,7 +272,7 @@ func TestBuildUsesNestedWorkingTreeModuleAndModuleRelativeOutput(t *testing.T) {
import ( import (
"fmt" "fmt"
"os" "os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { func main() {
if len(os.Args) == 2 && os.Args[1] == "version" { if len(os.Args) == 2 && os.Args[1] == "version" {
@@ -406,7 +406,7 @@ func TestBuildResolvesDirtyWorkingTreeDeclaration(t *testing.T) {
func TestBuildRegeneratesOwnedOutputsAndPreservesUnrelatedFiles(t *testing.T) { func TestBuildRegeneratesOwnedOutputsAndPreservesUnrelatedFiles(t *testing.T) {
const source = `package main const source = `package main
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo") import("fmt";"go.maximumdirect.net/releasekit/buildinfo")
func main(){fmt.Println("fixture",buildinfo.Version())} func main(){fmt.Println("fixture",buildinfo.Version())}
` `
repository := newBuildConsumer(t, source) repository := newBuildConsumer(t, source)
@@ -616,7 +616,7 @@ func TestCheckBuildsAfterValidationAndKeepsOutputOutsideWorktree(t *testing.T) {
import ( import (
"fmt" "fmt"
"os" "os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixture %s\n", buildinfo.Version()) } } func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Printf("fixture %s\n", buildinfo.Version()) } }
`) `)
@@ -713,7 +713,7 @@ func newBuildConsumer(t *testing.T, mainSource string) string {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
module := fmt.Sprintf("module example.com/buildfixture\n\ngo 1.26.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.0.0\n\nreplace gitea.maximumdirect.net/eric/ReleaseKit => %s\n", filepath.ToSlash(root)) module := fmt.Sprintf("module example.com/buildfixture\n\ngo 1.26.0\n\nrequire go.maximumdirect.net/releasekit v0.0.0\n\nreplace go.maximumdirect.net/releasekit => %s\n", filepath.ToSlash(root))
writeBuildFixture(t, repository, "go.mod", module) writeBuildFixture(t, repository, "go.mod", module)
writeBuildFixture(t, repository, "cmd/app/main.go", mainSource) writeBuildFixture(t, repository, "cmd/app/main.go", mainSource)
writeBuildFixture(t, repository, "cmd/app/main_test.go", "package main\nimport \"testing\"\nfunc TestCandidateFixture(t *testing.T) {}\n") writeBuildFixture(t, repository, "cmd/app/main_test.go", "package main\nimport \"testing\"\nfunc TestCandidateFixture(t *testing.T) {}\n")
@@ -731,7 +731,7 @@ func writeNestedBuildConsumer(t *testing.T, repository, moduleDir, modulePath, m
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
module := fmt.Sprintf("module %s\n\ngo 1.26.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.0.0\n\nreplace gitea.maximumdirect.net/eric/ReleaseKit => %s\n", modulePath, filepath.ToSlash(root)) module := fmt.Sprintf("module %s\n\ngo 1.26.0\n\nrequire go.maximumdirect.net/releasekit v0.0.0\n\nreplace go.maximumdirect.net/releasekit => %s\n", modulePath, filepath.ToSlash(root))
writeBuildFixture(t, repository, filepath.ToSlash(filepath.Join(moduleDir, "go.mod")), module) writeBuildFixture(t, repository, filepath.ToSlash(filepath.Join(moduleDir, "go.mod")), module)
writeBuildFixture(t, repository, filepath.ToSlash(filepath.Join(moduleDir, "cmd/app/main.go")), mainSource) writeBuildFixture(t, repository, filepath.ToSlash(filepath.Join(moduleDir, "cmd/app/main.go")), mainSource)
return filepath.Join(repository, filepath.FromSlash(moduleDir)) return filepath.Join(repository, filepath.FromSlash(moduleDir))
@@ -743,7 +743,7 @@ func prepareCandidateBuild(t *testing.T, repository, mainSource string) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
module := fmt.Sprintf("module example.com/candidatebuild\n\ngo 1.26.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.0.0\n\nreplace gitea.maximumdirect.net/eric/ReleaseKit => %s\n", filepath.ToSlash(root)) module := fmt.Sprintf("module example.com/candidatebuild\n\ngo 1.26.0\n\nrequire go.maximumdirect.net/releasekit v0.0.0\n\nreplace go.maximumdirect.net/releasekit => %s\n", filepath.ToSlash(root))
writeFile(t, repository, "go.mod", module) writeFile(t, repository, "go.mod", module)
writeFile(t, repository, ".gitignore", "dist/\n") writeFile(t, repository, ".gitignore", "dist/\n")
writeFile(t, repository, "cmd/app/main.go", mainSource) writeFile(t, repository, "cmd/app/main.go", mainSource)
@@ -777,7 +777,7 @@ func buildGit(t *testing.T, directory string, args ...string) string {
func TestBuildSourceEligibilitySurvivesLaterCheckoutCleanup(t *testing.T) { func TestBuildSourceEligibilitySurvivesLaterCheckoutCleanup(t *testing.T) {
const source = `package main const source = `package main
import("fmt";"os";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo") import("fmt";"os";"go.maximumdirect.net/releasekit/buildinfo")
func main(){ if len(os.Args)>1 && os.Args[1]=="version" {fmt.Println("fixture",buildinfo.Version())} } func main(){ if len(os.Args)>1 && os.Args[1]=="version" {fmt.Println("fixture",buildinfo.Version())} }
` `
for _, mode := range []string{"clean", "dirty before build", "changed during build", "commit during build"} { for _, mode := range []string{"clean", "dirty before build", "changed during build", "commit during build"} {
@@ -838,7 +838,7 @@ func TestCandidateBuildDoesNotRequireWritableRepositoryParent(t *testing.T) {
t.Skip("requires ordinary Unix permission checks") t.Skip("requires ordinary Unix permission checks")
} }
repository := newBuildConsumer(t, `package main repository := newBuildConsumer(t, `package main
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo") import("fmt";"go.maximumdirect.net/releasekit/buildinfo")
func main(){fmt.Println("fixture",buildinfo.Version())} func main(){fmt.Println("fixture",buildinfo.Version())}
`) `)
writeBuildFixture(t, repository, "docs/releases/v1.2.3.md", "# Fixture v1.2.3\n") writeBuildFixture(t, repository, "docs/releases/v1.2.3.md", "# Fixture v1.2.3\n")

View File

@@ -2,7 +2,7 @@
// built with ReleaseKit. // built with ReleaseKit.
package buildinfo package buildinfo
import "gitea.maximumdirect.net/eric/ReleaseKit/internal/runtimeinfo" import "go.maximumdirect.net/releasekit/internal/runtimeinfo"
// Version returns the injected release identifier when available. Otherwise, // Version returns the injected release identifier when available. Otherwise,
// it returns the executable's main module version, or "dev" when that metadata // it returns the executable's main module version, or "dev" when that metadata

View File

@@ -7,7 +7,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
) )
func TestVersionInConsumerExecutable(t *testing.T) { func TestVersionInConsumerExecutable(t *testing.T) {
@@ -21,9 +21,9 @@ func TestVersionInConsumerExecutable(t *testing.T) {
go 1.26.0 go 1.26.0
require gitea.maximumdirect.net/eric/ReleaseKit v0.0.0 require go.maximumdirect.net/releasekit v0.0.0
replace gitea.maximumdirect.net/eric/ReleaseKit => %s replace go.maximumdirect.net/releasekit => %s
`, filepath.ToSlash(repo)) `, filepath.ToSlash(repo))
if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(module), 0o600); err != nil { if err := os.WriteFile(filepath.Join(dir, "go.mod"), []byte(module), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -32,7 +32,7 @@ replace gitea.maximumdirect.net/eric/ReleaseKit => %s
import ( import (
"fmt" "fmt"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { fmt.Print(buildinfo.Version()) } func main() { fmt.Print(buildinfo.Version()) }

View File

@@ -4,10 +4,10 @@ import (
"context" "context"
"io" "io"
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" internalbuild "go.maximumdirect.net/releasekit/internal/build"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "go.maximumdirect.net/releasekit/internal/candidate"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/publish" "go.maximumdirect.net/releasekit/internal/publish"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" "go.maximumdirect.net/releasekit/internal/validation"
) )
// ErrInvalidVersion reports a version that is not stable vMAJOR.MINOR.PATCH. // ErrInvalidVersion reports a version that is not stable vMAJOR.MINOR.PATCH.

View File

@@ -7,7 +7,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
) )
func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) { func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
@@ -37,7 +37,7 @@ func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
} }
} }
writeRepositoryFile(t, repository, ".gitignore", "dist/\n") writeRepositoryFile(t, repository, ".gitignore", "dist/\n")
state := &deliveryServerState{tag: tag} state := &deliveryServerState{data: deliveryServerData{tag: tag}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
settings := cli.Settings{Product: "Example Product", ModuleDir: tt.module, settings := cli.Settings{Product: "Example Product", ModuleDir: tt.module,
@@ -47,7 +47,7 @@ func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
cliGitRun(t, repository, "add", ".") cliGitRun(t, repository, "add", ".")
cliGitRun(t, repository, "commit", "-m", "Prepare module source and output exclusion") cliGitRun(t, repository, "commit", "-m", "Prepare module source and output exclusion")
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", tag) cliGitRun(t, repository, "tag", tag)
output, resolved := "dist", filepath.Join(moduleDirectory, "dist") output, resolved := "dist", filepath.Join(moduleDirectory, "dist")
if tt.absolute { if tt.absolute {
@@ -73,12 +73,12 @@ func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
args := []string{"deliver", "--repo", invocationRepository, "--assets", output, "v1.2.3"} args := []string{"deliver", "--repo", invocationRepository, "--assets", output, "v1.2.3"}
stdout.Reset() stdout.Reset()
stderr.Reset() stderr.Reset()
if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.releaseCreates != 1 || state.assetUploads != 2 { if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 {
t.Fatalf("delivery status=%d, creates=%d, uploads=%d, stderr=%s", status, state.releaseCreates, state.assetUploads, stderr.String()) t.Fatalf("delivery status=%d, creates=%d, uploads=%d, stderr=%s", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String())
} }
if tt.alias { if tt.alias {
if status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", output, "v1.2.3"}, &stdout, &stderr); status != 0 || state.assetUploads != 2 { if status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", output, "v1.2.3"}, &stdout, &stderr); status != 0 || state.snapshot().assetUploads != 2 {
t.Fatalf("physical-path retry status=%d, uploads=%d, stderr=%s", status, state.assetUploads, stderr.String()) t.Fatalf("physical-path retry status=%d, uploads=%d, stderr=%s", status, state.snapshot().assetUploads, stderr.String())
} }
} }
if tt.module != "" { if tt.module != "" {
@@ -86,17 +86,17 @@ func TestBuildAndPrebuiltDeliveryUseSameDirectoryBase(t *testing.T) {
wrongModule := settings wrongModule := settings
wrongModule.ModuleDir = "" wrongModule.ModuleDir = ""
invocation := cli.Invocation{Repository: repository, AssetsDir: resolved, Version: "v1.2.3"} invocation := cli.Invocation{Repository: repository, AssetsDir: resolved, Version: "v1.2.3"}
requestsBefore := state.apiRequests requestsBefore := state.snapshot().apiRequests
if status := deliverWithSettings(context.Background(), invocation, wrongModule, &stdout, &stderr); status != 1 || state.apiRequests != requestsBefore { if status := deliverWithSettings(context.Background(), invocation, wrongModule, &stdout, &stderr); status != 1 || state.snapshot().apiRequests != requestsBefore {
t.Fatalf("wrong module status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.apiRequests, stderr.String()) t.Fatalf("wrong module status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.snapshot().apiRequests, stderr.String())
} }
} }
if err := os.WriteFile(receipt.Artifacts[0].Path, []byte("changed"), 0o600); err != nil { if err := os.WriteFile(receipt.Artifacts[0].Path, []byte("changed"), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
} }
requestsBefore := state.apiRequests requestsBefore := state.snapshot().apiRequests
if status := run(context.Background(), args, &stdout, &stderr); status != 1 || state.apiRequests != requestsBefore { if status := run(context.Background(), args, &stdout, &stderr); status != 1 || state.snapshot().apiRequests != requestsBefore {
t.Fatalf("changed content status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.apiRequests, stderr.String()) t.Fatalf("changed content status=%d, requests=%d->%d, stderr=%s", status, requestsBefore, state.snapshot().apiRequests, stderr.String())
} }
}) })
} }

View File

@@ -9,7 +9,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
) )
func TestCandidateCheckUsesCommittedDeclarationSymlink(t *testing.T) { func TestCandidateCheckUsesCommittedDeclarationSymlink(t *testing.T) {
@@ -36,15 +36,15 @@ func TestPageOnlyDeliveryUsesCommittedModuleInSparseCheckout(t *testing.T) {
writeRepositoryFile(t, repository, module+"/docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nNested release note.\n") writeRepositoryFile(t, repository, module+"/docs/releases/v1.2.3.md", "# Example Product v1.2.3\n\nNested release note.\n")
cliGitRun(t, repository, "add", ".") cliGitRun(t, repository, "add", ".")
cliGitRun(t, repository, "commit", "-m", "Add sparse release module") cliGitRun(t, repository, "commit", "-m", "Add sparse release module")
state := &deliveryServerState{tag: module + "/v1.2.3"} state := &deliveryServerState{data: deliveryServerData{tag: module + "/v1.2.3"}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", ModuleDir: module, writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", ModuleDir: module,
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", state.tag) cliGitRun(t, repository, "tag", state.snapshot().tag)
cliGitRun(t, repository, "checkout", "--detach", state.tag) cliGitRun(t, repository, "checkout", "--detach", state.snapshot().tag)
cliGitRun(t, repository, "sparse-checkout", "set", "--cone", "docs") cliGitRun(t, repository, "sparse-checkout", "set", "--cone", "docs")
if _, err := os.Stat(filepath.Join(repository, filepath.FromSlash(module))); !errors.Is(err, os.ErrNotExist) { if _, err := os.Stat(filepath.Join(repository, filepath.FromSlash(module))); !errors.Is(err, os.ErrNotExist) {
t.Fatalf("module fixture is materialized: %v", err) t.Fatalf("module fixture is materialized: %v", err)
@@ -61,11 +61,11 @@ func TestPageOnlyDeliveryUsesCommittedModuleInSparseCheckout(t *testing.T) {
t.Setenv("PATH", gitOnly) t.Setenv("PATH", gitOnly)
for _, args := range [][]string{ for _, args := range [][]string{
{"deliver", "--repo", repository, "v1.2.3"}, {"deliver", "--repo", repository, "v1.2.3"},
{"ci-deliver", "--repo", repository, "--prepared", "--expected-module", module, "--commit", state.commit, state.tag}, {"ci-deliver", "--repo", repository, "--prepared", "--expected-module", module, "--commit", state.snapshot().commit, state.snapshot().tag},
} { } {
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.releaseCreates != 1 || state.assetUploads != 0 { if status := run(context.Background(), args, &stdout, &stderr); status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 {
t.Fatalf("%s sparse delivery status=%d creates=%d uploads=%d stderr=%s", args[0], status, state.releaseCreates, state.assetUploads, stderr.String()) t.Fatalf("%s sparse delivery status=%d creates=%d uploads=%d stderr=%s", args[0], status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String())
} }
} }
} }

View File

@@ -12,7 +12,7 @@ func TestMultiModuleExampleSetupUsesCurrentSourceTool(t *testing.T) {
root := cliRepositoryRoot(t) root := cliRepositoryRoot(t)
repository := newCLIRepository(t) repository := newCLIRepository(t)
copyExampleTree(t, filepath.Join(root, "examples", "multi-module"), repository) copyExampleTree(t, filepath.Join(root, "examples", "multi-module"), repository)
const module = "gitea.maximumdirect.net/eric/ReleaseKit" const module = "go.maximumdirect.net/releasekit"
environment := append(os.Environ(), "GOTOOLCHAIN=local", "GOWORK=off", "GOPROXY=off", "GOSUMDB=off", "GOFLAGS=") environment := append(os.Environ(), "GOTOOLCHAIN=local", "GOWORK=off", "GOPROXY=off", "GOSUMDB=off", "GOFLAGS=")
goCommand := func(directory string, args ...string) string { goCommand := func(directory string, args ...string) string {
t.Helper() t.Helper()

View File

@@ -13,8 +13,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process" "go.maximumdirect.net/releasekit/internal/process"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
) )
const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION\n releasekit publish [--repo DIR] [--config FILE] [--expected-module DIR] VERSION\n releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSION\n releasekit deliver [--repo DIR] [--config FILE] [--assets DIR] VERSION\n releasekit ci-deliver [--repo DIR] [--config FILE] [--remote NAME] [--prepared] [--expected-module DIR] --commit OID TAG" const Usage = "usage: releasekit version\n releasekit check [--repo DIR] [--config FILE] VERSION\n releasekit publish [--repo DIR] [--config FILE] [--expected-module DIR] VERSION\n releasekit build [--repo DIR] [--config FILE] [--output DIR] VERSION\n releasekit deliver [--repo DIR] [--config FILE] [--assets DIR] VERSION\n releasekit ci-deliver [--repo DIR] [--config FILE] [--remote NAME] [--prepared] [--expected-module DIR] --commit OID TAG"

View File

@@ -10,9 +10,9 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact" "go.maximumdirect.net/releasekit/internal/artifact"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/releaseidentity"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
) )
const BuildReceiptName = ".releasekit-build.json" const BuildReceiptName = ".releasekit-build.json"

View File

@@ -7,7 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
) )
func TestMainModuleWithoutDomainSupportsCheckBuildAndPageDelivery(t *testing.T) { func TestMainModuleWithoutDomainSupportsCheckBuildAndPageDelivery(t *testing.T) {
@@ -34,11 +34,11 @@ func TestMainModuleWithoutDomainSupportsCheckBuildAndPageDelivery(t *testing.T)
} }
settings.Build = nil settings.Build = nil
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
if status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr); status != 0 || state.releaseCreates != 1 || state.assetUploads != 0 { if status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr); status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 {
t.Fatalf("page delivery status=%d creates=%d uploads=%d stderr=%s", status, state.releaseCreates, state.assetUploads, stderr.String()) t.Fatalf("page delivery status=%d creates=%d uploads=%d stderr=%s", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String())
} }
} }

View File

@@ -11,12 +11,12 @@ import (
"strings" "strings"
"syscall" "syscall"
"gitea.maximumdirect.net/eric/ReleaseKit" "go.maximumdirect.net/releasekit"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact" "go.maximumdirect.net/releasekit/internal/artifact"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/releaseidentity"
) )
func main() { func main() {

View File

@@ -15,11 +15,12 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
) )
func TestVersionCommand(t *testing.T) { func TestVersionCommand(t *testing.T) {
@@ -471,7 +472,7 @@ import (
"fmt" "fmt"
"os" "os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { func main() {
@@ -641,7 +642,7 @@ func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
} }
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") // Historical lightweight tags are valid delivery inputs. cliGitRun(t, repository, "tag", "v1.2.3") // Historical lightweight tags are valid delivery inputs.
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
for attempt := 0; attempt < 2; attempt++ { for attempt := 0; attempt < 2; attempt++ {
@@ -654,11 +655,11 @@ func TestDeliverPageOnlyCreatesAndReusesRelease(t *testing.T) {
t.Fatalf("delivery output = %q", stdout.String()) t.Fatalf("delivery output = %q", stdout.String())
} }
} }
if state.releaseCreates != 1 || state.assetUploads != 0 || state.assetListReads != 0 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 || state.snapshot().assetListReads != 0 {
t.Fatalf("page-only calls = creates:%d asset uploads:%d asset reads:%d", state.releaseCreates, state.assetUploads, state.assetListReads) t.Fatalf("page-only calls = creates:%d asset uploads:%d asset reads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads, state.snapshot().assetListReads)
} }
if state.noteBody != "# Example Product v1.2.3\n\nRelease note.\n" { if state.snapshot().noteBody != "# Example Product v1.2.3\n\nRelease note.\n" {
t.Fatalf("hosted note body = %q", state.noteBody) t.Fatalf("hosted note body = %q", state.snapshot().noteBody)
} }
} }
@@ -675,7 +676,7 @@ func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
} }
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
cliGitRun(t, repository, "checkout", "--detach", "v1.2.3") cliGitRun(t, repository, "checkout", "--detach", "v1.2.3")
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
@@ -689,14 +690,15 @@ func TestDeliverBuildsTaggedArtifactsAndReusesAssets(t *testing.T) {
t.Fatalf("second delivery did not report matching assets: %q", stdout.String()) t.Fatalf("second delivery did not report matching assets: %q", stdout.String())
} }
} }
if state.releaseCreates != 1 || state.assetUploads != 2 { observed := state.snapshot()
t.Fatalf("delivery calls = creates:%d asset uploads:%d", state.releaseCreates, state.assetUploads) if observed.releaseCreates != 1 || observed.assetUploads != 2 {
t.Fatalf("delivery calls = creates:%d asset uploads:%d", observed.releaseCreates, observed.assetUploads)
} }
if _, ok := state.assets["helper-v1.2.3-linux-amd64"]; !ok { if _, ok := observed.assets["helper-v1.2.3-linux-amd64"]; !ok {
t.Fatalf("binary was not uploaded: %#v", state.assets) t.Fatalf("binary was not uploaded: %#v", observed.assets)
} }
if _, ok := state.assets["SHA256SUMS"]; !ok { if _, ok := observed.assets["SHA256SUMS"]; !ok {
t.Fatalf("checksum manifest was not uploaded: %#v", state.assets) t.Fatalf("checksum manifest was not uploaded: %#v", observed.assets)
} }
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
t.Fatalf("facade-owned build output remains after delivery: entries=%v error=%v", entries, err) t.Fatalf("facade-owned build output remains after delivery: entries=%v error=%v", entries, err)
@@ -717,7 +719,7 @@ func TestCIDeliverLoadsTaggedProfileAndSkipsUnavailablePageOnlyDependencies(t *t
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{Product: "Later Branch Profile"}) writeDeliveryProfileAt(t, repository, "profiles/ci-delivery.json", cli.Settings{Product: "Later Branch Profile"})
cliGitRun(t, repository, "checkout", "--detach", expectedCommit) cliGitRun(t, repository, "checkout", "--detach", expectedCommit)
cliGitRun(t, repository, "remote", "remove", "origin") cliGitRun(t, repository, "remote", "remove", "origin")
@@ -730,8 +732,8 @@ func TestCIDeliverLoadsTaggedProfileAndSkipsUnavailablePageOnlyDependencies(t *t
if status := run(context.Background(), args, &stdout, &stderr); status != 0 { if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String())
} }
if state.releaseCreates != 1 || state.apiRequests == 0 { if state.snapshot().releaseCreates != 1 || state.snapshot().apiRequests == 0 {
t.Fatalf("page delivery did not reach the server: creates=%d requests=%d", state.releaseCreates, state.apiRequests) t.Fatalf("page delivery did not reach the server: creates=%d requests=%d", state.snapshot().releaseCreates, state.snapshot().apiRequests)
} }
if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != expectedCommit { if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != expectedCommit {
t.Fatalf("prepared HEAD = %s, want %s", got, expectedCommit) t.Fatalf("prepared HEAD = %s, want %s", got, expectedCommit)
@@ -752,7 +754,7 @@ func TestCIDeliverRejectsWrongCommitBeforeGoOrHostedRequests(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
shimDir := t.TempDir() shimDir := t.TempDir()
markerPath := filepath.Join(shimDir, "go-invoked") markerPath := filepath.Join(shimDir, "go-invoked")
@@ -775,8 +777,8 @@ func TestCIDeliverRejectsWrongCommitBeforeGoOrHostedRequests(t *testing.T) {
if _, err := os.Stat(markerPath); !os.IsNotExist(err) { if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
t.Fatalf("Go was invoked before commit agreement: %v", err) t.Fatalf("Go was invoked before commit agreement: %v", err)
} }
if state.apiRequests != 0 { if state.snapshot().apiRequests != 0 {
t.Fatalf("hosted API requests = %d, want 0", state.apiRequests) t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests)
} }
} }
@@ -803,7 +805,7 @@ func TestCIDeliverRejectsExpectedModuleMismatchBeforeDependencyPreparation(t *te
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
shimDir := t.TempDir() shimDir := t.TempDir()
markerPath := filepath.Join(shimDir, "go-invoked") markerPath := filepath.Join(shimDir, "go-invoked")
goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n" goShim := "#!/bin/sh\n: > \"$RELEASEKIT_GO_MARKER\"\nexit 99\n"
@@ -824,8 +826,8 @@ func TestCIDeliverRejectsExpectedModuleMismatchBeforeDependencyPreparation(t *te
if _, err := os.Stat(markerPath); !os.IsNotExist(err) { if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
t.Fatalf("Go was invoked before profile agreement: %v", err) t.Fatalf("Go was invoked before profile agreement: %v", err)
} }
if state.apiRequests != 0 { if state.snapshot().apiRequests != 0 {
t.Fatalf("hosted API requests = %d, want 0", state.apiRequests) t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests)
} }
} }
@@ -840,18 +842,18 @@ func TestCIDeliverPreparesDependenciesAndBuildsConfiguredTargets(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"} args := []string{"ci-deliver", "--repo", repository, "--commit", expectedCommit, "v1.2.3"}
if status := run(context.Background(), args, &stdout, &stderr); status != 0 { if status := run(context.Background(), args, &stdout, &stderr); status != 0 {
t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String()) t.Fatalf("run() status=%d stdout=%q stderr=%q", status, stdout.String(), stderr.String())
} }
if state.releaseCreates != 1 || state.assetUploads != 2 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 {
t.Fatalf("delivery counts = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) t.Fatalf("delivery counts = creates:%d uploads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads)
} }
if _, ok := state.assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok { if _, ok := state.snapshot().assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok {
t.Fatalf("binary not uploaded: %#v", state.assets) t.Fatalf("binary not uploaded: %#v", state.snapshot().assets)
} }
} }
@@ -866,7 +868,7 @@ func TestCIDeliverValidatesCredentialsBeforePreparingDependencies(t *testing.T)
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
expectedCommit := pushTaggedCLIRepository(t, repository) expectedCommit := pushTaggedCLIRepository(t, repository)
state.commit = expectedCommit state.update(func(state *deliveryServerData) { state.commit = expectedCommit })
shimDir := t.TempDir() shimDir := t.TempDir()
markerPath := filepath.Join(shimDir, "go-invoked") markerPath := filepath.Join(shimDir, "go-invoked")
@@ -888,8 +890,8 @@ func TestCIDeliverValidatesCredentialsBeforePreparingDependencies(t *testing.T)
if _, err := os.Stat(markerPath); !os.IsNotExist(err) { if _, err := os.Stat(markerPath); !os.IsNotExist(err) {
t.Fatalf("Go was invoked before credential validation: %v", err) t.Fatalf("Go was invoked before credential validation: %v", err)
} }
if state.apiRequests != 0 { if state.snapshot().apiRequests != 0 {
t.Fatalf("hosted API requests = %d, want 0", state.apiRequests) t.Fatalf("hosted API requests = %d, want 0", state.snapshot().apiRequests)
} }
} }
@@ -904,7 +906,7 @@ func TestDeliverRejectsInvalidTaggedSourceBeforeManagedBuild(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
} }
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
if err := os.WriteFile(filepath.Join(repository, "uncommitted.txt"), []byte("dirty"), 0600); err != nil { if err := os.WriteFile(filepath.Join(repository, "uncommitted.txt"), []byte("dirty"), 0600); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -912,8 +914,8 @@ func TestDeliverRejectsInvalidTaggedSourceBeforeManagedBuild(t *testing.T) {
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
if status != 1 || state.apiRequests != 0 || strings.Contains(stderr.String(), "building ") { if status != 1 || state.snapshot().apiRequests != 0 || strings.Contains(stderr.String(), "building ") {
t.Fatalf("invalid-source delivery status=%d requests=%d stdout=%q stderr=%q", status, state.apiRequests, stdout.String(), stderr.String()) t.Fatalf("invalid-source delivery status=%d requests=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, stdout.String(), stderr.String())
} }
} }
@@ -958,16 +960,18 @@ func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *test
Build: &build, Build: &build,
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
assetsDir, _ := writeDeliveryReceipt(t, repository, build) assetsDir, _ := writeDeliveryReceipt(t, repository, build)
switch test.name { switch test.name {
case "partial": case "partial":
state.failAssetName = "helper-v1.2.3-linux-amd64" state.update(func(state *deliveryServerData) { state.failAssetName = "helper-v1.2.3-linux-amd64" })
case "conflict": case "conflict":
state.assets["helper-v1.2.3-linux-amd64"] = []byte("different hosted bytes") state.update(func(state *deliveryServerData) {
state.assets["helper-v1.2.3-linux-amd64"] = []byte("different hosted bytes")
})
case "unknown": case "unknown":
state.ambiguousAssetName = "helper-v1.2.3-linux-amd64" state.update(func(state *deliveryServerData) { state.ambiguousAssetName = "helper-v1.2.3-linux-amd64" })
} }
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
@@ -995,17 +999,17 @@ func TestDeliverFailureReportsRecoveryForPartialConflictAndUnknownStates(t *test
func TestDeliverRejectsWrongHostedTagBeforeReleaseWrite(t *testing.T) { func TestDeliverRejectsWrongHostedTagBeforeReleaseWrite(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
state := &deliveryServerState{remoteCommit: strings.Repeat("0", 40)} state := &deliveryServerState{data: deliveryServerData{remoteCommit: strings.Repeat("0", 40)}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
if status != 1 || state.releaseCreates != 0 || state.assetUploads != 0 { if status != 1 || state.snapshot().releaseCreates != 0 || state.snapshot().assetUploads != 0 {
t.Fatalf("wrong hosted tag status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.releaseCreates, state.assetUploads, stdout.String(), stderr.String()) t.Fatalf("wrong hosted tag status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stdout.String(), stderr.String())
} }
} }
@@ -1015,13 +1019,13 @@ func TestDeliverRequiresCredentialBeforeRemoteRequests(t *testing.T) {
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example", TokenEnv: "RELEASEKIT_TEST_DELIVERY_TOKEN"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example", TokenEnv: "RELEASEKIT_TEST_DELIVERY_TOKEN"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
t.Setenv("RELEASEKIT_TEST_DELIVERY_TOKEN", "") t.Setenv("RELEASEKIT_TEST_DELIVERY_TOKEN", "")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
if status != 2 || state.apiRequests != 0 || !strings.Contains(stderr.String(), "RELEASEKIT_TEST_DELIVERY_TOKEN") { if status != 2 || state.snapshot().apiRequests != 0 || !strings.Contains(stderr.String(), "RELEASEKIT_TEST_DELIVERY_TOKEN") {
t.Fatalf("missing credential status=%d requests=%d stdout=%q stderr=%q", status, state.apiRequests, stdout.String(), stderr.String()) t.Fatalf("missing credential status=%d requests=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, stdout.String(), stderr.String())
} }
} }
@@ -1031,7 +1035,7 @@ func TestDeliverRejectsCheckoutThatDiffersFromLocalTag(t *testing.T) {
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later commit"), 0o600); err != nil { if err := os.WriteFile(filepath.Join(repository, "later.txt"), []byte("later commit"), 0o600); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -1041,11 +1045,11 @@ func TestDeliverRejectsCheckoutThatDiffersFromLocalTag(t *testing.T) {
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "v1.2.3"}, &stdout, &stderr)
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 { if status != 1 || state.snapshot().apiRequests != 0 || state.snapshot().releaseCreates != 0 {
t.Fatalf("mismatched checkout status=%d requests=%d creates=%d stdout=%q stderr=%q", status, state.apiRequests, state.releaseCreates, stdout.String(), stderr.String()) t.Fatalf("mismatched checkout status=%d requests=%d creates=%d stdout=%q stderr=%q", status, state.snapshot().apiRequests, state.snapshot().releaseCreates, stdout.String(), stderr.String())
} }
if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3"); got != state.commit { if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/v1.2.3"); got != state.snapshot().commit {
t.Fatalf("delivery moved tag from %s to %s", state.commit, got) t.Fatalf("delivery moved tag from %s to %s", state.snapshot().commit, got)
} }
} }
@@ -1059,7 +1063,7 @@ func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testin
current := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} current := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
settings := cli.Settings{Product: "Example Product", Build: &current, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}} settings := cli.Settings{Product: "Example Product", Build: &current, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
receiptSettings := current receiptSettings := current
if mode == "stale settings" { if mode == "stale settings" {
@@ -1085,16 +1089,16 @@ func TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites(t *testin
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assetsDir, "v1.2.3"}, &stdout, &stderr)
if mode == "valid receipt" { if mode == "valid receipt" {
if status != 0 || state.releaseCreates != 1 || state.assetUploads != 2 { if status != 0 || state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 {
t.Fatalf("valid receipt status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.releaseCreates, state.assetUploads, stdout.String(), stderr.String()) t.Fatalf("valid receipt status=%d creates=%d uploads=%d stdout=%q stderr=%q", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stdout.String(), stderr.String())
} }
if _, err := os.Stat(artifactPath); err != nil { if _, err := os.Stat(artifactPath); err != nil {
t.Fatalf("prebuilt artifact was not retained: %v", err) t.Fatalf("prebuilt artifact was not retained: %v", err)
} }
return return
} }
if status != 1 || state.apiRequests != 0 || state.releaseCreates != 0 || state.assetUploads != 0 { if status != 1 || state.snapshot().apiRequests != 0 || state.snapshot().releaseCreates != 0 || state.snapshot().assetUploads != 0 {
t.Fatalf("%s receipt status=%d requests=%d stdout=%q stderr=%q", mode, status, state.apiRequests, stdout.String(), stderr.String()) t.Fatalf("%s receipt status=%d requests=%d stdout=%q stderr=%q", mode, status, state.snapshot().apiRequests, stdout.String(), stderr.String())
} }
if mode == "tampered" || mode == "missing artifact" { if mode == "tampered" || mode == "missing artifact" {
if strings.Contains(stderr.String(), "load verified build receipt") { if strings.Contains(stderr.String(), "load verified build receipt") {
@@ -1136,6 +1140,30 @@ func writeDeliveryReceipt(t *testing.T, repository string, settings cli.BuildSet
} }
type deliveryServerState struct { type deliveryServerState struct {
mu sync.Mutex
data deliveryServerData
}
func (state *deliveryServerState) update(change func(*deliveryServerData)) {
state.mu.Lock()
defer state.mu.Unlock()
change(&state.data)
}
func (state *deliveryServerState) snapshot() deliveryServerData {
state.mu.Lock()
defer state.mu.Unlock()
result := state.data
result.assets = make(map[string][]byte, len(state.data.assets))
for key, value := range state.data.assets {
result.assets[key] = append([]byte(nil), value...)
}
return result
}
type deliveryServerData struct {
tag string tag string
malformedCreate bool malformedCreate bool
failAssetListing bool failAssetListing bool
@@ -1156,36 +1184,38 @@ type deliveryServerState struct {
func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server { func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Server {
t.Helper() t.Helper()
if state.tag == "" { data := &state.data
state.tag = "v1.2.3" if data.tag == "" {
data.tag = "v1.2.3"
} }
if state.assets == nil { if data.assets == nil {
state.assets = make(map[string][]byte) data.assets = make(map[string][]byte)
} }
serverURL := ""
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
state.mu.Lock()
defer state.mu.Unlock()
if strings.HasPrefix(r.URL.Path, "/api/") { if strings.HasPrefix(r.URL.Path, "/api/") {
state.apiRequests++ data.apiRequests++
if r.Header.Get("Authorization") != "token fixture-token" { if r.Header.Get("Authorization") != "token fixture-token" {
t.Errorf("authorization = %q", r.Header.Get("Authorization")) t.Errorf("authorization = %q", r.Header.Get("Authorization"))
} }
} }
switch r.URL.Path { switch r.URL.Path {
case "/api/v1/repos/owner/example/tags/" + state.tag: case "/api/v1/repos/owner/example/tags/" + data.tag:
if state.commit == "" { if data.commit == "" {
t.Error("tag source commit was not initialized") t.Error("tag source commit was not initialized")
} }
commit := state.remoteCommit commit := data.remoteCommit
if commit == "" { if commit == "" {
commit = state.commit commit = data.commit
} }
_, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, state.tag, commit) _, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, data.tag, commit)
case "/api/v1/repos/owner/example/releases/tags/" + state.tag: case "/api/v1/repos/owner/example/releases/tags/" + data.tag:
if !state.pageExists { if !data.pageExists {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
writeDeliveryRelease(w, serverURL, state) writeDeliveryRelease(w, serverURL(r), data)
case "/api/v1/repos/owner/example/releases": case "/api/v1/repos/owner/example/releases":
if r.Method != http.MethodPost { if r.Method != http.MethodPost {
t.Errorf("release request method = %s", r.Method) t.Errorf("release request method = %s", r.Method)
@@ -1201,26 +1231,26 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Errorf("decode create payload: %v", err) t.Errorf("decode create payload: %v", err)
} }
if payload.Tag != state.tag || payload.Target != state.commit { if payload.Tag != data.tag || payload.Target != data.commit {
t.Errorf("release source payload = %#v", payload) t.Errorf("release source payload = %#v", payload)
} }
state.pageExists, state.releaseCreates, state.noteBody = true, state.releaseCreates+1, payload.Body data.pageExists, data.releaseCreates, data.noteBody = true, data.releaseCreates+1, payload.Body
if state.malformedCreate { if data.malformedCreate {
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "tag_name": state.tag, "name": "inconsistent response", "body": payload.Body}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "tag_name": data.tag, "name": "inconsistent response", "body": payload.Body})
return return
} }
writeDeliveryRelease(w, serverURL, state) writeDeliveryRelease(w, serverURL(r), data)
case "/api/v1/repos/owner/example/releases/10/assets": case "/api/v1/repos/owner/example/releases/10/assets":
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
state.assetListReads++ data.assetListReads++
if state.failAssetListing { if data.failAssetListing {
http.Error(w, "unavailable", 503) http.Error(w, "unavailable", 503)
return return
} }
list := make([]map[string]any, 0, len(state.assets)) list := make([]map[string]any, 0, len(data.assets))
for name, content := range state.assets { for name, content := range data.assets {
list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": serverURL + "/files/" + name}) list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": serverURL(r) + "/files/" + name})
} }
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
return return
@@ -1242,28 +1272,28 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
if err != nil { if err != nil {
t.Errorf("read asset bytes: %v", err) t.Errorf("read asset bytes: %v", err)
} }
if _, exists := state.assets[name]; exists { if _, exists := data.assets[name]; exists {
t.Errorf("asset %q uploaded more than once", name) t.Errorf("asset %q uploaded more than once", name)
} }
if name == state.failAssetName { if name == data.failAssetName {
http.Error(w, "upload failed", http.StatusBadRequest) http.Error(w, "upload failed", http.StatusBadRequest)
return return
} }
if name == state.ambiguousAssetName { if name == data.ambiguousAssetName {
http.Error(w, "upload outcome ambiguous", http.StatusInternalServerError) http.Error(w, "upload outcome ambiguous", http.StatusInternalServerError)
return return
} }
state.assets[name] = content data.assets[name] = content
state.assetUploads++ data.assetUploads++
_ = json.NewEncoder(w).Encode(map[string]any{"id": state.assetUploads, "name": name, "size": len(content)}) _ = json.NewEncoder(w).Encode(map[string]any{"id": data.assetUploads, "name": name, "size": len(content)})
default: default:
if strings.HasPrefix(r.URL.Path, "/files/") { if strings.HasPrefix(r.URL.Path, "/files/") {
if state.failAssetDownloads { if data.failAssetDownloads {
http.Error(w, "unavailable", 503) http.Error(w, "unavailable", 503)
return return
} }
name := strings.TrimPrefix(r.URL.Path, "/files/") name := strings.TrimPrefix(r.URL.Path, "/files/")
content, ok := state.assets[name] content, ok := data.assets[name]
if !ok { if !ok {
http.NotFound(w, r) http.NotFound(w, r)
return return
@@ -1276,17 +1306,17 @@ func newDeliveryServer(t *testing.T, state *deliveryServerState) *httptest.Serve
} }
}) })
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
serverURL = server.URL
return server return server
} }
func writeDeliveryRelease(w http.ResponseWriter, serverURL string, state *deliveryServerState) { // writeDeliveryRelease reads data while the request handler holds state.mu.
func writeDeliveryRelease(w http.ResponseWriter, baseURL string, data *deliveryServerData) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
product := state.product product := data.product
if product == "" { if product == "" {
product = "Example Product" product = "Example Product"
} }
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL + "/releases/10", "tag_name": state.tag, "target_commitish": state.commit, "name": product + " v1.2.3", "body": state.noteBody, "draft": false, "prerelease": false}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": baseURL + "/releases/10", "tag_name": data.tag, "target_commitish": data.commit, "name": product + " v1.2.3", "body": data.noteBody, "draft": false, "prerelease": false})
} }
func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) { func writeDeliveryProfile(t *testing.T, repository string, settings cli.Settings) {
@@ -1345,7 +1375,7 @@ func newCLIBuildRepository(t *testing.T, mainSource string) string {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
module := fmt.Sprintf("module example.com/cli-candidate\n\ngo 1.26.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.0.0\n\nreplace gitea.maximumdirect.net/eric/ReleaseKit => %s\n", filepath.ToSlash(root)) module := fmt.Sprintf("module example.com/cli-candidate\n\ngo 1.26.0\n\nrequire go.maximumdirect.net/releasekit v0.0.0\n\nreplace go.maximumdirect.net/releasekit => %s\n", filepath.ToSlash(root))
write := func(name, contents string) { write := func(name, contents string) {
t.Helper() t.Helper()
path := filepath.Join(repository, filepath.FromSlash(name)) path := filepath.Join(repository, filepath.FromSlash(name))
@@ -1417,7 +1447,7 @@ func TestDeliverRejectsReceiptBuiltFromDirtySourceAfterCleanup(t *testing.T) {
defer server.Close() defer server.Close()
settings := cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}} settings := cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}
writeDeliveryProfile(t, repository, settings) writeDeliveryProfile(t, repository, settings)
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
mainPath := filepath.Join(repository, "main.go") mainPath := filepath.Join(repository, "main.go")
original, err := os.ReadFile(mainPath) original, err := os.ReadFile(mainPath)
@@ -1444,26 +1474,26 @@ func TestDeliverRejectsReceiptBuiltFromDirtySourceAfterCleanup(t *testing.T) {
stdout.Reset() stdout.Reset()
stderr.Reset() stderr.Reset()
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr)
if status != 1 || state.apiRequests != 0 || !strings.Contains(stderr.String(), "build source") { if status != 1 || state.snapshot().apiRequests != 0 || !strings.Contains(stderr.String(), "build source") {
t.Fatalf("status=%d requests=%d %s", status, state.apiRequests, stderr.String()) t.Fatalf("status=%d requests=%d %s", status, state.snapshot().apiRequests, stderr.String())
} }
} }
func TestRecoveredPageCreatePreservesPartialDeliveryOnInspectionFailure(t *testing.T) { func TestRecoveredPageCreatePreservesPartialDeliveryOnInspectionFailure(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
state := &deliveryServerState{malformedCreate: true, failAssetListing: true} state := &deliveryServerState{data: deliveryServerData{malformedCreate: true, failAssetListing: true}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
assets, _ := writeDeliveryReceipt(t, repository, build) assets, _ := writeDeliveryReceipt(t, repository, build)
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
var stdout, stderr bytes.Buffer var stdout, stderr bytes.Buffer
status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr) status := run(context.Background(), []string{"deliver", "--repo", repository, "--assets", assets, "v1.2.3"}, &stdout, &stderr)
if status != 1 || !strings.Contains(stderr.String(), "state=partial") || state.releaseCreates != 1 || state.assetListReads != 1 || state.assetUploads != 0 { if status != 1 || !strings.Contains(stderr.String(), "state=partial") || state.snapshot().releaseCreates != 1 || state.snapshot().assetListReads != 1 || state.snapshot().assetUploads != 0 {
t.Fatalf("status=%d state=%#v %s", status, state, stderr.String()) t.Fatalf("status=%d state=%#v %s", status, state.snapshot(), stderr.String())
} }
} }
@@ -1471,12 +1501,12 @@ func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) {
for _, existingPage := range []bool{false, true} { for _, existingPage := range []bool{false, true} {
t.Run(fmt.Sprintf("existing-page-%t", existingPage), func(t *testing.T) { t.Run(fmt.Sprintf("existing-page-%t", existingPage), func(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
state := &deliveryServerState{pageExists: existingPage, noteBody: "# Example Product v1.2.3\n\nRelease note.\n", failAssetDownloads: true, assets: map[string][]byte{"helper-v1.2.3-linux-amd64": []byte("fixture artifact")}} state := &deliveryServerState{data: deliveryServerData{pageExists: existingPage, noteBody: "# Example Product v1.2.3\n\nRelease note.\n", failAssetDownloads: true, assets: map[string][]byte{"helper-v1.2.3-linux-amd64": []byte("fixture artifact")}}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}} build := cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &build, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
assets, _ := writeDeliveryReceipt(t, repository, build) assets, _ := writeDeliveryReceipt(t, repository, build)
t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token") t.Setenv("RELEASEKIT_GITEA_TOKEN", "fixture-token")
@@ -1488,8 +1518,8 @@ func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) {
expectedState = "not_attempted" expectedState = "not_attempted"
expectedCreates = 0 expectedCreates = 0
} }
if status != 1 || !strings.Contains(stderr.String(), "state="+expectedState) || state.assetUploads != 0 || state.releaseCreates != expectedCreates { if status != 1 || !strings.Contains(stderr.String(), "state="+expectedState) || state.snapshot().assetUploads != 0 || state.snapshot().releaseCreates != expectedCreates {
t.Fatalf("status=%d creates=%d uploads=%d %s", status, state.releaseCreates, state.assetUploads, stderr.String()) t.Fatalf("status=%d creates=%d uploads=%d %s", status, state.snapshot().releaseCreates, state.snapshot().assetUploads, stderr.String())
} }
if existingPage && !strings.Contains(stderr.String(), "recovery: fix the local or preflight failure") { if existingPage && !strings.Contains(stderr.String(), "recovery: fix the local or preflight failure") {
t.Fatalf("not-attempted recovery guidance missing: %s", stderr.String()) t.Fatalf("not-attempted recovery guidance missing: %s", stderr.String())
@@ -1500,7 +1530,7 @@ func TestUnreadableAssetsDoNotClaimAnUnknownWrite(t *testing.T) {
func TestFailedRebuildInvalidatesPreviousReceipt(t *testing.T) { func TestFailedRebuildInvalidatesPreviousReceipt(t *testing.T) {
const source = `package main const source = `package main
import("fmt";"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo") import("fmt";"go.maximumdirect.net/releasekit/buildinfo")
func main(){fmt.Println("fixture",buildinfo.Version())} func main(){fmt.Println("fixture",buildinfo.Version())}
` `
repository := newCLIBuildRepository(t, source) repository := newCLIBuildRepository(t, source)
@@ -1532,7 +1562,7 @@ func TestTemporaryDeliveryBuildDoesNotRequireWritableRepositoryParent(t *testing
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}}) writeDeliveryProfile(t, repository, cli.Settings{Product: "Example Product", Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}})
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "v1.2.3") cliGitRun(t, repository, "tag", "v1.2.3")
parent := t.TempDir() parent := t.TempDir()
moved := filepath.Join(parent, "repo") moved := filepath.Join(parent, "repo")

View File

@@ -59,11 +59,11 @@ func TestMultiModuleLauncherUsesSelectedModuleAndProfile(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
copyExampleTree(t, example, repository) copyExampleTree(t, example, repository)
rootModule := "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.197\n" rootModule := "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit v0.2.197\n"
selectedModule := "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.198\n" selectedModule := "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit v0.2.198\n"
writeRepositoryFile(t, repository, "go.mod", rootModule) writeRepositoryFile(t, repository, "go.mod", rootModule)
writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", selectedModule) writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", selectedModule)
spaceModule := "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.299\n" spaceModule := "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit v0.2.299\n"
writeRepositoryFile(t, repository, "integrations/ck config/go.mod", spaceModule) writeRepositoryFile(t, repository, "integrations/ck config/go.mod", spaceModule)
writeRepositoryFile(t, repository, "profiles/core release.json", `{"product":"Example Consumer Integration","module_dir":"integrations/ck config"}`) writeRepositoryFile(t, repository, "profiles/core release.json", `{"product":"Example Consumer Integration","module_dir":"integrations/ck config"}`)
cliGitRun(t, repository, "add", ".") cliGitRun(t, repository, "add", ".")
@@ -138,8 +138,8 @@ func TestMultiModuleLauncherBuildsToolFromSelectedDependency(t *testing.T) {
example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module") example := filepath.Join(cliRepositoryRoot(t), "examples", "multi-module")
repository := newCLIRepository(t) repository := newCLIRepository(t)
copyExampleTree(t, example, repository) copyExampleTree(t, example, repository)
writeRepositoryFile(t, repository, "go.mod", "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.197\n") writeRepositoryFile(t, repository, "go.mod", "module example.com/multi-module-consumer\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit v0.2.197\n")
writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit v0.2.198\n") writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", "module example.com/multi-module-consumer/integrations/ckconfig\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit v0.2.198\n")
cliGitRun(t, repository, "add", ".") cliGitRun(t, repository, "add", ".")
cliGitRun(t, repository, "commit", "-m", "prepare selected tool module") cliGitRun(t, repository, "commit", "-m", "prepare selected tool module")

View File

@@ -12,9 +12,10 @@ import (
"os/exec" "os/exec"
"path/filepath" "path/filepath"
"strings" "strings"
"sync"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
) )
func TestMultiModuleWoodpeckerDelivery(t *testing.T) { func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
@@ -43,18 +44,19 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
repository := newWorkflowRepository(t, example) repository := newWorkflowRepository(t, example)
writeMappedProfile(t, repository, "releasekit-ckconfig.json", cli.Settings{ writeMappedProfile(t, repository, "releasekit-ckconfig.json", cli.Settings{
Product: "Example Consumer Integration", ModuleDir: "integrations/ckconfig", Product: "Example Consumer Integration", ModuleDir: "integrations/ckconfig",
Delivery: workflowDeliverySettings(delivery.URL), Delivery: workflowDeliverySettings(delivery.snapshot().URL),
}) })
writeRepositoryFile(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Example Consumer Integration v1.2.3\n\nTagged integration note.\n") writeRepositoryFile(t, repository, "integrations/ckconfig/docs/releases/v1.2.3.md", "# Example Consumer Integration v1.2.3\n\nTagged integration note.\n")
writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/ckconfig", "v0.2.197")) writeRepositoryFile(t, repository, "integrations/ckconfig/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/ckconfig", "v0.2.197"))
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.commit = commit delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy)
env["CI_COMMIT_SHA"] = cliGitOutput(t, repository, "rev-parse", "refs/tags/"+tag)
if output := runWoodpeckerBlock(t, command, repository, env); !strings.Contains(output, "state complete") || !strings.Contains(output, "tool-version=v0.2.197") { if output := runWoodpeckerBlock(t, command, repository, env); !strings.Contains(output, "state complete") || !strings.Contains(output, "tool-version=v0.2.197") {
t.Fatalf("tagged page delivery output = %q", output) t.Fatalf("tagged page delivery output = %q", output)
} }
if delivery.creates != 1 || delivery.requests == 0 || delivery.tag != tag { if delivery.snapshot().creates != 1 || delivery.snapshot().requests == 0 || delivery.snapshot().tag != tag {
t.Fatalf("page delivery fixture = %#v", delivery) t.Fatalf("page delivery fixture = %#v", delivery.snapshot())
} }
cliGitRun(t, repository, "checkout", "main") cliGitRun(t, repository, "checkout", "main")
@@ -68,8 +70,8 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
if !strings.Contains(output, "tool-version=v0.2.197") || strings.Contains(output, "tool-version=v0.2.198") { if !strings.Contains(output, "tool-version=v0.2.197") || strings.Contains(output, "tool-version=v0.2.198") {
t.Fatalf("tag retry did not use its committed tool version: %q", output) t.Fatalf("tag retry did not use its committed tool version: %q", output)
} }
if delivery.creates != 1 || delivery.tag != tag { if delivery.snapshot().creates != 1 || delivery.snapshot().tag != tag {
t.Fatalf("tag retry did not reuse the tagged mapping: %#v", delivery) t.Fatalf("tag retry did not reuse the tagged mapping: %#v", delivery.snapshot())
} }
}) })
@@ -80,25 +82,26 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{ writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{
Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2", Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2",
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "core", Targets: []string{"linux/amd64"}}, Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "core", Targets: []string{"linux/amd64"}},
Delivery: workflowDeliverySettings(delivery.URL), Delivery: workflowDeliverySettings(delivery.snapshot().URL),
}) })
writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n") writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n")
prepareWorkflowModuleSum(t, repository, "integrations/core/v2", proxy) prepareWorkflowModuleSum(t, repository, "integrations/core/v2", proxy)
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.commit = commit delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy)
output := runWoodpeckerBlock(t, command, repository, env) output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") { if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") {
t.Fatalf("binary delivery output = %q", output) t.Fatalf("binary delivery output = %q", output)
} }
if delivery.creates != 1 || delivery.uploads != 2 || delivery.tag != tag { observed := delivery.snapshot()
t.Fatalf("binary delivery fixture = %#v", delivery) if observed.creates != 1 || observed.uploads != 2 || observed.tag != tag {
t.Fatalf("binary delivery fixture = %#v", observed)
} }
if _, ok := delivery.assets["core-v2.1.0-linux-amd64"]; !ok { if _, ok := observed.assets["core-v2.1.0-linux-amd64"]; !ok {
t.Fatalf("binary asset missing: %#v", delivery.assets) t.Fatalf("binary asset missing: %#v", observed.assets)
} }
if _, ok := delivery.assets["SHA256SUMS"]; !ok { if _, ok := observed.assets["SHA256SUMS"]; !ok {
t.Fatalf("checksum asset missing: %#v", delivery.assets) t.Fatalf("checksum asset missing: %#v", observed.assets)
} }
}) })
@@ -109,21 +112,21 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
writeRepositoryFile(t, repository, "release-modules.tsv", strings.ReplaceAll(readWorkflowFile(t, repository, "release-modules.tsv"), "integrations/core/v2\treleasekit-core-v2.json", "integrations/core\treleasekit-core-v2.json")) writeRepositoryFile(t, repository, "release-modules.tsv", strings.ReplaceAll(readWorkflowFile(t, repository, "release-modules.tsv"), "integrations/core/v2\treleasekit-core-v2.json", "integrations/core\treleasekit-core-v2.json"))
writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{ writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{
Product: "Example Consumer Core v2", ModuleDir: "integrations/core", Product: "Example Consumer Core v2", ModuleDir: "integrations/core",
Delivery: workflowDeliverySettings(delivery.URL), Delivery: workflowDeliverySettings(delivery.snapshot().URL),
}) })
writeRepositoryFile(t, repository, "integrations/core/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/core/v2", "v0.2.197")) writeRepositoryFile(t, repository, "integrations/core/go.mod", workflowModuleFile("example.com/multi-module-consumer/integrations/core/v2", "v0.2.197"))
writeRepositoryFile(t, repository, "integrations/core/main.go", readWorkflowFile(t, example, "integrations/core/v2/main.go")) writeRepositoryFile(t, repository, "integrations/core/main.go", readWorkflowFile(t, example, "integrations/core/v2/main.go"))
writeRepositoryFile(t, repository, "integrations/core/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n") writeRepositoryFile(t, repository, "integrations/core/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nTagged core note.\n")
prepareWorkflowModuleSum(t, repository, "integrations/core", proxy) prepareWorkflowModuleSum(t, repository, "integrations/core", proxy)
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.commit = commit delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy)
output := runWoodpeckerBlock(t, command, repository, env) output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") { if !strings.Contains(output, "tool-version=v0.2.197") || !strings.Contains(output, "state complete") {
t.Fatalf("virtual-major delivery output = %q", output) t.Fatalf("virtual-major delivery output = %q", output)
} }
if delivery.creates != 1 || delivery.tag != tag { if delivery.snapshot().creates != 1 || delivery.snapshot().tag != tag {
t.Fatalf("virtual-major delivery fixture = %#v", delivery) t.Fatalf("virtual-major delivery fixture = %#v", delivery.snapshot())
} }
}) })
@@ -138,24 +141,48 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
writeRepositoryFile(t, repository, "release-modules.tsv", mapping) writeRepositoryFile(t, repository, "release-modules.tsv", mapping)
writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{ writeMappedProfile(t, repository, "releasekit-core-v2.json", cli.Settings{
Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2", Product: "Example Consumer Core v2", ModuleDir: "integrations/core/v2",
Delivery: workflowDeliverySettings(delivery.URL), Delivery: workflowDeliverySettings(delivery.snapshot().URL),
}) })
writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nMismatched mapped note.\n") writeRepositoryFile(t, repository, "integrations/core/v2/docs/releases/v2.1.0.md", "# Example Consumer Core v2 v2.1.0\n\nMismatched mapped note.\n")
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.commit = commit delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy)
if output, err := executeWoodpeckerBlock(command, repository, env); err == nil { if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
t.Fatalf("inconsistent mapping succeeded: %s", output) t.Fatalf("inconsistent mapping succeeded: %s", output)
} else if !strings.Contains(output, tag) || !strings.Contains(output, "integrations/core/v2.1.0") { } else if !strings.Contains(output, tag) || !strings.Contains(output, "integrations/core/v2.1.0") {
t.Fatalf("inconsistent mapping did not fail the exact tag comparison: %s", output) t.Fatalf("inconsistent mapping did not fail the exact tag comparison: %s", output)
} }
if delivery.requests != 0 { if delivery.snapshot().requests != 0 {
t.Fatalf("exact event tag mismatch reached hosted delivery: %#v", delivery) t.Fatalf("exact event tag mismatch reached hosted delivery: %#v", delivery.snapshot())
} }
}) })
} }
type mappedDeliveryFixture struct { type mappedDeliveryFixture struct {
mu sync.Mutex
data mappedDeliveryData
}
func (state *mappedDeliveryFixture) update(change func(*mappedDeliveryData)) {
state.mu.Lock()
defer state.mu.Unlock()
change(&state.data)
}
func (state *mappedDeliveryFixture) snapshot() mappedDeliveryData {
state.mu.Lock()
defer state.mu.Unlock()
result := state.data
result.assets = make(map[string][]byte, len(state.data.assets))
for key, value := range state.data.assets {
result.assets[key] = append([]byte(nil), value...)
}
return result
}
type mappedDeliveryData struct {
URL string URL string
tag string tag string
commit string commit string
@@ -170,9 +197,12 @@ type mappedDeliveryFixture struct {
func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture { func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
t.Helper() t.Helper()
state := &mappedDeliveryFixture{tag: tag, assets: make(map[string][]byte)} state := &mappedDeliveryFixture{data: mappedDeliveryData{tag: tag, assets: make(map[string][]byte)}}
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { data := &state.data
state.requests++ server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
state.mu.Lock()
defer state.mu.Unlock()
data.requests++
if r.Header.Get("Authorization") != "token fixture-token" { if r.Header.Get("Authorization") != "token fixture-token" {
t.Errorf("delivery authorization = %q", r.Header.Get("Authorization")) t.Errorf("delivery authorization = %q", r.Header.Get("Authorization"))
} }
@@ -180,13 +210,13 @@ func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
escapedTag := url.PathEscape(tag) escapedTag := url.PathEscape(tag)
switch r.URL.EscapedPath() { switch r.URL.EscapedPath() {
case apiRoot + "tags/" + escapedTag: case apiRoot + "tags/" + escapedTag:
_ = json.NewEncoder(w).Encode(map[string]any{"name": tag, "commit": map[string]string{"sha": state.commit}}) _ = json.NewEncoder(w).Encode(map[string]any{"name": tag, "commit": map[string]string{"sha": data.commit}})
case apiRoot + "releases/tags/" + escapedTag: case apiRoot + "releases/tags/" + escapedTag:
if !state.page { if !data.page {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": state.commit, "name": state.name, "body": state.body, "draft": false, "prerelease": false}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": data.commit, "name": data.name, "body": data.body, "draft": false, "prerelease": false})
case apiRoot + "releases": case apiRoot + "releases":
var payload struct { var payload struct {
Tag string `json:"tag_name"` Tag string `json:"tag_name"`
@@ -197,18 +227,18 @@ func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Errorf("decode release create: %v", err) t.Errorf("decode release create: %v", err)
} }
if payload.Tag != tag || payload.Commit != state.commit { if payload.Tag != tag || payload.Commit != data.commit {
t.Errorf("release payload = %#v", payload) t.Errorf("release payload = %#v", payload)
} }
state.page = true data.page = true
state.name = payload.Name data.name = payload.Name
state.body = payload.Body data.body = payload.Body
state.creates++ data.creates++
_ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": state.commit, "name": payload.Name, "body": payload.Body, "draft": false, "prerelease": false}) _ = json.NewEncoder(w).Encode(map[string]any{"id": 10, "html_url": serverURL(r) + "/releases/10", "tag_name": tag, "target_commitish": data.commit, "name": payload.Name, "body": payload.Body, "draft": false, "prerelease": false})
case apiRoot + "releases/10/assets": case apiRoot + "releases/10/assets":
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
listing := make([]map[string]any, 0, len(state.assets)) listing := make([]map[string]any, 0, len(data.assets))
for name, content := range state.assets { for name, content := range data.assets {
listing = append(listing, map[string]any{"id": len(listing) + 1, "name": name, "size": len(content), "browser_download_url": serverURL(r) + "/files/" + name}) listing = append(listing, map[string]any{"id": len(listing) + 1, "name": name, "size": len(content), "browser_download_url": serverURL(r) + "/files/" + name})
} }
_ = json.NewEncoder(w).Encode(listing) _ = json.NewEncoder(w).Encode(listing)
@@ -231,13 +261,13 @@ func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
if err != nil { if err != nil {
t.Errorf("read asset bytes: %v", err) t.Errorf("read asset bytes: %v", err)
} }
state.assets[name] = content data.assets[name] = content
state.uploads++ data.uploads++
_ = json.NewEncoder(w).Encode(map[string]any{"id": state.uploads, "name": name, "size": len(content)}) _ = json.NewEncoder(w).Encode(map[string]any{"id": data.uploads, "name": name, "size": len(content)})
default: default:
if strings.HasPrefix(r.URL.Path, "/files/") { if strings.HasPrefix(r.URL.Path, "/files/") {
name := strings.TrimPrefix(r.URL.Path, "/files/") name := strings.TrimPrefix(r.URL.Path, "/files/")
if content, ok := state.assets[name]; ok { if content, ok := data.assets[name]; ok {
_, _ = w.Write(content) _, _ = w.Write(content)
return return
} }
@@ -248,8 +278,9 @@ func newMappedDeliveryFixture(t *testing.T, tag string) *mappedDeliveryFixture {
http.NotFound(w, r) http.NotFound(w, r)
} }
})) }))
data.URL = "http://" + server.Listener.Addr().String()
server.Start()
t.Cleanup(server.Close) t.Cleanup(server.Close)
state.URL = server.URL
return state return state
} }
@@ -271,7 +302,7 @@ func buildWorkflowCLI(t *testing.T, root string) string {
func writeWorkflowToolProxy(t *testing.T) string { func writeWorkflowToolProxy(t *testing.T) string {
t.Helper() t.Helper()
const module = "gitea.maximumdirect.net/eric/ReleaseKit" const module = "go.maximumdirect.net/releasekit"
const helper = "example.com/releasekit-helper" const helper = "example.com/releasekit-helper"
proxyDir := t.TempDir() proxyDir := t.TempDir()
moduleFile := "module " + module + "\n\ngo 1.25.0\n\nrequire " + helper + " v1.0.0\n" moduleFile := "module " + module + "\n\ngo 1.25.0\n\nrequire " + helper + " v1.0.0\n"
@@ -300,7 +331,7 @@ func main() {
`, helper, version) `, helper, version)
files := map[string]string{ files := map[string]string{
"cmd/releasekit/main.go": main, "cmd/releasekit/main.go": main,
"buildinfo/version.go": "package buildinfo\nimport \"gitea.maximumdirect.net/eric/ReleaseKit/internal/runtimeinfo\"\nfunc Version() string { return runtimeinfo.Version() }\n", "buildinfo/version.go": "package buildinfo\nimport \"go.maximumdirect.net/releasekit/internal/runtimeinfo\"\nfunc Version() string { return runtimeinfo.Version() }\n",
"internal/runtimeinfo/version.go": "package runtimeinfo\nvar injectedVersion string\nfunc Version() string { if injectedVersion != \"\" { return injectedVersion }; return \"dev\" }\n", "internal/runtimeinfo/version.go": "package runtimeinfo\nvar injectedVersion string\nfunc Version() string { if injectedVersion != \"\" { return injectedVersion }; return \"dev\" }\n",
} }
writeModuleProxyVersion(t, proxyDir, module, version, moduleFile, files) writeModuleProxyVersion(t, proxyDir, module, version, moduleFile, files)
@@ -331,7 +362,7 @@ func newWorkflowRepository(t *testing.T, example string) string {
} }
func workflowModuleFile(module, version string) string { func workflowModuleFile(module, version string) string {
return fmt.Sprintf("module %s\n\ngo 1.25.0\n\nrequire gitea.maximumdirect.net/eric/ReleaseKit %s\n", module, version) return fmt.Sprintf("module %s\n\ngo 1.25.0\n\nrequire go.maximumdirect.net/releasekit %s\n", module, version)
} }
func workflowDeliverySettings(baseURL string) *cli.DeliverySettings { func workflowDeliverySettings(baseURL string) *cli.DeliverySettings {
@@ -352,7 +383,7 @@ func tagAndPushWorkflowRepository(t *testing.T, repository, tag string) string {
cliGitRun(t, repository, "add", "--all") cliGitRun(t, repository, "add", "--all")
cliGitRun(t, repository, "commit", "-m", "prepare mapped delivery") cliGitRun(t, repository, "commit", "-m", "prepare mapped delivery")
commit := cliGitOutput(t, repository, "rev-parse", "HEAD") commit := cliGitOutput(t, repository, "rev-parse", "HEAD")
cliGitRun(t, repository, "tag", tag) cliGitRun(t, repository, "tag", "-a", tag, "-m", "mapped release")
remote := filepath.Join(filepath.Dir(repository), "origin.git") remote := filepath.Join(filepath.Dir(repository), "origin.git")
cliGitRun(t, repository, "init", "--bare", remote) cliGitRun(t, repository, "init", "--bare", remote)
cliGitRun(t, repository, "remote", "add", "origin", remote) cliGitRun(t, repository, "remote", "add", "origin", remote)
@@ -390,7 +421,7 @@ func prepareWorkflowModuleSum(t *testing.T, repository, moduleDir, proxy string)
return nil return nil
}) })
}) })
command := exec.Command("go", "mod", "download", "gitea.maximumdirect.net/eric/ReleaseKit@v0.2.197") command := exec.Command("go", "mod", "download", "go.maximumdirect.net/releasekit@v0.2.197")
command.Dir = filepath.Join(repository, filepath.FromSlash(moduleDir)) command.Dir = filepath.Join(repository, filepath.FromSlash(moduleDir))
command.Env = append(os.Environ(), "GOMODCACHE="+cache, "GOPROXY=file://"+filepath.ToSlash(proxy), "GOSUMDB=off", "GOWORK=off", "GOTOOLCHAIN=local", "GOFLAGS=-modcacherw") command.Env = append(os.Environ(), "GOMODCACHE="+cache, "GOPROXY=file://"+filepath.ToSlash(proxy), "GOSUMDB=off", "GOWORK=off", "GOTOOLCHAIN=local", "GOFLAGS=-modcacherw")
if output, err := command.CombinedOutput(); err != nil { if output, err := command.CombinedOutput(); err != nil {

View File

@@ -79,9 +79,9 @@ func TestReleaseCandidate(t *testing.T) {
"product": "ReleaseKit", "product": "ReleaseKit",
"checks": []map[string]any{{"args": []string{"sh", "-c", "if [ -n \"$RELEASEKIT_TEST_ARM_GIT_SHIM\" ]; then : > \"$RELEASEKIT_TEST_ARM_GIT_SHIM\"; fi"}}}, "checks": []map[string]any{{"args": []string{"sh", "-c", "if [ -n \"$RELEASEKIT_TEST_ARM_GIT_SHIM\" ]; then : > \"$RELEASEKIT_TEST_ARM_GIT_SHIM\"; fi"}}},
"delivery": map[string]any{ "delivery": map[string]any{
"base_url": "https://gitea.maximumdirect.net", "base_url": "https://forge.maximumdirect.net",
"owner": "eric", "owner": "base-layer",
"repository": "ReleaseKit", "repository": "releasekit",
"token_env": "RELEASEKIT_GITEA_TOKEN", "token_env": "RELEASEKIT_GITEA_TOKEN",
}, },
} }

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
) )
func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) { func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
@@ -24,7 +24,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
if profile.Build != nil && len(profile.Build.Targets) != 0 { if profile.Build != nil && len(profile.Build.Targets) != 0 {
t.Fatalf("ReleaseKit profile declares binary targets: %#v", profile.Build.Targets) t.Fatalf("ReleaseKit profile declares binary targets: %#v", profile.Build.Targets)
} }
if profile.Delivery == nil || profile.Delivery.BaseURL != "https://gitea.maximumdirect.net" || profile.Delivery.Owner != "eric" || profile.Delivery.Repository != "ReleaseKit" || profile.Delivery.TokenEnv != "RELEASEKIT_GITEA_TOKEN" { if profile.Delivery == nil || profile.Delivery.BaseURL != "https://forge.maximumdirect.net" || profile.Delivery.Owner != "base-layer" || profile.Delivery.Repository != "releasekit" || profile.Delivery.TokenEnv != "RELEASEKIT_GITEA_TOKEN" {
t.Fatalf("ReleaseKit delivery profile = %#v", profile.Delivery) t.Fatalf("ReleaseKit delivery profile = %#v", profile.Delivery)
} }
repository := filepath.Join(t.TempDir(), "tagged ReleaseKit") repository := filepath.Join(t.TempDir(), "tagged ReleaseKit")
@@ -37,9 +37,9 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
const version = "v1.2.3" const version = "v1.2.3"
note := "# ReleaseKit v1.2.3\n\nSource-only delivery fixture.\n\n" + note := "# ReleaseKit v1.2.3\n\nSource-only delivery fixture.\n\n" +
"[COPYRIGHT](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v1.2.3/COPYRIGHT) |\n" + "[COPYRIGHT](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v1.2.3/COPYRIGHT) |\n" +
"[LICENSE](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v1.2.3/LICENSE) |\n" + "[LICENSE](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v1.2.3/LICENSE) |\n" +
"[ATTRIBUTION](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v1.2.3/docs/oss/attribution.md)\n" "[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v1.2.3/docs/oss/attribution.md)\n"
notePath := filepath.Join(repository, "docs", "releases", version+".md") notePath := filepath.Join(repository, "docs", "releases", version+".md")
if err := os.MkdirAll(filepath.Dir(notePath), 0o700); err != nil { if err := os.MkdirAll(filepath.Dir(notePath), 0o700); err != nil {
t.Fatal(err) t.Fatal(err)
@@ -50,14 +50,14 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
cliGitRun(t, repository, "add", "docs/releases/"+version+".md") cliGitRun(t, repository, "add", "docs/releases/"+version+".md")
cliGitRun(t, repository, "commit", "-m", "prepare source-only delivery") cliGitRun(t, repository, "commit", "-m", "prepare source-only delivery")
state := &deliveryServerState{product: "ReleaseKit"} state := &deliveryServerState{data: deliveryServerData{product: "ReleaseKit"}}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{ writeDeliveryProfile(t, repository, cli.Settings{
Product: "ReleaseKit", Product: "ReleaseKit",
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") state.update(func(state *deliveryServerData) { state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") })
cliGitRun(t, repository, "tag", "-a", version, "-m", "ReleaseKit v1.2.3") cliGitRun(t, repository, "tag", "-a", version, "-m", "ReleaseKit v1.2.3")
remote := filepath.Join(t.TempDir(), "origin.git") remote := filepath.Join(t.TempDir(), "origin.git")
@@ -78,7 +78,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
} }
env := map[string]string{ env := map[string]string{
"CI_WORKSPACE": repository, "CI_WORKSPACE": repository,
"CI_COMMIT_SHA": state.commit, "CI_COMMIT_SHA": cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version),
"CI_COMMIT_TAG": version, "CI_COMMIT_TAG": version,
"RELEASEKIT_GITEA_TOKEN": "fixture-token", "RELEASEKIT_GITEA_TOKEN": "fixture-token",
"GOTOOLCHAIN": "local", "GOTOOLCHAIN": "local",
@@ -87,14 +87,14 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
} }
command := strings.ReplaceAll(block, "$${", "${") command := strings.ReplaceAll(block, "$${", "${")
state.remoteCommit = strings.Repeat("f", 40) state.update(func(state *deliveryServerData) { state.remoteCommit = strings.Repeat("f", 40) })
if output, err := executeWoodpeckerBlock(command, repository, env); err == nil { if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
t.Fatalf("delivery with a mismatched hosted tag succeeded: %s", output) t.Fatalf("delivery with a mismatched hosted tag succeeded: %s", output)
} }
if state.releaseCreates != 0 || cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}") != state.commit { if state.snapshot().releaseCreates != 0 || cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}") != state.snapshot().commit {
t.Fatal("delivery failure changed hosted release state or source tag") t.Fatal("delivery failure changed hosted release state or source tag")
} }
state.remoteCommit = "" state.update(func(state *deliveryServerData) { state.remoteCommit = "" })
for attempt := 0; attempt < 2; attempt++ { for attempt := 0; attempt < 2; attempt++ {
output := runWoodpeckerBlock(t, command, repository, env) output := runWoodpeckerBlock(t, command, repository, env)
@@ -102,14 +102,14 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
t.Fatalf("delivery attempt %d output = %q", attempt+1, output) t.Fatalf("delivery attempt %d output = %q", attempt+1, output)
} }
} }
if state.releaseCreates != 1 || state.assetUploads != 0 || state.assetListReads != 0 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 || state.snapshot().assetListReads != 0 {
t.Fatalf("page-only workflow calls = creates:%d uploads:%d asset reads:%d", state.releaseCreates, state.assetUploads, state.assetListReads) t.Fatalf("page-only workflow calls = creates:%d uploads:%d asset reads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads, state.snapshot().assetListReads)
} }
if state.noteBody != note { if state.snapshot().noteBody != note {
t.Fatalf("hosted note body = %q, want complete note %q", state.noteBody, note) t.Fatalf("hosted note body = %q, want complete note %q", state.snapshot().noteBody, note)
} }
if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}"); got != state.commit { if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}"); got != state.snapshot().commit {
t.Fatalf("source tag moved after delivery: %s, want %s", got, state.commit) t.Fatalf("source tag moved after delivery: %s, want %s", got, state.snapshot().commit)
} }
if status := cliGitOutput(t, repository, "status", "--porcelain"); status != "" { if status := cliGitOutput(t, repository, "status", "--porcelain"); status != "" {
t.Fatalf("tagged checkout became dirty after delivery: %s", status) t.Fatalf("tagged checkout became dirty after delivery: %s", status)

View File

@@ -15,7 +15,7 @@ import (
"sync/atomic" "sync/atomic"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "go.maximumdirect.net/releasekit/cmd/releasekit/internal/cli"
) )
func TestWoodpeckerTagDeliveryTemplate(t *testing.T) { func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
@@ -42,7 +42,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
commit := pushTaggedCLIRepository(t, repository) commit := pushTaggedCLIRepository(t, repository)
state.commit = commit state.update(func(state *deliveryServerData) { state.commit = commit })
goDir, goLog := installGoBootstrapShim(t) goDir, goLog := installGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog) env := templateEnvironment(repository, commit, goDir, goLog)
@@ -51,8 +51,8 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
if !strings.Contains(output, "state complete") { if !strings.Contains(output, "state complete") {
t.Fatalf("delivery output=%q", output) t.Fatalf("delivery output=%q", output)
} }
if state.releaseCreates != 1 || state.assetUploads != 0 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 0 {
t.Fatalf("source-only calls = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) t.Fatalf("source-only calls = creates:%d uploads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads)
} }
assertModuleBootstrap(t, repository, goLog, moduleBefore) assertModuleBootstrap(t, repository, goLog, moduleBefore)
}) })
@@ -73,7 +73,7 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
commit := pushTaggedCLIRepository(t, repository) commit := pushTaggedCLIRepository(t, repository)
state.commit = commit state.update(func(state *deliveryServerData) { state.commit = commit })
goDir, goLog := installGoBootstrapShim(t) goDir, goLog := installGoBootstrapShim(t)
env := templateEnvironment(repository, commit, goDir, goLog) env := templateEnvironment(repository, commit, goDir, goLog)
@@ -88,14 +88,14 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
t.Fatalf("second delivery did not reuse matching assets: %q", output) t.Fatalf("second delivery did not reuse matching assets: %q", output)
} }
} }
if state.releaseCreates != 1 || state.assetUploads != 2 { if state.snapshot().releaseCreates != 1 || state.snapshot().assetUploads != 2 {
t.Fatalf("binary calls = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) t.Fatalf("binary calls = creates:%d uploads:%d", state.snapshot().releaseCreates, state.snapshot().assetUploads)
} }
if _, ok := state.assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok { if _, ok := state.snapshot().assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok {
t.Fatalf("binary asset was not delivered: %#v", state.assets) t.Fatalf("binary asset was not delivered: %#v", state.snapshot().assets)
} }
if _, ok := state.assets["SHA256SUMS"]; !ok { if _, ok := state.snapshot().assets["SHA256SUMS"]; !ok {
t.Fatalf("checksum manifest was not delivered: %#v", state.assets) t.Fatalf("checksum manifest was not delivered: %#v", state.snapshot().assets)
} }
assertModuleBootstrap(t, repository, goLog, moduleBefore, sumBefore) assertModuleBootstrap(t, repository, goLog, moduleBefore, sumBefore)
}) })
@@ -104,10 +104,14 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
selectReleaseKitModule(t, repository, root, false) selectReleaseKitModule(t, repository, root, false)
commit := pushTaggedCLIRepository(t, repository) commit := pushTaggedCLIRepository(t, repository)
cliGitRun(t, repository, "commit", "--allow-empty", "-m", "unrelated event commit")
unrelated := cliGitOutput(t, repository, "rev-parse", "HEAD")
goDir, goLog := installControlledGoBootstrapShim(t) goDir, goLog := installControlledGoBootstrapShim(t)
env := templateEnvironment(repository, strings.Repeat("f", len(commit)), goDir, goLog) env := templateEnvironment(repository, unrelated, goDir, goLog)
if output, err := executeWoodpeckerBlock(command, repository, env); err == nil { if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
t.Fatalf("wrong tag succeeded: %s", output) t.Fatalf("wrong tag succeeded: %s", output)
} else if !strings.Contains(output, commit) || !strings.Contains(output, unrelated) {
t.Fatalf("tag mismatch omitted commit identities: %s", output)
} }
assertNoCommands(t, goLog) assertNoCommands(t, goLog)
}) })
@@ -126,26 +130,49 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
}) })
t.Run("selected executable receives event arguments and its status", func(t *testing.T) { t.Run("selected executable receives event arguments and its status", func(t *testing.T) {
repository := newCLIRepository(t) for _, event := range []struct {
selectReleaseKitModule(t, repository, root, false) name, tag string
commit := pushTaggedCLIRepository(t, repository) tagObject bool
goDir, goLog := installControlledGoBootstrapShim(t) }{
toolArgs := filepath.Join(t.TempDir(), "tool-arguments") {"lightweight commit", "v1.2.3", false},
env := templateEnvironment(repository, strings.ToUpper(commit), goDir, goLog) {"annotated commit", "v1.2.4", false},
env["RELEASEKIT_TOOL_ARGS_LOG"] = toolArgs {"annotated tag object", "v1.2.4", true},
env["RELEASEKIT_TOOL_EXIT"] = "37" } {
output, err := executeWoodpeckerBlock(command, repository, env) t.Run(event.name, func(t *testing.T) {
var exitError *exec.ExitError repository := newCLIRepository(t)
if !errors.As(err, &exitError) || exitError.ExitCode() != 37 { selectReleaseKitModule(t, repository, root, false)
t.Fatalf("template error = %v, want propagated exit 37; output=%q", err, output) commit := pushTaggedCLIRepository(t, repository)
if event.tag == "v1.2.4" {
cliGitRun(t, repository, "tag", "-a", event.tag, "-m", "annotated release")
cliGitRun(t, repository, "push", "origin", "refs/tags/"+event.tag)
}
eventID := commit
if event.tagObject {
eventID = cliGitOutput(t, repository, "rev-parse", "refs/tags/"+event.tag)
}
goDir, goLog := installControlledGoBootstrapShim(t)
toolArgs := filepath.Join(t.TempDir(), "tool-arguments")
env := templateEnvironment(repository, strings.ToUpper(eventID), goDir, goLog)
env["CI_COMMIT_TAG"] = event.tag
env["RELEASEKIT_TOOL_ARGS_LOG"] = toolArgs
env["RELEASEKIT_TOOL_EXIT"] = "37"
output, err := executeWoodpeckerBlock(command, repository, env)
var exitError *exec.ExitError
if !errors.As(err, &exitError) || exitError.ExitCode() != 37 {
t.Fatalf("template error = %v, want propagated exit 37; output=%q", err, output)
}
args := readWoodpeckerFixtureFile(t, toolArgs)
for _, expected := range []string{"<ci-deliver>", "<--repo>", "<" + repository + ">", "<--prepared>", "<" + event.tag + ">"} {
if !strings.Contains(args, expected) {
t.Errorf("selected executable arguments %q omit %q", args, expected)
}
}
if !strings.Contains(strings.ToLower(args), "<--commit>\n<"+commit+">") {
t.Errorf("selected executable did not receive source commit %s: %q", commit, args)
}
assertGoSelection(t, repository, goLog)
})
} }
args := readWoodpeckerFixtureFile(t, toolArgs)
for _, expected := range []string{"<ci-deliver>", "<--repo>", "<" + repository + ">", "<--prepared>", "<--commit>", "<" + strings.ToUpper(commit) + ">", "<v1.2.3>"} {
if !strings.Contains(args, expected) {
t.Errorf("selected executable arguments %q omit %q", args, expected)
}
}
assertGoSelection(t, repository, goLog)
}) })
} }
@@ -178,7 +205,7 @@ func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) {
t.Run("older tag retry uses its committed module selection", func(t *testing.T) { t.Run("older tag retry uses its committed module selection", func(t *testing.T) {
goDir, goLog := installGoBootstrapShim(t) goDir, goLog := installGoBootstrapShim(t)
repository := newCLIRepository(t) repository := newCLIRepository(t)
const module = "gitea.maximumdirect.net/eric/ReleaseKit" const module = "go.maximumdirect.net/releasekit"
writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire "+module+" v0.2.197\n") writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire "+module+" v0.2.197\n")
cliGitRun(t, repository, "add", "go.mod") cliGitRun(t, repository, "add", "go.mod")
cliGitRun(t, repository, "commit", "-m", "select the earlier ReleaseKit version") cliGitRun(t, repository, "commit", "-m", "select the earlier ReleaseKit version")
@@ -212,7 +239,7 @@ func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) {
t.Run("versioned replacement selects its declared source", func(t *testing.T) { t.Run("versioned replacement selects its declared source", func(t *testing.T) {
goDir, goLog := installGoBootstrapShim(t) goDir, goLog := installGoBootstrapShim(t)
repository := newCLIRepository(t) repository := newCLIRepository(t)
const module = "gitea.maximumdirect.net/eric/ReleaseKit" const module = "go.maximumdirect.net/releasekit"
goMod := "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire " + module + " v0.2.197\n\nreplace " + module + " v0.2.197 => " + module + " v0.2.198\n" goMod := "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire " + module + " v0.2.197\n\nreplace " + module + " v0.2.197 => " + module + " v0.2.198\n"
writeRepositoryFile(t, repository, "go.mod", goMod) writeRepositoryFile(t, repository, "go.mod", goMod)
cliGitRun(t, repository, "add", "go.mod") cliGitRun(t, repository, "add", "go.mod")
@@ -453,7 +480,7 @@ func templateEnvironment(repository, commit, shimDir, goLog string) map[string]s
"CI_COMMIT_SHA": commit, "CI_COMMIT_SHA": commit,
"CI_COMMIT_TAG": "v1.2.3", "CI_COMMIT_TAG": "v1.2.3",
"RELEASEKIT_GITEA_TOKEN": "fixture-token", "RELEASEKIT_GITEA_TOKEN": "fixture-token",
"RELEASEKIT_MODULE": "gitea.maximumdirect.net/eric/ReleaseKit", "RELEASEKIT_MODULE": "go.maximumdirect.net/releasekit",
"RELEASEKIT_REAL_GO": realGo, "RELEASEKIT_REAL_GO": realGo,
"RELEASEKIT_GO_LOG": goLog, "RELEASEKIT_GO_LOG": goLog,
"GOTOOLCHAIN": "local", "GOTOOLCHAIN": "local",
@@ -486,7 +513,7 @@ func executeWoodpeckerBlock(block, repository string, environment map[string]str
func selectReleaseKitModule(t *testing.T, repository, releaseKitSource string, includeSum bool) { func selectReleaseKitModule(t *testing.T, repository, releaseKitSource string, includeSum bool) {
t.Helper() t.Helper()
const module = "gitea.maximumdirect.net/eric/ReleaseKit" const module = "go.maximumdirect.net/releasekit"
goMod := fmt.Sprintf("module example.com/tagged-app\n\ngo 1.26.0\n\nrequire %s v0.2.0\n\nreplace %s => %s\n", module, module, strconv.Quote(releaseKitSource)) goMod := fmt.Sprintf("module example.com/tagged-app\n\ngo 1.26.0\n\nrequire %s v0.2.0\n\nreplace %s => %s\n", module, module, strconv.Quote(releaseKitSource))
writeRepositoryFile(t, repository, "go.mod", goMod) writeRepositoryFile(t, repository, "go.mod", goMod)
cliGitRun(t, repository, "add", "go.mod") cliGitRun(t, repository, "add", "go.mod")
@@ -499,7 +526,7 @@ func selectReleaseKitModule(t *testing.T, repository, releaseKitSource string, i
func writeReleaseKitModuleProxy(t *testing.T) string { func writeReleaseKitModuleProxy(t *testing.T) string {
t.Helper() t.Helper()
const module = "gitea.maximumdirect.net/eric/ReleaseKit" const module = "go.maximumdirect.net/releasekit"
const toolDependency = "example.com/releasekit-helper" const toolDependency = "example.com/releasekit-helper"
proxyDir := t.TempDir() proxyDir := t.TempDir()
toolModule := "module " + module + "\n\ngo 1.25.0\n\nrequire " + toolDependency + " v1.0.0\n" toolModule := "module " + module + "\n\ngo 1.25.0\n\nrequire " + toolDependency + " v1.0.0\n"
@@ -607,7 +634,7 @@ func assertGoSelection(t *testing.T, repository, goLog string) {
log := readWoodpeckerFixtureFile(t, goLog) log := readWoodpeckerFixtureFile(t, goLog)
list := "list -mod=mod -modfile=" list := "list -mod=mod -modfile="
build := "build -mod=mod -modfile=" build := "build -mod=mod -modfile="
module := "gitea.maximumdirect.net/eric/ReleaseKit" module := "go.maximumdirect.net/releasekit"
if !strings.Contains(log, "GOWORK=off GOTOOLCHAIN=local") || !strings.Contains(log, list) || !strings.Contains(log, "-m "+module) { if !strings.Contains(log, "GOWORK=off GOTOOLCHAIN=local") || !strings.Contains(log, list) || !strings.Contains(log, "-m "+module) {
t.Fatalf("Go module selection command is missing required settings: %q", log) t.Fatalf("Go module selection command is missing required settings: %q", log)
} }

View File

@@ -4,9 +4,9 @@ import (
"context" "context"
"io" "io"
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" internalbuild "go.maximumdirect.net/releasekit/internal/build"
internaldelivery "gitea.maximumdirect.net/eric/ReleaseKit/internal/delivery" internaldelivery "go.maximumdirect.net/releasekit/internal/delivery"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea" "go.maximumdirect.net/releasekit/internal/gitea"
) )
// DeliveryState describes the result of one delivery invocation. // DeliveryState describes the result of one delivery invocation.

View File

@@ -8,7 +8,7 @@ import (
"syscall" "syscall"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
) )
func TestDeliverReadsLocalBinaryOnceForVerificationAndOnceForUpload(t *testing.T) { func TestDeliverReadsLocalBinaryOnceForVerificationAndOnceForUpload(t *testing.T) {
@@ -24,9 +24,10 @@ func TestDeliverReadsLocalBinaryOnceForVerificationAndOnceForUpload(t *testing.T
} }
state := &deliveryBenchmarkServer{commit: build.CandidateCommit, assets: expected} state := &deliveryBenchmarkServer{commit: build.CandidateCommit, assets: expected}
state.reset(false) state.reset(false)
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP)) server := httptest.NewUnstartedServer(http.HandlerFunc(state.serveHTTP))
state.baseURL = "http://" + server.Listener.Addr().String()
server.Start()
defer server.Close() defer server.Close()
state.baseURL = server.URL
result, err := releasekit.Deliver(t.Context(), releasekit.DeliveryRequest{ result, err := releasekit.Deliver(t.Context(), releasekit.DeliveryRequest{
RepositoryDir: repository, Product: "Sample App", Version: build.Version, RepositoryDir: repository, Product: "Sample App", Version: build.Version,
Destination: facadeDeliveryDestination(server.URL), Prebuilt: &build, Destination: facadeDeliveryDestination(server.URL), Prebuilt: &build,

View File

@@ -13,9 +13,10 @@ import (
"path/filepath" "path/filepath"
"runtime" "runtime"
"strings" "strings"
"sync"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
) )
func TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput(t *testing.T) { func TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput(t *testing.T) {
@@ -41,11 +42,12 @@ func TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput(t *testing.
if err != nil { if err != nil {
t.Fatalf("Deliver() error = %v; result = %#v", err, result) t.Fatalf("Deliver() error = %v; result = %#v", err, result)
} }
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != remote.commit { observed := remote.snapshot()
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != observed.commit {
t.Fatalf("delivery result = %#v", result) t.Fatalf("delivery result = %#v", result)
} }
if len(result.Uploaded) != 2 || len(remote.assets) != 2 { if len(result.Uploaded) != 2 || len(observed.assets) != 2 {
t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(remote.assets)) t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(observed.assets))
} }
if !strings.Contains(progress.String(), "checking fixture version output") { if !strings.Contains(progress.String(), "checking fixture version output") {
t.Fatalf("build progress = %q", progress.String()) t.Fatalf("build progress = %q", progress.String())
@@ -79,7 +81,8 @@ func TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Deliver() error = %v; result = %#v", err, result) t.Fatalf("Deliver() error = %v; result = %#v", err, result)
} }
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != remote.commit { observed := remote.snapshot()
if result.State != releasekit.DeliveryComplete || result.CandidateCommit != observed.commit {
t.Fatalf("delivery result = %#v", result) t.Fatalf("delivery result = %#v", result)
} }
wantAssets := []string{ wantAssets := []string{
@@ -87,12 +90,12 @@ func TestDeliverBinaryConsumerWithBuildinfoAndSelectedTargets(t *testing.T) {
"fixture-v1.2.3-linux-arm64", "fixture-v1.2.3-linux-arm64",
"SHA256SUMS", "SHA256SUMS",
} }
if len(result.Uploaded) != len(wantAssets) || len(remote.assets) != len(wantAssets) { if len(result.Uploaded) != len(wantAssets) || len(observed.assets) != len(wantAssets) {
t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(remote.assets)) t.Fatalf("uploaded = %#v; hosted assets = %v", result.Uploaded, mapKeys(observed.assets))
} }
for _, name := range wantAssets { for _, name := range wantAssets {
if _, ok := remote.assets[name]; !ok { if _, ok := observed.assets[name]; !ok {
t.Errorf("hosted assets omit %q: %v", name, mapKeys(remote.assets)) t.Errorf("hosted assets omit %q: %v", name, mapKeys(observed.assets))
} }
} }
if !strings.Contains(progress.String(), "checking fixture version output") { if !strings.Contains(progress.String(), "checking fixture version output") {
@@ -174,8 +177,8 @@ func TestDeliverRejectsSameSizePrebuiltTamperingBeforeHostedWrites(t *testing.T)
if err == nil || !strings.Contains(err.Error(), "verify build assets") || result.State != releasekit.DeliveryNotAttempted { if err == nil || !strings.Contains(err.Error(), "verify build assets") || result.State != releasekit.DeliveryNotAttempted {
t.Fatalf("Deliver() = %#v, %v; want local asset verification failure", result, err) t.Fatalf("Deliver() = %#v, %v; want local asset verification failure", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none before local verification", remote.requests) t.Fatalf("hosted requests = %d, want none before local verification", remote.snapshot().requests)
} }
} }
@@ -217,8 +220,8 @@ func TestDeliverPageOnlySkipsBuildAndHostedAssets(t *testing.T) {
if err != nil || result.State != releasekit.DeliveryComplete { if err != nil || result.State != releasekit.DeliveryComplete {
t.Fatalf("Deliver() = %#v, %v", result, err) t.Fatalf("Deliver() = %#v, %v", result, err)
} }
if len(remote.assets) != 0 || len(result.Uploaded) != 0 || len(result.Pending) != 0 { if len(remote.snapshot().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)) t.Fatalf("page-only delivery included assets: result=%#v assets=%v", result, mapKeys(remote.snapshot().assets))
} }
if progress.Len() != 0 { if progress.Len() != 0 {
t.Fatalf("page-only delivery emitted build progress: %q", progress.String()) t.Fatalf("page-only delivery emitted build progress: %q", progress.String())
@@ -242,8 +245,8 @@ func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) {
ExpectedTag: "integrations/wrong/v1.2.3", ExpectedTag: "integrations/wrong/v1.2.3",
Destination: facadeDeliveryDestination(server.URL), Destination: facadeDeliveryDestination(server.URL),
}) })
if err == nil || wrongTag.Tag != "v1.2.3" || remote.requests != 0 { if err == nil || wrongTag.Tag != "v1.2.3" || remote.snapshot().requests != 0 {
t.Fatalf("delivery with inconsistent expected tag = %#v, %v; hosted requests %d", wrongTag, err, remote.requests) t.Fatalf("delivery with inconsistent expected tag = %#v, %v; hosted requests %d", wrongTag, err, remote.snapshot().requests)
} }
rootResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{ rootResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
@@ -258,8 +261,10 @@ func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) {
} }
nestedTag := "integrations/ckconfig/v1.2.3" nestedTag := "integrations/ckconfig/v1.2.3"
remote.tag = nestedTag remote.update(func(remote *facadeDeliveryData) {
remote.noteBody = "# Fixture v1.2.3\n\nNested delivery fixture.\n" remote.tag = nestedTag
remote.noteBody = "# Fixture v1.2.3\n\nNested delivery fixture.\n"
})
nestedResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{ nestedResult, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
RepositoryDir: repository, RepositoryDir: repository,
ModuleDir: "integrations/ckconfig", ModuleDir: "integrations/ckconfig",
@@ -271,8 +276,8 @@ func TestDeliverUsesCommittedModuleIdentityForSeparatePages(t *testing.T) {
if err != nil || nestedResult.State != releasekit.DeliveryComplete || nestedResult.Tag != nestedTag || nestedResult.ModulePath != "example.com/ckconfig" { if err != nil || nestedResult.State != releasekit.DeliveryComplete || nestedResult.Tag != nestedTag || nestedResult.ModulePath != "example.com/ckconfig" {
t.Fatalf("nested delivery = %#v, %v", nestedResult, err) t.Fatalf("nested delivery = %#v, %v", nestedResult, err)
} }
if len(remote.pages) != 2 || !remote.pages["v1.2.3"] || !remote.pages[nestedTag] { if len(remote.snapshot().pages) != 2 || !remote.snapshot().pages["v1.2.3"] || !remote.snapshot().pages[nestedTag] {
t.Fatalf("created hosted pages = %#v", remote.pages) t.Fatalf("created hosted pages = %#v", remote.snapshot().pages)
} }
} }
@@ -293,21 +298,21 @@ func TestDeliverRejectsPrebuiltArtifactsForAnotherModuleBeforeHostedRequests(t *
} }
server, remote := newFacadeDeliveryServer(t, repository) server, remote := newFacadeDeliveryServer(t, repository)
defer server.Close() defer server.Close()
remote.tag = "integrations/ckconfig/v1.2.3" remote.update(func(remote *facadeDeliveryData) { remote.tag = "integrations/ckconfig/v1.2.3" })
result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{ result, err := releasekit.Deliver(context.Background(), releasekit.DeliveryRequest{
RepositoryDir: repository, RepositoryDir: repository,
ModuleDir: "integrations/ckconfig", ModuleDir: "integrations/ckconfig",
Product: "Fixture", Product: "Fixture",
Version: "v1.2.3", Version: "v1.2.3",
ExpectedTag: remote.tag, ExpectedTag: remote.snapshot().tag,
Destination: facadeDeliveryDestination(server.URL), Destination: facadeDeliveryDestination(server.URL),
Prebuilt: &prebuilt, Prebuilt: &prebuilt,
}) })
if err == nil || !strings.Contains(err.Error(), "selected tagged module") || result.Tag != remote.tag { if err == nil || !strings.Contains(err.Error(), "selected tagged module") || result.Tag != remote.snapshot().tag {
t.Fatalf("nested delivery with root artifacts = %#v, %v", result, err) t.Fatalf("nested delivery with root artifacts = %#v, %v", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests before prebuilt identity rejection = %d", remote.requests) t.Fatalf("hosted requests before prebuilt identity rejection = %d", remote.snapshot().requests)
} }
} }
@@ -318,7 +323,7 @@ func newTwoModuleDeliveryConsumer(t *testing.T) string {
import ( import (
"fmt" "fmt"
"os" "os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("fixture", buildinfo.Version()) } } func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("fixture", buildinfo.Version()) } }
`) `)
@@ -351,14 +356,14 @@ func TestDeliverRejectsDirtyTaggedSourceBeforeBuildOrHostedWrites(t *testing.T)
}, },
Output: &progress, Output: &progress,
}) })
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit { if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
t.Fatalf("Deliver() = %#v, %v; want preflight failure for known commit", result, err) t.Fatalf("Deliver() = %#v, %v; want preflight failure for known commit", result, err)
} }
if progress.Len() != 0 { if progress.Len() != 0 {
t.Fatalf("build started before source eligibility passed: %q", progress.String()) t.Fatalf("build started before source eligibility passed: %q", progress.String())
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
} }
@@ -392,8 +397,8 @@ func TestDeliverRejectsCheckoutAwayFromTagBeforeBuildOrHostedWrites(t *testing.T
if progress.Len() != 0 { if progress.Len() != 0 {
t.Fatalf("build started before tag/HEAD agreement: %q", progress.String()) t.Fatalf("build started before tag/HEAD agreement: %q", progress.String())
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
} }
@@ -417,14 +422,14 @@ func TestDeliverRejectsInvalidCommittedNoteBeforeBuildOrHostedWrites(t *testing.
}, },
Output: &progress, Output: &progress,
}) })
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit { if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
t.Fatalf("Deliver() = %#v, %v; want committed-note preflight failure", result, err) t.Fatalf("Deliver() = %#v, %v; want committed-note preflight failure", result, err)
} }
if progress.Len() != 0 { if progress.Len() != 0 {
t.Fatalf("build started before committed-note validation: %q", progress.String()) t.Fatalf("build started before committed-note validation: %q", progress.String())
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
} }
@@ -458,8 +463,8 @@ func TestDeliverManagedBuildRejectsCheckoutMutationBeforeHostedWrites(t *testing
if err == nil || !strings.Contains(err.Error(), "build source") || result.State != releasekit.DeliveryNotAttempted { 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) t.Fatalf("Deliver() = %#v, %v; want build-source eligibility failure", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
t.Fatalf("ineligible managed build output remains: entries=%v error=%v", entries, err) t.Fatalf("ineligible managed build output remains: entries=%v error=%v", entries, err)
@@ -484,11 +489,11 @@ func TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites(t *te
Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}}, Targets: []releasekit.Target{{OS: runtime.GOOS, Arch: runtime.GOARCH}},
}, },
}) })
if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.commit { if err == nil || result.State != releasekit.DeliveryNotAttempted || result.CandidateCommit != remote.snapshot().commit {
t.Fatalf("Deliver() = %#v, %v; want managed build failure before hosted writes", result, err) t.Fatalf("Deliver() = %#v, %v; want managed build failure before hosted writes", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
t.Fatalf("failed managed build output remains: entries=%v error=%v", entries, err) t.Fatalf("failed managed build output remains: entries=%v error=%v", entries, err)
@@ -520,8 +525,8 @@ func TestDeliverManagedBuildCancellationCleansTemporaryOutput(t *testing.T) {
if !errors.Is(err, context.Canceled) || result.State != releasekit.DeliveryNotAttempted { if !errors.Is(err, context.Canceled) || result.State != releasekit.DeliveryNotAttempted {
t.Fatalf("Deliver() = %#v, %v; want cancellation before hosted writes", result, err) t.Fatalf("Deliver() = %#v, %v; want cancellation before hosted writes", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 { if entries, err := os.ReadDir(temporaryRoot); err != nil || len(entries) != 0 {
t.Fatalf("canceled managed build output remains: entries=%v error=%v", entries, err) t.Fatalf("canceled managed build output remains: entries=%v error=%v", entries, err)
@@ -544,8 +549,8 @@ func TestDeliverRejectsBothBuildSourcesBeforeHostedWrites(t *testing.T) {
if err == nil || result.State != releasekit.DeliveryNotAttempted || !strings.Contains(err.Error(), "both") { if err == nil || result.State != releasekit.DeliveryNotAttempted || !strings.Contains(err.Error(), "both") {
t.Fatalf("Deliver() = %#v, %v; want invalid build-source combination", result, err) t.Fatalf("Deliver() = %#v, %v; want invalid build-source combination", result, err)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("hosted requests = %d, want none", remote.requests) t.Fatalf("hosted requests = %d, want none", remote.snapshot().requests)
} }
} }
@@ -559,7 +564,7 @@ func newTaggedDeliveryConsumerWithNote(t *testing.T, note string) string {
import ( import (
"fmt" "fmt"
"os" "os"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("fixture", buildinfo.Version()) } } func main() { if len(os.Args) == 2 && os.Args[1] == "version" { fmt.Println("fixture", buildinfo.Version()) } }
`) `)
@@ -582,6 +587,35 @@ func (writer deliveryCancelWriter) Write(content []byte) (int, error) {
} }
type facadeDeliveryServer struct { type facadeDeliveryServer struct {
mu sync.Mutex
data facadeDeliveryData
}
func (state *facadeDeliveryServer) update(change func(*facadeDeliveryData)) {
state.mu.Lock()
defer state.mu.Unlock()
change(&state.data)
}
func (state *facadeDeliveryServer) snapshot() facadeDeliveryData {
state.mu.Lock()
defer state.mu.Unlock()
result := state.data
result.assets = make(map[string][]byte, len(state.data.assets))
for key, value := range state.data.assets {
result.assets[key] = append([]byte(nil), value...)
}
result.pages = make(map[string]bool, len(state.data.pages))
for key, value := range state.data.pages {
result.pages[key] = value
}
return result
}
type facadeDeliveryData struct {
t *testing.T t *testing.T
commit string commit string
tag string tag string
@@ -595,24 +629,28 @@ type facadeDeliveryServer struct {
func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) { func newFacadeDeliveryServer(t *testing.T, repository string) (*httptest.Server, *facadeDeliveryServer) {
t.Helper() t.Helper()
state := &facadeDeliveryServer{t: t, commit: buildGit(t, repository, "rev-parse", "HEAD"), tag: "v1.2.3", version: "v1.2.3", noteBody: "# Fixture v1.2.3\n\nDelivery fixture.\n", pages: make(map[string]bool), assets: make(map[string][]byte)} state := &facadeDeliveryServer{data: facadeDeliveryData{t: t, commit: buildGit(t, repository, "rev-parse", "HEAD"), tag: "v1.2.3", version: "v1.2.3", noteBody: "# Fixture v1.2.3\n\nDelivery fixture.\n", pages: make(map[string]bool), assets: make(map[string][]byte)}}
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP)) server := httptest.NewUnstartedServer(http.HandlerFunc(state.serveHTTP))
state.url = server.URL state.data.url = "http://" + server.Listener.Addr().String()
server.Start()
return server, state return server, state
} }
func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Request) { func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Request) {
state.mu.Lock()
defer state.mu.Unlock()
data := &state.data
if strings.HasPrefix(r.URL.Path, "/api/") { if strings.HasPrefix(r.URL.Path, "/api/") {
state.requests++ data.requests++
if r.Header.Get("Authorization") != "token fixture-token" { if r.Header.Get("Authorization") != "token fixture-token" {
state.t.Errorf("authorization = %q", r.Header.Get("Authorization")) data.t.Errorf("authorization = %q", r.Header.Get("Authorization"))
} }
} }
switch r.URL.EscapedPath() { switch r.URL.EscapedPath() {
case "/api/v1/repos/owner/example/tags/" + url.PathEscape(state.tag): case "/api/v1/repos/owner/example/tags/" + url.PathEscape(data.tag):
_, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, state.tag, state.commit) _, _ = fmt.Fprintf(w, `{"name":%q,"commit":{"sha":%q}}`, data.tag, data.commit)
case "/api/v1/repos/owner/example/releases/tags/" + url.PathEscape(state.tag): case "/api/v1/repos/owner/example/releases/tags/" + url.PathEscape(data.tag):
if !state.pages[state.tag] { if !data.pages[data.tag] {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -625,20 +663,20 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
Body string `json:"body"` Body string `json:"body"`
} }
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
state.t.Errorf("decode release payload: %v", err) data.t.Errorf("decode release payload: %v", err)
http.Error(w, "invalid payload", http.StatusBadRequest) http.Error(w, "invalid payload", http.StatusBadRequest)
return return
} }
if payload.Tag != state.tag || payload.Target != state.commit || payload.Name != "Fixture "+state.version { if payload.Tag != data.tag || payload.Target != data.commit || payload.Name != "Fixture "+data.version {
state.t.Errorf("release payload = %#v", payload) data.t.Errorf("release payload = %#v", payload)
} }
state.pages[state.tag] = true data.pages[data.tag] = true
state.writeRelease(w) state.writeRelease(w)
case "/api/v1/repos/owner/example/releases/10/assets": case "/api/v1/repos/owner/example/releases/10/assets":
if r.Method == http.MethodGet { if r.Method == http.MethodGet {
list := make([]map[string]any, 0, len(state.assets)) list := make([]map[string]any, 0, len(data.assets))
for name, content := range state.assets { for name, content := range data.assets {
list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": state.url + "/files/" + name}) list = append(list, map[string]any{"id": len(list) + 1, "name": name, "size": len(content), "browser_download_url": data.url + "/files/" + name})
} }
_ = json.NewEncoder(w).Encode(list) _ = json.NewEncoder(w).Encode(list)
return return
@@ -646,22 +684,22 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
name := r.URL.Query().Get("name") name := r.URL.Query().Get("name")
file, _, err := r.FormFile("attachment") file, _, err := r.FormFile("attachment")
if err != nil { if err != nil {
state.t.Errorf("read upload %q: %v", name, err) data.t.Errorf("read upload %q: %v", name, err)
http.Error(w, "invalid multipart upload", http.StatusBadRequest) http.Error(w, "invalid multipart upload", http.StatusBadRequest)
return return
} }
content, err := io.ReadAll(file) content, err := io.ReadAll(file)
_ = file.Close() _ = file.Close()
if err != nil { if err != nil {
state.t.Errorf("read upload %q: %v", name, err) data.t.Errorf("read upload %q: %v", name, err)
} }
state.assets[name] = content data.assets[name] = content
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
_ = json.NewEncoder(w).Encode(map[string]any{"id": len(state.assets), "name": name, "size": len(content)}) _ = json.NewEncoder(w).Encode(map[string]any{"id": len(data.assets), "name": name, "size": len(content)})
default: default:
if strings.HasPrefix(r.URL.Path, "/files/") { if strings.HasPrefix(r.URL.Path, "/files/") {
name := strings.TrimPrefix(r.URL.Path, "/files/") name := strings.TrimPrefix(r.URL.Path, "/files/")
content, ok := state.assets[name] content, ok := data.assets[name]
if !ok { if !ok {
http.NotFound(w, r) http.NotFound(w, r)
return return
@@ -669,17 +707,19 @@ func (state *facadeDeliveryServer) serveHTTP(w http.ResponseWriter, r *http.Requ
_, _ = w.Write(content) _, _ = w.Write(content)
return return
} }
state.t.Errorf("unexpected delivery request %s %s", r.Method, r.URL) data.t.Errorf("unexpected delivery request %s %s", r.Method, r.URL)
http.NotFound(w, r) http.NotFound(w, r)
} }
} }
func (state *facadeDeliveryServer) writeRelease(w http.ResponseWriter) { func (state *facadeDeliveryServer) writeRelease(w http.ResponseWriter) {
// The request handler holds state.mu.
data := &state.data
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
_ = json.NewEncoder(w).Encode(map[string]any{ _ = json.NewEncoder(w).Encode(map[string]any{
"id": 10, "html_url": state.url + "/releases/10", "tag_name": state.tag, "id": 10, "html_url": data.url + "/releases/10", "tag_name": data.tag,
"target_commitish": state.commit, "name": "Fixture " + state.version, "target_commitish": data.commit, "name": "Fixture " + data.version,
"body": state.noteBody, "draft": false, "prerelease": false, "body": data.noteBody, "draft": false, "prerelease": false,
}) })
} }

View File

@@ -241,8 +241,11 @@ changes the selected checkout; `--prepared` reuses a verified checkout.
Ordinary `deliver` continues to operate on already prepared source without Ordinary `deliver` continues to operate on already prepared source without
fetching, checking out, or downloading dependencies. fetching, checking out, or downloading dependencies.
In Woodpecker, pass `CI_WORKSPACE`, `CI_COMMIT_SHA`, and `CI_COMMIT_TAG` as the In Woodpecker, pass `CI_WORKSPACE`, the source commit resolved from
repository, expected commit, and full tag arguments. The CLI does not read `CI_COMMIT_SHA`, and `CI_COMMIT_TAG` as the repository, expected commit, and full
tag arguments. The event ID may identify an annotated tag object; see the
[delivery guide](consumers/delivery.md#woodpecker-tag-workflows) for resolution
and workflow preparation. The CLI does not read
provider-specific environment variable names. Checkout and dependency provider-specific environment variable names. Checkout and dependency
preparation failures return status 1; usage, profile, credential, and invalid preparation failures return status 1; usage, profile, credential, and invalid
version errors return status 2. Final delivery uses the same statuses as version errors return status 2. Final delivery uses the same statuses as

View File

@@ -10,12 +10,14 @@ From the consumer module's root, select the ReleaseKit version to adopt:
```sh ```sh
: "${RELEASEKIT_TOOL_VERSION:?Set the released ReleaseKit version to adopt}" : "${RELEASEKIT_TOOL_VERSION:?Set the released ReleaseKit version to adopt}"
go get -tool "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@${RELEASEKIT_TOOL_VERSION}" go get -tool "go.maximumdirect.net/releasekit/cmd/releasekit@${RELEASEKIT_TOOL_VERSION}"
go mod tidy go mod tidy
go tool releasekit version go tool releasekit version
``` ```
Select a release that implements the API, profile, and workflow you are adopting. Select a release that implements the API, profile, and workflow you are adopting.
The vanity module path starts with v0.5.0. Versions through v0.4.2 declare the
original module path and use their corresponding historical instructions.
The module-release interface requires a release containing that feature; The module-release interface requires a release containing that feature;
pre-feature versions through v0.3.0 use their corresponding historical interface. pre-feature versions through v0.3.0 use their corresponding historical interface.
Go records the executable package in Go records the executable package in
@@ -43,13 +45,47 @@ required imports and select the module version:
```sh ```sh
: "${RELEASEKIT_TOOL_VERSION:?Set the released ReleaseKit version to adopt}" : "${RELEASEKIT_TOOL_VERSION:?Set the released ReleaseKit version to adopt}"
go get "gitea.maximumdirect.net/eric/ReleaseKit@${RELEASEKIT_TOOL_VERSION}" go get "go.maximumdirect.net/releasekit@${RELEASEKIT_TOOL_VERSION}"
go mod tidy go mod tidy
``` ```
API consumers can also register the CLI with `go get -tool` as above. Both uses API consumers can also register the CLI with `go get -tool` as above. Both uses
share the module-selected ReleaseKit version. share the module-selected ReleaseKit version.
## Migrating from the original module path
Starting with v0.5.0, ReleaseKit's module is `go.maximumdirect.net/releasekit`.
Earlier releases declare `gitea.maximumdirect.net/eric/ReleaseKit`. The migration
is a direct cutover with the existing facade roles and command behavior.
For API consumers, update imports of the root facade and `buildinfo` to
`go.maximumdirect.net/releasekit` and
`go.maximumdirect.net/releasekit/buildinfo`, including generated code and tests.
Select v0.5.0 or a later compatible release using the package-dependency command
above. Packages imported through the old and new paths have distinct Go
identities; coordinate updates in packages that exchange ReleaseKit facade
types. Builders and applications using `buildinfo` should adopt the new path
together so version injection addresses the runtime implementation they use.
For CLI tool consumers, remove the old tool declaration before registering the
new CLI package with the tool-dependency command above:
```sh
go mod edit -droptool=gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit
```
Update copied release launchers and Woodpecker workflows so their
`releasekit_module` value is `go.maximumdirect.net/releasekit`. Use the maintained
examples matching the selected tool version. Run `go mod tidy` after updating
imports and tool declarations, review any old-path dependencies retained
transitively, and commit the resulting module files with the workflow changes.
Existing consumer tags retain their recorded module files, tool selection, and
matching workflows for retries. Apply these migration steps to new consumer
revisions. Source repository and hosted release URLs use
`https://forge.maximumdirect.net/base-layer/releasekit`; the vanity path is the
Go module identity. The Go consumer minimum remains 1.26.0.
## CI adoption ## CI adoption
The maintained [consumer tag-delivery workflow](delivery.md#woodpecker-tag-workflows) The maintained [consumer tag-delivery workflow](delivery.md#woodpecker-tag-workflows)

View File

@@ -8,7 +8,7 @@ import (
"context" "context"
"fmt" "fmt"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
) )
func build(ctx context.Context) error { func build(ctx context.Context) error {

View File

@@ -196,14 +196,16 @@ a `delivery` profile. Page-only and binary behavior both use this template; the
profile's build target list selects whether application binaries are built and profile's build target list selects whether application binaries are built and
delivered. delivered.
Before running Go, the template fetches the explicit event tag, compares its Before running Go, the template fetches the explicit event tag and independently
peeled commit with the provider's full commit ID without regard to hex case, resolves both the tag and `CI_COMMIT_SHA` to source commits. The event ID may
checks out that commit detached, and confirms the source tree is clean. It then identify an annotated tag object or a commit. The template requires the resolved
copies the tagged `go.mod` and any `go.sum` to a temporary directory outside commits to match, checks out that commit detached, and confirms the source tree
the checkout. `go list -m` confirms that the module selects ReleaseKit; Go uses is clean. It then copies the tagged `go.mod` and any `go.sum` to a temporary
directory outside the checkout. `go list -m` confirms that the module selects
ReleaseKit; Go uses
an alternate `-modfile` to prepare dependencies and build the selected an alternate `-modfile` to prepare dependencies and build the selected
`cmd/releasekit` package without changing the tagged module files. The template `cmd/releasekit` package without changing the tagged module files. The template
invokes that executable with `CI_WORKSPACE`, `CI_COMMIT_SHA`, and invokes that executable with `CI_WORKSPACE`, the resolved event commit, and
`CI_COMMIT_TAG`. A cleanup trap removes the temporary metadata and executable. `CI_COMMIT_TAG`. A cleanup trap removes the temporary metadata and executable.
Tool bootstrap and application artifact preparation are separate. The bootstrap Tool bootstrap and application artifact preparation are separate. The bootstrap
@@ -242,11 +244,13 @@ and application artifact compilation retain their separate environments.
The normal Go module and build caches are sufficient. Cache mounts may be added The normal Go module and build caches are sufficient. Cache mounts may be added
as deployment configuration when they help the runner; they are not needed by as deployment configuration when they help the runner; they are not needed by
the template. A separately installed executable can be installed with the template. A separately installed executable can be installed with
`go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@PIN`, then run `go install go.maximumdirect.net/releasekit/cmd/releasekit@PIN`, then run
with the same arguments: with the same arguments. With the event object available in the CI checkout,
resolve it to its source commit before invocation:
```sh ```sh
releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$CI_COMMIT_SHA" "$CI_COMMIT_TAG" expected_commit=$(git -C "$CI_WORKSPACE" rev-parse --verify "${CI_COMMIT_SHA}^{commit}")
releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$expected_commit" "$CI_COMMIT_TAG"
``` ```
The module-based workflow follows the ReleaseKit version selected by the tagged The module-based workflow follows the ReleaseKit version selected by the tagged
@@ -255,6 +259,12 @@ consumer module configuration when adopting a newer ReleaseKit version and
verify it with a tag fixture. The command and profile behavior are documented verify it with a tag fixture. The command and profile behavior are documented
in the [CLI contract](../cli.md) and [configuration reference](../config.md). in the [CLI contract](../cli.md) and [configuration reference](../config.md).
Update copied workflow YAML when adopting corrected examples. A dependency
upgrade alone leaves that copy unchanged. An older tagged workflow that passes
a raw annotated tag object ID still fails on retry. Recover delivery from the
original tagged source with the resolved event commit through the command above,
preserving the published tag and existing content.
Woodpecker runs delivery after the tag event, independently from the source Woodpecker runs delivery after the tag event, independently from the source
publication result. Read its workflow status as delivery status: a delivery publication result. Read its workflow status as delivery status: a delivery
failure does not undo source publication. A failed, partial, or uncertain run failure does not undo source publication. A failed, partial, or uncertain run
@@ -276,7 +286,9 @@ describes these checks.
To isolate a public ReleaseKit download from application settings and the To isolate a public ReleaseKit download from application settings and the
existing module cache, run the following with a compatible local Go toolchain. existing module cache, run the following with a compatible local Go toolchain.
Replace `v0.2.0` with the affected version. This diagnostic accesses the public Replace `v0.5.0` with the affected vanity-path version. For an earlier release,
use its original `gitea.maximumdirect.net/eric/ReleaseKit` module path instead.
This diagnostic accesses the public
Go proxy and checksum database and retains its temporary directory for inspection. Go proxy and checksum database and retains its temporary directory for inspection.
```sh ```sh
@@ -288,7 +300,7 @@ Go proxy and checksum database and retains its temporary directory for inspectio
GOPRIVATE= GONOPROXY= GONOSUMDB= \ GOPRIVATE= GONOPROXY= GONOSUMDB= \
GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org \ GOPROXY=https://proxy.golang.org GOSUMDB=sum.golang.org \
GOMODCACHE="$checksum_dir/modules" \ GOMODCACHE="$checksum_dir/modules" \
go mod download -json gitea.maximumdirect.net/eric/ReleaseKit@v0.2.0 go mod download -json go.maximumdirect.net/releasekit@v0.5.0
) )
``` ```

View File

@@ -4,7 +4,7 @@ Applications can read their embedded version through the public `buildinfo`
package: package:
```go ```go
import "gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" import "go.maximumdirect.net/releasekit/buildinfo"
version := buildinfo.Version() version := buildinfo.Version()
``` ```

View File

@@ -24,7 +24,7 @@ Go declarations and GoDoc remain the owners of exact exported contracts.
## Module and dependencies ## Module and dependencies
The module is `gitea.maximumdirect.net/eric/ReleaseKit`, with Go language The module is `go.maximumdirect.net/releasekit`, with Go language
version `1.26.0`. `go.mod` requires `golang.org/x/mod` and Goldmark for internal version `1.26.0`. `go.mod` requires `golang.org/x/mod` and Goldmark for internal
validation; public facades do not expose either dependency. `go list ./...` validation; public facades do not expose either dependency. `go list ./...`
reports these implemented packages: reports these implemented packages:

View File

@@ -5,14 +5,16 @@ the complete checked-in release note. The page is delivered asynchronously by
the prepared [Woodpecker workflow](../examples/woodpecker/releasekit-source-delivery.yml); the prepared [Woodpecker workflow](../examples/woodpecker/releasekit-source-delivery.yml);
it is not a source-release completion check and carries no binary assets. The it is not a source-release completion check and carries no binary assets. The
authoritative repository is `origin` at authoritative repository is `origin` at
`https://gitea.maximumdirect.net/eric/ReleaseKit.git`, and the release branch `ssh://git@forge.maximumdirect.net:22222/base-layer/releasekit.git`, and the release
is `main`. A release completes when the intended annotated tag is successfully branch is `main`. The hosted Gitea destination is
`https://forge.maximumdirect.net/base-layer/releasekit`. A release completes
when the intended annotated tag is successfully
pushed, or when one exact-object recovery query confirms that the push pushed, or when one exact-object recovery query confirms that the push
succeeded. CI and consumer installation are not completion checks. succeeded. CI and consumer installation are not completion checks.
## Prepare a candidate ## Prepare a candidate
Use a clean clone on `main`. The chosen build toolchain is Go 1.26.8. Make that Use a clean clone on `main`. The chosen build toolchain is Go 1.26.10. Make that
local installation the active `go` command for preparation, building the release local installation the active `go` command for preparation, building the release
executable, and candidate validation: executable, and candidate validation:
@@ -43,9 +45,9 @@ the exact version. Summarize the change and any compatibility or upgrade impact.
End the note with the three license links at the immutable source tag: End the note with the three license links at the immutable source tag:
```markdown ```markdown
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/vX.Y.Z/COPYRIGHT) | [COPYRIGHT](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/vX.Y.Z/COPYRIGHT) |
[LICENSE](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/vX.Y.Z/LICENSE) | [LICENSE](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/vX.Y.Z/LICENSE) |
[ATTRIBUTION](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/vX.Y.Z/docs/oss/attribution.md) [ATTRIBUTION](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/vX.Y.Z/docs/oss/attribution.md)
``` ```
Replace every `vX.Y.Z` with the selected version. These are links to the same Replace every `vX.Y.Z` with the selected version. These are links to the same

29
docs/releases/v0.4.1.md Normal file
View File

@@ -0,0 +1,29 @@
# ReleaseKit v0.4.1
## Summary
This patch synchronizes shared state in the delivery and Gitea HTTP test
fixtures. It makes the test suite reliable under concurrent request handling
and the Go race detector. Production code and public behavior are unchanged.
## Changes
- Protect delivery fixture state with mutexes and independent snapshots; use
atomic observations for independent Gitea fixture counters and flags.
- Keep fatal test assertions in the test goroutine and ensure release test
servers close with their owning tests.
- Preserve existing delivery, conflict, retry, and source-tag assertions while
exercising the synchronized fixtures.
## Compatibility
There are no API, configuration, or runtime behavior changes, and no consumer
migration is required. The Go minimum remains 1.26.0. The published v0.4.0 tag
and its contents are unchanged.
Consumers may upgrade to v0.4.1 with their existing API, profile, and workflow
configuration.
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.1/COPYRIGHT) |
[LICENSE](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.1/LICENSE) |
[ATTRIBUTION](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.1/docs/oss/attribution.md)

36
docs/releases/v0.4.2.md Normal file
View File

@@ -0,0 +1,36 @@
# ReleaseKit v0.4.2
## Summary
This patch corrects Woodpecker delivery bootstrap for events whose
`CI_COMMIT_SHA` identifies an annotated tag object. The maintained workflows
resolve the event object to its source commit before verification and delivery.
## Changes
- Correct module-selected, multi-module, and ReleaseKit source delivery
workflows to resolve event and tag commits independently and pass the
resolved event commit to `ci-deliver`.
- Report both commit identities when an event and its selected tag disagree.
- Extend executable-workflow regression coverage for annotated tag-object
events, annotated and lightweight commit events, resolved delivery arguments,
and rejection of unrelated commits.
## Compatibility and upgrade guidance
Public APIs, CLI contracts, configuration formats, and the Go 1.26.0 consumer
minimum retain their v0.4 definitions. `ci-deliver --commit` continues to accept
the expected source commit ID.
Update copied workflow YAML from the maintained examples when adopting this
patch. Upgrading the ReleaseKit dependency alone leaves copied workflows
unchanged. The [delivery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.2/docs/consumers/delivery.md#woodpecker-tag-workflows)
describes event resolution, workflow adoption, and delivery recovery.
Existing published tags and contents remain unchanged. Recover a failed older
workflow from its original tagged source using the resolved event commit through
the documented delivery command, preserving existing hosted content.
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.2/COPYRIGHT) |
[LICENSE](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.2/LICENSE) |
[ATTRIBUTION](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.2/docs/oss/attribution.md)

59
docs/releases/v0.5.0.md Normal file
View File

@@ -0,0 +1,59 @@
# ReleaseKit v0.5.0
## Summary
ReleaseKit adopts `go.maximumdirect.net/releasekit` as its Go module identifier.
The vanity path resolves to the Gitea repository at
`https://forge.maximumdirect.net/base-layer/releasekit`.
## Changes
- The orchestration facade is `go.maximumdirect.net/releasekit`, the runtime
facade is `go.maximumdirect.net/releasekit/buildinfo`, and the executable
package is `go.maximumdirect.net/releasekit/cmd/releasekit`.
- Imports, version-injection linkage, compiled API checks, consumer fixtures,
and maintained release launchers and Woodpecker workflows use the new module
identity.
- ReleaseKit's hosted-page delivery profile and maintainer guidance use Gitea
at `forge.maximumdirect.net`, owner `base-layer`, repository `releasekit`.
## Compatibility
This pre-1.0 minor release changes package import paths and requires consumer
migration. Facade roles, command behavior, configuration formats, and the Go
1.26.0 consumer minimum retain their existing definitions. The runtime version
accessor and builder use the new module's runtime implementation for injection.
Packages imported through different module paths have distinct Go identities.
Coordinate import changes in packages that exchange ReleaseKit facade types,
and update builders and applications using `buildinfo` together.
Published versions through v0.4.2 retain the original
`gitea.maximumdirect.net/eric/ReleaseKit` module declaration. Those versions
continue to describe their original source and use their matching historical
instructions. A vanity-path download of an older tag does not change its module
declaration.
## Upgrade guidance
1. Update application, library, test, and generated imports for the root facade
and `buildinfo` to the new paths. Select v0.5.0 or a later compatible release
using the [adoption guide](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.5.0/docs/consumers/adoption.md#migrating-from-the-original-module-path).
2. For CLI tool dependencies, remove the old tool declaration and register
`go.maximumdirect.net/releasekit/cmd/releasekit` at the selected version.
3. Update copied launchers and Woodpecker workflows together with the module
selection. The maintained examples use
`releasekit_module=go.maximumdirect.net/releasekit`.
4. Run `go mod tidy`, review any old-path dependencies retained transitively,
and commit module and workflow changes together. Keep existing consumer tags
intact; their delivery retries use their recorded module files and matching
workflows.
Source repository and hosted release URLs use the Gitea host; Go imports and
tool dependencies use the vanity path. ReleaseKit's source publication and
hosted-page delivery retain their separate outcomes under the
[maintainer procedure](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.5.0/docs/release.md).
[COPYRIGHT](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.5.0/COPYRIGHT) |
[LICENSE](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.5.0/LICENSE) |
[ATTRIBUTION](https://forge.maximumdirect.net/base-layer/releasekit/src/tag/v0.5.0/docs/oss/attribution.md)

View File

@@ -15,13 +15,13 @@ minimum.
## Select the release tool ## Select the release tool
The module files are setup skeletons. Set `RELEASEKIT_TOOL_VERSION` to a released The module files are setup skeletons. Set `RELEASEKIT_TOOL_VERSION` to a released
ReleaseKit version containing module-release support, then register that tool in ReleaseKit version at or after the v0.5.0 vanity-path migration, then register
each consumer module before running the launcher or workflow: that tool in each consumer module before running the launcher or workflow:
```sh ```sh
: "${RELEASEKIT_TOOL_VERSION:?Select a ReleaseKit release with module-release support}" : "${RELEASEKIT_TOOL_VERSION:?Select a ReleaseKit release with module-release support}"
for module_dir in . integrations/ckconfig integrations/core/v2; do for module_dir in . integrations/ckconfig integrations/core/v2; do
go -C "$module_dir" get -tool "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@${RELEASEKIT_TOOL_VERSION}" go -C "$module_dir" get -tool "go.maximumdirect.net/releasekit/cmd/releasekit@${RELEASEKIT_TOOL_VERSION}"
go -C "$module_dir" mod tidy go -C "$module_dir" mod tidy
done done
``` ```
@@ -108,9 +108,9 @@ modules:
: "${RELEASEKIT_SOURCE:?Set the absolute ReleaseKit checkout path}" : "${RELEASEKIT_SOURCE:?Set the absolute ReleaseKit checkout path}"
for module_dir in . integrations/ckconfig integrations/core/v2; do for module_dir in . integrations/ckconfig integrations/core/v2; do
go -C "$module_dir" mod edit \ go -C "$module_dir" mod edit \
-require=gitea.maximumdirect.net/eric/ReleaseKit@v0.0.0 \ -require=go.maximumdirect.net/releasekit@v0.0.0 \
-replace="gitea.maximumdirect.net/eric/ReleaseKit=$RELEASEKIT_SOURCE" \ -replace="go.maximumdirect.net/releasekit=$RELEASEKIT_SOURCE" \
-tool=gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit -tool=go.maximumdirect.net/releasekit/cmd/releasekit
GOTOOLCHAIN=local GOWORK=off go -C "$module_dir" mod tidy GOTOOLCHAIN=local GOWORK=off go -C "$module_dir" mod tidy
done done
``` ```

View File

@@ -3,7 +3,7 @@ package main
import ( import (
"fmt" "fmt"
"gitea.maximumdirect.net/eric/ReleaseKit/buildinfo" "go.maximumdirect.net/releasekit/buildinfo"
) )
func main() { func main() {

View File

@@ -31,7 +31,7 @@ else
: > "$bootstrap_dir/releasekit.sum" : > "$bootstrap_dir/releasekit.sum"
fi fi
releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit releasekit_module=go.maximumdirect.net/releasekit
( (
cd "$module_path" cd "$module_path"
export GOWORK=off export GOWORK=off

View File

@@ -8,7 +8,7 @@ when:
steps: steps:
- name: deliver - name: deliver
image: golang:1.26.8-bookworm image: golang:1.26.10-bookworm
environment: environment:
RELEASEKIT_GITEA_TOKEN: RELEASEKIT_GITEA_TOKEN:
from_secret: releasekit_gitea_token from_secret: releasekit_gitea_token
@@ -20,8 +20,12 @@ steps:
git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git check-ref-format "refs/tags/$${CI_COMMIT_TAG}"
git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}"
tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}") tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}")
expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}")
test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" if [ "$${tag_commit}" != "$${expected_commit}" ]; then
printf 'Tag %s resolves to commit %s; event %s resolves to commit %s\n' \
"$${CI_COMMIT_TAG}" "$${tag_commit}" "$${CI_COMMIT_SHA}" "$${expected_commit}" >&2
exit 1
fi
git checkout --detach "$${tag_commit}" git checkout --detach "$${tag_commit}"
test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
test -z "$(git status --porcelain --untracked-files=all)" test -z "$(git status --porcelain --untracked-files=all)"
@@ -47,7 +51,7 @@ steps:
export GOWORK=off export GOWORK=off
export GOTOOLCHAIN=local export GOTOOLCHAIN=local
releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit releasekit_module=go.maximumdirect.net/releasekit
( (
cd "$${module_path}" cd "$${module_path}"
go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -m "$${releasekit_module}" go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -m "$${releasekit_module}"
@@ -59,5 +63,5 @@ steps:
--config "$${profile_path}" \ --config "$${profile_path}" \
--expected-module "$${module_dir}" \ --expected-module "$${module_dir}" \
--prepared \ --prepared \
--commit "$${CI_COMMIT_SHA}" \ --commit "$${expected_commit}" \
"$${CI_COMMIT_TAG}" "$${CI_COMMIT_TAG}"

View File

@@ -4,7 +4,7 @@ when:
steps: steps:
- name: deliver - name: deliver
image: golang:1.26.8-bookworm image: golang:1.26.10-bookworm
environment: environment:
RELEASEKIT_GITEA_TOKEN: RELEASEKIT_GITEA_TOKEN:
from_secret: releasekit_gitea_token from_secret: releasekit_gitea_token
@@ -15,9 +15,13 @@ steps:
test -n "$${CI_COMMIT_SHA}" test -n "$${CI_COMMIT_SHA}"
git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git check-ref-format "refs/tags/$${CI_COMMIT_TAG}"
git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}"
tag_commit=$(git rev-parse "refs/tags/$${CI_COMMIT_TAG}^{commit}") tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}")
expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}")
test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" if [ "$${tag_commit}" != "$${expected_commit}" ]; then
printf 'Tag %s resolves to commit %s; event %s resolves to commit %s\n' \
"$${CI_COMMIT_TAG}" "$${tag_commit}" "$${CI_COMMIT_SHA}" "$${expected_commit}" >&2
exit 1
fi
git checkout --detach "$${tag_commit}" git checkout --detach "$${tag_commit}"
test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
test -z "$(git status --porcelain --untracked-files=all)" test -z "$(git status --porcelain --untracked-files=all)"
@@ -30,5 +34,5 @@ steps:
"$${bootstrap_dir}/releasekit" ci-deliver \ "$${bootstrap_dir}/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \ --repo "$${CI_WORKSPACE}" \
--prepared \ --prepared \
--commit "$${CI_COMMIT_SHA}" \ --commit "$${expected_commit}" \
"$${CI_COMMIT_TAG}" "$${CI_COMMIT_TAG}"

View File

@@ -1,6 +1,6 @@
matrix: matrix:
include: include:
- GO_IMAGE: golang:1.26.8-bookworm - GO_IMAGE: golang:1.26.10-bookworm
when: when:
- event: tag - event: tag
@@ -19,9 +19,13 @@ steps:
test -n "$${CI_COMMIT_SHA}" test -n "$${CI_COMMIT_SHA}"
git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git check-ref-format "refs/tags/$${CI_COMMIT_TAG}"
git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}"
tag_commit=$(git rev-parse "refs/tags/$${CI_COMMIT_TAG}^{commit}") tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}")
expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}")
test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" if [ "$${tag_commit}" != "$${expected_commit}" ]; then
printf 'Tag %s resolves to commit %s; event %s resolves to commit %s\n' \
"$${CI_COMMIT_TAG}" "$${tag_commit}" "$${CI_COMMIT_SHA}" "$${expected_commit}" >&2
exit 1
fi
git checkout --detach "$${tag_commit}" git checkout --detach "$${tag_commit}"
test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
test -z "$(git status --porcelain --untracked-files=all)" test -z "$(git status --porcelain --untracked-files=all)"
@@ -39,12 +43,12 @@ steps:
export GOWORK=off export GOWORK=off
export GOTOOLCHAIN=local export GOTOOLCHAIN=local
releasekit_module=gitea.maximumdirect.net/eric/ReleaseKit releasekit_module=go.maximumdirect.net/releasekit
go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -m "$${releasekit_module}" go list -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" -m "$${releasekit_module}"
go build -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" \ go build -mod=mod -modfile="$${bootstrap_dir}/releasekit.mod" \
-o "$${bootstrap_dir}/releasekit" "$${releasekit_module}/cmd/releasekit" -o "$${bootstrap_dir}/releasekit" "$${releasekit_module}/cmd/releasekit"
"$${bootstrap_dir}/releasekit" ci-deliver \ "$${bootstrap_dir}/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \ --repo "$${CI_WORKSPACE}" \
--prepared \ --prepared \
--commit "$${CI_COMMIT_SHA}" \ --commit "$${expected_commit}" \
"$${CI_COMMIT_TAG}" "$${CI_COMMIT_TAG}"

2
go.mod
View File

@@ -1,4 +1,4 @@
module gitea.maximumdirect.net/eric/ReleaseKit module go.maximumdirect.net/releasekit
go 1.26.0 go 1.26.0

View File

@@ -15,7 +15,7 @@ import (
"strings" "strings"
) )
const modulePath = "gitea.maximumdirect.net/eric/ReleaseKit" const modulePath = "go.maximumdirect.net/releasekit"
var facades = []string{modulePath, modulePath + "/buildinfo"} var facades = []string{modulePath, modulePath + "/buildinfo"}

View File

@@ -13,12 +13,12 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact" "go.maximumdirect.net/releasekit/internal/artifact"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "go.maximumdirect.net/releasekit/internal/candidate"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "go.maximumdirect.net/releasekit/internal/gotool"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/releaseidentity"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir" "go.maximumdirect.net/releasekit/internal/tempdir"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
) )
type Target = artifact.Target type Target = artifact.Target

View File

@@ -7,10 +7,10 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "go.maximumdirect.net/releasekit/internal/candidate"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "go.maximumdirect.net/releasekit/internal/gotool"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/releaseidentity"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir" "go.maximumdirect.net/releasekit/internal/tempdir"
) )
func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candidate.Candidate, *gotool.Owner) error { func CandidateCallback(spec *Spec, output io.Writer) func(context.Context, candidate.Candidate, *gotool.Owner) error {

View File

@@ -10,12 +10,12 @@ import (
"os/exec" "os/exec"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "go.maximumdirect.net/releasekit/internal/gotool"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process" "go.maximumdirect.net/releasekit/internal/process"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/releaseidentity"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" "go.maximumdirect.net/releasekit/internal/validation"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
) )
var ErrInvalidVersion = errors.New("invalid stable version") var ErrInvalidVersion = errors.New("invalid stable version")

View File

@@ -13,8 +13,8 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "go.maximumdirect.net/releasekit/internal/gotool"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" "go.maximumdirect.net/releasekit/internal/validation"
) )
const testVersion = "v1.2.3" const testVersion = "v1.2.3"

View File

@@ -5,9 +5,9 @@ import (
"os" "os"
"path/filepath" "path/filepath"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact" "go.maximumdirect.net/releasekit/internal/artifact"
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" internalbuild "go.maximumdirect.net/releasekit/internal/build"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea" "go.maximumdirect.net/releasekit/internal/gitea"
) )
func declaredBuildAssets(result internalbuild.Result) ([]gitea.ExpectedAsset, error) { func declaredBuildAssets(result internalbuild.Result) ([]gitea.ExpectedAsset, error) {

View File

@@ -8,9 +8,9 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact" "go.maximumdirect.net/releasekit/internal/artifact"
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" internalbuild "go.maximumdirect.net/releasekit/internal/build"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea" "go.maximumdirect.net/releasekit/internal/gitea"
) )
func TestDeclaredBuildAssetsGenerateManifestFromBuildResult(t *testing.T) { func TestDeclaredBuildAssetsGenerateManifestFromBuildResult(t *testing.T) {

View File

@@ -9,14 +9,14 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" internalbuild "go.maximumdirect.net/releasekit/internal/build"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "go.maximumdirect.net/releasekit/internal/candidate"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea" "go.maximumdirect.net/releasekit/internal/gitea"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "go.maximumdirect.net/releasekit/internal/gotool"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/releaseidentity"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/tempdir" "go.maximumdirect.net/releasekit/internal/tempdir"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
) )
type State string type State string

View File

@@ -8,9 +8,9 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact" "go.maximumdirect.net/releasekit/internal/artifact"
internalbuild "gitea.maximumdirect.net/eric/ReleaseKit/internal/build" internalbuild "go.maximumdirect.net/releasekit/internal/build"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea" "go.maximumdirect.net/releasekit/internal/gitea"
) )
const performanceArtifactSize = 16 << 20 const performanceArtifactSize = 16 << 20

View File

@@ -12,7 +12,7 @@ import (
"strings" "strings"
"sync" "sync"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process" "go.maximumdirect.net/releasekit/internal/process"
) )
var errBlobObjectMissing = errors.New("Git object is missing") var errBlobObjectMissing = errors.New("Git object is missing")

View File

@@ -9,7 +9,7 @@ import (
"fmt" "fmt"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process" "go.maximumdirect.net/releasekit/internal/process"
) )
var ( var (

View File

@@ -10,7 +10,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process" "go.maximumdirect.net/releasekit/internal/process"
) )
// TreeEntry records one committed path and its Git object metadata. // TreeEntry records one committed path and its Git object metadata.

View File

@@ -14,7 +14,7 @@ import (
"strconv" "strconv"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/artifact" "go.maximumdirect.net/releasekit/internal/artifact"
) )
const ManifestFilename = artifact.ManifestFilename const ManifestFilename = artifact.ManifestFilename

View File

@@ -47,7 +47,8 @@ func TestInspectAssetsAcceptsMatchingContentAndPreservesUnrelatedFiles(t *testin
case "/files/notes": case "/files/notes":
_, _ = w.Write([]byte("unrelated")) _, _ = w.Write([]byte("unrelated"))
default: default:
t.Fatalf("unexpected path %s", r.URL.Path) t.Errorf("unexpected path %s", r.URL.Path)
return
} }
})) }))
defer server.Close() defer server.Close()
@@ -107,7 +108,8 @@ func TestInspectAssetsAcceptsFullManifestWhenBinariesAreMissing(t *testing.T) {
return return
} }
if r.URL.Path != "/files/manifest" { if r.URL.Path != "/files/manifest" {
t.Fatalf("unexpected path %s", r.URL.Path) t.Errorf("unexpected path %s", r.URL.Path)
return
} }
_, _ = w.Write(manifest.Content) _, _ = w.Write(manifest.Content)
})) }))
@@ -237,7 +239,8 @@ func TestInspectAssetsFollowsSameOriginRedirectWithAuthorization(t *testing.T) {
_, _ = w.Write(content) _, _ = w.Write(content)
return return
} }
t.Fatalf("unexpected path %s", r.URL.Path) t.Errorf("unexpected path %s", r.URL.Path)
return
})) }))
defer server.Close() defer server.Close()
inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{expected}) inspection, err := testClient(server).InspectAssets(context.Background(), testDestination(server.URL), 10, []ExpectedAsset{expected})
@@ -253,9 +256,9 @@ func TestInspectAssetsVerifiesExternalContentWithoutGiteaToken(t *testing.T) {
for _, mode := range []string{"direct", "redirect", "conflicting"} { for _, mode := range []string{"direct", "redirect", "conflicting"} {
t.Run(mode, func(t *testing.T) { t.Run(mode, func(t *testing.T) {
expected := assetFor("app.bin", []byte("matching bytes")) expected := assetFor("app.bin", []byte("matching bytes"))
externalGets := 0 var externalGets atomic.Int64
external := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { external := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
externalGets++ externalGets.Add(1)
if r.Header.Get("Authorization") != "" { if r.Header.Get("Authorization") != "" {
t.Error("Gitea token reached external host") t.Error("Gitea token reached external host")
} }
@@ -287,8 +290,8 @@ func TestInspectAssetsVerifiesExternalContentWithoutGiteaToken(t *testing.T) {
if mode == "conflicting" { if mode == "conflicting" {
want = AssetConflicting want = AssetConflicting
} }
if inspection.Assets[0].State != want || externalGets != 1 { if inspection.Assets[0].State != want || externalGets.Load() != 1 {
t.Fatalf("inspection=%#v downloads=%d", inspection, externalGets) t.Fatalf("inspection=%#v downloads=%d", inspection, externalGets.Load())
} }
}) })
} }

View File

@@ -8,6 +8,7 @@ import (
"net/http" "net/http"
"net/http/httptest" "net/http/httptest"
"strings" "strings"
"sync/atomic"
"testing" "testing"
) )
@@ -40,19 +41,23 @@ func TestEnsureReleaseEscapesSlashContainingTag(t *testing.T) {
http.NotFound(w, r) http.NotFound(w, r)
case "/api/v1/repos/owner/project/releases": case "/api/v1/repos/owner/project/releases":
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
state.posts++ state.posts.Add(1)
var payload createReleaseRequest var payload createReleaseRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err) t.Error(err)
return
} }
if payload.TagName != tag || payload.Name != "Example v1.2.3" || payload.Body != testNote { if payload.TagName != tag || payload.Name != "Example v1.2.3" || payload.Body != testNote {
t.Fatalf("create payload = %#v", payload) t.Errorf("create payload = %#v", payload)
return
} }
if err := json.NewEncoder(w).Encode(releaseResponse{ID: 7, HTMLURL: "https://example.test/release", TagName: tag, TargetCommitish: testCommit, Name: payload.Name, Body: payload.Body}); err != nil { if err := json.NewEncoder(w).Encode(releaseResponse{ID: 7, HTMLURL: "https://example.test/release", TagName: tag, TargetCommitish: testCommit, Name: payload.Name, Body: payload.Body}); err != nil {
t.Fatal(err) t.Error(err)
return
} }
default: default:
t.Fatalf("unexpected escaped request %s %s", r.Method, r.URL.EscapedPath()) t.Errorf("unexpected escaped request %s %s", r.Method, r.URL.EscapedPath())
return
} }
})) }))
defer server.Close() defer server.Close()
@@ -60,8 +65,8 @@ func TestEnsureReleaseEscapesSlashContainingTag(t *testing.T) {
request := testRequest(server.URL) request := testRequest(server.URL)
request.Tag = tag request.Tag = tag
result, err := testClient(server).EnsureRelease(context.Background(), request) result, err := testClient(server).EnsureRelease(context.Background(), request)
if err != nil || !result.Created || state.posts != 1 { if err != nil || !result.Created || state.posts.Load() != 1 {
t.Fatalf("nested tag release = %#v, posts %d, error %v", result, state.posts, err) t.Fatalf("nested tag release = %#v, posts %d, error %v", result, state.posts.Load(), err)
} }
} }
@@ -82,7 +87,7 @@ func TestEnsureReleaseConflictDoesNotEditExistingPage(t *testing.T) {
if failureKind(t, err) != FailureConflict { if failureKind(t, err) != FailureConflict {
t.Fatalf("error = %v", err) t.Fatalf("error = %v", err)
} }
if got := state.posts; got != 0 { if got := state.posts.Load(); got != 0 {
t.Fatalf("release creates = %d, want 0", got) t.Fatalf("release creates = %d, want 0", got)
} }
} }
@@ -103,68 +108,68 @@ func TestEnsureReleaseUnauthorizedIsStructuredAndHidesToken(t *testing.T) {
} }
func TestEnsureReleaseMalformedTagResponseStopsBeforeCreate(t *testing.T) { func TestEnsureReleaseMalformedTagResponseStopsBeforeCreate(t *testing.T) {
posts := 0 var posts atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
posts++ posts.Add(1)
} }
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3") assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
_, _ = w.Write([]byte("{")) _, _ = w.Write([]byte("{"))
})) }))
defer server.Close() defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) _, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureMalformed || posts != 0 { if failureKind(t, err) != FailureMalformed || posts.Load() != 0 {
t.Fatalf("error = %v, posts = %d", err, posts) t.Fatalf("error = %v, posts = %d", err, posts.Load())
} }
} }
func TestEnsureReleaseWrongHostedTagStopsBeforeCreate(t *testing.T) { func TestEnsureReleaseWrongHostedTagStopsBeforeCreate(t *testing.T) {
posts := 0 var posts atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3") assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
posts++ posts.Add(1)
} }
_, _ = fmt.Fprintf(w, `{"commit":{"sha":"%040d"}}`, 7) _, _ = fmt.Fprintf(w, `{"commit":{"sha":"%040d"}}`, 7)
})) }))
defer server.Close() defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) _, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureConflict || posts != 0 { if failureKind(t, err) != FailureConflict || posts.Load() != 0 {
t.Fatalf("error = %v, posts = %d", err, posts) t.Fatalf("error = %v, posts = %d", err, posts.Load())
} }
} }
func TestEnsureReleaseMissingHostedTagStopsBeforeCreate(t *testing.T) { func TestEnsureReleaseMissingHostedTagStopsBeforeCreate(t *testing.T) {
posts := 0 var posts atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3") assertRequest(t, r, http.MethodGet, "/api/v1/repos/owner/project/tags/v1.2.3")
if r.Method == http.MethodPost { if r.Method == http.MethodPost {
posts++ posts.Add(1)
} }
http.NotFound(w, r) http.NotFound(w, r)
})) }))
defer server.Close() defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) _, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureNotFound || posts != 0 { if failureKind(t, err) != FailureNotFound || posts.Load() != 0 {
t.Fatalf("error = %v, posts = %d", err, posts) t.Fatalf("error = %v, posts = %d", err, posts.Load())
} }
} }
func TestEnsureReleaseHonorsContextCancellation(t *testing.T) { func TestEnsureReleaseHonorsContextCancellation(t *testing.T) {
called := false var called atomic.Bool
server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called = true })) server := httptest.NewServer(http.HandlerFunc(func(http.ResponseWriter, *http.Request) { called.Store(true) }))
defer server.Close() defer server.Close()
ctx, cancel := context.WithCancel(context.Background()) ctx, cancel := context.WithCancel(context.Background())
cancel() cancel()
_, err := testClient(server).EnsureRelease(ctx, testRequest(server.URL)) _, err := testClient(server).EnsureRelease(ctx, testRequest(server.URL))
if !errors.Is(err, context.Canceled) || called { if !errors.Is(err, context.Canceled) || called.Load() {
t.Fatalf("error = %v, server called = %t", err, called) t.Fatalf("error = %v, server called = %t", err, called.Load())
} }
} }
func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T) { func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T) {
created := false var created atomic.Bool
lookups := 0 var lookups atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r) assertAuth(t, r)
switch r.URL.Path { switch r.URL.Path {
@@ -173,8 +178,8 @@ func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T
writeTag(t, w, testCommit) writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3": case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path) assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++ lookups.Add(1)
if !created { if !created.Load() {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -183,23 +188,28 @@ func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
var payload createReleaseRequest var payload createReleaseRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err) t.Error(err)
return
} }
if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease { if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease {
t.Fatalf("create payload = %#v", payload) t.Errorf("create payload = %#v", payload)
return
} }
created = true created.Store(true)
hijacker, ok := w.(http.Hijacker) hijacker, ok := w.(http.Hijacker)
if !ok { if !ok {
t.Fatal("HTTP writer does not support hijacking") t.Error("HTTP writer does not support hijacking")
return
} }
connection, _, err := hijacker.Hijack() connection, _, err := hijacker.Hijack()
if err != nil { if err != nil {
t.Fatal(err) t.Error(err)
return
} }
_ = connection.Close() _ = connection.Close()
default: default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
return
} }
})) }))
defer server.Close() defer server.Close()
@@ -207,13 +217,13 @@ func TestEnsureReleaseResolvesLostCreateAcknowledgmentWithOneLookup(t *testing.T
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if result.Created || lookups != 2 { if result.Created || lookups.Load() != 2 {
t.Fatalf("result = %#v, release lookups = %d", result, lookups) t.Fatalf("result = %#v, release lookups = %d", result, lookups.Load())
} }
} }
func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) { func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
lookups := 0 var lookups atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r) assertAuth(t, r)
switch r.URL.Path { switch r.URL.Path {
@@ -222,8 +232,8 @@ func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
writeTag(t, w, testCommit) writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3": case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path) assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++ lookups.Add(1)
if lookups == 1 { if lookups.Load() == 1 {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -232,7 +242,8 @@ func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
w.WriteHeader(http.StatusConflict) w.WriteHeader(http.StatusConflict)
default: default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
return
} }
})) }))
defer server.Close() defer server.Close()
@@ -240,13 +251,13 @@ func TestEnsureReleaseResolvesCreateConflictWithOneLookup(t *testing.T) {
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
if result.Created || result.Release.Title != "Created by another request" || lookups != 2 { if result.Created || result.Release.Title != "Created by another request" || lookups.Load() != 2 {
t.Fatalf("result = %#v, release lookups = %d", result, lookups) t.Fatalf("result = %#v, release lookups = %d", result, lookups.Load())
} }
} }
func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *testing.T) { func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *testing.T) {
lookups := 0 var lookups atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
assertAuth(t, r) assertAuth(t, r)
switch r.URL.Path { switch r.URL.Path {
@@ -255,8 +266,8 @@ func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *test
writeTag(t, w, testCommit) writeTag(t, w, testCommit)
case "/api/v1/repos/owner/project/releases/tags/v1.2.3": case "/api/v1/repos/owner/project/releases/tags/v1.2.3":
assertRequest(t, r, http.MethodGet, r.URL.Path) assertRequest(t, r, http.MethodGet, r.URL.Path)
lookups++ lookups.Add(1)
if lookups == 1 { if lookups.Load() == 1 {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -265,13 +276,14 @@ func TestEnsureReleaseReportsUnknownWhenAmbiguousCreateCannotBeInspected(t *test
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
w.WriteHeader(http.StatusInternalServerError) w.WriteHeader(http.StatusInternalServerError)
default: default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
return
} }
})) }))
defer server.Close() defer server.Close()
_, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) _, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if failureKind(t, err) != FailureUnknown || lookups != 2 { if failureKind(t, err) != FailureUnknown || lookups.Load() != 2 {
t.Fatalf("error = %v, release lookups = %d", err, lookups) t.Fatalf("error = %v, release lookups = %d", err, lookups.Load())
} }
} }
@@ -282,7 +294,7 @@ type releaseFixture struct {
pre bool pre bool
} }
type fixtureState struct{ posts int } type fixtureState struct{ posts atomic.Int64 }
func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool) (*httptest.Server, *fixtureState) { func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool) (*httptest.Server, *fixtureState) {
t.Helper() t.Helper()
@@ -302,13 +314,15 @@ func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool)
writeRelease(t, w, existing) writeRelease(t, w, existing)
case "/api/v1/repos/owner/project/releases": case "/api/v1/repos/owner/project/releases":
assertRequest(t, r, http.MethodPost, r.URL.Path) assertRequest(t, r, http.MethodPost, r.URL.Path)
state.posts++ state.posts.Add(1)
var payload createReleaseRequest var payload createReleaseRequest
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {
t.Fatal(err) t.Error(err)
return
} }
if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease { if payload.TagName != "v1.2.3" || payload.TargetCommitish != testCommit || payload.Name != "Example v1.2.3" || payload.Body != testNote || payload.Draft || payload.Prerelease {
t.Fatalf("create payload = %#v", payload) t.Errorf("create payload = %#v", payload)
return
} }
if !createMissing { if !createMissing {
w.WriteHeader(http.StatusConflict) w.WriteHeader(http.StatusConflict)
@@ -316,10 +330,12 @@ func newReleaseServer(t *testing.T, existing releaseFixture, createMissing bool)
} }
writeRelease(t, w, releaseFixture{title: payload.Name}) writeRelease(t, w, releaseFixture{title: payload.Name})
default: default:
t.Fatalf("unexpected request %s %s", r.Method, r.URL.Path) t.Errorf("unexpected request %s %s", r.Method, r.URL.Path)
return
} }
}) })
server := httptest.NewServer(handler) server := httptest.NewServer(handler)
t.Cleanup(server.Close)
return server, state return server, state
} }
@@ -346,7 +362,8 @@ func (fixture releaseFixture) bodyOrDefault() string {
func assertRequest(t *testing.T, request *http.Request, method, path string) { func assertRequest(t *testing.T, request *http.Request, method, path string) {
t.Helper() t.Helper()
if request.Method != method || request.URL.Path != path { if request.Method != method || request.URL.Path != path {
t.Fatalf("request = %s %s, want %s %s", request.Method, request.URL.Path, method, path) t.Errorf("request = %s %s, want %s %s", request.Method, request.URL.Path, method, path)
return
} }
assertAuth(t, request) assertAuth(t, request)
} }
@@ -354,7 +371,8 @@ func assertRequest(t *testing.T, request *http.Request, method, path string) {
func assertAuth(t *testing.T, request *http.Request) { func assertAuth(t *testing.T, request *http.Request) {
t.Helper() t.Helper()
if got := request.Header.Get("Authorization"); got != "token "+testToken { if got := request.Header.Get("Authorization"); got != "token "+testToken {
t.Fatalf("Authorization = %q", got) t.Errorf("Authorization = %q", got)
return
} }
} }
@@ -382,14 +400,14 @@ func contains(value, substring string) bool {
func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) { func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) {
for _, mode := range []string{"matching", "conflicting", "unavailable"} { for _, mode := range []string{"matching", "conflicting", "unavailable"} {
t.Run(mode, func(t *testing.T) { t.Run(mode, func(t *testing.T) {
posts, lookups := 0, 0 var posts, lookups atomic.Int64
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch { switch {
case strings.Contains(r.URL.Path, "/tags/") && !strings.Contains(r.URL.Path, "/releases/"): case strings.Contains(r.URL.Path, "/tags/") && !strings.Contains(r.URL.Path, "/releases/"):
writeTag(t, w, testCommit) writeTag(t, w, testCommit)
case strings.Contains(r.URL.Path, "/releases/tags/"): case strings.Contains(r.URL.Path, "/releases/tags/"):
lookups++ lookups.Add(1)
if posts == 0 { if posts.Load() == 0 {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
@@ -403,7 +421,7 @@ func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) {
} }
writeRelease(t, w, releaseFixture{title: "Example v1.2.3", body: body}) writeRelease(t, w, releaseFixture{title: "Example v1.2.3", body: body})
case r.Method == http.MethodPost: case r.Method == http.MethodPost:
posts++ posts.Add(1)
w.WriteHeader(http.StatusCreated) w.WriteHeader(http.StatusCreated)
json.NewEncoder(w).Encode(releaseResponse{ID: 10, TagName: "v1.2.3", Name: "inconsistent title", Body: testNote}) json.NewEncoder(w).Encode(releaseResponse{ID: 10, TagName: "v1.2.3", Name: "inconsistent title", Body: testNote})
default: default:
@@ -412,8 +430,8 @@ func TestEnsureReleaseReconcilesInconsistentSuccessfulCreate(t *testing.T) {
})) }))
defer server.Close() defer server.Close()
result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL)) result, err := testClient(server).EnsureRelease(context.Background(), testRequest(server.URL))
if posts != 1 || lookups != 2 { if posts.Load() != 1 || lookups.Load() != 2 {
t.Fatalf("posts=%d lookups=%d", posts, lookups) t.Fatalf("posts=%d lookups=%d", posts.Load(), lookups.Load())
} }
switch mode { switch mode {
case "matching": case "matching":

View File

@@ -12,7 +12,7 @@ import (
"strings" "strings"
"sync" "sync"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/process" "go.maximumdirect.net/releasekit/internal/process"
) )
// Mode selects the target environment for a Go command. // Mode selects the target environment for a Go command.

View File

@@ -8,10 +8,10 @@ import (
"fmt" "fmt"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "go.maximumdirect.net/releasekit/internal/candidate"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "go.maximumdirect.net/releasekit/internal/gotool"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
) )
type TaggedCheckoutRequest struct { type TaggedCheckoutRequest struct {

View File

@@ -8,10 +8,10 @@ import (
"io" "io"
"time" "time"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/candidate" "go.maximumdirect.net/releasekit/internal/candidate"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "go.maximumdirect.net/releasekit/internal/gotool"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" "go.maximumdirect.net/releasekit/internal/validation"
) )
type State string type State string

View File

@@ -10,8 +10,8 @@ import (
"path/filepath" "path/filepath"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/versioncontract" "go.maximumdirect.net/releasekit/internal/versioncontract"
"golang.org/x/mod/modfile" "golang.org/x/mod/modfile"
"golang.org/x/mod/module" "golang.org/x/mod/module"
) )

View File

@@ -6,7 +6,7 @@ import (
"path" "path"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
) )
const defaultFileSizeLimit int64 = 1_000_000 const defaultFileSizeLimit int64 = 1_000_000

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
) )
func TestCheckFileSizeUsesStrictLimitsAndDefault(t *testing.T) { func TestCheckFileSizeUsesStrictLimitsAndDefault(t *testing.T) {

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
) )
func TestCheckGoFormatUsesCapturedRegularGoBlobs(t *testing.T) { func TestCheckGoFormatUsesCapturedRegularGoBlobs(t *testing.T) {

View File

@@ -6,7 +6,7 @@ import (
"path" "path"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
"golang.org/x/mod/modfile" "golang.org/x/mod/modfile"
) )

View File

@@ -8,7 +8,7 @@ import (
"path" "path"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
) )
func checkMarkdownLinks(ctx context.Context, tree *CandidateTree, enforceCheckoutBoundary bool, output io.Writer) error { func checkMarkdownLinks(ctx context.Context, tree *CandidateTree, enforceCheckoutBoundary bool, output io.Writer) error {

View File

@@ -4,7 +4,7 @@ import (
"fmt" "fmt"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
) )
func BenchmarkSelectInputEntriesByModuleCount(b *testing.B) { func BenchmarkSelectInputEntriesByModuleCount(b *testing.B) {

View File

@@ -8,7 +8,7 @@ import (
"sort" "sort"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
) )
// CandidateTree is an inventory of one captured commit with blob contents // CandidateTree is an inventory of one captured commit with blob contents

View File

@@ -12,7 +12,7 @@ import (
"path" "path"
"strings" "strings"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gotool" "go.maximumdirect.net/releasekit/internal/gotool"
) )
// Settings selects shared validation behavior. // Settings selects shared validation behavior.

View File

@@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
) )
func TestCheckWorkspaceFilesReportsCommittedBlobNames(t *testing.T) { func TestCheckWorkspaceFilesReportsCommittedBlobNames(t *testing.T) {

View File

@@ -4,4 +4,4 @@ package versioncontract
// InjectedVersionLinkerSymbol is the -X linker target used to set the runtime // InjectedVersionLinkerSymbol is the -X linker target used to set the runtime
// version. Keep it in sync with runtimeinfo.injectedVersion. // version. Keep it in sync with runtimeinfo.injectedVersion.
const InjectedVersionLinkerSymbol = "gitea.maximumdirect.net/eric/ReleaseKit/internal/runtimeinfo.injectedVersion" const InjectedVersionLinkerSymbol = "go.maximumdirect.net/releasekit/internal/runtimeinfo.injectedVersion"

View File

@@ -6,7 +6,7 @@ import (
"path/filepath" "path/filepath"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit" "go.maximumdirect.net/releasekit"
) )
func BenchmarkPerformanceFacadeCheck(b *testing.B) { func BenchmarkPerformanceFacadeCheck(b *testing.B) {

View File

@@ -18,8 +18,8 @@ import (
"sync" "sync"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/gitea" "go.maximumdirect.net/releasekit/internal/gitea"
) )
const performanceDeliveryArtifactSize = 16 << 20 const performanceDeliveryArtifactSize = 16 << 20
@@ -47,8 +47,9 @@ type deliveryBenchmarkIdentity struct {
func BenchmarkPerformanceFacadeDeliver(b *testing.B) { func BenchmarkPerformanceFacadeDeliver(b *testing.B) {
repository, build, expected := newDeliveryBenchmarkFixture(b) repository, build, expected := newDeliveryBenchmarkFixture(b)
state := &deliveryBenchmarkServer{commit: gitText(b, repository, "rev-parse", "HEAD"), assets: expected} state := &deliveryBenchmarkServer{commit: gitText(b, repository, "rev-parse", "HEAD"), assets: expected}
server := httptest.NewServer(http.HandlerFunc(state.serveHTTP)) server := httptest.NewUnstartedServer(http.HandlerFunc(state.serveHTTP))
state.baseURL = server.URL state.baseURL = "http://" + server.Listener.Addr().String()
server.Start()
b.Cleanup(server.Close) b.Cleanup(server.Close)
request := releasekit.DeliveryRequest{ request := releasekit.DeliveryRequest{
RepositoryDir: repository, RepositoryDir: repository,

View File

@@ -6,8 +6,8 @@ import (
"fmt" "fmt"
"strings" "strings"
internalpreparation "gitea.maximumdirect.net/eric/ReleaseKit/internal/preparation" internalpreparation "go.maximumdirect.net/releasekit/internal/preparation"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/releaseidentity" "go.maximumdirect.net/releasekit/internal/releaseidentity"
) )
// TaggedCheckoutRequest selects a full Git tag and the Git object ID expected // TaggedCheckoutRequest selects a full Git tag and the Git object ID expected

View File

@@ -9,7 +9,7 @@ import (
"strings" "strings"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
) )
func TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches(t *testing.T) { func TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches(t *testing.T) {

View File

@@ -3,7 +3,7 @@ package releasekit
import ( import (
"context" "context"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/publish" "go.maximumdirect.net/releasekit/internal/publish"
) )
type PublicationState string type PublicationState string

View File

@@ -12,8 +12,8 @@ import (
"testing" "testing"
"time" "time"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/git" "go.maximumdirect.net/releasekit/internal/git"
) )
const releaseNote = "# Sample App v1.2.3\n\nA source release.\n\nUpgrade notes stay intact.\n" const releaseNote = "# Sample App v1.2.3\n\nA source release.\n\nUpgrade notes stay intact.\n"

View File

@@ -2,9 +2,9 @@
"product": "ReleaseKit", "product": "ReleaseKit",
"remote": "origin", "remote": "origin",
"delivery": { "delivery": {
"base_url": "https://gitea.maximumdirect.net", "base_url": "https://forge.maximumdirect.net",
"owner": "eric", "owner": "base-layer",
"repository": "ReleaseKit", "repository": "releasekit",
"token_env": "RELEASEKIT_GITEA_TOKEN" "token_env": "RELEASEKIT_GITEA_TOKEN"
} }
} }

View File

@@ -7,7 +7,7 @@ import (
"strings" "strings"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
) )
func TestModuleSelectionFollowsSymlinkBeforeParentAcrossOperations(t *testing.T) { func TestModuleSelectionFollowsSymlinkBeforeParentAcrossOperations(t *testing.T) {
@@ -40,7 +40,7 @@ func TestModuleSelectionFollowsSymlinkBeforeParentAcrossOperations(t *testing.T)
buildGit(t, repository, "tag", wantTag) buildGit(t, repository, "tag", wantTag)
server, remote := newFacadeDeliveryServer(t, repository) server, remote := newFacadeDeliveryServer(t, repository)
defer server.Close() defer server.Close()
remote.tag, remote.noteBody = wantTag, note remote.update(func(remote *facadeDeliveryData) { remote.tag, remote.noteBody = wantTag, note })
delivered, err := releasekit.Deliver(t.Context(), releasekit.DeliveryRequest{ delivered, err := releasekit.Deliver(t.Context(), releasekit.DeliveryRequest{
RepositoryDir: repository, ModuleDir: "link/..", Product: "Fixture", Version: version, RepositoryDir: repository, ModuleDir: "link/..", Product: "Fixture", Version: version,
ExpectedTag: wantTag, Destination: facadeDeliveryDestination(server.URL), ExpectedTag: wantTag, Destination: facadeDeliveryDestination(server.URL),

View File

@@ -8,7 +8,7 @@ import (
"strings" "strings"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
) )
func TestSourceOperationsRejectUnavailableNoteWithoutTransport(t *testing.T) { func TestSourceOperationsRejectUnavailableNoteWithoutTransport(t *testing.T) {
@@ -40,8 +40,8 @@ func TestSourceOperationsRejectUnavailableNoteWithoutTransport(t *testing.T) {
if checked.Commit != commit || checked.Tag != "v1.2.3" || delivered.CandidateCommit != commit || delivered.Tag != checked.Tag || delivered.State != releasekit.DeliveryNotAttempted { if checked.Commit != commit || checked.Tag != "v1.2.3" || delivered.CandidateCommit != commit || delivered.Tag != checked.Tag || delivered.State != releasekit.DeliveryNotAttempted {
t.Fatalf("missing-note evidence: check=%#v, delivery=%#v", checked, delivered) t.Fatalf("missing-note evidence: check=%#v, delivery=%#v", checked, delivered)
} }
if remote.requests != 0 { if remote.snapshot().requests != 0 {
t.Fatalf("missing note caused %d hosted requests", remote.requests) t.Fatalf("missing note caused %d hosted requests", remote.snapshot().requests)
} }
assertInspectionStayedLocal(t, repository, attempts, before) assertInspectionStayedLocal(t, repository, attempts, before)
} }

View File

@@ -4,7 +4,7 @@ import (
"errors" "errors"
"slices" "slices"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" "go.maximumdirect.net/releasekit/internal/validation"
) )
// ValidationCheck identifies a shared validation check that failed. // ValidationCheck identifies a shared validation check that failed.

View File

@@ -6,7 +6,7 @@ import (
"strings" "strings"
"testing" "testing"
releasekit "gitea.maximumdirect.net/eric/ReleaseKit" releasekit "go.maximumdirect.net/releasekit"
) )
func TestValidationErrorPresentsFindingsAlongsideCause(t *testing.T) { func TestValidationErrorPresentsFindingsAlongsideCause(t *testing.T) {

View File

@@ -5,7 +5,7 @@ import (
"strings" "strings"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" "go.maximumdirect.net/releasekit/internal/validation"
) )
func TestSharedFileFailureKeepsPublicIdentityPathAndCause(t *testing.T) { func TestSharedFileFailureKeepsPublicIdentityPathAndCause(t *testing.T) {

View File

@@ -4,7 +4,7 @@ import (
"errors" "errors"
"testing" "testing"
"gitea.maximumdirect.net/eric/ReleaseKit/internal/validation" "go.maximumdirect.net/releasekit/internal/validation"
) )
func TestCandidateRequestMapsGoValidation(t *testing.T) { func TestCandidateRequestMapsGoValidation(t *testing.T) {