Consolidate Woodpecker delivery templates

This commit is contained in:
2026-10-06 19:49:19 +00:00
parent c6a5db8aeb
commit 21294404d7
12 changed files with 295 additions and 184 deletions

View File

@@ -78,22 +78,30 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
cliGitRun(t, repository, "remote", "set-url", "origin", remote) cliGitRun(t, repository, "remote", "set-url", "origin", remote)
cliGitRun(t, repository, "push", "origin", "HEAD:refs/heads/main", "refs/tags/"+version) 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") 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) t.Fatal(err)
} }
build := exec.Command("go", "build", "-o", tool, "./cmd/releasekit") buildBlock, err := extractWoodpeckerCommand(string(template), "build-tool")
build.Dir = repository if err != nil {
build.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOPROXY=off", "GOSUMDB=off") t.Fatal(err)
if output, err := build.CombinedOutput(); err != nil { }
t.Fatalf("build CLI from tagged checkout: %v\n%s", err, output) 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) state.remoteCommit = strings.Repeat("f", 40)
@@ -109,7 +117,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
state.remoteCommit = "" state.remoteCommit = ""
for attempt := 0; attempt < 2; attempt++ { 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") { if !strings.Contains(output, "state complete") {
t.Fatalf("delivery attempt %d output = %q", attempt+1, output) t.Fatalf("delivery attempt %d output = %q", attempt+1, output)
} }

View File

