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

mendersoftware / integration-test-runner / 1887464315

16 Jun 2025 02:47PM UTC coverage: 64.238% (+64.2%) from 0.0%
1887464315

push

gitlab-ci

web-flow
Merge pull request #385 from mzedel/qa-1017

QA-1017 - dependabot reviewers to CODEOWNERS

1904 of 2964 relevant lines covered (64.24%)

2.14 hits per line

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

77.89
/main.go
1
package main
2

3
import (
4
        "context"
5
        "errors"
6
        "fmt"
7
        "io"
8
        "net/http"
9
        "os"
10
        "os/signal"
11
        "strings"
12
        "time"
13

14
        "github.com/davecgh/go-spew/spew"
15
        "github.com/gin-gonic/gin"
16
        "github.com/google/go-github/v28/github"
17
        "github.com/sirupsen/logrus"
18
        "golang.org/x/sys/unix"
19

20
        clientgithub "github.com/mendersoftware/integration-test-runner/client/github"
21
        "github.com/mendersoftware/integration-test-runner/git"
22
        "github.com/mendersoftware/integration-test-runner/logger"
23
)
24

25
type config struct {
26
        dryRunMode             bool
27
        githubSecret           []byte
28
        githubProtocol         gitProtocol
29
        githubOrganization     string
30
        githubToken            string
31
        gitlabToken            string
32
        gitlabBaseURL          string
33
        integrationDirectory   string
34
        isProcessPushEvents    bool
35
        isProcessPREvents      bool
36
        isProcessCommentEvents bool
37
        reposSyncList          []string
38
}
39

40
type buildOptions struct {
41
        pr         string
42
        repo       string
43
        baseBranch string
44
        commitSHA  string
45
        makeQEMU   bool
46
}
47

48
// Mapping https://github.com/<org> -> https://gitlab.com/Northern.tech/<group>
49
var gitHubOrganizationToGitLabGroup = map[string]string{
50
        "mendersoftware": "Mender",
51
        "cfengine":       "CFEngine",
52
        "NorthernTechHQ": "NorthernTechHQ",
53
}
54

55
// Mapping of special repos that have a custom group/project
56
var gitHubRepoToGitLabProjectCustom = map[string]string{
57
        "saas": "Northern.tech/MenderSaaS/saas",
58
}
59

60
// Mender Client LTS components according to
61
// https://docs.mender.io/release-information/supported-releases#mender-client-subcomponents
62
var clientRepositories = []string{
63
        "mender",
64
        "mender-connect",
65
        "mender-configure-module",
66
        "monitor-client",
67
        "mender-flash",
68
        // TODO: QA-XXX: add when defining the new Mender Client release process
69
        // "mender-binary-delta",
70
}
71

72
// Repositories for which to build the Client Pipeline
73
var clientPipelineRepositories = append(clientRepositories,
74
        // Yocto layer
75
        "meta-mender",
76
        // TODO: QA-985: remove independent tools
77
        "mender-artifact",
78
        "mender-snapshot",
79
)
80

81
// Repositories with opt-in pipelines
82
var pipelineRepositories = append(clientPipelineRepositories,
83
        "integration",
84
)
85

86
// LTS repositories for which to suggest cherry-picks.
87
var ltsRepositories = append(clientRepositories,
88
        "mender-gateway",
89
)
90

91
const (
92
        KiB = 1024
93
        MiB = 1024 * KiB
94
)
95

96
const (
97
        gitOperationTimeout = 30
98
)
99

100
const (
101
        featureBranchPrefix = "feature-"
102
)
103

104
const (
105
        githubBotName = "mender-test-bot"
106
)
107

108
const (
109
        commandStartIntegrationPipeline = "start integration pipeline"
110
        commandStartClientPipeline      = "start client pipeline"
111
        commandCherryPickBranch         = "cherry-pick to:"
112
        commandConventionalCommit       = "mark-pr as"
113
        commandSyncRepos                = "sync"
114
)
115

