Skip to content

fix: anchor github-url host to GITHUB_SERVER_URL for GHES support - #40

Merged
phorcys420 merged 2 commits into
mainfrom
phorcys/ghes-github-url
Aug 26, 2026
Merged

fix: anchor github-url host to GITHUB_SERVER_URL for GHES support#40
phorcys420 merged 2 commits into
mainfrom
phorcys/ghes-github-url

Conversation

@phorcys420

@phorcys420 phorcys420 commented Aug 24, 2026

Copy link
Copy Markdown
Member

What

github-url was validated against a regex hardcoded to github.com, so on GitHub Enterprise Server every issue/PR URL (https://github.example.com/owner/repo/pull/1) failed validation and the action exited before commenting.

This anchors the host to the runner-provided GITHUB_SERVER_URL instead. The anchor stays runner-controlled, never user-controlled, so a workflow that templates user input into github-url still cannot redirect the action to an attacker-chosen host.

Fixes #39.

How

  • parseGithubItemURL / deriveCommentKey take an optional serverURL (default https://github.com); GITHUB_SERVER_URL is read at the action.ts edge, matching how GITHUB_WORKFLOW is already handled.
  • The matcher is built with the built-in RegExp.escape(normalizeBaseUrl(serverURL)), so host metacharacters (the dots in github.com) stay literal. No hand-rolled escaping, no URL parsing.
  • Host anchoring semantics are unchanged (scheme + host must equal the server; extra path segments and non-server hosts rejected).
  • The parseGithubURL error message now reflects the resolved server URL.

Tests

bun test (208 pass), typecheck, lint, format:check all clean. New coverage: dotcom still parses, GHES parses when GITHUB_SERVER_URL matches, host-mismatch is rejected (the security case), a dot-in-host isn't treated as a regex wildcard, and deriveCommentKey derives owner/repo#n on GHES.

Implementation plan

Fix: github-url host regex hardcoded to github.com (issue #39)

Make agents-chat-action work on GitHub Enterprise Server by anchoring the
github-url validation to the runner-provided GITHUB_SERVER_URL instead of
the literal github.com, without weakening the security property (the host
anchor stays runner-controlled, never user-controlled).

Repo: coder/agents-chat-action · Branch: phorcys/ghes-github-url

Approach (decisions locked)

  • Build one regex from the server URL, escaped with the built-in
    RegExp.escape.
    No hand-rolled escape helper, no URL parsing. The server
    origin is escaped and interpolated into a single anchored pattern.
    RegExp.escape is Stage 4 and ships in Node 24 (the action runtime).
  • Plumb serverURL in as an optional param (default https://github.com),
    reading process.env.GITHUB_SERVER_URL at the action.ts edge, consistent
    with how GITHUB_WORKFLOW is already read there.
  • deriveCommentKey is GHES-aware too, so markers resolve to
    owner/repo#n on enterprise hosts instead of falling back to the raw URL.
  • Host anchoring semantics preserved and unchanged: the escaped literal
    keeps host matching case-sensitive, exactly as the current github.com
    literal does. Scheme + host must equal the server; extra path segments and
    non-server hosts are rejected.
  • Error messages reflect the real server URL; no extensive docs rewrite.

Changes

src/comment.ts

Replace the module-level GITHUB_URL_REGEX constant with a builder that anchors
to the escaped server origin, and thread serverURL through the two consumers.

const DEFAULT_GITHUB_SERVER_URL = "https://github.com";

// Anchored issue/PR URL matcher for a given GitHub server. RegExp.escape
// neutralizes metacharacters in the host (e.g. the dots in github.com) so it
// matches literally. Anchored at both ends; extra path segments are rejected.
// The trailing (?:[?#].*)? keeps query strings and fragments tolerated.
function githubURLRegex(serverURL: string): RegExp {
	const base = RegExp.escape(normalizeBaseUrl(serverURL));
	return new RegExp(
		`^${base}/([^/]+)/([^/]+)/(?:issues|pull)/(\\d+)/?(?:[?#].*)?$`,
	);
}
  • parseGithubItemURL(input, serverURL = DEFAULT_GITHUB_SERVER_URL): bail on
    empty input, then githubURLRegex(serverURL).exec(input); groups stay
    owner=1, repo=2, number=3, returned via tuple destructuring.
  • normalizeBaseUrl still trims a trailing slash / query / fragment off
    serverURL before it is escaped, so a stray GITHUB_SERVER_URL=https://host/
    does not double the slash in the pattern.
  • Runtime check (must verify in the workspace): confirm RegExp.escape
    exists in the pinned Bun test runtime. It is guaranteed on Node 24 (action
    runtime) but Bun's JSC support is version-dependent. If the pinned Bun lacks
    it, fall back to the generic-host capture-and-compare variant (static regex
    with (https:\/\/[^/]+) origin group compared to normalizeBaseUrl(serverURL)),
    which needs no escaping and no RegExp.escape. Decide based on the test run.
  • deriveCommentKey(...): accept serverURL in its param object and forward it
    to parseGithubItemURL.
  • Update the doc comments on both to say the host is anchored to the current
    GitHub server (GITHUB_SERVER_URL) rather than github.com, preserving the
    "user-controlled github-url cannot redirect to an attacker host" note.
  • normalizeBaseUrl is already imported; reused for the server origin so a
    trailing slash / query / fragment on GITHUB_SERVER_URL is tolerated.

src/action.ts

  • Add a small edge read, e.g. process.env.GITHUB_SERVER_URL || undefined,
    used in the three sites that currently hardcode dotcom:
    • parseGithubURL() → pass to parseGithubItemURL, and rebuild the error
      message to reference the resolved server URL instead of the hardcoded
      https://github.com/... example and the "rejects non-github.com hosts"
      phrasing.
    • commentOnIssue() → pass serverURL into deriveCommentKey.
    • handleFailure() → pass serverURL into deriveCommentKey.
  • Passing undefined triggers the helper default (https://github.com), so
    local/non-Actions callers and existing behavior are unchanged.

src/comment.test.ts

Add parseGithubItemURL coverage (currently only exercised indirectly via
deriveCommentKey):

  • dotcom URL parses with the default server (no serverURL arg).
  • GHES URL (e.g. https://github.acme.example/owner/repo/pull/1) parses when
    serverURL matches.
  • host that does not match serverURL is rejected (returns undefined) — the
    security-relevant case.
  • a deriveCommentKey case passing a GHES serverURL yields owner/repo#n
    (not the raw-URL fallback).

The existing deriveCommentKey "falls back to raw URL for non-github.com host"
test still passes: with the default server, code.acme.com doesn't match.

Docs

No meaningful changes (per scope). The README.md github-url row and the
api_error troubleshooting line still read fine; leaving them avoids churn on
an edge case. (Can revisit if you want a one-line mention.)

Validation

In a workspace (dogfood template, coder org):

  1. Clone coder/agents-chat-action, bun install.
  2. bun test — all pass, new cases included.
  3. bun run typecheck.
  4. bun run lint (Biome).
  5. bun run build if the built dist/ is committed and expected to stay in
    sync (verify whether the repo commits build output before touching it).

Delivery

Open / to confirm during implementation

  • Whether dist/ is committed (affects step 5 and the diff size).
  • Exact final error-message wording in parseGithubURL().

🤖 Opened by Coder Agents on behalf of @phorcys420.

@phorcys420
phorcys420 force-pushed the phorcys/ghes-github-url branch 4 times, most recently from 4c0edbd to f24ecb5 Compare August 24, 2026 22:40
The github-url host was hardcoded to github.com, so every issue/PR URL
on GitHub Enterprise Server failed validation and the action exited
before commenting.

Anchor the host to the runner-provided GITHUB_SERVER_URL instead. The
anchor stays runner-controlled, not user-controlled, so a workflow that
templates user input into github-url still cannot redirect the action to
an attacker-chosen host.

- parseGithubItemURL/deriveCommentKey take an optional serverURL
  (default https://github.com); env is read at the action.ts edge.
- Build the matcher with RegExp.escape so host metacharacters stay
  literal, no hand-rolled escaping.
- Error message reflects the resolved server URL.

Fixes #39
@phorcys420
phorcys420 force-pushed the phorcys/ghes-github-url branch from f24ecb5 to 90dce8b Compare August 24, 2026 22:44
@phorcys420
phorcys420 marked this pull request as ready for review August 26, 2026 11:28
@phorcys420

Copy link
Copy Markdown
Member Author

I don't have a GHES instance to test this on but the tests pass.

@phorcys420
phorcys420 requested review from johnstcn and mafredri and removed request for mafredri August 26, 2026 12:02

@bpmct bpmct left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Approving, but curious about the following

  • what is the point of the "cache" and "map" for GitHub URLs? seems strange
  • how will users discover that this supports self-hosted GitHub Enterprise? is this a common enough convention or will people need this in the README to discover this organically?
From Fable:
Details Yes, both points are valid and supported by the diff:

Cache/Map — legitimate question. The cache holds exactly one entry in production (GITHUB_SERVER_URL is fixed per run, and the parser is called ~3 times total), so it buys nothing. The code comment in the PR even concedes it only grows past one entry under tests.
Discoverability — also legitimate, and arguably the stronger point. GITHUB_SERVER_URL being auto-set by the runner is a common Actions convention (well-behaved actions inherit GHES support silently), so it works organically — but nothing tells users it's supported. Worse than silence: the README currently says the opposite. The github-url inputs row still reads "only https://github.com/... are accepted," and the troubleshooting table lists "non-github.com github-url" as a failure cause. A GHES user reading the README would conclude it doesn't work. So the honest answer to your question is "it needs a README line, and two existing lines are now stale."

@phorcys420

phorcys420 commented Aug 26, 2026

Copy link
Copy Markdown
Member Author

what is the point of the "cache" and "map" for GitHub URLs? seems strange
so, given that the URL is different you need to compute the regex based on the URL, and you don't want to be doing this every single time so you want to keep it around.

the reason it's a map is that the tests will try both github.com and github.example.com and i didn't want to have a more complex logic just for this, or a variable that gets overwritten if the URL is different because it'd be uglier and more code, i'm okay to change it if necessary though.

how will users discover that this supports self-hosted GitHub Enterprise? is this a common enough convention or will people need this in the README to discover this organically?+

GITHUB_SERVER_URL is autofilled but yea the docs can use a change

@phorcys420
phorcys420 requested a review from bpmct August 26, 2026 19:17

@bpmct bpmct left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Again I'm good to merge as-is just a few questions from a noob.

Comment thread README.md
@phorcys420
phorcys420 merged commit 39e7ac2 into main Aug 26, 2026
1 check passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

github-url host regex is hardcoded to github.com, blocking GitHub Enterprise Server

2 participants