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

mendersoftware / integration-test-runner / 1687972984

25 Feb 2025 04:02PM UTC coverage: 65.959% (+1.0%) from 64.916%
1687972984

Pull #359

gitlab-ci

danielskinstad
fix: wait for `pr_xxx_protected` to be created

There is a potential race condition between syncing branches and
protecting them. Add a sleep between sync and protect in syncProtectdBranch

Ticket: None
Changelog: None

Signed-off-by: Daniel Skinstad Drabitzius <daniel.drabitzius@northern.tech>
Pull Request #359: Fix integration pipeline

26 of 47 new or added lines in 4 files covered. (55.32%)

7 existing lines in 3 files now uncovered.

1926 of 2920 relevant lines covered (65.96%)

2.23 hits per line

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

77.29
/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
func processGitHubPullRequest(
51
        ctx *gin.Context,
52
        pr *github.PullRequestEvent,
53
        githubClient clientgithub.Client,
54
        conf *config,
55
) error {
2✔
56

2✔
57
        var (
2✔
58
                prRef  string
2✔
59
                err    error
2✔
60
                action = pr.GetAction()
2✔
61
        )
2✔
62
        log := getCustomLoggerFromContext(ctx).
2✔
63
                WithField("pull", pr.GetNumber()).
2✔
64
                WithField("action", action)
2✔
65
        req := pr.GetPullRequest()
2✔
66

2✔
67
        // Do not run if the PR is a draft
2✔
68
        if req.GetDraft() {
2✔
69
                log.Infof(
×
70
                        "The PR: %s/%d is a draft. Do not run tests",
×
71
                        pr.GetRepo().GetName(),
×
72
                        pr.GetNumber(),
×
73
                )
×
74
                return nil
×
75
        }
×
76

77
        log.Debugf("Processing pull request action %s", action)
2✔
78
        switch action {
2✔
79
        case "opened", "reopened", "synchronize", "ready_for_review":
1✔
80
                // We always create a pr_* branch
1✔
81
                if prRef, err = syncPullRequestBranch(log, pr, conf); err != nil {
1✔
82
                        log.Errorf("Could not create PR branch: %s", err.Error())
×
83
                        msg := "There was an error syncing branches, " + msgDetailsKubernetesLog
×
84
                        postGitHubMessage(ctx, pr, log, msg)
×
85
                }
×
86
                //and we run a pipeline only for the pr_* branch
87
                if prRef != "" {
2✔
88
                        prNum := strconv.Itoa(pr.GetNumber())
1✔
89
                        prBranchName := "pr_" + prNum
1✔
90
                        isOrgMember := func() bool {
2✔
91
                                return githubClient.IsOrganizationMember(
1✔
92
                                        ctx,
1✔
93
                                        conf.githubOrganization,
1✔
94
                                        pr.Sender.GetLogin(),
1✔
95
                                )
1✔
96
                        }
1✔
97
                        err = retryOnError(retryParams{
1✔
98
                                retryFunc: func() error {
2✔
99
                                        return startPRPipeline(log, prBranchName, pr, conf, isOrgMember)
1✔
100
                                },
1✔
101
                                compFunc: func(compareError error) bool {
1✔
102
                                        re := regexp.MustCompile("Missing CI config file|" +
1✔
103
                                                "No stages / jobs for this pipeline")
1✔
104
                                        switch {
1✔
105
                                        case compareError == nil:
1✔
106
                                                return noRetry
1✔
107
                                        case re.MatchString(compareError.Error()):
×
108
                                                log.Infof("start client pipeline for PR '%d' is skipped", pr.Number)
×
109
                                                return noRetry
×
110
                                        default:
×
111
                                                log.Errorf("failed to start client pipeline for PR: %s", compareError)
×
112
                                                return doRetry
×
113
                                        }
114
                                },
115
                        })
116
                        if err != nil {
1✔
117
                                msg := "There was an error running your pipeline, " + msgDetailsKubernetesLog
×
118
                                postGitHubMessage(ctx, pr, log, msg)
×
119
                        }
×
120
                }
121

122
                handleChangelogComments(log, ctx, githubClient, pr, conf)
1✔
123

124
        case "closed":
1✔
125
                // Delete merged pr branches in GitLab
1✔
126
                if err := deleteStaleGitlabPRBranch(log, pr, conf); err != nil {
1✔
127
                        log.Errorf(
×
128
                                "Failed to delete the stale PR branch after the PR: %v was merged or closed. "+
×
129
                                        "Error: %v",
×
130
                                pr,
×
131
                                err,
×
132
                        )
×
133
                }
×
134

135
                // If the pr was merged, suggest cherry-picks
136
                if err := suggestCherryPicks(log, pr, githubClient, conf); err != nil {
1✔
137
                        log.Errorf("Failed to suggest cherry picks for the pr %v. Error: %v", pr, err)
×
138
                }
×
139
        }
140

141
        // Continue to the integration Pipeline only for organization members
142
        if member := githubClient.IsOrganizationMember(
2✔
143
                ctx,
2✔
144
                conf.githubOrganization,
2✔
145
                pr.Sender.GetLogin(),
2✔
146
        ); !member {
2✔
147
                log.Warnf(
×
148
                        "%s is making a pullrequest, but he/she is not a member of our organization, ignoring",
×
149
                        pr.Sender.GetLogin(),
×
150
                )
×
151
                return nil
×
152
        }
×
153

154
        // First check if the PR has been merged. If so, stop
155
        // the pipeline, and do nothing else.
156
        if err := stopBuildsOfStaleClientPRs(log, pr, conf); err != nil {
2✔
157
                log.Errorf(
×
158
                        "Failed to stop a stale build after the PR: %v was merged or closed. Error: %v",
×
159
                        pr,
×
160
                        err,
×
161
                )
×
162
        }
×
163

164
        // Keep the OS and Enterprise repos in sync
165
        if err := syncIfOSHasEnterpriseRepo(log, conf, pr); err != nil {
2✔
166
                log.Errorf("Failed to sync the OS and Enterprise repos: %s", err.Error())
×
167
        }
×
168

169
        // get the list of builds
170
        builds := parseClientPullRequest(log, conf, action, pr)
2✔
171
        log.Infof("%s:%d would trigger %d builds", pr.GetRepo().GetName(), pr.GetNumber(), len(builds))
2✔
172

2✔
173
        // do not start the builds, inform the user about the `start client pipeline` command instead
2✔
174
        if len(builds) > 0 {
3✔
175
                // Only comment, if not already commented on a PR
1✔
176
                botCommentString := ", Let me know if you want to start the client pipeline by " +
1✔
177
                        "mentioning me and the command \""
1✔
178
                if getFirstMatchingBotCommentInPR(log, githubClient, pr, botCommentString, conf) == nil {
2✔
179

1✔
180
                        msg := "@" + pr.GetSender().GetLogin() + botCommentString +
1✔
181
                                commandStartClientPipeline + "\"."
1✔
182
                        // nolint:lll
1✔
183
                        msg += `
1✔
184

1✔
185
   ---
1✔
186

1✔
187
   <details>
1✔
188
   <summary>my commands and options</summary>
1✔
189
   <br />
1✔
190

1✔
191
   You can trigger a pipeline on multiple prs with:
1✔
192
   - mentioning me and ` + "`" + `start client pipeline --pr mender/127 --pr mender-connect/255` + "`" + `
1✔
193

1✔
194
   You can start a fast pipeline, disabling full integration tests with:
1✔
195
   - mentioning me and ` + "`" + `start client pipeline --fast` + "`" + `
1✔
196

1✔
197
   You can trigger a full integration test pipeline with:
1✔
198
   - mentioning me and ` + "`" + `start integration pipeline` + "`" + `
1✔
199

1✔
200
   You can trigger GitHub->GitLab branch sync with:
1✔
201
   - mentioning me and ` + "`" + `sync` + "`" + `
1✔
202

1✔
203
   You can cherry pick to a given branch or branches with:
1✔
204
   - mentioning me and:
1✔
205
   ` + "```" + `
1✔
206
    cherry-pick to:
1✔
207
    * 1.0.x
1✔
208
    * 2.0.x
1✔
209
   ` + "```" + `
1✔
210
   </details>
1✔
211
   `
1✔
212
                        postGitHubMessage(ctx, pr, log, msg)
1✔
213
                } else {
1✔
UNCOV
214
                        log.Infof(
×
UNCOV
215
                                "I have already commented on the pr: %s/%d, no need to keep on nagging",
×
UNCOV
216
                                pr.GetRepo().GetName(), pr.GetNumber())
×
UNCOV
217
                }
×
218
        }
219

220
        return nil
2✔
221
}
222