116
func getConfig() (*config, error) {
1✔
117
        var reposSyncList []string
1✔
118
        dryRunMode := os.Getenv("DRY_RUN") != ""
1✔
119
        githubSecret := os.Getenv("GITHUB_SECRET")
1✔
120
        githubToken := os.Getenv("GITHUB_TOKEN")
1✔
121
        gitlabToken := os.Getenv("GITLAB_TOKEN")
1✔
122
        gitlabBaseURL := os.Getenv("GITLAB_BASE_URL")
1✔
123
        integrationDirectory := "/integration/"
1✔
124
        if integrationDirEnv := os.Getenv("INTEGRATION_DIRECTORY"); integrationDirEnv != "" {
1✔
125
                integrationDirectory = integrationDirEnv
×
126
        }
×
127

128
        //
129
        // Currently we don't have a distinguishment between GitHub events and features.
130
        // Different features might be implemented across different events, but in future
131
        // it's probability that we might implement proper features selection. For now the
132
        // straight goal is to being able to configure the runner to only sync repos on
133
        // push events and disable all the rest (to be used by the CFEngine team).
134
        //
135
        // default: process push events (sync repos) if not explicitly disabled
136
        isProcessPushEvents := os.Getenv("DISABLE_PUSH_EVENTS_PROCESSING") == ""
1✔
137
        // default: process PR events if not explicitly disabled
1✔
138
        isProcessPREvents := os.Getenv("DISABLE_PR_EVENTS_PROCESSING") == ""
1✔
139
        // default: process comment events if not explicitly disabled
1✔
140
        isProcessCommentEvents := os.Getenv("DISABLE_COMMENT_EVENTS_PROCESSING") == ""
1✔
141

1✔
142
        logLevel, found := os.LookupEnv("INTEGRATION_TEST_RUNNER_LOG_LEVEL")
1✔
143
        logrus.SetLevel(logrus.InfoLevel)
1✔
144
        if found {
2✔
145
                lvl, err := logrus.ParseLevel(logLevel)
1✔
146
                if err != nil {
1✔
147
                        logrus.Infof(
×
148
                                "Failed to parse the 'INTEGRATION_TEST_RUNNER_LOG_LEVEL' variable, " +
×
149
                                        "defaulting to 'InfoLevel'",
×
150
                        )
×
151
                } else {
1✔
152
                        logrus.Infof("Set 'LogLevel' to %s", lvl)
1✔
153
                        logrus.SetLevel(lvl)
1✔
154
                }
1✔
155
        }
156

157
        // Comma separated list of repos to sync (GitHub->GitLab)
158
        reposSyncListRaw, found := os.LookupEnv("SYNC_REPOS_LIST")
1✔
159
        if found {
1✔
160
                reposSyncList = strings.Split(reposSyncListRaw, ",")
×
161
        }
×
162

163
        switch {
1✔
164
        case githubSecret == "" && !dryRunMode:
×
165
                return &config{}, fmt.Errorf("set GITHUB_SECRET")
×
166
        case githubToken == "":
×
167
                return &config{}, fmt.Errorf("set GITHUB_TOKEN")
×
168
        case gitlabToken == "":
×
169
                return &config{}, fmt.Errorf("set GITLAB_TOKEN")
×
170
        case gitlabBaseURL == "":
×
171
                return &config{}, fmt.Errorf("set GITLAB_BASE_URL")
×
172
        case integrationDirectory == "":
×
173
                return &config{}, fmt.Errorf("set INTEGRATION_DIRECTORY")
×
174
        }
175

176
        return &config{
1✔
177
                dryRunMode:             dryRunMode,
1✔
178
                githubSecret:           []byte(githubSecret),
1✔
179
                githubProtocol:         gitProtocolSSH,
1✔
180
                githubToken:            githubToken,
1✔
181
                gitlabToken:            gitlabToken,
1✔
182
                gitlabBaseURL:          gitlabBaseURL,
1✔
183
                integrationDirectory:   integrationDirectory,
1✔
184
                isProcessPushEvents:    isProcessPushEvents,
1✔
185
                isProcessPREvents:      isProcessPREvents,
1✔
186
                isProcessCommentEvents: isProcessCommentEvents,
1✔
187
                reposSyncList:          reposSyncList,
1✔
188
        }, nil
1✔
189
}
190

