Resolve annotated tag events before CI delivery

This commit is contained in:
2026-10-11 01:51:44 +00:00
parent 2d41748006
commit 7cd5edd650
3 changed files with 111 additions and 5 deletions

View File

@@ -20,7 +20,7 @@ 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 "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}"
git checkout --detach "$${tag_commit}"
test "$(git rev-parse HEAD | tr '[:upper:]' '[:lower:]')" = "$${expected_commit}"
@@ -46,5 +46,5 @@ steps:
"$${bootstrap_dir}/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \
--prepared \
--commit "$${CI_COMMIT_SHA}" \
--commit "$${expected_commit}" \
"$${CI_COMMIT_TAG}"

View File

@@ -104,7 +104,9 @@ Woodpecker runs ReleaseKit's `ci-deliver` command for tags under
- `SHA256SUMS`
Before running Go, the workflow fetches and verifies the event tag, checks it
out detached, and confirms the source tree is clean. It copies the tagged
out detached, and confirms the source tree is clean. It peels the event object
ID to its commit before comparing it with the tag's commit, accepting both
annotated tag objects and commit IDs from Woodpecker. It copies the tagged
`go.mod` and `go.sum` into temporary module metadata outside the checkout. Go
confirms that this configuration selects ReleaseKit, then builds its CLI using
an alternate `-modfile`. The bootstrap executable and temporary files remain
@@ -120,7 +122,7 @@ The resulting executable receives:
"$${bootstrap_dir}/releasekit" ci-deliver \
--repo "$${CI_WORKSPACE}" \
--prepared \
--commit "$${CI_COMMIT_SHA}" \
--commit "$${expected_commit}" \
"$${CI_COMMIT_TAG}"
```

View File

@@ -118,7 +118,7 @@ func TestDeliveryWorkflowUsesModuleSelectedReleaseKit(t *testing.T) {
"git fetch origin", "refs/tags/$${CI_COMMIT_TAG}",
"git checkout --detach", "git status --porcelain",
"go list", "-modfile=", "-m", "go build", "$${releasekit_module}/cmd/releasekit",
"ci-deliver", "--repo \"$${CI_WORKSPACE}\"", "--prepared", "--commit \"$${CI_COMMIT_SHA}\"",
"ci-deliver", "--repo \"$${CI_WORKSPACE}\"", "--prepared", "--commit",
"\"$${CI_COMMIT_TAG}\"",
} {
if !strings.Contains(command, fragment) {
@@ -160,6 +160,110 @@ func TestReleaseWrapperStopsWhenBootstrapFails(t *testing.T) {
}
}
func TestDeliveryWorkflowAcceptsTagObjectsAndCommits(t *testing.T) {
var workflow struct {
Steps []struct {
Commands []string `yaml:"commands"`
} `yaml:"steps"`
}
if err := yaml.Unmarshal([]byte(readText(t, "../.woodpecker/release.yml")), &workflow); err != nil {
t.Fatal(err)
}
command := strings.ReplaceAll(workflow.Steps[0].Commands[0], "$${", "${")
root := t.TempDir()
source := filepath.Join(root, "source")
layout := releaseTestLayout{
repo: filepath.Join(root, "checkout"), path: filepath.Join(root, "bin"),
argsLog: filepath.Join(root, "tool-arguments"),
}
for _, directory := range []string{source, layout.path} {
if err := os.MkdirAll(directory, 0755); err != nil {
t.Fatal(err)
}
}
git := func(dir string, args ...string) string {
t.Helper()
cmd := exec.Command("git", args...)
cmd.Dir = dir
output, err := cmd.CombinedOutput()
if err != nil {
t.Fatalf("git %q: %v\n%s", args, err, output)
}
return strings.TrimSpace(string(output))
}
git(source, "init", "-b", "main")
git(source, "config", "user.name", "Release test")
git(source, "config", "user.email", "release-test@example.invalid")
writeReleaseTestFile(t, filepath.Join(source, "go.mod"), []byte(readText(t, "../go.mod")), 0644)
writeReleaseTestFile(t, filepath.Join(source, "go.sum"), nil, 0644)
git(source, "add", "go.mod", "go.sum")
git(source, "commit", "-m", "Initial fixture")
unrelated := git(source, "rev-parse", "HEAD")
git(source, "commit", "--allow-empty", "-m", "Release fixture")
commit := git(source, "rev-parse", "HEAD")
git(source, "tag", "-a", "v3.1.0", "-m", "Annotated release")
git(source, "tag", "v3.1.1")
tagObject := git(source, "rev-parse", "v3.1.0")
git(root, "clone", "--quiet", source, layout.repo)
toolStub := filepath.Join(root, "releasekit-stub")
writeReleaseTestFile(t, toolStub, []byte(`#!/bin/sh
printf '%s\0' "$@" > "$RELEASEKIT_ARGS_LOG"
`), 0755)
writeReleaseTestFile(t, filepath.Join(layout.path, "go"), []byte(`#!/bin/sh
set -eu
case "$1" in
list) exit 0 ;;
build)
while [ "$#" -gt 0 ]; do
if [ "$1" = "-o" ]; then
cp "$RELEASEKIT_STUB" "$2"
exit 0
fi
shift
done ;;
esac
exit 1
`), 0755)
for _, tc := range []struct {
name, tag, eventID string
success bool
}{
{"annotated tag object", "v3.1.0", tagObject, true},
{"annotated tag commit", "v3.1.0", commit, true},
{"lightweight tag commit", "v3.1.1", commit, true},
{"unrelated commit", "v3.1.0", unrelated, false},
} {
t.Run(tc.name, func(t *testing.T) {
if err := os.Remove(layout.argsLog); err != nil && !os.IsNotExist(err) {
t.Fatal(err)
}
cmd := exec.Command("sh", "-c", command)
cmd.Dir = layout.repo
cmd.Env = layout.environment(map[string]string{
"CI_WORKSPACE": layout.repo, "CI_COMMIT_TAG": tc.tag, "CI_COMMIT_SHA": tc.eventID,
"RELEASEKIT_ARGS_LOG": layout.argsLog, "RELEASEKIT_STUB": toolStub,
})
output, err := cmd.CombinedOutput()
if tc.success {
if err != nil {
t.Fatalf("workflow failed: %v\n%s", err, output)
}
want := []string{"ci-deliver", "--repo", layout.repo, "--prepared", "--commit", commit, tc.tag}
if got := readNULArguments(t, layout.argsLog); !reflect.DeepEqual(got, want) {
t.Fatalf("delivery arguments = %q, want %q", got, want)
}
} else {
if err == nil {
t.Fatal("workflow accepted an unrelated commit")
}
if _, err := os.Stat(layout.argsLog); !os.IsNotExist(err) {
t.Fatalf("delivery ran after mismatched event; stat error = %v", err)
}
}
})
}
}
func TestReleaseKitProfileDeclaresProductAndDeliverySettings(t *testing.T) {
data, err := os.ReadFile("../releasekit.json")
if err != nil {