From d878f780905d41ed3a1e48a27bb6eb904154e848 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Wed, 22 Apr 2026 11:55:05 -0500 Subject: [PATCH 1/6] Use appcmd for changed-plugins --- internal/cmd/changed-plugins/main.go | 42 +++++++++++++++++----------- 1 file changed, 25 insertions(+), 17 deletions(-) diff --git a/internal/cmd/changed-plugins/main.go b/internal/cmd/changed-plugins/main.go index 915ef02c6..363369aa7 100644 --- a/internal/cmd/changed-plugins/main.go +++ b/internal/cmd/changed-plugins/main.go @@ -2,38 +2,46 @@ package main import ( "context" - "flag" "fmt" - "log" - "os" "strings" + "buf.build/go/app/appcmd" + "buf.build/go/app/appext" + "github.com/bufbuild/plugins/internal/plugin" ) func main() { - flag.Parse() - if len(flag.Args()) != 1 { - flag.Usage() - os.Exit(2) + appcmd.Main(context.Background(), newRootCommand("changed-plugins")) +} + +func newRootCommand(name string) *appcmd.Command { + builder := appext.NewBuilder(name) + return &appcmd.Command{ + Use: name + " ", + Short: "Outputs plugins that changed relative to a base git ref.", + Args: appcmd.ExactArgs(1), + Run: builder.NewRunFunc(run), + BindPersistentFlags: builder.BindRoot, } - basedir := flag.Args()[0] +} - plugins, err := plugin.FindAll(basedir) +func run(ctx context.Context, container appext.Container) error { + plugins, err := plugin.FindAll(container.Arg(0)) if err != nil { - log.Fatalf("failed to find plugins: %v", err) + return fmt.Errorf("find plugins: %w", err) } - // Filter by changed plugins (for PR builds) - includedPlugins, err := plugin.FilterByBaseRefDiff(context.Background(), plugins) + includedPlugins, err := plugin.FilterByBaseRefDiff(ctx, plugins) if err != nil { - log.Fatalf("failed to filter plugins by changed files: %v", err) + return fmt.Errorf("filter plugins by changed files: %w", err) } var sb strings.Builder - for _, includedPlugin := range includedPlugins { - sb.WriteString(strings.TrimPrefix(includedPlugin.Name, "buf.build/")) + for _, p := range includedPlugins { + sb.WriteString(strings.TrimPrefix(p.Name, "buf.build/")) sb.WriteByte(':') - sb.WriteString(includedPlugin.PluginVersion) + sb.WriteString(p.PluginVersion) sb.WriteByte(' ') } - fmt.Println(strings.TrimSpace(sb.String())) //nolint:forbidigo + fmt.Fprintln(container.Stdout(), strings.TrimSpace(sb.String())) + return nil } From 7e344b836fcc4e912690dd03d664498bc88f2dfc Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Wed, 22 Apr 2026 12:08:40 -0500 Subject: [PATCH 2/6] Prefer flags for --dir, --base-ref, --include-testdata --- .github/workflows/ci.yml | 4 +-- .github/workflows/pr.yml | 5 +--- internal/cmd/changed-plugins/main.go | 28 ++++++++++++++----- internal/plugin/plugin.go | 40 +++------------------------- 4 files changed, 28 insertions(+), 49 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a4b445530..76bbeb999 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -47,10 +47,8 @@ jobs: check-latest: true - name: Calculate changed plugins and set PLUGINS env var from last successful commit push if: ${{ inputs.plugins == '' }} - env: - BASE_REF: ${{ steps.last_successful_commit_push.outputs.base }} run: | - val=`go run ./internal/cmd/changed-plugins .` + val=`go run ./internal/cmd/changed-plugins --base-ref '${{ steps.last_successful_commit_push.outputs.base }}'` if [[ -n "${val}" && -z "${PLUGINS}" ]]; then echo "PLUGINS=${val}" >> $GITHUB_ENV fi diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 33e14f524..70e487b97 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -41,11 +41,8 @@ jobs: check-latest: true - name: Calculate changed plugins and set PLUGINS env var from base branch if: ${{ inputs.plugins == '' }} - env: - BASE_REF: 'origin/${{ github.base_ref }}' # we can use a remote ref because we fetch everything back in the checkout step - INCLUDE_TESTDATA: 'true' run: | - val=`go run ./internal/cmd/changed-plugins .` + val=`go run ./internal/cmd/changed-plugins --base-ref 'origin/${{ github.base_ref }}' --include-testdata` if [[ -n "${val}" && -z "${PLUGINS}" ]]; then echo "PLUGINS=${val}" >> $GITHUB_ENV fi diff --git a/internal/cmd/changed-plugins/main.go b/internal/cmd/changed-plugins/main.go index 363369aa7..8d0927d5c 100644 --- a/internal/cmd/changed-plugins/main.go +++ b/internal/cmd/changed-plugins/main.go @@ -7,6 +7,7 @@ import ( "buf.build/go/app/appcmd" "buf.build/go/app/appext" + "github.com/spf13/pflag" "github.com/bufbuild/plugins/internal/plugin" ) @@ -17,21 +18,36 @@ func main() { func newRootCommand(name string) *appcmd.Command { builder := appext.NewBuilder(name) + f := &flags{} return &appcmd.Command{ - Use: name + " ", + Use: name, Short: "Outputs plugins that changed relative to a base git ref.", - Args: appcmd.ExactArgs(1), - Run: builder.NewRunFunc(run), + Args: appcmd.NoArgs, + Run: builder.NewRunFunc(func(ctx context.Context, container appext.Container) error { return run(ctx, container, f) }), + BindFlags: f.Bind, BindPersistentFlags: builder.BindRoot, } } -func run(ctx context.Context, container appext.Container) error { - plugins, err := plugin.FindAll(container.Arg(0)) +type flags struct { + dir string + baseRef string + includeTestdata bool +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + flagSet.StringVar(&f.dir, "dir", ".", "directory path to plugins") + flagSet.StringVar(&f.baseRef, "base-ref", "", "base git ref to diff against") + flagSet.BoolVar(&f.includeTestdata, "include-testdata", false, "include testdata plugin paths in the diff") + _ = appcmd.MarkFlagRequired(flagSet, "base-ref") +} + +func run(ctx context.Context, container appext.Container, f *flags) error { + plugins, err := plugin.FindAll(f.dir) if err != nil { return fmt.Errorf("find plugins: %w", err) } - includedPlugins, err := plugin.FilterByBaseRefDiff(ctx, plugins) + includedPlugins, err := plugin.FilterByBaseRefDiff(ctx, plugins, f.baseRef, f.includeTestdata) if err != nil { return fmt.Errorf("filter plugins by changed files: %w", err) } diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 7b56b6cc3..c30d82c65 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -4,7 +4,6 @@ import ( "bytes" "cmp" "context" - "errors" "fmt" "io/fs" "log" @@ -12,7 +11,6 @@ import ( "os/exec" "path/filepath" "slices" - "strconv" "strings" "sync" "unicode" @@ -205,16 +203,12 @@ func FilterByPluginsEnv(plugins []*Plugin, pluginsEnv string) ([]*Plugin, error) // FilterByBaseRefDiff filters the passed plugins to the ones that changed from a base Git ref to // diff against. It calculates the changed files from that ref, and filters the relevant files in // the plugins directory(ies) to determine which plugins changed from the ones passed. -func FilterByBaseRefDiff(ctx context.Context, plugins []*Plugin) ([]*Plugin, error) { - diffEnv, err := readDiffEnv() +func FilterByBaseRefDiff(ctx context.Context, plugins []*Plugin, baseRef string, includeTestdata bool) ([]*Plugin, error) { + allChangedFiles, err := git.ChangedFilesFrom(ctx, baseRef) if err != nil { - return nil, fmt.Errorf("get diff env: %w", err) + return nil, fmt.Errorf("calculate changed files from base ref %q: %w", baseRef, err) } - allChangedFiles, err := git.ChangedFilesFrom(ctx, diffEnv.baseRef) - if err != nil { - return nil, fmt.Errorf("calculate changed files from base ref %q: %w", diffEnv.baseRef, err) - } - return filterPluginsByChangedFiles(plugins, allChangedFiles, diffEnv.includeTestdata) + return filterPluginsByChangedFiles(plugins, allChangedFiles, includeTestdata) } func filterPluginsByChangedFiles(plugins []*Plugin, allChangedFiles []string, includeTestdata bool) ([]*Plugin, error) { @@ -346,32 +340,6 @@ func calculateGitModified(ctx context.Context, pluginYamlPath string) (bool, err return strings.TrimSpace(output.String()) != "", nil } -type diffEnv struct { - baseRef string - includeTestdata bool -} - -func readDiffEnv() (*diffEnv, error) { - baseRef, ok := os.LookupEnv("BASE_REF") - if !ok { - return nil, errors.New("missing BASE_REF") - } else if baseRef == "" { - return nil, errors.New("empty BASE_REF") - } - var includeTestdata bool // default false - if includeTestdataStr, _ := os.LookupEnv("INCLUDE_TESTDATA"); includeTestdataStr != "" { - var err error - includeTestdata, err = strconv.ParseBool(includeTestdataStr) - if err != nil { - return nil, fmt.Errorf("invalid INCLUDE_TESTDATA value %s: %w", includeTestdataStr, err) - } - } - return &diffEnv{ - baseRef: baseRef, - includeTestdata: includeTestdata, - }, nil -} - // filterPluginPaths returns the filepaths that are considered to be a relevant plugin file, based // on the plugins dir(s). func filterPluginPaths(filePaths []string, includeTestdata bool) []string { From 7872698b0c8734675ae35efea8abc55c56dee45a Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Wed, 22 Apr 2026 13:30:10 -0500 Subject: [PATCH 3/6] Remove nrwl/nx-set-shas, add last-successful-commit command --- .github/workflows/ci.yml | 11 ++-- internal/cmd/last-successful-commit/main.go | 71 +++++++++++++++++++++ 2 files changed, 75 insertions(+), 7 deletions(-) create mode 100644 internal/cmd/last-successful-commit/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 76bbeb999..3245fa25c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -34,12 +34,6 @@ jobs: - uses: actions/checkout@v6 with: fetch-depth: 0 - - uses: nrwl/nx-set-shas@afb73a62d26e41464e9254689e1fd6122ee683c1 # v5.0.1 - id: last_successful_commit_push - if: ${{ inputs.plugins == '' }} - with: - main-branch-name: ${{ github.ref_name }} - workflow-id: 'ci.yml' - name: Install Go uses: actions/setup-go@v6 with: @@ -47,8 +41,11 @@ jobs: check-latest: true - name: Calculate changed plugins and set PLUGINS env var from last successful commit push if: ${{ inputs.plugins == '' }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - val=`go run ./internal/cmd/changed-plugins --base-ref '${{ steps.last_successful_commit_push.outputs.base }}'` + base_ref=$(go run ./internal/cmd/last-successful-commit --workflow ci.yml --branch '${{ github.ref_name }}') + val=$(go run ./internal/cmd/changed-plugins --base-ref "${base_ref}") if [[ -n "${val}" && -z "${PLUGINS}" ]]; then echo "PLUGINS=${val}" >> $GITHUB_ENV fi diff --git a/internal/cmd/last-successful-commit/main.go b/internal/cmd/last-successful-commit/main.go new file mode 100644 index 000000000..9e082b5ac --- /dev/null +++ b/internal/cmd/last-successful-commit/main.go @@ -0,0 +1,71 @@ +package main + +import ( + "context" + "fmt" + + "buf.build/go/app/appcmd" + "buf.build/go/app/appext" + "github.com/google/go-github/v72/github" + "github.com/spf13/pflag" + + "github.com/bufbuild/plugins/internal/release" +) + +func main() { + appcmd.Main(context.Background(), newRootCommand("last-successful-commit")) +} + +func newRootCommand(name string) *appcmd.Command { + builder := appext.NewBuilder(name) + f := &flags{} + return &appcmd.Command{ + Use: name, + Short: "Prints the HEAD SHA of the last successful run of a GitHub Actions workflow.", + Args: appcmd.NoArgs, + Run: builder.NewRunFunc(func(ctx context.Context, container appext.Container) error { + return run(ctx, container, f) + }), + BindFlags: f.Bind, + BindPersistentFlags: builder.BindRoot, + } +} + +type flags struct { + owner string + repo string + workflow string + branch string +} + +func (f *flags) Bind(flagSet *pflag.FlagSet) { + flagSet.StringVar(&f.owner, "owner", "bufbuild", "GitHub repository owner") + flagSet.StringVar(&f.repo, "repo", "plugins", "GitHub repository name") + flagSet.StringVar(&f.workflow, "workflow", "", "workflow filename (e.g. ci.yml)") + flagSet.StringVar(&f.branch, "branch", "", "branch to query") + _ = appcmd.MarkFlagRequired(flagSet, "workflow") + _ = appcmd.MarkFlagRequired(flagSet, "branch") +} + +func run(ctx context.Context, container appext.Container, f *flags) error { + client := release.NewClient() + runs, _, err := client.GitHub.Actions.ListWorkflowRunsByFileName( + ctx, + f.owner, + f.repo, + f.workflow, + &github.ListWorkflowRunsOptions{ + Branch: f.branch, + Status: "success", + ListOptions: github.ListOptions{PerPage: 1}, + }, + ) + if err != nil { + return fmt.Errorf("list workflow runs for %s on %s: %w", f.workflow, f.branch, err) + } + if len(runs.WorkflowRuns) == 0 { + return fmt.Errorf("no successful runs found for workflow %q on branch %q", f.workflow, f.branch) + } + fmt.Fprintln(container.Stdout(), runs.WorkflowRuns[0].GetHeadSHA()) + return nil +} From 597de91fd6c27714ddee9996abece7f87d727d04 Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Wed, 22 Apr 2026 13:31:03 -0500 Subject: [PATCH 4/6] Add /last-successful-commit to .gitignore --- .gitignore | 1 + 1 file changed, 1 insertion(+) diff --git a/.gitignore b/.gitignore index 4d65d7d54..87c681654 100644 --- a/.gitignore +++ b/.gitignore @@ -7,4 +7,5 @@ tests/testdata/**/buf.gen.yaml tests/testdata/**/gen/ tests/testdata/**/protoc-gen-plugin /fetcher +/last-successful-commit /release From 64a7ce007a2423fa55ce565db32f3ff804477f1c Mon Sep 17 00:00:00 2001 From: Julian Figueroa Date: Wed, 22 Apr 2026 13:33:33 -0500 Subject: [PATCH 5/6] Revert "Add /last-successful-commit to .gitignore" This reverts commit 597de91fd6c27714ddee9996abece7f87d727d04. --- .gitignore | 1 - 1 file changed, 1 deletion(-) diff --git a/.gitignore b/.gitignore index 87c681654..4d65d7d54 100644 --- a/.gitignore +++ b/.gitignore @@ -7,5 +7,4 @@ tests/testdata/**/buf.gen.yaml tests/testdata/**/gen/ tests/testdata/**/protoc-gen-plugin /fetcher -/last-successful-commit /release From f282b5c98d632cf80f247b2cbf1a780dd38a282c Mon Sep 17 00:00:00 2001 From: Stefan VanBuren Date: Wed, 22 Apr 2026 16:55:09 -0400 Subject: [PATCH 6/6] Use `gh` approach --- .github/workflows/ci.yml | 2 +- internal/cmd/last-successful-commit/main.go | 71 --------------------- 2 files changed, 1 insertion(+), 72 deletions(-) delete mode 100644 internal/cmd/last-successful-commit/main.go diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 3245fa25c..94aad3938 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -44,7 +44,7 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - base_ref=$(go run ./internal/cmd/last-successful-commit --workflow ci.yml --branch '${{ github.ref_name }}') + base_ref=$(gh api "repos/${{ github.repository }}/actions/workflows/ci.yml/runs?branch=${{ github.ref_name }}&status=success&per_page=1" --jq '.workflow_runs[0].head_sha') val=$(go run ./internal/cmd/changed-plugins --base-ref "${base_ref}") if [[ -n "${val}" && -z "${PLUGINS}" ]]; then echo "PLUGINS=${val}" >> $GITHUB_ENV diff --git a/internal/cmd/last-successful-commit/main.go b/internal/cmd/last-successful-commit/main.go deleted file mode 100644 index 9e082b5ac..000000000 --- a/internal/cmd/last-successful-commit/main.go +++ /dev/null @@ -1,71 +0,0 @@ -package main - -import ( - "context" - "fmt" - - "buf.build/go/app/appcmd" - "buf.build/go/app/appext" - "github.com/google/go-github/v72/github" - "github.com/spf13/pflag" - - "github.com/bufbuild/plugins/internal/release" -) - -func main() { - appcmd.Main(context.Background(), newRootCommand("last-successful-commit")) -} - -func newRootCommand(name string) *appcmd.Command { - builder := appext.NewBuilder(name) - f := &flags{} - return &appcmd.Command{ - Use: name, - Short: "Prints the HEAD SHA of the last successful run of a GitHub Actions workflow.", - Args: appcmd.NoArgs, - Run: builder.NewRunFunc(func(ctx context.Context, container appext.Container) error { - return run(ctx, container, f) - }), - BindFlags: f.Bind, - BindPersistentFlags: builder.BindRoot, - } -} - -type flags struct { - owner string - repo string - workflow string - branch string -} - -func (f *flags) Bind(flagSet *pflag.FlagSet) { - flagSet.StringVar(&f.owner, "owner", "bufbuild", "GitHub repository owner") - flagSet.StringVar(&f.repo, "repo", "plugins", "GitHub repository name") - flagSet.StringVar(&f.workflow, "workflow", "", "workflow filename (e.g. ci.yml)") - flagSet.StringVar(&f.branch, "branch", "", "branch to query") - _ = appcmd.MarkFlagRequired(flagSet, "workflow") - _ = appcmd.MarkFlagRequired(flagSet, "branch") -} - -func run(ctx context.Context, container appext.Container, f *flags) error { - client := release.NewClient() - runs, _, err := client.GitHub.Actions.ListWorkflowRunsByFileName( - ctx, - f.owner, - f.repo, - f.workflow, - &github.ListWorkflowRunsOptions{ - Branch: f.branch, - Status: "success", - ListOptions: github.ListOptions{PerPage: 1}, - }, - ) - if err != nil { - return fmt.Errorf("list workflow runs for %s on %s: %w", f.workflow, f.branch, err) - } - if len(runs.WorkflowRuns) == 0 { - return fmt.Errorf("no successful runs found for workflow %q on branch %q", f.workflow, f.branch) - } - fmt.Fprintln(container.Stdout(), runs.WorkflowRuns[0].GetHeadSHA()) - return nil -}