Skip to content

Commit 6e8df3f

Browse files
committed
more log line grouping in GitHub actions
1 parent 886cc84 commit 6e8df3f

File tree

3 files changed

+122
-4
lines changed

3 files changed

+122
-4
lines changed

cmd/bob/build.go

Lines changed: 40 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@ package main
33
import (
44
"errors"
55
"fmt"
6+
"io"
67
"os"
78
"os/exec"
89
"path/filepath"
@@ -35,7 +36,27 @@ func runBuilder(builder BuilderSpec, buildCtx *BuildContext, opDesc string, cmdT
3536
return errWd
3637
}
3738

38-
printHeading(fmt.Sprintf("%s/%s (%s)", builder.Name, opDesc, builderCommandToHumanReadable(cmdToRun)))
39+
builderNameOpDesc := fmt.Sprintf("%s/%s", builder.Name, opDesc)
40+
41+
switch must(parseBuilderUsesType(builder.Uses)) {
42+
case builderUsesTypeImage:
43+
// the "$ docker run ..." later would do an implicit pull, but let's do an explicit pull
44+
// here in order to nicely put the download progress output in its own log line group
45+
if err := withLogLineGroup(fmt.Sprintf("%s > pull", builderNameOpDesc), func() error {
46+
return dockerPullIfRequired(builderImageName(buildCtx.Bobfile, builder))
47+
}); err != nil {
48+
return err
49+
}
50+
case builderUsesTypeDockerfile:
51+
// no-op (doesn't need a pull)
52+
default:
53+
panic("unknown builderType")
54+
}
55+
56+
// empty work just to emit a "starting" log group. this log group is important because if the
57+
// command-to-run itself doesn't create log groups (to which we could insert script name), then
58+
// script name won't be visible at all in none of the group names
59+
_ = withLogLineGroup(fmt.Sprintf("%s > starting %s", builderNameOpDesc, builderCommandToHumanReadable(cmdToRun)), func() error { return nil })
3960

4061
buildArgs := []string{
4162
"docker",
@@ -78,7 +99,23 @@ func runBuilder(builder BuilderSpec, buildCtx *BuildContext, opDesc string, cmdT
7899
}
79100

80101
//nolint:gosec // ok
81-
buildCmd := passthroughStdoutAndStderr(exec.Command(buildArgs[0], buildArgs[1:]...))
102+
buildCmd := exec.Command(buildArgs[0], buildArgs[1:]...)
103+
buildCmd.Stdout = newLineSplitterTee(io.Discard, func(line string) {
104+
lineMaybeModified := func() string {
105+
// for each log line group, add "breadcrumb prefix" of builder / operation description.
106+
// example group name: "staticAnalysis" => "default/build > staticAnalysis"
107+
if strings.HasPrefix(line, "::group::") {
108+
originalGroupName := line[len("::group::"):]
109+
110+
return fmt.Sprintf("::group::%s > %s", builderNameOpDesc, originalGroupName)
111+
} else {
112+
return line // as-is
113+
}
114+
}()
115+
116+
_, _ = os.Stdout.Write([]byte(lineMaybeModified + "\n")) // need to add newline back
117+
})
118+
buildCmd.Stderr = os.Stderr
82119

83120
if err := buildCmd.Run(); err != nil {
84121
return err
@@ -435,7 +472,7 @@ func buildEntry() *cobra.Command {
435472
Args: cobra.NoArgs,
436473
Run: func(cmd *cobra.Command, args []string) {
437474
osutil.ExitIfError(func() error {
438-
if os.Getenv("GITHUB_ACTIONS") != "true" {
475+
if !runningInGitHubActions() {
439476
return errors.New("expecting GITHUB_ACTIONS=true")
440477
}
441478

cmd/bob/docker.go

Lines changed: 15 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -159,7 +159,7 @@ func dockerRelayEnvVars(
159159

160160
// "Always set to true when GitHub Actions is running the workflow. You can use this variable to
161161
// differentiate when tests are being run locally or by GitHub Actions."
162-
if os.Getenv("GITHUB_ACTIONS") == "true" { // relay as-is
162+
if runningInGitHubActions() { // relay as-is
163163
env("GITHUB_ACTIONS", "true")
164164
}
165165

@@ -212,6 +212,20 @@ func loginToDockerRegistry(dockerImage DockerImageSpec, cache *dockerRegistryLog
212212
return nil
213213
}
214214

215+
func dockerPullIfRequired(imageRef string) error {
216+
// there's no flag to "$ docker pull" that could allow us to say "please don't check the registry
217+
// for newest version of that tag if we already have any version", so let's first query Docker
218+
// if we already have the image, in order to not query the registry many times. use cases:
219+
// - repeated "$ bob build" invocations
220+
// - same builder used multiple times in the project
221+
alreadyHave := exec.Command("docker", "image", "inspect", imageRef).Run()
222+
if alreadyHave == nil { // this has error if we don't have the image yet
223+
return nil
224+
}
225+
226+
return passthroughStdoutAndStderr(exec.Command("docker", "pull", imageRef)).Run()
227+
}
228+
215229
type dockerRegistryLoginCacheKey string
216230

217231
func newDockerRegistryLoginCacheKey(registry string, creds DockerCredentials) dockerRegistryLoginCacheKey {

cmd/bob/utils.go

Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,12 @@
11
package main
22

33
import (
4+
"fmt"
5+
"io"
46
"os"
57
"os/exec"
8+
"strings"
9+
"sync"
610
)
711

812
func passthroughStdoutAndStderr(cmd *exec.Cmd) *exec.Cmd {
@@ -24,3 +28,66 @@ func allDevShellCommands(devShellCommands []DevShellCommand) []string {
2428

2529
return commands
2630
}
31+
32+
func runningInGitHubActions() bool {
33+
return os.Getenv("GITHUB_ACTIONS") == "true"
34+
}
35+
36+
func must(input builderUsesType, _ string, err error) builderUsesType {
37+
if err != nil {
38+
panic(err)
39+
}
40+
return input
41+
}
42+
43+
func withLogLineGroup(group string, work func() error) error {
44+
if !runningInGitHubActions() {
45+
printHeading(group)
46+
return work()
47+
}
48+
49+
fmt.Println("::group::" + group)
50+
51+
err := work()
52+
53+
fmt.Println("::endgroup::")
54+
55+
return err
56+
}
57+
58+
// FIXME: lineSplitterTee should maybe go to gokit? other use is in Varasto
59+
60+
type lineSplitterTee struct {
61+
buf []byte // buffer before receiving \n
62+
lineCompleted func(string)
63+
mu sync.Mutex
64+
}
65+
66+
// returns io.Writer that tees full lines to lineCompleted callback
67+
func newLineSplitterTee(sink io.Writer, lineCompleted func(string)) io.Writer {
68+
return io.MultiWriter(sink, &lineSplitterTee{
69+
buf: []byte{},
70+
lineCompleted: lineCompleted,
71+
})
72+
}
73+
74+
func (l *lineSplitterTee) Write(data []byte) (int, error) {
75+
l.mu.Lock()
76+
defer l.mu.Unlock()
77+
78+
l.buf = append(l.buf, data...)
79+
80+
// as long as we have lines, chop the buffer down
81+
for {
82+
idx := strings.IndexByte(string(l.buf), '\n')
83+
if idx == -1 {
84+
break
85+
}
86+
87+
l.lineCompleted(string(l.buf[0:idx]))
88+
89+
l.buf = l.buf[idx+1:]
90+
}
91+
92+
return len(data), nil
93+
}

0 commit comments

Comments
 (0)