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

mendersoftware / integration-test-runner / 1681953125

21 Feb 2025 08:24AM UTC coverage: 59.965% (-4.4%) from 64.328%
1681953125

Pull #357

gitlab-ci

danielskinstad
feat: run integration pipeline on demand

Allow you to run the full integration pipeline on a protected branch
when a trusted member runs `start integration pipeline`.

As of now, this will only run with main - and you cannot specify PRs to
run the pipeline with.

Ticket: QA-863

Signed-off-by: Daniel Skinstad Drabitzius <daniel.drabitzius@northern.tech>
Pull Request #357: wip: integration tests on demand

77 of 311 new or added lines in 7 files covered. (24.76%)

2 existing lines in 1 file now uncovered.

1736 of 2895 relevant lines covered (59.97%)

2.19 hits per line

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

59.43
/main_comment.go
1
package main
2

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

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

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

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

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

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

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

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

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

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

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

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

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

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

×
NEW
106
                }
×
107

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

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

179
        return nil
3✔
180
}
181

NEW
182
func protectBranch(conf *config, branchName string, pipelinePath string) error {
×
NEW
183
        // https://docs.gitlab.com/ee/api/protected_branches.html#protect-repository-branches
×
NEW
184
        opt := &gitlab.ProtectRepositoryBranchesOptions{
×
NEW
185
                Name: &branchName,
×
NEW
186
        }
×
NEW
187

×
NEW
188
        client, err := clientgitlab.NewGitLabClient(
×
NEW
189
                conf.gitlabToken,
×
NEW
190
                conf.gitlabBaseURL,
×
NEW
191
                conf.dryRunMode,
×
NEW
192
        )
×
NEW
193

×
NEW
194
        if err != nil {
×
NEW
195
                return err
×
NEW
196
        }
×
197

NEW
198
        _, err = client.ProtectRepositoryBranches(pipelinePath, opt)
×
NEW
199
        if err != nil {
×
NEW
200
                return fmt.Errorf("%v returned error: %s", err, err.Error())
×
NEW
201
        }
×
NEW
202
        return nil
×
203
}
204

205
func syncProtectedBranch(
206
        log *logrus.Entry,
207
        pr *github.PullRequestEvent,
208
        conf *config,
209
        pipelinePath string,
NEW
210
) (string, error) {
×
NEW
211
        prBranchName := "pr_" + strconv.Itoa(pr.GetNumber()) + "_protected"
×
NEW
212
        if err := syncBranch(prBranchName, log, pr, conf); err != nil {
×
NEW
213
                mainErrMsg := "There was an error syncing branches"
×
NEW
214
                return "", fmt.Errorf("%v returned error: %s: %s", err, mainErrMsg, err.Error())
×
NEW
215
        }
×
NEW
216
        if err := protectBranch(conf, prBranchName, pipelinePath); err != nil {
×
NEW
217
                return "", fmt.Errorf("%v returned error: %s", err, err.Error())
×
NEW
218
        }
×
NEW
219
        return prBranchName, nil
×
220
}
221

222
func syncPRBranch(
223
        ctx *gin.Context,
224
        comment *github.IssueCommentEvent,
225
        pr *github.PullRequest,
226
        log *logrus.Entry,
227
        conf *config,
UNCOV
228
) {
×
UNCOV
229
        prEvent := &github.PullRequestEvent{
×
230
                Repo:        comment.GetRepo(),
×
231
                Number:      github.Int(pr.GetNumber()),
×
232
                PullRequest: pr,
×
233
        }
×
234
        if _, err := syncPullRequestBranch(log, prEvent, conf); err != nil {
×
235
                mainErrMsg := "There was an error syncing branches"
×
236
                log.Errorf(mainErrMsg+": %s", err.Error())
×
237
                msg := mainErrMsg + ", " + msgDetailsKubernetesLog
×
238
                postGitHubMessage(ctx, prEvent, log, msg)
×
239
        }
×
240
}
241

242
// parsing `start client pipeline --pr mender-connect/pull/88/head --pr deviceconnect/pull/12/head
243
// --pr mender/3.1.x --fast sugar pretty please`
244
//
245
//        BuildOptions {
246
//                Fast: true,
247
//                PullRequests: map[string]string{
248
//                        "mender-connect": "pull/88/head",
249
//                        "deviceconnect": "pull/12/head",
250
//                }
251
//        }
252
func parseBuildOptions(commentBody string) (*BuildOptions, error) {
16✔
253
        buildOptions := NewBuildOptions()
16✔
254
        var err error
16✔
255
        words := strings.Fields(commentBody)
16✔
256
        tokensCount := len(words)
16✔
257
        for id, word := range words {
135✔
258
                if word == "--pr" && id < (tokensCount-1) {
151✔
259
                        userInput := strings.TrimSpace(words[id+1])
32✔
260
                        userInputParts := strings.Split(userInput, "/")
32✔
261

32✔
262
                        if len(userInput) > 0 {
64✔
263
                                var revision string
32✔
264
                                switch len(userInputParts) {
32✔
265
                                case 2: // we can have both deviceauth/1 and mender/3.1.x syntax
12✔
266
                                        // repo/<pr_number> syntax
12✔
267
                                        if _, err := strconv.Atoi(userInputParts[1]); err == nil {
15✔
268
                                                revision = "pull/" + userInputParts[1] + "/head"
3✔
269
                                        } else {
13✔
270
                                                // feature branch
10✔
271
                                                revision = userInputParts[1]
10✔
272
                                        }
10✔
273
                                case 3: // deviceconnect/pull/12 syntax
2✔
274
                                        revision = strings.Join(userInputParts[1:], "/") + "/head"
2✔
275
                                case 4: // deviceauth/pull/1/head syntax
13✔
276
                                        revision = strings.Join(userInputParts[1:], "/")
13✔
277
                                default:
6✔
278
                                        err = errors.New(
6✔
279
                                                "parse error near '" + userInput + "', I need, e.g.: start client" +
6✔
280
                                                        " pipeline --pr somerepo/pull/12/head --pr somerepo/1.0.x ",
6✔
281
                                        )
6✔
282
                                }
283
                                buildOptions.PullRequests[userInputParts[0]] = revision
32✔
284
                        }
285
                } else if word == "--fast" {
89✔
286
                        buildOptions.Fast = true
1✔
287
                }
1✔
288
        }
289

290
        return buildOptions, err
16✔
291
}
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