From 85929497f2ea08365ad31577f0f222591fe60c96 Mon Sep 17 00:00:00 2001 From: "Philip K. Warren" Date: Fri, 10 Nov 2023 09:18:14 -0600 Subject: [PATCH] Only rebuild plugins if changed (#906) Update the dockerbuild command to add a label containing the git revision of the plugin's directory (if there are no changes to the working directory of the plugin). If we determine that an image exists for the same revision of the plugin, skip costly rebuilds. Additionally, update commands to use exec.CommandContext consistently and stop calling exec.LookPath as the exec package does this automatically for us if the command doesn't contain path separators. --- Makefile | 4 +- internal/cmd/dockerbuild/main.go | 33 ++++++++++++- internal/cmd/fetcher/main.go | 69 ++++++++++------------------ internal/cmd/release/main.go | 52 ++++++++------------- internal/cmd/restore-release/main.go | 67 ++++++++++----------------- internal/docker/build.go | 29 +++++------- internal/docker/push.go | 6 +-- internal/plugin/plugin.go | 50 +++++++++++++++++++- tests/plugins_test.go | 3 +- 9 files changed, 162 insertions(+), 151 deletions(-) diff --git a/Makefile b/Makefile index 8faa5cb3d..5d9249715 100644 --- a/Makefile +++ b/Makefile @@ -32,10 +32,10 @@ all: build .PHONY: build build: - docker buildx inspect "$(DOCKER_BUILDER)" 2> /dev/null || docker buildx create --use --bootstrap --name="$(DOCKER_BUILDER)" + docker buildx inspect "$(DOCKER_BUILDER)" 2> /dev/null || docker buildx create --use --bootstrap --name="$(DOCKER_BUILDER)" > /dev/null go run ./internal/cmd/dockerbuild -cache-dir "$(DOCKER_CACHE_DIR)" -org "$(DOCKER_ORG)" -- $(DOCKER_BUILD_EXTRA_ARGS) || \ (docker buildx rm "$(DOCKER_BUILDER)"; exit 1) - docker buildx rm "$(DOCKER_BUILDER)" + docker buildx rm "$(DOCKER_BUILDER)" > /dev/null .PHONY: dockerpush dockerpush: diff --git a/internal/cmd/dockerbuild/main.go b/internal/cmd/dockerbuild/main.go index b86e3edfd..79a35382a 100644 --- a/internal/cmd/dockerbuild/main.go +++ b/internal/cmd/dockerbuild/main.go @@ -8,6 +8,7 @@ import ( "fmt" "log" "os" + "os/exec" "path/filepath" "runtime" "slices" @@ -155,9 +156,14 @@ func (c *command) buildPluginGroup(ctx context.Context, pluginGroup string, plug pluginCacheDir = filepath.Join(c.cacheDir, pluginIdentity.Owner(), pluginIdentity.Plugin(), pluginToBuild.PluginVersion) } } + if !c.shouldBuild(ctx, pluginToBuild) { + log.Println("skipping (up-to-date):", pluginToBuild.Name, pluginToBuild.PluginVersion) + continue + } log.Println("building:", pluginToBuild.Name, pluginToBuild.PluginVersion) start := time.Now() - output, err := docker.Build(ctx, pluginToBuild, c.dockerOrg, pluginCacheDir, c.dockerBuildArgs) + imageName := docker.ImageName(pluginToBuild, c.dockerOrg) + output, err := docker.Build(ctx, pluginToBuild, imageName, pluginCacheDir, c.dockerBuildArgs) if err != nil { if errors.Is(err, context.Canceled) || strings.Contains(err.Error(), "signal: killed") { return err @@ -175,3 +181,28 @@ func (c *command) buildPluginGroup(ctx context.Context, pluginGroup string, plug } return nil } + +func (c *command) shouldBuild(ctx context.Context, plugin *plugin.Plugin) bool { + gitCommit := plugin.GitCommit(ctx) + // There are uncommitted changes to the plugin's directory - we should always rebuild. + if gitCommit == "" { + return true + } + imageName := docker.ImageName(plugin, c.dockerOrg) + cmd := exec.CommandContext( + ctx, + "docker", + "image", + "inspect", + imageName, + "--format", + `{{ index .Config.Labels "org.opencontainers.image.revision" }}`, + ) + output, err := cmd.Output() + if err != nil { + // This could be because the image doesn't exist or any other error. + // Err on the side of caution and assume that we should rebuild the image. + return true + } + return strings.TrimSpace(string(output)) != gitCommit +} diff --git a/internal/cmd/fetcher/main.go b/internal/cmd/fetcher/main.go index 26ba81f92..9f8832416 100644 --- a/internal/cmd/fetcher/main.go +++ b/internal/cmd/fetcher/main.go @@ -15,6 +15,7 @@ import ( "strings" "time" + "github.com/bufbuild/buf/private/pkg/interrupt" "go.uber.org/multierr" "golang.org/x/mod/semver" @@ -34,12 +35,13 @@ func main() { os.Exit(2) } root := os.Args[1] - created, err := run(context.Background(), root) + ctx, _ := interrupt.WithCancel(context.Background()) + created, err := run(ctx, root) if err != nil { _, _ = fmt.Fprintf(os.Stderr, "failed to fetch versions: %v\n", err) os.Exit(1) } - if err := postProcessCreatedPlugins(created); err != nil { + if err := postProcessCreatedPlugins(ctx, created); err != nil { _, _ = fmt.Fprintf(os.Stderr, "failed to run post-processing on plugins: %v\n", err) os.Exit(1) } @@ -53,20 +55,20 @@ type createdPlugin struct { newVersion string } -func postProcessCreatedPlugins(plugins []createdPlugin) error { +func postProcessCreatedPlugins(ctx context.Context, plugins []createdPlugin) error { if len(plugins) == 0 { return nil } for _, plugin := range plugins { newPluginRef := fmt.Sprintf("%s/%s:%s", plugin.org, plugin.name, plugin.newVersion) - if err := runGoModTidy(plugin); err != nil { + if err := runGoModTidy(ctx, plugin); err != nil { return fmt.Errorf("failed to run go mod tidy for %s: %w", newPluginRef, err) } - if err := recreateNPMPackageLock(plugin); err != nil { + if err := recreateNPMPackageLock(ctx, plugin); err != nil { return fmt.Errorf("failed to recreate package-lock.json for %s: %w", newPluginRef, err) } } - if err := runPluginTests(plugins); err != nil { + if err := runPluginTests(ctx, plugins); err != nil { return fmt.Errorf("failed to run plugin tests: %w", err) } return nil @@ -74,7 +76,7 @@ func postProcessCreatedPlugins(plugins []createdPlugin) error { // runGoModTidy runs 'go mod tidy' for plugins (like twirp-go) which don't use modules. // In order to get more reproducible builds, we check in a go.mod/go.sum file. -func runGoModTidy(plugin createdPlugin) error { +func runGoModTidy(ctx context.Context, plugin createdPlugin) error { versionDir := filepath.Join(plugin.pluginDir, plugin.newVersion) goMod := filepath.Join(versionDir, "go.mod") _, err := os.Stat(goMod) @@ -85,24 +87,17 @@ func runGoModTidy(plugin createdPlugin) error { // no go.mod/go.sum to update return nil } - goPath, err := exec.LookPath("go") - if err != nil { - return err - } log.Printf("running go mod tidy for %s/%s:%s", plugin.org, plugin.name, plugin.newVersion) - cmd := exec.Cmd{ - Path: goPath, - Args: []string{goPath, "mod", "tidy"}, - Dir: versionDir, - Stdout: os.Stdout, - Stderr: os.Stderr, - } + cmd := exec.CommandContext(ctx, "go", "mod", "tidy") + cmd.Dir = versionDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr return cmd.Run() } // recreateNPMPackageLock will remove an existing package-lock.json file and recreate it. // This will ensure that we correctly resolve any updated versions in package.json. -func recreateNPMPackageLock(plugin createdPlugin) error { +func recreateNPMPackageLock(ctx context.Context, plugin createdPlugin) error { versionDir := filepath.Join(plugin.pluginDir, plugin.newVersion) npmPackageLock := filepath.Join(versionDir, "package-lock.json") _, err := os.Stat(npmPackageLock) @@ -116,49 +111,31 @@ func recreateNPMPackageLock(plugin createdPlugin) error { if err := os.Remove(npmPackageLock); err != nil { return err } - npmPath, err := exec.LookPath("npm") - if err != nil { - return err - } log.Printf("recreating package-lock.json for %s/%s:%s", plugin.org, plugin.name, plugin.newVersion) - cmd := exec.Cmd{ - Path: npmPath, - Args: []string{npmPath, "install"}, - Dir: versionDir, - Stdout: os.Stdout, - Stderr: os.Stderr, - } + cmd := exec.CommandContext(ctx, "npm", "install") + cmd.Dir = versionDir + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr return cmd.Run() } // runPluginTests runs 'make test PLUGINS="org/name:v"' in order to generate plugin.sum files. -func runPluginTests(plugins []createdPlugin) error { +func runPluginTests(ctx context.Context, plugins []createdPlugin) error { pluginsEnv := make([]string, 0, len(plugins)) for _, plugin := range plugins { pluginsEnv = append(pluginsEnv, fmt.Sprintf("%s/%s:%s", plugin.org, plugin.name, plugin.newVersion)) } - makePath, err := exec.LookPath("make") - if err != nil { - return err - } env := os.Environ() env = append(env, "ALLOW_EMPTY_PLUGIN_SUM=true") - cmd := exec.Cmd{ - Path: makePath, - Args: []string{ - makePath, - "test", - fmt.Sprintf("PLUGINS=%s", strings.Join(pluginsEnv, ",")), - }, - Env: env, - Stdout: os.Stdout, - Stderr: os.Stderr, - } start := time.Now() log.Printf("starting running tests for %d plugins", len(plugins)) defer func() { log.Printf("finished running tests in: %.2fs", time.Since(start).Seconds()) }() + cmd := exec.CommandContext(ctx, "make", "test", fmt.Sprintf("PLUGINS=%s", strings.Join(pluginsEnv, ","))) //nolint:gosec + cmd.Env = env + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr return cmd.Run() } diff --git a/internal/cmd/release/main.go b/internal/cmd/release/main.go index 6a72a3bc5..02777dd73 100644 --- a/internal/cmd/release/main.go +++ b/internal/cmd/release/main.go @@ -21,6 +21,7 @@ import ( "time" "aead.dev/minisign" + "github.com/bufbuild/buf/private/pkg/interrupt" githubkeychain "github.com/google/go-containerregistry/pkg/authn/github" "github.com/google/go-containerregistry/pkg/name" "github.com/google/go-containerregistry/pkg/v1/remote" @@ -80,6 +81,8 @@ type command struct { } func (c *command) run() error { + ctx, cancel := interrupt.WithCancel(context.Background()) + defer cancel() // Create temporary directory tmpDir, err := os.MkdirTemp("", "plugins-release") if err != nil { @@ -94,8 +97,6 @@ func (c *command) run() error { log.Printf("failed to remove %q: %v", tmpDir, err) } }() - - ctx := context.Background() client := release.NewClient(ctx) latestRelease, err := client.GetLatestRelease(ctx, c.githubReleaseOwner, release.GithubRepoPlugins) if err != nil && !errors.Is(err, release.ErrNotFound) { @@ -128,7 +129,7 @@ func (c *command) run() error { return fmt.Errorf("failed to determine next release name: %w", err) } - plugins, err := c.calculateNewReleasePlugins(releases, releaseName, now, tmpDir) + plugins, err := c.calculateNewReleasePlugins(ctx, releases, releaseName, now, tmpDir) if err != nil { return fmt.Errorf("failed to calculate new release contents: %w", err) } @@ -166,7 +167,7 @@ func (c *command) run() error { return nil } -func (c *command) calculateNewReleasePlugins(currentRelease *release.PluginReleases, releaseName string, now time.Time, tmpDir string) ( +func (c *command) calculateNewReleasePlugins(ctx context.Context, currentRelease *release.PluginReleases, releaseName string, now time.Time, tmpDir string) ( []release.PluginRelease, error, ) { pluginNameVersionToRelease := make(map[pluginNameVersion]release.PluginRelease, len(currentRelease.Releases)) @@ -200,7 +201,7 @@ func (c *command) calculateNewReleasePlugins(currentRelease *release.PluginRelea // Found existing release - only rebuild if changed image digest or buf.plugin.yaml digest if pluginRelease.ImageID != imageID || pluginRelease.PluginYAMLDigest != pluginYamlDigest { downloadURL := c.pluginDownloadURL(plugin, releaseName) - zipDigest, err := createPluginZip(tmpDir, plugin, registryImage, imageID) + zipDigest, err := createPluginZip(ctx, tmpDir, plugin, registryImage, imageID) if err != nil { return err } @@ -409,8 +410,8 @@ func signPluginReleases(dir string, privateKey minisign.PrivateKey) error { return nil } -func createPluginZip(basedir string, plugin *plugin.Plugin, registryImage string, imageID string) (string, error) { - if err := pullImage(registryImage); err != nil { +func createPluginZip(ctx context.Context, basedir string, plugin *plugin.Plugin, registryImage string, imageID string) (string, error) { + if err := pullImage(ctx, registryImage); err != nil { return "", err } zipName := pluginZipName(plugin) @@ -423,7 +424,7 @@ func createPluginZip(basedir string, plugin *plugin.Plugin, registryImage string log.Printf("failed to remove %q: %v", pluginTempDir, err) } }() - if err := saveImageToDir(imageID, pluginTempDir); err != nil { + if err := saveImageToDir(ctx, imageID, pluginTempDir); err != nil { return "", err } log.Printf("creating %s", zipName) @@ -480,11 +481,8 @@ func addFileToZip(zipWriter *zip.Writer, path string) error { return nil } -func saveImageToDir(imageRef string, dir string) error { - cmd, err := dockerCmd("save", imageRef, "-o", "image.tar") - if err != nil { - return err - } +func saveImageToDir(ctx context.Context, imageRef string, dir string) error { + cmd := dockerCmd(ctx, "save", imageRef, "-o", "image.tar") cmd.Dir = dir return cmd.Run() } @@ -504,30 +502,16 @@ func createPluginReleases(dir string, plugins []release.PluginRelease) error { return encoder.Encode(&release.PluginReleases{Releases: plugins}) } -func pullImage(name string) error { - cmd, err := dockerCmd("pull", name) - if err != nil { - return err - } +func pullImage(ctx context.Context, name string) error { log.Printf("pulling image: %s", name) - return cmd.Run() + return dockerCmd(ctx, "pull", name).Run() } -func dockerCmd(command string, args ...string) (*exec.Cmd, error) { - dockerPath, err := exec.LookPath("docker") - if err != nil { - return nil, err - } - cmd := &exec.Cmd{ - Path: dockerPath, - Args: append([]string{ - dockerPath, - command, - }, args...), - Stdout: os.Stdout, - Stderr: os.Stderr, - } - return cmd, nil +func dockerCmd(ctx context.Context, command string, args ...string) *exec.Cmd { + cmd := exec.CommandContext(ctx, "docker", append([]string{command}, args...)...) //nolint:gosec + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd } func calculateNextRelease(now time.Time, latestRelease *github.RepositoryRelease) (string, error) { diff --git a/internal/cmd/restore-release/main.go b/internal/cmd/restore-release/main.go index 0a588d3db..5c2beaad6 100644 --- a/internal/cmd/restore-release/main.go +++ b/internal/cmd/restore-release/main.go @@ -21,6 +21,8 @@ import ( "os/exec" "strings" + "github.com/bufbuild/buf/private/pkg/interrupt" + "github.com/bufbuild/plugins/internal/release" ) @@ -49,7 +51,8 @@ type command struct { } func (c *command) run() error { - ctx := context.Background() + ctx, cancel := interrupt.WithCancel(context.Background()) + defer cancel() client := release.NewClient(ctx) githubRelease, err := client.GetReleaseByTag(ctx, release.GithubOwnerBufbuild, release.GithubRepoPlugins, c.release) if err != nil { @@ -64,7 +67,7 @@ func (c *command) run() error { return fmt.Errorf("invalid plugin-releases.json format: %w", err) } for _, pluginRelease := range pluginReleases.Releases { - image, err := fetchRegistryImage(pluginRelease) + image, err := fetchRegistryImage(ctx, pluginRelease) if err != nil { return err } @@ -78,14 +81,14 @@ func (c *command) run() error { } taggedImage += ":" + pluginRelease.PluginVersion log.Printf("updating image tag %q to point from %q to %q", taggedImage, image, pluginRelease.RegistryImage) - if err := pullImage(pluginRelease.RegistryImage); err != nil { + if err := pullImage(ctx, pluginRelease.RegistryImage); err != nil { return fmt.Errorf("failed to pull %q: %w", pluginRelease.RegistryImage, err) } - if err := tagImage(pluginRelease.RegistryImage, taggedImage); err != nil { + if err := tagImage(ctx, pluginRelease.RegistryImage, taggedImage); err != nil { return fmt.Errorf("failed to tag %q: %w", taggedImage, err) } if !c.dryRun { - if err := pushImage(taggedImage); err != nil { + if err := pushImage(ctx, taggedImage); err != nil { return fmt.Errorf("failed to push %q: %w", taggedImage, err) } } @@ -93,60 +96,38 @@ func (c *command) run() error { return nil } -func pullImage(name string) error { - cmd, err := dockerCmd("pull", name) - if err != nil { - return err - } +func pullImage(ctx context.Context, name string) error { log.Printf("pulling image: %s", name) - return cmd.Run() + return dockerCmd(ctx, "pull", name).Run() } -func tagImage(previousName, newName string) error { - cmd, err := dockerCmd("tag", previousName, newName) - if err != nil { - return err - } +func tagImage(ctx context.Context, previousName, newName string) error { log.Printf("tagging image: %s => %s", previousName, newName) - return cmd.Run() + return dockerCmd(ctx, "tag", previousName, newName).Run() } -func pushImage(name string) error { - cmd, err := dockerCmd("push", name) - if err != nil { - return err - } +func pushImage(ctx context.Context, name string) error { log.Printf("pushing image: %s", name) - return cmd.Run() + return dockerCmd(ctx, "push", name).Run() } -func dockerCmd(command string, args ...string) (*exec.Cmd, error) { - dockerPath, err := exec.LookPath("docker") - if err != nil { - return nil, err - } - cmd := &exec.Cmd{ - Path: dockerPath, - Args: append([]string{ - dockerPath, - command, - }, args...), - Stdout: os.Stdout, - Stderr: os.Stderr, - } - return cmd, nil +func dockerCmd(ctx context.Context, command string, args ...string) *exec.Cmd { + commandArgs := make([]string, 0, len(args)+1) + commandArgs = append(commandArgs, command) + commandArgs = append(commandArgs, args...) + cmd := exec.CommandContext(ctx, "docker", commandArgs...) + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + return cmd } -func fetchRegistryImage(pluginRelease release.PluginRelease) (string, error) { +func fetchRegistryImage(ctx context.Context, pluginRelease release.PluginRelease) (string, error) { owner, pluginName, found := strings.Cut(pluginRelease.PluginName, "/") if !found { return "", fmt.Errorf("invalid plugin name: %q", pluginRelease.PluginName) } imageName := fmt.Sprintf("ghcr.io/%s/plugins-%s-%s", release.GithubOwnerBufbuild, owner, pluginName) - cmd, err := dockerCmd("manifest", "inspect", "--verbose", imageName+":"+pluginRelease.PluginVersion) - if err != nil { - return "", err - } + cmd := dockerCmd(ctx, "manifest", "inspect", "--verbose", imageName+":"+pluginRelease.PluginVersion) var bb bytes.Buffer cmd.Stdout = &bb if err := cmd.Run(); err != nil { diff --git a/internal/docker/build.go b/internal/docker/build.go index 82ea52a4b..2336907c6 100644 --- a/internal/docker/build.go +++ b/internal/docker/build.go @@ -7,26 +7,22 @@ import ( "os" "os/exec" "path/filepath" + "time" "github.com/bufbuild/plugins/internal/plugin" ) -// Build runs a Docker build command for the specified plugin tagging it with the given organization. +// Build runs a Docker build command for the specified plugin tagging it with the given image name. // The args parameter passes any additional arguments to be passed to the build. // Returns the combined stdout/stderr of the build along with any error. func Build( ctx context.Context, plugin *plugin.Plugin, - dockerOrg string, + imageName string, cachePath string, args []string, ) (_ []byte, retErr error) { - dockerCmd, err := exec.LookPath("docker") - if err != nil { - return nil, err - } identity := plugin.Identity - imageName := ImageName(plugin, dockerOrg) commonArgs := []string{ "buildx", "build", @@ -40,12 +36,19 @@ func Build( "--label", "org.opencontainers.image.source=https://github.com/bufbuild/plugins", "--label", + fmt.Sprintf("org.opencontainers.image.created=%s", time.Now().UTC().Format(time.RFC3339)), + "--label", fmt.Sprintf("org.opencontainers.image.description=%s", plugin.Description), "--label", fmt.Sprintf("org.opencontainers.image.licenses=%s", plugin.SPDXLicenseID), + "--label", + fmt.Sprintf("org.opencontainers.image.vendor=%s", "Buf Technologies, Inc."), "--progress", "plain", } + if gitCommit := plugin.GitCommit(ctx); gitCommit != "" { + commonArgs = append(commonArgs, "--label", fmt.Sprintf("org.opencontainers.image.revision=%s", gitCommit)) + } if cachePath != "" { cacheDir, err := filepath.Abs(cachePath) if err != nil { @@ -71,16 +74,6 @@ func Build( defer func() { retErr = errors.Join(retErr, f.Close()) }() - cmd := exec.CommandContext( - ctx, - dockerCmd, - commonArgs..., - ) - cmd.Args = append( - cmd.Args, - "-t", - imageName, - ) - cmd.Args = append(cmd.Args, filepath.Dir(plugin.Path)) + cmd := exec.CommandContext(ctx, "docker", append(commonArgs, "-t", imageName, filepath.Dir(plugin.Path))...) //nolint:gosec return cmd.CombinedOutput() } diff --git a/internal/docker/push.go b/internal/docker/push.go index a724d3d3a..d3854a5da 100644 --- a/internal/docker/push.go +++ b/internal/docker/push.go @@ -10,14 +10,10 @@ import ( // Push pushes a docker image for the given plugin to the Docker organization. // It assumes it has already been built in a previous step. func Push(ctx context.Context, plugin *plugin.Plugin, dockerOrg string) ([]byte, error) { - dockerCmd, err := exec.LookPath("docker") - if err != nil { - return nil, err - } imageName := ImageName(plugin, dockerOrg) cmd := exec.CommandContext( ctx, - dockerCmd, + "docker", "push", imageName, ) diff --git a/internal/plugin/plugin.go b/internal/plugin/plugin.go index 12e6ae3eb..15568ae78 100644 --- a/internal/plugin/plugin.go +++ b/internal/plugin/plugin.go @@ -1,16 +1,19 @@ package plugin import ( + "bytes" "cmp" "context" "fmt" "io/fs" "log" "os" + "os/exec" "path/filepath" "slices" "strconv" "strings" + "sync" "unicode" "github.com/bufbuild/buf/private/bufpkg/bufplugin/bufpluginconfig" @@ -28,10 +31,13 @@ type Plugin struct { bufpluginconfig.ExternalConfig `yaml:"-"` // Plugin identity (parsed from ExternalConfig.Name). Identity bufpluginref.PluginIdentity `yaml:"-"` + // For callers that need git commit info - ensure we only calculate it once. + gitCommitOnce sync.Once `yaml:"-"` + gitCommit string `yaml:"-"` } func (p *Plugin) String() string { - return fmt.Sprintf("%+v", *p) + return fmt.Sprintf("%s:%s", p.Identity.IdentityString(), p.Version) } // Dependency represents a dependency one plugin has on another. @@ -291,6 +297,48 @@ func (p IncludePlugin) Matches(pluginName, pluginVersion, latestVersion string) return p.version == pluginVersion } +// GitCommit calculates the last git commit for the plugin's directory. +// This will return an empty string if there are uncommitted changes to the plugin's directory. +// This is used to label the built Docker image and also avoid unnecessary Docker builds. +func (p *Plugin) GitCommit(ctx context.Context) string { + p.gitCommitOnce.Do(func() { + if gitModified, err := calculateGitModified(ctx, p.Path); err != nil { + log.Printf("failed to calculate git modified status: %v", err) + } else if !gitModified { + p.gitCommit, err = calculateGitCommit(ctx, p.Path) + if err != nil { + log.Printf("failed to calculate git commit: %v", err) + } + } + }) + return p.gitCommit +} + +// calculateGitCommit returns the last commit in the plugin's directory (used to determine the "revision" of a plugin). +func calculateGitCommit(ctx context.Context, pluginYamlPath string) (string, error) { + cmd := exec.CommandContext(ctx, "git", "log", "-n", "1", "--pretty=%H", filepath.Dir(pluginYamlPath)) //nolint:gosec + var output bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return "", err + } + return strings.TrimSpace(output.String()), nil +} + +// calculateGitModified determines if there are uncommitted changes to the plugin's directory. +// If this returns true, we don't add the plugin's git commit to the built Docker image. +func calculateGitModified(ctx context.Context, pluginYamlPath string) (bool, error) { + cmd := exec.CommandContext(ctx, "git", "status", "--porcelain", filepath.Dir(pluginYamlPath)) //nolint:gosec + var output bytes.Buffer + cmd.Stdout = &output + cmd.Stderr = os.Stderr + if err := cmd.Run(); err != nil { + return false, err + } + return strings.TrimSpace(output.String()) != "", nil +} + // changedFiles contains data from the tj-actions/changed-files action. // See https://github.com/tj-actions/changed-files#outputs for more details. type changedFiles struct { diff --git a/tests/plugins_test.go b/tests/plugins_test.go index 9974273ca..3bad7120b 100644 --- a/tests/plugins_test.go +++ b/tests/plugins_test.go @@ -65,6 +65,7 @@ func TestGeneration(t *testing.T) { if testing.Short() { t.Skip("skipping code generation test") } + ctx := context.Background() allowEmpty, _ := strconv.ParseBool(os.Getenv("ALLOW_EMPTY_PLUGIN_SUM")) testPluginWithImage := func(t *testing.T, pluginMeta *plugin.Plugin, image string) { t.Helper() @@ -78,7 +79,7 @@ func TestGeneration(t *testing.T) { require.NoError(t, os.MkdirAll(pluginDir, 0o755)) require.NoError(t, createBufGenYaml(t, pluginDir, pluginMeta)) require.NoError(t, createProtocGenPlugin(t, pluginDir, pluginMeta)) - bufCmd := exec.Command("buf", "generate", filepath.Join(imageDir, image+".bin.gz")) + bufCmd := exec.CommandContext(ctx, "buf", "generate", filepath.Join(imageDir, image+".bin.gz")) bufCmd.Dir = pluginDir output, err := bufCmd.CombinedOutput() require.NoErrorf(t, err, "buf generate failed - output: %s", string(output))