Add Woodpecker tagged-release workflows
This commit is contained in:
106
cmd/releasekit/woodpecker_test.go
Normal file
106
cmd/releasekit/woodpecker_test.go
Normal 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()
|
||||
}
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
45
examples/woodpecker/tag-delivery-binary.yml
Normal file
45
examples/woodpecker/tag-delivery-binary.yml
Normal 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}"
|
||||
36
examples/woodpecker/tag-delivery-source-only.yml
Normal file
36
examples/woodpecker/tag-delivery-source-only.yml
Normal 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}"
|
||||
Reference in New Issue
Block a user