• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

mendersoftware / integration-test-runner / 1887464315

16 Jun 2025 02:47PM UTC coverage: 64.238% (+64.2%) from 0.0%
1887464315

push

gitlab-ci

web-flow
Merge pull request #385 from mzedel/qa-1017

QA-1017 - dependabot reviewers to CODEOWNERS

1904 of 2964 relevant lines covered (64.24%)

2.14 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

79.39
/cherrypicks.go
1
package main
2

3
import (
4
        "context"
5
        "encoding/json"
6
        "errors"
7
        "fmt"
8
        "io"
9
        "net/http"
10
        "os/exec"
11
        "regexp"
12
        "sort"
13
        "strconv"
14
        "strings"
15
        "time"
16

17
        "github.com/google/go-github/v28/github"
18
        "github.com/sirupsen/logrus"
19

20
        "slices"
21

22
        clientgithub "github.com/mendersoftware/integration-test-runner/client/github"
23
        "github.com/mendersoftware/integration-test-runner/git"
24
)
25

26
var versionsUrl = "https://docs.mender.io/releases/versions.json"
27

28
var errorCherryPickConflict = errors.New("Cherry pick had conflicts")
29

30
type versions struct {
31
        Releases map[string]map[string]interface{} `json:"releases"`
32
        Lts      []string                          `json:",omitempty"`
33
}
34

35
// Returns the supported LTS versions, as well as the latest release if it is
36
// not LTS.
37
func getLatestReleaseFromApi(url string) ([]string, error) {
3✔
38
        client := http.Client{
3✔
39
                Timeout: time.Second * 2,
3✔
40
        }
3✔
41
        req, err := http.NewRequest(http.MethodGet, url, nil)
3✔
42
        if err != nil {
3✔
43
                return nil, err
×
44
        }
×
45
        res, err := client.Do(req)
3✔
46
        if err != nil {
3✔
47
                return nil, err
×
48
        }
×
49
        defer res.Body.Close()
3✔
50
        body, err := io.ReadAll(res.Body)
3✔
51
        if err != nil {
3✔
52
                return nil, err
×
53
        }
×
54
        v := versions{}
3✔
55
        err = json.Unmarshal(body, &v)
3✔
56
        if err != nil {
3✔
57
                return nil, err
×
58
        }
×
59
        if len(v.Lts) == 0 {
3✔
60
                return nil, errors.New("getLatestReleaseFromApi: lts version list is empty")
×
61
        }
×
62
        for idx, val := range v.Lts {
9✔
63
                v.Lts[idx] = val + ".x"
6✔
64
        }
6✔
65
        allReleases := []string{}
3✔
66
        for key := range v.Releases {
18✔
67
                allReleases = append(allReleases, key+".x")
15✔
68
        }
15✔
69
        // Only add to the list if the latest patch != latest LTS
70
        sort.Sort(sort.Reverse(sort.StringSlice(allReleases)))
3✔
71
        if allReleases[0] != v.Lts[0] {
6✔
72
                return append([]string{allReleases[0]}, v.Lts...), nil
3✔
73
        }
3✔
74
        return v.Lts, nil
×
75
}
76

77
func getReleaseBranchesForCherryPick(
78
        log *logrus.Entry,
79
        pr *github.PullRequestEvent,
80
        conf *config,
81
        state *git.State,
82
) ([]string, error) {
3✔
83

3✔
84
        releaseBranches := []string{}
3✔
85

3✔
86
        // fetch all the branches
3✔
87
        err := git.Command("fetch", "github").With(state).Run()
3✔
88
        if err != nil {
3✔
89
                return releaseBranches, err
×
90
        }
×
91

92
        // get list of release versions
93
        versions, err := getLatestReleaseFromApi(versionsUrl)
3✔
94
        if err != nil {
3✔
95
                return releaseBranches, err
×
96
        }
×
97

98
        repo := pr.GetRepo().GetName()
3✔
99
        for _, version := range versions {
12✔
100
                releaseBranch, err := getServiceRevisionFromIntegration(repo, "origin/"+version, conf)
9✔
101
                if err != nil {
9✔
102
                        return releaseBranches, err
×
103
                } else if releaseBranch != "" {
18✔
104
                        if isCherryPickBottable(
9✔
105
                                pr.GetRepo().GetName(),
9✔
106
                                conf, pr.GetPullRequest(),
9✔
107
                                releaseBranch,
9✔
108
                        ) {
9✔
109
                                releaseBranches = append(
×
110
                                        releaseBranches,
×
111
                                        releaseBranch+" (release "+version+")"+" - :robot: :cherries:",
×
112
                                )
×
113
                        } else {
9✔
114
                                releaseBranches = append(releaseBranches, releaseBranch+" (release "+version+")")
9✔
115
                        }
9✔
116
                }
117
        }
118

119
        return releaseBranches, nil
3✔
120
}
121

