From f076da99b68474822c9506d326cf6c44e860a505 Mon Sep 17 00:00:00 2001 From: Eric Rakestraw Date: Sun, 11 Oct 2026 02:07:54 +0000 Subject: [PATCH] Resolve annotated tag events in Woodpecker delivery workflows --- .../multi_module_woodpecker_test.go | 3 +- .../releasekit_delivery_workflow_test.go | 2 +- cmd/releasekit/woodpecker_test.go | 67 +++++++++++++------ docs/cli.md | 7 +- docs/consumers/delivery.md | 26 ++++--- docs/roadmap/audita-bug-report.md | 16 ++++- examples/multi-module/woodpecker.yml | 10 ++- .../woodpecker/releasekit-source-delivery.yml | 12 ++-- examples/woodpecker/tag-delivery.yml | 12 ++-- 9 files changed, 111 insertions(+), 44 deletions(-) diff --git a/cmd/releasekit/multi_module_woodpecker_test.go b/cmd/releasekit/multi_module_woodpecker_test.go index b8f57db..1005fd4 100644 --- a/cmd/releasekit/multi_module_woodpecker_test.go +++ b/cmd/releasekit/multi_module_woodpecker_test.go @@ -51,6 +51,7 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) { commit := tagAndPushWorkflowRepository(t, repository, tag) delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit }) env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) + env["CI_COMMIT_SHA"] = cliGitOutput(t, repository, "rev-parse", "refs/tags/"+tag) if output := runWoodpeckerBlock(t, command, repository, env); !strings.Contains(output, "state complete") || !strings.Contains(output, "tool-version=v0.2.197") { t.Fatalf("tagged page delivery output = %q", output) } @@ -382,7 +383,7 @@ func tagAndPushWorkflowRepository(t *testing.T, repository, tag string) string { cliGitRun(t, repository, "add", "--all") cliGitRun(t, repository, "commit", "-m", "prepare mapped delivery") commit := cliGitOutput(t, repository, "rev-parse", "HEAD") - cliGitRun(t, repository, "tag", tag) + cliGitRun(t, repository, "tag", "-a", tag, "-m", "mapped release") remote := filepath.Join(filepath.Dir(repository), "origin.git") cliGitRun(t, repository, "init", "--bare", remote) cliGitRun(t, repository, "remote", "add", "origin", remote) diff --git a/cmd/releasekit/releasekit_delivery_workflow_test.go b/cmd/releasekit/releasekit_delivery_workflow_test.go index 9410845..f413e7b 100644 --- a/cmd/releasekit/releasekit_delivery_workflow_test.go +++ b/cmd/releasekit/releasekit_delivery_workflow_test.go @@ -78,7 +78,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) { } env := map[string]string{ "CI_WORKSPACE": repository, - "CI_COMMIT_SHA": state.snapshot().commit, + "CI_COMMIT_SHA": cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version), "CI_COMMIT_TAG": version, "RELEASEKIT_GITEA_TOKEN": "fixture-token", "GOTOOLCHAIN": "local", diff --git a/cmd/releasekit/woodpecker_test.go b/cmd/releasekit/woodpecker_test.go index 9a9fb04..fb2945c 100644 --- a/cmd/releasekit/woodpecker_test.go +++ b/cmd/releasekit/woodpecker_test.go @@ -104,10 +104,14 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) { repository := newCLIRepository(t) selectReleaseKitModule(t, repository, root, false) commit := pushTaggedCLIRepository(t, repository) + cliGitRun(t, repository, "commit", "--allow-empty", "-m", "unrelated event commit") + unrelated := cliGitOutput(t, repository, "rev-parse", "HEAD") goDir, goLog := installControlledGoBootstrapShim(t) - env := templateEnvironment(repository, strings.Repeat("f", len(commit)), goDir, goLog) + env := templateEnvironment(repository, unrelated, goDir, goLog) if output, err := executeWoodpeckerBlock(command, repository, env); err == nil { t.Fatalf("wrong tag succeeded: %s", output) + } else if !strings.Contains(output, commit) || !strings.Contains(output, unrelated) { + t.Fatalf("tag mismatch omitted commit identities: %s", output) } assertNoCommands(t, goLog) }) @@ -126,26 +130,49 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) { }) t.Run("selected executable receives event arguments and its status", func(t *testing.T) { - repository := newCLIRepository(t) - selectReleaseKitModule(t, repository, root, false) - commit := pushTaggedCLIRepository(t, repository) - goDir, goLog := installControlledGoBootstrapShim(t) - toolArgs := filepath.Join(t.TempDir(), "tool-arguments") - env := templateEnvironment(repository, strings.ToUpper(commit), goDir, goLog) - env["RELEASEKIT_TOOL_ARGS_LOG"] = toolArgs - env["RELEASEKIT_TOOL_EXIT"] = "37" - output, err := executeWoodpeckerBlock(command, repository, env) - var exitError *exec.ExitError - if !errors.As(err, &exitError) || exitError.ExitCode() != 37 { - t.Fatalf("template error = %v, want propagated exit 37; output=%q", err, output) + for _, event := range []struct { + name, tag string + tagObject bool + }{ + {"lightweight commit", "v1.2.3", false}, + {"annotated commit", "v1.2.4", false}, + {"annotated tag object", "v1.2.4", true}, + } { + t.Run(event.name, func(t *testing.T) { + repository := newCLIRepository(t) + selectReleaseKitModule(t, repository, root, false) + commit := pushTaggedCLIRepository(t, repository) + if event.tag == "v1.2.4" { + cliGitRun(t, repository, "tag", "-a", event.tag, "-m", "annotated release") + cliGitRun(t, repository, "push", "origin", "refs/tags/"+event.tag) + } + eventID := commit + if event.tagObject { + eventID = cliGitOutput(t, repository, "rev-parse", "refs/tags/"+event.tag) + } + goDir, goLog := installControlledGoBootstrapShim(t) + toolArgs := filepath.Join(t.TempDir(), "tool-arguments") + env := templateEnvironment(repository, strings.ToUpper(eventID), goDir, goLog) + env["CI_COMMIT_TAG"] = event.tag + env["RELEASEKIT_TOOL_ARGS_LOG"] = toolArgs + env["RELEASEKIT_TOOL_EXIT"] = "37" + output, err := executeWoodpeckerBlock(command, repository, env) + var exitError *exec.ExitError + if !errors.As(err, &exitError) || exitError.ExitCode() != 37 { + t.Fatalf("template error = %v, want propagated exit 37; output=%q", err, output) + } + args := readWoodpeckerFixtureFile(t, toolArgs) + for _, expected := range []string{"", "<--repo>", "<" + repository + ">", "<--prepared>", "<" + event.tag + ">"} { + if !strings.Contains(args, expected) { + t.Errorf("selected executable arguments %q omit %q", args, expected) + } + } + if !strings.Contains(strings.ToLower(args), "<--commit>\n<"+commit+">") { + t.Errorf("selected executable did not receive source commit %s: %q", commit, args) + } + assertGoSelection(t, repository, goLog) + }) } - args := readWoodpeckerFixtureFile(t, toolArgs) - for _, expected := range []string{"", "<--repo>", "<" + repository + ">", "<--prepared>", "<--commit>", "<" + strings.ToUpper(commit) + ">", ""} { - if !strings.Contains(args, expected) { - t.Errorf("selected executable arguments %q omit %q", args, expected) - } - } - assertGoSelection(t, repository, goLog) }) } diff --git a/docs/cli.md b/docs/cli.md index 142be64..62ad0c6 100644 --- a/docs/cli.md +++ b/docs/cli.md @@ -241,8 +241,11 @@ changes the selected checkout; `--prepared` reuses a verified checkout. Ordinary `deliver` continues to operate on already prepared source without fetching, checking out, or downloading dependencies. -In Woodpecker, pass `CI_WORKSPACE`, `CI_COMMIT_SHA`, and `CI_COMMIT_TAG` as the -repository, expected commit, and full tag arguments. The CLI does not read +In Woodpecker, pass `CI_WORKSPACE`, the source commit resolved from +`CI_COMMIT_SHA`, and `CI_COMMIT_TAG` as the repository, expected commit, and full +tag arguments. The event ID may identify an annotated tag object; see the +[delivery guide](consumers/delivery.md#woodpecker-tag-workflows) for resolution +and workflow preparation. The CLI does not read provider-specific environment variable names. Checkout and dependency preparation failures return status 1; usage, profile, credential, and invalid version errors return status 2. Final delivery uses the same statuses as diff --git a/docs/consumers/delivery.md b/docs/consumers/delivery.md index 0967cf2..77d25a5 100644 --- a/docs/consumers/delivery.md +++ b/docs/consumers/delivery.md @@ -196,14 +196,16 @@ a `delivery` profile. Page-only and binary behavior both use this template; the profile's build target list selects whether application binaries are built and delivered. -Before running Go, the template fetches the explicit event tag, compares its -peeled commit with the provider's full commit ID without regard to hex case, -checks out that commit detached, and confirms the source tree is clean. It then -copies the tagged `go.mod` and any `go.sum` to a temporary directory outside -the checkout. `go list -m` confirms that the module selects ReleaseKit; Go uses +Before running Go, the template fetches the explicit event tag and independently +resolves both the tag and `CI_COMMIT_SHA` to source commits. The event ID may +identify an annotated tag object or a commit. The template requires the resolved +commits to match, checks out that commit detached, and confirms the source tree +is clean. It then copies the tagged `go.mod` and any `go.sum` to a temporary +directory outside the checkout. `go list -m` confirms that the module selects +ReleaseKit; Go uses an alternate `-modfile` to prepare dependencies and build the selected `cmd/releasekit` package without changing the tagged module files. The template -invokes that executable with `CI_WORKSPACE`, `CI_COMMIT_SHA`, and +invokes that executable with `CI_WORKSPACE`, the resolved event commit, and `CI_COMMIT_TAG`. A cleanup trap removes the temporary metadata and executable. Tool bootstrap and application artifact preparation are separate. The bootstrap @@ -243,10 +245,12 @@ 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 separately installed executable can be installed with `go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@PIN`, then run -with the same arguments: +with the same arguments. With the event object available in the CI checkout, +resolve it to its source commit before invocation: ```sh -releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$CI_COMMIT_SHA" "$CI_COMMIT_TAG" +expected_commit=$(git -C "$CI_WORKSPACE" rev-parse --verify "${CI_COMMIT_SHA}^{commit}") +releasekit ci-deliver --repo "$CI_WORKSPACE" --commit "$expected_commit" "$CI_COMMIT_TAG" ``` The module-based workflow follows the ReleaseKit version selected by the tagged @@ -255,6 +259,12 @@ consumer module configuration when adopting a newer ReleaseKit version and verify it with a tag fixture. The command and profile behavior are documented in the [CLI contract](../cli.md) and [configuration reference](../config.md). +Update copied workflow YAML when adopting corrected examples. A dependency +upgrade alone leaves that copy unchanged. An older tagged workflow that passes +a raw annotated tag object ID still fails on retry. Recover delivery from the +original tagged source with the resolved event commit through the command above, +preserving the published tag and existing content. + 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 diff --git a/docs/roadmap/audita-bug-report.md b/docs/roadmap/audita-bug-report.md index a215c71..e74b13e 100644 --- a/docs/roadmap/audita-bug-report.md +++ b/docs/roadmap/audita-bug-report.md @@ -1,6 +1,7 @@ # Audita Bug Report: Annotated Tag IDs in Woodpecker Delivery Examples -**Status:** Reported; upstream correction requested. +**Status:** Remediated in current source; offline race-enabled workflow +verification passed. **Reviewed release:** ReleaseKit v0.4.1, commit `2d5a8110c61e31daf8698766114ae305d0e0e469`. **Affected consumer:** Audita v3.1.0. @@ -107,3 +108,16 @@ receive corrected examples; retrying an existing tag's unchanged faulty workflow will still fail. Existing artifacts can be delivered from the original tagged source using the resolved commit ID through the documented delivery path, without moving the source tag or replacing published contents. + +## ReleaseKit Resolution + +All three maintained workflows independently resolve the event ID to its source +commit, compare it with the selected tag's commit, and pass that resolved commit +to delivery. Mismatch diagnostics show both identities. The CLI and delivery +guides describe event resolution and adoption of corrected workflow copies. + +Regression coverage reproduced the annotated-tag-object failure before the fix. +The affected workflow tests passed afterward with offline module settings and +the race detector. Coverage includes annotated tag-object events, annotated and +lightweight commit events, case-insensitive object IDs, resolved delivery +arguments, unrelated-commit rejection, tagged retries, and source bootstrap. diff --git a/examples/multi-module/woodpecker.yml b/examples/multi-module/woodpecker.yml index 86467bb..dc8e99b 100644 --- a/examples/multi-module/woodpecker.yml +++ b/examples/multi-module/woodpecker.yml @@ -20,8 +20,12 @@ steps: git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}") - expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') - test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" + expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}") + if [ "$${tag_commit}" != "$${expected_commit}" ]; then + printf 'Tag %s resolves to commit %s; event %s resolves to commit %s\n' \ + "$${CI_COMMIT_TAG}" "$${tag_commit}" "$${CI_COMMIT_SHA}" "$${expected_commit}" >&2 + exit 1 + fi git checkout --detach "$${tag_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test -z "$(git status --porcelain --untracked-files=all)" @@ -59,5 +63,5 @@ steps: --config "$${profile_path}" \ --expected-module "$${module_dir}" \ --prepared \ - --commit "$${CI_COMMIT_SHA}" \ + --commit "$${expected_commit}" \ "$${CI_COMMIT_TAG}" diff --git a/examples/woodpecker/releasekit-source-delivery.yml b/examples/woodpecker/releasekit-source-delivery.yml index 0506387..acc097b 100644 --- a/examples/woodpecker/releasekit-source-delivery.yml +++ b/examples/woodpecker/releasekit-source-delivery.yml @@ -15,9 +15,13 @@ steps: test -n "$${CI_COMMIT_SHA}" git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" - tag_commit=$(git rev-parse "refs/tags/$${CI_COMMIT_TAG}^{commit}") - expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') - test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" + tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}") + expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}") + if [ "$${tag_commit}" != "$${expected_commit}" ]; then + printf 'Tag %s resolves to commit %s; event %s resolves to commit %s\n' \ + "$${CI_COMMIT_TAG}" "$${tag_commit}" "$${CI_COMMIT_SHA}" "$${expected_commit}" >&2 + exit 1 + fi git checkout --detach "$${tag_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test -z "$(git status --porcelain --untracked-files=all)" @@ -30,5 +34,5 @@ steps: "$${bootstrap_dir}/releasekit" ci-deliver \ --repo "$${CI_WORKSPACE}" \ --prepared \ - --commit "$${CI_COMMIT_SHA}" \ + --commit "$${expected_commit}" \ "$${CI_COMMIT_TAG}" diff --git a/examples/woodpecker/tag-delivery.yml b/examples/woodpecker/tag-delivery.yml index 728f321..8d99e07 100644 --- a/examples/woodpecker/tag-delivery.yml +++ b/examples/woodpecker/tag-delivery.yml @@ -19,9 +19,13 @@ steps: test -n "$${CI_COMMIT_SHA}" git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git fetch origin "refs/tags/$${CI_COMMIT_TAG}:refs/tags/$${CI_COMMIT_TAG}" - tag_commit=$(git rev-parse "refs/tags/$${CI_COMMIT_TAG}^{commit}") - expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') - test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" + tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}") + expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}") + if [ "$${tag_commit}" != "$${expected_commit}" ]; then + printf 'Tag %s resolves to commit %s; event %s resolves to commit %s\n' \ + "$${CI_COMMIT_TAG}" "$${tag_commit}" "$${CI_COMMIT_SHA}" "$${expected_commit}" >&2 + exit 1 + fi git checkout --detach "$${tag_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test -z "$(git status --porcelain --untracked-files=all)" @@ -46,5 +50,5 @@ steps: "$${bootstrap_dir}/releasekit" ci-deliver \ --repo "$${CI_WORKSPACE}" \ --prepared \ - --commit "$${CI_COMMIT_SHA}" \ + --commit "$${expected_commit}" \ "$${CI_COMMIT_TAG}"