From a009eac9e10e7f1bd170de43d403dd8ecfaf60ce Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 28 Oct 2024 10:58:21 +0100 Subject: [PATCH 01/44] feat: add github bot --- .github/workflows/bot.yml | 104 +++++++ contribs/github_bot/client/client.go | 247 +++++++++++++++ contribs/github_bot/comment.go | 283 ++++++++++++++++++ contribs/github_bot/comment.tmpl | 51 ++++ contribs/github_bot/condition/assignee.go | 59 ++++ contribs/github_bot/condition/author.go | 53 ++++ contribs/github_bot/condition/boolean.go | 100 +++++++ contribs/github_bot/condition/branch.go | 48 +++ contribs/github_bot/condition/condition.go | 12 + contribs/github_bot/condition/constant.go | 34 +++ contribs/github_bot/condition/file.go | 57 ++++ contribs/github_bot/condition/label.go | 33 ++ contribs/github_bot/config.go | 97 ++++++ contribs/github_bot/go.mod | 11 + contribs/github_bot/go.sum | 22 ++ contribs/github_bot/logger/action.go | 43 +++ contribs/github_bot/logger/logger.go | 34 +++ contribs/github_bot/logger/terminal.go | 55 ++++ contribs/github_bot/main.go | 117 ++++++++ contribs/github_bot/param/param.go | 109 +++++++ contribs/github_bot/param/prlist.go | 45 +++ contribs/github_bot/requirement/assignee.go | 52 ++++ contribs/github_bot/requirement/author.go | 53 ++++ contribs/github_bot/requirement/boolean.go | 100 +++++++ contribs/github_bot/requirement/branch.go | 52 ++++ contribs/github_bot/requirement/label.go | 52 ++++ contribs/github_bot/requirement/maintainer.go | 25 ++ .../github_bot/requirement/requirement.go | 12 + contribs/github_bot/requirement/reviewer.go | 130 ++++++++ contribs/github_bot/utils/tree.go | 17 ++ misc/github-bot/client/client.go | 235 +++++++++++++++ misc/github-bot/comment.go | 20 ++ misc/github-bot/comment.tmpl | 21 ++ misc/github-bot/condition/assignee.go | 64 ++++ misc/github-bot/condition/author.go | 57 ++++ misc/github-bot/condition/boolean.go | 100 +++++++ misc/github-bot/condition/branch.go | 50 ++++ misc/github-bot/condition/condition.go | 13 + misc/github-bot/condition/constant.go | 43 +++ misc/github-bot/condition/file.go | 61 ++++ misc/github-bot/condition/label.go | 34 +++ misc/github-bot/config.go | 59 ++++ misc/github-bot/go.mod | 10 + misc/github-bot/go.sum | 10 + misc/github-bot/logger/action.go | 43 +++ misc/github-bot/logger/logger.go | 34 +++ misc/github-bot/logger/terminal.go | 55 ++++ misc/github-bot/main.go | 112 +++++++ misc/github-bot/param/param.go | 92 ++++++ misc/github-bot/param/prlist.go | 45 +++ misc/github-bot/requirement/assignee.go | 47 +++ misc/github-bot/requirement/author.go | 14 + misc/github-bot/requirement/boolean.go | 100 +++++++ misc/github-bot/requirement/checkbox.go | 29 ++ misc/github-bot/requirement/label.go | 47 +++ misc/github-bot/requirement/maintainer.go | 24 ++ misc/github-bot/requirement/requirement.go | 13 + misc/github-bot/requirement/reviewer.go | 139 +++++++++ 58 files changed, 3678 insertions(+) create mode 100644 .github/workflows/bot.yml create mode 100644 contribs/github_bot/client/client.go create mode 100644 contribs/github_bot/comment.go create mode 100644 contribs/github_bot/comment.tmpl create mode 100644 contribs/github_bot/condition/assignee.go create mode 100644 contribs/github_bot/condition/author.go create mode 100644 contribs/github_bot/condition/boolean.go create mode 100644 contribs/github_bot/condition/branch.go create mode 100644 contribs/github_bot/condition/condition.go create mode 100644 contribs/github_bot/condition/constant.go create mode 100644 contribs/github_bot/condition/file.go create mode 100644 contribs/github_bot/condition/label.go create mode 100644 contribs/github_bot/config.go create mode 100644 contribs/github_bot/go.mod create mode 100644 contribs/github_bot/go.sum create mode 100644 contribs/github_bot/logger/action.go create mode 100644 contribs/github_bot/logger/logger.go create mode 100644 contribs/github_bot/logger/terminal.go create mode 100644 contribs/github_bot/main.go create mode 100644 contribs/github_bot/param/param.go create mode 100644 contribs/github_bot/param/prlist.go create mode 100644 contribs/github_bot/requirement/assignee.go create mode 100644 contribs/github_bot/requirement/author.go create mode 100644 contribs/github_bot/requirement/boolean.go create mode 100644 contribs/github_bot/requirement/branch.go create mode 100644 contribs/github_bot/requirement/label.go create mode 100644 contribs/github_bot/requirement/maintainer.go create mode 100644 contribs/github_bot/requirement/requirement.go create mode 100644 contribs/github_bot/requirement/reviewer.go create mode 100644 contribs/github_bot/utils/tree.go create mode 100644 misc/github-bot/client/client.go create mode 100644 misc/github-bot/comment.go create mode 100644 misc/github-bot/comment.tmpl create mode 100644 misc/github-bot/condition/assignee.go create mode 100644 misc/github-bot/condition/author.go create mode 100644 misc/github-bot/condition/boolean.go create mode 100644 misc/github-bot/condition/branch.go create mode 100644 misc/github-bot/condition/condition.go create mode 100644 misc/github-bot/condition/constant.go create mode 100644 misc/github-bot/condition/file.go create mode 100644 misc/github-bot/condition/label.go create mode 100644 misc/github-bot/config.go create mode 100644 misc/github-bot/go.mod create mode 100644 misc/github-bot/go.sum create mode 100644 misc/github-bot/logger/action.go create mode 100644 misc/github-bot/logger/logger.go create mode 100644 misc/github-bot/logger/terminal.go create mode 100644 misc/github-bot/main.go create mode 100644 misc/github-bot/param/param.go create mode 100644 misc/github-bot/param/prlist.go create mode 100644 misc/github-bot/requirement/assignee.go create mode 100644 misc/github-bot/requirement/author.go create mode 100644 misc/github-bot/requirement/boolean.go create mode 100644 misc/github-bot/requirement/checkbox.go create mode 100644 misc/github-bot/requirement/label.go create mode 100644 misc/github-bot/requirement/maintainer.go create mode 100644 misc/github-bot/requirement/requirement.go create mode 100644 misc/github-bot/requirement/reviewer.go diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml new file mode 100644 index 00000000000..f51cecd9269 --- /dev/null +++ b/.github/workflows/bot.yml @@ -0,0 +1,104 @@ +name: GitHub Bot + +on: + # Watch for changes on PR state, assignees, labels and head branch + pull_request: + types: + - assigned + - unassigned + - labeled + - unlabeled + - opened + - reopened + - synchronize # PR head updated + + # Watch for changes on PR comment + issue_comment: + types: [created, edited, deleted] + + # Manual run from GitHub Actions interface + workflow_dispatch: + inputs: + pull-request-list: + description: "PR(s) to process : specify 'all' or a comma separated list of PR numbers, e.g. '42,1337,7890'" + required: true + default: all + type: string + +jobs: + # This job creates a matrix of PR numbers based on the inputs from the various + # events that can trigger this workflow so that the process-pr job below can + # handle the parallel processing of the pull-requests + define-prs-matrix: + name: Define PRs matrix + # Prevent bot from retriggering itself + if: ${{ github.actor != vars.GH_BOT_LOGIN }} + runs-on: ubuntu-latest + permissions: + pull-requests: read + outputs: + pr-numbers: ${{ steps.pr-numbers.outputs.pr-numbers }} + + steps: + - name: Parse event inputs + id: pr-numbers + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Triggered by a workflow dispatch event + if [ '${{ github.event_name }}' = 'workflow_dispatch' ]; then + # If the input is 'all', create a matrix with every open PRs + if [ '${{ inputs.pull-request-list }}' = 'all' ]; then + pr_list=`gh pr list --state 'open' --repo '${{ github.repository }}' --json 'number' --template '{{$first := true}}{{range .}}{{if $first}}{{$first = false}}{{else}}, {{end}}{{"\""}}{{.number}}{{"\""}}{{end}}'` + [ -z "$pr_list" ] && echo 'Error : no opened PR found' >&2 && exit 1 + echo "pr-numbers=[$pr_list]" >> "$GITHUB_OUTPUT" + # If the input is not 'all', test for each number in the comma separated + # list if the associated PR is opened, then add it to the matrix + else + pr_list_raw='${{ inputs.pull-request-list }}' + pr_list='' + IFS=',' + for number in $pr_list; do + trimed=`echo "$number" | xargs` + pr_state=`gh pr view "$trimed" --repo '${{ github.repository }}' --json 'state' --template '{{.state}}' 2> /dev/null` + [ "$pr_state" != 'OPEN' ] && echo "Error : PR with number <$trimed> is not opened" >&2 && exit 1 + done + echo "pr-numbers=[$pr_list]" >> "$GITHUB_OUTPUT" + fi + # Triggered by comment event, just add the associated PR number to the matrix + elif [ '${{ github.event_name }}' = 'issue_comment' ]; then + echo 'pr-numbers=["${{ github.event.issue.number }}"]' >> "$GITHUB_OUTPUT" + # Triggered by pull request event, just add the associated PR number to the matrix + elif [ '${{ github.event_name }}' = 'pull_request' ]; then + echo 'pr-numbers=["${{ github.event.pull_request.number }}"]' >> "$GITHUB_OUTPUT" + else + echo 'Error : unknown event ${{ github.event_name }}' >&2 && exit 1 + fi + + # 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 + runs-on: ubuntu-latest + strategy: + matrix: + # Run one job for each PR to process + pr-number: ${{ fromJSON(needs.define-prs-matrix.outputs.pr-numbers) }} + concurrency: + # Prevent running concurrent jobs for a given PR number + group: ${{ matrix.pr-number }} + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Install Go + uses: actions/setup-go@v5 + with: + go-version-file: go.mod + + - name: Run GitHub Bot + env: + GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT }} + run: go run . -pr-numbers '${{ matrix.pr-number }}' -verbose diff --git a/contribs/github_bot/client/client.go b/contribs/github_bot/client/client.go new file mode 100644 index 00000000000..51ff3bdba72 --- /dev/null +++ b/contribs/github_bot/client/client.go @@ -0,0 +1,247 @@ +package client + +import ( + "bot/logger" + "bot/param" + "context" + "log" + "os" + "time" + + "github.com/google/go-github/v66/github" +) + +const PageSize = 100 + +type GitHub struct { + Client *github.Client + Ctx context.Context + DryRun bool + Logger logger.Logger + Owner string + Repo string +} + +func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { + // List existing comments + var ( + allComments []*github.IssueComment + sort = "created" + direction = "desc" + opts = &github.IssueListCommentsOptions{ + Sort: &sort, + Direction: &direction, + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + ) + + for { + comments, response, err := gh.Client.Issues.ListComments( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + gh.Logger.Errorf("Unable to list comments for PR %d : %v", prNum, err) + return nil + } + + allComments = append(allComments, comments...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + // Get current user (bot) + currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") + if err != nil { + gh.Logger.Errorf("Unable to get current user : %v", err) + return nil + } + + // Get the comment created by current user + for _, comment := range allComments { + if comment.GetUser().GetLogin() == currentUser.GetLogin() { + return comment + } + } + + return nil +} + +func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { + // Create bot comment if it not already exists + if comment := gh.GetBotComment(prNum); comment == nil { + newComment, _, err := gh.Client.Issues.CreateComment( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + &github.IssueComment{Body: &body}, + ) + if err != nil { + gh.Logger.Errorf("Unable to create bot comment for PR %d : %v", prNum, err) + return nil + } + return newComment + } else { + comment.Body = &body + editComment, _, err := gh.Client.Issues.EditComment( + gh.Ctx, + gh.Owner, + gh.Repo, + comment.GetID(), + comment, + ) + if err != nil { + gh.Logger.Errorf("Unable to edit bot comment with ID %d : %v", comment.GetID(), err) + return nil + } + return editComment + } +} + +func (gh *GitHub) ListTeamMembers(team string) []*github.User { + var ( + allMembers []*github.User + opts = &github.TeamListTeamMembersOptions{ + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + ) + + for { + members, response, err := gh.Client.Teams.ListTeamMembersBySlug( + gh.Ctx, + gh.Owner, + team, + opts, + ) + if err != nil { + gh.Logger.Errorf("Unable to list members for team %s : %v", team, err) + return nil + } + + allMembers = append(allMembers, members...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allMembers +} + +func (gh *GitHub) IsUserInTeams(user string, teams []string) bool { + for _, team := range teams { + for _, member := range gh.ListTeamMembers(team) { + if member.GetLogin() == user { + return true + } + } + } + + return false +} + +func (gh *GitHub) ListPrReviewers(prNum int) *github.Reviewers { + var ( + allReviewers = &github.Reviewers{} + opts = &github.ListOptions{ + PerPage: PageSize, + } + ) + + for { + reviewers, response, err := gh.Client.PullRequests.ListReviewers( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + gh.Logger.Errorf("Unable to list reviewers for PR %d : %v", prNum, err) + return nil + } + + allReviewers.Teams = append(allReviewers.Teams, reviewers.Teams...) + allReviewers.Users = append(allReviewers.Users, reviewers.Users...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allReviewers +} + +func (gh *GitHub) ListPrReviews(prNum int) []*github.PullRequestReview { + var ( + allReviews []*github.PullRequestReview + opts = &github.ListOptions{ + PerPage: PageSize, + } + ) + + for { + reviews, response, err := gh.Client.PullRequests.ListReviews( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + gh.Logger.Errorf("Unable to list reviews for PR %d : %v", prNum, err) + return nil + } + + allReviews = append(allReviews, reviews...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allReviews +} + +func New(params param.Params) *GitHub { + gh := &GitHub{ + Owner: params.Owner, + Repo: params.Repo, + DryRun: params.DryRun, + } + + // This method will detect if the current process was launched by + // a GitHub Action or not and will accordingly return a logger suitable for + // the terminal output or for the GitHub Actions web interface + gh.Logger = logger.NewLogger(params.Verbose) + + // Create context with timeout if specified in flags + if params.Timeout > 0 { + gh.Ctx, _ = context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Millisecond) + } else { + gh.Ctx = context.Background() + } + + // Init GitHub API Client using token from env + token, set := os.LookupEnv("GITHUB_TOKEN") + if !set { + log.Fatalf("GITHUB_TOKEN is not set in env") + } + gh.Client = github.NewClient(nil).WithAuthToken(token) + + return gh +} diff --git a/contribs/github_bot/comment.go b/contribs/github_bot/comment.go new file mode 100644 index 00000000000..e61b82637a2 --- /dev/null +++ b/contribs/github_bot/comment.go @@ -0,0 +1,283 @@ +package main + +import ( + "bot/client" + "bytes" + "fmt" + "os" + "regexp" + "strings" + "text/template" + + "github.com/google/go-github/v66/github" + "github.com/sethvargo/go-githubactions" +) + +type AutoContent struct { + Description string + Satisfied bool + ConditionDetails string + RequirementDetails string +} +type ManualContent struct { + Description string + ConditionDetails string + CheckedBy string + Teams []string +} + +type CommentContent struct { + AutoRules []AutoContent + ManualRules []ManualContent +} + +// getCommentManualChecks parses the bot comment to get both the checkbox status, +// the check description and the username who checked it +func getCommentManualChecks(commentBody string) map[string][2]string { + checks := make(map[string][2]string) + + reg := regexp.MustCompile(`(?m:^- \[([ x])\] (.+)?$)`) + subReg := regexp.MustCompile(`(?m:(.+) \(checked by @(\w+)\)$)`) + matches := reg.FindAllStringSubmatch(commentBody, -1) + + for _, match := range matches { + if subMatches := subReg.FindAllStringSubmatch(match[2], -1); len(subMatches) > 0 { + checks[subMatches[0][1]] = [2]string{match[1], subMatches[0][2]} + } else { + checks[match[2]] = [2]string{match[1]} + } + } + + return checks +} + +// handleCommentUpdate checks if : +// - the current run was triggered by GitHub Actions +// - the triggering event is an edit of the bot comment +// - the comment was not edited by the bot itself (prevent infinite loop) +// - the comment change is only a checkbox being checked or unckecked (or restore it) +// - the actor / comment editor has permission to modify this checkbox (or restore it) +func handleCommentUpdate(gh *client.GitHub) { + // Get GitHub Actions context to retrieve comment update + actionCtx, err := githubactions.Context() + if err != nil { + gh.Logger.Debugf("Unable to retrieve GitHub Actions context : %v", err) + return + } + + // Ignore if it's not an comment related event + if actionCtx.EventName != "issue_comment" { + gh.Logger.Debugf("Event is not issue comment related : %s", actionCtx.EventName) + return + } + + // Ignore if action type is not deleted or edited + actionType, ok := actionCtx.Event["action"].(string) + if !ok { + gh.Logger.Errorf("Unable to get type on issue comment event") + os.Exit(1) + } + + if actionType != "deleted" && actionType != "edited" { + return + } + + // Exit if comment was edited by bot (current authenticated user) + authUser, _, err := gh.Client.Users.Get(gh.Ctx, "") + if err != nil { + gh.Logger.Errorf("Unable to get authenticated user : %v", err) + os.Exit(1) + } + + if actionCtx.Actor == authUser.GetLogin() { + gh.Logger.Debugf("Prevent infinite loop if the bot comment was edited by the bot itself") + os.Exit(0) + } + + // Ignore if edited comment author is not the bot + comment, ok := actionCtx.Event["comment"].(map[string]any) + if !ok { + gh.Logger.Errorf("Unable to get comment on issue comment event") + os.Exit(1) + } + + author, ok := comment["user"].(map[string]any) + if !ok { + gh.Logger.Errorf("Unable to get comment user on issue comment event") + os.Exit(1) + } + + login, ok := author["login"].(string) + if !ok { + gh.Logger.Errorf("Unable to get comment user login on issue comment event") + os.Exit(1) + } + + if login != authUser.GetLogin() { + return + } + + // Get comment current body + current, ok := comment["body"].(string) + if !ok { + gh.Logger.Errorf("Unable to get comment body on issue comment event") + os.Exit(1) + } + + // Get comment updated body + changes, ok := actionCtx.Event["changes"].(map[string]any) + if !ok { + gh.Logger.Errorf("Unable to get changes on issue comment event") + os.Exit(1) + } + + changesBody, ok := changes["body"].(map[string]any) + if !ok { + gh.Logger.Errorf("Unable to get changes body on issue comment event") + os.Exit(1) + } + + previous, ok := changesBody["from"].(string) + if !ok { + gh.Logger.Errorf("Unable to get changes body content on issue comment event") + os.Exit(1) + } + + // Get PR number from GitHub Actions context + issue, ok := actionCtx.Event["issue"].(map[string]any) + if !ok { + gh.Logger.Errorf("Unable to get issue on issue comment event") + os.Exit(1) + } + + num, ok := issue["number"].(float64) + if !ok || num <= 0 { + gh.Logger.Errorf("Unable to get issue number on issue comment event") + os.Exit(1) + } + + // Check if change is only a checkbox being checked or unckecked + checkboxes := regexp.MustCompile(`(?m:^- \[[ x]\])`) + if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") { + // If not, restore previous comment body + gh.Logger.Errorf("Bot comment edited outside of checkboxes") + gh.SetBotComment(previous, int(num)) + os.Exit(1) + } + + // Check if actor / comment editor has permission to modify changed boxes + currentChecks := getCommentManualChecks(current) + previousChecks := getCommentManualChecks(previous) + edited := "" + for key := range currentChecks { + if currentChecks[key][0] != previousChecks[key][0] { + // Get teams allowed to edit this box from config + var teams []string + found := false + _, manualRules := config(gh) + + for _, manualRule := range manualRules { + if manualRule.Description == key { + found = true + teams = manualRule.Teams + } + } + + // If rule were not found, return to reprocess the bot comment entirely + // (maybe bot config was updated since last run?) + if !found { + gh.Logger.Debugf("Updated rule not found in config : %s", key) + return + } + + // 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) { + gh.Logger.Errorf("Checkbox edited by a user not allowed to") + gh.SetBotComment(previous, int(num)) + os.Exit(1) + } + } + + // If box was checked + reg := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key][0], key)) + if strings.TrimSpace(currentChecks[key][0]) == "x" { + replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key][0], key, actionCtx.Actor) + edited = reg.ReplaceAllString(current, replacement) + } else { + replacement := fmt.Sprintf("- [%s] %s", currentChecks[key][0], key) + edited = reg.ReplaceAllString(current, replacement) + } + } + } + + // Update comment with username + if edited != "" { + gh.SetBotComment(edited, int(num)) + gh.Logger.Debugf("Comment manual checks updated successfuly") + } +} + +func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentContent) { + // Create bot comment using template file + const tmplFile = "comment.tmpl" + tmpl, err := template.New(tmplFile).ParseFiles(tmplFile) + if err != nil { + panic(err) + } + + var commentBytes bytes.Buffer + if err := tmpl.Execute(&commentBytes, content); err != nil { + panic(err) + } + + // Create commit status + var ( + comment = gh.SetBotComment(commentBytes.String(), pr.GetNumber()) + context = "Merge Requirements" + targetURL = comment.GetHTMLURL() + state = "pending" + description = "Some requirements are not satisfied yet. See bot comment." + allSatisfied = true + ) + + // Check if every requirements are satisfied + for _, auto := range content.AutoRules { + if !auto.Satisfied { + gh.Logger.Infof("AUTO NOT Satisfied", auto.Description) + allSatisfied = false + break + } + } + + if allSatisfied { + for _, manual := range content.ManualRules { + if manual.CheckedBy == "" { + gh.Logger.Infof("AUTO NOT Satisfied", manual.Description) + allSatisfied = false + break + } + } + } + + if allSatisfied { + state = "success" + description = "All requirements are satisfied." + } + + gh.Logger.Infof("STATUS", state, description) + + if _, _, err := gh.Client.Repositories.CreateStatus( + gh.Ctx, + gh.Owner, + gh.Repo, + pr.GetHead().GetSHA(), + &github.RepoStatus{ + Context: &context, + State: &state, + TargetURL: &targetURL, + Description: &description, + }); err != nil { + gh.Logger.Errorf("Unable to create status on PR %d : %v", pr.GetNumber(), err) + } +} diff --git a/contribs/github_bot/comment.tmpl b/contribs/github_bot/comment.tmpl new file mode 100644 index 00000000000..a1f94dab75d --- /dev/null +++ b/contribs/github_bot/comment.tmpl @@ -0,0 +1,51 @@ +# 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/gnolang/gno/tree/master/contribs/github-bot/config.go). + +## Automated Checks + +{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🟠{{ end }} {{ .Description }} +{{ end }} + +{{ if .AutoRules }}
Details
+{{ range .AutoRules }} +
{{ .Description }}
+ +### If : +``` +{{ .ConditionDetails }} +``` +### Then : +``` +{{ .RequirementDetails }} +``` +
+{{ 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 }}
+ +### If : +``` +{{ .ConditionDetails }} +``` +### Can be checked by : +{{range $item := .Teams }} - team {{ $item }} +{{ else }} +- Any user with comment edit permission +{{end}} +
+{{ end }} +
+{{ else }}*No manual checks match this pull request.*{{ end }} diff --git a/contribs/github_bot/condition/assignee.go b/contribs/github_bot/condition/assignee.go new file mode 100644 index 00000000000..b1e9debb261 --- /dev/null +++ b/contribs/github_bot/condition/assignee.go @@ -0,0 +1,59 @@ +package condition + +import ( + "bot/client" + "bot/utils" + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Assignee Condition +type assignee struct { + user string +} + +var _ Condition = &assignee{} + +func (a *assignee) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A pull request assignee is user : %s", a.user) + + for _, assignee := range pr.Assignees { + if a.user == assignee.GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func Assignee(user string) Condition { + return &assignee{user: user} +} + +// AssigneeInTeam Condition +type assigneeInTeam struct { + gh *client.GitHub + team string +} + +var _ Condition = &assigneeInTeam{} + +func (a *assigneeInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A pull request assignee is a member of the team : %s", a.team) + + for _, member := range a.gh.ListTeamMembers(a.team) { + for _, assignee := range pr.Assignees { + if member.GetLogin() == assignee.GetLogin() { + return utils.AddStatusNode(true, fmt.Sprintf("%s (member : %s)", detail, member.GetLogin()), details) + } + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func AssigneeInTeam(gh *client.GitHub, team string) Condition { + return &assigneeInTeam{gh: gh, team: team} +} diff --git a/contribs/github_bot/condition/author.go b/contribs/github_bot/condition/author.go new file mode 100644 index 00000000000..be2b293e27e --- /dev/null +++ b/contribs/github_bot/condition/author.go @@ -0,0 +1,53 @@ +package condition + +import ( + "bot/client" + "bot/utils" + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Author Condition +type author struct { + user string +} + +var _ Condition = &author{} + +func (a *author) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + a.user == pr.GetUser().GetLogin(), + fmt.Sprintf("Pull request author is user : %v", a.user), + details, + ) +} + +func Author(user string) Condition { + return &author{user: user} +} + +// AuthorInTeam Condition +type authorInTeam struct { + gh *client.GitHub + team string +} + +var _ Condition = &authorInTeam{} + +func (a *authorInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("Pull request author is a member of the team : %s", a.team) + + for _, member := range a.gh.ListTeamMembers(a.team) { + if member.GetLogin() == pr.GetUser().GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func AuthorInTeam(gh *client.GitHub, team string) Condition { + return &authorInTeam{gh: gh, team: team} +} diff --git a/contribs/github_bot/condition/boolean.go b/contribs/github_bot/condition/boolean.go new file mode 100644 index 00000000000..db9d1fb45dd --- /dev/null +++ b/contribs/github_bot/condition/boolean.go @@ -0,0 +1,100 @@ +package condition + +import ( + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// And Condition +type and struct { + conditions []Condition +} + +var _ Condition = &and{} + +func (a *and) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := true + branch := details.AddBranch("") + + for _, condition := range a.conditions { + if !condition.IsMet(pr, branch) { + met = false + } + } + + if met { + branch.SetValue("🟢 And") + } else { + branch.SetValue("🔴 And") + } + + return met +} + +func And(conditions ...Condition) Condition { + if len(conditions) < 2 { + panic("You should pass at least 2 conditions to And()") + } + + return &and{conditions} +} + +// Or Condition +type or struct { + conditions []Condition +} + +var _ Condition = &or{} + +func (o *or) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := false + branch := details.AddBranch("") + + for _, condition := range o.conditions { + if condition.IsMet(pr, branch) { + met = true + } + } + + if met { + branch.SetValue("🟢 Or") + } else { + branch.SetValue("🔴 Or") + } + + return met +} + +func Or(conditions ...Condition) Condition { + if len(conditions) < 2 { + panic("You should pass at least 2 conditions to Or()") + } + + return &or{conditions} +} + +// Not Condition +type not struct { + cond Condition +} + +var _ Condition = ¬{} + +func (n *not) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + met := n.cond.IsMet(pr, details) + node := details.FindLastNode() + + if met { + node.SetValue(fmt.Sprintf("🔴 Not (%s)", node.(*treeprint.Node).Value.(string))) + } else { + node.SetValue(fmt.Sprintf("🟢 Not (%s)", node.(*treeprint.Node).Value.(string))) + } + + return !met +} + +func Not(cond Condition) Condition { + return ¬{cond} +} diff --git a/contribs/github_bot/condition/branch.go b/contribs/github_bot/condition/branch.go new file mode 100644 index 00000000000..bfb0dd78d3a --- /dev/null +++ b/contribs/github_bot/condition/branch.go @@ -0,0 +1,48 @@ +package condition + +import ( + "bot/utils" + "fmt" + "regexp" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// BaseBranch Condition +type baseBranch struct { + pattern *regexp.Regexp +} + +var _ Condition = &baseBranch{} + +func (b *baseBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + b.pattern.MatchString(pr.GetBase().GetRef()), + fmt.Sprintf("The base branch match this pattern : %s", b.pattern.String()), + details, + ) +} + +func BaseBranch(pattern string) Condition { + return &baseBranch{pattern: regexp.MustCompile(pattern)} +} + +// HeadBranch Condition +type headBranch struct { + pattern *regexp.Regexp +} + +var _ Condition = &headBranch{} + +func (h *headBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + h.pattern.MatchString(pr.GetHead().GetRef()), + fmt.Sprintf("The head branch match this pattern : %s", h.pattern.String()), + details, + ) +} + +func HeadBranch(pattern string) Condition { + return &headBranch{pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github_bot/condition/condition.go b/contribs/github_bot/condition/condition.go new file mode 100644 index 00000000000..9dce8ea1a70 --- /dev/null +++ b/contribs/github_bot/condition/condition.go @@ -0,0 +1,12 @@ +package condition + +import ( + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +type Condition interface { + // Check if the Condition is met and add the detail + // to the tree passed as a parameter + IsMet(pr *github.PullRequest, details treeprint.Tree) bool +} diff --git a/contribs/github_bot/condition/constant.go b/contribs/github_bot/condition/constant.go new file mode 100644 index 00000000000..aa673875583 --- /dev/null +++ b/contribs/github_bot/condition/constant.go @@ -0,0 +1,34 @@ +package condition + +import ( + "bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Always Condition +type always struct{} + +var _ Condition = &always{} + +func (*always) IsMet(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(true, "On every pull request", details) +} + +func Always() Condition { + return &always{} +} + +// Never Condition +type never struct{} + +var _ Condition = &never{} + +func (*never) IsMet(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(false, "On no pull request", details) +} + +func Never() Condition { + return &never{} +} diff --git a/contribs/github_bot/condition/file.go b/contribs/github_bot/condition/file.go new file mode 100644 index 00000000000..71be92e6edd --- /dev/null +++ b/contribs/github_bot/condition/file.go @@ -0,0 +1,57 @@ +package condition + +import ( + "bot/client" + "bot/utils" + "fmt" + "regexp" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// FileChanged Condition +type fileChanged struct { + gh *client.GitHub + pattern *regexp.Regexp +} + +var _ Condition = &fileChanged{} + +func (fc *fileChanged) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A changed file match this pattern : %s", fc.pattern.String()) + opts := &github.ListOptions{ + PerPage: client.PageSize, + } + + for { + files, response, err := fc.gh.Client.PullRequests.ListFiles( + fc.gh.Ctx, + fc.gh.Owner, + fc.gh.Repo, + pr.GetNumber(), + opts, + ) + if err != nil { + fc.gh.Logger.Errorf("Unable to list changed files for PR %d : %v", pr.GetNumber(), err) + break + } + + for _, file := range files { + if fc.pattern.MatchString(file.GetFilename()) { + return utils.AddStatusNode(true, fmt.Sprintf("%s (filename : %s)", detail, file.GetFilename()), details) + } + } + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return utils.AddStatusNode(false, detail, details) +} + +func FileChanged(gh *client.GitHub, pattern string) Condition { + return &fileChanged{gh: gh, pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github_bot/condition/label.go b/contribs/github_bot/condition/label.go new file mode 100644 index 00000000000..c346002d051 --- /dev/null +++ b/contribs/github_bot/condition/label.go @@ -0,0 +1,33 @@ +package condition + +import ( + "bot/utils" + "fmt" + "regexp" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Label Condition +type label struct { + pattern *regexp.Regexp +} + +var _ Condition = &label{} + +func (l *label) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("A label match this pattern : %s", l.pattern.String()) + + for _, label := range pr.Labels { + if l.pattern.MatchString(label.GetName()) { + return utils.AddStatusNode(true, fmt.Sprintf("%s (label : %s)", detail, label.GetName()), details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func Label(pattern string) Condition { + return &label{pattern: regexp.MustCompile(pattern)} +} diff --git a/contribs/github_bot/config.go b/contribs/github_bot/config.go new file mode 100644 index 00000000000..51e11345583 --- /dev/null +++ b/contribs/github_bot/config.go @@ -0,0 +1,97 @@ +package main + +import ( + "bot/client" + c "bot/condition" + r "bot/requirement" +) + +type automaticCheck struct { + Description string + If c.Condition + Then r.Requirement +} + +type manualCheck struct { + Description string + If c.Condition + Teams []string +} + +func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { + auto := []automaticCheck{ + { + Description: "Changes on 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", + If: c.And( + c.FileChanged(gh, "tm2"), + c.BaseBranch("main"), + ), + Then: r.And( + r.Or( + r.ReviewByTeamMembers(gh, "eu", 1), + r.AuthorInTeam(gh, "eu"), + ), + r.Or( + r.ReviewByTeamMembers(gh, "us", 1), + r.AuthorInTeam(gh, "us"), + ), + ), + }, + { + Description: "Maintainer must be able to edit this pull request", + If: c.Always(), + Then: r.MaintainerCanModify(), + }, + { + Description: "Pull request head branch must be up to date with its base", + If: c.Always(), // Or only if c.BaseBranch("main") ? + Then: r.UpToDateWith(gh, r.PR_BASE), + }, + } + + manual := []manualCheck{ + { + Description: "Determine if infra needs to be updated", + If: c.And( + c.BaseBranch("main"), + c.Or( + c.FileChanged(gh, "misc/deployments"), + c.FileChanged(gh, `misc/docker-\.*`), + c.FileChanged(gh, "tm2/pkg/p2p"), + ), + ), + Teams: []string{"tech-staff"}, + }, + { + Description: "The code style is satisfactory", + If: c.And( + c.BaseBranch("main"), + c.Or( + c.FileChanged(gh, `.*\.go`), + c.FileChanged(gh, `.*\.js`), + ), + ), + Teams: []string{"tech-staff"}, + }, + { + Description: "The documentation is accurate and relevant", + If: c.FileChanged(gh, `.*\.md`), + Teams: []string{ + "tech-staff", + "devrels", + }, + }, + } + + // Check for duplicates in manual rule descriptions + // (need 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 description must be unique (duplicate : %s)", rule.Description) + } + unique[rule.Description] = struct{}{} + } + + return auto, manual +} diff --git a/contribs/github_bot/go.mod b/contribs/github_bot/go.mod new file mode 100644 index 00000000000..32ddb2b2cb2 --- /dev/null +++ b/contribs/github_bot/go.mod @@ -0,0 +1,11 @@ +module bot + +go 1.22.2 + +require ( + github.com/google/go-github/v66 v66.0.0 + github.com/sethvargo/go-githubactions v1.3.0 + github.com/xlab/treeprint v1.2.0 +) + +require github.com/google/go-querystring v1.1.0 // indirect diff --git a/contribs/github_bot/go.sum b/contribs/github_bot/go.sum new file mode 100644 index 00000000000..5e2d8a93984 --- /dev/null +++ b/contribs/github_bot/go.sum @@ -0,0 +1,22 @@ +github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= +github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= +github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A= +github.com/sethvargo/go-githubactions v1.3.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80= +github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= +github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= +github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= +github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= +gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contribs/github_bot/logger/action.go b/contribs/github_bot/logger/action.go new file mode 100644 index 00000000000..c6d10429e62 --- /dev/null +++ b/contribs/github_bot/logger/action.go @@ -0,0 +1,43 @@ +package logger + +import ( + "github.com/sethvargo/go-githubactions" +) + +type actionLogger struct{} + +var _ Logger = &actionLogger{} + +// Debugf implements Logger. +func (a *actionLogger) Debugf(msg string, args ...any) { + githubactions.Debugf(msg, args...) +} + +// Errorf implements Logger. +func (a *actionLogger) Errorf(msg string, args ...any) { + githubactions.Errorf(msg, args...) +} + +// Fatalf implements Logger. +func (a *actionLogger) Fatalf(msg string, args ...any) { + githubactions.Fatalf(msg, args...) +} + +// Infof implements Logger. +func (a *actionLogger) Infof(msg string, args ...any) { + githubactions.Infof(msg, args...) +} + +// Noticef implements Logger. +func (a *actionLogger) Noticef(msg string, args ...any) { + githubactions.Noticef(msg, args...) +} + +// Warningf implements Logger. +func (a *actionLogger) Warningf(msg string, args ...any) { + githubactions.Warningf(msg, args...) +} + +func newActionLogger() Logger { + return &actionLogger{} +} diff --git a/contribs/github_bot/logger/logger.go b/contribs/github_bot/logger/logger.go new file mode 100644 index 00000000000..53b50c6ed9a --- /dev/null +++ b/contribs/github_bot/logger/logger.go @@ -0,0 +1,34 @@ +package logger + +import ( + "os" +) + +// All Logger methods follow the standard fmt.Printf convention +type Logger interface { + // Debugf prints a debug-level message + Debugf(msg string, args ...any) + + // Noticef prints a notice-level message + Noticef(msg string, args ...any) + + // Warningf prints a warning-level message + Warningf(msg string, args ...any) + + // Errorf prints a error-level message + Errorf(msg string, args ...any) + + // Fatalf prints a error-level message and exits + Fatalf(msg string, args ...any) + + // Infof prints message to stdout without any level annotations + Infof(msg string, args ...any) +} + +func NewLogger(verbose bool) Logger { + if _, isAction := os.LookupEnv("GITHUB_ACTION"); isAction { + return newActionLogger() + } + + return newTermLogger(verbose) +} diff --git a/contribs/github_bot/logger/terminal.go b/contribs/github_bot/logger/terminal.go new file mode 100644 index 00000000000..cc12022011a --- /dev/null +++ b/contribs/github_bot/logger/terminal.go @@ -0,0 +1,55 @@ +package logger + +import ( + "fmt" + "log/slog" + "os" +) + +type termLogger struct{} + +var _ Logger = &termLogger{} + +// Debugf implements Logger +func (s *termLogger) Debugf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Debug(fmt.Sprintf(msg, args...)) +} + +// Errorf implements Logger +func (s *termLogger) Errorf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Error(fmt.Sprintf(msg, args...)) +} + +// Fatalf implements Logger +func (s *termLogger) Fatalf(msg string, args ...any) { + s.Errorf(msg, args...) + os.Exit(1) +} + +// Infof implements Logger +func (s *termLogger) Infof(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Info(fmt.Sprintf(msg, args...)) +} + +// Noticef implements Logger +func (s *termLogger) Noticef(msg string, args ...any) { + // Alias to info on terminal since notice level only exists on GitHub Actions + s.Infof(msg, args...) +} + +// Warningf implements Logger +func (s *termLogger) Warningf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Warn(fmt.Sprintf(msg, args...)) +} + +func newTermLogger(verbose bool) Logger { + if verbose { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + + return &termLogger{} +} diff --git a/contribs/github_bot/main.go b/contribs/github_bot/main.go new file mode 100644 index 00000000000..6e524deb1f0 --- /dev/null +++ b/contribs/github_bot/main.go @@ -0,0 +1,117 @@ +package main + +import ( + "bot/client" + "bot/param" + "sync" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func main() { + // Retrieve params by parsing CLI flags and/or GitHub Actions context + params := param.Get() + + // Init GitHub API client + gh := client.New(params) + + // Handle comment update, if any + handleCommentUpdate(gh) + + // Retrieve a slice of pull requests to process + var ( + prs []*github.PullRequest + err error + ) + + // If requested, retrieve all opened pull requests + if params.PrAll { + opts := &github.PullRequestListOptions{ + State: "open", + Sort: "updated", + Direction: "desc", + } + + prs, _, err = gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) + if err != nil { + gh.Logger.Fatalf("Unable to retrieve all opened pull requests : %v", err) + } + + // Otherwise, retrieve only specified pull request(s) (flag or GitHub Action context) + } else { + 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) + if err != nil { + gh.Logger.Fatalf("Unable to retrieve specified pull request (%d) : %v", prNum, err) + } + prs[i] = pr + } + } + + // Process all pull requests in parrallel + autoRules, manualRules := config(gh) + var wg sync.WaitGroup + wg.Add(len(prs)) + + for _, pr := range prs { + go func(pr *github.PullRequest) { + defer wg.Done() + commentContent := CommentContent{} + + // Iterate over all automatic rules in config + for _, autoRule := range autoRules { + ifDetails := treeprint.NewWithRoot("🟢 Condition met") + + // Check if conditions of this rule are met by this PR + if autoRule.If.IsMet(pr, ifDetails) { + c := AutoContent{Description: autoRule.Description, Satisfied: false} + thenDetails := treeprint.NewWithRoot("🔴 Requirement not satisfied") + + // Check if requirements of this rule are satisfied by this PR + if autoRule.Then.IsSatisfied(pr, thenDetails) { + thenDetails.SetValue("🟢 Requirement satisfied") + c.Satisfied = true + } + + c.ConditionDetails = ifDetails.String() + c.RequirementDetails = thenDetails.String() + commentContent.AutoRules = append(commentContent.AutoRules, c) + } + } + + // Iterate over all manual rules in config + for _, manualRule := range manualRules { + ifDetails := treeprint.NewWithRoot("🟢 Condition met") + + // Retrieve manual check states + checks := make(map[string][2]string) + if comment := gh.GetBotComment(pr.GetNumber()); comment != nil { + checks = getCommentManualChecks(comment.GetBody()) + } + + // Check if conditions of this rule are met by this PR + if manualRule.If.IsMet(pr, ifDetails) { + commentContent.ManualRules = append( + commentContent.ManualRules, + ManualContent{ + Description: manualRule.Description, + ConditionDetails: ifDetails.String(), + CheckedBy: checks[manualRule.Description][1], + Teams: manualRule.Teams, + }, + ) + } + } + + // Print results in PR comment or in logs + if gh.DryRun { + // TODO: Pretty print dry run + } else { + updateComment(gh, pr, commentContent) + } + }(pr) + } + wg.Wait() +} diff --git a/contribs/github_bot/param/param.go b/contribs/github_bot/param/param.go new file mode 100644 index 00000000000..ea6af698ca3 --- /dev/null +++ b/contribs/github_bot/param/param.go @@ -0,0 +1,109 @@ +package param + +import ( + "flag" + "fmt" + "os" + + "github.com/sethvargo/go-githubactions" +) + +type Params struct { + Owner string + Repo string + PrAll bool + PrNums PrList + Verbose bool + DryRun bool + Timeout uint +} + +// Get Params from both cli flags and/or GitHub Actions context +func Get() Params { + p := Params{} + + // Add cmd description to usage message + flag.Usage = func() { + fmt.Fprint(flag.CommandLine.Output(), "This tool checks if requirements for a PR to be merged are satisfied (defined in config.go) and display PR status checks accordingly.\n") + fmt.Fprint(flag.CommandLine.Output(), "A valid GitHub Token must be provided by setting the GITHUB_TOKEN env variable.\n\n") + flag.PrintDefaults() + } + + // Helper to display an error + usage message before exiting + errorUsage := func(error string) { + fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", error) + flag.Usage() + os.Exit(1) + } + + // Flags definition + flag.StringVar(&p.Owner, "owner", "", "owner of the repo to process, if empty, will be retrieved from GitHub Actions context") + flag.StringVar(&p.Repo, "repo", "", "repo to process, if empty, will be retrieved from GitHub Actions context") + flag.BoolVar(&p.PrAll, "pr-all", false, "process all opened pull requests") + flag.TextVar(&p.PrNums, "pr-numbers", PrList(nil), "pull request(s) to process, must be a comma seperated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrived from GitHub Actions context") + flag.BoolVar(&p.Verbose, "verbose", false, "set logging level to debug") + flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are met without updating PR checks on GitHub web interface") + flag.UintVar(&p.Timeout, "timeout", 0, "timeout in milliseconds") + flag.Parse() + + // If any arg remain after flags processing + if len(flag.Args()) > 0 { + errorUsage(fmt.Sprintf("Unknown arg(s) provided : %v", flag.Args())) + } + + // Check if flags are coherents + if p.PrAll && len(p.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) { + 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 == "" { + 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 == "" { + 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 { + const errMsg = "Unable to retrieve pull request number from GitHub Actions context, you may want to set it using -pr-numbers flag" + var num float64 + + switch actionCtx.EventName { + case "issue_comment": + issue, ok := actionCtx.Event["issue"].(map[string]any) + if !ok { + errorUsage(errMsg) + } + num, ok = issue["number"].(float64) + if !ok || num <= 0 { + errorUsage(errMsg) + } + case "pull_request": + pr, ok := actionCtx.Event["pull_request"].(map[string]any) + if !ok { + errorUsage(errMsg) + } + num, ok = pr["number"].(float64) + if !ok || num <= 0 { + errorUsage(errMsg) + } + default: + errorUsage(errMsg) + } + + p.PrNums = PrList([]int{int(num)}) + } + } + + return p +} diff --git a/contribs/github_bot/param/prlist.go b/contribs/github_bot/param/prlist.go new file mode 100644 index 00000000000..96a04ebce14 --- /dev/null +++ b/contribs/github_bot/param/prlist.go @@ -0,0 +1,45 @@ +package param + +import ( + "encoding" + "fmt" + "strconv" + "strings" +) + +type PrList []int + +// PrList is both a TextMarshaler and a TextUnmarshaler +var ( + _ encoding.TextMarshaler = PrList{} + _ encoding.TextUnmarshaler = &PrList{} +) + +// MarshalText implements encoding.TextMarshaler. +func (p PrList) MarshalText() (text []byte, err error) { + prNumsStr := make([]string, len(p)) + + for i, prNum := range p { + prNumsStr[i] = strconv.Itoa(prNum) + } + + return []byte(strings.Join(prNumsStr, ",")), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (p *PrList) UnmarshalText(text []byte) error { + for _, prNumStr := range strings.Split(string(text), ",") { + prNum, err := strconv.Atoi(strings.TrimSpace(prNumStr)) + if err != nil { + return err + } + + if prNum <= 0 { + return fmt.Errorf("invalid pull request number (<= 0) : original(%s) parsed(%d)", prNumStr, prNum) + } + + *p = append(*p, prNum) + } + + return nil +} diff --git a/contribs/github_bot/requirement/assignee.go b/contribs/github_bot/requirement/assignee.go new file mode 100644 index 00000000000..6854322521a --- /dev/null +++ b/contribs/github_bot/requirement/assignee.go @@ -0,0 +1,52 @@ +package requirement + +import ( + "bot/client" + "bot/utils" + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Assignee Requirement +type assignee struct { + gh *client.GitHub + user string +} + +var _ Requirement = &assignee{} + +func (a *assignee) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This user is assigned to pull request : %s", a.user) + + // Check if user was already assigned to PR + for _, assignee := range pr.Assignees { + if a.user == assignee.GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + // If in a dry run, skip assigning the user + if a.gh.DryRun { + return utils.AddStatusNode(false, detail, details) + } + + // If user not already assigned, assign it + if _, _, err := a.gh.Client.Issues.AddAssignees( + a.gh.Ctx, + a.gh.Owner, + a.gh.Repo, + pr.GetNumber(), + []string{a.user}, + ); err != nil { + a.gh.Logger.Errorf("Unable to assign user %s to PR %d : %v", a.user, pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + + return utils.AddStatusNode(true, detail, details) +} + +func Assignee(gh *client.GitHub, user string) Requirement { + return &assignee{gh: gh, user: user} +} diff --git a/contribs/github_bot/requirement/author.go b/contribs/github_bot/requirement/author.go new file mode 100644 index 00000000000..29c3f6d1404 --- /dev/null +++ b/contribs/github_bot/requirement/author.go @@ -0,0 +1,53 @@ +package requirement + +import ( + "bot/client" + "bot/utils" + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// AuthorInTeam Requirement +type author struct { + user string +} + +var _ Requirement = &author{} + +func (a *author) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + a.user == pr.GetUser().GetLogin(), + fmt.Sprintf("Pull request author is user : %v", a.user), + details, + ) +} + +func Author(user string) Requirement { + return &author{user: user} +} + +// AuthorInTeam Requirement +type authorInTeam struct { + gh *client.GitHub + team string +} + +var _ Requirement = &authorInTeam{} + +func (a *authorInTeam) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("Pull request author is a member of the team : %s", a.team) + + for _, member := range a.gh.ListTeamMembers(a.team) { + if member.GetLogin() == pr.GetUser().GetLogin() { + return utils.AddStatusNode(true, detail, details) + } + } + + return utils.AddStatusNode(false, detail, details) +} + +func AuthorInTeam(gh *client.GitHub, team string) Requirement { + return &authorInTeam{gh: gh, team: team} +} diff --git a/contribs/github_bot/requirement/boolean.go b/contribs/github_bot/requirement/boolean.go new file mode 100644 index 00000000000..1deff3b0531 --- /dev/null +++ b/contribs/github_bot/requirement/boolean.go @@ -0,0 +1,100 @@ +package requirement + +import ( + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// And Requirement +type and struct { + requirements []Requirement +} + +var _ Requirement = &and{} + +func (a *and) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := true + branch := details.AddBranch("") + + for _, requirement := range a.requirements { + if !requirement.IsSatisfied(pr, branch) { + satisfied = false + } + } + + if satisfied { + branch.SetValue("🟢 And") + } else { + branch.SetValue("🔴 And") + } + + return satisfied +} + +func And(requirements ...Requirement) Requirement { + if len(requirements) < 2 { + panic("You should pass at least 2 requirements to And()") + } + + return &and{requirements} +} + +// Or Requirement +type or struct { + requirements []Requirement +} + +var _ Requirement = &or{} + +func (o *or) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := false + branch := details.AddBranch("") + + for _, requirement := range o.requirements { + if requirement.IsSatisfied(pr, branch) { + satisfied = true + } + } + + if satisfied { + branch.SetValue("🟢 Or") + } else { + branch.SetValue("🔴 Or") + } + + return satisfied +} + +func Or(requirements ...Requirement) Requirement { + if len(requirements) < 2 { + panic("You should pass at least 2 requirements to Or()") + } + + return &or{requirements} +} + +// Not Requirement +type not struct { + req Requirement +} + +var _ Requirement = ¬{} + +func (n *not) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + satisfied := n.req.IsSatisfied(pr, details) + node := details.FindLastNode() + + if satisfied { + node.SetValue(fmt.Sprintf("🔴 Not (%s)", node.(*treeprint.Node).Value.(string))) + } else { + node.SetValue(fmt.Sprintf("🟢 Not (%s)", node.(*treeprint.Node).Value.(string))) + } + + return !satisfied +} + +func Not(req Requirement) Requirement { + return ¬{req} +} diff --git a/contribs/github_bot/requirement/branch.go b/contribs/github_bot/requirement/branch.go new file mode 100644 index 00000000000..9b1eafde05d --- /dev/null +++ b/contribs/github_bot/requirement/branch.go @@ -0,0 +1,52 @@ +package requirement + +import ( + "bot/client" + "bot/utils" + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Pass this to UpToDateWith constructor to check the PR head branch +// against its base branch +const PR_BASE = "PR_BASE" + +// UpToDateWith Requirement +type upToDateWith struct { + gh *client.GitHub + base string +} + +var _ Requirement = &author{} + +func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + base := u.base + if u.base == PR_BASE { + base = pr.GetBase().GetRef() + } + head := pr.GetHead().GetRef() + + cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil) + if err != nil { + u.gh.Logger.Errorf("Unable to compare head branch (%s) and base (%s) : %v", head, base, err) + return false + } + + return utils.AddStatusNode( + cmp.GetBehindBy() == 0, + fmt.Sprintf( + "Head branch (%s) is up to date with (%s) : behind by %d / ahead by %d", + head, + base, + cmp.GetBehindBy(), + cmp.GetAheadBy(), + ), + details, + ) +} + +func UpToDateWith(gh *client.GitHub, base string) Requirement { + return &upToDateWith{gh, base} +} diff --git a/contribs/github_bot/requirement/label.go b/contribs/github_bot/requirement/label.go new file mode 100644 index 00000000000..c1a0bbd7518 --- /dev/null +++ b/contribs/github_bot/requirement/label.go @@ -0,0 +1,52 @@ +package requirement + +import ( + "bot/client" + "bot/utils" + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Label Requirement +type label struct { + gh *client.GitHub + name string +} + +var _ Requirement = &label{} + +func (l *label) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This label is applied to pull request : %s", l.name) + + // Check if label was already applied to PR + for _, label := range pr.Labels { + if l.name == label.GetName() { + return utils.AddStatusNode(true, detail, details) + } + } + + // If in a dry run, skip applying the label + if l.gh.DryRun { + return utils.AddStatusNode(false, detail, details) + } + + // If label not already applied, apply it + if _, _, err := l.gh.Client.Issues.AddLabelsToIssue( + l.gh.Ctx, + l.gh.Owner, + l.gh.Repo, + pr.GetNumber(), + []string{l.name}, + ); err != nil { + l.gh.Logger.Errorf("Unable to add label %s to PR %d : %v", l.name, pr.GetNumber(), err) + return utils.AddStatusNode(false, detail, details) + } + + return utils.AddStatusNode(true, detail, details) +} + +func Label(gh *client.GitHub, name string) Requirement { + return &label{gh, name} +} diff --git a/contribs/github_bot/requirement/maintainer.go b/contribs/github_bot/requirement/maintainer.go new file mode 100644 index 00000000000..6d89206ed92 --- /dev/null +++ b/contribs/github_bot/requirement/maintainer.go @@ -0,0 +1,25 @@ +package requirement + +import ( + "bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// MaintainerCanModify Requirement +type maintainerCanModify struct{} + +var _ Requirement = &maintainerCanModify{} + +func (a *maintainerCanModify) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode( + pr.GetMaintainerCanModify(), + "Maintainer can modify this pull request", + details, + ) +} + +func MaintainerCanModify() Requirement { + return &maintainerCanModify{} +} diff --git a/contribs/github_bot/requirement/requirement.go b/contribs/github_bot/requirement/requirement.go new file mode 100644 index 00000000000..ae48a1e9648 --- /dev/null +++ b/contribs/github_bot/requirement/requirement.go @@ -0,0 +1,12 @@ +package requirement + +import ( + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +type Requirement interface { + // Check if the Requirement is satisfied and add the detail + // to the tree passed as a parameter + IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool +} diff --git a/contribs/github_bot/requirement/reviewer.go b/contribs/github_bot/requirement/reviewer.go new file mode 100644 index 00000000000..ce6e46becdb --- /dev/null +++ b/contribs/github_bot/requirement/reviewer.go @@ -0,0 +1,130 @@ +package requirement + +import ( + "bot/client" + "bot/utils" + "fmt" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Reviewer Requirement +type reviewByUser struct { + gh *client.GitHub + user string +} + +var _ Requirement = &reviewByUser{} + +func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("This user approved pull request : %s", r.user) + + // If not a dry run, make the user a reviewer if he's not already + if !r.gh.DryRun { + requested := false + if reviewers := r.gh.ListPrReviewers(pr.GetNumber()); reviewers != nil { + for _, user := range reviewers.Users { + if user.GetLogin() == r.user { + requested = true + break + } + } + } + + if requested { + r.gh.Logger.Debugf("Review of user %s already requested on PR %d", r.user, pr.GetNumber()) + } else { + r.gh.Logger.Debugf("Requesting review from user %s on PR %d", r.user, pr.GetNumber()) + if _, _, err := r.gh.Client.PullRequests.RequestReviewers( + r.gh.Ctx, + r.gh.Owner, + r.gh.Repo, + pr.GetNumber(), + github.ReviewersRequest{ + Reviewers: []string{r.user}, + }, + ); err != nil { + r.gh.Logger.Errorf("Unable to request review from user %s on PR %d : %v", r.user, pr.GetNumber(), err) + } + } + } + + // Check if user already approved this PR + for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { + if review.GetUser().GetLogin() == r.user { + r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) + return utils.AddStatusNode(review.GetState() == "APPROVED", detail, details) + } + } + r.gh.Logger.Debugf("User %s has not reviewed PR %d yet", r.user, pr.GetNumber()) + + return utils.AddStatusNode(false, detail, details) +} + +func ReviewByUser(gh *client.GitHub, user string) Requirement { + return &reviewByUser{gh, user} +} + +// Reviewer Requirement +type reviewByTeamMembers struct { + gh *client.GitHub + team string + count uint +} + +var _ Requirement = &reviewByTeamMembers{} + +func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { + detail := fmt.Sprintf("At least %d user(s) of the team %s approved pull request", r.count, r.team) + + // If not a dry run, make the user a reviewer if he's not already + if !r.gh.DryRun { + requested := false + if reviewers := r.gh.ListPrReviewers(pr.GetNumber()); reviewers != nil { + for _, team := range reviewers.Teams { + if team.GetSlug() == r.team { + requested = true + break + } + } + } + + if requested { + r.gh.Logger.Debugf("Review of team %s already requested on PR %d", r.team, pr.GetNumber()) + } else { + r.gh.Logger.Debugf("Requesting review from team %s on PR %d", r.team, pr.GetNumber()) + if _, _, err := r.gh.Client.PullRequests.RequestReviewers( + r.gh.Ctx, + r.gh.Owner, + r.gh.Repo, + pr.GetNumber(), + github.ReviewersRequest{ + TeamReviewers: []string{r.team}, + }, + ); err != nil { + r.gh.Logger.Errorf("Unable to request review from team %s on PR %d : %v", r.team, pr.GetNumber(), err) + } + } + } + + // Check how many members of this team already approved this PR + approved := uint(0) + members := r.gh.ListTeamMembers(r.team) + for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { + for _, member := range members { + if review.GetUser().GetLogin() == member.GetLogin() { + if review.GetState() == "APPROVED" { + approved += 1 + } + r.gh.Logger.Debugf("Member %s from team %s already reviewed PR %d with state %s (%d/%d required approval(s))", member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), approved, r.count) + } + } + } + + return utils.AddStatusNode(approved >= r.count, detail, details) +} + +func ReviewByTeamMembers(gh *client.GitHub, team string, count uint) Requirement { + return &reviewByTeamMembers{gh, team, count} +} diff --git a/contribs/github_bot/utils/tree.go b/contribs/github_bot/utils/tree.go new file mode 100644 index 00000000000..502f87e398d --- /dev/null +++ b/contribs/github_bot/utils/tree.go @@ -0,0 +1,17 @@ +package utils + +import ( + "fmt" + + "github.com/xlab/treeprint" +) + +func AddStatusNode(b bool, desc string, details treeprint.Tree) bool { + if b { + details.AddNode(fmt.Sprintf("🟢 %s", desc)) + } else { + details.AddNode(fmt.Sprintf("🔴 %s", desc)) + } + + return b +} diff --git a/misc/github-bot/client/client.go b/misc/github-bot/client/client.go new file mode 100644 index 00000000000..11e3f90a80b --- /dev/null +++ b/misc/github-bot/client/client.go @@ -0,0 +1,235 @@ +package client + +import ( + "bot/logger" + "bot/param" + "context" + "log" + "os" + "time" + + "github.com/google/go-github/v66/github" +) + +const PageSize = 100 + +type Github struct { + Client *github.Client + Ctx context.Context + DryRun bool + Logger logger.Logger + Owner string + Repo string +} + +func (gh *Github) GetBotComment(prNum int) *github.IssueComment { + // List existing comments + var ( + allComments []*github.IssueComment + sort = "created" + direction = "desc" + opts = &github.IssueListCommentsOptions{ + Sort: &sort, + Direction: &direction, + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + ) + + for { + comments, response, err := gh.Client.Issues.ListComments( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + gh.Logger.Errorf("Unable to list comments for PR %d : %v", prNum, err) + return nil + } + + allComments = append(allComments, comments...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + // Get current user (bot) + currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") + if err != nil { + gh.Logger.Errorf("Unable to get current user : %v", err) + return nil + } + + // Get the comment created by current user + for _, comment := range allComments { + if comment.GetUser().GetLogin() == currentUser.GetLogin() { + return comment + } + } + + return nil +} + +func (gh *Github) SetBotComment(body string, prNum int) *github.IssueComment { + // Create bot comment if it not already exists + if comment := gh.GetBotComment(prNum); comment == nil { + newComment, _, err := gh.Client.Issues.CreateComment( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + &github.IssueComment{Body: &body}, + ) + if err != nil { + gh.Logger.Errorf("Unable to create bot comment for PR %d : %v", prNum, err) + return nil + } + return newComment + } else { + comment.Body = &body + editComment, _, err := gh.Client.Issues.EditComment( + gh.Ctx, + gh.Owner, + gh.Repo, + comment.GetID(), + comment, + ) + if err != nil { + gh.Logger.Errorf("Unable to edit bot comment with ID %d : %v", comment.GetID(), err) + return nil + } + return editComment + } +} + +func (gh *Github) ListTeamMembers(team string) []*github.User { + var ( + allMembers []*github.User + opts = &github.TeamListTeamMembersOptions{ + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + ) + + for { + members, response, err := gh.Client.Teams.ListTeamMembersBySlug( + gh.Ctx, + gh.Owner, + team, + opts, + ) + if err != nil { + gh.Logger.Errorf("Unable to list members for team %s : %v", team, err) + return nil + } + + allMembers = append(allMembers, members...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allMembers +} + +func (gh *Github) ListPrReviewers(prNum int) *github.Reviewers { + var ( + allReviewers = &github.Reviewers{} + opts = &github.ListOptions{ + PerPage: PageSize, + } + ) + + for { + reviewers, response, err := gh.Client.PullRequests.ListReviewers( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + gh.Logger.Errorf("Unable to list reviewers for PR %d : %v", prNum, err) + return nil + } + + allReviewers.Teams = append(allReviewers.Teams, reviewers.Teams...) + allReviewers.Users = append(allReviewers.Users, reviewers.Users...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allReviewers +} + +func (gh *Github) ListPrReviews(prNum int) []*github.PullRequestReview { + var ( + allReviews []*github.PullRequestReview + opts = &github.ListOptions{ + PerPage: PageSize, + } + ) + + for { + reviews, response, err := gh.Client.PullRequests.ListReviews( + gh.Ctx, + gh.Owner, + gh.Repo, + prNum, + opts, + ) + if err != nil { + gh.Logger.Errorf("Unable to list reviews for PR %d : %v", prNum, err) + return nil + } + + allReviews = append(allReviews, reviews...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return allReviews +} + +func New(params param.Params) *Github { + gh := &Github{ + Owner: params.Owner, + Repo: params.Repo, + DryRun: params.DryRun, + } + + // This method will detect if the current process was launched by + // a Github Action or not and will accordingly return a logger suitable for + // the terminal output or for the Github Actions web interface + gh.Logger = logger.NewLogger(params.Verbose) + + // Create context with timeout if specified in flags + if params.Timeout > 0 { + gh.Ctx, _ = context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Millisecond) + } else { + gh.Ctx = context.Background() + } + + // Init Github API Client using token from env + token, set := os.LookupEnv("GITHUB_TOKEN") + if !set { + log.Fatalf("GITHUB_TOKEN is not set in env") + } + gh.Client = github.NewClient(nil).WithAuthToken(token) + + return gh +} diff --git a/misc/github-bot/comment.go b/misc/github-bot/comment.go new file mode 100644 index 00000000000..ec884c1bb1c --- /dev/null +++ b/misc/github-bot/comment.go @@ -0,0 +1,20 @@ +package main + +import "bot/client" + +type Auto struct { + Met bool + Description string +} +type Manual struct { + CheckedBy string + Description string +} + +type Comment struct { + Auto []Auto + Manual []Manual +} + +func onCommentUpdated(gh *client.Github) { +} diff --git a/misc/github-bot/comment.tmpl b/misc/github-bot/comment.tmpl new file mode 100644 index 00000000000..1f7cd0ace58 --- /dev/null +++ b/misc/github-bot/comment.tmpl @@ -0,0 +1,21 @@ +# 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 [config file](https://github.com/gnolang/gno/blob/master/misc/github-bot/config.go). + +## Automated Checks + +{{ range .Auto }} +
+ {{ if .Met }}🟢{{ else }}🟠{{ end }} {{ .Description }} + + **TODO** +
+{{ end }} + +## Manual Checks + +{{ range .Manual }} +- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }} {{ if .CheckedBy }}(checked by @{{ .CheckedBy }}){{ end }}{{ end }} diff --git a/misc/github-bot/condition/assignee.go b/misc/github-bot/condition/assignee.go new file mode 100644 index 00000000000..4190268e49d --- /dev/null +++ b/misc/github-bot/condition/assignee.go @@ -0,0 +1,64 @@ +package condition + +import ( + "bot/client" + "fmt" + + "github.com/google/go-github/v66/github" +) + +// Assignee Condition +type assignee struct { + user string +} + +var _ Condition = &assignee{} + +// GetText implements Condition +func (a *assignee) GetText() string { + return fmt.Sprintf("A pull request assignee is user : %s", a.user) +} + +// Validate implements Condition +func (a *assignee) Validate(pr *github.PullRequest) bool { + for _, assignee := range pr.Assignees { + if a.user == assignee.GetLogin() { + return true + } + } + return false +} + +func Assignee(user string) Condition { + return &assignee{user: user} +} + +// AssigneeInTeam Condition +type assigneeInTeam struct { + gh *client.Github + team string +} + +var _ Condition = &assigneeInTeam{} + +// GetText implements Condition +func (a *assigneeInTeam) GetText() string { + return fmt.Sprintf("A pull request assignee is a member of the team : %s", a.team) +} + +// Validate implements Condition +func (a *assigneeInTeam) Validate(pr *github.PullRequest) bool { + for _, member := range a.gh.ListTeamMembers(a.team) { + for _, assignee := range pr.Assignees { + if member.GetLogin() == assignee.GetLogin() { + return true + } + } + } + + return false +} + +func AssigneeInTeam(gh *client.Github, team string) Condition { + return &assigneeInTeam{gh: gh, team: team} +} diff --git a/misc/github-bot/condition/author.go b/misc/github-bot/condition/author.go new file mode 100644 index 00000000000..a2821267b6b --- /dev/null +++ b/misc/github-bot/condition/author.go @@ -0,0 +1,57 @@ +package condition + +import ( + "bot/client" + "fmt" + + "github.com/google/go-github/v66/github" +) + +// Author Condition +type author struct { + user string +} + +var _ Condition = &author{} + +// GetText implements Condition +func (a *author) GetText() string { + return fmt.Sprintf("Pull request author is user : %v", a.user) +} + +// Validate implements Condition +func (a *author) Validate(pr *github.PullRequest) bool { + return a.user == pr.GetUser().GetLogin() +} + +func Author(user string) Condition { + return &author{user: user} +} + +// AuthorInTeam Condition +type authorInTeam struct { + gh *client.Github + team string +} + +var _ Condition = &authorInTeam{} + +// GetText implements Condition +func (a *authorInTeam) GetText() string { + return fmt.Sprintf("Pull request author is a member of the team : %s", a.team) +} + +// Validate implements Condition +func (a *authorInTeam) Validate(pr *github.PullRequest) bool { + for _, member := range a.gh.ListTeamMembers(a.team) { + if member.GetLogin() == pr.GetUser().GetLogin() { + return true + } + } + + return false +} + +func AuthorInTeam(gh *client.Github, team string) Condition { + return &authorInTeam{gh: gh, team: team} +} diff --git a/misc/github-bot/condition/boolean.go b/misc/github-bot/condition/boolean.go new file mode 100644 index 00000000000..0163da31a4f --- /dev/null +++ b/misc/github-bot/condition/boolean.go @@ -0,0 +1,100 @@ +package condition + +import ( + "fmt" + + "github.com/google/go-github/v66/github" +) + +// And Condition +type and struct { + conditions []Condition +} + +var _ Condition = &and{} + +// Validate implements Condition +func (a *and) Validate(pr *github.PullRequest) bool { + for _, condition := range a.conditions { + if !condition.Validate(pr) { + return false + } + } + + return true +} + +// GetText implements Condition +func (a *and) GetText() string { + text := fmt.Sprintf("(%s", a.conditions[0].GetText()) + for _, condition := range a.conditions[1:] { + text = fmt.Sprintf("%s AND %s", text, condition.GetText()) + } + + return text + ")" +} + +func And(conditions ...Condition) Condition { + if len(conditions) < 2 { + panic("You should pass at least 2 conditions to And()") + } + + return &and{conditions} +} + +// Or Condition +type or struct { + conditions []Condition +} + +var _ Condition = &or{} + +// Validate implements Condition +func (o *or) Validate(pr *github.PullRequest) bool { + for _, condition := range o.conditions { + if condition.Validate(pr) { + return true + } + } + + return false +} + +// GetText implements Condition +func (o *or) GetText() string { + text := fmt.Sprintf("(%s", o.conditions[0].GetText()) + for _, condition := range o.conditions[1:] { + text = fmt.Sprintf("%s OR %s", text, condition.GetText()) + } + + return text + ")" +} + +func Or(conditions ...Condition) Condition { + if len(conditions) < 2 { + panic("You should pass at least 2 conditions to Or()") + } + + return &or{conditions} +} + +// Not Condition +type not struct { + cond Condition +} + +var _ Condition = ¬{} + +// Validate implements Condition +func (n *not) Validate(pr *github.PullRequest) bool { + return !n.cond.Validate(pr) +} + +// GetText implements Condition +func (n *not) GetText() string { + return fmt.Sprintf("NOT %s", n.cond.GetText()) +} + +func Not(cond Condition) Condition { + return ¬{cond} +} diff --git a/misc/github-bot/condition/branch.go b/misc/github-bot/condition/branch.go new file mode 100644 index 00000000000..db8003adf2b --- /dev/null +++ b/misc/github-bot/condition/branch.go @@ -0,0 +1,50 @@ +package condition + +import ( + "fmt" + "regexp" + + "github.com/google/go-github/v66/github" +) + +// BaseBranch Condition +type baseBranch struct { + pattern *regexp.Regexp +} + +var _ Condition = &baseBranch{} + +// Validate implements Condition +func (b *baseBranch) Validate(pr *github.PullRequest) bool { + return b.pattern.MatchString(pr.GetBase().GetRef()) +} + +// GetText implements Condition +func (b *baseBranch) GetText() string { + return fmt.Sprintf("The base branch match this pattern : %s", b.pattern.String()) +} + +func BaseBranch(pattern string) Condition { + return &baseBranch{pattern: regexp.MustCompile(pattern)} +} + +// HeadBranch Condition +type headBranch struct { + pattern *regexp.Regexp +} + +var _ Condition = &headBranch{} + +// Validate implements Condition +func (h *headBranch) Validate(pr *github.PullRequest) bool { + return h.pattern.MatchString(pr.GetHead().GetRef()) +} + +// GetText implements Condition +func (h *headBranch) GetText() string { + return fmt.Sprintf("The head branch match this pattern : %s", h.pattern.String()) +} + +func HeadBranch(pattern string) Condition { + return &headBranch{pattern: regexp.MustCompile(pattern)} +} diff --git a/misc/github-bot/condition/condition.go b/misc/github-bot/condition/condition.go new file mode 100644 index 00000000000..a3226647e79 --- /dev/null +++ b/misc/github-bot/condition/condition.go @@ -0,0 +1,13 @@ +package condition + +import ( + "github.com/google/go-github/v66/github" +) + +type Condition interface { + // Check if the Condition is met by this PR + Validate(pr *github.PullRequest) bool + + // Get a text representation of this Condition + GetText() string +} diff --git a/misc/github-bot/condition/constant.go b/misc/github-bot/condition/constant.go new file mode 100644 index 00000000000..ed01a01d9d1 --- /dev/null +++ b/misc/github-bot/condition/constant.go @@ -0,0 +1,43 @@ +package condition + +import ( + "github.com/google/go-github/v66/github" +) + +// Always Condition +type always struct{} + +var _ Condition = &always{} + +// Validate implements Condition +func (*always) Validate(_ *github.PullRequest) bool { + return true +} + +// GetText implements Condition +func (*always) GetText() string { + return "On every pull request" +} + +func Always() Condition { + return &always{} +} + +// Never Condition +type never struct{} + +var _ Condition = &never{} + +// Validate implements Condition +func (*never) Validate(_ *github.PullRequest) bool { + return false +} + +// GetText implements Condition +func (*never) GetText() string { + return "On no pull request" +} + +func Never() Condition { + return &never{} +} diff --git a/misc/github-bot/condition/file.go b/misc/github-bot/condition/file.go new file mode 100644 index 00000000000..a6f49800f65 --- /dev/null +++ b/misc/github-bot/condition/file.go @@ -0,0 +1,61 @@ +package condition + +import ( + "bot/client" + "fmt" + "regexp" + + "github.com/google/go-github/v66/github" +) + +// FileChanged Condition +type fileChanged struct { + gh *client.Github + pattern *regexp.Regexp +} + +var _ Condition = &fileChanged{} + +// Validate implements Condition +func (fc *fileChanged) Validate(pr *github.PullRequest) bool { + opts := &github.ListOptions{ + PerPage: client.PageSize, + } + + for { + files, response, err := fc.gh.Client.PullRequests.ListFiles( + fc.gh.Ctx, + fc.gh.Owner, + fc.gh.Repo, + pr.GetNumber(), + opts, + ) + if err != nil { + fc.gh.Logger.Errorf("Unable to list changed files for PR %d : %v", pr.GetNumber(), err) + break + } + + for _, file := range files { + if fc.pattern.MatchString(file.GetFilename()) { + fc.gh.Logger.Debugf("File %s is matching pattern %s", file.GetFilename(), fc.pattern.String()) + return true + } + } + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return false +} + +// GetText implements Condition +func (fc *fileChanged) GetText() string { + return fmt.Sprintf("A changed file match this pattern : %s", fc.pattern.String()) +} + +func FileChanged(gh *client.Github, pattern string) Condition { + return &fileChanged{gh: gh, pattern: regexp.MustCompile(pattern)} +} diff --git a/misc/github-bot/condition/label.go b/misc/github-bot/condition/label.go new file mode 100644 index 00000000000..3c6b929afe5 --- /dev/null +++ b/misc/github-bot/condition/label.go @@ -0,0 +1,34 @@ +package condition + +import ( + "fmt" + "regexp" + + "github.com/google/go-github/v66/github" +) + +// Label Condition +type label struct { + pattern *regexp.Regexp +} + +var _ Condition = &label{} + +// Validate implements Condition +func (l *label) Validate(pr *github.PullRequest) bool { + for _, label := range pr.Labels { + if l.pattern.MatchString(label.GetName()) { + return true + } + } + return false +} + +// GetText implements Condition +func (l *label) GetText() string { + return fmt.Sprintf("A label match this pattern : %s", l.pattern.String()) +} + +func Label(pattern string) Condition { + return &label{pattern: regexp.MustCompile(pattern)} +} diff --git a/misc/github-bot/config.go b/misc/github-bot/config.go new file mode 100644 index 00000000000..d5c586fa52e --- /dev/null +++ b/misc/github-bot/config.go @@ -0,0 +1,59 @@ +package main + +import ( + "bot/client" + c "bot/condition" + r "bot/requirement" +) + +type automaticCheck struct { + Description string + If c.Condition + Then r.Requirement +} + +type manualCheck struct { + Description string + If c.Condition + // TODO: remomve that + CheckedBy string +} + +func config(gh *client.Github) ([]automaticCheck, []manualCheck) { + return []automaticCheck{ + { + Description: "Changes on 'tm2' folder should be reviewed/authored at least one member of both EU and US teams", + If: c.And( + c.FileChanged(gh, "tm2"), + c.BaseBranch("main"), + ), + Then: r.And( + r.Or( + r.ReviewByTeamMembers(gh, "eu", 1), + r.AuthorInTeam(gh, "eu"), + ), + r.Or( + r.ReviewByTeamMembers(gh, "us", 1), + r.AuthorInTeam(gh, "us"), + ), + ), + }, { + Description: "Maintainer must be able to edit this pull request", + If: c.Always(), + Then: r.MaintainerCanModify(), + }, + }, []manualCheck{ + { + Description: "Manual check #1", + CheckedBy: "", + }, + { + Description: "Manual check #2", + CheckedBy: "aeddi", + }, + { + Description: "Manual check #3", + CheckedBy: "moul", + }, + } +} diff --git a/misc/github-bot/go.mod b/misc/github-bot/go.mod new file mode 100644 index 00000000000..b72011215f8 --- /dev/null +++ b/misc/github-bot/go.mod @@ -0,0 +1,10 @@ +module bot + +go 1.22.2 + +require ( + github.com/google/go-github/v66 v66.0.0 + github.com/sethvargo/go-githubactions v1.3.0 +) + +require github.com/google/go-querystring v1.1.0 // indirect diff --git a/misc/github-bot/go.sum b/misc/github-bot/go.sum new file mode 100644 index 00000000000..ee3974d68e8 --- /dev/null +++ b/misc/github-bot/go.sum @@ -0,0 +1,10 @@ +github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= +github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= +github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= +github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= +github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= +github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A= +github.com/sethvargo/go-githubactions v1.3.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80= +golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/misc/github-bot/logger/action.go b/misc/github-bot/logger/action.go new file mode 100644 index 00000000000..c6d10429e62 --- /dev/null +++ b/misc/github-bot/logger/action.go @@ -0,0 +1,43 @@ +package logger + +import ( + "github.com/sethvargo/go-githubactions" +) + +type actionLogger struct{} + +var _ Logger = &actionLogger{} + +// Debugf implements Logger. +func (a *actionLogger) Debugf(msg string, args ...any) { + githubactions.Debugf(msg, args...) +} + +// Errorf implements Logger. +func (a *actionLogger) Errorf(msg string, args ...any) { + githubactions.Errorf(msg, args...) +} + +// Fatalf implements Logger. +func (a *actionLogger) Fatalf(msg string, args ...any) { + githubactions.Fatalf(msg, args...) +} + +// Infof implements Logger. +func (a *actionLogger) Infof(msg string, args ...any) { + githubactions.Infof(msg, args...) +} + +// Noticef implements Logger. +func (a *actionLogger) Noticef(msg string, args ...any) { + githubactions.Noticef(msg, args...) +} + +// Warningf implements Logger. +func (a *actionLogger) Warningf(msg string, args ...any) { + githubactions.Warningf(msg, args...) +} + +func newActionLogger() Logger { + return &actionLogger{} +} diff --git a/misc/github-bot/logger/logger.go b/misc/github-bot/logger/logger.go new file mode 100644 index 00000000000..53b50c6ed9a --- /dev/null +++ b/misc/github-bot/logger/logger.go @@ -0,0 +1,34 @@ +package logger + +import ( + "os" +) + +// All Logger methods follow the standard fmt.Printf convention +type Logger interface { + // Debugf prints a debug-level message + Debugf(msg string, args ...any) + + // Noticef prints a notice-level message + Noticef(msg string, args ...any) + + // Warningf prints a warning-level message + Warningf(msg string, args ...any) + + // Errorf prints a error-level message + Errorf(msg string, args ...any) + + // Fatalf prints a error-level message and exits + Fatalf(msg string, args ...any) + + // Infof prints message to stdout without any level annotations + Infof(msg string, args ...any) +} + +func NewLogger(verbose bool) Logger { + if _, isAction := os.LookupEnv("GITHUB_ACTION"); isAction { + return newActionLogger() + } + + return newTermLogger(verbose) +} diff --git a/misc/github-bot/logger/terminal.go b/misc/github-bot/logger/terminal.go new file mode 100644 index 00000000000..aeb3835e170 --- /dev/null +++ b/misc/github-bot/logger/terminal.go @@ -0,0 +1,55 @@ +package logger + +import ( + "fmt" + "log/slog" + "os" +) + +type termLogger struct{} + +var _ Logger = &termLogger{} + +// Debugf implements Logger +func (s *termLogger) Debugf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Debug(fmt.Sprintf(msg, args...)) +} + +// Errorf implements Logger +func (s *termLogger) Errorf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Error(fmt.Sprintf(msg, args...)) +} + +// Fatalf implements Logger +func (s *termLogger) Fatalf(msg string, args ...any) { + s.Errorf(msg, args...) + os.Exit(1) +} + +// Infof implements Logger +func (s *termLogger) Infof(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Info(fmt.Sprintf(msg, args...)) +} + +// Noticef implements Logger +func (s *termLogger) Noticef(msg string, args ...any) { + // Alias to info on terminal since notice level only exists on Github Actions + s.Infof(msg, args...) +} + +// Warningf implements Logger +func (s *termLogger) Warningf(msg string, args ...any) { + msg = fmt.Sprintf("%s\n", msg) + slog.Warn(fmt.Sprintf(msg, args...)) +} + +func newTermLogger(verbose bool) Logger { + if verbose { + slog.SetLogLoggerLevel(slog.LevelDebug) + } + + return &termLogger{} +} diff --git a/misc/github-bot/main.go b/misc/github-bot/main.go new file mode 100644 index 00000000000..e7548ebec8c --- /dev/null +++ b/misc/github-bot/main.go @@ -0,0 +1,112 @@ +package main + +import ( + "bot/client" + "bot/param" + "bytes" + "text/template" + + "github.com/google/go-github/v66/github" +) + +func main() { + // Get params by parsing CLI flags and/or Github Actions context + params := param.Get() + + // Init Github API client + gh := client.New(params) + + // TODO:cleanup + onCommentUpdated(gh) + + // Get a slice of pull requests to process + var ( + prs []*github.PullRequest + err error + ) + + // If requested, get all opened pull requests + if params.PrAll { + opts := &github.PullRequestListOptions{ + State: "open", + Sort: "updated", + Direction: "desc", + } + + prs, _, err = gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) + if err != nil { + gh.Logger.Fatalf("Unable to get all opened pull requests : %v", err) + } + + // Or get only specified pull request(s) (flag or Github Action context) + } else { + 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) + if err != nil { + gh.Logger.Fatalf("Unable to get specified pull request (%d) : %v", prNum, err) + } + prs[i] = pr + } + } + + tmplFile := "comment.tmpl" + tmpl, err := template.New(tmplFile).ParseFiles(tmplFile) + if err != nil { + panic(err) + } + + auto, manual := config(gh) + // Process all pull requests + for _, pr := range prs { + com := Comment{} + for _, rule := range auto { + if rule.If.Validate(pr) { + gh.Logger.Infof(rule.If.GetText()) + + c := Auto{Description: rule.Description, Met: false} + + if !rule.Then.Validate(pr) { + gh.Logger.Infof(rule.Then.GetText()) + c.Met = true + } + + com.Auto = append(com.Auto, c) + } + } + + for _, rule := range manual { + com.Manual = append(com.Manual, Manual{ + Description: rule.Description, + CheckedBy: rule.CheckedBy, + }) + } + + var commentBytes bytes.Buffer + err = tmpl.Execute(&commentBytes, com) + if err != nil { + panic(err) + } + + comment := gh.SetBotComment(commentBytes.String(), pr.GetNumber()) + + context := "Merge Requirements" + state := "pending" + targetURL := comment.GetHTMLURL() + description := "Some requirements are not met yet. See bot comment." + + if _, _, err := gh.Client.Repositories.CreateStatus( + gh.Ctx, + gh.Owner, + gh.Repo, + pr.GetHead().GetSHA(), + &github.RepoStatus{ + Context: &context, + State: &state, + TargetURL: &targetURL, + Description: &description, + }); err != nil { + gh.Logger.Errorf("Unable to create status on PR %d : %v", pr.GetNumber(), err) + } + } +} diff --git a/misc/github-bot/param/param.go b/misc/github-bot/param/param.go new file mode 100644 index 00000000000..54717511a21 --- /dev/null +++ b/misc/github-bot/param/param.go @@ -0,0 +1,92 @@ +package param + +import ( + "flag" + "fmt" + "os" + + "github.com/sethvargo/go-githubactions" +) + +type Params struct { + Owner string + Repo string + PrAll bool + PrNums PrList + Verbose bool + DryRun bool + Timeout uint +} + +// Get Params from both cli flags and/or Github Actions context +func Get() Params { + p := Params{} + + // Add cmd description to usage message + flag.Usage = func() { + fmt.Fprint(flag.CommandLine.Output(), "This tool checks if requirements for a PR to be merged are met (defined in config.go) and display PR status checks accordingly.\n") + fmt.Fprint(flag.CommandLine.Output(), "A valid Github Token must be provided by setting the GITHUB_TOKEN env variable.\n\n") + flag.PrintDefaults() + } + + // Helper to display an error + usage message before exiting + errorUsage := func(error string) { + fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", error) + flag.Usage() + os.Exit(1) + } + + // Flags definition + flag.StringVar(&p.Owner, "owner", "", "owner of the repo to check, if empty, will be retrieved from Github Actions context") + flag.StringVar(&p.Repo, "repo", "", "repo to check, if empty, will be retrieved from Github Actions context") + flag.BoolVar(&p.PrAll, "pr-all", false, "validate all pull requests opened on the repo") + flag.TextVar(&p.PrNums, "pr-numbers", PrList(nil), "pull request(s) to validate, must be a comma seperated list of PR numbers, e.g '42,1337,2345'. If empty, PR to check will be retrived from Github Actions context") + flag.BoolVar(&p.Verbose, "verbose", false, "set logging level to debug") + flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are met without updating PR checks on Github web interface") + flag.UintVar(&p.Timeout, "timeout", 0, "timeout in milliseconds") + flag.Parse() + + // If any arg remain after flags processing + if len(flag.Args()) > 0 { + errorUsage(fmt.Sprintf("Unknown arg(s) provided : %v", flag.Args())) + } + + // Check if flags are coherents + if p.PrAll && len(p.PrNums) != 0 { + errorUsage("You must specify at most one of '-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) { + 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 == "" { + 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 == "" { + 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 { + const errMsg = "Unable to retrieve pull request number from Github Actions context, you may want to set it using -pr-numbers flag" + issue, ok := actionCtx.Event["issue"].(map[string]any) + if !ok { + errorUsage(errMsg) + } + num, ok := issue["number"].(float64) + if !ok || num <= 0 { + errorUsage(errMsg) + } + p.PrNums = PrList([]int{int(num)}) + } + } + + return p +} diff --git a/misc/github-bot/param/prlist.go b/misc/github-bot/param/prlist.go new file mode 100644 index 00000000000..96a04ebce14 --- /dev/null +++ b/misc/github-bot/param/prlist.go @@ -0,0 +1,45 @@ +package param + +import ( + "encoding" + "fmt" + "strconv" + "strings" +) + +type PrList []int + +// PrList is both a TextMarshaler and a TextUnmarshaler +var ( + _ encoding.TextMarshaler = PrList{} + _ encoding.TextUnmarshaler = &PrList{} +) + +// MarshalText implements encoding.TextMarshaler. +func (p PrList) MarshalText() (text []byte, err error) { + prNumsStr := make([]string, len(p)) + + for i, prNum := range p { + prNumsStr[i] = strconv.Itoa(prNum) + } + + return []byte(strings.Join(prNumsStr, ",")), nil +} + +// UnmarshalText implements encoding.TextUnmarshaler. +func (p *PrList) UnmarshalText(text []byte) error { + for _, prNumStr := range strings.Split(string(text), ",") { + prNum, err := strconv.Atoi(strings.TrimSpace(prNumStr)) + if err != nil { + return err + } + + if prNum <= 0 { + return fmt.Errorf("invalid pull request number (<= 0) : original(%s) parsed(%d)", prNumStr, prNum) + } + + *p = append(*p, prNum) + } + + return nil +} diff --git a/misc/github-bot/requirement/assignee.go b/misc/github-bot/requirement/assignee.go new file mode 100644 index 00000000000..0b308e95c41 --- /dev/null +++ b/misc/github-bot/requirement/assignee.go @@ -0,0 +1,47 @@ +package requirement + +import ( + "bot/client" + + "github.com/google/go-github/v66/github" +) + +// Assignee Requirement +type assignee struct { + gh *client.Github + user string +} + +var _ Requirement = &assignee{} + +// GetText implements Requirement +func (a *assignee) GetText() string { + return "TODO" +} + +// Validate implements Requirement +func (a *assignee) Validate(pr *github.PullRequest) bool { + // Check if user was already assigned to PR + for _, assignee := range pr.Assignees { + if a.user == assignee.GetLogin() { + return true + } + } + + // If not, assign it + if _, _, err := a.gh.Client.Issues.AddAssignees( + a.gh.Ctx, + a.gh.Owner, + a.gh.Repo, + pr.GetNumber(), + []string{a.user}, + ); err != nil { + a.gh.Logger.Errorf("Unable to assign user %s to PR %d : %v", a.user, pr.GetNumber(), err) + return false + } + return true +} + +func Assignee(gh *client.Github, user string) Requirement { + return &assignee{gh: gh, user: user} +} diff --git a/misc/github-bot/requirement/author.go b/misc/github-bot/requirement/author.go new file mode 100644 index 00000000000..0d5808436f5 --- /dev/null +++ b/misc/github-bot/requirement/author.go @@ -0,0 +1,14 @@ +package requirement + +import ( + "bot/client" + "bot/condition" +) + +func Author(user string) Requirement { + return condition.Author(user) +} + +func AuthorInTeam(gh *client.Github, team string) Requirement { + return condition.AuthorInTeam(gh, team) +} diff --git a/misc/github-bot/requirement/boolean.go b/misc/github-bot/requirement/boolean.go new file mode 100644 index 00000000000..ddb2af3c91a --- /dev/null +++ b/misc/github-bot/requirement/boolean.go @@ -0,0 +1,100 @@ +package requirement + +import ( + "fmt" + + "github.com/google/go-github/v66/github" +) + +// And Requirement +type and struct { + requirements []Requirement +} + +var _ Requirement = &and{} + +// Validate implements Requirement +func (a *and) Validate(pr *github.PullRequest) bool { + for _, requirement := range a.requirements { + if !requirement.Validate(pr) { + return false + } + } + + return true +} + +// GetText implements Requirement +func (a *and) GetText() string { + text := fmt.Sprintf("(%s", a.requirements[0].GetText()) + for _, requirement := range a.requirements[1:] { + text = fmt.Sprintf("%s AND %s", text, requirement.GetText()) + } + + return text + ")" +} + +func And(requirements ...Requirement) Requirement { + if len(requirements) < 2 { + panic("You should pass at least 2 requirements to And()") + } + + return &and{requirements} +} + +// Or Requirement +type or struct { + requirements []Requirement +} + +var _ Requirement = &or{} + +// Validate implements Requirement +func (o *or) Validate(pr *github.PullRequest) bool { + for _, requirement := range o.requirements { + if !requirement.Validate(pr) { + return false + } + } + + return true +} + +// GetText implements Requirement +func (o *or) GetText() string { + text := fmt.Sprintf("(%s", o.requirements[0].GetText()) + for _, requirement := range o.requirements[1:] { + text = fmt.Sprintf("%s OR %s", text, requirement.GetText()) + } + + return text + ")" +} + +func Or(requirements ...Requirement) Requirement { + if len(requirements) < 2 { + panic("You should pass at least 2 requirements to Or()") + } + + return &or{requirements} +} + +// Not Requirement +type not struct { + req Requirement +} + +var _ Requirement = ¬{} + +// Validate implements Requirement +func (n *not) Validate(pr *github.PullRequest) bool { + return !n.req.Validate(pr) +} + +// GetText implements Requirement +func (n *not) GetText() string { + return fmt.Sprintf("NOT %s", n.req.GetText()) +} + +func Not(req Requirement) Requirement { + return ¬{req} +} diff --git a/misc/github-bot/requirement/checkbox.go b/misc/github-bot/requirement/checkbox.go new file mode 100644 index 00000000000..fe60467a9e2 --- /dev/null +++ b/misc/github-bot/requirement/checkbox.go @@ -0,0 +1,29 @@ +package requirement + +import ( + "bot/client" + + "github.com/google/go-github/v66/github" +) + +// Checkbox Requirement +type checkbox struct { + gh *client.Github + desc string +} + +var _ Requirement = &checkbox{} + +// GetText implements Requirement +func (c *checkbox) GetText() string { + return "" +} + +// Validate implements Requirement +func (c *checkbox) Validate(pr *github.PullRequest) bool { + return false +} + +func Checkbox(gh *client.Github, desc string) Requirement { + return &checkbox{gh: gh, desc: desc} +} diff --git a/misc/github-bot/requirement/label.go b/misc/github-bot/requirement/label.go new file mode 100644 index 00000000000..f9523ce5238 --- /dev/null +++ b/misc/github-bot/requirement/label.go @@ -0,0 +1,47 @@ +package requirement + +import ( + "bot/client" + + "github.com/google/go-github/v66/github" +) + +// Label Requirement +type label struct { + gh *client.Github + name string +} + +var _ Requirement = &label{} + +// Validate implements Requirement +func (l *label) Validate(pr *github.PullRequest) bool { + // Check if label was already added to PR + for _, label := range pr.Labels { + if l.name == label.GetName() { + return true + } + } + + // If not, add it + if _, _, err := l.gh.Client.Issues.AddLabelsToIssue( + l.gh.Ctx, + l.gh.Owner, + l.gh.Repo, + pr.GetNumber(), + []string{l.name}, + ); err != nil { + l.gh.Logger.Errorf("Unable to add label %s to PR %d : %v", l.name, pr.GetNumber(), err) + return false + } + return true +} + +// GetText implements Requirement +func (l *label) GetText() string { + return "TODO" +} + +func Label(gh *client.Github, name string) Requirement { + return &label{gh, name} +} diff --git a/misc/github-bot/requirement/maintainer.go b/misc/github-bot/requirement/maintainer.go new file mode 100644 index 00000000000..96ba5910cbe --- /dev/null +++ b/misc/github-bot/requirement/maintainer.go @@ -0,0 +1,24 @@ +package requirement + +import ( + "github.com/google/go-github/v66/github" +) + +// MaintainerCanModify Requirement +type maintainerCanModify struct{} + +var _ Requirement = &maintainerCanModify{} + +// GetText implements Requirement +func (a *maintainerCanModify) GetText() string { + return "TODO" +} + +// Validate implements Requirement +func (a *maintainerCanModify) Validate(pr *github.PullRequest) bool { + return pr.GetMaintainerCanModify() +} + +func MaintainerCanModify() Requirement { + return &maintainerCanModify{} +} diff --git a/misc/github-bot/requirement/requirement.go b/misc/github-bot/requirement/requirement.go new file mode 100644 index 00000000000..982cf7ee14a --- /dev/null +++ b/misc/github-bot/requirement/requirement.go @@ -0,0 +1,13 @@ +package requirement + +import ( + "github.com/google/go-github/v66/github" +) + +type Requirement interface { + // Check if the Requirement is met by this PR + Validate(pr *github.PullRequest) bool + + // Get a text representation of this Requirement + GetText() string +} diff --git a/misc/github-bot/requirement/reviewer.go b/misc/github-bot/requirement/reviewer.go new file mode 100644 index 00000000000..8882be00634 --- /dev/null +++ b/misc/github-bot/requirement/reviewer.go @@ -0,0 +1,139 @@ +package requirement + +import ( + "bot/client" + + "github.com/google/go-github/v66/github" +) + +// Reviewer Requirement +type reviewByUser struct { + gh *client.Github + user string +} + +var _ Requirement = &reviewByUser{} + +// GetText implements Requirement +func (r *reviewByUser) GetText() string { + return "TODO" +} + +// Validate implements Requirement +func (r *reviewByUser) Validate(pr *github.PullRequest) bool { + // If not a dry run, make the user a reviewer if he's not already + if !r.gh.DryRun { + requested := false + if reviewers := r.gh.ListPrReviewers(pr.GetNumber()); reviewers != nil { + for _, user := range reviewers.Users { + if user.GetLogin() == r.user { + requested = true + break + } + } + } + + if requested { + r.gh.Logger.Debugf("Review of user %s already requested on PR %d", r.user, pr.GetNumber()) + } else { + r.gh.Logger.Debugf("Requesting review from user %s on PR %d", r.user, pr.GetNumber()) + if _, _, err := r.gh.Client.PullRequests.RequestReviewers( + r.gh.Ctx, + r.gh.Owner, + r.gh.Repo, + pr.GetNumber(), + github.ReviewersRequest{ + Reviewers: []string{r.user}, + }, + ); err != nil { + r.gh.Logger.Errorf("Unable to request review from user %s on PR %d : %v", r.user, pr.GetNumber(), err) + } + } + } + + // Check if user already approved this PR + for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { + if review.GetUser().GetLogin() == r.user { + r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) + return review.GetState() == "APPROVED" + } + } + r.gh.Logger.Debugf("User %s has not approved PR %d yet", r.user, pr.GetNumber()) + + return false +} + +func ReviewByUser(gh *client.Github, user string) Requirement { + return &reviewByUser{gh, user} +} + +// Reviewer Requirement +type reviewByTeamMembers struct { + gh *client.Github + team string + count uint +} + +var _ Requirement = &reviewByTeamMembers{} + +// GetText implements Requirement +func (r *reviewByTeamMembers) GetText() string { + return "TODO" +} + +// Validate implements Requirement +func (r *reviewByTeamMembers) Validate(pr *github.PullRequest) bool { + // If not a dry run, make the user a reviewer if he's not already + if !r.gh.DryRun { + requested := false + if reviewers := r.gh.ListPrReviewers(pr.GetNumber()); reviewers != nil { + for _, team := range reviewers.Teams { + if team.GetSlug() == r.team { + requested = true + break + } + } + } + + if requested { + r.gh.Logger.Debugf("Review of team %s already requested on PR %d", r.team, pr.GetNumber()) + } else { + r.gh.Logger.Debugf("Requesting review from team %s on PR %d", r.team, pr.GetNumber()) + if _, _, err := r.gh.Client.PullRequests.RequestReviewers( + r.gh.Ctx, + r.gh.Owner, + r.gh.Repo, + pr.GetNumber(), + github.ReviewersRequest{ + TeamReviewers: []string{r.team}, + }, + ); err != nil { + r.gh.Logger.Errorf("Unable to request review from team %s on PR %d : %v", r.team, pr.GetNumber(), err) + } + } + } + + // Check how many members of this team already approved this PR + var approved uint = 0 + members := r.gh.ListTeamMembers(r.team) + for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { + for _, member := range members { + if review.GetUser().GetLogin() == member.GetLogin() { + if review.GetState() == "APPROVED" { + approved += 1 + } + r.gh.Logger.Debugf("Member %s from team %s already reviewed PR %d with state %s (%d/%d required approval(s))", member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), approved, r.count) + if approved >= r.count { + return true + } + } + } + } + r.gh.Logger.Debugf("Not enough members from team %s have approved PR %d (%d/%d required approval(s))", r.team, pr.GetNumber(), approved, r.count) + + return false +} + +func ReviewByTeamMembers(gh *client.Github, team string, count uint) Requirement { + return &reviewByTeamMembers{gh, team, count} +} From cc585b0cda680c6bcf0fff8e47d517016c1c4d0c Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 6 Nov 2024 02:18:40 +0100 Subject: [PATCH 02/44] typos --- contribs/github_bot/comment.go | 2 +- contribs/github_bot/main.go | 2 +- contribs/github_bot/param/param.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contribs/github_bot/comment.go b/contribs/github_bot/comment.go index e61b82637a2..15cab433bbd 100644 --- a/contribs/github_bot/comment.go +++ b/contribs/github_bot/comment.go @@ -214,7 +214,7 @@ func handleCommentUpdate(gh *client.GitHub) { // Update comment with username if edited != "" { gh.SetBotComment(edited, int(num)) - gh.Logger.Debugf("Comment manual checks updated successfuly") + gh.Logger.Debugf("Comment manual checks updated successfully") } } diff --git a/contribs/github_bot/main.go b/contribs/github_bot/main.go index 6e524deb1f0..3b2546f3b9e 100644 --- a/contribs/github_bot/main.go +++ b/contribs/github_bot/main.go @@ -50,7 +50,7 @@ func main() { } } - // Process all pull requests in parrallel + // Process all pull requests in parallel autoRules, manualRules := config(gh) var wg sync.WaitGroup wg.Add(len(prs)) diff --git a/contribs/github_bot/param/param.go b/contribs/github_bot/param/param.go index ea6af698ca3..3a3dbfc7bb2 100644 --- a/contribs/github_bot/param/param.go +++ b/contribs/github_bot/param/param.go @@ -30,8 +30,8 @@ func Get() Params { } // Helper to display an error + usage message before exiting - errorUsage := func(error string) { - fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", error) + errorUsage := func(err string) { + fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", err) flag.Usage() os.Exit(1) } @@ -40,7 +40,7 @@ func Get() Params { flag.StringVar(&p.Owner, "owner", "", "owner of the repo to process, if empty, will be retrieved from GitHub Actions context") flag.StringVar(&p.Repo, "repo", "", "repo to process, if empty, will be retrieved from GitHub Actions context") flag.BoolVar(&p.PrAll, "pr-all", false, "process all opened pull requests") - flag.TextVar(&p.PrNums, "pr-numbers", PrList(nil), "pull request(s) to process, must be a comma seperated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrived from GitHub Actions context") + flag.TextVar(&p.PrNums, "pr-numbers", 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") flag.BoolVar(&p.Verbose, "verbose", false, "set logging level to debug") flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are met without updating PR checks on GitHub web interface") flag.UintVar(&p.Timeout, "timeout", 0, "timeout in milliseconds") From 3a33f2ac8f8ea41642a74b4778099d7d93bb2fb2 Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 6 Nov 2024 12:00:27 +0100 Subject: [PATCH 03/44] chore: move folder --- .../client/client.go | 0 .../{github_bot => github-bot}/comment.go | 0 .../{github_bot => github-bot}/comment.tmpl | 0 .../condition/assignee.go | 0 .../condition/author.go | 0 .../condition/boolean.go | 0 .../condition/branch.go | 0 .../condition/condition.go | 0 .../condition/constant.go | 0 .../condition/file.go | 0 .../condition/label.go | 0 contribs/{github_bot => github-bot}/config.go | 0 contribs/{github_bot => github-bot}/go.mod | 0 contribs/{github_bot => github-bot}/go.sum | 0 .../logger/action.go | 0 .../logger/logger.go | 0 .../logger/terminal.go | 0 contribs/{github_bot => github-bot}/main.go | 0 .../{github_bot => github-bot}/param/param.go | 0 .../param/prlist.go | 0 .../requirement/assignee.go | 0 .../requirement/author.go | 0 .../requirement/boolean.go | 0 .../requirement/branch.go | 0 .../requirement/label.go | 0 .../requirement/maintainer.go | 0 .../requirement/requirement.go | 0 .../requirement/reviewer.go | 0 .../{github_bot => github-bot}/utils/tree.go | 0 misc/github-bot/client/client.go | 235 ------------------ misc/github-bot/comment.go | 20 -- misc/github-bot/comment.tmpl | 21 -- misc/github-bot/condition/assignee.go | 64 ----- misc/github-bot/condition/author.go | 57 ----- misc/github-bot/condition/boolean.go | 100 -------- misc/github-bot/condition/branch.go | 50 ---- misc/github-bot/condition/condition.go | 13 - misc/github-bot/condition/constant.go | 43 ---- misc/github-bot/condition/file.go | 61 ----- misc/github-bot/condition/label.go | 34 --- misc/github-bot/config.go | 59 ----- misc/github-bot/go.mod | 10 - misc/github-bot/go.sum | 10 - misc/github-bot/logger/action.go | 43 ---- misc/github-bot/logger/logger.go | 34 --- misc/github-bot/logger/terminal.go | 55 ---- misc/github-bot/main.go | 112 --------- misc/github-bot/param/param.go | 92 ------- misc/github-bot/param/prlist.go | 45 ---- misc/github-bot/requirement/assignee.go | 47 ---- misc/github-bot/requirement/author.go | 14 -- misc/github-bot/requirement/boolean.go | 100 -------- misc/github-bot/requirement/checkbox.go | 29 --- misc/github-bot/requirement/label.go | 47 ---- misc/github-bot/requirement/maintainer.go | 24 -- misc/github-bot/requirement/requirement.go | 13 - misc/github-bot/requirement/reviewer.go | 139 ----------- 57 files changed, 1571 deletions(-) rename contribs/{github_bot => github-bot}/client/client.go (100%) rename contribs/{github_bot => github-bot}/comment.go (100%) rename contribs/{github_bot => github-bot}/comment.tmpl (100%) rename contribs/{github_bot => github-bot}/condition/assignee.go (100%) rename contribs/{github_bot => github-bot}/condition/author.go (100%) rename contribs/{github_bot => github-bot}/condition/boolean.go (100%) rename contribs/{github_bot => github-bot}/condition/branch.go (100%) rename contribs/{github_bot => github-bot}/condition/condition.go (100%) rename contribs/{github_bot => github-bot}/condition/constant.go (100%) rename contribs/{github_bot => github-bot}/condition/file.go (100%) rename contribs/{github_bot => github-bot}/condition/label.go (100%) rename contribs/{github_bot => github-bot}/config.go (100%) rename contribs/{github_bot => github-bot}/go.mod (100%) rename contribs/{github_bot => github-bot}/go.sum (100%) rename contribs/{github_bot => github-bot}/logger/action.go (100%) rename contribs/{github_bot => github-bot}/logger/logger.go (100%) rename contribs/{github_bot => github-bot}/logger/terminal.go (100%) rename contribs/{github_bot => github-bot}/main.go (100%) rename contribs/{github_bot => github-bot}/param/param.go (100%) rename contribs/{github_bot => github-bot}/param/prlist.go (100%) rename contribs/{github_bot => github-bot}/requirement/assignee.go (100%) rename contribs/{github_bot => github-bot}/requirement/author.go (100%) rename contribs/{github_bot => github-bot}/requirement/boolean.go (100%) rename contribs/{github_bot => github-bot}/requirement/branch.go (100%) rename contribs/{github_bot => github-bot}/requirement/label.go (100%) rename contribs/{github_bot => github-bot}/requirement/maintainer.go (100%) rename contribs/{github_bot => github-bot}/requirement/requirement.go (100%) rename contribs/{github_bot => github-bot}/requirement/reviewer.go (100%) rename contribs/{github_bot => github-bot}/utils/tree.go (100%) delete mode 100644 misc/github-bot/client/client.go delete mode 100644 misc/github-bot/comment.go delete mode 100644 misc/github-bot/comment.tmpl delete mode 100644 misc/github-bot/condition/assignee.go delete mode 100644 misc/github-bot/condition/author.go delete mode 100644 misc/github-bot/condition/boolean.go delete mode 100644 misc/github-bot/condition/branch.go delete mode 100644 misc/github-bot/condition/condition.go delete mode 100644 misc/github-bot/condition/constant.go delete mode 100644 misc/github-bot/condition/file.go delete mode 100644 misc/github-bot/condition/label.go delete mode 100644 misc/github-bot/config.go delete mode 100644 misc/github-bot/go.mod delete mode 100644 misc/github-bot/go.sum delete mode 100644 misc/github-bot/logger/action.go delete mode 100644 misc/github-bot/logger/logger.go delete mode 100644 misc/github-bot/logger/terminal.go delete mode 100644 misc/github-bot/main.go delete mode 100644 misc/github-bot/param/param.go delete mode 100644 misc/github-bot/param/prlist.go delete mode 100644 misc/github-bot/requirement/assignee.go delete mode 100644 misc/github-bot/requirement/author.go delete mode 100644 misc/github-bot/requirement/boolean.go delete mode 100644 misc/github-bot/requirement/checkbox.go delete mode 100644 misc/github-bot/requirement/label.go delete mode 100644 misc/github-bot/requirement/maintainer.go delete mode 100644 misc/github-bot/requirement/requirement.go delete mode 100644 misc/github-bot/requirement/reviewer.go diff --git a/contribs/github_bot/client/client.go b/contribs/github-bot/client/client.go similarity index 100% rename from contribs/github_bot/client/client.go rename to contribs/github-bot/client/client.go diff --git a/contribs/github_bot/comment.go b/contribs/github-bot/comment.go similarity index 100% rename from contribs/github_bot/comment.go rename to contribs/github-bot/comment.go diff --git a/contribs/github_bot/comment.tmpl b/contribs/github-bot/comment.tmpl similarity index 100% rename from contribs/github_bot/comment.tmpl rename to contribs/github-bot/comment.tmpl diff --git a/contribs/github_bot/condition/assignee.go b/contribs/github-bot/condition/assignee.go similarity index 100% rename from contribs/github_bot/condition/assignee.go rename to contribs/github-bot/condition/assignee.go diff --git a/contribs/github_bot/condition/author.go b/contribs/github-bot/condition/author.go similarity index 100% rename from contribs/github_bot/condition/author.go rename to contribs/github-bot/condition/author.go diff --git a/contribs/github_bot/condition/boolean.go b/contribs/github-bot/condition/boolean.go similarity index 100% rename from contribs/github_bot/condition/boolean.go rename to contribs/github-bot/condition/boolean.go diff --git a/contribs/github_bot/condition/branch.go b/contribs/github-bot/condition/branch.go similarity index 100% rename from contribs/github_bot/condition/branch.go rename to contribs/github-bot/condition/branch.go diff --git a/contribs/github_bot/condition/condition.go b/contribs/github-bot/condition/condition.go similarity index 100% rename from contribs/github_bot/condition/condition.go rename to contribs/github-bot/condition/condition.go diff --git a/contribs/github_bot/condition/constant.go b/contribs/github-bot/condition/constant.go similarity index 100% rename from contribs/github_bot/condition/constant.go rename to contribs/github-bot/condition/constant.go diff --git a/contribs/github_bot/condition/file.go b/contribs/github-bot/condition/file.go similarity index 100% rename from contribs/github_bot/condition/file.go rename to contribs/github-bot/condition/file.go diff --git a/contribs/github_bot/condition/label.go b/contribs/github-bot/condition/label.go similarity index 100% rename from contribs/github_bot/condition/label.go rename to contribs/github-bot/condition/label.go diff --git a/contribs/github_bot/config.go b/contribs/github-bot/config.go similarity index 100% rename from contribs/github_bot/config.go rename to contribs/github-bot/config.go diff --git a/contribs/github_bot/go.mod b/contribs/github-bot/go.mod similarity index 100% rename from contribs/github_bot/go.mod rename to contribs/github-bot/go.mod diff --git a/contribs/github_bot/go.sum b/contribs/github-bot/go.sum similarity index 100% rename from contribs/github_bot/go.sum rename to contribs/github-bot/go.sum diff --git a/contribs/github_bot/logger/action.go b/contribs/github-bot/logger/action.go similarity index 100% rename from contribs/github_bot/logger/action.go rename to contribs/github-bot/logger/action.go diff --git a/contribs/github_bot/logger/logger.go b/contribs/github-bot/logger/logger.go similarity index 100% rename from contribs/github_bot/logger/logger.go rename to contribs/github-bot/logger/logger.go diff --git a/contribs/github_bot/logger/terminal.go b/contribs/github-bot/logger/terminal.go similarity index 100% rename from contribs/github_bot/logger/terminal.go rename to contribs/github-bot/logger/terminal.go diff --git a/contribs/github_bot/main.go b/contribs/github-bot/main.go similarity index 100% rename from contribs/github_bot/main.go rename to contribs/github-bot/main.go diff --git a/contribs/github_bot/param/param.go b/contribs/github-bot/param/param.go similarity index 100% rename from contribs/github_bot/param/param.go rename to contribs/github-bot/param/param.go diff --git a/contribs/github_bot/param/prlist.go b/contribs/github-bot/param/prlist.go similarity index 100% rename from contribs/github_bot/param/prlist.go rename to contribs/github-bot/param/prlist.go diff --git a/contribs/github_bot/requirement/assignee.go b/contribs/github-bot/requirement/assignee.go similarity index 100% rename from contribs/github_bot/requirement/assignee.go rename to contribs/github-bot/requirement/assignee.go diff --git a/contribs/github_bot/requirement/author.go b/contribs/github-bot/requirement/author.go similarity index 100% rename from contribs/github_bot/requirement/author.go rename to contribs/github-bot/requirement/author.go diff --git a/contribs/github_bot/requirement/boolean.go b/contribs/github-bot/requirement/boolean.go similarity index 100% rename from contribs/github_bot/requirement/boolean.go rename to contribs/github-bot/requirement/boolean.go diff --git a/contribs/github_bot/requirement/branch.go b/contribs/github-bot/requirement/branch.go similarity index 100% rename from contribs/github_bot/requirement/branch.go rename to contribs/github-bot/requirement/branch.go diff --git a/contribs/github_bot/requirement/label.go b/contribs/github-bot/requirement/label.go similarity index 100% rename from contribs/github_bot/requirement/label.go rename to contribs/github-bot/requirement/label.go diff --git a/contribs/github_bot/requirement/maintainer.go b/contribs/github-bot/requirement/maintainer.go similarity index 100% rename from contribs/github_bot/requirement/maintainer.go rename to contribs/github-bot/requirement/maintainer.go diff --git a/contribs/github_bot/requirement/requirement.go b/contribs/github-bot/requirement/requirement.go similarity index 100% rename from contribs/github_bot/requirement/requirement.go rename to contribs/github-bot/requirement/requirement.go diff --git a/contribs/github_bot/requirement/reviewer.go b/contribs/github-bot/requirement/reviewer.go similarity index 100% rename from contribs/github_bot/requirement/reviewer.go rename to contribs/github-bot/requirement/reviewer.go diff --git a/contribs/github_bot/utils/tree.go b/contribs/github-bot/utils/tree.go similarity index 100% rename from contribs/github_bot/utils/tree.go rename to contribs/github-bot/utils/tree.go diff --git a/misc/github-bot/client/client.go b/misc/github-bot/client/client.go deleted file mode 100644 index 11e3f90a80b..00000000000 --- a/misc/github-bot/client/client.go +++ /dev/null @@ -1,235 +0,0 @@ -package client - -import ( - "bot/logger" - "bot/param" - "context" - "log" - "os" - "time" - - "github.com/google/go-github/v66/github" -) - -const PageSize = 100 - -type Github struct { - Client *github.Client - Ctx context.Context - DryRun bool - Logger logger.Logger - Owner string - Repo string -} - -func (gh *Github) GetBotComment(prNum int) *github.IssueComment { - // List existing comments - var ( - allComments []*github.IssueComment - sort = "created" - direction = "desc" - opts = &github.IssueListCommentsOptions{ - Sort: &sort, - Direction: &direction, - ListOptions: github.ListOptions{ - PerPage: PageSize, - }, - } - ) - - for { - comments, response, err := gh.Client.Issues.ListComments( - gh.Ctx, - gh.Owner, - gh.Repo, - prNum, - opts, - ) - if err != nil { - gh.Logger.Errorf("Unable to list comments for PR %d : %v", prNum, err) - return nil - } - - allComments = append(allComments, comments...) - - if response.NextPage == 0 { - break - } - opts.Page = response.NextPage - } - - // Get current user (bot) - currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") - if err != nil { - gh.Logger.Errorf("Unable to get current user : %v", err) - return nil - } - - // Get the comment created by current user - for _, comment := range allComments { - if comment.GetUser().GetLogin() == currentUser.GetLogin() { - return comment - } - } - - return nil -} - -func (gh *Github) SetBotComment(body string, prNum int) *github.IssueComment { - // Create bot comment if it not already exists - if comment := gh.GetBotComment(prNum); comment == nil { - newComment, _, err := gh.Client.Issues.CreateComment( - gh.Ctx, - gh.Owner, - gh.Repo, - prNum, - &github.IssueComment{Body: &body}, - ) - if err != nil { - gh.Logger.Errorf("Unable to create bot comment for PR %d : %v", prNum, err) - return nil - } - return newComment - } else { - comment.Body = &body - editComment, _, err := gh.Client.Issues.EditComment( - gh.Ctx, - gh.Owner, - gh.Repo, - comment.GetID(), - comment, - ) - if err != nil { - gh.Logger.Errorf("Unable to edit bot comment with ID %d : %v", comment.GetID(), err) - return nil - } - return editComment - } -} - -func (gh *Github) ListTeamMembers(team string) []*github.User { - var ( - allMembers []*github.User - opts = &github.TeamListTeamMembersOptions{ - ListOptions: github.ListOptions{ - PerPage: PageSize, - }, - } - ) - - for { - members, response, err := gh.Client.Teams.ListTeamMembersBySlug( - gh.Ctx, - gh.Owner, - team, - opts, - ) - if err != nil { - gh.Logger.Errorf("Unable to list members for team %s : %v", team, err) - return nil - } - - allMembers = append(allMembers, members...) - - if response.NextPage == 0 { - break - } - opts.Page = response.NextPage - } - - return allMembers -} - -func (gh *Github) ListPrReviewers(prNum int) *github.Reviewers { - var ( - allReviewers = &github.Reviewers{} - opts = &github.ListOptions{ - PerPage: PageSize, - } - ) - - for { - reviewers, response, err := gh.Client.PullRequests.ListReviewers( - gh.Ctx, - gh.Owner, - gh.Repo, - prNum, - opts, - ) - if err != nil { - gh.Logger.Errorf("Unable to list reviewers for PR %d : %v", prNum, err) - return nil - } - - allReviewers.Teams = append(allReviewers.Teams, reviewers.Teams...) - allReviewers.Users = append(allReviewers.Users, reviewers.Users...) - - if response.NextPage == 0 { - break - } - opts.Page = response.NextPage - } - - return allReviewers -} - -func (gh *Github) ListPrReviews(prNum int) []*github.PullRequestReview { - var ( - allReviews []*github.PullRequestReview - opts = &github.ListOptions{ - PerPage: PageSize, - } - ) - - for { - reviews, response, err := gh.Client.PullRequests.ListReviews( - gh.Ctx, - gh.Owner, - gh.Repo, - prNum, - opts, - ) - if err != nil { - gh.Logger.Errorf("Unable to list reviews for PR %d : %v", prNum, err) - return nil - } - - allReviews = append(allReviews, reviews...) - - if response.NextPage == 0 { - break - } - opts.Page = response.NextPage - } - - return allReviews -} - -func New(params param.Params) *Github { - gh := &Github{ - Owner: params.Owner, - Repo: params.Repo, - DryRun: params.DryRun, - } - - // This method will detect if the current process was launched by - // a Github Action or not and will accordingly return a logger suitable for - // the terminal output or for the Github Actions web interface - gh.Logger = logger.NewLogger(params.Verbose) - - // Create context with timeout if specified in flags - if params.Timeout > 0 { - gh.Ctx, _ = context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Millisecond) - } else { - gh.Ctx = context.Background() - } - - // Init Github API Client using token from env - token, set := os.LookupEnv("GITHUB_TOKEN") - if !set { - log.Fatalf("GITHUB_TOKEN is not set in env") - } - gh.Client = github.NewClient(nil).WithAuthToken(token) - - return gh -} diff --git a/misc/github-bot/comment.go b/misc/github-bot/comment.go deleted file mode 100644 index ec884c1bb1c..00000000000 --- a/misc/github-bot/comment.go +++ /dev/null @@ -1,20 +0,0 @@ -package main - -import "bot/client" - -type Auto struct { - Met bool - Description string -} -type Manual struct { - CheckedBy string - Description string -} - -type Comment struct { - Auto []Auto - Manual []Manual -} - -func onCommentUpdated(gh *client.Github) { -} diff --git a/misc/github-bot/comment.tmpl b/misc/github-bot/comment.tmpl deleted file mode 100644 index 1f7cd0ace58..00000000000 --- a/misc/github-bot/comment.tmpl +++ /dev/null @@ -1,21 +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 [config file](https://github.com/gnolang/gno/blob/master/misc/github-bot/config.go). - -## Automated Checks - -{{ range .Auto }} -
- {{ if .Met }}🟢{{ else }}🟠{{ end }} {{ .Description }} - - **TODO** -
-{{ end }} - -## Manual Checks - -{{ range .Manual }} -- [{{ if .CheckedBy }}x{{ else }} {{ end }}] {{ .Description }} {{ if .CheckedBy }}(checked by @{{ .CheckedBy }}){{ end }}{{ end }} diff --git a/misc/github-bot/condition/assignee.go b/misc/github-bot/condition/assignee.go deleted file mode 100644 index 4190268e49d..00000000000 --- a/misc/github-bot/condition/assignee.go +++ /dev/null @@ -1,64 +0,0 @@ -package condition - -import ( - "bot/client" - "fmt" - - "github.com/google/go-github/v66/github" -) - -// Assignee Condition -type assignee struct { - user string -} - -var _ Condition = &assignee{} - -// GetText implements Condition -func (a *assignee) GetText() string { - return fmt.Sprintf("A pull request assignee is user : %s", a.user) -} - -// Validate implements Condition -func (a *assignee) Validate(pr *github.PullRequest) bool { - for _, assignee := range pr.Assignees { - if a.user == assignee.GetLogin() { - return true - } - } - return false -} - -func Assignee(user string) Condition { - return &assignee{user: user} -} - -// AssigneeInTeam Condition -type assigneeInTeam struct { - gh *client.Github - team string -} - -var _ Condition = &assigneeInTeam{} - -// GetText implements Condition -func (a *assigneeInTeam) GetText() string { - return fmt.Sprintf("A pull request assignee is a member of the team : %s", a.team) -} - -// Validate implements Condition -func (a *assigneeInTeam) Validate(pr *github.PullRequest) bool { - for _, member := range a.gh.ListTeamMembers(a.team) { - for _, assignee := range pr.Assignees { - if member.GetLogin() == assignee.GetLogin() { - return true - } - } - } - - return false -} - -func AssigneeInTeam(gh *client.Github, team string) Condition { - return &assigneeInTeam{gh: gh, team: team} -} diff --git a/misc/github-bot/condition/author.go b/misc/github-bot/condition/author.go deleted file mode 100644 index a2821267b6b..00000000000 --- a/misc/github-bot/condition/author.go +++ /dev/null @@ -1,57 +0,0 @@ -package condition - -import ( - "bot/client" - "fmt" - - "github.com/google/go-github/v66/github" -) - -// Author Condition -type author struct { - user string -} - -var _ Condition = &author{} - -// GetText implements Condition -func (a *author) GetText() string { - return fmt.Sprintf("Pull request author is user : %v", a.user) -} - -// Validate implements Condition -func (a *author) Validate(pr *github.PullRequest) bool { - return a.user == pr.GetUser().GetLogin() -} - -func Author(user string) Condition { - return &author{user: user} -} - -// AuthorInTeam Condition -type authorInTeam struct { - gh *client.Github - team string -} - -var _ Condition = &authorInTeam{} - -// GetText implements Condition -func (a *authorInTeam) GetText() string { - return fmt.Sprintf("Pull request author is a member of the team : %s", a.team) -} - -// Validate implements Condition -func (a *authorInTeam) Validate(pr *github.PullRequest) bool { - for _, member := range a.gh.ListTeamMembers(a.team) { - if member.GetLogin() == pr.GetUser().GetLogin() { - return true - } - } - - return false -} - -func AuthorInTeam(gh *client.Github, team string) Condition { - return &authorInTeam{gh: gh, team: team} -} diff --git a/misc/github-bot/condition/boolean.go b/misc/github-bot/condition/boolean.go deleted file mode 100644 index 0163da31a4f..00000000000 --- a/misc/github-bot/condition/boolean.go +++ /dev/null @@ -1,100 +0,0 @@ -package condition - -import ( - "fmt" - - "github.com/google/go-github/v66/github" -) - -// And Condition -type and struct { - conditions []Condition -} - -var _ Condition = &and{} - -// Validate implements Condition -func (a *and) Validate(pr *github.PullRequest) bool { - for _, condition := range a.conditions { - if !condition.Validate(pr) { - return false - } - } - - return true -} - -// GetText implements Condition -func (a *and) GetText() string { - text := fmt.Sprintf("(%s", a.conditions[0].GetText()) - for _, condition := range a.conditions[1:] { - text = fmt.Sprintf("%s AND %s", text, condition.GetText()) - } - - return text + ")" -} - -func And(conditions ...Condition) Condition { - if len(conditions) < 2 { - panic("You should pass at least 2 conditions to And()") - } - - return &and{conditions} -} - -// Or Condition -type or struct { - conditions []Condition -} - -var _ Condition = &or{} - -// Validate implements Condition -func (o *or) Validate(pr *github.PullRequest) bool { - for _, condition := range o.conditions { - if condition.Validate(pr) { - return true - } - } - - return false -} - -// GetText implements Condition -func (o *or) GetText() string { - text := fmt.Sprintf("(%s", o.conditions[0].GetText()) - for _, condition := range o.conditions[1:] { - text = fmt.Sprintf("%s OR %s", text, condition.GetText()) - } - - return text + ")" -} - -func Or(conditions ...Condition) Condition { - if len(conditions) < 2 { - panic("You should pass at least 2 conditions to Or()") - } - - return &or{conditions} -} - -// Not Condition -type not struct { - cond Condition -} - -var _ Condition = ¬{} - -// Validate implements Condition -func (n *not) Validate(pr *github.PullRequest) bool { - return !n.cond.Validate(pr) -} - -// GetText implements Condition -func (n *not) GetText() string { - return fmt.Sprintf("NOT %s", n.cond.GetText()) -} - -func Not(cond Condition) Condition { - return ¬{cond} -} diff --git a/misc/github-bot/condition/branch.go b/misc/github-bot/condition/branch.go deleted file mode 100644 index db8003adf2b..00000000000 --- a/misc/github-bot/condition/branch.go +++ /dev/null @@ -1,50 +0,0 @@ -package condition - -import ( - "fmt" - "regexp" - - "github.com/google/go-github/v66/github" -) - -// BaseBranch Condition -type baseBranch struct { - pattern *regexp.Regexp -} - -var _ Condition = &baseBranch{} - -// Validate implements Condition -func (b *baseBranch) Validate(pr *github.PullRequest) bool { - return b.pattern.MatchString(pr.GetBase().GetRef()) -} - -// GetText implements Condition -func (b *baseBranch) GetText() string { - return fmt.Sprintf("The base branch match this pattern : %s", b.pattern.String()) -} - -func BaseBranch(pattern string) Condition { - return &baseBranch{pattern: regexp.MustCompile(pattern)} -} - -// HeadBranch Condition -type headBranch struct { - pattern *regexp.Regexp -} - -var _ Condition = &headBranch{} - -// Validate implements Condition -func (h *headBranch) Validate(pr *github.PullRequest) bool { - return h.pattern.MatchString(pr.GetHead().GetRef()) -} - -// GetText implements Condition -func (h *headBranch) GetText() string { - return fmt.Sprintf("The head branch match this pattern : %s", h.pattern.String()) -} - -func HeadBranch(pattern string) Condition { - return &headBranch{pattern: regexp.MustCompile(pattern)} -} diff --git a/misc/github-bot/condition/condition.go b/misc/github-bot/condition/condition.go deleted file mode 100644 index a3226647e79..00000000000 --- a/misc/github-bot/condition/condition.go +++ /dev/null @@ -1,13 +0,0 @@ -package condition - -import ( - "github.com/google/go-github/v66/github" -) - -type Condition interface { - // Check if the Condition is met by this PR - Validate(pr *github.PullRequest) bool - - // Get a text representation of this Condition - GetText() string -} diff --git a/misc/github-bot/condition/constant.go b/misc/github-bot/condition/constant.go deleted file mode 100644 index ed01a01d9d1..00000000000 --- a/misc/github-bot/condition/constant.go +++ /dev/null @@ -1,43 +0,0 @@ -package condition - -import ( - "github.com/google/go-github/v66/github" -) - -// Always Condition -type always struct{} - -var _ Condition = &always{} - -// Validate implements Condition -func (*always) Validate(_ *github.PullRequest) bool { - return true -} - -// GetText implements Condition -func (*always) GetText() string { - return "On every pull request" -} - -func Always() Condition { - return &always{} -} - -// Never Condition -type never struct{} - -var _ Condition = &never{} - -// Validate implements Condition -func (*never) Validate(_ *github.PullRequest) bool { - return false -} - -// GetText implements Condition -func (*never) GetText() string { - return "On no pull request" -} - -func Never() Condition { - return &never{} -} diff --git a/misc/github-bot/condition/file.go b/misc/github-bot/condition/file.go deleted file mode 100644 index a6f49800f65..00000000000 --- a/misc/github-bot/condition/file.go +++ /dev/null @@ -1,61 +0,0 @@ -package condition - -import ( - "bot/client" - "fmt" - "regexp" - - "github.com/google/go-github/v66/github" -) - -// FileChanged Condition -type fileChanged struct { - gh *client.Github - pattern *regexp.Regexp -} - -var _ Condition = &fileChanged{} - -// Validate implements Condition -func (fc *fileChanged) Validate(pr *github.PullRequest) bool { - opts := &github.ListOptions{ - PerPage: client.PageSize, - } - - for { - files, response, err := fc.gh.Client.PullRequests.ListFiles( - fc.gh.Ctx, - fc.gh.Owner, - fc.gh.Repo, - pr.GetNumber(), - opts, - ) - if err != nil { - fc.gh.Logger.Errorf("Unable to list changed files for PR %d : %v", pr.GetNumber(), err) - break - } - - for _, file := range files { - if fc.pattern.MatchString(file.GetFilename()) { - fc.gh.Logger.Debugf("File %s is matching pattern %s", file.GetFilename(), fc.pattern.String()) - return true - } - } - - if response.NextPage == 0 { - break - } - opts.Page = response.NextPage - } - - return false -} - -// GetText implements Condition -func (fc *fileChanged) GetText() string { - return fmt.Sprintf("A changed file match this pattern : %s", fc.pattern.String()) -} - -func FileChanged(gh *client.Github, pattern string) Condition { - return &fileChanged{gh: gh, pattern: regexp.MustCompile(pattern)} -} diff --git a/misc/github-bot/condition/label.go b/misc/github-bot/condition/label.go deleted file mode 100644 index 3c6b929afe5..00000000000 --- a/misc/github-bot/condition/label.go +++ /dev/null @@ -1,34 +0,0 @@ -package condition - -import ( - "fmt" - "regexp" - - "github.com/google/go-github/v66/github" -) - -// Label Condition -type label struct { - pattern *regexp.Regexp -} - -var _ Condition = &label{} - -// Validate implements Condition -func (l *label) Validate(pr *github.PullRequest) bool { - for _, label := range pr.Labels { - if l.pattern.MatchString(label.GetName()) { - return true - } - } - return false -} - -// GetText implements Condition -func (l *label) GetText() string { - return fmt.Sprintf("A label match this pattern : %s", l.pattern.String()) -} - -func Label(pattern string) Condition { - return &label{pattern: regexp.MustCompile(pattern)} -} diff --git a/misc/github-bot/config.go b/misc/github-bot/config.go deleted file mode 100644 index d5c586fa52e..00000000000 --- a/misc/github-bot/config.go +++ /dev/null @@ -1,59 +0,0 @@ -package main - -import ( - "bot/client" - c "bot/condition" - r "bot/requirement" -) - -type automaticCheck struct { - Description string - If c.Condition - Then r.Requirement -} - -type manualCheck struct { - Description string - If c.Condition - // TODO: remomve that - CheckedBy string -} - -func config(gh *client.Github) ([]automaticCheck, []manualCheck) { - return []automaticCheck{ - { - Description: "Changes on 'tm2' folder should be reviewed/authored at least one member of both EU and US teams", - If: c.And( - c.FileChanged(gh, "tm2"), - c.BaseBranch("main"), - ), - Then: r.And( - r.Or( - r.ReviewByTeamMembers(gh, "eu", 1), - r.AuthorInTeam(gh, "eu"), - ), - r.Or( - r.ReviewByTeamMembers(gh, "us", 1), - r.AuthorInTeam(gh, "us"), - ), - ), - }, { - Description: "Maintainer must be able to edit this pull request", - If: c.Always(), - Then: r.MaintainerCanModify(), - }, - }, []manualCheck{ - { - Description: "Manual check #1", - CheckedBy: "", - }, - { - Description: "Manual check #2", - CheckedBy: "aeddi", - }, - { - Description: "Manual check #3", - CheckedBy: "moul", - }, - } -} diff --git a/misc/github-bot/go.mod b/misc/github-bot/go.mod deleted file mode 100644 index b72011215f8..00000000000 --- a/misc/github-bot/go.mod +++ /dev/null @@ -1,10 +0,0 @@ -module bot - -go 1.22.2 - -require ( - github.com/google/go-github/v66 v66.0.0 - github.com/sethvargo/go-githubactions v1.3.0 -) - -require github.com/google/go-querystring v1.1.0 // indirect diff --git a/misc/github-bot/go.sum b/misc/github-bot/go.sum deleted file mode 100644 index ee3974d68e8..00000000000 --- a/misc/github-bot/go.sum +++ /dev/null @@ -1,10 +0,0 @@ -github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= -github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= -github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= -github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= -github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= -github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= -github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A= -github.com/sethvargo/go-githubactions v1.3.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80= -golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= diff --git a/misc/github-bot/logger/action.go b/misc/github-bot/logger/action.go deleted file mode 100644 index c6d10429e62..00000000000 --- a/misc/github-bot/logger/action.go +++ /dev/null @@ -1,43 +0,0 @@ -package logger - -import ( - "github.com/sethvargo/go-githubactions" -) - -type actionLogger struct{} - -var _ Logger = &actionLogger{} - -// Debugf implements Logger. -func (a *actionLogger) Debugf(msg string, args ...any) { - githubactions.Debugf(msg, args...) -} - -// Errorf implements Logger. -func (a *actionLogger) Errorf(msg string, args ...any) { - githubactions.Errorf(msg, args...) -} - -// Fatalf implements Logger. -func (a *actionLogger) Fatalf(msg string, args ...any) { - githubactions.Fatalf(msg, args...) -} - -// Infof implements Logger. -func (a *actionLogger) Infof(msg string, args ...any) { - githubactions.Infof(msg, args...) -} - -// Noticef implements Logger. -func (a *actionLogger) Noticef(msg string, args ...any) { - githubactions.Noticef(msg, args...) -} - -// Warningf implements Logger. -func (a *actionLogger) Warningf(msg string, args ...any) { - githubactions.Warningf(msg, args...) -} - -func newActionLogger() Logger { - return &actionLogger{} -} diff --git a/misc/github-bot/logger/logger.go b/misc/github-bot/logger/logger.go deleted file mode 100644 index 53b50c6ed9a..00000000000 --- a/misc/github-bot/logger/logger.go +++ /dev/null @@ -1,34 +0,0 @@ -package logger - -import ( - "os" -) - -// All Logger methods follow the standard fmt.Printf convention -type Logger interface { - // Debugf prints a debug-level message - Debugf(msg string, args ...any) - - // Noticef prints a notice-level message - Noticef(msg string, args ...any) - - // Warningf prints a warning-level message - Warningf(msg string, args ...any) - - // Errorf prints a error-level message - Errorf(msg string, args ...any) - - // Fatalf prints a error-level message and exits - Fatalf(msg string, args ...any) - - // Infof prints message to stdout without any level annotations - Infof(msg string, args ...any) -} - -func NewLogger(verbose bool) Logger { - if _, isAction := os.LookupEnv("GITHUB_ACTION"); isAction { - return newActionLogger() - } - - return newTermLogger(verbose) -} diff --git a/misc/github-bot/logger/terminal.go b/misc/github-bot/logger/terminal.go deleted file mode 100644 index aeb3835e170..00000000000 --- a/misc/github-bot/logger/terminal.go +++ /dev/null @@ -1,55 +0,0 @@ -package logger - -import ( - "fmt" - "log/slog" - "os" -) - -type termLogger struct{} - -var _ Logger = &termLogger{} - -// Debugf implements Logger -func (s *termLogger) Debugf(msg string, args ...any) { - msg = fmt.Sprintf("%s\n", msg) - slog.Debug(fmt.Sprintf(msg, args...)) -} - -// Errorf implements Logger -func (s *termLogger) Errorf(msg string, args ...any) { - msg = fmt.Sprintf("%s\n", msg) - slog.Error(fmt.Sprintf(msg, args...)) -} - -// Fatalf implements Logger -func (s *termLogger) Fatalf(msg string, args ...any) { - s.Errorf(msg, args...) - os.Exit(1) -} - -// Infof implements Logger -func (s *termLogger) Infof(msg string, args ...any) { - msg = fmt.Sprintf("%s\n", msg) - slog.Info(fmt.Sprintf(msg, args...)) -} - -// Noticef implements Logger -func (s *termLogger) Noticef(msg string, args ...any) { - // Alias to info on terminal since notice level only exists on Github Actions - s.Infof(msg, args...) -} - -// Warningf implements Logger -func (s *termLogger) Warningf(msg string, args ...any) { - msg = fmt.Sprintf("%s\n", msg) - slog.Warn(fmt.Sprintf(msg, args...)) -} - -func newTermLogger(verbose bool) Logger { - if verbose { - slog.SetLogLoggerLevel(slog.LevelDebug) - } - - return &termLogger{} -} diff --git a/misc/github-bot/main.go b/misc/github-bot/main.go deleted file mode 100644 index e7548ebec8c..00000000000 --- a/misc/github-bot/main.go +++ /dev/null @@ -1,112 +0,0 @@ -package main - -import ( - "bot/client" - "bot/param" - "bytes" - "text/template" - - "github.com/google/go-github/v66/github" -) - -func main() { - // Get params by parsing CLI flags and/or Github Actions context - params := param.Get() - - // Init Github API client - gh := client.New(params) - - // TODO:cleanup - onCommentUpdated(gh) - - // Get a slice of pull requests to process - var ( - prs []*github.PullRequest - err error - ) - - // If requested, get all opened pull requests - if params.PrAll { - opts := &github.PullRequestListOptions{ - State: "open", - Sort: "updated", - Direction: "desc", - } - - prs, _, err = gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) - if err != nil { - gh.Logger.Fatalf("Unable to get all opened pull requests : %v", err) - } - - // Or get only specified pull request(s) (flag or Github Action context) - } else { - 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) - if err != nil { - gh.Logger.Fatalf("Unable to get specified pull request (%d) : %v", prNum, err) - } - prs[i] = pr - } - } - - tmplFile := "comment.tmpl" - tmpl, err := template.New(tmplFile).ParseFiles(tmplFile) - if err != nil { - panic(err) - } - - auto, manual := config(gh) - // Process all pull requests - for _, pr := range prs { - com := Comment{} - for _, rule := range auto { - if rule.If.Validate(pr) { - gh.Logger.Infof(rule.If.GetText()) - - c := Auto{Description: rule.Description, Met: false} - - if !rule.Then.Validate(pr) { - gh.Logger.Infof(rule.Then.GetText()) - c.Met = true - } - - com.Auto = append(com.Auto, c) - } - } - - for _, rule := range manual { - com.Manual = append(com.Manual, Manual{ - Description: rule.Description, - CheckedBy: rule.CheckedBy, - }) - } - - var commentBytes bytes.Buffer - err = tmpl.Execute(&commentBytes, com) - if err != nil { - panic(err) - } - - comment := gh.SetBotComment(commentBytes.String(), pr.GetNumber()) - - context := "Merge Requirements" - state := "pending" - targetURL := comment.GetHTMLURL() - description := "Some requirements are not met yet. See bot comment." - - if _, _, err := gh.Client.Repositories.CreateStatus( - gh.Ctx, - gh.Owner, - gh.Repo, - pr.GetHead().GetSHA(), - &github.RepoStatus{ - Context: &context, - State: &state, - TargetURL: &targetURL, - Description: &description, - }); err != nil { - gh.Logger.Errorf("Unable to create status on PR %d : %v", pr.GetNumber(), err) - } - } -} diff --git a/misc/github-bot/param/param.go b/misc/github-bot/param/param.go deleted file mode 100644 index 54717511a21..00000000000 --- a/misc/github-bot/param/param.go +++ /dev/null @@ -1,92 +0,0 @@ -package param - -import ( - "flag" - "fmt" - "os" - - "github.com/sethvargo/go-githubactions" -) - -type Params struct { - Owner string - Repo string - PrAll bool - PrNums PrList - Verbose bool - DryRun bool - Timeout uint -} - -// Get Params from both cli flags and/or Github Actions context -func Get() Params { - p := Params{} - - // Add cmd description to usage message - flag.Usage = func() { - fmt.Fprint(flag.CommandLine.Output(), "This tool checks if requirements for a PR to be merged are met (defined in config.go) and display PR status checks accordingly.\n") - fmt.Fprint(flag.CommandLine.Output(), "A valid Github Token must be provided by setting the GITHUB_TOKEN env variable.\n\n") - flag.PrintDefaults() - } - - // Helper to display an error + usage message before exiting - errorUsage := func(error string) { - fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", error) - flag.Usage() - os.Exit(1) - } - - // Flags definition - flag.StringVar(&p.Owner, "owner", "", "owner of the repo to check, if empty, will be retrieved from Github Actions context") - flag.StringVar(&p.Repo, "repo", "", "repo to check, if empty, will be retrieved from Github Actions context") - flag.BoolVar(&p.PrAll, "pr-all", false, "validate all pull requests opened on the repo") - flag.TextVar(&p.PrNums, "pr-numbers", PrList(nil), "pull request(s) to validate, must be a comma seperated list of PR numbers, e.g '42,1337,2345'. If empty, PR to check will be retrived from Github Actions context") - flag.BoolVar(&p.Verbose, "verbose", false, "set logging level to debug") - flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are met without updating PR checks on Github web interface") - flag.UintVar(&p.Timeout, "timeout", 0, "timeout in milliseconds") - flag.Parse() - - // If any arg remain after flags processing - if len(flag.Args()) > 0 { - errorUsage(fmt.Sprintf("Unknown arg(s) provided : %v", flag.Args())) - } - - // Check if flags are coherents - if p.PrAll && len(p.PrNums) != 0 { - errorUsage("You must specify at most one of '-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) { - 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 == "" { - 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 == "" { - 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 { - const errMsg = "Unable to retrieve pull request number from Github Actions context, you may want to set it using -pr-numbers flag" - issue, ok := actionCtx.Event["issue"].(map[string]any) - if !ok { - errorUsage(errMsg) - } - num, ok := issue["number"].(float64) - if !ok || num <= 0 { - errorUsage(errMsg) - } - p.PrNums = PrList([]int{int(num)}) - } - } - - return p -} diff --git a/misc/github-bot/param/prlist.go b/misc/github-bot/param/prlist.go deleted file mode 100644 index 96a04ebce14..00000000000 --- a/misc/github-bot/param/prlist.go +++ /dev/null @@ -1,45 +0,0 @@ -package param - -import ( - "encoding" - "fmt" - "strconv" - "strings" -) - -type PrList []int - -// PrList is both a TextMarshaler and a TextUnmarshaler -var ( - _ encoding.TextMarshaler = PrList{} - _ encoding.TextUnmarshaler = &PrList{} -) - -// MarshalText implements encoding.TextMarshaler. -func (p PrList) MarshalText() (text []byte, err error) { - prNumsStr := make([]string, len(p)) - - for i, prNum := range p { - prNumsStr[i] = strconv.Itoa(prNum) - } - - return []byte(strings.Join(prNumsStr, ",")), nil -} - -// UnmarshalText implements encoding.TextUnmarshaler. -func (p *PrList) UnmarshalText(text []byte) error { - for _, prNumStr := range strings.Split(string(text), ",") { - prNum, err := strconv.Atoi(strings.TrimSpace(prNumStr)) - if err != nil { - return err - } - - if prNum <= 0 { - return fmt.Errorf("invalid pull request number (<= 0) : original(%s) parsed(%d)", prNumStr, prNum) - } - - *p = append(*p, prNum) - } - - return nil -} diff --git a/misc/github-bot/requirement/assignee.go b/misc/github-bot/requirement/assignee.go deleted file mode 100644 index 0b308e95c41..00000000000 --- a/misc/github-bot/requirement/assignee.go +++ /dev/null @@ -1,47 +0,0 @@ -package requirement - -import ( - "bot/client" - - "github.com/google/go-github/v66/github" -) - -// Assignee Requirement -type assignee struct { - gh *client.Github - user string -} - -var _ Requirement = &assignee{} - -// GetText implements Requirement -func (a *assignee) GetText() string { - return "TODO" -} - -// Validate implements Requirement -func (a *assignee) Validate(pr *github.PullRequest) bool { - // Check if user was already assigned to PR - for _, assignee := range pr.Assignees { - if a.user == assignee.GetLogin() { - return true - } - } - - // If not, assign it - if _, _, err := a.gh.Client.Issues.AddAssignees( - a.gh.Ctx, - a.gh.Owner, - a.gh.Repo, - pr.GetNumber(), - []string{a.user}, - ); err != nil { - a.gh.Logger.Errorf("Unable to assign user %s to PR %d : %v", a.user, pr.GetNumber(), err) - return false - } - return true -} - -func Assignee(gh *client.Github, user string) Requirement { - return &assignee{gh: gh, user: user} -} diff --git a/misc/github-bot/requirement/author.go b/misc/github-bot/requirement/author.go deleted file mode 100644 index 0d5808436f5..00000000000 --- a/misc/github-bot/requirement/author.go +++ /dev/null @@ -1,14 +0,0 @@ -package requirement - -import ( - "bot/client" - "bot/condition" -) - -func Author(user string) Requirement { - return condition.Author(user) -} - -func AuthorInTeam(gh *client.Github, team string) Requirement { - return condition.AuthorInTeam(gh, team) -} diff --git a/misc/github-bot/requirement/boolean.go b/misc/github-bot/requirement/boolean.go deleted file mode 100644 index ddb2af3c91a..00000000000 --- a/misc/github-bot/requirement/boolean.go +++ /dev/null @@ -1,100 +0,0 @@ -package requirement - -import ( - "fmt" - - "github.com/google/go-github/v66/github" -) - -// And Requirement -type and struct { - requirements []Requirement -} - -var _ Requirement = &and{} - -// Validate implements Requirement -func (a *and) Validate(pr *github.PullRequest) bool { - for _, requirement := range a.requirements { - if !requirement.Validate(pr) { - return false - } - } - - return true -} - -// GetText implements Requirement -func (a *and) GetText() string { - text := fmt.Sprintf("(%s", a.requirements[0].GetText()) - for _, requirement := range a.requirements[1:] { - text = fmt.Sprintf("%s AND %s", text, requirement.GetText()) - } - - return text + ")" -} - -func And(requirements ...Requirement) Requirement { - if len(requirements) < 2 { - panic("You should pass at least 2 requirements to And()") - } - - return &and{requirements} -} - -// Or Requirement -type or struct { - requirements []Requirement -} - -var _ Requirement = &or{} - -// Validate implements Requirement -func (o *or) Validate(pr *github.PullRequest) bool { - for _, requirement := range o.requirements { - if !requirement.Validate(pr) { - return false - } - } - - return true -} - -// GetText implements Requirement -func (o *or) GetText() string { - text := fmt.Sprintf("(%s", o.requirements[0].GetText()) - for _, requirement := range o.requirements[1:] { - text = fmt.Sprintf("%s OR %s", text, requirement.GetText()) - } - - return text + ")" -} - -func Or(requirements ...Requirement) Requirement { - if len(requirements) < 2 { - panic("You should pass at least 2 requirements to Or()") - } - - return &or{requirements} -} - -// Not Requirement -type not struct { - req Requirement -} - -var _ Requirement = ¬{} - -// Validate implements Requirement -func (n *not) Validate(pr *github.PullRequest) bool { - return !n.req.Validate(pr) -} - -// GetText implements Requirement -func (n *not) GetText() string { - return fmt.Sprintf("NOT %s", n.req.GetText()) -} - -func Not(req Requirement) Requirement { - return ¬{req} -} diff --git a/misc/github-bot/requirement/checkbox.go b/misc/github-bot/requirement/checkbox.go deleted file mode 100644 index fe60467a9e2..00000000000 --- a/misc/github-bot/requirement/checkbox.go +++ /dev/null @@ -1,29 +0,0 @@ -package requirement - -import ( - "bot/client" - - "github.com/google/go-github/v66/github" -) - -// Checkbox Requirement -type checkbox struct { - gh *client.Github - desc string -} - -var _ Requirement = &checkbox{} - -// GetText implements Requirement -func (c *checkbox) GetText() string { - return "" -} - -// Validate implements Requirement -func (c *checkbox) Validate(pr *github.PullRequest) bool { - return false -} - -func Checkbox(gh *client.Github, desc string) Requirement { - return &checkbox{gh: gh, desc: desc} -} diff --git a/misc/github-bot/requirement/label.go b/misc/github-bot/requirement/label.go deleted file mode 100644 index f9523ce5238..00000000000 --- a/misc/github-bot/requirement/label.go +++ /dev/null @@ -1,47 +0,0 @@ -package requirement - -import ( - "bot/client" - - "github.com/google/go-github/v66/github" -) - -// Label Requirement -type label struct { - gh *client.Github - name string -} - -var _ Requirement = &label{} - -// Validate implements Requirement -func (l *label) Validate(pr *github.PullRequest) bool { - // Check if label was already added to PR - for _, label := range pr.Labels { - if l.name == label.GetName() { - return true - } - } - - // If not, add it - if _, _, err := l.gh.Client.Issues.AddLabelsToIssue( - l.gh.Ctx, - l.gh.Owner, - l.gh.Repo, - pr.GetNumber(), - []string{l.name}, - ); err != nil { - l.gh.Logger.Errorf("Unable to add label %s to PR %d : %v", l.name, pr.GetNumber(), err) - return false - } - return true -} - -// GetText implements Requirement -func (l *label) GetText() string { - return "TODO" -} - -func Label(gh *client.Github, name string) Requirement { - return &label{gh, name} -} diff --git a/misc/github-bot/requirement/maintainer.go b/misc/github-bot/requirement/maintainer.go deleted file mode 100644 index 96ba5910cbe..00000000000 --- a/misc/github-bot/requirement/maintainer.go +++ /dev/null @@ -1,24 +0,0 @@ -package requirement - -import ( - "github.com/google/go-github/v66/github" -) - -// MaintainerCanModify Requirement -type maintainerCanModify struct{} - -var _ Requirement = &maintainerCanModify{} - -// GetText implements Requirement -func (a *maintainerCanModify) GetText() string { - return "TODO" -} - -// Validate implements Requirement -func (a *maintainerCanModify) Validate(pr *github.PullRequest) bool { - return pr.GetMaintainerCanModify() -} - -func MaintainerCanModify() Requirement { - return &maintainerCanModify{} -} diff --git a/misc/github-bot/requirement/requirement.go b/misc/github-bot/requirement/requirement.go deleted file mode 100644 index 982cf7ee14a..00000000000 --- a/misc/github-bot/requirement/requirement.go +++ /dev/null @@ -1,13 +0,0 @@ -package requirement - -import ( - "github.com/google/go-github/v66/github" -) - -type Requirement interface { - // Check if the Requirement is met by this PR - Validate(pr *github.PullRequest) bool - - // Get a text representation of this Requirement - GetText() string -} diff --git a/misc/github-bot/requirement/reviewer.go b/misc/github-bot/requirement/reviewer.go deleted file mode 100644 index 8882be00634..00000000000 --- a/misc/github-bot/requirement/reviewer.go +++ /dev/null @@ -1,139 +0,0 @@ -package requirement - -import ( - "bot/client" - - "github.com/google/go-github/v66/github" -) - -// Reviewer Requirement -type reviewByUser struct { - gh *client.Github - user string -} - -var _ Requirement = &reviewByUser{} - -// GetText implements Requirement -func (r *reviewByUser) GetText() string { - return "TODO" -} - -// Validate implements Requirement -func (r *reviewByUser) Validate(pr *github.PullRequest) bool { - // If not a dry run, make the user a reviewer if he's not already - if !r.gh.DryRun { - requested := false - if reviewers := r.gh.ListPrReviewers(pr.GetNumber()); reviewers != nil { - for _, user := range reviewers.Users { - if user.GetLogin() == r.user { - requested = true - break - } - } - } - - if requested { - r.gh.Logger.Debugf("Review of user %s already requested on PR %d", r.user, pr.GetNumber()) - } else { - r.gh.Logger.Debugf("Requesting review from user %s on PR %d", r.user, pr.GetNumber()) - if _, _, err := r.gh.Client.PullRequests.RequestReviewers( - r.gh.Ctx, - r.gh.Owner, - r.gh.Repo, - pr.GetNumber(), - github.ReviewersRequest{ - Reviewers: []string{r.user}, - }, - ); err != nil { - r.gh.Logger.Errorf("Unable to request review from user %s on PR %d : %v", r.user, pr.GetNumber(), err) - } - } - } - - // Check if user already approved this PR - for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { - if review.GetUser().GetLogin() == r.user { - r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) - return review.GetState() == "APPROVED" - } - } - r.gh.Logger.Debugf("User %s has not approved PR %d yet", r.user, pr.GetNumber()) - - return false -} - -func ReviewByUser(gh *client.Github, user string) Requirement { - return &reviewByUser{gh, user} -} - -// Reviewer Requirement -type reviewByTeamMembers struct { - gh *client.Github - team string - count uint -} - -var _ Requirement = &reviewByTeamMembers{} - -// GetText implements Requirement -func (r *reviewByTeamMembers) GetText() string { - return "TODO" -} - -// Validate implements Requirement -func (r *reviewByTeamMembers) Validate(pr *github.PullRequest) bool { - // If not a dry run, make the user a reviewer if he's not already - if !r.gh.DryRun { - requested := false - if reviewers := r.gh.ListPrReviewers(pr.GetNumber()); reviewers != nil { - for _, team := range reviewers.Teams { - if team.GetSlug() == r.team { - requested = true - break - } - } - } - - if requested { - r.gh.Logger.Debugf("Review of team %s already requested on PR %d", r.team, pr.GetNumber()) - } else { - r.gh.Logger.Debugf("Requesting review from team %s on PR %d", r.team, pr.GetNumber()) - if _, _, err := r.gh.Client.PullRequests.RequestReviewers( - r.gh.Ctx, - r.gh.Owner, - r.gh.Repo, - pr.GetNumber(), - github.ReviewersRequest{ - TeamReviewers: []string{r.team}, - }, - ); err != nil { - r.gh.Logger.Errorf("Unable to request review from team %s on PR %d : %v", r.team, pr.GetNumber(), err) - } - } - } - - // Check how many members of this team already approved this PR - var approved uint = 0 - members := r.gh.ListTeamMembers(r.team) - for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { - for _, member := range members { - if review.GetUser().GetLogin() == member.GetLogin() { - if review.GetState() == "APPROVED" { - approved += 1 - } - r.gh.Logger.Debugf("Member %s from team %s already reviewed PR %d with state %s (%d/%d required approval(s))", member.GetLogin(), r.team, pr.GetNumber(), review.GetState(), approved, r.count) - if approved >= r.count { - return true - } - } - } - } - r.gh.Logger.Debugf("Not enough members from team %s have approved PR %d (%d/%d required approval(s))", r.team, pr.GetNumber(), approved, r.count) - - return false -} - -func ReviewByTeamMembers(gh *client.Github, team string, count uint) Requirement { - return &reviewByTeamMembers{gh, team, count} -} From 88573df113eabfddc27491bde519d44b8352fa96 Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 6 Nov 2024 12:10:01 +0100 Subject: [PATCH 04/44] fix: typos and comments --- contribs/github-bot/client/client.go | 7 +++---- contribs/github-bot/comment.go | 24 +++++++++++++++--------- contribs/github-bot/comment.tmpl | 12 ++++++------ contribs/github-bot/config.go | 12 ++++++------ contribs/github-bot/main.go | 2 +- contribs/github-bot/param/param.go | 6 +++--- 6 files changed, 34 insertions(+), 29 deletions(-) diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 51ff3bdba72..54d822baa7a 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -224,12 +224,11 @@ func New(params param.Params) *GitHub { DryRun: params.DryRun, } - // This method will detect if the current process was launched by - // a GitHub Action or not and will accordingly return a logger suitable for - // the terminal output or for the GitHub Actions web interface + // 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) - // Create context with timeout if specified in flags + // Create context with timeout if specified in the parameters if params.Timeout > 0 { gh.Ctx, _ = context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Millisecond) } else { diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 15cab433bbd..48f92bd6413 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -13,6 +13,8 @@ import ( "github.com/sethvargo/go-githubactions" ) +// These structures contain the necessary information to generate +// the bot's comment from the template file type AutoContent struct { Description string Satisfied bool @@ -25,7 +27,6 @@ type ManualContent struct { CheckedBy string Teams []string } - type CommentContent struct { AutoRules []AutoContent ManualRules []ManualContent @@ -214,14 +215,22 @@ func handleCommentUpdate(gh *client.GitHub) { // Update comment with username if edited != "" { gh.SetBotComment(edited, int(num)) - gh.Logger.Debugf("Comment manual checks updated successfully") + gh.Logger.Debugf("Comment manual checks updated successfuly") } } func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentContent) { - // Create bot comment using template file + // Custom function to string markdown links + funcMap := template.FuncMap{ + "stripLinks": func(input string) string { + reg := regexp.MustCompile(`\[(.*)\]\(.*\)`) + return reg.ReplaceAllString(input, "$1") + }, + } + + // Generate bot comment using template file const tmplFile = "comment.tmpl" - tmpl, err := template.New(tmplFile).ParseFiles(tmplFile) + tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile) if err != nil { panic(err) } @@ -231,7 +240,7 @@ func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentCon panic(err) } - // Create commit status + // Prepare commit status content var ( comment = gh.SetBotComment(commentBytes.String(), pr.GetNumber()) context = "Merge Requirements" @@ -244,7 +253,6 @@ func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentCon // Check if every requirements are satisfied for _, auto := range content.AutoRules { if !auto.Satisfied { - gh.Logger.Infof("AUTO NOT Satisfied", auto.Description) allSatisfied = false break } @@ -253,7 +261,6 @@ func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentCon if allSatisfied { for _, manual := range content.ManualRules { if manual.CheckedBy == "" { - gh.Logger.Infof("AUTO NOT Satisfied", manual.Description) allSatisfied = false break } @@ -265,8 +272,7 @@ func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentCon description = "All requirements are satisfied." } - gh.Logger.Infof("STATUS", state, description) - + // Update or create commit status if _, _, err := gh.Client.Repositories.CreateStatus( gh.Ctx, gh.Owner, diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl index a1f94dab75d..4236d4a71c6 100644 --- a/contribs/github-bot/comment.tmpl +++ b/contribs/github-bot/comment.tmpl @@ -3,7 +3,7 @@ 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/config.go). +These requirements are defined in this [configuration file](https://github.com/GnoCheckBot/demo/blob/main/config.go). ## Automated Checks @@ -12,15 +12,15 @@ These requirements are defined in this [configuration file](https://github.com/g {{ if .AutoRules }}
Details
{{ range .AutoRules }} -
{{ .Description }}
+
{{ .Description | stripLinks }}
### If : ``` -{{ .ConditionDetails }} +{{ .ConditionDetails | stripLinks }} ``` ### Then : ``` -{{ .RequirementDetails }} +{{ .RequirementDetails | stripLinks }} ```
{{ end }} @@ -34,14 +34,14 @@ These requirements are defined in this [configuration file](https://github.com/g {{ if .ManualRules }}
Details
{{ range .ManualRules }} -
{{ .Description }}
+
{{ .Description | stripLinks }}
### If : ``` {{ .ConditionDetails }} ``` ### Can be checked by : -{{range $item := .Teams }} - team {{ $item }} +{{range $item := .Teams }} - team {{ $item | stripLinks }} {{ else }} - Any user with comment edit permission {{end}} diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index 51e11345583..713b86367c7 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -21,7 +21,7 @@ type manualCheck struct { func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { auto := []automaticCheck{ { - Description: "Changes on 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", + Description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", If: c.And( c.FileChanged(gh, "tm2"), c.BaseBranch("main"), @@ -38,12 +38,12 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { ), }, { - Description: "Maintainer must be able to edit this pull request", + Description: "A maintainer must be able to edit this pull request", If: c.Always(), Then: r.MaintainerCanModify(), }, { - Description: "Pull request head branch must be up to date with its base", + Description: "The pull request head branch must be up-to-date with its base", If: c.Always(), // Or only if c.BaseBranch("main") ? Then: r.UpToDateWith(gh, r.PR_BASE), }, @@ -63,7 +63,7 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { Teams: []string{"tech-staff"}, }, { - Description: "The code style is satisfactory", + Description: "Ensure the code style is satisfactory", If: c.And( c.BaseBranch("main"), c.Or( @@ -74,7 +74,7 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { Teams: []string{"tech-staff"}, }, { - Description: "The documentation is accurate and relevant", + Description: "Ensure the documentation is accurate and relevant", If: c.FileChanged(gh, `.*\.md`), Teams: []string{ "tech-staff", @@ -88,7 +88,7 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { unique := make(map[string]struct{}) for _, rule := range manual { if _, exists := unique[rule.Description]; exists { - gh.Logger.Fatalf("Manual rule description must be unique (duplicate : %s)", rule.Description) + gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate : %s)", rule.Description) } unique[rule.Description] = struct{}{} } diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 3b2546f3b9e..6e524deb1f0 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -50,7 +50,7 @@ func main() { } } - // Process all pull requests in parallel + // Process all pull requests in parrallel autoRules, manualRules := config(gh) var wg sync.WaitGroup wg.Add(len(prs)) diff --git a/contribs/github-bot/param/param.go b/contribs/github-bot/param/param.go index 3a3dbfc7bb2..ea6af698ca3 100644 --- a/contribs/github-bot/param/param.go +++ b/contribs/github-bot/param/param.go @@ -30,8 +30,8 @@ func Get() Params { } // Helper to display an error + usage message before exiting - errorUsage := func(err string) { - fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", err) + errorUsage := func(error string) { + fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", error) flag.Usage() os.Exit(1) } @@ -40,7 +40,7 @@ func Get() Params { flag.StringVar(&p.Owner, "owner", "", "owner of the repo to process, if empty, will be retrieved from GitHub Actions context") flag.StringVar(&p.Repo, "repo", "", "repo to process, if empty, will be retrieved from GitHub Actions context") flag.BoolVar(&p.PrAll, "pr-all", false, "process all opened pull requests") - flag.TextVar(&p.PrNums, "pr-numbers", 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") + flag.TextVar(&p.PrNums, "pr-numbers", PrList(nil), "pull request(s) to process, must be a comma seperated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrived from GitHub Actions context") flag.BoolVar(&p.Verbose, "verbose", false, "set logging level to debug") flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are met without updating PR checks on GitHub web interface") flag.UintVar(&p.Timeout, "timeout", 0, "timeout in milliseconds") From 8d33a1a266efaf0f8ddeafd6369cf5d50bd54524 Mon Sep 17 00:00:00 2001 From: aeddi Date: Thu, 7 Nov 2024 07:56:49 +0100 Subject: [PATCH 05/44] style: typo fix lint --- contribs/github-bot/comment.go | 2 +- contribs/github-bot/main.go | 2 +- contribs/github-bot/param/param.go | 6 +++--- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 48f92bd6413..21be26e40f0 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -215,7 +215,7 @@ func handleCommentUpdate(gh *client.GitHub) { // Update comment with username if edited != "" { gh.SetBotComment(edited, int(num)) - gh.Logger.Debugf("Comment manual checks updated successfuly") + gh.Logger.Debugf("Comment manual checks updated successfully") } } diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 6e524deb1f0..3b2546f3b9e 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -50,7 +50,7 @@ func main() { } } - // Process all pull requests in parrallel + // Process all pull requests in parallel autoRules, manualRules := config(gh) var wg sync.WaitGroup wg.Add(len(prs)) diff --git a/contribs/github-bot/param/param.go b/contribs/github-bot/param/param.go index ea6af698ca3..3a3dbfc7bb2 100644 --- a/contribs/github-bot/param/param.go +++ b/contribs/github-bot/param/param.go @@ -30,8 +30,8 @@ func Get() Params { } // Helper to display an error + usage message before exiting - errorUsage := func(error string) { - fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", error) + errorUsage := func(err string) { + fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", err) flag.Usage() os.Exit(1) } @@ -40,7 +40,7 @@ func Get() Params { flag.StringVar(&p.Owner, "owner", "", "owner of the repo to process, if empty, will be retrieved from GitHub Actions context") flag.StringVar(&p.Repo, "repo", "", "repo to process, if empty, will be retrieved from GitHub Actions context") flag.BoolVar(&p.PrAll, "pr-all", false, "process all opened pull requests") - flag.TextVar(&p.PrNums, "pr-numbers", PrList(nil), "pull request(s) to process, must be a comma seperated list of PR numbers, e.g '42,1337,7890'. If empty, will be retrived from GitHub Actions context") + flag.TextVar(&p.PrNums, "pr-numbers", 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") flag.BoolVar(&p.Verbose, "verbose", false, "set logging level to debug") flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are met without updating PR checks on GitHub web interface") flag.UintVar(&p.Timeout, "timeout", 0, "timeout in milliseconds") From 3df876ace77dd4375c669020107361799597b963 Mon Sep 17 00:00:00 2001 From: aeddi Date: Thu, 7 Nov 2024 13:12:12 +0100 Subject: [PATCH 06/44] fix: small fixes + dry-run --- .github/workflows/bot.yml | 8 +- contribs/github-bot/client/client.go | 14 ++-- contribs/github-bot/comment.go | 69 ++++++++--------- contribs/github-bot/comment.tmpl | 8 +- contribs/github-bot/condition/assignee.go | 6 +- contribs/github-bot/condition/author.go | 4 +- contribs/github-bot/condition/boolean.go | 13 ++-- contribs/github-bot/condition/branch.go | 4 +- contribs/github-bot/condition/file.go | 6 +- contribs/github-bot/condition/label.go | 4 +- contribs/github-bot/config.go | 4 +- contribs/github-bot/main.go | 85 ++++++++++++++++++--- contribs/github-bot/param/param.go | 14 ++-- contribs/github-bot/param/prlist.go | 2 +- contribs/github-bot/requirement/assignee.go | 4 +- contribs/github-bot/requirement/author.go | 4 +- contribs/github-bot/requirement/boolean.go | 13 ++-- contribs/github-bot/requirement/branch.go | 4 +- contribs/github-bot/requirement/label.go | 4 +- contribs/github-bot/requirement/reviewer.go | 6 +- contribs/github-bot/utils/tree.go | 9 ++- 21 files changed, 178 insertions(+), 107 deletions(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index f51cecd9269..1459a33db1a 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -20,7 +20,7 @@ on: workflow_dispatch: inputs: pull-request-list: - description: "PR(s) to process : specify 'all' or a comma separated list of PR numbers, e.g. '42,1337,7890'" + description: "PR(s) to process: specify 'all' or a comma separated list of PR numbers, e.g. '42,1337,7890'" required: true default: all type: string @@ -50,7 +50,7 @@ jobs: # If the input is 'all', create a matrix with every open PRs if [ '${{ inputs.pull-request-list }}' = 'all' ]; then pr_list=`gh pr list --state 'open' --repo '${{ github.repository }}' --json 'number' --template '{{$first := true}}{{range .}}{{if $first}}{{$first = false}}{{else}}, {{end}}{{"\""}}{{.number}}{{"\""}}{{end}}'` - [ -z "$pr_list" ] && echo 'Error : no opened PR found' >&2 && exit 1 + [ -z "$pr_list" ] && echo 'Error: no opened PR found' >&2 && exit 1 echo "pr-numbers=[$pr_list]" >> "$GITHUB_OUTPUT" # If the input is not 'all', test for each number in the comma separated # list if the associated PR is opened, then add it to the matrix @@ -61,7 +61,7 @@ jobs: for number in $pr_list; do trimed=`echo "$number" | xargs` pr_state=`gh pr view "$trimed" --repo '${{ github.repository }}' --json 'state' --template '{{.state}}' 2> /dev/null` - [ "$pr_state" != 'OPEN' ] && echo "Error : PR with number <$trimed> is not opened" >&2 && exit 1 + [ "$pr_state" != 'OPEN' ] && echo "Error: PR with number <$trimed> is not opened" >&2 && exit 1 done echo "pr-numbers=[$pr_list]" >> "$GITHUB_OUTPUT" fi @@ -72,7 +72,7 @@ jobs: elif [ '${{ github.event_name }}' = 'pull_request' ]; then echo 'pr-numbers=["${{ github.event.pull_request.number }}"]' >> "$GITHUB_OUTPUT" else - echo 'Error : unknown event ${{ github.event_name }}' >&2 && exit 1 + echo 'Error: unknown event ${{ github.event_name }}' >&2 && exit 1 fi # This job processes each pull request in the matrix individually while ensuring diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 54d822baa7a..d67b654d0c3 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -46,7 +46,7 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { opts, ) if err != nil { - gh.Logger.Errorf("Unable to list comments for PR %d : %v", prNum, err) + gh.Logger.Errorf("Unable to list comments for PR %d: %v", prNum, err) return nil } @@ -61,7 +61,7 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { // Get current user (bot) currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") if err != nil { - gh.Logger.Errorf("Unable to get current user : %v", err) + gh.Logger.Errorf("Unable to get current user: %v", err) return nil } @@ -86,7 +86,7 @@ func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { &github.IssueComment{Body: &body}, ) if err != nil { - gh.Logger.Errorf("Unable to create bot comment for PR %d : %v", prNum, err) + gh.Logger.Errorf("Unable to create bot comment for PR %d: %v", prNum, err) return nil } return newComment @@ -100,7 +100,7 @@ func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { comment, ) if err != nil { - gh.Logger.Errorf("Unable to edit bot comment with ID %d : %v", comment.GetID(), err) + gh.Logger.Errorf("Unable to edit bot comment with ID %d: %v", comment.GetID(), err) return nil } return editComment @@ -125,7 +125,7 @@ func (gh *GitHub) ListTeamMembers(team string) []*github.User { opts, ) if err != nil { - gh.Logger.Errorf("Unable to list members for team %s : %v", team, err) + gh.Logger.Errorf("Unable to list members for team %s: %v", team, err) return nil } @@ -169,7 +169,7 @@ func (gh *GitHub) ListPrReviewers(prNum int) *github.Reviewers { opts, ) if err != nil { - gh.Logger.Errorf("Unable to list reviewers for PR %d : %v", prNum, err) + gh.Logger.Errorf("Unable to list reviewers for PR %d: %v", prNum, err) return nil } @@ -202,7 +202,7 @@ func (gh *GitHub) ListPrReviews(prNum int) []*github.PullRequestReview { opts, ) if err != nil { - gh.Logger.Errorf("Unable to list reviews for PR %d : %v", prNum, err) + gh.Logger.Errorf("Unable to list reviews for PR %d: %v", prNum, err) return nil } diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 21be26e40f0..dbcb11ae3cb 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -28,11 +28,12 @@ type ManualContent struct { Teams []string } type CommentContent struct { - AutoRules []AutoContent - ManualRules []ManualContent + AutoRules []AutoContent + ManualRules []ManualContent + allSatisfied bool } -// getCommentManualChecks parses the bot comment to get both the checkbox status, +// getCommentManualChecks parses the bot comment to get the checkbox status, // the check description and the username who checked it func getCommentManualChecks(commentBody string) map[string][2]string { checks := make(map[string][2]string) @@ -52,7 +53,7 @@ func getCommentManualChecks(commentBody string) map[string][2]string { return checks } -// handleCommentUpdate checks if : +// handleCommentUpdate checks if: // - the current run was triggered by GitHub Actions // - the triggering event is an edit of the bot comment // - the comment was not edited by the bot itself (prevent infinite loop) @@ -62,17 +63,17 @@ func handleCommentUpdate(gh *client.GitHub) { // Get GitHub Actions context to retrieve comment update actionCtx, err := githubactions.Context() if err != nil { - gh.Logger.Debugf("Unable to retrieve GitHub Actions context : %v", err) + gh.Logger.Debugf("Unable to retrieve GitHub Actions context: %v", err) return } - // Ignore if it's not an comment related event + // Ignore if it's not a comment related event if actionCtx.EventName != "issue_comment" { - gh.Logger.Debugf("Event is not issue comment related : %s", actionCtx.EventName) + gh.Logger.Debugf("Event is not issue comment related (%s)", actionCtx.EventName) return } - // Ignore if action type is not deleted or edited + // Ignore if the action type is not deleted or edited actionType, ok := actionCtx.Event["action"].(string) if !ok { gh.Logger.Errorf("Unable to get type on issue comment event") @@ -86,7 +87,7 @@ func handleCommentUpdate(gh *client.GitHub) { // Exit if comment was edited by bot (current authenticated user) authUser, _, err := gh.Client.Users.Get(gh.Ctx, "") if err != nil { - gh.Logger.Errorf("Unable to get authenticated user : %v", err) + gh.Logger.Errorf("Unable to get authenticated user: %v", err) os.Exit(1) } @@ -162,7 +163,9 @@ func handleCommentUpdate(gh *client.GitHub) { if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") { // If not, restore previous comment body gh.Logger.Errorf("Bot comment edited outside of checkboxes") - gh.SetBotComment(previous, int(num)) + if !gh.DryRun { + gh.SetBotComment(previous, int(num)) + } os.Exit(1) } @@ -187,7 +190,7 @@ func handleCommentUpdate(gh *client.GitHub) { // If rule were not found, return to reprocess the bot comment entirely // (maybe bot config was updated since last run?) if !found { - gh.Logger.Debugf("Updated rule not found in config : %s", key) + gh.Logger.Debugf("Updated rule not found in config: %s", key) return } @@ -195,7 +198,9 @@ func handleCommentUpdate(gh *client.GitHub) { if len(teams) > 0 { if gh.IsUserInTeams(actionCtx.Actor, teams) { gh.Logger.Errorf("Checkbox edited by a user not allowed to") - gh.SetBotComment(previous, int(num)) + if !gh.DryRun { + gh.SetBotComment(previous, int(num)) + } os.Exit(1) } } @@ -213,7 +218,7 @@ func handleCommentUpdate(gh *client.GitHub) { } // Update comment with username - if edited != "" { + if edited != "" && !gh.DryRun { gh.SetBotComment(edited, int(num)) gh.Logger.Debugf("Comment manual checks updated successfully") } @@ -240,34 +245,24 @@ func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentCon panic(err) } + comment := gh.SetBotComment(commentBytes.String(), pr.GetNumber()) + if comment != nil { + gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber()) + } + // Prepare commit status content var ( - comment = gh.SetBotComment(commentBytes.String(), pr.GetNumber()) - context = "Merge Requirements" - targetURL = comment.GetHTMLURL() - state = "pending" - description = "Some requirements are not satisfied yet. See bot comment." - allSatisfied = true + context = "Merge Requirements" + targetURL = "" + state = "pending" + description = "Some requirements are not satisfied yet. See bot comment." ) - // Check if every requirements are satisfied - for _, auto := range content.AutoRules { - if !auto.Satisfied { - allSatisfied = false - break - } - } - - if allSatisfied { - for _, manual := range content.ManualRules { - if manual.CheckedBy == "" { - allSatisfied = false - break - } - } + if comment != nil { + targetURL = comment.GetHTMLURL() } - if allSatisfied { + if content.allSatisfied { state = "success" description = "All requirements are satisfied." } @@ -284,6 +279,8 @@ func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentCon TargetURL: &targetURL, Description: &description, }); err != nil { - gh.Logger.Errorf("Unable to create status on PR %d : %v", pr.GetNumber(), err) + gh.Logger.Errorf("Unable to create status on PR %d: %v", pr.GetNumber(), err) + } else { + gh.Logger.Infof("Commit status successfully updated on PR %d", pr.GetNumber()) } } diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl index 4236d4a71c6..2cd3a613b5d 100644 --- a/contribs/github-bot/comment.tmpl +++ b/contribs/github-bot/comment.tmpl @@ -14,11 +14,11 @@ These requirements are defined in this [configuration file](https://github.com/G {{ range .AutoRules }}
{{ .Description | stripLinks }}
-### If : +### If ``` {{ .ConditionDetails | stripLinks }} ``` -### Then : +### Then ``` {{ .RequirementDetails | stripLinks }} ``` @@ -36,11 +36,11 @@ These requirements are defined in this [configuration file](https://github.com/G {{ range .ManualRules }}
{{ .Description | stripLinks }}
-### If : +### If ``` {{ .ConditionDetails }} ``` -### Can be checked by : +### Can be checked by {{range $item := .Teams }} - team {{ $item | stripLinks }} {{ else }} - Any user with comment edit permission diff --git a/contribs/github-bot/condition/assignee.go b/contribs/github-bot/condition/assignee.go index b1e9debb261..9886a6b5ec6 100644 --- a/contribs/github-bot/condition/assignee.go +++ b/contribs/github-bot/condition/assignee.go @@ -17,7 +17,7 @@ type assignee struct { var _ Condition = &assignee{} func (a *assignee) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("A pull request assignee is user : %s", a.user) + detail := fmt.Sprintf("A pull request assignee is user: %s", a.user) for _, assignee := range pr.Assignees { if a.user == assignee.GetLogin() { @@ -41,12 +41,12 @@ type assigneeInTeam struct { var _ Condition = &assigneeInTeam{} func (a *assigneeInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("A pull request assignee is a member of the team : %s", a.team) + detail := fmt.Sprintf("A pull request assignee is a member of the team: %s", a.team) for _, member := range a.gh.ListTeamMembers(a.team) { for _, assignee := range pr.Assignees { if member.GetLogin() == assignee.GetLogin() { - return utils.AddStatusNode(true, fmt.Sprintf("%s (member : %s)", detail, member.GetLogin()), details) + return utils.AddStatusNode(true, fmt.Sprintf("%s (member: %s)", detail, member.GetLogin()), details) } } } diff --git a/contribs/github-bot/condition/author.go b/contribs/github-bot/condition/author.go index be2b293e27e..25d4da76d3b 100644 --- a/contribs/github-bot/condition/author.go +++ b/contribs/github-bot/condition/author.go @@ -19,7 +19,7 @@ var _ Condition = &author{} func (a *author) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { return utils.AddStatusNode( a.user == pr.GetUser().GetLogin(), - fmt.Sprintf("Pull request author is user : %v", a.user), + fmt.Sprintf("Pull request author is user: %v", a.user), details, ) } @@ -37,7 +37,7 @@ type authorInTeam struct { var _ Condition = &authorInTeam{} func (a *authorInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("Pull request author is a member of the team : %s", a.team) + detail := fmt.Sprintf("Pull request author is a member of the team: %s", a.team) for _, member := range a.gh.ListTeamMembers(a.team) { if member.GetLogin() == pr.GetUser().GetLogin() { diff --git a/contribs/github-bot/condition/boolean.go b/contribs/github-bot/condition/boolean.go index db9d1fb45dd..ed6fa2f3a08 100644 --- a/contribs/github-bot/condition/boolean.go +++ b/contribs/github-bot/condition/boolean.go @@ -1,6 +1,7 @@ package condition import ( + "bot/utils" "fmt" "github.com/google/go-github/v66/github" @@ -25,9 +26,9 @@ func (a *and) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { } if met { - branch.SetValue("🟢 And") + branch.SetValue(fmt.Sprintf("%s And", utils.StatusSuccess)) } else { - branch.SetValue("🔴 And") + branch.SetValue(fmt.Sprintf("%s And", utils.StatusFail)) } return met @@ -59,9 +60,9 @@ func (o *or) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { } if met { - branch.SetValue("🟢 Or") + branch.SetValue(fmt.Sprintf("%s Or", utils.StatusSuccess)) } else { - branch.SetValue("🔴 Or") + branch.SetValue(fmt.Sprintf("%s Or", utils.StatusFail)) } return met @@ -87,9 +88,9 @@ func (n *not) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { node := details.FindLastNode() if met { - node.SetValue(fmt.Sprintf("🔴 Not (%s)", node.(*treeprint.Node).Value.(string))) + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.StatusFail, node.(*treeprint.Node).Value.(string))) } else { - node.SetValue(fmt.Sprintf("🟢 Not (%s)", node.(*treeprint.Node).Value.(string))) + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.StatusSuccess, node.(*treeprint.Node).Value.(string))) } return !met diff --git a/contribs/github-bot/condition/branch.go b/contribs/github-bot/condition/branch.go index bfb0dd78d3a..2b3b90b7051 100644 --- a/contribs/github-bot/condition/branch.go +++ b/contribs/github-bot/condition/branch.go @@ -19,7 +19,7 @@ var _ Condition = &baseBranch{} func (b *baseBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { return utils.AddStatusNode( b.pattern.MatchString(pr.GetBase().GetRef()), - fmt.Sprintf("The base branch match this pattern : %s", b.pattern.String()), + fmt.Sprintf("The base branch match this pattern: %s", b.pattern.String()), details, ) } @@ -38,7 +38,7 @@ var _ Condition = &headBranch{} func (h *headBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { return utils.AddStatusNode( h.pattern.MatchString(pr.GetHead().GetRef()), - fmt.Sprintf("The head branch match this pattern : %s", h.pattern.String()), + fmt.Sprintf("The head branch match this pattern: %s", h.pattern.String()), details, ) } diff --git a/contribs/github-bot/condition/file.go b/contribs/github-bot/condition/file.go index 71be92e6edd..641cf0cb4b4 100644 --- a/contribs/github-bot/condition/file.go +++ b/contribs/github-bot/condition/file.go @@ -19,7 +19,7 @@ type fileChanged struct { var _ Condition = &fileChanged{} func (fc *fileChanged) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("A changed file match this pattern : %s", fc.pattern.String()) + detail := fmt.Sprintf("A changed file match this pattern: %s", fc.pattern.String()) opts := &github.ListOptions{ PerPage: client.PageSize, } @@ -33,13 +33,13 @@ func (fc *fileChanged) IsMet(pr *github.PullRequest, details treeprint.Tree) boo opts, ) if err != nil { - fc.gh.Logger.Errorf("Unable to list changed files for PR %d : %v", pr.GetNumber(), err) + fc.gh.Logger.Errorf("Unable to list changed files for PR %d: %v", pr.GetNumber(), err) break } for _, file := range files { if fc.pattern.MatchString(file.GetFilename()) { - return utils.AddStatusNode(true, fmt.Sprintf("%s (filename : %s)", detail, file.GetFilename()), details) + return utils.AddStatusNode(true, fmt.Sprintf("%s (filename: %s)", detail, file.GetFilename()), details) } } diff --git a/contribs/github-bot/condition/label.go b/contribs/github-bot/condition/label.go index c346002d051..423fa41dc50 100644 --- a/contribs/github-bot/condition/label.go +++ b/contribs/github-bot/condition/label.go @@ -17,11 +17,11 @@ type label struct { var _ Condition = &label{} func (l *label) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("A label match this pattern : %s", l.pattern.String()) + detail := fmt.Sprintf("A label match this pattern: %s", l.pattern.String()) for _, label := range pr.Labels { if l.pattern.MatchString(label.GetName()) { - return utils.AddStatusNode(true, fmt.Sprintf("%s (label : %s)", detail, label.GetName()), details) + return utils.AddStatusNode(true, fmt.Sprintf("%s (label: %s)", detail, label.GetName()), details) } } diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index 713b86367c7..31c6018c89e 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -84,11 +84,11 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { } // Check for duplicates in manual rule descriptions - // (need to be unique for the bot operations) + // (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) + gh.Logger.Fatalf("Manual rule descriptions must be unique (duplicate: %s)", rule.Description) } unique[rule.Description] = struct{}{} } diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 3b2546f3b9e..99b1f1362b2 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -2,7 +2,11 @@ package main import ( "bot/client" + "bot/logger" "bot/param" + "bot/utils" + "fmt" + "strings" "sync" "github.com/google/go-github/v66/github" @@ -25,7 +29,7 @@ func main() { err error ) - // If requested, retrieve all opened pull requests + // If requested, retrieve all open pull requests if params.PrAll { opts := &github.PullRequestListOptions{ State: "open", @@ -35,7 +39,7 @@ func main() { prs, _, err = gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) if err != nil { - gh.Logger.Fatalf("Unable to retrieve all opened pull requests : %v", err) + gh.Logger.Fatalf("Unable to retrieve all open pull requests: %v", err) } // Otherwise, retrieve only specified pull request(s) (flag or GitHub Action context) @@ -44,35 +48,50 @@ func main() { for i, prNum := range params.PrNums { pr, _, err := gh.Client.PullRequests.Get(gh.Ctx, gh.Owner, gh.Repo, prNum) if err != nil { - gh.Logger.Fatalf("Unable to retrieve specified pull request (%d) : %v", prNum, err) + gh.Logger.Fatalf("Unable to retrieve specified pull request (%d): %v", prNum, err) } prs[i] = pr } } + if len(prs) > 1 { + prNums := make([]int, len(prs)) + for i, pr := range prs { + prNums[i] = pr.GetNumber() + } + + gh.Logger.Infof("%d pull requests to process: %v\n", len(prNums), prNums) + } + // Process all pull requests in parallel autoRules, manualRules := config(gh) var wg sync.WaitGroup wg.Add(len(prs)) + // Used in dry-run mode to log cleanly from different goroutines + logMutex := sync.Mutex{} + for _, pr := range prs { go func(pr *github.PullRequest) { defer wg.Done() commentContent := CommentContent{} + commentContent.allSatisfied = true // Iterate over all automatic rules in config for _, autoRule := range autoRules { - ifDetails := treeprint.NewWithRoot("🟢 Condition met") + ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) // Check if conditions of this rule are met by this PR if autoRule.If.IsMet(pr, ifDetails) { c := AutoContent{Description: autoRule.Description, Satisfied: false} - thenDetails := treeprint.NewWithRoot("🔴 Requirement not satisfied") + thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.StatusFail)) // Check if requirements of this rule are satisfied by this PR if autoRule.Then.IsSatisfied(pr, thenDetails) { - thenDetails.SetValue("🟢 Requirement satisfied") + thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.StatusSuccess)) c.Satisfied = true + } else { + commentContent.allSatisfied = false } c.ConditionDetails = ifDetails.String() @@ -83,7 +102,7 @@ func main() { // Iterate over all manual rules in config for _, manualRule := range manualRules { - ifDetails := treeprint.NewWithRoot("🟢 Condition met") + ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) // Retrieve manual check states checks := make(map[string][2]string) @@ -102,12 +121,18 @@ func main() { Teams: manualRule.Teams, }, ) + + if checks[manualRule.Description][1] == "" { + commentContent.allSatisfied = false + } } } - // Print results in PR comment or in logs + // Logs results or write them in bot PR comment if gh.DryRun { - // TODO: Pretty print dry run + logMutex.Lock() + logResults(gh.Logger, pr.GetNumber(), commentContent) + logMutex.Unlock() } else { updateComment(gh, pr, commentContent) } @@ -115,3 +140,45 @@ func main() { } wg.Wait() } + +func logResults(logger logger.Logger, prNum int, commentContent CommentContent) { + logger.Infof("Pull request #%d requirements", prNum) + if len(commentContent.AutoRules) > 0 { + logger.Infof("Automated Checks:") + } + + for _, rule := range commentContent.AutoRules { + status := utils.StatusFail + if rule.Satisfied { + status = utils.StatusSuccess + } + logger.Infof("%s %s", status, rule.Description) + logger.Debugf("If:\n%s", rule.ConditionDetails) + logger.Debugf("Then:\n%s", rule.RequirementDetails) + } + + if len(commentContent.ManualRules) > 0 { + logger.Infof("Manual Checks:") + } + + for _, rule := range commentContent.ManualRules { + status := utils.StatusFail + checker := "any user with comment edit permission" + if rule.CheckedBy != "" { + status = utils.StatusSuccess + } + if len(rule.Teams) == 0 { + checker = fmt.Sprintf("a member of one of these teams: %s", strings.Join(rule.Teams, ", ")) + } + logger.Infof("%s %s", status, rule.Description) + logger.Debugf("If:\n%s", rule.ConditionDetails) + logger.Debugf("Can be checked by %s", checker) + } + + logger.Infof("Conclusion:") + if commentContent.allSatisfied { + logger.Infof("%s All requirements are satisfied\n", utils.StatusSuccess) + } else { + logger.Infof("%s Not all requirements are satisfied\n", utils.StatusFail) + } +} diff --git a/contribs/github-bot/param/param.go b/contribs/github-bot/param/param.go index 3a3dbfc7bb2..1cf3a1f5683 100644 --- a/contribs/github-bot/param/param.go +++ b/contribs/github-bot/param/param.go @@ -24,14 +24,14 @@ func Get() Params { // Add cmd description to usage message flag.Usage = func() { - fmt.Fprint(flag.CommandLine.Output(), "This tool checks if requirements for a PR to be merged are satisfied (defined in config.go) and display PR status checks accordingly.\n") - fmt.Fprint(flag.CommandLine.Output(), "A valid GitHub Token must be provided by setting the GITHUB_TOKEN env variable.\n\n") + fmt.Fprint(flag.CommandLine.Output(), "This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\n") + fmt.Fprint(flag.CommandLine.Output(), "A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.\n\n") flag.PrintDefaults() } // Helper to display an error + usage message before exiting errorUsage := func(err string) { - fmt.Fprintf(flag.CommandLine.Output(), "Error : %s\n\n", err) + fmt.Fprintf(flag.CommandLine.Output(), "Error: %s\n\n", err) flag.Usage() os.Exit(1) } @@ -42,16 +42,16 @@ func Get() Params { flag.BoolVar(&p.PrAll, "pr-all", false, "process all opened pull requests") flag.TextVar(&p.PrNums, "pr-numbers", 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") flag.BoolVar(&p.Verbose, "verbose", false, "set logging level to debug") - flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are met without updating PR checks on GitHub web interface") + flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are satisfied without updating anything on GitHub") flag.UintVar(&p.Timeout, "timeout", 0, "timeout in milliseconds") flag.Parse() // If any arg remain after flags processing if len(flag.Args()) > 0 { - errorUsage(fmt.Sprintf("Unknown arg(s) provided : %v", flag.Args())) + errorUsage(fmt.Sprintf("Unknown arg(s) provided: %v", flag.Args())) } - // Check if flags are coherents + // Check if flags are coherent if p.PrAll && len(p.PrNums) != 0 { errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags") } @@ -61,7 +61,7 @@ func Get() Params { if p.Owner == "" || p.Repo == "" || (len(p.PrNums) == 0 && !p.PrAll) { actionCtx, err := githubactions.Context() if err != nil { - errorUsage(fmt.Sprintf("Unable to get GitHub Actions context : %v", err)) + errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v", err)) } if p.Owner == "" { diff --git a/contribs/github-bot/param/prlist.go b/contribs/github-bot/param/prlist.go index 96a04ebce14..c88e0be8fe7 100644 --- a/contribs/github-bot/param/prlist.go +++ b/contribs/github-bot/param/prlist.go @@ -35,7 +35,7 @@ func (p *PrList) UnmarshalText(text []byte) error { } if prNum <= 0 { - return fmt.Errorf("invalid pull request number (<= 0) : original(%s) parsed(%d)", prNumStr, prNum) + return fmt.Errorf("invalid pull request number (<= 0): original(%s) parsed(%d)", prNumStr, prNum) } *p = append(*p, prNum) diff --git a/contribs/github-bot/requirement/assignee.go b/contribs/github-bot/requirement/assignee.go index 6854322521a..608fc8f0662 100644 --- a/contribs/github-bot/requirement/assignee.go +++ b/contribs/github-bot/requirement/assignee.go @@ -18,7 +18,7 @@ type assignee struct { var _ Requirement = &assignee{} func (a *assignee) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("This user is assigned to pull request : %s", a.user) + detail := fmt.Sprintf("This user is assigned to pull request: %s", a.user) // Check if user was already assigned to PR for _, assignee := range pr.Assignees { @@ -40,7 +40,7 @@ func (a *assignee) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) b pr.GetNumber(), []string{a.user}, ); err != nil { - a.gh.Logger.Errorf("Unable to assign user %s to PR %d : %v", a.user, pr.GetNumber(), err) + a.gh.Logger.Errorf("Unable to assign user %s to PR %d: %v", a.user, pr.GetNumber(), err) return utils.AddStatusNode(false, detail, details) } diff --git a/contribs/github-bot/requirement/author.go b/contribs/github-bot/requirement/author.go index 29c3f6d1404..705b7a83e01 100644 --- a/contribs/github-bot/requirement/author.go +++ b/contribs/github-bot/requirement/author.go @@ -19,7 +19,7 @@ var _ Requirement = &author{} func (a *author) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { return utils.AddStatusNode( a.user == pr.GetUser().GetLogin(), - fmt.Sprintf("Pull request author is user : %v", a.user), + fmt.Sprintf("Pull request author is user: %v", a.user), details, ) } @@ -37,7 +37,7 @@ type authorInTeam struct { var _ Requirement = &authorInTeam{} func (a *authorInTeam) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("Pull request author is a member of the team : %s", a.team) + detail := fmt.Sprintf("Pull request author is a member of the team: %s", a.team) for _, member := range a.gh.ListTeamMembers(a.team) { if member.GetLogin() == pr.GetUser().GetLogin() { diff --git a/contribs/github-bot/requirement/boolean.go b/contribs/github-bot/requirement/boolean.go index 1deff3b0531..7ffb4f781a5 100644 --- a/contribs/github-bot/requirement/boolean.go +++ b/contribs/github-bot/requirement/boolean.go @@ -1,6 +1,7 @@ package requirement import ( + "bot/utils" "fmt" "github.com/google/go-github/v66/github" @@ -25,9 +26,9 @@ func (a *and) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { } if satisfied { - branch.SetValue("🟢 And") + branch.SetValue(fmt.Sprintf("%s And", utils.StatusSuccess)) } else { - branch.SetValue("🔴 And") + branch.SetValue(fmt.Sprintf("%s And", utils.StatusFail)) } return satisfied @@ -59,9 +60,9 @@ func (o *or) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { } if satisfied { - branch.SetValue("🟢 Or") + branch.SetValue(fmt.Sprintf("%s Or", utils.StatusSuccess)) } else { - branch.SetValue("🔴 Or") + branch.SetValue(fmt.Sprintf("%s Or", utils.StatusFail)) } return satisfied @@ -87,9 +88,9 @@ func (n *not) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { node := details.FindLastNode() if satisfied { - node.SetValue(fmt.Sprintf("🔴 Not (%s)", node.(*treeprint.Node).Value.(string))) + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.StatusFail, node.(*treeprint.Node).Value.(string))) } else { - node.SetValue(fmt.Sprintf("🟢 Not (%s)", node.(*treeprint.Node).Value.(string))) + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.StatusSuccess, node.(*treeprint.Node).Value.(string))) } return !satisfied diff --git a/contribs/github-bot/requirement/branch.go b/contribs/github-bot/requirement/branch.go index 9b1eafde05d..ba246161209 100644 --- a/contribs/github-bot/requirement/branch.go +++ b/contribs/github-bot/requirement/branch.go @@ -30,14 +30,14 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil) if err != nil { - u.gh.Logger.Errorf("Unable to compare head branch (%s) and base (%s) : %v", head, base, err) + u.gh.Logger.Errorf("Unable to compare head branch (%s) and base (%s): %v", head, base, err) return false } return utils.AddStatusNode( cmp.GetBehindBy() == 0, fmt.Sprintf( - "Head branch (%s) is up to date with (%s) : behind by %d / ahead by %d", + "Head branch (%s) is up to date with (%s): behind by %d / ahead by %d", head, base, cmp.GetBehindBy(), diff --git a/contribs/github-bot/requirement/label.go b/contribs/github-bot/requirement/label.go index c1a0bbd7518..fa01670543f 100644 --- a/contribs/github-bot/requirement/label.go +++ b/contribs/github-bot/requirement/label.go @@ -18,7 +18,7 @@ type label struct { var _ Requirement = &label{} func (l *label) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("This label is applied to pull request : %s", l.name) + detail := fmt.Sprintf("This label is applied to pull request: %s", l.name) // Check if label was already applied to PR for _, label := range pr.Labels { @@ -40,7 +40,7 @@ func (l *label) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool pr.GetNumber(), []string{l.name}, ); err != nil { - l.gh.Logger.Errorf("Unable to add label %s to PR %d : %v", l.name, pr.GetNumber(), err) + l.gh.Logger.Errorf("Unable to add label %s to PR %d: %v", l.name, pr.GetNumber(), err) return utils.AddStatusNode(false, detail, details) } diff --git a/contribs/github-bot/requirement/reviewer.go b/contribs/github-bot/requirement/reviewer.go index ce6e46becdb..e9e8e138bca 100644 --- a/contribs/github-bot/requirement/reviewer.go +++ b/contribs/github-bot/requirement/reviewer.go @@ -18,7 +18,7 @@ type reviewByUser struct { var _ Requirement = &reviewByUser{} func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("This user approved pull request : %s", r.user) + detail := fmt.Sprintf("This user approved pull request: %s", r.user) // If not a dry run, make the user a reviewer if he's not already if !r.gh.DryRun { @@ -45,7 +45,7 @@ func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tre Reviewers: []string{r.user}, }, ); err != nil { - r.gh.Logger.Errorf("Unable to request review from user %s on PR %d : %v", r.user, pr.GetNumber(), err) + r.gh.Logger.Errorf("Unable to request review from user %s on PR %d: %v", r.user, pr.GetNumber(), err) } } } @@ -103,7 +103,7 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr TeamReviewers: []string{r.team}, }, ); err != nil { - r.gh.Logger.Errorf("Unable to request review from team %s on PR %d : %v", r.team, pr.GetNumber(), err) + r.gh.Logger.Errorf("Unable to request review from team %s on PR %d: %v", r.team, pr.GetNumber(), err) } } } diff --git a/contribs/github-bot/utils/tree.go b/contribs/github-bot/utils/tree.go index 502f87e398d..f79bd45f118 100644 --- a/contribs/github-bot/utils/tree.go +++ b/contribs/github-bot/utils/tree.go @@ -6,11 +6,16 @@ import ( "github.com/xlab/treeprint" ) +const ( + StatusSuccess = "🟢" + StatusFail = "🔴" +) + func AddStatusNode(b bool, desc string, details treeprint.Tree) bool { if b { - details.AddNode(fmt.Sprintf("🟢 %s", desc)) + details.AddNode(fmt.Sprintf("%s %s", StatusSuccess, desc)) } else { - details.AddNode(fmt.Sprintf("🔴 %s", desc)) + details.AddNode(fmt.Sprintf("%s %s", StatusFail, desc)) } return b From 886a83cae714b0604d903d290042e39f3190f7aa Mon Sep 17 00:00:00 2001 From: aeddi Date: Thu, 7 Nov 2024 13:49:35 +0100 Subject: [PATCH 07/44] style: space out bash script in workflow --- .github/workflows/bot.yml | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 1459a33db1a..2d8298d82c2 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -47,11 +47,13 @@ jobs: run: | # Triggered by a workflow dispatch event if [ '${{ github.event_name }}' = 'workflow_dispatch' ]; then + # If the input is 'all', create a matrix with every open PRs if [ '${{ inputs.pull-request-list }}' = 'all' ]; then pr_list=`gh pr list --state 'open' --repo '${{ github.repository }}' --json 'number' --template '{{$first := true}}{{range .}}{{if $first}}{{$first = false}}{{else}}, {{end}}{{"\""}}{{.number}}{{"\""}}{{end}}'` [ -z "$pr_list" ] && echo 'Error: no opened PR found' >&2 && exit 1 echo "pr-numbers=[$pr_list]" >> "$GITHUB_OUTPUT" + # If the input is not 'all', test for each number in the comma separated # list if the associated PR is opened, then add it to the matrix else @@ -65,12 +67,16 @@ jobs: done echo "pr-numbers=[$pr_list]" >> "$GITHUB_OUTPUT" fi + # Triggered by comment event, just add the associated PR number to the matrix elif [ '${{ github.event_name }}' = 'issue_comment' ]; then echo 'pr-numbers=["${{ github.event.issue.number }}"]' >> "$GITHUB_OUTPUT" + # Triggered by pull request event, just add the associated PR number to the matrix elif [ '${{ github.event_name }}' = 'pull_request' ]; then echo 'pr-numbers=["${{ github.event.pull_request.number }}"]' >> "$GITHUB_OUTPUT" + + # Should never happen else echo 'Error: unknown event ${{ github.event_name }}' >&2 && exit 1 fi From 8e14e626bf9441951ce2bf39b963c841e1357b65 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 8 Nov 2024 11:43:03 +0100 Subject: [PATCH 08/44] chore: change package path --- contribs/github-bot/client/client.go | 5 +++-- contribs/github-bot/comment.go | 3 ++- contribs/github-bot/condition/assignee.go | 5 +++-- contribs/github-bot/condition/author.go | 5 +++-- contribs/github-bot/condition/boolean.go | 3 ++- contribs/github-bot/condition/branch.go | 3 ++- contribs/github-bot/condition/constant.go | 2 +- contribs/github-bot/condition/file.go | 5 +++-- contribs/github-bot/condition/label.go | 3 ++- contribs/github-bot/config.go | 6 +++--- contribs/github-bot/go.mod | 2 +- contribs/github-bot/main.go | 5 +++++ contribs/github-bot/requirement/assignee.go | 5 +++-- contribs/github-bot/requirement/author.go | 5 +++-- contribs/github-bot/requirement/boolean.go | 3 ++- contribs/github-bot/requirement/branch.go | 7 ++++--- contribs/github-bot/requirement/label.go | 5 +++-- contribs/github-bot/requirement/maintainer.go | 2 +- contribs/github-bot/requirement/reviewer.go | 5 +++-- 19 files changed, 49 insertions(+), 30 deletions(-) diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index d67b654d0c3..9d3433ff209 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -1,13 +1,14 @@ package client import ( - "bot/logger" - "bot/param" "context" "log" "os" "time" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/param" + "github.com/google/go-github/v66/github" ) diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index dbcb11ae3cb..c37a577ffa2 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -1,7 +1,6 @@ package main import ( - "bot/client" "bytes" "fmt" "os" @@ -9,6 +8,8 @@ import ( "strings" "text/template" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/google/go-github/v66/github" "github.com/sethvargo/go-githubactions" ) diff --git a/contribs/github-bot/condition/assignee.go b/contribs/github-bot/condition/assignee.go index 9886a6b5ec6..6004c990a45 100644 --- a/contribs/github-bot/condition/assignee.go +++ b/contribs/github-bot/condition/assignee.go @@ -1,10 +1,11 @@ package condition import ( - "bot/client" - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/author.go b/contribs/github-bot/condition/author.go index 25d4da76d3b..a186475e5fe 100644 --- a/contribs/github-bot/condition/author.go +++ b/contribs/github-bot/condition/author.go @@ -1,10 +1,11 @@ package condition import ( - "bot/client" - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/boolean.go b/contribs/github-bot/condition/boolean.go index ed6fa2f3a08..e7b3880e209 100644 --- a/contribs/github-bot/condition/boolean.go +++ b/contribs/github-bot/condition/boolean.go @@ -1,9 +1,10 @@ package condition import ( - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/branch.go b/contribs/github-bot/condition/branch.go index 2b3b90b7051..f1908114d52 100644 --- a/contribs/github-bot/condition/branch.go +++ b/contribs/github-bot/condition/branch.go @@ -1,10 +1,11 @@ package condition import ( - "bot/utils" "fmt" "regexp" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/constant.go b/contribs/github-bot/condition/constant.go index aa673875583..0b8b48c0c05 100644 --- a/contribs/github-bot/condition/constant.go +++ b/contribs/github-bot/condition/constant.go @@ -1,7 +1,7 @@ package condition import ( - "bot/utils" + "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/condition/file.go b/contribs/github-bot/condition/file.go index 641cf0cb4b4..ce614cd511b 100644 --- a/contribs/github-bot/condition/file.go +++ b/contribs/github-bot/condition/file.go @@ -1,11 +1,12 @@ package condition import ( - "bot/client" - "bot/utils" "fmt" "regexp" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/label.go b/contribs/github-bot/condition/label.go index 423fa41dc50..d5305924b9c 100644 --- a/contribs/github-bot/condition/label.go +++ b/contribs/github-bot/condition/label.go @@ -1,10 +1,11 @@ package condition import ( - "bot/utils" "fmt" "regexp" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index 31c6018c89e..451564e8593 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -1,9 +1,9 @@ package main import ( - "bot/client" - c "bot/condition" - r "bot/requirement" + "github.com/gnolang/gno/contribs/github-bot/client" + c "github.com/gnolang/gno/contribs/github-bot/condition" + r "github.com/gnolang/gno/contribs/github-bot/requirement" ) type automaticCheck struct { diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod index 32ddb2b2cb2..02136376e63 100644 --- a/contribs/github-bot/go.mod +++ b/contribs/github-bot/go.mod @@ -1,4 +1,4 @@ -module bot +module github.com/gnolang/gno/contribs/github-bot go 1.22.2 diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 99b1f1362b2..6e7d1cfde51 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -9,6 +9,11 @@ import ( "strings" "sync" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/param" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/assignee.go b/contribs/github-bot/requirement/assignee.go index 608fc8f0662..7f89e127e6b 100644 --- a/contribs/github-bot/requirement/assignee.go +++ b/contribs/github-bot/requirement/assignee.go @@ -1,10 +1,11 @@ package requirement import ( - "bot/client" - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/author.go b/contribs/github-bot/requirement/author.go index 705b7a83e01..42c5f1c0f15 100644 --- a/contribs/github-bot/requirement/author.go +++ b/contribs/github-bot/requirement/author.go @@ -1,10 +1,11 @@ package requirement import ( - "bot/client" - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/boolean.go b/contribs/github-bot/requirement/boolean.go index 7ffb4f781a5..2b675d94860 100644 --- a/contribs/github-bot/requirement/boolean.go +++ b/contribs/github-bot/requirement/boolean.go @@ -1,9 +1,10 @@ package requirement import ( - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/branch.go b/contribs/github-bot/requirement/branch.go index ba246161209..d047a3a23a7 100644 --- a/contribs/github-bot/requirement/branch.go +++ b/contribs/github-bot/requirement/branch.go @@ -1,10 +1,11 @@ package requirement import ( - "bot/client" - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) @@ -37,7 +38,7 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre return utils.AddStatusNode( cmp.GetBehindBy() == 0, fmt.Sprintf( - "Head branch (%s) is up to date with (%s): behind by %d / ahead by %d", + "Head branch (%s) is up to date with base (%s): behind by %d / ahead by %d", head, base, cmp.GetBehindBy(), diff --git a/contribs/github-bot/requirement/label.go b/contribs/github-bot/requirement/label.go index fa01670543f..57b04c37a2b 100644 --- a/contribs/github-bot/requirement/label.go +++ b/contribs/github-bot/requirement/label.go @@ -1,10 +1,11 @@ package requirement import ( - "bot/client" - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/maintainer.go b/contribs/github-bot/requirement/maintainer.go index 6d89206ed92..e1f95bc175a 100644 --- a/contribs/github-bot/requirement/maintainer.go +++ b/contribs/github-bot/requirement/maintainer.go @@ -1,7 +1,7 @@ package requirement import ( - "bot/utils" + "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirement/reviewer.go b/contribs/github-bot/requirement/reviewer.go index e9e8e138bca..3fcb7c1afa9 100644 --- a/contribs/github-bot/requirement/reviewer.go +++ b/contribs/github-bot/requirement/reviewer.go @@ -1,10 +1,11 @@ package requirement import ( - "bot/client" - "bot/utils" "fmt" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" "github.com/xlab/treeprint" ) From 42744e99adb5871da05074bf145613339452ffc6 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 8 Nov 2024 11:45:33 +0100 Subject: [PATCH 09/44] docs: add a README and improve comments --- contribs/github-bot/README.md | 54 +++++++++++++++++++++++++++++++++++ contribs/github-bot/config.go | 15 ++++++---- contribs/github-bot/main.go | 6 ++-- 3 files changed, 65 insertions(+), 10 deletions(-) create mode 100644 contribs/github-bot/README.md diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md new file mode 100644 index 00000000000..5f1bcd19098 --- /dev/null +++ b/contribs/github-bot/README.md @@ -0,0 +1,54 @@ +# GitHub Bot + +## Overview + +The GitHub Bot is designed to automate and streamline the process of managing pull requests. It can automate certain tasks such as requesting reviews, assigning users or applying labels, but it also ensures that certain requirements are satisfied before allowing a pull request to be merged. Interaction with the bot occurs through a comment on the pull request, providing all the information to the user and allowing them to check boxes for the manual validation of certain rules. + +## How It Works + +### Configuration + +The bot operates by defining a set of rules that are evaluated against each pull request passed as parameter. These rules are categorized into automatic and manual checks: + +- **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 exectued. 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 in specific files. + +The bot configuration is defined in Go and is located in the file [config.go](./config.go). + +### Conditions + +// TODO + +### Requirements + +// TODO + +### GitHub Token + +// TODO + +## Usage + +```bash +> go install github.com/gnolang/gno/contribs/github-bot@latest +// (go: downloading ...) + +> github-bot --help +This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly. +A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable. + + -dry-run + print if pull request requirements are satisfied without updating anything on GitHub + -owner string + owner of the repo to process, if empty, will be retrieved from GitHub Actions context + -pr-all + process all opened pull requests + -pr-numbers value + 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 + -repo string + repo to process, if empty, will be retrieved from GitHub Actions context + -timeout uint + timeout in milliseconds + -verbose + set logging level to debug +``` diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index 451564e8593..1668a3207dd 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -6,18 +6,22 @@ import ( r "github.com/gnolang/gno/contribs/github-bot/requirement" ) +// Automatic check that will be performed by the bot type automaticCheck struct { Description string - If c.Condition - Then r.Requirement + 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 - If c.Condition - Teams []string + If c.Condition // If the condition is met, a checkbox will be displayed on bot comment + Teams []string // 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{ { @@ -83,8 +87,7 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { }, } - // Check for duplicates in manual rule descriptions - // (needs to be unique for the bot operations) + // 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 { diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 6e7d1cfde51..6ebf7cd06ce 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -1,10 +1,6 @@ package main import ( - "bot/client" - "bot/logger" - "bot/param" - "bot/utils" "fmt" "strings" "sync" @@ -146,6 +142,8 @@ func main() { wg.Wait() } +// logResults is called in dry-run mode and outputs the status of each check +// and a conclusion func logResults(logger logger.Logger, prNum int, commentContent CommentContent) { logger.Infof("Pull request #%d requirements", prNum) if len(commentContent.AutoRules) > 0 { From 96060e0d50d701a50f21c23a4e1d0860ae0f7a7c Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 8 Nov 2024 18:55:20 +0100 Subject: [PATCH 10/44] test: add units tests (wip) --- .../github-bot/condition/assignee_test.go | 46 +++++++ contribs/github-bot/condition/author_test.go | 99 +++++++++++++++ contribs/github-bot/condition/boolean_test.go | 118 ++++++++++++++++++ contribs/github-bot/condition/branch_test.go | 51 ++++++++ .../github-bot/condition/constant_test.go | 32 +++++ contribs/github-bot/condition/file_test.go | 70 +++++++++++ contribs/github-bot/condition/label_test.go | 50 ++++++++ contribs/github-bot/go.mod | 12 +- contribs/github-bot/go.sum | 8 ++ contribs/github-bot/logger/logger.go | 6 + contribs/github-bot/logger/noop.go | 27 ++++ .../github-bot/requirement/assignee_test.go | 73 +++++++++++ contribs/github-bot/requirement/author.go | 2 +- .../github-bot/requirement/author_test.go | 99 +++++++++++++++ .../github-bot/requirement/boolean_test.go | 118 ++++++++++++++++++ contribs/github-bot/requirement/constant.go | 34 +++++ .../github-bot/requirement/constant_test.go | 32 +++++ contribs/github-bot/requirement/label_test.go | 77 ++++++++++++ .../github-bot/requirement/maintener_test.go | 36 ++++++ .../github-bot/requirement/reviewer_test.go | 107 ++++++++++++++++ contribs/github-bot/utils/testing.go | 19 +++ 21 files changed, 1113 insertions(+), 3 deletions(-) create mode 100644 contribs/github-bot/condition/assignee_test.go create mode 100644 contribs/github-bot/condition/author_test.go create mode 100644 contribs/github-bot/condition/boolean_test.go create mode 100644 contribs/github-bot/condition/branch_test.go create mode 100644 contribs/github-bot/condition/constant_test.go create mode 100644 contribs/github-bot/condition/file_test.go create mode 100644 contribs/github-bot/condition/label_test.go create mode 100644 contribs/github-bot/logger/noop.go create mode 100644 contribs/github-bot/requirement/assignee_test.go create mode 100644 contribs/github-bot/requirement/author_test.go create mode 100644 contribs/github-bot/requirement/boolean_test.go create mode 100644 contribs/github-bot/requirement/constant.go create mode 100644 contribs/github-bot/requirement/constant_test.go create mode 100644 contribs/github-bot/requirement/label_test.go create mode 100644 contribs/github-bot/requirement/maintener_test.go create mode 100644 contribs/github-bot/requirement/reviewer_test.go create mode 100644 contribs/github-bot/utils/testing.go diff --git a/contribs/github-bot/condition/assignee_test.go b/contribs/github-bot/condition/assignee_test.go new file mode 100644 index 00000000000..a5fdd73066c --- /dev/null +++ b/contribs/github-bot/condition/assignee_test.go @@ -0,0 +1,46 @@ +package condition + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestAssignee(t *testing.T) { + t.Parallel() + + assignees := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + assignees []*github.User + isMet bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", assignees, true}, + {"assignee list doesn't contain user", "user2", assignees, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Assignees: testCase.assignees} + details := treeprint.New() + condition := Assignee(testCase.user) + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} diff --git a/contribs/github-bot/condition/author_test.go b/contribs/github-bot/condition/author_test.go new file mode 100644 index 00000000000..ff716c531af --- /dev/null +++ b/contribs/github-bot/condition/author_test.go @@ -0,0 +1,99 @@ +package condition + +import ( + "context" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestAuthor(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + user string + author string + isMet bool + }{ + {"author match", "user", "user", true}, + {"author doesn't match", "user", "author", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.author)}, + } + details := treeprint.New() + condition := Author(testCase.user) + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} + +func TestAuthorInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isMet bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", members, true}, + {"assignee list doesn't contain user", "user2", members, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.user)}, + } + details := treeprint.New() + condition := AuthorInTeam(gh, "team") + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} diff --git a/contribs/github-bot/condition/boolean_test.go b/contribs/github-bot/condition/boolean_test.go new file mode 100644 index 00000000000..955aaccd35d --- /dev/null +++ b/contribs/github-bot/condition/boolean_test.go @@ -0,0 +1,118 @@ +package condition + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestAnd(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + conditions []Condition + isMet bool + }{ + {"and is true", []Condition{Always(), Always()}, true}, + {"and is false", []Condition{Always(), Always(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := And(testCase.conditions...) + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} + +func TestAndPanic(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + t.Errorf("and constructor should panic if less than 2 conditions are provided") + } + }() + + And(Always()) // Only 1 condition provided +} + +func TestOr(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + conditions []Condition + isMet bool + }{ + {"or is true", []Condition{Never(), Always()}, true}, + {"or is false", []Condition{Never(), Never(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := Or(testCase.conditions...) + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} + +func TestOrPanic(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + t.Errorf("and constructor should panic if less than 2 conditions are provided") + } + }() + + Or(Always()) // Only 1 condition provided +} + +func TestNot(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + condition Condition + isMet bool + }{ + {"not is true", Never(), true}, + {"not is false", Always(), false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + condition := Not(testCase.condition) + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} diff --git a/contribs/github-bot/condition/branch_test.go b/contribs/github-bot/condition/branch_test.go new file mode 100644 index 00000000000..073d2cc6365 --- /dev/null +++ b/contribs/github-bot/condition/branch_test.go @@ -0,0 +1,51 @@ +package condition + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestHeadBaseBranch(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + pattern string + base string + isMet bool + }{ + {"perfectly match", "base", "base", true}, + {"prefix match", "^dev/", "dev/test-bot", true}, + {"prefix doesn't match", "dev/$", "dev/test-bot", false}, + {"suffix match", "/test-bot$", "dev/test-bot", true}, + {"suffix doesn't match", "^/test-bot", "dev/test-bot", false}, + {"doesn't match", "base", "notatall", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + Base: &github.PullRequestBranch{Ref: github.String(testCase.base)}, + Head: &github.PullRequestBranch{Ref: github.String(testCase.base)}, + } + conditions := []Condition{ + BaseBranch(testCase.pattern), + HeadBranch(testCase.pattern), + } + + for _, condition := range conditions { + details := treeprint.New() + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + } + }) + } +} diff --git a/contribs/github-bot/condition/constant_test.go b/contribs/github-bot/condition/constant_test.go new file mode 100644 index 00000000000..cc41a8326c3 --- /dev/null +++ b/contribs/github-bot/condition/constant_test.go @@ -0,0 +1,32 @@ +package condition + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/xlab/treeprint" +) + +func TestAlways(t *testing.T) { + t.Parallel() + + details := treeprint.New() + if !Always().IsMet(nil, details) { + t.Errorf("condition should have a met status: %t", true) + } + if !utils.TestLastNodeStatus(t, true, details) { + t.Errorf("condition details should have a status: %t", true) + } +} + +func TestNever(t *testing.T) { + t.Parallel() + + details := treeprint.New() + if Never().IsMet(nil, details) { + t.Errorf("condition should have a met status: %t", false) + } + if !utils.TestLastNodeStatus(t, false, details) { + t.Errorf("condition details should have a status: %t", false) + } +} diff --git a/contribs/github-bot/condition/file_test.go b/contribs/github-bot/condition/file_test.go new file mode 100644 index 00000000000..f3d59475aff --- /dev/null +++ b/contribs/github-bot/condition/file_test.go @@ -0,0 +1,70 @@ +package condition + +import ( + "context" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestFileChanged(t *testing.T) { + t.Parallel() + + filenames := []*github.CommitFile{ + {Filename: github.String("foo")}, + {Filename: github.String("bar")}, + {Filename: github.String("baz")}, + } + + for _, testCase := range []struct { + name string + pattern string + files []*github.CommitFile + isMet bool + }{ + {"empty file list", "foo", []*github.CommitFile{}, false}, + {"file list contains exact match", "foo", filenames, true}, + {"file list contains prefix match", "^fo", filenames, true}, + {"file list contains prefix doesn't match", "fo$", filenames, false}, + {"file list contains suffix match", "oo$", filenames, true}, + {"file list contains suffix doesn't match", "^oo", filenames, false}, + {"file list doesn't contains match", "foobar", filenames, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/files", + Method: "GET", + }, + testCase.files, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + condition := FileChanged(gh, testCase.pattern) + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} diff --git a/contribs/github-bot/condition/label_test.go b/contribs/github-bot/condition/label_test.go new file mode 100644 index 00000000000..48f4c8273e7 --- /dev/null +++ b/contribs/github-bot/condition/label_test.go @@ -0,0 +1,50 @@ +package condition + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestLabel(t *testing.T) { + t.Parallel() + + labels := []*github.Label{ + {Name: github.String("notTheRightOne")}, + {Name: github.String("label")}, + {Name: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + pattern string + labels []*github.Label + isMet bool + }{ + {"empty label list", "label", []*github.Label{}, false}, + {"label list contains exact match", "label", labels, true}, + {"label list contains prefix match", "^lab", labels, true}, + {"label list contains prefix doesn't match", "lab$", labels, false}, + {"label list contains suffix match", "bel$", labels, true}, + {"label list contains suffix doesn't match", "^bel", labels, false}, + {"label list doesn't contains match", "baleb", labels, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{Labels: testCase.labels} + details := treeprint.New() + condition := Label(testCase.pattern) + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod index 02136376e63..2f1b7611b35 100644 --- a/contribs/github-bot/go.mod +++ b/contribs/github-bot/go.mod @@ -1,11 +1,19 @@ module github.com/gnolang/gno/contribs/github-bot -go 1.22.2 +go 1.23 + +toolchain go1.23.3 require ( github.com/google/go-github/v66 v66.0.0 + github.com/migueleliasweb/go-github-mock v1.1.0 github.com/sethvargo/go-githubactions v1.3.0 github.com/xlab/treeprint v1.2.0 ) -require github.com/google/go-querystring v1.1.0 // indirect +require ( + github.com/google/go-github/v64 v64.0.0 // indirect + github.com/google/go-querystring v1.1.0 // indirect + github.com/gorilla/mux v1.8.0 // indirect + golang.org/x/time v0.3.0 // indirect +) diff --git a/contribs/github-bot/go.sum b/contribs/github-bot/go.sum index 5e2d8a93984..5bf04978766 100644 --- a/contribs/github-bot/go.sum +++ b/contribs/github-bot/go.sum @@ -3,10 +3,16 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= +github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg= +github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= +github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= +github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE= +github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A= @@ -16,6 +22,8 @@ github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5Cc github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= +golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= diff --git a/contribs/github-bot/logger/logger.go b/contribs/github-bot/logger/logger.go index 53b50c6ed9a..2139fe07584 100644 --- a/contribs/github-bot/logger/logger.go +++ b/contribs/github-bot/logger/logger.go @@ -25,6 +25,7 @@ type Logger interface { Infof(msg string, args ...any) } +// Returns a logger suitable for Github Actions or terminal output func NewLogger(verbose bool) Logger { if _, isAction := os.LookupEnv("GITHUB_ACTION"); isAction { return newActionLogger() @@ -32,3 +33,8 @@ func NewLogger(verbose bool) Logger { return newTermLogger(verbose) } + +// NewNoopLogger returns a logger that does not log anything +func NewNoopLogger() Logger { + return newNoopLogger() +} diff --git a/contribs/github-bot/logger/noop.go b/contribs/github-bot/logger/noop.go new file mode 100644 index 00000000000..629ed9d52d9 --- /dev/null +++ b/contribs/github-bot/logger/noop.go @@ -0,0 +1,27 @@ +package logger + +type noopLogger struct{} + +var _ Logger = &noopLogger{} + +// Debugf implements Logger. +func (*noopLogger) Debugf(_ string, _ ...any) {} + +// Errorf implements Logger. +func (*noopLogger) Errorf(_ string, _ ...any) {} + +// Fatalf implements Logger. +func (*noopLogger) Fatalf(_ string, _ ...any) {} + +// Infof implements Logger. +func (*noopLogger) Infof(_ string, _ ...any) {} + +// Noticef implements Logger. +func (*noopLogger) Noticef(_ string, _ ...any) {} + +// Warningf implements Logger. +func (*noopLogger) Warningf(_ string, _ ...any) {} + +func newNoopLogger() Logger { + return &noopLogger{} +} diff --git a/contribs/github-bot/requirement/assignee_test.go b/contribs/github-bot/requirement/assignee_test.go new file mode 100644 index 00000000000..31f8ab02c61 --- /dev/null +++ b/contribs/github-bot/requirement/assignee_test.go @@ -0,0 +1,73 @@ +package requirement + +import ( + "context" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestAssignee(t *testing.T) { + t.Parallel() + + assignees := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + assignees []*github.User + exists bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", assignees, true}, + {"assignee list doesn't contain user", "user2", assignees, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/issues/0/assignees", + Method: "GET", // It looks like this mock package doesn't support mocking POST requests + }, + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + requested = true + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{Assignees: testCase.assignees} + details := treeprint.New() + requirement := Assignee(gh, testCase.user) + + if !requirement.IsSatisfied(pr, details) { + t.Errorf("requirement should have a satisfied status: %t", true) + } + if !utils.TestLastNodeStatus(t, true, details) { + t.Errorf("requirement details should have a status: %t", true) + } + if !testCase.exists && !requested { + t.Errorf("requirement should have requested to create item") + } + }) + } +} diff --git a/contribs/github-bot/requirement/author.go b/contribs/github-bot/requirement/author.go index 42c5f1c0f15..7e65bbb1ed1 100644 --- a/contribs/github-bot/requirement/author.go +++ b/contribs/github-bot/requirement/author.go @@ -10,7 +10,7 @@ import ( "github.com/xlab/treeprint" ) -// AuthorInTeam Requirement +// Author Requirement type author struct { user string } diff --git a/contribs/github-bot/requirement/author_test.go b/contribs/github-bot/requirement/author_test.go new file mode 100644 index 00000000000..fbfb956a2c5 --- /dev/null +++ b/contribs/github-bot/requirement/author_test.go @@ -0,0 +1,99 @@ +package requirement + +import ( + "context" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestAuthor(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + user string + author string + isSatisfied bool + }{ + {"author match", "user", "user", true}, + {"author doesn't match", "user", "author", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.author)}, + } + details := treeprint.New() + requirement := Author(testCase.user) + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + }) + } +} + +func TestAuthorInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isSatisfied bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", members, true}, + {"assignee list doesn't contain user", "user2", members, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + User: &github.User{Login: github.String(testCase.user)}, + } + details := treeprint.New() + requirement := AuthorInTeam(gh, "team") + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + }) + } +} diff --git a/contribs/github-bot/requirement/boolean_test.go b/contribs/github-bot/requirement/boolean_test.go new file mode 100644 index 00000000000..9ab5ce3ea27 --- /dev/null +++ b/contribs/github-bot/requirement/boolean_test.go @@ -0,0 +1,118 @@ +package requirement + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestAnd(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirements []Requirement + isSatisfied bool + }{ + {"and is true", []Requirement{Always(), Always()}, true}, + {"and is false", []Requirement{Always(), Always(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := And(testCase.requirements...) + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + }) + } +} + +func TestAndPanic(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + t.Errorf("and constructor should panic if less than 2 requirements are provided") + } + }() + + And(Always()) // Only 1 requirement provided +} + +func TestOr(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirements []Requirement + isSatisfied bool + }{ + {"or is true", []Requirement{Never(), Always()}, true}, + {"or is false", []Requirement{Never(), Never(), Never()}, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := Or(testCase.requirements...) + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + }) + } +} + +func TestOrPanic(t *testing.T) { + t.Parallel() + + defer func() { + if r := recover(); r == nil { + t.Errorf("and constructor should panic if less than 2 requirements are provided") + } + }() + + Or(Always()) // Only 1 requirement provided +} + +func TestNot(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + requirement Requirement + isSatisfied bool + }{ + {"not is true", Never(), true}, + {"not is false", Always(), false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := Not(testCase.requirement) + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + }) + } +} diff --git a/contribs/github-bot/requirement/constant.go b/contribs/github-bot/requirement/constant.go new file mode 100644 index 00000000000..9d1f4dc0d09 --- /dev/null +++ b/contribs/github-bot/requirement/constant.go @@ -0,0 +1,34 @@ +package requirement + +import ( + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +// Always Requirement +type always struct{} + +var _ Requirement = &always{} + +func (*always) IsSatisfied(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(true, "On every pull request", details) +} + +func Always() Requirement { + return &always{} +} + +// Never Requirement +type never struct{} + +var _ Requirement = &never{} + +func (*never) IsSatisfied(_ *github.PullRequest, details treeprint.Tree) bool { + return utils.AddStatusNode(false, "On no pull request", details) +} + +func Never() Requirement { + return &never{} +} diff --git a/contribs/github-bot/requirement/constant_test.go b/contribs/github-bot/requirement/constant_test.go new file mode 100644 index 00000000000..0f95286431b --- /dev/null +++ b/contribs/github-bot/requirement/constant_test.go @@ -0,0 +1,32 @@ +package requirement + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/xlab/treeprint" +) + +func TestAlways(t *testing.T) { + t.Parallel() + + details := treeprint.New() + if !Always().IsSatisfied(nil, details) { + t.Errorf("requirement should have a satisfied status: %t", true) + } + if !utils.TestLastNodeStatus(t, true, details) { + t.Errorf("requirement details should have a status: %t", true) + } +} + +func TestNever(t *testing.T) { + t.Parallel() + + details := treeprint.New() + if Never().IsSatisfied(nil, details) { + t.Errorf("requirement should have a satisfied status: %t", false) + } + if !utils.TestLastNodeStatus(t, false, details) { + t.Errorf("requirement details should have a status: %t", false) + } +} diff --git a/contribs/github-bot/requirement/label_test.go b/contribs/github-bot/requirement/label_test.go new file mode 100644 index 00000000000..415fb99241a --- /dev/null +++ b/contribs/github-bot/requirement/label_test.go @@ -0,0 +1,77 @@ +package requirement + +import ( + "context" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" + + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestLabel(t *testing.T) { + t.Parallel() + + labels := []*github.Label{ + {Name: github.String("notTheRightOne")}, + {Name: github.String("label")}, + {Name: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + pattern string + labels []*github.Label + exists bool + }{ + {"empty label list", "label", []*github.Label{}, false}, + {"label list contains exact match", "label", labels, true}, + {"label list contains prefix match", "^lab", labels, true}, + {"label list contains prefix doesn't match", "lab$", labels, false}, + {"label list contains suffix match", "bel$", labels, true}, + {"label list contains suffix doesn't match", "^bel", labels, false}, + {"label list doesn't contains match", "baleb", labels, false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/issues/0/labels", + Method: "GET", // It looks like this mock package doesn't support mocking POST requests + }, + http.HandlerFunc(func(_ http.ResponseWriter, _ *http.Request) { + requested = true + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{Labels: testCase.labels} + details := treeprint.New() + requirement := Label(gh, testCase.pattern) + + if !requirement.IsSatisfied(pr, details) { + t.Errorf("requirement should have a satisfied status: %t", true) + } + if !utils.TestLastNodeStatus(t, true, details) { + t.Errorf("requirement details should have a status: %t", true) + } + if !testCase.exists && !requested { + t.Errorf("requirement should have requested to create item") + } + }) + } +} diff --git a/contribs/github-bot/requirement/maintener_test.go b/contribs/github-bot/requirement/maintener_test.go new file mode 100644 index 00000000000..6902754c485 --- /dev/null +++ b/contribs/github-bot/requirement/maintener_test.go @@ -0,0 +1,36 @@ +package requirement + +import ( + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v66/github" + "github.com/xlab/treeprint" +) + +func TestMaintenerCanModify(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + isSatisfied bool + }{ + {"modify is true", true}, + {"modify is false", false}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + pr := &github.PullRequest{MaintainerCanModify: &testCase.isSatisfied} + details := treeprint.New() + requirement := MaintainerCanModify() + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + }) + } +} diff --git a/contribs/github-bot/requirement/reviewer_test.go b/contribs/github-bot/requirement/reviewer_test.go new file mode 100644 index 00000000000..8327dec17d4 --- /dev/null +++ b/contribs/github-bot/requirement/reviewer_test.go @@ -0,0 +1,107 @@ +package requirement + +import ( + "context" + "net/http" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v66/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestReviewByUser(t *testing.T) { + t.Parallel() + + reviewers := github.Reviewers{ + Users: []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + }, + } + + reviews := []*github.PullRequestReview{ + { + User: &github.User{Login: github.String("notTheRightOne")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("anotherOne")}, + State: github.String("REQUEST_CHANGES"), + }, + } + + for _, testCase := range []struct { + name string + user string + isSatisfied bool + create bool + }{ + {"reviewer match", "user", true, false}, + {"reviewer match without approval", "anotherOne", false, false}, + {"reviewer doesn't match", "user2", false, true}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + firstRequest := true + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/requested_reviewers", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if firstRequest { + w.Write(mock.MustMarshal(reviewers)) + firstRequest = false + } else { + requested = true + } + }), + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/reviews", + Method: "GET", + }, + reviews, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := ReviewByUser(gh, testCase.user) + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + if testCase.create != requested { + t.Errorf("requirement should have requested to create item: %t", testCase.create) + } + }) + } +} + +func TestReviewByTeamMembers(t *testing.T) { + t.Parallel() + + // TODO +} diff --git a/contribs/github-bot/utils/testing.go b/contribs/github-bot/utils/testing.go new file mode 100644 index 00000000000..f22ba21a97e --- /dev/null +++ b/contribs/github-bot/utils/testing.go @@ -0,0 +1,19 @@ +package utils + +import ( + "strings" + "testing" + + "github.com/xlab/treeprint" +) + +func TestLastNodeStatus(t *testing.T, success bool, details treeprint.Tree) bool { + t.Helper() + + detail := details.FindLastNode().(*treeprint.Node).Value.(string) + + if success { + return strings.HasPrefix(detail, StatusSuccess) + } + return strings.HasPrefix(detail, StatusFail) +} From ed02af262e67ab6464b80c133831bdcddc0d5e22 Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 11 Nov 2024 00:25:12 +0100 Subject: [PATCH 11/44] test: all req tested --- .github/workflows/bot.yml | 5 +- contribs/github-bot/README.md | 17 +-- contribs/github-bot/requirement/reviewer.go | 3 +- .../github-bot/requirement/reviewer_test.go | 121 +++++++++++++++++- 4 files changed, 132 insertions(+), 14 deletions(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 2d8298d82c2..f54f78421f6 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -1,7 +1,7 @@ name: GitHub Bot on: - # Watch for changes on PR state, assignees, labels and head branch + # Watch for changes on PR state, assignees, labels, head branch and draft/ready status pull_request: types: - assigned @@ -11,6 +11,8 @@ on: - opened - reopened - synchronize # PR head updated + - converted_to_draft + - ready_for_review # Watch for changes on PR comment issue_comment: @@ -105,6 +107,7 @@ jobs: go-version-file: go.mod - name: Run GitHub Bot + working-directory: contribs/github-bot env: GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT }} run: go run . -pr-numbers '${{ matrix.pr-number }}' -verbose diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md index 5f1bcd19098..1a15c461787 100644 --- a/contribs/github-bot/README.md +++ b/contribs/github-bot/README.md @@ -11,21 +11,18 @@ The GitHub Bot is designed to automate and streamline the process of managing pu The bot operates by defining a set of rules that are evaluated against each pull request passed as parameter. These rules are categorized into automatic and manual checks: - **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 exectued. 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 in specific files. +- **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). -### Conditions - -// TODO - -### Requirements - -// TODO - ### GitHub Token -// TODO +For the bot to make requests to the GitHub API, it needs a Personal Access Token. The fine-grained permissions to assign to the token for the bot to function are: + +- `pull_requests` scope to read is the bare minimum to run the bot in dry-run mode +- `pull_requests` scope to write to be able to update bot comment, assign user, apply label and request review +- `contents` scope to read to be able to check if the head branch is up to date with another one +- `commit_statuses` scope to write to be able to update pull request bot status check ## Usage diff --git a/contribs/github-bot/requirement/reviewer.go b/contribs/github-bot/requirement/reviewer.go index 3fcb7c1afa9..b45432bc042 100644 --- a/contribs/github-bot/requirement/reviewer.go +++ b/contribs/github-bot/requirement/reviewer.go @@ -111,9 +111,8 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr // Check how many members of this team already approved this PR approved := uint(0) - members := r.gh.ListTeamMembers(r.team) for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { - for _, member := range members { + for _, member := range r.gh.ListTeamMembers(r.team) { if review.GetUser().GetLogin() == member.GetLogin() { if review.GetState() == "APPROVED" { approved += 1 diff --git a/contribs/github-bot/requirement/reviewer_test.go b/contribs/github-bot/requirement/reviewer_test.go index 8327dec17d4..276267f848a 100644 --- a/contribs/github-bot/requirement/reviewer_test.go +++ b/contribs/github-bot/requirement/reviewer_test.go @@ -2,6 +2,7 @@ package requirement import ( "context" + "fmt" "net/http" "testing" @@ -103,5 +104,123 @@ func TestReviewByUser(t *testing.T) { func TestReviewByTeamMembers(t *testing.T) { t.Parallel() - // TODO + reviewers := github.Reviewers{ + Teams: []*github.Team{ + {Slug: github.String("team1")}, + {Slug: github.String("team2")}, + {Slug: github.String("team3")}, + }, + } + + members := map[string][]*github.User{ + "team1": { + {Login: github.String("user1")}, + {Login: github.String("user2")}, + {Login: github.String("user3")}, + }, + "team2": { + {Login: github.String("user3")}, + {Login: github.String("user4")}, + {Login: github.String("user5")}, + }, + "team3": { + {Login: github.String("user4")}, + {Login: github.String("user5")}, + }, + } + + reviews := []*github.PullRequestReview{ + { + User: &github.User{Login: github.String("user1")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user2")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user3")}, + State: github.String("APPROVED"), + }, { + User: &github.User{Login: github.String("user4")}, + State: github.String("REQUEST_CHANGES"), + }, { + User: &github.User{Login: github.String("user5")}, + State: github.String("REQUEST_CHANGES"), + }, + } + + for _, testCase := range []struct { + name string + team string + count uint + isSatisfied bool + testRequest bool + }{ + {"3/3 team members approved;", "team1", 3, true, false}, + {"1/1 team member approved", "team2", 1, true, false}, + {"1/2 team member approved", "team2", 2, false, false}, + {"0/1 team member approved", "team3", 1, false, false}, + {"0/1 team member approved with request", "team3", 1, false, true}, + {"team doesn't exist with request", "team4", 1, false, true}, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + firstRequest := true + requested := false + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/requested_reviewers", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if firstRequest { + if testCase.testRequest { + w.Write(mock.MustMarshal(github.Reviewers{})) + } else { + w.Write(mock.MustMarshal(reviewers)) + } + firstRequest = false + } else { + requested = true + } + }), + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: fmt.Sprintf("/orgs/teams/%s/members", testCase.team), + Method: "GET", + }, + members[testCase.team], + ), + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/pulls/0/reviews", + Method: "GET", + }, + reviews, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := ReviewByTeamMembers(gh, testCase.team, testCase.count) + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + if testCase.testRequest != requested { + t.Errorf("requirement should have requested to create item: %t", testCase.testRequest) + } + }) + } } From 3e1d086f6d229b50b7fc97beec32b658555d7fd1 Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 11 Nov 2024 19:03:20 +0100 Subject: [PATCH 12/44] lint: try downgrading to a compatible go version --- contribs/github-bot/client/client.go | 2 +- contribs/github-bot/comment.go | 2 +- contribs/github-bot/condition/assignee.go | 2 +- contribs/github-bot/condition/assignee_test.go | 2 +- contribs/github-bot/condition/author.go | 2 +- contribs/github-bot/condition/author_test.go | 2 +- contribs/github-bot/condition/boolean.go | 2 +- contribs/github-bot/condition/boolean_test.go | 2 +- contribs/github-bot/condition/branch.go | 2 +- contribs/github-bot/condition/branch_test.go | 2 +- contribs/github-bot/condition/condition.go | 2 +- contribs/github-bot/condition/constant.go | 2 +- contribs/github-bot/condition/file.go | 2 +- contribs/github-bot/condition/file_test.go | 2 +- contribs/github-bot/condition/label.go | 2 +- contribs/github-bot/condition/label_test.go | 2 +- contribs/github-bot/go.mod | 9 +++------ contribs/github-bot/go.sum | 6 ++---- contribs/github-bot/main.go | 2 +- contribs/github-bot/requirement/assignee.go | 2 +- contribs/github-bot/requirement/assignee_test.go | 2 +- contribs/github-bot/requirement/author.go | 2 +- contribs/github-bot/requirement/author_test.go | 2 +- contribs/github-bot/requirement/boolean.go | 2 +- contribs/github-bot/requirement/boolean_test.go | 2 +- contribs/github-bot/requirement/branch.go | 2 +- contribs/github-bot/requirement/constant.go | 2 +- contribs/github-bot/requirement/label.go | 2 +- contribs/github-bot/requirement/label_test.go | 2 +- contribs/github-bot/requirement/maintainer.go | 2 +- contribs/github-bot/requirement/maintener_test.go | 2 +- contribs/github-bot/requirement/requirement.go | 2 +- contribs/github-bot/requirement/reviewer.go | 2 +- contribs/github-bot/requirement/reviewer_test.go | 2 +- 34 files changed, 37 insertions(+), 42 deletions(-) diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 9d3433ff209..03d96b086e2 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -9,7 +9,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/param" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" ) const PageSize = 100 diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index c37a577ffa2..f6fd1ed36ad 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -10,7 +10,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/sethvargo/go-githubactions" ) diff --git a/contribs/github-bot/condition/assignee.go b/contribs/github-bot/condition/assignee.go index 6004c990a45..6976a0f51b0 100644 --- a/contribs/github-bot/condition/assignee.go +++ b/contribs/github-bot/condition/assignee.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/assignee_test.go b/contribs/github-bot/condition/assignee_test.go index a5fdd73066c..8973454878d 100644 --- a/contribs/github-bot/condition/assignee_test.go +++ b/contribs/github-bot/condition/assignee_test.go @@ -5,7 +5,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/author.go b/contribs/github-bot/condition/author.go index a186475e5fe..fd595d636f4 100644 --- a/contribs/github-bot/condition/author.go +++ b/contribs/github-bot/condition/author.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/author_test.go b/contribs/github-bot/condition/author_test.go index ff716c531af..42dc1817c1d 100644 --- a/contribs/github-bot/condition/author_test.go +++ b/contribs/github-bot/condition/author_test.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/boolean.go b/contribs/github-bot/condition/boolean.go index e7b3880e209..bea45bfc405 100644 --- a/contribs/github-bot/condition/boolean.go +++ b/contribs/github-bot/condition/boolean.go @@ -5,7 +5,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/boolean_test.go b/contribs/github-bot/condition/boolean_test.go index 955aaccd35d..9dd2e25c0c6 100644 --- a/contribs/github-bot/condition/boolean_test.go +++ b/contribs/github-bot/condition/boolean_test.go @@ -5,7 +5,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/branch.go b/contribs/github-bot/condition/branch.go index f1908114d52..8f5d7e6224c 100644 --- a/contribs/github-bot/condition/branch.go +++ b/contribs/github-bot/condition/branch.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/branch_test.go b/contribs/github-bot/condition/branch_test.go index 073d2cc6365..2e3f4533a73 100644 --- a/contribs/github-bot/condition/branch_test.go +++ b/contribs/github-bot/condition/branch_test.go @@ -5,7 +5,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/condition.go b/contribs/github-bot/condition/condition.go index 9dce8ea1a70..87b42c0c19d 100644 --- a/contribs/github-bot/condition/condition.go +++ b/contribs/github-bot/condition/condition.go @@ -1,7 +1,7 @@ package condition import ( - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/constant.go b/contribs/github-bot/condition/constant.go index 0b8b48c0c05..42084617f74 100644 --- a/contribs/github-bot/condition/constant.go +++ b/contribs/github-bot/condition/constant.go @@ -3,7 +3,7 @@ package condition import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/file.go b/contribs/github-bot/condition/file.go index ce614cd511b..1c3350970da 100644 --- a/contribs/github-bot/condition/file.go +++ b/contribs/github-bot/condition/file.go @@ -7,7 +7,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/file_test.go b/contribs/github-bot/condition/file_test.go index f3d59475aff..621da5d4f04 100644 --- a/contribs/github-bot/condition/file_test.go +++ b/contribs/github-bot/condition/file_test.go @@ -9,7 +9,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/migueleliasweb/go-github-mock/src/mock" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/label.go b/contribs/github-bot/condition/label.go index d5305924b9c..91012c03439 100644 --- a/contribs/github-bot/condition/label.go +++ b/contribs/github-bot/condition/label.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/condition/label_test.go b/contribs/github-bot/condition/label_test.go index 48f4c8273e7..04055004f1a 100644 --- a/contribs/github-bot/condition/label_test.go +++ b/contribs/github-bot/condition/label_test.go @@ -5,7 +5,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod index 2f1b7611b35..97b7fe4eb0e 100644 --- a/contribs/github-bot/go.mod +++ b/contribs/github-bot/go.mod @@ -1,18 +1,15 @@ module github.com/gnolang/gno/contribs/github-bot -go 1.23 - -toolchain go1.23.3 +go 1.21 require ( - github.com/google/go-github/v66 v66.0.0 - github.com/migueleliasweb/go-github-mock v1.1.0 + github.com/google/go-github/v64 v64.0.0 + github.com/migueleliasweb/go-github-mock v1.0.1 github.com/sethvargo/go-githubactions v1.3.0 github.com/xlab/treeprint v1.2.0 ) require ( - github.com/google/go-github/v64 v64.0.0 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/mux v1.8.0 // indirect golang.org/x/time v0.3.0 // indirect diff --git a/contribs/github-bot/go.sum b/contribs/github-bot/go.sum index 5bf04978766..5ea770790ed 100644 --- a/contribs/github-bot/go.sum +++ b/contribs/github-bot/go.sum @@ -5,14 +5,12 @@ github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKbyUb2Qdg= github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= -github.com/google/go-github/v66 v66.0.0 h1:ADJsaXj9UotwdgK8/iFZtv7MLc8E8WBl62WLd/D/9+M= -github.com/google/go-github/v66 v66.0.0/go.mod h1:+4SO9Zkuyf8ytMj0csN1NR/5OTR+MfqPp8P8dVlcvY4= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= -github.com/migueleliasweb/go-github-mock v1.1.0 h1:GKaOBPsrPGkAKgtfuWY8MclS1xR6MInkx1SexJucMwE= -github.com/migueleliasweb/go-github-mock v1.1.0/go.mod h1:pYe/XlGs4BGMfRY4vmeixVsODHnVDDhJ9zoi0qzSMHc= +github.com/migueleliasweb/go-github-mock v1.0.1 h1:amLEECVny28RCD1ElALUpQxrAimamznkg9rN2O7t934= +github.com/migueleliasweb/go-github-mock v1.0.1/go.mod h1:8PJ7MpMoIiCBBNpuNmvndHm0QicjsE+hjex1yMGmjYQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A= diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 6ebf7cd06ce..8d4efd4bc99 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -10,7 +10,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/param" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/assignee.go b/contribs/github-bot/requirement/assignee.go index 7f89e127e6b..601558e58ac 100644 --- a/contribs/github-bot/requirement/assignee.go +++ b/contribs/github-bot/requirement/assignee.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/assignee_test.go b/contribs/github-bot/requirement/assignee_test.go index 31f8ab02c61..98a73b06ee8 100644 --- a/contribs/github-bot/requirement/assignee_test.go +++ b/contribs/github-bot/requirement/assignee_test.go @@ -10,7 +10,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/migueleliasweb/go-github-mock/src/mock" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/author.go b/contribs/github-bot/requirement/author.go index 7e65bbb1ed1..a88663c1d20 100644 --- a/contribs/github-bot/requirement/author.go +++ b/contribs/github-bot/requirement/author.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/author_test.go b/contribs/github-bot/requirement/author_test.go index fbfb956a2c5..a65fc5bcf69 100644 --- a/contribs/github-bot/requirement/author_test.go +++ b/contribs/github-bot/requirement/author_test.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/boolean.go b/contribs/github-bot/requirement/boolean.go index 2b675d94860..916b91a49e3 100644 --- a/contribs/github-bot/requirement/boolean.go +++ b/contribs/github-bot/requirement/boolean.go @@ -5,7 +5,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/boolean_test.go b/contribs/github-bot/requirement/boolean_test.go index 9ab5ce3ea27..2307f5b69f2 100644 --- a/contribs/github-bot/requirement/boolean_test.go +++ b/contribs/github-bot/requirement/boolean_test.go @@ -5,7 +5,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/branch.go b/contribs/github-bot/requirement/branch.go index d047a3a23a7..eada48ea7c2 100644 --- a/contribs/github-bot/requirement/branch.go +++ b/contribs/github-bot/requirement/branch.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/constant.go b/contribs/github-bot/requirement/constant.go index 9d1f4dc0d09..d495c3c9dff 100644 --- a/contribs/github-bot/requirement/constant.go +++ b/contribs/github-bot/requirement/constant.go @@ -3,7 +3,7 @@ package requirement import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/label.go b/contribs/github-bot/requirement/label.go index 57b04c37a2b..e434a746901 100644 --- a/contribs/github-bot/requirement/label.go +++ b/contribs/github-bot/requirement/label.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/label_test.go b/contribs/github-bot/requirement/label_test.go index 415fb99241a..d7db65da2a4 100644 --- a/contribs/github-bot/requirement/label_test.go +++ b/contribs/github-bot/requirement/label_test.go @@ -10,7 +10,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/migueleliasweb/go-github-mock/src/mock" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/maintainer.go b/contribs/github-bot/requirement/maintainer.go index e1f95bc175a..b3ff81c203a 100644 --- a/contribs/github-bot/requirement/maintainer.go +++ b/contribs/github-bot/requirement/maintainer.go @@ -3,7 +3,7 @@ package requirement import ( "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/maintener_test.go b/contribs/github-bot/requirement/maintener_test.go index 6902754c485..ca3c5dcd4b8 100644 --- a/contribs/github-bot/requirement/maintener_test.go +++ b/contribs/github-bot/requirement/maintener_test.go @@ -4,7 +4,7 @@ import ( "testing" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/requirement.go b/contribs/github-bot/requirement/requirement.go index ae48a1e9648..0ed338708d6 100644 --- a/contribs/github-bot/requirement/requirement.go +++ b/contribs/github-bot/requirement/requirement.go @@ -1,7 +1,7 @@ package requirement import ( - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/reviewer.go b/contribs/github-bot/requirement/reviewer.go index b45432bc042..3b06c4f9c4f 100644 --- a/contribs/github-bot/requirement/reviewer.go +++ b/contribs/github-bot/requirement/reviewer.go @@ -6,7 +6,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirement/reviewer_test.go b/contribs/github-bot/requirement/reviewer_test.go index 276267f848a..6d141c8f340 100644 --- a/contribs/github-bot/requirement/reviewer_test.go +++ b/contribs/github-bot/requirement/reviewer_test.go @@ -10,7 +10,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" - "github.com/google/go-github/v66/github" + "github.com/google/go-github/v64/github" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/xlab/treeprint" ) From 5565a0d3e87dff6d00b53cfc78fc64599f3158af Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 11 Nov 2024 19:14:35 +0100 Subject: [PATCH 13/44] lint: ignore lostcancel warning --- contribs/github-bot/client/client.go | 1 + 1 file changed, 1 insertion(+) diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 03d96b086e2..56db29292d5 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -231,6 +231,7 @@ func New(params param.Params) *GitHub { // Create context with timeout if specified in the parameters if params.Timeout > 0 { + //nolint:govet,lostcancel // Keeping this cancel function is useless in this context gh.Ctx, _ = context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Millisecond) } else { gh.Ctx = context.Background() From 4483391267865ed2aecb154758bf51bac715679b Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 11 Nov 2024 21:16:44 +0100 Subject: [PATCH 14/44] test: add branch req + fix loop closure --- .../github-bot/condition/assignee_test.go | 1 + contribs/github-bot/condition/author_test.go | 2 + contribs/github-bot/condition/boolean_test.go | 3 + contribs/github-bot/condition/branch_test.go | 1 + contribs/github-bot/condition/file_test.go | 1 + contribs/github-bot/condition/label_test.go | 1 + .../github-bot/requirement/assignee_test.go | 1 + .../github-bot/requirement/author_test.go | 2 + .../github-bot/requirement/boolean_test.go | 3 + contribs/github-bot/requirement/branch.go | 1 + .../github-bot/requirement/branch_test.go | 65 +++++++++++++++++++ contribs/github-bot/requirement/label_test.go | 1 + .../github-bot/requirement/maintener_test.go | 1 + .../github-bot/requirement/reviewer_test.go | 2 + 14 files changed, 85 insertions(+) create mode 100644 contribs/github-bot/requirement/branch_test.go diff --git a/contribs/github-bot/condition/assignee_test.go b/contribs/github-bot/condition/assignee_test.go index 8973454878d..527c479ffb0 100644 --- a/contribs/github-bot/condition/assignee_test.go +++ b/contribs/github-bot/condition/assignee_test.go @@ -28,6 +28,7 @@ func TestAssignee(t *testing.T) { {"assignee list contains user", "user", assignees, true}, {"assignee list doesn't contain user", "user2", assignees, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/condition/author_test.go b/contribs/github-bot/condition/author_test.go index 42dc1817c1d..607045e4068 100644 --- a/contribs/github-bot/condition/author_test.go +++ b/contribs/github-bot/condition/author_test.go @@ -25,6 +25,7 @@ func TestAuthor(t *testing.T) { {"author match", "user", "user", true}, {"author doesn't match", "user", "author", false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -63,6 +64,7 @@ func TestAuthorInTeam(t *testing.T) { {"assignee list contains user", "user", members, true}, {"assignee list doesn't contain user", "user2", members, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/condition/boolean_test.go b/contribs/github-bot/condition/boolean_test.go index 9dd2e25c0c6..35530fd3e3d 100644 --- a/contribs/github-bot/condition/boolean_test.go +++ b/contribs/github-bot/condition/boolean_test.go @@ -20,6 +20,7 @@ func TestAnd(t *testing.T) { {"and is true", []Condition{Always(), Always()}, true}, {"and is false", []Condition{Always(), Always(), Never()}, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -60,6 +61,7 @@ func TestOr(t *testing.T) { {"or is true", []Condition{Never(), Always()}, true}, {"or is false", []Condition{Never(), Never(), Never()}, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -100,6 +102,7 @@ func TestNot(t *testing.T) { {"not is true", Never(), true}, {"not is false", Always(), false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/condition/branch_test.go b/contribs/github-bot/condition/branch_test.go index 2e3f4533a73..4dc4a6030a6 100644 --- a/contribs/github-bot/condition/branch_test.go +++ b/contribs/github-bot/condition/branch_test.go @@ -25,6 +25,7 @@ func TestHeadBaseBranch(t *testing.T) { {"suffix doesn't match", "^/test-bot", "dev/test-bot", false}, {"doesn't match", "base", "notatall", false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/condition/file_test.go b/contribs/github-bot/condition/file_test.go index 621da5d4f04..b7299061fee 100644 --- a/contribs/github-bot/condition/file_test.go +++ b/contribs/github-bot/condition/file_test.go @@ -36,6 +36,7 @@ func TestFileChanged(t *testing.T) { {"file list contains suffix doesn't match", "^oo", filenames, false}, {"file list doesn't contains match", "foobar", filenames, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/condition/label_test.go b/contribs/github-bot/condition/label_test.go index 04055004f1a..36d3d6708b3 100644 --- a/contribs/github-bot/condition/label_test.go +++ b/contribs/github-bot/condition/label_test.go @@ -32,6 +32,7 @@ func TestLabel(t *testing.T) { {"label list contains suffix doesn't match", "^bel", labels, false}, {"label list doesn't contains match", "baleb", labels, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirement/assignee_test.go b/contribs/github-bot/requirement/assignee_test.go index 98a73b06ee8..9f277f2c31f 100644 --- a/contribs/github-bot/requirement/assignee_test.go +++ b/contribs/github-bot/requirement/assignee_test.go @@ -33,6 +33,7 @@ func TestAssignee(t *testing.T) { {"assignee list contains user", "user", assignees, true}, {"assignee list doesn't contain user", "user2", assignees, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirement/author_test.go b/contribs/github-bot/requirement/author_test.go index a65fc5bcf69..d32f5fb4257 100644 --- a/contribs/github-bot/requirement/author_test.go +++ b/contribs/github-bot/requirement/author_test.go @@ -25,6 +25,7 @@ func TestAuthor(t *testing.T) { {"author match", "user", "user", true}, {"author doesn't match", "user", "author", false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -63,6 +64,7 @@ func TestAuthorInTeam(t *testing.T) { {"assignee list contains user", "user", members, true}, {"assignee list doesn't contain user", "user2", members, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirement/boolean_test.go b/contribs/github-bot/requirement/boolean_test.go index 2307f5b69f2..f20b7f67cbe 100644 --- a/contribs/github-bot/requirement/boolean_test.go +++ b/contribs/github-bot/requirement/boolean_test.go @@ -20,6 +20,7 @@ func TestAnd(t *testing.T) { {"and is true", []Requirement{Always(), Always()}, true}, {"and is false", []Requirement{Always(), Always(), Never()}, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -60,6 +61,7 @@ func TestOr(t *testing.T) { {"or is true", []Requirement{Never(), Always()}, true}, {"or is false", []Requirement{Never(), Never(), Never()}, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -100,6 +102,7 @@ func TestNot(t *testing.T) { {"not is true", Never(), true}, {"not is false", Always(), false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirement/branch.go b/contribs/github-bot/requirement/branch.go index eada48ea7c2..3700a7d6776 100644 --- a/contribs/github-bot/requirement/branch.go +++ b/contribs/github-bot/requirement/branch.go @@ -31,6 +31,7 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil) if err != nil { + fmt.Println(err) u.gh.Logger.Errorf("Unable to compare head branch (%s) and base (%s): %v", head, base, err) return false } diff --git a/contribs/github-bot/requirement/branch_test.go b/contribs/github-bot/requirement/branch_test.go new file mode 100644 index 00000000000..67a3760e968 --- /dev/null +++ b/contribs/github-bot/requirement/branch_test.go @@ -0,0 +1,65 @@ +package requirement + +import ( + "context" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + "github.com/gnolang/gno/contribs/github-bot/utils" + + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/xlab/treeprint" +) + +func TestUpToDateWith(t *testing.T) { + t.Parallel() + + for _, testCase := range []struct { + name string + behind int + ahead int + isSatisfied bool + }{ + {"up-to-date without commit ahead", 0, 0, true}, + {"up-to-date with commits ahead", 0, 3, true}, + {"not up-to-date with commits behind", 3, 0, false}, + {"not up-to-date with commits behind and ahead", 3, 3, false}, + } { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/repos/compare/base...", + Method: "GET", + }, + github.CommitsComparison{ + AheadBy: &testCase.ahead, + BehindBy: &testCase.behind, + }, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{} + details := treeprint.New() + requirement := UpToDateWith(gh, "base") + + if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { + t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) + } + if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { + t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) + } + }) + } +} diff --git a/contribs/github-bot/requirement/label_test.go b/contribs/github-bot/requirement/label_test.go index d7db65da2a4..f012b746cee 100644 --- a/contribs/github-bot/requirement/label_test.go +++ b/contribs/github-bot/requirement/label_test.go @@ -37,6 +37,7 @@ func TestLabel(t *testing.T) { {"label list contains suffix doesn't match", "^bel", labels, false}, {"label list doesn't contains match", "baleb", labels, false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirement/maintener_test.go b/contribs/github-bot/requirement/maintener_test.go index ca3c5dcd4b8..b56d155ca7b 100644 --- a/contribs/github-bot/requirement/maintener_test.go +++ b/contribs/github-bot/requirement/maintener_test.go @@ -18,6 +18,7 @@ func TestMaintenerCanModify(t *testing.T) { {"modify is true", true}, {"modify is false", false}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirement/reviewer_test.go b/contribs/github-bot/requirement/reviewer_test.go index 6d141c8f340..9468bc5e48e 100644 --- a/contribs/github-bot/requirement/reviewer_test.go +++ b/contribs/github-bot/requirement/reviewer_test.go @@ -49,6 +49,7 @@ func TestReviewByUser(t *testing.T) { {"reviewer match without approval", "anotherOne", false, false}, {"reviewer doesn't match", "user2", false, true}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -162,6 +163,7 @@ func TestReviewByTeamMembers(t *testing.T) { {"0/1 team member approved with request", "team3", 1, false, true}, {"team doesn't exist with request", "team4", 1, false, true}, } { + testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() From d809c229910747dc9e07816b2d321a850fef531d Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 11 Nov 2024 21:41:33 +0100 Subject: [PATCH 15/44] test: improve coverage --- .../github-bot/condition/assignee_test.go | 61 +++++++++++++++++++ contribs/github-bot/condition/author_test.go | 6 +- .../github-bot/requirement/assignee_test.go | 16 +++-- .../github-bot/requirement/author_test.go | 6 +- contribs/github-bot/requirement/label_test.go | 27 +++++--- 5 files changed, 94 insertions(+), 22 deletions(-) diff --git a/contribs/github-bot/condition/assignee_test.go b/contribs/github-bot/condition/assignee_test.go index 527c479ffb0..f0090ffb249 100644 --- a/contribs/github-bot/condition/assignee_test.go +++ b/contribs/github-bot/condition/assignee_test.go @@ -1,9 +1,13 @@ package condition import ( + "context" "testing" + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -45,3 +49,60 @@ func TestAssignee(t *testing.T) { }) } } + +func TestAssigneeInTeam(t *testing.T) { + t.Parallel() + + members := []*github.User{ + {Login: github.String("notTheRightOne")}, + {Login: github.String("user")}, + {Login: github.String("anotherOne")}, + } + + for _, testCase := range []struct { + name string + user string + members []*github.User + isMet bool + }{ + {"empty assignee list", "user", []*github.User{}, false}, + {"assignee list contains user", "user", members, true}, + {"assignee list doesn't contain user", "user2", members, false}, + } { + testCase := testCase + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/orgs/teams/team/members", + Method: "GET", + }, + testCase.members, + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + pr := &github.PullRequest{ + Assignees: []*github.User{ + {Login: github.String(testCase.user)}, + }, + } + details := treeprint.New() + condition := AssigneeInTeam(gh, "team") + + if condition.IsMet(pr, details) != testCase.isMet { + t.Errorf("condition should have a met status: %t", testCase.isMet) + } + if !utils.TestLastNodeStatus(t, testCase.isMet, details) { + t.Errorf("condition details should have a status: %t", testCase.isMet) + } + }) + } +} diff --git a/contribs/github-bot/condition/author_test.go b/contribs/github-bot/condition/author_test.go index 607045e4068..d93a50ba89a 100644 --- a/contribs/github-bot/condition/author_test.go +++ b/contribs/github-bot/condition/author_test.go @@ -60,9 +60,9 @@ func TestAuthorInTeam(t *testing.T) { members []*github.User isMet bool }{ - {"empty assignee list", "user", []*github.User{}, false}, - {"assignee list contains user", "user", members, true}, - {"assignee list doesn't contain user", "user2", members, false}, + {"empty member list", "user", []*github.User{}, false}, + {"member list contains user", "user", members, true}, + {"member list doesn't contain user", "user2", members, false}, } { testCase := testCase t.Run(testCase.name, func(t *testing.T) { diff --git a/contribs/github-bot/requirement/assignee_test.go b/contribs/github-bot/requirement/assignee_test.go index 9f277f2c31f..d6e10a86b63 100644 --- a/contribs/github-bot/requirement/assignee_test.go +++ b/contribs/github-bot/requirement/assignee_test.go @@ -27,11 +27,14 @@ func TestAssignee(t *testing.T) { name string user string assignees []*github.User + dryRun bool exists bool }{ - {"empty assignee list", "user", []*github.User{}, false}, - {"assignee list contains user", "user", assignees, true}, - {"assignee list doesn't contain user", "user2", assignees, false}, + {"empty assignee list", "user", []*github.User{}, false, false}, + {"empty assignee list with dry-run", "user", []*github.User{}, true, false}, + {"assignee list contains user", "user", assignees, false, true}, + {"assignee list doesn't contain user", "user2", assignees, false, false}, + {"assignee list doesn't contain user with dry-run", "user2", assignees, true, false}, } { testCase := testCase t.Run(testCase.name, func(t *testing.T) { @@ -54,19 +57,20 @@ func TestAssignee(t *testing.T) { Client: github.NewClient(mockedHTTPClient), Ctx: context.Background(), Logger: logger.NewNoopLogger(), + DryRun: testCase.dryRun, } pr := &github.PullRequest{Assignees: testCase.assignees} details := treeprint.New() requirement := Assignee(gh, testCase.user) - if !requirement.IsSatisfied(pr, details) { + if !requirement.IsSatisfied(pr, details) && !testCase.dryRun { t.Errorf("requirement should have a satisfied status: %t", true) } - if !utils.TestLastNodeStatus(t, true, details) { + if !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun { t.Errorf("requirement details should have a status: %t", true) } - if !testCase.exists && !requested { + if !testCase.exists && !requested && !testCase.dryRun { t.Errorf("requirement should have requested to create item") } }) diff --git a/contribs/github-bot/requirement/author_test.go b/contribs/github-bot/requirement/author_test.go index d32f5fb4257..b318de9a80f 100644 --- a/contribs/github-bot/requirement/author_test.go +++ b/contribs/github-bot/requirement/author_test.go @@ -60,9 +60,9 @@ func TestAuthorInTeam(t *testing.T) { members []*github.User isSatisfied bool }{ - {"empty assignee list", "user", []*github.User{}, false}, - {"assignee list contains user", "user", members, true}, - {"assignee list doesn't contain user", "user2", members, false}, + {"empty member list", "user", []*github.User{}, false}, + {"member list contains user", "user", members, true}, + {"member list doesn't contain user", "user2", members, false}, } { testCase := testCase t.Run(testCase.name, func(t *testing.T) { diff --git a/contribs/github-bot/requirement/label_test.go b/contribs/github-bot/requirement/label_test.go index f012b746cee..28927125c31 100644 --- a/contribs/github-bot/requirement/label_test.go +++ b/contribs/github-bot/requirement/label_test.go @@ -27,15 +27,21 @@ func TestLabel(t *testing.T) { name string pattern string labels []*github.Label + dryRun bool exists bool }{ - {"empty label list", "label", []*github.Label{}, false}, - {"label list contains exact match", "label", labels, true}, - {"label list contains prefix match", "^lab", labels, true}, - {"label list contains prefix doesn't match", "lab$", labels, false}, - {"label list contains suffix match", "bel$", labels, true}, - {"label list contains suffix doesn't match", "^bel", labels, false}, - {"label list doesn't contains match", "baleb", labels, false}, + {"empty label list", "label", []*github.Label{}, false, false}, + {"empty label list with dry-run", "label", []*github.Label{}, true, false}, + {"label list contains exact match", "label", labels, false, true}, + {"label list contains prefix match", "^lab", labels, false, true}, + {"label list contains prefix doesn't match", "lab$", labels, false, false}, + {"label list contains prefix doesn't match with dry-run", "lab$", labels, true, false}, + {"label list contains suffix match", "bel$", labels, false, true}, + {"label list contains suffix match with dry-run", "bel$", labels, true, true}, + {"label list contains suffix doesn't match", "^bel", labels, false, false}, + {"label list contains suffix doesn't match with dry-run", "^bel", labels, true, false}, + {"label list doesn't contains match", "baleb", labels, false, false}, + {"label list doesn't contains match with dry-run", "baleb", labels, true, false}, } { testCase := testCase t.Run(testCase.name, func(t *testing.T) { @@ -58,19 +64,20 @@ func TestLabel(t *testing.T) { Client: github.NewClient(mockedHTTPClient), Ctx: context.Background(), Logger: logger.NewNoopLogger(), + DryRun: testCase.dryRun, } pr := &github.PullRequest{Labels: testCase.labels} details := treeprint.New() requirement := Label(gh, testCase.pattern) - if !requirement.IsSatisfied(pr, details) { + if !requirement.IsSatisfied(pr, details) && !testCase.dryRun { t.Errorf("requirement should have a satisfied status: %t", true) } - if !utils.TestLastNodeStatus(t, true, details) { + if !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun { t.Errorf("requirement details should have a status: %t", true) } - if !testCase.exists && !requested { + if !testCase.exists && !requested && !testCase.dryRun { t.Errorf("requirement should have requested to create item") } }) From fa15c705a64c17684e4b10c2052b13dd8f44657f Mon Sep 17 00:00:00 2001 From: aeddi Date: Thu, 14 Nov 2024 18:29:34 +0100 Subject: [PATCH 16/44] ci: change for pull request target --- .github/workflows/bot.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index f54f78421f6..4d95b7650da 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -2,7 +2,7 @@ name: GitHub Bot on: # Watch for changes on PR state, assignees, labels, head branch and draft/ready status - pull_request: + pull_request_target: types: - assigned - unassigned @@ -74,8 +74,8 @@ jobs: elif [ '${{ github.event_name }}' = 'issue_comment' ]; then echo 'pr-numbers=["${{ github.event.issue.number }}"]' >> "$GITHUB_OUTPUT" - # Triggered by pull request event, just add the associated PR number to the matrix - elif [ '${{ github.event_name }}' = 'pull_request' ]; then + # Triggered by pull request target event, just add the associated PR number to the matrix + elif [ '${{ github.event_name }}' = 'pull_request_target' ]; then echo 'pr-numbers=["${{ github.event.pull_request.number }}"]' >> "$GITHUB_OUTPUT" # Should never happen From a03f67db29dfac323bc7f7cd97c1be58daf84b49 Mon Sep 17 00:00:00 2001 From: aeddi Date: Thu, 14 Nov 2024 18:48:14 +0100 Subject: [PATCH 17/44] feat: add github client context cancelation --- contribs/github-bot/client/client.go | 10 ++++++++-- contribs/github-bot/main.go | 1 + 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 56db29292d5..3c3f5375b59 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -21,6 +21,7 @@ type GitHub struct { Logger logger.Logger Owner string Repo string + cancel context.CancelFunc } func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { @@ -218,6 +219,12 @@ func (gh *GitHub) ListPrReviews(prNum int) []*github.PullRequestReview { return allReviews } +func (gh *GitHub) Close() { + if gh.cancel != nil { + gh.cancel() + } +} + func New(params param.Params) *GitHub { gh := &GitHub{ Owner: params.Owner, @@ -231,8 +238,7 @@ func New(params param.Params) *GitHub { // Create context with timeout if specified in the parameters if params.Timeout > 0 { - //nolint:govet,lostcancel // Keeping this cancel function is useless in this context - gh.Ctx, _ = context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Millisecond) + gh.Ctx, gh.cancel = context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Millisecond) } else { gh.Ctx = context.Background() } diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 8d4efd4bc99..5be991bbf50 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -20,6 +20,7 @@ func main() { // Init GitHub API client gh := client.New(params) + defer gh.Close() // Handle comment update, if any handleCommentUpdate(gh) From 76f5f3dcebb6ec6d79789aa11779e1d7435702c8 Mon Sep 17 00:00:00 2001 From: aeddi Date: Thu, 14 Nov 2024 18:48:43 +0100 Subject: [PATCH 18/44] refacto: remove useless else statement --- contribs/github-bot/client/client.go | 31 ++++++++++++++-------------- 1 file changed, 16 insertions(+), 15 deletions(-) diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 3c3f5375b59..b6b01d8c403 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -79,7 +79,8 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { // Create bot comment if it not already exists - if comment := gh.GetBotComment(prNum); comment == nil { + comment := gh.GetBotComment(prNum) + if comment == nil { newComment, _, err := gh.Client.Issues.CreateComment( gh.Ctx, gh.Owner, @@ -92,21 +93,21 @@ func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { return nil } return newComment - } else { - comment.Body = &body - editComment, _, err := gh.Client.Issues.EditComment( - gh.Ctx, - gh.Owner, - gh.Repo, - comment.GetID(), - comment, - ) - if err != nil { - gh.Logger.Errorf("Unable to edit bot comment with ID %d: %v", comment.GetID(), err) - return nil - } - return editComment } + + comment.Body = &body + editComment, _, err := gh.Client.Issues.EditComment( + gh.Ctx, + gh.Owner, + gh.Repo, + comment.GetID(), + comment, + ) + if err != nil { + gh.Logger.Errorf("Unable to edit bot comment with ID %d: %v", comment.GetID(), err) + return nil + } + return editComment } func (gh *GitHub) ListTeamMembers(team string) []*github.User { From 61f1a43f1a03d26a739c7abb8d48841703013e22 Mon Sep 17 00:00:00 2001 From: aeddi Date: Thu, 14 Nov 2024 19:01:05 +0100 Subject: [PATCH 19/44] refacto: exit from main instead of handleCommentUpdate func --- contribs/github-bot/comment.go | 55 ++++++++++++++-------------------- contribs/github-bot/main.go | 6 +++- 2 files changed, 27 insertions(+), 34 deletions(-) diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index f6fd1ed36ad..7ed25643e53 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -60,36 +60,34 @@ func getCommentManualChecks(commentBody string) map[string][2]string { // - the comment was not edited by the bot itself (prevent infinite loop) // - the comment change is only a checkbox being checked or unckecked (or restore it) // - the actor / comment editor has permission to modify this checkbox (or restore it) -func handleCommentUpdate(gh *client.GitHub) { +func handleCommentUpdate(gh *client.GitHub) error { // Get GitHub Actions context to retrieve comment update actionCtx, err := githubactions.Context() if err != nil { gh.Logger.Debugf("Unable to retrieve GitHub Actions context: %v", err) - return + return nil } // Ignore if it's not a comment related event if actionCtx.EventName != "issue_comment" { gh.Logger.Debugf("Event is not issue comment related (%s)", actionCtx.EventName) - return + return nil } // Ignore if the action type is not deleted or edited actionType, ok := actionCtx.Event["action"].(string) if !ok { - gh.Logger.Errorf("Unable to get type on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get type on issue comment event") } if actionType != "deleted" && actionType != "edited" { - return + return nil } - // Exit if comment was edited by bot (current authenticated user) + // Return if comment was edited by bot (current authenticated user) authUser, _, err := gh.Client.Users.Get(gh.Ctx, "") if err != nil { - gh.Logger.Errorf("Unable to get authenticated user: %v", err) - os.Exit(1) + return fmt.Errorf("unable to get authenticated user: %w", err) } if actionCtx.Actor == authUser.GetLogin() { @@ -100,74 +98,64 @@ func handleCommentUpdate(gh *client.GitHub) { // Ignore if edited comment author is not the bot comment, ok := actionCtx.Event["comment"].(map[string]any) if !ok { - gh.Logger.Errorf("Unable to get comment on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get comment on issue comment event") } author, ok := comment["user"].(map[string]any) if !ok { - gh.Logger.Errorf("Unable to get comment user on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get comment user on issue comment event") } login, ok := author["login"].(string) if !ok { - gh.Logger.Errorf("Unable to get comment user login on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get comment user login on issue comment event") } if login != authUser.GetLogin() { - return + return nil } // Get comment current body current, ok := comment["body"].(string) if !ok { - gh.Logger.Errorf("Unable to get comment body on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get comment body on issue comment event") } // Get comment updated body changes, ok := actionCtx.Event["changes"].(map[string]any) if !ok { - gh.Logger.Errorf("Unable to get changes on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get changes on issue comment event") } changesBody, ok := changes["body"].(map[string]any) if !ok { - gh.Logger.Errorf("Unable to get changes body on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get changes body on issue comment event") } previous, ok := changesBody["from"].(string) if !ok { - gh.Logger.Errorf("Unable to get changes body content on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get changes body content on issue comment event") } // Get PR number from GitHub Actions context issue, ok := actionCtx.Event["issue"].(map[string]any) if !ok { - gh.Logger.Errorf("Unable to get issue on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get issue on issue comment event") } num, ok := issue["number"].(float64) if !ok || num <= 0 { - gh.Logger.Errorf("Unable to get issue number on issue comment event") - os.Exit(1) + return fmt.Errorf("unable to get issue number on issue comment event") } // Check if change is only a checkbox being checked or unckecked checkboxes := regexp.MustCompile(`(?m:^- \[[ x]\])`) if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") { // If not, restore previous comment body - gh.Logger.Errorf("Bot comment edited outside of checkboxes") if !gh.DryRun { gh.SetBotComment(previous, int(num)) } - os.Exit(1) + return fmt.Errorf("bot comment edited outside of checkboxes") } // Check if actor / comment editor has permission to modify changed boxes @@ -192,17 +180,16 @@ func handleCommentUpdate(gh *client.GitHub) { // (maybe bot config was updated since last run?) if !found { gh.Logger.Debugf("Updated rule not found in config: %s", key) - return + return nil } // 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) { - gh.Logger.Errorf("Checkbox edited by a user not allowed to") if !gh.DryRun { gh.SetBotComment(previous, int(num)) } - os.Exit(1) + return fmt.Errorf("checkbox edited by a user not allowed to") } } @@ -223,6 +210,8 @@ func handleCommentUpdate(gh *client.GitHub) { gh.SetBotComment(edited, int(num)) gh.Logger.Debugf("Comment manual checks updated successfully") } + + return nil } func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentContent) { diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 5be991bbf50..97c8ed8657c 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -2,6 +2,7 @@ package main import ( "fmt" + "os" "strings" "sync" @@ -23,7 +24,10 @@ func main() { defer gh.Close() // Handle comment update, if any - handleCommentUpdate(gh) + if err := handleCommentUpdate(gh); err != nil { + gh.Logger.Errorf("Comment update handling failed: %v", err) + os.Exit(1) + } // Retrieve a slice of pull requests to process var ( From fbe29725c9172886cc64b186dc460ec5964dba8a Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 15 Nov 2024 14:51:49 +0100 Subject: [PATCH 20/44] style: consistency plural package names --- contribs/github-bot/client/client.go | 4 ++-- contribs/github-bot/{condition => conditions}/assignee.go | 2 +- .../github-bot/{condition => conditions}/assignee_test.go | 2 +- contribs/github-bot/{condition => conditions}/author.go | 2 +- contribs/github-bot/{condition => conditions}/author_test.go | 2 +- contribs/github-bot/{condition => conditions}/boolean.go | 2 +- contribs/github-bot/{condition => conditions}/boolean_test.go | 2 +- contribs/github-bot/{condition => conditions}/branch.go | 2 +- contribs/github-bot/{condition => conditions}/branch_test.go | 2 +- contribs/github-bot/{condition => conditions}/condition.go | 2 +- contribs/github-bot/{condition => conditions}/constant.go | 2 +- .../github-bot/{condition => conditions}/constant_test.go | 2 +- contribs/github-bot/{condition => conditions}/file.go | 2 +- contribs/github-bot/{condition => conditions}/file_test.go | 2 +- contribs/github-bot/{condition => conditions}/label.go | 2 +- contribs/github-bot/{condition => conditions}/label_test.go | 2 +- contribs/github-bot/config.go | 4 ++-- contribs/github-bot/main.go | 4 ++-- contribs/github-bot/{param/param.go => params/params.go} | 2 +- contribs/github-bot/{param => params}/prlist.go | 2 +- contribs/github-bot/{requirement => requirements}/assignee.go | 2 +- .../github-bot/{requirement => requirements}/assignee_test.go | 2 +- contribs/github-bot/{requirement => requirements}/author.go | 2 +- .../github-bot/{requirement => requirements}/author_test.go | 2 +- contribs/github-bot/{requirement => requirements}/boolean.go | 2 +- .../github-bot/{requirement => requirements}/boolean_test.go | 2 +- contribs/github-bot/{requirement => requirements}/branch.go | 2 +- .../github-bot/{requirement => requirements}/branch_test.go | 2 +- contribs/github-bot/{requirement => requirements}/constant.go | 2 +- .../github-bot/{requirement => requirements}/constant_test.go | 2 +- contribs/github-bot/{requirement => requirements}/label.go | 2 +- .../github-bot/{requirement => requirements}/label_test.go | 2 +- .../github-bot/{requirement => requirements}/maintainer.go | 2 +- .../{requirement => requirements}/maintener_test.go | 2 +- .../github-bot/{requirement => requirements}/requirement.go | 2 +- contribs/github-bot/{requirement => requirements}/reviewer.go | 2 +- .../github-bot/{requirement => requirements}/reviewer_test.go | 2 +- 37 files changed, 40 insertions(+), 40 deletions(-) rename contribs/github-bot/{condition => conditions}/assignee.go (98%) rename contribs/github-bot/{condition => conditions}/assignee_test.go (99%) rename contribs/github-bot/{condition => conditions}/author.go (98%) rename contribs/github-bot/{condition => conditions}/author_test.go (99%) rename contribs/github-bot/{condition => conditions}/boolean.go (99%) rename contribs/github-bot/{condition => conditions}/boolean_test.go (99%) rename contribs/github-bot/{condition => conditions}/branch.go (98%) rename contribs/github-bot/{condition => conditions}/branch_test.go (98%) rename contribs/github-bot/{condition => conditions}/condition.go (93%) rename contribs/github-bot/{condition => conditions}/constant.go (97%) rename contribs/github-bot/{condition => conditions}/constant_test.go (97%) rename contribs/github-bot/{condition => conditions}/file.go (98%) rename contribs/github-bot/{condition => conditions}/file_test.go (99%) rename contribs/github-bot/{condition => conditions}/label.go (97%) rename contribs/github-bot/{condition => conditions}/label_test.go (98%) rename contribs/github-bot/{param/param.go => params/params.go} (99%) rename contribs/github-bot/{param => params}/prlist.go (98%) rename contribs/github-bot/{requirement => requirements}/assignee.go (98%) rename contribs/github-bot/{requirement => requirements}/assignee_test.go (99%) rename contribs/github-bot/{requirement => requirements}/author.go (98%) rename contribs/github-bot/{requirement => requirements}/author_test.go (99%) rename contribs/github-bot/{requirement => requirements}/boolean.go (99%) rename contribs/github-bot/{requirement => requirements}/boolean_test.go (99%) rename contribs/github-bot/{requirement => requirements}/branch.go (98%) rename contribs/github-bot/{requirement => requirements}/branch_test.go (98%) rename contribs/github-bot/{requirement => requirements}/constant.go (97%) rename contribs/github-bot/{requirement => requirements}/constant_test.go (97%) rename contribs/github-bot/{requirement => requirements}/label.go (98%) rename contribs/github-bot/{requirement => requirements}/label_test.go (99%) rename contribs/github-bot/{requirement => requirements}/maintainer.go (96%) rename contribs/github-bot/{requirement => requirements}/maintener_test.go (97%) rename contribs/github-bot/{requirement => requirements}/requirement.go (93%) rename contribs/github-bot/{requirement => requirements}/reviewer.go (99%) rename contribs/github-bot/{requirement => requirements}/reviewer_test.go (99%) diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index b6b01d8c403..227fbb4be7c 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -7,7 +7,7 @@ import ( "time" "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/param" + p "github.com/gnolang/gno/contribs/github-bot/params" "github.com/google/go-github/v64/github" ) @@ -226,7 +226,7 @@ func (gh *GitHub) Close() { } } -func New(params param.Params) *GitHub { +func New(params p.Params) *GitHub { gh := &GitHub{ Owner: params.Owner, Repo: params.Repo, diff --git a/contribs/github-bot/condition/assignee.go b/contribs/github-bot/conditions/assignee.go similarity index 98% rename from contribs/github-bot/condition/assignee.go rename to contribs/github-bot/conditions/assignee.go index 6976a0f51b0..bf1d9c360bc 100644 --- a/contribs/github-bot/condition/assignee.go +++ b/contribs/github-bot/conditions/assignee.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "fmt" diff --git a/contribs/github-bot/condition/assignee_test.go b/contribs/github-bot/conditions/assignee_test.go similarity index 99% rename from contribs/github-bot/condition/assignee_test.go rename to contribs/github-bot/conditions/assignee_test.go index f0090ffb249..bb1c5adfdc0 100644 --- a/contribs/github-bot/condition/assignee_test.go +++ b/contribs/github-bot/conditions/assignee_test.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "context" diff --git a/contribs/github-bot/condition/author.go b/contribs/github-bot/conditions/author.go similarity index 98% rename from contribs/github-bot/condition/author.go rename to contribs/github-bot/conditions/author.go index fd595d636f4..3a2aed6c396 100644 --- a/contribs/github-bot/condition/author.go +++ b/contribs/github-bot/conditions/author.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "fmt" diff --git a/contribs/github-bot/condition/author_test.go b/contribs/github-bot/conditions/author_test.go similarity index 99% rename from contribs/github-bot/condition/author_test.go rename to contribs/github-bot/conditions/author_test.go index d93a50ba89a..1abbdf26a3a 100644 --- a/contribs/github-bot/condition/author_test.go +++ b/contribs/github-bot/conditions/author_test.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "context" diff --git a/contribs/github-bot/condition/boolean.go b/contribs/github-bot/conditions/boolean.go similarity index 99% rename from contribs/github-bot/condition/boolean.go rename to contribs/github-bot/conditions/boolean.go index bea45bfc405..5cd790fc3b9 100644 --- a/contribs/github-bot/condition/boolean.go +++ b/contribs/github-bot/conditions/boolean.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "fmt" diff --git a/contribs/github-bot/condition/boolean_test.go b/contribs/github-bot/conditions/boolean_test.go similarity index 99% rename from contribs/github-bot/condition/boolean_test.go rename to contribs/github-bot/conditions/boolean_test.go index 35530fd3e3d..6c9cb05604c 100644 --- a/contribs/github-bot/condition/boolean_test.go +++ b/contribs/github-bot/conditions/boolean_test.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "testing" diff --git a/contribs/github-bot/condition/branch.go b/contribs/github-bot/conditions/branch.go similarity index 98% rename from contribs/github-bot/condition/branch.go rename to contribs/github-bot/conditions/branch.go index 8f5d7e6224c..4b5666dee66 100644 --- a/contribs/github-bot/condition/branch.go +++ b/contribs/github-bot/conditions/branch.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "fmt" diff --git a/contribs/github-bot/condition/branch_test.go b/contribs/github-bot/conditions/branch_test.go similarity index 98% rename from contribs/github-bot/condition/branch_test.go rename to contribs/github-bot/conditions/branch_test.go index 4dc4a6030a6..2570fc00370 100644 --- a/contribs/github-bot/condition/branch_test.go +++ b/contribs/github-bot/conditions/branch_test.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "testing" diff --git a/contribs/github-bot/condition/condition.go b/contribs/github-bot/conditions/condition.go similarity index 93% rename from contribs/github-bot/condition/condition.go rename to contribs/github-bot/conditions/condition.go index 87b42c0c19d..afc436b8209 100644 --- a/contribs/github-bot/condition/condition.go +++ b/contribs/github-bot/conditions/condition.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/condition/constant.go b/contribs/github-bot/conditions/constant.go similarity index 97% rename from contribs/github-bot/condition/constant.go rename to contribs/github-bot/conditions/constant.go index 42084617f74..0181d331ec2 100644 --- a/contribs/github-bot/condition/constant.go +++ b/contribs/github-bot/conditions/constant.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "github.com/gnolang/gno/contribs/github-bot/utils" diff --git a/contribs/github-bot/condition/constant_test.go b/contribs/github-bot/conditions/constant_test.go similarity index 97% rename from contribs/github-bot/condition/constant_test.go rename to contribs/github-bot/conditions/constant_test.go index cc41a8326c3..2a497d42aa8 100644 --- a/contribs/github-bot/condition/constant_test.go +++ b/contribs/github-bot/conditions/constant_test.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "testing" diff --git a/contribs/github-bot/condition/file.go b/contribs/github-bot/conditions/file.go similarity index 98% rename from contribs/github-bot/condition/file.go rename to contribs/github-bot/conditions/file.go index 1c3350970da..75b1a1bdd01 100644 --- a/contribs/github-bot/condition/file.go +++ b/contribs/github-bot/conditions/file.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "fmt" diff --git a/contribs/github-bot/condition/file_test.go b/contribs/github-bot/conditions/file_test.go similarity index 99% rename from contribs/github-bot/condition/file_test.go rename to contribs/github-bot/conditions/file_test.go index b7299061fee..484d068a555 100644 --- a/contribs/github-bot/condition/file_test.go +++ b/contribs/github-bot/conditions/file_test.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "context" diff --git a/contribs/github-bot/condition/label.go b/contribs/github-bot/conditions/label.go similarity index 97% rename from contribs/github-bot/condition/label.go rename to contribs/github-bot/conditions/label.go index 91012c03439..e6efe66095b 100644 --- a/contribs/github-bot/condition/label.go +++ b/contribs/github-bot/conditions/label.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "fmt" diff --git a/contribs/github-bot/condition/label_test.go b/contribs/github-bot/conditions/label_test.go similarity index 98% rename from contribs/github-bot/condition/label_test.go rename to contribs/github-bot/conditions/label_test.go index 36d3d6708b3..cc2469f0eae 100644 --- a/contribs/github-bot/condition/label_test.go +++ b/contribs/github-bot/conditions/label_test.go @@ -1,4 +1,4 @@ -package condition +package conditions import ( "testing" diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index 1668a3207dd..0cd21ff62aa 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -2,8 +2,8 @@ package main import ( "github.com/gnolang/gno/contribs/github-bot/client" - c "github.com/gnolang/gno/contribs/github-bot/condition" - r "github.com/gnolang/gno/contribs/github-bot/requirement" + c "github.com/gnolang/gno/contribs/github-bot/conditions" + r "github.com/gnolang/gno/contribs/github-bot/requirements" ) // Automatic check that will be performed by the bot diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 97c8ed8657c..35531ada549 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -8,7 +8,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/param" + "github.com/gnolang/gno/contribs/github-bot/params" "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/google/go-github/v64/github" @@ -17,7 +17,7 @@ import ( func main() { // Retrieve params by parsing CLI flags and/or GitHub Actions context - params := param.Get() + params := params.Get() // Init GitHub API client gh := client.New(params) diff --git a/contribs/github-bot/param/param.go b/contribs/github-bot/params/params.go similarity index 99% rename from contribs/github-bot/param/param.go rename to contribs/github-bot/params/params.go index 1cf3a1f5683..3a2cfd691ee 100644 --- a/contribs/github-bot/param/param.go +++ b/contribs/github-bot/params/params.go @@ -1,4 +1,4 @@ -package param +package params import ( "flag" diff --git a/contribs/github-bot/param/prlist.go b/contribs/github-bot/params/prlist.go similarity index 98% rename from contribs/github-bot/param/prlist.go rename to contribs/github-bot/params/prlist.go index c88e0be8fe7..3b05590b0b2 100644 --- a/contribs/github-bot/param/prlist.go +++ b/contribs/github-bot/params/prlist.go @@ -1,4 +1,4 @@ -package param +package params import ( "encoding" diff --git a/contribs/github-bot/requirement/assignee.go b/contribs/github-bot/requirements/assignee.go similarity index 98% rename from contribs/github-bot/requirement/assignee.go rename to contribs/github-bot/requirements/assignee.go index 601558e58ac..aabbcb845d9 100644 --- a/contribs/github-bot/requirement/assignee.go +++ b/contribs/github-bot/requirements/assignee.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "fmt" diff --git a/contribs/github-bot/requirement/assignee_test.go b/contribs/github-bot/requirements/assignee_test.go similarity index 99% rename from contribs/github-bot/requirement/assignee_test.go rename to contribs/github-bot/requirements/assignee_test.go index d6e10a86b63..15c83321b26 100644 --- a/contribs/github-bot/requirement/assignee_test.go +++ b/contribs/github-bot/requirements/assignee_test.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "context" diff --git a/contribs/github-bot/requirement/author.go b/contribs/github-bot/requirements/author.go similarity index 98% rename from contribs/github-bot/requirement/author.go rename to contribs/github-bot/requirements/author.go index a88663c1d20..957d085594e 100644 --- a/contribs/github-bot/requirement/author.go +++ b/contribs/github-bot/requirements/author.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "fmt" diff --git a/contribs/github-bot/requirement/author_test.go b/contribs/github-bot/requirements/author_test.go similarity index 99% rename from contribs/github-bot/requirement/author_test.go rename to contribs/github-bot/requirements/author_test.go index b318de9a80f..be5bc98717a 100644 --- a/contribs/github-bot/requirement/author_test.go +++ b/contribs/github-bot/requirements/author_test.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "context" diff --git a/contribs/github-bot/requirement/boolean.go b/contribs/github-bot/requirements/boolean.go similarity index 99% rename from contribs/github-bot/requirement/boolean.go rename to contribs/github-bot/requirements/boolean.go index 916b91a49e3..78d4c9a2396 100644 --- a/contribs/github-bot/requirement/boolean.go +++ b/contribs/github-bot/requirements/boolean.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "fmt" diff --git a/contribs/github-bot/requirement/boolean_test.go b/contribs/github-bot/requirements/boolean_test.go similarity index 99% rename from contribs/github-bot/requirement/boolean_test.go rename to contribs/github-bot/requirements/boolean_test.go index f20b7f67cbe..d41407c9db1 100644 --- a/contribs/github-bot/requirement/boolean_test.go +++ b/contribs/github-bot/requirements/boolean_test.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "testing" diff --git a/contribs/github-bot/requirement/branch.go b/contribs/github-bot/requirements/branch.go similarity index 98% rename from contribs/github-bot/requirement/branch.go rename to contribs/github-bot/requirements/branch.go index 3700a7d6776..dca20a67311 100644 --- a/contribs/github-bot/requirement/branch.go +++ b/contribs/github-bot/requirements/branch.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "fmt" diff --git a/contribs/github-bot/requirement/branch_test.go b/contribs/github-bot/requirements/branch_test.go similarity index 98% rename from contribs/github-bot/requirement/branch_test.go rename to contribs/github-bot/requirements/branch_test.go index 67a3760e968..2d60a11b06d 100644 --- a/contribs/github-bot/requirement/branch_test.go +++ b/contribs/github-bot/requirements/branch_test.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "context" diff --git a/contribs/github-bot/requirement/constant.go b/contribs/github-bot/requirements/constant.go similarity index 97% rename from contribs/github-bot/requirement/constant.go rename to contribs/github-bot/requirements/constant.go index d495c3c9dff..8f17e546992 100644 --- a/contribs/github-bot/requirement/constant.go +++ b/contribs/github-bot/requirements/constant.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "github.com/gnolang/gno/contribs/github-bot/utils" diff --git a/contribs/github-bot/requirement/constant_test.go b/contribs/github-bot/requirements/constant_test.go similarity index 97% rename from contribs/github-bot/requirement/constant_test.go rename to contribs/github-bot/requirements/constant_test.go index 0f95286431b..9871dec8f1a 100644 --- a/contribs/github-bot/requirement/constant_test.go +++ b/contribs/github-bot/requirements/constant_test.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "testing" diff --git a/contribs/github-bot/requirement/label.go b/contribs/github-bot/requirements/label.go similarity index 98% rename from contribs/github-bot/requirement/label.go rename to contribs/github-bot/requirements/label.go index e434a746901..5586aa4c4ee 100644 --- a/contribs/github-bot/requirement/label.go +++ b/contribs/github-bot/requirements/label.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "fmt" diff --git a/contribs/github-bot/requirement/label_test.go b/contribs/github-bot/requirements/label_test.go similarity index 99% rename from contribs/github-bot/requirement/label_test.go rename to contribs/github-bot/requirements/label_test.go index 28927125c31..b9f689a3f54 100644 --- a/contribs/github-bot/requirement/label_test.go +++ b/contribs/github-bot/requirements/label_test.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "context" diff --git a/contribs/github-bot/requirement/maintainer.go b/contribs/github-bot/requirements/maintainer.go similarity index 96% rename from contribs/github-bot/requirement/maintainer.go rename to contribs/github-bot/requirements/maintainer.go index b3ff81c203a..11c2c351cf2 100644 --- a/contribs/github-bot/requirement/maintainer.go +++ b/contribs/github-bot/requirements/maintainer.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "github.com/gnolang/gno/contribs/github-bot/utils" diff --git a/contribs/github-bot/requirement/maintener_test.go b/contribs/github-bot/requirements/maintener_test.go similarity index 97% rename from contribs/github-bot/requirement/maintener_test.go rename to contribs/github-bot/requirements/maintener_test.go index b56d155ca7b..dae732fabb2 100644 --- a/contribs/github-bot/requirement/maintener_test.go +++ b/contribs/github-bot/requirements/maintener_test.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "testing" diff --git a/contribs/github-bot/requirement/requirement.go b/contribs/github-bot/requirements/requirement.go similarity index 93% rename from contribs/github-bot/requirement/requirement.go rename to contribs/github-bot/requirements/requirement.go index 0ed338708d6..a83646c1428 100644 --- a/contribs/github-bot/requirement/requirement.go +++ b/contribs/github-bot/requirements/requirement.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/requirement/reviewer.go b/contribs/github-bot/requirements/reviewer.go similarity index 99% rename from contribs/github-bot/requirement/reviewer.go rename to contribs/github-bot/requirements/reviewer.go index 3b06c4f9c4f..1ce148881e2 100644 --- a/contribs/github-bot/requirement/reviewer.go +++ b/contribs/github-bot/requirements/reviewer.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "fmt" diff --git a/contribs/github-bot/requirement/reviewer_test.go b/contribs/github-bot/requirements/reviewer_test.go similarity index 99% rename from contribs/github-bot/requirement/reviewer_test.go rename to contribs/github-bot/requirements/reviewer_test.go index 9468bc5e48e..6c91a7e3d71 100644 --- a/contribs/github-bot/requirement/reviewer_test.go +++ b/contribs/github-bot/requirements/reviewer_test.go @@ -1,4 +1,4 @@ -package requirement +package requirements import ( "context" From f912c51dccb64b90813ac809ba37d8085d6eba60 Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 18 Nov 2024 11:43:34 +0900 Subject: [PATCH 21/44] refacto: uses tm2/commands instead of stdlib flags --- contribs/github-bot/bot.go | 188 +++++++++++++++++++++++++ contribs/github-bot/client/client.go | 2 +- contribs/github-bot/go.mod | 10 +- contribs/github-bot/go.sum | 21 ++- contribs/github-bot/main.go | 199 +++------------------------ contribs/github-bot/params/params.go | 83 +++++++---- 6 files changed, 286 insertions(+), 217 deletions(-) create mode 100644 contribs/github-bot/bot.go diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go new file mode 100644 index 00000000000..70712a0924c --- /dev/null +++ b/contribs/github-bot/bot.go @@ -0,0 +1,188 @@ +package main + +import ( + "fmt" + "strings" + "sync" + + "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/logger" + p "github.com/gnolang/gno/contribs/github-bot/params" + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/google/go-github/v64/github" + "github.com/xlab/treeprint" +) + +func execBot(params *p.Params) error { + // Init GitHub API client + gh := client.New(params) + defer gh.Close() + + // Handle comment update, if any + if err := handleCommentUpdate(gh); err != nil { + return fmt.Errorf("comment update handling failed: %w", err) + } + + // Retrieve a slice of pull requests to process + var ( + prs []*github.PullRequest + err error + ) + + // If requested, retrieve all open pull requests + if params.PrAll { + opts := &github.PullRequestListOptions{ + State: "open", + Sort: "updated", + Direction: "desc", + } + + prs, _, err = gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) + if err != nil { + return fmt.Errorf("unable to retrieve all open pull requests: %w", err) + } + + // Otherwise, retrieve only specified pull request(s) (flag or GitHub Action context) + } else { + 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) + if err != nil { + return fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err) + } + prs[i] = pr + } + } + + if len(prs) > 1 { + prNums := make([]int, len(prs)) + for i, pr := range prs { + prNums[i] = pr.GetNumber() + } + + gh.Logger.Infof("%d pull requests to process: %v\n", len(prNums), prNums) + } + + // Process all pull requests in parallel + autoRules, manualRules := config(gh) + var wg sync.WaitGroup + wg.Add(len(prs)) + + // Used in dry-run mode to log cleanly from different goroutines + logMutex := sync.Mutex{} + + for _, pr := range prs { + go func(pr *github.PullRequest) { + defer wg.Done() + commentContent := CommentContent{} + commentContent.allSatisfied = true + + // Iterate over all automatic rules in config + for _, autoRule := range autoRules { + ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) + + // Check if conditions of this rule are met by this PR + if autoRule.If.IsMet(pr, ifDetails) { + c := AutoContent{Description: autoRule.Description, Satisfied: false} + thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.StatusFail)) + + // Check if requirements of this rule are satisfied by this PR + if autoRule.Then.IsSatisfied(pr, thenDetails) { + thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.StatusSuccess)) + c.Satisfied = true + } else { + commentContent.allSatisfied = false + } + + c.ConditionDetails = ifDetails.String() + c.RequirementDetails = thenDetails.String() + commentContent.AutoRules = append(commentContent.AutoRules, c) + } + } + + // Iterate over all manual rules in config + for _, manualRule := range manualRules { + ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) + + // Retrieve manual check states + checks := make(map[string][2]string) + if comment := gh.GetBotComment(pr.GetNumber()); comment != nil { + checks = getCommentManualChecks(comment.GetBody()) + } + + // Check if conditions of this rule are met by this PR + if manualRule.If.IsMet(pr, ifDetails) { + commentContent.ManualRules = append( + commentContent.ManualRules, + ManualContent{ + Description: manualRule.Description, + ConditionDetails: ifDetails.String(), + CheckedBy: checks[manualRule.Description][1], + Teams: manualRule.Teams, + }, + ) + + if checks[manualRule.Description][1] == "" { + commentContent.allSatisfied = false + } + } + } + + // Logs results or write them in bot PR comment + if gh.DryRun { + logMutex.Lock() + logResults(gh.Logger, pr.GetNumber(), commentContent) + logMutex.Unlock() + } else { + updateComment(gh, pr, commentContent) + } + }(pr) + } + wg.Wait() + + return nil +} + +// logResults is called in dry-run mode and outputs the status of each check +// and a conclusion +func logResults(logger logger.Logger, prNum int, commentContent CommentContent) { + logger.Infof("Pull request #%d requirements", prNum) + if len(commentContent.AutoRules) > 0 { + logger.Infof("Automated Checks:") + } + + for _, rule := range commentContent.AutoRules { + status := utils.StatusFail + if rule.Satisfied { + status = utils.StatusSuccess + } + logger.Infof("%s %s", status, rule.Description) + logger.Debugf("If:\n%s", rule.ConditionDetails) + logger.Debugf("Then:\n%s", rule.RequirementDetails) + } + + if len(commentContent.ManualRules) > 0 { + logger.Infof("Manual Checks:") + } + + for _, rule := range commentContent.ManualRules { + status := utils.StatusFail + checker := "any user with comment edit permission" + if rule.CheckedBy != "" { + status = utils.StatusSuccess + } + if len(rule.Teams) == 0 { + checker = fmt.Sprintf("a member of one of these teams: %s", strings.Join(rule.Teams, ", ")) + } + logger.Infof("%s %s", status, rule.Description) + logger.Debugf("If:\n%s", rule.ConditionDetails) + logger.Debugf("Can be checked by %s", checker) + } + + logger.Infof("Conclusion:") + if commentContent.allSatisfied { + logger.Infof("%s All requirements are satisfied\n", utils.StatusSuccess) + } else { + logger.Infof("%s Not all requirements are satisfied\n", utils.StatusFail) + } +} diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 227fbb4be7c..229627edf6a 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -226,7 +226,7 @@ func (gh *GitHub) Close() { } } -func New(params p.Params) *GitHub { +func New(params *p.Params) *GitHub { gh := &GitHub{ Owner: params.Owner, Repo: params.Repo, diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod index 97b7fe4eb0e..a8e971a7f93 100644 --- a/contribs/github-bot/go.mod +++ b/contribs/github-bot/go.mod @@ -1,8 +1,11 @@ module github.com/gnolang/gno/contribs/github-bot -go 1.21 +go 1.22 + +toolchain go1.22.2 require ( + github.com/gnolang/gno v0.2.0 github.com/google/go-github/v64 v64.0.0 github.com/migueleliasweb/go-github-mock v1.0.1 github.com/sethvargo/go-githubactions v1.3.0 @@ -11,6 +14,9 @@ require ( require ( github.com/google/go-querystring v1.1.0 // indirect - github.com/gorilla/mux v1.8.0 // indirect + github.com/gorilla/mux v1.8.1 // indirect + github.com/peterbourgon/ff/v3 v3.4.0 // indirect + golang.org/x/sys v0.22.0 // indirect + golang.org/x/term v0.22.0 // indirect golang.org/x/time v0.3.0 // indirect ) diff --git a/contribs/github-bot/go.sum b/contribs/github-bot/go.sum index 5ea770790ed..bb452e4c136 100644 --- a/contribs/github-bot/go.sum +++ b/contribs/github-bot/go.sum @@ -1,5 +1,8 @@ -github.com/davecgh/go-spew v1.1.0 h1:ZDRjVQ15GmhC3fiQ8ni8+OwkZQO4DARzQgrnXU1Liz8= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= +github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/gnolang/gno v0.2.0 h1:s33kyRByweDxxHMZMQKJtCDQpueTCYCFBqlLHwr6P+Y= +github.com/gnolang/gno v0.2.0/go.mod h1:dBaL1Au2MNLol+3FXdCv+IKLJnMKtTmIt778zsKjVu0= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -7,22 +10,30 @@ github.com/google/go-github/v64 v64.0.0 h1:4G61sozmY3eiPAjjoOHponXDBONm+utovTKby github.com/google/go-github/v64 v64.0.0/go.mod h1:xB3vqMQNdHzilXBiO2I+M7iEFtHf+DP/omBOv6tQzVo= github.com/google/go-querystring v1.1.0 h1:AnCroh3fv4ZBgVIf1Iwtovgjaw/GiKJo8M8yD/fhyJ8= github.com/google/go-querystring v1.1.0/go.mod h1:Kcdr2DB4koayq7X8pmAG4sNG59So17icRSOU623lUBU= -github.com/gorilla/mux v1.8.0 h1:i40aqfkR1h2SlN9hojwV5ZA91wcXFOvkdNIeFDP5koI= -github.com/gorilla/mux v1.8.0/go.mod h1:DVbg23sWSpFRCP0SfiEN6jmj59UnW/n46BH5rLB71So= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/migueleliasweb/go-github-mock v1.0.1 h1:amLEECVny28RCD1ElALUpQxrAimamznkg9rN2O7t934= github.com/migueleliasweb/go-github-mock v1.0.1/go.mod h1:8PJ7MpMoIiCBBNpuNmvndHm0QicjsE+hjex1yMGmjYQ= +github.com/peterbourgon/ff/v3 v3.4.0 h1:QBvM/rizZM1cB0p0lGMdmR7HxZeI/ZrBWB4DqLkMUBc= +github.com/peterbourgon/ff/v3 v3.4.0/go.mod h1:zjJVUhx+twciwfDl0zBcFzl4dW8axCRyXE/eKY9RztQ= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/sethvargo/go-githubactions v1.3.0 h1:Kg633LIUV2IrJsqy2MfveiED/Ouo+H2P0itWS0eLh8A= github.com/sethvargo/go-githubactions v1.3.0/go.mod h1:7/4WeHgYfSz9U5vwuToCK9KPnELVHAhGtRwLREOQV80= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= -github.com/stretchr/testify v1.7.0 h1:nwc3DEeHmmLAfoZucVR881uASk0Mfjw8xYJ99tb5CcY= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= +github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= +github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= +golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= +golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= +golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= -gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c h1:dUUwHk2QECo/6vqA44rthZ8ie2QXMNeKRTHCNY2nXvo= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 35531ada549..1c308021c52 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -1,192 +1,29 @@ package main import ( - "fmt" + "context" "os" - "strings" - "sync" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/params" - "github.com/gnolang/gno/contribs/github-bot/utils" - - "github.com/google/go-github/v64/github" - "github.com/xlab/treeprint" + p "github.com/gnolang/gno/contribs/github-bot/params" + "github.com/gnolang/gno/tm2/pkg/commands" ) func main() { - // Retrieve params by parsing CLI flags and/or GitHub Actions context - params := params.Get() - - // Init GitHub API client - gh := client.New(params) - defer gh.Close() - - // Handle comment update, if any - if err := handleCommentUpdate(gh); err != nil { - gh.Logger.Errorf("Comment update handling failed: %v", err) - os.Exit(1) - } - - // Retrieve a slice of pull requests to process - var ( - prs []*github.PullRequest - err error + params := &p.Params{} + + cmd := commands.NewCommand( + commands.Metadata{ + Name: "github-bot", + ShortUsage: "github-bot [flags]", + ShortHelp: "checks requirements for a PR to be merged.", + 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.", + }, + params, + func(_ context.Context, _ []string) error { + params.ValidateFlags() + return execBot(params) + }, ) - // If requested, retrieve all open pull requests - if params.PrAll { - opts := &github.PullRequestListOptions{ - State: "open", - Sort: "updated", - Direction: "desc", - } - - prs, _, err = gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) - if err != nil { - gh.Logger.Fatalf("Unable to retrieve all open pull requests: %v", err) - } - - // Otherwise, retrieve only specified pull request(s) (flag or GitHub Action context) - } else { - 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) - if err != nil { - gh.Logger.Fatalf("Unable to retrieve specified pull request (%d): %v", prNum, err) - } - prs[i] = pr - } - } - - if len(prs) > 1 { - prNums := make([]int, len(prs)) - for i, pr := range prs { - prNums[i] = pr.GetNumber() - } - - gh.Logger.Infof("%d pull requests to process: %v\n", len(prNums), prNums) - } - - // Process all pull requests in parallel - autoRules, manualRules := config(gh) - var wg sync.WaitGroup - wg.Add(len(prs)) - - // Used in dry-run mode to log cleanly from different goroutines - logMutex := sync.Mutex{} - - for _, pr := range prs { - go func(pr *github.PullRequest) { - defer wg.Done() - commentContent := CommentContent{} - commentContent.allSatisfied = true - - // Iterate over all automatic rules in config - for _, autoRule := range autoRules { - ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) - - // Check if conditions of this rule are met by this PR - if autoRule.If.IsMet(pr, ifDetails) { - c := AutoContent{Description: autoRule.Description, Satisfied: false} - thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.StatusFail)) - - // Check if requirements of this rule are satisfied by this PR - if autoRule.Then.IsSatisfied(pr, thenDetails) { - thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.StatusSuccess)) - c.Satisfied = true - } else { - commentContent.allSatisfied = false - } - - c.ConditionDetails = ifDetails.String() - c.RequirementDetails = thenDetails.String() - commentContent.AutoRules = append(commentContent.AutoRules, c) - } - } - - // Iterate over all manual rules in config - for _, manualRule := range manualRules { - ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) - - // Retrieve manual check states - checks := make(map[string][2]string) - if comment := gh.GetBotComment(pr.GetNumber()); comment != nil { - checks = getCommentManualChecks(comment.GetBody()) - } - - // Check if conditions of this rule are met by this PR - if manualRule.If.IsMet(pr, ifDetails) { - commentContent.ManualRules = append( - commentContent.ManualRules, - ManualContent{ - Description: manualRule.Description, - ConditionDetails: ifDetails.String(), - CheckedBy: checks[manualRule.Description][1], - Teams: manualRule.Teams, - }, - ) - - if checks[manualRule.Description][1] == "" { - commentContent.allSatisfied = false - } - } - } - - // Logs results or write them in bot PR comment - if gh.DryRun { - logMutex.Lock() - logResults(gh.Logger, pr.GetNumber(), commentContent) - logMutex.Unlock() - } else { - updateComment(gh, pr, commentContent) - } - }(pr) - } - wg.Wait() -} - -// logResults is called in dry-run mode and outputs the status of each check -// and a conclusion -func logResults(logger logger.Logger, prNum int, commentContent CommentContent) { - logger.Infof("Pull request #%d requirements", prNum) - if len(commentContent.AutoRules) > 0 { - logger.Infof("Automated Checks:") - } - - for _, rule := range commentContent.AutoRules { - status := utils.StatusFail - if rule.Satisfied { - status = utils.StatusSuccess - } - logger.Infof("%s %s", status, rule.Description) - logger.Debugf("If:\n%s", rule.ConditionDetails) - logger.Debugf("Then:\n%s", rule.RequirementDetails) - } - - if len(commentContent.ManualRules) > 0 { - logger.Infof("Manual Checks:") - } - - for _, rule := range commentContent.ManualRules { - status := utils.StatusFail - checker := "any user with comment edit permission" - if rule.CheckedBy != "" { - status = utils.StatusSuccess - } - if len(rule.Teams) == 0 { - checker = fmt.Sprintf("a member of one of these teams: %s", strings.Join(rule.Teams, ", ")) - } - logger.Infof("%s %s", status, rule.Description) - logger.Debugf("If:\n%s", rule.ConditionDetails) - logger.Debugf("Can be checked by %s", checker) - } - - logger.Infof("Conclusion:") - if commentContent.allSatisfied { - logger.Infof("%s All requirements are satisfied\n", utils.StatusSuccess) - } else { - logger.Infof("%s Not all requirements are satisfied\n", utils.StatusFail) - } + cmd.Execute(context.Background(), os.Args[1:]) } diff --git a/contribs/github-bot/params/params.go b/contribs/github-bot/params/params.go index 3a2cfd691ee..456553d9348 100644 --- a/contribs/github-bot/params/params.go +++ b/contribs/github-bot/params/params.go @@ -16,41 +16,70 @@ type Params struct { Verbose bool DryRun bool Timeout uint + flagSet *flag.FlagSet } -// Get Params from both cli flags and/or GitHub Actions context -func Get() Params { - p := Params{} +func (p *Params) RegisterFlags(fs *flag.FlagSet) { + fs.StringVar( + &p.Owner, + "owner", + "", + "owner of the repo to process, if empty, will be retrieved from GitHub Actions context", + ) - // Add cmd description to usage message - flag.Usage = func() { - fmt.Fprint(flag.CommandLine.Output(), "This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly.\n") - fmt.Fprint(flag.CommandLine.Output(), "A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable.\n\n") - flag.PrintDefaults() - } + fs.StringVar( + &p.Repo, + "repo", + "", + "repo to process, if empty, will be retrieved from GitHub Actions context", + ) + + fs.BoolVar( + &p.PrAll, + "pr-all", + false, + "process all opened pull requests", + ) + + fs.TextVar( + &p.PrNums, + "pr-numbers", + 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, + "dry-run", + false, + "print if pull request requirements are satisfied without updating anything on GitHub", + ) + + fs.UintVar( + &p.Timeout, + "timeout", + 0, + "timeout in milliseconds", + ) + p.flagSet = fs +} + +func (p *Params) ValidateFlags() { // Helper to display an error + usage message before exiting errorUsage := func(err string) { - fmt.Fprintf(flag.CommandLine.Output(), "Error: %s\n\n", err) - flag.Usage() + fmt.Fprintf(p.flagSet.Output(), "Error: %s\n\n", err) + p.flagSet.Usage() os.Exit(1) } - // Flags definition - flag.StringVar(&p.Owner, "owner", "", "owner of the repo to process, if empty, will be retrieved from GitHub Actions context") - flag.StringVar(&p.Repo, "repo", "", "repo to process, if empty, will be retrieved from GitHub Actions context") - flag.BoolVar(&p.PrAll, "pr-all", false, "process all opened pull requests") - flag.TextVar(&p.PrNums, "pr-numbers", 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") - flag.BoolVar(&p.Verbose, "verbose", false, "set logging level to debug") - flag.BoolVar(&p.DryRun, "dry-run", false, "print if pull request requirements are satisfied without updating anything on GitHub") - flag.UintVar(&p.Timeout, "timeout", 0, "timeout in milliseconds") - flag.Parse() - - // If any arg remain after flags processing - if len(flag.Args()) > 0 { - errorUsage(fmt.Sprintf("Unknown arg(s) provided: %v", flag.Args())) - } - // Check if flags are coherent if p.PrAll && len(p.PrNums) != 0 { errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags") @@ -104,6 +133,4 @@ func Get() Params { p.PrNums = PrList([]int{int(num)}) } } - - return p } From ae7fce990cddfb890c27b0f2e27a726e79d1330d Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 11:13:38 +0900 Subject: [PATCH 22/44] refactor: remove loop var (useless in go 1.22) --- contribs/github-bot/conditions/assignee_test.go | 2 -- contribs/github-bot/conditions/author_test.go | 2 -- contribs/github-bot/conditions/boolean_test.go | 3 --- contribs/github-bot/conditions/branch_test.go | 1 - contribs/github-bot/conditions/file_test.go | 1 - contribs/github-bot/conditions/label_test.go | 1 - contribs/github-bot/requirements/assignee_test.go | 1 - contribs/github-bot/requirements/author_test.go | 2 -- contribs/github-bot/requirements/boolean_test.go | 3 --- contribs/github-bot/requirements/branch_test.go | 1 - contribs/github-bot/requirements/label_test.go | 1 - contribs/github-bot/requirements/maintener_test.go | 1 - contribs/github-bot/requirements/reviewer_test.go | 2 -- 13 files changed, 21 deletions(-) diff --git a/contribs/github-bot/conditions/assignee_test.go b/contribs/github-bot/conditions/assignee_test.go index bb1c5adfdc0..afef9919368 100644 --- a/contribs/github-bot/conditions/assignee_test.go +++ b/contribs/github-bot/conditions/assignee_test.go @@ -32,7 +32,6 @@ func TestAssignee(t *testing.T) { {"assignee list contains user", "user", assignees, true}, {"assignee list doesn't contain user", "user2", assignees, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -69,7 +68,6 @@ func TestAssigneeInTeam(t *testing.T) { {"assignee list contains user", "user", members, true}, {"assignee list doesn't contain user", "user2", members, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/conditions/author_test.go b/contribs/github-bot/conditions/author_test.go index 1abbdf26a3a..a61bdf3a041 100644 --- a/contribs/github-bot/conditions/author_test.go +++ b/contribs/github-bot/conditions/author_test.go @@ -25,7 +25,6 @@ func TestAuthor(t *testing.T) { {"author match", "user", "user", true}, {"author doesn't match", "user", "author", false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -64,7 +63,6 @@ func TestAuthorInTeam(t *testing.T) { {"member list contains user", "user", members, true}, {"member list doesn't contain user", "user2", members, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/conditions/boolean_test.go b/contribs/github-bot/conditions/boolean_test.go index 6c9cb05604c..fa081672f11 100644 --- a/contribs/github-bot/conditions/boolean_test.go +++ b/contribs/github-bot/conditions/boolean_test.go @@ -20,7 +20,6 @@ func TestAnd(t *testing.T) { {"and is true", []Condition{Always(), Always()}, true}, {"and is false", []Condition{Always(), Always(), Never()}, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -61,7 +60,6 @@ func TestOr(t *testing.T) { {"or is true", []Condition{Never(), Always()}, true}, {"or is false", []Condition{Never(), Never(), Never()}, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -102,7 +100,6 @@ func TestNot(t *testing.T) { {"not is true", Never(), true}, {"not is false", Always(), false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/conditions/branch_test.go b/contribs/github-bot/conditions/branch_test.go index 2570fc00370..411869b65cb 100644 --- a/contribs/github-bot/conditions/branch_test.go +++ b/contribs/github-bot/conditions/branch_test.go @@ -25,7 +25,6 @@ func TestHeadBaseBranch(t *testing.T) { {"suffix doesn't match", "^/test-bot", "dev/test-bot", false}, {"doesn't match", "base", "notatall", false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/conditions/file_test.go b/contribs/github-bot/conditions/file_test.go index 484d068a555..9f7fd6d3c4b 100644 --- a/contribs/github-bot/conditions/file_test.go +++ b/contribs/github-bot/conditions/file_test.go @@ -36,7 +36,6 @@ func TestFileChanged(t *testing.T) { {"file list contains suffix doesn't match", "^oo", filenames, false}, {"file list doesn't contains match", "foobar", filenames, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/conditions/label_test.go b/contribs/github-bot/conditions/label_test.go index cc2469f0eae..fb78a416085 100644 --- a/contribs/github-bot/conditions/label_test.go +++ b/contribs/github-bot/conditions/label_test.go @@ -32,7 +32,6 @@ func TestLabel(t *testing.T) { {"label list contains suffix doesn't match", "^bel", labels, false}, {"label list doesn't contains match", "baleb", labels, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirements/assignee_test.go b/contribs/github-bot/requirements/assignee_test.go index 15c83321b26..3844c67b24e 100644 --- a/contribs/github-bot/requirements/assignee_test.go +++ b/contribs/github-bot/requirements/assignee_test.go @@ -36,7 +36,6 @@ func TestAssignee(t *testing.T) { {"assignee list doesn't contain user", "user2", assignees, false, false}, {"assignee list doesn't contain user with dry-run", "user2", assignees, true, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirements/author_test.go b/contribs/github-bot/requirements/author_test.go index be5bc98717a..9d4cbe4b2ef 100644 --- a/contribs/github-bot/requirements/author_test.go +++ b/contribs/github-bot/requirements/author_test.go @@ -25,7 +25,6 @@ func TestAuthor(t *testing.T) { {"author match", "user", "user", true}, {"author doesn't match", "user", "author", false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -64,7 +63,6 @@ func TestAuthorInTeam(t *testing.T) { {"member list contains user", "user", members, true}, {"member list doesn't contain user", "user2", members, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirements/boolean_test.go b/contribs/github-bot/requirements/boolean_test.go index d41407c9db1..18155c0e45c 100644 --- a/contribs/github-bot/requirements/boolean_test.go +++ b/contribs/github-bot/requirements/boolean_test.go @@ -20,7 +20,6 @@ func TestAnd(t *testing.T) { {"and is true", []Requirement{Always(), Always()}, true}, {"and is false", []Requirement{Always(), Always(), Never()}, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -61,7 +60,6 @@ func TestOr(t *testing.T) { {"or is true", []Requirement{Never(), Always()}, true}, {"or is false", []Requirement{Never(), Never(), Never()}, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -102,7 +100,6 @@ func TestNot(t *testing.T) { {"not is true", Never(), true}, {"not is false", Always(), false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirements/branch_test.go b/contribs/github-bot/requirements/branch_test.go index 2d60a11b06d..4a4b5fed875 100644 --- a/contribs/github-bot/requirements/branch_test.go +++ b/contribs/github-bot/requirements/branch_test.go @@ -27,7 +27,6 @@ func TestUpToDateWith(t *testing.T) { {"not up-to-date with commits behind", 3, 0, false}, {"not up-to-date with commits behind and ahead", 3, 3, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirements/label_test.go b/contribs/github-bot/requirements/label_test.go index b9f689a3f54..377cdd1fbb0 100644 --- a/contribs/github-bot/requirements/label_test.go +++ b/contribs/github-bot/requirements/label_test.go @@ -43,7 +43,6 @@ func TestLabel(t *testing.T) { {"label list doesn't contains match", "baleb", labels, false, false}, {"label list doesn't contains match with dry-run", "baleb", labels, true, false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirements/maintener_test.go b/contribs/github-bot/requirements/maintener_test.go index dae732fabb2..30075fc2ff4 100644 --- a/contribs/github-bot/requirements/maintener_test.go +++ b/contribs/github-bot/requirements/maintener_test.go @@ -18,7 +18,6 @@ func TestMaintenerCanModify(t *testing.T) { {"modify is true", true}, {"modify is false", false}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() diff --git a/contribs/github-bot/requirements/reviewer_test.go b/contribs/github-bot/requirements/reviewer_test.go index 6c91a7e3d71..bdd36bce71f 100644 --- a/contribs/github-bot/requirements/reviewer_test.go +++ b/contribs/github-bot/requirements/reviewer_test.go @@ -49,7 +49,6 @@ func TestReviewByUser(t *testing.T) { {"reviewer match without approval", "anotherOne", false, false}, {"reviewer doesn't match", "user2", false, true}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() @@ -163,7 +162,6 @@ func TestReviewByTeamMembers(t *testing.T) { {"0/1 team member approved with request", "team3", 1, false, true}, {"team doesn't exist with request", "team4", 1, false, true}, } { - testCase := testCase t.Run(testCase.name, func(t *testing.T) { t.Parallel() From de41ab1d94109e334455676e0e36bd16adc586dc Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 13:11:49 +0900 Subject: [PATCH 23/44] test: use testify assert in tests --- .../github-bot/conditions/assignee_test.go | 18 +++----- contribs/github-bot/conditions/author_test.go | 18 +++----- .../github-bot/conditions/boolean_test.go | 42 +++++-------------- contribs/github-bot/conditions/branch_test.go | 10 ++--- .../github-bot/conditions/constant_test.go | 17 +++----- contribs/github-bot/conditions/file_test.go | 10 ++--- contribs/github-bot/conditions/label_test.go | 10 ++--- contribs/github-bot/go.mod | 4 ++ contribs/github-bot/go.sum | 1 + .../github-bot/requirements/assignee_test.go | 13 ++---- .../github-bot/requirements/author_test.go | 18 +++----- .../github-bot/requirements/boolean_test.go | 42 +++++-------------- .../github-bot/requirements/branch_test.go | 10 ++--- .../github-bot/requirements/constant_test.go | 17 +++----- .../github-bot/requirements/label_test.go | 13 ++---- .../github-bot/requirements/maintener_test.go | 10 ++--- .../github-bot/requirements/reviewer_test.go | 25 ++++------- 17 files changed, 88 insertions(+), 190 deletions(-) diff --git a/contribs/github-bot/conditions/assignee_test.go b/contribs/github-bot/conditions/assignee_test.go index afef9919368..ebf401120d5 100644 --- a/contribs/github-bot/conditions/assignee_test.go +++ b/contribs/github-bot/conditions/assignee_test.go @@ -2,12 +2,14 @@ package conditions import ( "context" + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -39,12 +41,8 @@ func TestAssignee(t *testing.T) { details := treeprint.New() condition := Assignee(testCase.user) - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } @@ -95,12 +93,8 @@ func TestAssigneeInTeam(t *testing.T) { details := treeprint.New() condition := AssigneeInTeam(gh, "team") - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } diff --git a/contribs/github-bot/conditions/author_test.go b/contribs/github-bot/conditions/author_test.go index a61bdf3a041..34d9bf0ca6a 100644 --- a/contribs/github-bot/conditions/author_test.go +++ b/contribs/github-bot/conditions/author_test.go @@ -2,11 +2,13 @@ package conditions import ( "context" + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -34,12 +36,8 @@ func TestAuthor(t *testing.T) { details := treeprint.New() condition := Author(testCase.user) - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } @@ -88,12 +86,8 @@ func TestAuthorInTeam(t *testing.T) { details := treeprint.New() condition := AuthorInTeam(gh, "team") - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } diff --git a/contribs/github-bot/conditions/boolean_test.go b/contribs/github-bot/conditions/boolean_test.go index fa081672f11..2aa6b7c32af 100644 --- a/contribs/github-bot/conditions/boolean_test.go +++ b/contribs/github-bot/conditions/boolean_test.go @@ -1,9 +1,11 @@ package conditions import ( + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -27,12 +29,8 @@ func TestAnd(t *testing.T) { details := treeprint.New() condition := And(testCase.conditions...) - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } @@ -40,13 +38,7 @@ func TestAnd(t *testing.T) { func TestAndPanic(t *testing.T) { t.Parallel() - defer func() { - if r := recover(); r == nil { - t.Errorf("and constructor should panic if less than 2 conditions are provided") - } - }() - - And(Always()) // Only 1 condition provided + assert.Panics(t, func() { And(Always()) }, "and constructor should panic if less than 2 conditions are provided") } func TestOr(t *testing.T) { @@ -67,12 +59,8 @@ func TestOr(t *testing.T) { details := treeprint.New() condition := Or(testCase.conditions...) - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } @@ -80,13 +68,7 @@ func TestOr(t *testing.T) { func TestOrPanic(t *testing.T) { t.Parallel() - defer func() { - if r := recover(); r == nil { - t.Errorf("and constructor should panic if less than 2 conditions are provided") - } - }() - - Or(Always()) // Only 1 condition provided + assert.Panics(t, func() { Or(Always()) }, "or constructor should panic if less than 2 conditions are provided") } func TestNot(t *testing.T) { @@ -107,12 +89,8 @@ func TestNot(t *testing.T) { details := treeprint.New() condition := Not(testCase.condition) - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } diff --git a/contribs/github-bot/conditions/branch_test.go b/contribs/github-bot/conditions/branch_test.go index 411869b65cb..e0a847bf082 100644 --- a/contribs/github-bot/conditions/branch_test.go +++ b/contribs/github-bot/conditions/branch_test.go @@ -1,9 +1,11 @@ package conditions import ( + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -39,12 +41,8 @@ func TestHeadBaseBranch(t *testing.T) { for _, condition := range conditions { details := treeprint.New() - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) } }) } diff --git a/contribs/github-bot/conditions/constant_test.go b/contribs/github-bot/conditions/constant_test.go index 2a497d42aa8..304daa0d8f6 100644 --- a/contribs/github-bot/conditions/constant_test.go +++ b/contribs/github-bot/conditions/constant_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/xlab/treeprint" ) @@ -11,22 +12,14 @@ func TestAlways(t *testing.T) { t.Parallel() details := treeprint.New() - if !Always().IsMet(nil, details) { - t.Errorf("condition should have a met status: %t", true) - } - if !utils.TestLastNodeStatus(t, true, details) { - t.Errorf("condition details should have a status: %t", true) - } + assert.True(t, Always().IsMet(nil, details), "condition should have a met status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details), "condition details should have a status: true") } func TestNever(t *testing.T) { t.Parallel() details := treeprint.New() - if Never().IsMet(nil, details) { - t.Errorf("condition should have a met status: %t", false) - } - if !utils.TestLastNodeStatus(t, false, details) { - t.Errorf("condition details should have a status: %t", false) - } + assert.False(t, Never().IsMet(nil, details), "condition should have a met status: false") + assert.True(t, utils.TestLastNodeStatus(t, false, details), "condition details should have a status: false") } diff --git a/contribs/github-bot/conditions/file_test.go b/contribs/github-bot/conditions/file_test.go index 9f7fd6d3c4b..d2f9e8e0311 100644 --- a/contribs/github-bot/conditions/file_test.go +++ b/contribs/github-bot/conditions/file_test.go @@ -2,12 +2,14 @@ package conditions import ( "context" + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -59,12 +61,8 @@ func TestFileChanged(t *testing.T) { details := treeprint.New() condition := FileChanged(gh, testCase.pattern) - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } diff --git a/contribs/github-bot/conditions/label_test.go b/contribs/github-bot/conditions/label_test.go index fb78a416085..c77943491ff 100644 --- a/contribs/github-bot/conditions/label_test.go +++ b/contribs/github-bot/conditions/label_test.go @@ -1,9 +1,11 @@ package conditions import ( + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -39,12 +41,8 @@ func TestLabel(t *testing.T) { details := treeprint.New() condition := Label(testCase.pattern) - if condition.IsMet(pr, details) != testCase.isMet { - t.Errorf("condition should have a met status: %t", testCase.isMet) - } - if !utils.TestLastNodeStatus(t, testCase.isMet, details) { - t.Errorf("condition details should have a status: %t", testCase.isMet) - } + assert.Equal(t, condition.IsMet(pr, details), testCase.isMet, fmt.Sprintf("condition should have a met status: %t", testCase.isMet)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isMet, details), fmt.Sprintf("condition details should have a status: %t", testCase.isMet)) }) } } diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod index a8e971a7f93..0925e1599cd 100644 --- a/contribs/github-bot/go.mod +++ b/contribs/github-bot/go.mod @@ -9,14 +9,18 @@ require ( github.com/google/go-github/v64 v64.0.0 github.com/migueleliasweb/go-github-mock v1.0.1 github.com/sethvargo/go-githubactions v1.3.0 + github.com/stretchr/testify v1.9.0 github.com/xlab/treeprint v1.2.0 ) require ( + github.com/davecgh/go-spew v1.1.1 // indirect github.com/google/go-querystring v1.1.0 // indirect github.com/gorilla/mux v1.8.1 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.22.0 // indirect golang.org/x/term v0.22.0 // indirect golang.org/x/time v0.3.0 // indirect + gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contribs/github-bot/go.sum b/contribs/github-bot/go.sum index bb452e4c136..43e5c2c876e 100644 --- a/contribs/github-bot/go.sum +++ b/contribs/github-bot/go.sum @@ -33,6 +33,7 @@ golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= diff --git a/contribs/github-bot/requirements/assignee_test.go b/contribs/github-bot/requirements/assignee_test.go index 3844c67b24e..10a77bb9523 100644 --- a/contribs/github-bot/requirements/assignee_test.go +++ b/contribs/github-bot/requirements/assignee_test.go @@ -9,6 +9,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -63,15 +64,9 @@ func TestAssignee(t *testing.T) { details := treeprint.New() requirement := Assignee(gh, testCase.user) - if !requirement.IsSatisfied(pr, details) && !testCase.dryRun { - t.Errorf("requirement should have a satisfied status: %t", true) - } - if !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun { - t.Errorf("requirement details should have a status: %t", true) - } - if !testCase.exists && !requested && !testCase.dryRun { - t.Errorf("requirement should have requested to create item") - } + assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true") + assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true") + assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item") }) } } diff --git a/contribs/github-bot/requirements/author_test.go b/contribs/github-bot/requirements/author_test.go index 9d4cbe4b2ef..9937497e5ce 100644 --- a/contribs/github-bot/requirements/author_test.go +++ b/contribs/github-bot/requirements/author_test.go @@ -2,11 +2,13 @@ package requirements import ( "context" + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -34,12 +36,8 @@ func TestAuthor(t *testing.T) { details := treeprint.New() requirement := Author(testCase.user) - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) }) } } @@ -88,12 +86,8 @@ func TestAuthorInTeam(t *testing.T) { details := treeprint.New() requirement := AuthorInTeam(gh, "team") - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) }) } } diff --git a/contribs/github-bot/requirements/boolean_test.go b/contribs/github-bot/requirements/boolean_test.go index 18155c0e45c..fee94b1d465 100644 --- a/contribs/github-bot/requirements/boolean_test.go +++ b/contribs/github-bot/requirements/boolean_test.go @@ -1,9 +1,11 @@ package requirements import ( + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -27,12 +29,8 @@ func TestAnd(t *testing.T) { details := treeprint.New() requirement := And(testCase.requirements...) - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) }) } } @@ -40,13 +38,7 @@ func TestAnd(t *testing.T) { func TestAndPanic(t *testing.T) { t.Parallel() - defer func() { - if r := recover(); r == nil { - t.Errorf("and constructor should panic if less than 2 requirements are provided") - } - }() - - And(Always()) // Only 1 requirement provided + assert.Panics(t, func() { And(Always()) }, "and constructor should panic if less than 2 conditions are provided") } func TestOr(t *testing.T) { @@ -67,12 +59,8 @@ func TestOr(t *testing.T) { details := treeprint.New() requirement := Or(testCase.requirements...) - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) }) } } @@ -80,13 +68,7 @@ func TestOr(t *testing.T) { func TestOrPanic(t *testing.T) { t.Parallel() - defer func() { - if r := recover(); r == nil { - t.Errorf("and constructor should panic if less than 2 requirements are provided") - } - }() - - Or(Always()) // Only 1 requirement provided + assert.Panics(t, func() { Or(Always()) }, "or constructor should panic if less than 2 conditions are provided") } func TestNot(t *testing.T) { @@ -107,12 +89,8 @@ func TestNot(t *testing.T) { details := treeprint.New() requirement := Not(testCase.requirement) - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) }) } } diff --git a/contribs/github-bot/requirements/branch_test.go b/contribs/github-bot/requirements/branch_test.go index 4a4b5fed875..91d0a773069 100644 --- a/contribs/github-bot/requirements/branch_test.go +++ b/contribs/github-bot/requirements/branch_test.go @@ -2,11 +2,13 @@ package requirements import ( "context" + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -53,12 +55,8 @@ func TestUpToDateWith(t *testing.T) { details := treeprint.New() requirement := UpToDateWith(gh, "base") - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) }) } } diff --git a/contribs/github-bot/requirements/constant_test.go b/contribs/github-bot/requirements/constant_test.go index 9871dec8f1a..31e7b25d9eb 100644 --- a/contribs/github-bot/requirements/constant_test.go +++ b/contribs/github-bot/requirements/constant_test.go @@ -4,6 +4,7 @@ import ( "testing" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/xlab/treeprint" ) @@ -11,22 +12,14 @@ func TestAlways(t *testing.T) { t.Parallel() details := treeprint.New() - if !Always().IsSatisfied(nil, details) { - t.Errorf("requirement should have a satisfied status: %t", true) - } - if !utils.TestLastNodeStatus(t, true, details) { - t.Errorf("requirement details should have a status: %t", true) - } + assert.True(t, Always().IsSatisfied(nil, details), "requirement should have a satisfied status: true") + assert.True(t, utils.TestLastNodeStatus(t, true, details), "requirement details should have a status: true") } func TestNever(t *testing.T) { t.Parallel() details := treeprint.New() - if Never().IsSatisfied(nil, details) { - t.Errorf("requirement should have a satisfied status: %t", false) - } - if !utils.TestLastNodeStatus(t, false, details) { - t.Errorf("requirement details should have a status: %t", false) - } + assert.False(t, Never().IsSatisfied(nil, details), "requirement should have a satisfied status: false") + assert.True(t, utils.TestLastNodeStatus(t, false, details), "requirement details should have a status: false") } diff --git a/contribs/github-bot/requirements/label_test.go b/contribs/github-bot/requirements/label_test.go index 377cdd1fbb0..acb18019305 100644 --- a/contribs/github-bot/requirements/label_test.go +++ b/contribs/github-bot/requirements/label_test.go @@ -9,6 +9,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -70,15 +71,9 @@ func TestLabel(t *testing.T) { details := treeprint.New() requirement := Label(gh, testCase.pattern) - if !requirement.IsSatisfied(pr, details) && !testCase.dryRun { - t.Errorf("requirement should have a satisfied status: %t", true) - } - if !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun { - t.Errorf("requirement details should have a status: %t", true) - } - if !testCase.exists && !requested && !testCase.dryRun { - t.Errorf("requirement should have requested to create item") - } + assert.False(t, !requirement.IsSatisfied(pr, details) && !testCase.dryRun, "requirement should have a satisfied status: true") + assert.False(t, !utils.TestLastNodeStatus(t, true, details) && !testCase.dryRun, "requirement details should have a status: true") + assert.False(t, !testCase.exists && !requested && !testCase.dryRun, "requirement should have requested to create item") }) } } diff --git a/contribs/github-bot/requirements/maintener_test.go b/contribs/github-bot/requirements/maintener_test.go index 30075fc2ff4..7d8efd4e10d 100644 --- a/contribs/github-bot/requirements/maintener_test.go +++ b/contribs/github-bot/requirements/maintener_test.go @@ -1,10 +1,12 @@ package requirements import ( + "fmt" "testing" "github.com/gnolang/gno/contribs/github-bot/utils" "github.com/google/go-github/v64/github" + "github.com/stretchr/testify/assert" "github.com/xlab/treeprint" ) @@ -25,12 +27,8 @@ func TestMaintenerCanModify(t *testing.T) { details := treeprint.New() requirement := MaintainerCanModify() - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) }) } } diff --git a/contribs/github-bot/requirements/reviewer_test.go b/contribs/github-bot/requirements/reviewer_test.go index bdd36bce71f..f707d32f128 100644 --- a/contribs/github-bot/requirements/reviewer_test.go +++ b/contribs/github-bot/requirements/reviewer_test.go @@ -9,6 +9,7 @@ import ( "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/logger" "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" "github.com/migueleliasweb/go-github-mock/src/mock" @@ -88,15 +89,9 @@ func TestReviewByUser(t *testing.T) { details := treeprint.New() requirement := ReviewByUser(gh, testCase.user) - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } - if testCase.create != requested { - t.Errorf("requirement should have requested to create item: %t", testCase.create) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + assert.Equal(t, testCase.create, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.create)) }) } } @@ -212,15 +207,9 @@ func TestReviewByTeamMembers(t *testing.T) { details := treeprint.New() requirement := ReviewByTeamMembers(gh, testCase.team, testCase.count) - if requirement.IsSatisfied(pr, details) != testCase.isSatisfied { - t.Errorf("requirement should have a satisfied status: %t", testCase.isSatisfied) - } - if !utils.TestLastNodeStatus(t, testCase.isSatisfied, details) { - t.Errorf("requirement details should have a status: %t", testCase.isSatisfied) - } - if testCase.testRequest != requested { - t.Errorf("requirement should have requested to create item: %t", testCase.testRequest) - } + assert.Equal(t, requirement.IsSatisfied(pr, details), testCase.isSatisfied, fmt.Sprintf("requirement should have a satisfied status: %t", testCase.isSatisfied)) + assert.True(t, utils.TestLastNodeStatus(t, testCase.isSatisfied, details), fmt.Sprintf("requirement details should have a status: %t", testCase.isSatisfied)) + assert.Equal(t, testCase.testRequest, requested, fmt.Sprintf("requirement should have requested to create item: %t", testCase.testRequest)) }) } } From a15bc43ebb37cd9e22af0d94b7a84a64f8331504 Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 15:16:26 +0900 Subject: [PATCH 24/44] style: fix typos and nit picks --- contribs/github-bot/bot.go | 64 ++++++++++--------- contribs/github-bot/client/client.go | 6 +- contribs/github-bot/conditions/boolean.go | 4 ++ contribs/github-bot/conditions/branch.go | 4 +- contribs/github-bot/conditions/file.go | 2 +- contribs/github-bot/conditions/label.go | 2 +- contribs/github-bot/config.go | 6 +- contribs/github-bot/requirements/boolean.go | 4 ++ contribs/github-bot/requirements/branch.go | 1 - contribs/github-bot/requirements/reviewer.go | 8 +-- .../github-bot/requirements/reviewer_test.go | 4 +- contribs/github-bot/utils/testing.go | 6 +- 12 files changed, 62 insertions(+), 49 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index 70712a0924c..2efa210e83d 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -66,12 +66,12 @@ func execBot(params *p.Params) error { // Process all pull requests in parallel autoRules, manualRules := config(gh) var wg sync.WaitGroup - wg.Add(len(prs)) // Used in dry-run mode to log cleanly from different goroutines logMutex := sync.Mutex{} for _, pr := range prs { + wg.Add(1) go func(pr *github.PullRequest) { defer wg.Done() commentContent := CommentContent{} @@ -82,22 +82,24 @@ func execBot(params *p.Params) error { ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) // Check if conditions of this rule are met by this PR - if autoRule.If.IsMet(pr, ifDetails) { - c := AutoContent{Description: autoRule.Description, Satisfied: false} - thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.StatusFail)) - - // Check if requirements of this rule are satisfied by this PR - if autoRule.Then.IsSatisfied(pr, thenDetails) { - thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.StatusSuccess)) - c.Satisfied = true - } else { - commentContent.allSatisfied = false - } - - c.ConditionDetails = ifDetails.String() - c.RequirementDetails = thenDetails.String() - commentContent.AutoRules = append(commentContent.AutoRules, c) + if !autoRule.If.IsMet(pr, ifDetails) { + continue } + + c := AutoContent{Description: autoRule.Description, Satisfied: false} + thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.StatusFail)) + + // Check if requirements of this rule are satisfied by this PR + if autoRule.Then.IsSatisfied(pr, thenDetails) { + thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.StatusSuccess)) + c.Satisfied = true + } else { + commentContent.allSatisfied = false + } + + c.ConditionDetails = ifDetails.String() + c.RequirementDetails = thenDetails.String() + commentContent.AutoRules = append(commentContent.AutoRules, c) } // Iterate over all manual rules in config @@ -111,20 +113,22 @@ func execBot(params *p.Params) error { } // Check if conditions of this rule are met by this PR - if manualRule.If.IsMet(pr, ifDetails) { - commentContent.ManualRules = append( - commentContent.ManualRules, - ManualContent{ - Description: manualRule.Description, - ConditionDetails: ifDetails.String(), - CheckedBy: checks[manualRule.Description][1], - Teams: manualRule.Teams, - }, - ) - - if checks[manualRule.Description][1] == "" { - commentContent.allSatisfied = false - } + if !manualRule.If.IsMet(pr, ifDetails) { + continue + } + + commentContent.ManualRules = append( + commentContent.ManualRules, + ManualContent{ + Description: manualRule.Description, + ConditionDetails: ifDetails.String(), + CheckedBy: checks[manualRule.Description][1], + Teams: manualRule.Teams, + }, + ) + + if checks[manualRule.Description][1] == "" { + commentContent.allSatisfied = false } } diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 229627edf6a..9211f4ace8d 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -78,7 +78,7 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { } func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { - // Create bot comment if it not already exists + // Create bot comment if it does not already exist comment := gh.GetBotComment(prNum) if comment == nil { newComment, _, err := gh.Client.Issues.CreateComment( @@ -155,7 +155,7 @@ func (gh *GitHub) IsUserInTeams(user string, teams []string) bool { return false } -func (gh *GitHub) ListPrReviewers(prNum int) *github.Reviewers { +func (gh *GitHub) ListPRReviewers(prNum int) *github.Reviewers { var ( allReviewers = &github.Reviewers{} opts = &github.ListOptions{ @@ -188,7 +188,7 @@ func (gh *GitHub) ListPrReviewers(prNum int) *github.Reviewers { return allReviewers } -func (gh *GitHub) ListPrReviews(prNum int) []*github.PullRequestReview { +func (gh *GitHub) ListPRReviews(prNum int) []*github.PullRequestReview { var ( allReviews []*github.PullRequestReview opts = &github.ListOptions{ diff --git a/contribs/github-bot/conditions/boolean.go b/contribs/github-bot/conditions/boolean.go index 5cd790fc3b9..bd84bdf3bc3 100644 --- a/contribs/github-bot/conditions/boolean.go +++ b/contribs/github-bot/conditions/boolean.go @@ -23,6 +23,8 @@ func (a *and) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { for _, condition := range a.conditions { if !condition.IsMet(pr, branch) { met = false + // We don't break here because we need to call IsMet on all conditions + // to populate the details tree } } @@ -57,6 +59,8 @@ func (o *or) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { for _, condition := range o.conditions { if condition.IsMet(pr, branch) { met = true + // We don't break here because we need to call IsMet on all conditions + // to populate the details tree } } diff --git a/contribs/github-bot/conditions/branch.go b/contribs/github-bot/conditions/branch.go index 4b5666dee66..30d521d0c8e 100644 --- a/contribs/github-bot/conditions/branch.go +++ b/contribs/github-bot/conditions/branch.go @@ -20,7 +20,7 @@ var _ Condition = &baseBranch{} func (b *baseBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { return utils.AddStatusNode( b.pattern.MatchString(pr.GetBase().GetRef()), - fmt.Sprintf("The base branch match this pattern: %s", b.pattern.String()), + fmt.Sprintf("The base branch matches this pattern: %s", b.pattern.String()), details, ) } @@ -39,7 +39,7 @@ var _ Condition = &headBranch{} func (h *headBranch) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { return utils.AddStatusNode( h.pattern.MatchString(pr.GetHead().GetRef()), - fmt.Sprintf("The head branch match this pattern: %s", h.pattern.String()), + fmt.Sprintf("The head branch matches this pattern: %s", h.pattern.String()), details, ) } diff --git a/contribs/github-bot/conditions/file.go b/contribs/github-bot/conditions/file.go index 75b1a1bdd01..a0beeab81c4 100644 --- a/contribs/github-bot/conditions/file.go +++ b/contribs/github-bot/conditions/file.go @@ -20,7 +20,7 @@ type fileChanged struct { var _ Condition = &fileChanged{} func (fc *fileChanged) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("A changed file match this pattern: %s", fc.pattern.String()) + detail := fmt.Sprintf("A changed file matches this pattern: %s", fc.pattern.String()) opts := &github.ListOptions{ PerPage: client.PageSize, } diff --git a/contribs/github-bot/conditions/label.go b/contribs/github-bot/conditions/label.go index e6efe66095b..741f7c0d9b0 100644 --- a/contribs/github-bot/conditions/label.go +++ b/contribs/github-bot/conditions/label.go @@ -18,7 +18,7 @@ type label struct { var _ Condition = &label{} func (l *label) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("A label match this pattern: %s", l.pattern.String()) + detail := fmt.Sprintf("A label matches this pattern: %s", l.pattern.String()) for _, label := range pr.Labels { if l.pattern.MatchString(label.GetName()) { diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index 0cd21ff62aa..f5ac9b3173e 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -28,7 +28,7 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { Description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", If: c.And( c.FileChanged(gh, "tm2"), - c.BaseBranch("main"), + c.BaseBranch("master"), ), Then: r.And( r.Or( @@ -57,7 +57,7 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { { Description: "Determine if infra needs to be updated", If: c.And( - c.BaseBranch("main"), + c.BaseBranch("master"), c.Or( c.FileChanged(gh, "misc/deployments"), c.FileChanged(gh, `misc/docker-\.*`), @@ -69,7 +69,7 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { { Description: "Ensure the code style is satisfactory", If: c.And( - c.BaseBranch("main"), + c.BaseBranch("master"), c.Or( c.FileChanged(gh, `.*\.go`), c.FileChanged(gh, `.*\.js`), diff --git a/contribs/github-bot/requirements/boolean.go b/contribs/github-bot/requirements/boolean.go index 78d4c9a2396..9556d1be0cc 100644 --- a/contribs/github-bot/requirements/boolean.go +++ b/contribs/github-bot/requirements/boolean.go @@ -23,6 +23,8 @@ func (a *and) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { for _, requirement := range a.requirements { if !requirement.IsSatisfied(pr, branch) { satisfied = false + // We don't break here because we need to call IsSatisfied on all + // requirements to populate the details tree } } @@ -57,6 +59,8 @@ func (o *or) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { for _, requirement := range o.requirements { if requirement.IsSatisfied(pr, branch) { satisfied = true + // We don't break here because we need to call IsSatisfied on all + // requirements to populate the details tree } } diff --git a/contribs/github-bot/requirements/branch.go b/contribs/github-bot/requirements/branch.go index dca20a67311..68160628934 100644 --- a/contribs/github-bot/requirements/branch.go +++ b/contribs/github-bot/requirements/branch.go @@ -31,7 +31,6 @@ func (u *upToDateWith) IsSatisfied(pr *github.PullRequest, details treeprint.Tre cmp, _, err := u.gh.Client.Repositories.CompareCommits(u.gh.Ctx, u.gh.Owner, u.gh.Repo, base, head, nil) if err != nil { - fmt.Println(err) u.gh.Logger.Errorf("Unable to compare head branch (%s) and base (%s): %v", head, base, err) return false } diff --git a/contribs/github-bot/requirements/reviewer.go b/contribs/github-bot/requirements/reviewer.go index 1ce148881e2..735672f2bc8 100644 --- a/contribs/github-bot/requirements/reviewer.go +++ b/contribs/github-bot/requirements/reviewer.go @@ -24,7 +24,7 @@ func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tre // If not a dry run, make the user a reviewer if he's not already if !r.gh.DryRun { requested := false - if reviewers := r.gh.ListPrReviewers(pr.GetNumber()); reviewers != nil { + if reviewers := r.gh.ListPRReviewers(pr.GetNumber()); reviewers != nil { for _, user := range reviewers.Users { if user.GetLogin() == r.user { requested = true @@ -52,7 +52,7 @@ func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tre } // Check if user already approved this PR - for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { + for _, review := range r.gh.ListPRReviews(pr.GetNumber()) { if review.GetUser().GetLogin() == r.user { r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) return utils.AddStatusNode(review.GetState() == "APPROVED", detail, details) @@ -82,7 +82,7 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr // If not a dry run, make the user a reviewer if he's not already if !r.gh.DryRun { requested := false - if reviewers := r.gh.ListPrReviewers(pr.GetNumber()); reviewers != nil { + if reviewers := r.gh.ListPRReviewers(pr.GetNumber()); reviewers != nil { for _, team := range reviewers.Teams { if team.GetSlug() == r.team { requested = true @@ -111,7 +111,7 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr // Check how many members of this team already approved this PR approved := uint(0) - for _, review := range r.gh.ListPrReviews(pr.GetNumber()) { + for _, review := range r.gh.ListPRReviews(pr.GetNumber()) { for _, member := range r.gh.ListTeamMembers(r.team) { if review.GetUser().GetLogin() == member.GetLogin() { if review.GetState() == "APPROVED" { diff --git a/contribs/github-bot/requirements/reviewer_test.go b/contribs/github-bot/requirements/reviewer_test.go index f707d32f128..0fc27ccb356 100644 --- a/contribs/github-bot/requirements/reviewer_test.go +++ b/contribs/github-bot/requirements/reviewer_test.go @@ -46,8 +46,8 @@ func TestReviewByUser(t *testing.T) { isSatisfied bool create bool }{ - {"reviewer match", "user", true, false}, - {"reviewer match without approval", "anotherOne", false, false}, + {"reviewer matches", "user", true, false}, + {"reviewer matches without approval", "anotherOne", false, false}, {"reviewer doesn't match", "user2", false, true}, } { t.Run(testCase.name, func(t *testing.T) { diff --git a/contribs/github-bot/utils/testing.go b/contribs/github-bot/utils/testing.go index f22ba21a97e..39dbc8b82d7 100644 --- a/contribs/github-bot/utils/testing.go +++ b/contribs/github-bot/utils/testing.go @@ -11,9 +11,11 @@ func TestLastNodeStatus(t *testing.T, success bool, details treeprint.Tree) bool t.Helper() detail := details.FindLastNode().(*treeprint.Node).Value.(string) + status := StatusFail if success { - return strings.HasPrefix(detail, StatusSuccess) + status = StatusSuccess } - return strings.HasPrefix(detail, StatusFail) + + return strings.HasPrefix(detail, status) } From 0fe9c67c366cc3558e85a21ce29680b300cb0c66 Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 15:19:44 +0900 Subject: [PATCH 25/44] chore: fix go.mod --- contribs/github-bot/go.mod | 8 +++++--- contribs/github-bot/go.sum | 10 ++++------ 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/contribs/github-bot/go.mod b/contribs/github-bot/go.mod index 0925e1599cd..8df55e3f282 100644 --- a/contribs/github-bot/go.mod +++ b/contribs/github-bot/go.mod @@ -4,8 +4,10 @@ go 1.22 toolchain go1.22.2 +replace github.com/gnolang/gno => ../.. + require ( - github.com/gnolang/gno v0.2.0 + github.com/gnolang/gno v0.0.0-00010101000000-000000000000 github.com/google/go-github/v64 v64.0.0 github.com/migueleliasweb/go-github-mock v1.0.1 github.com/sethvargo/go-githubactions v1.3.0 @@ -19,8 +21,8 @@ require ( github.com/gorilla/mux v1.8.1 // indirect github.com/peterbourgon/ff/v3 v3.4.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect - golang.org/x/sys v0.22.0 // indirect - golang.org/x/term v0.22.0 // indirect + golang.org/x/sys v0.24.0 // indirect + golang.org/x/term v0.23.0 // indirect golang.org/x/time v0.3.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect ) diff --git a/contribs/github-bot/go.sum b/contribs/github-bot/go.sum index 43e5c2c876e..2dae4e83e72 100644 --- a/contribs/github-bot/go.sum +++ b/contribs/github-bot/go.sum @@ -1,8 +1,6 @@ github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= -github.com/gnolang/gno v0.2.0 h1:s33kyRByweDxxHMZMQKJtCDQpueTCYCFBqlLHwr6P+Y= -github.com/gnolang/gno v0.2.0/go.mod h1:dBaL1Au2MNLol+3FXdCv+IKLJnMKtTmIt778zsKjVu0= github.com/google/go-cmp v0.5.2/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= @@ -26,10 +24,10 @@ github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsT github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/xlab/treeprint v1.2.0 h1:HzHnuAF1plUN2zGlAFHbSQP2qJ0ZAD3XF5XD7OesXRQ= github.com/xlab/treeprint v1.2.0/go.mod h1:gj5Gd3gPdKtR1ikdDK6fnFLdmIS0X30kTTuNd/WEJu0= -golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI= -golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= -golang.org/x/term v0.22.0 h1:BbsgPEJULsl2fV/AT3v15Mjva5yXKQDyKf+TbDz7QJk= -golang.org/x/term v0.22.0/go.mod h1:F3qCibpT5AMpCRfhfT53vVJwhLtIVHhB9XDjfFvnMI4= +golang.org/x/sys v0.24.0 h1:Twjiwq9dn6R1fQcyiK+wQyHWfaz/BJB+YIpzU/Cv3Xg= +golang.org/x/sys v0.24.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= +golang.org/x/term v0.23.0 h1:F6D4vR+EHoL9/sWAWgAR1H2DcHr4PareCbAaCo1RpuU= +golang.org/x/term v0.23.0/go.mod h1:DgV24QBUrK6jhZXl+20l6UWznPlwAHm1Q1mGHtydmSk= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= From 3032c16e9d9bc95a3ad7ef0fcbc8444856d187d8 Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 18:58:17 +0900 Subject: [PATCH 26/44] feat: return comment earlier if already fetched --- contribs/github-bot/client/client.go | 52 ++++++++++++++-------------- 1 file changed, 26 insertions(+), 26 deletions(-) diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 9211f4ace8d..92a989b21e8 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -12,6 +12,7 @@ import ( "github.com/google/go-github/v64/github" ) +// PageSize is the number of items to load for each iteration when fetching a list const PageSize = 100 type GitHub struct { @@ -26,19 +27,27 @@ type GitHub struct { func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { // List existing comments - var ( - allComments []*github.IssueComment - sort = "created" - direction = "desc" - opts = &github.IssueListCommentsOptions{ - Sort: &sort, - Direction: &direction, - ListOptions: github.ListOptions{ - PerPage: PageSize, - }, - } + const ( + sort = "created" + direction = "desc" ) + // Get current user (bot) + currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") + if err != nil { + gh.Logger.Errorf("Unable to get current user: %v", err) + return nil + } + + // Pagination option + opts := &github.IssueListCommentsOptions{ + Sort: github.String(sort), + Direction: github.String(direction), + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + for { comments, response, err := gh.Client.Issues.ListComments( gh.Ctx, @@ -52,7 +61,12 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { return nil } - allComments = append(allComments, comments...) + // Get the comment created by current user + for _, comment := range comments { + if comment.GetUser().GetLogin() == currentUser.GetLogin() { + return comment + } + } if response.NextPage == 0 { break @@ -60,20 +74,6 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { opts.Page = response.NextPage } - // Get current user (bot) - currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") - if err != nil { - gh.Logger.Errorf("Unable to get current user: %v", err) - return nil - } - - // Get the comment created by current user - for _, comment := range allComments { - if comment.GetUser().GetLogin() == currentUser.GetLogin() { - return comment - } - } - return nil } From 586d9704bf990499fad9f8dbe846f160333fe3fc Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 21:21:49 +0900 Subject: [PATCH 27/44] refacor: remove Close method, create ctx from main --- contribs/github-bot/bot.go | 23 ++++++++++++++++------- contribs/github-bot/client/client.go | 28 ++++++++-------------------- 2 files changed, 24 insertions(+), 27 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index 2efa210e83d..0f95b59def7 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -1,6 +1,7 @@ package main import ( + "context" "fmt" "strings" "sync" @@ -14,9 +15,19 @@ import ( ) func execBot(params *p.Params) error { + // Create context with timeout if specified in the parameters + ctx := context.Background() + if params.Timeout > 0 { + var cancel context.CancelFunc + ctx, cancel = context.WithTimeout(context.Background(), params.Timeout) + defer cancel() + } + // Init GitHub API client - gh := client.New(params) - defer gh.Close() + gh, err := client.New(ctx, params) + if err != nil { + return fmt.Errorf("comment update handling failed: %w", err) + } // Handle comment update, if any if err := handleCommentUpdate(gh); err != nil { @@ -24,10 +35,7 @@ func execBot(params *p.Params) error { } // Retrieve a slice of pull requests to process - var ( - prs []*github.PullRequest - err error - ) + var prs []*github.PullRequest // If requested, retrieve all open pull requests if params.PrAll { @@ -42,7 +50,8 @@ func execBot(params *p.Params) error { return fmt.Errorf("unable to retrieve all open pull requests: %w", err) } - // Otherwise, retrieve only specified pull request(s) (flag or GitHub Action context) + // Otherwise, retrieve only specified pull request(s) + // (flag or GitHub Action context) } else { prs = make([]*github.PullRequest, len(params.PrNums)) for i, prNum := range params.PrNums { diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 92a989b21e8..9e7333b0c15 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -2,9 +2,8 @@ package client import ( "context" - "log" + "errors" "os" - "time" "github.com/gnolang/gno/contribs/github-bot/logger" p "github.com/gnolang/gno/contribs/github-bot/params" @@ -22,7 +21,6 @@ type GitHub struct { Logger logger.Logger Owner string Repo string - cancel context.CancelFunc } func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { @@ -220,14 +218,9 @@ func (gh *GitHub) ListPRReviews(prNum int) []*github.PullRequestReview { return allReviews } -func (gh *GitHub) Close() { - if gh.cancel != nil { - gh.cancel() - } -} - -func New(params *p.Params) *GitHub { +func New(ctx context.Context, params *p.Params) (*GitHub, error) { gh := &GitHub{ + Ctx: ctx, Owner: params.Owner, Repo: params.Repo, DryRun: params.DryRun, @@ -237,19 +230,14 @@ func New(params *p.Params) *GitHub { // a logger suitable for terminal output or the GitHub Actions web interface gh.Logger = logger.NewLogger(params.Verbose) - // Create context with timeout if specified in the parameters - if params.Timeout > 0 { - gh.Ctx, gh.cancel = context.WithTimeout(context.Background(), time.Duration(params.Timeout)*time.Millisecond) - } else { - gh.Ctx = context.Background() - } - - // Init GitHub API Client using token from env + // Retrieve GitHub API token from env token, set := os.LookupEnv("GITHUB_TOKEN") if !set { - log.Fatalf("GITHUB_TOKEN is not set in env") + return nil, errors.New("GITHUB_TOKEN is not set in env") } + + // Init GitHub API client using token gh.Client = github.NewClient(nil).WithAuthToken(token) - return gh + return gh, nil } From a3cf220a8d8c9746db29276eeaebab32670e3623 Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 21:24:56 +0900 Subject: [PATCH 28/44] refactor: use standard error instead of exiting --- contribs/github-bot/bot.go | 4 +++- contribs/github-bot/comment.go | 7 +++++-- 2 files changed, 8 insertions(+), 3 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index 0f95b59def7..abada24a62d 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -30,7 +30,9 @@ func execBot(params *p.Params) error { } // Handle comment update, if any - if err := handleCommentUpdate(gh); err != nil { + if err := handleCommentUpdate(gh); err == errTriggeredByBot { + return nil // Ignore if this run was triggered by a previous run + } else if err != nil { return fmt.Errorf("comment update handling failed: %w", err) } diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 7ed25643e53..bc04fd8af36 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -2,6 +2,7 @@ package main import ( "bytes" + "errors" "fmt" "os" "regexp" @@ -14,6 +15,8 @@ import ( "github.com/sethvargo/go-githubactions" ) +var errTriggeredByBot = errors.New("event triggered by bot") + // These structures contain the necessary information to generate // the bot's comment from the template file type AutoContent struct { @@ -92,10 +95,10 @@ func handleCommentUpdate(gh *client.GitHub) error { if actionCtx.Actor == authUser.GetLogin() { gh.Logger.Debugf("Prevent infinite loop if the bot comment was edited by the bot itself") - os.Exit(0) + return errTriggeredByBot } - // Ignore if edited comment author is not the bot + // Ignore if comment edition author is not the bot comment, ok := actionCtx.Event["comment"].(map[string]any) if !ok { return fmt.Errorf("unable to get comment on issue comment event") From e879e84781bcfd0e302fb14fe66b59c9b7217ab1 Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 21:26:08 +0900 Subject: [PATCH 29/44] chore: remove useless Errorf --- contribs/github-bot/comment.go | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index bc04fd8af36..3adb7116c2a 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -80,7 +80,7 @@ func handleCommentUpdate(gh *client.GitHub) error { // Ignore if the action type is not deleted or edited actionType, ok := actionCtx.Event["action"].(string) if !ok { - return fmt.Errorf("unable to get type on issue comment event") + return errors.New("unable to get type on issue comment event") } if actionType != "deleted" && actionType != "edited" { @@ -101,17 +101,17 @@ func handleCommentUpdate(gh *client.GitHub) error { // Ignore if comment edition author is not the bot comment, ok := actionCtx.Event["comment"].(map[string]any) if !ok { - return fmt.Errorf("unable to get comment on issue comment event") + return errors.New("unable to get comment on issue comment event") } author, ok := comment["user"].(map[string]any) if !ok { - return fmt.Errorf("unable to get comment user on issue comment event") + return errors.New("unable to get comment user on issue comment event") } login, ok := author["login"].(string) if !ok { - return fmt.Errorf("unable to get comment user login on issue comment event") + return errors.New("unable to get comment user login on issue comment event") } if login != authUser.GetLogin() { @@ -121,34 +121,34 @@ func handleCommentUpdate(gh *client.GitHub) error { // Get comment current body current, ok := comment["body"].(string) if !ok { - return fmt.Errorf("unable to get comment body on issue comment event") + return errors.New("unable to get comment body on issue comment event") } // Get comment updated body changes, ok := actionCtx.Event["changes"].(map[string]any) if !ok { - return fmt.Errorf("unable to get changes on issue comment event") + return errors.New("unable to get changes on issue comment event") } changesBody, ok := changes["body"].(map[string]any) if !ok { - return fmt.Errorf("unable to get changes body on issue comment event") + return errors.New("unable to get changes body on issue comment event") } previous, ok := changesBody["from"].(string) if !ok { - return fmt.Errorf("unable to get changes body content on issue comment event") + return errors.New("unable to get changes body content on issue comment event") } // Get PR number from GitHub Actions context issue, ok := actionCtx.Event["issue"].(map[string]any) if !ok { - return fmt.Errorf("unable to get issue on issue comment event") + return errors.New("unable to get issue on issue comment event") } num, ok := issue["number"].(float64) if !ok || num <= 0 { - return fmt.Errorf("unable to get issue number on issue comment event") + return errors.New("unable to get issue number on issue comment event") } // Check if change is only a checkbox being checked or unckecked @@ -158,7 +158,7 @@ func handleCommentUpdate(gh *client.GitHub) error { if !gh.DryRun { gh.SetBotComment(previous, int(num)) } - return fmt.Errorf("bot comment edited outside of checkboxes") + return errors.New("bot comment edited outside of checkboxes") } // Check if actor / comment editor has permission to modify changed boxes @@ -192,7 +192,7 @@ func handleCommentUpdate(gh *client.GitHub) error { if !gh.DryRun { gh.SetBotComment(previous, int(num)) } - return fmt.Errorf("checkbox edited by a user not allowed to") + return errors.New("checkbox edited by a user not allowed to") } } From 6d45ab998d5502895315aec07573cc65d437e74e Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 20 Nov 2024 21:36:29 +0900 Subject: [PATCH 30/44] docs: fix README and improve flag help --- contribs/github-bot/README.md | 27 ++++++++++++--------------- contribs/github-bot/main.go | 5 ++--- contribs/github-bot/params/params.go | 7 ++++--- 3 files changed, 18 insertions(+), 21 deletions(-) diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md index 1a15c461787..8f81a0d9f04 100644 --- a/contribs/github-bot/README.md +++ b/contribs/github-bot/README.md @@ -10,7 +10,7 @@ The GitHub Bot is designed to automate and streamline the process of managing pu The bot operates by defining a set of rules that are evaluated against each pull request passed as parameter. These rules are categorized into automatic and manual checks: -- **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 exectued. For example, ensuring that changes to specific directories are reviewed by specific team members. +- **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). @@ -31,21 +31,18 @@ For the bot to make requests to the GitHub API, it needs a Personal Access Token // (go: downloading ...) > github-bot --help +USAGE + [flags] + This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly. A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable. - -dry-run - print if pull request requirements are satisfied without updating anything on GitHub - -owner string - owner of the repo to process, if empty, will be retrieved from GitHub Actions context - -pr-all - process all opened pull requests - -pr-numbers value - 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 - -repo string - repo to process, if empty, will be retrieved from GitHub Actions context - -timeout uint - timeout in milliseconds - -verbose - set logging level to debug +FLAGS + -dry-run=false print if pull request requirements are satisfied without updating anything on GitHub + -owner ... owner of the repo to process, if empty, will be retrieved from GitHub Actions context + -pr-all=false process all opened pull requests + -pr-numbers ... 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 + -repo ... repo to process, if empty, will be retrieved from GitHub Actions context + -timeout 0s timeout after which the bot execution is interrupted + -verbose=false set logging level to debug ``` diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 1c308021c52..dca825568b4 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -13,9 +13,8 @@ func main() { cmd := commands.NewCommand( commands.Metadata{ - Name: "github-bot", - ShortUsage: "github-bot [flags]", - ShortHelp: "checks requirements for a PR to be merged.", + ShortUsage: "[flags]", + ShortHelp: "checks requirements for a PR to be merged", 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.", }, params, diff --git a/contribs/github-bot/params/params.go b/contribs/github-bot/params/params.go index 456553d9348..1bdf261a7b8 100644 --- a/contribs/github-bot/params/params.go +++ b/contribs/github-bot/params/params.go @@ -4,6 +4,7 @@ import ( "flag" "fmt" "os" + "time" "github.com/sethvargo/go-githubactions" ) @@ -15,7 +16,7 @@ type Params struct { PrNums PrList Verbose bool DryRun bool - Timeout uint + Timeout time.Duration flagSet *flag.FlagSet } @@ -62,11 +63,11 @@ func (p *Params) RegisterFlags(fs *flag.FlagSet) { "print if pull request requirements are satisfied without updating anything on GitHub", ) - fs.UintVar( + fs.DurationVar( &p.Timeout, "timeout", 0, - "timeout in milliseconds", + "timeout after which the bot execution is interrupted", ) p.flagSet = fs From 6ca5c8c9673f69f4c46cd3be9209ab8f9f6fc534 Mon Sep 17 00:00:00 2001 From: aeddi Date: Thu, 21 Nov 2024 23:44:51 +0900 Subject: [PATCH 31/44] refacor: better comment, split logic, add tests --- contribs/github-bot/bot.go | 2 +- contribs/github-bot/comment.go | 137 +++++++++++++++++----------- contribs/github-bot/comment.tmpl | 2 +- contribs/github-bot/comment_test.go | 64 +++++++++++++ 4 files changed, 150 insertions(+), 55 deletions(-) create mode 100644 contribs/github-bot/comment_test.go diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index abada24a62d..a565f85b93f 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -149,7 +149,7 @@ func execBot(params *p.Params) error { logResults(gh.Logger, pr.GetNumber(), commentContent) logMutex.Unlock() } else { - updateComment(gh, pr, commentContent) + updatePullRequest(gh, pr, commentContent) } }(pr) } diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 3adb7116c2a..5f8f734a721 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -4,7 +4,6 @@ import ( "bytes" "errors" "fmt" - "os" "regexp" "strings" "text/template" @@ -17,6 +16,18 @@ import ( var errTriggeredByBot = errors.New("event triggered by bot") +// Compile regex only once +var ( + // Regex for capturing the entire line of a manual check + manualCheckLine = regexp.MustCompile(`(?m:^- \[([ x])\] (.+)?$)`) + // Regex for capturing only the user who checked it + manualCheckDetails = regexp.MustCompile(`(?m:(.+) \(checked by @(\w+)\)$)`) + // Regex for capturing only the checkboxes + checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) + // Regex used to capture markdown links + markdownLink = regexp.MustCompile(`\[(.*)\]\(.*\)`) +) + // These structures contain the necessary information to generate // the bot's comment from the template file type AutoContent struct { @@ -27,8 +38,8 @@ type AutoContent struct { } type ManualContent struct { Description string - ConditionDetails string CheckedBy string + ConditionDetails string Teams []string } type CommentContent struct { @@ -42,16 +53,21 @@ type CommentContent struct { func getCommentManualChecks(commentBody string) map[string][2]string { checks := make(map[string][2]string) - reg := regexp.MustCompile(`(?m:^- \[([ x])\] (.+)?$)`) - subReg := regexp.MustCompile(`(?m:(.+) \(checked by @(\w+)\)$)`) - matches := reg.FindAllStringSubmatch(commentBody, -1) - - for _, match := range matches { - if subMatches := subReg.FindAllStringSubmatch(match[2], -1); len(subMatches) > 0 { - checks[subMatches[0][1]] = [2]string{match[1], subMatches[0][2]} - } else { - checks[match[2]] = [2]string{match[1]} + // For each line that matches the "Manual check" regex + for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { + status := match[1] + // Try to capture an occurence of : (checked by @user) + if details := manualCheckDetails.FindAllStringSubmatch(match[2], -1); len(details) > 0 { + // If found, set both the status and the user that checked the box + description := details[0][1] + checkedBy := details[0][2] + checks[description] = [2]string{status, checkedBy} + continue } + + // If not found, set only the status of the box + description := match[2] + checks[description] = [2]string{status} } return checks @@ -152,7 +168,6 @@ func handleCommentUpdate(gh *client.GitHub) error { } // Check if change is only a checkbox being checked or unckecked - checkboxes := regexp.MustCompile(`(?m:^- \[[ x]\])`) if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") { // If not, restore previous comment body if !gh.DryRun { @@ -166,45 +181,50 @@ func handleCommentUpdate(gh *client.GitHub) error { previousChecks := getCommentManualChecks(previous) edited := "" for key := range currentChecks { - if currentChecks[key][0] != previousChecks[key][0] { - // Get teams allowed to edit this box from config - var teams []string - found := false - _, manualRules := config(gh) - - for _, manualRule := range manualRules { - if manualRule.Description == key { - found = true - teams = manualRule.Teams - } - } + // If there is no diff for this check, ignore it + if currentChecks[key][0] == previousChecks[key][0] { + continue + } - // If rule were not found, return to reprocess the bot comment entirely - // (maybe bot config was updated since last run?) - if !found { - gh.Logger.Debugf("Updated rule not found in config: %s", key) - return nil + // Get teams allowed to edit this box from config + var teams []string + found := false + _, manualRules := config(gh) + + for _, manualRule := range manualRules { + if manualRule.Description == key { + found = true + teams = manualRule.Teams } + } - // 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 !gh.DryRun { - gh.SetBotComment(previous, int(num)) - } - return errors.New("checkbox edited by a user not allowed to") + // If rule were not found, return to reprocess the bot comment entirely + // (maybe bot config was updated since last run?) + if !found { + gh.Logger.Debugf("Updated rule not found in config: %s", key) + return nil + } + + // 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 !gh.DryRun { + gh.SetBotComment(previous, int(num)) } + return errors.New("checkbox edited by a user not allowed to") } + } - // If box was checked - reg := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key][0], key)) - if strings.TrimSpace(currentChecks[key][0]) == "x" { - replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key][0], key, actionCtx.Actor) - edited = reg.ReplaceAllString(current, replacement) - } else { - replacement := fmt.Sprintf("- [%s] %s", currentChecks[key][0], key) - edited = reg.ReplaceAllString(current, replacement) - } + // This regex capture only the line of the current check + specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key][0], key)) + + // If the box is checked, append the username of the user who checked it + if strings.TrimSpace(currentChecks[key][0]) == "x" { + replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key][0], key, actionCtx.Actor) + edited = specificManualCheck.ReplaceAllString(current, replacement) + } else { // Else, remove the username of the user + replacement := fmt.Sprintf("- [%s] %s", currentChecks[key][0], key) + edited = specificManualCheck.ReplaceAllString(current, replacement) } } @@ -217,28 +237,39 @@ func handleCommentUpdate(gh *client.GitHub) error { return nil } -func updateComment(gh *client.GitHub, pr *github.PullRequest, content CommentContent) { - // Custom function to string markdown links +// generateComment generates a comment using the template file and the +// content passed as parameter +func generateComment(content CommentContent) string { + // Custom function to strip markdown links funcMap := template.FuncMap{ "stripLinks": func(input string) string { - reg := regexp.MustCompile(`\[(.*)\]\(.*\)`) - return reg.ReplaceAllString(input, "$1") + return markdownLink.ReplaceAllString(input, "$1") }, } - // Generate bot comment using template file + // Bind markdown stripping function to template generator const tmplFile = "comment.tmpl" tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile) if err != nil { - panic(err) + panic(err) // Should never happen } + // Generate bot comment using template file var commentBytes bytes.Buffer if err := tmpl.Execute(&commentBytes, content); err != nil { - panic(err) + panic(err) // Should never happen } - comment := gh.SetBotComment(commentBytes.String(), pr.GetNumber()) + return commentBytes.String() +} + +// updatePullRequest updates or creates both the bot comment and the commit status +func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content CommentContent) { + // Generate comment text content + commentText := generateComment(content) + + // Update comment on pull request + comment := gh.SetBotComment(commentText, pr.GetNumber()) if comment != nil { gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber()) } diff --git a/contribs/github-bot/comment.tmpl b/contribs/github-bot/comment.tmpl index 2cd3a613b5d..ebd07fdd4b9 100644 --- a/contribs/github-bot/comment.tmpl +++ b/contribs/github-bot/comment.tmpl @@ -7,7 +7,7 @@ These requirements are defined in this [configuration file](https://github.com/G ## Automated Checks -{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🟠{{ end }} {{ .Description }} +{{ range .AutoRules }} {{ if .Satisfied }}🟢{{ else }}🔴{{ end }} {{ .Description }} {{ end }} {{ if .AutoRules }}
Details
diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/comment_test.go new file mode 100644 index 00000000000..f530d11f877 --- /dev/null +++ b/contribs/github-bot/comment_test.go @@ -0,0 +1,64 @@ +package main + +import ( + "fmt" + "regexp" + "strings" + "testing" + + "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/stretchr/testify/assert" +) + +func TestGeneratedComment(t *testing.T) { + t.Parallel() + + autoCheckSuccessLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.StatusSuccess)) + autoCheckFailLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.StatusFail)) + + content := CommentContent{} + autoRules := []AutoContent{ + {Description: "Test automatic 1", Satisfied: false}, + {Description: "Test automatic 2", Satisfied: false}, + {Description: "Test automatic 3", Satisfied: true}, + {Description: "Test automatic 4", Satisfied: true}, + {Description: "Test automatic 5", Satisfied: false}, + } + manualRules := []ManualContent{ + {Description: "Test manual 1", CheckedBy: "user_1"}, + {Description: "Test manual 2", CheckedBy: ""}, + {Description: "Test manual 3", CheckedBy: ""}, + {Description: "Test manual 4", CheckedBy: "user_4"}, + {Description: "Test manual 5", CheckedBy: "user_5"}, + } + + commentText := generateComment(content) + assert.True(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + + content.AutoRules = autoRules + commentText = generateComment(content) + fmt.Println(commentText) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") + assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") + assert.Equal(t, 3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") + + content.ManualRules = manualRules + commentText = generateComment(content) + assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") + assert.False(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should not contains manual check placeholder") + + manualChecks := getCommentManualChecks(commentText) + assert.Equal(t, len(manualChecks), len(manualRules), "wrong number of manual checks found") + for _, rule := range manualRules { + val, ok := manualChecks[rule.Description] + assert.True(t, ok, "manual check should exist") + if rule.CheckedBy == "" { + assert.Equal(t, " ", val[0], "manual rule should not be checked") + } else { + assert.Equal(t, "x", val[0], "manual rule should be checked") + } + assert.Equal(t, rule.CheckedBy, val[1], "invalid username found for CheckedBy") + } +} From e161f77bbab45cea9189d0e92626dff056566f77 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 22 Nov 2024 00:31:43 +0900 Subject: [PATCH 32/44] refactor: make config fields private --- contribs/github-bot/bot.go | 16 +++++----- contribs/github-bot/comment.go | 5 ++-- contribs/github-bot/config.go | 54 +++++++++++++++++----------------- 3 files changed, 38 insertions(+), 37 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index a565f85b93f..4018b75a662 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -93,15 +93,15 @@ func execBot(params *p.Params) error { ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) // Check if conditions of this rule are met by this PR - if !autoRule.If.IsMet(pr, ifDetails) { + if !autoRule.ifC.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.StatusFail)) // Check if requirements of this rule are satisfied by this PR - if autoRule.Then.IsSatisfied(pr, thenDetails) { + if autoRule.thenR.IsSatisfied(pr, thenDetails) { thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.StatusSuccess)) c.Satisfied = true } else { @@ -124,21 +124,21 @@ func execBot(params *p.Params) error { } // Check if conditions of this rule are met by this PR - if !manualRule.If.IsMet(pr, ifDetails) { + if !manualRule.ifC.IsMet(pr, ifDetails) { continue } commentContent.ManualRules = append( commentContent.ManualRules, ManualContent{ - Description: manualRule.Description, + Description: manualRule.description, ConditionDetails: ifDetails.String(), - CheckedBy: checks[manualRule.Description][1], - Teams: manualRule.Teams, + CheckedBy: checks[manualRule.description][1], + Teams: manualRule.teams, }, ) - if checks[manualRule.Description][1] == "" { + if checks[manualRule.description][1] == "" { commentContent.allSatisfied = false } } diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 5f8f734a721..20d19a6efda 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -130,6 +130,7 @@ func handleCommentUpdate(gh *client.GitHub) error { return errors.New("unable to get comment user login on issue comment event") } + // If comment edition author is not the bot, return if login != authUser.GetLogin() { return nil } @@ -192,9 +193,9 @@ func handleCommentUpdate(gh *client.GitHub) error { _, manualRules := config(gh) for _, manualRule := range manualRules { - if manualRule.Description == key { + if manualRule.description == key { found = true - teams = manualRule.Teams + teams = manualRule.teams } } diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index f5ac9b3173e..d0d90c1cc91 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -8,16 +8,16 @@ import ( // Automatic check that will be performed by the bot 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 + 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 } // Manual check that will be performed by users type manualCheck struct { - Description string - If c.Condition // If the condition is met, a checkbox will be displayed on bot comment - Teams []string // Members of these teams can check the checkbox to make the check pass + description string + ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment + teams []string // 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 @@ -25,12 +25,12 @@ type manualCheck struct { func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { auto := []automaticCheck{ { - Description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", - If: c.And( + description: "Changes to 'tm2' folder should be reviewed/authored by at least one member of both EU and US teams", + ifC: c.And( c.FileChanged(gh, "tm2"), c.BaseBranch("master"), ), - Then: r.And( + thenR: r.And( r.Or( r.ReviewByTeamMembers(gh, "eu", 1), r.AuthorInTeam(gh, "eu"), @@ -42,21 +42,21 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { ), }, { - Description: "A maintainer must be able to edit this pull request", - If: c.Always(), - Then: r.MaintainerCanModify(), + description: "A maintainer must be able to edit this pull request", + ifC: c.Always(), + thenR: r.MaintainerCanModify(), }, { - Description: "The pull request head branch must be up-to-date with its base", - If: c.Always(), // Or only if c.BaseBranch("main") ? - Then: r.UpToDateWith(gh, r.PR_BASE), + description: "The pull request head branch must be up-to-date with its base", + ifC: c.Always(), // Or only if c.BaseBranch("main") ? + thenR: r.UpToDateWith(gh, r.PR_BASE), }, } manual := []manualCheck{ { - Description: "Determine if infra needs to be updated", - If: c.And( + description: "Determine if infra needs to be updated", + ifC: c.And( c.BaseBranch("master"), c.Or( c.FileChanged(gh, "misc/deployments"), @@ -64,23 +64,23 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { c.FileChanged(gh, "tm2/pkg/p2p"), ), ), - Teams: []string{"tech-staff"}, + teams: []string{"tech-staff"}, }, { - Description: "Ensure the code style is satisfactory", - If: c.And( + description: "Ensure the code style is satisfactory", + ifC: c.And( c.BaseBranch("master"), c.Or( c.FileChanged(gh, `.*\.go`), c.FileChanged(gh, `.*\.js`), ), ), - Teams: []string{"tech-staff"}, + teams: []string{"tech-staff"}, }, { - Description: "Ensure the documentation is accurate and relevant", - If: c.FileChanged(gh, `.*\.md`), - Teams: []string{ + description: "Ensure the documentation is accurate and relevant", + ifC: c.FileChanged(gh, `.*\.md`), + teams: []string{ "tech-staff", "devrels", }, @@ -90,10 +90,10 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { // 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 From 49da4cf017393f0a454d55c1ceb1d88260b95c38 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 22 Nov 2024 02:12:04 +0900 Subject: [PATCH 33/44] refactor: return err instead of panic --- contribs/github-bot/bot.go | 14 +++++++++++++- contribs/github-bot/comment.go | 19 ++++++++++++------- 2 files changed, 25 insertions(+), 8 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index 4018b75a662..ed9218831b4 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -2,9 +2,11 @@ package main import ( "context" + "errors" "fmt" "strings" "sync" + "sync/atomic" "github.com/gnolang/gno/contribs/github-bot/client" "github.com/gnolang/gno/contribs/github-bot/logger" @@ -81,6 +83,9 @@ func execBot(params *p.Params) error { // Used in dry-run mode to log cleanly from different goroutines logMutex := sync.Mutex{} + // Used in regular-run mode to return an error if one PR processing failed + var failed atomic.Bool + for _, pr := range prs { wg.Add(1) go func(pr *github.PullRequest) { @@ -149,12 +154,19 @@ func execBot(params *p.Params) error { logResults(gh.Logger, pr.GetNumber(), commentContent) logMutex.Unlock() } else { - updatePullRequest(gh, pr, commentContent) + if err := updatePullRequest(gh, pr, commentContent); err != nil { + gh.Logger.Errorf("unable to update pull request: %v", err) + failed.Store(true) + } } }(pr) } wg.Wait() + if failed.Load() { + return errors.New("error occured while processing pull requests") + } + return nil } diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 20d19a6efda..39f5eca5837 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -240,7 +240,7 @@ func handleCommentUpdate(gh *client.GitHub) error { // generateComment generates a comment using the template file and the // content passed as parameter -func generateComment(content CommentContent) string { +func generateComment(content CommentContent) (string, error) { // Custom function to strip markdown links funcMap := template.FuncMap{ "stripLinks": func(input string) string { @@ -252,22 +252,25 @@ func generateComment(content CommentContent) string { const tmplFile = "comment.tmpl" tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile) if err != nil { - panic(err) // Should never happen + return "", fmt.Errorf("unable to init template: %v", err) } // Generate bot comment using template file var commentBytes bytes.Buffer if err := tmpl.Execute(&commentBytes, content); err != nil { - panic(err) // Should never happen + return "", fmt.Errorf("unable to execute template: %v", err) } - return commentBytes.String() + return commentBytes.String(), nil } // updatePullRequest updates or creates both the bot comment and the commit status -func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content CommentContent) { +func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content CommentContent) error { // Generate comment text content - commentText := generateComment(content) + commentText, err := generateComment(content) + if err != nil { + return fmt.Errorf("unable to generate comment on PR %d: %v", pr.GetNumber(), err) + } // Update comment on pull request comment := gh.SetBotComment(commentText, pr.GetNumber()) @@ -304,8 +307,10 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen TargetURL: &targetURL, Description: &description, }); err != nil { - gh.Logger.Errorf("Unable to create status on PR %d: %v", pr.GetNumber(), err) + return fmt.Errorf("unable to create status on PR %d: %v", pr.GetNumber(), err) } else { gh.Logger.Infof("Commit status successfully updated on PR %d", pr.GetNumber()) } + + return nil } From ca463b1d4c843e1f2dd93d47702bfb3ad392bc5b Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 22 Nov 2024 02:13:30 +0900 Subject: [PATCH 34/44] refactor: return err to caller --- contribs/github-bot/bot.go | 23 ++++--- contribs/github-bot/client/client.go | 63 +++++++++++--------- contribs/github-bot/comment.go | 6 +- contribs/github-bot/comment_test.go | 9 ++- contribs/github-bot/conditions/assignee.go | 8 ++- contribs/github-bot/conditions/author.go | 8 ++- contribs/github-bot/requirements/author.go | 8 ++- contribs/github-bot/requirements/reviewer.go | 56 ++++++++++++----- 8 files changed, 121 insertions(+), 60 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index ed9218831b4..bfb1d1d6cc2 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -118,32 +118,39 @@ func execBot(params *p.Params) error { commentContent.AutoRules = append(commentContent.AutoRules, c) } + // Retrieve manual check states + checks := make(map[string][2]string) + if comment, err := gh.GetBotComment(pr.GetNumber()); err == nil { + checks = getCommentManualChecks(comment.GetBody()) + } + // Iterate over all manual rules in config for _, manualRule := range manualRules { ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) - // Retrieve manual check states - checks := make(map[string][2]string) - if comment := gh.GetBotComment(pr.GetNumber()); comment != nil { - checks = getCommentManualChecks(comment.GetBody()) - } - // Check if conditions of this rule are met by this PR if !manualRule.ifC.IsMet(pr, ifDetails) { continue } + // Get check status from current comment, if any + checkedBy := "" + check, ok := checks[manualRule.description] + if ok { + checkedBy = check[1] + } + commentContent.ManualRules = append( commentContent.ManualRules, ManualContent{ Description: manualRule.description, ConditionDetails: ifDetails.String(), - CheckedBy: checks[manualRule.description][1], + CheckedBy: checkedBy, Teams: manualRule.teams, }, ) - if checks[manualRule.description][1] == "" { + if checkedBy == "" { commentContent.allSatisfied = false } } diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/client/client.go index 9e7333b0c15..f824826cd40 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/client/client.go @@ -3,6 +3,7 @@ package client import ( "context" "errors" + "fmt" "os" "github.com/gnolang/gno/contribs/github-bot/logger" @@ -14,6 +15,8 @@ import ( // PageSize is the number of items to load for each iteration when fetching a list const PageSize = 100 +var ErrBotCommentNotFound = errors.New("bot comment not found") + type GitHub struct { Client *github.Client Ctx context.Context @@ -23,7 +26,7 @@ type GitHub struct { Repo string } -func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { +func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { // List existing comments const ( sort = "created" @@ -33,8 +36,7 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { // Get current user (bot) currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") if err != nil { - gh.Logger.Errorf("Unable to get current user: %v", err) - return nil + return nil, fmt.Errorf("unable to get current user: %v", err) } // Pagination option @@ -55,14 +57,13 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { opts, ) if err != nil { - gh.Logger.Errorf("Unable to list comments for PR %d: %v", prNum, err) - return nil + return nil, fmt.Errorf("unable to list comments for PR %d: %v", prNum, err) } // Get the comment created by current user for _, comment := range comments { if comment.GetUser().GetLogin() == currentUser.GetLogin() { - return comment + return comment, nil } } @@ -72,13 +73,13 @@ func (gh *GitHub) GetBotComment(prNum int) *github.IssueComment { opts.Page = response.NextPage } - return nil + return nil, errors.New("bot comment not found") } -func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { +func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, error) { // Create bot comment if it does not already exist - comment := gh.GetBotComment(prNum) - if comment == nil { + comment, err := gh.GetBotComment(prNum) + if err == ErrBotCommentNotFound { newComment, _, err := gh.Client.Issues.CreateComment( gh.Ctx, gh.Owner, @@ -87,10 +88,11 @@ func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { &github.IssueComment{Body: &body}, ) if err != nil { - gh.Logger.Errorf("Unable to create bot comment for PR %d: %v", prNum, err) - return nil + return nil, fmt.Errorf("unable to create bot comment for PR %d: %v", prNum, err) } - return newComment + return newComment, nil + } else if err != nil { + return nil, fmt.Errorf("unable to get bot comment: %v", err) } comment.Body = &body @@ -102,13 +104,13 @@ func (gh *GitHub) SetBotComment(body string, prNum int) *github.IssueComment { comment, ) if err != nil { - gh.Logger.Errorf("Unable to edit bot comment with ID %d: %v", comment.GetID(), err) - return nil + return nil, fmt.Errorf("unable to edit bot comment with ID %d: %v", comment.GetID(), err) } - return editComment + + return editComment, nil } -func (gh *GitHub) ListTeamMembers(team string) []*github.User { +func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) { var ( allMembers []*github.User opts = &github.TeamListTeamMembersOptions{ @@ -126,8 +128,7 @@ func (gh *GitHub) ListTeamMembers(team string) []*github.User { opts, ) if err != nil { - gh.Logger.Errorf("Unable to list members for team %s: %v", team, err) - return nil + return nil, fmt.Errorf("unable to list members for team %s: %v", team, err) } allMembers = append(allMembers, members...) @@ -138,12 +139,18 @@ func (gh *GitHub) ListTeamMembers(team string) []*github.User { opts.Page = response.NextPage } - return allMembers + return allMembers, nil } func (gh *GitHub) IsUserInTeams(user string, teams []string) bool { for _, team := range teams { - for _, member := range gh.ListTeamMembers(team) { + teamMembers, err := gh.ListTeamMembers(team) + if err != nil { + gh.Logger.Errorf("unable to check if user %s in team %s", user, team) + continue + } + + for _, member := range teamMembers { if member.GetLogin() == user { return true } @@ -153,7 +160,7 @@ func (gh *GitHub) IsUserInTeams(user string, teams []string) bool { return false } -func (gh *GitHub) ListPRReviewers(prNum int) *github.Reviewers { +func (gh *GitHub) ListPRReviewers(prNum int) (*github.Reviewers, error) { var ( allReviewers = &github.Reviewers{} opts = &github.ListOptions{ @@ -170,8 +177,7 @@ func (gh *GitHub) ListPRReviewers(prNum int) *github.Reviewers { opts, ) if err != nil { - gh.Logger.Errorf("Unable to list reviewers for PR %d: %v", prNum, err) - return nil + return nil, fmt.Errorf("unable to list reviewers for PR %d: %v", prNum, err) } allReviewers.Teams = append(allReviewers.Teams, reviewers.Teams...) @@ -183,10 +189,10 @@ func (gh *GitHub) ListPRReviewers(prNum int) *github.Reviewers { opts.Page = response.NextPage } - return allReviewers + return allReviewers, nil } -func (gh *GitHub) ListPRReviews(prNum int) []*github.PullRequestReview { +func (gh *GitHub) ListPRReviews(prNum int) ([]*github.PullRequestReview, error) { var ( allReviews []*github.PullRequestReview opts = &github.ListOptions{ @@ -203,8 +209,7 @@ func (gh *GitHub) ListPRReviews(prNum int) []*github.PullRequestReview { opts, ) if err != nil { - gh.Logger.Errorf("Unable to list reviews for PR %d: %v", prNum, err) - return nil + return nil, fmt.Errorf("unable to list reviews for PR %d: %v", prNum, err) } allReviews = append(allReviews, reviews...) @@ -215,7 +220,7 @@ func (gh *GitHub) ListPRReviews(prNum int) []*github.PullRequestReview { opts.Page = response.NextPage } - return allReviews + return allReviews, nil } func New(ctx context.Context, params *p.Params) (*GitHub, error) { diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 39f5eca5837..cd8e2d29548 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -273,8 +273,10 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen } // Update comment on pull request - comment := gh.SetBotComment(commentText, pr.GetNumber()) - if comment != nil { + comment, err := gh.SetBotComment(commentText, pr.GetNumber()) + if err != nil { + return fmt.Errorf("unable to update comment on PR %d: %v", pr.GetNumber(), err) + } else { gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber()) } diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/comment_test.go index f530d11f877..933289ef63d 100644 --- a/contribs/github-bot/comment_test.go +++ b/contribs/github-bot/comment_test.go @@ -32,20 +32,23 @@ func TestGeneratedComment(t *testing.T) { {Description: "Test manual 5", CheckedBy: "user_5"}, } - commentText := generateComment(content) + commentText, err := generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.True(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should contains automated check placeholder") assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") content.AutoRules = autoRules - commentText = generateComment(content) + commentText, err = generateComment(content) fmt.Println(commentText) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") assert.Equal(t, 2, len(autoCheckSuccessLine.FindAllStringSubmatch(commentText, -1)), "wrong number of succeeded automatic check") assert.Equal(t, 3, len(autoCheckFailLine.FindAllStringSubmatch(commentText, -1)), "wrong number of failed automatic check") content.ManualRules = manualRules - commentText = generateComment(content) + commentText, err = generateComment(content) + assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") assert.False(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should not contains manual check placeholder") diff --git a/contribs/github-bot/conditions/assignee.go b/contribs/github-bot/conditions/assignee.go index bf1d9c360bc..52e681796f6 100644 --- a/contribs/github-bot/conditions/assignee.go +++ b/contribs/github-bot/conditions/assignee.go @@ -44,7 +44,13 @@ var _ Condition = &assigneeInTeam{} func (a *assigneeInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { detail := fmt.Sprintf("A pull request assignee is a member of the team: %s", a.team) - for _, member := range a.gh.ListTeamMembers(a.team) { + teamMembers, err := a.gh.ListTeamMembers(a.team) + if err != nil { + a.gh.Logger.Errorf("unable to check if assignee is in team %s: %v", a.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, member := range teamMembers { for _, assignee := range pr.Assignees { if member.GetLogin() == assignee.GetLogin() { return utils.AddStatusNode(true, fmt.Sprintf("%s (member: %s)", detail, member.GetLogin()), details) diff --git a/contribs/github-bot/conditions/author.go b/contribs/github-bot/conditions/author.go index 3a2aed6c396..96fdbd4a707 100644 --- a/contribs/github-bot/conditions/author.go +++ b/contribs/github-bot/conditions/author.go @@ -40,7 +40,13 @@ var _ Condition = &authorInTeam{} func (a *authorInTeam) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { detail := fmt.Sprintf("Pull request author is a member of the team: %s", a.team) - for _, member := range a.gh.ListTeamMembers(a.team) { + teamMembers, err := a.gh.ListTeamMembers(a.team) + if err != nil { + a.gh.Logger.Errorf("unable to check if author is in team %s: %v", a.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, member := range teamMembers { if member.GetLogin() == pr.GetUser().GetLogin() { return utils.AddStatusNode(true, detail, details) } diff --git a/contribs/github-bot/requirements/author.go b/contribs/github-bot/requirements/author.go index 957d085594e..799bed214b1 100644 --- a/contribs/github-bot/requirements/author.go +++ b/contribs/github-bot/requirements/author.go @@ -40,7 +40,13 @@ var _ Requirement = &authorInTeam{} func (a *authorInTeam) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { detail := fmt.Sprintf("Pull request author is a member of the team: %s", a.team) - for _, member := range a.gh.ListTeamMembers(a.team) { + teamMembers, err := a.gh.ListTeamMembers(a.team) + if err != nil { + a.gh.Logger.Errorf("unable to check if author is in team %s: %v", a.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, member := range teamMembers { if member.GetLogin() == pr.GetUser().GetLogin() { return utils.AddStatusNode(true, detail, details) } diff --git a/contribs/github-bot/requirements/reviewer.go b/contribs/github-bot/requirements/reviewer.go index 735672f2bc8..71c983d4d2a 100644 --- a/contribs/github-bot/requirements/reviewer.go +++ b/contribs/github-bot/requirements/reviewer.go @@ -24,12 +24,16 @@ func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tre // If not a dry run, make the user a reviewer if he's not already if !r.gh.DryRun { requested := false - if reviewers := r.gh.ListPRReviewers(pr.GetNumber()); reviewers != nil { - for _, user := range reviewers.Users { - if user.GetLogin() == r.user { - requested = true - break - } + reviewers, err := r.gh.ListPRReviewers(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if user %s review is already requested: %v", r.user, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, user := range reviewers.Users { + if user.GetLogin() == r.user { + requested = true + break } } @@ -52,7 +56,13 @@ func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tre } // Check if user already approved this PR - for _, review := range r.gh.ListPRReviews(pr.GetNumber()) { + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, review := range reviews { if review.GetUser().GetLogin() == r.user { r.gh.Logger.Debugf("User %s already reviewed PR %d with state %s", r.user, pr.GetNumber(), review.GetState()) return utils.AddStatusNode(review.GetState() == "APPROVED", detail, details) @@ -82,12 +92,16 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr // If not a dry run, make the user a reviewer if he's not already if !r.gh.DryRun { requested := false - if reviewers := r.gh.ListPRReviewers(pr.GetNumber()); reviewers != nil { - for _, team := range reviewers.Teams { - if team.GetSlug() == r.team { - requested = true - break - } + reviewers, err := r.gh.ListPRReviewers(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if team %s review is already requested: %v", r.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, team := range reviewers.Teams { + if team.GetSlug() == r.team { + requested = true + break } } @@ -111,8 +125,20 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr // Check how many members of this team already approved this PR approved := uint(0) - for _, review := range r.gh.ListPRReviews(pr.GetNumber()) { - for _, member := range r.gh.ListTeamMembers(r.team) { + reviews, err := r.gh.ListPRReviews(pr.GetNumber()) + if err != nil { + r.gh.Logger.Errorf("unable to check if a member of team %s already approved this PR: %v", r.team, err) + return utils.AddStatusNode(false, detail, details) + } + + for _, review := range reviews { + teamMembers, err := r.gh.ListTeamMembers(r.team) + if err != nil { + r.gh.Logger.Errorf(err.Error()) + continue + } + + for _, member := range teamMembers { if review.GetUser().GetLogin() == member.GetLogin() { if review.GetState() == "APPROVED" { approved += 1 From dfc70f3daa9368d8839197721037f0fdf5405eb7 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 22 Nov 2024 02:48:53 +0900 Subject: [PATCH 35/44] refactor: move every package in internal --- contribs/github-bot/bot.go | 8 ++++---- contribs/github-bot/comment.go | 2 +- contribs/github-bot/comment_test.go | 2 +- contribs/github-bot/config.go | 6 +++--- contribs/github-bot/{ => internal}/client/client.go | 4 ++-- contribs/github-bot/{ => internal}/conditions/assignee.go | 4 ++-- .../github-bot/{ => internal}/conditions/assignee_test.go | 6 +++--- contribs/github-bot/{ => internal}/conditions/author.go | 4 ++-- .../github-bot/{ => internal}/conditions/author_test.go | 6 +++--- contribs/github-bot/{ => internal}/conditions/boolean.go | 2 +- .../github-bot/{ => internal}/conditions/boolean_test.go | 2 +- contribs/github-bot/{ => internal}/conditions/branch.go | 2 +- .../github-bot/{ => internal}/conditions/branch_test.go | 2 +- .../github-bot/{ => internal}/conditions/condition.go | 0 contribs/github-bot/{ => internal}/conditions/constant.go | 2 +- .../github-bot/{ => internal}/conditions/constant_test.go | 2 +- contribs/github-bot/{ => internal}/conditions/file.go | 4 ++-- .../github-bot/{ => internal}/conditions/file_test.go | 6 +++--- contribs/github-bot/{ => internal}/conditions/label.go | 2 +- .../github-bot/{ => internal}/conditions/label_test.go | 2 +- contribs/github-bot/{ => internal}/logger/action.go | 0 contribs/github-bot/{ => internal}/logger/logger.go | 0 contribs/github-bot/{ => internal}/logger/noop.go | 0 contribs/github-bot/{ => internal}/logger/terminal.go | 0 contribs/github-bot/{ => internal}/params/params.go | 0 contribs/github-bot/{ => internal}/params/prlist.go | 0 .../github-bot/{ => internal}/requirements/assignee.go | 4 ++-- .../{ => internal}/requirements/assignee_test.go | 6 +++--- contribs/github-bot/{ => internal}/requirements/author.go | 4 ++-- .../github-bot/{ => internal}/requirements/author_test.go | 6 +++--- .../github-bot/{ => internal}/requirements/boolean.go | 2 +- .../{ => internal}/requirements/boolean_test.go | 2 +- contribs/github-bot/{ => internal}/requirements/branch.go | 4 ++-- .../github-bot/{ => internal}/requirements/branch_test.go | 6 +++--- .../github-bot/{ => internal}/requirements/constant.go | 2 +- .../{ => internal}/requirements/constant_test.go | 2 +- contribs/github-bot/{ => internal}/requirements/label.go | 4 ++-- .../github-bot/{ => internal}/requirements/label_test.go | 6 +++--- .../github-bot/{ => internal}/requirements/maintainer.go | 2 +- .../{ => internal}/requirements/maintener_test.go | 2 +- .../github-bot/{ => internal}/requirements/requirement.go | 0 .../github-bot/{ => internal}/requirements/reviewer.go | 4 ++-- .../{ => internal}/requirements/reviewer_test.go | 6 +++--- contribs/github-bot/{ => internal}/utils/testing.go | 0 contribs/github-bot/{ => internal}/utils/tree.go | 0 contribs/github-bot/main.go | 2 +- 46 files changed, 66 insertions(+), 66 deletions(-) rename contribs/github-bot/{ => internal}/client/client.go (97%) rename contribs/github-bot/{ => internal}/conditions/assignee.go (92%) rename contribs/github-bot/{ => internal}/conditions/assignee_test.go (93%) rename contribs/github-bot/{ => internal}/conditions/author.go (91%) rename contribs/github-bot/{ => internal}/conditions/author_test.go (92%) rename contribs/github-bot/{ => internal}/conditions/boolean.go (97%) rename contribs/github-bot/{ => internal}/conditions/boolean_test.go (97%) rename contribs/github-bot/{ => internal}/conditions/branch.go (94%) rename contribs/github-bot/{ => internal}/conditions/branch_test.go (95%) rename contribs/github-bot/{ => internal}/conditions/condition.go (100%) rename contribs/github-bot/{ => internal}/conditions/constant.go (91%) rename contribs/github-bot/{ => internal}/conditions/constant_test.go (91%) rename contribs/github-bot/{ => internal}/conditions/file.go (90%) rename contribs/github-bot/{ => internal}/conditions/file_test.go (90%) rename contribs/github-bot/{ => internal}/conditions/label.go (92%) rename contribs/github-bot/{ => internal}/conditions/label_test.go (95%) rename contribs/github-bot/{ => internal}/logger/action.go (100%) rename contribs/github-bot/{ => internal}/logger/logger.go (100%) rename contribs/github-bot/{ => internal}/logger/noop.go (100%) rename contribs/github-bot/{ => internal}/logger/terminal.go (100%) rename contribs/github-bot/{ => internal}/params/params.go (100%) rename contribs/github-bot/{ => internal}/params/prlist.go (100%) rename contribs/github-bot/{ => internal}/requirements/assignee.go (90%) rename contribs/github-bot/{ => internal}/requirements/assignee_test.go (91%) rename contribs/github-bot/{ => internal}/requirements/author.go (91%) rename contribs/github-bot/{ => internal}/requirements/author_test.go (93%) rename contribs/github-bot/{ => internal}/requirements/boolean.go (97%) rename contribs/github-bot/{ => internal}/requirements/boolean_test.go (97%) rename contribs/github-bot/{ => internal}/requirements/branch.go (90%) rename contribs/github-bot/{ => internal}/requirements/branch_test.go (89%) rename contribs/github-bot/{ => internal}/requirements/constant.go (91%) rename contribs/github-bot/{ => internal}/requirements/constant_test.go (91%) rename contribs/github-bot/{ => internal}/requirements/label.go (90%) rename contribs/github-bot/{ => internal}/requirements/label_test.go (93%) rename contribs/github-bot/{ => internal}/requirements/maintainer.go (89%) rename contribs/github-bot/{ => internal}/requirements/maintener_test.go (93%) rename contribs/github-bot/{ => internal}/requirements/requirement.go (100%) rename contribs/github-bot/{ => internal}/requirements/reviewer.go (97%) rename contribs/github-bot/{ => internal}/requirements/reviewer_test.go (97%) rename contribs/github-bot/{ => internal}/utils/testing.go (100%) rename contribs/github-bot/{ => internal}/utils/tree.go (100%) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index bfb1d1d6cc2..9a0466da069 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -8,10 +8,10 @@ import ( "sync" "sync/atomic" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - p "github.com/gnolang/gno/contribs/github-bot/params" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "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" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index cd8e2d29548..e413486faea 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -8,7 +8,7 @@ import ( "strings" "text/template" - "github.com/gnolang/gno/contribs/github-bot/client" + "github.com/gnolang/gno/contribs/github-bot/internal/client" "github.com/google/go-github/v64/github" "github.com/sethvargo/go-githubactions" diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/comment_test.go index 933289ef63d..97bb0eca18f 100644 --- a/contribs/github-bot/comment_test.go +++ b/contribs/github-bot/comment_test.go @@ -6,7 +6,7 @@ import ( "strings" "testing" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/stretchr/testify/assert" ) diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index d0d90c1cc91..8a2f2e3c12e 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -1,9 +1,9 @@ package main import ( - "github.com/gnolang/gno/contribs/github-bot/client" - c "github.com/gnolang/gno/contribs/github-bot/conditions" - r "github.com/gnolang/gno/contribs/github-bot/requirements" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + c "github.com/gnolang/gno/contribs/github-bot/internal/conditions" + r "github.com/gnolang/gno/contribs/github-bot/internal/requirements" ) // Automatic check that will be performed by the bot diff --git a/contribs/github-bot/client/client.go b/contribs/github-bot/internal/client/client.go similarity index 97% rename from contribs/github-bot/client/client.go rename to contribs/github-bot/internal/client/client.go index f824826cd40..4e8a605c4d3 100644 --- a/contribs/github-bot/client/client.go +++ b/contribs/github-bot/internal/client/client.go @@ -6,8 +6,8 @@ import ( "fmt" "os" - "github.com/gnolang/gno/contribs/github-bot/logger" - p "github.com/gnolang/gno/contribs/github-bot/params" + "github.com/gnolang/gno/contribs/github-bot/internal/logger" + p "github.com/gnolang/gno/contribs/github-bot/internal/params" "github.com/google/go-github/v64/github" ) diff --git a/contribs/github-bot/conditions/assignee.go b/contribs/github-bot/internal/conditions/assignee.go similarity index 92% rename from contribs/github-bot/conditions/assignee.go rename to contribs/github-bot/internal/conditions/assignee.go index 52e681796f6..9682e224f11 100644 --- a/contribs/github-bot/conditions/assignee.go +++ b/contribs/github-bot/internal/conditions/assignee.go @@ -3,8 +3,8 @@ package conditions import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/conditions/assignee_test.go b/contribs/github-bot/internal/conditions/assignee_test.go similarity index 93% rename from contribs/github-bot/conditions/assignee_test.go rename to contribs/github-bot/internal/conditions/assignee_test.go index ebf401120d5..9207e4604b7 100644 --- a/contribs/github-bot/conditions/assignee_test.go +++ b/contribs/github-bot/internal/conditions/assignee_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/utils" + "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/utils" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" diff --git a/contribs/github-bot/conditions/author.go b/contribs/github-bot/internal/conditions/author.go similarity index 91% rename from contribs/github-bot/conditions/author.go rename to contribs/github-bot/internal/conditions/author.go index 96fdbd4a707..e5079b9e4f4 100644 --- a/contribs/github-bot/conditions/author.go +++ b/contribs/github-bot/internal/conditions/author.go @@ -3,8 +3,8 @@ package conditions import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/conditions/author_test.go b/contribs/github-bot/internal/conditions/author_test.go similarity index 92% rename from contribs/github-bot/conditions/author_test.go rename to contribs/github-bot/internal/conditions/author_test.go index 34d9bf0ca6a..c5836f1ea76 100644 --- a/contribs/github-bot/conditions/author_test.go +++ b/contribs/github-bot/internal/conditions/author_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/utils" + "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/utils" "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/conditions/boolean.go b/contribs/github-bot/internal/conditions/boolean.go similarity index 97% rename from contribs/github-bot/conditions/boolean.go rename to contribs/github-bot/internal/conditions/boolean.go index bd84bdf3bc3..ce92e10d3ab 100644 --- a/contribs/github-bot/conditions/boolean.go +++ b/contribs/github-bot/internal/conditions/boolean.go @@ -3,7 +3,7 @@ package conditions import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/conditions/boolean_test.go b/contribs/github-bot/internal/conditions/boolean_test.go similarity index 97% rename from contribs/github-bot/conditions/boolean_test.go rename to contribs/github-bot/internal/conditions/boolean_test.go index 2aa6b7c32af..52f028cf2b4 100644 --- a/contribs/github-bot/conditions/boolean_test.go +++ b/contribs/github-bot/internal/conditions/boolean_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/conditions/branch.go b/contribs/github-bot/internal/conditions/branch.go similarity index 94% rename from contribs/github-bot/conditions/branch.go rename to contribs/github-bot/internal/conditions/branch.go index 30d521d0c8e..ef29e2d39cb 100644 --- a/contribs/github-bot/conditions/branch.go +++ b/contribs/github-bot/internal/conditions/branch.go @@ -4,7 +4,7 @@ import ( "fmt" "regexp" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/conditions/branch_test.go b/contribs/github-bot/internal/conditions/branch_test.go similarity index 95% rename from contribs/github-bot/conditions/branch_test.go rename to contribs/github-bot/internal/conditions/branch_test.go index e0a847bf082..3e53ef2db1c 100644 --- a/contribs/github-bot/conditions/branch_test.go +++ b/contribs/github-bot/internal/conditions/branch_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/conditions/condition.go b/contribs/github-bot/internal/conditions/condition.go similarity index 100% rename from contribs/github-bot/conditions/condition.go rename to contribs/github-bot/internal/conditions/condition.go diff --git a/contribs/github-bot/conditions/constant.go b/contribs/github-bot/internal/conditions/constant.go similarity index 91% rename from contribs/github-bot/conditions/constant.go rename to contribs/github-bot/internal/conditions/constant.go index 0181d331ec2..d00af13dca2 100644 --- a/contribs/github-bot/conditions/constant.go +++ b/contribs/github-bot/internal/conditions/constant.go @@ -1,7 +1,7 @@ package conditions import ( - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/conditions/constant_test.go b/contribs/github-bot/internal/conditions/constant_test.go similarity index 91% rename from contribs/github-bot/conditions/constant_test.go rename to contribs/github-bot/internal/conditions/constant_test.go index 304daa0d8f6..92bbe9b318a 100644 --- a/contribs/github-bot/conditions/constant_test.go +++ b/contribs/github-bot/internal/conditions/constant_test.go @@ -3,7 +3,7 @@ package conditions import ( "testing" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/stretchr/testify/assert" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/conditions/file.go b/contribs/github-bot/internal/conditions/file.go similarity index 90% rename from contribs/github-bot/conditions/file.go rename to contribs/github-bot/internal/conditions/file.go index a0beeab81c4..c51a13c39cd 100644 --- a/contribs/github-bot/conditions/file.go +++ b/contribs/github-bot/internal/conditions/file.go @@ -4,8 +4,8 @@ import ( "fmt" "regexp" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/conditions/file_test.go b/contribs/github-bot/internal/conditions/file_test.go similarity index 90% rename from contribs/github-bot/conditions/file_test.go rename to contribs/github-bot/internal/conditions/file_test.go index d2f9e8e0311..3fd7a33fa4a 100644 --- a/contribs/github-bot/conditions/file_test.go +++ b/contribs/github-bot/internal/conditions/file_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/utils" + "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/utils" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" diff --git a/contribs/github-bot/conditions/label.go b/contribs/github-bot/internal/conditions/label.go similarity index 92% rename from contribs/github-bot/conditions/label.go rename to contribs/github-bot/internal/conditions/label.go index 741f7c0d9b0..de3844b9747 100644 --- a/contribs/github-bot/conditions/label.go +++ b/contribs/github-bot/internal/conditions/label.go @@ -4,7 +4,7 @@ import ( "fmt" "regexp" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/conditions/label_test.go b/contribs/github-bot/internal/conditions/label_test.go similarity index 95% rename from contribs/github-bot/conditions/label_test.go rename to contribs/github-bot/internal/conditions/label_test.go index c77943491ff..ea895b28ad1 100644 --- a/contribs/github-bot/conditions/label_test.go +++ b/contribs/github-bot/internal/conditions/label_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/logger/action.go b/contribs/github-bot/internal/logger/action.go similarity index 100% rename from contribs/github-bot/logger/action.go rename to contribs/github-bot/internal/logger/action.go diff --git a/contribs/github-bot/logger/logger.go b/contribs/github-bot/internal/logger/logger.go similarity index 100% rename from contribs/github-bot/logger/logger.go rename to contribs/github-bot/internal/logger/logger.go diff --git a/contribs/github-bot/logger/noop.go b/contribs/github-bot/internal/logger/noop.go similarity index 100% rename from contribs/github-bot/logger/noop.go rename to contribs/github-bot/internal/logger/noop.go diff --git a/contribs/github-bot/logger/terminal.go b/contribs/github-bot/internal/logger/terminal.go similarity index 100% rename from contribs/github-bot/logger/terminal.go rename to contribs/github-bot/internal/logger/terminal.go diff --git a/contribs/github-bot/params/params.go b/contribs/github-bot/internal/params/params.go similarity index 100% rename from contribs/github-bot/params/params.go rename to contribs/github-bot/internal/params/params.go diff --git a/contribs/github-bot/params/prlist.go b/contribs/github-bot/internal/params/prlist.go similarity index 100% rename from contribs/github-bot/params/prlist.go rename to contribs/github-bot/internal/params/prlist.go diff --git a/contribs/github-bot/requirements/assignee.go b/contribs/github-bot/internal/requirements/assignee.go similarity index 90% rename from contribs/github-bot/requirements/assignee.go rename to contribs/github-bot/internal/requirements/assignee.go index aabbcb845d9..c48a63c34db 100644 --- a/contribs/github-bot/requirements/assignee.go +++ b/contribs/github-bot/internal/requirements/assignee.go @@ -3,8 +3,8 @@ package requirements import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/assignee_test.go b/contribs/github-bot/internal/requirements/assignee_test.go similarity index 91% rename from contribs/github-bot/requirements/assignee_test.go rename to contribs/github-bot/internal/requirements/assignee_test.go index 10a77bb9523..df6ffdf0cd3 100644 --- a/contribs/github-bot/requirements/assignee_test.go +++ b/contribs/github-bot/internal/requirements/assignee_test.go @@ -5,9 +5,9 @@ import ( "net/http" "testing" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/utils" + "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/utils" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" diff --git a/contribs/github-bot/requirements/author.go b/contribs/github-bot/internal/requirements/author.go similarity index 91% rename from contribs/github-bot/requirements/author.go rename to contribs/github-bot/internal/requirements/author.go index 799bed214b1..b7bd7cd9411 100644 --- a/contribs/github-bot/requirements/author.go +++ b/contribs/github-bot/internal/requirements/author.go @@ -3,8 +3,8 @@ package requirements import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/author_test.go b/contribs/github-bot/internal/requirements/author_test.go similarity index 93% rename from contribs/github-bot/requirements/author_test.go rename to contribs/github-bot/internal/requirements/author_test.go index 9937497e5ce..768ca44f24e 100644 --- a/contribs/github-bot/requirements/author_test.go +++ b/contribs/github-bot/internal/requirements/author_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/utils" + "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/utils" "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/requirements/boolean.go b/contribs/github-bot/internal/requirements/boolean.go similarity index 97% rename from contribs/github-bot/requirements/boolean.go rename to contribs/github-bot/internal/requirements/boolean.go index 9556d1be0cc..638ac073ee1 100644 --- a/contribs/github-bot/requirements/boolean.go +++ b/contribs/github-bot/internal/requirements/boolean.go @@ -3,7 +3,7 @@ package requirements import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/boolean_test.go b/contribs/github-bot/internal/requirements/boolean_test.go similarity index 97% rename from contribs/github-bot/requirements/boolean_test.go rename to contribs/github-bot/internal/requirements/boolean_test.go index fee94b1d465..0043a44985c 100644 --- a/contribs/github-bot/requirements/boolean_test.go +++ b/contribs/github-bot/internal/requirements/boolean_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go similarity index 90% rename from contribs/github-bot/requirements/branch.go rename to contribs/github-bot/internal/requirements/branch.go index 68160628934..bd5a1e3ea89 100644 --- a/contribs/github-bot/requirements/branch.go +++ b/contribs/github-bot/internal/requirements/branch.go @@ -3,8 +3,8 @@ package requirements import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/branch_test.go b/contribs/github-bot/internal/requirements/branch_test.go similarity index 89% rename from contribs/github-bot/requirements/branch_test.go rename to contribs/github-bot/internal/requirements/branch_test.go index 91d0a773069..54387beb605 100644 --- a/contribs/github-bot/requirements/branch_test.go +++ b/contribs/github-bot/internal/requirements/branch_test.go @@ -5,9 +5,9 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/utils" + "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/utils" "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/requirements/constant.go b/contribs/github-bot/internal/requirements/constant.go similarity index 91% rename from contribs/github-bot/requirements/constant.go rename to contribs/github-bot/internal/requirements/constant.go index 8f17e546992..5ab33c2573a 100644 --- a/contribs/github-bot/requirements/constant.go +++ b/contribs/github-bot/internal/requirements/constant.go @@ -1,7 +1,7 @@ package requirements import ( - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/constant_test.go b/contribs/github-bot/internal/requirements/constant_test.go similarity index 91% rename from contribs/github-bot/requirements/constant_test.go rename to contribs/github-bot/internal/requirements/constant_test.go index 31e7b25d9eb..b04addcb672 100644 --- a/contribs/github-bot/requirements/constant_test.go +++ b/contribs/github-bot/internal/requirements/constant_test.go @@ -3,7 +3,7 @@ package requirements import ( "testing" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/stretchr/testify/assert" "github.com/xlab/treeprint" ) diff --git a/contribs/github-bot/requirements/label.go b/contribs/github-bot/internal/requirements/label.go similarity index 90% rename from contribs/github-bot/requirements/label.go rename to contribs/github-bot/internal/requirements/label.go index 5586aa4c4ee..7963c47b0cd 100644 --- a/contribs/github-bot/requirements/label.go +++ b/contribs/github-bot/internal/requirements/label.go @@ -3,8 +3,8 @@ package requirements import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/label_test.go b/contribs/github-bot/internal/requirements/label_test.go similarity index 93% rename from contribs/github-bot/requirements/label_test.go rename to contribs/github-bot/internal/requirements/label_test.go index acb18019305..6fbe8ff7f25 100644 --- a/contribs/github-bot/requirements/label_test.go +++ b/contribs/github-bot/internal/requirements/label_test.go @@ -5,9 +5,9 @@ import ( "net/http" "testing" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/utils" + "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/utils" "github.com/migueleliasweb/go-github-mock/src/mock" "github.com/stretchr/testify/assert" diff --git a/contribs/github-bot/requirements/maintainer.go b/contribs/github-bot/internal/requirements/maintainer.go similarity index 89% rename from contribs/github-bot/requirements/maintainer.go rename to contribs/github-bot/internal/requirements/maintainer.go index 11c2c351cf2..261f415f852 100644 --- a/contribs/github-bot/requirements/maintainer.go +++ b/contribs/github-bot/internal/requirements/maintainer.go @@ -1,7 +1,7 @@ package requirements import ( - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/maintener_test.go b/contribs/github-bot/internal/requirements/maintener_test.go similarity index 93% rename from contribs/github-bot/requirements/maintener_test.go rename to contribs/github-bot/internal/requirements/maintener_test.go index 7d8efd4e10d..5b71803b468 100644 --- a/contribs/github-bot/requirements/maintener_test.go +++ b/contribs/github-bot/internal/requirements/maintener_test.go @@ -4,7 +4,7 @@ import ( "fmt" "testing" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/stretchr/testify/assert" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/requirement.go b/contribs/github-bot/internal/requirements/requirement.go similarity index 100% rename from contribs/github-bot/requirements/requirement.go rename to contribs/github-bot/internal/requirements/requirement.go diff --git a/contribs/github-bot/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go similarity index 97% rename from contribs/github-bot/requirements/reviewer.go rename to contribs/github-bot/internal/requirements/reviewer.go index 71c983d4d2a..174c4b6208c 100644 --- a/contribs/github-bot/requirements/reviewer.go +++ b/contribs/github-bot/internal/requirements/reviewer.go @@ -3,8 +3,8 @@ package requirements import ( "fmt" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" diff --git a/contribs/github-bot/requirements/reviewer_test.go b/contribs/github-bot/internal/requirements/reviewer_test.go similarity index 97% rename from contribs/github-bot/requirements/reviewer_test.go rename to contribs/github-bot/internal/requirements/reviewer_test.go index 0fc27ccb356..16c50e13743 100644 --- a/contribs/github-bot/requirements/reviewer_test.go +++ b/contribs/github-bot/internal/requirements/reviewer_test.go @@ -6,9 +6,9 @@ import ( "net/http" "testing" - "github.com/gnolang/gno/contribs/github-bot/client" - "github.com/gnolang/gno/contribs/github-bot/logger" - "github.com/gnolang/gno/contribs/github-bot/utils" + "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/utils" "github.com/stretchr/testify/assert" "github.com/google/go-github/v64/github" diff --git a/contribs/github-bot/utils/testing.go b/contribs/github-bot/internal/utils/testing.go similarity index 100% rename from contribs/github-bot/utils/testing.go rename to contribs/github-bot/internal/utils/testing.go diff --git a/contribs/github-bot/utils/tree.go b/contribs/github-bot/internal/utils/tree.go similarity index 100% rename from contribs/github-bot/utils/tree.go rename to contribs/github-bot/internal/utils/tree.go diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index dca825568b4..32ab19eb4dd 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -4,7 +4,7 @@ import ( "context" "os" - p "github.com/gnolang/gno/contribs/github-bot/params" + p "github.com/gnolang/gno/contribs/github-bot/internal/params" "github.com/gnolang/gno/tm2/pkg/commands" ) From 28f1b6a3282c8d9944ded1bfbb83f3ac625d9192 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 22 Nov 2024 02:52:27 +0900 Subject: [PATCH 36/44] refactor: replace pending by failure in commit status --- contribs/github-bot/comment.go | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index e413486faea..f48568c0c39 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -283,15 +283,11 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen // Prepare commit status content var ( context = "Merge Requirements" - targetURL = "" - state = "pending" + targetURL = comment.GetHTMLURL() + state = "failure" description = "Some requirements are not satisfied yet. See bot comment." ) - if comment != nil { - targetURL = comment.GetHTMLURL() - } - if content.allSatisfied { state = "success" description = "All requirements are satisfied." From 80b8904fe07481266acb5adba7acab354853ff63 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 22 Nov 2024 03:07:09 +0900 Subject: [PATCH 37/44] refactor: replace string by Status type --- contribs/github-bot/bot.go | 20 ++++++------- contribs/github-bot/comment_test.go | 4 +-- .../github-bot/internal/conditions/boolean.go | 28 +++++++------------ .../internal/requirements/boolean.go | 28 +++++++------------ contribs/github-bot/internal/utils/testing.go | 6 ++-- contribs/github-bot/internal/utils/tree.go | 10 ++++--- 6 files changed, 41 insertions(+), 55 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index 9a0466da069..72a2878897a 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -95,7 +95,7 @@ func execBot(params *p.Params) error { // Iterate over all automatic rules in config for _, autoRule := range autoRules { - ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) + 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) { @@ -103,11 +103,11 @@ func execBot(params *p.Params) error { } c := AutoContent{Description: autoRule.description, Satisfied: false} - thenDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Requirement not satisfied", utils.StatusFail)) + 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) { - thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.StatusSuccess)) + thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success)) c.Satisfied = true } else { commentContent.allSatisfied = false @@ -126,7 +126,7 @@ func execBot(params *p.Params) error { // Iterate over all manual rules in config for _, manualRule := range manualRules { - ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.StatusSuccess)) + 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) { @@ -186,9 +186,9 @@ func logResults(logger logger.Logger, prNum int, commentContent CommentContent) } for _, rule := range commentContent.AutoRules { - status := utils.StatusFail + status := utils.Fail if rule.Satisfied { - status = utils.StatusSuccess + status = utils.Success } logger.Infof("%s %s", status, rule.Description) logger.Debugf("If:\n%s", rule.ConditionDetails) @@ -200,10 +200,10 @@ func logResults(logger logger.Logger, prNum int, commentContent CommentContent) } for _, rule := range commentContent.ManualRules { - status := utils.StatusFail + status := utils.Fail checker := "any user with comment edit permission" if rule.CheckedBy != "" { - status = utils.StatusSuccess + status = utils.Success } if len(rule.Teams) == 0 { checker = fmt.Sprintf("a member of one of these teams: %s", strings.Join(rule.Teams, ", ")) @@ -215,8 +215,8 @@ func logResults(logger logger.Logger, prNum int, commentContent CommentContent) logger.Infof("Conclusion:") if commentContent.allSatisfied { - logger.Infof("%s All requirements are satisfied\n", utils.StatusSuccess) + logger.Infof("%s All requirements are satisfied\n", utils.Success) } else { - logger.Infof("%s Not all requirements are satisfied\n", utils.StatusFail) + logger.Infof("%s Not all requirements are satisfied\n", utils.Fail) } } diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/comment_test.go index 97bb0eca18f..054c49177f9 100644 --- a/contribs/github-bot/comment_test.go +++ b/contribs/github-bot/comment_test.go @@ -13,8 +13,8 @@ import ( func TestGeneratedComment(t *testing.T) { t.Parallel() - autoCheckSuccessLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.StatusSuccess)) - autoCheckFailLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.StatusFail)) + autoCheckSuccessLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.Success)) + autoCheckFailLine := regexp.MustCompile(fmt.Sprintf(`(?m:^ %s .+$)`, utils.Fail)) content := CommentContent{} autoRules := []AutoContent{ diff --git a/contribs/github-bot/internal/conditions/boolean.go b/contribs/github-bot/internal/conditions/boolean.go index ce92e10d3ab..e030563709a 100644 --- a/contribs/github-bot/internal/conditions/boolean.go +++ b/contribs/github-bot/internal/conditions/boolean.go @@ -17,24 +17,20 @@ type and struct { var _ Condition = &and{} func (a *and) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - met := true + met := utils.Success branch := details.AddBranch("") for _, condition := range a.conditions { if !condition.IsMet(pr, branch) { - met = false + met = utils.Fail // We don't break here because we need to call IsMet on all conditions // to populate the details tree } } - if met { - branch.SetValue(fmt.Sprintf("%s And", utils.StatusSuccess)) - } else { - branch.SetValue(fmt.Sprintf("%s And", utils.StatusFail)) - } + branch.SetValue(fmt.Sprintf("%s And", met)) - return met + return (met == utils.Success) } func And(conditions ...Condition) Condition { @@ -53,24 +49,20 @@ type or struct { var _ Condition = &or{} func (o *or) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { - met := false + met := utils.Fail branch := details.AddBranch("") for _, condition := range o.conditions { if condition.IsMet(pr, branch) { - met = true + met = utils.Success // We don't break here because we need to call IsMet on all conditions // to populate the details tree } } - if met { - branch.SetValue(fmt.Sprintf("%s Or", utils.StatusSuccess)) - } else { - branch.SetValue(fmt.Sprintf("%s Or", utils.StatusFail)) - } + branch.SetValue(fmt.Sprintf("%s Or", met)) - return met + return (met == utils.Success) } func Or(conditions ...Condition) Condition { @@ -93,9 +85,9 @@ func (n *not) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { node := details.FindLastNode() if met { - node.SetValue(fmt.Sprintf("%s Not (%s)", utils.StatusFail, node.(*treeprint.Node).Value.(string))) + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Fail, node.(*treeprint.Node).Value.(string))) } else { - node.SetValue(fmt.Sprintf("%s Not (%s)", utils.StatusSuccess, node.(*treeprint.Node).Value.(string))) + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Success, node.(*treeprint.Node).Value.(string))) } return !met diff --git a/contribs/github-bot/internal/requirements/boolean.go b/contribs/github-bot/internal/requirements/boolean.go index 638ac073ee1..1b6840f6aa9 100644 --- a/contribs/github-bot/internal/requirements/boolean.go +++ b/contribs/github-bot/internal/requirements/boolean.go @@ -17,24 +17,20 @@ type and struct { var _ Requirement = &and{} func (a *and) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { - satisfied := true + satisfied := utils.Success branch := details.AddBranch("") for _, requirement := range a.requirements { if !requirement.IsSatisfied(pr, branch) { - satisfied = false + satisfied = utils.Fail // We don't break here because we need to call IsSatisfied on all // requirements to populate the details tree } } - if satisfied { - branch.SetValue(fmt.Sprintf("%s And", utils.StatusSuccess)) - } else { - branch.SetValue(fmt.Sprintf("%s And", utils.StatusFail)) - } + branch.SetValue(fmt.Sprintf("%s And", satisfied)) - return satisfied + return (satisfied == utils.Success) } func And(requirements ...Requirement) Requirement { @@ -53,24 +49,20 @@ type or struct { var _ Requirement = &or{} func (o *or) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { - satisfied := false + satisfied := utils.Fail branch := details.AddBranch("") for _, requirement := range o.requirements { if requirement.IsSatisfied(pr, branch) { - satisfied = true + satisfied = utils.Success // We don't break here because we need to call IsSatisfied on all // requirements to populate the details tree } } - if satisfied { - branch.SetValue(fmt.Sprintf("%s Or", utils.StatusSuccess)) - } else { - branch.SetValue(fmt.Sprintf("%s Or", utils.StatusFail)) - } + branch.SetValue(fmt.Sprintf("%s Or", satisfied)) - return satisfied + return (satisfied == utils.Success) } func Or(requirements ...Requirement) Requirement { @@ -93,9 +85,9 @@ func (n *not) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { node := details.FindLastNode() if satisfied { - node.SetValue(fmt.Sprintf("%s Not (%s)", utils.StatusFail, node.(*treeprint.Node).Value.(string))) + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Fail, node.(*treeprint.Node).Value.(string))) } else { - node.SetValue(fmt.Sprintf("%s Not (%s)", utils.StatusSuccess, node.(*treeprint.Node).Value.(string))) + node.SetValue(fmt.Sprintf("%s Not (%s)", utils.Success, node.(*treeprint.Node).Value.(string))) } return !satisfied diff --git a/contribs/github-bot/internal/utils/testing.go b/contribs/github-bot/internal/utils/testing.go index 39dbc8b82d7..3c7f7bfef88 100644 --- a/contribs/github-bot/internal/utils/testing.go +++ b/contribs/github-bot/internal/utils/testing.go @@ -11,11 +11,11 @@ func TestLastNodeStatus(t *testing.T, success bool, details treeprint.Tree) bool t.Helper() detail := details.FindLastNode().(*treeprint.Node).Value.(string) - status := StatusFail + status := Fail if success { - status = StatusSuccess + status = Success } - return strings.HasPrefix(detail, status) + return strings.HasPrefix(detail, string(status)) } diff --git a/contribs/github-bot/internal/utils/tree.go b/contribs/github-bot/internal/utils/tree.go index f79bd45f118..c6ff57bcd99 100644 --- a/contribs/github-bot/internal/utils/tree.go +++ b/contribs/github-bot/internal/utils/tree.go @@ -6,16 +6,18 @@ import ( "github.com/xlab/treeprint" ) +type Status string + const ( - StatusSuccess = "🟢" - StatusFail = "🔴" + Success Status = "🟢" + Fail Status = "🔴" ) func AddStatusNode(b bool, desc string, details treeprint.Tree) bool { if b { - details.AddNode(fmt.Sprintf("%s %s", StatusSuccess, desc)) + details.AddNode(fmt.Sprintf("%s %s", Success, desc)) } else { - details.AddNode(fmt.Sprintf("%s %s", StatusFail, desc)) + details.AddNode(fmt.Sprintf("%s %s", Fail, desc)) } return b From 4efa3fb27d9a3177b1ca9a4d5bf775a21db46139 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 22 Nov 2024 03:15:26 +0900 Subject: [PATCH 38/44] style: fix lint --- contribs/github-bot/bot.go | 4 ++-- contribs/github-bot/comment.go | 12 ++++++------ contribs/github-bot/internal/client/client.go | 18 +++++++++--------- 3 files changed, 17 insertions(+), 17 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index 72a2878897a..e16165e73f1 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -32,7 +32,7 @@ func execBot(params *p.Params) error { } // Handle comment update, if any - if err := handleCommentUpdate(gh); err == errTriggeredByBot { + if err := handleCommentUpdate(gh); errors.Is(err, errTriggeredByBot) { return nil // Ignore if this run was triggered by a previous run } else if err != nil { return fmt.Errorf("comment update handling failed: %w", err) @@ -171,7 +171,7 @@ func execBot(params *p.Params) error { wg.Wait() if failed.Load() { - return errors.New("error occured while processing pull requests") + return errors.New("error occurred while processing pull requests") } return nil diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index f48568c0c39..dc70ecbe4f6 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -56,7 +56,7 @@ func getCommentManualChecks(commentBody string) map[string][2]string { // For each line that matches the "Manual check" regex for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { status := match[1] - // Try to capture an occurence of : (checked by @user) + // Try to capture an occurrence of '(checked by @user)' if details := manualCheckDetails.FindAllStringSubmatch(match[2], -1); len(details) > 0 { // If found, set both the status and the user that checked the box description := details[0][1] @@ -252,13 +252,13 @@ func generateComment(content CommentContent) (string, error) { const tmplFile = "comment.tmpl" tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile) if err != nil { - return "", fmt.Errorf("unable to init template: %v", err) + return "", fmt.Errorf("unable to init template: %w", err) } // Generate bot comment using template file var commentBytes bytes.Buffer if err := tmpl.Execute(&commentBytes, content); err != nil { - return "", fmt.Errorf("unable to execute template: %v", err) + return "", fmt.Errorf("unable to execute template: %w", err) } return commentBytes.String(), nil @@ -269,13 +269,13 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen // Generate comment text content commentText, err := generateComment(content) if err != nil { - return fmt.Errorf("unable to generate comment on PR %d: %v", pr.GetNumber(), err) + return fmt.Errorf("unable to generate comment on PR %d: %w", pr.GetNumber(), err) } // Update comment on pull request comment, err := gh.SetBotComment(commentText, pr.GetNumber()) if err != nil { - return fmt.Errorf("unable to update comment on PR %d: %v", pr.GetNumber(), err) + return fmt.Errorf("unable to update comment on PR %d: %w", pr.GetNumber(), err) } else { gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber()) } @@ -305,7 +305,7 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen TargetURL: &targetURL, Description: &description, }); err != nil { - return fmt.Errorf("unable to create status on PR %d: %v", pr.GetNumber(), err) + return fmt.Errorf("unable to create status on PR %d: %w", pr.GetNumber(), err) } else { gh.Logger.Infof("Commit status successfully updated on PR %d", pr.GetNumber()) } diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go index 4e8a605c4d3..19f96339233 100644 --- a/contribs/github-bot/internal/client/client.go +++ b/contribs/github-bot/internal/client/client.go @@ -36,7 +36,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { // Get current user (bot) currentUser, _, err := gh.Client.Users.Get(gh.Ctx, "") if err != nil { - return nil, fmt.Errorf("unable to get current user: %v", err) + return nil, fmt.Errorf("unable to get current user: %w", err) } // Pagination option @@ -57,7 +57,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { opts, ) if err != nil { - return nil, fmt.Errorf("unable to list comments for PR %d: %v", prNum, err) + return nil, fmt.Errorf("unable to list comments for PR %d: %w", prNum, err) } // Get the comment created by current user @@ -79,7 +79,7 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, error) { // Create bot comment if it does not already exist comment, err := gh.GetBotComment(prNum) - if err == ErrBotCommentNotFound { + if errors.Is(err, ErrBotCommentNotFound) { newComment, _, err := gh.Client.Issues.CreateComment( gh.Ctx, gh.Owner, @@ -88,11 +88,11 @@ func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, e &github.IssueComment{Body: &body}, ) if err != nil { - return nil, fmt.Errorf("unable to create bot comment for PR %d: %v", prNum, err) + return nil, fmt.Errorf("unable to create bot comment for PR %d: %w", prNum, err) } return newComment, nil } else if err != nil { - return nil, fmt.Errorf("unable to get bot comment: %v", err) + return nil, fmt.Errorf("unable to get bot comment: %w", err) } comment.Body = &body @@ -104,7 +104,7 @@ func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, e comment, ) if err != nil { - return nil, fmt.Errorf("unable to edit bot comment with ID %d: %v", comment.GetID(), err) + return nil, fmt.Errorf("unable to edit bot comment with ID %d: %w", comment.GetID(), err) } return editComment, nil @@ -128,7 +128,7 @@ func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) { opts, ) if err != nil { - return nil, fmt.Errorf("unable to list members for team %s: %v", team, err) + return nil, fmt.Errorf("unable to list members for team %s: %w", team, err) } allMembers = append(allMembers, members...) @@ -177,7 +177,7 @@ func (gh *GitHub) ListPRReviewers(prNum int) (*github.Reviewers, error) { opts, ) if err != nil { - return nil, fmt.Errorf("unable to list reviewers for PR %d: %v", prNum, err) + return nil, fmt.Errorf("unable to list reviewers for PR %d: %w", prNum, err) } allReviewers.Teams = append(allReviewers.Teams, reviewers.Teams...) @@ -209,7 +209,7 @@ func (gh *GitHub) ListPRReviews(prNum int) ([]*github.PullRequestReview, error) opts, ) if err != nil { - return nil, fmt.Errorf("unable to list reviews for PR %d: %v", prNum, err) + return nil, fmt.Errorf("unable to list reviews for PR %d: %w", prNum, err) } allReviews = append(allReviews, reviews...) From 609da3110ad8b677dff754f47bc63c9b843aef67 Mon Sep 17 00:00:00 2001 From: aeddi Date: Fri, 22 Nov 2024 18:39:41 +0900 Subject: [PATCH 39/44] refactor: various nits, added more tests --- contribs/github-bot/README.md | 2 +- contribs/github-bot/bot.go | 41 +++-- contribs/github-bot/comment.go | 105 ++++++------- contribs/github-bot/comment_test.go | 141 +++++++++++++++++- contribs/github-bot/internal/params/params.go | 18 +-- contribs/github-bot/internal/params/prlist.go | 12 +- contribs/github-bot/main.go | 2 +- 7 files changed, 232 insertions(+), 89 deletions(-) diff --git a/contribs/github-bot/README.md b/contribs/github-bot/README.md index 8f81a0d9f04..e3cc12fe01a 100644 --- a/contribs/github-bot/README.md +++ b/contribs/github-bot/README.md @@ -32,7 +32,7 @@ For the bot to make requests to the GitHub API, it needs a Personal Access Token > github-bot --help USAGE - [flags] + github-bot [flags] This tool checks if the requirements for a PR to be merged are satisfied (defined in config.go) and displays PR status checks accordingly. A valid GitHub Token must be provided by setting the GITHUB_TOKEN environment variable. diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index e16165e73f1..6264a0d8b61 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -13,6 +13,7 @@ import ( 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" + "github.com/sethvargo/go-githubactions" "github.com/xlab/treeprint" ) @@ -31,8 +32,15 @@ func execBot(params *p.Params) error { return fmt.Errorf("comment update handling failed: %w", err) } + // Get GitHub Actions context to retrieve comment update + actionCtx, err := githubactions.Context() + if err != nil { + gh.Logger.Debugf("Unable to retrieve GitHub Actions context: %v", err) + return nil + } + // Handle comment update, if any - if err := handleCommentUpdate(gh); errors.Is(err, errTriggeredByBot) { + if err := handleCommentUpdate(gh, actionCtx); errors.Is(err, errTriggeredByBot) { return nil // Ignore if this run was triggered by a previous run } else if err != nil { return fmt.Errorf("comment update handling failed: %w", err) @@ -42,23 +50,34 @@ func execBot(params *p.Params) error { var prs []*github.PullRequest // If requested, retrieve all open pull requests - if params.PrAll { + if params.PRAll { opts := &github.PullRequestListOptions{ State: "open", Sort: "updated", Direction: "desc", + ListOptions: github.ListOptions{ + PerPage: client.PageSize, + }, } - prs, _, err = gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) - if err != nil { - return fmt.Errorf("unable to retrieve all open pull requests: %w", err) - } + for { + prsPage, response, err := gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) + if err != nil { + return fmt.Errorf("unable to retrieve all open pull requests: %w", err) + } + prs = append(prs, prsPage...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + } else { // Otherwise, retrieve only specified pull request(s) // (flag or GitHub Action context) - } else { - prs = make([]*github.PullRequest, len(params.PrNums)) - for i, prNum := range params.PrNums { + 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) if err != nil { return fmt.Errorf("unable to retrieve specified pull request (%d): %w", prNum, err) @@ -119,7 +138,7 @@ func execBot(params *p.Params) error { } // Retrieve manual check states - checks := make(map[string][2]string) + checks := make(map[string]manualCheckDetails) if comment, err := gh.GetBotComment(pr.GetNumber()); err == nil { checks = getCommentManualChecks(comment.GetBody()) } @@ -137,7 +156,7 @@ func execBot(params *p.Params) error { checkedBy := "" check, ok := checks[manualRule.description] if ok { - checkedBy = check[1] + checkedBy = check.checkedBy } commentContent.ManualRules = append( diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index dc70ecbe4f6..29258170766 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -21,7 +21,7 @@ var ( // Regex for capturing the entire line of a manual check manualCheckLine = regexp.MustCompile(`(?m:^- \[([ x])\] (.+)?$)`) // Regex for capturing only the user who checked it - manualCheckDetails = regexp.MustCompile(`(?m:(.+) \(checked by @(\w+)\)$)`) + manualCheckStatus = regexp.MustCompile(`(?m:(.+) \(checked by @(\w+)\)$)`) // Regex for capturing only the checkboxes checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) // Regex used to capture markdown links @@ -48,45 +48,60 @@ type CommentContent struct { allSatisfied bool } +type manualCheckDetails struct { + status string + checkedBy string +} + // getCommentManualChecks parses the bot comment to get the checkbox status, // the check description and the username who checked it -func getCommentManualChecks(commentBody string) map[string][2]string { - checks := make(map[string][2]string) +func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { + checks := make(map[string]manualCheckDetails) // For each line that matches the "Manual check" regex for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { status := match[1] // Try to capture an occurrence of '(checked by @user)' - if details := manualCheckDetails.FindAllStringSubmatch(match[2], -1); len(details) > 0 { + if details := manualCheckStatus.FindAllStringSubmatch(match[2], -1); len(details) > 0 { // If found, set both the status and the user that checked the box description := details[0][1] checkedBy := details[0][2] - checks[description] = [2]string{status, checkedBy} + checks[description] = manualCheckDetails{status, checkedBy} continue } // If not found, set only the status of the box description := match[2] - checks[description] = [2]string{status} + checks[description] = manualCheckDetails{status: status} } return checks } +// Recursively search for nested values using the keys provided +func indexMap(m map[string]any, keys ...string) any { + if len(keys) == 0 { + return m + } + + if val, ok := m[keys[0]]; ok { + if keys = keys[1:]; len(keys) == 0 { + return val + } + subMap, _ := val.(map[string]any) + return indexMap(subMap, keys...) + } + + return nil +} + // handleCommentUpdate checks if: // - the current run was triggered by GitHub Actions // - the triggering event is an edit of the bot comment // - the comment was not edited by the bot itself (prevent infinite loop) // - the comment change is only a checkbox being checked or unckecked (or restore it) // - the actor / comment editor has permission to modify this checkbox (or restore it) -func handleCommentUpdate(gh *client.GitHub) error { - // Get GitHub Actions context to retrieve comment update - actionCtx, err := githubactions.Context() - if err != nil { - gh.Logger.Debugf("Unable to retrieve GitHub Actions context: %v", err) - return nil - } - +func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubContext) error { // Ignore if it's not a comment related event if actionCtx.EventName != "issue_comment" { gh.Logger.Debugf("Event is not issue comment related (%s)", actionCtx.EventName) @@ -114,57 +129,32 @@ func handleCommentUpdate(gh *client.GitHub) error { return errTriggeredByBot } - // Ignore if comment edition author is not the bot - comment, ok := actionCtx.Event["comment"].(map[string]any) - if !ok { - return errors.New("unable to get comment on issue comment event") - } - - author, ok := comment["user"].(map[string]any) - if !ok { - return errors.New("unable to get comment user on issue comment event") - } - - login, ok := author["login"].(string) + // Get login of the author of the edited comment + login, ok := indexMap(actionCtx.Event, "comment", "user", "login").(string) if !ok { return errors.New("unable to get comment user login on issue comment event") } - // If comment edition author is not the bot, return + // If the author is not the bot, return if login != authUser.GetLogin() { return nil } - // Get comment current body - current, ok := comment["body"].(string) - if !ok { - return errors.New("unable to get comment body on issue comment event") - } - // Get comment updated body - changes, ok := actionCtx.Event["changes"].(map[string]any) + current, ok := indexMap(actionCtx.Event, "comment", "body").(string) if !ok { - return errors.New("unable to get changes on issue comment event") - } - - changesBody, ok := changes["body"].(map[string]any) - if !ok { - return errors.New("unable to get changes body on issue comment event") + return errors.New("unable to get comment body on issue comment event") } - previous, ok := changesBody["from"].(string) + // Get comment previous body + previous, ok := indexMap(actionCtx.Event, "changes", "body", "from").(string) if !ok { return errors.New("unable to get changes body content on issue comment event") } // Get PR number from GitHub Actions context - issue, ok := actionCtx.Event["issue"].(map[string]any) - if !ok { - return errors.New("unable to get issue on issue comment event") - } - - num, ok := issue["number"].(float64) - if !ok || num <= 0 { + prNum, ok := indexMap(actionCtx.Event, "issue", "number").(float64) + if !ok || prNum <= 0 { return errors.New("unable to get issue number on issue comment event") } @@ -172,7 +162,7 @@ func handleCommentUpdate(gh *client.GitHub) error { if checkboxes.ReplaceAllString(current, "") != checkboxes.ReplaceAllString(previous, "") { // If not, restore previous comment body if !gh.DryRun { - gh.SetBotComment(previous, int(num)) + gh.SetBotComment(previous, int(prNum)) } return errors.New("bot comment edited outside of checkboxes") } @@ -183,7 +173,7 @@ func handleCommentUpdate(gh *client.GitHub) error { edited := "" for key := range currentChecks { // If there is no diff for this check, ignore it - if currentChecks[key][0] == previousChecks[key][0] { + if currentChecks[key].status == previousChecks[key].status { continue } @@ -210,28 +200,29 @@ func handleCommentUpdate(gh *client.GitHub) error { if len(teams) > 0 { if gh.IsUserInTeams(actionCtx.Actor, teams) { if !gh.DryRun { - gh.SetBotComment(previous, int(num)) + gh.SetBotComment(previous, int(prNum)) } return errors.New("checkbox edited by a user not allowed to") } } // This regex capture only the line of the current check - specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key][0], key)) + specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key].status, key)) // If the box is checked, append the username of the user who checked it - if strings.TrimSpace(currentChecks[key][0]) == "x" { - replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key][0], key, actionCtx.Actor) + if strings.TrimSpace(currentChecks[key].status) == "x" { + replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key].status, key, actionCtx.Actor) edited = specificManualCheck.ReplaceAllString(current, replacement) - } else { // Else, remove the username of the user - replacement := fmt.Sprintf("- [%s] %s", currentChecks[key][0], key) + } else { + // Else, remove the username of the user + replacement := fmt.Sprintf("- [%s] %s", currentChecks[key].status, key) edited = specificManualCheck.ReplaceAllString(current, replacement) } } // Update comment with username if edited != "" && !gh.DryRun { - gh.SetBotComment(edited, int(num)) + gh.SetBotComment(edited, int(prNum)) gh.Logger.Debugf("Comment manual checks updated successfully") } diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/comment_test.go index 054c49177f9..9c0155ecf85 100644 --- a/contribs/github-bot/comment_test.go +++ b/contribs/github-bot/comment_test.go @@ -1,12 +1,18 @@ package main import ( + "context" "fmt" "regexp" "strings" "testing" + "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/utils" + "github.com/google/go-github/v64/github" + "github.com/migueleliasweb/go-github-mock/src/mock" + "github.com/sethvargo/go-githubactions" "github.com/stretchr/testify/assert" ) @@ -39,7 +45,6 @@ func TestGeneratedComment(t *testing.T) { content.AutoRules = autoRules commentText, err = generateComment(content) - fmt.Println(commentText) assert.Nil(t, err, fmt.Sprintf("error is not nil: %v", err)) assert.False(t, strings.Contains(commentText, "*No automated checks match this pull request.*"), "should not contains automated check placeholder") assert.True(t, strings.Contains(commentText, "*No manual checks match this pull request.*"), "should contains manual check placeholder") @@ -58,10 +63,138 @@ func TestGeneratedComment(t *testing.T) { val, ok := manualChecks[rule.Description] assert.True(t, ok, "manual check should exist") if rule.CheckedBy == "" { - assert.Equal(t, " ", val[0], "manual rule should not be checked") + assert.Equal(t, " ", val.status, "manual rule should not be checked") } else { - assert.Equal(t, "x", val[0], "manual rule should be checked") + assert.Equal(t, "x", val.status, "manual rule should be checked") } - assert.Equal(t, rule.CheckedBy, val[1], "invalid username found for CheckedBy") + assert.Equal(t, rule.CheckedBy, val.checkedBy, "invalid username found for CheckedBy") } } + +func TestIndexMap(t *testing.T) { + t.Parallel() + + m := map[string]any{ + "Key1": map[string]any{ + "Key2": map[string]any{ + "Key3": 1, + }, + }, + } + + test := indexMap(m) + assert.NotNil(t, test, "should return m") + _, ok := test.(map[string]any) + assert.True(t, ok, "returned m should be a map") + + test = indexMap(m, "Key1") + assert.NotNil(t, test, "should return Key1 value") + _, ok = test.(map[string]any) + assert.True(t, ok, "Key1 value type should be a map") + + test = indexMap(m, "Key1", "Key2") + assert.NotNil(t, test, "should return Key2 value") + _, ok = test.(map[string]any) + assert.True(t, ok, "Key2 value type should be a map") + + test = indexMap(m, "Key1", "Key2", "Key3") + assert.NotNil(t, test, "should return Key3 value") + val, ok := test.(int) + assert.True(t, ok, "Key3 value type should be an int") + assert.Equal(t, 1, val, "Key3 value should be a 1") + + test = indexMap(m, "Key1", "Key2", "Key3", "Key4") + assert.Nil(t, test, "Key4 value should not exist") +} + +func setValue(t *testing.T, m map[string]any, value any, keys ...string) map[string]any { + t.Helper() + + if len(keys) > 1 { + currMap, ok := m[keys[0]].(map[string]any) + if !ok { + currMap = map[string]any{} + } + m[keys[0]] = setValue(t, currMap, value, keys[1:]...) + } else if len(keys) == 1 { + m[keys[0]] = value + } + + return m +} + +func TestCommentUpdateHandler(t *testing.T) { + t.Parallel() + + const ( + user = "user" + bot = "bot" + ) + actionCtx := &githubactions.GitHubContext{ + Event: make(map[string]any), + } + + mockOptions := []mock.MockBackendOption{} + newGHClient := func() *client.GitHub { + return &client.GitHub{ + Client: github.NewClient(mock.NewMockedHTTPClient(mockOptions...)), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + } + gh := newGHClient() + + // Exit without error because EventName is empty + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.EventName = "issue_comment" + + // 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' + assert.NoError(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event["action"] = "deleted" + + // Exit with error because mock not setup to return authUser + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + mockOptions = append(mockOptions, mock.WithRequestMatchPages( + mock.EndpointPattern{ + Pattern: "/user", + Method: "GET", + }, + github.User{Login: github.String(bot)}, + )) + gh = newGHClient() + actionCtx.Actor = bot + + // 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 + 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 + 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 + 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 + 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 + assert.Error(t, handleCommentUpdate(gh, actionCtx)) + actionCtx.Event = setValue(t, actionCtx.Event, "current_body", "changes", "body", "from") + + assert.Nil(t, handleCommentUpdate(gh, actionCtx)) +} diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/params/params.go index 1bdf261a7b8..8925d0905cf 100644 --- a/contribs/github-bot/internal/params/params.go +++ b/contribs/github-bot/internal/params/params.go @@ -12,8 +12,8 @@ import ( type Params struct { Owner string Repo string - PrAll bool - PrNums PrList + PRAll bool + PRNums PRList Verbose bool DryRun bool Timeout time.Duration @@ -36,16 +36,16 @@ func (p *Params) RegisterFlags(fs *flag.FlagSet) { ) fs.BoolVar( - &p.PrAll, + &p.PRAll, "pr-all", false, "process all opened pull requests", ) fs.TextVar( - &p.PrNums, + &p.PRNums, "pr-numbers", - PrList(nil), + 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", ) @@ -82,13 +82,13 @@ func (p *Params) ValidateFlags() { } // Check if flags are coherent - if p.PrAll && len(p.PrNums) != 0 { + if p.PRAll && len(p.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 p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) { actionCtx, err := githubactions.Context() if err != nil { errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v", err)) @@ -104,7 +104,7 @@ func (p *Params) ValidateFlags() { 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(p.PRNums) == 0 && !p.PRAll { const errMsg = "Unable to retrieve pull request number from GitHub Actions context, you may want to set it using -pr-numbers flag" var num float64 @@ -131,7 +131,7 @@ func (p *Params) ValidateFlags() { errorUsage(errMsg) } - p.PrNums = PrList([]int{int(num)}) + p.PRNums = PRList([]int{int(num)}) } } } diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/params/prlist.go index 3b05590b0b2..028986dcbfd 100644 --- a/contribs/github-bot/internal/params/prlist.go +++ b/contribs/github-bot/internal/params/prlist.go @@ -7,16 +7,16 @@ import ( "strings" ) -type PrList []int +type PRList []int -// PrList is both a TextMarshaler and a TextUnmarshaler +// PRList is both a TextMarshaler and a TextUnmarshaler var ( - _ encoding.TextMarshaler = PrList{} - _ encoding.TextUnmarshaler = &PrList{} + _ encoding.TextMarshaler = PRList{} + _ encoding.TextUnmarshaler = &PRList{} ) // MarshalText implements encoding.TextMarshaler. -func (p PrList) MarshalText() (text []byte, err error) { +func (p PRList) MarshalText() (text []byte, err error) { prNumsStr := make([]string, len(p)) for i, prNum := range p { @@ -27,7 +27,7 @@ func (p PrList) MarshalText() (text []byte, err error) { } // UnmarshalText implements encoding.TextUnmarshaler. -func (p *PrList) UnmarshalText(text []byte) error { +func (p *PRList) UnmarshalText(text []byte) error { for _, prNumStr := range strings.Split(string(text), ",") { prNum, err := strconv.Atoi(strings.TrimSpace(prNumStr)) if err != nil { diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 32ab19eb4dd..45ad97ecf8c 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -13,7 +13,7 @@ func main() { cmd := commands.NewCommand( commands.Metadata{ - ShortUsage: "[flags]", + ShortUsage: "github-bot [flags]", ShortHelp: "checks requirements for a PR to be merged", 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.", }, From ecc6dc551a802bf3a3f16f6920c4a308ddbcfcb0 Mon Sep 17 00:00:00 2001 From: aeddi Date: Sun, 24 Nov 2024 23:26:00 +0900 Subject: [PATCH 40/44] refactor: merge regex --- contribs/github-bot/comment.go | 19 ++++++------------- 1 file changed, 6 insertions(+), 13 deletions(-) diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 29258170766..7cd9aafe90a 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -19,9 +19,7 @@ var errTriggeredByBot = errors.New("event triggered by bot") // Compile regex only once var ( // Regex for capturing the entire line of a manual check - manualCheckLine = regexp.MustCompile(`(?m:^- \[([ x])\] (.+)?$)`) - // Regex for capturing only the user who checked it - manualCheckStatus = regexp.MustCompile(`(?m:(.+) \(checked by @(\w+)\)$)`) + manualCheckLine = regexp.MustCompile(`(?m:^-\s\[([ xX])\]\s+(.+?)\s*(\(checked by @(\w+)\))?$)`) // Regex for capturing only the checkboxes checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) // Regex used to capture markdown links @@ -60,19 +58,14 @@ func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { // For each line that matches the "Manual check" regex for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { + description := match[2] status := match[1] - // Try to capture an occurrence of '(checked by @user)' - if details := manualCheckStatus.FindAllStringSubmatch(match[2], -1); len(details) > 0 { - // If found, set both the status and the user that checked the box - description := details[0][1] - checkedBy := details[0][2] - checks[description] = manualCheckDetails{status, checkedBy} - continue + checkedBy := "" + if len(match) > 4 { + checkedBy = strings.ToLower(match[4]) // if X captured, convert it to x } - // If not found, set only the status of the box - description := match[2] - checks[description] = manualCheckDetails{status: status} + checks[description] = manualCheckDetails{status: status, checkedBy: checkedBy} } return checks From f1ec2778001c2a58b3d7ff5eac3a87d9301c5a77 Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 25 Nov 2024 10:49:59 +0900 Subject: [PATCH 41/44] refactor: alias author --- .../internal/requirements/author.go | 35 ++++--------------- 1 file changed, 7 insertions(+), 28 deletions(-) diff --git a/contribs/github-bot/internal/requirements/author.go b/contribs/github-bot/internal/requirements/author.go index b7bd7cd9411..c60139ba192 100644 --- a/contribs/github-bot/internal/requirements/author.go +++ b/contribs/github-bot/internal/requirements/author.go @@ -1,10 +1,8 @@ package requirements import ( - "fmt" - "github.com/gnolang/gno/contribs/github-bot/internal/client" - "github.com/gnolang/gno/contribs/github-bot/internal/utils" + "github.com/gnolang/gno/contribs/github-bot/internal/conditions" "github.com/google/go-github/v64/github" "github.com/xlab/treeprint" @@ -12,49 +10,30 @@ import ( // Author Requirement type author struct { - user string + c conditions.Condition // Alias Author requirement to identical condition } var _ Requirement = &author{} func (a *author) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { - return utils.AddStatusNode( - a.user == pr.GetUser().GetLogin(), - fmt.Sprintf("Pull request author is user: %v", a.user), - details, - ) + return a.c.IsMet(pr, details) } func Author(user string) Requirement { - return &author{user: user} + return &author{conditions.Author(user)} } // AuthorInTeam Requirement type authorInTeam struct { - gh *client.GitHub - team string + c conditions.Condition // Alias AuthorInTeam requirement to identical condition } var _ Requirement = &authorInTeam{} func (a *authorInTeam) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { - detail := fmt.Sprintf("Pull request author is a member of the team: %s", a.team) - - teamMembers, err := a.gh.ListTeamMembers(a.team) - if err != nil { - a.gh.Logger.Errorf("unable to check if author is in team %s: %v", a.team, err) - return utils.AddStatusNode(false, detail, details) - } - - for _, member := range teamMembers { - if member.GetLogin() == pr.GetUser().GetLogin() { - return utils.AddStatusNode(true, detail, details) - } - } - - return utils.AddStatusNode(false, detail, details) + return a.c.IsMet(pr, details) } func AuthorInTeam(gh *client.GitHub, team string) Requirement { - return &authorInTeam{gh: gh, team: team} + return &authorInTeam{conditions.AuthorInTeam(gh, team)} } From d19edbecc7dd0a7f129e235ce6209268e2f37554 Mon Sep 17 00:00:00 2001 From: aeddi Date: Mon, 25 Nov 2024 12:12:25 +0900 Subject: [PATCH 42/44] docs: document GitHub object + consistency --- contribs/github-bot/bot.go | 40 +++++----- contribs/github-bot/comment.go | 74 +++++++++---------- contribs/github-bot/config.go | 16 ++-- contribs/github-bot/internal/client/client.go | 17 ++++- .../internal/conditions/assignee.go | 4 +- .../github-bot/internal/conditions/author.go | 4 +- .../github-bot/internal/conditions/boolean.go | 10 +-- .../github-bot/internal/conditions/branch.go | 4 +- .../internal/conditions/condition.go | 4 +- .../internal/conditions/constant.go | 4 +- .../github-bot/internal/conditions/file.go | 2 +- .../github-bot/internal/conditions/label.go | 2 +- contribs/github-bot/internal/logger/logger.go | 18 ++--- .../github-bot/internal/logger/terminal.go | 14 ++-- contribs/github-bot/internal/params/params.go | 6 +- contribs/github-bot/internal/params/prlist.go | 2 +- .../internal/requirements/assignee.go | 8 +- .../internal/requirements/author.go | 8 +- .../internal/requirements/boolean.go | 10 +-- .../internal/requirements/branch.go | 4 +- .../internal/requirements/constant.go | 4 +- .../github-bot/internal/requirements/label.go | 8 +- .../internal/requirements/maintainer.go | 2 +- .../internal/requirements/requirement.go | 2 +- .../internal/requirements/reviewer.go | 12 +-- 25 files changed, 147 insertions(+), 132 deletions(-) diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/bot.go index 6264a0d8b61..1dda6b5e2dd 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/bot.go @@ -18,7 +18,7 @@ import ( ) func execBot(params *p.Params) error { - // Create context with timeout if specified in the parameters + // Create context with timeout if specified in the parameters. ctx := context.Background() if params.Timeout > 0 { var cancel context.CancelFunc @@ -26,30 +26,30 @@ func execBot(params *p.Params) error { defer cancel() } - // Init GitHub API client + // Init GitHub API client. gh, err := client.New(ctx, params) if err != nil { return fmt.Errorf("comment update handling failed: %w", err) } - // Get GitHub Actions context to retrieve comment update + // Get GitHub Actions context to retrieve comment update. actionCtx, err := githubactions.Context() if err != nil { gh.Logger.Debugf("Unable to retrieve GitHub Actions context: %v", err) return nil } - // Handle comment update, if any + // Handle comment update, if any. if err := handleCommentUpdate(gh, actionCtx); errors.Is(err, errTriggeredByBot) { - return nil // Ignore if this run was triggered by a previous run + return nil // Ignore if this run was triggered by a previous run. } else if err != nil { return fmt.Errorf("comment update handling failed: %w", err) } - // Retrieve a slice of pull requests to process + // Retrieve a slice of pull requests to process. var prs []*github.PullRequest - // If requested, retrieve all open pull requests + // If requested, retrieve all open pull requests. if params.PRAll { opts := &github.PullRequestListOptions{ State: "open", @@ -75,7 +75,7 @@ func execBot(params *p.Params) error { } } else { // Otherwise, retrieve only specified pull request(s) - // (flag or GitHub Action context) + // (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) @@ -95,14 +95,14 @@ func execBot(params *p.Params) error { gh.Logger.Infof("%d pull requests to process: %v\n", len(prNums), prNums) } - // Process all pull requests in parallel + // Process all pull requests in parallel. autoRules, manualRules := config(gh) var wg sync.WaitGroup - // Used in dry-run mode to log cleanly from different goroutines + // Used in dry-run mode to log cleanly from different goroutines. logMutex := sync.Mutex{} - // Used in regular-run mode to return an error if one PR processing failed + // Used in regular-run mode to return an error if one PR processing failed. var failed atomic.Bool for _, pr := range prs { @@ -112,11 +112,11 @@ func execBot(params *p.Params) error { commentContent := CommentContent{} commentContent.allSatisfied = true - // Iterate over all automatic rules in config + // Iterate over all automatic rules in config. for _, autoRule := range autoRules { ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success)) - // Check if conditions of this rule are met by this PR + // Check if conditions of this rule are met by this PR. if !autoRule.ifC.IsMet(pr, ifDetails) { continue } @@ -124,7 +124,7 @@ func execBot(params *p.Params) error { 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 + // Check if requirements of this rule are satisfied by this PR. if autoRule.thenR.IsSatisfied(pr, thenDetails) { thenDetails.SetValue(fmt.Sprintf("%s Requirement satisfied", utils.Success)) c.Satisfied = true @@ -137,22 +137,22 @@ func execBot(params *p.Params) error { commentContent.AutoRules = append(commentContent.AutoRules, c) } - // Retrieve manual check states + // Retrieve manual check states. checks := make(map[string]manualCheckDetails) if comment, err := gh.GetBotComment(pr.GetNumber()); err == nil { checks = getCommentManualChecks(comment.GetBody()) } - // Iterate over all manual rules in config + // Iterate over all manual rules in config. for _, manualRule := range manualRules { ifDetails := treeprint.NewWithRoot(fmt.Sprintf("%s Condition met", utils.Success)) - // Check if conditions of this rule are met by this PR + // Check if conditions of this rule are met by this PR. if !manualRule.ifC.IsMet(pr, ifDetails) { continue } - // Get check status from current comment, if any + // Get check status from current comment, if any. checkedBy := "" check, ok := checks[manualRule.description] if ok { @@ -174,7 +174,7 @@ func execBot(params *p.Params) error { } } - // Logs results or write them in bot PR comment + // Logs results or write them in bot PR comment. if gh.DryRun { logMutex.Lock() logResults(gh.Logger, pr.GetNumber(), commentContent) @@ -197,7 +197,7 @@ func execBot(params *p.Params) error { } // logResults is called in dry-run mode and outputs the status of each check -// and a conclusion +// and a conclusion. func logResults(logger logger.Logger, prNum int, commentContent CommentContent) { logger.Infof("Pull request #%d requirements", prNum) if len(commentContent.AutoRules) > 0 { diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 7cd9aafe90a..7ed22a63b35 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -16,18 +16,18 @@ import ( var errTriggeredByBot = errors.New("event triggered by bot") -// Compile regex only once +// Compile regex only once. var ( - // Regex for capturing the entire line of a manual check + // Regex for capturing the entire line of a manual check. manualCheckLine = regexp.MustCompile(`(?m:^-\s\[([ xX])\]\s+(.+?)\s*(\(checked by @(\w+)\))?$)`) - // Regex for capturing only the checkboxes + // Regex for capturing only the checkboxes. checkboxes = regexp.MustCompile(`(?m:^- \[[ x]\])`) - // Regex used to capture markdown links + // Regex used to capture markdown links. markdownLink = regexp.MustCompile(`\[(.*)\]\(.*\)`) ) // These structures contain the necessary information to generate -// the bot's comment from the template file +// the bot's comment from the template file. type AutoContent struct { Description string Satisfied bool @@ -52,17 +52,17 @@ type manualCheckDetails struct { } // getCommentManualChecks parses the bot comment to get the checkbox status, -// the check description and the username who checked it +// the check description and the username who checked it. func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { checks := make(map[string]manualCheckDetails) - // For each line that matches the "Manual check" regex + // For each line that matches the "Manual check" regex. for _, match := range manualCheckLine.FindAllStringSubmatch(commentBody, -1) { description := match[2] status := match[1] checkedBy := "" if len(match) > 4 { - checkedBy = strings.ToLower(match[4]) // if X captured, convert it to x + checkedBy = strings.ToLower(match[4]) // if X captured, convert it to x. } checks[description] = manualCheckDetails{status: status, checkedBy: checkedBy} @@ -71,7 +71,7 @@ func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { return checks } -// Recursively search for nested values using the keys provided +// Recursively search for nested values using the keys provided. func indexMap(m map[string]any, keys ...string) any { if len(keys) == 0 { return m @@ -95,13 +95,13 @@ func indexMap(m map[string]any, keys ...string) any { // - the comment change is only a checkbox being checked or unckecked (or restore it) // - the actor / comment editor has permission to modify this checkbox (or restore it) func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubContext) error { - // Ignore if it's not a comment related event + // Ignore if it's not a comment related event. if actionCtx.EventName != "issue_comment" { gh.Logger.Debugf("Event is not issue comment related (%s)", actionCtx.EventName) return nil } - // Ignore if the action type is not deleted or edited + // Ignore if the action type is not deleted or edited. actionType, ok := actionCtx.Event["action"].(string) if !ok { return errors.New("unable to get type on issue comment event") @@ -111,7 +111,7 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte return nil } - // Return if comment was edited by bot (current authenticated user) + // Return if comment was edited by bot (current authenticated user). authUser, _, err := gh.Client.Users.Get(gh.Ctx, "") if err != nil { return fmt.Errorf("unable to get authenticated user: %w", err) @@ -122,55 +122,55 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte return errTriggeredByBot } - // Get login of the author of the edited comment + // Get login of the author of the edited comment. login, ok := indexMap(actionCtx.Event, "comment", "user", "login").(string) if !ok { return errors.New("unable to get comment user login on issue comment event") } - // If the author is not the bot, return + // If the author is not the bot, return. if login != authUser.GetLogin() { return nil } - // Get comment updated body + // Get comment updated body. current, ok := indexMap(actionCtx.Event, "comment", "body").(string) if !ok { return errors.New("unable to get comment body on issue comment event") } - // Get comment previous body + // Get comment previous body. previous, ok := indexMap(actionCtx.Event, "changes", "body", "from").(string) if !ok { return errors.New("unable to get changes body content on issue comment event") } - // Get PR number from GitHub Actions context + // Get PR number from GitHub Actions context. prNum, ok := 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 + // 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 not, restore previous comment body. if !gh.DryRun { gh.SetBotComment(previous, int(prNum)) } return errors.New("bot comment edited outside of checkboxes") } - // Check if actor / comment editor has permission to modify changed boxes + // Check if actor / comment editor has permission to modify changed boxes. currentChecks := getCommentManualChecks(current) previousChecks := getCommentManualChecks(previous) edited := "" for key := range currentChecks { - // If there is no diff for this check, ignore it + // If there is no diff for this check, ignore it. if currentChecks[key].status == previousChecks[key].status { continue } - // Get teams allowed to edit this box from config + // Get teams allowed to edit this box from config. var teams []string found := false _, manualRules := config(gh) @@ -183,13 +183,13 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte } // If rule were not found, return to reprocess the bot comment entirely - // (maybe bot config was updated since last run?) + // (maybe bot config was updated since last run?). if !found { gh.Logger.Debugf("Updated rule not found in config: %s", key) return nil } - // If teams specified in rule, check if actor is a member of one of them + // 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 !gh.DryRun { @@ -199,21 +199,21 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte } } - // This regex capture only the line of the current check + // This regex capture only the line of the current check. specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key].status, key)) - // If the box is checked, append the username of the user who checked it + // If the box is checked, append the username of the user who checked it. if strings.TrimSpace(currentChecks[key].status) == "x" { replacement := fmt.Sprintf("- [%s] %s (checked by @%s)", currentChecks[key].status, key, actionCtx.Actor) edited = specificManualCheck.ReplaceAllString(current, replacement) } else { - // Else, remove the username of the user + // Else, remove the username of the user. replacement := fmt.Sprintf("- [%s] %s", currentChecks[key].status, key) edited = specificManualCheck.ReplaceAllString(current, replacement) } } - // Update comment with username + // Update comment with username. if edited != "" && !gh.DryRun { gh.SetBotComment(edited, int(prNum)) gh.Logger.Debugf("Comment manual checks updated successfully") @@ -223,23 +223,23 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte } // generateComment generates a comment using the template file and the -// content passed as parameter +// content passed as parameter. func generateComment(content CommentContent) (string, error) { - // Custom function to strip markdown links + // Custom function to strip markdown links. funcMap := template.FuncMap{ "stripLinks": func(input string) string { return markdownLink.ReplaceAllString(input, "$1") }, } - // Bind markdown stripping function to template generator + // Bind markdown stripping function to template generator. const tmplFile = "comment.tmpl" tmpl, err := template.New(tmplFile).Funcs(funcMap).ParseFiles(tmplFile) if err != nil { return "", fmt.Errorf("unable to init template: %w", err) } - // Generate bot comment using template file + // Generate bot comment using template file. var commentBytes bytes.Buffer if err := tmpl.Execute(&commentBytes, content); err != nil { return "", fmt.Errorf("unable to execute template: %w", err) @@ -248,15 +248,15 @@ func generateComment(content CommentContent) (string, error) { return commentBytes.String(), nil } -// updatePullRequest updates or creates both the bot comment and the commit status +// updatePullRequest updates or creates both the bot comment and the commit status. func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content CommentContent) error { - // Generate comment text content + // Generate comment text content. commentText, err := generateComment(content) if err != nil { return fmt.Errorf("unable to generate comment on PR %d: %w", pr.GetNumber(), err) } - // Update comment on pull request + // Update comment on pull request. comment, err := gh.SetBotComment(commentText, pr.GetNumber()) if err != nil { return fmt.Errorf("unable to update comment on PR %d: %w", pr.GetNumber(), err) @@ -264,7 +264,7 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber()) } - // Prepare commit status content + // Prepare commit status content. var ( context = "Merge Requirements" targetURL = comment.GetHTMLURL() @@ -277,7 +277,7 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen description = "All requirements are satisfied." } - // Update or create commit status + // Update or create commit status. if _, _, err := gh.Client.Repositories.CreateStatus( gh.Ctx, gh.Owner, diff --git a/contribs/github-bot/config.go b/contribs/github-bot/config.go index 8a2f2e3c12e..4504844e289 100644 --- a/contribs/github-bot/config.go +++ b/contribs/github-bot/config.go @@ -6,22 +6,22 @@ import ( r "github.com/gnolang/gno/contribs/github-bot/internal/requirements" ) -// Automatic check that will be performed by the bot +// 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 + 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. } -// Manual check that will be performed by users +// 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 []string // Members of these teams can check the checkbox to make the check pass + ifC c.Condition // If the condition is met, a checkbox will be displayed on bot comment. + teams []string // 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 +// in which the GitHub client is injected. func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { auto := []automaticCheck{ { @@ -87,7 +87,7 @@ func config(gh *client.GitHub) ([]automaticCheck, []manualCheck) { }, } - // Check for duplicates in manual rule descriptions (needs to be unique for the bot operations) + // 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 { diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go index 19f96339233..2aec5c8681f 100644 --- a/contribs/github-bot/internal/client/client.go +++ b/contribs/github-bot/internal/client/client.go @@ -12,11 +12,17 @@ import ( "github.com/google/go-github/v64/github" ) -// PageSize is the number of items to load for each iteration when fetching a list +// PageSize is the number of items to load for each iteration when fetching a list. const PageSize = 100 var ErrBotCommentNotFound = errors.New("bot comment not found") +// GitHub contains everything necessary to interact with the GitHub API, +// including the client, a context (which must be passed with each request), +// a logger, etc. This object will be passed to each condition or requirement +// that requires fetching additional information or modifying things on GitHub. +// The object also provides methods for performing more complex operations than +// a simple API call. type GitHub struct { Client *github.Client Ctx context.Context @@ -26,6 +32,7 @@ type GitHub struct { Repo string } +// GetBotComment retrieves the bot's (current user) comment on provided PR number. func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { // List existing comments const ( @@ -76,6 +83,8 @@ func (gh *GitHub) GetBotComment(prNum int) (*github.IssueComment, error) { return nil, errors.New("bot comment not found") } +// 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 comment, err := gh.GetBotComment(prNum) @@ -110,6 +119,7 @@ func (gh *GitHub) SetBotComment(body string, prNum int) (*github.IssueComment, e return editComment, nil } +// ListTeamMembers lists the members of the specified team. func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) { var ( allMembers []*github.User @@ -142,6 +152,8 @@ func (gh *GitHub) ListTeamMembers(team string) ([]*github.User, error) { return allMembers, nil } +// IsUserInTeams checks if the specified user is a member of any of the +// provided teams. func (gh *GitHub) IsUserInTeams(user string, teams []string) bool { for _, team := range teams { teamMembers, err := gh.ListTeamMembers(team) @@ -160,6 +172,7 @@ func (gh *GitHub) IsUserInTeams(user string, teams []string) bool { return false } +// ListPRReviewers returns the list of reviewers for the specified PR number. func (gh *GitHub) ListPRReviewers(prNum int) (*github.Reviewers, error) { var ( allReviewers = &github.Reviewers{} @@ -192,6 +205,7 @@ func (gh *GitHub) ListPRReviewers(prNum int) (*github.Reviewers, error) { return allReviewers, nil } +// ListPRReviewers returns the list of reviews for the specified PR number. func (gh *GitHub) ListPRReviews(prNum int) ([]*github.PullRequestReview, error) { var ( allReviews []*github.PullRequestReview @@ -223,6 +237,7 @@ func (gh *GitHub) ListPRReviews(prNum int) ([]*github.PullRequestReview, error) return allReviews, nil } +// New initializes the API client, the logger, and creates an instance of GitHub. func New(ctx context.Context, params *p.Params) (*GitHub, error) { gh := &GitHub{ Ctx: ctx, diff --git a/contribs/github-bot/internal/conditions/assignee.go b/contribs/github-bot/internal/conditions/assignee.go index 9682e224f11..7024259909c 100644 --- a/contribs/github-bot/internal/conditions/assignee.go +++ b/contribs/github-bot/internal/conditions/assignee.go @@ -10,7 +10,7 @@ import ( "github.com/xlab/treeprint" ) -// Assignee Condition +// Assignee Condition. type assignee struct { user string } @@ -33,7 +33,7 @@ func Assignee(user string) Condition { return &assignee{user: user} } -// AssigneeInTeam Condition +// AssigneeInTeam Condition. type assigneeInTeam struct { gh *client.GitHub team string diff --git a/contribs/github-bot/internal/conditions/author.go b/contribs/github-bot/internal/conditions/author.go index e5079b9e4f4..9052f781bd5 100644 --- a/contribs/github-bot/internal/conditions/author.go +++ b/contribs/github-bot/internal/conditions/author.go @@ -10,7 +10,7 @@ import ( "github.com/xlab/treeprint" ) -// Author Condition +// Author Condition. type author struct { user string } @@ -29,7 +29,7 @@ func Author(user string) Condition { return &author{user: user} } -// AuthorInTeam Condition +// AuthorInTeam Condition. type authorInTeam struct { gh *client.GitHub team string diff --git a/contribs/github-bot/internal/conditions/boolean.go b/contribs/github-bot/internal/conditions/boolean.go index e030563709a..2fa3a25f7ac 100644 --- a/contribs/github-bot/internal/conditions/boolean.go +++ b/contribs/github-bot/internal/conditions/boolean.go @@ -9,7 +9,7 @@ import ( "github.com/xlab/treeprint" ) -// And Condition +// And Condition. type and struct { conditions []Condition } @@ -24,7 +24,7 @@ func (a *and) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { if !condition.IsMet(pr, branch) { met = utils.Fail // We don't break here because we need to call IsMet on all conditions - // to populate the details tree + // to populate the details tree. } } @@ -41,7 +41,7 @@ func And(conditions ...Condition) Condition { return &and{conditions} } -// Or Condition +// Or Condition. type or struct { conditions []Condition } @@ -56,7 +56,7 @@ func (o *or) IsMet(pr *github.PullRequest, details treeprint.Tree) bool { if condition.IsMet(pr, branch) { met = utils.Success // We don't break here because we need to call IsMet on all conditions - // to populate the details tree + // to populate the details tree. } } @@ -73,7 +73,7 @@ func Or(conditions ...Condition) Condition { return &or{conditions} } -// Not Condition +// Not Condition. type not struct { cond Condition } diff --git a/contribs/github-bot/internal/conditions/branch.go b/contribs/github-bot/internal/conditions/branch.go index ef29e2d39cb..6977d633d98 100644 --- a/contribs/github-bot/internal/conditions/branch.go +++ b/contribs/github-bot/internal/conditions/branch.go @@ -10,7 +10,7 @@ import ( "github.com/xlab/treeprint" ) -// BaseBranch Condition +// BaseBranch Condition. type baseBranch struct { pattern *regexp.Regexp } @@ -29,7 +29,7 @@ func BaseBranch(pattern string) Condition { return &baseBranch{pattern: regexp.MustCompile(pattern)} } -// HeadBranch Condition +// HeadBranch Condition. type headBranch struct { pattern *regexp.Regexp } diff --git a/contribs/github-bot/internal/conditions/condition.go b/contribs/github-bot/internal/conditions/condition.go index afc436b8209..8c2fa5a2948 100644 --- a/contribs/github-bot/internal/conditions/condition.go +++ b/contribs/github-bot/internal/conditions/condition.go @@ -6,7 +6,7 @@ import ( ) type Condition interface { - // Check if the Condition is met and add the detail - // to the tree passed as a parameter + // Check if the Condition is met and add the details + // to the tree passed as a parameter. IsMet(pr *github.PullRequest, details treeprint.Tree) bool } diff --git a/contribs/github-bot/internal/conditions/constant.go b/contribs/github-bot/internal/conditions/constant.go index d00af13dca2..26bbe9e8110 100644 --- a/contribs/github-bot/internal/conditions/constant.go +++ b/contribs/github-bot/internal/conditions/constant.go @@ -7,7 +7,7 @@ import ( "github.com/xlab/treeprint" ) -// Always Condition +// Always Condition. type always struct{} var _ Condition = &always{} @@ -20,7 +20,7 @@ func Always() Condition { return &always{} } -// Never Condition +// Never Condition. type never struct{} var _ Condition = &never{} diff --git a/contribs/github-bot/internal/conditions/file.go b/contribs/github-bot/internal/conditions/file.go index c51a13c39cd..e3854a7734a 100644 --- a/contribs/github-bot/internal/conditions/file.go +++ b/contribs/github-bot/internal/conditions/file.go @@ -11,7 +11,7 @@ import ( "github.com/xlab/treeprint" ) -// FileChanged Condition +// FileChanged Condition. type fileChanged struct { gh *client.GitHub pattern *regexp.Regexp diff --git a/contribs/github-bot/internal/conditions/label.go b/contribs/github-bot/internal/conditions/label.go index de3844b9747..ace94ed436c 100644 --- a/contribs/github-bot/internal/conditions/label.go +++ b/contribs/github-bot/internal/conditions/label.go @@ -10,7 +10,7 @@ import ( "github.com/xlab/treeprint" ) -// Label Condition +// Label Condition. type label struct { pattern *regexp.Regexp } diff --git a/contribs/github-bot/internal/logger/logger.go b/contribs/github-bot/internal/logger/logger.go index 2139fe07584..570ca027e5c 100644 --- a/contribs/github-bot/internal/logger/logger.go +++ b/contribs/github-bot/internal/logger/logger.go @@ -4,28 +4,28 @@ import ( "os" ) -// All Logger methods follow the standard fmt.Printf convention +// All Logger methods follow the standard fmt.Printf convention. type Logger interface { - // Debugf prints a debug-level message + // Debugf prints a debug-level message. Debugf(msg string, args ...any) - // Noticef prints a notice-level message + // Noticef prints a notice-level message. Noticef(msg string, args ...any) - // Warningf prints a warning-level message + // Warningf prints a warning-level message. Warningf(msg string, args ...any) - // Errorf prints a error-level message + // Errorf prints a error-level message. Errorf(msg string, args ...any) - // Fatalf prints a error-level message and exits + // Fatalf prints a error-level message and exits. Fatalf(msg string, args ...any) - // Infof prints message to stdout without any level annotations + // Infof prints message to stdout without any level annotations. Infof(msg string, args ...any) } -// Returns a logger suitable for Github Actions or terminal output +// Returns a logger suitable for Github Actions or terminal output. func NewLogger(verbose bool) Logger { if _, isAction := os.LookupEnv("GITHUB_ACTION"); isAction { return newActionLogger() @@ -34,7 +34,7 @@ func NewLogger(verbose bool) Logger { return newTermLogger(verbose) } -// NewNoopLogger returns a logger that does not log anything +// NewNoopLogger returns a logger that does not log anything. func NewNoopLogger() Logger { return newNoopLogger() } diff --git a/contribs/github-bot/internal/logger/terminal.go b/contribs/github-bot/internal/logger/terminal.go index cc12022011a..d0e5671a3c8 100644 --- a/contribs/github-bot/internal/logger/terminal.go +++ b/contribs/github-bot/internal/logger/terminal.go @@ -10,37 +10,37 @@ type termLogger struct{} var _ Logger = &termLogger{} -// Debugf implements Logger +// Debugf implements Logger. func (s *termLogger) Debugf(msg string, args ...any) { msg = fmt.Sprintf("%s\n", msg) slog.Debug(fmt.Sprintf(msg, args...)) } -// Errorf implements Logger +// Errorf implements Logger. func (s *termLogger) Errorf(msg string, args ...any) { msg = fmt.Sprintf("%s\n", msg) slog.Error(fmt.Sprintf(msg, args...)) } -// Fatalf implements Logger +// Fatalf implements Logger. func (s *termLogger) Fatalf(msg string, args ...any) { s.Errorf(msg, args...) os.Exit(1) } -// Infof implements Logger +// Infof implements Logger. func (s *termLogger) Infof(msg string, args ...any) { msg = fmt.Sprintf("%s\n", msg) slog.Info(fmt.Sprintf(msg, args...)) } -// Noticef implements Logger +// Noticef implements Logger. func (s *termLogger) Noticef(msg string, args ...any) { - // Alias to info on terminal since notice level only exists on GitHub Actions + // Alias to info on terminal since notice level only exists on GitHub Actions. s.Infof(msg, args...) } -// Warningf implements Logger +// Warningf implements Logger. func (s *termLogger) Warningf(msg string, args ...any) { msg = fmt.Sprintf("%s\n", msg) slog.Warn(fmt.Sprintf(msg, args...)) diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/params/params.go index 8925d0905cf..3a76e0c7684 100644 --- a/contribs/github-bot/internal/params/params.go +++ b/contribs/github-bot/internal/params/params.go @@ -74,20 +74,20 @@ func (p *Params) RegisterFlags(fs *flag.FlagSet) { } func (p *Params) ValidateFlags() { - // Helper to display an error + usage message before exiting + // 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() os.Exit(1) } - // Check if flags are coherent + // Check if flags are coherent. if p.PRAll && len(p.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 + // from GitHub Actions context. if p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) { actionCtx, err := githubactions.Context() if err != nil { diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/params/prlist.go index 028986dcbfd..5e51181452f 100644 --- a/contribs/github-bot/internal/params/prlist.go +++ b/contribs/github-bot/internal/params/prlist.go @@ -9,7 +9,7 @@ import ( type PRList []int -// PRList is both a TextMarshaler and a TextUnmarshaler +// PRList is both a TextMarshaler and a TextUnmarshaler. var ( _ encoding.TextMarshaler = PRList{} _ encoding.TextUnmarshaler = &PRList{} diff --git a/contribs/github-bot/internal/requirements/assignee.go b/contribs/github-bot/internal/requirements/assignee.go index c48a63c34db..9a2723ad18f 100644 --- a/contribs/github-bot/internal/requirements/assignee.go +++ b/contribs/github-bot/internal/requirements/assignee.go @@ -10,7 +10,7 @@ import ( "github.com/xlab/treeprint" ) -// Assignee Requirement +// Assignee Requirement. type assignee struct { gh *client.GitHub user string @@ -21,19 +21,19 @@ var _ Requirement = &assignee{} func (a *assignee) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { detail := fmt.Sprintf("This user is assigned to pull request: %s", a.user) - // Check if user was already assigned to PR + // Check if user was already assigned to PR. for _, assignee := range pr.Assignees { if a.user == assignee.GetLogin() { return utils.AddStatusNode(true, detail, details) } } - // If in a dry run, skip assigning the user + // If in a dry run, skip assigning the user. if a.gh.DryRun { return utils.AddStatusNode(false, detail, details) } - // If user not already assigned, assign it + // If user not already assigned, assign it. if _, _, err := a.gh.Client.Issues.AddAssignees( a.gh.Ctx, a.gh.Owner, diff --git a/contribs/github-bot/internal/requirements/author.go b/contribs/github-bot/internal/requirements/author.go index c60139ba192..eed2c510b97 100644 --- a/contribs/github-bot/internal/requirements/author.go +++ b/contribs/github-bot/internal/requirements/author.go @@ -8,9 +8,9 @@ import ( "github.com/xlab/treeprint" ) -// Author Requirement +// Author Requirement. type author struct { - c conditions.Condition // Alias Author requirement to identical condition + c conditions.Condition // Alias Author requirement to identical condition. } var _ Requirement = &author{} @@ -23,9 +23,9 @@ func Author(user string) Requirement { return &author{conditions.Author(user)} } -// AuthorInTeam Requirement +// AuthorInTeam Requirement. type authorInTeam struct { - c conditions.Condition // Alias AuthorInTeam requirement to identical condition + c conditions.Condition // Alias AuthorInTeam requirement to identical condition. } var _ Requirement = &authorInTeam{} diff --git a/contribs/github-bot/internal/requirements/boolean.go b/contribs/github-bot/internal/requirements/boolean.go index 1b6840f6aa9..6b441c92f80 100644 --- a/contribs/github-bot/internal/requirements/boolean.go +++ b/contribs/github-bot/internal/requirements/boolean.go @@ -9,7 +9,7 @@ import ( "github.com/xlab/treeprint" ) -// And Requirement +// And Requirement. type and struct { requirements []Requirement } @@ -24,7 +24,7 @@ func (a *and) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { if !requirement.IsSatisfied(pr, branch) { satisfied = utils.Fail // We don't break here because we need to call IsSatisfied on all - // requirements to populate the details tree + // requirements to populate the details tree. } } @@ -41,7 +41,7 @@ func And(requirements ...Requirement) Requirement { return &and{requirements} } -// Or Requirement +// Or Requirement. type or struct { requirements []Requirement } @@ -56,7 +56,7 @@ func (o *or) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { if requirement.IsSatisfied(pr, branch) { satisfied = utils.Success // We don't break here because we need to call IsSatisfied on all - // requirements to populate the details tree + // requirements to populate the details tree. } } @@ -73,7 +73,7 @@ func Or(requirements ...Requirement) Requirement { return &or{requirements} } -// Not Requirement +// Not Requirement. type not struct { req Requirement } diff --git a/contribs/github-bot/internal/requirements/branch.go b/contribs/github-bot/internal/requirements/branch.go index bd5a1e3ea89..65d00d06ae8 100644 --- a/contribs/github-bot/internal/requirements/branch.go +++ b/contribs/github-bot/internal/requirements/branch.go @@ -11,10 +11,10 @@ import ( ) // Pass this to UpToDateWith constructor to check the PR head branch -// against its base branch +// against its base branch. const PR_BASE = "PR_BASE" -// UpToDateWith Requirement +// UpToDateWith Requirement. type upToDateWith struct { gh *client.GitHub base string diff --git a/contribs/github-bot/internal/requirements/constant.go b/contribs/github-bot/internal/requirements/constant.go index 5ab33c2573a..cbe932da830 100644 --- a/contribs/github-bot/internal/requirements/constant.go +++ b/contribs/github-bot/internal/requirements/constant.go @@ -7,7 +7,7 @@ import ( "github.com/xlab/treeprint" ) -// Always Requirement +// Always Requirement. type always struct{} var _ Requirement = &always{} @@ -20,7 +20,7 @@ func Always() Requirement { return &always{} } -// Never Requirement +// Never Requirement. type never struct{} var _ Requirement = &never{} diff --git a/contribs/github-bot/internal/requirements/label.go b/contribs/github-bot/internal/requirements/label.go index 7963c47b0cd..d1ee475db92 100644 --- a/contribs/github-bot/internal/requirements/label.go +++ b/contribs/github-bot/internal/requirements/label.go @@ -10,7 +10,7 @@ import ( "github.com/xlab/treeprint" ) -// Label Requirement +// Label Requirement. type label struct { gh *client.GitHub name string @@ -21,19 +21,19 @@ var _ Requirement = &label{} func (l *label) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { detail := fmt.Sprintf("This label is applied to pull request: %s", l.name) - // Check if label was already applied to PR + // Check if label was already applied to PR. for _, label := range pr.Labels { if l.name == label.GetName() { return utils.AddStatusNode(true, detail, details) } } - // If in a dry run, skip applying the label + // If in a dry run, skip applying the label. if l.gh.DryRun { return utils.AddStatusNode(false, detail, details) } - // If label not already applied, apply it + // If label not already applied, apply it. if _, _, err := l.gh.Client.Issues.AddLabelsToIssue( l.gh.Ctx, l.gh.Owner, diff --git a/contribs/github-bot/internal/requirements/maintainer.go b/contribs/github-bot/internal/requirements/maintainer.go index 261f415f852..8e3f356bebf 100644 --- a/contribs/github-bot/internal/requirements/maintainer.go +++ b/contribs/github-bot/internal/requirements/maintainer.go @@ -7,7 +7,7 @@ import ( "github.com/xlab/treeprint" ) -// MaintainerCanModify Requirement +// MaintainerCanModify Requirement. type maintainerCanModify struct{} var _ Requirement = &maintainerCanModify{} diff --git a/contribs/github-bot/internal/requirements/requirement.go b/contribs/github-bot/internal/requirements/requirement.go index a83646c1428..296c4a1461d 100644 --- a/contribs/github-bot/internal/requirements/requirement.go +++ b/contribs/github-bot/internal/requirements/requirement.go @@ -7,6 +7,6 @@ import ( type Requirement interface { // Check if the Requirement is satisfied and add the detail - // to the tree passed as a parameter + // to the tree passed as a parameter. IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool } diff --git a/contribs/github-bot/internal/requirements/reviewer.go b/contribs/github-bot/internal/requirements/reviewer.go index 174c4b6208c..aa3914d4c4a 100644 --- a/contribs/github-bot/internal/requirements/reviewer.go +++ b/contribs/github-bot/internal/requirements/reviewer.go @@ -10,7 +10,7 @@ import ( "github.com/xlab/treeprint" ) -// Reviewer Requirement +// Reviewer Requirement. type reviewByUser struct { gh *client.GitHub user string @@ -21,7 +21,7 @@ var _ Requirement = &reviewByUser{} func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { detail := fmt.Sprintf("This user approved pull request: %s", r.user) - // If not a dry run, make the user a reviewer if he's not already + // If not a dry run, make the user a reviewer if he's not already. if !r.gh.DryRun { requested := false reviewers, err := r.gh.ListPRReviewers(pr.GetNumber()) @@ -55,7 +55,7 @@ func (r *reviewByUser) IsSatisfied(pr *github.PullRequest, details treeprint.Tre } } - // Check if user already approved this PR + // Check if user already approved this PR. reviews, err := r.gh.ListPRReviews(pr.GetNumber()) if err != nil { r.gh.Logger.Errorf("unable to check if user %s already approved this PR: %v", r.user, err) @@ -77,7 +77,7 @@ func ReviewByUser(gh *client.GitHub, user string) Requirement { return &reviewByUser{gh, user} } -// Reviewer Requirement +// Reviewer Requirement. type reviewByTeamMembers struct { gh *client.GitHub team string @@ -89,7 +89,7 @@ var _ Requirement = &reviewByTeamMembers{} func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treeprint.Tree) bool { detail := fmt.Sprintf("At least %d user(s) of the team %s approved pull request", r.count, r.team) - // If not a dry run, make the user a reviewer if he's not already + // If not a dry run, make the user a reviewer if he's not already. if !r.gh.DryRun { requested := false reviewers, err := r.gh.ListPRReviewers(pr.GetNumber()) @@ -123,7 +123,7 @@ func (r *reviewByTeamMembers) IsSatisfied(pr *github.PullRequest, details treepr } } - // Check how many members of this team already approved this PR + // Check how many members of this team already approved this PR. approved := uint(0) reviews, err := r.gh.ListPRReviews(pr.GetNumber()) if err != nil { From 00bc5765a9298b86c2e10d0d20be76470ff43dfe Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 27 Nov 2024 23:05:11 +0900 Subject: [PATCH 43/44] feat: add matrix subcommand + move bot to check --- contribs/github-bot/{bot.go => check.go} | 49 ++-- contribs/github-bot/comment.go | 30 +-- contribs/github-bot/comment_test.go | 38 +-- contribs/github-bot/internal/client/client.go | 30 +++ contribs/github-bot/internal/params/params.go | 39 +-- contribs/github-bot/internal/params/prlist.go | 12 +- contribs/github-bot/internal/utils/actions.go | 45 ++++ .../github-bot/internal/utils/actions_test.go | 43 +++ .../github-bot/internal/utils/github_const.go | 14 + contribs/github-bot/main.go | 20 +- contribs/github-bot/matrix.go | 111 ++++++++ contribs/github-bot/matrix_test.go | 248 ++++++++++++++++++ 12 files changed, 553 insertions(+), 126 deletions(-) rename contribs/github-bot/{bot.go => check.go} (87%) create mode 100644 contribs/github-bot/internal/utils/actions.go create mode 100644 contribs/github-bot/internal/utils/actions_test.go create mode 100644 contribs/github-bot/internal/utils/github_const.go create mode 100644 contribs/github-bot/matrix.go create mode 100644 contribs/github-bot/matrix_test.go diff --git a/contribs/github-bot/bot.go b/contribs/github-bot/check.go similarity index 87% rename from contribs/github-bot/bot.go rename to contribs/github-bot/check.go index 1dda6b5e2dd..8019246d27c 100644 --- a/contribs/github-bot/bot.go +++ b/contribs/github-bot/check.go @@ -12,12 +12,31 @@ import ( "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 execBot(params *p.Params) error { +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 { // Create context with timeout if specified in the parameters. ctx := context.Background() if params.Timeout > 0 { @@ -51,27 +70,9 @@ func execBot(params *p.Params) error { // If requested, retrieve all open pull requests. if params.PRAll { - opts := &github.PullRequestListOptions{ - State: "open", - Sort: "updated", - Direction: "desc", - ListOptions: github.ListOptions{ - PerPage: client.PageSize, - }, - } - - for { - prsPage, response, err := gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) - if err != nil { - return fmt.Errorf("unable to retrieve all open pull requests: %w", err) - } - - prs = append(prs, prsPage...) - - if response.NextPage == 0 { - break - } - opts.Page = response.NextPage + prs, err = gh.ListPR(utils.PRStateOpen) + if err != nil { + return fmt.Errorf("unable to list all PR: %w", err) } } else { // Otherwise, retrieve only specified pull request(s) @@ -86,6 +87,10 @@ func execBot(params *p.Params) error { } } + return processPRList(gh, prs) +} + +func processPRList(gh *client.GitHub, prs []*github.PullRequest) error { if len(prs) > 1 { prNums := make([]int, len(prs)) for i, pr := range prs { diff --git a/contribs/github-bot/comment.go b/contribs/github-bot/comment.go index 7ed22a63b35..8bf4a158745 100644 --- a/contribs/github-bot/comment.go +++ b/contribs/github-bot/comment.go @@ -9,6 +9,7 @@ import ( "text/template" "github.com/gnolang/gno/contribs/github-bot/internal/client" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/google/go-github/v64/github" "github.com/sethvargo/go-githubactions" @@ -71,23 +72,6 @@ func getCommentManualChecks(commentBody string) map[string]manualCheckDetails { return checks } -// Recursively search for nested values using the keys provided. -func indexMap(m map[string]any, keys ...string) any { - if len(keys) == 0 { - return m - } - - if val, ok := m[keys[0]]; ok { - if keys = keys[1:]; len(keys) == 0 { - return val - } - subMap, _ := val.(map[string]any) - return indexMap(subMap, keys...) - } - - return nil -} - // handleCommentUpdate checks if: // - the current run was triggered by GitHub Actions // - the triggering event is an edit of the bot comment @@ -96,7 +80,7 @@ func indexMap(m map[string]any, keys ...string) any { // - the actor / comment editor has permission to modify this checkbox (or restore it) func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubContext) error { // Ignore if it's not a comment related event. - if actionCtx.EventName != "issue_comment" { + if actionCtx.EventName != utils.EventIssueComment { gh.Logger.Debugf("Event is not issue comment related (%s)", actionCtx.EventName) return nil } @@ -123,7 +107,7 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte } // Get login of the author of the edited comment. - login, ok := indexMap(actionCtx.Event, "comment", "user", "login").(string) + login, ok := utils.IndexMap(actionCtx.Event, "comment", "user", "login").(string) if !ok { return errors.New("unable to get comment user login on issue comment event") } @@ -134,19 +118,19 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte } // Get comment updated body. - current, ok := indexMap(actionCtx.Event, "comment", "body").(string) + current, ok := utils.IndexMap(actionCtx.Event, "comment", "body").(string) if !ok { return errors.New("unable to get comment body on issue comment event") } // Get comment previous body. - previous, ok := indexMap(actionCtx.Event, "changes", "body", "from").(string) + previous, ok := utils.IndexMap(actionCtx.Event, "changes", "body", "from").(string) if !ok { return errors.New("unable to get changes body content on issue comment event") } // Get PR number from GitHub Actions context. - prNum, ok := indexMap(actionCtx.Event, "issue", "number").(float64) + 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") } @@ -200,7 +184,7 @@ func handleCommentUpdate(gh *client.GitHub, actionCtx *githubactions.GitHubConte } // This regex capture only the line of the current check. - specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key].status, key)) + specificManualCheck := regexp.MustCompile(fmt.Sprintf(`(?m:^- \[%s\] %s.*$)`, currentChecks[key].status, regexp.QuoteMeta(key))) // If the box is checked, append the username of the user who checked it. if strings.TrimSpace(currentChecks[key].status) == "x" { diff --git a/contribs/github-bot/comment_test.go b/contribs/github-bot/comment_test.go index 9c0155ecf85..fd8790dd9e1 100644 --- a/contribs/github-bot/comment_test.go +++ b/contribs/github-bot/comment_test.go @@ -71,42 +71,6 @@ func TestGeneratedComment(t *testing.T) { } } -func TestIndexMap(t *testing.T) { - t.Parallel() - - m := map[string]any{ - "Key1": map[string]any{ - "Key2": map[string]any{ - "Key3": 1, - }, - }, - } - - test := indexMap(m) - assert.NotNil(t, test, "should return m") - _, ok := test.(map[string]any) - assert.True(t, ok, "returned m should be a map") - - test = indexMap(m, "Key1") - assert.NotNil(t, test, "should return Key1 value") - _, ok = test.(map[string]any) - assert.True(t, ok, "Key1 value type should be a map") - - test = indexMap(m, "Key1", "Key2") - assert.NotNil(t, test, "should return Key2 value") - _, ok = test.(map[string]any) - assert.True(t, ok, "Key2 value type should be a map") - - test = indexMap(m, "Key1", "Key2", "Key3") - assert.NotNil(t, test, "should return Key3 value") - val, ok := test.(int) - assert.True(t, ok, "Key3 value type should be an int") - assert.Equal(t, 1, val, "Key3 value should be a 1") - - test = indexMap(m, "Key1", "Key2", "Key3", "Key4") - assert.Nil(t, test, "Key4 value should not exist") -} - func setValue(t *testing.T, m map[string]any, value any, keys ...string) map[string]any { t.Helper() @@ -146,7 +110,7 @@ func TestCommentUpdateHandler(t *testing.T) { // Exit without error because EventName is empty assert.NoError(t, handleCommentUpdate(gh, actionCtx)) - actionCtx.EventName = "issue_comment" + actionCtx.EventName = utils.EventIssueComment // Exit with error because Event.action is not set assert.Error(t, handleCommentUpdate(gh, actionCtx)) diff --git a/contribs/github-bot/internal/client/client.go b/contribs/github-bot/internal/client/client.go index 2aec5c8681f..229c3e90631 100644 --- a/contribs/github-bot/internal/client/client.go +++ b/contribs/github-bot/internal/client/client.go @@ -237,6 +237,36 @@ func (gh *GitHub) ListPRReviews(prNum int) ([]*github.PullRequestReview, error) return allReviews, nil } +// ListPR returns the list of pull requests in the specified state. +func (gh *GitHub) ListPR(state string) ([]*github.PullRequest, error) { + var prs []*github.PullRequest + + opts := &github.PullRequestListOptions{ + State: state, + Sort: "updated", + Direction: "desc", + ListOptions: github.ListOptions{ + PerPage: PageSize, + }, + } + + for { + prsPage, response, err := gh.Client.PullRequests.List(gh.Ctx, gh.Owner, gh.Repo, opts) + if err != nil { + return nil, fmt.Errorf("unable to list pull requests with state %s: %w", state, err) + } + + prs = append(prs, prsPage...) + + if response.NextPage == 0 { + break + } + opts.Page = response.NextPage + } + + return prs, nil +} + // New initializes the API client, the logger, and creates an instance of GitHub. func New(ctx context.Context, params *p.Params) (*GitHub, error) { gh := &GitHub{ diff --git a/contribs/github-bot/internal/params/params.go b/contribs/github-bot/internal/params/params.go index 3a76e0c7684..c11d1b62419 100644 --- a/contribs/github-bot/internal/params/params.go +++ b/contribs/github-bot/internal/params/params.go @@ -6,6 +6,7 @@ import ( "os" "time" + "github.com/gnolang/gno/contribs/github-bot/internal/utils" "github.com/sethvargo/go-githubactions" ) @@ -83,7 +84,7 @@ func (p *Params) ValidateFlags() { // Check if flags are coherent. if p.PRAll && len(p.PRNums) != 0 { - errorUsage("You can specify only one of the '-pr-all' and '-pr-numbers' flags") + 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 @@ -91,47 +92,27 @@ func (p *Params) ValidateFlags() { if p.Owner == "" || p.Repo == "" || (len(p.PRNums) == 0 && !p.PRAll) { actionCtx, err := githubactions.Context() if err != nil { - errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v", err)) + errorUsage(fmt.Sprintf("Unable to get GitHub Actions context: %v.", err)) } if p.Owner == "" { if p.Owner, _ = actionCtx.Repo(); p.Owner == "" { - errorUsage("Unable to retrieve owner from GitHub Actions context, you may want to set it using -onwer flag") + 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 == "" { - errorUsage("Unable to retrieve repo from GitHub Actions context, you may want to set it using -repo flag") + 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 { - const errMsg = "Unable to retrieve pull request number from GitHub Actions context, you may want to set it using -pr-numbers flag" - var num float64 - - switch actionCtx.EventName { - case "issue_comment": - issue, ok := actionCtx.Event["issue"].(map[string]any) - if !ok { - errorUsage(errMsg) - } - num, ok = issue["number"].(float64) - if !ok || num <= 0 { - errorUsage(errMsg) - } - case "pull_request": - pr, ok := actionCtx.Event["pull_request"].(map[string]any) - if !ok { - errorUsage(errMsg) - } - num, ok = pr["number"].(float64) - if !ok || num <= 0 { - errorUsage(errMsg) - } - default: - errorUsage(errMsg) + 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([]int{int(num)}) + p.PRNums = PRList{prNum} } } } diff --git a/contribs/github-bot/internal/params/prlist.go b/contribs/github-bot/internal/params/prlist.go index 5e51181452f..51aed8dc457 100644 --- a/contribs/github-bot/internal/params/prlist.go +++ b/contribs/github-bot/internal/params/prlist.go @@ -28,18 +28,22 @@ func (p PRList) MarshalText() (text []byte, err error) { // UnmarshalText implements encoding.TextUnmarshaler. func (p *PRList) UnmarshalText(text []byte) error { - for _, prNumStr := range strings.Split(string(text), ",") { - prNum, err := strconv.Atoi(strings.TrimSpace(prNumStr)) + prNumsStr := strings.Split(string(text), ",") + prNums := make([]int, len(prNumsStr)) + + for i := range prNumsStr { + prNum, err := strconv.Atoi(strings.TrimSpace(prNumsStr[i])) if err != nil { return err } if prNum <= 0 { - return fmt.Errorf("invalid pull request number (<= 0): original(%s) parsed(%d)", prNumStr, prNum) + return fmt.Errorf("invalid pull request number (<= 0): original(%s) parsed(%d)", prNumsStr[i], prNum) } - *p = append(*p, prNum) + prNums[i] = prNum } + *p = prNums return nil } diff --git a/contribs/github-bot/internal/utils/actions.go b/contribs/github-bot/internal/utils/actions.go new file mode 100644 index 00000000000..91b8ac7e6b4 --- /dev/null +++ b/contribs/github-bot/internal/utils/actions.go @@ -0,0 +1,45 @@ +package utils + +import ( + "fmt" + + "github.com/sethvargo/go-githubactions" +) + +// Recursively search for nested values using the keys provided. +func IndexMap(m map[string]any, keys ...string) any { + if len(keys) == 0 { + return m + } + + if val, ok := m[keys[0]]; ok { + if keys = keys[1:]; len(keys) == 0 { + return val + } + subMap, _ := val.(map[string]any) + return IndexMap(subMap, keys...) + } + + return nil +} + +// Retrieve PR number from GitHub Actions context +func GetPRNumFromActionsCtx(actionCtx *githubactions.GitHubContext) (int, error) { + firstKey := "" + + switch actionCtx.EventName { + case EventIssueComment: + firstKey = "issue" + case EventPullRequest, EventPullRequestTarget: + firstKey = "pull_request" + default: + return 0, fmt.Errorf("unsupported event: %s", actionCtx.EventName) + } + + num, ok := IndexMap(actionCtx.Event, firstKey, "number").(float64) + if !ok || num <= 0 { + return 0, fmt.Errorf("invalid value: %d", int(num)) + } + + return int(num), nil +} diff --git a/contribs/github-bot/internal/utils/actions_test.go b/contribs/github-bot/internal/utils/actions_test.go new file mode 100644 index 00000000000..3114bb8a061 --- /dev/null +++ b/contribs/github-bot/internal/utils/actions_test.go @@ -0,0 +1,43 @@ +package utils + +import ( + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestIndexMap(t *testing.T) { + t.Parallel() + + m := map[string]any{ + "Key1": map[string]any{ + "Key2": map[string]any{ + "Key3": 1, + }, + }, + } + + test := IndexMap(m) + assert.NotNil(t, test, "should return m") + _, ok := test.(map[string]any) + assert.True(t, ok, "returned m should be a map") + + test = IndexMap(m, "Key1") + assert.NotNil(t, test, "should return Key1 value") + _, ok = test.(map[string]any) + assert.True(t, ok, "Key1 value type should be a map") + + test = IndexMap(m, "Key1", "Key2") + assert.NotNil(t, test, "should return Key2 value") + _, ok = test.(map[string]any) + assert.True(t, ok, "Key2 value type should be a map") + + test = IndexMap(m, "Key1", "Key2", "Key3") + assert.NotNil(t, test, "should return Key3 value") + val, ok := test.(int) + assert.True(t, ok, "Key3 value type should be an int") + assert.Equal(t, 1, val, "Key3 value should be a 1") + + test = IndexMap(m, "Key1", "Key2", "Key3", "Key4") + assert.Nil(t, test, "Key4 value should not exist") +} diff --git a/contribs/github-bot/internal/utils/github_const.go b/contribs/github-bot/internal/utils/github_const.go new file mode 100644 index 00000000000..564b7d3fb38 --- /dev/null +++ b/contribs/github-bot/internal/utils/github_const.go @@ -0,0 +1,14 @@ +package utils + +// GitHub const +const ( + // GitHub Actions Event Names + EventIssueComment = "issue_comment" + EventPullRequest = "pull_request" + EventPullRequestTarget = "pull_request_target" + EventWorkflowDispatch = "workflow_dispatch" + + // Pull Request States + PRStateOpen = "open" + PRStateClosed = "closed" +) diff --git a/contribs/github-bot/main.go b/contribs/github-bot/main.go index 45ad97ecf8c..9895f44dc70 100644 --- a/contribs/github-bot/main.go +++ b/contribs/github-bot/main.go @@ -4,24 +4,22 @@ import ( "context" "os" - p "github.com/gnolang/gno/contribs/github-bot/internal/params" "github.com/gnolang/gno/tm2/pkg/commands" ) func main() { - params := &p.Params{} - cmd := commands.NewCommand( commands.Metadata{ - ShortUsage: "github-bot [flags]", - ShortHelp: "checks requirements for a PR to be merged", - 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.", - }, - params, - func(_ context.Context, _ []string) error { - params.ValidateFlags() - return execBot(params) + ShortUsage: "github-bot [flags]", + LongHelp: "Bot that allows for advanced management of GitHub pull requests.", }, + commands.NewEmptyConfig(), + commands.HelpExec, + ) + + cmd.AddSubCommands( + newCheckCmd(), + newMatrixCmd(), ) cmd.Execute(context.Background(), os.Args[1:]) diff --git a/contribs/github-bot/matrix.go b/contribs/github-bot/matrix.go new file mode 100644 index 00000000000..2442a6d94d6 --- /dev/null +++ b/contribs/github-bot/matrix.go @@ -0,0 +1,111 @@ +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 + } + + fmt.Println(prList) + 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 +} diff --git a/contribs/github-bot/matrix_test.go b/contribs/github-bot/matrix_test.go new file mode 100644 index 00000000000..bce4ec1bd8f --- /dev/null +++ b/contribs/github-bot/matrix_test.go @@ -0,0 +1,248 @@ +package main + +import ( + "context" + "net/http" + "strconv" + "strings" + "testing" + + "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" + "github.com/sethvargo/go-githubactions" + "github.com/stretchr/testify/assert" +) + +func TestProcessEvent(t *testing.T) { + t.Parallel() + + prs := []*github.PullRequest{ + {Number: github.Int(1), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(2), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(3), State: github.String(utils.PRStateOpen)}, + {Number: github.Int(4), State: github.String(utils.PRStateClosed)}, + {Number: github.Int(5), State: github.String(utils.PRStateClosed)}, + {Number: github.Int(6), State: github.String(utils.PRStateClosed)}, + } + openPRs := prs[:3] + + for _, testCase := range []struct { + name string + gaCtx *githubactions.GitHubContext + prs []*github.PullRequest + expectedPRList params.PRList + expectedError bool + }{ + { + "valid issue_comment event", + &githubactions.GitHubContext{ + EventName: utils.EventIssueComment, + Event: map[string]any{"issue": map[string]any{"number": 1.}}, + }, + prs, + params.PRList{1}, + false, + }, { + "valid pull_request event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequest, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + params.PRList{1}, + false, + }, { + "valid pull_request_target event", + &githubactions.GitHubContext{ + EventName: utils.EventPullRequestTarget, + Event: map[string]any{"pull_request": map[string]any{"number": 1.}}, + }, + prs, + params.PRList{1}, + false, + }, { + "invalid event (PR number not set)", + &githubactions.GitHubContext{ + EventName: utils.EventIssueComment, + Event: map[string]any{"issue": nil}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid event name", + &githubactions.GitHubContext{ + EventName: "invalid_event", + Event: map[string]any{"issue": map[string]any{"number": 1.}}, + }, + prs, + params.PRList(nil), + true, + }, { + "valid workflow_dispatch all", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, + }, + openPRs, + params.PRList{1, 2, 3}, + false, + }, { + "valid workflow_dispatch all (no prs)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "all"}}, + }, + nil, + params.PRList{}, + false, + }, { + "valid workflow_dispatch list", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3"}}, + }, + prs, + params.PRList{1, 2, 3}, + false, + }, { + "valid workflow_dispatch list with spaces", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": " 1, 2 ,3 "}}, + }, + prs, + params.PRList{1, 2, 3}, + false, + }, { + "invalid workflow_dispatch list (1 closed)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,3,4"}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (1 doesn't exist)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "42"}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (all closed)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "4,5,6"}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (empty)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": ""}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (unset)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": ""}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (not a number list)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "foo"}}, + }, + prs, + params.PRList(nil), + true, + }, { + "invalid workflow_dispatch list (number list with invalid elem)", + &githubactions.GitHubContext{ + EventName: utils.EventWorkflowDispatch, + Event: map[string]any{"inputs": map[string]any{"pull-request-list": "1,2,foo"}}, + }, + prs, + params.PRList(nil), + true, + }, + } { + t.Run(testCase.name, func(t *testing.T) { + t.Parallel() + + mockedHTTPClient := mock.NewMockedHTTPClient( + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) { + if testCase.expectedPRList != nil { + w.Write(mock.MustMarshal(testCase.prs)) + } + }), + ), + mock.WithRequestMatchHandler( + mock.EndpointPattern{ + Pattern: "/repos/pulls/{number}", + Method: "GET", + }, + http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) { + var ( + err error + prNum int + parts = strings.Split(req.RequestURI, "/") + ) + + if len(parts) > 0 { + prNumStr := parts[len(parts)-1] + prNum, err = strconv.Atoi(prNumStr) + if err != nil { + panic(err) // Should never happen + } + } + + for _, pr := range prs { + if pr.GetNumber() == prNum { + w.Write(mock.MustMarshal(pr)) + return + } + } + + w.Write(mock.MustMarshal(nil)) + }), + ), + ) + + gh := &client.GitHub{ + Client: github.NewClient(mockedHTTPClient), + Ctx: context.Background(), + Logger: logger.NewNoopLogger(), + } + + prList, err := getPRListFromEvent(gh, testCase.gaCtx) + if testCase.expectedError { + assert.Error(t, err) + } else { + assert.NoError(t, err) + } + assert.Equal(t, testCase.expectedPRList, prList) + }) + } +} From 353d362bd2dfdc5707e2898e51c38a68a2e70ba6 Mon Sep 17 00:00:00 2001 From: aeddi Date: Wed, 27 Nov 2024 23:45:54 +0900 Subject: [PATCH 44/44] ci: replace bash in workflow by subcommand --- .github/workflows/bot.yml | 40 +++------------------------------------ 1 file changed, 3 insertions(+), 37 deletions(-) diff --git a/.github/workflows/bot.yml b/.github/workflows/bot.yml index 4d95b7650da..975f39f29dc 100644 --- a/.github/workflows/bot.yml +++ b/.github/workflows/bot.yml @@ -42,46 +42,12 @@ jobs: pr-numbers: ${{ steps.pr-numbers.outputs.pr-numbers }} steps: - - name: Parse event inputs + - name: Generate matrix from event id: pr-numbers + working-directory: contribs/github-bot env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - # Triggered by a workflow dispatch event - if [ '${{ github.event_name }}' = 'workflow_dispatch' ]; then - - # If the input is 'all', create a matrix with every open PRs - if [ '${{ inputs.pull-request-list }}' = 'all' ]; then - pr_list=`gh pr list --state 'open' --repo '${{ github.repository }}' --json 'number' --template '{{$first := true}}{{range .}}{{if $first}}{{$first = false}}{{else}}, {{end}}{{"\""}}{{.number}}{{"\""}}{{end}}'` - [ -z "$pr_list" ] && echo 'Error: no opened PR found' >&2 && exit 1 - echo "pr-numbers=[$pr_list]" >> "$GITHUB_OUTPUT" - - # If the input is not 'all', test for each number in the comma separated - # list if the associated PR is opened, then add it to the matrix - else - pr_list_raw='${{ inputs.pull-request-list }}' - pr_list='' - IFS=',' - for number in $pr_list; do - trimed=`echo "$number" | xargs` - pr_state=`gh pr view "$trimed" --repo '${{ github.repository }}' --json 'state' --template '{{.state}}' 2> /dev/null` - [ "$pr_state" != 'OPEN' ] && echo "Error: PR with number <$trimed> is not opened" >&2 && exit 1 - done - echo "pr-numbers=[$pr_list]" >> "$GITHUB_OUTPUT" - fi - - # Triggered by comment event, just add the associated PR number to the matrix - elif [ '${{ github.event_name }}' = 'issue_comment' ]; then - echo 'pr-numbers=["${{ github.event.issue.number }}"]' >> "$GITHUB_OUTPUT" - - # Triggered by pull request target event, just add the associated PR number to the matrix - elif [ '${{ github.event_name }}' = 'pull_request_target' ]; then - echo 'pr-numbers=["${{ github.event.pull_request.number }}"]' >> "$GITHUB_OUTPUT" - - # Should never happen - else - echo 'Error: unknown event ${{ github.event_name }}' >&2 && exit 1 - fi + run: go run . matrix >> "$GITHUB_OUTPUT" # This job processes each pull request in the matrix individually while ensuring # that a same PR cannot be processed concurrently by mutliple runners