122
func generateCommentBody(releaseBranches []string) string {
3✔
123
        // nolint:lll
3✔
124
        commentBody := "Hello :smiley_cat: This PR contains changelog entries. Please, verify the need of backporting it to"
3✔
125
        if len(releaseBranches) == 0 {
3✔
126
                // No suggestions for the client repo or not a client repo: drop a generic message
×
127
                commentBody += " the supported release branches."
×
128
        } else {
3✔
129
                commentBody += " the following release branches:\n"
3✔
130
                commentBody += strings.Join(releaseBranches, "\n")
3✔
131
        }
3✔
132

133
        return commentBody
3✔
134
}
135

136
// suggestCherryPicks suggests cherry-picks to release branches if the PR has been merged to master
137
func suggestCherryPicks(
138
        log *logrus.Entry,
139
        pr *github.PullRequestEvent,
140
        githubClient clientgithub.Client,
141
        conf *config,
142
) error {
9✔
143
        // ignore PRs if they are not closed and merged
9✔
144
        action := pr.GetAction()
9✔
145
        merged := pr.GetPullRequest().GetMerged()
9✔
146
        if action != "closed" || !merged {
12✔
147
                log.Infof("Ignoring cherry-pick suggestions for action: %s, merged: %v", action, merged)
3✔
148
                return nil
3✔
149
        }
3✔
150

151
        // ignore PRs if they don't target the master or main branch
152
        baseRef := pr.GetPullRequest().GetBase().GetRef()
6✔
153
        if baseRef != "master" && baseRef != "main" {
7✔
154
                log.Infof("Ignoring cherry-pick suggestions for base ref: %s", baseRef)
1✔
155
                return nil
1✔
156
        }
1✔
157

158
        repo := pr.GetRepo().GetName()
5✔
159

5✔
160
        var ltsRepo bool
5✔
161
        for _, watchRepo := range ltsRepositories {
22✔
162
                if watchRepo == repo {
20✔
163
                        ltsRepo = true
3✔
164
                        break
3✔
165
                }
166
        }
167

168
        if !ltsRepo {
7✔
169
                log.Infof("Ignoring non-LTS repository: %s", repo)
2✔
170
                return nil
2✔
171
        }
2✔
172

173
        // initialize the git work area
174
        repoURL := getRemoteURLGitHub(conf.githubProtocol, conf.githubOrganization, repo)
3✔
175
        prNumber := strconv.Itoa(pr.GetNumber())
3✔
176
        prBranchName := "pr_" + prNumber
3✔
177
        state, err := git.Commands(
3✔
178
                git.Command("init", "."),
3✔
179
                git.Command("remote", "add", "github", repoURL),
3✔
180
                git.Command("fetch", "github", baseRef+":local"),
3✔
181
                git.Command("fetch", "github", "pull/"+prNumber+"/head:"+prBranchName),
3✔
182
        )
3✔
183
        defer state.Cleanup()
3✔
184
        if err != nil {
3✔
185
                return err
×
186
        }
×
187

188
        // count the number commits with Changelog entries
189
        baseSHA := pr.GetPullRequest().GetBase().GetSHA()
3✔
190
        countCmd := exec.Command(
3✔
191
                "sh",
3✔
192
                "-c",
3✔
193
                "git log "+baseSHA+"...pr_"+prNumber+" | grep -i -e \"^    Changelog:\" "+
3✔
194
                        "| grep -v -i -e \"^    Changelog: *none\" | wc -l",
3✔
195
        )
3✔
196
        countCmd.Dir = state.Dir
3✔
197
        out, err := countCmd.CombinedOutput()
3✔
198
        if err != nil {
3✔
199
                return fmt.Errorf("%v returned error: %s: %s", countCmd.Args, out, err.Error())
×
200
        }
×
201

202
        changelogs, _ := strconv.Atoi(strings.TrimSpace(string(out)))
3✔
203
        if changelogs == 0 {
3✔
204
                log.Infof("Found no changelog entries, ignoring cherry-pick suggestions")
×
205
                return nil
×
206
        }
×
207

208
        var releaseBranches []string
3✔
209
        if slices.Contains(clientRepositories, repo) {
6✔
210
                releaseBranches, err = getReleaseBranchesForCherryPick(log, pr, conf, state)
3✔
211
                if err != nil {
3✔
212
                        return err
×
213
                }
×
214
        }
215

216
        // Comment with a pipeline-link on the PR
217
        commentBody := generateCommentBody(releaseBranches)
3✔
218
        comment := github.IssueComment{
3✔
219
                Body: &commentBody,
3✔
220
        }
3✔
221
        if err := githubClient.CreateComment(context.Background(), conf.githubOrganization,
3✔
222
                pr.GetRepo().GetName(), pr.GetNumber(), &comment); err != nil {
3✔
223
                log.Infof("Failed to comment on the pr: %v, Error: %s", pr, err.Error())
×
224
                return err
×
225
        }
×
226
        return nil
3✔
227
}
228

