Skip to content

Commit d94df0f

Browse files
authored
Support zero day cooldown (#643)
* Support zero day cooldown * improve validation
1 parent 67613e5 commit d94df0f

5 files changed

Lines changed: 225 additions & 4 deletions

File tree

internal/infra/run.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,9 @@ func (p *RunParams) Validate() error {
9696
if p.Job.Source.Commit != "" && !gitShaRegex.MatchString(p.Job.Source.Commit) {
9797
return fmt.Errorf("commit must be a SHA, or not provided")
9898
}
99+
if err := p.Job.UpdateCooldown.Validate(); err != nil {
100+
return err
101+
}
99102
// Allows for older smoke tests without the command field to keep working.
100103
if p.Job.Command == "" {
101104
p.Job.Command = model.UpdateFilesCommand

internal/infra/run_test.go

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,32 @@ func Test_setImageNames(t *testing.T) {
4949
})
5050
}
5151

52+
func Test_RunParamsValidateCooldown(t *testing.T) {
53+
days := func(n int) *int { return &n }
54+
55+
tests := []struct {
56+
name string
57+
cooldown *model.UpdateCooldown
58+
wantErr bool
59+
}{
60+
{"no cooldown", nil, false},
61+
{"zero is allowed", &model.UpdateCooldown{DefaultDays: days(0)}, false},
62+
{"positive is allowed", &model.UpdateCooldown{DefaultDays: days(7)}, false},
63+
{"negative is rejected", &model.UpdateCooldown{DefaultDays: days(-1)}, true},
64+
}
65+
66+
for _, tt := range tests {
67+
t.Run(tt.name, func(t *testing.T) {
68+
params := &RunParams{
69+
Job: &model.Job{PackageManager: "go_modules", UpdateCooldown: tt.cooldown},
70+
}
71+
if err := params.Validate(); (err != nil) != tt.wantErr {
72+
t.Errorf("Validate() error = %v, wantErr %t", err, tt.wantErr)
73+
}
74+
})
75+
}
76+
}
77+
5278
func Test_checkCredAccess(t *testing.T) {
5379
l, err := net.Listen("tcp", "127.0.0.1:0")
5480
if err != nil {

internal/model/job.go

Lines changed: 26 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -273,10 +273,32 @@ type CommitOptions struct {
273273
type Credential map[string]any
274274

275275
type UpdateCooldown struct {
276-
DefaultDays int `json:"default-days,omitempty" yaml:"default-days,omitempty"`
277-
SemverMajorDays int `json:"semver-major-days,omitempty" yaml:"semver-major-days,omitempty"`
278-
SemverMinorDays int `json:"semver-minor-days,omitempty" yaml:"semver-minor-days,omitempty"`
279-
SemverPatchDays int `json:"semver-patch-days,omitempty" yaml:"semver-patch-days,omitempty"`
276+
DefaultDays *int `json:"default-days,omitempty" yaml:"default-days,omitempty"`
277+
SemverMajorDays *int `json:"semver-major-days,omitempty" yaml:"semver-major-days,omitempty"`
278+
SemverMinorDays *int `json:"semver-minor-days,omitempty" yaml:"semver-minor-days,omitempty"`
279+
SemverPatchDays *int `json:"semver-patch-days,omitempty" yaml:"semver-patch-days,omitempty"`
280280
Include []string `json:"include,omitempty" yaml:"include,omitempty"`
281281
Exclude []string `json:"exclude,omitempty" yaml:"exclude,omitempty"`
282282
}
283+
284+
// Validate rejects negative cooldown days. Zero is valid and means no cooldown.
285+
func (c *UpdateCooldown) Validate() error {
286+
if c == nil {
287+
return nil
288+
}
289+
fields := []struct {
290+
name string
291+
days *int
292+
}{
293+
{"default-days", c.DefaultDays},
294+
{"semver-major-days", c.SemverMajorDays},
295+
{"semver-minor-days", c.SemverMinorDays},
296+
{"semver-patch-days", c.SemverPatchDays},
297+
}
298+
for _, field := range fields {
299+
if field.days != nil && *field.days < 0 {
300+
return fmt.Errorf("cooldown %s must be zero or greater, got %d", field.name, *field.days)
301+
}
302+
}
303+
return nil
304+
}

internal/model/job_test.go

Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,157 @@ func TestInput(t *testing.T) {
2121
compareMap(t, "job", input2["job"], input.Job)
2222
}
2323

24+
func TestUpdateCooldownAllowsZero(t *testing.T) {
25+
cooldownFields := []string{"default-days", "semver-major-days", "semver-minor-days", "semver-patch-days"}
26+
27+
tests := []struct {
28+
name string
29+
testYAML string
30+
wantPresent bool
31+
wantValue int
32+
}{
33+
{
34+
name: "explicit zero is preserved",
35+
testYAML: `---
36+
job:
37+
package-manager: npm_and_yarn
38+
source:
39+
provider: github
40+
repo: dependabot/test
41+
directory: "/"
42+
cooldown:
43+
default-days: 0
44+
semver-major-days: 0
45+
semver-minor-days: 0
46+
semver-patch-days: 0
47+
`,
48+
wantPresent: true,
49+
wantValue: 0,
50+
},
51+
{
52+
name: "non-zero is preserved",
53+
testYAML: `---
54+
job:
55+
package-manager: npm_and_yarn
56+
source:
57+
provider: github
58+
repo: dependabot/test
59+
directory: "/"
60+
cooldown:
61+
default-days: 3
62+
semver-major-days: 3
63+
semver-minor-days: 3
64+
semver-patch-days: 3
65+
`,
66+
wantPresent: true,
67+
wantValue: 3,
68+
},
69+
{
70+
name: "omitted days stay omitted",
71+
testYAML: `---
72+
job:
73+
package-manager: npm_and_yarn
74+
source:
75+
provider: github
76+
repo: dependabot/test
77+
directory: "/"
78+
cooldown:
79+
include:
80+
- dependency-name-1
81+
`,
82+
wantPresent: false,
83+
},
84+
}
85+
86+
for _, tt := range tests {
87+
t.Run(tt.name, func(t *testing.T) {
88+
var input Input
89+
if err := yaml.Unmarshal([]byte(tt.testYAML), &input); err != nil {
90+
t.Fatal(err)
91+
}
92+
93+
data, err := json.Marshal(input.Job)
94+
if err != nil {
95+
t.Fatal(err)
96+
}
97+
98+
var payload map[string]any
99+
if err := json.Unmarshal(data, &payload); err != nil {
100+
t.Fatal(err)
101+
}
102+
cooldown, ok := payload["cooldown"].(map[string]any)
103+
if !ok {
104+
t.Fatalf("expected cooldown object, got %v", payload["cooldown"])
105+
}
106+
107+
for _, field := range cooldownFields {
108+
value, present := cooldown[field]
109+
if present != tt.wantPresent {
110+
t.Errorf("cooldown %s present = %t, want %t", field, present, tt.wantPresent)
111+
continue
112+
}
113+
if tt.wantPresent && value != float64(tt.wantValue) {
114+
t.Errorf("cooldown %s = %v, want %d", field, value, tt.wantValue)
115+
}
116+
}
117+
118+
// Verify round-trip: smoke tests marshal the job back to YAML, which must preserve zero too.
119+
out, err := yaml.Marshal(input.Job)
120+
if err != nil {
121+
t.Fatal(err)
122+
}
123+
var roundTripped Job
124+
if err := yaml.Unmarshal(out, &roundTripped); err != nil {
125+
t.Fatal(err)
126+
}
127+
128+
days := map[string]*int{
129+
"default-days": roundTripped.UpdateCooldown.DefaultDays,
130+
"semver-major-days": roundTripped.UpdateCooldown.SemverMajorDays,
131+
"semver-minor-days": roundTripped.UpdateCooldown.SemverMinorDays,
132+
"semver-patch-days": roundTripped.UpdateCooldown.SemverPatchDays,
133+
}
134+
for field, got := range days {
135+
if (got != nil) != tt.wantPresent {
136+
t.Errorf("round-trip: cooldown %s set = %t, want %t", field, got != nil, tt.wantPresent)
137+
continue
138+
}
139+
if tt.wantPresent && *got != tt.wantValue {
140+
t.Errorf("round-trip: cooldown %s = %d, want %d", field, *got, tt.wantValue)
141+
}
142+
}
143+
})
144+
}
145+
}
146+
147+
func TestUpdateCooldownValidate(t *testing.T) {
148+
days := func(n int) *int { return &n }
149+
150+
tests := []struct {
151+
name string
152+
cooldown *UpdateCooldown
153+
wantErr bool
154+
}{
155+
{"nil cooldown", nil, false},
156+
{"no days set", &UpdateCooldown{Include: []string{"a"}}, false},
157+
{"zero is allowed", &UpdateCooldown{DefaultDays: days(0)}, false},
158+
{"positive is allowed", &UpdateCooldown{DefaultDays: days(7)}, false},
159+
{"negative default", &UpdateCooldown{DefaultDays: days(-1)}, true},
160+
{"negative semver-major", &UpdateCooldown{SemverMajorDays: days(-1)}, true},
161+
{"negative semver-minor", &UpdateCooldown{SemverMinorDays: days(-1)}, true},
162+
{"negative semver-patch", &UpdateCooldown{SemverPatchDays: days(-1)}, true},
163+
}
164+
165+
for _, tt := range tests {
166+
t.Run(tt.name, func(t *testing.T) {
167+
err := tt.cooldown.Validate()
168+
if (err != nil) != tt.wantErr {
169+
t.Errorf("Validate() error = %v, wantErr %t", err, tt.wantErr)
170+
}
171+
})
172+
}
173+
}
174+
24175
func TestUseCaseInsensitiveFileSystem(t *testing.T) {
25176
tests := []struct {
26177
name string

testdata/cooldown.yml

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
job:
2+
package-manager: go_modules
3+
allowed-updates:
4+
- dependency-type: direct
5+
update-type: all
6+
source:
7+
provider: github
8+
repo: dependabot/cli
9+
directory: /
10+
cooldown:
11+
default-days: 0
12+
semver-major-days: 0
13+
semver-minor-days: 0
14+
semver-patch-days: 0
15+
credentials:
16+
- type: git_source
17+
host: github.com
18+
username: x-access-token
19+
password: $LOCAL_GITHUB_ACCESS_TOKEN

0 commit comments

Comments
 (0)