Skip to content

Commit 00b3977

Browse files
committed
feat: initial github support
1 parent d71528c commit 00b3977

23 files changed

+958
-28
lines changed

.gitignore

+1
Original file line numberDiff line numberDiff line change
@@ -17,3 +17,4 @@
1717
/scm-engine
1818
/scm-engine.exe
1919
/docs/gitlab/script-attributes.md
20+
/docs/github/script-attributes.md

cmd/cmd_evaluate.go

+4-2
Original file line numberDiff line numberDiff line change
@@ -5,23 +5,25 @@ import (
55

66
"github.com/jippi/scm-engine/pkg/config"
77
"github.com/jippi/scm-engine/pkg/scm"
8-
"github.com/jippi/scm-engine/pkg/scm/gitlab"
98
"github.com/jippi/scm-engine/pkg/state"
109
"github.com/urfave/cli/v2"
1110
)
1211

1312
func Evaluate(cCtx *cli.Context) error {
1413
ctx := state.WithProjectID(cCtx.Context, cCtx.String(FlagSCMProject))
14+
ctx = state.WithBaseURL(ctx, cCtx.String(FlagSCMBaseURL))
1515
ctx = state.WithCommitSHA(ctx, cCtx.String(FlagCommitSHA))
1616
ctx = state.WithDryRun(ctx, cCtx.Bool(FlagDryRun))
17+
ctx = state.WithProjectID(ctx, cCtx.String(FlagProvider))
18+
ctx = state.WithToken(ctx, cCtx.String(FlagAPIToken))
1719
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline))
1820

1921
cfg, err := config.LoadFile(cCtx.String(FlagConfigFile))
2022
if err != nil {
2123
return err
2224
}
2325

24-
client, err := gitlab.NewClient(cCtx.String(FlagAPIToken), cCtx.String(FlagSCMBaseURL))
26+
client, err := getClient(ctx)
2527
if err != nil {
2628
return err
2729
}

cmd/cmd_server.go

+10-8
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import (
1414
"time"
1515

1616
"github.com/jippi/scm-engine/pkg/config"
17-
"github.com/jippi/scm-engine/pkg/scm/gitlab"
1817
"github.com/jippi/scm-engine/pkg/state"
1918
"github.com/urfave/cli/v2"
2019
slogctx "github.com/veqryn/slog-context"
@@ -55,15 +54,21 @@ func errHandler(ctx context.Context, w http.ResponseWriter, code int, err error)
5554
return
5655
}
5756

58-
func Server(cCtx *cli.Context) error { //nolint:unparam
59-
slogctx.Info(cCtx.Context, "Starting HTTP server", slog.String("listen", cCtx.String(FlagServerListen)))
57+
func Server(cCtx *cli.Context) error {
58+
// Initialize context
59+
ctx := state.WithDryRun(cCtx.Context, cCtx.Bool(FlagDryRun))
60+
ctx = state.WithBaseURL(ctx, cCtx.String(FlagSCMBaseURL))
61+
ctx = state.WithToken(ctx, cCtx.String(FlagAPIToken))
62+
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline))
63+
64+
slogctx.Info(ctx, "Starting HTTP server", slog.String("listen", cCtx.String(FlagServerListen)))
6065

6166
mux := http.NewServeMux()
6267

6368
ourSecret := cCtx.String(FlagWebhookSecret)
6469

65-
// Initialize GitLab client
66-
client, err := gitlab.NewClient(cCtx.String(FlagAPIToken), cCtx.String(FlagSCMBaseURL))
70+
// Initialize client
71+
client, err := getClient(cCtx.Context)
6772
if err != nil {
6873
return err
6974
}
@@ -179,9 +184,6 @@ func Server(cCtx *cli.Context) error { //nolint:unparam
179184
ReadTimeout: 5 * time.Second,
180185
WriteTimeout: 5 * time.Second,
181186
BaseContext: func(l net.Listener) context.Context {
182-
ctx := state.WithDryRun(cCtx.Context, cCtx.Bool(FlagDryRun))
183-
ctx = state.WithUpdatePipeline(ctx, cCtx.Bool(FlagUpdatePipeline))
184-
185187
return ctx
186188
},
187189
}

cmd/conventions.go

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

33
const (
44
FlagAPIToken = "api-token"
5+
FlagCommitSHA = "commit"
56
FlagConfigFile = "config"
67
FlagDryRun = "dry-run"
78
FlagMergeRequestID = "id"
8-
FlagUpdatePipeline = "update-pipeline"
9-
FlagCommitSHA = "commit"
9+
FlagProvider = "provider"
1010
FlagSCMBaseURL = "base-url"
1111
FlagSCMProject = "project"
1212
FlagServerListen = "listen"
13+
FlagUpdatePipeline = "update-pipeline"
1314
FlagWebhookSecret = "webhook-secret"
1415
)

