Skip to content

Commit 0aba870

Browse files
committed
feat: implement CI pipeline status reporting when evaluating MR
1 parent c35eee6 commit 0aba870

12 files changed

+168
-55
lines changed

cmd/cmd_evaluate.go

+14-5
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,10 @@ import (
1111
)
1212

1313
func Evaluate(cCtx *cli.Context) error {
14-
ctx := state.ContextWithProjectID(cCtx.Context, cCtx.String(FlagSCMProject))
15-
ctx = state.ContextWithDryRun(ctx, cCtx.Bool(FlagDryRun))
14+
ctx := state.WithProjectID(cCtx.Context, cCtx.String(FlagSCMProject))
15+
ctx = state.WithCommitSHA(ctx, cCtx.String(FlagCommitSHA))
16+
ctx = state.WithDryRun(ctx, cCtx.Bool(FlagDryRun))
17+
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline))
1618

1719
cfg, err := config.LoadFile(cCtx.String(FlagConfigFile))
1820
if err != nil {
@@ -33,22 +35,29 @@ func Evaluate(cCtx *cli.Context) error {
3335
}
3436

3537
for _, mr := range res {
36-
if err := ProcessMR(ctx, client, cfg, mr.ID, nil); err != nil {
38+
ctx := state.ContextWithMergeRequestID(ctx, mr.ID)
39+
ctx = state.WithCommitSHA(ctx, mr.SHA)
40+
41+
if err := ProcessMR(ctx, client, cfg, nil); err != nil {
3742
return err
3843
}
3944
}
4045

4146
// If the flag is set, use that for evaluation
4247
case cCtx.String(FlagMergeRequestID) != "":
43-
return ProcessMR(ctx, client, cfg, cCtx.String(FlagMergeRequestID), nil)
48+
ctx = state.ContextWithMergeRequestID(ctx, cCtx.String(FlagMergeRequestID))
49+
50+
return ProcessMR(ctx, client, cfg, nil)
4451

4552
// If no flag is set, we require arguments
4653
case cCtx.Args().Len() == 0:
4754
return fmt.Errorf("Missing required argument: %s", FlagMergeRequestID)
4855

4956
default:
5057
for _, mr := range cCtx.Args().Slice() {
51-
if err := ProcessMR(ctx, client, cfg, mr, nil); err != nil {
58+
ctx = state.ContextWithMergeRequestID(ctx, mr)
59+
60+
if err := ProcessMR(ctx, client, cfg, nil); err != nil {
5261
return err
5362
}
5463
}

cmd/cmd_server.go

+13-9
Original file line numberDiff line numberDiff line change
@@ -112,31 +112,34 @@ func Server(cCtx *cli.Context) error { //nolint:unparam
112112
}
113113

114114
// Initialize context
115-
ctx = state.ContextWithProjectID(ctx, payload.Project.PathWithNamespace)
115+
ctx = state.WithProjectID(ctx, payload.Project.PathWithNamespace)
116116

117117
// Grab event specific information
118118
var (
119-
id string
120-
ref string
119+
id string
120+
gitSha string
121121
)
122122

123123
switch payload.EventType {
124124
case "merge_request":
125125
id = strconv.Itoa(payload.ObjectAttributes.IID)
126-
ref = payload.ObjectAttributes.LastCommit.ID
126+
gitSha = payload.ObjectAttributes.LastCommit.ID
127127

128128
case "note":
129129
id = strconv.Itoa(payload.MergeRequest.IID)
130-
ref = payload.MergeRequest.LastCommit.ID
130+
gitSha = payload.MergeRequest.LastCommit.ID
131131

132132
default:
133133
errHandler(ctx, w, http.StatusInternalServerError, fmt.Errorf("unknown event type: %s", payload.EventType))
134134
}
135135

136-
ctx = slogctx.With(ctx, slog.String("event_type", payload.EventType), slog.String("merge_request_id", id), slog.String("sha_reference", ref))
136+
// Build context for rest of the pipeline
137+
ctx = state.WithCommitSHA(ctx, gitSha)
138+
ctx = state.ContextWithMergeRequestID(ctx, id)
139+
ctx = slogctx.With(ctx, slog.String("event_type", payload.EventType))
137140

138141
// Get the remote config file
139-
file, err := client.MergeRequests().GetRemoteConfig(ctx, cCtx.String(FlagConfigFile), ref)
142+
file, err := client.MergeRequests().GetRemoteConfig(ctx, cCtx.String(FlagConfigFile), gitSha)
140143
if err != nil {
141144
errHandler(ctx, w, http.StatusOK, fmt.Errorf("could not read remote config file: %w", err))
142145

@@ -160,7 +163,7 @@ func Server(cCtx *cli.Context) error { //nolint:unparam
160163
}
161164

162165
// Process the MR
163-
if err := ProcessMR(ctx, client, cfg, id, fullEventPayload); err != nil {
166+
if err := ProcessMR(ctx, client, cfg, fullEventPayload); err != nil {
164167
errHandler(ctx, w, http.StatusOK, err)
165168

166169
return
@@ -176,7 +179,8 @@ func Server(cCtx *cli.Context) error { //nolint:unparam
176179
ReadTimeout: 5 * time.Second,
177180
WriteTimeout: 5 * time.Second,
178181
BaseContext: func(l net.Listener) context.Context {
179-
ctx := state.ContextWithDryRun(cCtx.Context, cCtx.Bool(FlagDryRun))
182+
ctx := state.WithDryRun(cCtx.Context, cCtx.Bool(FlagDryRun))
183+
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline))
180184

181185
return ctx
182186
},

cmd/conventions.go

+2
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,8 @@ const (
55
FlagConfigFile = "config"
66
FlagDryRun = "dry-run"
77
FlagMergeRequestID = "id"
8+
FlagUpdatePipeline = "update-pipeline"
9+
FlagCommitSHA = "commit"
810
FlagSCMBaseURL = "base-url"
911
FlagSCMProject = "project"
1012
FlagServerListen = "listen"

cmd/shared.go

+13-3
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package cmd
22

33
import (
44
"context"
5+
"fmt"
56
"log/slog"
67
"net/http"
78

@@ -11,10 +12,19 @@ import (
1112
slogctx "github.com/veqryn/slog-context"
1213
)
1314

14-
func ProcessMR(ctx context.Context, client scm.Client, cfg *config.Config, mr string, event any) error {
15-
ctx = state.ContextWithMergeRequestID(ctx, mr)
15+
func ProcessMR(ctx context.Context, client scm.Client, cfg *config.Config, event any) (err error) {
16+
// Stop the pipeline when we leave this func
17+
defer func() {
18+
if stopErr := client.Stop(ctx, err); err != nil {
19+
slogctx.Error(ctx, "Failed to update pipeline", slog.Any("error", stopErr))
20+
}
21+
}()
22+
23+
// Start the pipeline
24+
if err = client.Start(ctx); err != nil {
25+
return fmt.Errorf("failed to update pipeline monitor: %w", err)
26+
}
1627

17-
// for mr := 900; mr <= 1000; mr++ {
1828
slogctx.Info(ctx, "Processing MR")
1929

2030
remoteLabels, err := client.Labels().List(ctx)

docs/commands/evaluate.md

+6-4
Original file line numberDiff line numberDiff line change
@@ -12,12 +12,14 @@ NAME:
1212
scm-engine evaluate - Evaluate a Merge Request
1313
1414
USAGE:
15-
scm-engine evaluate [command options] [id, id, ...]
15+
scm-engine evaluate [command options] [mr_id, mr_id, ...]
1616
1717
OPTIONS:
18-
--project value GitLab project (example: 'gitlab-org/gitlab') [$GITLAB_PROJECT, $CI_PROJECT_PATH]
19-
--id value, --merge-request-id value, --pull-request-id value The pull/merge to process, if not provided as a CLI flag [$CI_MERGE_REQUEST_IID]
20-
--help, -h show help
18+
--project value GitLab project (example: 'gitlab-org/gitlab') [$GITLAB_PROJECT, $CI_PROJECT_PATH]
19+
--id value The pull/merge ID to process, if not provided as a CLI flag [$CI_MERGE_REQUEST_IID]
20+
--commit value The git commit sha [$CI_COMMIT_SHA]
21+
--update-pipeline Update the CI pipeline status with progress (default: true) [$SCM_ENGINE_SKIP_PIPELINE]
22+
--help, -h show help
2123
2224
GLOBAL OPTIONS:
2325
--config value Path to the scm-engine config file (default: ".scm-engine.yml") [$SCM_ENGINE_CONFIG_FILE]

docs/commands/server.md

+2-1
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,8 @@ USAGE:
2525
2626
OPTIONS:
2727
--webhook-secret value Used to validate received payloads. Sent with the request in the X-Gitlab-Token HTTP header [$SCM_ENGINE_WEBHOOK_SECRET]
28-
--listen value Port the HTTP server should listen on (default: "0.0.0.0:3000") [$SCM_ENGINE_LISTEN]
28+
--listen value IP + Port that the HTTP server should listen on (default: "0.0.0.0:3000") [$SCM_ENGINE_LISTEN]
29+
--update-pipeline Update the CI pipeline status with progress (default: true) [$SCM_ENGINE_SKIP_PIPELINE]
2930
--help, -h show help
3031
3132
GLOBAL OPTIONS:

docs/gitlab/setup.md

+13-13
Original file line numberDiff line numberDiff line change
@@ -4,14 +4,14 @@
44

55
Using `scm-engine` as a webhook server allows for richer feature set compared to [GitLab CI pipeline](#gitlab-ci-pipeline) mode
66

7-
- `+` Reacting to comments
8-
- `+` Access to webhook event data in scripts via `webhook_event.*` (see [server docs](../commands/server.md) for more information)
9-
- `+` A single `scm-engine` instance (and single token) for your GitLab project, group, or instance depending on where you configure the webhook.
10-
- `+` Each Project still have their own `.scm-engine.yml` file, it's downloaded via the API when the server is processing a webhook event.
11-
- `+` A single "bot" identity across your projects.
12-
- `+` Turn key once configured; if a project want to use `scm-engine` they just need to create the `.scm-engine.yml` file in their project.
13-
- `+` Real-time reactions to changes
14-
- `-` No intuitive access to [`evaluation` logs](../commands/evaluate.md) within GitLab (you can see them in the server logs or in the webhook failure log)
7+
- [X] Real-time reactions to changes.
8+
- [X] Reacting to comments.
9+
- [X] Access to webhook event data in scripts via `webhook_event.*` (see [server docs](../commands/server.md) for more information).
10+
- [X] A single `scm-engine` instance (and single token) for your GitLab project, group, or instance depending on where you configure the webhook.
11+
- [X] Each Project still have their own `.scm-engine.yml` file, it's downloaded via the API when the server is processing a webhook event.
12+
- [X] A single "bot" identity across your projects.
13+
- [X] Turn key once configured; if a project want to use `scm-engine` they just need to create the `.scm-engine.yml` file in their project.
14+
- [ ] No intuitive access to [`evaluation` logs](../commands/evaluate.md) within GitLab (you can see them in the server logs or in the webhook failure log).
1515

1616
**Setup**:
1717

@@ -22,11 +22,11 @@ Using `scm-engine` as a webhook server allows for richer feature set compared to
2222

2323
Using `scm-engine` within a GitLab CI pipeline is straight forward - every time a CI pipeline runs, `scm-engine` will [evaluate](../commands/evaluate.md) the Merge Request.
2424

25-
- `+` Simple & quick installation.
26-
- `+` Limited access token permissions.
27-
- `+` Easy access to [`evaluation` logs](../commands/evaluate.md) within the GitLab CI job.
28-
- `-` Can't react to comments; only works within a CI pipeline.
29-
- `-` Higher latency for reacting to changes depending on how fast CI jobs run (and where in the pipeline it runs).
25+
- [X] Simple & quick installation.
26+
- [X] Limited access token permissions.
27+
- [X] Easy access to [`evaluation` logs](../commands/evaluate.md) within the GitLab CI job.
28+
- [ ] Can't react to comments; only works within a CI pipeline.
29+
- [ ] Higher latency for reacting to changes depending on how fast CI jobs run (and where in the pipeline it runs).
3030

3131
**Setup**:
3232

main.go

+28-7
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,7 @@ func main() {
7979
Name: "evaluate",
8080
Usage: "Evaluate a Merge Request",
8181
Args: true,
82-
ArgsUsage: " [id, id, ...]",
82+
ArgsUsage: " [mr_id, mr_id, ...]",
8383
Action: cmd.Evaluate,
8484
Flags: []cli.Flag{
8585
&cli.StringFlag{
@@ -92,16 +92,29 @@ func main() {
9292
},
9393
},
9494
&cli.StringFlag{
95-
Name: cmd.FlagMergeRequestID,
96-
Usage: "The pull/merge to process, if not provided as a CLI flag",
97-
Aliases: []string{
98-
"merge-request-id", // GitLab naming
99-
"pull-request-id", // GitHub naming
100-
},
95+
Name: cmd.FlagMergeRequestID,
96+
Usage: "The pull/merge ID to process, if not provided as a CLI flag",
97+
Required: true,
10198
EnvVars: []string{
10299
"CI_MERGE_REQUEST_IID", // GitLab CI
103100
},
104101
},
102+
&cli.StringFlag{
103+
Name: cmd.FlagCommitSHA,
104+
Usage: "The git commit sha",
105+
Required: true,
106+
EnvVars: []string{
107+
"CI_COMMIT_SHA", // GitLab CI
108+
},
109+
},
110+
&cli.BoolFlag{
111+
Name: cmd.FlagUpdatePipeline,
112+
Usage: "Update the CI pipeline status with progress",
113+
Value: true,
114+
EnvVars: []string{
115+
"SCM_ENGINE_SKIP_PIPELINE",
116+
},
117+
},
105118
},
106119
},
107120
{
@@ -124,6 +137,14 @@ func main() {
124137
"SCM_ENGINE_LISTEN",
125138
},
126139
},
140+
&cli.BoolFlag{
141+
Name: cmd.FlagUpdatePipeline,
142+
Usage: "Update the CI pipeline status with progress",
143+
Value: true,
144+
EnvVars: []string{
145+
"SCM_ENGINE_SKIP_PIPELINE",
146+
},
147+
},
127148
},
128149
},
129150
},

pkg/scm/gitlab/client.go

+45
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77

88
"github.com/jippi/scm-engine/pkg/scm"
9+
"github.com/jippi/scm-engine/pkg/state"
910
go_gitlab "github.com/xanzy/go-gitlab"
1011
)
1112

@@ -59,6 +60,50 @@ func (client *Client) EvalContext(ctx context.Context) (scm.EvalContext, error)
5960
return res, nil
6061
}
6162

63+
// Start pipeline
64+
func (client *Client) Start(ctx context.Context) error {
65+
if !state.ShouldUpdatePipeline(ctx) {
66+
return nil
67+
}
68+
69+
_, _, err := client.wrapped.Commits.SetCommitStatus(state.ProjectID(ctx), state.CommitSHA(ctx), &go_gitlab.SetCommitStatusOptions{
70+
State: go_gitlab.Running,
71+
Name: go_gitlab.Ptr("scm-engine"),
72+
Description: go_gitlab.Ptr("Currently evaluating MR"),
73+
})
74+
if err != nil {
75+
return err
76+
}
77+
78+
return nil
79+
}
80+
81+
// Stop pipeline
82+
func (client *Client) Stop(ctx context.Context, err error) error {
83+
if !state.ShouldUpdatePipeline(ctx) {
84+
return nil
85+
}
86+
87+
status := go_gitlab.Success
88+
message := "OK"
89+
90+
if err != nil {
91+
status = go_gitlab.Failed
92+
message = err.Error()
93+
}
94+
95+
_, _, err = client.wrapped.Commits.SetCommitStatus(state.ProjectID(ctx), state.CommitSHA(ctx), &go_gitlab.SetCommitStatusOptions{
96+
State: status,
97+
Name: go_gitlab.Ptr("scm-engine"),
98+
Description: go_gitlab.Ptr(message),
99+
})
100+
if err != nil {
101+
return err
102+
}
103+
104+
return nil
105+
}
106+
62107
func graphqlBaseURL(inputURL *url.URL) string {
63108
var buf strings.Builder
64109
if inputURL.Scheme != "" {

pkg/scm/interfaces.go

+2
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ type Client interface {
1010
MergeRequests() MergeRequestClient
1111
EvalContext(ctx context.Context) (EvalContext, error)
1212
ApplyStep(ctx context.Context, update *UpdateMergeRequestOptions, step EvaluationActionStep) error
13+
Start(ctx context.Context) error
14+
Stop(ctx context.Context, err error) error
1315
}
1416

1517
type LabelClient interface {

pkg/scm/types.go

+2-1
Original file line numberDiff line numberDiff line change
@@ -101,7 +101,8 @@ type ListMergeRequestsOptions struct {
101101
}
102102

103103
type ListMergeRequest struct {
104-
ID string `expr:"id" graphql:"id"`
104+
ID string `expr:"id" graphql:"id"`
105+
SHA string `expr:"sha" graphql:"sha"`
105106
}
106107

107108
// Response is a GitLab API response. This wraps the standard http.Response

0 commit comments

Comments
 (0)