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

mendersoftware / integration-test-runner / 1652522410

03 Feb 2025 03:55AM UTC coverage: 64.315%. Remained the same
1652522410

Pull #349

gitlab-ci

web-flow
chore: bump gitlab.com/gitlab-org/api/client-go

Bumps the golang-dependencies group with 1 update: [gitlab.com/gitlab-org/api/client-go](https://gitlab.com/gitlab-org/api/client-go).


Updates `gitlab.com/gitlab-org/api/client-go` from 0.120.0 to 0.121.0
- [Release notes](https://gitlab.com/gitlab-org/api/client-go/tags)
- [Commits](https://gitlab.com/gitlab-org/api/client-go/compare/v0.120.0...v0.121.0)

---
updated-dependencies:
- dependency-name: gitlab.com/gitlab-org/api/client-go
  dependency-type: direct:production
  update-type: version-update:semver-minor
  dependency-group: golang-dependencies
...

Signed-off-by: dependabot[bot] <support@github.com>
Pull Request #349: chore: bump gitlab.com/gitlab-org/api/client-go from 0.120.0 to 0.121.0 in the golang-dependencies group

1732 of 2693 relevant lines covered (64.31%)

2.33 hits per line

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

77.97
/cherrypicks.go
1
package main
2

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

19
        "github.com/google/go-github/v28/github"
20
        "github.com/sirupsen/logrus"
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) {
4✔
38
        client := http.Client{
4✔
39
                Timeout: time.Second * 2,
4✔
40
        }
4✔
41
        req, err := http.NewRequest(http.MethodGet, url, nil)
4✔
42
        if err != nil {
4✔
43
                return nil, err
×
44
        }
×
45
        res, err := client.Do(req)
4✔
46
        if err != nil {
4✔
47
                return nil, err
×
48
        }
×
49
        defer res.Body.Close()
4✔
50
        body, err := io.ReadAll(res.Body)
4✔
51
        if err != nil {
4✔
52
                return nil, err
×
53
        }
×
54
        v := versions{}
4✔
55
        err = json.Unmarshal(body, &v)
4✔
56
        if err != nil {
4✔
57
                return nil, err
×
58
        }
×
59
        if len(v.Lts) == 0 {
4✔
60
                return nil, errors.New("getLatestReleaseFromApi: lts version list is empty")
×
61
        }
×
62
        for idx, val := range v.Lts {
12✔
63
                v.Lts[idx] = val + ".x"
8✔
64
        }
8✔
65
        allReleases := []string{}
4✔
66
        for key := range v.Releases {
24✔
67
                allReleases = append(allReleases, key+".x")
20✔
68
        }
20✔
69
        // Only add to the list if the latest patch != latest LTS
70
        sort.Sort(sort.Reverse(sort.StringSlice(allReleases)))
4✔
71
        if allReleases[0] != v.Lts[0] {
8✔
72
                return append([]string{allReleases[0]}, v.Lts...), nil
4✔
73
        }
4✔
74
        return v.Lts, nil
×
75
}
76

77
// suggestCherryPicks suggests cherry-picks to release branches if the PR has been merged to master
78
func suggestCherryPicks(
79
        log *logrus.Entry,
80
        pr *github.PullRequestEvent,
81
        githubClient clientgithub.Client,
82
        conf *config,
83
) error {
9✔
84
        // ignore PRs if they are not closed and merged
9✔
85
        action := pr.GetAction()
9✔
86
        merged := pr.GetPullRequest().GetMerged()
9✔
87
        if action != "closed" || !merged {
12✔
88
                log.Infof("Ignoring cherry-pick suggestions for action: %s, merged: %v", action, merged)
3✔
89
                return nil
3✔
90
        }
3✔
91

92
        // ignore PRs if they don't target the master or main branch
93
        baseRef := pr.GetPullRequest().GetBase().GetRef()
6✔
94
        if baseRef != "master" && baseRef != "main" {
7✔
95
                log.Infof("Ignoring cherry-pick suggestions for base ref: %s", baseRef)
1✔
96
                return nil
1✔
97
        }
1✔
98

99
        // initialize the git work area
100
        repo := pr.GetRepo().GetName()
5✔
101
        repoURL := getRemoteURLGitHub(conf.githubProtocol, conf.githubOrganization, repo)
5✔
102
        prNumber := strconv.Itoa(pr.GetNumber())
5✔
103
        prBranchName := "pr_" + prNumber
5✔
104
        state, err := git.Commands(
5✔
105
                git.Command("init", "."),
5✔
106
                git.Command("remote", "add", "github", repoURL),
5✔
107
                git.Command("fetch", "github", baseRef+":local"),
5✔
108
                git.Command("fetch", "github", "pull/"+prNumber+"/head:"+prBranchName),
5✔
109
        )
5✔
110
        defer state.Cleanup()
5✔
111
        if err != nil {
5✔
112
                return err
×
113
        }
×
114

115
        // count the number commits with Changelog entries
116
        baseSHA := pr.GetPullRequest().GetBase().GetSHA()
5✔
117
        countCmd := exec.Command(
5✔
118
                "sh",
5✔
119
                "-c",
5✔
120
                "git log "+baseSHA+"...pr_"+prNumber+" | grep -i -e \"^    Changelog:\" "+
5✔
121
                        "| grep -v -i -e \"^    Changelog: *none\" | wc -l",
5✔
122
        )
5✔
123
        countCmd.Dir = state.Dir
5✔
124
        out, err := countCmd.CombinedOutput()
5✔
125
        if err != nil {
5✔
126
                return fmt.Errorf("%v returned error: %s: %s", countCmd.Args, out, err.Error())
×
127
        }
×
128

129
        changelogs, _ := strconv.Atoi(strings.TrimSpace(string(out)))
5✔
130
        if changelogs == 0 {
6✔
131
                log.Infof("Found no changelog entries, ignoring cherry-pick suggestions")
1✔
132
                return nil
1✔
133
        }
1✔
134

135
        // fetch all the branches
136
        err = git.Command("fetch", "github").With(state).Run()
4✔
137
        if err != nil {
4✔
138
                return err
×
139
        }
×
140

141
        // get list of release versions
142
        versions, err := getLatestReleaseFromApi(versionsUrl)
4✔
143
        if err != nil {
4✔
144
                return err
×
145
        }
×
146
        releaseBranches := []string{}
4✔
147
        for _, version := range versions {
16✔
148
                releaseBranch, err := getServiceRevisionFromIntegration(repo, "origin/"+version, conf)
12✔
149
                if err != nil {
12✔
150
                        return err
×
151
                } else if releaseBranch != "" {
24✔
152
                        if isCherryPickBottable(
12✔
153
                                pr.GetRepo().GetName(),
12✔
154
                                conf, pr.GetPullRequest(),
12✔
155
                                releaseBranch,
12✔
156
                        ) {
12✔
157
                                releaseBranches = append(
×
158
                                        releaseBranches,
×
159
                                        releaseBranch+" (release "+version+")"+" - :robot: :cherries:",
×
160
                                )
×
161
                        } else {
12✔
162
                                releaseBranches = append(releaseBranches, releaseBranch+" (release "+version+")")
12✔
163
                        }
12✔
164
                }
165
        }
166

167
        // no suggestions, stop here
168
        if len(releaseBranches) == 0 {
4✔
169
                return nil
×
170
        }
×
171
        // nolint:lll
172
        tmplString := `
4✔
173
Hello :smiley_cat: This PR contains changelog entries. Please, verify the need of backporting it to the following release branches:
4✔
174
{{.ReleaseBranches}}
4✔
175
`
4✔
176
        // suggest cherry-picking with a comment
4✔
177
        tmpl, err := template.New("Main").Parse(tmplString)
4✔
178
        if err != nil {
4✔
179
                log.Errorf(
×
180
                        "Failed to parse the build matrix template. Should never happen! Error: %s\n",
×
181
                        err.Error(),
×
182
                )
×
183
        }
×
184
        var buf bytes.Buffer
4✔
185
        if err = tmpl.Execute(&buf, struct {
4✔
186
                ReleaseBranches string
4✔
187
        }{
4✔
188
                ReleaseBranches: strings.Join(releaseBranches, "\n"),
4✔
189
        }); err != nil {
4✔
190
                log.Errorf("Failed to execute the build matrix template. Error: %s\n", err.Error())
×
191
        }
×
192

193
        // Comment with a pipeline-link on the PR
194
        commentBody := buf.String()
4✔
195
        comment := github.IssueComment{
4✔
196
                Body: &commentBody,
4✔
197
        }
4✔
198
        if err := githubClient.CreateComment(context.Background(), conf.githubOrganization,
4✔
199
                pr.GetRepo().GetName(), pr.GetNumber(), &comment); err != nil {
4✔
200
                log.Infof("Failed to comment on the pr: %v, Error: %s", pr, err.Error())
×
201
                return err
×
202
        }
×
203
        return nil
4✔
204
}
205

206
func isCherryPickBottable(
207
        repoName string,
208
        conf *config,
209
        pr *github.PullRequest,
210
        targetBranch string,
211
) bool {
12✔
212
        _, state, err := tryCherryPickToBranch(repoName, conf, pr, targetBranch)
12✔
213
        state.Cleanup()
12✔
214
        if err != nil {
24✔
215
                logrus.Errorf("isCherryPickBottable received error: %s", err.Error())
12✔
216
        }
12✔
217
        return err == nil
12✔
218
}
219

220
func tryCherryPickToBranch(
221
        repoName string,
222
        conf *config,
223
        pr *github.PullRequest,
224
        targetBranch string,
225
) (string, *git.State, error) {
16✔
226
        prBranchName := fmt.Sprintf("cherry-%s-%s",
16✔
227
                targetBranch, pr.GetHead().GetRef())
16✔
228
        state, err := git.Commands(
16✔
229
                git.Command("init", "."),
16✔
230
                git.Command("remote", "add", "mendersoftware",
16✔
231
                        getRemoteURLGitHub(conf.githubProtocol, "mendersoftware", repoName)),
16✔
232
                git.Command("fetch", "mendersoftware"),
16✔
233
                git.Command("checkout", "mendersoftware/"+targetBranch),
16✔
234
                git.Command("checkout", "-b", prBranchName),
16✔
235
        )
16✔
236
        if err != nil {
16✔
237
                return "", state, err
×
238
        }
×
239

240
        if err = git.Command("cherry-pick", "-x", "--allow-empty",
16✔
241
                pr.GetHead().GetSHA(), "^"+pr.GetBase().GetSHA()).
16✔
242
                With(state).Run(); err != nil {
28✔
243
                if strings.Contains(err.Error(), "conflict") {
15✔
244
                        return "", state, errorCherryPickConflict
3✔
245
                }
3✔
246
                return "", state, err
9✔
247
        }
248
        return prBranchName, state, nil
4✔
249
}
250

251
func cherryPickToBranch(
252
        log *logrus.Entry,
253
        comment *github.IssueCommentEvent,
254
        pr *github.PullRequest,
255
        conf *config,
256
        targetBranch string,
257
        client clientgithub.Client,
258
) (*github.PullRequest, error) {
4✔
259

4✔
260
        prBranchName, state, err := tryCherryPickToBranch(
4✔
261
                comment.GetRepo().GetName(),
4✔
262
                conf,
4✔
263
                pr,
4✔
264
                targetBranch,
4✔
265
        )
4✔
266
        defer state.Cleanup()
4✔
267
        if err != nil {
4✔
268
                return nil, err
×
269
        }
×
270

271
        if err = git.Command("push",
4✔
272
                "mendersoftware",
4✔
273
                prBranchName+":"+prBranchName).
4✔
274
                With(state).Run(); err != nil {
4✔
275
                return nil, err
×
276
        }
×
277

278
        newPR := &github.NewPullRequest{
4✔
279
                Title: github.String(fmt.Sprintf("[Cherry %s]: %s",
4✔
280
                        targetBranch, comment.GetIssue().GetTitle())),
4✔
281
                Head: github.String(prBranchName),
4✔
282
                Base: github.String(targetBranch),
4✔
283
                Body: github.String(
4✔
284
                        fmt.Sprintf("Cherry pick of PR: #%d\nFor you %s :)",
4✔
285
                                pr.GetNumber(), comment.Sender.GetName())),
4✔
286
                MaintainerCanModify: github.Bool(true),
4✔
287
        }
4✔
288
        newPRRes, err := client.CreatePullRequest(
4✔
289
                context.Background(),
4✔
290
                conf.githubOrganization,
4✔
291
                comment.GetRepo().GetName(),
4✔
292
                newPR)
4✔
293
        if err != nil {
4✔
294
                return nil, fmt.Errorf("Failed to create the PR for: (%s) %v",
×
295
                        comment.GetRepo().GetName(), err)
×
296
        }
×
297
        return newPRRes, nil
4✔
298
}
299

300
func cherryPickPR(
301
        log *logrus.Entry,
302
        comment *github.IssueCommentEvent,
303
        pr *github.PullRequest,
304
        conf *config,
305
        body string,
306
        githubClient clientgithub.Client,
307
) error {
2✔
308
        targetBranches, err := parseCherryTargetBranches(body)
2✔
309
        if err != nil {
2✔
310
                return err
×
311
        }
×
312
        conflicts := make(map[string]bool)
2✔
313
        errors := make(map[string]string)
2✔
314
        success := make(map[string]string)
2✔
315
        for _, targetBranch := range targetBranches {
6✔
316
                if newPR, err := cherryPickToBranch(
4✔
317
                        log,
4✔
318
                        comment,
4✔
319
                        pr,
4✔
320
                        conf,
4✔
321
                        targetBranch,
4✔
322
                        githubClient,
4✔
323
                ); err != nil {
4✔
324
                        if err == errorCherryPickConflict {
×
325
                                conflicts[targetBranch] = true
×
326
                                continue
×
327
                        }
328
                        log.Errorf("Failed to cherry pick: %s to %s, err: %s",
×
329
                                comment.GetIssue().GetTitle(), targetBranch, err)
×
330
                        errors[targetBranch] = err.Error()
×
331
                } else {
4✔
332
                        success[targetBranch] = fmt.Sprintf("#%d", newPR.GetNumber())
4✔
333
                }
4✔
334
        }
335
        // Comment with cherry links on the PR
336
        commentText := `Hi :smiley_cat:
2✔
337
I did my very best, and this is the result of the cherry pick operation:
2✔
338
`
2✔
339
        for _, targetBranch := range targetBranches {
6✔
340
                if !conflicts[targetBranch] && errors[targetBranch] != "" {
4✔
341
                        commentText = commentText +
×
342
                                fmt.Sprintf("* %s :red_circle: Error: %s\n", targetBranch, errors[targetBranch])
×
343
                } else if success[targetBranch] != "" {
8✔
344
                        commentText = commentText +
4✔
345
                                fmt.Sprintf("* %s :heavy_check_mark: %s\n", targetBranch, success[targetBranch])
4✔
346
                } else {
4✔
347
                        commentText = commentText +
×
348
                                fmt.Sprintf("* %s Had merge conflicts, you will have to fix this yourself "+
×
349
                                        ":crying_cat_face:\n", targetBranch)
×
350
                }
×
351
        }
352

353
        commentBody := github.IssueComment{
2✔
354
                Body: &commentText,
2✔
355
        }
2✔
356
        if err := githubClient.CreateComment(
2✔
357
                context.Background(),
2✔
358
                conf.githubOrganization,
2✔
359
                comment.GetRepo().GetName(),
2✔
360
                pr.GetNumber(),
2✔
361
                &commentBody,
2✔
362
        ); err != nil {
2✔
363
                log.Infof("Failed to comment on the pr: %v, Error: %s", pr, err.Error())
×
364
                return err
×
365
        }
×
366
        return nil
2✔
367
}
368

369
func parseCherryTargetBranches(body string) ([]string, error) {
21✔
370
        if matches := parseCherryTargetBranchesMultiLine(body); len(matches) > 0 {
33✔
371
                return matches, nil
12✔
372
        } else if matches := parseCherryTargetBranchesSingleLine(body); len(matches) > 0 {
32✔
373
                return matches, nil
10✔
374
        }
10✔
375
        return nil, fmt.Errorf("No target branches found in the comment body: %s", body)
×
376
}
377

378
func parseCherryTargetBranchesMultiLine(body string) []string {
21✔
379
        matches := []string{}
21✔
380
        regex := regexp.MustCompile(` *\* *(([[:word:]]+[_\.-]?)+)`)
21✔
381
        for _, line := range strings.Split(body, "\n") {
65✔
382
                if m := regex.FindStringSubmatch(line); len(m) > 1 {
64✔
383
                        matches = append(matches, m[1])
20✔
384
                }
20✔
385
        }
386
        return matches
21✔
387
}
388

389
func parseCherryTargetBranchesSingleLine(body string) []string {
10✔
390
        body = strings.TrimPrefix(body, commandCherryPickBranch)
10✔
391
        matches := []string{}
10✔
392
        regex := regexp.MustCompile(`\x60(([[:word:]]+[_\.-]?)+)\x60`)
10✔
393
        for _, m := range regex.FindAllStringSubmatch(body, -1) {
24✔
394
                if len(m) > 1 {
28✔
395
                        matches = append(matches, m[1])
14✔
396
                }
14✔
397
        }
398
        return matches
10✔
399
}
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