Skip to content

Commit

Permalink
docs: document GitHub object + consistency
Browse files Browse the repository at this point in the history
  • Loading branch information
aeddi committed Nov 25, 2024
1 parent 6cca766 commit 6526e12
Show file tree
Hide file tree
Showing 25 changed files with 147 additions and 132 deletions.
40 changes: 20 additions & 20 deletions contribs/github-bot/bot.go
Original file line number Diff line number Diff line change
Expand Up @@ -18,38 +18,38 @@ 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
ctx, cancel = context.WithTimeout(context.Background(), params.Timeout)
defer cancel()
}

Check warning on line 27 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L20-L27

Added lines #L20 - L27 were not covered by tests

// 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)
}

Check warning on line 33 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L30-L33

Added lines #L30 - L33 were not covered by tests

// 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
}

Check warning on line 40 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L36-L40

Added lines #L36 - L40 were not covered by tests

// 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)
}

Check warning on line 47 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L43-L47

Added lines #L43 - L47 were not covered by tests

// 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",
Expand All @@ -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)
Expand All @@ -95,14 +95,14 @@ func execBot(params *p.Params) error {
gh.Logger.Infof("%d pull requests to process: %v\n", len(prNums), prNums)

Check warning on line 95 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L95

Added line #L95 was not covered by tests
}

// 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 {
Expand All @@ -112,19 +112,19 @@ 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

Check warning on line 121 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L99-L121

Added lines #L99 - L121 were not covered by tests
}

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
Expand All @@ -137,22 +137,22 @@ func execBot(params *p.Params) error {
commentContent.AutoRules = append(commentContent.AutoRules, c)

Check warning on line 137 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L135-L137

Added lines #L135 - L137 were not covered by tests
}

// 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())
}

Check warning on line 144 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L141-L144

Added lines #L141 - L144 were not covered by tests

// 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

Check warning on line 152 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L147-L152

Added lines #L147 - L152 were not covered by tests
}

// Get check status from current comment, if any
// Get check status from current comment, if any.
checkedBy := ""
check, ok := checks[manualRule.description]
if ok {
Expand All @@ -174,7 +174,7 @@ func execBot(params *p.Params) error {
}

Check warning on line 174 in contribs/github-bot/bot.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/bot.go#L162-L174

Added lines #L162 - L174 were not covered by tests
}

// 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)
Expand All @@ -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 {
Expand Down
74 changes: 37 additions & 37 deletions contribs/github-bot/comment.go
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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}
Expand All @@ -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
Expand All @@ -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")
Expand All @@ -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)
Expand All @@ -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

Check warning on line 170 in contribs/github-bot/comment.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/comment.go#L168-L170

Added lines #L168 - L170 were not covered by tests
}

// 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)
Expand All @@ -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
}

Check warning on line 190 in contribs/github-bot/comment.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/comment.go#L187-L190

Added lines #L187 - L190 were not covered by tests

// 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 {
Expand All @@ -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)
}

Check warning on line 213 in contribs/github-bot/comment.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/comment.go#L203-L213

Added lines #L203 - L213 were not covered by tests
}

// 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")
Expand All @@ -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)
}

Check warning on line 240 in contribs/github-bot/comment.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/comment.go#L239-L240

Added lines #L239 - L240 were not covered by tests

// 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)
Expand All @@ -248,23 +248,23 @@ 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)
}

Check warning on line 257 in contribs/github-bot/comment.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/comment.go#L252-L257

Added lines #L252 - L257 were not covered by tests

// 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)
} else {
gh.Logger.Infof("Comment successfully updated on PR %d", pr.GetNumber())
}

Check warning on line 265 in contribs/github-bot/comment.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/comment.go#L260-L265

Added lines #L260 - L265 were not covered by tests

// Prepare commit status content
// Prepare commit status content.
var (
context = "Merge Requirements"
targetURL = comment.GetHTMLURL()
Expand All @@ -277,7 +277,7 @@ func updatePullRequest(gh *client.GitHub, pr *github.PullRequest, content Commen
description = "All requirements are satisfied."
}

Check warning on line 278 in contribs/github-bot/comment.go

View check run for this annotation

Codecov / codecov/patch

contribs/github-bot/comment.go#L268-L278

Added lines #L268 - L278 were not covered by tests

// Update or create commit status
// Update or create commit status.
if _, _, err := gh.Client.Repositories.CreateStatus(
gh.Ctx,
gh.Owner,
Expand Down
Loading

0 comments on commit 6526e12

Please sign in to comment.