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

mendersoftware / integration-test-runner / 1800617471

05 May 2025 11:53AM UTC coverage: 64.109% (-1.9%) from 65.983%
1800617471

Pull #376

gitlab-ci

lluiscampos
test: Remove unused `pr_merged` golden tests payload

The cherry-pick suggestion functionality cannot be verified with these
acceptance tests because they only run Git commands in dry mode. This is
presumably the reason why this test was never implemented...

Signed-off-by: Lluis Campos <lluis.campos@northern.tech>
Pull Request #376: QA-1007: Replace source of truth for watch repos from release_tool to code

54 of 85 new or added lines in 3 files covered. (63.53%)

46 existing lines in 3 files now uncovered.

1913 of 2984 relevant lines covered (64.11%)

2.13 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

© 2026 Coveralls, Inc