From 9f8cedc00fec6151b8eb9fef2750f65836806d16 Mon Sep 17 00:00:00 2001 From: Adrian Orive Date: Fri, 27 Nov 2020 08:46:32 +0100 Subject: [PATCH] Organize verify directory --- Dockerfile | 2 +- README.md | 7 +- notes/verify/title.go | 67 ---- verify/cmd/runner.go | 91 ----- verify/descriptiveness.go | 53 +++ verify/issue.go | 50 +++ verify/main.go | 41 +++ verify/pkg/action/action.go | 84 +++++ verify/{ => pkg/action}/check_run_status.go | 2 +- .../{common.go => pkg/action/environment.go} | 85 +---- verify/pkg/action/interfaces.go | 32 ++ verify/{ => pkg/action}/plugin.go | 320 +++++++++--------- verify/type.go | 68 ++++ 13 files changed, 516 insertions(+), 386 deletions(-) delete mode 100644 notes/verify/title.go delete mode 100644 verify/cmd/runner.go create mode 100644 verify/descriptiveness.go create mode 100644 verify/issue.go create mode 100644 verify/main.go create mode 100644 verify/pkg/action/action.go rename verify/{ => pkg/action}/check_run_status.go (98%) rename verify/{common.go => pkg/action/environment.go} (54%) create mode 100644 verify/pkg/action/interfaces.go rename verify/{ => pkg/action}/plugin.go (67%) create mode 100644 verify/type.go diff --git a/Dockerfile b/Dockerfile index 056b8da..e58d7e9 100644 --- a/Dockerfile +++ b/Dockerfile @@ -6,7 +6,7 @@ COPY notes notes WORKDIR /go/src/verify/verify ENV CGO_ENABLED=0 -RUN go build -o /go/bin/verifypr ./cmd/ +RUN go build -o /go/bin/verifypr ./ FROM gcr.io/distroless/static-debian10 diff --git a/README.md b/README.md index c73d2fd..c741379 100644 --- a/README.md +++ b/README.md @@ -46,9 +46,10 @@ jobs: github_token: ${{ secrets.GITHUB_TOKEN }} ``` -The code that actually runs lives in [verify/cmd](/verify/cmd), while -[/verify](/verify) contains a framework for running PR description checks -from GitHub actions & uploading the result via the GitHub checks API. +The code that actually runs lives in [verify](/verify), while +[/verify/pkg/action](/verify/pkg/action) contains a framework for running PR +description checks from GitHub actions & uploading the result via the GitHub +checks API. This repo itself uses a "live" version of the action that always rebuilds from the local code (master branch), which lives in diff --git a/notes/verify/title.go b/notes/verify/title.go deleted file mode 100644 index 3bd9e1c..0000000 --- a/notes/verify/title.go +++ /dev/null @@ -1,67 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package verify - -import ( - "fmt" - "regexp" - - "sigs.k8s.io/kubebuilder-release-tools/notes/common" -) - -// Extracted from kubernetes/test-infra/prow/plugins/wip/wip-label.go -var wipRegex = regexp.MustCompile(`(?i)^\W?WIP\W`) - -type prTitleError struct { - title string -} -func (e *prTitleError) Error() string { - return "no matching PR type indicator found in title" -} -func (e *prTitleError) Help() string { - return fmt.Sprintf( -`I saw a title of %[2]s%[1]s%[2]s, which doesn't seem to have any of the acceptable prefixes. - -You need to have one of these as the prefix of your PR title: - -- Breaking change: ⚠ (%[2]s:warning:%[2]s) -- Non-breaking feature: ✨ (%[2]s:sparkles:%[2]s) -- Patch fix: 🐛 (%[2]s:bug:%[2]s) -- Docs: 📖 (%[2]s:book:%[2]s) -- Infra/Tests/Other: 🌱 (%[2]s:seedling:%[2]s) - -More details can be found at [sigs.k8s.io/controller-runtime/VERSIONING.md](https://sigs.k8s.io/controller-runtime/VERSIONING.md).`, e.title, "`") -} - -// VerifyPRTitle checks that the PR title matches a valid PR type prefix, -// returning a message describing what was found on success, and a special -// error (with more detailed help via .Help) on failure. -func VerifyPRTitle(title string) (string, error) { - // Remove the WIP prefix if found - title = wipRegex.ReplaceAllString(title, "") - - prType, finalTitle := common.PRTypeFromTitle(title) - if prType == common.UncategorizedPR { - return "", &prTitleError{title: title} - } - - return fmt.Sprintf( -`Found %s PR (%s) with final title: - - %s -`, prType.Emoji(), prType, finalTitle), nil -} diff --git a/verify/cmd/runner.go b/verify/cmd/runner.go deleted file mode 100644 index d24b094..0000000 --- a/verify/cmd/runner.go +++ /dev/null @@ -1,91 +0,0 @@ -/* -Copyright 2020 The Kubernetes Authors. - -Licensed under the Apache License, Version 2.0 (the "License"); -you may not use this file except in compliance with the License. -You may obtain a copy of the License at - - http://www.apache.org/licenses/LICENSE-2.0 - -Unless required by applicable law or agreed to in writing, software -distributed under the License is distributed on an "AS IS" BASIS, -WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. -See the License for the specific language governing permissions and -limitations under the License. -*/ - -package main - -import ( - "fmt" - "regexp" - "strings" - - "github.com/google/go-github/v32/github" - - notes "sigs.k8s.io/kubebuilder-release-tools/notes/common" - notesver "sigs.k8s.io/kubebuilder-release-tools/notes/verify" - "sigs.k8s.io/kubebuilder-release-tools/verify" -) - -type prErrs struct { - errs []string -} - -func (e prErrs) Error() string { - return fmt.Sprintf("%d issues found with your PR description", len(e.errs)) -} - -func (e prErrs) Help() string { - res := make([]string, len(e.errs)) - for _, err := range e.errs { - parts := strings.Split(err, "\n") - for i, part := range parts[1:] { - parts[i+1] = " " + part - } - res = append(res, "- "+strings.Join(parts, "\n")) - } - return strings.Join(res, "\n") -} - -func main() { - verify.ActionsEntrypoint(verify.RunPlugins( - verify.PRPlugin{ - Name: "PR Type", - Title: "PR Type in Title", - ProcessPR: func(pr *github.PullRequest) (string, error) { - return notesver.VerifyPRTitle(pr.GetTitle()) - }, - }, - - verify.PRPlugin{ - Name: "PR Desc", - Title: "Basic PR Descriptiveness Check", - ProcessPR: func(pr *github.PullRequest) (string, error) { - var errs []string - // TODO(directxman12): add warnings when we have them - - lineCnt := 0 - for _, line := range strings.Split(pr.GetBody(), "\n") { - if strings.TrimSpace(line) == "" { - continue - } - lineCnt++ - } - if lineCnt < 2 { - errs = append(errs, "**your PR body is *really* short**.\n\nIt probably isn't descriptive enough.\nYou should give a description that highlights both what you're doing it and *why* you're doing it. Someone reading the PR description without clicking any issue links should be able to roughly understand what's going on") - } - - _, title := notes.PRTypeFromTitle(pr.GetTitle()) - if regexp.MustCompile(`#\d+\b`).MatchString(title) { - errs = append(errs, "**Your PR has an issue number in the title.**\n\nThe title should just be descriptive.\nIssue numbers belong in the PR body as either `Fixes #XYZ` (if it closes the issue or PR), or something like `Related to #XYZ` (if it's just related).") - } - - if len(errs) == 0 { - return "Your PR description looks okay!", nil - } - return "", prErrs{errs: errs} - }, - }, - )) -} diff --git a/verify/descriptiveness.go b/verify/descriptiveness.go new file mode 100644 index 0000000..6e13900 --- /dev/null +++ b/verify/descriptiveness.go @@ -0,0 +1,53 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "strings" + + "github.com/google/go-github/v32/github" + + "sigs.k8s.io/kubebuilder-release-tools/verify/pkg/action" +) + +type prDescriptivenessError struct{} + +func (e prDescriptivenessError) Error() string { + return "Your PR description is *really* short." +} +func (e prDescriptivenessError) Details() string { + return `It probably isn't descriptive enough. +You should give a description that highlights both what you're doing it and *why* you're doing it. +Someone reading the PR description without clicking any issue links should be able to roughly understand what's going on.` +} + +// checkPRDescriptiveness +func checkPRDescriptiveness(requiredLines int) action.ValidateFunc { + return func(pr *github.PullRequest) (string, string, error) { + lineCnt := 0 + for _, line := range strings.Split(pr.GetBody(), "\n") { + if strings.TrimSpace(line) == "" { + continue + } + lineCnt++ + } + if lineCnt < requiredLines { + return "", "", &prDescriptivenessError{} + } + return "Your PR looks descriptive enough!", "", nil + } +} diff --git a/verify/issue.go b/verify/issue.go new file mode 100644 index 0000000..17a1193 --- /dev/null +++ b/verify/issue.go @@ -0,0 +1,50 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "regexp" + + "github.com/google/go-github/v32/github" + + notes "sigs.k8s.io/kubebuilder-release-tools/notes/common" +) + +var tagRegexp = regexp.MustCompile(`#\d+\b`) + +type prIssueInTitleError struct{} + +func (e prIssueInTitleError) Error() string { + return "Your PR has an Issue or PR number in the title." +} +func (e prIssueInTitleError) Details() string { + return fmt.Sprintf(`The title should just be descriptive. +Issue numbers belong in the PR body as either %#q (if it closes the issue or PR), or something like %#q (if it's just related).`, + "Fixes #XYZ", "Related to #XYZ", + ) +} + +// checkIssueInTitle verifies that the PR title does not contain any Issue or PR tag +func checkIssueInTitle(pr *github.PullRequest) (string, string, error) { + _, title := notes.PRTypeFromTitle(pr.GetTitle()) + if tagRegexp.MatchString(title) { + return "", "", prIssueInTitleError{} + } + + return "Your PR title does not contain any Issue or PR tags", "", nil +} diff --git a/verify/main.go b/verify/main.go new file mode 100644 index 0000000..7bf283d --- /dev/null +++ b/verify/main.go @@ -0,0 +1,41 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "sigs.k8s.io/kubebuilder-release-tools/verify/pkg/action" +) + +func main() { + action.New( + action.NewPlugin( + "PR Type", + "PR Type in Title", + verifyPRType, + ), + action.NewPlugin( + "PR Issue", + "Issue/PR tag in PR title", + checkIssueInTitle, + ), + action.NewPlugin( + "PR Desc", + "Basic PR Descriptiveness Check", + checkPRDescriptiveness(2), + ), + ).Run() +} diff --git a/verify/pkg/action/action.go b/verify/pkg/action/action.go new file mode 100644 index 0000000..da4df83 --- /dev/null +++ b/verify/pkg/action/action.go @@ -0,0 +1,84 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +import ( + "fmt" + "sync" + + "sigs.k8s.io/kubebuilder-release-tools/verify/pkg/log" +) + +// action executes the wrapped plugins concurrently +type action struct { + plugins []Plugin + + log.Logger +} + +// New creates a new Action which will run the provided plugins +func New(plugins ...Plugin) Action { + return action{ + plugins: plugins, + Logger: log.New(), + } +} + +// Run implements Action +func (a action) Run() { + env, err := newPREnv() + if err != nil { + a.Fatalf(1, "%v", err) + } + a.Debugf("environment for %s/%s ready", env.Owner, env.Repo) + + res := make(chan error) + var done sync.WaitGroup + + for _, p := range a.plugins { + // Required to scope it to prevent the use of a loop variable inside a function literal + plugin := p + + a.Debugf("launching %q plugins", plugin.Name()) + done.Add(1) + go func() { + defer done.Done() + res <- plugin.Entrypoint(env) + }() + } + + go func() { + done.Wait() + close(res) + }() + + a.Debug("retrieving plugin results") + errCount := 0 + for err := range res { + if err == nil { + continue + } + errCount++ + a.Errorf("%v", err) + } + + a.Infof("%d plugins ran", len(a.plugins)) + if errCount > 0 { + a.Fatalf(2, "%v", fmt.Errorf("%d plugins had errors", errCount)) + } + a.Info("Success!") +} diff --git a/verify/check_run_status.go b/verify/pkg/action/check_run_status.go similarity index 98% rename from verify/check_run_status.go rename to verify/pkg/action/check_run_status.go index 1a0b25c..262dff8 100644 --- a/verify/check_run_status.go +++ b/verify/pkg/action/check_run_status.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package verify +package action import ( "github.com/google/go-github/v32/github" diff --git a/verify/common.go b/verify/pkg/action/environment.go similarity index 54% rename from verify/common.go rename to verify/pkg/action/environment.go index ec24618..5b7e4ac 100644 --- a/verify/common.go +++ b/verify/pkg/action/environment.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package verify +package action import ( "context" @@ -22,33 +22,35 @@ import ( "fmt" "os" "strings" - "sync" "github.com/google/go-github/v32/github" "golang.org/x/oauth2" - - "sigs.k8s.io/kubebuilder-release-tools/verify/pkg/log" ) -var l = log.New() +const ( + envActionsKey = "GITHUB_ACTIONS" + envRepositoryKey = "GITHUB_REPOSITORY" + envEventPathKey = "GITHUB_EVENT_PATH" + envTokenKey = "INPUT_GITHUB_TOKEN" +) -type ActionsEnv struct { +type PREnv struct { Owner string Repo string Event *github.PullRequestEvent Client *github.Client } -func setupEnv() (*ActionsEnv, error) { - if os.Getenv("GITHUB_ACTIONS") != "true" { +func newPREnv() (*PREnv, error) { + if os.Getenv(envActionsKey) != "true" { return nil, fmt.Errorf("not running in an action, bailing. Set GITHUB_ACTIONS and the other appropriate env vars if you really want to do this.") } // Get owner and repository - ownerAndRepo := strings.Split(os.Getenv("GITHUB_REPOSITORY"), "/") + ownerAndRepo := strings.Split(os.Getenv(envRepositoryKey), "/") // Get event path - eventPath := os.Getenv("GITHUB_EVENT_PATH") + eventPath := os.Getenv(envEventPathKey) if eventPath == "" { return nil, fmt.Errorf("no event path set, something weird is up") } @@ -59,7 +61,10 @@ func setupEnv() (*ActionsEnv, error) { if err != nil { return github.PullRequestEvent{}, fmt.Errorf("unable to load event file: %w", err) } - defer eventFile.Close() + defer func() { + // As we are not writing to the file, we can omit the error + _ = eventFile.Close() + }() var event github.PullRequestEvent if err := json.NewDecoder(eventFile).Decode(&event); err != nil { @@ -73,67 +78,13 @@ func setupEnv() (*ActionsEnv, error) { // Create the client client := github.NewClient(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource( - &oauth2.Token{AccessToken: os.Getenv("INPUT_GITHUB_TOKEN")}, + &oauth2.Token{AccessToken: os.Getenv(envTokenKey)}, ))) - return &ActionsEnv{ + return &PREnv{ Owner: ownerAndRepo[0], Repo: ownerAndRepo[1], Event: &event, Client: client, }, nil } - -type ActionsCallback func(*ActionsEnv) error - -func ActionsEntrypoint(cb ActionsCallback) { - env, err := setupEnv() - if err != nil { - l.Fatalf(1, "%v", err) - } - l.Debugf("environment for %s/%s ready", env.Owner, env.Repo) - - if err := cb(env); err != nil { - l.Fatalf(2, "%v", err) - } - l.Info("Success!") -} - -func RunPlugins(plugins ...PRPlugin) ActionsCallback { - l.Debugf("creating cb for %d plugins", len(plugins)) - return func(env *ActionsEnv) error { - res := make(chan error) - var done sync.WaitGroup - - for _, plugin := range plugins { - l.Debugf("launching %q plugin", plugin.Name) - done.Add(1) - go func(plugin PRPlugin) { - defer done.Done() - plugin.init() - res <- plugin.entrypoint(env) - }(plugin) - } - - go func() { - done.Wait() - close(res) - }() - - l.Debug("retrieving plugin results") - errCount := 0 - for err := range res { - if err == nil { - continue - } - errCount++ - l.Errorf("%v", err) - } - - l.Infof("%d plugins ran", len(plugins)) - if errCount > 0 { - return fmt.Errorf("%d plugins had errors", errCount) - } - return nil - } -} diff --git a/verify/pkg/action/interfaces.go b/verify/pkg/action/interfaces.go new file mode 100644 index 0000000..ce3556e --- /dev/null +++ b/verify/pkg/action/interfaces.go @@ -0,0 +1,32 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package action + +type Action interface { + Run() +} + +type Plugin interface { + Name() string + Entrypoint(env *PREnv) error +} + +// ErrorWithDetails allows to provide extended descriptions. +type ErrorWithDetails interface { + error + Details() string +} diff --git a/verify/plugin.go b/verify/pkg/action/plugin.go similarity index 67% rename from verify/plugin.go rename to verify/pkg/action/plugin.go index de1bcc7..84bd425 100644 --- a/verify/plugin.go +++ b/verify/pkg/action/plugin.go @@ -14,7 +14,7 @@ See the License for the specific language governing permissions and limitations under the License. */ -package verify +package action import ( "context" @@ -34,42 +34,151 @@ const ( actionSync = "synchronize" ) -// ErrorWithHelp allows PRPlugin.ProcessPR to provide extended descriptions -type ErrorWithHelp interface { - error - Help() string -} +// ValidateFunc is the type of the callback that a Plugin will use to validate the PR contents +type ValidateFunc func(*github.PullRequest) (string, string, error) + +// plugin performs the wrapped validate and uploads the results using GitHub Check API +type plugin struct { + checkRunName string + checkRunOutputTitle string -// PRPlugin handles pull request events -type PRPlugin struct { - ProcessPR func(pr *github.PullRequest) (string, error) - Name string - Title string + validate ValidateFunc log.Logger } -// init initializes the PRPlugin -func (p *PRPlugin) init() { - p.Logger = log.NewFor(p.Name) - p.Debug("plugin initialized") +// New creates a new Plugin that validates a PR event uploading the results +// using GitHub Check API with the provided name and output title. +func NewPlugin(name, title string, validate ValidateFunc) Plugin { + return plugin{ + checkRunName: name, + checkRunOutputTitle: title, + validate: validate, + Logger: log.NewFor(name), + } +} + +// Name implements Plugin interface. +func (p plugin) Name() string { + return p.checkRunName } -// processPR executes the provided ProcessPR and parses the result -func (p PRPlugin) processPR(pr *github.PullRequest) (conclusion, summary, text string, err error) { - p.Debug("execute the plugin checks") - text, err = p.ProcessPR(pr) +// Entrypoint implements Plugin interface. +func (p plugin) Entrypoint(env *PREnv) error { + switch env.Event.GetAction() { + case actionOpen: + return p.onOpen(env) + case actionReopen: + return p.onReopen(env) + case actionEdit: + return p.onEdit(env) + case actionSync: + return p.onSync(env) + default: + p.Warningf("action %q received with no defined procedure, skipping", env.Event.GetAction()) + } + + return nil +} + +// onOpen handles "opened" actions +func (p plugin) onOpen(env *PREnv) error { + p.Debugf("%q handler", actionOpen) + // Create the check run + checkRun, err := p.createCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetPullRequest().GetHead().GetSHA()) + if err != nil { + return err + } + + // Process the PR and submit the results + _, err = p.validateAndSubmit(env, checkRun) + return err +} + +// onReopen handles "reopened" actions +func (p plugin) onReopen(env *PREnv) error { + p.Debugf("%q handler", actionReopen) + // Get the check run + checkRun, err := p.getCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetPullRequest().GetHead().GetSHA()) + if err != nil { + return err + } + + // Rerun the tests if they weren't finished + if !Finished.Equal(checkRun.GetStatus()) { + // Process the PR and submit the results + _, err = p.validateAndSubmit(env, checkRun) + return err + } + + // Return failure here too so that the whole suite fails (since the actions + // suite seems to ignore failing check runs when calculating general failure) + if *checkRun.Conclusion == "failure" { + return fmt.Errorf("failed: %v", *checkRun.Output.Summary) + } + return nil +} + +// onEdit handles "edited" actions +func (p plugin) onEdit(env *PREnv) error { + p.Debugf("%q handler", actionEdit) + // Reset the check run + checkRun, err := p.resetCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetPullRequest().GetHead().GetSHA()) + if err != nil { + return err + } + + // Process the PR and submit the results + _, err = p.validateAndSubmit(env, checkRun) + return err +} + +// onSync handles "synchronize" actions +func (p plugin) onSync(env *PREnv) error { + p.Debugf("%q handler", actionSync) + // Get the check run + checkRun, err := p.getCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetBefore()) + if err != nil { + return err + } + + // Rerun the tests if they weren't finished + if !Finished.Equal(checkRun.GetStatus()) { + // Process the PR and submit the results + checkRun, err = p.validateAndSubmit(env, checkRun) + if err != nil { + return err + } + } + // Create a duplicate for the new commit + checkRun, err = p.duplicateCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetAfter(), checkRun) if err != nil { + return err + } + + // Return failure here too so that the whole suite fails (since the actions + // suite seems to ignore failing check runs when calculating general failure) + if *checkRun.Conclusion == "failure" { + return fmt.Errorf("failed: %v", *checkRun.Output.Summary) + } + return nil +} + +// validatePR executes the provided validating function and parses the result +func (p plugin) validatePR(pr *github.PullRequest) (conclusion, summary, text string, err error) { + p.Debug("execute the plugin checks") + summary, text, err = p.validate(pr) + + if err == nil { + conclusion = "success" + } else { conclusion = "failure" summary = err.Error() - var helpErr ErrorWithHelp - if errors.As(err, &helpErr) { - text = helpErr.Help() + var detailedErr ErrorWithDetails + if errors.As(err, &detailedErr) { + text = detailedErr.Details() } - } else { - conclusion = "success" - summary = "Success" } // Log in case we can't submit the result for some reason @@ -80,10 +189,10 @@ func (p PRPlugin) processPR(pr *github.PullRequest) (conclusion, summary, text s return conclusion, summary, text, err } -// processAndSubmit performs the checks and submits the result -func (p PRPlugin) processAndSubmit(env *ActionsEnv, checkRun *github.CheckRun) (*github.CheckRun, error) { - // Process the PR - conclusion, summary, text, procErr := p.processPR(env.Event.PullRequest) +// validateAndSubmit performs the validation and submits the result +func (p plugin) validateAndSubmit(env *PREnv, checkRun *github.CheckRun) (*github.CheckRun, error) { + // Validate the PR + conclusion, summary, text, validateErr := p.validatePR(env.Event.PullRequest) // Update the check run checkRun, err := p.finishCheckRun(env.Client, env.Owner, env.Repo, checkRun.GetID(), conclusion, summary, text) @@ -93,8 +202,8 @@ func (p PRPlugin) processAndSubmit(env *ActionsEnv, checkRun *github.CheckRun) ( // Return failure here too so that the whole suite fails (since the actions // suite seems to ignore failing check runs when calculating general failure) - if procErr != nil { - return checkRun, fmt.Errorf("failed: %v", procErr) + if validateErr != nil { + return checkRun, fmt.Errorf("failed: %v", validateErr) } return checkRun, nil } @@ -105,15 +214,15 @@ func (p PRPlugin) processAndSubmit(env *ActionsEnv, checkRun *github.CheckRun) ( // createCheckRun creates a new Check-Run. // It returns an error in case it couldn't be created. -func (p PRPlugin) createCheckRun(client *github.Client, owner, repo, headSHA string) (*github.CheckRun, error) { - p.Debugf("creating check run %q on %s/%s @ %s...", p.Name, owner, repo, headSHA) +func (p plugin) createCheckRun(client *github.Client, owner, repo, headSHA string) (*github.CheckRun, error) { + p.Debugf("creating check run %q on %s/%s @ %s...", p.checkRunName, owner, repo, headSHA) checkRun, res, err := client.Checks.CreateCheckRun( context.TODO(), owner, repo, github.CreateCheckRunOptions{ - Name: p.Name, + Name: p.checkRunName, HeadSHA: headSHA, Status: Started.StringP(), }, @@ -130,8 +239,8 @@ func (p PRPlugin) createCheckRun(client *github.Client, owner, repo, headSHA str // getCheckRun returns the Check-Run, creating it if it doesn't exist. // It returns an error in case it didn't exist and couldn't be created, or if there are multiple matches. -func (p PRPlugin) getCheckRun(client *github.Client, owner, repo, headSHA string) (*github.CheckRun, error) { - p.Debugf("getting check run %q on %s/%s @ %s...", p.Name, owner, repo, headSHA) +func (p plugin) getCheckRun(client *github.Client, owner, repo, headSHA string) (*github.CheckRun, error) { + p.Debugf("getting check run %q on %s/%s @ %s...", p.checkRunName, owner, repo, headSHA) checkRunList, res, err := client.Checks.ListCheckRunsForRef( context.TODO(), @@ -139,7 +248,7 @@ func (p PRPlugin) getCheckRun(client *github.Client, owner, repo, headSHA string repo, headSHA, &github.ListCheckRunsOptions{ - CheckName: github.String(p.Name), + CheckName: github.String(p.checkRunName), }, ) @@ -157,24 +266,24 @@ func (p PRPlugin) getCheckRun(client *github.Client, owner, repo, headSHA string return checkRunList.CheckRuns[0], nil case n > 1: return nil, fmt.Errorf("multiple instances of `%s` check run found on %s/%s @ %s", - p.Name, owner, repo, headSHA) + p.checkRunName, owner, repo, headSHA) default: // Should never happen return nil, fmt.Errorf("negative number of instances (%d) of `%s` check run found on %s/%s @ %s", - n, p.Name, owner, repo, headSHA) + n, p.checkRunName, owner, repo, headSHA) } } // resetCheckRun returns the Check-Run with executing status, creating it if it doesn't exist. // It returns an error in case it didn't exist and couldn't be created, if there are multiple matches, // or if it exists but couldn't be updated. -func (p PRPlugin) resetCheckRun(client *github.Client, owner, repo string, headSHA string) (*github.CheckRun, error) { +func (p plugin) resetCheckRun(client *github.Client, owner, repo string, headSHA string) (*github.CheckRun, error) { checkRun, err := p.getCheckRun(client, owner, repo, headSHA) // If it errored, or it was created but not finished, we don't need to update it if err != nil || Started.Equal(checkRun.GetStatus()) { return checkRun, err } - p.Debugf("resetting check run %q on %s/%s...", p.Name, owner, repo) + p.Debugf("resetting check run %q on %s/%s...", p.checkRunName, owner, repo) checkRun, updateResp, err := client.Checks.UpdateCheckRun( context.TODO(), @@ -182,7 +291,7 @@ func (p PRPlugin) resetCheckRun(client *github.Client, owner, repo string, headS repo, checkRun.GetID(), github.UpdateCheckRunOptions{ - Name: p.Name, + Name: p.checkRunName, Status: Started.StringP(), }, ) @@ -198,17 +307,22 @@ func (p PRPlugin) resetCheckRun(client *github.Client, owner, repo string, headS // finishCheckRun updates the Check-Run with id checkRunID setting its output. // It returns an error in case it couldn't be updated. -func (p PRPlugin) finishCheckRun(client *github.Client, owner, repo string, checkRunID int64, conclusion, summary, text string) (*github.CheckRun, error) { - p.Debugf("adding results to check run %q on %s/%s...", p.Name, owner, repo) +func (p plugin) finishCheckRun(client *github.Client, owner, repo string, checkRunID int64, conclusion, summary, text string) (*github.CheckRun, error) { + p.Debugf("adding results to check run %q on %s/%s...", p.checkRunName, owner, repo) + // CheckRun.Output.Text is optional, so empty text strings should actually be nil pointers + var testPointer *string + if text != "" { + testPointer = github.String(text) + } checkRun, updateResp, err := client.Checks.UpdateCheckRun(context.TODO(), owner, repo, checkRunID, github.UpdateCheckRunOptions{ - Name: p.Name, + Name: p.checkRunName, Conclusion: github.String(conclusion), CompletedAt: &github.Timestamp{Time: time.Now()}, Output: &github.CheckRunOutput{ - Title: github.String(p.Title), + Title: github.String(p.checkRunOutputTitle), Summary: github.String(summary), - Text: github.String(text), + Text: testPointer, }, }) @@ -222,15 +336,15 @@ func (p PRPlugin) finishCheckRun(client *github.Client, owner, repo string, chec } // duplicateCheckRun creates a new Check-Run with the same info as the provided one but for a new headSHA -func (p PRPlugin) duplicateCheckRun(client *github.Client, owner, repo, headSHA string, checkRun *github.CheckRun) (*github.CheckRun, error) { - p.Debugf("duplicating check run %q on %s/%s @ %s...", p.Name, owner, repo, headSHA) +func (p plugin) duplicateCheckRun(client *github.Client, owner, repo, headSHA string, checkRun *github.CheckRun) (*github.CheckRun, error) { + p.Debugf("duplicating check run %q on %s/%s @ %s...", p.checkRunName, owner, repo, headSHA) checkRun, res, err := client.Checks.CreateCheckRun( context.TODO(), owner, repo, github.CreateCheckRunOptions{ - Name: p.Name, + Name: p.checkRunName, HeadSHA: headSHA, DetailsURL: checkRun.DetailsURL, ExternalID: checkRun.ExternalID, @@ -250,109 +364,3 @@ func (p PRPlugin) duplicateCheckRun(client *github.Client, owner, repo, headSHA } return checkRun, nil } - -//////////////////////////////////////////////////////////////////////////////// -// Entrypoint // -//////////////////////////////////////////////////////////////////////////////// - -// entrypoint will call the corresponding handler -func (p PRPlugin) entrypoint(env *ActionsEnv) (err error) { - switch env.Event.GetAction() { - case actionOpen: - err = p.onOpen(env) - case actionReopen: - err = p.onReopen(env) - case actionEdit: - err = p.onEdit(env) - case actionSync: - err = p.onSync(env) - default: - p.Warningf("action %q received with no defined procedure, skipping", env.Event.GetAction()) - } - - return -} - -// onOpen handles "open" actions -func (p PRPlugin) onOpen(env *ActionsEnv) error { - p.Debugf("%q handler", actionOpen) - // Create the check run - checkRun, err := p.createCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetPullRequest().GetHead().GetSHA()) - if err != nil { - return err - } - - // Process the PR and submit the results - _, err = p.processAndSubmit(env, checkRun) - return err -} - -// onReopen handles "reopen" actions -func (p PRPlugin) onReopen(env *ActionsEnv) error { - p.Debugf("%q handler", actionReopen) - // Get the check run - checkRun, err := p.getCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetPullRequest().GetHead().GetSHA()) - if err != nil { - return err - } - - // Rerun the tests if they weren't finished - if !Finished.Equal(checkRun.GetStatus()) { - // Process the PR and submit the results - _, err = p.processAndSubmit(env, checkRun) - return err - } - - // Return failure here too so that the whole suite fails (since the actions - // suite seems to ignore failing check runs when calculating general failure) - if *checkRun.Conclusion == "failure" { - return fmt.Errorf("failed: %v", *checkRun.Output.Summary) - } - return nil -} - -// onEdit handles "edit" actions -func (p PRPlugin) onEdit(env *ActionsEnv) error { - p.Debugf("%q handler", actionEdit) - // Reset the check run - checkRun, err := p.resetCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetPullRequest().GetHead().GetSHA()) - if err != nil { - return err - } - - // Process the PR and submit the results - _, err = p.processAndSubmit(env, checkRun) - return err -} - -// onSync handles "synchronize" actions -func (p PRPlugin) onSync(env *ActionsEnv) error { - p.Debugf("%q handler", actionSync) - // Get the check run - checkRun, err := p.getCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetBefore()) - if err != nil { - return err - } - - // Rerun the tests if they weren't finished - if !Finished.Equal(checkRun.GetStatus()) { - // Process the PR and submit the results - checkRun, err = p.processAndSubmit(env, checkRun) - if err != nil { - return err - } - } - - // Create a duplicate for the new commit - checkRun, err = p.duplicateCheckRun(env.Client, env.Owner, env.Repo, env.Event.GetAfter(), checkRun) - if err != nil { - return err - } - - // Return failure here too so that the whole suite fails (since the actions - // suite seems to ignore failing check runs when calculating general failure) - if *checkRun.Conclusion == "failure" { - return fmt.Errorf("failed: %v", *checkRun.Output.Summary) - } - return nil -} diff --git a/verify/type.go b/verify/type.go new file mode 100644 index 0000000..cf7a28b --- /dev/null +++ b/verify/type.go @@ -0,0 +1,68 @@ +/* +Copyright 2020 The Kubernetes Authors. + +Licensed under the Apache License, Version 2.0 (the "License"); +you may not use this file except in compliance with the License. +You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + +Unless required by applicable law or agreed to in writing, software +distributed under the License is distributed on an "AS IS" BASIS, +WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +See the License for the specific language governing permissions and +limitations under the License. +*/ + +package main + +import ( + "fmt" + "regexp" + + "github.com/google/go-github/v32/github" + + notes "sigs.k8s.io/kubebuilder-release-tools/notes/common" +) + +// Extracted from kubernetes/test-infra/prow/plugins/wip/wip-label.go +var wipRegex = regexp.MustCompile(`(?i)^\W?WIP\W`) + +type prTitleTypeError struct { + title string +} + +func (e prTitleTypeError) Error() string { + return "No matching PR type indicator found in title." +} +func (e prTitleTypeError) Details() string { + return fmt.Sprintf( + `I saw a title of %#q, which doesn't seem to have any of the acceptable prefixes. + +You need to have one of these as the prefix of your PR title: + +- Breaking change: ⚠ (%#q) +- Non-breaking feature: ✨ (%#q) +- Patch fix: 🐛 (%#q) +- Docs: 📖 (%#q) +- Infra/Tests/Other: 🌱 (%#q) + +More details can be found at [sigs.k8s.io/kubebuilder-release-tools/VERSIONING.md](https://sigs.k8s.io/kubebuilder-release-tools/VERSIONING.md).`, + e.title, ":warning:", ":sparkles:", ":bug:", ":book:", ":seedling:") +} + +// verifyPRType checks that the PR title contains a prefix that defines its type +func verifyPRType(pr *github.PullRequest) (string, string, error) { + // Remove the WIP prefix if found + title := wipRegex.ReplaceAllString(pr.GetTitle(), "") + + prType, finalTitle := notes.PRTypeFromTitle(title) + if prType == notes.UncategorizedPR { + return "", "", prTitleTypeError{title: title} + } + + return fmt.Sprintf("Found %s PR (%s)", prType.Emoji(), prType), fmt.Sprintf(`Final title: + + %s +`, finalTitle), nil +}