@@ -1,8 +1,7 @@
package main package main
import ( import (
"bytes" "fmt"
"context"
"os" "os"
"os/exec" "os/exec"
"path/filepath" "path/filepath"
@@ -13,12 +12,24 @@ import (
"gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli" "gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit/internal/cli"
) )
func TestWoodpeckerDeliveryCommandSequences(t *testing.T) { func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
_, sourceFile, _, ok := runtime.Caller(0) root := cliRepositoryRoot(t)
if !ok { templatePath := filepath.Join(root, "examples", "woodpecker", "tag-delivery.yml")
t.Fatal("locate integration test source") 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") executable := filepath.Join(t.TempDir(), "releasekit")
build := exec.Command("go", "build", "-o", executable, "./cmd/releasekit") build := exec.Command("go", "build", "-o", executable, "./cmd/releasekit")
build.Dir = root build.Dir = root
@@ -26,8 +37,12 @@ func TestWoodpeckerDeliveryCommandSequences(t *testing.T) {
t.Fatalf("build local ReleaseKit executable: %v\n%s", err, output) 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) { t.Run("source-only", func(t *testing.T) {
repository := newCLIRepository(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{} state := &deliveryServerState{}
server := newDeliveryServer(t, state) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
@@ -35,13 +50,22 @@ func TestWoodpeckerDeliveryCommandSequences(t *testing.T) {
Product: "Example Product", Product: "Example Product",
Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") commit := pushTaggedCLIRepository(t, repository)
cliGitRun(t, repository, "tag", "v1.2.3") state.commit = commit
cliGitRun(t, repository, "checkout", "--detach", "v1.2.3")
output := runWoodpeckerCommand(t, executable, repository, true, "deliver", "--repo", repository, "v1.2.3") shimDir, goLog := installGoBootstrapShim(t)
if !strings.Contains(output, "state complete") || state.releaseCreates != 1 || state.assetUploads != 0 { env := templateEnvironment(repository, commit, executable, module, shimDir, goLog)
t.Fatalf("source-only workflow output=%q creates=%d uploads=%d", output, state.releaseCreates, state.assetUploads) 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) server := newDeliveryServer(t, state)
defer server.Close() defer server.Close()
writeDeliveryProfile(t, repository, cli.Settings{ writeDeliveryProfile(t, repository, cli.Settings{
Product: "Example Product", Product: "Example Product",
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}}, 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"}, Delivery: &cli.DeliverySettings{BaseURL: server.URL, Owner: "owner", Repository: "example"},
}) })
state.commit = cliGitOutput(t, repository, "rev-parse", "HEAD") commit := pushTaggedCLIRepository(t, repository)
cliGitRun(t, repository, "tag", "v1.2.3") state.commit = commit
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 shimDir, goLog := installGoBootstrapShim(t)
// the executable built above replacing its pinned go install step. env := templateEnvironment(repository, commit, executable, module, shimDir, goLog)
runWoodpeckerCommand(t, executable, repository, false, "build", "--repo", repository, "--output", assets, "v1.2.3") for attempt := 0; attempt < 2; attempt++ {
output := runWoodpeckerCommand(t, executable, repository, true, "deliver", "--repo", repository, "--assets", assets, "v1.2.3") output := runWoodpeckerBlock(t, command, repository, env)
if !strings.Contains(output, "state complete") || state.releaseCreates != 1 || state.assetUploads != 2 { if !strings.Contains(output, "state complete") {
t.Fatalf("binary workflow output=%q creates=%d uploads=%d", output, state.releaseCreates, state.assetUploads) 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) t.Fatalf("binary asset was not delivered: %#v", state.assets)
} }
if _, ok := state.assets["SHA256SUMS"]; !ok { if _, ok := state.assets["SHA256SUMS"]; !ok {
t.Fatalf("checksum manifest was not delivered: %#v", state.assets) 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() 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.Dir = repository
command.Env = os.Environ() command.Env = os.Environ()
if credential { for key, value := range environment {
command.Env = append(command.Env, "RELEASEKIT_GITEA_TOKEN=fixture-token") command.Env = setEnvironment(command.Env, key, value)
} }
var stdout, stderr bytes.Buffer output, err := command.CombinedOutput()
command.Stdout, command.Stderr = &stdout, &stderr if err != nil {
if err := command.Run(); err != nil { t.Fatalf("Woodpecker command failed: %v\ncommand: %s\noutput: %s", err, block, output)
t.Fatalf("ReleaseKit %v failed: %v\nstdout: %s\nstderr: %s", args, err, stdout.String(), stderr.String())
} }
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)
} }

View File

@@ -148,31 +148,41 @@ exit codes, and transport behavior.
## Woodpecker tag workflows ## 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) The command runs versioned source directly with `go run`, passing
- [Binary tag workflow](../../examples/woodpecker/tag-delivery-binary.yml) `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 The normal Go module and build caches are sufficient. Cache mounts may be added
matrix entry, set `GO_IMAGE` to a Go image compatible with the consumer module as deployment configuration when they help the runner; they are not needed by
and replace `RELEASEKIT_SOURCE` with an immutable ReleaseKit commit. The the template. A locally retained executable can be installed with
examples deliberately contain no claimed published ReleaseKit pin. Configure `go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@PIN`, then run
the Woodpecker repository secret `releasekit_gitea_token`; the workflow maps it with the same arguments:
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 ```sh
`CI_COMMIT_SHA`, and checks out that commit detached. Woodpecker shares the releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$CI_COMMIT_SHA" "$CI_COMMIT_TAG"
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.
Preparation can access the Go module proxy to install the pinned ReleaseKit Upgrade by changing the selected immutable
executable. The binary workflow also runs `go mod download` before delivery. `RELEASEKIT_VERSION` (or the `@PIN` used by `go install`) and validating the
The single `releasekit deliver` invocation builds selected targets through the workflow against a tag fixture. The command and profile behavior are documented
facade and uploads them with the release page. The source-only workflow sends in the [CLI contract](../cli.md) and [configuration reference](../config.md).
the committed note page directly. Neither template runs `check` or `publish`.
Woodpecker runs delivery after the tag event, independently from the source Woodpecker runs delivery after the tag event, independently from the source
publication result. Read its workflow status as delivery status: a delivery 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 can be inspected and retried against the same immutable tag and commit; do not
move the tag to make a retry pass. See move the tag to make a retry pass. See
[Woodpecker's environment reference](https://woodpecker-ci.org/docs/usage/environment) [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 [workflow syntax](https://woodpecker-ci.org/docs/usage/workflow-syntax) for tag
events, sequential steps, and the shared workspace. events, sequential steps, and the shared workspace.

View File

@@ -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` | | 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` | | 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` | | 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 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 check is `go run ./internal/apicheck`. Local HTTP and Git fixtures exercise

View File

@@ -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 commit the candidate, publish `main`, and run the command again. A retained
local tag still requires the authoritative-state check above before removal; local tag still requires the authoritative-state check above before removal;
a concurrent publisher may already have published the version. The prepared a concurrent publisher may already have published the version. The prepared
Woodpecker workflow builds the CLI from the tagged Woodpecker workflow builds the CLI from the tagged checkout, then uses
checkout and runs page-only delivery. To activate it after configuring the `ci-deliver` for page-only delivery. To activate it after configuring the
destination and secret, copy it to `.woodpecker/releasekit-source-delivery.yml` destination and secret, copy it to `.woodpecker/releasekit-source-delivery.yml`
and add the Woodpecker repository secret `releasekit_gitea_token`. If page and add the Woodpecker repository secret `releasekit_gitea_token`. If page
delivery fails or is uncertain, inspect the hosted release and retry the same delivery fails or is uncertain, inspect the hosted release and retry the same

View File

@@ -227,11 +227,12 @@ when needed.
### F6 — Adoption priority: shared deployment still depends on copied mechanics ### F6 — Adoption priority: shared deployment still depends on copied mechanics
**Evidence.** The [source-only](../../examples/woodpecker/tag-delivery-source-only.yml) **Evidence.** The original source-only and binary templates repeated
and [binary](../../examples/woodpecker/tag-delivery-binary.yml) templates repeat fetch/checkout guards, local exclusions, cache setup, and tool installation;
fetch/checkout guards, local exclusions, cache setup, and tool installation. they are now consolidated in the maintained
The binary template adds an explicit build/receipt/deliver sequence even though [tag-delivery template](../../examples/woodpecker/tag-delivery.yml). The binary
the CLI already supports combined delivery. ReleaseKit's 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 [own workflow](../../examples/woodpecker/releasekit-source-delivery.yml) has a
separate first-release bootstrap. Consumer pins remain placeholders as documented. 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 build/delivery flow remains useful for an actual credential boundary or artifact
handoff. See Design Choice 4 for packaging alternatives. handoff. See Design Choice 4 for packaging alternatives.
`TestWoodpeckerDeliveryCommandSequences` in The original `TestWoodpeckerDeliveryCommandSequences` and the ReleaseKit
[the CLI workflow tests](../../cmd/releasekit/woodpecker_test.go) executes workflow test reconstructed workflow commands, so YAML drift could leave the
equivalent commands; tests green. The maintained tag template and ReleaseKit's bootstrap workflow
[the ReleaseKit workflow test](../../cmd/releasekit/releasekit_delivery_workflow_test.go) are now exercised from their command blocks by local Git/HTTP fixtures. The Go
similarly reconstructs the sequence. Execution of the maintained YAML and bootstrap shim verifies template arguments while mapping the pinned module
installation step remains unverified. A YAML drift can leave these tests green. invocation to the test executable; it does not verify published-module
Validate the selected integration during adoption and keep a small check tied to availability. Real installation and live Woodpecker execution remain
the maintained artifact. Use that evidence to maintain the shared integration. deployment work.
### F7 — Medium: delivery asset classifications overlap inconsistently ### F7 — Medium: delivery asset classifications overlap inconsistently

View File

@@ -1,6 +1,6 @@
# ReleaseKit audit implementation plan # 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, Baseline: `e4be7fa` (2026-10-06). The [audit](audit.md) records the evidence,
product goals, and selected direction. This document owns implementation order product goals, and selected direction. This document owns implementation order
and completion criteria for that work. 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 published-module availability. Record that distinction and retain real
installation/live Woodpecker verification as deployment work. 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 ## Stage 11 — Validate representative adoption and close the sprint
**Covers:** audit-wide acceptance and adoption evidence. **Primary owners:** **Covers:** audit-wide acceptance and adoption evidence. **Primary owners:**

View File

@@ -211,9 +211,9 @@ infrastructure and one coherent release model.
- Choose the broader consumer integration test plan and migration sequence. - Choose the broader consumer integration test plan and migration sequence.
Evaluate API clarity, product-specific checks, and the infrastructure removed Evaluate API clarity, product-specific checks, and the infrastructure removed
from each consuming repository. from each consuming repository.
- Select immutable ReleaseKit deployment pins and Go image digests for consumer - Replace the maintained example's ReleaseKit version placeholder with a real
workflows. Provision delivery credentials and activate the prepared Woodpecker immutable revision or published version. Provision delivery credentials and
workflows as part of deployment. activate consumer Woodpecker workflows as part of deployment.
- Verify live Gitea delivery and Woodpecker execution. Existing verification uses - Verify live Gitea delivery and Woodpecker execution. Existing verification uses
local Git and HTTP fixtures; no live deployment has been verified. local Git and HTTP fixtures; no live deployment has been verified.
- Keep ReleaseKit executable binary distribution deferred until deliverables - Keep ReleaseKit executable binary distribution deferred until deliverables

View File

@@ -3,7 +3,7 @@ when:
ref: refs/tags/v* ref: refs/tags/v*
steps: steps:
- name: prepare - name: build-tool
image: golang:1.25.0-bookworm image: golang:1.25.0-bookworm
commands: commands:
- | - |
@@ -14,11 +14,7 @@ steps:
git checkout --detach "$${CI_COMMIT_SHA}" git checkout --detach "$${CI_COMMIT_SHA}"
test "$(git rev-parse HEAD)" = "$${CI_COMMIT_SHA}" test "$(git rev-parse HEAD)" = "$${CI_COMMIT_SHA}"
mkdir -p "$${CI_WORKSPACE}/.releasekit/bin" mkdir -p "$${CI_WORKSPACE}/.releasekit/bin"
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off go build -o "$${CI_WORKSPACE}/.releasekit/bin/releasekit" ./cmd/releasekit
- 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
- name: deliver-page - name: deliver-page
image: golang:1.25.0-bookworm image: golang:1.25.0-bookworm
@@ -27,4 +23,7 @@ steps:
from_secret: releasekit_gitea_token from_secret: releasekit_gitea_token
commands: 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}"

View File

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

View File

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

View File

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