The workflow
Use the GitHub plugin against a GitHub Enterprise Server host instead of github.com. An org whose repos live on ghe.example.com wants the same issue/PR panel, mention providers, and thread-spawning the plugin gives github.com users.
What happens today
The plugin cannot reach a GHE host at all. Three independent pins, at 7e10ea4b3742f3d14bde086c2963118238fec3ab:
-
Auth is pinned. GH_HOST = "github.com" is passed explicitly as --hostname to both auth probes — gh auth status and gh auth token. A gh logged into only a GHE host fails both, so the plugin latches needs_configuration and nothing downstream runs.
-
Repo discovery is pinned. parseGithubRemote matches github.com[:/] literally, so a GHE remote yields no repos.
-
The manual escape hatch is closed. extraRepos entries are filtered by isRepoName at the discovery loop, a two-segment pattern that rejects the host/owner/repo form gh -R accepts for enterprise hosts.
Repro: plugins/github/server.ghe.test.ts (inlined at the end of this issue), built on the fake-gh harness already in server.auth-latch.test.ts — a gh 2.96 stub on PATH whose only credentials are for ghe.acme.com, driving the real plugin through createFakePluginHost.
pnpm exec turbo run test --filter=bb-plugin-github
✓ cannot authenticate against a GHE host: the probe is pinned to github.com 251ms
× keeps a GHE repo from extraRepos instead of silently dropping it 303ms
AssertionError: expected [] to deeply equal [ 'ghe.acme.com/platform/tools' ]
Tests 1 failed | 26 passed (27)
The first test passes on main and characterizes the bug: with valid GHE credentials present, ghState is needs_configuration, every auth call carries --hostname github.com, and ghe.acme.com never appears in the gh call log at all. The second asserts the desired extraRepos behavior and fails on main.
Scope of what this proves: the plugin refuses a correctly-configured GHE gh and never contacts the host. It does not prove the three pins are the only github.com assumptions — nothing has executed past the auth gate against a real GHE API, so there may be more behind it. Tested at 7e10ea4b3742f3d14bde086c2963118238fec3ab, macOS 15.6, bb 0.39.0 from source.
What you would expect
A configurable GitHub host — a plugin setting defaulting to github.com — threaded through all three pins: both --hostname flags, the remote pattern, and the accepted repo-name shape.
Context and alternatives
Ruled out — setting GH_HOST in the server environment is not a workaround. run() passes no env override, so gh does inherit it and gh api / gh pr list would follow the enterprise host. But the explicit --hostname github.com on the auth probes beats GH_HOST, and repo discovery still matches nothing. The data plane would work; the auth and discovery planes still fail.
Ruled out — not a duplicate. Searched open and closed issues for "GitHub Enterprise", "GHE", "GH_HOST", and "enterprise"; no matches.
Alternative considered: github is in OFFICIAL_PLUGINS, not BUILTIN_PLUGINS (builtin-registry.ts#L158-L160), so it is store-only and an org can ship a private fork under a different plugin id instead. That works, but every GHE org carries the same four-line divergence forever, which is why this seems worth having upstream.
Scope: four edits in one file — the constant becomes a setting, it feeds both --hostname flags, the remote pattern is built from it, and isRepoName widens to accept the three-segment form. Happy to send the PR if the approach sounds right.
Repro test
Drop this at plugins/github/server.ghe.test.ts and run pnpm exec turbo run test --filter=bb-plugin-github.
plugins/github/server.ghe.test.ts
// Repro: the github plugin is pinned to github.com and cannot be pointed at a
// GitHub Enterprise Server host.
//
// A fake `gh` on PATH holds credentials for `ghe.acme.com` only — the exact
// state of a developer at an org that runs GHE. github.com is not configured.
import {
chmodSync,
existsSync,
mkdtempSync,
readFileSync,
rmSync,
writeFileSync,
} from "node:fs";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { afterEach, beforeEach, describe, expect, it } from "vitest";
import { createFakePluginHost } from "@get-bb/plugin-sdk/testing";
import plugin from "./server";
const GHE_HOST = "ghe.acme.com";
let binDir: string;
let callLog: string;
const originalPath = process.env.PATH;
function ghCalls(): string[] {
if (!existsSync(callLog)) return [];
return readFileSync(callLog, "utf8")
.trim()
.split("\n")
.filter((line: string) => line.length > 0);
}
beforeEach(() => {
binDir = mkdtempSync(join(tmpdir(), "bb-ghe-"));
callLog = join(binDir, "gh-calls.log");
// Mimics real `gh` 2.96 logged into GHE only: any github.com-scoped auth
// command fails with gh's verbatim wording, the GHE host succeeds.
writeFileSync(
join(binDir, "gh"),
`#!/usr/bin/env bash
echo "$*" >> "${callLog}"
case "$1 $2" in
"--version ") echo "gh version 2.96.0 (fake)"; exit 0;;
"auth token")
case " $* " in
*" ${GHE_HOST} "*) echo "gho_fake_ghe_token"; exit 0;;
*) echo "no oauth token found for github.com" >&2; exit 1;;
esac;;
"auth status")
case " $* " in
*" ${GHE_HOST} "*)
echo "${GHE_HOST}"
echo " ✓ Logged in to ${GHE_HOST} account someone (keyring)"; exit 0;;
*)
echo "You are not logged into any accounts on github.com" >&2; exit 1;;
esac;;
*) echo "[]"; exit 0;;
esac
`,
);
chmodSync(join(binDir, "gh"), 0o755);
process.env.PATH = `${binDir}:${originalPath ?? ""}`;
});
afterEach(() => {
process.env.PATH = originalPath;
rmSync(binDir, { recursive: true, force: true });
});
describe("github plugin on GitHub Enterprise", () => {
// Characterization of today's behavior: passes on main, and should be
// inverted by any fix that makes the host configurable.
it("cannot authenticate against a GHE host: the probe is pinned to github.com", async () => {
const { bb, harness } = createFakePluginHost({ pluginId: "github" });
await plugin(bb);
const status = (await harness.callRpc("status")) as {
ghOk: boolean;
ghState: string;
};
// gh is installed and holds valid credentials — for GHE. The plugin
// reports itself unconfigured anyway.
expect(status.ghOk).toBe(false);
expect(status.ghState).toBe("needs_configuration");
// Root cause: every auth call names github.com explicitly, so `GH_HOST`
// in the server environment cannot redirect it. The GHE host is never
// asked about at all.
const authCalls = ghCalls().filter((line) => line.startsWith("auth "));
expect(authCalls.length).toBeGreaterThan(0);
for (const call of authCalls) {
expect(call).toContain("--hostname github.com");
}
expect(ghCalls().join("\n")).not.toContain(GHE_HOST);
});
it("keeps a GHE repo from extraRepos instead of silently dropping it", async () => {
const { bb, harness } = createFakePluginHost({
pluginId: "github",
settings: { extraRepos: `${GHE_HOST}/platform/tools` },
});
await plugin(bb);
const status = (await harness.callRpc("status")) as {
repos: { repo: string }[];
};
// Desired behavior — FAILS on main. `gh -R host/owner/repo` is the
// documented enterprise form, but isRepoName() accepts exactly two
// segments, so today the entry is discarded silently: no error, no
// warning, `repos` just comes back empty and the panel stays blank.
expect(status.repos.map((entry) => entry.repo)).toEqual([
`${GHE_HOST}/platform/tools`,
]);
});
});
AGENT GENERATED
The workflow
Use the GitHub plugin against a GitHub Enterprise Server host instead of github.com. An org whose repos live on
ghe.example.comwants the same issue/PR panel, mention providers, and thread-spawning the plugin gives github.com users.What happens today
The plugin cannot reach a GHE host at all. Three independent pins, at
7e10ea4b3742f3d14bde086c2963118238fec3ab:Auth is pinned.
GH_HOST = "github.com"is passed explicitly as--hostnameto both auth probes —gh auth statusandgh auth token. Aghlogged into only a GHE host fails both, so the plugin latchesneeds_configurationand nothing downstream runs.Repo discovery is pinned.
parseGithubRemotematchesgithub.com[:/]literally, so a GHE remote yields no repos.The manual escape hatch is closed.
extraReposentries are filtered byisRepoNameat the discovery loop, a two-segment pattern that rejects thehost/owner/repoformgh -Raccepts for enterprise hosts.Repro:
plugins/github/server.ghe.test.ts(inlined at the end of this issue), built on the fake-ghharness already inserver.auth-latch.test.ts— agh2.96 stub onPATHwhose only credentials are forghe.acme.com, driving the real plugin throughcreateFakePluginHost.The first test passes on
mainand characterizes the bug: with valid GHE credentials present,ghStateisneeds_configuration, everyauthcall carries--hostname github.com, andghe.acme.comnever appears in theghcall log at all. The second asserts the desiredextraReposbehavior and fails onmain.Scope of what this proves: the plugin refuses a correctly-configured GHE
ghand never contacts the host. It does not prove the three pins are the only github.com assumptions — nothing has executed past the auth gate against a real GHE API, so there may be more behind it. Tested at7e10ea4b3742f3d14bde086c2963118238fec3ab, macOS 15.6, bb 0.39.0 from source.What you would expect
A configurable GitHub host — a plugin setting defaulting to
github.com— threaded through all three pins: both--hostnameflags, the remote pattern, and the accepted repo-name shape.Context and alternatives
Ruled out — setting
GH_HOSTin the server environment is not a workaround.run()passes noenvoverride, soghdoes inherit it andgh api/gh pr listwould follow the enterprise host. But the explicit--hostname github.comon the auth probes beatsGH_HOST, and repo discovery still matches nothing. The data plane would work; the auth and discovery planes still fail.Ruled out — not a duplicate. Searched open and closed issues for "GitHub Enterprise", "GHE", "GH_HOST", and "enterprise"; no matches.
Alternative considered:
githubis inOFFICIAL_PLUGINS, notBUILTIN_PLUGINS(builtin-registry.ts#L158-L160), so it is store-only and an org can ship a private fork under a different plugin id instead. That works, but every GHE org carries the same four-line divergence forever, which is why this seems worth having upstream.Scope: four edits in one file — the constant becomes a setting, it feeds both
--hostnameflags, the remote pattern is built from it, andisRepoNamewidens to accept the three-segment form. Happy to send the PR if the approach sounds right.Repro test
Drop this at
plugins/github/server.ghe.test.tsand runpnpm exec turbo run test --filter=bb-plugin-github.plugins/github/server.ghe.test.ts