Add Woodpecker tagged-release workflows

This commit is contained in:
2026-10-05 20:49:45 +00:00
parent bdf6a155b5
commit 9e619c98a4
5 changed files with 247 additions and 2 deletions

View File

@@ -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()
}

View File

@@ -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.

View File

@@ -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.

View File

@@ -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}"

View File

@@ -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}"