diff --git a/README.md b/README.md index d7abdaa..6b0ee30 100644 --- a/README.md +++ b/README.md @@ -204,6 +204,25 @@ export SHUTTLE_CACHE_DURATION_MIN=60 # Cache a plan for 60 minutes This feature caches pr. repo, as such the cache isn't shared between working repositories. +#### Skipping the pull + +Every shuttle invocation pulls the plan again, which is wasted time in CI where +the plan was just cloned and cannot have changed during the job. Shuttle +therefore skips pulling an already cloned plan when the `CI` environment +variable is set, as most CI systems do. + +Use `SHUTTLE_SKIP_PULL` to control this explicitly. It takes precedence over +`CI`, so it can also force pulling back on: + +```bash +export SHUTTLE_SKIP_PULL=true # never pull an already cloned plan +export SHUTTLE_SKIP_PULL=false # always pull, even when CI is set +``` + +The `--skip-pull` flag does the same for a single invocation. Note that none of +these prevent the initial clone; a plan that isn't available locally yet is +always cloned. + ### Overloading the plan It is possible to overload the plan specified in `shuttle.yaml` file by using diff --git a/pkg/git/git.go b/pkg/git/git.go index 25e356a..4a106c1 100644 --- a/pkg/git/git.go +++ b/pkg/git/git.go @@ -29,6 +29,34 @@ var gitRegex = regexp.MustCompile( const cacheDurationMinKey = "SHUTTLE_CACHE_DURATION_MIN" +const skipPullKey = "SHUTTLE_SKIP_PULL" + +// skipPullFromEnv reports whether plan pulling should be skipped based on the +// environment. SHUTTLE_SKIP_PULL is the explicit opt in and takes precedence, +// so it can also force pulling back on with a falsy value. Otherwise CI is +// honoured, as CI jobs start from a fresh clone and pulling the plan again on +// every shuttle invocation only costs time. +func skipPullFromEnv(uii *ui.UI) bool { + if v, ok := os.LookupEnv(skipPullKey); ok { + skip, err := strconv.ParseBool(v) + if err != nil { + uii.Verboseln("%s is not a boolean, treating '%s' as false", skipPullKey, v) + return false + } + if skip { + uii.Verboseln("Skipping git plan pulling because %s=%s", skipPullKey, v) + } + return skip + } + + if os.Getenv("CI") != "" { + uii.Verboseln("Skipping git plan pulling because CI is set") + return true + } + + return false +} + func ParsePlan(plan string) Plan { if !gitRegex.MatchString(plan) { return Plan{ @@ -121,6 +149,9 @@ func GetGitPlan( uii.Verboseln("Skipping git plan pulling") return planPath, nil } + if skipPullFromEnv(uii) { + return planPath, nil + } valid, err := cacheIsValid(planPath) if err != nil { return "", err @@ -172,7 +203,7 @@ func GetGitPlan( if cloneToken != "" { uii.Verboseln("Found clone token in env, but shuttle path was ssh-based. This override will not work.") } - + cloneArg = parsedGitPlan.User + "@" + parsedGitPlan.Repository } else { panic(fmt.Sprintf("Unknown protocol '%s'", parsedGitPlan.Protocol)) diff --git a/pkg/git/skip_pull_test.go b/pkg/git/skip_pull_test.go new file mode 100644 index 0000000..b9e49c6 --- /dev/null +++ b/pkg/git/skip_pull_test.go @@ -0,0 +1,86 @@ +package git + +import ( + "io" + "os" + "testing" + + "github.com/lunarway/shuttle/pkg/ui" + "github.com/stretchr/testify/assert" +) + +func TestSkipPullFromEnv(t *testing.T) { + tt := []struct { + name string + skipPull *string + ci *string + skipsPull bool + }{ + { + name: "nothing set", + skipsPull: false, + }, + { + name: "CI set", + ci: strPtr("true"), + skipsPull: true, + }, + { + name: "CI set to any non-empty value", + ci: strPtr("1"), + skipsPull: true, + }, + { + name: "CI set but empty", + ci: strPtr(""), + skipsPull: false, + }, + { + name: "SHUTTLE_SKIP_PULL set", + skipPull: strPtr("true"), + skipsPull: true, + }, + { + name: "SHUTTLE_SKIP_PULL set but empty is treated as false", + skipPull: strPtr(""), + skipsPull: false, + }, + { + name: "SHUTTLE_SKIP_PULL takes precedence over CI", + skipPull: strPtr("false"), + ci: strPtr("true"), + skipsPull: false, + }, + { + name: "SHUTTLE_SKIP_PULL with an unparsable value is treated as false", + skipPull: strPtr("yes-please"), + skipsPull: false, + }, + } + + for _, tc := range tt { + t.Run(tc.name, func(t *testing.T) { + // Unset by default so the developer's own environment does not leak + // into the test. + t.Setenv(skipPullKey, "") + os.Unsetenv(skipPullKey) + t.Setenv("CI", "") + os.Unsetenv("CI") + + if tc.skipPull != nil { + t.Setenv(skipPullKey, *tc.skipPull) + } + if tc.ci != nil { + t.Setenv("CI", *tc.ci) + } + + uii := ui.Create(io.Discard, io.Discard) + + assert.Equal(t, tc.skipsPull, skipPullFromEnv(uii)) + }) + } +} + +func strPtr(s string) *string { + return &s +}