#2540 · GitHub plugin silently discards extraRepos entries that are not owner/repo
Verdict: REPRODUCED · Root-cause confidence: high
1. TL;DR
The GitHub plugin accepts a free-form extraRepos string but does not use an invalid entry.
The plugin accepts only an exact owner/repo name. It ignores a wildcard or a typo without a useful message.
A focused plugin test reproduced the problem. The CLI returned the valid sibling, empty standard error, and no related log entry.
The cause is one conditional branch. The branch adds valid names and has no path for invalid names.
Pull request 2545 fixes the full user path. Pull request 2541 adds a warning but repeats it after each repository discovery.
2. Claims vs findings
| Claim from the issue | Status | Evidence |
|---|---|---|
Repository discovery drops MPIV-AI/* when it receives that entry. |
Verified | The focused test passed a mixed value directly to the plugin host. Discovery returned only acme/widgets. |
The real settings route stores MPIV-AI/*. |
Unverified | The schema accepts any string, but the focused test does not write or read the value through the server route. |
| The plugin gives no warning for the invalid entry. | Verified | The test returned stderr: "" and logEntries: []. |
Any value that fails /^[\w.-]+\/[\w.-]+$/ has the same result. |
Verified | The discovery loop uses that check as its only entry path. It has no invalid-entry branch. |
| A valid sibling still works. | Verified | The mixed value acme/widgets, ACME/* returned acme/widgets. |
| The problem is not a GitHub CLI authentication or sync failure. | Verified | The reproduction reaches repository discovery without a network call. The filter runs before a repository sync. |
The bug remains after the base commit on origin/main. |
Verified | A fresh fetch resolved origin/main to 6ab90856f5a1148707529b8e46f2a6687fc1d20a. The server.ts log and diff stayed empty. |
| The packaged desktop application 0.39.0 has the same result. | Unverified | This investigation tested the source commit. It did not start the packaged desktop application. |
3. Environment
- bb commit:
ad79bbb5ec909524f8f281e62d860c588a86f332. - System: Linux 7.0.0-29-generic, x86_64.
- Node: 24.18.0. pnpm: 9.15.0. Vitest: 4.1.1.
- GitHub CLI: 2.96.0. Git: 2.53.0.
- No AI provider process was necessary.
- The fake plugin host ran the real plugin entry and the registered CLI command.
- No dev instance ran. The focused test used no ports or data directory.
4. Minimal reproduction
- Check out the base commit and install dependencies.
git checkout ad79bbb5ec909524f8f281e62d860c588a86f332 pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build
- Create the focused test in the GitHub plugin package.
cat > plugins/github/issue-2540.repro.test.ts <<'EOF' import { describe, expect, it } from "vitest"; import { createFakePluginHost } from "@get-bb/plugin-sdk/testing"; import plugin from "./server"; describe("issue 2540: invalid extraRepos diagnostics", () => { it("reports an invalid entry while it keeps a valid sibling", async () => { const { bb, harness } = createFakePluginHost({ pluginId: "github", settings: { extraRepos: "acme/widgets, ACME/*" }, sdk: { projects: { list: async () => [] } }, }); await plugin(bb); const result = await harness.runCli(["repos"]); console.info(JSON.stringify({ result, logEntries: harness.logEntries }, null, 2)); expect(result.stdout).toBe("acme/widgets"); expect(harness.logEntries).toContainEqual( expect.objectContaining({ level: "warn", message: expect.stringMatching(/extraRepos.*ACME\/\*/i), }), ); }); }); EOF - Run the test through Turbo.
pnpm exec turbo run test --filter=bb-plugin-github -- --run issue-2540.repro.test.ts
Expected: The plugin adds a warning that identifies ACME/*.
{
"result": {
"exitCode": 0,
"stdout": "acme/widgets",
"stderr": "<a diagnostic that identifies ACME/*>"
},
"logEntries": [
{ "level": "warn", "message": "<a diagnostic that identifies ACME/*>" }
]
}
Actual at the base commit:
{
"result": {
"exitCode": 0,
"stdout": "acme/widgets",
"stderr": ""
},
"logEntries": []
}
AssertionError: expected [] to deep equally contain ObjectContaining{…}
The valid sibling proves that the setting reached discovery. The empty diagnostic data proves the silent failure.
Repro files: test source · base log
Focused test source
import { describe, expect, it } from "vitest";
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import plugin from "./server";
describe("issue 2540: invalid extraRepos diagnostics", () => {
it("reports an invalid entry while it keeps a valid sibling", async () => {
const { bb, harness } = createFakePluginHost({
pluginId: "github",
settings: { extraRepos: "acme/widgets, ACME/*" },
sdk: { projects: { list: async () => [] } },
});
await plugin(bb);
const result = await harness.runCli(["repos"]);
console.info(JSON.stringify({ result, logEntries: harness.logEntries }, null, 2));
expect(result.stdout).toBe("acme/widgets");
expect(harness.logEntries).toContainEqual(
expect.objectContaining({
level: "warn",
message: expect.stringMatching(/extraRepos.*ACME\/\*/i),
}),
);
});
});
5. Root cause
The settings schema accepts any string. Its description gives the expected list shape but does not validate the value.
479 extraRepos: {
480 type: "string",
481 label: "Extra repositories",
482 description:
483 'Comma-separated "owner/repo" list to track in addition to repos discovered from BB projects.',
484 default: "",
485 },
See plugins/github/server.ts lines 479–485.
The name check excludes *. It permits only word characters, periods, and hyphens on each side.
344 function isRepoName(value: unknown): value is string {
345 return typeof value === "string" && /^[\w.-]+\/[\w.-]+$/.test(value);
346 }
See plugins/github/server.ts lines 344–346.
The discovery loop adds a name only when the check succeeds. The loop has no branch for a failed check.
611 const { extraRepos } = await settings.get();
612 for (const raw of extraRepos.split(/[\s,]+/)) {
613 if (isRepoName(raw) && !byRepo.has(raw)) {
614 byRepo.set(raw, { repo: raw, projectId: null });
615 }
616 }
See plugins/github/server.ts lines 611–616.
The repos command prints only the discovery result. Discovery does not return invalid names or diagnostics.
See plugins/github/server.ts lines 1568–1578.
This mechanism explains all visible results. The setting remains stored, the wildcard adds nothing, and the CLI has nothing to report.
The cause is local to the GitHub plugin server. No server-to-daemon message changes.
6. Proposed fix (first principles)
Treat the settings read as a data boundary. Parse the string into valid and invalid lists once.
Keep valid names in discovery. Report invalid names on the CLI surface and in the plugin log.
Emit one log warning for each distinct invalid set. This prevents a repeated warning during each five-minute sync.
State that wildcards do not work in the setting documentation. Issue 2543 can address repository selection as a separate feature.
This fix does not change a wire contract. It does not need a host daemon protocol version change.
Pull request 2545 implements this design. It also keeps standard output stable for scripts.
7. PR review
PR 2541 · Warn when an extraRepos entry cannot track a repository
This change separates valid and invalid entries. It logs each invalid entry from repository discovery.
Root cause: It addresses the silent filter directly.
| Severity | Finding | Evidence |
|---|---|---|
| Major | The change emits the same warning after each forced discovery. | plugins/github/server.ts:637–641 has no state check. Two repos calls produced two equal warnings. |
| Moderate | The repository-list command still returns empty standard error. | The reporter used this command. The fix puts the message only in the server log. |
| Test gap | The added behavior test checks only that one warning exists. | plugins/github/server.rpc.test.ts:351–366 does not call discovery twice. |
Tests: The typecheck and 28 tests passed, including the base failure test. The hostile repeat test failed with two warnings.
CI: All required GitHub checks shown for the PR passed. A local merge-tree check against origin/main passed.
Protocol: No protocol version change is necessary because the patch changes only plugin-local behavior.
Verdict: REQUEST CHANGES. Add warning suppression and a direct CLI diagnostic, or close this PR for PR 2545.
Evidence: diff · suite log · repeat failure
PR 2545 · Report extraRepos entries that are not owner/repo
This change parses and removes duplicate entries. It reports invalid names on standard error and in the plugin log.
It stores the last invalid set. It emits a new log warning only when that set changes.
It also updates the GitHub plugin documentation.
Root cause: It addresses the silent filter and the user-visible CLI path.
| Severity | Finding | Evidence |
|---|---|---|
| No blocker | The warning state prevents repeated log messages. | plugins/github/server.ts:653–663 compares each invalid set before it logs. |
| No blocker | The CLI returns the diagnostic on standard error and keeps the repository list on standard output. | plugins/github/server.ts:1615–1634 keeps the two outputs separate. |
| Known limit | The settings form still stores an invalid value without an inline error. | The plugin settings contract has no custom validation hook. The CLI and log now give a clear message. |
Tests: Typecheck passed. All 30 tests passed, including both hostile tests.
CI: All required GitHub checks shown for the PR passed. A local merge-tree check against origin/main passed.
Protocol: No protocol version change is necessary because the patch changes only plugin-local behavior.
Verdict: MERGE. This PR fixes the root cause without log noise or output breakage.
8. Related issues
- #2543 requests a repository picker. It covers repository selection instead of invalid input diagnostics.
- #2313 covers GitHub Enterprise Server hosts. It has a different cause.
9. Appendix
Artifacts
- Base failure test
- Warning repeat test
- Base failure log
- PR 2541 suite log
- PR 2541 hostile failure log
- PR 2545 suite log
- PR 2541 diff
- PR 2545 diff
- Later main path log
Commands
gh issue view 2540 --comments gh issue view 2540 --json number,title,body,comments,labels,state,author,createdAt,updatedAt,url,projectItems pnpm install --frozen-lockfile --prefer-offline pnpm exec turbo run build git fetch origin main git blame -L 330,365 -- plugins/github/server.ts git blame -L 460,500 -- plugins/github/server.ts git blame -L 590,635 -- plugins/github/server.ts pnpm exec turbo run test --filter=bb-plugin-github -- --run issue-2540.repro.test.ts gh pr view 2541 --json ... gh pr diff 2541 gh pr checkout 2541 --detach pnpm exec turbo run typecheck test --filter=bb-plugin-github pnpm exec turbo run test --filter=bb-plugin-github -- --run issue-2540-warning-repeat.test.ts gh pr view 2545 --json ... gh pr diff 2545 gh pr checkout 2545 --detach pnpm exec turbo run typecheck test --filter=bb-plugin-github git merge-tree --write-tree origin/main 840e6d5f15d719575bde78d9dee6a9cd25e9160c git merge-tree --write-tree origin/main d49902d156a1cc680a11e101cd2ba9d581301547 git log ad79bbb5ec909524f8f281e62d860c588a86f332..origin/main --oneline -- plugins/github/server.ts git diff --stat ad79bbb5ec909524f8f281e62d860c588a86f332..origin/main -- plugins/github/server.ts
No screenshot applies. The failure affects CLI and log output only.
Verification
The verifier ran the original download command and received HTTP 404. The verifier then used the local artifact and reproduced the failure.
This revision embeds the complete test creation command. I ran that final test at the base commit and replaced the base log.
I also fetched current origin/main, limited the path check to plugins/github/server.ts, and replaced the path log.
The first claim now states only what the focused test proves. A separate row marks the real settings route as unverified.