191
func getCustomLoggerFromContext(ctx *gin.Context) *logrus.Entry {
13✔
192
        deliveryID, ok := ctx.Get("delivery")
13✔
193
        if !ok || !isStringType(deliveryID) {
13✔
194
                return logrus.WithField("delivery", "nil")
×
195
        }
×
196
        return logrus.WithField("delivery", deliveryID)
13✔
197
}
198

199
func isStringType(i interface{}) bool {
13✔
200
        switch i.(type) {
13✔
201
        case string:
13✔
202
                return true
13✔
203
        default:
×
204
                return false
×
205
        }
206
}
207

208
func processGitHubWebhookRequest(
209
        ctx *gin.Context,
210
        payload []byte,
211
        githubClient clientgithub.Client,
212
        conf *config,
213
) {
1✔
214
        webhookType := github.WebHookType(ctx.Request)
1✔
215
        webhookEvent, _ := github.ParseWebHook(webhookType, payload)
1✔
216
        _ = processGitHubWebhook(ctx, webhookType, webhookEvent, githubClient, conf)
1✔
217
}
1✔
218

219
func processGitHubWebhook(
220
        ctx *gin.Context,
221
        webhookType string,
222
        webhookEvent interface{},
223
        githubClient clientgithub.Client,
224
        conf *config,
225
) error {
16✔
226
        githubOrganization, err := getGitHubOrganization(webhookType, webhookEvent)
16✔
227
        if err != nil {
16✔
228
                logrus.Warnln("ignoring event: ", err.Error())
×
229
                return nil
×
230
        }
×
231
        conf.githubOrganization = githubOrganization
16✔
232
        switch webhookType {
16✔
233
        case "pull_request":
3✔
234
                if conf.isProcessPREvents {
5✔
235
                        pr := webhookEvent.(*github.PullRequestEvent)
2✔
236
                        return processGitHubPullRequest(ctx, pr, githubClient, conf)
2✔
237
                } else {
3✔
238
                        logrus.Infof("Webhook event %s processing is skipped", webhookType)
1✔
239
                }
1✔
240
        case "push":
3✔
241
                if conf.isProcessPushEvents {
5✔
242
                        push := webhookEvent.(*github.PushEvent)
2✔
243
                        return processGitHubPush(ctx, push, githubClient, conf)
2✔
244
                } else {
3✔
245
                        logrus.Infof("Webhook event %s processing is skipped", webhookType)
1✔
246
                }
1✔
247
        case "issue_comment":
12✔
248
                if conf.isProcessCommentEvents {
23✔
249
                        comment := webhookEvent.(*github.IssueCommentEvent)
11✔
250
                        return processGitHubComment(ctx, comment, githubClient, conf)
11✔
251
                } else {
12✔
252
                        logrus.Infof("Webhook event %s processing is skipped", webhookType)
1✔
253
                }
1✔
254
        }
255
        return nil
3✔
256
}
257

258
func setupLogging(conf *config, requestLogger logger.RequestLogger) {
2✔
259
        // Log to stdout and with JSON format; suitable for GKE
2✔
260
        formatter := &logrus.JSONFormatter{
2✔
261
                FieldMap: logrus.FieldMap{
2✔
262
                        logrus.FieldKeyTime:  "time",
2✔
263
                        logrus.FieldKeyLevel: "level",
2✔
264
                        logrus.FieldKeyMsg:   "message",
2✔
265
                },
2✔
266
        }
2✔
267

2✔
268
        if conf.dryRunMode {
3✔
269
                mw := io.MultiWriter(os.Stdout, requestLogger)
1✔
270
                logrus.SetOutput(mw)
1✔
271
        } else {
2✔
272
                logrus.SetOutput(os.Stdout)
1✔
273
        }
1✔
274
        logrus.SetFormatter(formatter)
2✔
275
}
276

277
func main() {
×
278
        doMain()
×
279
}
×
280

281
var githubClient clientgithub.Client
282

