From c8328099f8ffea45ad81501ec140f5d4087c287e Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 2 Apr 2026 13:34:24 -0700 Subject: [PATCH 1/6] Improve hash focus: handle initial page load (#60298) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/frame/components/ClientSideHashFocus.tsx | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/frame/components/ClientSideHashFocus.tsx b/src/frame/components/ClientSideHashFocus.tsx index b832b6fd02fa..c601d406d3eb 100644 --- a/src/frame/components/ClientSideHashFocus.tsx +++ b/src/frame/components/ClientSideHashFocus.tsx @@ -17,6 +17,10 @@ export function ClientSideHashFocus() { } } + // Handle initial page load with a hash (e.g. direct link to + // docs.github.com/en/discussions#guides-2) + handleHashChange() + window.addEventListener('hashchange', handleHashChange) return () => window.removeEventListener('hashchange', handleHashChange) }, []) From 61430dffc327f70f1babe8b3ceaa1cbbfaa0bd5d Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 2 Apr 2026 13:34:36 -0700 Subject: [PATCH 2/6] Fix tab indicator contrast ratio for WCAG 1.4.11 (#60300) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/tools/components/InArticlePicker.module.scss | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/tools/components/InArticlePicker.module.scss b/src/tools/components/InArticlePicker.module.scss index f274c4144f23..47edd48cfd44 100644 --- a/src/tools/components/InArticlePicker.module.scss +++ b/src/tools/components/InArticlePicker.module.scss @@ -1,4 +1,10 @@ .container { + // Override Primer's selected tab indicator color to meet WCAG 1.4.11 + // non-text contrast minimum of 3:1. The default --color-primer-border-active + // (#FD8C73) has only 2.3:1 contrast. Using a Primer theme variable + // that meets contrast in both light and dark color modes. + --underlineNav-borderColor-active: var(--color-severe-emphasis); + // target the ActionList dropdown that appears when UnderlineNav overflows ul[class*="prc-ActionList-ActionList"] { background-color: var( From 61357b1dffd22cbe8ba8d75a6177daa7c7e6acf2 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 2 Apr 2026 14:03:54 -0700 Subject: [PATCH 3/6] Guard against undefined featuredLinks in TocLanding (#60529) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/landings/components/TocLanding.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/landings/components/TocLanding.tsx b/src/landings/components/TocLanding.tsx index 56e508e9093a..0227af5a82fc 100644 --- a/src/landings/components/TocLanding.tsx +++ b/src/landings/components/TocLanding.tsx @@ -54,7 +54,7 @@ export const TocLanding = () => {
- {featuredLinks.gettingStarted && featuredLinks.popular && ( + {featuredLinks?.gettingStarted && featuredLinks?.popular && (
From 39eae154e1ed6867ca4c78705de78ff4375d1d49 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Thu, 2 Apr 2026 14:22:11 -0700 Subject: [PATCH 4/6] Add tests for structured logging: BUILD_SHA, error serialization, toError() (#60538) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- src/observability/lib/handle-exceptions.ts | 12 +-- src/observability/lib/to-error.ts | 10 +++ src/observability/tests/logger.ts | 93 ++++++++++++++++++++++ src/observability/tests/to-error.ts | 60 ++++++++++++++ 4 files changed, 164 insertions(+), 11 deletions(-) create mode 100644 src/observability/lib/to-error.ts create mode 100644 src/observability/tests/to-error.ts diff --git a/src/observability/lib/handle-exceptions.ts b/src/observability/lib/handle-exceptions.ts index d618d221afb1..08e3d56d721e 100644 --- a/src/observability/lib/handle-exceptions.ts +++ b/src/observability/lib/handle-exceptions.ts @@ -1,19 +1,9 @@ import FailBot from './failbot' +import { toError } from '@/observability/lib/to-error' import { createLogger } from '@/observability/logger' const logger = createLogger(import.meta.url) -// Safely convert an unknown thrown value to an Error, avoiding JSON.stringify -// which can throw on circular references. -function toError(value: Error | unknown): Error { - if (value instanceof Error) return value - try { - return new Error(JSON.stringify(value)) - } catch { - return new Error(String(value)) - } -} - process.on('uncaughtException', async (err: Error | unknown) => { const error = toError(err) logger.error('uncaughtException', { error }) diff --git a/src/observability/lib/to-error.ts b/src/observability/lib/to-error.ts new file mode 100644 index 000000000000..19f7e551b1e5 --- /dev/null +++ b/src/observability/lib/to-error.ts @@ -0,0 +1,10 @@ +// Safely convert an unknown thrown value to an Error, avoiding JSON.stringify +// which can throw on circular references. +export function toError(value: Error | unknown): Error { + if (value instanceof Error) return value + try { + return new Error(JSON.stringify(value)) + } catch { + return new Error(String(value)) + } +} diff --git a/src/observability/tests/logger.ts b/src/observability/tests/logger.ts index 49f3b1d24331..d583e06d203b 100644 --- a/src/observability/tests/logger.ts +++ b/src/observability/tests/logger.ts @@ -431,4 +431,97 @@ describe('createLogger', () => { expect(logOutput).not.toContain('nodeHostname=') }) }) + + describe('BUILD_SHA in production logs', () => { + it('should include build_sha in logfmt output when BUILD_SHA env var is set', async () => { + vi.stubEnv('BUILD_SHA', 'abc123def456') + vi.stubEnv('LOG_LIKE_PRODUCTION', 'true') + + vi.resetModules() + const { createLogger: freshCreateLogger } = await import('@/observability/logger') + + const logger = freshCreateLogger('file:///path/to/test.js') + logger.info('Build SHA test') + + expect(consoleLogs).toHaveLength(1) + const logOutput = consoleLogs[0] + expect(logOutput).toContain('build_sha=abc123def456') + }) + + it('should not include build_sha in logfmt output when BUILD_SHA env var is absent', async () => { + vi.stubEnv('LOG_LIKE_PRODUCTION', 'true') + delete process.env.BUILD_SHA + + vi.resetModules() + const { createLogger: freshCreateLogger } = await import('@/observability/logger') + + const logger = freshCreateLogger('file:///path/to/test.js') + logger.info('No build SHA test') + + expect(consoleLogs).toHaveLength(1) + const logOutput = consoleLogs[0] + expect(logOutput).not.toContain('build_sha=') + }) + }) + + describe('error serialization in production logs', () => { + it('should include error_code and error_name when Error has a .code property', async () => { + vi.stubEnv('LOG_LIKE_PRODUCTION', 'true') + + vi.resetModules() + const { createLogger: freshCreateLogger } = await import('@/observability/logger') + + const logger = freshCreateLogger('file:///path/to/test.js') + const error = new Error('Connection reset') as NodeJS.ErrnoException + error.code = 'ECONNRESET' + logger.error('Network failure', error) + + expect(consoleLogs).toHaveLength(1) + const logOutput = consoleLogs[0] + expect(logOutput).toContain('included.error="Connection reset"') + expect(logOutput).toContain('included.error_code=ECONNRESET') + expect(logOutput).toContain('included.error_name=Error') + expect(logOutput).toContain('included.error_stack=') + }) + + it('should include error_name even when .code is undefined', async () => { + vi.stubEnv('LOG_LIKE_PRODUCTION', 'true') + + vi.resetModules() + const { createLogger: freshCreateLogger } = await import('@/observability/logger') + + const logger = freshCreateLogger('file:///path/to/test.js') + const error = new TypeError('Cannot read property') + logger.error('Type error occurred', error) + + expect(consoleLogs).toHaveLength(1) + const logOutput = consoleLogs[0] + expect(logOutput).toContain('included.error="Cannot read property"') + expect(logOutput).toContain('included.error_name=TypeError') + // When .code is undefined, error_code is present but empty + expect(logOutput).toMatch(/included\.error_code= /) + expect(logOutput).toContain('included.error_stack=') + }) + + it('should serialize multiple errors with indexed keys', async () => { + vi.stubEnv('LOG_LIKE_PRODUCTION', 'true') + + vi.resetModules() + const { createLogger: freshCreateLogger } = await import('@/observability/logger') + + const logger = freshCreateLogger('file:///path/to/test.js') + const error1 = new Error('First') as NodeJS.ErrnoException + error1.code = 'ERR_FIRST' + const error2 = new Error('Second') + logger.error('Multiple errors', error1, error2) + + expect(consoleLogs).toHaveLength(1) + const logOutput = consoleLogs[0] + expect(logOutput).toContain('included.error_1=First') + expect(logOutput).toContain('included.error_1_code=ERR_FIRST') + expect(logOutput).toContain('included.error_1_name=Error') + expect(logOutput).toContain('included.error_2=Second') + expect(logOutput).toContain('included.error_2_name=Error') + }) + }) }) diff --git a/src/observability/tests/to-error.ts b/src/observability/tests/to-error.ts new file mode 100644 index 000000000000..819621bd6428 --- /dev/null +++ b/src/observability/tests/to-error.ts @@ -0,0 +1,60 @@ +import { describe, expect, it } from 'vitest' + +import { toError } from '@/observability/lib/to-error' + +describe('toError', () => { + it('should return Error instances as-is', () => { + const error = new Error('test error') + const result = toError(error) + expect(result).toBe(error) + expect(result.message).toBe('test error') + }) + + it('should return subclassed Error instances as-is', () => { + const error = new TypeError('type error') + const result = toError(error) + expect(result).toBe(error) + expect(result).toBeInstanceOf(TypeError) + }) + + it('should convert a plain string to an Error via JSON.stringify', () => { + const result = toError('something went wrong') + expect(result).toBeInstanceOf(Error) + expect(result.message).toBe('"something went wrong"') + }) + + it('should convert a number to an Error', () => { + const result = toError(42) + expect(result).toBeInstanceOf(Error) + expect(result.message).toBe('42') + }) + + it('should convert null to an Error', () => { + const result = toError(null) + expect(result).toBeInstanceOf(Error) + expect(result.message).toBe('null') + }) + + it('should convert undefined to an Error via JSON.stringify', () => { + const result = toError(undefined) + expect(result).toBeInstanceOf(Error) + // JSON.stringify(undefined) returns undefined (not a string), + // so new Error(undefined) has an empty message + expect(result.message).toBe('') + }) + + it('should convert a plain object to an Error via JSON.stringify', () => { + const result = toError({ code: 'ERR_TIMEOUT', detail: 'took too long' }) + expect(result).toBeInstanceOf(Error) + expect(result.message).toBe('{"code":"ERR_TIMEOUT","detail":"took too long"}') + }) + + it('should fall back to String() for circular references', () => { + const circular: Record = { name: 'loop' } + circular.self = circular + const result = toError(circular) + expect(result).toBeInstanceOf(Error) + // String() on an object returns '[object Object]' + expect(result.message).toBe('[object Object]') + }) +}) From fc697287d2e9295de4ed032c877c1ddd278193e3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Holly=20=F0=9F=A6=92?= <104800384+holly-kassel@users.noreply.github.com> Date: Thu, 2 Apr 2026 18:04:24 -0500 Subject: [PATCH 5/6] Fix misleading artifact storage deletion note (#60553) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- content/billing/concepts/product-billing/github-actions.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/content/billing/concepts/product-billing/github-actions.md b/content/billing/concepts/product-billing/github-actions.md index 63bfa3e0fb3d..de150bb04414 100644 --- a/content/billing/concepts/product-billing/github-actions.md +++ b/content/billing/concepts/product-billing/github-actions.md @@ -165,7 +165,7 @@ If you use 3 GB of artifact storage for 10 days of March and 12 GB for 21 days o At the end of the month, {% data variables.product.github %} rounds your artifact storage to the nearest MB. Therefore, your artifact storage usage for March would be 9.097 GB. > [!NOTE] -> {% data variables.product.github %} updates your artifact storage space within a 6 to 12-hour window. If you delete artifacts, the available space will be reflected in your account during the next scheduled update. +> {% data variables.product.github %} updates your artifact storage usage within 6 to 12 hours. Deleting artifacts frees up space for current storage, but does not reduce your accrued storage usage, which is used to calculate your storage billing for the current billing cycle. ### Example cache storage cost calculation From 83d5c696157d3ed3801cf72f3a0cb450df8dab4e Mon Sep 17 00:00:00 2001 From: Brigit Murtaugh <25310137+bamurtaugh@users.noreply.github.com> Date: Thu, 2 Apr 2026 16:06:38 -0700 Subject: [PATCH 6/6] Revise model hosting documentation for GitHub Copilot (#60552) Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> --- content/copilot/reference/ai-models/model-hosting.md | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md index 9b82998524d9..9d31e94baad5 100644 --- a/content/copilot/reference/ai-models/model-hosting.md +++ b/content/copilot/reference/ai-models/model-hosting.md @@ -1,8 +1,8 @@ --- -title: Hosting of models for GitHub Copilot Chat +title: Hosting of models for GitHub Copilot shortTitle: Model hosting allowTitleToDifferFromFilename: true -intro: 'Learn how different AI models are hosted for {% data variables.copilot.copilot_chat_short %}.' +intro: 'Learn how different AI models are hosted for {% data variables.product.prodname_copilot %}.' versions: feature: copilot category: @@ -99,3 +99,7 @@ Will **only**: When using xAI, input prompts and output completions continue to run through {% data variables.product.prodname_copilot %}'s content filters for public code matching, when applied, along with those for harmful or offensive content. For more information, see [xAI's enterprise terms of service](https://x.ai/legal/terms-of-service-enterprise) on the xAI website. + +## Inline suggestions + +Inline suggestions, including ghost text and next edit suggestions, are powered by models hosted on Azure for {% data variables.copilot.copilot_business_short %} and {% data variables.copilot.copilot_enterprise_short %} plans. {% data variables.copilot.copilot_free_short %} and {% data variables.copilot.copilot_student_short %} user models are hosted on Fireworks AI.