From 21294404d7dd63c546fbd6881f6c1ec37e9c7a79 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Tue, 6 Oct 2026 19:49:19 +0000 Subject: [PATCH] Consolidate Woodpecker delivery templates --- .../releasekit_delivery_workflow_test.go | 36 +-- cmd/releasekit/woodpecker_test.go | 227 ++++++++++++++---- docs/consumers/delivery.md | 54 +++-- docs/internal/overview.md | 2 +- docs/release.md | 4 +- docs/roadmap/audit.md | 27 ++- docs/roadmap/implementation.md | 16 +- docs/roadmap/releasekit.md | 6 +- .../woodpecker/releasekit-source-delivery.yml | 13 +- examples/woodpecker/tag-delivery-binary.yml | 37 --- .../woodpecker/tag-delivery-source-only.yml | 36 --- examples/woodpecker/tag-delivery.yml | 21 ++ 12 files changed, 295 insertions(+), 184 deletions(-) delete mode 100644 examples/woodpecker/tag-delivery-binary.yml delete mode 100644 examples/woodpecker/tag-delivery-source-only.yml create mode 100644 examples/woodpecker/tag-delivery.yml diff --git a/cmd/releasekit/releasekit_delivery_workflow_test.go b/cmd/releasekit/releasekit_delivery_workflow_test.go index 2f4c04c..54583e9 100644 --- a/cmd/releasekit/releasekit_delivery_workflow_test.go +++ b/cmd/releasekit/releasekit_delivery_workflow_test.go @@ -78,22 +78,30 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) { cliGitRun(t, repository, "remote", "set-url", "origin", remote) cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main", "refs/tags/"+version) - // Match the maintained workflow: fetch and bind the tag, then build the - // executable from that detached tagged checkout before page-only delivery. - cliGitRun(t, repository, "fetch", "origin", "refs/tags/"+version+":refs/tags/"+version) - if got := cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version+"^{commit}"); got != state.commit { - t.Fatalf("fetched tag commit = %s, want %s", got, state.commit) - } - cliGitRun(t, repository, "checkout", "--detach", state.commit) tool := filepath.Join(repository, ".releasekit", "bin", "releasekit") - if err := os.MkdirAll(filepath.Dir(tool), 0o700); err != nil { + template, err := os.ReadFile(filepath.Join(root, "examples", "woodpecker", "releasekit-source-delivery.yml")) + if err != nil { t.Fatal(err) } - build := exec.Command("go", "build", "-o", tool, "./cmd/releasekit") - build.Dir = repository - build.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOPROXY=off", "GOSUMDB=off") - if output, err := build.CombinedOutput(); err != nil { - t.Fatalf("build CLI from tagged checkout: %v\n%s", err, output) + buildBlock, err := extractWoodpeckerCommand(string(template), "build-tool") + if err != nil { + t.Fatal(err) + } + deliveryBlock, err := extractWoodpeckerCommand(string(template), "deliver-page") + if err != nil { + t.Fatal(err) + } + env := map[string]string{ + "CI_WORKSPACE": repository, + "CI_COMMIT_SHA": state.commit, + "CI_COMMIT_TAG": version, + "RELEASEKIT_GITEA_TOKEN": "fixture-token", + "GOPROXY": "off", + "GOSUMDB": "off", + } + runWoodpeckerBlock(t, strings.ReplaceAll(buildBlock, "$${", "${"), repository, env) + if got := cliGitOutput(t, repository, "rev-parse", "HEAD"); got != state.commit { + t.Fatalf("template checkout = %s, want %s", got, state.commit) } state.remoteCommit = strings.Repeat("f", 40) @@ -109,7 +117,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) { state.remoteCommit = "" for attempt := 0; attempt < 2; attempt++ { - output := runWoodpeckerCommand(t, tool, repository, true, "deliver", "--repo", repository, version) + output := runWoodpeckerBlock(t, strings.ReplaceAll(deliveryBlock, "$${", "${"), repository, env) if !strings.Contains(output, "state complete") { t.Fatalf("delivery attempt %d output = %q", attempt+1, output) } diff --git a/cmd/releasekit/woodpecker_test.go b/cmd/releasekit/woodpecker_test.go index e21105b..d2f3269 100644 --- a/cmd/releasekit/woodpecker_test.go +++ b/cmd/releasekit/woodpecker_test.go @@ -1,8 +1,7 @@ package main import ( - "bytes" - "context" + "fmt" "os" "os/exec" "path/filepath" @@ -13,12 +12,24 @@ import ( "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" ) -func TestWoodpeckerDeliveryCommandSequences(t *testing.T) { - _, sourceFile, _, ok := runtime.Caller(0) - if !ok { - t.Fatal("locate integration test source") +func TestWoodpeckerTagDeliveryTemplate(t *testing.T) { + root := cliRepositoryRoot(t) + templatePath := filepath.Join(root, "examples", "woodpecker", "tag-delivery.yml") + contents, err := os.ReadFile(templatePath) + if err != nil { + t.Fatal(err) } - root := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "../..")) + pin, err := woodpeckerMatrixValue(string(contents), "RELEASEKIT_VERSION") + if err != nil { + t.Fatal(err) + } + block, err := extractWoodpeckerCommand(string(contents), "deliver") + if err != nil { + t.Fatal(err) + } + command := strings.ReplaceAll(block, "${RELEASEKIT_VERSION}", pin) + command = strings.ReplaceAll(command, "$${", "${") + executable := filepath.Join(t.TempDir(), "releasekit") build := exec.Command("go", "build", "-o", executable, "./cmd/releasekit") build.Dir = root @@ -26,8 +37,12 @@ func TestWoodpeckerDeliveryCommandSequences(t *testing.T) { t.Fatalf("build local ReleaseKit executable: %v\n%s", err, output) } + module := "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@" + pin t.Run("source-only", func(t *testing.T) { repository := newCLIRepository(t) + writeRepositoryFile(t, repository, "go.mod", "module example.com/cli-candidate\n\ngo 1.25.0\n\nrequire example.com/unavailable/module v1.2.3\n") + cliGitRun(t, repository, "add", "go.mod") + cliGitRun(t, repository, "commit", "-m", "declare unavailable dependency") state := &deliveryServerState{} server := newDeliveryServer(t, state) defer server.Close() @@ -35,13 +50,22 @@ func TestWoodpeckerDeliveryCommandSequences(t *testing.T) { Product: "Example Product", Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) - state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") - cliGitRun(t, repository, "tag", "v1.2.3") - cliGitRun(t, repository, "checkout", "--detach", "v1.2.3") + commit := pushTaggedCLIRepository(t, repository) + state.commit = commit - output := runWoodpeckerCommand(t, executable, repository, true, "deliver", "--repo", repository, "v1.2.3") - if !strings.Contains(output, "state complete") || state.releaseCreates != 1 || state.assetUploads != 0 { - t.Fatalf("source-only workflow output=%q creates=%d uploads=%d", output, state.releaseCreates, state.assetUploads) + shimDir, goLog := installGoBootstrapShim(t) + env := templateEnvironment(repository, commit, executable, module, shimDir, goLog) + for attempt := 0; attempt < 2; attempt++ { + output := runWoodpeckerBlock(t, command, repository, env) + if !strings.Contains(output, "state complete") { + t.Fatalf("delivery attempt %d output=%q", attempt+1, output) + } + } + if state.releaseCreates != 1 || state.assetUploads != 0 { + t.Fatalf("source-only calls = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) + } + if log, err := os.ReadFile(goLog); err != nil || strings.Contains(string(log), "mod download") { + t.Fatalf("source-only command downloaded application dependencies: log=%q error=%v", log, err) } }) @@ -51,56 +75,163 @@ func TestWoodpeckerDeliveryCommandSequences(t *testing.T) { server := newDeliveryServer(t, state) defer server.Close() writeDeliveryProfile(t, repository, cli.Settings{ - Product: "Example Product", - Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, + Product: "Example Product", + Build: &cli.BuildSettings{ + MainPackage: ".", + BinaryName: "helper", + Targets: []string{fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH)}, + }, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, }) - state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") - cliGitRun(t, repository, "tag", "v1.2.3") - cliGitRun(t, repository, "checkout", "--detach", "v1.2.3") - exclude, err := os.OpenFile(filepath.Join(repository, ".git", "info", "exclude"), os.O_APPEND|os.O_WRONLY, 0) - if err != nil { - t.Fatal(err) - } - if _, err := exclude.WriteString("\n/.releasekit/\n"); err != nil { - t.Fatal(err) - } - if err := exclude.Close(); err != nil { - t.Fatal(err) - } - assets := filepath.Join(repository, ".releasekit", "assets") - if err := os.MkdirAll(filepath.Dir(assets), 0o700); err != nil { - t.Fatal(err) - } + commit := pushTaggedCLIRepository(t, repository) + state.commit = commit - // This follows the binary template's build then delivery commands, with - // the executable built above replacing its pinned go install step. - runWoodpeckerCommand(t, executable, repository, false, "build", "--repo", repository, "--output", assets, "v1.2.3") - output := runWoodpeckerCommand(t, executable, repository, true, "deliver", "--repo", repository, "--assets", assets, "v1.2.3") - if !strings.Contains(output, "state complete") || state.releaseCreates != 1 || state.assetUploads != 2 { - t.Fatalf("binary workflow output=%q creates=%d uploads=%d", output, state.releaseCreates, state.assetUploads) + shimDir, goLog := installGoBootstrapShim(t) + env := templateEnvironment(repository, commit, executable, module, shimDir, goLog) + for attempt := 0; attempt < 2; attempt++ { + output := runWoodpeckerBlock(t, command, repository, env) + if !strings.Contains(output, "state complete") { + t.Fatalf("delivery attempt %d output=%q", attempt+1, output) + } + if attempt == 1 && !strings.Contains(output, "matching SHA256SUMS") { + t.Fatalf("second delivery did not reuse matching assets: %q", output) + } } - if _, ok := state.assets["helper-v1.2.3-linux-amd64"]; !ok { + if state.releaseCreates != 1 || state.assetUploads != 2 { + t.Fatalf("binary calls = creates:%d uploads:%d", state.releaseCreates, state.assetUploads) + } + if _, ok := state.assets["helper-v1.2.3-"+runtime.GOOS+"-"+runtime.GOARCH]; !ok { t.Fatalf("binary asset was not delivered: %#v", state.assets) } if _, ok := state.assets["SHA256SUMS"]; !ok { t.Fatalf("checksum manifest was not delivered: %#v", state.assets) } + if log, err := os.ReadFile(goLog); err != nil || !strings.Contains(string(log), "mod download") { + t.Fatalf("binary command did not prepare application dependencies: log=%q error=%v", log, err) + } }) } -func runWoodpeckerCommand(t *testing.T, executable, repository string, credential bool, args ...string) string { +func cliRepositoryRoot(t *testing.T) string { t.Helper() - command := exec.CommandContext(context.Background(), executable, args...) + _, sourceFile, _, ok := runtime.Caller(0) + if !ok { + t.Fatal("locate integration test source") + } + return filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "../..")) +} + +func extractWoodpeckerCommand(contents, stepName string) (string, error) { + lines := strings.Split(contents, "\n") + stepHeader := " - name: " + stepName + for i, line := range lines { + if strings.TrimSpace(line) != strings.TrimSpace(stepHeader) { + continue + } + for j := i + 1; j < len(lines); j++ { + trimmed := strings.TrimSpace(lines[j]) + if strings.HasPrefix(lines[j], " - name:") { + break + } + if trimmed != "- |" { + continue + } + var block []string + for k := j + 1; k < len(lines); k++ { + current := lines[k] + if strings.TrimSpace(current) == "" { + block = append(block, "") + continue + } + indent := len(current) - len(strings.TrimLeft(current, " ")) + if indent < 8 { + break + } + block = append(block, current[8:]) + } + return strings.TrimSpace(strings.Join(block, "\n")), nil + } + return "", fmt.Errorf("step %q has no literal command block", stepName) + } + return "", fmt.Errorf("step %q was not found", stepName) +} + +func woodpeckerMatrixValue(contents, key string) (string, error) { + for _, line := range strings.Split(contents, "\n") { + field, value, ok := strings.Cut(strings.TrimSpace(line), ":") + if ok && field == key { + return strings.TrimSpace(value), nil + } + } + return "", fmt.Errorf("matrix value %q was not found", key) +} + +func installGoBootstrapShim(t *testing.T) (string, string) { + t.Helper() + shimDir := t.TempDir() + goLog := filepath.Join(shimDir, "go-commands") + shim := `#!/bin/sh +if [ "$1" = "run" ]; then + shift + [ "$1" = "$RELEASEKIT_EXPECTED_MODULE" ] || { echo "unexpected module: $1" >&2; exit 90; } + shift + [ "$1" = "ci-deliver" ] || { echo "unexpected command: $1" >&2; exit 91; } + [ "$2" = "--repo" ] && [ "$3" = "$CI_WORKSPACE" ] || { echo "unexpected repository arguments" >&2; exit 92; } + [ "$4" = "--commit" ] && [ "$5" = "$CI_COMMIT_SHA" ] || { echo "unexpected commit arguments" >&2; exit 93; } + [ "$6" = "$CI_COMMIT_TAG" ] && [ "$#" = 6 ] || { echo "unexpected version argument" >&2; exit 94; } + printf '%s\n' "run $RELEASEKIT_EXPECTED_MODULE" >> "$RELEASEKIT_GO_LOG" + exec "$RELEASEKIT_LOCAL_EXECUTABLE" "$@" +fi +printf '%s\n' "$*" >> "$RELEASEKIT_GO_LOG" +exec "$RELEASEKIT_REAL_GO" "$@" +` + if err := os.WriteFile(filepath.Join(shimDir, "go"), []byte(shim), 0o700); err != nil { + t.Fatal(err) + } + return shimDir, goLog +} + +func templateEnvironment(repository, commit, executable, module, shimDir, goLog string) map[string]string { + realGo, _ := exec.LookPath("go") + return map[string]string{ + "CI_WORKSPACE": repository, + "CI_COMMIT_SHA": commit, + "CI_COMMIT_TAG": "v1.2.3", + "RELEASEKIT_GITEA_TOKEN": "fixture-token", + "RELEASEKIT_EXPECTED_MODULE": module, + "RELEASEKIT_LOCAL_EXECUTABLE": executable, + "RELEASEKIT_REAL_GO": realGo, + "RELEASEKIT_GO_LOG": goLog, + "GOTOOLCHAIN": "local", + "GOPROXY": "off", + "GOSUMDB": "off", + "GOFLAGS": "", + "PATH": shimDir + string(os.PathListSeparator) + os.Getenv("PATH"), + } +} + +func runWoodpeckerBlock(t *testing.T, block, repository string, environment map[string]string) string { + t.Helper() + command := exec.Command("sh", "-e", "-c", block) command.Dir = repository command.Env = os.Environ() - if credential { - command.Env = append(command.Env, "RELEASEKIT_GITEA_TOKEN=fixture-token") + for key, value := range environment { + command.Env = setEnvironment(command.Env, key, value) } - var stdout, stderr bytes.Buffer - command.Stdout, command.Stderr = &stdout, &stderr - if err := command.Run(); err != nil { - t.Fatalf("ReleaseKit %v failed: %v\nstdout: %s\nstderr: %s", args, err, stdout.String(), stderr.String()) + output, err := command.CombinedOutput() + if err != nil { + t.Fatalf("Woodpecker command failed: %v\ncommand: %s\noutput: %s", err, block, output) } - return stdout.String() + return string(output) +} + +func setEnvironment(environment []string, key, value string) []string { + prefix := key + "=" + for i, entry := range environment { + if strings.HasPrefix(entry, prefix) { + environment[i] = prefix + value + return environment + } + } + return append(environment, prefix+value) } diff --git a/docs/consumers/delivery.md b/docs/consumers/delivery.md index bc1bf4f..8ef7d38 100644 --- a/docs/consumers/delivery.md +++ b/docs/consumers/delivery.md @@ -148,31 +148,41 @@ exit codes, and transport behavior. ## Woodpecker tag workflows -Maintained workflow examples are available for source-only and binary delivery: +Use the maintained [tag-delivery workflow](../../examples/woodpecker/tag-delivery.yml) +in the consumer repository's `.woodpecker/` directory. Set `GO_IMAGE` to an +image with Go 1.25 or newer, Git, and a Go version compatible with the consumer +module. Replace `RELEASEKIT_VERSION` with an immutable ReleaseKit revision or +published version. The example deliberately leaves this deployment pin as a +placeholder. Configure the Woodpecker repository secret +`releasekit_gitea_token`; the template maps it to `RELEASEKIT_GITEA_TOKEN` only +for the delivery step. The repository needs a `delivery` profile. Page-only and +binary behavior both use this template; the profile's build target list selects +whether binaries are built and delivered. -- [Source-only tag workflow](../../examples/woodpecker/tag-delivery-source-only.yml) -- [Binary tag workflow](../../examples/woodpecker/tag-delivery-binary.yml) +The command runs versioned source directly with `go run`, passing +`CI_WORKSPACE`, `CI_COMMIT_SHA`, and `CI_COMMIT_TAG` as command arguments. +`ci-deliver` fetches and verifies the event tag against its commit before +checking out the commit detached. It then loads the profile from that source, +validates the destination and token, and downloads application dependencies +only for profiles with build targets. The facade builds and delivers those +targets with the note page. Source-only profiles skip dependency downloads and +binary work. The workflow does not repeat checkout guards or maintain a +separate build/receipt/delivery sequence. -Copy one into the consumer repository's `.woodpecker/` directory. In its -matrix entry, set `GO_IMAGE` to a Go image compatible with the consumer module -and replace `RELEASEKIT_SOURCE` with an immutable ReleaseKit commit. The -examples deliberately contain no claimed published ReleaseKit pin. Configure -the Woodpecker repository secret `releasekit_gitea_token`; the workflow maps it -to `RELEASEKIT_GITEA_TOKEN` only for the delivery step. The repository also -needs a `delivery` profile using the default `RELEASEKIT_GITEA_TOKEN` variable. -The binary profile must declare its build targets. +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 +the template. A locally retained executable can be installed with +`go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@PIN`, then run +with the same arguments: -The tag workflow fetches the event tag, verifies that the tag resolves to -`CI_COMMIT_SHA`, and checks out that commit detached. Woodpecker shares the -repository workspace between sequential steps, so the installed executable -and Go caches remain available. The workflow keeps its `.releasekit/` directory -out of Git status using the checkout's local exclude file. +```sh +releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$CI_COMMIT_SHA" "$CI_COMMIT_TAG" +``` -Preparation can access the Go module proxy to install the pinned ReleaseKit -executable. The binary workflow also runs `go mod download` before delivery. -The single `releasekit deliver` invocation builds selected targets through the -facade and uploads them with the release page. The source-only workflow sends -the committed note page directly. Neither template runs `check` or `publish`. +Upgrade by changing the selected immutable +`RELEASEKIT_VERSION` (or the `@PIN` used by `go install`) and validating the +workflow against a tag fixture. The command and profile behavior are documented +in the [CLI contract](../cli.md) and [configuration reference](../config.md). Woodpecker runs delivery after the tag event, independently from the source publication result. Read its workflow status as delivery status: a delivery @@ -180,6 +190,6 @@ failure does not undo source publication. A failed, partial, or uncertain run can be inspected and retried against the same immutable tag and commit; do not move the tag to make a retry pass. See [Woodpecker's environment reference](https://woodpecker-ci.org/docs/usage/environment) -for the runner-provided tag and commit variables and +for runner-provided tag and commit variables and [workflow syntax](https://woodpecker-ci.org/docs/usage/workflow-syntax) for tag events, sequential steps, and the shared workspace. diff --git a/docs/internal/overview.md b/docs/internal/overview.md index 3c532ea..4acc80c 100644 --- a/docs/internal/overview.md +++ b/docs/internal/overview.md @@ -74,7 +74,7 @@ not a separate release matrix: | Page-only and managed or prebuilt binary delivery | `releasekit.Deliver`, `internal/delivery`, `internal/gitea`, CLI | `TestDeliverPageOnlySkipsBuildAndHostedAssets`, `TestDeliverManagedBuildUploadsArtifactsAndCleansTemporaryOutput`, `TestDeliverPrebuiltArtifactsRemainCallerOwned`, `TestDeliverManagedBuildFailureCleansTemporaryOutputBeforeHostedWrites`, `TestDeliverManagedBuildCancellationCleansTemporaryOutput`, `TestDeliverBuildsTaggedArtifactsAndReusesAssets`, `TestDeliverRejectsTamperedAssetsAndStaleReceiptBeforeRemoteWrites` | | Tagged checkout and build dependency preparation | `releasekit.PrepareTaggedCheckout`, `releasekit.PrepareBuildDependencies`, `internal/preparation`, `internal/git`, `internal/gotool` | `TestPrepareTaggedCheckoutFetchesExpectedCommitAndDetaches`, `TestPrepareTaggedCheckoutRejectsWrongCommitBeforeCheckout`, `TestPrepareTaggedCheckoutReportsLocalCheckoutConflict`, `TestPrepareTaggedCheckoutReportsConflictingLocalTag`, `TestPrepareBuildDependenciesPreservesPreparationEnvironment`, `TestPrepareBuildDependenciesPropagatesCommandFailure`, `TestPrepareBuildDependenciesPropagatesCancellation` | | Page, manifest, asset comparison, and additive retry outcomes | `internal/gitea` | `TestEnsureReleaseCreatesMissingPageFromCommittedNote`, `TestInspectAssetsDetectsSameNameAndSizeWithDifferentBytes`, `TestUploadMissingPartialFailureThenRerunOnlyMissingAssets` | -| CLI profiles and prepared Woodpecker sequences | `cmd/releasekit`, `cmd/releasekit/internal/cli`, `examples/woodpecker/` | `TestLoadSettingsRejectsInvalidProfiles`, `TestWoodpeckerDeliveryCommandSequences`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout` | +| CLI profiles and prepared Woodpecker sequences | `cmd/releasekit`, `cmd/releasekit/internal/cli`, `examples/woodpecker/` | `TestLoadSettingsRejectsInvalidProfiles`, `TestWoodpeckerTagDeliveryTemplate`, `TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout` | Run the complete suite with `go test ./...`. The public API and import boundary check is `go run ./internal/apicheck`. Local HTTP and Git fixtures exercise diff --git a/docs/release.md b/docs/release.md index 25fcb19..0463903 100644 --- a/docs/release.md +++ b/docs/release.md @@ -111,8 +111,8 @@ If validation or a guard fails before this invocation pushes, correct and commit the candidate, publish `main`, and run the command again. A retained local tag still requires the authoritative-state check above before removal; a concurrent publisher may already have published the version. The prepared -Woodpecker workflow builds the CLI from the tagged -checkout and runs page-only delivery. To activate it after configuring the +Woodpecker workflow builds the CLI from the tagged checkout, then uses +`ci-deliver` for page-only delivery. To activate it after configuring the destination and secret, copy it to `.woodpecker/releasekit-source-delivery.yml` and add the Woodpecker repository secret `releasekit_gitea_token`. If page delivery fails or is uncertain, inspect the hosted release and retry the same diff --git a/docs/roadmap/audit.md b/docs/roadmap/audit.md index a42bdc3..413ec45 100644 --- a/docs/roadmap/audit.md +++ b/docs/roadmap/audit.md @@ -227,11 +227,12 @@ when needed. ### F6 — Adoption priority: shared deployment still depends on copied mechanics -**Evidence.** The [source-only](../../examples/woodpecker/tag-delivery-source-only.yml) -and [binary](../../examples/woodpecker/tag-delivery-binary.yml) templates repeat -fetch/checkout guards, local exclusions, cache setup, and tool installation. -The binary template adds an explicit build/receipt/deliver sequence even though -the CLI already supports combined delivery. ReleaseKit's +**Evidence.** The original source-only and binary templates repeated +fetch/checkout guards, local exclusions, cache setup, and tool installation; +they are now consolidated in the maintained +[tag-delivery template](../../examples/woodpecker/tag-delivery.yml). The binary +template added an explicit build/receipt/deliver sequence even though the CLI +already supports combined delivery. ReleaseKit's [own workflow](../../examples/woodpecker/releasekit-source-delivery.yml) has a separate first-release bootstrap. Consumer pins remain placeholders as documented. @@ -249,14 +250,14 @@ Keep dependency preparation separate from offline validation/builds. A split build/delivery flow remains useful for an actual credential boundary or artifact handoff. See Design Choice 4 for packaging alternatives. -`TestWoodpeckerDeliveryCommandSequences` in -[the CLI workflow tests](../../cmd/releasekit/woodpecker_test.go) executes -equivalent commands; -[the ReleaseKit workflow test](../../cmd/releasekit/releasekit_delivery_workflow_test.go) -similarly reconstructs the sequence. Execution of the maintained YAML and -installation step remains unverified. A YAML drift can leave these tests green. -Validate the selected integration during adoption and keep a small check tied to -the maintained artifact. Use that evidence to maintain the shared integration. +The original `TestWoodpeckerDeliveryCommandSequences` and the ReleaseKit +workflow test reconstructed workflow commands, so YAML drift could leave the +tests green. The maintained tag template and ReleaseKit's bootstrap workflow +are now exercised from their command blocks by local Git/HTTP fixtures. The Go +bootstrap shim verifies template arguments while mapping the pinned module +invocation to the test executable; it does not verify published-module +availability. Real installation and live Woodpecker execution remain +deployment work. ### F7 — Medium: delivery asset classifications overlap inconsistently diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 10d6c77..1bed442 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # ReleaseKit audit implementation plan -Status: Stages 1–9 complete; Stage 10 next. +Status: Stages 1–10 complete; Stage 11 next. Baseline: `e4be7fa` (2026-10-06). The [audit](audit.md) records the evidence, product goals, and selected direction. This document owns implementation order and completion criteria for that work. @@ -510,6 +510,20 @@ matching remote content. The bootstrap shim proves template wiring, not published-module availability. Record that distinction and retain real installation/live Woodpecker verification as deployment work. +**Completed:** Source-only and binary consumer delivery now share one +profile-driven `tag-delivery.yml` using versioned `go run ... ci-deliver`; the +duplicated templates were removed. ReleaseKit's separate first-source workflow +still builds its CLI from tagged source and then uses `ci-deliver`. CLI workflow +tests extract and execute the maintained command blocks with matrix +substitution, Woodpecker dollar escaping, local Git/HTTP fixtures, and a Go +bootstrap shim. Both profiles deliver successfully twice, preserving matching +pages and binary assets. The shim validates command wiring but does not prove +published-module availability. Consumer and maintainer guidance now covers +bootstrap requirements, credentials, Go compatibility, caches, dependency +preparation, and pin upgrades. Real pins and live integration remain deployment +work. The full offline suite, API boundary check, and documentation whitespace +checks pass. + ## Stage 11 — Validate representative adoption and close the sprint **Covers:** audit-wide acceptance and adoption evidence. **Primary owners:** diff --git a/docs/roadmap/releasekit.md b/docs/roadmap/releasekit.md index 0347b88..569b0dc 100644 --- a/docs/roadmap/releasekit.md +++ b/docs/roadmap/releasekit.md @@ -211,9 +211,9 @@ infrastructure and one coherent release model. - Choose the broader consumer integration test plan and migration sequence. Evaluate API clarity, product-specific checks, and the infrastructure removed from each consuming repository. -- Select immutable ReleaseKit deployment pins and Go image digests for consumer - workflows. Provision delivery credentials and activate the prepared Woodpecker - workflows as part of deployment. +- Replace the maintained example's ReleaseKit version placeholder with a real + immutable revision or published version. Provision delivery credentials and + activate consumer Woodpecker workflows as part of deployment. - Verify live Gitea delivery and Woodpecker execution. Existing verification uses local Git and HTTP fixtures; no live deployment has been verified. - Keep ReleaseKit executable binary distribution deferred until deliverables diff --git a/examples/woodpecker/releasekit-source-delivery.yml b/examples/woodpecker/releasekit-source-delivery.yml index faf318b..9e85691 100644 --- a/examples/woodpecker/releasekit-source-delivery.yml +++ b/examples/woodpecker/releasekit-source-delivery.yml @@ -3,7 +3,7 @@ when: ref: refs/tags/v* steps: - - name: prepare + - name: build-tool image: golang:1.25.0-bookworm commands: - | @@ -14,11 +14,7 @@ steps: git checkout --detach "$${CI_COMMIT_SHA}" test "$(git rev-parse HEAD)" = "$${CI_COMMIT_SHA}" mkdir -p "$${CI_WORKSPACE}/.releasekit/bin" - - - name: build-tool - image: golang:1.25.0-bookworm - commands: - - GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off go build -o "$${CI_WORKSPACE}/.releasekit/bin/releasekit" ./cmd/releasekit + GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off go build -o "$${CI_WORKSPACE}/.releasekit/bin/releasekit" ./cmd/releasekit - name: deliver-page image: golang:1.25.0-bookworm @@ -27,4 +23,7 @@ steps: from_secret: releasekit_gitea_token commands: - | - "$${CI_WORKSPACE}/.releasekit/bin/releasekit" deliver --repo "$${CI_WORKSPACE}" "$${CI_COMMIT_TAG}" + "$${CI_WORKSPACE}/.releasekit/bin/releasekit" ci-deliver \ + --repo "$${CI_WORKSPACE}" \ + --commit "$${CI_COMMIT_SHA}" \ + "$${CI_COMMIT_TAG}" diff --git a/examples/woodpecker/tag-delivery-binary.yml b/examples/woodpecker/tag-delivery-binary.yml deleted file mode 100644 index a608468..0000000 --- a/examples/woodpecker/tag-delivery-binary.yml +++ /dev/null @@ -1,37 +0,0 @@ -matrix: - include: - - GO_IMAGE: golang:1.25.0-bookworm - RELEASEKIT_SOURCE: REPLACE_WITH_IMMUTABLE_COMMIT - -when: - - event: tag - ref: refs/tags/v* - -steps: - - name: prepare - image: ${GO_IMAGE} - commands: - - | - test -n "$${CI_COMMIT_TAG}" - test -n "$${CI_COMMIT_SHA}" - git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" - test "$(git rev-parse "refs/tags/$${CI_COMMIT_TAG}^{commit}")" = "$${CI_COMMIT_SHA}" - git checkout --detach "$${CI_COMMIT_SHA}" - test "$(git rev-parse HEAD)" = "$${CI_COMMIT_SHA}" - printf '\n/.releasekit/\n' >> .git/info/exclude - mkdir -p "$${CI_WORKSPACE}/.releasekit/bin" "$${CI_WORKSPACE}/.releasekit/gocache" "$${CI_WORKSPACE}/.releasekit/gomodcache" - export GOCACHE="$${CI_WORKSPACE}/.releasekit/gocache" - export GOMODCACHE="$${CI_WORKSPACE}/.releasekit/gomodcache" - go mod download - GOBIN="$${CI_WORKSPACE}/.releasekit/bin" go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@${RELEASEKIT_SOURCE} - - - name: deliver - image: ${GO_IMAGE} - environment: - RELEASEKIT_GITEA_TOKEN: - from_secret: releasekit_gitea_token - commands: - - | - export GOCACHE="$${CI_WORKSPACE}/.releasekit/gocache" - export GOMODCACHE="$${CI_WORKSPACE}/.releasekit/gomodcache" - "$${CI_WORKSPACE}/.releasekit/bin/releasekit" deliver --repo "$${CI_WORKSPACE}" "$${CI_COMMIT_TAG}" diff --git a/examples/woodpecker/tag-delivery-source-only.yml b/examples/woodpecker/tag-delivery-source-only.yml deleted file mode 100644 index eb98a79..0000000 --- a/examples/woodpecker/tag-delivery-source-only.yml +++ /dev/null @@ -1,36 +0,0 @@ -matrix: - include: - - GO_IMAGE: golang:1.25.0-bookworm - RELEASEKIT_SOURCE: REPLACE_WITH_IMMUTABLE_COMMIT - -when: - - event: tag - ref: refs/tags/v* - -steps: - - name: prepare - image: ${GO_IMAGE} - commands: - - | - test -n "$${CI_COMMIT_TAG}" - test -n "$${CI_COMMIT_SHA}" - git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" - test "$(git rev-parse "refs/tags/$${CI_COMMIT_TAG}^{commit}")" = "$${CI_COMMIT_SHA}" - git checkout --detach "$${CI_COMMIT_SHA}" - test "$(git rev-parse HEAD)" = "$${CI_COMMIT_SHA}" - printf '\n/.releasekit/\n' >> .git/info/exclude - mkdir -p "$${CI_WORKSPACE}/.releasekit/bin" "$${CI_WORKSPACE}/.releasekit/gocache" "$${CI_WORKSPACE}/.releasekit/gomodcache" - export GOCACHE="$${CI_WORKSPACE}/.releasekit/gocache" - export GOMODCACHE="$${CI_WORKSPACE}/.releasekit/gomodcache" - GOBIN="$${CI_WORKSPACE}/.releasekit/bin" go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@${RELEASEKIT_SOURCE} - - - name: deliver - image: ${GO_IMAGE} - environment: - RELEASEKIT_GITEA_TOKEN: - from_secret: releasekit_gitea_token - commands: - - | - export GOCACHE="$${CI_WORKSPACE}/.releasekit/gocache" - export GOMODCACHE="$${CI_WORKSPACE}/.releasekit/gomodcache" - "$${CI_WORKSPACE}/.releasekit/bin/releasekit" deliver --repo "$${CI_WORKSPACE}" "$${CI_COMMIT_TAG}" diff --git a/examples/woodpecker/tag-delivery.yml b/examples/woodpecker/tag-delivery.yml new file mode 100644 index 0000000..69e051f --- /dev/null +++ b/examples/woodpecker/tag-delivery.yml @@ -0,0 +1,21 @@ +matrix: + include: + - GO_IMAGE: golang:1.25.0-bookworm + RELEASEKIT_VERSION: REPLACE_WITH_IMMUTABLE_VERSION + +when: + - event: tag + ref: refs/tags/v* + +steps: + - name: deliver + image: ${GO_IMAGE} + environment: + RELEASEKIT_GITEA_TOKEN: + from_secret: releasekit_gitea_token + commands: + - | + go run gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@${RELEASEKIT_VERSION} ci-deliver \ + --repo "$${CI_WORKSPACE}" \ + --commit "$${CI_COMMIT_SHA}" \ + "$${CI_COMMIT_TAG}"