Repository navigation
feat(github): decide whose PR feedback wakes agents with ordered rules #1746
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Open
johnmatthewtennant
wants to merge
31
commits into
main
Choose a base branch
from
jtennant/poppy/github-author-allowlist
base: main
Could not load branches
Branch not found: {{ refName }}
Loading
Could not load tags
Nothing to show
Loading
Are you sure you want to change the base?
Some commits from the old base branch may be removed from the timeline,
and old review comments may become outdated.
+1,888
−30
Open
Changes from all commits
Commits
Show all changes
31 commits
Select commit
Hold shift + click to select a range
52c3077
feat(github): limit whose PR feedback wakes agents
johnmatthewtennant e11d459
fix(github): keep membership lookups from holding feedback forever
johnmatthewtennant 2186cee
test(settings): expect the GitHub comment authors card
johnmatthewtennant 1fad9fe
feat(github): decide PR feedback by ordered allow and deny rules
johnmatthewtennant 3f1e0ef
refactor(github): drop converting saved author lists to rules
johnmatthewtennant bce98da
fix(github): judge waiting feedback by current rules and fresh member…
johnmatthewtennant 0a1f62c
style(github): lay out the comment author rules as a Settings form
johnmatthewtennant 067d37b
fix(github): hold waiting feedback whose author it cannot check
johnmatthewtennant ecb5490
feat(github): choose what feedback no rule matches gets
johnmatthewtennant 4beed20
test(github): cover rules saved in Settings changing a running watch
johnmatthewtennant 31cdefb
docs(github): say body rules see what a wake quotes
johnmatthewtennant a604e11
fix(github): recheck new feedback against rules saved during a lookup
johnmatthewtennant e0d5bef
fix(github): match body rules against the text a wake quotes
johnmatthewtennant 4ce1145
fix(github): leave feedback admission to the final recheck
johnmatthewtennant b7bbac8
fix(github): stop membership fallback when a watch stops
johnmatthewtennant 50aa164
fix(github): count failed membership reads once a poll
johnmatthewtennant e5ba22c
fix(github): match rule patterns in linear time
johnmatthewtennant 6490dfc
fix(github): show why rules are not rules on the Rules field
johnmatthewtennant 460f75b
docs(github): allow Copilot by its exact bot login
johnmatthewtennant d570324
fix(github): match an escaped surrogate pair as one character
johnmatthewtennant 88d72e4
fix(github): reject rules naming more organizations than a read can ask
johnmatthewtennant 9b1965e
fix(github): hold feedback while GitHub answers no membership read
johnmatthewtennant 1829b5f
fix(github): match a legacy octal escape as one character
johnmatthewtennant 5bad688
fix(github): hold feedback while saved rules no longer parse
johnmatthewtennant 69ffeb4
fix(github): read new feedback before rereading what is held
johnmatthewtennant 434b647
Refuse pattern escapes that mean something else alone
johnmatthewtennant 069617e
Keep held feedback held across limited reads
johnmatthewtennant 99676a2
Count membership retries from the requests sent
johnmatthewtennant 98458bb
Merge main: decide watch feedback from the pull request store
johnmatthewtennant 0bffb49
fix(github): read memberships apart from deciding a watch
johnmatthewtennant ee226db
fix(github): let a watch's first read learn feedback held elsewhere
johnmatthewtennant File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or 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
This file contains hidden or 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
This file contains hidden or 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,85 @@ | ||
| // @vitest-environment jsdom | ||
| import "@testing-library/jest-dom/vitest"; | ||
| import { cleanup, render, screen } from "@testing-library/react"; | ||
| import userEvent from "@testing-library/user-event"; | ||
| import { afterEach, expect, it } from "vitest"; | ||
| import { AuthorSettings } from "./AuthorSettings"; | ||
| import { DEFAULT_RULES, loadPolicy, POLICY_KEY } from "./authors"; | ||
|
|
||
| afterEach(() => { | ||
| cleanup(); | ||
| localStorage.clear(); | ||
| }); | ||
|
|
||
| it("shows the default rules, and saves only rules that parse", async () => { | ||
| const user = userEvent.setup(); | ||
| render(<AuthorSettings active={() => true} />); | ||
| expect(screen.getByRole("status")).toHaveTextContent( | ||
| "Comments from anyone no rule denies can wake your agents.", | ||
| ); | ||
| expect(screen.getByRole("radio", { name: "Wake the agent" })).toBeChecked(); | ||
| const rules = screen.getByRole("textbox", { name: /Rules/ }); | ||
| expect(rules).toHaveValue(DEFAULT_RULES); | ||
| const save = screen.getByRole("button", { name: "Save" }); | ||
| expect(save).toBeDisabled(); | ||
| await user.clear(rules); | ||
| await user.type(rules, "deny author:@me prefix:🤖{Enter}allow author:/(/"); | ||
| await user.click(save); | ||
| expect(rules).toBeInvalid(); | ||
| expect(rules).toHaveAccessibleDescription( | ||
| expect.stringContaining("Line 2: Invalid regular expression"), | ||
| ); | ||
| expect(loadPolicy().text).toBe(DEFAULT_RULES); | ||
| // Editing checks the rules again, so restoring the saved ones clears it. | ||
| await user.clear(rules); | ||
| await user.type(rules, DEFAULT_RULES); | ||
| expect(rules).toBeValid(); | ||
| expect(screen.queryByRole("alert")).not.toBeInTheDocument(); | ||
| expect(save).toBeDisabled(); | ||
| await user.clear(rules); | ||
| await user.type( | ||
| rules, | ||
| "deny author:@me prefix:🤖{Enter}allow org:block{Enter} allow author:copilot-pull-request-reviewer[[bot] ", | ||
| ); | ||
| await user.click(screen.getByRole("radio", { name: "Don’t wake it" })); | ||
| await user.click(save); | ||
| expect(loadPolicy().text).toBe( | ||
| "deny author:@me prefix:🤖\nallow org:block\nallow author:copilot-pull-request-reviewer[bot]", | ||
| ); | ||
| expect(loadPolicy().fallback).toBe("deny"); | ||
| expect(rules).toHaveValue(loadPolicy().text); | ||
| expect(screen.queryByRole("status")).not.toBeInTheDocument(); | ||
| expect(screen.queryByRole("alert")).not.toBeInTheDocument(); | ||
| expect(save).toBeDisabled(); | ||
| await user.click(screen.getByRole("radio", { name: "Wake the agent" })); | ||
| expect(save).toBeEnabled(); | ||
| await user.click(save); | ||
| expect(loadPolicy().fallback).toBe("allow"); | ||
| expect(screen.getByRole("status")).toBeInTheDocument(); | ||
| }); | ||
|
|
||
| it("shows saved rules that no longer parse, with why, until they are fixed", async () => { | ||
| const user = userEvent.setup(); | ||
| const text = "allow author:/^(?!eve$)[a-z]+$/"; | ||
| localStorage.setItem( | ||
| POLICY_KEY, | ||
| JSON.stringify({ version: 2, text, fallback: "deny" }), | ||
| ); | ||
| render(<AuthorSettings active={() => true} />); | ||
| const rules = screen.getByRole("textbox", { name: /Rules/ }); | ||
| expect(rules).toHaveValue(text); | ||
| expect(rules).toBeInvalid(); | ||
| expect(rules).toHaveAccessibleDescription( | ||
| expect.stringContaining("Line 1: a pattern's groups can only be"), | ||
| ); | ||
| expect(screen.getByRole("status")).toHaveTextContent( | ||
| "no comment or review wakes your agents", | ||
| ); | ||
| expect(screen.getByRole("radio", { name: "Don’t wake it" })).toBeChecked(); | ||
| await user.clear(rules); | ||
| await user.type(rules, "deny author:eve{Enter}allow author:/^[[a-z]+$/"); | ||
| await user.click(screen.getByRole("button", { name: "Save" })); | ||
| expect(loadPolicy().error).toBeUndefined(); | ||
| expect(rules).toBeValid(); | ||
| expect(screen.queryByRole("status")).not.toBeInTheDocument(); | ||
| }); |
This file contains hidden or 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,103 @@ | ||
| import { useState } from "react"; | ||
| import { Button } from "../../shared/design-system/ui/Button"; | ||
| import { Field } from "../../shared/design-system/ui/Field"; | ||
| import { Header } from "../../shared/design-system/ui/Header"; | ||
| import { Radio, RadioGroup } from "../../shared/design-system/ui/RadioGroup"; | ||
| import { SettingsGroup } from "../../shared/design-system/ui/SettingsGroup"; | ||
| import { Textarea } from "../../shared/design-system/ui/Textarea"; | ||
| import { loadPolicy, open, parse, savePolicy, type Rule } from "./authors"; | ||
|
|
||
| /** Whose pull request comments and reviews wake agents watching it. */ | ||
| export function AuthorSettings({ active }: { active(): boolean }) { | ||
| const [saved, setSaved] = useState(loadPolicy); | ||
| const [draft, setDraft] = useState(saved.text); | ||
| const [fallback, setFallback] = useState(saved.fallback); | ||
| /** Why the rules could not be saved, apart from why they are not rules. */ | ||
| const [error, setError] = useState(""); | ||
| // After a Save, each edit checks the rules again. | ||
| const [checked, setChecked] = useState(saved.error !== undefined); | ||
| const parsed = parse(draft); | ||
| const dirty = | ||
| typeof parsed === "string" || | ||
| parsed.text !== saved.text || | ||
| fallback !== saved.fallback; | ||
| return ( | ||
| <section aria-label="GitHub comment authors settings"> | ||
| <Header | ||
| title="GitHub comment authors" | ||
| subtitle="Agents watching a pull request are woken by CI, and by the comments and reviews these rules allow." | ||
| /> | ||
| <form | ||
| aria-label="GitHub comment author rules" | ||
| onSubmit={(event) => { | ||
| event.preventDefault(); | ||
| if (!active() || !dirty) return; | ||
| setChecked(true); | ||
| if (typeof parsed === "string") return; | ||
| try { | ||
| const policy = savePolicy(draft, fallback); | ||
| setSaved(policy); | ||
| setDraft(policy.text); | ||
| setChecked(false); | ||
| setError(""); | ||
| } catch (reason) { | ||
| setError(reason instanceof Error ? reason.message : String(reason)); | ||
| } | ||
| }} | ||
| > | ||
| <SettingsGroup layout="form"> | ||
| {saved.error !== undefined && ( | ||
| <p role="status" className="m-0 text-body-sm text-subtle"> | ||
| Your saved rules no longer parse, so no comment or review wakes | ||
| your agents until you fix them. | ||
| </p> | ||
| )} | ||
| {open(saved) && ( | ||
| <p role="status" className="m-0 text-body-sm text-subtle"> | ||
| Comments from anyone no rule denies can wake your agents. To limit | ||
| them, allow only org:name or author:login, and don’t wake them | ||
| when no rule matches. | ||
| </p> | ||
| )} | ||
| <Field | ||
| label="Rules" | ||
| error={checked && typeof parsed === "string" ? parsed : undefined} | ||
| description="One per line: allow or deny, then any of author:login, author:/pattern/, org:organization, body:/pattern/ and prefix:text, which runs to the end of the line. Patterns are JavaScript regular expressions without backreferences, lookaround or escapes JavaScript reads as plain letters, such as \q. body: sees a comment's first 1,000 characters, the most a wake quotes. A bare allow or deny matches everyone. author:@me is your GitHub account, a bot's login ends in [bot], and logins match case aside. The first rule that matches decides. Membership your GitHub account cannot see, or of a misspelled organization, counts as none." | ||
| > | ||
| <Textarea | ||
| rows={6} | ||
| autoCorrect="off" | ||
| autoCapitalize="none" | ||
| spellCheck={false} | ||
| autoComplete="off" | ||
| value={draft} | ||
| onChange={(event) => { | ||
| setDraft(event.target.value); | ||
| }} | ||
| /> | ||
| </Field> | ||
| <Field label="When no rule matches" nativeLabel={false}> | ||
| <RadioGroup<Rule["effect"]> | ||
| aria-label="When no rule matches" | ||
| value={fallback} | ||
| onValueChange={setFallback} | ||
| > | ||
| <Radio value="allow" label="Wake the agent" /> | ||
| <Radio value="deny" label="Don’t wake it" /> | ||
| </RadioGroup> | ||
| </Field> | ||
| {error && ( | ||
|
johnmatthewtennant marked this conversation as resolved.
|
||
| <p role="alert" className="m-0 text-body-sm text-danger"> | ||
| {error} | ||
| </p> | ||
| )} | ||
| <div className="flex flex-wrap items-center justify-end gap-3"> | ||
| <Button type="submit" variant="primary" disabled={!dirty}> | ||
| Save | ||
| </Button> | ||
| </div> | ||
| </SettingsGroup> | ||
| </form> | ||
| </section> | ||
| ); | ||
| } | ||
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.