Consolidate Woodpecker delivery templates
This commit is contained in:
@@ -78,22 +78,30 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
|
||||
cliGitRun(t, repository, "remote", "set-url", "origin", remote)
|
||||
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")
|
||||
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)
|
||||
}
|
||||
build := exec.Command("go", "build", "-o", tool, "./cmd/releasekit")
|
||||
build.Dir = repository
|
||||
build.Env = append(os.Environ(), "GOTOOLCHAIN=local", "GOPROXY=off", "GOSUMDB=off")
|
||||
if output, err := build.CombinedOutput(); err != nil {
|
||||
t.Fatalf("build CLI from tagged checkout: %v\n%s", err, output)
|
||||
buildBlock, err := extractWoodpeckerCommand(string(template), "build-tool")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
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)
|
||||
@@ -109,7 +117,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
|
||||
state.remoteCommit = ""
|
||||
|
||||
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") {
|
||||
t.Fatalf("delivery attempt %d output = %q", attempt+1, output)
|
||||
}
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
@@ -13,12 +12,24 @@ import (
|
||||
"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")
|
||||
func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
|
||||
root := cliRepositoryRoot(t)
|
||||
templatePath := filepath.Join(root, "examples", "woodpecker", "tag-delivery.yml")
|
||||
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")
|
||||
build := exec.Command("go", "build", "-o", executable, "./cmd/releasekit")
|
||||
build.Dir = root
|
||||
@@ -26,8 +37,12 @@ func TestWoodpeckerDeliveryCommandSequences(t *testing.T) {
|
||||
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) {
|
||||
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{}
|
||||
server := newDeliveryServer(t, state)
|
||||
defer server.Close()
|
||||
@@ -35,13 +50,22 @@ func TestWoodpeckerDeliveryCommandSequences(t *testing.T) {
|
||||
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")
|
||||
commit := pushTaggedCLIRepository(t, repository)
|
||||
state.commit = commit
|
||||
|
||||
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)
|
||||
shimDir, goLog := installGoBootstrapShim(t)
|
||||
env := templateEnvironment(repository, commit, executable, module, shimDir, goLog)
|
||||
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)
|
||||
defer server.Close()
|
||||
writeDeliveryProfile(t, repository, cli.Settings{
|
||||
Product: "Example Product",
|
||||
Build: &cli.BuildSettings{MainPackage: ".", BinaryName: "helper", Targets: []string{"linux/amd64"}},
|
||||
Product: "Example Product",
|
||||
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"},
|
||||
})
|
||||
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)
|
||||
}
|
||||
commit := pushTaggedCLIRepository(t, repository)
|
||||
state.commit = commit
|
||||
|
||||
// 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)
|
||||
shimDir, goLog := installGoBootstrapShim(t)
|
||||
env := templateEnvironment(repository, commit, executable, module, shimDir, goLog)
|
||||
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 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)
|
||||
}
|
||||
if _, ok := state.assets["SHA256SUMS"]; !ok {
|
||||
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()
|
||||
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.Env = os.Environ()
|
||||
if credential {
|
||||
command.Env = append(command.Env, "RELEASEKIT_GITEA_TOKEN=fixture-token")
|
||||
for key, value := range environment {
|
||||
command.Env = setEnvironment(command.Env, key, value)
|
||||
}
|
||||
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())
|
||||
output, err := command.CombinedOutput()
|
||||
if err != nil {
|
||||
t.Fatalf("Woodpecker command failed: %v\ncommand: %s\noutput: %s", err, block, output)
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -148,31 +148,41 @@ exit codes, and transport behavior.
|
||||
|
||||
## 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)
|
||||
- [Binary tag workflow](../../examples/woodpecker/tag-delivery-binary.yml)
|
||||
The command runs versioned source directly with `go run`, passing
|
||||
`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
|
||||
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 normal Go module and build caches are sufficient. Cache mounts may be added
|
||||
as deployment configuration when they help the runner; they are not needed by
|
||||
the template. A locally retained executable can be installed with
|
||||
`go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@PIN`, then run
|
||||
with the same arguments:
|
||||
|
||||
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
|
||||
and Go caches remain available. The workflow keeps its `.releasekit/` directory
|
||||
out of Git status using the checkout's local exclude file.
|
||||
```sh
|
||||
releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$CI_COMMIT_SHA" "$CI_COMMIT_TAG"
|
||||
```
|
||||
|
||||
Preparation can access the Go module proxy to install the pinned ReleaseKit
|
||||
executable. The binary workflow also runs `go mod download` before delivery.
|
||||
The single `releasekit deliver` invocation builds selected targets through the
|
||||
facade and uploads them with the release page. The source-only workflow sends
|
||||
the committed note page directly. Neither template runs `check` or `publish`.
|
||||
Upgrade by changing the selected immutable
|
||||
`RELEASEKIT_VERSION` (or the `@PIN` used by `go install`) and validating the
|
||||
workflow against a tag fixture. The command and profile behavior are documented
|
||||
in the [CLI contract](../cli.md) and [configuration reference](../config.md).
|
||||
|
||||
Woodpecker runs delivery after the tag event, independently from the source
|
||||
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
|
||||
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
|
||||
for 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.
|
||||
|
||||
@@ -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` |
|
||||
| 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` |
|
||||
| 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
|
||||
check is `go run ./internal/apicheck`. Local HTTP and Git fixtures exercise
|
||||
|
||||
@@ -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
|
||||
local tag still requires the authoritative-state check above before removal;
|
||||
a concurrent publisher may already have published the version. The prepared
|
||||
Woodpecker workflow builds the CLI from the tagged
|
||||
checkout and runs page-only delivery. To activate it after configuring the
|
||||
Woodpecker workflow builds the CLI from the tagged checkout, then uses
|
||||
`ci-deliver` for page-only delivery. To activate it after configuring the
|
||||
destination and secret, copy it to `.woodpecker/releasekit-source-delivery.yml`
|
||||
and add the Woodpecker repository secret `releasekit_gitea_token`. If page
|
||||
delivery fails or is uncertain, inspect the hosted release and retry the same
|
||||
|
||||
@@ -227,11 +227,12 @@ when needed.
|
||||
|
||||
### F6 — Adoption priority: shared deployment still depends on copied mechanics
|
||||
|
||||
**Evidence.** The [source-only](../../examples/woodpecker/tag-delivery-source-only.yml)
|
||||
and [binary](../../examples/woodpecker/tag-delivery-binary.yml) templates repeat
|
||||
fetch/checkout guards, local exclusions, cache setup, and tool installation.
|
||||
The binary template adds an explicit build/receipt/deliver sequence even though
|
||||
the CLI already supports combined delivery. ReleaseKit's
|
||||
**Evidence.** The original source-only and binary templates repeated
|
||||
fetch/checkout guards, local exclusions, cache setup, and tool installation;
|
||||
they are now consolidated in the maintained
|
||||
[tag-delivery template](../../examples/woodpecker/tag-delivery.yml). The binary
|
||||
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
|
||||
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
|
||||
handoff. See Design Choice 4 for packaging alternatives.
|
||||
|
||||
`TestWoodpeckerDeliveryCommandSequences` in
|
||||
[the CLI workflow tests](../../cmd/releasekit/woodpecker_test.go) executes
|
||||
equivalent commands;
|
||||
[the ReleaseKit workflow test](../../cmd/releasekit/releasekit_delivery_workflow_test.go)
|
||||
similarly reconstructs the sequence. Execution of the maintained YAML and
|
||||
installation step remains unverified. A YAML drift can leave these tests green.
|
||||
Validate the selected integration during adoption and keep a small check tied to
|
||||
the maintained artifact. Use that evidence to maintain the shared integration.
|
||||
The original `TestWoodpeckerDeliveryCommandSequences` and the ReleaseKit
|
||||
workflow test reconstructed workflow commands, so YAML drift could leave the
|
||||
tests green. The maintained tag template and ReleaseKit's bootstrap workflow
|
||||
are now exercised from their command blocks by local Git/HTTP fixtures. The Go
|
||||
bootstrap shim verifies template arguments while mapping the pinned module
|
||||
invocation to the test executable; it does not verify published-module
|
||||
availability. Real installation and live Woodpecker execution remain
|
||||
deployment work.
|
||||
|
||||
### F7 — Medium: delivery asset classifications overlap inconsistently
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# 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,
|
||||
product goals, and selected direction. This document owns implementation order
|
||||
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
|
||||
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
|
||||
|
||||
**Covers:** audit-wide acceptance and adoption evidence. **Primary owners:**
|
||||
|
||||
@@ -211,9 +211,9 @@ infrastructure and one coherent release model.
|
||||
- Choose the broader consumer integration test plan and migration sequence.
|
||||
Evaluate API clarity, product-specific checks, and the infrastructure removed
|
||||
from each consuming repository.
|
||||
- Select immutable ReleaseKit deployment pins and Go image digests for consumer
|
||||
workflows. Provision delivery credentials and activate the prepared Woodpecker
|
||||
workflows as part of deployment.
|
||||
- Replace the maintained example's ReleaseKit version placeholder with a real
|
||||
immutable revision or published version. Provision delivery credentials and
|
||||
activate consumer Woodpecker workflows as part of deployment.
|
||||
- Verify live Gitea delivery and Woodpecker execution. Existing verification uses
|
||||
local Git and HTTP fixtures; no live deployment has been verified.
|
||||
- Keep ReleaseKit executable binary distribution deferred until deliverables
|
||||
|
||||
@@ -3,7 +3,7 @@ when:
|
||||
ref: refs/tags/v*
|
||||
|
||||
steps:
|
||||
- name: prepare
|
||||
- name: build-tool
|
||||
image: golang:1.25.0-bookworm
|
||||
commands:
|
||||
- |
|
||||
@@ -14,11 +14,7 @@ steps:
|
||||
git checkout --detach "$${CI_COMMIT_SHA}"
|
||||
test "$(git rev-parse HEAD)" = "$${CI_COMMIT_SHA}"
|
||||
mkdir -p "$${CI_WORKSPACE}/.releasekit/bin"
|
||||
|
||||
- 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
|
||||
GOTOOLCHAIN=local GOPROXY=off GOSUMDB=off go build -o "$${CI_WORKSPACE}/.releasekit/bin/releasekit" ./cmd/releasekit
|
||||
|
||||
- name: deliver-page
|
||||
image: golang:1.25.0-bookworm
|
||||
@@ -27,4 +23,7 @@ steps:
|
||||
from_secret: releasekit_gitea_token
|
||||
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}"
|
||||
|
||||
@@ -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}"
|
||||
@@ -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}"
|
||||
21
examples/woodpecker/tag-delivery.yml
Normal file
21
examples/woodpecker/tag-delivery.yml
Normal 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}"
|
||||
Reference in New Issue
Block a user