Skip to content

Commit

Permalink
Better linting and error handling (#81)
Browse files Browse the repository at this point in the history
* fix: allow CI pipeline to fail if the Change Request changes scm-engine configuration file

otherwise, continue to fail open

* fix: MergeRequestIDUint() possible overflow

* feat: more schema validation and linting

* pre-rework

* feat: first draf of 'gitlab lint' command working

* feat: more linting and code gen for JSON schema

* feat: lint the configuration file
  • Loading branch information
jippi authored Sep 11, 2024
1 parent dd581f2 commit e99d7c3
Show file tree
Hide file tree
Showing 20 changed files with 532 additions and 204 deletions.
2 changes: 1 addition & 1 deletion .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -21,4 +21,4 @@
/schema/ignore
/scm-engine
/scm-engine.exe
/scm-engine.schema.json
scm-engine.schema.json
2 changes: 1 addition & 1 deletion Taskfile.yml
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,7 @@ tasks:
- go run . gitlab -h > docs/gitlab/_partials/cmd-gitlab.md
- go run . gitlab evaluate -h > docs/gitlab/_partials/cmd-gitlab-evaluate.md
- go run . gitlab server -h > docs/gitlab/_partials/cmd-gitlab-server.md
- go run . jsonschema > docs/scm-engine.schema.json
- cp pkg/generated/resources/scm-engine.schema.json docs/scm-engine.schema.json

docs:server:
desc: Run Docs dev server with live preview
Expand Down
13 changes: 13 additions & 0 deletions cmd/gitlab.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,19 @@ var GitLab = &cli.Command{
},
},
Subcommands: []*cli.Command{
{
Name: "lint",
Usage: "lint a configuration file",
Args: false,
Action: Lint,
Flags: []cli.Flag{
&cli.StringFlag{
Name: "schema",
Usage: "Where to find the JSON Schema file. Can load the file from either the embedded version (default), http://, https://, or a file:// URI",
Value: "embed://",
},
},
},
{
Name: "evaluate",
Usage: "Evaluate a Merge Request",
Expand Down
106 changes: 106 additions & 0 deletions cmd/gitlab_lint.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
package cmd

import (
"fmt"
"net/http"
"os"
"strings"
"time"

"github.com/jippi/scm-engine/pkg/config"
"github.com/jippi/scm-engine/pkg/generated/resources"
"github.com/jippi/scm-engine/pkg/scm/gitlab"
"github.com/jippi/scm-engine/pkg/state"
"github.com/santhosh-tekuri/jsonschema/v6"
"github.com/urfave/cli/v2"
slogctx "github.com/veqryn/slog-context"
"gopkg.in/yaml.v3"
)

func Lint(cCtx *cli.Context) error {
ctx := cCtx.Context
ctx = state.WithConfigFilePath(ctx, cCtx.String(FlagConfigFile))

// Read raw YAML file
raw, err := os.ReadFile(state.ConfigFilePath(ctx))
if err != nil {
return err
}

// Parse the YAML file into lose Go shape
var yamlOutput any
if err := yaml.Unmarshal(raw, &yamlOutput); err != nil {
return err
}

// Setup file loaders for reading the JSON schema file
loader := jsonschema.SchemeURLLoader{
"file": jsonschema.FileLoader{},
"http": newHTTPURLLoader(),
"https": newHTTPURLLoader(),
"embed": &EmbedLoader{},
}

// Create json schema compiler
compiler := jsonschema.NewCompiler()
compiler.UseLoader(loader)

// Compile the schema into validator format
sch, err := compiler.Compile(cCtx.String("schema"))
if err != nil {
return err
}

// Validate the json output
if err := sch.Validate(yamlOutput); err != nil {
return err
}

// Load the configuration file via our Go struct
cfg, err := config.LoadFile(state.ConfigFilePath(ctx))
if err != nil {
return err
}

if len(cfg.Includes) != 0 {
slogctx.Warn(ctx, "Configuration file contains 'include' settings, those are currently unsupported by 'lint' command and will be ignored")
}

// To scm-engine specific linting last
return cfg.Lint(ctx, &gitlab.Context{})
}

type EmbedLoader struct{}

func (l *EmbedLoader) Load(url string) (any, error) {
return jsonschema.UnmarshalJSON(strings.NewReader(resources.JSONSchema))
}

type HTTPURLLoader http.Client

func (l *HTTPURLLoader) Load(url string) (any, error) {
client := (*http.Client)(l)

resp, err := client.Get(url) //nolint
if err != nil {
return nil, err
}

if resp.StatusCode != http.StatusOK {
resp.Body.Close()

return nil, fmt.Errorf("%s returned status code %d", url, resp.StatusCode)
}

defer resp.Body.Close()

return jsonschema.UnmarshalJSON(resp.Body)
}

func newHTTPURLLoader() *HTTPURLLoader {
httpLoader := HTTPURLLoader(http.Client{
Timeout: 15 * time.Second,
})

return &httpLoader
}
5 changes: 5 additions & 0 deletions cmd/shared.go
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,11 @@ func ProcessMR(ctx context.Context, client scm.Client, cfg *config.Config, event
ctx = state.WithDryRun(ctx, *cfg.DryRun)
}

// Lint the configuration file to catch any misconfigurations
if err := cfg.Lint(ctx, evalContext); err != nil {
return fmt.Errorf("Configuration failed validation: %w", err)
}

// Write the config to context so we can pull it out later
ctx = config.WithConfig(ctx, cfg)

Expand Down
3 changes: 3 additions & 0 deletions generate.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
package main

//go:generate go run ./pkg/generated/main.go
8 changes: 7 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -11,19 +11,22 @@ require (
github.com/golang-cz/devslog v0.0.9
github.com/google/go-github/v64 v64.0.0
github.com/guregu/null/v5 v5.0.0
github.com/hashicorp/go-multierror v1.1.1
github.com/hasura/go-graphql-client v0.13.0
github.com/iancoleman/strcase v0.3.0
github.com/invopop/jsonschema v0.12.0
github.com/lmittmann/tint v1.0.5
github.com/muesli/termenv v0.15.2
github.com/samber/slog-multi v1.2.1
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1
github.com/stretchr/testify v1.9.0
github.com/teacat/noire v1.1.0
github.com/teris-io/shortid v0.0.0-20220617161101-71ec9f2aa569
github.com/urfave/cli/v2 v2.27.4
github.com/vektah/gqlparser/v2 v2.5.16
github.com/veqryn/slog-context v0.7.0
github.com/veqryn/slog-dedup v0.5.0
github.com/wk8/go-ordered-map/v2 v2.1.8
github.com/xanzy/go-gitlab v0.109.0
github.com/xhit/go-str2duration/v2 v2.1.0
golang.org/x/oauth2 v0.23.0
Expand All @@ -37,11 +40,14 @@ require (
github.com/buger/jsonparser v1.1.1 // indirect
github.com/charmbracelet/x/ansi v0.1.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/fatih/color v1.17.0 // indirect
github.com/go-logr/logr v1.4.1 // indirect
github.com/google/go-querystring v1.1.0 // indirect
github.com/google/uuid v1.6.0 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-retryablehttp v0.7.7 // indirect
github.com/kr/pretty v0.1.0 // indirect
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
github.com/mailru/easyjson v0.7.7 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect
Expand All @@ -51,7 +57,6 @@ require (
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/samber/lo v1.38.1 // indirect
github.com/sosodev/duration v1.3.1 // indirect
github.com/wk8/go-ordered-map/v2 v2.1.8 // indirect
github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 // indirect
golang.org/x/exp v0.0.0-20220303212507-bbda1eaf7a17 // indirect
golang.org/x/mod v0.20.0 // indirect
Expand All @@ -60,6 +65,7 @@ require (
golang.org/x/text v0.17.0 // indirect
golang.org/x/time v0.3.0 // indirect
golang.org/x/tools v0.24.0 // indirect
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 // indirect
modernc.org/b/v2 v2.1.0 // indirect
nhooyr.io/websocket v1.8.11 // indirect
)
20 changes: 17 additions & 3 deletions go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,12 @@ github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48 h1:fRzb/w+pyskVMQ+UbP35JkH8yB7MYb4q/qhBarqZE6g=
github.com/dgryski/trifles v0.0.0-20200323201526-dd97f9abfb48/go.mod h1:if7Fbed8SFyPtHLHbg49SI7NAdJiC5WIA09pe59rfAA=
github.com/dlclark/regexp2 v1.11.0 h1:G/nrcoOa7ZXlpoa/91N3X7mM3r8eIlMBBJZvsz/mxKI=
github.com/dlclark/regexp2 v1.11.0/go.mod h1:DHkYz0B9wPfa6wondMfaivmHpzrQ3v9q8cnmRbL6yW8=
github.com/expr-lang/expr v1.16.9 h1:WUAzmR0JNI9JCiF0/ewwHB1gmcGw5wW7nWt8gc6PpCI=
github.com/expr-lang/expr v1.16.9/go.mod h1:8/vRC7+7HBzESEqt5kKpYXxrxkr31SaO8r40VO/1IT4=
github.com/fatih/color v1.16.0 h1:zmkK9Ngbjj+K0yRhTVONQh1p/HknKYSlNT+vZCzyokM=
github.com/fatih/color v1.16.0/go.mod h1:fL2Sau1YI5c0pdGEVCbKQbLXB6edEj1ZgiY4NijnWvE=
github.com/fatih/color v1.17.0 h1:GlRw1BRJxkpqUCBKzKOw098ed57fEsKeNjpTe3cSjK4=
github.com/fatih/color v1.17.0/go.mod h1:YZ7TlrGPkiz6ku9fK3TLD/pl3CpsiFyu8N92HLgmosI=
github.com/fatih/structtag v1.2.0 h1:/OdNE99OxoI/PqaW/SuSK9uxxT3f/tcSZgon/ssNSx4=
github.com/fatih/structtag v1.2.0/go.mod h1:mBJUNpUnHmRKrKlQQlmCrh5PuhftFbNv8Ys4/aAZl94=
github.com/go-logr/logr v1.4.1 h1:pKouT5E8xu9zeFC39JXRDukb6JFQPXM5p5I91188VAQ=
Expand All @@ -46,10 +48,14 @@ github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWm
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/guregu/null/v5 v5.0.0 h1:PRxjqyOekS11W+w/7Vfz6jgJE/BCwELWtgvOJzddimw=
github.com/guregu/null/v5 v5.0.0/go.mod h1:SjupzNy+sCPtwQTKWhUCqjhVCO69hpsl2QsZrWHjlwU=
github.com/hashicorp/errwrap v1.0.0 h1:hLrqtEDnRye3+sgx6z4qVLNuviH3MR5aQ0ykNJa/UYA=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.7 h1:C8hUCYzor8PIfXHa4UrZkU4VvK8o9ISHxT2Q8+VepXU=
github.com/hashicorp/go-retryablehttp v0.7.7/go.mod h1:pkQpWZeYWskR+D1tR2O5OcBFOxfA7DoAO6xtkuQnHTk=
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
Expand All @@ -61,6 +67,11 @@ github.com/iancoleman/strcase v0.3.0/go.mod h1:iwCmte+B7n89clKwxIoIXy/HfoL7AsD47
github.com/invopop/jsonschema v0.12.0 h1:6ovsNSuvn9wEQVOyc72aycBMVQFKz7cPdMJn10CvzRI=
github.com/invopop/jsonschema v0.12.0/go.mod h1:ffZ5Km5SWWRAIN6wbDXItl95euhFz2uON45H2qjYt+0=
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
github.com/kr/pretty v0.1.0 h1:L/CwN0zerZDmRFUapSPitk6f+Q3+0za1rQkzVuMiMFI=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE=
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
github.com/lmittmann/tint v1.0.5 h1:NQclAutOfYsqs2F1Lenue6OoWCajs5wJcP3DfWVpePw=
github.com/lmittmann/tint v1.0.5/go.mod h1:HIS3gSy7qNwGCj+5oRjAutErFBl4BzdQP6cJZ0NfMwE=
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
Expand Down Expand Up @@ -90,6 +101,8 @@ github.com/samber/lo v1.38.1 h1:j2XEAqXKb09Am4ebOg31SpvzUTTs6EN3VfgeLUhPdXM=
github.com/samber/lo v1.38.1/go.mod h1:+m/ZKRl6ClXCE2Lgf3MsQlWfh4bn1bz6CXEOxnEXnEA=
github.com/samber/slog-multi v1.2.1 h1:MRVc6JxvGiZ+ubyANneZkMREAFAykoW0CACJZagT7so=
github.com/samber/slog-multi v1.2.1/go.mod h1:uLAvHpGqbYgX4FSL0p1ZwoLuveIAJvBECtE07XmYvFo=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1 h1:PKK9DyHxif4LZo+uQSgXNqs0jj5+xZwwfKHgph2lxBw=
github.com/santhosh-tekuri/jsonschema/v6 v6.0.1/go.mod h1:JXeL+ps8p7/KNMjDQk3TCwPpBy0wYklyWTfbkIzdIFU=
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
github.com/sosodev/duration v1.3.1 h1:qtHBDMQ6lvMQsL15g4aopM4HEfOaYuhWBw3NPTtlqq4=
Expand Down Expand Up @@ -136,8 +149,9 @@ golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.24.0 h1:J1shsA93PJUEVaUSaay7UXAyE8aimq3GW0pjlolpa24=
golang.org/x/tools v0.24.0/go.mod h1:YhNqVBIfWHdzvTLs0d8LCuMhkKUgSUKldakyV7W/WDQ=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127 h1:qIbj1fsPNlZgppZ+VLlY7N33q108Sa+fhmuc+sWQYwY=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY=
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
Expand Down
25 changes: 3 additions & 22 deletions main.go
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
//go:build !generate
// +build !generate

package main

import (
"encoding/json"
"fmt"
"io"
"log"
"os"

"github.com/davecgh/go-spew/spew"
"github.com/invopop/jsonschema"
"github.com/jippi/scm-engine/cmd"
"github.com/jippi/scm-engine/pkg/config"
"github.com/jippi/scm-engine/pkg/state"
"github.com/jippi/scm-engine/pkg/tui"
"github.com/urfave/cli/v2"
Expand Down Expand Up @@ -72,26 +72,7 @@ func main() {
Commands: []*cli.Command{
cmd.GitLab,
cmd.GitHub,
{
Name: "jsonschema",
Action: func(ctx *cli.Context) error {
r := new(jsonschema.Reflector)
if err := r.AddGoComments("github.com/jippi/scm-engine", "./"); err != nil {
return err
}

schema := r.Reflect(&config.Config{})

data, err := json.MarshalIndent(schema, "", " ")
if err != nil {
panic(err.Error())
}

fmt.Println(string(data))

return os.WriteFile("scm-engine.schema.json", data, 0o600)
},
},
// DEPRECATED COMMANDS
{
Name: "evaluate",
Expand Down
33 changes: 28 additions & 5 deletions pkg/config/action.go
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,32 @@ import (
slogctx "github.com/veqryn/slog-context"
)

type Actions []Action
type (
Actions []Action

Action struct {
// The name of the action, this is purely for debugging and your convenience.
//
// See: https://jippi.github.io/scm-engine/configuration/#actions.name
Name string `json:"name" yaml:"name"`

// (Optional) Only one action per group (in order) will be executed per evaluation cycle.
// Use this to 'stop' other actions from running with the same group name
Group string `json:"group,omitempty" yaml:"group"`

// A key controlling if the action should executed or not.
//
// This script is in Expr-lang: https://expr-lang.org/docs/language-definition
//
// See: https://jippi.github.io/scm-engine/configuration/#actions.if
If string `json:"if" yaml:"if"`

// The list of operations to take if the action.if returned true.
//
// See: https://jippi.github.io/scm-engine/configuration/#actions.if.then
Then []ActionStep `json:"then" yaml:"then"`
}
)

func (actions Actions) Evaluate(ctx context.Context, evalContext scm.EvalContext) ([]Action, error) {
results := []Action{}
Expand Down Expand Up @@ -42,10 +67,8 @@ func (actions Actions) Evaluate(ctx context.Context, evalContext scm.EvalContext
return results, nil
}

type Action scm.EvaluationActionResult

func (p *Action) Evaluate(ctx context.Context, evalContext scm.EvalContext) (bool, error) {
program, err := p.initialize(evalContext)
program, err := p.Setup(evalContext)
if err != nil {
return false, err
}
Expand All @@ -54,7 +77,7 @@ func (p *Action) Evaluate(ctx context.Context, evalContext scm.EvalContext) (boo
return runAndCheckBool(ctx, program, evalContext)
}

func (p *Action) initialize(evalContext scm.EvalContext) (*vm.Program, error) {
func (p *Action) Setup(evalContext scm.EvalContext) (*vm.Program, error) {
opts := []expr.Option{}
opts = append(opts, expr.AsBool())
opts = append(opts, expr.Env(evalContext))
Expand Down
Loading

0 comments on commit e99d7c3

Please sign in to comment.