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

mendersoftware / integration-test-runner / 1812297324

12 May 2025 06:44AM UTC coverage: 64.216% (-1.8%) from 65.983%
1812297324

push

gitlab-ci

web-flow
Merge pull request #376 from lluiscampos/QA-1007-limit-release-tool

QA-1007: Replace source of truth for watch repos from release_tool to code

49 of 70 new or added lines in 3 files covered. (70.0%)

46 existing lines in 3 files now uncovered.

1904 of 2965 relevant lines covered (64.22%)

2.14 hits per line

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

62.62
/main_pullrequest.go
1
package main
2

3
import (
4
        "context"
5
        "fmt"
6
        "regexp"
7
        "strconv"
8
        "strings"
9
        "time"
10

11
        "github.com/gin-gonic/gin"
12
        "github.com/google/go-github/v28/github"
13
        "github.com/pkg/errors"
14
        "github.com/sirupsen/logrus"
15

16
        clientgithub "github.com/mendersoftware/integration-test-runner/client/github"
17
)
18

19
var (
20
        changelogPrefix = "Merging these commits will result in the following changelog entries:\n\n"
21
        warningHeader   = "\n\n## Warning\n\nGenerating changelogs also resulted in these warnings:\n\n"
22

23
        msgDetailsKubernetesLog = "see <a href=\"https://console.cloud.google.com/kubernetes/" +
24
                "deployment/us-east1/company-websites/default/test-runner-mender-io/logs?" +
25
                "project=gp-kubernetes-269000\">logs</a> for details."
26
)
27

28
type retryParams struct {
29
        retryFunc func() error
30
        compFunc  func(error) bool
31
}
32

33
const (
34
        doRetry bool = true
35
        noRetry      = false
36
)
37

38
func retryOnError(args retryParams) error {
1✔
39
        var maxBackoff int = 8 * 8
1✔
40
        err := args.retryFunc()
1✔
41
        i := 1
1✔
42
        for i <= maxBackoff && args.compFunc(err) {
1✔
43
                err = args.retryFunc()
×
44
                i = i * 2
×
45
                time.Sleep(time.Duration(i) * time.Second)
×
46
        }
×
47
        return err
1✔
48
}
49

50
type TitleOptions struct {
51
        SkipCI bool
52
}
53

54
const (
55
        titleOptionSkipCI = "noci"
56
)
57

58
func getTitleOptions(title string) (titleOptions TitleOptions) {
5✔
59
        start, end := strings.Index(title, "["), strings.Index(title, "]")
5✔
60
        // First character must be '['
5✔
61
        if start != 0 || end < start {
8✔
62
                return
3✔
63
        }
3✔
64
        for _, option := range strings.Fields(title[start+1 : end]) {
5✔
65
                switch strings.ToLower(option) {
3✔
66
                case titleOptionSkipCI:
1✔
67
                        titleOptions.SkipCI = true
1✔
68
                }
69
        }
70
        return
2✔
71
}
72