229
func isCherryPickBottable(
230
        repoName string,
231
        conf *config,
232
        pr *github.PullRequest,
233
        targetBranch string,
234
) bool {
9✔
235
        _, state, err := tryCherryPickToBranch(repoName, conf, pr, targetBranch)
9✔
236
        state.Cleanup()
9✔
237
        if err != nil {
18✔
238
                logrus.Errorf("isCherryPickBottable received error: %s", err.Error())
9✔
239
        }
9✔
240
        return err == nil
9✔
241
}
242

243
func tryCherryPickToBranch(
244
        repoName string,
245
        conf *config,
246
        pr *github.PullRequest,
247
        targetBranch string,
248
) (string, *git.State, error) {
13✔
249
        prBranchName := fmt.Sprintf("cherry-%s-%s",
13✔
250
                targetBranch, pr.GetHead().GetRef())
13✔
251
        state, err := git.Commands(
13✔
252
                git.Command("init", "."),
13✔
253
                git.Command("remote", "add", "mendersoftware",
13✔
254
                        getRemoteURLGitHub(conf.githubProtocol, "mendersoftware", repoName)),
13✔
255
                git.Command("fetch", "mendersoftware"),
13✔
256
                git.Command("checkout", "mendersoftware/"+targetBranch),
13✔
257
                git.Command("checkout", "-b", prBranchName),
13✔
258
        )
13✔
259
        if err != nil {
13✔
260
                return "", state, err
×
261
        }
×
262

263
        if err = git.Command("cherry-pick", "-x", "--allow-empty",
13✔
264
                pr.GetHead().GetSHA(), "^"+pr.GetBase().GetSHA()).
13✔
265
                With(state).Run(); err != nil {
22✔
266
                if strings.Contains(err.Error(), "conflict") {
12✔
267
                        return "", state, errorCherryPickConflict
3✔
268
                }
3✔
269
                return "", state, err
6✔
270
        }
271
        return prBranchName, state, nil
4✔
272
}
273

274
func cherryPickToBranch(
275
        log *logrus.Entry,
276
        comment *github.IssueCommentEvent,
277
        pr *github.PullRequest,
278
        conf *config,
279
        targetBranch string,
280
        client clientgithub.Client,
281
) (*github.PullRequest, error) {
4✔
282

4✔
283
        prBranchName, state, err := tryCherryPickToBranch(
4✔
284
                comment.GetRepo().GetName(),
4✔
285
                conf,
4✔
286
                pr,
4✔
287
                targetBranch,
4✔
288
        )
4✔
289
        defer state.Cleanup()
4✔
290
        if err != nil {
4✔
291
                return nil, err
×
292
        }
×
293

294
        if err = git.Command("push",
4✔
295
                "mendersoftware",
4✔
296
                prBranchName+":"+prBranchName).
4✔
297
                With(state).Run(); err != nil {
4✔
298
                return nil, err
×
299
        }
×
300

301
        newPR := &github.NewPullRequest{
4✔
302
                Title: github.String(fmt.Sprintf("[Cherry %s]: %s",
4✔
303
                        targetBranch, comment.GetIssue().GetTitle())),
4✔
304
                Head: github.String(prBranchName),
4✔
305
                Base: github.String(targetBranch),
4✔
306
                Body: github.String(
4✔
307
                        fmt.Sprintf("Cherry pick of PR: #%d\nFor you %s :)",
4✔
308
                                pr.GetNumber(), comment.Sender.GetName())),
4✔
309
                MaintainerCanModify: github.Bool(true),
4✔
310
        }
4✔
311
        newPRRes, err := client.CreatePullRequest(
4✔
312
                context.Background(),
4✔
313
                conf.githubOrganization,
4✔
314
                comment.GetRepo().GetName(),
4✔
315
                newPR)
4✔
316
        if err != nil {
4✔
317
                return nil, fmt.Errorf("Failed to create the PR for: (%s) %v",
×
318
                        comment.GetRepo().GetName(), err)
×
319
        }
×
320
        return newPRRes, nil
4✔
321
}
322