223
func postGitHubMessage(
224
        ctx *gin.Context,
225
        pr *github.PullRequestEvent,
226
        log *logrus.Entry,
227
        msg string,
228
) {
1✔
229
        if err := githubClient.CreateComment(
1✔
230
                ctx,
1✔
231
                pr.GetOrganization().GetLogin(),
1✔
232
                pr.GetRepo().GetName(),
1✔
233
                pr.GetNumber(),
1✔
234
                &github.IssueComment{Body: github.String(msg)},
1✔
235
        ); err != nil {
1✔
236
                log.Infof("Failed to comment on the pr: %v, Error: %s", pr, err.Error())
×
237
        }
×
238
}
239

240
func getFirstMatchingBotCommentInPR(
241
        log *logrus.Entry,
242
        githubClient clientgithub.Client,
243
        pr *github.PullRequestEvent,
244
        botComment string,
245
        conf *config,
246
) *github.IssueComment {
13✔
247

13✔
248
        comments, err := githubClient.ListComments(
13✔
249
                context.Background(),
13✔
250
                conf.githubOrganization,
13✔
251
                pr.GetRepo().GetName(),
13✔
252
                pr.GetNumber(),
13✔
253
                &github.IssueListCommentsOptions{
13✔
254
                        Sort:      "created",
13✔
255
                        Direction: "asc",
13✔
256
                })
13✔
257
        if err != nil {
14✔
258
                log.Errorf("Failed to list the comments on PR: %s/%d, err: '%s'",
1✔
259
                        pr.GetRepo().GetName(), pr.GetNumber(), err)
1✔
260
                return nil
1✔
261
        }
1✔
262
        for _, comment := range comments {
22✔
263
                if comment.Body != nil &&
10✔
264
                        strings.Contains(*comment.Body, botComment) &&
10✔
265
                        comment.User != nil &&
10✔
266
                        comment.User.Login != nil &&
10✔
267
                        *comment.User.Login == githubBotName {
17✔
268
                        return comment
7✔
269
                }
7✔
270
        }
271
        return nil
5✔
272
}
273

