Skip to content

Commit 0695de5

Browse files
committed
feat: resolve GitHub host at runtime for portable OCI image
Read GITHUB_HOST at request time and inject it to the client via window.__GITHUB_HOST__, so a single prebuilt image can target any GitHub instance without baking NEXT_PUBLIC_GITHUB_HOST at build.
1 parent 26b00e0 commit 0695de5

6 files changed

Lines changed: 84 additions & 27 deletions

File tree

apps/web/.env.example

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,16 @@
99
GITHUB_CLIENT_ID=your_github_oauth_app_client_id
1010
GITHUB_CLIENT_SECRET=your_github_oauth_app_client_secret
1111

12-
# GitHub host (optional — defaults to "github.com" if both are unset).
13-
# Set BOTH variables to the SAME value:
12+
# GitHub host (optional — defaults to "github.com" when unset).
1413
# - github.com → GitHub.com (cloud, default)
1514
# - <tenant>.ghe.com → GitHub Enterprise Cloud with Data Residency
1615
# - github.your-company.com → GitHub Enterprise Server (self-hosted)
16+
# GITHUB_HOST is read at runtime (server + injected to the client), so it alone
17+
# is enough — including for the prebuilt OCI image. NEXT_PUBLIC_GITHUB_HOST is
18+
# optional and only needed if you build the client bundle yourself and want the
19+
# value baked in. When set, use the SAME value for both.
1720
# Example for a ghe.com tenant called "acme":
1821
# GITHUB_HOST=acme.ghe.com
19-
# NEXT_PUBLIC_GITHUB_HOST=acme.ghe.com
2022
# GITHUB_HOST=github.com
2123
# NEXT_PUBLIC_GITHUB_HOST=github.com
2224

