• 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

74.32
/main_comment.go
1
package main
2

3
import (
4
        "errors"
5
        "fmt"
6
        "strconv"
7
        "strings"
8
        "time"
9

10
        "github.com/davecgh/go-spew/spew"
11
        "github.com/gin-gonic/gin"
12
        "github.com/google/go-github/v28/github"
13
        "github.com/sirupsen/logrus"
14

15
        gitlab "gitlab.com/gitlab-org/api/client-go"
16

17
        clientgithub "github.com/mendersoftware/integration-test-runner/client/github"
18
        clientgitlab "github.com/mendersoftware/integration-test-runner/client/gitlab"
19
)
20

21
// nolint: gocyclo
22
func processGitHubComment(
23
        ctx *gin.Context,
24
        comment *github.IssueCommentEvent,
25
        githubClient clientgithub.Client,
26
        conf *config,
27
) error {
11✔
28
        log := getCustomLoggerFromContext(ctx)
11✔
29

11✔
30
        // process created actions only, ignore the others
11✔
31
        action := comment.GetAction()
11✔
32
        if action != "created" {
12✔
33
                log.Infof("Ignoring action %s on comment", action)
1✔
34
                return nil
1✔
35
        }
1✔
36

37
        // accept commands only from organization members
38
        if !githubClient.IsOrganizationMember(ctx, conf.githubOrganization, comment.Sender.GetLogin()) {
13✔
39
                log.Warnf(
3✔
40
                        "%s commented, but he/she is not a member of our organization, ignoring",
3✔
41
                        comment.Sender.GetLogin(),
3✔
42
                )
3✔
43
                return nil
3✔
44
        }
3✔
45

46
        // but ignore comments from myself
47
        if comment.Sender.GetLogin() == githubBotName {
7✔
48
                log.Warnf("%s commented, probably giving instructions, ignoring", comment.Sender.GetLogin())
×
49
                return nil
×
50
        }
×
51

52
        // filter comments mentioning the bot
53
        commentBody := comment.Comment.GetBody()
7✔
54
        if !strings.Contains(commentBody, "@"+githubBotName) {
7✔
55
                log.Info("ignoring comment not mentioning me")
×
56
                return nil
×
57
        }
×
58

59
        // retrieve the pull request
60
        prLink := comment.Issue.GetPullRequestLinks().GetURL()
7✔
61
        if prLink == "" {
8✔
62
                log.Warnf("ignoring comment not on a pull request")
1✔
63
                return nil
1✔
64
        }
1✔
65

66
        prLinkParts := strings.Split(prLink, "/")
6✔
67
        prNumber, err := strconv.Atoi(prLinkParts[len(prLinkParts)-1])
6✔
68
        if err != nil {
7✔
69
                log.Errorf("Unable to retrieve the pull request: %s", err.Error())
1✔
70
                return err
1✔
71
        }
1✔
72

73
        pr, err := githubClient.GetPullRequest(
5✔
74
                ctx,
5✔
75
                conf.githubOrganization,
5✔
76
                comment.GetRepo().GetName(),
5✔
77
                prNumber,
5✔
78
        )
5✔
79
        if err != nil {
6✔
80
                log.Errorf("Unable to retrieve the pull request: %s", err.Error())
1✔
81
                return err
1✔
82
        }
1✔
83

84
        // extract the command and check it is valid
85
        switch {
4✔
86
        case strings.Contains(commentBody, commandStartIntegrationPipeline):
1✔
87
                prRequest := &github.PullRequestEvent{
1✔
88
                        Repo:        comment.GetRepo(),
1✔
89
                        Number:      github.Int(pr.GetNumber()),
1✔
90
                        PullRequest: pr,
1✔
91
                }
1✔
92
                build := getIntegrationBuild(log, conf, prRequest)
1✔
93

1✔
94
                _, err = syncProtectedBranch(log, prRequest, conf, integrationPipelinePath)
1✔
95
                if err != nil {
1✔
96
                        _ = say(ctx, "There was an error while syncing branches: {{.ErrorMessage}}",
×
97
                                struct {
×
98
                                        ErrorMessage string
×
99
                                }{
×
100
                                        ErrorMessage: err.Error(),
×
101
                                },
×
102
                                log,
×
103
                                conf,
×
104
                                prRequest)
×
105
                        return err
×
106

×
107
                }
×
108

109
                // start the build
110
                if err := triggerIntegrationBuild(log, conf, &build, prRequest, nil); err != nil {
1✔
111
                        log.Errorf("Could not start build: %s", err.Error())
×
112
                }
×
113
        case strings.Contains(commentBody, commandStartClientPipeline):
4✔
114
                buildOptions, err := parseBuildOptions(commentBody)
4✔
115
                // get the list of builds
4✔
116
                prRequest := &github.PullRequestEvent{
4✔
117
                        Repo:        comment.GetRepo(),
4✔
118
                        Number:      github.Int(pr.GetNumber()),
4✔
119
                        PullRequest: pr,
4✔
120
                }
4✔
121
                if err != nil {
5✔
122
                        _ = say(ctx, "There was an error while parsing arguments: {{.ErrorMessage}}",
1✔
123
                                struct {
1✔
124
                                        ErrorMessage string
1✔
125
                                }{
1✔
126
                                        ErrorMessage: err.Error(),
1✔
127
                                },
1✔
128
                                log,
1✔
129
                                conf,
1✔
130
                                prRequest)
1✔
131
                        return err
1✔
132
                }
1✔
133
                builds := parseClientPullRequest(log, conf, "opened", prRequest)
3✔
134
                log.Infof(
3✔
135
                        "%s:%d will trigger %d builds",
3✔
136
                        comment.GetRepo().GetName(),
3✔
137
                        pr.GetNumber(),
3✔
138
                        len(builds),
3✔
139
                )
3✔
140

3✔
141
                // start the builds
3✔
142
                for idx, build := range builds {
4✔
143
                        log.Infof("%d: "+spew.Sdump(build)+"\n", idx+1)
1✔
144
                        if build.repo == "meta-mender" && build.baseBranch == "master-next" {
1✔
145
                                log.Info("Skipping build targeting meta-mender:master-next")
×
146
                                continue
×
147
                        }
148
                        if err := triggerClientBuild(log, conf, &build, prRequest, buildOptions); err != nil {
1✔
149
                                log.Errorf("Could not start build: %s", err.Error())
×
150
                        }
×
151
                }
152
        case strings.Contains(commentBody, commandCherryPickBranch):
1✔
153
                log.Infof("Attempting to cherry-pick the changes in PR: %s/%d",
1✔
154
                        comment.GetRepo().GetName(),
1✔
155
                        pr.GetNumber(),
1✔
156
                )
1✔
157
                err = cherryPickPR(log, comment, pr, conf, commentBody, githubClient)
1✔
158
                if err != nil {
1✔
159
                        log.Error(err)
×
160
                }
×
161
        case strings.Contains(commentBody, commandConventionalCommit) &&
162
                strings.Contains(pr.GetUser().GetLogin(), "dependabot"):
1✔
163
                log.Infof(
1✔
164
                        "Attempting to make the PR: %s/%d and commit: %s a conventional commit",
1✔
165
                        comment.GetRepo().GetName(),
1✔
166
                        pr.GetNumber(),
1✔
167
                        pr.GetHead().GetSHA(),
1✔
168
                )
1✔
169
                err = conventionalComittifyDependabotPr(log, comment, pr, conf, commentBody, githubClient)
1✔
170
                if err != nil {
1✔
171
                        log.Error(err)
×
172
                }
×
173
        case strings.Contains(commentBody, commandSyncRepos):
×
174
                syncPRBranch(ctx, comment, pr, log, conf)
×
175
        default:
×
176
                log.Warnf("no command found: %s", commentBody)
×
177
                return nil
×
178
        }
179

180
        return nil
3✔
181
}
182

