From 5e403d0e17db1c9dda882463c66939af9c1ef531 Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Tue, 31 Mar 2026 11:09:16 -0400 Subject: [PATCH] Linkify plugin versions in generated PR --- internal/cmd/fetcher/main.go | 86 +++++++++++++++---- internal/cmd/fetcher/main_test.go | 134 ++++++++++++++++++++++++++++++ 2 files changed, 202 insertions(+), 18 deletions(-) diff --git a/internal/cmd/fetcher/main.go b/internal/cmd/fetcher/main.go index 9d383524d..13c5c64a8 100644 --- a/internal/cmd/fetcher/main.go +++ b/internal/cmd/fetcher/main.go @@ -125,11 +125,13 @@ func newRootCommand(name string) *appcmd.Command { } type createdPlugin struct { - org string - name string - pluginDir string - previousVersion string - newVersion string + org string + name string + pluginDir string + previousVersion string + newVersion string + previousReleaseURL string + releaseURL string } func (p createdPlugin) String() string { @@ -382,9 +384,11 @@ func updatePluginDeps(ctx context.Context, logger *slog.Logger, content []byte, // pluginToCreate represents a plugin that needs a new version created. type pluginToCreate struct { - pluginDir string - previousVersion string - newVersion string + pluginDir string + previousVersion string + newVersion string + previousReleaseURL string + releaseURL string } type runOption func(*runOptions) @@ -499,16 +503,52 @@ func run( latestPluginVersions[p.Name] = pending.newVersion created = append(created, createdPlugin{ - org: filepath.Base(filepath.Dir(pending.pluginDir)), - name: filepath.Base(pending.pluginDir), - pluginDir: pending.pluginDir, - previousVersion: pending.previousVersion, - newVersion: pending.newVersion, + org: filepath.Base(filepath.Dir(pending.pluginDir)), + name: filepath.Base(pending.pluginDir), + pluginDir: pending.pluginDir, + previousVersion: pending.previousVersion, + newVersion: pending.newVersion, + previousReleaseURL: pending.previousReleaseURL, + releaseURL: pending.releaseURL, }) } return created, nil } +// pluginReleaseURL returns the release URL for a new plugin version, or empty string if not determinable. +// For GitHub sources, reads the previous version's buf.plugin.yaml to determine +// whether the repo uses v-prefixed tags (e.g. v1.2.3) or bare tags (e.g. 1.2.3). +func pluginReleaseURL(pluginDir, previousVersion, newVersion string, src source.Source) string { + bare := strings.TrimPrefix(newVersion, "v") + switch { + case src.GitHub != nil: + tag := newVersion // default: v-prefixed + // Check the previous buf.plugin.yaml to see which tag format the repo uses. + prevYAML := filepath.Join(pluginDir, previousVersion, "buf.plugin.yaml") + if content, err := os.ReadFile(prevYAML); err == nil { + prevBare := strings.TrimPrefix(previousVersion, "v") + // If the file contains the bare version but NOT the v-prefixed version + // in a blob/tree URL, the repo uses bare tags. + if strings.Contains(string(content), "/blob/"+prevBare) && !strings.Contains(string(content), "/blob/v"+prevBare) { + tag = bare + } + } + return "https://github.com/" + src.GitHub.Owner + "/" + src.GitHub.Repository + "/releases/tag/" + tag + case src.GoProxy != nil: + return "https://pkg.go.dev/" + src.GoProxy.Name + "@" + newVersion + case src.NPMRegistry != nil: + return "https://www.npmjs.com/package/" + src.NPMRegistry.Name + "/v/" + bare + case src.Maven != nil: + groupPath := strings.ReplaceAll(src.Maven.Group, ".", "/") + return "https://mvnrepository.com/artifact/" + groupPath + "/" + src.Maven.Name + "/" + bare + case src.Crates != nil: + return "https://crates.io/crates/" + src.Crates.CrateName + "/" + bare + case src.DartFlutter != nil: + return "https://pub.dev/packages/" + src.DartFlutter.Name + "/versions/" + bare + } + return "" +} + // fetchPendingCreations iterates over source configs, fetches the latest // version for each enabled plugin, and returns a map of plugin directories // that need a new version created. @@ -582,9 +622,11 @@ func fetchPendingCreations( } pendingCreations[pluginDir] = &pluginToCreate{ - pluginDir: pluginDir, - previousVersion: previousVersion, - newVersion: newVersion, + pluginDir: pluginDir, + previousVersion: previousVersion, + newVersion: newVersion, + previousReleaseURL: pluginReleaseURL(pluginDir, previousVersion, previousVersion, config.Source), + releaseURL: pluginReleaseURL(pluginDir, previousVersion, newVersion, config.Source), } } return pendingCreations, nil @@ -908,10 +950,18 @@ func generatePRBody(created []createdPlugin) string { } fmt.Fprintf(&sb, "### %s\n", g.name) for _, p := range g.plugins { + prevVersionLink := p.previousVersion + if p.previousReleaseURL != "" { + prevVersionLink = "[" + p.previousVersion + "](" + p.previousReleaseURL + ")" + } + newVersionLink := p.newVersion + if p.releaseURL != "" { + newVersionLink = "[" + p.newVersion + "](" + p.releaseURL + ")" + } if p.org == communityOrg { - fmt.Fprintf(&sb, "- %s → %s\n", p.previousVersion, p.newVersion) + fmt.Fprintf(&sb, "- %s → %s\n", prevVersionLink, newVersionLink) } else { - fmt.Fprintf(&sb, "- %s: %s → %s\n", p.name, p.previousVersion, p.newVersion) + fmt.Fprintf(&sb, "- %s: %s → %s\n", p.name, prevVersionLink, newVersionLink) } } } diff --git a/internal/cmd/fetcher/main_test.go b/internal/cmd/fetcher/main_test.go index 278db9781..74d56c701 100644 --- a/internal/cmd/fetcher/main_test.go +++ b/internal/cmd/fetcher/main_test.go @@ -398,6 +398,140 @@ COPY --from=consumer /binary /usr/local/bin/protoc-gen-consumer )) } +func TestPluginReleaseURL(t *testing.T) { + t.Parallel() + + t.Run("github with v-prefixed tags", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + pluginDir := filepath.Join(tmpDir, "plugins", "protocolbuffers", "go") + require.NoError(t, os.MkdirAll(filepath.Join(pluginDir, "v1.36.5"), 0755)) + require.NoError(t, os.WriteFile( + filepath.Join(pluginDir, "v1.36.5", "buf.plugin.yaml"), + []byte("license_url: https://github.com/protocolbuffers/protobuf-go/blob/v1.36.5/LICENSE\n"), + 0644, + )) + src := source.Source{GitHub: &source.GitHubConfig{Owner: "protocolbuffers", Repository: "protobuf-go"}} + url := pluginReleaseURL(pluginDir, "v1.36.5", "v1.37.0", src) + assert.Equal(t, "https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.37.0", url) + }) + + t.Run("github with bare tags (no v prefix)", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + pluginDir := filepath.Join(tmpDir, "plugins", "connectrpc", "swift") + require.NoError(t, os.MkdirAll(filepath.Join(pluginDir, "v1.2.1"), 0755)) + require.NoError(t, os.WriteFile( + filepath.Join(pluginDir, "v1.2.1", "buf.plugin.yaml"), + []byte("license_url: https://github.com/connectrpc/connect-swift/blob/1.2.1/LICENSE\n"), + 0644, + )) + src := source.Source{GitHub: &source.GitHubConfig{Owner: "connectrpc", Repository: "connect-swift"}} + url := pluginReleaseURL(pluginDir, "v1.2.1", "v1.2.2", src) + assert.Equal(t, "https://github.com/connectrpc/connect-swift/releases/tag/1.2.2", url) + }) + + t.Run("github falls back to v-prefix when no previous yaml", func(t *testing.T) { + t.Parallel() + tmpDir := t.TempDir() + pluginDir := filepath.Join(tmpDir, "plugins", "test", "myplugin") + require.NoError(t, os.MkdirAll(pluginDir, 0755)) + src := source.Source{GitHub: &source.GitHubConfig{Owner: "test", Repository: "myplugin"}} + url := pluginReleaseURL(pluginDir, "v1.0.0", "v1.1.0", src) + assert.Equal(t, "https://github.com/test/myplugin/releases/tag/v1.1.0", url) + }) + + t.Run("goproxy", func(t *testing.T) { + t.Parallel() + src := source.Source{GoProxy: &source.GoProxyConfig{Name: "google.golang.org/grpc/cmd/protoc-gen-go-grpc"}} + url := pluginReleaseURL("", "", "v1.5.1", src) + assert.Equal(t, "https://pkg.go.dev/google.golang.org/grpc/cmd/protoc-gen-go-grpc@v1.5.1", url) + }) + + t.Run("npm", func(t *testing.T) { + t.Parallel() + src := source.Source{NPMRegistry: &source.NPMRegistryConfig{Name: "@bufbuild/protoc-gen-es"}} + url := pluginReleaseURL("", "", "v2.5.0", src) + assert.Equal(t, "https://www.npmjs.com/package/@bufbuild/protoc-gen-es/v/2.5.0", url) + }) + + t.Run("crates", func(t *testing.T) { + t.Parallel() + src := source.Source{Crates: &source.CratesConfig{CrateName: "prost"}} + url := pluginReleaseURL("", "", "v0.13.4", src) + assert.Equal(t, "https://crates.io/crates/prost/0.13.4", url) + }) + + t.Run("maven", func(t *testing.T) { + t.Parallel() + src := source.Source{Maven: &source.MavenConfig{Group: "io.grpc", Name: "grpc-java"}} + url := pluginReleaseURL("", "", "v1.70.0", src) + assert.Equal(t, "https://mvnrepository.com/artifact/io/grpc/grpc-java/1.70.0", url) + }) + + t.Run("dart_flutter", func(t *testing.T) { + t.Parallel() + src := source.Source{DartFlutter: &source.DartFlutterConfig{Name: "protobuf"}} + url := pluginReleaseURL("", "", "v3.1.0", src) + assert.Equal(t, "https://pub.dev/packages/protobuf/versions/3.1.0", url) + }) +} + +func TestGeneratePRBody(t *testing.T) { + t.Parallel() + + t.Run("single plugin with release URLs", func(t *testing.T) { + t.Parallel() + created := []createdPlugin{ + { + org: "connectrpc", name: "swift", + previousVersion: "v1.2.1", + newVersion: "v1.2.2", + previousReleaseURL: "https://github.com/connectrpc/connect-swift/releases/tag/1.2.1", + releaseURL: "https://github.com/connectrpc/connect-swift/releases/tag/1.2.2", + }, + } + body := generatePRBody(created) + assert.Equal(t, "### connectrpc\n- swift: [v1.2.1](https://github.com/connectrpc/connect-swift/releases/tag/1.2.1) → [v1.2.2](https://github.com/connectrpc/connect-swift/releases/tag/1.2.2)", body) + }) + + t.Run("single plugin without release URLs", func(t *testing.T) { + t.Parallel() + created := []createdPlugin{ + {org: "protocolbuffers", name: "go", previousVersion: "v1.36.5", newVersion: "v1.37.0"}, + } + body := generatePRBody(created) + assert.Equal(t, "### protocolbuffers\n- go: v1.36.5 → v1.37.0", body) + }) + + t.Run("multiple plugins grouped by org", func(t *testing.T) { + t.Parallel() + created := []createdPlugin{ + { + org: "protocolbuffers", name: "go", + previousVersion: "v1.36.5", + newVersion: "v1.37.0", + previousReleaseURL: "https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.36.5", + releaseURL: "https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.37.0", + }, + { + org: "protocolbuffers", name: "java", + previousVersion: "v4.28.3", + newVersion: "v4.29.0", + previousReleaseURL: "https://github.com/protocolbuffers/protobuf/releases/tag/v4.28.3", + releaseURL: "https://github.com/protocolbuffers/protobuf/releases/tag/v4.29.0", + }, + } + body := generatePRBody(created) + assert.Equal(t, "### protocolbuffers\n- go: [v1.36.5](https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.36.5) → [v1.37.0](https://github.com/protocolbuffers/protobuf-go/releases/tag/v1.37.0)\n- java: [v4.28.3](https://github.com/protocolbuffers/protobuf/releases/tag/v4.28.3) → [v4.29.0](https://github.com/protocolbuffers/protobuf/releases/tag/v4.29.0)", body) + }) + + t.Run("empty created list", func(t *testing.T) { + t.Parallel() + assert.Empty(t, generatePRBody(nil)) + }) +} + type testWriter struct { tb testing.TB }