diff --git a/CI_MONITORING.md b/CI_MONITORING.md new file mode 100644 index 0000000..466e501 --- /dev/null +++ b/CI_MONITORING.md @@ -0,0 +1,194 @@ +# CI Monitoring and Auto-Fix Feature + +This document describes the CI monitoring and auto-fix feature implemented in Casbin-OA. + +## Overview + +This feature automatically monitors CI check failures in Pull Requests and engages GitHub Copilot to fix linter errors. It also enables automatic code review for all human-created PRs. + +## Features + +### 1. Automatic CI Failure Detection + +The system monitors GitHub webhook events for: +- `check_run` events (individual check completions) +- `check_suite` events (suite of checks completion) + +When a linter check fails, the system: +1. Records the failure in the database +2. Extracts failure details from the check run +3. Posts a comment on the PR tagging @copilot with the failure details +4. Tracks the number of fix attempts + +### 2. Retry Limitation + +To prevent infinite loops, the system: +- Tracks fix attempts per check per PR +- Limits attempts to a maximum of 3 per check +- Stops attempting fixes once the limit is reached + +### 3. Automatic Code Review + +For all human-created PRs: +- Automatically requests a review from @copilot +- Posts a comment notifying @copilot to review the PR + +## Configuration + +### Webhook Setup + +To enable this feature, configure a GitHub organization webhook with the following settings: + +1. **Payload URL**: `https://your-domain.com/api/webhook` +2. **Content type**: `application/json` +3. **Events to subscribe**: + - Pull requests + - Check runs + - Check suites + +### Database + +The system automatically creates a `pr_check` table to track CI check status: + +```sql +CREATE TABLE pr_check ( + id INT AUTO_INCREMENT PRIMARY KEY, + org VARCHAR(100), + repo VARCHAR(100), + pr_number INT, + check_run_id BIGINT, + check_name VARCHAR(200), + status VARCHAR(50), + conclusion VARCHAR(50), + failure_reason TEXT, + fix_attempts INT, + last_attempt_at DATETIME, + is_fixed BOOLEAN, + created_at DATETIME +); +``` + +### Supported Linter Checks + +The system recognizes the following linter check patterns: +- `lint`, `linter` +- `eslint`, `prettier` +- `golangci`, `golint` +- `rubocop`, `pylint`, `flake8` +- `clippy`, `checkstyle`, `pmd`, `spotbugs` +- `style`, `format` + +### Configuration Constants + +The following constants can be modified in `util/check_api.go`: + +- `MaxCheckFailureTextLength`: Maximum length of failure text in comments (default: 500) +- `CopilotUsername`: GitHub username of the copilot bot (default: "copilot") +- `MaxFixAttempts`: Maximum number of fix attempts per check (default: 3) + +## API Endpoints + +### Get PR Checks + +Retrieve all check records for a specific PR: + +``` +GET /api/get-pr-checks?org=&repo=&prNumber= +``` + +**Response:** +```json +[ + { + "id": 1, + "org": "casbin", + "repo": "casbin", + "prNumber": 1691, + "checkRunId": 12345678, + "checkName": "golangci-lint", + "status": "completed", + "conclusion": "failure", + "failureReason": "Check: golangci-lint\nStatus: completed\n...", + "fixAttempts": 1, + "lastAttemptAt": "2026-01-25T03:20:00Z", + "isFixed": false, + "createdAt": "2026-01-25T03:15:00Z" + } +] +``` + +### Get Specific PR Check + +Retrieve a specific check record: + +``` +GET /api/get-pr-check?org=&repo=&prNumber=&checkName= +``` + +## Usage Example + +### Scenario 1: Linter Failure + +1. A developer creates a PR with code that fails the linter check +2. GitHub sends a `check_run` webhook event with `conclusion: failure` +3. The system: + - Records the failure in the database + - Posts a comment: "@copilot The CI check has failed. Please help fix the following issue: **Attempt**: 1/3 ..." +4. Copilot receives the notification and can work on fixing the issue +5. If the fix doesn't work and CI fails again, the system repeats steps 3-4 +6. After 3 failed attempts, the system stops attempting fixes for that specific check + +### Scenario 2: Automatic Code Review + +1. A developer creates a new PR +2. GitHub sends a `pull_request` webhook event with `action: opened` +3. The system: + - Posts a comment: "@copilot Please review this PR." + - Attempts to request copilot as a reviewer + +## Implementation Details + +### Key Components + +1. **Database Model**: `object/pr_check.go` + - Manages PR check records + - Tracks fix attempts and status + +2. **GitHub API Utilities**: `util/check_api.go` + - Retrieves check runs and details + - Identifies linter checks + - Posts comments with copilot tags + +3. **Webhook Handler**: `controllers/webhook.go` + - Processes check_run and check_suite events + - Triggers fix attempts and copilot tagging + - Enables automatic code review + +4. **API Endpoints**: `controllers/pr_check.go` + - Provides access to PR check records + - Enables monitoring and debugging + +### Error Handling + +- Failed API calls are logged but don't crash the system +- Database errors are handled with panic recovery +- Invalid webhook payloads are gracefully ignored + +## Limitations + +1. The copilot username can be configured via the `CopilotUsername` constant in `util/check_api.go` +2. Only linter checks are monitored (build/test failures are ignored) +3. The system requires proper GitHub webhook configuration +4. GitHub API rate limits may affect functionality in high-traffic scenarios +5. Maximum fix attempts can be configured via the `MaxFixAttempts` constant + +## Future Enhancements + +Potential improvements: +- Load configuration constants from configuration file instead of code +- Support for custom linter check patterns via configuration +- Dashboard UI for monitoring PR checks +- Notifications for exceeded retry limits +- Integration with Slack/Discord for notifications +- Support for multiple copilot bots +- Webhook signature verification for security diff --git a/controllers/pr_check.go b/controllers/pr_check.go new file mode 100644 index 0000000..49acae9 --- /dev/null +++ b/controllers/pr_check.go @@ -0,0 +1,82 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package controllers + +import ( + "strconv" + + "github.com/casbin/casbin-oa/object" +) + +// GetPrChecks retrieves all PR check records for a specific PR +func (c *ApiController) GetPrChecks() { + org := c.Input().Get("org") + repo := c.Input().Get("repo") + prNumberStr := c.Input().Get("prNumber") + + if org == "" || repo == "" || prNumberStr == "" { + c.Data["json"] = map[string]interface{}{ + "status": "error", + "msg": "Missing required parameters", + } + c.ServeJSON() + return + } + + prNumber, err := strconv.Atoi(prNumberStr) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "status": "error", + "msg": "Invalid PR number", + } + c.ServeJSON() + return + } + + prChecks := object.GetPrChecksByPR(org, repo, prNumber) + c.Data["json"] = prChecks + c.ServeJSON() +} + +// GetPrCheck retrieves a specific PR check record +func (c *ApiController) GetPrCheck() { + org := c.Input().Get("org") + repo := c.Input().Get("repo") + prNumberStr := c.Input().Get("prNumber") + checkName := c.Input().Get("checkName") + + if org == "" || repo == "" || prNumberStr == "" || checkName == "" { + c.Data["json"] = map[string]interface{}{ + "status": "error", + "msg": "Missing required parameters", + } + c.ServeJSON() + return + } + + prNumber, err := strconv.Atoi(prNumberStr) + if err != nil { + c.Data["json"] = map[string]interface{}{ + "status": "error", + "msg": "Invalid PR number", + } + c.ServeJSON() + return + } + + prCheck := object.GetPrCheck(org, repo, prNumber, checkName) + c.Data["json"] = prCheck + c.ServeJSON() +} diff --git a/controllers/webhook.go b/controllers/webhook.go index aba248c..0c9a3dc 100644 --- a/controllers/webhook.go +++ b/controllers/webhook.go @@ -17,6 +17,7 @@ package controllers import ( "encoding/json" "fmt" + "time" "github.com/casbin/casbin-oa/object" "github.com/casbin/casbin-oa/util" @@ -26,9 +27,35 @@ import ( func (c *ApiController) WebhookOpen() { var issueEvent github.IssuesEvent var pullRequestEvent github.PullRequestEvent + var checkRunEvent github.CheckRunEvent + var checkSuiteEvent github.CheckSuiteEvent + + // Try to parse as different event types + eventType := c.Ctx.Request.Header.Get("X-GitHub-Event") + + result := false + switch eventType { + case "check_run": + err := json.Unmarshal(c.Ctx.Input.RequestBody, &checkRunEvent) + if err == nil && checkRunEvent.CheckRun != nil { + result = HandleCheckRunEvent(checkRunEvent) + c.Data["json"] = result + c.ServeJSON() + return + } + case "check_suite": + err := json.Unmarshal(c.Ctx.Input.RequestBody, &checkSuiteEvent) + if err == nil && checkSuiteEvent.CheckSuite != nil { + result = HandleCheckSuiteEvent(checkSuiteEvent) + c.Data["json"] = result + c.ServeJSON() + return + } + } + + // Legacy handling for issues and pull requests json.Unmarshal(c.Ctx.Input.RequestBody, &pullRequestEvent) - var result bool if pullRequestEvent.PullRequest != nil { result = PullRequestOpen(pullRequestEvent) } else { @@ -107,5 +134,180 @@ func PullRequestOpen(pullRequestEvent github.PullRequestEvent) bool { } } } + + // Request automatic code review from copilot for human-created PRs + // Check if webhook is configured for this repo + if issueWebhook != nil { + go util.RequestCopilotReview(owner, repo, pullRequestEvent.GetNumber()) + } + + return true +} + +// HandleCheckRunEvent handles check_run webhook events +func HandleCheckRunEvent(event github.CheckRunEvent) bool { + if event.GetAction() != "completed" { + return false + } + + checkRun := event.GetCheckRun() + if checkRun == nil { + return false + } + + // Only process failed checks + if checkRun.GetConclusion() != "failure" && checkRun.GetConclusion() != "cancelled" { + return false + } + + // Get PR information + prs := checkRun.PullRequests + if len(prs) == 0 { + return false + } + + owner, repo := util.GetOwnerAndNameFromId(event.Repo.GetFullName()) + issueWebhook := object.GetIssueIfExist(owner, repo) + if issueWebhook == nil { + return false + } + + for _, pr := range prs { + prNumber := pr.GetNumber() + checkName := checkRun.GetName() + + // Check if this is a linter check + if !util.IsLinterCheck(checkName) { + continue + } + + // Check if we should attempt to fix (max 3 attempts) + if !object.ShouldAttemptFix(owner, repo, prNumber, checkName) { + continue + } + + // Get or create PR check record + prCheck := object.GetPrCheck(owner, repo, prNumber, checkName) + if prCheck == nil { + // Create new record + prCheck = &object.PrCheck{ + Org: owner, + Repo: repo, + PrNumber: prNumber, + CheckRunId: checkRun.GetID(), + CheckName: checkName, + Status: checkRun.GetStatus(), + Conclusion: checkRun.GetConclusion(), + FailureReason: util.GetCheckFailureDetails(checkRun), + FixAttempts: 0, + LastAttemptAt: time.Now(), + IsFixed: false, + CreatedAt: time.Now(), + } + object.AddPrCheck(prCheck) + } else { + // Update existing record + prCheck.CheckRunId = checkRun.GetID() + prCheck.Status = checkRun.GetStatus() + prCheck.Conclusion = checkRun.GetConclusion() + prCheck.FailureReason = util.GetCheckFailureDetails(checkRun) + object.UpdatePrCheck(prCheck.Id, prCheck) + } + + // Increment fix attempts and get updated record + prCheck = object.IncrementFixAttempts(owner, repo, prNumber, checkName) + if prCheck != nil { + // Comment on PR with failure details and tag copilot + go util.CommentOnPRWithCopilotTag(owner, repo, prNumber, prCheck.FailureReason, prCheck.FixAttempts) + } + } + + return true +} + +// HandleCheckSuiteEvent handles check_suite webhook events +func HandleCheckSuiteEvent(event github.CheckSuiteEvent) bool { + if event.GetAction() != "completed" { + return false + } + + checkSuite := event.GetCheckSuite() + if checkSuite == nil { + return false + } + + // Only process failed check suites + if checkSuite.GetConclusion() != "failure" && checkSuite.GetConclusion() != "cancelled" { + return false + } + + // Get PR information + prs := checkSuite.PullRequests + if len(prs) == 0 { + return false + } + + owner, repo := util.GetOwnerAndNameFromId(event.Repo.GetFullName()) + issueWebhook := object.GetIssueIfExist(owner, repo) + if issueWebhook == nil { + return false + } + + // For check suites, we need to get individual check runs + for _, pr := range prs { + prNumber := pr.GetNumber() + headSHA := checkSuite.GetHeadSHA() + + // Get all check runs for this commit + checkRuns, err := util.GetPRCheckRuns(owner, repo, headSHA) + if err != nil { + continue + } + + for _, checkRun := range checkRuns { + if checkRun.GetConclusion() != "failure" && checkRun.GetConclusion() != "cancelled" { + continue + } + + checkName := checkRun.GetName() + + // Only process linter checks + if !util.IsLinterCheck(checkName) { + continue + } + + // Check if we should attempt to fix + if !object.ShouldAttemptFix(owner, repo, prNumber, checkName) { + continue + } + + // Get or create PR check record + prCheck := object.GetPrCheck(owner, repo, prNumber, checkName) + if prCheck == nil { + prCheck = &object.PrCheck{ + Org: owner, + Repo: repo, + PrNumber: prNumber, + CheckRunId: checkRun.GetID(), + CheckName: checkName, + Status: checkRun.GetStatus(), + Conclusion: checkRun.GetConclusion(), + FailureReason: util.GetCheckFailureDetails(checkRun), + FixAttempts: 0, + LastAttemptAt: time.Now(), + IsFixed: false, + CreatedAt: time.Now(), + } + object.AddPrCheck(prCheck) + } + + // Increment fix attempts and get updated record + prCheck = object.IncrementFixAttempts(owner, repo, prNumber, checkName) + if prCheck != nil { + go util.CommentOnPRWithCopilotTag(owner, repo, prNumber, prCheck.FailureReason, prCheck.FixAttempts) + } + } + } + return true } diff --git a/object/adapter.go b/object/adapter.go index 69c21e5..c7c4609 100644 --- a/object/adapter.go +++ b/object/adapter.go @@ -132,4 +132,9 @@ func (a *Adapter) createTable() { if err != nil { panic(err) } + + err = a.Engine.Sync2(new(PrCheck)) + if err != nil { + panic(err) + } } diff --git a/object/pr_check.go b/object/pr_check.go new file mode 100644 index 0000000..4240c00 --- /dev/null +++ b/object/pr_check.go @@ -0,0 +1,122 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package object + +import ( + "fmt" + "time" + + "github.com/casbin/casbin-oa/util" +) + +type PrCheck struct { + Id int `xorm:"int notnull pk autoincr" json:"id"` + Org string `xorm:"varchar(100)" json:"org"` + Repo string `xorm:"varchar(100)" json:"repo"` + PrNumber int `xorm:"int" json:"prNumber"` + CheckRunId int64 `xorm:"bigint" json:"checkRunId"` + CheckName string `xorm:"varchar(200)" json:"checkName"` + Status string `xorm:"varchar(50)" json:"status"` + Conclusion string `xorm:"varchar(50)" json:"conclusion"` + FailureReason string `xorm:"text" json:"failureReason"` + FixAttempts int `xorm:"int" json:"fixAttempts"` + LastAttemptAt time.Time `xorm:"datetime" json:"lastAttemptAt"` + IsFixed bool `xorm:"bool" json:"isFixed"` + CreatedAt time.Time `xorm:"datetime" json:"createdAt"` +} + +// GetPrCheck retrieves a PR check by org, repo, PR number and check name +func GetPrCheck(org string, repo string, prNumber int, checkName string) *PrCheck { + prCheck := PrCheck{} + existed, err := adapter.Engine.Where("org = ? and repo = ? and pr_number = ? and check_name = ?", + org, repo, prNumber, checkName).Desc("id").Get(&prCheck) + if err != nil { + panic(err) + } + if existed { + return &prCheck + } + return nil +} + +// GetPrChecksByPR retrieves all checks for a specific PR +func GetPrChecksByPR(org string, repo string, prNumber int) []*PrCheck { + prChecks := []*PrCheck{} + err := adapter.Engine.Where("org = ? and repo = ? and pr_number = ?", + org, repo, prNumber).Find(&prChecks) + if err != nil { + panic(err) + } + return prChecks +} + +// AddPrCheck adds a new PR check record +func AddPrCheck(prCheck *PrCheck) bool { + affected, err := adapter.Engine.Insert(prCheck) + if err != nil { + panic(err) + } + return affected != 0 +} + +// UpdatePrCheck updates an existing PR check record +func UpdatePrCheck(id int, prCheck *PrCheck) bool { + _, err := adapter.Engine.ID(id).AllCols().Update(prCheck) + if err != nil { + panic(err) + } + return true +} + +// IncrementFixAttempts increments the fix attempts counter and returns the updated record +func IncrementFixAttempts(org string, repo string, prNumber int, checkName string) *PrCheck { + prCheck := GetPrCheck(org, repo, prNumber, checkName) + if prCheck == nil { + return nil + } + prCheck.FixAttempts++ + prCheck.LastAttemptAt = time.Now() + UpdatePrCheck(prCheck.Id, prCheck) + return prCheck +} + +// MarkAsFixed marks a check as fixed +func MarkAsFixed(org string, repo string, prNumber int, checkName string) bool { + prCheck := GetPrCheck(org, repo, prNumber, checkName) + if prCheck == nil { + return false + } + prCheck.IsFixed = true + prCheck.Conclusion = "success" + return UpdatePrCheck(prCheck.Id, prCheck) +} + +// ShouldAttemptFix checks if we should attempt to fix this check (max attempts configurable) +func ShouldAttemptFix(org string, repo string, prNumber int, checkName string) bool { + prCheck := GetPrCheck(org, repo, prNumber, checkName) + if prCheck == nil { + return true // First time, should attempt + } + return prCheck.FixAttempts < util.MaxFixAttempts && !prCheck.IsFixed +} + +// GetFailureReason returns a formatted failure reason for display +func GetFailureReason(prCheck *PrCheck) string { + if prCheck == nil || prCheck.FailureReason == "" { + return "No failure details available" + } + return fmt.Sprintf("Check '%s' failed with status: %s\nDetails:\n%s", + prCheck.CheckName, prCheck.Conclusion, prCheck.FailureReason) +} diff --git a/routers/router.go b/routers/router.go index 4f4f3f0..6cef223 100644 --- a/routers/router.go +++ b/routers/router.go @@ -79,6 +79,9 @@ func initAPI() { beego.Router("/api/webhook", &controllers.ApiController{}, "Post:WebhookOpen") beego.Router("/api/is-mainland-ip", &controllers.ApiController{}, "GET:IsMainlandIp") + beego.Router("/api/get-pr-checks", &controllers.ApiController{}, "GET:GetPrChecks") + beego.Router("/api/get-pr-check", &controllers.ApiController{}, "GET:GetPrCheck") + beego.Router("/api/get-machines", &controllers.ApiController{}, "GET:GetMachines") beego.Router("/api/get-machine", &controllers.ApiController{}, "GET:GetMachine") beego.Router("/api/update-machine", &controllers.ApiController{}, "POST:UpdateMachine") diff --git a/util/check_api.go b/util/check_api.go new file mode 100644 index 0000000..411cc19 --- /dev/null +++ b/util/check_api.go @@ -0,0 +1,161 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "context" + "fmt" + "strings" + + "github.com/google/go-github/v74/github" +) + +const ( + // MaxCheckFailureTextLength is the maximum length of failure text to include in comments + MaxCheckFailureTextLength = 500 + // CopilotUsername is the GitHub username of the copilot bot + CopilotUsername = "copilot" + // MaxFixAttempts is the maximum number of fix attempts for a failing check + MaxFixAttempts = 3 +) + +// GetPRCheckRuns retrieves all check runs for a specific commit SHA +func GetPRCheckRuns(owner string, repo string, ref string) ([]*github.CheckRun, error) { + client := GetClient() + opts := &github.ListCheckRunsOptions{ + ListOptions: github.ListOptions{PerPage: 100}, + } + + result, _, err := client.Checks.ListCheckRunsForRef(context.Background(), owner, repo, ref, opts) + if err != nil { + return nil, err + } + + return result.CheckRuns, nil +} + +// GetCheckRunDetails retrieves details for a specific check run +func GetCheckRunDetails(owner string, repo string, checkRunID int64) (*github.CheckRun, error) { + client := GetClient() + checkRun, _, err := client.Checks.GetCheckRun(context.Background(), owner, repo, checkRunID) + if err != nil { + return nil, err + } + return checkRun, nil +} + +// IsLinterCheck determines if a check is a linter check based on its name +func IsLinterCheck(checkName string) bool { + linterKeywords := []string{ + "lint", "linter", "eslint", "golangci", "golint", + "prettier", "style", "format", "rubocop", "pylint", + "flake8", "clippy", "checkstyle", "pmd", "spotbugs", + } + + checkNameLower := strings.ToLower(checkName) + for _, keyword := range linterKeywords { + if strings.Contains(checkNameLower, keyword) { + return true + } + } + return false +} + +// GetCheckFailureDetails extracts failure details from a check run +func GetCheckFailureDetails(checkRun *github.CheckRun) string { + if checkRun == nil { + return "No check run details available" + } + + var details strings.Builder + details.WriteString(fmt.Sprintf("Check: %s\n", checkRun.GetName())) + details.WriteString(fmt.Sprintf("Status: %s\n", checkRun.GetStatus())) + details.WriteString(fmt.Sprintf("Conclusion: %s\n", checkRun.GetConclusion())) + + if checkRun.Output != nil { + if checkRun.Output.Title != nil { + details.WriteString(fmt.Sprintf("Title: %s\n", checkRun.Output.GetTitle())) + } + if checkRun.Output.Summary != nil { + details.WriteString(fmt.Sprintf("Summary: %s\n", checkRun.Output.GetSummary())) + } + if checkRun.Output.Text != nil { + text := checkRun.Output.GetText() + // Limit the text to avoid too long messages + if len(text) > MaxCheckFailureTextLength { + text = text[:MaxCheckFailureTextLength] + "...(truncated)" + } + details.WriteString(fmt.Sprintf("Details: %s\n", text)) + } + } + + return details.String() +} + +// CommentOnPRWithCopilotTag comments on a PR and tags the copilot for fixing +func CommentOnPRWithCopilotTag(owner string, repo string, prNumber int, failureDetails string, attemptNumber int) error { + commentBody := fmt.Sprintf(`@%s The CI check has failed. Please help fix the following issue: + +**Attempt**: %d/%d + +**Failure Details**: +%s + +Please investigate and fix this issue.`, CopilotUsername, attemptNumber, MaxFixAttempts, failureDetails) + + success := Comment(commentBody, owner, repo, prNumber) + if !success { + return fmt.Errorf("failed to post comment on PR #%d", prNumber) + } + return nil +} + +// RequestCopilotReview requests a review from copilot on a PR +func RequestCopilotReview(owner string, repo string, prNumber int) error { + // First, comment to notify about the review request + commentBody := fmt.Sprintf(`@%s Please review this PR.`, CopilotUsername) + success := Comment(commentBody, owner, repo, prNumber) + if !success { + return fmt.Errorf("failed to post review request comment on PR #%d", prNumber) + } + + // Try to request reviewer (may fail if copilot is not a collaborator) + // We ignore errors here as the comment is the primary notification + _ = RequestReviewers(owner, repo, prNumber, []string{CopilotUsername}) + + return nil +} + +// GetPRCommits retrieves commits for a PR +func GetPRCommits(owner string, repo string, prNumber int) ([]*github.RepositoryCommit, error) { + client := GetClient() + opts := &github.ListOptions{PerPage: 100} + + commits, _, err := client.PullRequests.ListCommits(context.Background(), owner, repo, prNumber, opts) + if err != nil { + return nil, err + } + return commits, nil +} + +// GetPRDetails retrieves details for a specific PR +func GetPRDetails(owner string, repo string, prNumber int) (*github.PullRequest, error) { + client := GetClient() + pr, _, err := client.PullRequests.Get(context.Background(), owner, repo, prNumber) + if err != nil { + return nil, err + } + return pr, nil +} diff --git a/util/check_api_test.go b/util/check_api_test.go new file mode 100644 index 0000000..416b866 --- /dev/null +++ b/util/check_api_test.go @@ -0,0 +1,46 @@ +// Copyright 2021 The casbin Authors. All Rights Reserved. +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +package util + +import ( + "testing" +) + +func TestIsLinterCheck(t *testing.T) { + tests := []struct { + name string + checkName string + want bool + }{ + {"Go linter", "golangci-lint", true}, + {"ESLint", "eslint", true}, + {"Prettier", "prettier", true}, + {"Go Lint", "golint", true}, + {"Rubocop", "rubocop", true}, + {"Pylint", "pylint", true}, + {"Generic Lint", "lint-check", true}, + {"Non-linter build", "build", false}, + {"Non-linter test", "test", false}, + {"Non-linter unit-tests", "unit-tests", false}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + if got := IsLinterCheck(tt.checkName); got != tt.want { + t.Errorf("IsLinterCheck(%q) = %v, want %v", tt.checkName, got, tt.want) + } + }) + } +}