Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(foreach): run against previously failed or successful repos #147

Merged
merged 7 commits into from
Nov 18, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 17 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -159,9 +159,24 @@ It is highly recommended that you run tests against affected repos, if it will h
#### Logging and re-running with foreach

Every time a command is run with `turbolift foreach`, logging output for each repository is collected in a temporary directory
divided into `successful` and `failed` subdirectories. Each of these also contains a separate file listing all the repositories that succeeded or failed.
with the following structure:

You can use `--successful` or `--failed` to run a foreach command only against the repositories that succeeded or failed in the preceding command.
```
temp-dir
\ successful
\ repos.txt # a list of repos where the command succeeded
\ org
\ repo
\ logs.txt # logs from the specific foreach execution on this repo
....
\ failed
\ repos.txt # a list of repos where the command succeeded
\ org
\ repo
\ logs.txt # logs from the specific foreach execution on this repo
```

You can use `--successful` or `--failed` to run a foreach command only against the repositories that succeeded or failed in the preceding foreach execution.

```
turbolift foreach --failed -- make test
Expand Down
38 changes: 16 additions & 22 deletions cmd/foreach/foreach.go
Original file line number Diff line number Diff line change
Expand Up @@ -43,13 +43,13 @@ var (

successfulResultsDirectory string
successfulReposFileName string
successfulReposSymlink = ".latest_successful"

failedResultsDirectory string
failedReposFileName string
failedReposSymlink = ".latest_failed"
)

const previousResultsSymlink = ".previous_results"

func formatArguments(arguments []string) string {
quotedArgs := make([]string, len(arguments))
for i, arg := range arguments {
Expand Down Expand Up @@ -99,18 +99,21 @@ func runE(c *cobra.Command, args []string) error {
return errors.New("only one repositories flag or option may be specified: either --successful; --failed; or --repos <file>")
}
if successful {
var err error
if repoFile, err = os.Readlink(successfulReposSymlink); err != nil {
return errors.New("no previous successful foreach logs found")
previousResults, err := os.Readlink(previousResultsSymlink)
if err != nil {
return errors.New("no previous foreach logs found")
}
repoFile = path.Join(previousResults, "successful", "repos.txt")
} else if failed {
var err error
if repoFile, err = os.Readlink(failedReposSymlink); err != nil {
return errors.New("no previous failed foreach logs found")
previousResults, err := os.Readlink(previousResultsSymlink)
if err != nil {
return errors.New("no previous foreach logs found")
}
repoFile = path.Join(previousResults, "failed", "repos.txt")
} else if !customRepoFile {
repoFile = "repos.txt"
}

readCampaignActivity := logger.StartActivity("Reading campaign data (%s)", repoFile)
options := campaign.NewCampaignOptions()
options.RepoFilename = repoFile
Expand Down Expand Up @@ -185,25 +188,16 @@ func setupOutputFiles(campaignName string, command string, logger *logging.Logge
defer successfulReposFile.Close()
defer failedReposFile.Close()

if _, err := os.Lstat(successfulReposSymlink); err == nil {
err := os.Remove(successfulReposSymlink)
// create symlink to the results
if _, err := os.Lstat(previousResultsSymlink); err == nil {
err := os.Remove(previousResultsSymlink)
if err != nil {
logger.Warnf("Failed to remove previous symlink for successful repos: %v", err)
}
}
err := os.Symlink(successfulReposFileName, successfulReposSymlink)
if err != nil {
logger.Warnf("Failed to create symlink for successful repos: %v", err)
}
if _, err := os.Lstat(failedReposSymlink); err == nil {
err := os.Remove(failedReposSymlink)
if err != nil {
logger.Warnf("Failed to remove previous symlink for failed repos: %v", err)
}
}
err = os.Symlink(failedReposFileName, failedReposSymlink)
err := os.Symlink(overallResultsDirectory, previousResultsSymlink)
if err != nil {
logger.Warnf("Failed to create symlink for failed repos: %v", err)
logger.Warnf("Failed to create symlink to foreach results: %v", err)
}

_, _ = 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))
Expand Down
74 changes: 48 additions & 26 deletions cmd/foreach/foreach_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ package foreach
import (
"bytes"
"os"
"path"
"regexp"
"strings"
"testing"

"github.com/stretchr/testify/assert"
Expand Down Expand Up @@ -209,8 +211,11 @@ func TestItRunsAgainstSuccessfulReposOnly(t *testing.T) {
exec = fakeExecutor

testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2", "org/repo3")
testsupport.CreateAnotherRepoFile("successful.txt", "org/repo1", "org/repo3")
testsupport.CreateNewSymlink("successful.txt", ".latest_successful")
err := setUpSymlink()
if err != nil {
t.Errorf("Error setting up symlink: %s", err)
}
defer os.RemoveAll("mock_output")

out, err := runCommandReposSuccessful("--", "some", "command")
assert.NoError(t, err)
Expand All @@ -224,22 +229,18 @@ func TestItRunsAgainstSuccessfulReposOnly(t *testing.T) {
{"work/org/repo1", "some", "command"},
{"work/org/repo3", "some", "command"},
})

// check that the symlink has been updated
successfulRepoFile, err := os.Readlink(".latest_successful")
if err != nil {
panic(err)
}
assert.NotEqual(t, successfulRepoFile, "successful.txt")
}

func TestItRunsAgainstFailedReposOnly(t *testing.T) {
fakeExecutor := executor.NewAlternatingSuccessFakeExecutor()
exec = fakeExecutor

testsupport.PrepareTempCampaign(true, "org/repo1", "org/repo2", "org/repo3")
testsupport.CreateAnotherRepoFile("failed.txt", "org/repo1", "org/repo3")
testsupport.CreateNewSymlink("failed.txt", ".latest_failed")
err := setUpSymlink()
if err != nil {
t.Errorf("Error setting up symlink: %s", err)
}
defer os.RemoveAll("mock_output")

out, err := runCommandReposFailed("--", "some", "command")
assert.NoError(t, err)
Expand All @@ -253,13 +254,6 @@ func TestItRunsAgainstFailedReposOnly(t *testing.T) {
{"work/org/repo1", "some", "command"},
{"work/org/repo3", "some", "command"},
})

// check that the symlink has been updated
failedRepoFile, err := os.Readlink(".latest_failed")
if err != nil {
panic(err)
}
assert.NotEqual(t, failedRepoFile, "failed.txt")
}

func TestItCreatesSymlinksSuccessfully(t *testing.T) {
Expand All @@ -273,25 +267,25 @@ func TestItCreatesSymlinksSuccessfully(t *testing.T) {
assert.Contains(t, out, "turbolift foreach completed")
assert.Contains(t, out, "2 OK, 0 skipped, 1 errored")

successfulRepoFile, err := os.Readlink(".latest_successful")
resultsDir, err := os.Readlink(".previous_results")
Dan7-7-7 marked this conversation as resolved.
Show resolved Hide resolved
if err != nil {
panic(err)

t.Errorf("Error reading symlink: %s", err)
}

successfulRepoFile := path.Join(resultsDir, "successful", "repos.txt")
successfulRepos, err := os.ReadFile(successfulRepoFile)
if err != nil {
panic(err)
t.Errorf("Error reading successful repos: %s", err)
}
assert.Contains(t, string(successfulRepos), "org/repo1")
assert.Contains(t, string(successfulRepos), "org/repo3")
assert.NotContains(t, string(successfulRepos), "org/repo2")

failedRepoFile, err := os.Readlink(".latest_failed")
if err != nil {
panic(err)
}
failedRepoFile := path.Join(resultsDir, "failed", "repos.txt")
failedRepos, err := os.ReadFile(failedRepoFile)
if err != nil {
panic(err)
t.Errorf("Error reading failed repos: %s", err)
}
assert.Contains(t, string(failedRepos), "org/repo2")
assert.NotContains(t, string(failedRepos), "org/repo1")
Expand Down Expand Up @@ -331,6 +325,34 @@ func TestItDoesNotAllowMultipleReposArguments(t *testing.T) {
fakeExecutor.AssertCalledWith(t, [][]string{})
}

func setUpSymlink() error {
err := os.MkdirAll("mock_output/successful", 0755)
if err != nil {
return err
}
err = os.MkdirAll("mock_output/failed", 0755)
if err != nil {
return err
}
err = os.Symlink("mock_output", ".previous_results")
if err != nil {
return err
}
_, err = os.Create("mock_output/successful/repos.txt")
if err != nil {
return err
}
_, err = os.Create("mock_output/failed/repos.txt")
if err != nil {
return err
}
repos := []string{"org/repo1", "org/repo3"}
delimitedList := strings.Join(repos, "\n")
_ = os.WriteFile("mock_output/successful/repos.txt", []byte(delimitedList), os.ModePerm|0o644)
_ = os.WriteFile("mock_output/failed/repos.txt", []byte(delimitedList), os.ModePerm|0o644)
return nil
}

func runCommand(args ...string) (string, error) {
cmd := NewForeachCmd()
outBuffer := bytes.NewBufferString("")
Expand Down
22 changes: 4 additions & 18 deletions internal/testsupport/testsupport.go
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,6 @@ package testsupport

import (
"fmt"
"io/ioutil"
"os"
"path"
"path/filepath"
Expand All @@ -33,7 +32,7 @@ func Pwd() string {
}

func CreateAndEnterTempDirectory() string {
tempDir, _ := ioutil.TempDir("", "turbolift-test-*")
tempDir, _ := os.MkdirTemp("", "turbolift-test-*")
err := os.Chdir(tempDir)
if err != nil {
panic(err)
Expand All @@ -45,7 +44,7 @@ func PrepareTempCampaign(createDirs bool, repos ...string) string {
tempDir := CreateAndEnterTempDirectory()

delimitedList := strings.Join(repos, "\n")
err := ioutil.WriteFile("repos.txt", []byte(delimitedList), os.ModePerm|0o644)
err := os.WriteFile("repos.txt", []byte(delimitedList), os.ModePerm|0o644)
if err != nil {
panic(err)
}
Expand All @@ -61,7 +60,7 @@ func PrepareTempCampaign(createDirs bool, repos ...string) string {
}

dummyPrDescription := "# PR title\nPR body"
err = ioutil.WriteFile("README.md", []byte(dummyPrDescription), os.ModePerm|0o644)
err = os.WriteFile("README.md", []byte(dummyPrDescription), os.ModePerm|0o644)
if err != nil {
panic(err)
}
Expand All @@ -71,7 +70,7 @@ func PrepareTempCampaign(createDirs bool, repos ...string) string {

func CreateAnotherRepoFile(filename string, repos ...string) {
delimitedList := strings.Join(repos, "\n")
err := ioutil.WriteFile(filename, []byte(delimitedList), os.ModePerm|0o644)
err := os.WriteFile(filename, []byte(delimitedList), os.ModePerm|0o644)
if err != nil {
panic(err)
}
Expand All @@ -96,16 +95,3 @@ func UsePrTitleTodoOnly() {
func UsePrBodyTodoOnly() {
CreateOrUpdatePrDescriptionFile("README.md", "updated PR title", originalPrBodyTodo)
}

func CreateNewSymlink(target string, linkName string) {
if _, err := os.Lstat(linkName); err == nil {
err = os.Remove(linkName)
if err != nil {
panic(err)
}
}
err := os.Symlink(target, linkName)
if err != nil {
panic(err)
}
}
Loading