diff --git a/cmd/releasekit/woodpecker_test.go b/cmd/releasekit/woodpecker_test.go new file mode 100644 index 0000000..e21105b --- /dev/null +++ b/cmd/releasekit/woodpecker_test.go @@ -0,0 +1,106 @@ +package main + +import ( + "bytes" + "context" + "os" + "os/exec" + "path/filepath" + "runtime" + "strings" + "testing" + + "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") + } + root := filepath.Clean(filepath.Join(filepath.Dir(sourceFile), "../..")) + executable := filepath.Join(t.TempDir(), "releasekit") + build := exec.Command("go", "build", "-o", executable, "./cmd/releasekit") + build.Dir = root + if output, err := build.CombinedOutput(); err != nil { + t.Fatalf("build local ReleaseKit executable: %v\n%s", err, output) + } + + t.Run("source-only", func(t *testing.T) { + repository := newCLIRepository(t) + state := &deliveryServerState{} + server := newDeliveryServer(t, state) + defer server.Close() + 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") + cliGitRun(t, repository, "tag", "v1.2.3") + cliGitRun(t, repository, "checkout", "--detach", "v1.2.3") + + 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) + } + }) + + t.Run("binary", func(t *testing.T) { + repository := newDeliveryBuildRepository(t) + state := &deliveryServerState{} + 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"}}, + 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) + } + + // 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) + } + if _, ok := state.assets["helper-v1.2.3-linux-amd64"]; !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) + } + }) +} + +func runWoodpeckerCommand(t *testing.T, executable, repository string, credential bool, args ...string) string { + t.Helper() + command := exec.CommandContext(context.Background(), executable, args...) + command.Dir = repository + command.Env = os.Environ() + if credential { + command.Env = append(command.Env, "RELEASEKIT_GITEA_TOKEN=fixture-token") + } + 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()) + } + return stdout.String() +} diff --git a/docs/consumers/delivery.md b/docs/consumers/delivery.md index 535ef15..2269c9d 100644 --- a/docs/consumers/delivery.md +++ b/docs/consumers/delivery.md @@ -79,3 +79,44 @@ fmt.Println(result.State, result.ReleaseURL) See the [CLI contract](../cli.md), [configuration reference](../config.md), and [Gitea integration guide](../integrations/gitea.md) for exact settings, exit codes, and transport behavior. + +## Woodpecker tag workflows + +Maintained workflow examples are available for source-only and binary delivery: + +- [Source-only tag workflow](../../examples/woodpecker/tag-delivery-source-only.yml) +- [Binary tag workflow](../../examples/woodpecker/tag-delivery-binary.yml) + +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 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, +Go caches, build outputs, and build receipt remain available to later steps. +The workflow keeps its `.releasekit/` directory out of Git status using the +checkout's local exclude file. + +Preparation can access the Go module proxy to install the pinned ReleaseKit +executable. The binary workflow also runs `go mod download` before the build. +The later `releasekit build` command runs with `GOPROXY=off` and +`GOTOOLCHAIN=local`; its declared binary targets and receipt are placed in +`.releasekit/assets`. Delivery then passes that directory with `--assets`. +The source-only workflow skips this build step and sends the committed note +page directly. Neither template runs `check` or `publish`. + +Woodpecker runs delivery after the tag event, independently from the source +publication result. Read its workflow status as delivery status: a delivery +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 +[workflow syntax](https://woodpecker-ci.org/docs/usage/workflow-syntax) for tag +events, sequential steps, and the shared workspace. diff --git a/docs/roadmap/implementation.md b/docs/roadmap/implementation.md index 3f4c2a7..cf73db8 100644 --- a/docs/roadmap/implementation.md +++ b/docs/roadmap/implementation.md @@ -1,6 +1,6 @@ # ReleaseKit staged implementation plan -Status: In progress. Stages 1–15 are complete; later stages remain pending. +Status: In progress. Stages 1–16 are complete; later stages remain pending. Date: 2026-10-05 @@ -835,7 +835,7 @@ no live Gitea service or credentials were used. ## Stage 16: Provide the common Woodpecker integration -Status: Pending. Depends on Stage 15. +Status: Complete. Depends on Stage 15. 1. Add maintained source-only and binary-producing tag-pipeline templates. Parameterize the Go image and immutable ReleaseKit source pin for eventual @@ -863,6 +863,23 @@ CI success is not a source release gate. The [Woodpecker environment reference](https://woodpecker-ci.org/docs/usage/environment) owns the runner-provided tag and commit variables. +Evidence: maintained source-only and binary tag workflows are in +`examples/woodpecker/`. Both parameterize the Go image and immutable ReleaseKit +commit through their matrix entries, verify the fetched tag against the runner +commit, and keep shared output under the locally ignored `.releasekit/` +directory. Only the delivery step maps the configured Woodpecker secret to the +CLI token variable. The source-only workflow delivers the note page; the binary +workflow prepares module caches, builds declared targets offline, and passes +the build receipt and assets into delivery. The delivery guide covers template +inputs, source publication versus delivery status, and retries against the same +tag. A test builds the CLI from this checkout and exercises both workflows' +delivery command sequences against fixture repositories and a local HTTP +server; it substitutes the built executable for the installation command. Both +YAML files parse with locally available PyYAML 6.0.3, and static checks confirm +their tag filters, step layout, and delivery-only secret mapping. `go test ./...`, +`go run ./internal/apicheck`, and the Markdown whitespace check pass. No live +Woodpecker runner, image pull, secret, or hosted delivery service was used. + ## Stage 17: Prepare ReleaseKit's own hosted-page delivery Status: Pending. Depends on Stage 16. diff --git a/examples/woodpecker/tag-delivery-binary.yml b/examples/woodpecker/tag-delivery-binary.yml new file mode 100644 index 0000000..2d143ab --- /dev/null +++ b/examples/woodpecker/tag-delivery-binary.yml @@ -0,0 +1,45 @@ +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: build + image: ${GO_IMAGE} + commands: + - | + export GOCACHE="$${CI_WORKSPACE}/.releasekit/gocache" + export GOMODCACHE="$${CI_WORKSPACE}/.releasekit/gomodcache" + GOPROXY=off GOTOOLCHAIN=local "$${CI_WORKSPACE}/.releasekit/bin/releasekit" build --repo "$${CI_WORKSPACE}" --output "$${CI_WORKSPACE}/.releasekit/assets" "$${CI_COMMIT_TAG}" + + - 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}" --assets "$${CI_WORKSPACE}/.releasekit/assets" "$${CI_COMMIT_TAG}" diff --git a/examples/woodpecker/tag-delivery-source-only.yml b/examples/woodpecker/tag-delivery-source-only.yml new file mode 100644 index 0000000..eb98a79 --- /dev/null +++ b/examples/woodpecker/tag-delivery-source-only.yml @@ -0,0 +1,36 @@ +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}"