323
func cherryPickPR(
324
        log *logrus.Entry,
325
        comment *github.IssueCommentEvent,
326
        pr *github.PullRequest,
327
        conf *config,
328
        body string,
329
        githubClient clientgithub.Client,
330
) error {
2✔
331
        targetBranches, err := parseCherryTargetBranches(body)
2✔
332
        if err != nil {
2✔
333
                return err
×
334
        }
×
335
        conflicts := make(map[string]bool)
2✔
336
        errors := make(map[string]string)
2✔
337
        success := make(map[string]string)
2✔
338
        for _, targetBranch := range targetBranches {
6✔
339
                if newPR, err := cherryPickToBranch(
4✔
340
                        log,
4✔
341
                        comment,
4✔
342
                        pr,
4✔
343
                        conf,
4✔
344
                        targetBranch,
4✔
345
                        githubClient,
4✔
346
                ); err != nil {
4✔
347
                        if err == errorCherryPickConflict {
×
348
                                conflicts[targetBranch] = true
×
349
                                continue
×
350
                        }
351
                        log.Errorf("Failed to cherry pick: %s to %s, err: %s",
×
352
                                comment.GetIssue().GetTitle(), targetBranch, err)
×
353
                        errors[targetBranch] = err.Error()
×
354
                } else {
4✔
355
                        success[targetBranch] = fmt.Sprintf("#%d", newPR.GetNumber())
4✔
356
                }
4✔
357
        }
358
        // Comment with cherry links on the PR
359
        commentText := `Hi :smiley_cat:
2✔
360
I did my very best, and this is the result of the cherry pick operation:
2✔
361
`
2✔
362
        for _, targetBranch := range targetBranches {
6✔
363
                if !conflicts[targetBranch] && errors[targetBranch] != "" {
4✔
364
                        commentText = commentText +
×
365
                                fmt.Sprintf("* %s :red_circle: Error: %s\n", targetBranch, errors[targetBranch])
×
366
                } else if success[targetBranch] != "" {
8✔
367
                        commentText = commentText +
4✔
368
                                fmt.Sprintf("* %s :heavy_check_mark: %s\n", targetBranch, success[targetBranch])
4✔
369
                } else {
4✔
370
                        commentText = commentText +
×
371
                                fmt.Sprintf("* %s Had merge conflicts, you will have to fix this yourself "+
×
372
                                        ":crying_cat_face:\n", targetBranch)
×
373
                }
×
374
        }
375

376
        commentBody := github.IssueComment{
2✔
377
                Body: &commentText,
2✔
378
        }
2✔
379
        if err := githubClient.CreateComment(
2✔
380
                context.Background(),
2✔
381
                conf.githubOrganization,
2✔
382
                comment.GetRepo().GetName(),
2✔
383
                pr.GetNumber(),
2✔
384
                &commentBody,
2✔
385
        ); err != nil {
2✔
386
                log.Infof("Failed to comment on the pr: %v, Error: %s", pr, err.Error())
×
387
                return err
×
388
        }
×
389
        return nil
2✔
390
}
391

392
func parseCherryTargetBranches(body string) ([]string, error) {
21✔
393
        if matches := parseCherryTargetBranchesMultiLine(body); len(matches) > 0 {
33✔
394
                return matches, nil
12✔
395
        } else if matches := parseCherryTargetBranchesSingleLine(body); len(matches) > 0 {
32✔
396
                return matches, nil
10✔
397
        }
10✔
398
        return nil, fmt.Errorf("No target branches found in the comment body: %s", body)
×
399
}
400

401
func parseCherryTargetBranchesMultiLine(body string) []string {
21✔
402
        matches := []string{}
21✔
403
        regex := regexp.MustCompile(` *\* *(([[:word:]]+[_\.-]?)+)`)
21✔
404
        for _, line := range strings.Split(body, "\n") {
65✔
405
                if m := regex.FindStringSubmatch(line); len(m) > 1 {
64✔
406
                        matches = append(matches, m[1])
20✔
407
                }
20✔
408
        }
409
        return matches
21✔
410
}
411

412
func parseCherryTargetBranchesSingleLine(body string) []string {
10✔
413
        body = strings.TrimPrefix(body, commandCherryPickBranch)
10✔
414
        matches := []string{}
10✔
415
        regex := regexp.MustCompile(`\x60(([[:word:]]+[_\.-]?)+)\x60`)
10✔
416
        for _, m := range regex.FindAllStringSubmatch(body, -1) {
24✔
417
                if len(m) > 1 {
28✔
418
                        matches = append(matches, m[1])
14✔
419
                }
14✔
420
        }
421
        return matches
10✔
422
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc