diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml new file mode 100644 index 000000000000..d140e6f3f928 --- /dev/null +++ b/.github/workflows/main.yml @@ -0,0 +1,16 @@ +on: + pull_request_target: + types: [opened, edited, reopened] + +jobs: + verify: + runs-on: ubuntu-latest + name: verify PR contents + steps: + - name: Checkout + uses: actions/checkout@v2 + - name: Verifier action + id: verifier + uses: ./ + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 000000000000..056b8da1a4d9 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:1.15 as build + +WORKDIR /go/src/verify +COPY verify verify +COPY notes notes +WORKDIR /go/src/verify/verify + +ENV CGO_ENABLED=0 +RUN go build -o /go/bin/verifypr ./cmd/ + +FROM gcr.io/distroless/static-debian10 + +COPY --from=build /go/bin/verifypr /verifypr + +ENTRYPOINT ["/verifypr"] diff --git a/action.yml b/action.yml new file mode 100644 index 000000000000..a89ca0a0d880 --- /dev/null +++ b/action.yml @@ -0,0 +1,9 @@ +name: 'Verify KubeBuilder PRs' +description: 'Verify PRs for the KubeBuilder project repos & similar' +inputs: + github_token: + description: "the github_token provided by the actions runner" + required: true +runs: + using: docker + image: 'Dockerfile' diff --git a/notes/common/prefix.go b/notes/common/prefix.go index db8fb59e675f..2a6b966ea8fe 100644 --- a/notes/common/prefix.go +++ b/notes/common/prefix.go @@ -17,10 +17,47 @@ limitations under the License. package common import ( + "fmt" "strings" ) type PRType int +func (t PRType) Emoji() string { + switch t { + case UncategorizedPR: + return "" + case BreakingPR: + return emojiBreaking + case FeaturePR: + return emojiFeature + case BugfixPR: + return emojiBugfix + case DocsPR: + return emojiDocs + case InfraPR: + return emojiInfra + default: + panic(fmt.Sprintf("unrecognized PR type %v", t)) + } +} +func (t PRType) String() string { + switch t { + case UncategorizedPR: + return "uncategorized" + case BreakingPR: + return "breaking" + case FeaturePR: + return "feature" + case BugfixPR: + return "bugfix" + case DocsPR: + return "docs" + case InfraPR: + return "infra" + default: + panic(fmt.Sprintf("unrecognized PR type %v", t)) + } +} const ( UncategorizedPR PRType = iota diff --git a/notes/verify/title.go b/notes/verify/title.go new file mode 100644 index 000000000000..92522bfff0fc --- /dev/null +++ b/notes/verify/title.go @@ -0,0 +1,60 @@ +/* +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" + + "sigs.k8s.io/kubebuilder-release-tools/notes/common" +) + +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) { + 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 new file mode 100644 index 000000000000..c6d48add5073 --- /dev/null +++ b/verify/cmd/runner.go @@ -0,0 +1,105 @@ +/* +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" + "strings" + "regexp" + + "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()) + }, + ForAction: func(action string) bool { + switch action { + case "opened", "edited", "reopened": + return true + default: + return false + } + }, + }, + + 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{1,}\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} + }, + ForAction: func(action string) bool { + switch action { + case "opened", "edited", "reopened": + return true + default: + return false + } + }, + }, + )) +}