-
Notifications
You must be signed in to change notification settings - Fork 375
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
29 changed files
with
2,015 additions
and
20 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,62 +1,104 @@ | ||
name: GitHub Bot | ||
|
||
on: | ||
pull_request: # Watch changes on PR assignement, label and code | ||
# Watch for changes on PR state, assignees, labels and head branch | ||
pull_request: | ||
types: | ||
- assigned | ||
- unassigned | ||
- labeled | ||
- unlabeled | ||
- opened | ||
- reopened | ||
- synchronize # PR code updated | ||
- synchronize # PR head updated | ||
|
||
issue_comment: # Watch PR comment changes | ||
# Watch for changes on PR comment | ||
issue_comment: | ||
types: [created, edited, deleted] | ||
|
||
workflow_dispatch: # Manual run from Github Actions interface | ||
# 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 like `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" | ||
default: all | ||
type: string | ||
|
||
jobs: | ||
prevent-concurrency: | ||
# 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: Set PR numbers inside matrix | ||
- name: Parse event inputs | ||
id: pr-numbers | ||
env: | ||
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | ||
run: | | ||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then | ||
if [ "${{ inputs.perform_deploy }}" = "all" ]; then | ||
echo 'pr-numbers=[""]' >> "$GITHUB_OUTPUT" | ||
# 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 | ||
echo 'pr-numbers=[""]' >> "$GITHUB_OUTPUT" | ||
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 'pr-numbers=[""]' >> "$GITHUB_OUTPUT" | ||
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: | ||
needs: prevent-concurrency | ||
name: Process PR | ||
needs: define-prs-matrix | ||
runs-on: ubuntu-latest | ||
strategy: | ||
matrix: | ||
pr-number: ${{ fromJSON(needs.prevent-concurrency.outputs.pr-numbers) }} | ||
# 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 | ||
- name: Checkout code | ||
uses: actions/checkout@v4 | ||
|
||
- name: Install Go | ||
uses: actions/setup-go@v5 | ||
with: | ||
go-version-file: "go.mod" | ||
go-version-file: go.mod | ||
|
||
- name: Start bot | ||
- name: Run GitHub Bot | ||
env: | ||
GITHUB_TOKEN: ${{ secrets.PAT }} | ||
run: go run . | ||
GITHUB_TOKEN: ${{ secrets.GH_BOT_PAT }} | ||
run: go run . -pr-numbers '${{ matrix.pr-number }}' -verbose |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 | ||
} |
Oops, something went wrong.