283
func doMain() {
1✔
284
        conf, err := getConfig()
1✔
285
        if err != nil {
1✔
286
                logrus.Fatalf("failed to load config: %s", err.Error())
×
287
        }
×
288

289
        requestLogger := logger.NewRequestLogger()
1✔
290
        logger.SetRequestLogger(requestLogger)
1✔
291

1✔
292
        setupLogging(conf, requestLogger)
1✔
293
        git.SetDryRunMode(conf.dryRunMode)
1✔
294

1✔
295
        logrus.Infoln("using settings: ", spew.Sdump(conf))
1✔
296

1✔
297
        githubClient = clientgithub.NewGitHubClient(conf.githubToken, conf.dryRunMode)
1✔
298

1✔
299
        r := gin.Default()
1✔
300
        filter := "/_health"
1✔
301
        if logrus.GetLevel() == logrus.DebugLevel || logrus.GetLevel() == logrus.TraceLevel {
2✔
302
                filter = ""
1✔
303
        }
1✔
304
        r.Use(gin.LoggerWithWriter(gin.DefaultWriter, filter))
1✔
305
        r.Use(gin.Recovery())
1✔
306

1✔
307
        // webhook for GitHub
1✔
308
        r.POST("/", func(context *gin.Context) {
2✔
309
                payload, err := github.ValidatePayload(context.Request, conf.githubSecret)
1✔
310
                if err != nil {
1✔
311
                        var mbErr *http.MaxBytesError
×
312
                        if errors.As(err, &mbErr) {
×
313
                                context.Status(http.StatusRequestEntityTooLarge)
×
314
                                return
×
315
                        }
×
316
                        logrus.Warnln("payload failed to validate, ignoring.")
×
317
                        context.Status(http.StatusForbidden)
×
318
                        return
×
319
                }
320
                context.Set("delivery", github.DeliveryID(context.Request))
1✔
321
                if conf.dryRunMode {
2✔
322
                        processGitHubWebhookRequest(context, payload, githubClient, conf)
1✔
323
                } else {
1✔
324
                        go processGitHubWebhookRequest(context, payload, githubClient, conf)
×
325
                }
×
326
                context.Status(http.StatusAccepted)
1✔
327
        })
328

329
        // 200 replay for the loadbalancer
330
        r.GET("/_health", func(_ *gin.Context) {})
1✔
331
        r.GET("/", func(_ *gin.Context) {})
1✔
332

333
        // dry-run mode, end-point to retrieve and clear logs
334
        if conf.dryRunMode {
2✔
335
                r.GET("/logs", func(context *gin.Context) {
2✔
336
                        logs := requestLogger.Get()
1✔
337
                        context.JSON(http.StatusOK, logs)
1✔
338
                })
1✔
339

340
                r.DELETE("/logs", func(context *gin.Context) {
2✔
341
                        requestLogger.Clear()
1✔
342
                        context.Writer.WriteHeader(http.StatusNoContent)
1✔
343
                })
1✔
344
        }
345

346
        srv := &http.Server{
1✔
347
                Addr:    "0.0.0.0:8080",
1✔
348
                Handler: http.MaxBytesHandler(r, 10*MiB),
1✔
349
        }
1✔
350

1✔
351
        go func() {
2✔
352
                if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
1✔
353
                        logrus.Fatalf("Failed listening: %s\n", err)
×
354
                }
×
355
        }()
356

357
        quit := make(chan os.Signal, 1)
1✔
358
        signal.Notify(quit, unix.SIGINT, unix.SIGTERM)
1✔
359
        <-quit
1✔
360

1✔
361
        logrus.Info("Shutdown server ...")
1✔
362

1✔
363
        ctx := context.Background()
1✔
364
        ctxWithTimeout, cancel := context.WithTimeout(ctx, 5*time.Second)
1✔
365
        defer cancel()
1✔
366
        if err := srv.Shutdown(ctxWithTimeout); err != nil {
1✔
367
                logrus.Fatal("Failed to shutdown the server: ", err)
×
368
        }
×
369
}
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