183
func protectBranch(conf *config, branchName string, pipelinePath string) error {
1✔
184
        // https://docs.gitlab.com/ee/api/protected_branches.html#protect-repository-branches
1✔
185
        allow_force_push := false
1✔
186
        opt := &gitlab.ProtectRepositoryBranchesOptions{
1✔
187
                Name:           &branchName,
1✔
188
                AllowForcePush: &allow_force_push,
1✔
189
        }
1✔
190

1✔
191
        client, err := clientgitlab.NewGitLabClient(
1✔
192
                conf.gitlabToken,
1✔
193
                conf.gitlabBaseURL,
1✔
194
                conf.dryRunMode,
1✔
195
        )
1✔
196

1✔
197
        if err != nil {
1✔
198
                return err
×
199
        }
×
200

201
        _, err = client.ProtectRepositoryBranches(pipelinePath, opt)
1✔
202
        if err != nil {
1✔
203
                return fmt.Errorf("%v returned error: %s", err, err.Error())
×
204
        }
×
205
        return nil
1✔
206
}
207

208
func syncProtectedBranch(
209
        log *logrus.Entry,
210
        pr *github.PullRequestEvent,
211
        conf *config,
212
        pipelinePath string,
213
) (string, error) {
1✔
214
        prBranchName := "pr_" + strconv.Itoa(pr.GetNumber()) + "_protected"
1✔
215

1✔
216
        // check if we have a protected branch and try to delete it
1✔
217
        response, err := deletePRBranch(pr, conf, fmt.Sprintf("pr_%d_protected", pr.GetNumber()), log)
1✔
218
        if err != nil {
1✔
NEW
219
                return "", fmt.Errorf("Got response: %d. Failed to delete PR branch: %s",
×
NEW
220
                        response.StatusCode, err.Error())
×
UNCOV
221
        }
×
222
        if err := syncBranch(prBranchName, log, pr, conf); err != nil {
1✔
223
                mainErrMsg := "There was an error syncing branches"
×
224
                return "", fmt.Errorf("%v returned error: %s: %s", err, mainErrMsg, err.Error())
×
225
        }
×
226
        // Arbitrary sleep to ensure the branch is
227
        // created before we protect it
228
        time.Sleep(time.Duration(5) * time.Second)
1✔
229
        if err := protectBranch(conf, prBranchName, pipelinePath); err != nil {
1✔
230
                return "", fmt.Errorf("%v returned error: %s", err, err.Error())
×
231
        }
×
232
        return prBranchName, nil
1✔
233
}
234

