From 587f646a55c57382f0bf42fdb82e536725a8e6ba Mon Sep 17 00:00:00 2001 From: Tyler Chong Date: Fri, 30 Oct 2026 14:33:08 +0000 Subject: [PATCH 01/12] Document GHES startup failure after restart (#64470) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com> Co-authored-by: isaacmbrown <101839405+isaacmbrown@users.noreply.github.com> --- data/release-notes/enterprise-server/3-18/16.yml | 2 ++ data/release-notes/enterprise-server/3-19/13.yml | 2 ++ data/release-notes/enterprise-server/3-20/9.yml | 2 ++ 3 files changed, 6 insertions(+) diff --git a/data/release-notes/enterprise-server/3-18/16.yml b/data/release-notes/enterprise-server/3-18/16.yml index f7827ae5ab16..aae46f8d36ec 100644 --- a/data/release-notes/enterprise-server/3-18/16.yml +++ b/data/release-notes/enterprise-server/3-18/16.yml @@ -20,6 +20,8 @@ sections: - | To avoid data processing delays on instances with a large number of internal message topics, administrators can configure the network timeouts used during topic discovery via `app.github.topic-checker-connect-timeout-sec` and `app.github.topic-checker-socket-timeout-sec`. The default timeout has been increased from 10 to 30 seconds, and values between 1 and 300 seconds are accepted. known_issues: + - | + After a restart, an instance can remain unavailable with "Starting..." displayed in the web interface. To recover, an administrator can connect using administrative SSH and rerun `ghe-config-apply`. This provides temporary recovery but does not prevent the issue from recurring. This issue is resolved in {% data variables.product.prodname_ghe_server %} 3.21.0 and later. [Updated: 2026-10-30] - | During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading. - | diff --git a/data/release-notes/enterprise-server/3-19/13.yml b/data/release-notes/enterprise-server/3-19/13.yml index 740600e85193..dde2a1b04851 100644 --- a/data/release-notes/enterprise-server/3-19/13.yml +++ b/data/release-notes/enterprise-server/3-19/13.yml @@ -28,6 +28,8 @@ sections: - | To avoid data processing delays on instances with a large number of internal message topics, administrators can configure the network timeouts used during topic discovery via `app.github.topic-checker-connect-timeout-sec` and `app.github.topic-checker-socket-timeout-sec`. The default timeout has been increased from 10 to 30 seconds, and values between 1 and 300 seconds are accepted. known_issues: + - | + After a restart, an instance can remain unavailable with "Starting..." displayed in the web interface. To recover, an administrator can connect using administrative SSH and rerun `ghe-config-apply`. This provides temporary recovery but does not prevent the issue from recurring. This issue is resolved in {% data variables.product.prodname_ghe_server %} 3.21.0 and later. [Updated: 2026-10-30] - | During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading. - | diff --git a/data/release-notes/enterprise-server/3-20/9.yml b/data/release-notes/enterprise-server/3-20/9.yml index 093a6d487c76..1e234cd9734a 100644 --- a/data/release-notes/enterprise-server/3-20/9.yml +++ b/data/release-notes/enterprise-server/3-20/9.yml @@ -35,6 +35,8 @@ sections: - | To avoid data processing delays on instances with a large number of internal message topics, administrators can configure the network timeouts used during topic discovery via `app.github.topic-checker-connect-timeout-sec` and `app.github.topic-checker-socket-timeout-sec`. The default timeout has been increased from 10 to 30 seconds, and values between 1 and 300 seconds are accepted. known_issues: + - | + After a restart, an instance can remain unavailable with "Starting..." displayed in the web interface. To recover, an administrator can connect using administrative SSH and rerun `ghe-config-apply`. This provides temporary recovery but does not prevent the issue from recurring. This issue is resolved in {% data variables.product.prodname_ghe_server %} 3.21.0 and later. [Updated: 2026-10-30] - | During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading. - | From fcb2b3d291f38ef08a7859318919f8022ce4a730 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:27:19 +0000 Subject: [PATCH 02/12] Remove duplicate GHES known issue from 5 release notes (#64462) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf5e163d-62e9-40c9-a65a-7eae1ee0392e --- data/release-notes/enterprise-server/3-18/15.yml | 2 -- data/release-notes/enterprise-server/3-19/12.yml | 2 -- data/release-notes/enterprise-server/3-20/8.yml | 2 -- data/release-notes/enterprise-server/3-21/6.yml | 2 -- data/release-notes/enterprise-server/3-22/1.yml | 2 -- src/release-notes/tests/yaml.ts | 14 ++++++++++++++ 6 files changed, 14 insertions(+), 10 deletions(-) diff --git a/data/release-notes/enterprise-server/3-18/15.yml b/data/release-notes/enterprise-server/3-18/15.yml index 9b7e274dc51c..ca805d3ffaec 100644 --- a/data/release-notes/enterprise-server/3-18/15.yml +++ b/data/release-notes/enterprise-server/3-18/15.yml @@ -84,8 +84,6 @@ sections: When restoring from a backup snapshot, a large number of `mapper_parsing_exception` errors may be displayed. - | When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - - | - When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - | In a cluster, the host running restore requires access the storage nodes via their private IPs. - | diff --git a/data/release-notes/enterprise-server/3-19/12.yml b/data/release-notes/enterprise-server/3-19/12.yml index 720035ec4ace..2f8ac4c746b2 100644 --- a/data/release-notes/enterprise-server/3-19/12.yml +++ b/data/release-notes/enterprise-server/3-19/12.yml @@ -90,8 +90,6 @@ sections: When restoring from a backup snapshot, a large number of `mapper_parsing_exception` errors may be displayed. - | When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - - | - When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - | In a cluster, the host running restore requires access the storage nodes via their private IPs. - | diff --git a/data/release-notes/enterprise-server/3-20/8.yml b/data/release-notes/enterprise-server/3-20/8.yml index 2c50abb16478..72eadf2898eb 100644 --- a/data/release-notes/enterprise-server/3-20/8.yml +++ b/data/release-notes/enterprise-server/3-20/8.yml @@ -93,8 +93,6 @@ sections: When restoring data originally backed up from a 3.13 or greater appliance version, the Elasticsearch indices need to be reindexed before some of the data will show up. This happens via a scheduled job that runs on the first Saturday of each month. It can also be forced by running `/usr/local/share/enterprise/ghe-es-search-repair`. - | When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - - | - When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - | In a cluster, the host running restore requires access the storage nodes via their private IPs. - | diff --git a/data/release-notes/enterprise-server/3-21/6.yml b/data/release-notes/enterprise-server/3-21/6.yml index 7e988bc19621..bf24a813ede3 100644 --- a/data/release-notes/enterprise-server/3-21/6.yml +++ b/data/release-notes/enterprise-server/3-21/6.yml @@ -101,8 +101,6 @@ sections: When restoring data originally backed up from a 3.13 or greater appliance version, the Elasticsearch indices need to be reindexed before some of the data will show up. This happens via a scheduled job that runs on the first Saturday of each month. It can also be forced by running `/usr/local/share/enterprise/ghe-es-search-repair`. - | When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - - | - When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - | In a cluster, the host running restore requires access the storage nodes via their private IPs. - | diff --git a/data/release-notes/enterprise-server/3-22/1.yml b/data/release-notes/enterprise-server/3-22/1.yml index 1ddee99b1ad5..9c9a5e5e3b69 100644 --- a/data/release-notes/enterprise-server/3-22/1.yml +++ b/data/release-notes/enterprise-server/3-22/1.yml @@ -95,8 +95,6 @@ sections: When restoring data originally backed up from a 3.13 or greater appliance version, the Elasticsearch indices need to be reindexed before some of the data will show up. This happens via a scheduled job that runs on the first Saturday of each month. It can also be forced by running `/usr/local/share/enterprise/ghe-es-search-repair`. - | When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - - | - When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration. - | In a cluster, the host running restore requires access the storage nodes via their private IPs. - | diff --git a/src/release-notes/tests/yaml.ts b/src/release-notes/tests/yaml.ts index 005f4c2fb355..c8e9a2420f4a 100644 --- a/src/release-notes/tests/yaml.ts +++ b/src/release-notes/tests/yaml.ts @@ -74,5 +74,19 @@ describe('lint enterprise release notes', () => { ).resolves.not.toThrow() } }) + + // PatchNotes uses each note as its React key. + test('has no duplicate notes in a list', () => { + for (const [key, section] of Object.entries(yamlContent.sections || {})) { + const lists = [section.filter((item) => typeof item === 'string')] + for (const item of section) { + if (typeof item !== 'string' && Array.isArray(item.notes)) lists.push(item.notes) + } + for (const notes of lists) { + const duplicates = notes.filter((note, i) => notes.indexOf(note) !== i) + expect(duplicates, `sections.${key} has duplicate notes`).toEqual([]) + } + } + }) }) }) From bcacf0a9ba177290df7725be67c745d1ae24b9f7 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:27:23 +0000 Subject: [PATCH 03/12] Add data-scroll-behavior to fix route change scrolling (#64469) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf5e163d-62e9-40c9-a65a-7eae1ee0392e --- src/pages/_document.tsx | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx index 92d962b3d572..226a488cd35a 100644 --- a/src/pages/_document.tsx +++ b/src/pages/_document.tsx @@ -6,10 +6,13 @@ import { colorModeScript } from '@/color-schemes/lib/color-mode-script' // MyDocument leaves SSR theme attributes at defaultCSSTheme so the HTML stays shared-cacheable. // colorModeScript updates them from the color_mode cookie before the browser's first paint. // colorModeScript injects executable JS, not content HTML, so RenderedHTML and hast do not apply. +// Primer Brand sets smooth scrolling on . data-scroll-behavior lets Next jump instantly on +// route changes and keep smooth scrolling for in-page anchors. export default class MyDocument extends Document { render() { return ( Date: Fri, 30 Oct 2026 15:27:31 +0000 Subject: [PATCH 04/12] Route Next.js dev overlay endpoints to Next (#64468) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf5e163d-62e9-40c9-a65a-7eae1ee0392e --- src/frame/middleware/index.ts | 6 ++++- src/frame/middleware/next.ts | 9 ++++++++ .../get-automatic-request-logger.ts | 4 ++-- .../tests/get-automatic-request-logger.ts | 23 +++++++++++-------- 4 files changed, 29 insertions(+), 13 deletions(-) diff --git a/src/frame/middleware/index.ts b/src/frame/middleware/index.ts index 41aa501ce6a8..d911cc29babf 100644 --- a/src/frame/middleware/index.ts +++ b/src/frame/middleware/index.ts @@ -49,7 +49,7 @@ import features from '@/versions/middleware/features' import productGroups from './context/product-groups' import featuredLinks from '@/landings/middleware/featured-links' import journeyTrack from '@/journeys/middleware/journey-track' -import next from './next' +import next, { renderNextDevEndpoints } from './next' import renderPage from './render-page' import assetPreprocessing from '@/assets/middleware/asset-preprocessing' import archivedAssetRedirects from '@/archives/middleware/archived-asset-redirects' @@ -160,6 +160,10 @@ export default function index(app: Express) { ) } + if (process.env.NODE_ENV === 'development') { + app.use(renderNextDevEndpoints) + } + app.use(shielding) app.use(handleNextDataPath) diff --git a/src/frame/middleware/next.ts b/src/frame/middleware/next.ts index a467ddffc588..f51c2c6ad4c8 100644 --- a/src/frame/middleware/next.ts +++ b/src/frame/middleware/next.ts @@ -20,4 +20,13 @@ function renderPageWithNext(req: ExtendedRequest, res: Response, nextFn: NextFun return nextFn() } +// Dev overlay endpoints, like /__nextjs_original-stack-frames, read the raw body, so they run before express.json(). +export function renderNextDevEndpoints(req: ExtendedRequest, res: Response, nextFn: NextFunction) { + if (req.path.startsWith('/__nextjs_')) { + return nextHandleRequest(req, res) + } + + return nextFn() +} + export default renderPageWithNext diff --git a/src/observability/logger/middleware/get-automatic-request-logger.ts b/src/observability/logger/middleware/get-automatic-request-logger.ts index 5cef1caecd88..0fd1c527575f 100644 --- a/src/observability/logger/middleware/get-automatic-request-logger.ts +++ b/src/observability/logger/middleware/get-automatic-request-logger.ts @@ -44,8 +44,8 @@ export function getAutomaticRequestLogger() { } else if (shouldEnableAutomaticDevLogging()) { const logLevelNum = getLogLevelNumber() - // Suppress /_next/ requests unless LOG_LEVEL is debug or more verbose. - if (url?.startsWith('/_next/') && logLevelNum < 3) { + // Suppress /_next/ and /__nextjs_ requests unless LOG_LEVEL is debug or more verbose. + if ((url?.startsWith('/_next/') || url?.startsWith('/__nextjs_')) && logLevelNum < 3) { return originalEnd.apply(this, args as Parameters) } diff --git a/src/observability/tests/get-automatic-request-logger.ts b/src/observability/tests/get-automatic-request-logger.ts index 1204552d4591..a33ee21da31e 100644 --- a/src/observability/tests/get-automatic-request-logger.ts +++ b/src/observability/tests/get-automatic-request-logger.ts @@ -164,20 +164,23 @@ describe('getAutomaticRequestLogger', () => { } }) - it('should filter out _next requests unless debug level', async () => { - vi.stubEnv('LOG_LEVEL', 'info') // info level = 2, debug = 3 + it.each(['/_next/static/file.js', '/__nextjs_font/geist-latin.woff2'])( + 'should filter out %s unless debug level', + async (url) => { + vi.stubEnv('LOG_LEVEL', 'info') // info level = 2, debug = 3 - mockReq.url = '/_next/static/file.js' - mockReq.originalUrl = '/_next/static/file.js' + mockReq.url = url + mockReq.originalUrl = url - const middleware = getAutomaticRequestLogger() - middleware(mockReq as Request, mockRes as Response, mockNext) - ;(mockRes as MockResponseWithEnd).end() + const middleware = getAutomaticRequestLogger() + middleware(mockReq as Request, mockRes as Response, mockNext) + ;(mockRes as MockResponseWithEnd).end() - await new Promise((resolve) => setTimeout(resolve, 20)) + await new Promise((resolve) => setTimeout(resolve, 20)) - expect(consoleLogs).toHaveLength(0) - }) + expect(consoleLogs).toHaveLength(0) + }, + ) it('should log _next requests when debug level is set', async () => { vi.stubEnv('LOG_LEVEL', 'debug') // debug level = 3 From c24759dc9ea96d35d357bda21908335f9917f163 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:27:37 +0000 Subject: [PATCH 05/12] Bump tsx and quiet dotenv to clean up dev startup (#64467) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf5e163d-62e9-40c9-a65a-7eae1ee0392e --- package-lock.json | 13 +++++++------ package.json | 2 +- src/frame/start-server.ts | 2 +- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/package-lock.json b/package-lock.json index 59513645ecbd..f5d8c377e41d 100644 --- a/package-lock.json +++ b/package-lock.json @@ -86,7 +86,7 @@ "strip-ansi": "7.1.0", "swr": "^2.4.0", "tcp-port-used": "1.0.2", - "tsx": "^4.21.0", + "tsx": "^4.23.15", "unified": "^11.0.5", "unist-util-find": "^3.0.0", "unist-util-visit": "^5.1.0", @@ -8926,6 +8926,7 @@ "version": "4.10.1", "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz", "integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==", + "dev": true, "license": "MIT", "dependencies": { "resolve-pkg-maps": "^1.0.0" @@ -13365,6 +13366,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, "funding": { "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" } @@ -14726,13 +14728,12 @@ "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" }, "node_modules/tsx": { - "version": "4.21.0", - "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", - "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "version": "4.23.15", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz", + "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==", "license": "MIT", "dependencies": { - "esbuild": "~0.27.0", - "get-tsconfig": "^4.7.5" + "esbuild": "~0.28.0" }, "bin": { "tsx": "dist/cli.mjs" diff --git a/package.json b/package.json index 2d382fc6f400..9ba51ad81cf8 100644 --- a/package.json +++ b/package.json @@ -245,7 +245,7 @@ "strip-ansi": "7.1.0", "swr": "^2.4.0", "tcp-port-used": "1.0.2", - "tsx": "^4.21.0", + "tsx": "^4.23.15", "unified": "^11.0.5", "unist-util-find": "^3.0.0", "unist-util-visit": "^5.1.0", diff --git a/src/frame/start-server.ts b/src/frame/start-server.ts index ec5f6620269e..30b415cf7aff 100644 --- a/src/frame/start-server.ts +++ b/src/frame/start-server.ts @@ -14,7 +14,7 @@ import createApp from './lib/app' import warmServer from './lib/warm-server' import { createLogger } from '@/observability/logger' -dotenv.config() +dotenv.config({ quiet: true }) checkNodeVersion() From cd4aba741ad2a370254f784f455cfdc60fa4189e Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:27:41 +0000 Subject: [PATCH 06/12] Stop duplicate Next.js request logs and large page data warnings (#64466) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf5e163d-62e9-40c9-a65a-7eae1ee0392e --- next.config.ts | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/next.config.ts b/next.config.ts index 2ae56cb8e80b..dad105d8397e 100644 --- a/next.config.ts +++ b/next.config.ts @@ -40,9 +40,10 @@ const config: NextConfig = { quietDeps: true, silenceDeprecations: ['legacy-js-api', 'import', 'global-builtin', 'color-4-api'], }, - // Don't use automatic Next.js logging in dev unless the log level is `debug` or higher + // Don't use automatic Next.js request logging unless the log level is `debug` or higher. + // Our own request logger already covers it. `false` would also stop `[browser]` console forwarding. // See `src/observability/logger/README.md` for log levels - logging: getLogLevelNumber() < 3 ? undefined : {}, + logging: getLogLevelNumber() < 3 ? { incomingRequests: false } : {}, async rewrites() { const DEFAULT_VERSION = 'free-pro-team@latest' return productIds.map((productId) => { @@ -101,6 +102,12 @@ const config: NextConfig = { // the CDN marks the cached content as "fresh". generateEtags: false, + experimental: { + // getServerSideProps returns rendered Markdown, so many pages exceed the 128 kB default. + // https://nextjs.org/docs/messages/large-page-data + largePageDataBytes: 1024 * 1024, // 1 MB + }, + // Disable Next.js's in-memory data cache. We don't use ISR or cached // fetch — all pages render via Express middleware with getServerSideProps. // The default 50 MB cache is unnecessary overhead during a memory-leak From cdcf23cd0b6bfbeeb38ce40e9b86e907bb72c339 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:27:49 +0000 Subject: [PATCH 07/12] Warn once per missing early-access path in dev (#64465) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf5e163d-62e9-40c9-a65a-7eae1ee0392e --- src/frame/lib/create-tree.ts | 9 ++++++++- src/frame/tests/create-tree.ts | 15 ++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/frame/lib/create-tree.ts b/src/frame/lib/create-tree.ts index 507864b7eba1..90dfaecb746d 100644 --- a/src/frame/lib/create-tree.ts +++ b/src/frame/lib/create-tree.ts @@ -8,6 +8,9 @@ const logger = createLogger(import.meta.url) const isProduction = process.env.NODE_ENV === 'production' +// Development rebuilds the tree on every request, so each missing path warns once until it appears. +const warnedMissingPaths = new Set() + export default async function createTree( originalPath: string, rootPath?: string, @@ -41,12 +44,16 @@ export default async function createTree( originalPath === 'content/early-access' || originalPath.startsWith('content/early-access/') ) { - logger.warn(msg, { path: originalPath }) + if (!warnedMissingPaths.has(originalPath)) { + warnedMissingPaths.add(originalPath) + logger.warn(msg, { path: originalPath }) + } return } throw new Error(msg) } } + warnedMissingPaths.delete(originalPath) const relativePath = filepath.replace(`${basePath}/`, '') diff --git a/src/frame/tests/create-tree.ts b/src/frame/tests/create-tree.ts index c0ba7396c1f1..09d991165eda 100644 --- a/src/frame/tests/create-tree.ts +++ b/src/frame/tests/create-tree.ts @@ -1,11 +1,14 @@ import fs from 'fs/promises' -import { describe, expect, test } from 'vitest' +import { describe, expect, test, vi } from 'vitest' import createTree from '@/frame/lib/create-tree' import { setCategoryApplicableVersions } from '@/frame/lib/page-data' import type { Page, UnversionedTree } from '@/types' +const logger = vi.hoisted(() => ({ error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() })) +vi.mock('@/observability/logger', () => ({ createLogger: () => logger })) + const contentDir = 'src/fixtures/fixtures/content' function flattenPages(tree: UnversionedTree): Page[] { @@ -55,6 +58,16 @@ describe('createTree', () => { expect(reused.length).toBe(declared.length - 1) }) + test('warns once for missing early-access content across rebuilds', async () => { + const missingPath = 'content/early-access/does-not-exist' + logger.warn.mockClear() + + expect(await createTree(missingPath)).toBeUndefined() + expect(await createTree(missingPath)).toBeUndefined() + + expect(logger.warn).toHaveBeenCalledTimes(1) + }) + test('stores the file mtime on the page', async () => { const tree = (await createTree(contentDir)) as UnversionedTree const { mtimeMs } = await fs.stat(tree.page.fullPath) From 1037bbb65a7ba87389b11f57bac240c4fcf65eca Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:27:54 +0000 Subject: [PATCH 08/12] Remove unsupported prefetch-src CSP directive (#64464) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf5e163d-62e9-40c9-a65a-7eae1ee0392e --- src/frame/middleware/helmet.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/frame/middleware/helmet.ts b/src/frame/middleware/helmet.ts index dadb83ab637e..1d28b2489a72 100644 --- a/src/frame/middleware/helmet.ts +++ b/src/frame/middleware/helmet.ts @@ -30,7 +30,6 @@ const DEFAULT_OPTIONS = { contentSecurityPolicy: { directives: { defaultSrc: ["'none'"], - prefetchSrc: ["'self'"], // Safari local development needs ws: for Next.js hot module reloading. connectSrc: ["'self'", 'https://collector.githubapp.com', isDev && 'ws:'].filter( Boolean, From 6be89d7f81bdca0aa25b9e30f0f7eb0d5e95da56 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:28:02 +0000 Subject: [PATCH 09/12] Fix carousel links that redirect on the default version (#64461) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cf5e163d-62e9-40c9-a65a-7eae1ee0392e --- .../components/shared/LandingCarousel.tsx | 5 +- src/landings/tests/landing-carousel.ts | 55 +++++++++++++++++++ 2 files changed, 58 insertions(+), 2 deletions(-) create mode 100644 src/landings/tests/landing-carousel.ts diff --git a/src/landings/components/shared/LandingCarousel.tsx b/src/landings/components/shared/LandingCarousel.tsx index 8e9a821cc0dd..c72db2031cd9 100644 --- a/src/landings/components/shared/LandingCarousel.tsx +++ b/src/landings/components/shared/LandingCarousel.tsx @@ -5,7 +5,7 @@ import { Card } from '@primer/react-brand' import cx from 'clsx' import type { ResolvedArticle } from '@/types' import { useTranslation } from '@/languages/components/useTranslation' -import { useVersion } from '@/versions/components/useVersion' +import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion' import styles from './LandingCarousel.module.scss' import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML' @@ -120,6 +120,7 @@ export const LandingCarousel = ({ }, 100) } + const versionPrefix = currentVersion !== DEFAULT_VERSION ? `/${currentVersion}` : '' const startIndex = currentPage * itemsPerView const visibleItems = processedItems.slice(startIndex, startIndex + itemsPerView) @@ -164,7 +165,7 @@ export const LandingCarousel = ({ {visibleItems.map((article: ResolvedArticle, index) => ( ({ + locale: 'en', + query: {} as Record, +})) + +vi.mock('next/router', () => ({ useRouter: () => router })) +vi.mock('@/languages/components/useTranslation', () => ({ + useTranslation: () => ({ t: (key: string) => key }), +})) +// Primer packages import CSS files, which Node cannot load outside a bundler. +vi.mock('@primer/react-brand', () => { + const passthrough = ({ children }: { children?: ReactNode }) => + createElement('div', null, children) + const Card = ({ href, children }: { href: string; children?: ReactNode }) => + createElement('a', { href }, children) + Card.Heading = passthrough + Card.Description = passthrough + return { Card } +}) +vi.mock('@/frame/components/ui/RenderedHTML/RenderedHTML', () => ({ RenderedHTML: () => null })) + +const { LandingCarousel } = await import('@/landings/components/shared/LandingCarousel') + +function renderHrefs() { + const carouselArticles = [ + { href: '/actions/quickstart', title: 'Quickstart', intro: '', category: [] }, + ] + const $ = load(renderToStaticMarkup(createElement(LandingCarousel, { carouselArticles }))) + return $('a') + .map((_, el) => $(el).attr('href')) + .get() +} + +describe('LandingCarousel card links', () => { + beforeEach(() => { + router.locale = 'en' + router.query = {} + }) + + test('omits the default version', () => { + expect(renderHrefs()).toEqual(['/en/actions/quickstart']) + }) + + test('includes a non-default version', () => { + router.locale = 'ja' + router.query = { versionId: 'enterprise-server@3.16' } + expect(renderHrefs()).toEqual(['/ja/enterprise-server@3.16/actions/quickstart']) + }) +}) From 564386fae2a4fbdf04cfd46b5656d8c78e06fb78 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:28:13 +0000 Subject: [PATCH 10/12] Use root-relative asset URLs on archived Enterprise 3.2-3.9 pages (#64440) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: cad62657-5a77-4f22-b274-41bb68ec5bb6 --- .../middleware/archived-enterprise-versions.ts | 5 ++--- .../tests/deprecated-enterprise-versions.ts | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/src/archives/middleware/archived-enterprise-versions.ts b/src/archives/middleware/archived-enterprise-versions.ts index 6a4451cfdee6..2a24fd4cf2dc 100644 --- a/src/archives/middleware/archived-enterprise-versions.ts +++ b/src/archives/middleware/archived-enterprise-versions.ts @@ -278,8 +278,6 @@ export default async function archivedEnterpriseVersions( versionSatisfiesRange(requestedVersion, `>=${firstReleaseStoredInBlobStorage}`) && versionSatisfiesRange(requestedVersion, `<=3.9`) ) { - // Fastly sets x-host, and GLB removes x-forwarded-host. - const host = req.get('x-host') || req.get('x-forwarded-host') || req.get('host') const modifiedBody = body .replaceAll( `${OLD_AZURE_BLOB_ENTERPRISE_DIR}/${requestedVersion}/assets/cb-`, @@ -287,7 +285,8 @@ export default async function archivedEnterpriseVersions( ) .replaceAll( `${OLD_AZURE_BLOB_ENTERPRISE_DIR}/${requestedVersion}/`, - `${req.protocol}://${host}/enterprise-server@${requestedVersion}/`, + // Root-relative, so request headers never end up in the page. + `/enterprise-server@${requestedVersion}/`, ) return res.send(modifiedBody) diff --git a/src/archives/tests/deprecated-enterprise-versions.ts b/src/archives/tests/deprecated-enterprise-versions.ts index 614cca0cef83..8e43625189b8 100644 --- a/src/archives/tests/deprecated-enterprise-versions.ts +++ b/src/archives/tests/deprecated-enterprise-versions.ts @@ -68,6 +68,23 @@ describe('enterprise deprecation', () => { expect(res.headers['set-cookie']).toBeUndefined() }) + test('does not put request host headers into deprecated Enterprise pages ( 3.2-3.9 )', async () => { + const res = await get( + '/en/enterprise-server@3.5/admin/overview/about-github-enterprise-server', + { + headers: { + 'x-host': 'evil.example">', + 'x-forwarded-host': 'evil.example', + 'x-forwarded-proto': 'javascript', + }, + }, + ) + expect(res.statusCode).toBe(200) + expect(res.body).not.toContain('evil.example') + expect(res.body).not.toContain('javascript://') + expect(res.body).toContain('src="/enterprise-server@3.5/_next/') + }) + test('handles requests for deprecated Enterprise pages ( <2.13 )', async () => { expect(enterpriseServerReleases.deprecated.includes('2.12')).toBe(true) const { $, res } = await getDOM('/enterprise/2.12/user/articles/about-branches') From 61c0afa85c0e2a9e0fb1d60574dc052617749b03 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:28:50 +0000 Subject: [PATCH 11/12] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Use=20the=20newest?= =?UTF-8?q?=20matching=20Node=20version=20in=20CI=20(#64460)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Copilot-Session: 2e7a0086-4439-4fb9-9306-74232c70d818 --- .github/actions/node-npm-setup/action.yml | 2 ++ .github/workflows/package-lock-lint.yml | 1 + .github/workflows/ready-for-doc-review.yml | 1 + 3 files changed, 4 insertions(+) diff --git a/.github/actions/node-npm-setup/action.yml b/.github/actions/node-npm-setup/action.yml index e25ce75a1f94..da7c0e2c875e 100644 --- a/.github/actions/node-npm-setup/action.yml +++ b/.github/actions/node-npm-setup/action.yml @@ -20,6 +20,8 @@ runs: uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0 with: node-version-file: 'package.json' + # Resolve from the Node manifest so CI uses the newest allowed version, not an older cached match. + check-latest: true package-manager-cache: false cache: ${{ steps.cache-node_modules.outputs.cache-hit != 'true' && 'npm' || '' }} diff --git a/.github/workflows/package-lock-lint.yml b/.github/workflows/package-lock-lint.yml index 43333e453355..516abe89df30 100644 --- a/.github/workflows/package-lock-lint.yml +++ b/.github/workflows/package-lock-lint.yml @@ -43,6 +43,7 @@ jobs: uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: 'package.json' + check-latest: true cache: npm - name: Run check diff --git a/.github/workflows/ready-for-doc-review.yml b/.github/workflows/ready-for-doc-review.yml index 552ecc7fade4..ab5613f1f064 100644 --- a/.github/workflows/ready-for-doc-review.yml +++ b/.github/workflows/ready-for-doc-review.yml @@ -29,6 +29,7 @@ jobs: uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: node-version-file: 'package.json' + check-latest: true cache: npm - name: Install dependencies From adf1d8a77fefd797c13a2c8688272eed89b3cc47 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Fri, 30 Oct 2026 15:29:01 +0000 Subject: [PATCH 12/12] =?UTF-8?q?=E2=9A=99=EF=B8=8F=20Use=20package.json?= =?UTF-8?q?=20Node=20version=20in=20changelog=20agent=20(#64458)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Copilot-Session: 2e7a0086-4439-4fb9-9306-74232c70d818 --- .github/workflows/changelog-agent.yml | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/.github/workflows/changelog-agent.yml b/.github/workflows/changelog-agent.yml index 1d02b16ef51d..e32621cb71a8 100644 --- a/.github/workflows/changelog-agent.yml +++ b/.github/workflows/changelog-agent.yml @@ -393,11 +393,22 @@ jobs: fs.writeFileSync('prompt.txt', userPrompt); core.setOutput('date_str', dateStr); + # Use a subdirectory: checkout would clear the workspace and the prompt files above. + - name: Check out package.json for the Node version + if: steps.check_parent.outputs.has_parent == 'true' && steps.check_existing.outputs.exists == 'false' + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + path: node-version-source + sparse-checkout: /package.json + sparse-checkout-cone-mode: false + persist-credentials: false + - name: Set up Node.js for Copilot CLI if: steps.check_parent.outputs.has_parent == 'true' && steps.check_existing.outputs.exists == 'false' uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 with: - node-version: 24 + node-version-file: node-version-source/package.json + check-latest: true - name: Install GitHub Copilot CLI if: steps.check_parent.outputs.has_parent == 'true' && steps.check_existing.outputs.exists == 'false'