73
func processGitHubPullRequest(
74
        ctx *gin.Context,
75
        pr *github.PullRequestEvent,
76
        githubClient clientgithub.Client,
77
        conf *config,
78
) error {
2✔
79

2✔
80
        var (
2✔
81
                prRef  string
2✔
82
                err    error
2✔
83
                action = pr.GetAction()
2✔
84
        )
2✔
85
        log := getCustomLoggerFromContext(ctx).
2✔
86
                WithField("pull", pr.GetNumber()).
2✔
87
                WithField("action", action)
2✔
88
        req := pr.GetPullRequest()
2✔
89

2✔
90
        // Do not run if the PR is a draft
2✔
91
        if req.GetDraft() {
2✔
92
                log.Infof(
×
93
                        "The PR: %s/%d is a draft. Do not run tests",
×
94
                        pr.GetRepo().GetName(),
×
95
                        pr.GetNumber(),
×
96
                )
×
97
                return nil
×
98
        }
×
99
        title := strings.TrimSpace(req.GetTitle())
2✔
100
        options := getTitleOptions(title)
2✔
101

2✔
102
        log.Debugf("Processing pull request action %s", action)
2✔
103
        switch action {
2✔
104
        case "opened", "reopened", "synchronize", "ready_for_review":
1✔
105
                // We always create a pr_* branch
1✔
106
                if prRef, err = syncPullRequestBranch(log, pr, conf); err != nil {
1✔
107
                        log.Errorf("Could not create PR branch: %s", err.Error())
×
108
                        msg := "There was an error syncing branches, " + msgDetailsKubernetesLog
×
109
                        postGitHubMessage(ctx, pr, log, msg)
×
110
                }
×
111
                //and we run a pipeline only for the pr_* branch
112
                if prRef != "" {
2✔
113
                        prNum := strconv.Itoa(pr.GetNumber())
1✔
114
                        prBranchName := "pr_" + prNum
1✔
115
                        isOrgMember := func() bool {
2✔
116
                                return githubClient.IsOrganizationMember(
1✔
117
                                        ctx,
1✔
118
                                        conf.githubOrganization,
1✔
119
                                        pr.Sender.GetLogin(),
1✔
120
                                )
1✔
121
                        }
1✔
122
                        if !options.SkipCI {
2✔
123
                                err = retryOnError(retryParams{
1✔
124
                                        retryFunc: func() error {
2✔
125
                                                return startPRPipeline(log, prBranchName, pr, conf, isOrgMember)
1✔
126
                                        },
1✔
127
                                        compFunc: func(compareError error) bool {
1✔
128
                                                re := regexp.MustCompile("Missing CI config file|" +
1✔
129
                                                        "No stages / jobs for this pipeline")
1✔
130
                                                switch {
1✔
131
                                                case compareError == nil:
1✔
132
                                                        return noRetry
1✔
133
                                                case re.MatchString(compareError.Error()):
×
134
                                                        log.Infof("start client pipeline for PR '%d' is skipped", pr.Number)
×
135
                                                        return noRetry
×
136
                                                default:
×
137
                                                        log.Errorf("failed to start client pipeline for PR: %s", compareError)
×
138
                                                        return doRetry
×
139
                                                }
140
                                        },
141
                                })
142
                        }
143
                        if err != nil {
1✔
144
                                msg := "There was an error running your pipeline, " + msgDetailsKubernetesLog
×
145
                                postGitHubMessage(ctx, pr, log, msg)
×
146
                        }
×
147
                }
148

149
                handleChangelogComments(log, ctx, githubClient, pr, conf)
1✔
150

151
        case "closed":
1✔
152
                // Delete merged pr branches in GitLab
1✔
153
                if err := deleteStaleGitlabPRBranch(log, pr, conf); err != nil {
1✔
154
                        log.Errorf(
×
155
                                "Failed to delete the stale PR branch after the PR: %v was merged or closed. "+
×
156
                                        "Error: %v",
×
157
                                pr,
×
158
                                err,
×
159
                        )
×
160
                }
×
161

162
                // If the pr was merged, suggest cherry-picks
163
                if err := suggestCherryPicks(log, pr, githubClient, conf); err != nil {
1✔
164
                        log.Errorf("Failed to suggest cherry picks for the pr %v. Error: %v", pr, err)
×
165
                }
×
166
        }
167

168
        // Continue to the integration Pipeline only for organization members
169
        if member := githubClient.IsOrganizationMember(
2✔
170
                ctx,
2✔
171
                conf.githubOrganization,
2✔
172
                pr.Sender.GetLogin(),
2✔
173
        ); !member {
2✔
174
                log.Warnf(
×
175
                        "%s is making a pullrequest, but he/she is not a member of our organization, ignoring",
×
176
                        pr.Sender.GetLogin(),
×
177
                )
×
178
                return nil
×
179
        }
×
180

181
        // First check if the PR has been merged. If so, stop
182
        // the pipeline, and do nothing else.
183
        if err := stopBuildsOfStaleClientPRs(log, pr, conf); err != nil {
2✔
184
                log.Errorf(
×
185
                        "Failed to stop a stale build after the PR: %v was merged or closed. Error: %v",
×
186
                        pr,
×
187
                        err,
×
188
                )
×
189
        }
×
190

191
        // Keep the OS and Enterprise repos in sync
192
        if err := syncIfOSHasEnterpriseRepo(log, conf, pr); err != nil {
2✔
193
                log.Errorf("Failed to sync the OS and Enterprise repos: %s", err.Error())
×
194
        }
×
195

196
        // get the list of builds
197
        builds := parseClientPullRequest(log, conf, action, pr)
2✔
198
        log.Infof("%s:%d would trigger %d builds", pr.GetRepo().GetName(), pr.GetNumber(), len(builds))
2✔
199

2✔
200
        // do not start the builds, inform the user about the `start client pipeline` command instead
2✔
201
        if len(builds) > 0 {
2✔
NEW
202
                // Two possible pipelines: client or integration
×
NEW
203
                var botCommentString string
×
NEW
204
                if pr.GetRepo().GetName() == "integration" {
×
NEW
205
                        botCommentString = `, start a full integration test pipeline with:
×
NEW
206
   - mentioning me and ` + "`" + `start integration pipeline` + "`" + ``
×
NEW
207
                } else {
×
NEW
208
                        botCommentString = `, start a full client pipeline with:
×
NEW
209
   - mentioning me and ` + "`" + commandStartClientPipeline + `"`
×
NEW
210
                }
×
211

UNCOV
212
                if getFirstMatchingBotCommentInPR(log, githubClient, pr, botCommentString, conf) == nil {
×
UNCOV
213

×
UNCOV
214
                        msg := "@" + pr.GetSender().GetLogin() + botCommentString +
×
UNCOV
215
                                commandStartClientPipeline + "\"."
×
UNCOV
216
                        // nolint:lll
×
UNCOV
217
                        msg += `
×
UNCOV
218

×
UNCOV
219
   ---
×
UNCOV
220

×
UNCOV
221
   <details>
×
UNCOV
222
   <summary>my commands and options</summary>
×
UNCOV
223
   <br />
×
UNCOV
224

×
UNCOV
225
   You can prevent me from automatically starting CI pipelines:
×
UNCOV
226
   - if your pull request title starts with "[NoCI] ..."
×
UNCOV
227

×
NEW
228
   You can trigger a client pipeline on multiple prs with:
×
UNCOV
229
   - mentioning me and ` + "`" + `start client pipeline --pr mender/127 --pr mender-connect/255` + "`" + `
×
UNCOV
230

×
UNCOV
231
   You can trigger GitHub->GitLab branch sync with:
×
UNCOV
232
   - mentioning me and ` + "`" + `sync` + "`" + `
×
UNCOV
233

×
UNCOV
234
   You can cherry pick to a given branch or branches with:
×
UNCOV
235
   - mentioning me and:
×
UNCOV
236
   ` + "```" + `
×
UNCOV
237
    cherry-pick to:
×
UNCOV
238
    * 1.0.x
×
UNCOV
239
    * 2.0.x
×
UNCOV
240
   ` + "```" + `
×
UNCOV
241
   </details>
×
UNCOV
242
   `
×
UNCOV
243
                        postGitHubMessage(ctx, pr, log, msg)
×
UNCOV
244
                } else {
×
245
                        log.Infof(
×
246
                                "I have already commented on the pr: %s/%d, no need to keep on nagging",
×
247
                                pr.GetRepo().GetName(), pr.GetNumber())
×
248
                }
×
249
        }
250

251
        return nil
2✔
252
}
253

