From 9df5e479133960b7151c2d8556ee992e456e67fd Mon Sep 17 00:00:00 2001 From: Chris Bargmann Date: Wed, 12 Aug 2026 13:44:28 +0200 Subject: [PATCH 1/4] feat(gen): derive go-github import path from go.mod --- gen/gomod.go | 32 ++++++++++++++++++++++++++++++++ gen/gomod_test.go | 43 +++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 75 insertions(+) create mode 100644 gen/gomod.go create mode 100644 gen/gomod_test.go diff --git a/gen/gomod.go b/gen/gomod.go new file mode 100644 index 00000000..352481b1 --- /dev/null +++ b/gen/gomod.go @@ -0,0 +1,32 @@ +package main + +import ( + "fmt" + "os" + "regexp" +) + +var goGithubModuleRE = regexp.MustCompile(`github\.com/google/go-github/v\d+`) + +// goGithubImportPath reads gomodPath and returns the go-github package import +// path (module path + "/github") derived from the module's require directive. +// This keeps the go-github major version defined in exactly one place. +func goGithubImportPath(gomodPath string) (string, error) { + data, err := os.ReadFile(gomodPath) + if err != nil { + return "", fmt.Errorf("read %s: %w", gomodPath, err) + } + matches := goGithubModuleRE.FindAllString(string(data), -1) + uniq := map[string]struct{}{} + for _, m := range matches { + uniq[m] = struct{}{} + } + switch len(uniq) { + case 0: + return "", fmt.Errorf("no github.com/google/go-github require found in %s", gomodPath) + case 1: + return matches[0] + "/github", nil + default: + return "", fmt.Errorf("multiple go-github module versions found in %s: %v", gomodPath, matches) + } +} diff --git a/gen/gomod_test.go b/gen/gomod_test.go new file mode 100644 index 00000000..bdc1357c --- /dev/null +++ b/gen/gomod_test.go @@ -0,0 +1,43 @@ +package main + +import ( + "os" + "path/filepath" + "testing" +) + +func writeGoMod(t *testing.T, content string) string { + t.Helper() + dir := t.TempDir() + p := filepath.Join(dir, "go.mod") + if err := os.WriteFile(p, []byte(content), 0o644); err != nil { + t.Fatal(err) + } + return p +} + +func TestGoGithubImportPath(t *testing.T) { + p := writeGoMod(t, `module github.com/cbrgm/githubevents/v2 + +go 1.25.0 + +require ( + github.com/google/go-github/v89 v89.0.0 + golang.org/x/sync v0.22.0 +) +`) + got, err := goGithubImportPath(p) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if want := "github.com/google/go-github/v89/github"; got != want { + t.Fatalf("got %q, want %q", got, want) + } +} + +func TestGoGithubImportPathMissing(t *testing.T) { + p := writeGoMod(t, "module x\n\ngo 1.25.0\n") + if _, err := goGithubImportPath(p); err == nil { + t.Fatal("expected error when no go-github require present") + } +} From 9481b59e67f163d18f02baeb2cdd523b83cadaaf Mon Sep 17 00:00:00 2001 From: Chris Bargmann Date: Wed, 12 Aug 2026 13:47:01 +0200 Subject: [PATCH 2/4] test(gen): cover multiple go-github versions error path --- gen/gomod.go | 8 +++++++- gen/gomod_test.go | 15 +++++++++++++++ 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/gen/gomod.go b/gen/gomod.go index 352481b1..fb833786 100644 --- a/gen/gomod.go +++ b/gen/gomod.go @@ -4,6 +4,7 @@ import ( "fmt" "os" "regexp" + "sort" ) var goGithubModuleRE = regexp.MustCompile(`github\.com/google/go-github/v\d+`) @@ -27,6 +28,11 @@ func goGithubImportPath(gomodPath string) (string, error) { case 1: return matches[0] + "/github", nil default: - return "", fmt.Errorf("multiple go-github module versions found in %s: %v", gomodPath, matches) + uniqueVersions := make([]string, 0, len(uniq)) + for v := range uniq { + uniqueVersions = append(uniqueVersions, v) + } + sort.Strings(uniqueVersions) + return "", fmt.Errorf("multiple go-github module versions found in %s: %v", gomodPath, uniqueVersions) } } diff --git a/gen/gomod_test.go b/gen/gomod_test.go index bdc1357c..3a5b3d71 100644 --- a/gen/gomod_test.go +++ b/gen/gomod_test.go @@ -41,3 +41,18 @@ func TestGoGithubImportPathMissing(t *testing.T) { t.Fatal("expected error when no go-github require present") } } + +func TestGoGithubImportPathMultiple(t *testing.T) { + p := writeGoMod(t, `module github.com/cbrgm/githubevents/v2 + +go 1.25.0 + +require ( + github.com/google/go-github/v89 v89.0.0 + github.com/google/go-github/v90 v90.0.0 +) +`) + if _, err := goGithubImportPath(p); err == nil { + t.Fatal("expected error when multiple distinct go-github versions present") + } +} From 4220b61e9f408ab294cfa7ce5623894244b38825 Mon Sep 17 00:00:00 2001 From: Chris Bargmann Date: Wed, 12 Aug 2026 13:49:17 +0200 Subject: [PATCH 3/4] refactor(gen): template go-github import from go.mod, drop hardcoded version --- gen/generate.go | 15 ++++++++++----- gen/template_params.go | 4 +++- gen/template_webhook_event.go.tmpl | 2 +- gen/template_webhook_event_tests.go.tmpl | 2 +- gen/template_webhook_event_types.go.tmpl | 2 +- 5 files changed, 16 insertions(+), 9 deletions(-) diff --git a/gen/generate.go b/gen/generate.go index 8b8d9c31..b0734491 100644 --- a/gen/generate.go +++ b/gen/generate.go @@ -41,15 +41,19 @@ func main() { return } - out := filepath.Join(".", *outputDir) - err := os.MkdirAll(out, os.ModePerm) + imp, err := goGithubImportPath("go.mod") if err != nil { + panic(err) + } + params.GoGithubImport = imp + + out := filepath.Join(".", *outputDir) + if err := os.MkdirAll(out, os.ModePerm); err != nil { panic("failed to create output directory") } // create events.go - err = ExecuteWebhookEventTemplate(filepath.Join(out, "events"), params) - if err != nil { + if err := ExecuteWebhookEventTemplate(filepath.Join(out, "events"), params); err != nil { panic(err) } @@ -59,7 +63,8 @@ func main() { fileName := "events_" + param.Name outFile := filepath.Join(out, fileName) err := ExecuteWebhookEventTypesTemplate(outFile, TemplateParameters{ - Webhooks: []GithubWebhooks{param}, + GoGithubImport: imp, + Webhooks: []GithubWebhooks{param}, }) if err != nil { panic(err) diff --git a/gen/template_params.go b/gen/template_params.go index 87a505e0..bd873ed3 100644 --- a/gen/template_params.go +++ b/gen/template_params.go @@ -2,7 +2,9 @@ package main // TemplateParameters represents template parameters. type TemplateParameters struct { - Webhooks []GithubWebhooks + // GoGithubImport is the go-github package import path, derived from go.mod. + GoGithubImport string + Webhooks []GithubWebhooks } // GithubWebhooks represents a Github webhook event type parameters. diff --git a/gen/template_webhook_event.go.tmpl b/gen/template_webhook_event.go.tmpl index b41d16a6..72362875 100644 --- a/gen/template_webhook_event.go.tmpl +++ b/gen/template_webhook_event.go.tmpl @@ -10,7 +10,7 @@ package githubevents import ( "context" "fmt" - "github.com/google/go-github/v89/github" + "{{ .GoGithubImport }}" "golang.org/x/sync/errgroup" "net/http" "sync" diff --git a/gen/template_webhook_event_tests.go.tmpl b/gen/template_webhook_event_tests.go.tmpl index 17531cd1..56d702eb 100644 --- a/gen/template_webhook_event_tests.go.tmpl +++ b/gen/template_webhook_event_tests.go.tmpl @@ -10,7 +10,7 @@ package githubevents import ( "context" "errors" - "github.com/google/go-github/v89/github" + "{{ .GoGithubImport }}" "testing" "sync" ) diff --git a/gen/template_webhook_event_types.go.tmpl b/gen/template_webhook_event_types.go.tmpl index e360ce39..11992675 100644 --- a/gen/template_webhook_event_types.go.tmpl +++ b/gen/template_webhook_event_types.go.tmpl @@ -10,7 +10,7 @@ package githubevents import ( "context" "fmt" - "github.com/google/go-github/v89/github" + "{{ .GoGithubImport }}" "golang.org/x/sync/errgroup" ) From d2bc033e54fb8d89bd4881f879cc8886ac70b95d Mon Sep 17 00:00:00 2001 From: Chris Bargmann Date: Wed, 12 Aug 2026 13:41:23 +0200 Subject: [PATCH 4/4] ci(gen): add apidiff exported-API gate --- .github/workflows/go.yaml | 10 ++++++++++ Makefile | 11 +++++++++++ scripts/apidiff.sh | 24 ++++++++++++++++++++++++ 3 files changed, 45 insertions(+) create mode 100755 scripts/apidiff.sh diff --git a/.github/workflows/go.yaml b/.github/workflows/go.yaml index cfd2755c..21e9b5c1 100644 --- a/.github/workflows/go.yaml +++ b/.github/workflows/go.yaml @@ -10,6 +10,8 @@ jobs: steps: - name: checkout uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + fetch-depth: 0 - name: install Go uses: actions/setup-go@b7ad1dad31e06c5925ef5d2fc7ad053ef454303e # v7.0.0 @@ -19,6 +21,11 @@ jobs: - name: get modules run: go mod vendor + - name: install tools + run: | + make tools + echo "$(go env GOPATH)/bin" >> "$GITHUB_PATH" + - name: generate run: make generate @@ -32,3 +39,6 @@ jobs: - name: run tests run: make test + + - name: apidiff + run: make apidiff diff --git a/Makefile b/Makefile index c86ffb70..fb2ddf45 100644 --- a/Makefile +++ b/Makefile @@ -1,9 +1,20 @@ PACKAGES = $(shell go list ./...) GO := CGO_ENABLED=0 go +GOFUMPT_VERSION := v0.7.0 +APIDIFF_VERSION := v0.0.0-20260811152304-ee035b5b010f .PHONY: all all: build +.PHONY: tools +tools: + $(GO) install mvdan.cc/gofumpt@$(GOFUMPT_VERSION) + $(GO) install golang.org/x/exp/cmd/apidiff@$(APIDIFF_VERSION) + +.PHONY: apidiff +apidiff: + ./scripts/apidiff.sh + .PHONY: clean clean: $(GO) clean -i ./... diff --git a/scripts/apidiff.sh b/scripts/apidiff.sh new file mode 100755 index 00000000..6524a821 --- /dev/null +++ b/scripts/apidiff.sh @@ -0,0 +1,24 @@ +#!/usr/bin/env bash +# Fail if the exported API of ./githubevents changed incompatibly vs a base ref. +set -euo pipefail + +BASE="${1:-origin/main}" +PKG="./githubevents" + +command -v apidiff >/dev/null 2>&1 || { echo "apidiff not installed; run 'make tools'"; exit 1; } + +work="$(mktemp -d)" +base_wt="$work/base" +cleanup() { git worktree remove --force "$base_wt" >/dev/null 2>&1 || true; rm -rf "$work"; } +trap cleanup EXIT + +git worktree add --quiet --detach "$base_wt" "$BASE" +( cd "$base_wt" && go mod download && apidiff -w "$work/base.api" "$PKG" ) + +out="$(apidiff "$work/base.api" "$PKG")" +printf '%s\n' "$out" +if printf '%s\n' "$out" | grep -q '^Incompatible changes:'; then + echo ">> incompatible API changes detected" + exit 1 +fi +echo ">> API compatible"