3 Commits

9 changed files with 132 additions and 43 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,26 +130,49 @@ 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) {
repository := newCLIRepository(t) for _, event := range []struct {
selectReleaseKitModule(t, repository, root, false) name, tag string
commit := pushTaggedCLIRepository(t, repository) tagObject bool
goDir, goLog := installControlledGoBootstrapShim(t) }{
toolArgs := filepath.Join(t.TempDir(), "tool-arguments") {"lightweight commit", "v1.2.3", false},
env := templateEnvironment(repository, strings.ToUpper(commit), goDir, goLog) {"annotated commit", "v1.2.4", false},
env["RELEASEKIT_TOOL_ARGS_LOG"] = toolArgs {"annotated tag object", "v1.2.4", true},
env["RELEASEKIT_TOOL_EXIT"] = "37" } {
output, err := executeWoodpeckerBlock(command, repository, env) t.Run(event.name, func(t *testing.T) {
var exitError *exec.ExitError repository := newCLIRepository(t)
if !errors.As(err, &exitError) || exitError.ExitCode() != 37 { selectReleaseKitModule(t, repository, root, false)
t.Fatalf("template error = %v, want propagated exit 37; output=%q", err, output) 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{"<ci-deliver>", "<--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{"<ci-deliver>", "<--repo>", "<" + repository + ">", "<--prepared>", "<--commit>", "<" + strings.ToUpper(commit) + ">", "<v1.2.3>"} {
if !strings.Contains(args, expected) {
t.Errorf("selected executable arguments %q omit %q", args, expected)
}
}
assertGoSelection(t, repository, goLog)
}) })
} }

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

36
docs/releases/v0.4.2.md Normal file
View File

@@ -0,0 +1,36 @@
# ReleaseKit v0.4.2
## Summary
This patch corrects Woodpecker delivery bootstrap for events whose
`CI_COMMIT_SHA` identifies an annotated tag object. The maintained workflows
resolve the event object to its source commit before verification and delivery.
## Changes
- Correct module-selected, multi-module, and ReleaseKit source delivery
workflows to resolve event and tag commits independently and pass the
resolved event commit to `ci-deliver`.
- Report both commit identities when an event and its selected tag disagree.
- Extend executable-workflow regression coverage for annotated tag-object
events, annotated and lightweight commit events, resolved delivery arguments,
and rejection of unrelated commits.
## Compatibility and upgrade guidance
Public APIs, CLI contracts, configuration formats, and the Go 1.26.0 consumer
minimum retain their v0.4 definitions. `ci-deliver --commit` continues to accept
the expected source commit ID.
Update copied workflow YAML from the maintained examples when adopting this
patch. Upgrading the ReleaseKit dependency alone leaves copied workflows
unchanged. The [delivery guide](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.2/docs/consumers/delivery.md#woodpecker-tag-workflows)
describes event resolution, workflow adoption, and delivery recovery.
Existing published tags and contents remain unchanged. Recover a failed older
workflow from its original tagged source using the resolved event commit through
the documented delivery command, preserving existing hosted content.
[COPYRIGHT](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.2/COPYRIGHT) |
[LICENSE](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.2/LICENSE) |
[ATTRIBUTION](https://gitea.maximumdirect.net/eric/ReleaseKit/src/tag/v0.4.2/docs/oss/attribution.md)

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