254
func postGitHubMessage(
255
        ctx *gin.Context,
256
        pr *github.PullRequestEvent,
257
        log *logrus.Entry,
258
        msg string,
UNCOV
259
) {
×
UNCOV
260
        if err := githubClient.CreateComment(
×
UNCOV
261
                ctx,
×
UNCOV
262
                pr.GetOrganization().GetLogin(),
×
UNCOV
263
                pr.GetRepo().GetName(),
×
UNCOV
264
                pr.GetNumber(),
×
UNCOV
265
                &github.IssueComment{Body: github.String(msg)},
×
UNCOV
266
        ); err != nil {
×
267
                log.Infof("Failed to comment on the pr: %v, Error: %s", pr, err.Error())
×
268
        }
×
269
}
270

271
func getFirstMatchingBotCommentInPR(
272
        log *logrus.Entry,
273
        githubClient clientgithub.Client,
274
        pr *github.PullRequestEvent,
275
        botComment string,
276
        conf *config,
277
) *github.IssueComment {
13✔
278

13✔
279
        comments, err := githubClient.ListComments(
13✔
280
                context.Background(),
13✔
281
                conf.githubOrganization,
13✔
282
                pr.GetRepo().GetName(),
13✔
283
                pr.GetNumber(),
13✔
284
                &github.IssueListCommentsOptions{
13✔
285
                        Sort:      "created",
13✔
286
                        Direction: "asc",
13✔
287
                })
13✔
288
        if err != nil {
14✔
289
                log.Errorf("Failed to list the comments on PR: %s/%d, err: '%s'",
1✔
290
                        pr.GetRepo().GetName(), pr.GetNumber(), err)
1✔
291
                return nil
1✔
292
        }
1✔
293
        for _, comment := range comments {
22✔
294
                if comment.Body != nil &&
10✔
295
                        strings.Contains(*comment.Body, botComment) &&
10✔
296
                        comment.User != nil &&
10✔
297
                        comment.User.Login != nil &&
10✔
298
                        *comment.User.Login == githubBotName {
17✔
299
                        return comment
7✔
300
                }
7✔
301
        }
302
        return nil
5✔
303
}
304

