Resolve annotated tag events in Woodpecker delivery workflows

This commit is contained in:
2026-10-11 02:07:54 +00:00
parent 3f2a1a1052
commit f076da99b6
9 changed files with 111 additions and 44 deletions

View File

@@ -51,6 +51,7 @@ func TestMultiModuleWoodpeckerDelivery(t *testing.T) {
commit := tagAndPushWorkflowRepository(t, repository, tag) commit := tagAndPushWorkflowRepository(t, repository, tag)
delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit }) delivery.update(func(delivery *mappedDeliveryData) { delivery.commit = commit })
env := multiModuleWorkflowEnvironment(t, repository, commit, tag, toolBinary, proxy) 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") { 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) 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, "add", "--all")
cliGitRun(t, repository, "commit", "-m", "prepare mapped delivery") cliGitRun(t, repository, "commit", "-m", "prepare mapped delivery")
commit := cliGitOutput(t, repository, "rev-parse", "HEAD") 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") remote := filepath.Join(filepath.Dir(repository), "origin.git")
cliGitRun(t, repository, "init", "--bare", remote) cliGitRun(t, repository, "init", "--bare", remote)
cliGitRun(t, repository, "remote", "add", "origin", remote) cliGitRun(t, repository, "remote", "add", "origin", remote)

View File

@@ -78,7 +78,7 @@ func TestReleaseKitSourceDeliveryWorkflowFromTaggedCheckout(t *testing.T) {
} }
env := map[string]string{ env := map[string]string{
"CI_WORKSPACE": repository, "CI_WORKSPACE": repository,
"CI_COMMIT_SHA": state.snapshot().commit, "CI_COMMIT_SHA": cliGitOutput(t, repository, "rev-parse", "refs/tags/"+version),
"CI_COMMIT_TAG": version, "CI_COMMIT_TAG": version,
"RELEASEKIT_GITEA_TOKEN": "fixture-token", "RELEASEKIT_GITEA_TOKEN": "fixture-token",
"GOTOOLCHAIN": "local", "GOTOOLCHAIN": "local",

View File

@@ -104,10 +104,14 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
repository := newCLIRepository(t) repository := newCLIRepository(t)
selectReleaseKitModule(t, repository, root, false) selectReleaseKitModule(t, repository, root, false)
commit := pushTaggedCLIRepository(t, repository) 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) 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 { if output, err := executeWoodpeckerBlock(command, repository, env); err == nil {
t.Fatalf("wrong tag succeeded: %s", output) 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) assertNoCommands(t, goLog)
}) })
@@ -126,12 +130,30 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
}) })
t.Run("selected executable receives event arguments and its status", func(t *testing.T) { t.Run("selected executable receives event arguments and its status", func(t *testing.T) {
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) repository := newCLIRepository(t)
selectReleaseKitModule(t, repository, root, false) selectReleaseKitModule(t, repository, root, false)
commit := pushTaggedCLIRepository(t, repository) 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) goDir, goLog := installControlledGoBootstrapShim(t)
toolArgs := filepath.Join(t.TempDir(), "tool-arguments") toolArgs := filepath.Join(t.TempDir(), "tool-arguments")
env := templateEnvironment(repository, strings.ToUpper(commit), goDir, goLog) env := templateEnvironment(repository, strings.ToUpper(eventID), goDir, goLog)
env["CI_COMMIT_TAG"] = event.tag
env["RELEASEKIT_TOOL_ARGS_LOG"] = toolArgs env["RELEASEKIT_TOOL_ARGS_LOG"] = toolArgs
env["RELEASEKIT_TOOL_EXIT"] = "37" env["RELEASEKIT_TOOL_EXIT"] = "37"
output, err := executeWoodpeckerBlock(command, repository, env) output, err := executeWoodpeckerBlock(command, repository, env)
@@ -140,14 +162,19 @@ func TestWoodpeckerTagDeliveryTemplate(t *testing.T) {
t.Fatalf("template error = %v, want propagated exit 37; output=%q", err, output) t.Fatalf("template error = %v, want propagated exit 37; output=%q", err, output)
} }
args := readWoodpeckerFixtureFile(t, toolArgs) args := readWoodpeckerFixtureFile(t, toolArgs)
for _, expected := range []string{"<ci-deliver>", "<--repo>", "<" + repository + ">", "<--prepared>", "<--commit>", "<" + strings.ToUpper(commit) + ">", "<v1.2.3>"} { for _, expected := range []string{"<ci-deliver>", "<--repo>", "<" + repository + ">", "<--prepared>", "<" + event.tag + ">"} {
if !strings.Contains(args, expected) { if !strings.Contains(args, expected) {
t.Errorf("selected executable arguments %q omit %q", 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) assertGoSelection(t, repository, goLog)
}) })
} }
})
}
func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) { func TestWoodpeckerTagDeliveryUsesTaggedModuleVersion(t *testing.T) {
// Synthetic downloads must leave the caller's module cache untouched. // Synthetic downloads must leave the caller's module cache untouched.

View File

@@ -241,8 +241,11 @@ changes the selected checkout; `--prepared` reuses a verified checkout.
Ordinary `deliver` continues to operate on already prepared source without Ordinary `deliver` continues to operate on already prepared source without
fetching, checking out, or downloading dependencies. fetching, checking out, or downloading dependencies.
In Woodpecker, pass `CI_WORKSPACE`, `CI_COMMIT_SHA`, and `CI_COMMIT_TAG` as the In Woodpecker, pass `CI_WORKSPACE`, the source commit resolved from
repository, expected commit, and full tag arguments. The CLI does not read `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 provider-specific environment variable names. Checkout and dependency
preparation failures return status 1; usage, profile, credential, and invalid preparation failures return status 1; usage, profile, credential, and invalid
version errors return status 2. Final delivery uses the same statuses as version errors return status 2. Final delivery uses the same statuses as

View File

@@ -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 profile's build target list selects whether application binaries are built and
delivered. delivered.
Before running Go, the template fetches the explicit event tag, compares its Before running Go, the template fetches the explicit event tag and independently
peeled commit with the provider's full commit ID without regard to hex case, resolves both the tag and `CI_COMMIT_SHA` to source commits. The event ID may
checks out that commit detached, and confirms the source tree is clean. It then identify an annotated tag object or a commit. The template requires the resolved
copies the tagged `go.mod` and any `go.sum` to a temporary directory outside commits to match, checks out that commit detached, and confirms the source tree
the checkout. `go list -m` confirms that the module selects ReleaseKit; Go uses 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 an alternate `-modfile` to prepare dependencies and build the selected
`cmd/releasekit` package without changing the tagged module files. The template `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. `CI_COMMIT_TAG`. A cleanup trap removes the temporary metadata and executable.
Tool bootstrap and application artifact preparation are separate. The bootstrap 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 as deployment configuration when they help the runner; they are not needed by
the template. A separately installed executable can be installed with the template. A separately installed executable can be installed with
`go install gitea.maximumdirect.net/eric/ReleaseKit/cmd/releasekit@PIN`, then run `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 ```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 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 verify it with a tag fixture. The command and profile behavior are documented
in the [CLI contract](../cli.md) and [configuration reference](../config.md). 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 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
failure does not undo source publication. A failed, partial, or uncertain run failure does not undo source publication. A failed, partial, or uncertain run

View File

@@ -1,6 +1,7 @@
# Audita Bug Report: Annotated Tag IDs in Woodpecker Delivery Examples # 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 **Reviewed release:** ReleaseKit v0.4.1, commit
`2d5a8110c61e31daf8698766114ae305d0e0e469`. `2d5a8110c61e31daf8698766114ae305d0e0e469`.
**Affected consumer:** Audita v3.1.0. **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 will still fail. Existing artifacts can be delivered from the original tagged
source using the resolved commit ID through the documented delivery path, source using the resolved commit ID through the documented delivery path,
without moving the source tag or replacing published contents. 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.

View File

@@ -20,8 +20,12 @@ steps:
git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git check-ref-format "refs/tags/$${CI_COMMIT_TAG}"
git fetch origin "refs/tags/$${CI_COMMIT_TAG}: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}") tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}")
expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}")
test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_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}" git checkout --detach "$${tag_commit}"
test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
test -z "$(git status --porcelain --untracked-files=all)" test -z "$(git status --porcelain --untracked-files=all)"
@@ -59,5 +63,5 @@ steps:
--config "$${profile_path}" \ --config "$${profile_path}" \
--expected-module "$${module_dir}" \ --expected-module "$${module_dir}" \
--prepared \ --prepared \
--commit "$${CI_COMMIT_SHA}" \ --commit "$${expected_commit}" \
"$${CI_COMMIT_TAG}" "$${CI_COMMIT_TAG}"