235
func syncPRBranch(
236
        ctx *gin.Context,
237
        comment *github.IssueCommentEvent,
238
        pr *github.PullRequest,
239
        log *logrus.Entry,
240
        conf *config,
241
) {
×
242
        prEvent := &github.PullRequestEvent{
×
243
                Repo:        comment.GetRepo(),
×
244
                Number:      github.Int(pr.GetNumber()),
×
245
                PullRequest: pr,
×
246
        }
×
247
        if _, err := syncPullRequestBranch(log, prEvent, conf); err != nil {
×
248
                mainErrMsg := "There was an error syncing branches"
×
249
                log.Errorf(mainErrMsg+": %s", err.Error())
×
250
                msg := mainErrMsg + ", " + msgDetailsKubernetesLog
×
251
                postGitHubMessage(ctx, prEvent, log, msg)
×
252
        }
×
253
}
254

255
// parsing `start client pipeline --pr mender-connect/pull/88/head --pr deviceconnect/pull/12/head
256
// --pr mender/3.1.x --fast sugar pretty please`
257
//
258
//        BuildOptions {
259
//                Fast: true,
260
//                PullRequests: map[string]string{
261
//                        "mender-connect": "pull/88/head",
262
//                        "deviceconnect": "pull/12/head",
263
//                }
264
//        }
265
func parseBuildOptions(commentBody string) (*BuildOptions, error) {
16✔
266
        buildOptions := NewBuildOptions()
16✔
267
        var err error
16✔
268
        words := strings.Fields(commentBody)
16✔
269
        tokensCount := len(words)
16✔
270
        for id, word := range words {
135✔
271
                if word == "--pr" && id < (tokensCount-1) {
151✔
272
                        userInput := strings.TrimSpace(words[id+1])
32✔
273
                        userInputParts := strings.Split(userInput, "/")
32✔
274

32✔
275
                        if len(userInput) > 0 {
64✔
276
                                var revision string
32✔
277
                                switch len(userInputParts) {
32✔
278
                                case 2: // we can have both deviceauth/1 and mender/3.1.x syntax
12✔
279
                                        // repo/<pr_number> syntax
12✔
280
                                        if _, err := strconv.Atoi(userInputParts[1]); err == nil {
15✔
281
                                                revision = "pull/" + userInputParts[1] + "/head"
3✔
282
                                        } else {
13✔
283
                                                // feature branch
10✔
284
                                                revision = userInputParts[1]
10✔
285
                                        }
10✔
286
                                case 3: // deviceconnect/pull/12 syntax
2✔
287
                                        revision = strings.Join(userInputParts[1:], "/") + "/head"
2✔
288
                                case 4: // deviceauth/pull/1/head syntax
13✔
289
                                        revision = strings.Join(userInputParts[1:], "/")
13✔
290
                                default:
6✔
291
                                        err = errors.New(
6✔
292
                                                "parse error near '" + userInput + "', I need, e.g.: start client" +
6✔
293
                                                        " pipeline --pr somerepo/pull/12/head --pr somerepo/1.0.x ",
6✔
294
                                        )
6✔
295
                                }
296
                                buildOptions.PullRequests[userInputParts[0]] = revision
32✔
297
                        }
298
                } else if word == "--fast" {
89✔
299
                        buildOptions.Fast = true
1✔
300
                }
1✔
301
        }
302

303
        return buildOptions, err
16✔
304
}
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