← reports

#2540 · GitHub plugin silently discards extraRepos entries that are not owner/repo

Bug Priority: Medium Effort: Low github cli ui open on GitHub 2026-08-27 · base ad79bbb5ec909524f8f281e62d860c588a86f332

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

4. Minimal reproduction

  1. Check out the base commit and install dependencies.
    git checkout ad79bbb5ec909524f8f281e62d860c588a86f332
    pnpm install --frozen-lockfile --prefer-offline
    pnpm exec turbo run build
  2. 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
  3. 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.

SeverityFindingEvidence
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.

SeverityFindingEvidence
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.

Evidence: diff · suite log

8. Related issues

9. Appendix

Artifacts

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.