View File

@@ -15,9 +15,13 @@ steps:
test -n "$${CI_COMMIT_SHA}" test -n "$${CI_COMMIT_SHA}"
git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git check-ref-format "refs/tags/$${CI_COMMIT_TAG}"
git fetch origin "refs/tags/$${CI_COMMIT_TAG}: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}") tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}")
expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}")
test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_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}" git checkout --detach "$${tag_commit}"
test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
test -z "$(git status --porcelain --untracked-files=all)" test -z "$(git status --porcelain --untracked-files=all)"
@@ -30,5 +34,5 @@ steps:
"$${bootstrap_dir}/releasekit" ci-deliver \ "$${bootstrap_dir}/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \ --repo "$${CI_WORKSPACE}" \
--prepared \ --prepared \
--commit "$${CI_COMMIT_SHA}" \ --commit "$${expected_commit}" \
"$${CI_COMMIT_TAG}" "$${CI_COMMIT_TAG}"

View File

@@ -19,9 +19,13 @@ steps:
test -n "$${CI_COMMIT_SHA}" test -n "$${CI_COMMIT_SHA}"
git check-ref-format "refs/tags/$${CI_COMMIT_TAG}" git check-ref-format "refs/tags/$${CI_COMMIT_TAG}"
git fetch origin "refs/tags/$${CI_COMMIT_TAG}: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}") tag_commit=$(git rev-parse --verify "refs/tags/$${CI_COMMIT_TAG}^{commit}")
expected_commit=$(printf '%s' "$${CI_COMMIT_SHA}" | tr '[:upper:]' '[:lower:]') expected_commit=$(git rev-parse --verify "$${CI_COMMIT_SHA}^{commit}")
test "$(printf '%s' "$${tag_commit}" | tr '[:upper:]' '[:lower:]')" = "$${expected_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}" git checkout --detach "$${tag_commit}"
test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}" test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
test -z "$(git status --porcelain --untracked-files=all)" test -z "$(git status --porcelain --untracked-files=all)"
@@ -46,5 +50,5 @@ steps:
"$${bootstrap_dir}/releasekit" ci-deliver \ "$${bootstrap_dir}/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \ --repo "$${CI_WORKSPACE}" \
--prepared \ --prepared \
--commit "$${CI_COMMIT_SHA}" \ --commit "$${expected_commit}" \
"$${CI_COMMIT_TAG}" "$${CI_COMMIT_TAG}"