apps/web/src/app/layout.tsx

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import { cookies } from "next/headers";
77
import "./globals.css";
88
import { generateThemeScript } from "@/lib/theme-script";
99
import { listThemes } from "@/lib/themes";
10+
import { getGithubHost } from "@/lib/github-host-client";
1011
import { QueryProvider } from "@/components/providers/query-provider";
1112
import { SWRegister } from "@/components/pwa/sw-register";
1213
import { Analytics } from "@vercel/analytics/next";
@@ -112,6 +113,16 @@ export default async function RootLayout({
112113
: {})}
113114
>
114115
<head>
116+
{/*
117+
* Expose the runtime GitHub host to the client before hydration
118+
* so a single prebuilt image can target any instance via the
119+
* GITHUB_HOST env var (read here on the server at request time).
120+
*/}
121+
<script
122+
dangerouslySetInnerHTML={{
123+
__html: `window.__GITHUB_HOST__=${JSON.stringify(getGithubHost())};`,
124+
}}
125+
/>
115126
{mpStyle && (
116127
<style
117128
dangerouslySetInnerHTML={{
Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,50 @@
1+
/**
2+
* Client-safe, **runtime** resolution of the configured GitHub host.
3+
*
4+
* `NEXT_PUBLIC_*` env vars are inlined into the client bundle at build time, so
5+
* a prebuilt image (e.g. the published OCI image) would otherwise freeze the
6+
* host at build. To keep a single image runtime-configurable, the host is read
7+
* dynamically instead:
8+
*
9+
* - In the browser it reads `window.__GITHUB_HOST__`, which the root layout
10+
* injects from the server's `GITHUB_HOST` env var at request time.
11+
* - During SSR (and as a fallback) it reads the runtime env directly.
12+
* - Defaults to `github.com` when nothing is configured.
13+
*
14+
* Setting `GITHUB_HOST` in the container is therefore enough;
15+
* `NEXT_PUBLIC_GITHUB_HOST` remains supported for local dev convenience.
16+
*/
17+
18+
declare global {
19+
interface Window {
20+
__GITHUB_HOST__?: string;
21+
}
22+
}
23+
24+
function normalizeHost(value: string): string {
25+
return value
26+
.trim()
27+
.toLowerCase()
28+
.replace(/^https?:\/\//, "")
29+
.replace(/\/+$/, "");
30+
}
31+
32+
/** Resolve the active GitHub hostname (e.g. `github.com`, `acme.ghe.com`). */
33+
export function getGithubHost(): string {
34+
if (typeof window !== "undefined" && window.__GITHUB_HOST__) {
35+
return normalizeHost(window.__GITHUB_HOST__);
36+
}
37+
return normalizeHost(
38+
process.env.NEXT_PUBLIC_GITHUB_HOST || process.env.GITHUB_HOST || "github.com",
39+
);
40+
}
41+
42+
/** Whether the active host is a GitHub Enterprise instance (not github.com). */
43+
export function isGithubEnterprise(): boolean {
44+
return getGithubHost() !== "github.com";
45+
}
46+
47+
/** Web URL for the active host, e.g. `https://github.com`. */
48+
export function githubWebOrigin(): string {
49+
return `https://${getGithubHost()}`;
50+
}

apps/web/src/lib/github-host.ts

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -8,8 +8,10 @@ import { Octokit } from "@octokit/rest";
88
* - `acme.ghe.com` → GitHub Enterprise Cloud with Data Residency
99
* - `github.acme-corp.com` → GitHub Enterprise Server (self-hosted)
1010
*
11-
* Set both `GITHUB_HOST` (server) and `NEXT_PUBLIC_GITHUB_HOST` (client) to the
12-
* same value so OAuth links rendered in the browser match what the server uses.
11+
* Set `GITHUB_HOST` (server) to point at an instance. It is read at runtime and
12+
* also injected to the client, so a single prebuilt image is configurable via
13+
* this one var. `NEXT_PUBLIC_GITHUB_HOST` is optional (build-time bake) and,
14+
* when set, should match `GITHUB_HOST`.
1315
*/
1416
export const GITHUB_HOST = (
1517
process.env.NEXT_PUBLIC_GITHUB_HOST ||

apps/web/src/lib/github-signin.ts

Lines changed: 11 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,25 @@
11
"use client";
22

33
import { authClient, signIn } from "./auth-client";
4+
import { getGithubHost, githubWebOrigin, isGithubEnterprise } from "./github-host-client";
45

56
/**
67
* Hostname of the configured GitHub instance, exposed to the client.
7-
* Defaults to `github.com` when `NEXT_PUBLIC_GITHUB_HOST` is unset.
8+
*
9+
* Resolved at **runtime** (see `github-host-client`), so a single prebuilt
10+
* image can target any GitHub instance via the `GITHUB_HOST` env var.
811
*/
9-
export const GITHUB_HOST = (process.env.NEXT_PUBLIC_GITHUB_HOST || "github.com")
10-
.trim()
11-
.toLowerCase()
12-
.replace(/^https?:\/\//, "")
13-
.replace(/\/+$/, "");
12+
export const GITHUB_HOST = getGithubHost();
1413

15-
export const IS_GITHUB_ENTERPRISE = GITHUB_HOST !== "github.com";
14+
export const IS_GITHUB_ENTERPRISE = isGithubEnterprise();
1615

1716
/** Convenience: web URL for the active host (e.g. for "Open in GitHub" links). */
18-
export const GITHUB_WEB_URL = `https://${GITHUB_HOST}`;
17+
export const GITHUB_WEB_URL = githubWebOrigin();
1918

2019
export function githubWebUrl(path = ""): string {
21-
if (!path) return GITHUB_WEB_URL;
22-
return `${GITHUB_WEB_URL}${path.startsWith("/") ? path : `/${path}`}`;
20+
const origin = githubWebOrigin();
21+
if (!path) return origin;
22+
return `${origin}${path.startsWith("/") ? path : `/${path}`}`;
2323
}
2424

2525
/**
@@ -31,7 +31,7 @@ export function signInWithGitHub(opts: {
3131
scopes: string[];
3232
callbackURL?: string;
3333
}): Promise<unknown> {
34-
if (IS_GITHUB_ENTERPRISE) {
34+
if (isGithubEnterprise()) {
3535
return authClient.signIn.oauth2({
3636
providerId: "github",
3737
callbackURL: opts.callbackURL,

apps/web/src/lib/github-utils.ts

Lines changed: 3 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,5 @@
1+
import { getGithubHost } from "./github-host-client";
2+
13
export const LANGUAGE_COLORS: Record<string, string> = {
24
TypeScript: "#3178c6",
35
JavaScript: "#f1e05a",
@@ -250,18 +252,8 @@ function parsePositiveInt(value: string | undefined): number | null {
250252
}
251253

252254
/** Hostname of the active GitHub instance (also `github.com` by default). */
253-
const GITHUB_WEB_HOSTNAME = (
254-
process.env.NEXT_PUBLIC_GITHUB_HOST ||
255-
process.env.GITHUB_HOST ||
256-
"github.com"
257-
)
258-
.trim()
259-
.toLowerCase()
260-
.replace(/^https?:\/\//, "")
261-
.replace(/\/+$/, "");
262-
263255
export function isKnownGithubHostname(hostname: string): boolean {
264-
return hostname === "github.com" || hostname === GITHUB_WEB_HOSTNAME;
256+
return hostname === "github.com" || hostname === getGithubHost();
265257
}
266258

267259
export function parseGitHubUrl(htmlUrl: string): ParsedGitHubUrl | null {

0 commit comments

Comments
 (0)