274
func handleChangelogComments(
275
        log *logrus.Entry,
276
        ctx *gin.Context,
277
        githubClient clientgithub.Client,
278
        pr *github.PullRequestEvent,
279
        conf *config,
280
) {
1✔
281
        // It would be semantically correct to update the integration repo
1✔
282
        // here. However, this step is carried out on every PR update, causing a
1✔
283
        // big amount of "git fetch" requests, which both reduces performance,
1✔
284
        // and could result in rate limiting. Instead, we assume that the
1✔
285
        // integration repo is recent enough, since it is still updated when
1✔
286
        // doing mender-qa builds.
1✔
287
        //
1✔
288
        // // First update integration repo.
1✔
289
        // err := updateIntegrationRepo(conf)
1✔
290
        // if err != nil {
1✔
291
        //         log.Errorf("Could not update integration repo: %s", err.Error())
1✔
292
        //         // Should still be safe to continue though.
1✔
293
        // }
1✔
294

1✔
295
        // Only do changelog commenting for mendersoftware repositories.
1✔
296
        if pr.GetPullRequest().GetBase().GetRepo().GetOwner().GetLogin() != "mendersoftware" {
1✔
297
                log.Info("Not a mendersoftware repository. Ignoring.")
×
298
                return
×
299
        }
×
300

301
        changelogText, warningText, err := fetchChangelogTextForPR(log, pr, conf)
1✔
302
        if err != nil {
1✔
303
                log.Errorf("Error while fetching changelog text: %s", err.Error())
×
304
                return
×
305
        }
×
306

307
        updatePullRequestChangelogComments(log, ctx, githubClient, pr, conf,
1✔
308
                changelogText, warningText)
1✔
309
}
310

