From 28158020e30783ff6e8acd7bafb0c22eb30f1294 Mon Sep 17 00:00:00 2001
From: Robert Sese <734194+rsese@users.noreply.github.com>
Date: Wed, 22 Oct 2025 13:23:22 -0500
Subject: [PATCH 1/8] remove journey article nav components (#58124)
---
.../tests/playwright-rendering.spec.ts | 16 -------------
src/frame/components/article/ArticlePage.tsx | 18 --------------
src/journeys/lib/journey-path-resolver.ts | 24 +++----------------
src/journeys/tests/journey-path-resolver.ts | 6 ++---
4 files changed, 6 insertions(+), 58 deletions(-)
diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts
index 17776def96ac..99bd97eb2684 100644
--- a/src/fixtures/tests/playwright-rendering.spec.ts
+++ b/src/fixtures/tests/playwright-rendering.spec.ts
@@ -1138,22 +1138,6 @@ test.describe('Journey Tracks', () => {
expect(trackContent).not.toContain('{%')
expect(trackContent).not.toContain('%}')
})
-
- test('journey navigation components show on article pages', async ({ page }) => {
- // go to an article that's part of a journey track
- await page.goto('/get-started/start-your-journey/hello-world?feature=journey-navigation')
-
- // journey next/prev nav components should rende
- const journeyCard = page.locator('[data-testid="journey-track-card"]')
- if (await journeyCard.isVisible()) {
- await expect(journeyCard).toBeVisible()
- }
-
- const journeyNav = page.locator('[data-testid="journey-track-nav"]')
- if (await journeyNav.isVisible()) {
- await expect(journeyNav).toBeVisible()
- }
- })
})
test.describe('LandingArticleGridWithFilter component', () => {
diff --git a/src/frame/components/article/ArticlePage.tsx b/src/frame/components/article/ArticlePage.tsx
index b6d42702003a..89ef49ed8842 100644
--- a/src/frame/components/article/ArticlePage.tsx
+++ b/src/frame/components/article/ArticlePage.tsx
@@ -7,8 +7,6 @@ import { DefaultLayout } from '@/frame/components/DefaultLayout'
import { ArticleTitle } from '@/frame/components/article/ArticleTitle'
import { useArticleContext } from '@/frame/components/context/ArticleContext'
import { LearningTrackNav } from '@/learning-track/components/article/LearningTrackNav'
-import { JourneyTrackNav } from '@/journeys/components/JourneyTrackNav'
-import { JourneyTrackCard } from '@/journeys/components/JourneyTrackCard'
import { MarkdownContent } from '@/frame/components/ui/MarkdownContent'
import { Lead } from '@/frame/components/ui/Lead'
import { PermissionsStatement } from '@/frame/components/ui/PermissionsStatement'
@@ -44,14 +42,10 @@ export const ArticlePage = () => {
productVideoUrl,
miniTocItems,
currentLearningTrack,
- currentJourneyTrack,
supportPortalVaIframeProps,
currentLayout,
} = useArticleContext()
const isLearningPath = !!currentLearningTrack?.trackName
- const isJourneyPath = !!currentJourneyTrack?.trackId
- // Only show journey track components when feature flag is enabled
- const showJourneyTracks = isJourneyPath && router.query?.feature === 'journey-navigation'
const { t } = useTranslation(['pages'])
const introProp = (
@@ -78,7 +72,6 @@ export const ArticlePage = () => {
const toc = (
<>
{isLearningPath && }
- {showJourneyTracks && }
{miniTocItems.length > 1 && }
>
)
@@ -129,11 +122,6 @@ export const ArticlePage = () => {
) : null}
- {showJourneyTracks ? (
-
-
-
- ) : null}
>
) : (
@@ -160,12 +148,6 @@ export const ArticlePage = () => {
) : null}
-
- {showJourneyTracks ? (
-
-
-
- ) : null}
)}
diff --git a/src/journeys/lib/journey-path-resolver.ts b/src/journeys/lib/journey-path-resolver.ts
index f2a69d4265cc..1bc9b22088bc 100644
--- a/src/journeys/lib/journey-path-resolver.ts
+++ b/src/journeys/lib/journey-path-resolver.ts
@@ -83,24 +83,6 @@ function normalizeGuidePath(path: string): string {
: `/${withoutLanguage || path}`
}
-/**
- * Helper function to append the journey-navigation feature flag to URLs
- */
-function appendJourneyFeatureFlag(href: string): string {
- if (!href) return href
-
- try {
- // we have to pass some URL here, we just throw it away though
- const url = new URL(href, 'https://docs.github.com')
- url.searchParams.set('feature', 'journey-navigation')
- return url.pathname + url.search
- } catch {
- // fallback if URL parsing fails
- const separator = href.includes('?') ? '&' : '?'
- return `${href}${separator}feature=journey-navigation`
- }
-}
-
/**
* Resolves the journey context for a given article path.
*
@@ -189,7 +171,7 @@ export async function resolveJourneyContext(
if (resultData && resultData.length > 0) {
const linkResult = resultData[0]
result.prevGuide = {
- href: appendJourneyFeatureFlag(linkResult.href),
+ href: linkResult.href,
title: linkResult.title || '',
}
}
@@ -210,7 +192,7 @@ export async function resolveJourneyContext(
if (resultData && resultData.length > 0) {
const linkResult = resultData[0]
result.nextGuide = {
- href: appendJourneyFeatureFlag(linkResult.href),
+ href: linkResult.href,
title: linkResult.title || '',
}
}
@@ -251,7 +233,7 @@ export async function resolveJourneyTracks(
const linkData = await getLinkData(guidePath, context, { title: true })
const baseHref = linkData?.[0]?.href || guidePath
return {
- href: appendJourneyFeatureFlag(baseHref),
+ href: baseHref,
title: linkData?.[0]?.title || 'Untitled Guide',
}
}),
diff --git a/src/journeys/tests/journey-path-resolver.ts b/src/journeys/tests/journey-path-resolver.ts
index 624d92f06087..510b14a99a8b 100644
--- a/src/journeys/tests/journey-path-resolver.ts
+++ b/src/journeys/tests/journey-path-resolver.ts
@@ -82,7 +82,7 @@ describe('journey-path-resolver', () => {
)
expect(result?.prevGuide).toEqual({
- href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup?feature=journey-navigation',
+ href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup',
title: 'Mock Title for /enterprise-onboarding/setup',
})
})
@@ -95,7 +95,7 @@ describe('journey-path-resolver', () => {
)
expect(result?.nextGuide).toEqual({
- href: '/en/enterprise-cloud@latest/enterprise-onboarding/deploy?feature=journey-navigation',
+ href: '/en/enterprise-cloud@latest/enterprise-onboarding/deploy',
title: 'Mock Title for /enterprise-onboarding/deploy',
})
})
@@ -184,7 +184,7 @@ describe('journey-path-resolver', () => {
expect(result[0].guides).toHaveLength(2)
expect(result[0].guides[0]).toEqual({
- href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup?feature=journey-navigation',
+ href: '/en/enterprise-cloud@latest/enterprise-onboarding/setup',
title: 'Mock Title for /enterprise-onboarding/setup',
})
})
From f41f1d335457b858ebdc5b5d124843efdfb6bde2 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Wed, 22 Oct 2025 11:55:53 -0700
Subject: [PATCH 2/8] Relax package-lock lint to only check top-level
dependencies (#58125)
---
.github/workflows/package-lock-lint.yml | 22 ++++++++++++++++------
1 file changed, 16 insertions(+), 6 deletions(-)
diff --git a/.github/workflows/package-lock-lint.yml b/.github/workflows/package-lock-lint.yml
index b912a11cbb2e..1b9581f94536 100644
--- a/.github/workflows/package-lock-lint.yml
+++ b/.github/workflows/package-lock-lint.yml
@@ -37,6 +37,9 @@ jobs:
run: |
npm --version
+ # Save the current top-level dependencies from package-lock.json
+ node -e "console.log(JSON.stringify(require('./package-lock.json').packages['']))" > /tmp/before.json
+
# From https://docs.npmjs.com/cli/v7/commands/npm-install
#
# The --package-lock-only argument will only update the
@@ -45,9 +48,16 @@ jobs:
#
npm install --package-lock-only --ignore-scripts --include=optional
- # If the package.json (dependencies and devDependencies) is
- # in correct sync with package-lock.json running the above command
- # should *not* make an edit to the package-lock.json. I.e.
- # running `git status` should
- # say "nothing to commit, working tree clean".
- git diff --exit-code
+ # Extract the top-level dependencies after regeneration
+ node -e "console.log(JSON.stringify(require('./package-lock.json').packages['']))" > /tmp/after.json
+
+ # Compare only the top-level package dependencies
+ # This ignores platform-specific differences in nested dependency resolution
+ # (like "peer" flags) that don't affect actual installed versions
+ if ! diff /tmp/before.json /tmp/after.json; then
+ echo "ERROR: Top-level dependencies in package-lock.json are out of sync with package.json"
+ echo "Please run 'npm install' locally and commit the updated package-lock.json"
+ exit 1
+ fi
+
+ echo "✓ Top-level dependencies are in sync"
From 8c6e5a307120b82da5d7d739c3baf88c739d9ffa Mon Sep 17 00:00:00 2001
From: Evan Bonsignori
Date: Wed, 22 Oct 2025 11:58:19 -0700
Subject: [PATCH 3/8] alphabetize dropdown (#58069)
---
.../components/shared/LandingArticleGridWithFilter.tsx | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/src/landings/components/shared/LandingArticleGridWithFilter.tsx b/src/landings/components/shared/LandingArticleGridWithFilter.tsx
index 1009ce4bdcbd..abc3d20205ff 100644
--- a/src/landings/components/shared/LandingArticleGridWithFilter.tsx
+++ b/src/landings/components/shared/LandingArticleGridWithFilter.tsx
@@ -61,7 +61,9 @@ export const ArticleGrid = ({ flatArticles }: ArticleGridProps) => {
// Extract unique categories from the articles
const categories: string[] = [
ALL_CATEGORIES,
- ...new Set(flatArticles.flatMap((item) => item.category || [])),
+ ...Array.from(new Set(flatArticles.flatMap((item) => item.category || []))).sort((a, b) =>
+ a.localeCompare(b),
+ ),
]
const applyFilters = () => {
From bba2c9cd79f0bc04627675763e776a001a68dee0 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Wed, 22 Oct 2025 12:06:03 -0700
Subject: [PATCH 4/8] Move eslint.config.js to Typescript (#58027)
---
eslint-plugins.d.ts | 57 +++++++++++++++++++
eslint.config.js => eslint.config.ts | 83 ----------------------------
package-lock.json | 21 +++++++
package.json | 2 +
src/ai-tools/scripts/ai-tools.ts | 2 -
src/metrics/scripts/docsaudit.ts | 2 -
src/metrics/scripts/docstat.ts | 2 -
7 files changed, 80 insertions(+), 89 deletions(-)
create mode 100644 eslint-plugins.d.ts
rename eslint.config.js => eslint.config.ts (65%)
diff --git a/eslint-plugins.d.ts b/eslint-plugins.d.ts
new file mode 100644
index 000000000000..acbb6a189037
--- /dev/null
+++ b/eslint-plugins.d.ts
@@ -0,0 +1,57 @@
+// Type declarations for ESLint plugins without official TypeScript definitions
+
+declare module 'eslint-plugin-github' {
+ import type { ESLint, Linter } from 'eslint'
+
+ const plugin: ESLint.Plugin & {
+ configs: {
+ recommended: Linter.FlatConfig
+ }
+ }
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-primer-react' {
+ import type { ESLint, Linter } from 'eslint'
+
+ const plugin: ESLint.Plugin & {
+ configs: {
+ recommended: Linter.FlatConfig
+ }
+ }
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-eslint-comments' {
+ import type { ESLint } from 'eslint'
+
+ const plugin: ESLint.Plugin
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-i18n-text' {
+ import type { ESLint } from 'eslint'
+
+ const plugin: ESLint.Plugin
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-filenames' {
+ import type { ESLint } from 'eslint'
+
+ const plugin: ESLint.Plugin
+
+ export default plugin
+}
+
+declare module 'eslint-plugin-no-only-tests' {
+ import type { ESLint } from 'eslint'
+
+ const plugin: ESLint.Plugin
+
+ export default plugin
+}
diff --git a/eslint.config.js b/eslint.config.ts
similarity index 65%
rename from eslint.config.js
rename to eslint.config.ts
index 8fa152a4b420..aeb30e33a9ad 100644
--- a/eslint.config.js
+++ b/eslint.config.ts
@@ -14,89 +14,6 @@ import prettier from 'eslint-config-prettier'
import globals from 'globals'
export default [
- // JavaScript and MJS files configuration
- {
- files: ['**/*.{js,mjs}'],
- languageOptions: {
- ecmaVersion: 2022,
- sourceType: 'module',
- globals: {
- ...globals.browser,
- ...globals.node,
- ...globals.commonjs,
- ...globals.es2020,
- },
- parserOptions: {
- requireConfigFile: false,
- },
- },
- settings: {
- 'import/resolver': {
- typescript: true,
- node: true,
- },
- },
- plugins: {
- github,
- import: importPlugin,
- 'eslint-comments': eslintComments,
- 'i18n-text': i18nText,
- filenames,
- 'no-only-tests': noOnlyTests,
- prettier: prettierPlugin,
- },
- rules: {
- // ESLint recommended rules
- ...js.configs.recommended.rules,
-
- // GitHub plugin recommended rules
- ...github.configs.recommended.rules,
-
- // Import plugin error rules
- ...importPlugin.configs.errors.rules,
-
- // JavaScript-specific overrides
- 'import/no-extraneous-dependencies': [
- 'error',
- {
- packageDir: '.',
- },
- ],
- 'import/extensions': 'off',
- 'no-console': 'off',
- camelcase: 'off',
- 'no-shadow': 'off',
- 'prefer-template': 'off',
- 'no-constant-condition': 'off',
- 'no-unused-vars': 'off',
- 'import/no-named-as-default-member': 'off',
- 'one-var': 'off',
- 'import/no-namespace': 'off',
- 'import/no-anonymous-default-export': 'off',
- 'object-shorthand': 'off',
- 'no-empty': 'off',
- 'prefer-const': 'off',
- 'import/no-named-as-default': 'off',
- 'no-useless-concat': 'off',
- 'func-style': 'off',
-
- // Disable GitHub plugin rules that were disabled in original config
- 'github/array-foreach': 'off',
- 'github/no-then': 'off',
-
- // Disable rules that might not exist or cause issues initially
- 'i18n-text/no-en': 'off',
- 'filenames/match-regex': 'off',
- 'eslint-comments/no-use': 'off',
- 'eslint-comments/no-unused-disable': 'off',
- 'eslint-comments/no-unlimited-disable': 'off',
-
- // Disable new ESLint 9 rules that are causing issues
- 'no-constant-binary-expression': 'off',
- },
- },
-
- // TypeScript and TSX files configuration
{
files: ['**/*.{ts,tsx}'],
languageOptions: {
diff --git a/package-lock.json b/package-lock.json
index d3f93f38b24f..2a7d689f08f6 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -116,6 +116,7 @@
"@types/connect-timeout": "1.9.0",
"@types/cookie": "0.6.0",
"@types/cookie-parser": "1.4.8",
+ "@types/eslint-plugin-jsx-a11y": "^6.10.1",
"@types/event-to-promise": "^0.7.5",
"@types/express": "^5.0.3",
"@types/imurmurhash": "^0.1.4",
@@ -156,6 +157,7 @@
"graphql": "^16.9.0",
"http-status-code": "^2.1.0",
"husky": "^9.1.7",
+ "jiti": "^2.6.1",
"json-schema-merge-allof": "^0.8.1",
"lint-staged": "^16.0.0",
"markdownlint": "^0.34.0",
@@ -4063,6 +4065,16 @@
"@types/ms": "*"
}
},
+ "node_modules/@types/eslint-plugin-jsx-a11y": {
+ "version": "6.10.1",
+ "resolved": "https://registry.npmjs.org/@types/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-6.10.1.tgz",
+ "integrity": "sha512-5RtuPVe0xz8BAhrkn2oww6Uw885atf962Q4fqZo48QdO3EQA7oCEDSXa6optgJ1ZMds3HD9ITK5bfm4AWuoXFQ==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "eslint": "^9"
+ }
+ },
"node_modules/@types/estree": {
"version": "1.0.7",
"resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.7.tgz",
@@ -10183,6 +10195,15 @@
"version": "2.1.0",
"license": "MIT"
},
+ "node_modules/jiti": {
+ "version": "2.6.1",
+ "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.6.1.tgz",
+ "integrity": "sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==",
+ "dev": true,
+ "bin": {
+ "jiti": "lib/jiti-cli.mjs"
+ }
+ },
"node_modules/joi": {
"version": "17.13.3",
"resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz",
diff --git a/package.json b/package.json
index a2e3b945928b..eae47fc34dcc 100644
--- a/package.json
+++ b/package.json
@@ -258,6 +258,7 @@
"@types/connect-timeout": "1.9.0",
"@types/cookie": "0.6.0",
"@types/cookie-parser": "1.4.8",
+ "@types/eslint-plugin-jsx-a11y": "^6.10.1",
"@types/event-to-promise": "^0.7.5",
"@types/express": "^5.0.3",
"@types/imurmurhash": "^0.1.4",
@@ -298,6 +299,7 @@
"graphql": "^16.9.0",
"http-status-code": "^2.1.0",
"husky": "^9.1.7",
+ "jiti": "^2.6.1",
"json-schema-merge-allof": "^0.8.1",
"lint-staged": "^16.0.0",
"markdownlint": "^0.34.0",
diff --git a/src/ai-tools/scripts/ai-tools.ts b/src/ai-tools/scripts/ai-tools.ts
index 0d2cc4b25f37..0237c017b2c7 100644
--- a/src/ai-tools/scripts/ai-tools.ts
+++ b/src/ai-tools/scripts/ai-tools.ts
@@ -1,5 +1,3 @@
-#!/usr/bin/env node
-
import { fileURLToPath } from 'url'
import { Command } from 'commander'
import fs from 'fs'
diff --git a/src/metrics/scripts/docsaudit.ts b/src/metrics/scripts/docsaudit.ts
index f37159959643..a49d6ccce9a4 100644
--- a/src/metrics/scripts/docsaudit.ts
+++ b/src/metrics/scripts/docsaudit.ts
@@ -1,5 +1,3 @@
-#!/usr/bin/env node
-
import fs from 'fs'
import path from 'path'
import { fileURLToPath } from 'url'
diff --git a/src/metrics/scripts/docstat.ts b/src/metrics/scripts/docstat.ts
index bc1850e09e1c..2e1f6bad7afc 100644
--- a/src/metrics/scripts/docstat.ts
+++ b/src/metrics/scripts/docstat.ts
@@ -1,5 +1,3 @@
-#!/usr/bin/env node
-
import fs from 'fs'
import path from 'path'
import { Command } from 'commander'
From aac022fdc1e8b7cb0ef44f09bc715509fa6256ae Mon Sep 17 00:00:00 2001
From: Robert Sese <734194+rsese@users.noreply.github.com>
Date: Wed, 22 Oct 2025 14:37:52 -0500
Subject: [PATCH 5/8] fix landing page carousel links (#58128)
---
src/frame/middleware/resolve-recommended.ts | 13 ++--
src/frame/tests/resolve-recommended.test.ts | 60 ++++++++++++++++++-
.../components/shared/LandingCarousel.tsx | 6 +-
3 files changed, 68 insertions(+), 11 deletions(-)
diff --git a/src/frame/middleware/resolve-recommended.ts b/src/frame/middleware/resolve-recommended.ts
index 842883bf6751..774585f7a510 100644
--- a/src/frame/middleware/resolve-recommended.ts
+++ b/src/frame/middleware/resolve-recommended.ts
@@ -2,6 +2,7 @@ import type { ExtendedRequest, ResolvedArticle } from '@/types'
import type { Response, NextFunction } from 'express'
import findPage from '@/frame/lib/find-page'
import { renderContent } from '@/content-render/index'
+import Permalink from '@/frame/lib/permalink'
import { createLogger } from '@/observability/logger/index'
@@ -83,12 +84,11 @@ function tryResolveArticlePath(
}
/**
- * Get the href for a page from its permalinks
+ * Get the path for a page (without language/version)
*/
-function getPageHref(page: any, currentLanguage: string = 'en'): string {
- if (page.permalinks?.length > 0) {
- const permalink = page.permalinks.find((p: any) => p.languageCode === currentLanguage)
- return permalink ? permalink.href : page.permalinks[0].href
+function getPageHref(page: any): string {
+ if (page.relativePath) {
+ return Permalink.relativePathToSuffix(page.relativePath)
}
return '' // fallback
}
@@ -126,7 +126,6 @@ async function resolveRecommended(
return next()
}
- const currentLanguage = req.context?.currentLanguage || 'en'
const resolved: ResolvedArticle[] = []
for (const rawPath of articlePaths) {
@@ -134,7 +133,7 @@ async function resolveRecommended(
const foundPage = tryResolveArticlePath(rawPath, page?.relativePath, req)
if (foundPage) {
- const href = getPageHref(foundPage, currentLanguage)
+ const href = getPageHref(foundPage)
const category = foundPage.relativePath
? foundPage.relativePath.split('/').slice(0, -1).filter(Boolean)
: []
diff --git a/src/frame/tests/resolve-recommended.test.ts b/src/frame/tests/resolve-recommended.test.ts
index 2ed9f26ab12f..d3324608e282 100644
--- a/src/frame/tests/resolve-recommended.test.ts
+++ b/src/frame/tests/resolve-recommended.test.ts
@@ -120,7 +120,7 @@ describe('resolveRecommended middleware', () => {
{
title: 'Test Article',
intro: 'Test intro
',
- href: '/en/copilot/tutorials/article',
+ href: '/copilot/tutorials/article',
category: ['copilot', 'tutorials'],
},
])
@@ -195,7 +195,7 @@ describe('resolveRecommended middleware', () => {
{
title: 'Valid Article',
intro: 'Valid intro
',
- href: '/en/test/valid',
+ href: '/test/valid',
category: ['test'],
},
])
@@ -256,10 +256,64 @@ describe('resolveRecommended middleware', () => {
{
title: 'Relative Article',
intro: 'Relative intro
',
- href: '/en/copilot/relative-article',
+ href: '/copilot/relative-article', // Updated to clean path
category: ['copilot'],
},
])
expect(mockNext).toHaveBeenCalled()
})
+
+ test('returns paths without language or version prefixes', async () => {
+ const testPage: Partial = {
+ mtime: Date.now(),
+ title: 'Tutorial Page',
+ rawTitle: 'Tutorial Page',
+ intro: 'Tutorial intro',
+ rawIntro: 'Tutorial intro',
+ relativePath: 'copilot/tutorials/tutorial-page/index.md',
+ fullPath: '/full/path/copilot/tutorials/tutorial-page/index.md',
+ languageCode: 'en',
+ documentType: 'article',
+ markdown: 'Tutorial content',
+ versions: {},
+ applicableVersions: ['free-pro-team@latest'],
+ permalinks: [
+ {
+ languageCode: 'en',
+ pageVersion: 'free-pro-team@latest',
+ title: 'Tutorial Page',
+ href: '/en/copilot/tutorials/tutorial-page',
+ hrefWithoutLanguage: '/copilot/tutorials/tutorial-page',
+ },
+ ],
+ renderProp: vi.fn().mockResolvedValue('rendered'),
+ renderTitle: vi.fn().mockResolvedValue('Tutorial Page'),
+ render: vi.fn().mockResolvedValue('rendered content'),
+ buildRedirects: vi.fn().mockReturnValue({}),
+ }
+
+ mockFindPage.mockReturnValue(testPage as any)
+
+ const req = createMockRequest({ rawRecommended: ['/copilot/tutorials/tutorial-page'] })
+
+ await resolveRecommended(req, mockRes, mockNext)
+
+ expect(mockFindPage).toHaveBeenCalledWith(
+ '/en/copilot/tutorials/tutorial-page',
+ req.context!.pages,
+ req.context!.redirects,
+ )
+
+ // Verify that the href is a clean path without language/version, that gets
+ // added on the React side.
+ expect((req.context!.page as any).recommended).toEqual([
+ {
+ title: 'Tutorial Page',
+ intro: 'Tutorial intro
',
+ href: '/copilot/tutorials/tutorial-page',
+ category: ['copilot', 'tutorials', 'tutorial-page'],
+ },
+ ])
+ expect(mockNext).toHaveBeenCalled()
+ })
})
diff --git a/src/landings/components/shared/LandingCarousel.tsx b/src/landings/components/shared/LandingCarousel.tsx
index f779d1488aba..0ec75e24cf69 100644
--- a/src/landings/components/shared/LandingCarousel.tsx
+++ b/src/landings/components/shared/LandingCarousel.tsx
@@ -1,8 +1,10 @@
import { useState, useEffect, useRef } from 'react'
+import { useRouter } from 'next/router'
import { ChevronLeftIcon, ChevronRightIcon } from '@primer/octicons-react'
import cx from 'classnames'
import type { ResolvedArticle } from '@/types'
import { useTranslation } from '@/languages/components/useTranslation'
+import { useVersion } from '@/versions/components/useVersion'
import styles from './LandingCarousel.module.scss'
type LandingCarouselProps = {
@@ -42,6 +44,8 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
const [isAnimating, setIsAnimating] = useState(false)
const itemsPerView = useResponsiveItemsPerView()
const { t } = useTranslation('product_landing')
+ const router = useRouter()
+ const { currentVersion } = useVersion()
const headingText = heading || t('carousel.recommended')
// Ref to store timeout IDs for cleanup
const animationTimeoutRef = useRef(null)
@@ -145,7 +149,7 @@ export const LandingCarousel = ({ heading = '', recommended }: LandingCarouselPr
{visibleItems.map((article: ResolvedArticle, index) => (
From a01a63b0521cbb21c7c6f44ffc15a277bf3eadfd Mon Sep 17 00:00:00 2001
From: docs-bot <77750099+docs-bot@users.noreply.github.com>
Date: Wed, 22 Oct 2025 12:37:52 -0700
Subject: [PATCH 6/8] Sync secret scanning data (#58122)
Co-authored-by: mc <42146119+mchammer01@users.noreply.github.com>
---
src/secret-scanning/data/public-docs.yml | 2 +-
src/secret-scanning/lib/config.json | 4 ++--
2 files changed, 3 insertions(+), 3 deletions(-)
diff --git a/src/secret-scanning/data/public-docs.yml b/src/secret-scanning/data/public-docs.yml
index cf23a1010e22..c72d61a2d935 100644
--- a/src/secret-scanning/data/public-docs.yml
+++ b/src/secret-scanning/data/public-docs.yml
@@ -2830,7 +2830,7 @@
isPublic: true
isPrivateWithGhas: true
hasPushProtection: true
- hasValidityCheck: false
+ hasValidityCheck: '{% ifversion ghes %}false{% else %}true{% endif %}'
base64Supported: false
isduplicate: false
- provider: Grafana
diff --git a/src/secret-scanning/lib/config.json b/src/secret-scanning/lib/config.json
index 715a40a023f4..be868558133c 100644
--- a/src/secret-scanning/lib/config.json
+++ b/src/secret-scanning/lib/config.json
@@ -1,5 +1,5 @@
{
- "sha": "967fe0aa24cea46ddd1b85d5daebfe2f29dec698",
- "blob-sha": "4c6a80effd77ff6f5d20f2fc58e0ec359ec58eed",
+ "sha": "8e351827006cf7e13d1cbbd96853679857f3261d",
+ "blob-sha": "5dd8a45310fe73ad5a1bb752be594c1adbd3cf38",
"targetFilename": "code-security/secret-scanning/introduction/supported-secret-scanning-patterns"
}
\ No newline at end of file
From fb8287034888939052b5e9246b81bbbb9c3536d3 Mon Sep 17 00:00:00 2001
From: Evan Bonsignori
Date: Wed, 22 Oct 2025 12:43:39 -0700
Subject: [PATCH 7/8] validate `heroImage` frontmatter for `index.md` files
(#58127)
---
.../linting-rules/frontmatter-hero-image.ts | 89 ++++++++++++
src/content-linter/lib/linting-rules/index.ts | 2 +
src/content-linter/style/github-docs.ts | 6 +
.../tests/unit/frontmatter-hero-image.ts | 128 ++++++++++++++++++
4 files changed, 225 insertions(+)
create mode 100644 src/content-linter/lib/linting-rules/frontmatter-hero-image.ts
create mode 100644 src/content-linter/tests/unit/frontmatter-hero-image.ts
diff --git a/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts b/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts
new file mode 100644
index 000000000000..e7cd45bd8b17
--- /dev/null
+++ b/src/content-linter/lib/linting-rules/frontmatter-hero-image.ts
@@ -0,0 +1,89 @@
+import fs from 'fs'
+import path from 'path'
+// @ts-ignore - markdownlint-rule-helpers doesn't have TypeScript declarations
+import { addError } from 'markdownlint-rule-helpers'
+
+import { getFrontmatter } from '../helpers/utils'
+import type { RuleParams, RuleErrorCallback, Rule } from '@/content-linter/types'
+
+interface Frontmatter {
+ heroImage?: string
+ [key: string]: any
+}
+
+// Get the list of valid hero images
+function getValidHeroImages(): string[] {
+ const ROOT = process.env.ROOT || '.'
+ const heroImageDir = path.join(ROOT, 'assets/images/banner-images')
+
+ try {
+ if (!fs.existsSync(heroImageDir)) {
+ return []
+ }
+
+ const files = fs.readdirSync(heroImageDir)
+ // Return absolute paths as they would appear in frontmatter
+ return files.map((file) => `/assets/images/banner-images/${file}`)
+ } catch {
+ return []
+ }
+}
+
+export const frontmatterHeroImage: Rule = {
+ names: ['GHD061', 'frontmatter-hero-image'],
+ description:
+ 'Hero image paths must be absolute and point to valid images in /assets/images/banner-images/',
+ tags: ['frontmatter', 'images'],
+ function: (params: RuleParams, onError: RuleErrorCallback) => {
+ // Only check index.md files
+ if (!params.name.endsWith('index.md')) return
+
+ const fm = getFrontmatter(params.lines) as Frontmatter | null
+ if (!fm || !fm.heroImage) return
+
+ const heroImage = fm.heroImage
+
+ // Check if heroImage is an absolute path
+ if (!heroImage.startsWith('/')) {
+ const line = params.lines.find((line: string) => line.trim().startsWith('heroImage:'))
+ const lineNumber = line ? params.lines.indexOf(line) + 1 : 1
+ addError(
+ onError,
+ lineNumber,
+ `Hero image path must be absolute (start with /). Found: ${heroImage}`,
+ line || '',
+ null, // No fix possible
+ )
+ return
+ }
+
+ // Check if heroImage points to banner-images directory
+ if (!heroImage.startsWith('/assets/images/banner-images/')) {
+ const line = params.lines.find((line: string) => line.trim().startsWith('heroImage:'))
+ const lineNumber = line ? params.lines.indexOf(line) + 1 : 1
+ addError(
+ onError,
+ lineNumber,
+ `Hero image must point to /assets/images/banner-images/. Found: ${heroImage}`,
+ line || '',
+ null, // No fix possible
+ )
+ return
+ }
+
+ // Check if the file actually exists
+ const validHeroImages = getValidHeroImages()
+ if (validHeroImages.length > 0 && !validHeroImages.includes(heroImage)) {
+ const line = params.lines.find((line: string) => line.trim().startsWith('heroImage:'))
+ const lineNumber = line ? params.lines.indexOf(line) + 1 : 1
+ const availableImages = validHeroImages.join(', ')
+ addError(
+ onError,
+ lineNumber,
+ `Hero image file does not exist: ${heroImage}. Available images: ${availableImages}`,
+ line || '',
+ null, // No fix possible
+ )
+ }
+ },
+}
diff --git a/src/content-linter/lib/linting-rules/index.ts b/src/content-linter/lib/linting-rules/index.ts
index eaa9706da08f..d3cabfa4489d 100644
--- a/src/content-linter/lib/linting-rules/index.ts
+++ b/src/content-linter/lib/linting-rules/index.ts
@@ -61,6 +61,7 @@ import { ctasSchema } from '@/content-linter/lib/linting-rules/ctas-schema'
import { journeyTracksLiquid } from './journey-tracks-liquid'
import { journeyTracksGuidePathExists } from './journey-tracks-guide-path-exists'
import { journeyTracksUniqueIds } from './journey-tracks-unique-ids'
+import { frontmatterHeroImage } from './frontmatter-hero-image'
// Using any type because @github/markdownlint-github doesn't provide TypeScript declarations
// The elements in the array have a 'names' property that contains rule identifiers
@@ -130,6 +131,7 @@ export const gitHubDocsMarkdownlint = {
journeyTracksLiquid, // GHD058
journeyTracksGuidePathExists, // GHD059
journeyTracksUniqueIds, // GHD060
+ frontmatterHeroImage, // GHD061
// Search-replace rules
searchReplace, // Open-source plugin
diff --git a/src/content-linter/style/github-docs.ts b/src/content-linter/style/github-docs.ts
index 8d53d4631fcb..eddf742b46e6 100644
--- a/src/content-linter/style/github-docs.ts
+++ b/src/content-linter/style/github-docs.ts
@@ -348,6 +348,12 @@ export const githubDocsFrontmatterConfig = {
'partial-markdown-files': false,
'yml-files': false,
},
+ 'frontmatter-hero-image': {
+ // GHD061
+ severity: 'error',
+ 'partial-markdown-files': false,
+ 'yml-files': false,
+ },
}
// Configures rules from the `github/markdownlint-github` repo
diff --git a/src/content-linter/tests/unit/frontmatter-hero-image.ts b/src/content-linter/tests/unit/frontmatter-hero-image.ts
new file mode 100644
index 000000000000..ac3096c70385
--- /dev/null
+++ b/src/content-linter/tests/unit/frontmatter-hero-image.ts
@@ -0,0 +1,128 @@
+import { describe, expect, test } from 'vitest'
+
+import { runRule } from '../../lib/init-test'
+import { frontmatterHeroImage } from '../../lib/linting-rules/frontmatter-hero-image'
+
+const fmOptions = { markdownlintOptions: { frontMatter: null } }
+
+describe(frontmatterHeroImage.names.join(' - '), () => {
+ test('valid absolute heroImage path passes', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: '/assets/images/banner-images/hero-1.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(0)
+ })
+
+ test('non-index.md file is ignored', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: 'invalid-path.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/article.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/article.md']
+ expect(errors.length).toBe(0)
+ })
+
+ test('missing heroImage is ignored', async () => {
+ const markdown = ['---', 'title: Test', '---', '', '# Test'].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(0)
+ })
+
+ test('relative heroImage path fails', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: 'images/hero-1.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(1)
+ expect(errors[0].errorDetail).toContain('must be absolute')
+ })
+
+ test('non-banner-images path fails', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: '/assets/images/other/hero-1.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(1)
+ expect(errors[0].errorDetail).toContain('/assets/images/banner-images/')
+ })
+
+ test('non-existent heroImage file fails', async () => {
+ const markdown = [
+ '---',
+ 'title: Test',
+ "heroImage: '/assets/images/banner-images/non-existent.png'",
+ '---',
+ '',
+ '# Test',
+ ].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(1)
+ expect(errors[0].errorDetail).toContain('does not exist')
+ })
+
+ test('all valid hero images pass', async () => {
+ // Test each valid hero image
+ const validImages = [
+ "heroImage: '/assets/images/banner-images/hero-1.png'",
+ "heroImage: '/assets/images/banner-images/hero-2.png'",
+ "heroImage: '/assets/images/banner-images/hero-3.png'",
+ "heroImage: '/assets/images/banner-images/hero-4.png'",
+ "heroImage: '/assets/images/banner-images/hero-5.png'",
+ "heroImage: '/assets/images/banner-images/hero-6.png'",
+ ]
+
+ for (const heroImageLine of validImages) {
+ const markdown = ['---', 'title: Test', heroImageLine, '---', '', '# Test'].join('\n')
+ const result = await runRule(frontmatterHeroImage, {
+ strings: { 'content/test/index.md': markdown },
+ ...fmOptions,
+ })
+ const errors = result['content/test/index.md']
+ expect(errors.length).toBe(0)
+ }
+ })
+})
From b61005d035d657617eabdd22559693cc68ed6fd1 Mon Sep 17 00:00:00 2001
From: Vanessa
Date: Thu, 23 Oct 2025 06:48:19 +1000
Subject: [PATCH 8/8] Update Copilot code review model usage note (#58130)
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
---
data/reusables/copilot/ccr-model-settings.md | 4 +++-
1 file changed, 3 insertions(+), 1 deletion(-)
diff --git a/data/reusables/copilot/ccr-model-settings.md b/data/reusables/copilot/ccr-model-settings.md
index 2840cf0449be..50ad303d5cc1 100644
--- a/data/reusables/copilot/ccr-model-settings.md
+++ b/data/reusables/copilot/ccr-model-settings.md
@@ -1,2 +1,4 @@
> [!NOTE]
-> {% data variables.copilot.copilot_code-review_short %} may use models that are not enabled on your organization's "Models" settings page. See [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies).
+> {% data variables.copilot.copilot_code-review_short %} may use models that are not enabled on your organization's "Models" settings page. The "Models" settings page only controls {% data variables.copilot.copilot_chat_short %}.
+>
+> Since {% data variables.copilot.copilot_code-review_short %} is generally available, all model usage will be subject to the generally available terms. See [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies).