Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

⚠️ Define specific behavior for each PR action including synchronize #12

Merged
merged 1 commit into from Nov 16, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
2 changes: 1 addition & 1 deletion .github/workflows/main.yml
@@ -1,6 +1,6 @@
on:
pull_request_target:
types: [opened, edited, reopened]
types: [opened, edited, reopened, synchronize]

jobs:
verify:
Expand Down
37 changes: 37 additions & 0 deletions verify/check_run_status.go
@@ -0,0 +1,37 @@
/*
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 (
"github.com/google/go-github/v32/github"
)

type CheckRunStatus string

const (
Queued CheckRunStatus = "queued"
Started CheckRunStatus = "in_progress"
Finished CheckRunStatus = "completed"
)

func (status CheckRunStatus) StringP() *string {
return github.String(string(status))
}

func (status CheckRunStatus) Equal(other string) bool {
return string(status) == other
}
28 changes: 7 additions & 21 deletions verify/cmd/runner.go
Expand Up @@ -18,8 +18,8 @@ package main

import (
"fmt"
"strings"
"regexp"
"strings"

"github.com/google/go-github/v32/github"

Expand All @@ -31,15 +31,17 @@ import (
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
parts[i+1] = " " + part
}
res = append(res, "- "+strings.Join(parts, "\n"))
}
Expand All @@ -49,23 +51,15 @@ func (e prErrs) Help() string {
func main() {
verify.ActionsEntrypoint(verify.RunPlugins(
verify.PRPlugin{
Name: "PR Type",
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",
Name: "PR Desc",
Title: "Basic PR Descriptiveness Check",
ProcessPR: func(pr *github.PullRequest) (string, error) {
var errs []string
Expand All @@ -83,7 +77,7 @@ func main() {
}

_, title := notes.PRTypeFromTitle(pr.GetTitle())
if regexp.MustCompile(`#\d{1,}\b`).MatchString(title) {
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).")
}

Expand All @@ -92,14 +86,6 @@ func main() {
}
return "", prErrs{errs: errs}
},
ForAction: func(action string) bool {
switch action {
case "opened", "edited", "reopened":
return true
default:
return false
}
},
},
))
}
156 changes: 37 additions & 119 deletions verify/common.go
Expand Up @@ -17,164 +17,82 @@ limitations under the License.
package verify

import (
"context"
"encoding/json"
"fmt"
"os"
"encoding/json"
"errors"
"context"
"time"
"strings"
"sync"

"github.com/google/go-github/v32/github"
"golang.org/x/oauth2"
)

type ErrWithHelp interface {
error
Help() string
}

type PRPlugin struct {
ForAction func(string) bool
ProcessPR func(pr *github.PullRequest) (string, error)
Name string
Title string
}

func (p *PRPlugin) Entrypoint(env *ActionsEnv) error {
if p.ForAction != nil && !p.ForAction(env.Event.GetAction()) {
return nil
}

repoParts := strings.Split(env.Event.GetRepo().GetFullName(), "/")
orgName, repoName := repoParts[0], repoParts[1]

headSHA := env.Event.GetPullRequest().GetHead().GetSHA()
fmt.Printf("::debug::creating check run %q on %s/%s @ %s...\n", p.Name, orgName, repoName, headSHA)

resRun, runResp, err := env.Client.Checks.CreateCheckRun(context.TODO(), orgName, repoName, github.CreateCheckRunOptions{
Name: p.Name,
HeadSHA: headSHA,
Status: github.String("in_progress"),
})
if err != nil {
return fmt.Errorf("unable to submit check result: %w", err)
}

env.Debugf("create check API response: %+v", runResp)
env.Debugf("created run: %+v", resRun)

successStatus, procErr := p.ProcessPR(env.Event.PullRequest)

var summary, fullHelp, conclusion string
if procErr != nil {
summary = procErr.Error()
var helpErr ErrWithHelp
if errors.As(procErr, &helpErr) {
fullHelp = helpErr.Help()
}
conclusion = "failure"
} else {
summary = "Success"
fullHelp = successStatus
conclusion = "success"
}
completedAt := github.Timestamp{Time: time.Now()}

// log in case we can't submit the result for some reason
env.Debugf("plugin result summary: %q", summary)
env.Debugf("plugin result details: %q", fullHelp)
env.Debugf("plugin conclusion: %q", conclusion)

resRun, updateResp, err := env.Client.Checks.UpdateCheckRun(context.TODO(), orgName, repoName, resRun.GetID(), github.UpdateCheckRunOptions{
Name: p.Name,
Status: github.String("completed"),
Conclusion: github.String(conclusion),
CompletedAt: &completedAt,
Output: &github.CheckRunOutput{
Title: github.String(p.Title),
Summary: github.String(summary),
Text: github.String(fullHelp),
},
})
if err != nil {
return fmt.Errorf("unable to update check result: %w", err)
}

env.Debugf("update check API response: %+v", updateResp)
env.Debugf("updated run: %+v", resRun)

// 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 fmt.Errorf("failed: %v", procErr)
}
return nil
}
var log logger

type ActionsEnv struct {
Event *github.PullRequestEvent
Owner string
Repo string
Event *github.PullRequestEvent
Client *github.Client
}
func (ActionsEnv) Errorf(fmtStr string, args ...interface{}) {
fmt.Printf("::error::"+fmtStr+"\n", args...)
}
func (ActionsEnv) Debugf(fmtStr string, args ...interface{}) {
fmt.Printf("::debug::"+fmtStr+"\n", args...)
}
func (ActionsEnv) Warnf(fmtStr string, args ...interface{}) {
fmt.Printf("::warning::"+fmtStr+"\n", args...)
}

func SetupEnv() (*ActionsEnv, error) {
func setupEnv() (*ActionsEnv, error) {
if os.Getenv("GITHUB_ACTIONS") != "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.")
}

payloadPath := os.Getenv("GITHUB_EVENT_PATH")
if payloadPath == "" {
return nil, fmt.Errorf("no payload path set, something weird is up")
// Get owner and repository
ownerAndRepo := strings.Split(os.Getenv("GITHUB_REPOSITORY"), "/")

// Get event path
eventPath := os.Getenv("GITHUB_EVENT_PATH")
if eventPath == "" {
return nil, fmt.Errorf("no event path set, something weird is up")
}

payload, err := func() (github.PullRequestEvent, error) {
payloadRaw, err := os.Open(payloadPath)
// Parse the event
event, err := func() (github.PullRequestEvent, error) {
eventFile, err := os.Open(eventPath)
if err != nil {
return github.PullRequestEvent{}, fmt.Errorf("unable to load payload file: %w", err)
return github.PullRequestEvent{}, fmt.Errorf("unable to load event file: %w", err)
}
defer payloadRaw.Close()
var payload github.PullRequestEvent
if err := json.NewDecoder(payloadRaw).Decode(&payload); err != nil {
return payload, fmt.Errorf("unable to unmarshal payload: %w", err)
defer eventFile.Close()

var event github.PullRequestEvent
if err := json.NewDecoder(eventFile).Decode(&event); err != nil {
return event, fmt.Errorf("unable to unmarshal event: %w", err)
}
return payload, nil
return event, nil
}()
if err != nil {
return nil, err
}

ctx := context.Background()
authClient := oauth2.NewClient(ctx, oauth2.StaticTokenSource(
// Create the client
client := github.NewClient(oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: os.Getenv("INPUT_GITHUB_TOKEN")},
))
)))

return &ActionsEnv{
Event: &payload,
Client: github.NewClient(authClient),
Owner: ownerAndRepo[0],
Repo: ownerAndRepo[1],
Event: &event,
Client: client,
}, nil
}

type ActionsCallback func(*ActionsEnv) error

func ActionsEntrypoint(cb ActionsCallback) {
env, err := SetupEnv()
env, err := setupEnv()
if err != nil {
env.Errorf("%v", err)
log.errorf("%v", err)
os.Exit(1)
}

if err := cb(env); err != nil {
env.Errorf("%v", err)
log.errorf("%v", err)
os.Exit(2)
}
fmt.Println("Success!")
Expand All @@ -189,7 +107,7 @@ func RunPlugins(plugins ...PRPlugin) ActionsCallback {
done.Add(1)
go func(plugin PRPlugin) {
defer done.Done()
res <- plugin.Entrypoint(env)
res <- plugin.entrypoint(env)
}(plugin)
}

Expand All @@ -204,7 +122,7 @@ func RunPlugins(plugins ...PRPlugin) ActionsCallback {
continue
}
errCount++
env.Errorf("%v", err)
log.errorf("%v", err)
}

fmt.Printf("%d plugins ran\n", len(plugins))
Expand Down
35 changes: 35 additions & 0 deletions verify/logger.go
@@ -0,0 +1,35 @@
/*
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"
)

type logger struct{}

func (logger) errorf(format string, args ...interface{}) {
fmt.Printf("::error::"+format+"\n", args...)
}

func (logger) debugf(format string, args ...interface{}) {
fmt.Printf("::debug::"+format+"\n", args...)
}

func (logger) warningf(format string, args ...interface{}) {
fmt.Printf("::warning::"+format+"\n", args...)
}