Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement CI pipeline status reporting when evaluating MR #17

Merged
merged 10 commits into from
May 11, 2024
22 changes: 22 additions & 0 deletions .github/workflows/lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -68,3 +68,25 @@ jobs:
- uses: github/codeql-action/autobuild@v3

- uses: github/codeql-action/analyze@v3

# ------------------------------

semgrep:
runs-on: ubuntu-latest
name: semgrep
container:
image: returntocorp/semgrep
steps:
- uses: actions/checkout@v4

- uses: actions/checkout@v4
with:
repository: dgryski/semgrep-go
path: rules

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: semgrep
run: semgrep scan --error --enable-nosem -f ./rules .
22 changes: 0 additions & 22 deletions .github/workflows/security.yml
Original file line number Diff line number Diff line change
Expand Up @@ -40,28 +40,6 @@ jobs:

# ------------------------------

semgrep:
runs-on: ubuntu-latest
name: semgrep
container:
image: returntocorp/semgrep
steps:
- uses: actions/checkout@v4

- uses: actions/checkout@v4
with:
repository: dgryski/semgrep-go
path: rules

- uses: actions/setup-go@v5
with:
go-version-file: go.mod

- name: semgrep
run: semgrep scan --error --enable-nosem -f ./rules .

# ------------------------------

gitleaks:
runs-on: ubuntu-latest
name: gitleaks
Expand Down
2 changes: 1 addition & 1 deletion .github/workflows/test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ jobs:
run: ./scm-engine -h

- name: Test scm-engine against test GitLab project
run: ./scm-engine evaluate 1
run: ./scm-engine evaluate all
env:
SCM_ENGINE_TOKEN: "${{ secrets.GITLAB_INTEGRATION_TEST_API_TOKEN }}"
SCM_ENGINE_CONFIG_FILE: ".scm-engine.example.yml"
Expand Down
19 changes: 14 additions & 5 deletions cmd/cmd_evaluate.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,10 @@ import (
)

func Evaluate(cCtx *cli.Context) error {
ctx := state.ContextWithProjectID(cCtx.Context, cCtx.String(FlagSCMProject))
ctx = state.ContextWithDryRun(ctx, cCtx.Bool(FlagDryRun))
ctx := state.WithProjectID(cCtx.Context, cCtx.String(FlagSCMProject))
ctx = state.WithCommitSHA(ctx, cCtx.String(FlagCommitSHA))
ctx = state.WithDryRun(ctx, cCtx.Bool(FlagDryRun))
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline))

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

