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

mendersoftware / integration-test-runner / 1583018314

10 Dec 2024 11:26PM UTC coverage: 68.703%. Remained the same
1583018314

Pull #336

gitlab-ci

danielskinstad
ci: pin docker to v27.3

Fixes runc errors in the hetzner runner
Use dependency proxy

Ticket: QA-823

Signed-off-by: Daniel Skinstad Drabitzius <daniel.drabitzius@northern.tech>
Pull Request #336: ci: pin docker to v27.3

1732 of 2521 relevant lines covered (68.7%)

2.49 hits per line

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

65.52
/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 pipeline for PR '%d' is skipped", pr.Number)
×
109
                                                return noRetry
×
110
                                        default:
×
111
                                                log.Errorf("failed to start 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 := stopBuildsOfStalePRs(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 := parsePullRequest(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 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 integration pipeline by " +
1✔
177
                        "mentioning me and the command \""
1✔
178
                if getFirstMatchingBotCommentInPR(log, githubClient, pr, botCommentString, conf) == nil {
1✔
179

×
180
                        msg := "@" + pr.GetSender().GetLogin() + botCommentString + commandStartPipeline + "\"."
×
181
                        msg += `
×
182

×
183
   ---
×
184

×
185
   <details>
×
186
   <summary>my commands and options</summary>
×
187
   <br />
×
188

×
189
   You can trigger a pipeline on multiple prs with:
×
190
   - mentioning me and ` + "`" + `start pipeline --pr mender/127 --pr mender-connect/255` + "`" + `
×
191

×
192
   You can start a fast pipeline, disabling full integration tests with:
×
193
   - mentioning me and ` + "`" + `start pipeline --fast` + "`" + `
×
194

×
195
   You can trigger GitHub->GitLab branch sync with:
×
196
   - mentioning me and ` + "`" + `sync` + "`" + `
×
197

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

215
        return nil
2✔
216
}
217

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

235
func getFirstMatchingBotCommentInPR(
236
        log *logrus.Entry,
237
        githubClient clientgithub.Client,
238
        pr *github.PullRequestEvent,
239
        botComment string,
240
        conf *config,
241
) *github.IssueComment {
13✔
242

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

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

1✔
290
        // Only do changelog commenting for mendersoftware repositories.
1✔
291
        if pr.GetPullRequest().GetBase().GetRepo().GetOwner().GetLogin() != "mendersoftware" {
1✔
292
                log.Info("Not a mendersoftware repository. Ignoring.")
×
293
                return
×
294
        }
×
295

296
        changelogText, warningText, err := fetchChangelogTextForPR(log, pr, conf)
1✔
297
        if err != nil {
1✔
298
                log.Errorf("Error while fetching changelog text: %s", err.Error())
×
299
                return
×
300
        }
×
301

302
        updatePullRequestChangelogComments(log, ctx, githubClient, pr, conf,
1✔
303
                changelogText, warningText)
1✔
304
}
305

306
func fetchChangelogTextForPR(
307
        log *logrus.Entry,
308
        pr *github.PullRequestEvent,
309
        conf *config,
310
) (string, string, error) {
1✔
311

1✔
312
        repo := pr.GetPullRequest().GetBase().GetRepo().GetName()
1✔
313
        baseSHA := pr.GetPullRequest().GetBase().GetSHA()
1✔
314
        headSHA := pr.GetPullRequest().GetHead().GetSHA()
1✔
315
        baseRef := pr.GetPullRequest().GetBase().GetRef()
1✔
316
        headRef := pr.GetPullRequest().GetHead().GetRef()
1✔
317
        versionRange := fmt.Sprintf(
1✔
318
                "%s..%s",
1✔
319
                baseSHA,
1✔
320
                headSHA,
1✔
321
        )
1✔
322

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

1✔
325
        // Generate the changelog text for this PR.
1✔
326
        changelogText, warningText, err := getChangelogText(
1✔
327
                repo, versionRange, conf)
1✔
328
        if err != nil {
1✔
329
                err = errors.Wrap(err, "Not able to get changelog text")
×
330
        }
×
331

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

1✔
345
        log.Debugf("Prepared changelog text: %s", changelogText)
1✔
346
        log.Debugf("Got warning text: %s", warningText)
1✔
347

1✔
348
        return changelogText, warningText, err
1✔
349
}
350

351
func assembleCommentText(changelogText, warningText string) string {
21✔
352
        commentText := changelogPrefix + changelogText
21✔
353
        if warningText != "" {
25✔
354
                commentText += warningHeader + warningText
4✔
355
        }
4✔
356
        return commentText
21✔
357
}
358

359
func updatePullRequestChangelogComments(
360
        log *logrus.Entry,
361
        ctx *gin.Context,
362
        githubClient clientgithub.Client,
363
        pr *github.PullRequestEvent,
364
        conf *config,
365
        changelogText string,
366
        warningText string,
367
) {
11✔
368
        var err error
11✔
369

11✔
370
        commentText := assembleCommentText(changelogText, warningText)
11✔
371
        emptyChangelog := (changelogText == "" ||
11✔
372
                strings.HasSuffix(changelogText, "### Changelogs\n\n"))
11✔
373

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

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