cmd/shared.go

+15
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,25 @@ import (
88

99
"github.com/jippi/scm-engine/pkg/config"
1010
"github.com/jippi/scm-engine/pkg/scm"
11+
"github.com/jippi/scm-engine/pkg/scm/github"
12+
"github.com/jippi/scm-engine/pkg/scm/gitlab"
1113
"github.com/jippi/scm-engine/pkg/state"
1214
slogctx "github.com/veqryn/slog-context"
1315
)
1416

17+
func getClient(ctx context.Context) (scm.Client, error) {
18+
switch state.Provider(ctx) {
19+
case "github":
20+
return github.NewClient(ctx), nil
21+
22+
case "gitlab":
23+
return gitlab.NewClient(ctx)
24+
25+
default:
26+
return nil, fmt.Errorf("unknown provider %q - we only support 'github' and 'gitlab'", state.Provider(ctx))
27+
}
28+
}
29+
1530
func ProcessMR(ctx context.Context, client scm.Client, cfg *config.Config, event any) (err error) {
1631
// Stop the pipeline when we leave this func
1732
defer func() {

go.mod

+1
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ require (
99
github.com/expr-lang/expr v1.16.7
1010
github.com/fatih/structtag v1.2.0
1111
github.com/golang-cz/devslog v0.0.8
12+
github.com/google/go-github/v62 v62.0.0
1213
github.com/guregu/null/v5 v5.0.0
1314
github.com/hasura/go-graphql-client v0.12.1
1415
github.com/iancoleman/strcase v0.3.0

go.sum

+4-2
Original file line numberDiff line numberDiff line change
@@ -26,8 +26,10 @@ github.com/go-logr/logr v1.4.1/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ4
2626
github.com/golang-cz/devslog v0.0.8 h1:53ipA2rC5JzWBWr9qB8EfenvXppenNiF/8DwgtNT5Q4=
2727
github.com/golang-cz/devslog v0.0.8/go.mod h1:bSe5bm0A7Nyfqtijf1OMNgVJHlWEuVSXnkuASiE1vV8=
2828
github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
29-
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
30-
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
29+
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
30+
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
31+
github.com/google/go-github/v62 v62.0.0 h1:/6mGCaRywZz9MuHyw9gD1CwsbmBX8GWsbFkwMmHdhl4=
32+
github.com/google/go-github/v62 v62.0.0/go.mod h1:EMxeUqGJq2xRu9DYBMwel/mr7kZrzUOfQmmpYrZn2a4=
3133
github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8=
3234
github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU=
3335
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=

main.go

+8
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,14 @@ func main() {
5252
"SCM_ENGINE_CONFIG_FILE",
5353
},
5454
},
55+
&cli.StringFlag{
56+
Name: cmd.FlagProvider,
57+
Usage: "Provider to use - can be 'github' or 'gitlab'",
58+
Value: "gitlab",
59+
EnvVars: []string{
60+
"SCM_ENGINE_PROVIDER",
61+
},
62+
},
5563
&cli.StringFlag{
5664
Name: cmd.FlagAPIToken,
5765
Usage: "GitHub/GitLab API token",

pkg/scm/github/client.go

+65
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package github
2+
3+
import (
4+
"context"
5+
6+
go_github "github.com/google/go-github/v62/github"
7+
"github.com/jippi/scm-engine/pkg/scm"
8+
"github.com/jippi/scm-engine/pkg/state"
9+
)
10+
11+
// Ensure the GitLab client implements the [scm.Client]
12+
var _ scm.Client = (*Client)(nil)
13+
14+
// Client is a wrapper around the GitLab specific implementation of [scm.Client] interface
15+
type Client struct {
16+
wrapped *go_github.Client
17+
18+
labels *LabelClient
19+
mergeRequests *MergeRequestClient
20+
}
21+
22+
// NewClient creates a new GitLab client
23+
func NewClient(ctx context.Context) *Client {
24+
client := go_github.NewClient(nil).WithAuthToken(state.Token(ctx))
25+
26+
return &Client{wrapped: client}
27+
}
28+
29+
// Labels returns a client target at managing labels/tags
30+
func (client *Client) Labels() scm.LabelClient {
31+
if client.labels == nil {
32+
client.labels = NewLabelClient(client)
33+
}
34+
35+
return client.labels
36+
}
37+
38+
// MergeRequests returns a client target at managing merge/pull requests
39+
func (client *Client) MergeRequests() scm.MergeRequestClient {
40+
if client.mergeRequests == nil {
41+
client.mergeRequests = NewMergeRequestClient(client)
42+
}
43+
44+
return client.mergeRequests
45+
}
46+
47+
// EvalContext creates a new evaluation context for GitLab specific usage
48+
func (client *Client) EvalContext(ctx context.Context) (scm.EvalContext, error) {
49+
res, err := NewContext(ctx, "https://api.github.com/", state.Token(ctx))
50+
if err != nil {
51+
return nil, err
52+
}
53+
54+
return res, nil
55+
}
56+
57+
// Start pipeline
58+
func (client *Client) Start(ctx context.Context) error {
59+
return nil
60+
}
61+
62+
// Stop pipeline
63+
func (client *Client) Stop(ctx context.Context, err error) error {
64+
return nil
65+
}

pkg/scm/github/client_actioner.go

+137
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
package github
2+
3+
import (
4+
"context"
5+
"errors"
6+
"fmt"
7+
"log/slog"
8+
9+
go_github "github.com/google/go-github/v62/github"
10+
"github.com/jippi/scm-engine/pkg/scm"
11+
"github.com/jippi/scm-engine/pkg/state"
12+
slogctx "github.com/veqryn/slog-context"
13+
)
14+
15+
func (c *Client) ApplyStep(ctx context.Context, update *scm.UpdateMergeRequestOptions, step scm.EvaluationActionStep) error {
16+
owner, repo := ownerAndRepo(ctx)
17+
18+
action, ok := step["action"]
19+
if !ok {
20+
return errors.New("step is missing an 'action' key")
21+
}
22+
23+
actionString, ok := action.(string)
24+
if !ok {
25+
return fmt.Errorf("step field 'action' must be of type string, got %T", action)
26+
}
27+
28+
switch actionString {
29+
case "add_label":
30+
name, ok := step["name"]
31+
if !ok {
32+
return errors.New("step field 'name' is required, but missing")
33+
}
34+
35+
nameVal, ok := name.(string)
36+
if !ok {
37+
return errors.New("step field 'name' must be a string")
38+
}
39+
40+
labels := update.AddLabels
41+
if labels == nil {
42+
labels = &scm.LabelOptions{}
43+
}
44+
45+
tmp := append(*labels, nameVal)
46+
47+
update.AddLabels = &tmp
48+
49+
case "remove_label":
50+
name, ok := step["name"]
51+
if !ok {
52+
return errors.New("step field 'name' is required, but missing")
53+
}
54+
55+
nameVal, ok := name.(string)
56+
if !ok {
57+
return errors.New("step field 'name' must be a string")
58+
}
59+
60+
labels := update.RemoveLabels
61+
if labels == nil {
62+
labels = &scm.LabelOptions{}
63+
}
64+
65+
tmp := append(*labels, nameVal)
66+
67+
update.AddLabels = &tmp
68+
69+
case "close":
70+
update.StateEvent = scm.Ptr("close")
71+
72+
case "reopen":
73+
update.StateEvent = scm.Ptr("reopen")
74+
75+
case "lock_discussion":
76+
update.DiscussionLocked = scm.Ptr(true)
77+
78+
case "unlock_discussion":
79+
update.DiscussionLocked = scm.Ptr(false)
80+
81+
case "approve":
82+
if state.IsDryRun(ctx) {
83+
slogctx.Info(ctx, "Approving MR")
84+
85+
return nil
86+
}
87+
88+
_, _, err := c.wrapped.PullRequests.CreateReview(ctx, owner, repo, state.MergeRequestIDInt(ctx), &go_github.PullRequestReviewRequest{
89+
Event: scm.Ptr("APPROVE"),
90+
})
91+
92+
return err
93+
94+
case "unapprove":
95+
if state.IsDryRun(ctx) {
96+
slogctx.Info(ctx, "Unapproving MR")
97+
98+
return nil
99+
}
100+
101+
_, _, err := c.wrapped.PullRequests.CreateReview(ctx, owner, repo, state.MergeRequestIDInt(ctx), &go_github.PullRequestReviewRequest{})
102+
103+
return err
104+
105+
case "comment":
106+
msg, ok := step["message"]
107+
if !ok {
108+
return errors.New("step field 'message' is required, but missing")
109+
}
110+
111+
msgString, ok := msg.(string)
112+
if !ok {
113+
return fmt.Errorf("step field 'message' must be a string, got %T", msg)
114+
}
115+
116+
if len(msgString) == 0 {
117+
return errors.New("step field 'message' must not be an empty string")
118+
}
119+
120+
if state.IsDryRun(ctx) {
121+
slogctx.Info(ctx, "Commenting on MR", slog.String("message", msgString))
122+
123+
return nil
124+
}
125+
126+
_, _, err := c.wrapped.PullRequests.CreateComment(ctx, owner, repo, state.MergeRequestIDInt(ctx), &go_github.PullRequestComment{
127+
Body: scm.Ptr(msgString),
128+
})
129+
130+
return err
131+
132+
default:
133+
return fmt.Errorf("GitLab client does not know how to apply action %q", step["action"])
134+
}
135+
136+
return nil
137+
}

0 commit comments

Comments
 (0)