for _, mr := range res {
if err := ProcessMR(ctx, client, cfg, mr.ID, nil); err != nil {
ctx := state.ContextWithMergeRequestID(ctx, mr.ID)
ctx = state.WithCommitSHA(ctx, mr.SHA)

if err := ProcessMR(ctx, client, cfg, nil); err != nil {
return err
}
}

// If the flag is set, use that for evaluation
case cCtx.String(FlagMergeRequestID) != "":
return ProcessMR(ctx, client, cfg, cCtx.String(FlagMergeRequestID), nil)
ctx = state.ContextWithMergeRequestID(ctx, cCtx.String(FlagMergeRequestID))

return ProcessMR(ctx, client, cfg, nil)

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

default:
for _, mr := range cCtx.Args().Slice() {
if err := ProcessMR(ctx, client, cfg, mr, nil); err != nil {
ctx = state.ContextWithMergeRequestID(ctx, mr)

if err := ProcessMR(ctx, client, cfg, nil); err != nil {
return err
}
}
Expand Down
22 changes: 13 additions & 9 deletions cmd/cmd_server.go
Original file line number Diff line number Diff line change
Expand Up @@ -112,31 +112,34 @@ func Server(cCtx *cli.Context) error { //nolint:unparam
}

// Initialize context
ctx = state.ContextWithProjectID(ctx, payload.Project.PathWithNamespace)
ctx = state.WithProjectID(ctx, payload.Project.PathWithNamespace)

// Grab event specific information
var (
id string
ref string
id string
gitSha string
)

switch payload.EventType {
case "merge_request":
id = strconv.Itoa(payload.ObjectAttributes.IID)
ref = payload.ObjectAttributes.LastCommit.ID
gitSha = payload.ObjectAttributes.LastCommit.ID

case "note":
id = strconv.Itoa(payload.MergeRequest.IID)
ref = payload.MergeRequest.LastCommit.ID
gitSha = payload.MergeRequest.LastCommit.ID

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

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

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

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

// Process the MR
if err := ProcessMR(ctx, client, cfg, id, fullEventPayload); err != nil {
if err := ProcessMR(ctx, client, cfg, fullEventPayload); err != nil {
errHandler(ctx, w, http.StatusOK, err)

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

return ctx
},
Expand Down
2 changes: 2 additions & 0 deletions cmd/conventions.go
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ const (
FlagConfigFile = "config"
FlagDryRun = "dry-run"
FlagMergeRequestID = "id"
FlagUpdatePipeline = "update-pipeline"
FlagCommitSHA = "commit"
FlagSCMBaseURL = "base-url"
FlagSCMProject = "project"
FlagServerListen = "listen"
Expand Down
16 changes: 13 additions & 3 deletions cmd/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package cmd

import (
"context"
"fmt"
"log/slog"
"net/http"

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

func ProcessMR(ctx context.Context, client scm.Client, cfg *config.Config, mr string, event any) error {
ctx = state.ContextWithMergeRequestID(ctx, mr)
func ProcessMR(ctx context.Context, client scm.Client, cfg *config.Config, event any) (err error) {
// Stop the pipeline when we leave this func
defer func() {
if stopErr := client.Stop(ctx, err); err != nil {
slogctx.Error(ctx, "Failed to update pipeline", slog.Any("error", stopErr))
}
}()

// Start the pipeline
if err = client.Start(ctx); err != nil {
return fmt.Errorf("failed to update pipeline monitor: %w", err)
}

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

remoteLabels, err := client.Labels().List(ctx)
Expand Down
10 changes: 6 additions & 4 deletions docs/commands/evaluate.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,12 +12,14 @@ NAME:
scm-engine evaluate - Evaluate a Merge Request

USAGE:
scm-engine evaluate [command options] [id, id, ...]
scm-engine evaluate [command options] [mr_id, mr_id, ...]

OPTIONS:
--project value GitLab project (example: 'gitlab-org/gitlab') [$GITLAB_PROJECT, $CI_PROJECT_PATH]
--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]
--help, -h show help
--project value GitLab project (example: 'gitlab-org/gitlab') [$GITLAB_PROJECT, $CI_PROJECT_PATH]
--id value The pull/merge ID to process, if not provided as a CLI flag [$CI_MERGE_REQUEST_IID]
--commit value The git commit sha [$CI_COMMIT_SHA]
--update-pipeline Update the CI pipeline status with progress (default: true) [$SCM_ENGINE_UPDATE_PIPELINE]
--help, -h show help

GLOBAL OPTIONS:
--config value Path to the scm-engine config file (default: ".scm-engine.yml") [$SCM_ENGINE_CONFIG_FILE]
Expand Down
3 changes: 2 additions & 1 deletion docs/commands/server.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@ USAGE:

OPTIONS:
--webhook-secret value Used to validate received payloads. Sent with the request in the X-Gitlab-Token HTTP header [$SCM_ENGINE_WEBHOOK_SECRET]
--listen value Port the HTTP server should listen on (default: "0.0.0.0:3000") [$SCM_ENGINE_LISTEN]
--listen value IP + Port that the HTTP server should listen on (default: "0.0.0.0:3000") [$SCM_ENGINE_LISTEN]
--update-pipeline Update the CI pipeline status with progress (default: true) [$SCM_ENGINE_UPDATE_PIPELINE]
--help, -h show help

GLOBAL OPTIONS:
Expand Down
26 changes: 13 additions & 13 deletions docs/gitlab/setup.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,14 +4,14 @@

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

- `+` Reacting to comments
- `+` Access to webhook event data in scripts via `webhook_event.*` (see [server docs](../commands/server.md) for more information)
- `+` A single `scm-engine` instance (and single token) for your GitLab project, group, or instance depending on where you configure the webhook.
- `+` Each Project still have their own `.scm-engine.yml` file, it's downloaded via the API when the server is processing a webhook event.
- `+` A single "bot" identity across your projects.
- `+` 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.
- `+` Real-time reactions to changes
- `-` 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)
- [X] Real-time reactions to changes.
- [X] Reacting to comments.
- [X] Access to webhook event data in scripts via `webhook_event.*` (see [server docs](../commands/server.md) for more information).
- [X] A single `scm-engine` instance (and single token) for your GitLab project, group, or instance depending on where you configure the webhook.
- [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.
- [X] A single "bot" identity across your projects.
- [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.
- [ ] 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).

**Setup**:

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

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.

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

**Setup**:

Expand Down
32 changes: 26 additions & 6 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -72,14 +72,15 @@ func main() {
&cli.BoolFlag{
Name: cmd.FlagDryRun,
Usage: "Dry run, don't actually _do_ actions, just print them",
Value: false,
},
},
Commands: []*cli.Command{
{
Name: "evaluate",
Usage: "Evaluate a Merge Request",
Args: true,
ArgsUsage: " [id, id, ...]",
ArgsUsage: " [mr_id, mr_id, ...]",
Action: cmd.Evaluate,
Flags: []cli.Flag{
&cli.StringFlag{
Expand All @@ -93,15 +94,26 @@ func main() {
},
&cli.StringFlag{
Name: cmd.FlagMergeRequestID,
Usage: "The pull/merge to process, if not provided as a CLI flag",
Aliases: []string{
"merge-request-id", // GitLab naming
"pull-request-id", // GitHub naming
},
Usage: "The pull/merge ID to process, if not provided as a CLI flag",
EnvVars: []string{
"CI_MERGE_REQUEST_IID", // GitLab CI
},
},
&cli.StringFlag{
Name: cmd.FlagCommitSHA,
Usage: "The git commit sha",
EnvVars: []string{
"CI_COMMIT_SHA", // GitLab CI
},
},
&cli.BoolFlag{
Name: cmd.FlagUpdatePipeline,
Usage: "Update the CI pipeline status with progress",
Value: true,
EnvVars: []string{
"SCM_ENGINE_UPDATE_PIPELINE",
},
},
},
},
{
Expand All @@ -124,6 +136,14 @@ func main() {
"SCM_ENGINE_LISTEN",
},
},
&cli.BoolFlag{
Name: cmd.FlagUpdatePipeline,
Usage: "Update the CI pipeline status with progress",
Value: true,
EnvVars: []string{
"SCM_ENGINE_UPDATE_PIPELINE",
},
},
},
},
},
Expand Down
39 changes: 39 additions & 0 deletions pkg/scm/gitlab/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (
"strings"

"github.com/jippi/scm-engine/pkg/scm"
"github.com/jippi/scm-engine/pkg/state"
go_gitlab "github.com/xanzy/go-gitlab"
)

Expand Down Expand Up @@ -59,6 +60,44 @@ func (client *Client) EvalContext(ctx context.Context) (scm.EvalContext, error)
return res, nil
}

// Start pipeline
func (client *Client) Start(ctx context.Context) error {
if !state.ShouldUpdatePipeline(ctx) {
return nil
}

_, _, err := client.wrapped.Commits.SetCommitStatus(state.ProjectID(ctx), state.CommitSHA(ctx), &go_gitlab.SetCommitStatusOptions{
State: go_gitlab.Running,
Name: go_gitlab.Ptr("scm-engine"),
Description: go_gitlab.Ptr("Currently evaluating MR"),
})

return err
}

// Stop pipeline
func (client *Client) Stop(ctx context.Context, err error) error {
if !state.ShouldUpdatePipeline(ctx) {
return nil
}

status := go_gitlab.Success
message := "OK"

if err != nil {
status = go_gitlab.Failed
message = err.Error()
}

_, _, err = client.wrapped.Commits.SetCommitStatus(state.ProjectID(ctx), state.CommitSHA(ctx), &go_gitlab.SetCommitStatusOptions{
State: status,
Name: go_gitlab.Ptr("scm-engine"),
Description: go_gitlab.Ptr(message),
})

return err
}

func graphqlBaseURL(inputURL *url.URL) string {
var buf strings.Builder
if inputURL.Scheme != "" {
Expand Down
Loading
Loading