Skip to content

Commit b5cd6d2

Browse files
authored
Add file output to foreach command (#141)
* Add file output to foreach command * Reorder * Simplify code
1 parent 6197cf2 commit b5cd6d2

File tree

4 files changed

+135
-3
lines changed

4 files changed

+135
-3
lines changed

cmd/foreach/foreach.go

+67-3
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ package foreach
1717

1818
import (
1919
"errors"
20+
"fmt"
2021
"os"
2122
"path"
2223
"strings"
@@ -34,7 +35,15 @@ import (
3435
var exec executor.Executor = executor.NewRealExecutor()
3536

3637
var (
37-
repoFile string = "repos.txt"
38+
repoFile = "repos.txt"
39+
40+
overallResultsDirectory string
41+
42+
successfulResultsDirectory string
43+
successfulReposFileName string
44+
45+
failedResultsDirectory string
46+
failedReposFileName string
3847
)
3948

4049
func formatArguments(arguments []string) string {
@@ -49,8 +58,7 @@ func NewForeachCmd() *cobra.Command {
4958
cmd := &cobra.Command{
5059
Use: "foreach [flags] -- COMMAND [ARGUMENT...]",
5160
Short: "Run COMMAND against each working copy",
52-
Long:
53-
`Run COMMAND against each working copy. Make sure to include a
61+
Long: `Run COMMAND against each working copy. Make sure to include a
5462
double hyphen -- with space on both sides before COMMAND, as this
5563
marks that no further options should be interpreted by turbolift.`,
5664
RunE: runE,
@@ -83,6 +91,10 @@ func runE(c *cobra.Command, args []string) error {
8391
// the user something they could copy and paste.
8492
prettyArgs := formatArguments(args)
8593

94+
setupOutputFiles(dir.Name, prettyArgs)
95+
96+
logger.Printf("Logs for all executions will be stored under %s", overallResultsDirectory)
97+
8698
var doneCount, skippedCount, errorCount int
8799
for _, repo := range dir.Repos {
88100
repoDirPath := path.Join("work", repo.OrgName, repo.RepoName) // i.e. work/org/repo
@@ -99,9 +111,11 @@ func runE(c *cobra.Command, args []string) error {
99111
err := exec.Execute(execActivity.Writer(), repoDirPath, args[0], args[1:]...)
100112

101113
if err != nil {
114+
emitOutcomeToFiles(repo, failedReposFileName, failedResultsDirectory, execActivity.Logs(), logger)
102115
execActivity.EndWithFailure(err)
103116
errorCount++
104117
} else {
118+
emitOutcomeToFiles(repo, successfulReposFileName, successfulResultsDirectory, execActivity.Logs(), logger)
105119
execActivity.EndWithSuccessAndEmitLogs()
106120
doneCount++
107121
}
@@ -113,5 +127,55 @@ func runE(c *cobra.Command, args []string) error {
113127
logger.Warnf("turbolift foreach completed with %s %s(%s, %s, %s)\n", colors.Red("errors"), colors.Normal(), colors.Green(doneCount, " OK"), colors.Yellow(skippedCount, " skipped"), colors.Red(errorCount, " errored"))
114128
}
115129

130+
logger.Printf("Logs for all executions have been stored under %s", overallResultsDirectory)
131+
logger.Printf("Names of successful repos have been written to %s", successfulReposFileName)
132+
logger.Printf("Names of failed repos have been written to %s", failedReposFileName)
133+
116134
return nil
117135
}
136+
137+
// sets up a temporary directory to store success/failure logs etc
138+
func setupOutputFiles(campaignName string, command string) {
139+
overallResultsDirectory, _ = os.MkdirTemp("", fmt.Sprintf("turbolift-foreach-%s-", campaignName))
140+
successfulResultsDirectory = path.Join(overallResultsDirectory, "successful")
141+
failedResultsDirectory = path.Join(overallResultsDirectory, "failed")
142+
_ = os.MkdirAll(successfulResultsDirectory, 0755)
143+
_ = os.MkdirAll(failedResultsDirectory, 0755)
144+
145+
successfulReposFileName = path.Join(successfulResultsDirectory, "repos.txt")
146+
failedReposFileName = path.Join(failedResultsDirectory, "repos.txt")
147+
148+
// create the files
149+
successfulReposFile, _ := os.Create(successfulReposFileName)
150+
failedReposFile, _ := os.Create(failedReposFileName)
151+
defer successfulReposFile.Close()
152+
defer failedReposFile.Close()
153+
154+
_, _ = successfulReposFile.WriteString(fmt.Sprintf("# This file contains the list of repositories that were successfully processed by turbolift foreach\n# for the command: %s\n", command))
155+
_, _ = failedReposFile.WriteString(fmt.Sprintf("# This file contains the list of repositories that failed to be processed by turbolift foreach\n# for the command: %s\n", command))
156+
}
157+
158+
func emitOutcomeToFiles(repo campaign.Repo, reposFileName string, logsDirectoryParent string, executionLogs string, logger *logging.Logger) {
159+
// write the repo name to the repos file
160+
reposFile, _ := os.OpenFile(reposFileName, os.O_RDWR|os.O_APPEND, 0644)
161+
defer reposFile.Close()
162+
_, err := reposFile.WriteString(repo.FullRepoName + "\n")
163+
if err != nil {
164+
logger.Errorf("Failed to write repo name to %s: %s", reposFile.Name(), err)
165+
}
166+
167+
// write logs to a file under the logsParent directory, in a directory structure that mirrors that of the work directory
168+
logsDir := path.Join(logsDirectoryParent, repo.FullRepoName)
169+
logsFile := path.Join(logsDir, "logs.txt")
170+
err = os.MkdirAll(logsDir, 0755)
171+
if err != nil {
172+
logger.Errorf("Failed to create directory %s: %s", logsDir, err)
173+
}
174+
175+
logs, _ := os.Create(logsFile)
176+
defer logs.Close()
177+
_, err = logs.WriteString(executionLogs)
178+
if err != nil {
179+
logger.Errorf("Failed to write logs to %s: %s", logsFile, err)
180+
}
181+
}

cmd/foreach/foreach_test.go

+42
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ package foreach
1818
import (
1919
"bytes"
2020
"os"
21+
"regexp"
2122
"testing"
2223

2324
"github.com/stretchr/testify/assert"
@@ -162,6 +163,47 @@ func TestFormatArguments(t *testing.T) {
162163
}
163164
}
164165

166+
func TestItCreatesLogFiles(t *testing.T) {
167+
fakeExecutor := executor.NewAlternatingSuccessFakeExecutor()
168+
exec = fakeExecutor
169+
170+
testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2")
171+
172+
out, err := runCommand("--", "some", "command")
173+
assert.NoError(t, err)
174+
assert.Contains(t, out, "turbolift foreach completed")
175+
assert.Contains(t, out, "1 OK, 0 skipped, 1 errored")
176+
177+
// Logs should describe where output was written
178+
r := regexp.MustCompile(`Logs for all executions have been stored under (.+)`)
179+
matches := r.FindStringSubmatch(out)
180+
assert.Len(t, matches, 2, "Expected to find the log directory path")
181+
path := matches[1]
182+
183+
// check that expected static directories and files exist
184+
_, err = os.Stat(path)
185+
assert.NoError(t, err, "Expected the log directory to exist")
186+
187+
_, err = os.Stat(path + "/successful")
188+
assert.NoError(t, err, "Expected the successful log directory to exist")
189+
190+
_, err = os.Stat(path + "/failed")
191+
assert.NoError(t, err, "Expected the failure log directory to exist")
192+
193+
_, err = os.Stat(path + "/successful/repos.txt")
194+
assert.NoError(t, err, "Expected the successful repos.txt file to exist")
195+
196+
_, err = os.Stat(path + "/failed/repos.txt")
197+
assert.NoError(t, err, "Expected the failure repos.txt file to exist")
198+
199+
// check that the expected logs files exist
200+
_, err = os.Stat(path + "/successful/org/repo1/logs.txt")
201+
assert.NoError(t, err, "Expected the successful log file for org/repo1 to exist")
202+
203+
_, err = os.Stat(path + "/failed/org/repo2/logs.txt")
204+
assert.NoError(t, err, "Expected the failure log file for org/repo2 to exist")
205+
}
206+
165207
func runCommand(args ...string) (string, error) {
166208
cmd := NewForeachCmd()
167209
outBuffer := bytes.NewBufferString("")

internal/executor/fake_executor.go

+22
Original file line numberDiff line numberDiff line change
@@ -70,3 +70,25 @@ func NewAlwaysFailsFakeExecutor() *FakeExecutor {
7070
return "", errors.New("synthetic error")
7171
})
7272
}
73+
74+
func NewAlternatingSuccessFakeExecutor() *FakeExecutor {
75+
i := 0
76+
return NewFakeExecutor(
77+
func(s string, s2 string, s3 ...string) error {
78+
i++
79+
if i%2 == 1 {
80+
return nil
81+
} else {
82+
return errors.New("synthetic error")
83+
}
84+
},
85+
func(s string, s2 string, s3 ...string) (string, error) {
86+
i++
87+
if i%2 == 1 {
88+
return "", nil
89+
} else {
90+
return "", errors.New("synthetic error")
91+
}
92+
},
93+
)
94+
}

internal/logging/activity.go

+4
Original file line numberDiff line numberDiff line change
@@ -110,3 +110,7 @@ func (a *Activity) Writer() io.Writer {
110110
activity: a,
111111
}
112112
}
113+
114+
func (a *Activity) Logs() string {
115+
return strings.Join(a.logs, "\n")
116+
}

0 commit comments

Comments
 (0)