From 50d8cec4ac0062ae5de274fc13f809395b5747d3 Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 18 Sep 2026 15:41:25 +0000 Subject: [PATCH 1/2] =?UTF-8?q?=F0=9F=A4=96=20ci:=20prepare=20local=20main?= =?UTF-8?q?=20tag=20for=20GoReleaser=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Create the synthetic main tag only in the CI checkout and skip its GitHub changelog. Keep tagged releases unchanged. The historical GoReleaser version rejects the missing local tag; GitHub cannot compare that local-only tag. Cover both channel boundaries with focused regression tests. Signed-off-by: Thomas Kosiewski --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh`_ Change-Id: I8bc997ac716a5082b9d5f15f1a4d9f833d7ef5a2 --- .github/workflows/ci.yaml | 5 ++ .goreleaser.yaml | 2 + go.mod | 2 +- hack/main_publish_test.go | 131 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 139 insertions(+), 1 deletion(-) create mode 100644 hack/main_publish_test.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index c827cfbd..d28693cb 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -497,6 +497,11 @@ jobs: username: ${{ github.actor }} password: ${{ secrets.GITHUB_TOKEN }} + - name: Prepare local main tag + id: prepare-main-tag + # GoReleaser validates that its current tag points at HEAD. Never push this tag. + run: git tag --force v0.0.0-main HEAD + - name: Run GoReleaser (:main) uses: goreleaser/goreleaser-action@e435ccd777264be153ace6237001ef4d979d3a7a # v6.4.0 with: diff --git a/.goreleaser.yaml b/.goreleaser.yaml index 661f4575..a472ad7d 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -90,4 +90,6 @@ dockers_v2: - "--provenance=false" changelog: + # The main channel's synthetic tag exists only in the CI checkout. + disable: '{{ eq .Env.GORELEASER_CHANNEL "main" }}' use: github diff --git a/go.mod b/go.mod index 14ffa42e..56a41e0d 100644 --- a/go.mod +++ b/go.mod @@ -18,6 +18,7 @@ require ( k8s.io/kube-openapi v0.0.0-20260127142750-a19766b6e2d4 sigs.k8s.io/controller-runtime v0.23.1 sigs.k8s.io/gateway-api v1.4.1 + sigs.k8s.io/yaml v1.6.0 ) tool ( @@ -412,7 +413,6 @@ require ( sigs.k8s.io/json v0.0.0-20250730193827-2d320260d730 // indirect sigs.k8s.io/randfill v1.0.0 // indirect sigs.k8s.io/structured-merge-diff/v6 v6.3.2-0.20260122202528-d9cc6641c482 // indirect - sigs.k8s.io/yaml v1.6.0 // indirect storj.io/drpc v0.0.34 // indirect tailscale.com v1.80.3 // indirect ) diff --git a/hack/main_publish_test.go b/hack/main_publish_test.go new file mode 100644 index 00000000..596cc95e --- /dev/null +++ b/hack/main_publish_test.go @@ -0,0 +1,131 @@ +package hack_test + +import ( + "os" + "os/exec" + "strings" + "testing" + "text/template" + + "sigs.k8s.io/yaml" +) + +func TestChangelogChannels(t *testing.T) { + data, err := os.ReadFile("../.goreleaser.yaml") + if err != nil { + t.Fatal(err) + } + var config struct { + Changelog struct { + Disable string `json:"disable"` + Use string `json:"use"` + } `json:"changelog"` + } + if err := yaml.Unmarshal(data, &config); err != nil { + t.Fatal(err) + } + if config.Changelog.Use != "github" { + t.Fatal("tagged releases must retain GitHub changelogs") + } + tmpl, err := template.New("disable").Parse(config.Changelog.Disable) + if err != nil { + t.Fatal(err) + } + for _, channel := range []string{"main", "", "release"} { + t.Run("channel="+channel, func(t *testing.T) { + var result strings.Builder + data := struct{ Env map[string]string }{Env: map[string]string{"GORELEASER_CHANNEL": channel}} + if err := tmpl.Execute(&result, data); err != nil { + t.Fatal(err) + } + disabled := result.String() == "true" + if disabled != (channel == "main") { + t.Fatalf("changelog disabled = %q for channel %q", result.String(), channel) + } + }) + } +} + +func TestMainPublishTag(t *testing.T) { + data, err := os.ReadFile("../.github/workflows/ci.yaml") + if err != nil { + t.Fatal(err) + } + var workflow struct { + Jobs map[string]struct { + Steps []struct { + ID string `json:"id"` + Run string `json:"run"` + Uses string `json:"uses"` + Env map[string]string `json:"env"` + } `json:"steps"` + } `json:"jobs"` + } + if err := yaml.Unmarshal(data, &workflow); err != nil { + t.Fatal(err) + } + + var prepare, tag string + prepareIndex, releaseIndex := -1, -1 + for i, step := range workflow.Jobs["publish-main"].Steps { + if step.ID == "prepare-main-tag" { + prepare, prepareIndex = step.Run, i + } + if strings.HasPrefix(step.Uses, "goreleaser/goreleaser-action@") { + tag, releaseIndex = step.Env["GORELEASER_CURRENT_TAG"], i + } + } + if tag == "" || releaseIndex < 0 { + t.Fatal("main publisher must select a GoReleaser tag") + } + if prepareIndex >= releaseIndex { + t.Fatal("main tag must be prepared before GoReleaser") + } + + for _, stale := range []bool{false, true} { + name := "absent" + if stale { + name = "stale" + } + t.Run(name, func(t *testing.T) { + dir := t.TempDir() + run := func(program string, args ...string) string { + t.Helper() + cmd := exec.CommandContext(t.Context(), program, args...) + cmd.Dir = dir + cmd.Env = []string{ + "PATH=" + os.Getenv("PATH"), "HOME=" + dir, + "GIT_CONFIG_NOSYSTEM=1", "GIT_CONFIG_GLOBAL=" + os.DevNull, + } + out, err := cmd.CombinedOutput() + if err != nil { + t.Fatalf("%s %v: %v\n%s", program, args, err, out) + } + return strings.TrimSpace(string(out)) + } + run("git", "init", "--initial-branch=main") + run("git", "config", "user.name", "Test") + run("git", "config", "user.email", "test@example.com") + run("git", "commit", "--allow-empty", "-m", "release") + releaseCommit := run("git", "rev-parse", "HEAD") + run("git", "tag", "v1.0.0") + if stale { + run("git", "tag", tag) + } + run("git", "commit", "--allow-empty", "-m", "next main") + + // Execute only the local-tag step, never the publisher or login steps. + run("bash", "-euo", "pipefail", "-c", prepare) + // This is GoReleaser's non-snapshot tag validation predicate. + if got := run("git", "describe", "--exact-match", "--tags", "--match", tag); got != tag { + t.Fatalf("main tag = %q, want %q", got, tag) + } + if got := run("git", "rev-parse", "v1.0.0"); got != releaseCommit { + t.Fatalf("release tag moved from %s to %s", releaseCommit, got) + } + if got := run("git", "status", "--porcelain"); got != "" { + t.Fatalf("tag preparation dirtied the worktree: %s", got) + } + }) + } +} From 8aa69887a61ccca0d21964eebc98d552c0a764bc Mon Sep 17 00:00:00 2001 From: Thomas Kosiewski Date: Fri, 18 Sep 2026 15:54:27 +0000 Subject: [PATCH 2/2] =?UTF-8?q?=F0=9F=A4=96=20ci:=20preserve=20changelogs?= =?UTF-8?q?=20when=20the=20channel=20is=20unset=20(#98)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Use a missing-key-safe lookup for the main-only changelog condition. Match GoReleaser strict template evaluation in the regression test. Signed-off-by: Thomas Kosiewski --- _Generated with [`xum`](https://github.com/coder/xum) • Model: `coder:openai/gpt-6-astra` • Thinking: `xhigh`_ Change-Id: I699b7faf0a94b10c6bc6a2a1351bf56e36e4f856 --- .goreleaser.yaml | 2 +- hack/main_publish_test.go | 10 +++++++--- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/.goreleaser.yaml b/.goreleaser.yaml index a472ad7d..c31f8064 100644 --- a/.goreleaser.yaml +++ b/.goreleaser.yaml @@ -91,5 +91,5 @@ dockers_v2: changelog: # The main channel's synthetic tag exists only in the CI checkout. - disable: '{{ eq .Env.GORELEASER_CHANNEL "main" }}' + disable: '{{ eq (index .Env "GORELEASER_CHANNEL") "main" }}' use: github diff --git a/hack/main_publish_test.go b/hack/main_publish_test.go index 596cc95e..264760da 100644 --- a/hack/main_publish_test.go +++ b/hack/main_publish_test.go @@ -27,14 +27,18 @@ func TestChangelogChannels(t *testing.T) { if config.Changelog.Use != "github" { t.Fatal("tagged releases must retain GitHub changelogs") } - tmpl, err := template.New("disable").Parse(config.Changelog.Disable) + tmpl, err := template.New("disable").Option("missingkey=error").Parse(config.Changelog.Disable) if err != nil { t.Fatal(err) } - for _, channel := range []string{"main", "", "release"} { + for _, channel := range []string{"main", "", "release", "unset"} { t.Run("channel="+channel, func(t *testing.T) { var result strings.Builder - data := struct{ Env map[string]string }{Env: map[string]string{"GORELEASER_CHANNEL": channel}} + env := map[string]string{} + if channel != "unset" { + env["GORELEASER_CHANNEL"] = channel + } + data := struct{ Env map[string]string }{Env: env} if err := tmpl.Execute(&result, data); err != nil { t.Fatal(err) }