Skip to content

Commit ac17468

Browse files
Add flag to enable quiet mode (run without PR comments) (#19)
Co-authored-by: Hans Baker <[email protected]>
1 parent 25c7bd9 commit ac17468

File tree

5 files changed

+342
-62
lines changed

5 files changed

+342
-62
lines changed

.github/workflows/codeowners.yml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ jobs:
1313
codeowners:
1414
name: 'Run Codeowners Plus'
1515
runs-on: ubuntu-latest
16-
if: ${{ !github.event.pull_request.draft }}
1716
steps:
1817
- name: 'Checkout Code Repository'
1918
uses: actions/checkout@v4
@@ -24,10 +23,11 @@ jobs:
2423
run: |
2524
sed -i "s/image: .*/image: 'Dockerfile'/" action.yml
2625
cat action.yml
27-
26+
2827
- name: 'Codeowners Plus'
2928
uses: ./
3029
with:
3130
github-token: '${{ secrets.GITHUB_TOKEN }}'
3231
pr: '${{ github.event.pull_request.number }}'
3332
verbose: true
33+
quiet: ${{ github.event.pull_request.draft }}

CONTRIBUTING.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -18,7 +18,7 @@ Thank you for considering contributing to Codeowners Plus! We welcome contributi
1818
> Running locally still requires real Github PRs to exist that you are testing against.
1919
2020
```bash
21-
go run main.go -token <your_gh_token> -dir ../chaturbate -pr <pr_num> -repo multimediallc/chaturbate -v true
21+
go run main.go -token <your_gh_token> -dir ../chaturbate -pr <pr_num> -repo multimediallc/chaturbate -v true -quiet=true
2222
```
2323

2424
#### Running the CLI tool Locally

README.md

Lines changed: 41 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ Code Ownership &amp; Review Assignment Tool - GitHub CODEOWNERS but better
44

55
[![Go Report Card](https://goreportcard.com/badge/github.com/multimediallc/codeowners-plus)](https://goreportcard.com/report/github.com/multimediallc/codeowners-plus?kill_cache=1)
66
[![Tests](https://github.com/multimediallc/codeowners-plus/actions/workflows/go.yml/badge.svg)](https://github.com/multimediallc/codeowners-plus/actions/workflows/go.yml)
7-
![Coverage](https://img.shields.io/badge/Coverage-83.6%25-brightgreen)
7+
![Coverage](https://img.shields.io/badge/Coverage-83.3%25-brightgreen)
88
[![License](https://img.shields.io/badge/License-BSD%203--Clause-blue.svg)](https://opensource.org/licenses/BSD-3-Clause)
99
[![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.1-4baaaa.svg)](CODE_OF_CONDUCT.md)
1010

@@ -70,9 +70,9 @@ jobs:
7070
uses: actions/checkout@v4
7171
with:
7272
fetch-depth: 0
73-
73+
7474
- name: 'Codeowners Plus'
75-
uses: multimediallc/[email protected].0
75+
uses: multimediallc/[email protected].4
7676
with:
7777
github-token: '${{ secrets.GITHUB_TOKEN }}'
7878
pr: '${{ github.event.pull_request.number }}'
@@ -121,7 +121,7 @@ By default, each file will resolve to a single reviewer. See [priority section]
121121

122122
To instead require an owner as an additional reviewer (`AND` rule), put an `&` at the start of the line:
123123
```
124-
# this rule will add `@task-auditor` as a required review in addition to the file owner's required review
124+
# this rule will add `@task-auditor` as a required review in addition to the file owner's required review
125125
& **/task.go @task-auditor
126126
```
127127

@@ -219,7 +219,7 @@ If you want to allow PR authors to bypass some reviews when there are a large nu
219219

220220
`codeowners.toml`
221221
```toml
222-
#
222+
#
223223
# `max_reviews` (default nil) allows you to skip some reviewers if the number of reviewers is greater than the max_reviewers
224224
max_reviews = 2
225225
```
@@ -254,13 +254,44 @@ high_priority_labels = ["high-priority", "urgent"]
254254

255255
When a PR has any of these labels, the comment will look like this:
256256
```
257-
❗High Prio❗
257+
❗High Prio❗
258258
259259
Codeowners approval required for this PR:
260260
- @user1
261261
- @user2
262262
```
263263

264+
## Quiet Mode
265+
266+
You can run Codeowners Plus in a "quiet" mode using the `quiet` input in the GitHub Action.
267+
268+
### When Quiet Mode is Enabled
269+
270+
* **No Comments:** The action will **not** post the review status comment (listing required/unapproved reviewers) or the optional reviewer "cc" comment to the Pull Request.
271+
* **No Review Requests:** The action will **not** automatically request reviews from required owners who have not yet approved via the GitHub API.
272+
273+
### Behavior:
274+
275+
Even in quiet mode, the tool still performs all its internal calculations: determining required/optional owners based on file changes, checking existing approvals, and determining if the ownership rules are satisfied. The primary outcome is still the success or failure status of the associated status check (unless you've configured `enforcement.fail_check = false`).
276+
277+
### Use Cases:
278+
279+
* **Draft Pull Requests:** This is a common use case. You might want the Codeowners Plus logic to run and report a status (e.g., pending or failed) on draft PRs, but without notifying reviewers prematurely by adding comments or requesting reviews until the PR is marked "Ready for review".
280+
* **Custom Notification Workflows:** You might prefer to handle notifications or review requests through a different mechanism and only use Codeowners Plus for the status check enforcement.
281+
282+
### Activation:
283+
284+
* **GitHub Action:** Set the `quiet` input to `'true'`.
285+
```yaml
286+
- name: 'Codeowners Plus (Quiet)'
287+
uses: multimediallc/[email protected]
288+
with:
289+
# ... other inputs ...
290+
quiet: 'true'
291+
```
292+
293+
**Default:** Quiet mode is **disabled** (`false`) by default.
294+
264295
## CLI Tool
265296

266297
A CLI tool is available which provides some utilities for working with `.codeowners` files.
@@ -276,7 +307,10 @@ Available commands are:
276307
* `owner` to check who owns a specific file
277308
* `verify` to check for typos in a `.codeowners` file
278309

310+
## Contributing
311+
312+
See [CONTRIBUTING.md](https://github.com/multimediallc/codeowners-plus/blob/main/CONTRIBUTING.md)
313+
279314
## Future Features
280315

281316
* Inline ownership comments for having owners for specific functions, classes, etc.
282-

main.go

Lines changed: 103 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@ type AppConfig struct {
2626
PR int
2727
Repo string
2828
Verbose bool
29+
Quiet bool
2930
}
3031

3132
// App represents the application with its dependencies
@@ -44,6 +45,7 @@ type Flags struct {
4445
PR *int
4546
Repo *string
4647
Verbose *bool
48+
Quiet *bool
4749
}
4850

4951
var (
@@ -53,6 +55,7 @@ var (
5355
PR: flag.Int("pr", ignoreError(strconv.Atoi(getEnv("INPUT_PR", ""))), "Pull Request number"),
5456
Repo: flag.String("repo", getEnv("INPUT_REPOSITORY", ""), "GitHub repo name"),
5557
Verbose: flag.Bool("v", ignoreError(strconv.ParseBool(getEnv("INPUT_VERBOSE", "0"))), "Verbose output"),
58+
Quiet: flag.Bool("quiet", ignoreError(strconv.ParseBool(getEnv("INPUT_QUIET", "0"))), "Prevents addition of comments to PR and requesting reviews from unapproved owners"),
5659
}
5760
WarningBuffer = bytes.NewBuffer([]byte{})
5861
InfoBuffer = bytes.NewBuffer([]byte{})
@@ -199,11 +202,12 @@ func (a *App) processApprovalsAndReviewers() (bool, string, error) {
199202
}
200203

201204
// Request reviews from required owners
202-
unapprovedOwners, err := a.requestReviews()
205+
err = a.requestReviews()
203206
if err != nil {
204207
return false, message, err
205208
}
206209

210+
unapprovedOwners := a.codeowners.AllRequired()
207211
maxReviewsMet := false
208212
if a.conf.MaxReviews != nil && *a.conf.MaxReviews > 0 {
209213
if validApprovalCount >= *a.conf.MaxReviews && len(f.Intersection(unapprovedOwners.Flatten(), a.conf.UnskippableReviewers)) == 0 {
@@ -212,51 +216,13 @@ func (a *App) processApprovalsAndReviewers() (bool, string, error) {
212216
}
213217

214218
// Add comments to the PR if necessary
215-
if len(unapprovedOwners) > 0 {
216-
// Comment on the PR with the codeowner teams that have not approved the PR
217-
comment := allRequiredOwners.ToCommentString()
218-
hasHighPriority, err := a.client.IsInLabels(a.conf.HighPriorityLabels)
219-
if err != nil {
220-
fmt.Fprintf(WarningBuffer, "WARNING: Error checking high priority labels: %v\n", err)
221-
} else if hasHighPriority {
222-
comment = "❗High Prio❗\n\n" + comment
223-
}
224-
if maxReviewsMet {
225-
comment += "\n\n"
226-
comment += "The PR has received the max number of required reviews. No further action is required."
227-
}
228-
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
229-
found, err := a.client.IsInComments(comment, &fiveDaysAgo)
230-
if err != nil {
231-
return false, message, fmt.Errorf("IsInComments Error: %v\n", err)
232-
}
233-
if !found {
234-
err = a.client.AddComment(comment)
235-
if err != nil {
236-
return false, message, fmt.Errorf("AddComment Error: %v\n", err)
237-
}
238-
}
219+
err = a.addReviewStatusComment(allRequiredOwners, unapprovedOwners, maxReviewsMet)
220+
if err != nil {
221+
return false, message, fmt.Errorf("failed to add review status comment: %w", err)
239222
}
240-
if len(allOptionalReviewerNames) > 0 {
241-
var isInCommentsError error = nil
242-
// Add CC comment to the PR with the optional reviewers that have not already been mentioned in the PR comments
243-
viewersToPing := f.Filtered(allOptionalReviewerNames, func(name string) bool {
244-
found, err := a.client.IsSubstringInComments(name, nil)
245-
if err != nil {
246-
isInCommentsError = err
247-
}
248-
return !found
249-
})
250-
if isInCommentsError != nil {
251-
return false, message, fmt.Errorf("IsInComments Error: %v\n", err)
252-
}
253-
if len(viewersToPing) > 0 {
254-
comment := fmt.Sprintf("cc %s", strings.Join(viewersToPing, " "))
255-
err = a.client.AddComment(comment)
256-
if err != nil {
257-
return false, message, fmt.Errorf("AddComment Error: %v\n", err)
258-
}
259-
}
223+
err = a.addOptionalCcComment(allOptionalReviewerNames)
224+
if err != nil {
225+
return false, message, fmt.Errorf("failed to add optional CC comment: %w", err)
260226
}
261227

262228
// Exit if there are any unapproved codeowner teams
@@ -294,6 +260,87 @@ func (a *App) processApprovalsAndReviewers() (bool, string, error) {
294260
return true, message, nil
295261
}
296262

263+
func (a *App) addReviewStatusComment(allRequiredOwners, unapprovedOwners codeowners.ReviewerGroups, maxReviewsMet bool) error {
264+
// Comment on the PR with the codeowner teams that have not approved the PR
265+
266+
if a.config.Quiet || len(unapprovedOwners) == 0 {
267+
printDebug("Skipping review status comment (disabled or no unapproved owners).\n")
268+
return nil
269+
}
270+
271+
comment := allRequiredOwners.ToCommentString()
272+
hasHighPriority, err := a.client.IsInLabels(a.conf.HighPriorityLabels)
273+
if err != nil {
274+
printWarning("WARNING: Error checking high priority labels: %v\n", err)
275+
} else if hasHighPriority {
276+
comment = "❗High Prio❗\n\n" + comment
277+
}
278+
279+
if maxReviewsMet {
280+
comment += "\n\nThe PR has received the max number of required reviews. No further action is required."
281+
}
282+
283+
fiveDaysAgo := time.Now().AddDate(0, 0, -5)
284+
found, err := a.client.IsInComments(comment, &fiveDaysAgo)
285+
if err != nil {
286+
return fmt.Errorf("IsInComments Error: %v\n", err)
287+
}
288+
289+
// Add the comment if it wasn't found recently
290+
if !found {
291+
printDebug("Adding review status comment: %q\n", comment)
292+
err = a.client.AddComment(comment)
293+
if err != nil {
294+
return fmt.Errorf("AddComment Error: %v\n", err)
295+
}
296+
} else {
297+
printDebug("Similar review status comment already exists.\n")
298+
}
299+
300+
return nil
301+
}
302+
303+
func (a *App) addOptionalCcComment(allOptionalReviewerNames []string) error {
304+
// Add CC comment to the PR with the optional reviewers that have not already been mentioned in the PR comments
305+
306+
if a.config.Quiet || len(allOptionalReviewerNames) == 0 {
307+
printDebug("Skipping optional CC comment (disabled or no optional reviewers).\n")
308+
return nil
309+
}
310+
311+
var isInCommentsError error
312+
viewersToPing := f.Filtered(allOptionalReviewerNames, func(name string) bool {
313+
if isInCommentsError != nil {
314+
return false
315+
}
316+
found, err := a.client.IsSubstringInComments(name, nil)
317+
if err != nil {
318+
printWarning("WARNING: Error checking comments for substring '%s': %v\n", name, err)
319+
isInCommentsError = err
320+
return false
321+
}
322+
return !found
323+
})
324+
325+
if isInCommentsError != nil {
326+
return fmt.Errorf("IsInComments Error: %v\n", isInCommentsError)
327+
}
328+
329+
// Add the CC comment if there are any viewers to ping
330+
if len(viewersToPing) > 0 {
331+
comment := fmt.Sprintf("cc %s", strings.Join(viewersToPing, " "))
332+
printDebug("Adding CC comment: %q\n", comment)
333+
err := a.client.AddComment(comment)
334+
if err != nil {
335+
return fmt.Errorf("AddComment Error: %v\n", err)
336+
}
337+
} else {
338+
printDebug("No new optional reviewers to CC.\n")
339+
}
340+
341+
return nil
342+
}
343+
297344
func (a *App) processTokenOwnerApproval() (*gh.CurrentApproval, error) {
298345
tokenOwner, err := a.client.GetTokenUser()
299346
if err != nil {
@@ -324,20 +371,25 @@ func (a *App) processApprovals(ghApprovals []*gh.CurrentApproval) (int, error) {
324371
return len(ghApprovals) - len(approvalsToDismiss), nil
325372
}
326373

327-
func (a *App) requestReviews() (codeowners.ReviewerGroups, error) {
374+
func (a *App) requestReviews() error {
375+
if a.config.Quiet {
376+
printDebug("Skipping review requests (disabled in quiet mode).\n")
377+
return nil
378+
}
379+
328380
unapprovedOwners := a.codeowners.AllRequired()
329381
unapprovedOwnerNames := unapprovedOwners.Flatten()
330382
printDebug("Remaining Required Owners: %s\n", unapprovedOwnerNames)
331383

332384
currentlyRequestedOwners, err := a.client.GetCurrentlyRequested()
333385
if err != nil {
334-
return nil, fmt.Errorf("GetCurrentlyRequested Error: %v", err)
386+
return fmt.Errorf("GetCurrentlyRequested Error: %v", err)
335387
}
336388
printDebug("Currently Requested Owners: %s\n", currentlyRequestedOwners)
337389

338390
previousReviewers, err := a.client.GetAlreadyReviewed()
339391
if err != nil {
340-
return nil, fmt.Errorf("GetAlreadyReviewed Error: %v", err)
392+
return fmt.Errorf("GetAlreadyReviewed Error: %v", err)
341393
}
342394
printDebug("Already Reviewed Owners: %s\n", previousReviewers)
343395

@@ -348,11 +400,11 @@ func (a *App) requestReviews() (codeowners.ReviewerGroups, error) {
348400
if len(filteredOwners) > 0 {
349401
printDebug("Requesting Reviews from: %s\n", filteredOwnerNames)
350402
if err := a.client.RequestReviewers(filteredOwnerNames); err != nil {
351-
return nil, fmt.Errorf("RequestReviewers Error: %v", err)
403+
return fmt.Errorf("RequestReviewers Error: %v", err)
352404
}
353405
}
354406

355-
return unapprovedOwners, nil
407+
return nil
356408
}
357409

358410
func printFileOwners(codeOwners codeowners.CodeOwners) {
@@ -425,6 +477,7 @@ func main() {
425477
PR: *flags.PR,
426478
Repo: *flags.Repo,
427479
Verbose: *flags.Verbose,
480+
Quiet: *flags.Quiet,
428481
}
429482

430483
app, err := NewApp(cfg)

0 commit comments

Comments
 (0)