From 890ab3a40f07da6651ab836144f0a1145480cdaa Mon Sep 17 00:00:00 2001
From: Antoine Eddi <5222525+aeddi@users.noreply.github.com>
Date: Mon, 2 Dec 2024 18:57:35 +0100
Subject: [PATCH] ci: add debug on github-bot matrix subcommand + fixes (#3244)
This PR will allow debugging errors of [this
type](https://github.com/gnolang/gno/actions/runs/12072757244) that
unfortunately cannot be tested locally since they rely on the context of
GitHub Actions.
Since I also had to add flags to the matrix subcommand, I moved the two
matrix and check subcommands into subfolders.
This PR also modify the comment to stick to moul's request and fixes
several Github Actions errors.
Related to #3238
Changes:
-
https://github.com/gnolang/gno/pull/3244/commits/d11ad5a08e457921907e3db32b8576921dde8563
moves matrix and check subcommands to their own packages in internal
-
https://github.com/gnolang/gno/pull/3244/commits/462ac01321ff15e34cbe956a7ecc07096e665e28
https://github.com/gnolang/gno/pull/3244/commits/5c1edda51950c74c8bccb7eb8c16c036df3bd1f7
https://github.com/gnolang/gno/pull/3244/commits/ffdce936c39c1ad587f0ed17158f579b4ded067e
adds a debug to matrix subcommand (print event input / matrix output) +
direct output of matrix to GitHub Actions using a matrix-key flag
-
https://github.com/gnolang/gno/pull/3244/commits/6af501d4cd923c122e8ea6791ab58f394e2bbf1f
embed comment template file as a string at compile time instead of
opening it at runtime
-
https://github.com/gnolang/gno/pull/3244/commits/59c3ad6835191cae92dc811de4484b6a6793ea74
modifies bot comment to meet [this
requirements](https://github.com/gnolang/gno/issues/3238#issuecomment-2506520120)
-
https://github.com/gnolang/gno/pull/3244/commits/241a75532ce5e035ac745b4cd66f3bea2d9a420f
filter out from the matrix generation and the PR processing all issues
or closed PRs (process / list only opened PRs)
Contributors' checklist...
- [x] Added new tests, or not needed, or not feasible
- [x] Provided an example (e.g. screenshot) to aid review or the PR is
self-explanatory
- [x] Updated the official documentation or not needed
- [x] No breaking changes were made, or a `BREAKING CHANGE: xxx` message
was included in the description
- [x] Added references to related issues and PRs
- [x] Provided any useful hints for running manual tests
---------
Co-authored-by: Morgan
---
.github/workflows/bot.yml | 4 +-
contribs/github-bot/README.md | 4 +-
contribs/github-bot/comment.tmpl | 51 -------
.../github-bot/{ => internal/check}/check.go | 62 +++-----
.../{params/params.go => check/cmd.go} | 75 ++++++----
.../{ => internal/check}/comment.go | 43 +++---
.../github-bot/internal/check/comment.tmpl | 54 +++++++
.../{ => internal/check}/comment_test.go | 41 ++++--
contribs/github-bot/internal/client/client.go | 52 +++++--
.../{ => internal/config}/config.go | 60 ++++----
contribs/github-bot/internal/matrix/cmd.go | 53 +++++++
contribs/github-bot/internal/matrix/matrix.go | 139 ++++++++++++++++++
.../{ => internal/matrix}/matrix_test.go | 45 +++---
.../internal/requirements/assignee_test.go | 2 +-
.../internal/requirements/branch.go | 2 +-
.../internal/requirements/label_test.go | 2 +-
contribs/github-bot/internal/utils/actions.go | 2 +-
.../github-bot/internal/utils/github_const.go | 6 +-
.../internal/{params => utils}/prlist.go | 3 +-
contribs/github-bot/main.go | 24 ++-
contribs/github-bot/matrix.go | 117 ---------------
21 files changed, 490 insertions(+), 351 deletions(-)
delete mode 100644 contribs/github-bot/comment.tmpl
rename contribs/github-bot/{ => internal/check}/check.go (78%)
rename contribs/github-bot/internal/{params/params.go => check/cmd.go} (56%)
rename contribs/github-bot/{ => internal/check}/comment.go (90%)
create mode 100644 contribs/github-bot/internal/check/comment.tmpl
rename contribs/github-bot/{ => internal/check}/comment_test.go (86%)
rename contribs/github-bot/{ => internal/config}/config.go (54%)
create mode 100644 contribs/github-bot/internal/matrix/cmd.go
create mode 100644 contribs/github-bot/internal/matrix/matrix.go
rename contribs/github-bot/{ => internal/matrix}/matrix_test.go (91%)
rename contribs/github-bot/internal/{params => utils}/prlist.go (91%)
delete mode 100644 contribs/github-bot/matrix.go
diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml
index 21950459ae8..cbfec5730fc 100644
--- a/.github/workflows/bot.yml
+++ b/.github/workflows/bot.yml
@@ -55,13 +55,15 @@ jobs:
working-directory: contribs/github-bot
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- run: echo "pr-numbers=$(go run . matrix)" >> "$GITHUB_OUTPUT"
+ run: go run . matrix -matrix-key 'pr-numbers' -verbose
# This job processes each pull request in the matrix individually while ensuring
# that a same PR cannot be processed concurrently by mutliple runners
process-pr:
name: Process PR
needs: define-prs-matrix
+ # Just skip this job if PR numbers matrix is empty (prevent failed state)
+ if: ${{ needs.define-prs-matrix.outputs.pr-numbers != '[]' && needs.define-prs-matrix.outputs.pr-numbers != '' }}
runs-on: ubuntu-latest
strategy:
matrix:
diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md
index 78c9c3c01b8..7932300cb9d 100644
--- a/contribs/github-bot/README.md
+++ b/contribs/github-bot/README.md
@@ -13,7 +13,7 @@ The bot operates by defining a set of rules that are evaluated against each pull
- **Automatic Checks**: These are rules that the bot evaluates automatically. If a pull request meets the conditions specified in the rule, then the corresponding requirements are executed. For example, ensuring that changes to specific directories are reviewed by specific team members.
- **Manual Checks**: These require human intervention. If a pull request meets the conditions specified in the rule, then a checkbox that can be checked only by specified teams is displayed on the bot comment. For example, determining if infrastructure needs to be updated based on changes to specific files.
-The bot configuration is defined in Go and is located in the file [config.go](./config.go).
+The bot configuration is defined in Go and is located in the file [config.go](./internal/config/config.go).
### GitHub Token
@@ -31,7 +31,7 @@ For the bot to make requests to the GitHub API, it needs a Personal Access Token
USAGE
github-bot check [flags]
-This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.
+This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly.
A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.
FLAGS
diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl
deleted file mode 100644
index ebd07fdd4b9..00000000000
--- a/contribs/github-bot/comment.tmpl
+++ /dev/null
@@ -1,51 +0,0 @@
-# Merge Requirements
-
-The following requirements must be fulfilled before a pull request can be merged.
-Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member.
-
-These requirements are defined in this [configuration file](https://github.com/GnoCheckBot/demo/blob/main/config.go).
-
-## Automated Checks
-
-{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }}
-{{ end }}
-
-{{ if .AutoRules }}Details
-{{ range .AutoRules }}
-{{ .Description | stripLinks }}
-
-### If
-```
-{{ .ConditionDetails | stripLinks }}
-```
-### Then
-```
-{{ .RequirementDetails | stripLinks }}
-```
-
-{{ end }}
-
-{{ else }}*No automated checks match this pull request.*{{ end }}
-
-## Manual Checks
-
-{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }}
-{{ end }}
-
-{{ if .ManualRules }}Details
-{{ range .ManualRules }}
-{{ .Description | stripLinks }}
-
-### If
-```
-{{ .ConditionDetails }}
-```
-### Can be checked by
-{{range $item := .Teams }} - team {{ $item | stripLinks }}
-{{ else }}
-- Any user with comment edit permission
-{{end}}
-
-{{ end }}
-
-{{ else }}*No manual checks match this pull request.*{{ end }}
diff --git a/contribs/github-bot/check.go b/contribs/github-bot/internal/check/check.go
similarity index 78%
rename from contribs/github-bot/check.go
rename to contribs/github-bot/internal/check/check.go
index 8019246d27c..5ca2235e823 100644
--- a/contribs/github-bot/check.go
+++ b/contribs/github-bot/internal/check/check.go
@@ -1,4 +1,4 @@
-package main
+package check
import (
"context"
@@ -9,44 +9,30 @@ import (
"sync/atomic"
"github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/config"
"github.com/gnolang/gno/contribs/github-bot/internal/logger"
- p "github.com/gnolang/gno/contribs/github-bot/internal/params"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
- "github.com/gnolang/gno/tm2/pkg/commands"
"github.com/google/go-github/v64/github"
"github.com/sethvargo/go-githubactions"
"github.com/xlab/treeprint"
)
-func newCheckCmd() *commands.Command {
- params := &p.Params{}
-
- return commands.NewCommand(
- commands.Metadata{
- Name: "check",
- ShortUsage: "github-bot check [flags]",
- ShortHelp: "checks requirements for a pull request to be merged",
- LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
- },
- params,
- func(_ context.Context, _ []string) error {
- params.ValidateFlags()
- return execCheck(params)
- },
- )
-}
-
-func execCheck(params *p.Params) error {
+func execCheck(flags *checkFlags) error {
// Create context with timeout if specified in the parameters.
ctx := context.Background()
- if params.Timeout > 0 {
+ if flags.Timeout > 0 {
var cancel context.CancelFunc
- ctx, cancel = context.WithTimeout(context.Background(), params.Timeout)
+ ctx, cancel = context.WithTimeout(context.Background(), flags.Timeout)
defer cancel()
}
// Init GitHub API client.
- gh, err := client.New(ctx, params)
+ gh, err := client.New(ctx, &client.Config{
+ Owner: flags.Owner,
+ Repo: flags.Repo,
+ Verbose: *flags.Verbose,
+ DryRun: flags.DryRun,
+ })
if err != nil {
return fmt.Errorf("comment update handling failed: %w", err)
}
@@ -69,7 +55,7 @@ func execCheck(params *p.Params) error {
var prs []*github.PullRequest
// If requested, retrieve all open pull requests.
- if params.PRAll {
+ if flags.PRAll {
prs, err = gh.ListPR(utils.PRStateOpen)
if err != nil {
return fmt.Errorf("unable to list all PR: %w", err)
@@ -77,11 +63,11 @@ func execCheck(params *p.Params) error {
} else {
// Otherwise, retrieve only specified pull request(s)
// (flag or GitHub Action context).
- prs = make([]*github.PullRequest, len(params.PRNums))
- for i, prNum := range params.PRNums {
- pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
+ prs = make([]*github.PullRequest, len(flags.PRNums))
+ for i, prNum := range flags.PRNums {
+ pr, err := gh.GetOpenedPullRequest(prNum)
if err != nil {
- return fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
+ return fmt.Errorf("unable to process PR list: %w", err)
}
prs[i] = pr
}
@@ -101,7 +87,7 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
}
// Process all pull requests in parallel.
- autoRules, manualRules := config(gh)
+ autoRules, manualRules := config.Config(gh)
var wg sync.WaitGroup
// Used in dry-run mode to log cleanly from different goroutines.
@@ -122,15 +108,15 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success))
// Check if conditions of this rule are met by this PR.
- if !autoRule.ifC.IsMet(pr, ifDetails) {
+ if !autoRule.If.IsMet(pr, ifDetails) {
continue
}
- c := AutoContent{Description: autoRule.description, Satisfied: false}
+ c := AutoContent{Description: autoRule.Description, Satisfied: false}
thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.Fail))
// Check if requirements of this rule are satisfied by this PR.
- if autoRule.thenR.IsSatisfied(pr, thenDetails) {
+ if autoRule.Then.IsSatisfied(pr, thenDetails) {
thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success))
c.Satisfied = true
} else {
@@ -153,13 +139,13 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success))
// Check if conditions of this rule are met by this PR.
- if !manualRule.ifC.IsMet(pr, ifDetails) {
+ if !manualRule.If.IsMet(pr, ifDetails) {
continue
}
// Get check status from current comment, if any.
checkedBy := ""
- check, ok := checks[manualRule.description]
+ check, ok := checks[manualRule.Description]
if ok {
checkedBy = check.checkedBy
}
@@ -167,10 +153,10 @@ func processPRList(gh *client.GitHub, prs []*github.PullRequest) error {
commentContent.ManualRules = append(
commentContent.ManualRules,
ManualContent{
- Description: manualRule.description,
+ Description: manualRule.Description,
ConditionDetails: ifDetails.String(),
CheckedBy: checkedBy,
- Teams: manualRule.teams,
+ Teams: manualRule.Teams,
},
)
diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/check/cmd.go
similarity index 56%
rename from contribs/github-bot/internal/params/params.go
rename to contribs/github-bot/internal/check/cmd.go
index c11d1b62419..7ea6c02795b 100644
--- a/contribs/github-bot/internal/params/params.go
+++ b/contribs/github-bot/internal/check/cmd.go
@@ -1,118 +1,131 @@
-package params
+package check
import (
+ "context"
"flag"
"fmt"
"os"
"time"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/gnolang/gno/tm2/pkg/commands"
"github.com/sethvargo/go-githubactions"
)
-type Params struct {
+type checkFlags struct {
Owner string
Repo string
PRAll bool
- PRNums PRList
- Verbose bool
+ PRNums utils.PRList
+ Verbose *bool
DryRun bool
Timeout time.Duration
flagSet *flag.FlagSet
}
-func (p *Params) RegisterFlags(fs *flag.FlagSet) {
+func NewCheckCmd(verbose *bool) *commands.Command {
+ flags := &checkFlags{Verbose: verbose}
+
+ return commands.NewCommand(
+ commands.Metadata{
+ Name: "check",
+ ShortUsage: "github-bot check [flags]",
+ ShortHelp: "checks requirements for a pull request to be merged",
+ LongHelp: "This tool checks if the requirements for a pull request to be merged are satisfied (defined in ./internal/config/config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
+ },
+ flags,
+ func(_ context.Context, _ []string) error {
+ flags.validateFlags()
+ return execCheck(flags)
+ },
+ )
+}
+
+func (flags *checkFlags) RegisterFlags(fs *flag.FlagSet) {
fs.StringVar(
- &p.Owner,
+ &flags.Owner,
"owner",
"",
"owner of the repo to process, if empty, will be retrieved from GitHub Actions context",
)
fs.StringVar(
- &p.Repo,
+ &flags.Repo,
"repo",
"",
"repo to process, if empty, will be retrieved from GitHub Actions context",
)
fs.BoolVar(
- &p.PRAll,
+ &flags.PRAll,
"pr-all",
false,
"process all opened pull requests",
)
fs.TextVar(
- &p.PRNums,
+ &flags.PRNums,
"pr-numbers",
- PRList(nil),
+ utils.PRList(nil),
"pull request(s) to process, must be a comma separated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrieved from GitHub Actions context",
)
fs.BoolVar(
- &p.Verbose,
- "verbose",
- false,
- "set logging level to debug",
- )
-
- fs.BoolVar(
- &p.DryRun,
+ &flags.DryRun,
"dry-run",
false,
"print if pull request requirements are satisfied without updating anything on GitHub",
)
fs.DurationVar(
- &p.Timeout,
+ &flags.Timeout,
"timeout",
0,
"timeout after which the bot execution is interrupted",
)
- p.flagSet = fs
+ flags.flagSet = fs
}
-func (p *Params) ValidateFlags() {
+func (flags *checkFlags) validateFlags() {
// Helper to display an error + usage message before exiting.
errorUsage := func(err string) {
- fmt.Fprintf(p.flagSet.Output(), "Error: %s\n\n", err)
- p.flagSet.Usage()
+ fmt.Fprintf(flags.flagSet.Output(), "Error: %s\n\n", err)
+ flags.flagSet.Usage()
os.Exit(1)
}
// Check if flags are coherent.
- if p.PRAll && len(p.PRNums) != 0 {
+ if flags.PRAll && len(flags.PRNums) != 0 {
errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags.")
}
// If one of these values is empty, it must be retrieved
// from GitHub Actions context.
- if p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) {
+ if flags.Owner == "" || flags.Repo == "" || (len(flags.PRNums) == 0 && !flags.PRAll) {
actionCtx, err := githubactions.Context()
if err != nil {
errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v.", err))
}
- if p.Owner == "" {
- if p.Owner, _ = actionCtx.Repo(); p.Owner == "" {
+ if flags.Owner == "" {
+ if flags.Owner, _ = actionCtx.Repo(); flags.Owner == "" {
errorUsage("Unable to retrieve owner from GitHub Actions context, you may want to set it using -onwer flag.")
}
}
- if p.Repo == "" {
- if _, p.Repo = actionCtx.Repo(); p.Repo == "" {
+ if flags.Repo == "" {
+ if _, flags.Repo = actionCtx.Repo(); flags.Repo == "" {
errorUsage("Unable to retrieve repo from GitHub Actions context, you may want to set it using -repo flag.")
}
}
- if len(p.PRNums) == 0 && !p.PRAll {
+ if len(flags.PRNums) == 0 && !flags.PRAll {
prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
if err != nil {
errorUsage(fmt.Sprintf("Unable to retrieve pull request number from GitHub Actions context: %s\nYou may want to set it using -pr-numbers flag.", err.Error()))
}
- p.PRNums = PRList{prNum}
+ flags.PRNums = utils.PRList{prNum}
}
}
}
diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/internal/check/comment.go
similarity index 90%
rename from contribs/github-bot/comment.go
rename to contribs/github-bot/internal/check/comment.go
index f6605ea8554..434df8f9e76 100644
--- a/contribs/github-bot/comment.go
+++ b/contribs/github-bot/internal/check/comment.go
@@ -1,7 +1,8 @@
-package main
+package check
import (
"bytes"
+ _ "embed"
"errors"
"fmt"
"regexp"
@@ -9,12 +10,15 @@ import (
"text/template"
"github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/config"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
-
"github.com/google/go-github/v64/github"
"github.com/sethvargo/go-githubactions"
)
+//go:embed comment.tmpl
+var tmplString string // Embed template used for comment generation.
+
var errTriggeredByBot = errors.New("event triggered by bot")
// Compile regex only once.
@@ -95,6 +99,18 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
return nil
}
+ // Get PR number from GitHub Actions context.
+ prNumFloat, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64)
+ if !ok || prNumFloat <= 0 {
+ return errors.New("unable to get issue number on issue comment event")
+ }
+ prNum := int(prNumFloat)
+
+ // Ignore if this comment update is not related to an opened PR.
+ if _, err := gh.GetOpenedPullRequest(prNum); err != nil {
+ return nil // May come from an issue or a closed PR
+ }
+
// Return if comment was edited by bot (current authenticated user).
authUser, _, err := gh.Client.Users.Get(gh.Ctx, "")
if err != nil {
@@ -129,17 +145,11 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
return errors.New("unable to get changes body content on issue comment event")
}
- // Get PR number from GitHub Actions context.
- prNum, ok := utils.IndexMap(actionCtx.Event, "issue", "number").(float64)
- if !ok || prNum <= 0 {
- return errors.New("unable to get issue number on issue comment event")
- }
-
// Check if change is only a checkbox being checked or unckecked.
if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") {
// If not, restore previous comment body.
if !gh.DryRun {
- gh.SetBotComment(previous, int(prNum))
+ gh.SetBotComment(previous, prNum)
}
return errors.New("bot comment edited outside of checkboxes")
}
@@ -157,12 +167,12 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// Get teams allowed to edit this box from config.
var teams []string
found := false
- _, manualRules := config(gh)
+ _, manualRules := config.Config(gh)
for _, manualRule := range manualRules {
- if manualRule.description == key {
+ if manualRule.Description == key {
found = true
- teams = manualRule.teams
+ teams = manualRule.Teams
}
}
@@ -175,9 +185,9 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// If teams specified in rule, check if actor is a member of one of them.
if len(teams) > 0 {
- if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed
+ if !gh.IsUserInTeams(actionCtx.Actor, teams) { // If user not allowed to check the boxes.
if !gh.DryRun {
- gh.SetBotComment(previous, int(prNum)) // Restore previous state
+ gh.SetBotComment(previous, prNum) // Then restore previous state.
}
return errors.New("checkbox edited by a user not allowed to")
}
@@ -199,7 +209,7 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte
// Update comment with username.
if edited != "" && !gh.DryRun {
- gh.SetBotComment(edited, int(prNum))
+ gh.SetBotComment(edited, prNum)
gh.Logger.Debugf("Comment manual checks updated successfully")
}
@@ -217,8 +227,7 @@ func generateComment(content CommentContent) (string, error) {
}
// Bind markdown stripping function to template generator.
- const tmplFile = "comment.tmpl"
- tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile)
+ tmpl, err := template.New("comment").Funcs(funcMap).Parse(tmplString)
if err != nil {
return "", fmt.Errorf("unable to init template: %w", err)
}
diff --git a/contribs/github-bot/internal/check/comment.tmpl b/contribs/github-bot/internal/check/comment.tmpl
new file mode 100644
index 00000000000..4312019dd2e
--- /dev/null
+++ b/contribs/github-bot/internal/check/comment.tmpl
@@ -0,0 +1,54 @@
+I'm a bot that assists the Gno Core team in maintaining this repository. My role is to ensure that contributors understand and follow our guidelines, helping to streamline the development process.
+
+The following requirements must be fulfilled before a pull request can be merged.
+Some requirement checks are automated and can be verified by the CI, while others need manual verification by a staff member.
+
+These requirements are defined in this [configuration file](https://github.com/gnolang/gno/tree/master/contribs/github-bot/internal/config/config.go).
+
+## Automated Checks
+
+{{ if .AutoRules }}{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }}
+{{ end }}{{ else }}*No automated checks match this pull request.*{{ end }}
+
+## Manual Checks
+
+{{ if .ManualRules }}{{ range .ManualRules }}- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }}{{ if .CheckedBy }} (checked by @{{ .CheckedBy }}){{ end }}
+{{ end }}{{ else }}*No manual checks match this pull request.*{{ end }}
+
+{{ if or .AutoRules .ManualRules }}Debug
+{{ if .AutoRules }}Automated Checks
+{{ range .AutoRules }}
+{{ .Description | stripLinks }}
+
+### If
+```
+{{ .ConditionDetails | stripLinks }}
+```
+### Then
+```
+{{ .RequirementDetails | stripLinks }}
+```
+
+{{ end }}
+
+{{ end }}
+
+{{ if .ManualRules }}Manual Checks
+{{ range .ManualRules }}
+{{ .Description | stripLinks }}
+
+### If
+```
+{{ .ConditionDetails }}
+```
+### Can be checked by
+{{range $item := .Teams }} - team {{ $item | stripLinks }}
+{{ else }}
+- Any user with comment edit permission
+{{end}}
+
+{{ end }}
+
+{{ end }}
+
+{{ end }}
diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/internal/check/comment_test.go
similarity index 86%
rename from contribs/github-bot/comment_test.go
rename to contribs/github-bot/internal/check/comment_test.go
index fd8790dd9e1..0334b76f95c 100644
--- a/contribs/github-bot/comment_test.go
+++ b/contribs/github-bot/internal/check/comment_test.go
@@ -1,4 +1,4 @@
-package main
+package check
import (
"context"
@@ -108,19 +108,34 @@ func TestCommentUpdateHandler(t *testing.T) {
}
gh := newGHClient()
- // Exit without error because EventName is empty
+ // Exit without error because EventName is empty.
assert.NoError(t, handleCommentUpdate(gh, actionCtx))
actionCtx.EventName = utils.EventIssueComment
- // Exit with error because Event.action is not set
+ // Exit with error because Event.action is not set.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event["action"] = ""
- // Exit without error because Event.action is set but not 'deleted'
+ // Exit without error because Event.action is set but not 'deleted'.
assert.NoError(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event["action"] = "deleted"
- // Exit with error because mock not setup to return authUser
+ // Exit with error because Event.issue.number is not set.
+ assert.Error(t, handleCommentUpdate(gh, actionCtx))
+ actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number")
+
+ // Exit without error can't get open pull request associated with PR num.
+ assert.NoError(t, handleCommentUpdate(gh, actionCtx))
+ mockOptions = append(mockOptions, mock.WithRequestMatchPages(
+ mock.EndpointPattern{
+ Pattern: "/repos/pulls/42",
+ Method: "GET",
+ },
+ github.PullRequest{Number: github.Int(42), State: github.String(utils.PRStateOpen)},
+ ))
+ gh = newGHClient()
+
+ // Exit with error because mock not setup to return authUser.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
mockOptions = append(mockOptions, mock.WithRequestMatchPages(
mock.EndpointPattern{
@@ -132,31 +147,27 @@ func TestCommentUpdateHandler(t *testing.T) {
gh = newGHClient()
actionCtx.Actor = bot
- // Exit with error because authUser and action actor is the same user
+ // Exit with error because authUser and action actor is the same user.
assert.ErrorIs(t, handleCommentUpdate(gh, actionCtx), errTriggeredByBot)
actionCtx.Actor = user
- // Exit with error because Event.comment.user.login is not set
+ // Exit with error because Event.comment.user.login is not set.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, user, "comment", "user", "login")
- // Exit without error because comment author is not the bot
+ // Exit without error because comment author is not the bot.
assert.NoError(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, bot, "comment", "user", "login")
- // Exit with error because Event.comment.body is not set
+ // Exit with error because Event.comment.body is not set.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "comment", "body")
- // Exit with error because Event.changes.body.from is not set
+ // Exit with error because Event.changes.body.from is not set.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, "updated_body", "changes", "body", "from")
- // Exit with error because Event.issue.number is not set
- assert.Error(t, handleCommentUpdate(gh, actionCtx))
- actionCtx.Event = setValue(t, actionCtx.Event, float64(42), "issue", "number")
-
- // Exit with error because checkboxes are differents
+ // Exit with error because checkboxes are differents.
assert.Error(t, handleCommentUpdate(gh, actionCtx))
actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "changes", "body", "from")
diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go
index 474146ad3da..a5c875e0d22 100644
--- a/contribs/github-bot/internal/client/client.go
+++ b/contribs/github-bot/internal/client/client.go
@@ -7,8 +7,7 @@ import (
"os"
"github.com/gnolang/gno/contribs/github-bot/internal/logger"
- p "github.com/gnolang/gno/contribs/github-bot/internal/params"
-
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
"github.com/google/go-github/v64/github"
)
@@ -32,21 +31,28 @@ type GitHub struct {
Repo string
}
+type Config struct {
+ Owner string
+ Repo string
+ Verbose bool
+ DryRun bool
+}
+
// GetBotComment retrieves the bot's (current user) comment on provided PR number.
func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) {
- // List existing comments
+ // List existing comments.
const (
sort = "created"
direction = "desc"
)
- // Get current user (bot)
+ // Get current user (bot).
currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "")
if err != nil {
return nil, fmt.Errorf("unable to get current user: %w", err)
}
- // Pagination option
+ // Pagination option.
opts := &github.IssueListCommentsOptions{
Sort: github.String(sort),
Direction: github.String(direction),
@@ -67,7 +73,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) {
return nil, fmt.Errorf("unable to list comments for PR %d: %w", prNum, err)
}
- // Get the comment created by current user
+ // Get the comment created by current user.
for _, comment := range comments {
if comment.GetUser().GetLogin() == currentUser.GetLogin() {
return comment, nil
@@ -86,7 +92,12 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) {
// SetBotComment creates a bot's comment on the provided PR number
// or updates it if it already exists.
func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, error) {
- // Create bot comment if it does not already exist
+ // Prevent updating anything in dry run mode.
+ if gh.DryRun {
+ return nil, errors.New("should not write bot comment in dry run mode")
+ }
+
+ // Create bot comment if it does not already exist.
comment, err := gh.GetBotComment(prNum)
if errors.Is(err, ErrBotCommentNotFound) {
newComment, _, err := gh.Client.Issues.CreateComment(
@@ -119,6 +130,17 @@ func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, e
return editComment, nil
}
+func (gh *GitHub) GetOpenedPullRequest(prNum int) (*github.PullRequest, error) {
+ pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
+ if err != nil {
+ return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
+ } else if pr.GetState() != utils.PRStateOpen {
+ return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState())
+ }
+
+ return pr, nil
+}
+
// ListTeamMembers lists the members of the specified team.
func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) {
var (
@@ -268,25 +290,25 @@ func (gh *GitHub) ListPR(state string) ([]*github.PullRequest, error) {
}
// New initializes the API client, the logger, and creates an instance of GitHub.
-func New(ctx context.Context, params *p.Params) (*GitHub, error) {
+func New(ctx context.Context, cfg *Config) (*GitHub, error) {
gh := &GitHub{
Ctx: ctx,
- Owner: params.Owner,
- Repo: params.Repo,
- DryRun: params.DryRun,
+ Owner: cfg.Owner,
+ Repo: cfg.Repo,
+ DryRun: cfg.DryRun,
}
// Detect if the current process was launched by a GitHub Action and return
- // a logger suitable for terminal output or the GitHub Actions web interface
- gh.Logger = logger.NewLogger(params.Verbose)
+ // a logger suitable for terminal output or the GitHub Actions web interface.
+ gh.Logger = logger.NewLogger(cfg.Verbose)
- // Retrieve GitHub API token from env
+ // Retrieve GitHub API token from env.
token, set := os.LookupEnv("GITHUB_TOKEN")
if !set {
return nil, errors.New("GITHUB_TOKEN is not set in env")
}
- // Init GitHub API client using token
+ // Init GitHub API client using token.
gh.Client = github.NewClient(nil).WithAuthToken(token)
return gh, nil
diff --git a/contribs/github-bot/config.go b/contribs/github-bot/internal/config/config.go
similarity index 54%
rename from contribs/github-bot/config.go
rename to contribs/github-bot/internal/config/config.go
index 4a28565ef7f..ac1d185f759 100644
--- a/contribs/github-bot/config.go
+++ b/contribs/github-bot/internal/config/config.go
@@ -1,4 +1,4 @@
-package main
+package config
import (
"github.com/gnolang/gno/contribs/github-bot/internal/client"
@@ -9,37 +9,37 @@ import (
type Teams []string
// Automatic check that will be performed by the bot.
-type automaticCheck struct {
- description string
- ifC c.Condition // If the condition is met, the rule is displayed and the requirement is executed.
- thenR r.Requirement // If the requirement is satisfied, the check passes.
+type AutomaticCheck struct {
+ Description string
+ If c.Condition // If the condition is met, the rule is displayed and the requirement is executed.
+ Then r.Requirement // If the requirement is satisfied, the check passes.
}
// Manual check that will be performed by users.
-type manualCheck struct {
- description string
- ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment.
- teams Teams // Members of these teams can check the checkbox to make the check pass.
+type ManualCheck struct {
+ Description string
+ If c.Condition // If the condition is met, a checkbox will be displayed on bot comment.
+ Teams Teams // Members of these teams can check the checkbox to make the check pass.
}
// This function returns the configuration of the bot consisting of automatic and manual checks
// in which the GitHub client is injected.
-func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) {
- auto := []automaticCheck{
+func Config(gh *client.GitHub) ([]AutomaticCheck, []ManualCheck) {
+ auto := []AutomaticCheck{
{
- description: "Maintainers must be able to edit this pull request",
- ifC: c.Always(),
- thenR: r.MaintainerCanModify(),
+ Description: "Maintainers must be able to edit this pull request",
+ If: c.Always(),
+ Then: r.MaintainerCanModify(),
},
{
- description: "The pull request head branch must be up-to-date with its base",
- ifC: c.Always(),
- thenR: r.UpToDateWith(gh, r.PR_BASE),
+ Description: "The pull request head branch must be up-to-date with its base",
+ If: c.Always(),
+ Then: r.UpToDateWith(gh, r.PR_BASE),
},
{
- description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff",
- ifC: c.FileChanged(gh, "^docs/"),
- thenR: r.Or(
+ Description: "Changes to 'docs' folder must be reviewed/authored by at least one devrel and one tech-staff",
+ If: c.FileChanged(gh, "^docs/"),
+ Then: r.Or(
r.And(
r.AuthorInTeam(gh, "devrels"),
r.ReviewByTeamMembers(gh, "tech-staff", 1),
@@ -52,15 +52,15 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) {
},
}
- manual := []manualCheck{
+ manual := []ManualCheck{
{
- description: "The pull request description provides enough details",
- ifC: c.Not(c.AuthorInTeam(gh, "core-contributors")),
- teams: Teams{"core-contributors"},
+ Description: "The pull request description provides enough details",
+ If: c.Not(c.AuthorInTeam(gh, "core-contributors")),
+ Teams: Teams{"core-contributors"},
},
{
- description: "Determine if infra needs to be updated before merging",
- ifC: c.And(
+ Description: "Determine if infra needs to be updated before merging",
+ If: c.And(
c.BaseBranch("master"),
c.Or(
c.FileChanged(gh, `Dockerfile`),
@@ -70,17 +70,17 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) {
c.FileChanged(gh, `^.github/workflows/portal-loop\.yml$`),
),
),
- teams: Teams{"devops"},
+ Teams: Teams{"devops"},
},
}
// Check for duplicates in manual rule descriptions (needs to be unique for the bot operations).
unique := make(map[string]struct{})
for _, rule := range manual {
- if _, exists := unique[rule.description]; exists {
- gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.description)
+ if _, exists := unique[rule.Description]; exists {
+ gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.Description)
}
- unique[rule.description] = struct{}{}
+ unique[rule.Description] = struct{}{}
}
return auto, manual
diff --git a/contribs/github-bot/internal/matrix/cmd.go b/contribs/github-bot/internal/matrix/cmd.go
new file mode 100644
index 00000000000..8bcc3a34424
--- /dev/null
+++ b/contribs/github-bot/internal/matrix/cmd.go
@@ -0,0 +1,53 @@
+package matrix
+
+import (
+ "context"
+ "flag"
+ "fmt"
+ "os"
+
+ "github.com/gnolang/gno/tm2/pkg/commands"
+)
+
+type matrixFlags struct {
+ verbose *bool
+ matrixKey string
+ flagSet *flag.FlagSet
+}
+
+func NewMatrixCmd(verbose *bool) *commands.Command {
+ flags := &matrixFlags{verbose: verbose}
+
+ return commands.NewCommand(
+ commands.Metadata{
+ Name: "matrix",
+ ShortUsage: "github-bot matrix [flags]",
+ ShortHelp: "parses GitHub Actions event and defines matrix accordingly",
+ LongHelp: "This tool retrieves the GitHub Actions context, parses the attached event, and defines the matrix with the pull request numbers to be processed accordingly",
+ },
+ flags,
+ func(_ context.Context, _ []string) error {
+ flags.validateFlags()
+ return execMatrix(flags)
+ },
+ )
+}
+
+func (flags *matrixFlags) RegisterFlags(fs *flag.FlagSet) {
+ fs.StringVar(
+ &flags.matrixKey,
+ "matrix-key",
+ "",
+ "key of the matrix to set in Github Actions output (required)",
+ )
+
+ flags.flagSet = fs
+}
+
+func (flags *matrixFlags) validateFlags() {
+ if flags.matrixKey == "" {
+ fmt.Fprintf(flags.flagSet.Output(), "Error: no matrix-key provided\n\n")
+ flags.flagSet.Usage()
+ os.Exit(1)
+ }
+}
diff --git a/contribs/github-bot/internal/matrix/matrix.go b/contribs/github-bot/internal/matrix/matrix.go
new file mode 100644
index 00000000000..9c8f12e4214
--- /dev/null
+++ b/contribs/github-bot/internal/matrix/matrix.go
@@ -0,0 +1,139 @@
+package matrix
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "os"
+ "strings"
+
+ "github.com/gnolang/gno/contribs/github-bot/internal/client"
+ "github.com/gnolang/gno/contribs/github-bot/internal/utils"
+ "github.com/sethvargo/go-githubactions"
+)
+
+func execMatrix(flags *matrixFlags) error {
+ // Get GitHub Actions context to retrieve event.
+ actionCtx, err := githubactions.Context()
+ if err != nil {
+ return fmt.Errorf("unable to get GitHub Actions context: %w", err)
+ }
+
+ // If verbose is set, print the Github Actions event for debugging purpose.
+ if *flags.verbose {
+ jsonBytes, err := json.MarshalIndent(actionCtx.Event, "", " ")
+ if err != nil {
+ return fmt.Errorf("unable to marshal event to json: %w", err)
+ }
+ fmt.Println("Event:", string(jsonBytes))
+ }
+
+ // Init Github client using only GitHub Actions context.
+ owner, repo := actionCtx.Repo()
+ gh, err := client.New(context.Background(), &client.Config{
+ Owner: owner,
+ Repo: repo,
+ Verbose: *flags.verbose,
+ DryRun: true,
+ })
+ if err != nil {
+ return fmt.Errorf("unable to init GitHub client: %w", err)
+ }
+
+ // Retrieve PR list from GitHub Actions event.
+ prList, err := getPRListFromEvent(gh, actionCtx)
+ if err != nil {
+ return err
+ }
+
+ // Format PR list for GitHub Actions matrix definition.
+ bytes, err := prList.MarshalText()
+ if err != nil {
+ return fmt.Errorf("unable to marshal PR list: %w", err)
+ }
+ matrix := fmt.Sprintf("%s=[%s]", flags.matrixKey, string(bytes))
+
+ // If verbose is set, print the matrix for debugging purpose.
+ if *flags.verbose {
+ fmt.Printf("Matrix: %s\n", matrix)
+ }
+
+ // Get the path of the GitHub Actions environment file used for output.
+ output, ok := os.LookupEnv("GITHUB_OUTPUT")
+ if !ok {
+ return errors.New("unable to get GITHUB_OUTPUT var")
+ }
+
+ // Open GitHub Actions output file
+ file, err := os.OpenFile(output, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
+ if err != nil {
+ return fmt.Errorf("unable to open GitHub Actions output file: %w", err)
+ }
+ defer file.Close()
+
+ // Append matrix to GitHub Actions output file
+ if _, err := fmt.Fprintf(file, "%s\n", matrix); err != nil {
+ return fmt.Errorf("unable to write matrix in GitHub Actions output file: %w", err)
+ }
+
+ return nil
+}
+
+func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (utils.PRList, error) {
+ var prList utils.PRList
+
+ switch actionCtx.EventName {
+ // Event triggered from GitHub Actions user interface.
+ case utils.EventWorkflowDispatch:
+ // Get input entered by the user.
+ rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string)
+ if !ok {
+ return nil, errors.New("unable to get workflow dispatch input")
+ }
+ input := strings.TrimSpace(rawInput)
+
+ // If all PR are requested, list them from GitHub API.
+ if input == "all" {
+ prs, err := gh.ListPR(utils.PRStateOpen)
+ if err != nil {
+ return nil, fmt.Errorf("unable to list all PR: %w", err)
+ }
+
+ prList = make(utils.PRList, len(prs))
+ for i := range prs {
+ prList[i] = prs[i].GetNumber()
+ }
+ } else {
+ // If a PR list is provided, parse it.
+ if err := prList.UnmarshalText([]byte(input)); err != nil {
+ return nil, fmt.Errorf("invalid PR list provided as input: %w", err)
+ }
+ }
+
+ // Event triggered by an issue / PR comment being created / edited / deleted
+ // or any update on a PR.
+ case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget:
+ // For these events, retrieve the number of the associated PR from the context.
+ prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
+ if err != nil {
+ return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err)
+ }
+ prList = utils.PRList{prNum}
+
+ default:
+ return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName)
+ }
+
+ // Then only keep provided PR that are opened.
+ var openedPRList utils.PRList = nil
+ for _, prNum := range prList {
+ if _, err := gh.GetOpenedPullRequest(prNum); err != nil {
+ gh.Logger.Warningf("Can't get PR from event: %v", err)
+ } else {
+ openedPRList = append(openedPRList, prNum)
+ }
+ }
+
+ return openedPRList, nil
+}
diff --git a/contribs/github-bot/matrix_test.go b/contribs/github-bot/internal/matrix/matrix_test.go
similarity index 91%
rename from contribs/github-bot/matrix_test.go
rename to contribs/github-bot/internal/matrix/matrix_test.go
index bce4ec1bd8f..fe5b7452a49 100644
--- a/contribs/github-bot/matrix_test.go
+++ b/contribs/github-bot/internal/matrix/matrix_test.go
@@ -1,4 +1,4 @@
-package main
+package matrix
import (
"context"
@@ -9,7 +9,6 @@ import (
"github.com/gnolang/gno/contribs/github-bot/internal/client"
"github.com/gnolang/gno/contribs/github-bot/internal/logger"
- "github.com/gnolang/gno/contribs/github-bot/internal/params"
"github.com/gnolang/gno/contribs/github-bot/internal/utils"
"github.com/google/go-github/v64/github"
"github.com/migueleliasweb/go-github-mock/src/mock"
@@ -34,7 +33,7 @@ func TestProcessEvent(t *testing.T) {
name string
gaCtx *githubactions.GitHubContext
prs []*github.PullRequest
- expectedPRList params.PRList
+ expectedPRList utils.PRList
expectedError bool
}{
{
@@ -44,7 +43,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"issue": map[string]any{"number": 1.}},
},
prs,
- params.PRList{1},
+ utils.PRList{1},
false,
}, {
"valid pull_request event",
@@ -53,7 +52,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"pull_request": map[string]any{"number": 1.}},
},
prs,
- params.PRList{1},
+ utils.PRList{1},
false,
}, {
"valid pull_request_target event",
@@ -62,7 +61,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"pull_request": map[string]any{"number": 1.}},
},
prs,
- params.PRList{1},
+ utils.PRList{1},
false,
}, {
"invalid event (PR number not set)",
@@ -71,7 +70,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"issue": nil},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"invalid event name",
@@ -80,7 +79,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"issue": map[string]any{"number": 1.}},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"valid workflow_dispatch all",
@@ -89,7 +88,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}},
},
openPRs,
- params.PRList{1, 2, 3},
+ utils.PRList{1, 2, 3},
false,
}, {
"valid workflow_dispatch all (no prs)",
@@ -98,7 +97,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}},
},
nil,
- params.PRList{},
+ utils.PRList(nil),
false,
}, {
"valid workflow_dispatch list",
@@ -107,7 +106,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3"}},
},
prs,
- params.PRList{1, 2, 3},
+ utils.PRList{1, 2, 3},
false,
}, {
"valid workflow_dispatch list with spaces",
@@ -116,7 +115,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": " 1, 2 ,3 "}},
},
prs,
- params.PRList{1, 2, 3},
+ utils.PRList{1, 2, 3},
false,
}, {
"invalid workflow_dispatch list (1 closed)",
@@ -125,8 +124,8 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3,4"}},
},
prs,
- params.PRList(nil),
- true,
+ utils.PRList{1, 2, 3},
+ false,
}, {
"invalid workflow_dispatch list (1 doesn't exist)",
&githubactions.GitHubContext{
@@ -134,8 +133,8 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "42"}},
},
prs,
- params.PRList(nil),
- true,
+ utils.PRList(nil),
+ false,
}, {
"invalid workflow_dispatch list (all closed)",
&githubactions.GitHubContext{
@@ -143,8 +142,8 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "4,5,6"}},
},
prs,
- params.PRList(nil),
- true,
+ utils.PRList(nil),
+ false,
}, {
"invalid workflow_dispatch list (empty)",
&githubactions.GitHubContext{
@@ -152,7 +151,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": ""}},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"invalid workflow_dispatch list (unset)",
@@ -161,7 +160,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": ""},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"invalid workflow_dispatch list (not a number list)",
@@ -170,7 +169,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "foo"}},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
}, {
"invalid workflow_dispatch list (number list with invalid elem)",
@@ -179,7 +178,7 @@ func TestProcessEvent(t *testing.T) {
Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,foo"}},
},
prs,
- params.PRList(nil),
+ utils.PRList(nil),
true,
},
} {
@@ -214,7 +213,7 @@ func TestProcessEvent(t *testing.T) {
prNumStr := parts[len(parts)-1]
prNum, err = strconv.Atoi(prNumStr)
if err != nil {
- panic(err) // Should never happen
+ panic(err) // Should never happen.
}
}
diff --git a/contribs/github-bot/internal/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go
index d72e8ad2a19..aa86fb0054d 100644
--- a/contribs/github-bot/internal/requirements/assignee_test.go
+++ b/contribs/github-bot/internal/requirements/assignee_test.go
@@ -45,7 +45,7 @@ func TestAssignee(t *testing.T) {
mock.WithRequestMatchHandler(
mock.EndpointPattern{
Pattern: "/repos/issues/0/assignees",
- Method: "GET", // It looks like this mock package doesn't support mocking POST requests
+ Method: "GET", // It looks like this mock package doesn't support mocking POST requests.
},
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
requested = true
diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go
index b686a093015..6481285ae82 100644
--- a/contribs/github-bot/internal/requirements/branch.go
+++ b/contribs/github-bot/internal/requirements/branch.go
@@ -29,7 +29,7 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre
}
head := pr.GetHead().GetRef()
- // If pull request is open from a fork, prepend head ref with fork owner login
+ // If pull request is open from a fork, prepend head ref with fork owner login.
if pr.GetHead().GetRepo().GetFullName() != pr.GetBase().GetRepo().GetFullName() {
head = fmt.Sprintf("%s:%s", pr.GetHead().GetRepo().GetOwner().GetLogin(), pr.GetHead().GetRef())
}
diff --git a/contribs/github-bot/internal/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go
index 7e991b55756..631bff9e64b 100644
--- a/contribs/github-bot/internal/requirements/label_test.go
+++ b/contribs/github-bot/internal/requirements/label_test.go
@@ -45,7 +45,7 @@ func TestLabel(t *testing.T) {
mock.WithRequestMatchHandler(
mock.EndpointPattern{
Pattern: "/repos/issues/0/labels",
- Method: "GET", // It looks like this mock package doesn't support mocking POST requests
+ Method: "GET", // It looks like this mock package doesn't support mocking POST requests.
},
http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) {
requested = true
diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go
index 91b8ac7e6b4..3e08a8e1548 100644
--- a/contribs/github-bot/internal/utils/actions.go
+++ b/contribs/github-bot/internal/utils/actions.go
@@ -23,7 +23,7 @@ func IndexMap(m map[string]any, keys ...string) any {
return nil
}
-// Retrieve PR number from GitHub Actions context
+// Retrieve PR number from GitHub Actions context.
func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) {
firstKey := ""
diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go
index 564b7d3fb38..26d7d54d477 100644
--- a/contribs/github-bot/internal/utils/github_const.go
+++ b/contribs/github-bot/internal/utils/github_const.go
@@ -1,14 +1,14 @@
package utils
-// GitHub const
+// GitHub API const.
const (
- // GitHub Actions Event Names
+ // GitHub Actions Event Names.
EventIssueComment = "issue_comment"
EventPullRequest = "pull_request"
EventPullRequestTarget = "pull_request_target"
EventWorkflowDispatch = "workflow_dispatch"
- // Pull Request States
+ // Pull Request States.
PRStateOpen = "open"
PRStateClosed = "closed"
)
diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/utils/prlist.go
similarity index 91%
rename from contribs/github-bot/internal/params/prlist.go
rename to contribs/github-bot/internal/utils/prlist.go
index ace7bcbe3b6..2893bf802b5 100644
--- a/contribs/github-bot/internal/params/prlist.go
+++ b/contribs/github-bot/internal/utils/prlist.go
@@ -1,4 +1,4 @@
-package params
+package utils
import (
"encoding"
@@ -7,6 +7,7 @@ import (
"strings"
)
+// Type used to (un)marshal input/output for check and matrix subcommands.
type PRList []int
// PRList is both a TextMarshaler and a TextUnmarshaler.
diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go
index 9895f44dc70..e11fe6ffd78 100644
--- a/contribs/github-bot/main.go
+++ b/contribs/github-bot/main.go
@@ -2,25 +2,43 @@ package main
import (
"context"
+ "flag"
"os"
+ "github.com/gnolang/gno/contribs/github-bot/internal/check"
+ "github.com/gnolang/gno/contribs/github-bot/internal/matrix"
"github.com/gnolang/gno/tm2/pkg/commands"
)
+type rootFlags struct {
+ verbose bool
+}
+
func main() {
+ flags := &rootFlags{}
+
cmd := commands.NewCommand(
commands.Metadata{
ShortUsage: "github-bot [flags]",
LongHelp: "Bot that allows for advanced management of GitHub pull requests.",
},
- commands.NewEmptyConfig(),
+ flags,
commands.HelpExec,
)
cmd.AddSubCommands(
- newCheckCmd(),
- newMatrixCmd(),
+ check.NewCheckCmd(&flags.verbose),
+ matrix.NewMatrixCmd(&flags.verbose),
)
cmd.Execute(context.Background(), os.Args[1:])
}
+
+func (flags *rootFlags) RegisterFlags(fs *flag.FlagSet) {
+ fs.BoolVar(
+ &flags.verbose,
+ "verbose",
+ false,
+ "set logging level to debug",
+ )
+}
diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go
deleted file mode 100644
index 56d6667589a..00000000000
--- a/contribs/github-bot/matrix.go
+++ /dev/null
@@ -1,117 +0,0 @@
-package main
-
-import (
- "context"
- "errors"
- "fmt"
- "strings"
-
- "github.com/gnolang/gno/contribs/github-bot/internal/client"
- "github.com/gnolang/gno/contribs/github-bot/internal/params"
- "github.com/gnolang/gno/contribs/github-bot/internal/utils"
- "github.com/gnolang/gno/tm2/pkg/commands"
- "github.com/sethvargo/go-githubactions"
-)
-
-func newMatrixCmd() *commands.Command {
- return commands.NewCommand(
- commands.Metadata{
- Name: "matrix",
- ShortUsage: "github-bot matrix",
- ShortHelp: "parses GitHub Actions event and defines matrix accordingly",
- LongHelp: "This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\nA valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.",
- },
- commands.NewEmptyConfig(),
- func(_ context.Context, _ []string) error {
- return execMatrix()
- },
- )
-}
-
-func execMatrix() error {
- // Get GitHub Actions context to retrieve event.
- actionCtx, err := githubactions.Context()
- if err != nil {
- return fmt.Errorf("unable to get GitHub Actions context: %w", err)
- }
-
- // Init Github client using only GitHub Actions context
- owner, repo := actionCtx.Repo()
- gh, err := client.New(context.Background(), ¶ms.Params{Owner: owner, Repo: repo})
- if err != nil {
- return fmt.Errorf("unable to init GitHub client: %w", err)
- }
-
- // Retrieve PR list from GitHub Actions event
- prList, err := getPRListFromEvent(gh, actionCtx)
- if err != nil {
- return err
- }
-
- // Print PR list for GitHub Actions matrix definition
- bytes, err := prList.MarshalText()
- if err != nil {
- return fmt.Errorf("unable to marshal PR list: %w", err)
- }
- fmt.Printf("[%s]", string(bytes))
-
- return nil
-}
-
-func getPRListFromEvent(gh *client.GitHub, actionCtx *githubactions.GitHubContext) (params.PRList, error) {
- var prList params.PRList
-
- switch actionCtx.EventName {
- // Event triggered from GitHub Actions user interface
- case utils.EventWorkflowDispatch:
- // Get input entered by the user
- rawInput, ok := utils.IndexMap(actionCtx.Event, "inputs", "pull-request-list").(string)
- if !ok {
- return nil, errors.New("unable to get workflow dispatch input")
- }
- input := strings.TrimSpace(rawInput)
-
- // If all PR are requested, list them from GitHub API
- if input == "all" {
- prs, err := gh.ListPR(utils.PRStateOpen)
- if err != nil {
- return nil, fmt.Errorf("unable to list all PR: %w", err)
- }
-
- prList = make(params.PRList, len(prs))
- for i := range prs {
- prList[i] = prs[i].GetNumber()
- }
- } else {
- // If a PR list is provided, parse it
- if err := prList.UnmarshalText([]byte(input)); err != nil {
- return nil, fmt.Errorf("invalid PR list provided as input: %w", err)
- }
-
- // Then check if all provided PR are opened
- for _, prNum := range prList {
- pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum)
- if err != nil {
- return nil, fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err)
- } else if pr.GetState() != utils.PRStateOpen {
- return nil, fmt.Errorf("pull request %d is not opened, actual state: %s", prNum, pr.GetState())
- }
- }
- }
-
- // Event triggered by an issue / PR comment being created / edited / deleted
- // or any update on a PR
- case utils.EventIssueComment, utils.EventPullRequest, utils.EventPullRequestTarget:
- // For these events, retrieve the number of the associated PR from the context
- prNum, err := utils.GetPRNumFromActionsCtx(actionCtx)
- if err != nil {
- return nil, fmt.Errorf("unable to retrieve PR number from GitHub Actions context: %w", err)
- }
- prList = params.PRList{prNum}
-
- default:
- return nil, fmt.Errorf("unsupported event type: %s", actionCtx.EventName)
- }
-
- return prList, nil
-}