Skip to content

Commit de142b8

Browse files
author
github-actions
committed
Automatically copy changelog message to the backport or put no-changelog label
1 parent 7ade89f commit de142b8

File tree

8 files changed

+280
-101
lines changed

8 files changed

+280
-101
lines changed

bot/internal/bot/backport.go

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ import (
3131
"text/template"
3232

3333
"github.com/gravitational/shared-workflows/bot/internal/github"
34+
"golang.org/x/exp/slices"
3435

3536
"github.com/gravitational/trace"
3637
)
@@ -79,6 +80,11 @@ func (b *Bot) Backport(ctx context.Context) error {
7980

8081
var rows []row
8182

83+
g := git
84+
if b.c.Git != nil {
85+
g = b.c.Git
86+
}
87+
8288
// Loop over all requested backport branches and create backport branch and
8389
// GitHub Pull Request.
8490
for _, base := range branches {
@@ -92,7 +98,7 @@ func (b *Bot) Backport(ctx context.Context) error {
9298
base,
9399
pull,
94100
head,
95-
git,
101+
g,
96102
)
97103
if err != nil {
98104
log.Printf("Failed to create backport branch:\n%v\n", trace.DebugReport(err))
@@ -104,6 +110,14 @@ func (b *Bot) Backport(ctx context.Context) error {
104110
continue
105111
}
106112

113+
bodyText := fmt.Sprintf("Backport #%v to %v", b.c.Environment.Number, base)
114+
if entries := b.getChangelogEntries(pull.UnsafeBody); len(entries) > 0 && !slices.Contains(pull.UnsafeLabels, NoChangelogLabel) {
115+
bodyText += "\n\n"
116+
for _, entry := range entries {
117+
bodyText += fmt.Sprintf("%s%s\n", ChangelogPrefix, entry)
118+
}
119+
}
120+
107121
rows = append(rows, row{
108122
Branch: base,
109123
Failed: false,
@@ -117,7 +131,7 @@ func (b *Bot) Backport(ctx context.Context) error {
117131
RawQuery: url.Values{
118132
"expand": []string{"1"},
119133
"title": []string{fmt.Sprintf("[%v] %v", strings.Trim(base, "branch/"), pull.UnsafeTitle)},
120-
"body": []string{fmt.Sprintf("Backport #%v to %v", b.c.Environment.Number, base)},
134+
"body": []string{bodyText},
121135
}.Encode(),
122136
},
123137
})
@@ -162,8 +176,10 @@ func (b *Bot) BackportLocal(ctx context.Context, branch string) error {
162176
return nil
163177
}
164178

179+
const botBackportBranchPrefix = "bot/backport"
180+
165181
func (b *Bot) backportBranchName(base string) string {
166-
return fmt.Sprintf("bot/backport-%v-%v", b.c.Environment.Number, base)
182+
return fmt.Sprintf("%s-%v-%v", botBackportBranchPrefix, b.c.Environment.Number, base)
167183
}
168184

169185
// findBranches looks through the labels attached to a Pull Request for all the

bot/internal/bot/backport_test.go

Lines changed: 112 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,10 @@ limitations under the License.
1717
package bot
1818

1919
import (
20+
"context"
21+
"github.com/gravitational/shared-workflows/bot/internal/env"
22+
"github.com/gravitational/shared-workflows/bot/internal/github"
23+
"github.com/gravitational/shared-workflows/bot/internal/review"
2024
"testing"
2125

2226
"github.com/stretchr/testify/require"
@@ -35,3 +39,111 @@ func TestFindBranches(t *testing.T) {
3539
"master",
3640
})
3741
}
42+
43+
func TestBackport(t *testing.T) {
44+
buildTestBot := func(github Client) (*Bot, context.Context) {
45+
r, _ := review.New(&review.Config{
46+
CodeReviewers: map[string]review.Reviewer{"dev": review.Reviewer{
47+
Team: "core",
48+
}},
49+
CodeReviewersOmit: map[string]bool{},
50+
DocsReviewers: map[string]review.Reviewer{},
51+
DocsReviewersOmit: map[string]bool{},
52+
Admins: []string{},
53+
})
54+
55+
return &Bot{
56+
c: &Config{
57+
Environment: &env.Environment{
58+
Organization: "foo",
59+
Author: "dev",
60+
Repository: "bar",
61+
Number: 42,
62+
UnsafeBase: "branch/v8",
63+
UnsafeHead: "fix",
64+
},
65+
GitHub: github,
66+
Review: r,
67+
Git: gitDryRun,
68+
},
69+
}, context.Background()
70+
}
71+
72+
tests := []struct {
73+
desc string
74+
github Client
75+
assertFunc require.ValueAssertionFunc
76+
}{
77+
{
78+
desc: "pr without backport labels",
79+
github: &fakeGithub{},
80+
assertFunc: require.Empty,
81+
},
82+
{
83+
desc: "pr with backport label, no changelog",
84+
github: &fakeGithub{
85+
pull: github.PullRequest{
86+
Author: "dev",
87+
Repository: "Teleport",
88+
Number: 42,
89+
UnsafeTitle: "Best PR",
90+
UnsafeBody: "This is PR body",
91+
UnsafeLabels: []string{"backport/branch/v7"},
92+
},
93+
jobs: []github.Job{{Name: "Job1", ID: 1}},
94+
},
95+
assertFunc: func(t require.TestingT, i interface{}, i2 ...interface{}) {
96+
comments, ok := i.([]github.Comment)
97+
require.True(t, ok)
98+
require.Len(t, comments, 1)
99+
require.Equal(t,
100+
`
101+
@dev See the table below for backport results.
102+
103+
| Branch | Result |
104+
|--------|--------|
105+
| branch/v7 | [Create PR](https://github.com/foo/bar/compare/branch/v7...bot/backport-42-branch/v7?body=Backport+%2342+to+branch%2Fv7&expand=1&title=%5Bv7%5D+Best+PR) |
106+
`, comments[0].Body)
107+
},
108+
},
109+
{
110+
desc: "pr with backport label and with changelog",
111+
github: &fakeGithub{
112+
pull: github.PullRequest{
113+
Author: "dev",
114+
Repository: "Teleport",
115+
Number: 42,
116+
UnsafeTitle: "Best PR",
117+
UnsafeBody: "This is PR body\n\nchangelog: important change",
118+
UnsafeLabels: []string{"backport/branch/v7"},
119+
},
120+
jobs: []github.Job{{Name: "Job1", ID: 1}},
121+
},
122+
assertFunc: func(t require.TestingT, i interface{}, i2 ...interface{}) {
123+
comments, ok := i.([]github.Comment)
124+
require.True(t, ok)
125+
require.Len(t, comments, 1)
126+
require.Equal(t,
127+
`
128+
@dev See the table below for backport results.
129+
130+
| Branch | Result |
131+
|--------|--------|
132+
| branch/v7 | [Create PR](https://github.com/foo/bar/compare/branch/v7...bot/backport-42-branch/v7?body=Backport+%2342+to+branch%2Fv7%0A%0Achangelog%3A+important+change%0A&expand=1&title=%5Bv7%5D+Best+PR) |
133+
`, comments[0].Body)
134+
},
135+
},
136+
}
137+
138+
for _, test := range tests {
139+
t.Run(test.desc, func(t *testing.T) {
140+
b, ctx := buildTestBot(test.github)
141+
142+
err := b.Backport(ctx)
143+
require.NoError(t, err)
144+
145+
comments, _ := b.c.GitHub.ListComments(nil, "", "", 0)
146+
test.assertFunc(t, comments)
147+
})
148+
}
149+
}

bot/internal/bot/bot.go

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,7 +44,7 @@ type Client interface {
4444
// GetPullRequest returns a specific Pull Request.
4545
GetPullRequest(ctx context.Context, organization string, repository string, number int) (github.PullRequest, error)
4646

47-
// GetPullRequestWithCommitsd returns a specific Pull Request with commits.
47+
// GetPullRequestWithCommits returns a specific Pull Request with commits.
4848
GetPullRequestWithCommits(ctx context.Context, organization string, repository string, number int) (github.PullRequest, error)
4949

5050
// CreatePullRequest will create a Pull Request.
@@ -97,6 +97,8 @@ type Config struct {
9797

9898
// Review is used to get code and docs reviewers.
9999
Review *review.Assignments
100+
101+
Git func(...string) error
100102
}
101103

102104
// CheckAndSetDefaults checks and sets defaults.

bot/internal/bot/bot_test.go

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -18,11 +18,10 @@ package bot
1818

1919
import (
2020
"context"
21-
"testing"
22-
2321
"github.com/gravitational/shared-workflows/bot/internal/env"
2422
"github.com/gravitational/shared-workflows/bot/internal/github"
2523
"github.com/gravitational/shared-workflows/bot/internal/review"
24+
"testing"
2625

2726
"github.com/stretchr/testify/require"
2827
)
@@ -311,6 +310,7 @@ func TestDoNotMerge(t *testing.T) {
311310
type fakeGithub struct {
312311
files []github.PullRequestFile
313312
pull github.PullRequest
313+
jobs []github.Job
314314
reviewers []string
315315
reviews []github.Review
316316
orgMembers map[string]struct{}
@@ -364,7 +364,7 @@ func (f *fakeGithub) ListWorkflowRuns(ctx context.Context, organization string,
364364
}
365365

366366
func (f *fakeGithub) ListWorkflowJobs(ctx context.Context, organization string, repository string, runID int64) ([]github.Job, error) {
367-
return nil, nil
367+
return f.jobs, nil
368368
}
369369

370370
func (f *fakeGithub) DeleteWorkflowRun(ctx context.Context, organization string, repository string, runID int64) error {
@@ -377,6 +377,10 @@ func (f *fakeGithub) IsOrgMember(ctx context.Context, user string, org string) (
377377
}
378378

379379
func (f *fakeGithub) CreateComment(ctx context.Context, organization string, repository string, number int, comment string) error {
380+
f.comments = append(f.comments, github.Comment{
381+
Body: comment,
382+
})
383+
380384
return nil
381385
}
382386

bot/internal/bot/changelog.go

Lines changed: 10 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -51,9 +51,11 @@ func (b *Bot) CheckChangelog(ctx context.Context) error {
5151
return nil
5252
}
5353

54-
changelogEntries, err := b.getChangelogEntries(ctx, pull.UnsafeBody)
55-
if err != nil {
56-
return trace.Wrap(err, "failed to get changelog entry")
54+
changelogEntries := b.getChangelogEntries(pull.UnsafeBody)
55+
if len(changelogEntries) == 0 {
56+
return trace.Wrap(
57+
b.logFailedCheck(ctx, "Changelog entry not found in the PR body. Please add a %q label to the PR, or changelog lines starting with `%s` followed by the changelog entries for the PR.", NoChangelogLabel, ChangelogPrefix),
58+
"failed to get changelog entry")
5759
}
5860

5961
for _, changelogEntry := range changelogEntries {
@@ -66,20 +68,18 @@ func (b *Bot) CheckChangelog(ctx context.Context) error {
6668
return nil
6769
}
6870

69-
func (b *Bot) getChangelogEntries(ctx context.Context, prBody string) ([]string, error) {
71+
func (b *Bot) getChangelogEntries(prBody string) []string {
7072
changelogRegex := regexp.MustCompile(ChangelogRegex)
7173

7274
changelogMatches := changelogRegex.FindAllString(prBody, -1)
73-
if len(changelogMatches) == 0 {
74-
return nil, b.logFailedCheck(ctx, "Changelog entry not found in the PR body. Please add a %q label to the PR, or changelog lines starting with `%s` followed by the changelog entries for the PR.", NoChangelogLabel, ChangelogPrefix)
75-
}
76-
7775
for i, changelogMatch := range changelogMatches {
7876
changelogMatches[i] = changelogMatch[len(ChangelogPrefix):] // Case insensitive prefix removal
7977
}
8078

81-
log.Printf("Found changelog entries %v", changelogMatches)
82-
return changelogMatches, nil
79+
if len(changelogMatches) > 0 {
80+
log.Printf("Found changelog entries %v", changelogMatches)
81+
}
82+
return changelogMatches
8383
}
8484

8585
// Checks for common issues with the changelog entry.

bot/internal/bot/changelog_test.go

Lines changed: 24 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -40,34 +40,29 @@ func TestChangelog(t *testing.T) {
4040

4141
func TestGetChangelogEntry(t *testing.T) {
4242
tests := []struct {
43-
desc string
44-
body string
45-
shouldError bool
46-
expected []string
43+
desc string
44+
body string
45+
expected []string
4746
}{
4847
{
49-
desc: "pass-simple",
50-
body: strings.Join([]string{"some typical PR entry", fmt.Sprintf("%schangelog entry", ChangelogPrefix), "some extra text"}, "\n"),
51-
shouldError: false,
52-
expected: []string{"changelog entry"},
48+
desc: "pass-simple",
49+
body: strings.Join([]string{"some typical PR entry", fmt.Sprintf("%schangelog entry", ChangelogPrefix), "some extra text"}, "\n"),
50+
expected: []string{"changelog entry"},
5351
},
5452
{
55-
desc: "pass-case-invariant",
56-
body: strings.Join([]string{"some typical PR entry", fmt.Sprintf("%schangelog entry", strings.ToUpper(ChangelogPrefix))}, "\n"),
57-
shouldError: false,
58-
expected: []string{"changelog entry"},
53+
desc: "pass-case-invariant",
54+
body: strings.Join([]string{"some typical PR entry", fmt.Sprintf("%schangelog entry", strings.ToUpper(ChangelogPrefix))}, "\n"),
55+
expected: []string{"changelog entry"},
5956
},
6057
{
61-
desc: "pass-prefix-in-changelog-entry",
62-
body: strings.Join([]string{"some typical PR entry", strings.Repeat(ChangelogPrefix, 5)}, "\n"),
63-
shouldError: false,
64-
expected: []string{strings.Repeat(ChangelogPrefix, 4)},
58+
desc: "pass-prefix-in-changelog-entry",
59+
body: strings.Join([]string{"some typical PR entry", strings.Repeat(ChangelogPrefix, 5)}, "\n"),
60+
expected: []string{strings.Repeat(ChangelogPrefix, 4)},
6561
},
6662
{
67-
desc: "pass-only-changelog-in-body",
68-
body: fmt.Sprintf("%schangelog entry", ChangelogPrefix),
69-
shouldError: false,
70-
expected: []string{"changelog entry"},
63+
desc: "pass-only-changelog-in-body",
64+
body: fmt.Sprintf("%schangelog entry", ChangelogPrefix),
65+
expected: []string{"changelog entry"},
7166
},
7267
{
7368
desc: "pass-multiple-entries",
@@ -81,28 +76,24 @@ func TestGetChangelogEntry(t *testing.T) {
8176
"entry 2",
8277
"entry 3",
8378
},
84-
shouldError: false,
8579
},
8680
{
87-
desc: "fail-if-no-body",
88-
body: "",
89-
shouldError: true,
81+
desc: "empty-if-no-body",
82+
body: "",
83+
expected: nil,
9084
},
9185
{
92-
desc: "fail-if-no-entry",
93-
body: "some typical PR entry",
94-
shouldError: true,
86+
desc: "empty-if-no-entry",
87+
body: "some typical PR entry",
88+
expected: nil,
9589
},
9690
}
9791
for _, test := range tests {
9892
t.Run(test.desc, func(t *testing.T) {
99-
b, ctx := buildTestingFixtures()
93+
b, _ := buildTestingFixtures()
10094

101-
changelogEntries, err := b.getChangelogEntries(ctx, test.body)
102-
require.Equal(t, test.shouldError, err != nil)
103-
if !test.shouldError {
104-
require.Exactly(t, test.expected, changelogEntries)
105-
}
95+
changelogEntries := b.getChangelogEntries(test.body)
96+
require.Exactly(t, test.expected, changelogEntries)
10697
})
10798
}
10899
}

0 commit comments

Comments
 (0)