305
func handleChangelogComments(
306
        log *logrus.Entry,
307
        ctx *gin.Context,
308
        githubClient clientgithub.Client,
309
        pr *github.PullRequestEvent,
310
        conf *config,
311
) {
1✔
312
        // It would be semantically correct to update the integration repo
1✔
313
        // here. However, this step is carried out on every PR update, causing a
1✔
314
        // big amount of "git fetch" requests, which both reduces performance,
1✔
315
        // and could result in rate limiting. Instead, we assume that the
1✔
316
        // integration repo is recent enough, since it is still updated when
1✔
317
        // doing mender-qa builds.
1✔
318
        //
1✔
319
        // // First update integration repo.
1✔
320
        // err := updateIntegrationRepo(conf)
1✔
321
        // if err != nil {
1✔
322
        //         log.Errorf("Could not update integration repo: %s", err.Error())
1✔
323
        //         // Should still be safe to continue though.
1✔
324
        // }
1✔
325

1✔
326
        // Only do changelog commenting for mendersoftware repositories.
1✔
327
        if pr.GetPullRequest().GetBase().GetRepo().GetOwner().GetLogin() != "mendersoftware" {
1✔
328
                log.Info("Not a mendersoftware repository. Ignoring.")
×
329
                return
×
330
        }
×
331

332
        changelogText, warningText, err := fetchChangelogTextForPR(log, pr, conf)
1✔
333
        if err != nil {
1✔
334
                log.Errorf("Error while fetching changelog text: %s", err.Error())
×
335
                return
×
336
        }
×
337

338
        updatePullRequestChangelogComments(log, ctx, githubClient, pr, conf,
1✔
339
                changelogText, warningText)
1✔
340
}
341

