Skip to content

Commit ecb9523

Browse files
committed
feat: skip pulling an already cloned plan when CI is set
Every shuttle invocation pulls the plan again. In CI the plan was just cloned at the start of the job and cannot change during it, so every invocation after the first pays for a pull that can never find anything. Skip pulling when CI is set, which most CI systems do. SHUTTLE_SKIP_PULL is the explicit control and takes precedence, so pulling can be forced back on with a falsy value. This only affects pulling an existing clone. A plan that is not yet available locally is still cloned.
1 parent 7d252cb commit ecb9523

3 files changed

Lines changed: 137 additions & 1 deletion

File tree

README.md

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -204,6 +204,25 @@ export SHUTTLE_CACHE_DURATION_MIN=60 # Cache a plan for 60 minutes
204204
This feature caches pr. repo, as such the cache isn't shared between working
205205
repositories.
206206

207+
#### Skipping the pull
208+
209+
Every shuttle invocation pulls the plan again, which is wasted time in CI where
210+
the plan was just cloned and cannot have changed during the job. Shuttle
211+
therefore skips pulling an already cloned plan when the `CI` environment
212+
variable is set, as most CI systems do.
213+
214+
Use `SHUTTLE_SKIP_PULL` to control this explicitly. It takes precedence over
215+
`CI`, so it can also force pulling back on:
216+
217+
```bash
218+
export SHUTTLE_SKIP_PULL=true # never pull an already cloned plan
219+
export SHUTTLE_SKIP_PULL=false # always pull, even when CI is set
220+
```
221+
222+
The `--skip-pull` flag does the same for a single invocation. Note that none of
223+
these prevent the initial clone; a plan that isn't available locally yet is
224+
always cloned.
225+
207226
### Overloading the plan
208227

209228
It is possible to overload the plan specified in `shuttle.yaml` file by using

pkg/git/git.go

Lines changed: 32 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,34 @@ var gitRegex = regexp.MustCompile(
2929

3030
const cacheDurationMinKey = "SHUTTLE_CACHE_DURATION_MIN"
3131

32+
const skipPullKey = "SHUTTLE_SKIP_PULL"
33+
34+
// skipPullFromEnv reports whether plan pulling should be skipped based on the
35+
// environment. SHUTTLE_SKIP_PULL is the explicit opt in and takes precedence,
36+
// so it can also force pulling back on with a falsy value. Otherwise CI is
37+
// honoured, as CI jobs start from a fresh clone and pulling the plan again on
38+
// every shuttle invocation only costs time.
39+
func skipPullFromEnv(uii *ui.UI) bool {
40+
if v, ok := os.LookupEnv(skipPullKey); ok {
41+
skip, err := strconv.ParseBool(v)
42+
if err != nil {
43+
// An unparsable value is treated as set, matching how CI systems
44+
// tend to use env vars as mere presence flags.
45+
uii.Verboseln("%s is not a boolean, treating '%s' as true", skipPullKey, v)
46+
return true
47+
}
48+
uii.Verboseln("Skipping git plan pulling because %s=%s", skipPullKey, v)
49+
return skip
50+
}
51+
52+
if os.Getenv("CI") != "" {
53+
uii.Verboseln("Skipping git plan pulling because CI is set")
54+
return true
55+
}
56+
57+
return false
58+
}
59+
3260
func ParsePlan(plan string) Plan {
3361
if !gitRegex.MatchString(plan) {
3462
return Plan{
@@ -121,6 +149,9 @@ func GetGitPlan(
121149
uii.Verboseln("Skipping git plan pulling")
122150
return planPath, nil
123151
}
152+
if skipPullFromEnv(uii) {
153+
return planPath, nil
154+
}
124155
valid, err := cacheIsValid(planPath)
125156
if err != nil {
126157
return "", err
@@ -172,7 +203,7 @@ func GetGitPlan(
172203
if cloneToken != "" {
173204
uii.Verboseln("Found clone token in env, but shuttle path was ssh-based. This override will not work.")
174205
}
175-
206+
176207
cloneArg = parsedGitPlan.User + "@" + parsedGitPlan.Repository
177208
} else {
178209
panic(fmt.Sprintf("Unknown protocol '%s'", parsedGitPlan.Protocol))

pkg/git/skip_pull_test.go

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
package git
2+
3+
import (
4+
"io"
5+
"os"
6+
"testing"
7+
8+
"github.com/lunarway/shuttle/pkg/ui"
9+
"github.com/stretchr/testify/assert"
10+
)
11+
12+
func TestSkipPullFromEnv(t *testing.T) {
13+
tt := []struct {
14+
name string
15+
skipPull *string
16+
ci *string
17+
skipsPull bool
18+
}{
19+
{
20+
name: "nothing set",
21+
skipsPull: false,
22+
},
23+
{
24+
name: "CI set",
25+
ci: strPtr("true"),
26+
skipsPull: true,
27+
},
28+
{
29+
name: "CI set to any non-empty value",
30+
ci: strPtr("1"),
31+
skipsPull: true,
32+
},
33+
{
34+
name: "CI set but empty",
35+
ci: strPtr(""),
36+
skipsPull: false,
37+
},
38+
{
39+
name: "SHUTTLE_SKIP_PULL set",
40+
skipPull: strPtr("true"),
41+
skipsPull: true,
42+
},
43+
{
44+
name: "SHUTTLE_SKIP_PULL set but empty is treated as set",
45+
skipPull: strPtr(""),
46+
skipsPull: true,
47+
},
48+
{
49+
name: "SHUTTLE_SKIP_PULL takes precedence over CI",
50+
skipPull: strPtr("false"),
51+
ci: strPtr("true"),
52+
skipsPull: false,
53+
},
54+
{
55+
name: "SHUTTLE_SKIP_PULL with an unparsable value is treated as set",
56+
skipPull: strPtr("yes-please"),
57+
skipsPull: true,
58+
},
59+
}
60+
61+
for _, tc := range tt {
62+
t.Run(tc.name, func(t *testing.T) {
63+
// Unset by default so the developer's own environment does not leak
64+
// into the test.
65+
t.Setenv(skipPullKey, "")
66+
os.Unsetenv(skipPullKey)
67+
t.Setenv("CI", "")
68+
os.Unsetenv("CI")
69+
70+
if tc.skipPull != nil {
71+
t.Setenv(skipPullKey, *tc.skipPull)
72+
}
73+
if tc.ci != nil {
74+
t.Setenv("CI", *tc.ci)
75+
}
76+
77+
uii := ui.Create(io.Discard, io.Discard)
78+
79+
assert.Equal(t, tc.skipsPull, skipPullFromEnv(uii))
80+
})
81+
}
82+
}
83+
84+
func strPtr(s string) *string {
85+
return &s
86+
}

0 commit comments

Comments
 (0)