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),
|
See the [CLI contract](../cli.md), [configuration reference](../config.md),
|
||||||
and [Gitea integration guide](../integrations/gitea.md) for exact settings,
|
and [Gitea integration guide](../integrations/gitea.md) for exact settings,
|
||||||
exit codes, and transport behavior.
|
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
|
# 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
|
Date: 2026-10-05
|
||||||
|
|
||||||
@@ -835,7 +835,7 @@ no live Gitea service or credentials were used.
|
|||||||
|
|
||||||
## Stage 16: Provide the common Woodpecker integration
|
## 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.
|
1. Add maintained source-only and binary-producing tag-pipeline templates.
|
||||||
Parameterize the Go image and immutable ReleaseKit source pin for eventual
|
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)
|
[Woodpecker environment reference](https://woodpecker-ci.org/docs/usage/environment)
|
||||||
owns the runner-provided tag and commit variables.
|
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
|
## Stage 17: Prepare ReleaseKit's own hosted-page delivery
|
||||||
|
|
||||||
Status: Pending. Depends on Stage 16.
|
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