diff --git a/.github/actions/registry-check/action.yaml b/.github/actions/registry-check/action.yaml new file mode 100644 index 000000000..302940a1e --- /dev/null +++ b/.github/actions/registry-check/action.yaml @@ -0,0 +1,41 @@ +name: Check Registry +description: Runs registry check command on registry. Exits with non-zero if any problems detected. +author: Apigee Registry Authors +branding: + color: red + icon: cloud-lightning +inputs: + pattern: + description: a shell command to use to generate an authorization token for the Registry + required: false + enable: + description: rules to enable + required: false + disable: + description: rules to disable + required: false + filter: + description: filter selected resources + required: false + jobs: + description: number of actions to perform concurrently (default 10) + required: false + error-level: + description: level at which to error + required: false +outputs: + report: + description: Check Report + value: ${{ steps.check.outputs.REPORT }} +runs: + using: composite + steps: + - name: Run Registry Check + id: check + shell: bash + run: | + REPORT=$(registry check ${{ inputs.pattern }} --enable "${{ inputs.enable }}" --disable "${{ inputs.enable }}" --filter "${{ inputs.filter }}" --error-level "${{ inputs.error-level }}") + echo "REPORT<> $GITHUB_OUTPUT + echo "Check Report:" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo ""$'\n'"${REPORT}"$'\n' >> $GITHUB_STEP_SUMMARY diff --git a/cmd/registry/cmd/check/check.go b/cmd/registry/cmd/check/check.go index 3998cb850..a034ffbb7 100644 --- a/cmd/registry/cmd/check/check.go +++ b/cmd/registry/cmd/check/check.go @@ -22,6 +22,7 @@ import ( "github.com/apigee/registry/cmd/registry/cmd/check/lint" "github.com/apigee/registry/cmd/registry/cmd/check/rules" + "github.com/apigee/registry/pkg/application/check" "github.com/apigee/registry/pkg/connection" "github.com/apigee/registry/pkg/names" "github.com/spf13/cobra" @@ -51,6 +52,7 @@ var ( disable []string configFile string listRules bool + errorlevel string ) func Command() *cobra.Command { @@ -59,6 +61,20 @@ func Command() *cobra.Command { Short: "Check entities in the API Registry", Args: cobra.RangeArgs(0, 1), RunE: func(cmd *cobra.Command, args []string) error { + var exitOnErrorLevel check.Problem_Severity + if errorlevel != "" { + switch errorlevel { + case "ERROR": + exitOnErrorLevel = check.Problem_ERROR + case "WARNING": + exitOnErrorLevel = check.Problem_WARNING + case "INFO": + exitOnErrorLevel = check.Problem_INFO + default: + return fmt.Errorf("invalid level: %q, must be INFO, WARNING, or ERROR", errorlevel) + } + } + ctx := cmd.Context() c, err := connection.ActiveConfig() if err != nil { @@ -124,7 +140,19 @@ func Command() *cobra.Command { } _, err = cmd.OutOrStdout().Write(serialized) - return err + if err != nil { + return err + } + + if exitOnErrorLevel != 0 { + for _, p := range response.Problems { + if p.Severity <= exitOnErrorLevel { + cmd.SilenceUsage = true + return fmt.Errorf("exceeded designated error-level %q", errorlevel) + } + } + } + return nil }, } @@ -134,6 +162,7 @@ func Command() *cobra.Command { cmd.Flags().StringArrayVar(&enable, "enable", nil, "enable rules") cmd.Flags().StringArrayVar(&disable, "disable", nil, "disable rules") cmd.Flags().BoolVar(&listRules, "list-rules", false, "print enabled rules and exit") + cmd.Flags().StringVar(&errorlevel, "error-level", "", "exit code 1 if problems at specified level or above [INFO|WARNING|ERROR]") return cmd } diff --git a/cmd/registry/cmd/check/check_test.go b/cmd/registry/cmd/check/check_test.go index 91c2d5425..0ac30d82e 100644 --- a/cmd/registry/cmd/check/check_test.go +++ b/cmd/registry/cmd/check/check_test.go @@ -15,15 +15,19 @@ package check import ( + "bufio" "bytes" "context" - "strings" "testing" + "github.com/apigee/registry/pkg/application/check" "github.com/apigee/registry/pkg/connection/grpctest" "github.com/apigee/registry/rpc" "github.com/apigee/registry/server/registry" "github.com/apigee/registry/server/registry/test/seeder" + "github.com/google/go-cmp/cmp" + "google.golang.org/protobuf/testing/protocmp" + "gopkg.in/yaml.v3" ) // TestMain will set up a local RegistryServer and grpc.Server for all @@ -52,7 +56,74 @@ func TestCheck(t *testing.T) { if err := cmd.Execute(); err != nil { t.Fatalf("Execute() with args %v returned error: %s", args, err) } - if !strings.Contains(buf.String(), `Unexpected mime_type "application/html"`) { - t.Errorf("unexpected result: %s", buf) + + got := new(check.CheckReport) + if err := yaml.Unmarshal(buf.Bytes(), got); err != nil { + t.Fatal(err) + } + + want := &check.CheckReport{ + Id: "check-report", + Kind: "CheckReport", + Problems: []*check.Problem{ + { + Message: `Unexpected mime_type "application/html" for contents.`, + Suggestion: `Detected mime_type: "text/plain; charset=utf-8".`, + Location: `projects/my-project/locations/global/apis/a/versions/v/specs/bad::MimeType`, + RuleId: `registry::0111::mime-type-detected-contents`, + Severity: check.Problem_WARNING, + }, + }, + } + + opts := cmp.Options{ + protocmp.Transform(), + protocmp.IgnoreFields(new(check.CheckReport), "create_time"), + } + if diff := cmp.Diff(want, got, opts); diff != "" { + t.Errorf("unexpected diff: (-want +got):\n%s", diff) + } +} + +func TestExitCode(t *testing.T) { + ctx := context.Background() + grpctest.SetupRegistry(ctx, t, "my-project", []seeder.RegistryResource{ + &rpc.ApiSpec{ + Name: "projects/my-project/locations/global/apis/a/versions/v/specs/bad", + MimeType: "application/html", + Contents: []byte("some text"), + }, + }) + + // problem >= warning + buf := &bytes.Buffer{} + cmd := Command() + args := []string{"projects/my-project", "--error-level", "WARNING"} + cmd.SetArgs(args) + cmd.SetOut(buf) + cmd.SetErr(buf) + if err := cmd.Execute(); err == nil { + t.Fatalf("expected err") + } + last := lastLine(buf) + want := `Error: exceeded designated error-level "WARNING"` + if last != want { + t.Errorf("want: %q, got: %q", want, last) + } + + // problem < error + args = []string{"projects/my-project", "--error-level", "ERROR"} + cmd.SetArgs(args) + if err := cmd.Execute(); err != nil { + t.Fatalf("unexpected err: %s", err) + } +} + +func lastLine(buf *bytes.Buffer) string { + s := bufio.NewScanner(buf) + last := "" + for s.Scan() { + last = s.Text() } + return last }