311
func fetchChangelogTextForPR(
312
        log *logrus.Entry,
313
        pr *github.PullRequestEvent,
314
        conf *config,
315
) (string, string, error) {
1✔
316

1✔
317
        repo := pr.GetPullRequest().GetBase().GetRepo().GetName()
1✔
318
        baseSHA := pr.GetPullRequest().GetBase().GetSHA()
1✔
319
        headSHA := pr.GetPullRequest().GetHead().GetSHA()
1✔
320
        baseRef := pr.GetPullRequest().GetBase().GetRef()
1✔
321
        headRef := pr.GetPullRequest().GetHead().GetRef()
1✔
322
        versionRange := fmt.Sprintf(
1✔
323
                "%s..%s",
1✔
324
                baseSHA,
1✔
325
                headSHA,
1✔
326
        )
1✔
327

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

1✔
330
        // Generate the changelog text for this PR.
1✔
331
        changelogText, warningText, err := getChangelogText(
1✔
332
                repo, versionRange, conf)
1✔
333
        if err != nil {
1✔
334
                err = errors.Wrap(err, "Not able to get changelog text")
×
335
        }
×
336

337
        // Replace SHAs with the original ref names, so that the changelog text
338
        // does not change on every commit amend. The reason we did not use ref
339
        // names to begin with is that they may live in personal forks, so it
340
        // complicates the fetching mechanism. SHAs however, are always present
341
        // in the repository you are merging into.
342
        //
343
        // Fetching changelogs online from personal forks is pretty unlikely to
344
        // be useful outside of the integration-test-runner niche (better to use
345
        // the local version), therefore we do this replacement instead of
346
        // making the changelog-generator "fork aware".
347
        changelogText = strings.ReplaceAll(changelogText, baseSHA, baseRef)
1✔
348
        changelogText = strings.ReplaceAll(changelogText, headSHA, headRef)
1✔
349

1✔
350
        log.Debugf("Prepared changelog text: %s", changelogText)
1✔
351
        log.Debugf("Got warning text: %s", warningText)
1✔
352

1✔
353
        return changelogText, warningText, err
1✔
354
}
355

356
func assembleCommentText(changelogText, warningText string) string {
21✔
357
        commentText := changelogPrefix + changelogText
21✔
358
        if warningText != "" {
25✔
359
                commentText += warningHeader + warningText
4✔
360
        }
4✔
361
        return commentText
21✔
362
}
363

364
func updatePullRequestChangelogComments(
365
        log *logrus.Entry,
366
        ctx *gin.Context,
367
        githubClient clientgithub.Client,
368
        pr *github.PullRequestEvent,
369
        conf *config,
370
        changelogText string,
371
        warningText string,
372
) {
11✔
373
        var err error
11✔
374

11✔
375
        commentText := assembleCommentText(changelogText, warningText)
11✔
376
        emptyChangelog := (changelogText == "" ||
11✔
377
                strings.HasSuffix(changelogText, "### Changelogs\n\n"))
11✔
378

11✔
379
        comment := getFirstMatchingBotCommentInPR(log, githubClient, pr, changelogPrefix, conf)
11✔
380
        if comment != nil {
17✔
381
                // There is a previous comment about changelog.
6✔
382
                if *comment.Body == commentText {
9✔
383
                        log.Debugf("The changelog hasn't changed (comment ID: %d). Leave it alone.",
3✔
384
                                comment.ID)
3✔
385
                        return
3✔
386
                } else {
6✔
387
                        log.Debugf("Deleting old changelog comment (comment ID: %d).",
3✔
388
                                comment.ID)
3✔
389
                        err = githubClient.DeleteComment(
3✔
390
                                ctx,
3✔
391
                                conf.githubOrganization,
3✔
392
                                pr.GetRepo().GetName(),
3✔
393
                                *comment.ID,
3✔
394
                        )
3✔
395
                        if err != nil {
3✔
396
                                log.Errorf("Could not delete changelog comment: %s",
×
397
                                        err.Error())
×
398
                        }
×
399
                }
400
        } else if emptyChangelog {
7✔
401
                log.Info("Changelog is empty, and there is no previous changelog comment. Stay silent.")
2✔
402
                return
2✔
403
        }
2✔
404

405
        commentBody := &github.IssueComment{
6✔
406
                Body: &commentText,
6✔
407
        }
6✔
408
        err = githubClient.CreateComment(
6✔
409
                ctx,
6✔
410
                conf.githubOrganization,
6✔
411
                pr.GetRepo().GetName(),
6✔
412
                pr.GetNumber(),
6✔
413
                commentBody,
6✔
414
        )
6✔
415
        if err != nil {
6✔
416
                log.Errorf("Could not post changelog comment: %s. Comment text: %s",
×
417
                        err.Error(), commentText)
×
418
        }
×
419
}
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