342
func fetchChangelogTextForPR(
343
        log *logrus.Entry,
344
        pr *github.PullRequestEvent,
345
        conf *config,
346
) (string, string, error) {
1✔
347

1✔
348
        repo := pr.GetPullRequest().GetBase().GetRepo().GetName()
1✔
349
        baseSHA := pr.GetPullRequest().GetBase().GetSHA()
1✔
350
        headSHA := pr.GetPullRequest().GetHead().GetSHA()
1✔
351
        baseRef := pr.GetPullRequest().GetBase().GetRef()
1✔
352
        headRef := pr.GetPullRequest().GetHead().GetRef()
1✔
353
        versionRange := fmt.Sprintf(
1✔
354
                "%s..%s",
1✔
355
                baseSHA,
1✔
356
                headSHA,
1✔
357
        )
1✔
358

1✔
359
        log.Debugf("Getting changelog for repo (%s) and range (%s)", repo, versionRange)
1✔
360

1✔
361
        // Generate the changelog text for this PR.
1✔
362
        changelogText, warningText, err := getChangelogText(
1✔
363
                repo, versionRange, conf)
1✔
364
        if err != nil {
1✔
365
                err = errors.Wrap(err, "Not able to get changelog text")
×
366
        }
×
367

368
        // Replace SHAs with the original ref names, so that the changelog text
369
        // does not change on every commit amend. The reason we did not use ref
370
        // names to begin with is that they may live in personal forks, so it
371
        // complicates the fetching mechanism. SHAs however, are always present
372
        // in the repository you are merging into.
373
        //
374
        // Fetching changelogs online from personal forks is pretty unlikely to
375
        // be useful outside of the integration-test-runner niche (better to use
376
        // the local version), therefore we do this replacement instead of
377
        // making the changelog-generator "fork aware".
378
        changelogText = strings.ReplaceAll(changelogText, baseSHA, baseRef)
1✔
379
        changelogText = strings.ReplaceAll(changelogText, headSHA, headRef)
1✔
380

1✔
381
        log.Debugf("Prepared changelog text: %s", changelogText)
1✔
382
        log.Debugf("Got warning text: %s", warningText)
1✔
383

1✔
384
        return changelogText, warningText, err
1✔
385
}
386

387
func assembleCommentText(changelogText, warningText string) string {
21✔
388
        commentText := changelogPrefix + changelogText
21✔
389
        if warningText != "" {
25✔
390
                commentText += warningHeader + warningText
4✔
391
        }
4✔
392
        return commentText
21✔
393
}
394

395
func updatePullRequestChangelogComments(
396
        log *logrus.Entry,
397
        ctx *gin.Context,
398
        githubClient clientgithub.Client,
399
        pr *github.PullRequestEvent,
400
        conf *config,
401
        changelogText string,
402
        warningText string,
403
) {
11✔
404
        var err error
11✔
405

11✔
406
        commentText := assembleCommentText(changelogText, warningText)
11✔
407
        emptyChangelog := (changelogText == "" ||
11✔
408
                strings.HasSuffix(changelogText, "### Changelogs\n\n"))
11✔
409

11✔
410
        comment := getFirstMatchingBotCommentInPR(log, githubClient, pr, changelogPrefix, conf)
11✔
411
        if comment != nil {
17✔
412
                // There is a previous comment about changelog.
6✔
413
                if *comment.Body == commentText {
9✔
414
                        log.Debugf("The changelog hasn't changed (comment ID: %d). Leave it alone.",
3✔
415
                                comment.ID)
3✔
416
                        return
3✔
417
                } else {
6✔
418
                        log.Debugf("Deleting old changelog comment (comment ID: %d).",
3✔
419
                                comment.ID)
3✔
420
                        err = githubClient.DeleteComment(
3✔
421
                                ctx,
3✔
422
                                conf.githubOrganization,
3✔
423
                                pr.GetRepo().GetName(),
3✔
424
                                *comment.ID,
3✔
425
                        )
3✔
426
                        if err != nil {
3✔
427
                                log.Errorf("Could not delete changelog comment: %s",
×
428
                                        err.Error())
×
429
                        }
×
430
                }
431
        } else if emptyChangelog {
7✔
432
                log.Info("Changelog is empty, and there is no previous changelog comment. Stay silent.")
2✔
433
                return
2✔
434
        }
2✔
435

436
        commentBody := &github.IssueComment{
6✔
437
                Body: &commentText,
6✔
438
        }
6✔
439
        err = githubClient.CreateComment(
6✔
440
                ctx,
6✔
441
                conf.githubOrganization,
6✔
442
                pr.GetRepo().GetName(),
6✔
443
                pr.GetNumber(),
6✔
444
                commentBody,
6✔
445
        )
6✔
446
        if err != nil {
6✔
447
                log.Errorf("Could not post changelog comment: %s. Comment text: %s",
×
448
                        err.Error(), commentText)
×
449
        }
×
450
}
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