Skip to content

Commit 12f4e94

Browse files
authored
feat(internal/command): add command execution package with logging (#2968)
An internal/command package is created to execute external commands and print them to stderr before execution, in order to provide progress reports to the user. Existing call sites from internal/sidekick/external are migrated to use the new package and the old package is deleted. For #2966
1 parent 265ee47 commit 12f4e94

File tree

13 files changed

+89
-99
lines changed

13 files changed

+89
-99
lines changed

internal/sidekick/external/external.go renamed to internal/command/command.go

Lines changed: 7 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -12,25 +12,21 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
// Package external provides helper functions to work with external commands.
16-
package external
15+
// Package command provides helpers to execute external commands with logging.
16+
package command
1717

1818
import (
1919
"fmt"
20+
"os"
2021
"os/exec"
2122
)
2223

23-
// Exec executes a command and captures any error output.
24-
func Exec(cmd *exec.Cmd) error {
24+
// Run executes a program (with arguments) and captures any error output.
25+
func Run(command string, arg ...string) error {
26+
cmd := exec.Command(command, arg...)
27+
fmt.Fprintf(os.Stderr, "Running: %s\n", cmd.String())
2528
if output, err := cmd.CombinedOutput(); err != nil {
2629
return fmt.Errorf("%v: %v\n%s", cmd, err, output)
2730
}
2831
return nil
2932
}
30-
31-
// Run executes a program (with arguments) and captures any error output.
32-
func Run(command string, arg ...string) error {
33-
cmd := exec.Command(command, arg...)
34-
cmd.Dir = "."
35-
return Exec(cmd)
36-
}

internal/sidekick/external/external_test.go renamed to internal/command/command_test.go

Lines changed: 10 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -12,32 +12,25 @@
1212
// See the License for the specific language governing permissions and
1313
// limitations under the License.
1414

15-
package external
15+
package command
1616

1717
import (
18-
"os/exec"
18+
"strings"
1919
"testing"
2020
)
2121

22-
func TestSuccess(t *testing.T) {
23-
// "go" must be installed, otherwise: how are you running the unit tests?
24-
cmd := exec.Command("go", "help")
25-
if err := Exec(cmd); err != nil {
22+
func TestRun(t *testing.T) {
23+
if err := Run("go", "version"); err != nil {
2624
t.Fatal(err)
2725
}
2826
}
2927

30-
func TestError(t *testing.T) {
31-
// Seems unlikely that `go` will gain this subcommand. I will buy you a cold
32-
// beverage if I am wrong.
33-
cmd := exec.Command("go", "invalid-subcommand-bad-bad-bad")
34-
if err := Exec(cmd); err == nil {
35-
t.Errorf("expected an error using go invalid-subcommand-bad-bad-bad")
28+
func TestRunError(t *testing.T) {
29+
err := Run("go", "invalid-subcommand-bad-bad-bad")
30+
if err == nil {
31+
t.Fatal("expected error, got nil")
3632
}
37-
}
38-
39-
func TestRun(t *testing.T) {
40-
if err := Run("go", "help"); err != nil {
41-
t.Fatal(err)
33+
if !strings.Contains(err.Error(), "invalid-subcommand-bad-bad-bad") {
34+
t.Errorf("error should mention the invalid subcommand, got: %v", err)
4235
}
4336
}

internal/sidekick/dart/dart.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,8 @@ import (
2121
"strings"
2222
"unicode"
2323

24+
"github.com/googleapis/librarian/internal/command"
2425
"github.com/googleapis/librarian/internal/sidekick/api"
25-
"github.com/googleapis/librarian/internal/sidekick/external"
2626
"github.com/iancoleman/strcase"
2727
)
2828

@@ -257,7 +257,7 @@ func shouldGenerateMethod(m *api.Method) bool {
257257
}
258258

259259
func formatDirectory(dir string) error {
260-
if err := external.Run("dart", "format", dir); err != nil {
260+
if err := command.Run("dart", "format", dir); err != nil {
261261
return fmt.Errorf("got an error trying to run `dart format`; perhaps try https://dart.dev/get-dart (%w)", err)
262262
}
263263
return nil

internal/sidekick/rust_prost/generate.go

Lines changed: 8 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -21,9 +21,9 @@ import (
2121
"os/exec"
2222
"path/filepath"
2323

24+
"github.com/googleapis/librarian/internal/command"
2425
"github.com/googleapis/librarian/internal/sidekick/api"
2526
"github.com/googleapis/librarian/internal/sidekick/config"
26-
"github.com/googleapis/librarian/internal/sidekick/external"
2727
"github.com/googleapis/librarian/internal/sidekick/language"
2828
)
2929

@@ -35,10 +35,10 @@ func Generate(model *api.API, outdir string, cfg *config.Config) error {
3535
if cfg.General.SpecificationFormat != "protobuf" {
3636
return fmt.Errorf("the `rust+prost` generator only supports `protobuf` as a specification source, outdir=%s", outdir)
3737
}
38-
if err := external.Run("cargo", "--version"); err != nil {
38+
if err := command.Run("cargo", "--version"); err != nil {
3939
return fmt.Errorf("got an error trying to run `cargo --version`, the instructions on https://www.rust-lang.org/learn/get-started may solve this problem: %w", err)
4040
}
41-
if err := external.Run("protoc", "--version"); err != nil {
41+
if err := command.Run("protoc", "--version"); err != nil {
4242
return fmt.Errorf("got an error trying to run `protoc --version`, the instructions on https://grpc.io/docs/protoc-installation/ may solve this problem: %w", err)
4343
}
4444

@@ -81,5 +81,9 @@ func buildRS(rootName, tmpDir, outDir string) error {
8181
cmd.Dir = tmpDir
8282
cmd.Env = append(os.Environ(), fmt.Sprintf("SOURCE_ROOT=%s", absRoot))
8383
cmd.Env = append(cmd.Env, fmt.Sprintf("DEST=%s", absOutDir))
84-
return external.Exec(cmd)
84+
fmt.Fprintf(os.Stderr, "Running: %s\n", cmd.String())
85+
if output, err := cmd.CombinedOutput(); err != nil {
86+
return fmt.Errorf("%v: %v\n%s", cmd, err, output)
87+
}
88+
return nil
8589
}

internal/sidekick/rust_release/bump_versions.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,8 @@ import (
1919
"log/slog"
2020
"slices"
2121

22+
"github.com/googleapis/librarian/internal/command"
2223
"github.com/googleapis/librarian/internal/sidekick/config"
23-
"github.com/googleapis/librarian/internal/sidekick/external"
2424
)
2525

2626
// BumpVersions finds all the crates that need a version bump and performs the
@@ -54,7 +54,7 @@ func BumpVersions(config *config.Release) error {
5454
}
5555
for _, name := range crates {
5656
slog.Info("runnning cargo semver-checks", "crate", name)
57-
if err := external.Run(cargoExe(config), "semver-checks", "--all-features", "-p", name); err != nil {
57+
if err := command.Run(cargoExe(config), "semver-checks", "--all-features", "-p", name); err != nil {
5858
return err
5959
}
6060
}

internal/sidekick/rust_release/bump_versions_test.go

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,8 @@ import (
2323
"strings"
2424
"testing"
2525

26+
"github.com/googleapis/librarian/internal/command"
2627
"github.com/googleapis/librarian/internal/sidekick/config"
27-
"github.com/googleapis/librarian/internal/sidekick/external"
2828
)
2929

3030
const (
@@ -58,7 +58,7 @@ func TestBumpVersionsSuccess(t *testing.T) {
5858
if err := os.WriteFile(name, []byte(newLibRsContents), 0644); err != nil {
5959
t.Fatal(err)
6060
}
61-
if err := external.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
61+
if err := command.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
6262
t.Fatal(err)
6363
}
6464
if err := BumpVersions(config); err != nil {
@@ -87,7 +87,7 @@ func TestBumpVersionsNoCargoTools(t *testing.T) {
8787
if err := os.WriteFile(name, []byte(newLibRsContents), 0644); err != nil {
8888
t.Fatal(err)
8989
}
90-
if err := external.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
90+
if err := command.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
9191
t.Fatal(err)
9292
}
9393
if err := BumpVersions(config); err != nil {
@@ -116,7 +116,7 @@ func TestBumpVersionsNoSemverChecks(t *testing.T) {
116116
if err := os.WriteFile(name, []byte(newLibRsContents), 0644); err != nil {
117117
t.Fatal(err)
118118
}
119-
if err := external.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
119+
if err := command.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
120120
t.Fatal(err)
121121
}
122122
if err := BumpVersions(config); err != nil {
@@ -167,7 +167,7 @@ func TestBumpVersionsManifestError(t *testing.T) {
167167
if err := os.WriteFile(name, []byte("invalid-toml-file = {"), 0644); err != nil {
168168
t.Fatal(err)
169169
}
170-
if err := external.Run("git", "commit", "-m", "feat: broke storage manifest file", "."); err != nil {
170+
if err := command.Run("git", "commit", "-m", "feat: broke storage manifest file", "."); err != nil {
171171
t.Fatal(err)
172172
}
173173
if err := BumpVersions(config); err == nil {
@@ -179,12 +179,12 @@ func setupForVersionBump(t *testing.T, wantTag string) {
179179
remoteDir := t.TempDir()
180180
continueInNewGitRepository(t, remoteDir)
181181
initRepositoryContents(t)
182-
if err := external.Run("git", "tag", wantTag); err != nil {
182+
if err := command.Run("git", "tag", wantTag); err != nil {
183183
t.Fatal(err)
184184
}
185185
cloneDir := t.TempDir()
186186
t.Chdir(cloneDir)
187-
if err := external.Run("git", "clone", remoteDir, "."); err != nil {
187+
if err := command.Run("git", "clone", remoteDir, "."); err != nil {
188188
t.Fatal(err)
189189
}
190190
configNewGitRepository(t)
@@ -194,14 +194,14 @@ func setupForPublish(t *testing.T, wantTag string) string {
194194
remoteDir := t.TempDir()
195195
continueInNewGitRepository(t, remoteDir)
196196
initRepositoryContents(t)
197-
if err := external.Run("git", "tag", wantTag); err != nil {
197+
if err := command.Run("git", "tag", wantTag); err != nil {
198198
t.Fatal(err)
199199
}
200200
name := path.Join("src", "storage", "src", "lib.rs")
201201
if err := os.WriteFile(name, []byte(newLibRsContents), 0644); err != nil {
202202
t.Fatal(err)
203203
}
204-
if err := external.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
204+
if err := command.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
205205
t.Fatal(err)
206206
}
207207
return remoteDir
@@ -210,7 +210,7 @@ func setupForPublish(t *testing.T, wantTag string) string {
210210
func cloneRepository(t *testing.T, remoteDir string) {
211211
cloneDir := t.TempDir()
212212
t.Chdir(cloneDir)
213-
if err := external.Run("git", "clone", remoteDir, "."); err != nil {
213+
if err := command.Run("git", "clone", remoteDir, "."); err != nil {
214214
t.Fatal(err)
215215
}
216216
configNewGitRepository(t)
@@ -220,7 +220,7 @@ func continueInNewGitRepository(t *testing.T, tmpDir string) {
220220
t.Helper()
221221
requireCommand(t, "git")
222222
t.Chdir(tmpDir)
223-
if err := external.Run("git", "init", "-b", "main"); err != nil {
223+
if err := command.Run("git", "init", "-b", "main"); err != nil {
224224
t.Fatal(err)
225225
}
226226
configNewGitRepository(t)
@@ -265,10 +265,10 @@ func requireGitVersion(t *testing.T) {
265265
}
266266

267267
func configNewGitRepository(t *testing.T) {
268-
if err := external.Run("git", "config", "user.email", "[email protected]"); err != nil {
268+
if err := command.Run("git", "config", "user.email", "[email protected]"); err != nil {
269269
t.Fatal(err)
270270
}
271-
if err := external.Run("git", "config", "user.name", "Test Account"); err != nil {
271+
if err := command.Run("git", "config", "user.name", "Test Account"); err != nil {
272272
t.Fatal(err)
273273
}
274274
}
@@ -283,10 +283,10 @@ func initRepositoryContents(t *testing.T) {
283283
addCrate(t, path.Join("src", "gax-internal"), "google-cloud-gax-internal")
284284
addCrate(t, path.Join("src", "gax-internal", "echo-server"), "echo-server")
285285
addGeneratedCrate(t, path.Join("src", "generated", "cloud", "secretmanager", "v1"), "google-cloud-secretmanager-v1")
286-
if err := external.Run("git", "add", "."); err != nil {
286+
if err := command.Run("git", "add", "."); err != nil {
287287
t.Fatal(err)
288288
}
289-
if err := external.Run("git", "commit", "-m", "initial version"); err != nil {
289+
if err := command.Run("git", "commit", "-m", "initial version"); err != nil {
290290
t.Fatal(err)
291291
}
292292
}

internal/sidekick/rust_release/changes_test.go

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -20,8 +20,8 @@ import (
2020
"testing"
2121

2222
"github.com/google/go-cmp/cmp"
23+
"github.com/googleapis/librarian/internal/command"
2324
"github.com/googleapis/librarian/internal/sidekick/config"
24-
"github.com/googleapis/librarian/internal/sidekick/external"
2525
)
2626

2727
const (
@@ -63,10 +63,10 @@ func TestMatchesDirtyCloneError(t *testing.T) {
6363
remoteDir := setupForPublish(t, "v1.0.0")
6464
cloneRepository(t, remoteDir)
6565
addCrate(t, path.Join("src", "pubsub"), "google-cloud-pubsub")
66-
if err := external.Run("git", "add", path.Join("src", "pubsub")); err != nil {
66+
if err := command.Run("git", "add", path.Join("src", "pubsub")); err != nil {
6767
t.Fatal(err)
6868
}
69-
if err := external.Run("git", "commit", "-m", "feat: created pubsub", "."); err != nil {
69+
if err := command.Run("git", "commit", "-m", "feat: created pubsub", "."); err != nil {
7070
t.Fatal(err)
7171
}
7272

@@ -91,10 +91,10 @@ func TestIsNewFile(t *testing.T) {
9191
if err := os.WriteFile(newName, []byte(newLibRsContents), 0644); err != nil {
9292
t.Fatal(err)
9393
}
94-
if err := external.Run("git", "add", "."); err != nil {
94+
if err := command.Run("git", "add", "."); err != nil {
9595
t.Fatal(err)
9696
}
97-
if err := external.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
97+
if err := command.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
9898
t.Fatal(err)
9999
}
100100
if isNewFile(&release, wantTag, existingName) {
@@ -136,7 +136,7 @@ func TestFilesChangedSuccess(t *testing.T) {
136136
if err := os.WriteFile(name, []byte(newLibRsContents), 0644); err != nil {
137137
t.Fatal(err)
138138
}
139-
if err := external.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
139+
if err := command.Run("git", "commit", "-m", "feat: changed storage", "."); err != nil {
140140
t.Fatal(err)
141141
}
142142

internal/sidekick/rust_release/preflight.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -18,19 +18,19 @@ import (
1818
"fmt"
1919
"log/slog"
2020

21+
"github.com/googleapis/librarian/internal/command"
2122
"github.com/googleapis/librarian/internal/sidekick/config"
22-
"github.com/googleapis/librarian/internal/sidekick/external"
2323
)
2424

2525
// PreFlight() verifies all the necessary tools are installed.
2626
func PreFlight(config *config.Release) error {
27-
if err := external.Run(gitExe(config), "--version"); err != nil {
27+
if err := command.Run(gitExe(config), "--version"); err != nil {
2828
return err
2929
}
30-
if err := external.Run(cargoExe(config), "--version"); err != nil {
30+
if err := command.Run(cargoExe(config), "--version"); err != nil {
3131
return err
3232
}
33-
if err := external.Run(gitExe(config), "remote", "get-url", config.Remote); err != nil {
33+
if err := command.Run(gitExe(config), "remote", "get-url", config.Remote); err != nil {
3434
return err
3535
}
3636
tools, ok := config.Tools["cargo"]
@@ -40,7 +40,7 @@ func PreFlight(config *config.Release) error {
4040
for _, tool := range tools {
4141
slog.Info("installing cargo tool", "name", tool.Name, "version", tool.Version)
4242
spec := fmt.Sprintf("%s@%s", tool.Name, tool.Version)
43-
if err := external.Run(cargoExe(config), "install", "--locked", spec); err != nil {
43+
if err := command.Run(cargoExe(config), "install", "--locked", spec); err != nil {
4444
return err
4545
}
4646
}

internal/sidekick/rust_release/publish.go

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,13 +19,13 @@ import (
1919
"log/slog"
2020
"maps"
2121
"os"
22-
"os/exec"
22+
stdexec "os/exec"
2323
"slices"
2424
"strings"
2525

2626
"github.com/google/go-cmp/cmp"
27+
"github.com/googleapis/librarian/internal/command"
2728
"github.com/googleapis/librarian/internal/sidekick/config"
28-
"github.com/googleapis/librarian/internal/sidekick/external"
2929
)
3030

3131
// Publish finds all the crates that should be published, (optionally) runs
@@ -56,7 +56,7 @@ func Publish(config *config.Release, dryRun bool, skipSemverChecks bool) error {
5656
}
5757
}
5858
slog.Info("computing publication plan with: cargo workspaces plan")
59-
cmd := exec.Command(cargoExe(config), "workspaces", "plan", "--skip-published")
59+
cmd := stdexec.Command(cargoExe(config), "workspaces", "plan", "--skip-published")
6060
if config.RootsPem != "" {
6161
cmd.Env = append(os.Environ(), fmt.Sprintf("CARGO_HTTP_CAINFO=%s", config.RootsPem))
6262
}
@@ -85,7 +85,7 @@ func Publish(config *config.Release, dryRun bool, skipSemverChecks bool) error {
8585
continue
8686
}
8787
slog.Info("runnning cargo semver-checks to detect breaking changes", "crate", name)
88-
if err := external.Run(cargoExe(config), "semver-checks", "--all-features", "-p", name); err != nil {
88+
if err := command.Run(cargoExe(config), "semver-checks", "--all-features", "-p", name); err != nil {
8989
return err
9090
}
9191
}
@@ -95,7 +95,7 @@ func Publish(config *config.Release, dryRun bool, skipSemverChecks bool) error {
9595
if dryRun {
9696
args = append(args, "--dry-run")
9797
}
98-
cmd = exec.Command(cargoExe(config), args...)
98+
cmd = stdexec.Command(cargoExe(config), args...)
9999
if config.RootsPem != "" {
100100
cmd.Env = append(os.Environ(), fmt.Sprintf("CARGO_HTTP_CAINFO=%s", config.RootsPem))
101101
}

0 commit comments

Comments
 (0)