From e41197d366df97da6e0599cc652d92ecf7819b74 Mon Sep 17 00:00:00 2001 From: Peter Wagner Date: Wed, 7 Oct 2020 20:26:32 -0400 Subject: [PATCH 1/2] updater.Group CoolDown This is still vaporware, but with a new name. I think it's more clear: check whenever, but if a PR was opened more than this ago I don't want to see it. --- updater/group.go | 58 ++++++++++++++++++++++++++++++++---------- updater/group_test.go | 21 +++++++++++++++ updater/groups_test.go | 8 +++--- 3 files changed, 69 insertions(+), 18 deletions(-) diff --git a/updater/group.go b/updater/group.go index 0e0ffce..c6fae68 100644 --- a/updater/group.go +++ b/updater/group.go @@ -3,7 +3,9 @@ package updater import ( "fmt" "regexp" + "strconv" "strings" + "time" "github.com/dependabot/gomodules-extracted/cmd/go/_internal_/semver" ) @@ -17,19 +19,12 @@ type Group struct { // Parameters that apply to members: // Range is a comma separated list of allowed semver ranges - Range string `yaml:"range"` - Frequency Frequency `yaml:"frequency"` + Range string `yaml:"range"` + CoolDown string `yaml:"cooldown"` compiledPattern *regexp.Regexp } -type Frequency string - -const ( - FrequencyDaily Frequency = "daily" - FrequencyWeekly Frequency = "weekly" -) - func (g *Group) Validate() error { if g.Name == "" { return fmt.Errorf("groups must specify name") @@ -37,10 +32,8 @@ func (g *Group) Validate() error { if g.Pattern == "" { return fmt.Errorf("groups must specify pattern") } - switch g.Frequency { - case "", FrequencyDaily, FrequencyWeekly: - default: - return fmt.Errorf("frequency must be: [%s,%s]", FrequencyDaily, FrequencyWeekly) + if !durPattern.MatchString(g.CoolDown) { + return fmt.Errorf("invalid cooldown, expected ISO8601 duration: %q", g.CoolDown) } if strings.HasPrefix(g.Pattern, "/") && strings.HasSuffix(g.Pattern, "/") { @@ -87,3 +80,42 @@ func cleanRange(rangeCond string, prefixLen int) string { } return s } + +var durPattern = regexp.MustCompile(`P?(((?P\d+)Y)?((?P\d+)M)?((?P\d+)D)|(?P\d+)W)?`) + +const ( + oneYear = 8766 * time.Hour + oneMonth = 730*time.Hour + 30*time.Minute + oneWeek = 7 * 24 * time.Hour + oneDay = 24 * time.Hour +) + +func (g Group) CoolDownDuration() time.Duration { + m := durPattern.FindStringSubmatch(g.CoolDown) + + var ret time.Duration + for i, name := range durPattern.SubexpNames() { + part := m[i] + if i == 0 || name == "" || part == "" { + continue + } + + val, err := strconv.Atoi(part) + if err != nil { + return 0 + } + valDur := time.Duration(val) + switch name { + case "year": + ret += valDur * oneYear + case "month": + ret += valDur * oneMonth + case "week": + ret += valDur * oneWeek + case "day": + ret += valDur * oneDay + } + } + + return ret +} diff --git a/updater/group_test.go b/updater/group_test.go index de4fff7..c5dbdb2 100644 --- a/updater/group_test.go +++ b/updater/group_test.go @@ -3,11 +3,32 @@ package updater_test import ( "fmt" "testing" + "time" "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" "github.com/thepwagner/action-update/updater" ) +func TestGroup_CoolDownDuration(t *testing.T) { + g := updater.Group{Name: "test", Pattern: "test"} + + cases := map[string]time.Duration{ + "P1D": 24 * time.Hour, + "1D": 24 * time.Hour, + "1W": 7 * 24 * time.Hour, + } + + for in, expected := range cases { + t.Run(in, func(t *testing.T) { + g.CoolDown = in + err := g.Validate() + require.NoError(t, err) + assert.Equal(t, expected, g.CoolDownDuration()) + }) + } +} + func TestGroup_InRange(t *testing.T) { cases := map[string]struct { included []string diff --git a/updater/groups_test.go b/updater/groups_test.go index ed8efbe..1bb3fda 100644 --- a/updater/groups_test.go +++ b/updater/groups_test.go @@ -25,10 +25,9 @@ func TestParseGroups(t *testing.T) { frequency: weekly range: ">=v1.4.0, =v1.4.0, =v1.4.0, Date: Wed, 7 Oct 2020 20:55:14 -0400 Subject: [PATCH 2/2] pre/post update scripts I want this for code generation, like regenerating protobufs when `protoc` is updated. --- updater/group.go | 10 +++++---- updater/group_test.go | 5 +++-- updater/updater.go | 27 ++++++++++++++++++++++++ updater/updater_test.go | 46 +++++++++++++++++++++++++++++++++++++++++ 4 files changed, 82 insertions(+), 6 deletions(-) diff --git a/updater/group.go b/updater/group.go index c6fae68..d874d39 100644 --- a/updater/group.go +++ b/updater/group.go @@ -19,8 +19,10 @@ type Group struct { // Parameters that apply to members: // Range is a comma separated list of allowed semver ranges - Range string `yaml:"range"` - CoolDown string `yaml:"cooldown"` + Range string `yaml:"range"` + CoolDown string `yaml:"cooldown"` + PreScript string `yaml:"pre-script"` + PostScript string `yaml:"post-script"` compiledPattern *regexp.Regexp } @@ -48,7 +50,7 @@ func (g *Group) Validate() error { return nil } -func (g Group) InRange(v string) bool { +func (g *Group) InRange(v string) bool { for _, rangeCond := range strings.Split(g.Range, ",") { rangeCond = strings.TrimSpace(rangeCond) switch { @@ -90,7 +92,7 @@ const ( oneDay = 24 * time.Hour ) -func (g Group) CoolDownDuration() time.Duration { +func (g *Group) CoolDownDuration() time.Duration { m := durPattern.FindStringSubmatch(g.CoolDown) var ret time.Duration diff --git a/updater/group_test.go b/updater/group_test.go index c5dbdb2..9c97583 100644 --- a/updater/group_test.go +++ b/updater/group_test.go @@ -65,14 +65,15 @@ func TestGroup_InRange(t *testing.T) { for r, tc := range cases { t.Run(r, func(t *testing.T) { + u := &updater.Group{Range: r} for _, v := range tc.included { t.Run(fmt.Sprintf("includes %s", v), func(t *testing.T) { - assert.True(t, updater.Group{Range: r}.InRange(v)) + assert.True(t, u.InRange(v)) }) } for _, v := range tc.excluded { t.Run(fmt.Sprintf("excludes %q", v), func(t *testing.T) { - assert.False(t, updater.Group{Range: r}.InRange(v)) + assert.False(t, u.InRange(v)) }) } }) diff --git a/updater/updater.go b/updater/updater.go index 42b4202..6211158 100644 --- a/updater/updater.go +++ b/updater/updater.go @@ -3,6 +3,8 @@ package updater import ( "context" "fmt" + "os" + "os/exec" "github.com/sirupsen/logrus" ) @@ -214,14 +216,39 @@ func (u *RepoUpdater) groupedUpdate(ctx context.Context, log logrus.FieldLogger, return 0, fmt.Errorf("switching to target branch: %w", err) } + if err := u.updateScript(ctx, "pre", group.PreScript); err != nil { + return 0, fmt.Errorf("executing pre-update script: %w", err) + } + for _, update := range updates { if err := u.updater.ApplyUpdate(ctx, update); err != nil { return 0, fmt.Errorf("applying batched update: %w", err) } } + if err := u.updateScript(ctx, "post", group.PostScript); err != nil { + return 0, fmt.Errorf("executing pre-update script: %w", err) + } + if err := u.repo.Push(ctx, updates...); err != nil { return 0, fmt.Errorf("pushing update: %w", err) } return len(updates), nil } + +func (u *RepoUpdater) updateScript(ctx context.Context, label, script string) error { + if script == "" { + return nil + } + cmd := exec.CommandContext(ctx, "/bin/sh", "-c", script) + cmd.Dir = u.repo.Root() + out := os.Stdout + _, _ = fmt.Fprintf(out, "--- start %s update script ---\n", label) + cmd.Stdout = out + cmd.Stderr = out + if err := cmd.Run(); err != nil { + return err + } + _, _ = fmt.Fprintf(out, "--- end %s update script ---\n", label) + return nil +} diff --git a/updater/updater_test.go b/updater/updater_test.go index 4a3461e..bed985c 100644 --- a/updater/updater_test.go +++ b/updater/updater_test.go @@ -3,6 +3,8 @@ package updater_test import ( "context" "fmt" + "os" + "path/filepath" "testing" "github.com/stretchr/testify/mock" @@ -120,3 +122,47 @@ func TestRepoUpdater_UpdateAll_MultipleGrouped(t *testing.T) { r.AssertExpectations(t) u.AssertExpectations(t) } + +func TestRepoUpdater_UpdateAll_Scripts(t *testing.T) { + cases := []*updater.Group{ + { + Name: groupName, + Pattern: "github.com/foo", + PreScript: `echo "sup" && touch token`, + }, + { + Name: groupName, + Pattern: "github.com/foo", + PostScript: `echo "sup" && touch token`, + }, + } + + for _, group := range cases { + err := group.Validate() + require.NoError(t, err) + + tmpDir := t.TempDir() + tokenPath := filepath.Join(tmpDir, "token") + r := &mockRepo{} + u := &mockUpdater{} + ru := updater.NewRepoUpdater(r, u, updater.WithGroups(group)) + ctx := context.Background() + + r.On("SetBranch", baseBranch).Return(nil) + dep := updater.Dependency{Path: mockUpdate.Path, Version: mockUpdate.Previous} + u.On("Dependencies", ctx).Return([]updater.Dependency{dep}, nil) + availableUpdate := mockUpdate // avoid pointer to shared reference + u.On("Check", ctx, dep, mock.Anything).Return(&availableUpdate, nil) + r.On("NewBranch", baseBranch, "action-update-go/main/foo").Times(1).Return(nil) + u.On("ApplyUpdate", ctx, mock.Anything).Times(1).Return(nil) + r.On("Push", ctx, mock.Anything, mock.Anything).Times(1).Return(nil) + r.On("Root").Return(tmpDir) + + err = ru.UpdateAll(ctx, baseBranch) + require.NoError(t, err) + r.AssertExpectations(t) + u.AssertExpectations(t) + _, err = os.Stat(tokenPath) + require.NoError(t, err) + } +}