diff --git a/action-nightly/action.yml b/.github/actions/verifier/action.yml similarity index 88% rename from action-nightly/action.yml rename to .github/actions/verifier/action.yml index 77cd889..a644b73 100644 --- a/action-nightly/action.yml +++ b/.github/actions/verifier/action.yml @@ -6,4 +6,4 @@ inputs: required: true runs: using: docker - image: '../Dockerfile' + image: '../../../Dockerfile' diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index ae54e85..22ffd27 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -1,16 +1,17 @@ +name: PR Verifier + on: pull_request_target: types: [opened, edited, reopened, synchronize] jobs: verify: + name: Verify PR contents runs-on: ubuntu-latest - name: verify PR contents steps: - - name: Checkout - uses: actions/checkout@v2 - - name: Verifier action - id: verifier - uses: ./action-nightly - with: - github_token: ${{ secrets.GITHUB_TOKEN }} + - name: Checkout + uses: actions/checkout@v2 + - name: Verifier action + uses: ./.github/actions/verifier + with: + github_token: ${{ secrets.GITHUB_TOKEN }} diff --git a/README.md b/README.md index e18994d..c73d2fd 100644 --- a/README.md +++ b/README.md @@ -51,7 +51,8 @@ The code that actually runs lives in [verify/cmd](/verify/cmd), while 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, which lives in [action-nightly](/action-nightly). +from the local code (master branch), which lives in +[.github/actions/verifier](/.github/actions/verifier). ### Updating the action diff --git a/verify/common.go b/verify/common.go index 336af28..1e476f9 100644 --- a/verify/common.go +++ b/verify/common.go @@ -26,9 +26,11 @@ import ( "github.com/google/go-github/v32/github" "golang.org/x/oauth2" + + "sigs.k8s.io/kubebuilder-release-tools/verify/pkg/log" ) -var log logger +var l = log.New() type ActionsEnv struct { Owner string @@ -87,15 +89,13 @@ type ActionsCallback func(*ActionsEnv) error func ActionsEntrypoint(cb ActionsCallback) { env, err := setupEnv() if err != nil { - log.errorf("%v", err) - os.Exit(1) + l.Fatalf(1, "%v", err) } if err := cb(env); err != nil { - log.errorf("%v", err) - os.Exit(2) + l.Fatalf(2, "%v", err) } - fmt.Println("Success!") + l.Info("Success!") } func RunPlugins(plugins ...PRPlugin) ActionsCallback { @@ -107,6 +107,7 @@ func RunPlugins(plugins ...PRPlugin) ActionsCallback { done.Add(1) go func(plugin PRPlugin) { defer done.Done() + plugin.init() res <- plugin.entrypoint(env) }(plugin) } @@ -122,10 +123,10 @@ func RunPlugins(plugins ...PRPlugin) ActionsCallback { continue } errCount++ - log.errorf("%v", err) + l.Errorf("%v", err) } - fmt.Printf("%d plugins ran\n", len(plugins)) + l.Infof("%d plugins ran", len(plugins)) if errCount > 0 { return fmt.Errorf("%d plugins had errors", errCount) } diff --git a/verify/pkg/log/interface.go b/verify/pkg/log/interface.go new file mode 100644 index 0000000..144dbcc --- /dev/null +++ b/verify/pkg/log/interface.go @@ -0,0 +1,30 @@ +/* +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 log + +type Logger interface { + Debug(content string) + Debugf(format string, args ...interface{}) + Info(content string) + Infof(format string, args ...interface{}) + Warning(content string) + Warningf(format string, args ...interface{}) + Error(content string) + Errorf(format string, args ...interface{}) + Fatal(exitCode int, content string) + Fatalf(exitCode int, format string, args ...interface{}) +} diff --git a/verify/logger.go b/verify/pkg/log/level.go similarity index 60% rename from verify/logger.go rename to verify/pkg/log/level.go index 0d4a497..eb2870a 100644 --- a/verify/logger.go +++ b/verify/pkg/log/level.go @@ -14,22 +14,28 @@ See the License for the specific language governing permissions and limitations under the License. */ -package verify +package log -import ( - "fmt" -) - -type logger struct{} - -func (logger) errorf(format string, args ...interface{}) { - fmt.Printf("::error::"+format+"\n", args...) -} +type LoggingLevel uint8 -func (logger) debugf(format string, args ...interface{}) { - fmt.Printf("::debug::"+format+"\n", args...) -} +const ( + Debug = iota + Info + Warning + Error +) -func (logger) warningf(format string, args ...interface{}) { - fmt.Printf("::warning::"+format+"\n", args...) +func (level LoggingLevel) String() string { + switch level { + case Debug: + return "debug" + case Info: + return "info" + case Warning: + return "warning" + case Error: + return "error" + default: + return "unknown logging level" + } } diff --git a/verify/pkg/log/logger.go b/verify/pkg/log/logger.go new file mode 100644 index 0000000..996d775 --- /dev/null +++ b/verify/pkg/log/logger.go @@ -0,0 +1,125 @@ +/* +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 log + +import ( + "fmt" + "os" + "strings" +) + +// Prefixes used in GitHub actions +const ( + ghPrefixDebug = "::debug::" + ghPrefixWarning = "::warning::" + ghPrefixError = "::error::" +) + +// Verify that logger implements Logger +var _ Logger = logger{} + +// logger provides logging functions for GitHub actions +type logger struct{ + // Prefixes for the different logging methods + prefixDebug string + prefixInfo string + prefixWarning string + prefixError string +} + +// New returns a basic logger for GitHub actions +func New() Logger { + return &logger{ + prefixDebug: ghPrefixDebug, + prefixInfo: "", + prefixWarning: ghPrefixWarning, + prefixError: ghPrefixError, + } +} + +// NewFor returns a named logger for GitHub actions +func NewFor(name string) Logger { + return &logger{ + prefixDebug: fmt.Sprintf("%s[%s]", ghPrefixDebug, name), + prefixInfo: fmt.Sprintf("[%s]", name), + prefixWarning: fmt.Sprintf("%s[%s]", ghPrefixWarning, name), + prefixError: fmt.Sprintf("%s[%s]", ghPrefixError, name), + } +} + +func (l logger) prefixFor(level LoggingLevel) string { + switch level { + case Debug: + return l.prefixDebug + case Info: + return l.prefixInfo + case Warning: + return l.prefixWarning + case Error: + return l.prefixError + default: + panic("invalid logging level") + } +} + +func (l logger) log(content string, level LoggingLevel) { + prefix := l.prefixFor(level) + for _, s := range strings.Split(content, "\n") { + fmt.Println(prefix + s) + } +} + +func (l logger) Debug(content string) { + l.log(content, Debug) +} + +func (l logger) Debugf(format string, args ...interface{}) { + l.Debug(fmt.Sprintf(format, args...)) +} + +func (l logger) Info(content string) { + l.log(content, Info) +} + +func (l logger) Infof(format string, args ...interface{}) { + l.Info(fmt.Sprintf(format, args...)) +} + +func (l logger) Warning(content string) { + l.log(content, Warning) +} + +func (l logger) Warningf(format string, args ...interface{}) { + l.Warning(fmt.Sprintf(format, args...)) +} + +func (l logger) Error(content string) { + l.log(content, Error) +} + +func (l logger) Errorf(format string, args ...interface{}) { + l.Error(fmt.Sprintf(format, args...)) +} + +func (l logger) Fatal(exitCode int, content string) { + l.log(content, Error) + os.Exit(exitCode) +} + +func (l logger) Fatalf(exitCode int, format string, args ...interface{}) { + l.Fatal(exitCode, fmt.Sprintf(format, args...)) +} diff --git a/verify/plugin.go b/verify/plugin.go index ce5813d..eaa85e7 100644 --- a/verify/plugin.go +++ b/verify/plugin.go @@ -23,6 +23,8 @@ import ( "time" "github.com/google/go-github/v32/github" + + "sigs.k8s.io/kubebuilder-release-tools/verify/pkg/log" ) // ErrorWithHelp allows PRPlugin.ProcessPR to provide extended descriptions @@ -37,7 +39,12 @@ type PRPlugin struct { Name string Title string - logger + log.Logger +} + +// init initializes the PRPlugin +func (p *PRPlugin) init() { + p.Logger = log.NewFor(p.Name) } // processPR executes the provided ProcessPR and parses the result @@ -57,9 +64,9 @@ func (p PRPlugin) processPR(pr *github.PullRequest) (conclusion, summary, text s } // Log in case we can't submit the result for some reason - p.debugf("plugin conclusion: %q", conclusion) - p.debugf("plugin result summary: %q", summary) - p.debugf("plugin result details: %q", text) + p.Debugf("plugin conclusion: %q", conclusion) + p.Debugf("plugin result summary: %q", summary) + p.Debugf("plugin result details: %q", text) return conclusion, summary, text, err } @@ -89,7 +96,7 @@ func (p PRPlugin) processAndSubmit(env *ActionsEnv, checkRun *github.CheckRun) e // 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) + p.Debugf("creating check run %q on %s/%s @ %s...", p.Name, owner, repo, headSHA) checkRun, res, err := client.Checks.CreateCheckRun( context.TODO(), @@ -101,20 +108,20 @@ func (p PRPlugin) createCheckRun(client *github.Client, owner, repo, headSHA str Status: Started.StringP(), }, ) - if err != nil { - return nil, fmt.Errorf("unable to submit check result: %w", err) - } - p.debugf("create check API response: %+v", res) - p.debugf("created run: %+v", checkRun) + p.Debugf("create check API response: %+v", res) + p.Debugf("created run: %+v", checkRun) + if err != nil { + return nil, fmt.Errorf("unable to create check run: %w", err) + } return checkRun, nil } // 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) + p.Debugf("getting check run %q on %s/%s @ %s...", p.Name, owner, repo, headSHA) checkRunList, res, err := client.Checks.ListCheckRunsForRef( context.TODO(), @@ -125,22 +132,25 @@ func (p PRPlugin) getCheckRun(client *github.Client, owner, repo, headSHA string CheckName: github.String(p.Name), }, ) + + p.Debugf("list check API response: %+v", res) + p.Debugf("listed runs: %+v", checkRunList) + if err != nil { - return nil, err + return nil, fmt.Errorf("unable to get check run: %w", err) } - p.debugf("list check API response: %+v", res) - p.debugf("listed runs: %+v", checkRunList) - switch n := *checkRunList.Total; { case n == 0: return p.createCheckRun(client, owner, repo, headSHA) case n == 1: 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) + return nil, fmt.Errorf("multiple instances of `%s` check run found on %s/%s @ %s", + p.Name, 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) + return nil, fmt.Errorf("negative number of instances (%d) of `%s` check run found on %s/%s @ %s", + n, p.Name, owner, repo, headSHA) } } @@ -154,7 +164,7 @@ func (p PRPlugin) resetCheckRun(client *github.Client, owner, repo string, headS return checkRun, err } - p.debugf("updating check run %q on %s/%s...", p.Name, owner, repo) + p.Debugf("resetting check run %q on %s/%s...", p.Name, owner, repo) checkRun, updateResp, err := client.Checks.UpdateCheckRun( context.TODO(), @@ -166,20 +176,20 @@ func (p PRPlugin) resetCheckRun(client *github.Client, owner, repo string, headS Status: github.String("in-progress"), }, ) - if err != nil { - return checkRun, fmt.Errorf("unable to update check result: %w", err) - } - p.debugf("update check API response: %+v", updateResp) - p.debugf("updated run: %+v", checkRun) + p.Debugf("update check API response: %+v", updateResp) + p.Debugf("updated run: %+v", checkRun) + if err != nil { + return checkRun, fmt.Errorf("unable to reset check run: %w", err) + } return checkRun, nil } // 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) error { - p.debugf("updating check run %q on %s/%s...", p.Name, owner, repo) + p.Debugf("adding results to check run %q on %s/%s...", p.Name, owner, repo) checkRun, updateResp, err := client.Checks.UpdateCheckRun(context.TODO(), owner, repo, checkRunID, github.UpdateCheckRunOptions{ Name: p.Name, @@ -191,19 +201,19 @@ func (p PRPlugin) finishCheckRun(client *github.Client, owner, repo string, chec Text: github.String(text), }, }) - if err != nil { - return fmt.Errorf("unable to update check result: %w", err) - } - p.debugf("update check API response: %+v", updateResp) - p.debugf("updated run: %+v", checkRun) + p.Debugf("update check API response: %+v", updateResp) + p.Debugf("updated run: %+v", checkRun) + if err != nil { + return fmt.Errorf("unable to update check run with results: %w", err) + } return nil } // 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("creating check run %q on %s/%s @ %s...", p.Name, owner, repo, headSHA) + p.Debugf("duplicating check run %q on %s/%s @ %s...", p.Name, owner, repo, headSHA) checkRun, res, err := client.Checks.CreateCheckRun( context.TODO(), @@ -221,13 +231,13 @@ func (p PRPlugin) duplicateCheckRun(client *github.Client, owner, repo, headSHA Output: checkRun.Output, }, ) - if err != nil { - return nil, fmt.Errorf("unable to submit check result: %w", err) - } - p.debugf("create check API response: %+v", res) - p.debugf("created run: %+v", checkRun) + p.Debugf("create check API response: %+v", res) + p.Debugf("created run: %+v", checkRun) + if err != nil { + return checkRun, fmt.Errorf("unable to duplicate check run: %w", err) + } return checkRun, nil }