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

mendersoftware / integration-test-runner / 2365629823

26 Feb 2026 12:57PM UTC coverage: 66.223% (-4.7%) from 70.917%
2365629823

push

gitlab-ci

oldgiova
ci: allow coveralls to fail

Ticket: QA-1232

Signed-off-by: Roberto Giovanardi <roberto.giovanardi@northern.tech>

1641 of 2478 relevant lines covered (66.22%)

0.7 hits per line

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

50.49
/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) {
1✔
59
        start, end := strings.Index(title, "["), strings.Index(title, "]")
1✔
60
        // First character must be '['
1✔
61
        if start != 0 || end < start {
2✔
62
                return
1✔
63
        }
1✔
64
        for _, option := range strings.Fields(title[start+1 : end]) {
×
65
                switch strings.ToLower(option) {
×
66
                case titleOptionSkipCI:
×
67
                        titleOptions.SkipCI = true
×
68
                }
69
        }
70
        return
×
71
}
72

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

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

1✔
90
        // Do not run if the PR is a draft
1✔
91
        if req.GetDraft() {
1✔
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())
1✔
100
        options := getTitleOptions(title)
1✔
101

1✔
102
        log.Debugf("Processing pull request action %s", action)
1✔
103
        switch action {
1✔
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(
1✔
170
                ctx,
1✔
171
                conf.githubOrganization,
1✔
172
                pr.Sender.GetLogin(),
1✔
173
        ); !member {
1✔
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 {
1✔
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
        // get the list of builds
192
        builds := parseClientPullRequest(log, conf, action, pr)
1✔
193
        log.Infof("%s:%d would trigger %d builds", pr.GetRepo().GetName(), pr.GetNumber(), len(builds))
1✔
194

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

207
                if getFirstMatchingBotCommentInPR(log, githubClient, pr, botCommentString, conf) == nil {
×
208

×
209
                        msg := "@" + pr.GetSender().GetLogin() + botCommentString
×
210
                        // nolint:lll
×
211
                        msg += `
×
212

×
213
   ---
×
214

×
215
   <details>
×
216
   <summary>my commands and options</summary>
×
217
   <br />
×
218

×
219
   You can prevent me from automatically starting CI pipelines:
×
220
   - if your pull request title starts with "[NoCI] ..."
×
221

×
222
   You can trigger a client pipeline on multiple prs with:
×
223
   - mentioning me and ` + "`" + `start client pipeline --pr mender/127 --pr mender-connect/255` + "`" + `
×
224

×
225
   You can trigger GitHub->GitLab branch sync with:
×
226
   - mentioning me and ` + "`" + `sync` + "`" + `
×
227

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

245
        return nil
1✔
246
}
247

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

265
func getFirstMatchingBotCommentInPR(
266
        log *logrus.Entry,
267
        githubClient clientgithub.Client,
268
        pr *github.PullRequestEvent,
269
        botComment string,
270
        conf *config,
271
) *github.IssueComment {
1✔
272

1✔
273
        comments, err := githubClient.ListComments(
1✔
274
                context.Background(),
1✔
275
                conf.githubOrganization,
1✔
276
                pr.GetRepo().GetName(),
1✔
277
                pr.GetNumber(),
1✔
278
                &github.IssueListCommentsOptions{
1✔
279
                        Sort:      "created",
1✔
280
                        Direction: "asc",
1✔
281
                })
1✔
282
        if err != nil {
1✔
283
                log.Errorf("Failed to list the comments on PR: %s/%d, err: '%s'",
×
284
                        pr.GetRepo().GetName(), pr.GetNumber(), err)
×
285
                return nil
×
286
        }
×
287
        for _, comment := range comments {
2✔
288
                if comment.Body != nil &&
1✔
289
                        strings.Contains(*comment.Body, botComment) &&
1✔
290
                        comment.User != nil &&
1✔
291
                        comment.User.Login != nil &&
1✔
292
                        *comment.User.Login == githubBotName {
1✔
293
                        return comment
×
294
                }
×
295
        }
296
        return nil
1✔
297
}
298

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

1✔
320
        // Only do changelog commenting for mendersoftware repositories.
1✔
321
        if pr.GetPullRequest().GetBase().GetRepo().GetOwner().GetLogin() != "mendersoftware" {
1✔
322
                log.Info("Not a mendersoftware repository. Ignoring.")
×
323
                return
×
324
        }
×
325

326
        changelogText, warningText, err := fetchChangelogTextForPR(log, pr, conf)
1✔
327
        if err != nil {
1✔
328
                log.Errorf("Error while fetching changelog text: %s", err.Error())
×
329
                return
×
330
        }
×
331

332
        updatePullRequestChangelogComments(log, ctx, githubClient, pr, conf,
1✔
333
                changelogText, warningText)
1✔
334
}
335

336
func fetchChangelogTextForPR(
337
        log *logrus.Entry,
338
        pr *github.PullRequestEvent,
339
        conf *config,
340
) (string, string, error) {
1✔
341

1✔
342
        repo := pr.GetPullRequest().GetBase().GetRepo().GetName()
1✔
343
        baseSHA := pr.GetPullRequest().GetBase().GetSHA()
1✔
344
        headSHA := pr.GetPullRequest().GetHead().GetSHA()
1✔
345
        baseRef := pr.GetPullRequest().GetBase().GetRef()
1✔
346
        headRef := pr.GetPullRequest().GetHead().GetRef()
1✔
347
        versionRange := fmt.Sprintf(
1✔
348
                "%s..%s",
1✔
349
                baseSHA,
1✔
350
                headSHA,
1✔
351
        )
1✔
352

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

1✔
355
        // Generate the changelog text for this PR.
1✔
356
        changelogText, warningText, err := getChangelogText(
1✔
357
                repo, versionRange, conf)
1✔
358
        if err != nil {
1✔
359
                err = errors.Wrap(err, "Not able to get changelog text")
×
360
        }
×
361

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

1✔
375
        log.Debugf("Prepared changelog text: %s", changelogText)
1✔
376
        log.Debugf("Got warning text: %s", warningText)
1✔
377

1✔
378
        return changelogText, warningText, err
1✔
379
}
380

381
func assembleCommentText(changelogText, warningText string) string {
1✔
382
        commentText := changelogPrefix + changelogText
1✔
383
        if warningText != "" {
1✔
384
                commentText += warningHeader + warningText
×
385
        }
×
386
        return commentText
1✔
387
}
388

389
func updatePullRequestChangelogComments(
390
        log *logrus.Entry,
391
        ctx *gin.Context,
392
        githubClient clientgithub.Client,
393
        pr *github.PullRequestEvent,
394
        conf *config,
395
        changelogText string,
396
        warningText string,
397
) {
1✔
398
        var err error
1✔
399

1✔
400
        commentText := assembleCommentText(changelogText, warningText)
1✔
401
        emptyChangelog := (changelogText == "" ||
1✔
402
                strings.HasSuffix(changelogText, "### Changelogs\n\n"))
1✔
403

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

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

© 2026 Coveralls, Inc