From ad8c362cc8fc46a004cdb2b55e8109494b1479ed Mon Sep 17 00:00:00 2001 From: hubwriter Date: Wed, 5 Nov 2025 18:16:15 +0000 Subject: [PATCH 1/6] Coding agent supports org level custom instructions [GA] (#58115) --- .../prompting/response-customization.md | 18 +++++++++--------- .../copilot/custom-instructions-org-support.md | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/content/copilot/concepts/prompting/response-customization.md b/content/copilot/concepts/prompting/response-customization.md index fd3e7bf07666..26ea2735cbd4 100644 --- a/content/copilot/concepts/prompting/response-customization.md +++ b/content/copilot/concepts/prompting/response-customization.md @@ -30,11 +30,11 @@ category: There are three main types of custom instructions that you can use to customize {% data variables.product.prodname_copilot_short %} responses on the {% data variables.product.github %} website: -* **[Personal custom instructions](#about-personal-custom-instructions)** apply to all conversations you have with {% data variables.copilot.copilot_chat_short %} across the {% data variables.product.github %} website. They allow you to specify your individual preferences, such as preferred language or response style, ensuring that the responses are tailored to your personal needs. +* **[Personal instructions](#about-personal-instructions)** apply to all conversations you have with {% data variables.copilot.copilot_chat_short %} across the {% data variables.product.github %} website. They allow you to specify your individual preferences, such as preferred language or response style, ensuring that the responses are tailored to your personal needs. * **[Repository custom instructions](#about-repository-custom-instructions)** apply to conversations within the context of a specific repository. They are useful for defining project-specific coding standards, frameworks, or tools. For example, you can specify that a repository uses TypeScript and a particular library, ensuring consistent responses for all contributors. * **[Organization custom instructions](#about-organization-custom-instructions)** (public preview) apply to conversations within the context of an organization on the {% data variables.product.github %} website. They are ideal for enforcing organization-wide preferences, such as a common language or security guidelines. Organization custom instructions can only be set by organization owners for organizations with a {% data variables.copilot.copilot_enterprise_short %} subscription. -## About personal custom instructions +## About personal instructions {% data reusables.copilot.personal-instructions-note %} @@ -67,12 +67,12 @@ Some examples of instructions you could add are: The following list shows the complete order of precedence, with instructions higher in this list taking precedence over those lower in the list: -* Personal custom instructions -* Repository custom instructions: - * Path-specific instructions in any applicable `.github/instructions/**/NAME.instructions.md` file - * Repository-wide instructions in the `.github/copilot-instructions.md` file - * Agent instructions (for example, in an `AGENTS.md` file) -* Organization custom instructions +* **Personal** instructions +* **Repository** custom instructions: + * **Path-specific** instructions in any applicable `.github/instructions/**/NAME.instructions.md` file + * **Repository-wide** instructions in the `.github/copilot-instructions.md` file + * **Agent** instructions (for example, in an `AGENTS.md` file) +* **Organization** custom instructions {% data reusables.copilot.custom-instructions-conflict %} @@ -81,7 +81,7 @@ The following list shows the complete order of precedence, with instructions hig Custom instructions consist of natural language instructions and are most effective when they are short, self-contained statements. Consider the scope over which you want the instruction to apply when choosing whether to add an instruction on the personal, repository, or organization level. Here are some common use cases and examples for each type of custom instructions: -* **Personal custom instructions:** +* **Personal instructions:** * Preferred individual language: `Always respond in Portuguese.` * Individual response preferences: `Explain a single concept per line. Be clear and concise.` * **Repository custom instructions:** diff --git a/data/reusables/copilot/custom-instructions-org-support.md b/data/reusables/copilot/custom-instructions-org-support.md index 9477534df5ec..e9a87bc82949 100644 --- a/data/reusables/copilot/custom-instructions-org-support.md +++ b/data/reusables/copilot/custom-instructions-org-support.md @@ -1,4 +1,4 @@ > [!NOTE] > This feature is currently in {% data variables.release-phases.public_preview %} and is subject to change. > -> **Support:** Organization custom instructions are currently only supported for {% data variables.copilot.copilot_chat_short %} on {% data variables.product.prodname_dotcom_the_website %} and {% data variables.copilot.copilot_code-review_short %} on {% data variables.product.prodname_dotcom_the_website %}. +> **Support:** Organization custom instructions are currently only supported for {% data variables.copilot.copilot_chat_short %} on {% data variables.product.prodname_dotcom_the_website %}, {% data variables.copilot.copilot_code-review_short %} on {% data variables.product.prodname_dotcom_the_website %} and {% data variables.copilot.copilot_coding_agent %} on {% data variables.product.prodname_dotcom_the_website %}. From c55f5c064e891e0225e0171b399c7fc4cd90d99e Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:31:26 -0800 Subject: [PATCH 2/6] Sync secret scanning data (#58360) Co-authored-by: mc <42146119+mchammer01@users.noreply.github.com> --- src/secret-scanning/data/public-docs.yml | 12 ++++++------ src/secret-scanning/lib/config.json | 4 ++-- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/secret-scanning/data/public-docs.yml b/src/secret-scanning/data/public-docs.yml index f7ac53854689..a3cc589780ee 100644 --- a/src/secret-scanning/data/public-docs.yml +++ b/src/secret-scanning/data/public-docs.yml @@ -2805,7 +2805,7 @@ isPrivateWithGhas: true hasPushProtection: true hasValidityCheck: false - base64Supported: false + base64Supported: true isduplicate: true - provider: Google supportedSecret: Google OAuth Refresh Token @@ -4067,7 +4067,7 @@ fpt: '*' ghec: '*' isPublic: true - isPrivateWithGhas: false + isPrivateWithGhas: true hasPushProtection: false hasValidityCheck: false base64Supported: false @@ -4272,7 +4272,7 @@ fpt: '*' ghec: '*' isPublic: true - isPrivateWithGhas: false + isPrivateWithGhas: true hasPushProtection: false hasValidityCheck: false base64Supported: false @@ -4647,7 +4647,7 @@ fpt: '*' ghec: '*' isPublic: true - isPrivateWithGhas: false + isPrivateWithGhas: true hasPushProtection: false hasValidityCheck: false base64Supported: false @@ -4659,7 +4659,7 @@ fpt: '*' ghec: '*' isPublic: true - isPrivateWithGhas: false + isPrivateWithGhas: true hasPushProtection: false hasValidityCheck: false base64Supported: false @@ -4710,7 +4710,7 @@ fpt: '*' ghec: '*' isPublic: true - isPrivateWithGhas: false + isPrivateWithGhas: true hasPushProtection: false hasValidityCheck: false base64Supported: false diff --git a/src/secret-scanning/lib/config.json b/src/secret-scanning/lib/config.json index c27a54dddca6..c2d68b18b2c0 100644 --- a/src/secret-scanning/lib/config.json +++ b/src/secret-scanning/lib/config.json @@ -1,5 +1,5 @@ { - "sha": "2b0240ae95dae6b0857b3b679d3c01d88da5aa2e", - "blob-sha": "60dd4263fa67848500bb3595fff950b7b8b81520", + "sha": "3bed2fa063dac9b9fe32ad40e443f9f2d13552cb", + "blob-sha": "4f068f52cef990e0c1be118f1ca4772ec504a33c", "targetFilename": "code-security/secret-scanning/introduction/supported-secret-scanning-patterns" } \ No newline at end of file From f677421627f7f5a68d254d037d19e7ce2e04cbeb Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Wed, 5 Nov 2025 10:37:06 -0800 Subject: [PATCH 3/6] Update OpenAPI Description (#58361) Co-authored-by: Sunbrye Ly <56200261+sunbrye@users.noreply.github.com> --- .../fine-grained-pat-permissions.json | 42 ++++++++++++++++ .../ghec-2022-11-28/fine-grained-pat.json | 24 +++++++++ .../server-to-server-permissions.json | 50 +++++++++++++++++++ .../server-to-server-rest.json | 24 +++++++++ .../ghec-2022-11-28/user-to-server-rest.json | 24 +++++++++ src/github-apps/lib/config.json | 2 +- src/rest/data/ghec-2022-11-28/schema.json | 48 ++++++++++++------ src/rest/lib/config.json | 2 +- src/webhooks/lib/config.json | 2 +- 9 files changed, 199 insertions(+), 19 deletions(-) diff --git a/src/github-apps/data/ghec-2022-11-28/fine-grained-pat-permissions.json b/src/github-apps/data/ghec-2022-11-28/fine-grained-pat-permissions.json index c426fd69cd0a..4fb3219f6d04 100644 --- a/src/github-apps/data/ghec-2022-11-28/fine-grained-pat-permissions.json +++ b/src/github-apps/data/ghec-2022-11-28/fine-grained-pat-permissions.json @@ -1,4 +1,46 @@ { + "enterprise_copilot_metrics": { + "title": "Enterprise Copilot metrics", + "displayTitle": "Enterprise permissions for \"Enterprise Copilot metrics\"", + "permissions": [ + { + "category": "copilot", + "slug": "get-copilot-enterprise-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-1-day", + "additional-permissions": false, + "access": "read" + }, + { + "category": "copilot", + "slug": "get-copilot-enterprise-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-28-day/latest", + "additional-permissions": false, + "access": "read" + }, + { + "category": "copilot", + "slug": "get-copilot-users-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-1-day", + "additional-permissions": false, + "access": "read" + }, + { + "category": "copilot", + "slug": "get-copilot-users-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-28-day/latest", + "additional-permissions": false, + "access": "read" + } + ] + }, "enterprise_custom_properties_for_organizations": { "title": "Enterprise custom properties for organizations", "displayTitle": "Enterprise permissions for \"Enterprise custom properties for organizations\"", diff --git a/src/github-apps/data/ghec-2022-11-28/fine-grained-pat.json b/src/github-apps/data/ghec-2022-11-28/fine-grained-pat.json index 4d17557a4693..cf30feebc91f 100644 --- a/src/github-apps/data/ghec-2022-11-28/fine-grained-pat.json +++ b/src/github-apps/data/ghec-2022-11-28/fine-grained-pat.json @@ -2152,6 +2152,30 @@ } ], "copilot": [ + { + "slug": "get-copilot-enterprise-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-1-day" + }, + { + "slug": "get-copilot-enterprise-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-28-day/latest" + }, + { + "slug": "get-copilot-users-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-1-day" + }, + { + "slug": "get-copilot-users-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-28-day/latest" + }, { "slug": "get-copilot-seat-information-and-settings-for-an-organization", "subcategory": "copilot-user-management", diff --git a/src/github-apps/data/ghec-2022-11-28/server-to-server-permissions.json b/src/github-apps/data/ghec-2022-11-28/server-to-server-permissions.json index 977613a34d7e..f639905e557f 100644 --- a/src/github-apps/data/ghec-2022-11-28/server-to-server-permissions.json +++ b/src/github-apps/data/ghec-2022-11-28/server-to-server-permissions.json @@ -187,6 +187,56 @@ } ] }, + "enterprise_copilot_metrics": { + "title": "Enterprise Copilot metrics", + "displayTitle": "Enterprise permissions for \"Enterprise Copilot metrics\"", + "permissions": [ + { + "category": "copilot", + "slug": "get-copilot-enterprise-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-1-day", + "access": "read", + "user-to-server": true, + "server-to-server": true, + "additional-permissions": false + }, + { + "category": "copilot", + "slug": "get-copilot-enterprise-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-28-day/latest", + "access": "read", + "user-to-server": true, + "server-to-server": true, + "additional-permissions": false + }, + { + "category": "copilot", + "slug": "get-copilot-users-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-1-day", + "access": "read", + "user-to-server": true, + "server-to-server": true, + "additional-permissions": false + }, + { + "category": "copilot", + "slug": "get-copilot-users-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-28-day/latest", + "access": "read", + "user-to-server": true, + "server-to-server": true, + "additional-permissions": false + } + ] + }, "enterprise_scim": { "title": "Enterprise SCIM", "displayTitle": "Enterprise permissions for \"Enterprise SCIM\"", diff --git a/src/github-apps/data/ghec-2022-11-28/server-to-server-rest.json b/src/github-apps/data/ghec-2022-11-28/server-to-server-rest.json index 82445dd1a5ce..9770f9b67a6c 100644 --- a/src/github-apps/data/ghec-2022-11-28/server-to-server-rest.json +++ b/src/github-apps/data/ghec-2022-11-28/server-to-server-rest.json @@ -2092,6 +2092,30 @@ } ], "copilot": [ + { + "slug": "get-copilot-enterprise-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-1-day" + }, + { + "slug": "get-copilot-enterprise-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-28-day/latest" + }, + { + "slug": "get-copilot-users-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-1-day" + }, + { + "slug": "get-copilot-users-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-28-day/latest" + }, { "slug": "get-copilot-seat-information-and-settings-for-an-organization", "subcategory": "copilot-user-management", diff --git a/src/github-apps/data/ghec-2022-11-28/user-to-server-rest.json b/src/github-apps/data/ghec-2022-11-28/user-to-server-rest.json index 0799399fd726..0776b01ba43c 100644 --- a/src/github-apps/data/ghec-2022-11-28/user-to-server-rest.json +++ b/src/github-apps/data/ghec-2022-11-28/user-to-server-rest.json @@ -2376,6 +2376,30 @@ } ], "copilot": [ + { + "slug": "get-copilot-enterprise-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-1-day" + }, + { + "slug": "get-copilot-enterprise-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/enterprise-28-day/latest" + }, + { + "slug": "get-copilot-users-usage-metrics-for-a-specific-day", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-1-day" + }, + { + "slug": "get-copilot-users-usage-metrics", + "subcategory": "copilot-metrics", + "verb": "get", + "requestPath": "/enterprises/{enterprise}/copilot/metrics/reports/users-28-day/latest" + }, { "slug": "get-copilot-seat-information-and-settings-for-an-organization", "subcategory": "copilot-user-management", diff --git a/src/github-apps/lib/config.json b/src/github-apps/lib/config.json index c78454438a91..9c5aeeef728d 100644 --- a/src/github-apps/lib/config.json +++ b/src/github-apps/lib/config.json @@ -60,5 +60,5 @@ "2022-11-28" ] }, - "sha": "b972b823e3572c9e6b64c3341401184e4eb0a474" + "sha": "e973182b530ffad0765b7713d0a9f80a0f46a220" } \ No newline at end of file diff --git a/src/rest/data/ghec-2022-11-28/schema.json b/src/rest/data/ghec-2022-11-28/schema.json index 330a28ffb9a1..9640f9829a83 100644 --- a/src/rest/data/ghec-2022-11-28/schema.json +++ b/src/rest/data/ghec-2022-11-28/schema.json @@ -265784,10 +265784,14 @@ ], "previews": [], "progAccess": { - "userToServerRest": false, - "serverToServer": false, - "fineGrainedPat": false, - "permissions": [] + "userToServerRest": true, + "serverToServer": true, + "fineGrainedPat": true, + "permissions": [ + { + "\"Enterprise Copilot metrics\" enterprise permissions": "read" + } + ] } }, { @@ -265885,10 +265889,14 @@ ], "previews": [], "progAccess": { - "userToServerRest": false, - "serverToServer": false, - "fineGrainedPat": false, - "permissions": [] + "userToServerRest": true, + "serverToServer": true, + "fineGrainedPat": true, + "permissions": [ + { + "\"Enterprise Copilot metrics\" enterprise permissions": "read" + } + ] } }, { @@ -265992,10 +266000,14 @@ ], "previews": [], "progAccess": { - "userToServerRest": false, - "serverToServer": false, - "fineGrainedPat": false, - "permissions": [] + "userToServerRest": true, + "serverToServer": true, + "fineGrainedPat": true, + "permissions": [ + { + "\"Enterprise Copilot metrics\" enterprise permissions": "read" + } + ] } }, { @@ -266093,10 +266105,14 @@ ], "previews": [], "progAccess": { - "userToServerRest": false, - "serverToServer": false, - "fineGrainedPat": false, - "permissions": [] + "userToServerRest": true, + "serverToServer": true, + "fineGrainedPat": true, + "permissions": [ + { + "\"Enterprise Copilot metrics\" enterprise permissions": "read" + } + ] } }, { diff --git a/src/rest/lib/config.json b/src/rest/lib/config.json index 91332e600899..342c5cad5b5b 100644 --- a/src/rest/lib/config.json +++ b/src/rest/lib/config.json @@ -47,5 +47,5 @@ ] } }, - "sha": "b972b823e3572c9e6b64c3341401184e4eb0a474" + "sha": "e973182b530ffad0765b7713d0a9f80a0f46a220" } \ No newline at end of file diff --git a/src/webhooks/lib/config.json b/src/webhooks/lib/config.json index d66288ac1d5b..e2868013455a 100644 --- a/src/webhooks/lib/config.json +++ b/src/webhooks/lib/config.json @@ -1,3 +1,3 @@ { - "sha": "b972b823e3572c9e6b64c3341401184e4eb0a474" + "sha": "e973182b530ffad0765b7713d0a9f80a0f46a220" } \ No newline at end of file From 752ce26cf1c44cd05d83cf17dd40203a70820980 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 5 Nov 2025 11:37:59 -0800 Subject: [PATCH 4/6] Auto-generate aria-labels for octicons and remove linter rule (#58349) --- .../contributing/content-linter-rules.md | 1 - src/article-api/tests/article-body.ts | 69 ++++++++ src/content-linter/lib/linting-rules/index.ts | 3 +- .../lib/linting-rules/octicon-aria-labels.ts | 58 ------- src/content-linter/style/github-docs.ts | 7 +- .../tests/unit/octicon-aria-labels.ts | 155 ------------------ src/content-render/liquid/octicon.ts | 12 +- src/content-render/tests/octicon.ts | 30 ++++ .../start-your-journey/hello-world.md | 14 ++ 9 files changed, 126 insertions(+), 223 deletions(-) create mode 100644 src/article-api/tests/article-body.ts delete mode 100644 src/content-linter/lib/linting-rules/octicon-aria-labels.ts delete mode 100644 src/content-linter/tests/unit/octicon-aria-labels.ts diff --git a/data/reusables/contributing/content-linter-rules.md b/data/reusables/contributing/content-linter-rules.md index a2555eb777e0..243d60f22a38 100644 --- a/data/reusables/contributing/content-linter-rules.md +++ b/data/reusables/contributing/content-linter-rules.md @@ -52,7 +52,6 @@ | GHD041 | third-party-action-pinning | Code examples that use third-party actions must always pin to a full length commit SHA | error | feature, actions | | GHD042 | liquid-tag-whitespace | Liquid tags should start and end with one whitespace. Liquid tag arguments should be separated by only one whitespace. | error | liquid, format | | GHD043 | link-quotation | Internal link titles must not be surrounded by quotations | error | links, url | -| GHD044 | octicon-aria-labels | Octicons should always have an aria-label attribute even if aria-hidden. | warning | accessibility, octicons | | GHD045 | code-annotation-comment-spacing | Code comments in annotation blocks must have exactly one space after the comment character(s) | warning | code, comments, annotate, spacing | | GHD046 | outdated-release-phase-terminology | Outdated release phase terminology should be replaced with current GitHub terminology | warning | terminology, consistency, release-phases | | GHD047 | table-column-integrity | Tables must have consistent column counts across all rows | warning | tables, accessibility, formatting | diff --git a/src/article-api/tests/article-body.ts b/src/article-api/tests/article-body.ts new file mode 100644 index 000000000000..dadb514f616f --- /dev/null +++ b/src/article-api/tests/article-body.ts @@ -0,0 +1,69 @@ +import { beforeAll, describe, expect, test } from 'vitest' + +import { get } from '@/tests/helpers/e2etest' + +const makeURL = (pathname: string): string => + `/api/article/body?${new URLSearchParams({ pathname })}` + +describe('article body api', () => { + beforeAll(() => { + // If you didn't set the `ROOT` variable, the tests will fail rather + // cryptically. So as a warning for engineers running these tests, + // alert in case it was accidentally forgotten. + if (!process.env.ROOT) { + console.warn( + 'WARNING: The article body tests require the ROOT environment variable to be set to the fixture root', + ) + } + }) + + test('happy path', async () => { + const res = await get(makeURL('/en/get-started/start-your-journey/hello-world')) + expect(res.statusCode).toBe(200) + expect(res.body).toContain('## Introduction') + expect(res.body).toContain('This is just a test.') + expect(res.headers['content-type']).toContain('text/markdown') + }) + + test('octicons auto-generate aria-labels', async () => { + const res = await get(makeURL('/en/get-started/start-your-journey/hello-world')) + expect(res.statusCode).toBe(200) + + // Check that octicons without aria-label get auto-generated ones + expect(res.body).toContain('aria-label="check icon"') + expect(res.body).toContain('aria-label="git branch icon"') + }) + + test('octicons with custom aria-labels use the custom value', async () => { + const res = await get(makeURL('/en/get-started/start-your-journey/hello-world')) + expect(res.statusCode).toBe(200) + + // Check that custom aria-labels are preserved + expect(res.body).toContain('aria-label="Supported"') + expect(res.body).toContain('aria-label="Not supported"') + }) + + test('octicons with other attributes still get auto-generated aria-labels', async () => { + const res = await get(makeURL('/en/get-started/start-your-journey/hello-world')) + expect(res.statusCode).toBe(200) + + // Check that octicons with width attribute still get aria-labels + expect(res.body).toContain('aria-label="rocket icon"') + expect(res.body).toContain('width="32"') + }) + + test('a pathname that does not exist', async () => { + const res = await get(makeURL('/en/never/heard/of')) + expect(res.statusCode).toBe(404) + const { error } = JSON.parse(res.body) + expect(error).toBe("No page found for '/en/never/heard/of'") + }) + + test('non-article pages return error', async () => { + // Index pages are not articles and should not be renderable + const res = await get(makeURL('/en/get-started')) + expect(res.statusCode).toBe(403) + const { error } = JSON.parse(res.body) + expect(error).toContain("isn't yet available in markdown") + }) +}) diff --git a/src/content-linter/lib/linting-rules/index.ts b/src/content-linter/lib/linting-rules/index.ts index 233de0259637..3afc9fa151bd 100644 --- a/src/content-linter/lib/linting-rules/index.ts +++ b/src/content-linter/lib/linting-rules/index.ts @@ -42,7 +42,6 @@ import { tableColumnIntegrity } from '@/content-linter/lib/linting-rules/table-c import { thirdPartyActionPinning } from '@/content-linter/lib/linting-rules/third-party-action-pinning' import { liquidTagWhitespace } from '@/content-linter/lib/linting-rules/liquid-tag-whitespace' import { linkQuotation } from '@/content-linter/lib/linting-rules/link-quotation' -import { octiconAriaLabels } from '@/content-linter/lib/linting-rules/octicon-aria-labels' import { liquidIfversionVersions } from '@/content-linter/lib/linting-rules/liquid-ifversion-versions' import { outdatedReleasePhaseTerminology } from '@/content-linter/lib/linting-rules/outdated-release-phase-terminology' import { frontmatterVersionsWhitespace } from '@/content-linter/lib/linting-rules/frontmatter-versions-whitespace' @@ -105,7 +104,7 @@ export const gitHubDocsMarkdownlint = { thirdPartyActionPinning, // GHD041 liquidTagWhitespace, // GHD042 linkQuotation, // GHD043 - octiconAriaLabels, // GHD044 + // GHD044 removed - octicon aria-labels are now auto-generated codeAnnotationCommentSpacing, // GHD045 outdatedReleasePhaseTerminology, // GHD046 tableColumnIntegrity, // GHD047 diff --git a/src/content-linter/lib/linting-rules/octicon-aria-labels.ts b/src/content-linter/lib/linting-rules/octicon-aria-labels.ts deleted file mode 100644 index 376c67185f8e..000000000000 --- a/src/content-linter/lib/linting-rules/octicon-aria-labels.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { TokenKind } from 'liquidjs' -import { getLiquidTokens, getPositionData } from '../helpers/liquid-utils' -import { addFixErrorDetail } from '../helpers/utils' -import type { RuleParams, RuleErrorCallback, Rule } from '../../types' -/* -Octicons should always have an aria-label attribute even if aria hidden. For example: - - DO use aria-label - {% octicon "alert" aria-label="alert" %} - {% octicon "alert" aria-label="alert" aria-hidden="true" %} - {% octicon "alert" aria-label="alert" aria-hidden="true" class="foo" %} - - This is necessary for copilot to be able to recognize the svgs correctly when using our API. - -*/ - -export const octiconAriaLabels: Rule = { - names: ['GHD044', 'octicon-aria-labels'], - description: 'Octicons should always have an aria-label attribute even if aria-hidden.', - tags: ['accessibility', 'octicons'], - parser: 'markdownit', - function: (params: RuleParams, onError: RuleErrorCallback) => { - const content = params.lines.join('\n') - // Using 'any' type for tokens as getLiquidTokens returns tokens from liquid-utils.ts which lacks type definitions - const tokens = getLiquidTokens(content) - .filter((token: any) => token.kind === TokenKind.Tag) - .filter((token: any) => token.name === 'octicon') - - for (const token of tokens) { - const { lineNumber, column, length } = getPositionData(token, params.lines) - - const hasAriaLabel = token.args.includes('aria-label=') - - if (!hasAriaLabel) { - const range = [column, length] - - const octiconNameMatch = token.args.match(/["']([^"']+)["']/) - const octiconName = octiconNameMatch ? octiconNameMatch[1] : 'icon' - const originalContent = token.content - const fixedContent = `${originalContent} aria-label="${octiconName}"` - - addFixErrorDetail( - onError, - lineNumber, - `octicon should have an aria-label even if aria hidden. Try using 'aria-label=${octiconName}'`, - token.content, - range, - { - lineNumber, - editColumn: column, - deleteCount: length, - insertText: `{% ${fixedContent} %}`, - }, - ) - } - } - }, -} diff --git a/src/content-linter/style/github-docs.ts b/src/content-linter/style/github-docs.ts index aa914dc108a5..fd9312af5aa8 100644 --- a/src/content-linter/style/github-docs.ts +++ b/src/content-linter/style/github-docs.ts @@ -162,12 +162,7 @@ const githubDocsConfig = { 'partial-markdown-files': true, 'yml-files': true, }, - 'octicon-aria-labels': { - // GHD044 - severity: 'warning', - 'partial-markdown-files': true, - 'yml-files': true, - }, + // GHD044 removed - octicon aria-labels are now auto-generated 'code-annotation-comment-spacing': { // GHD045 severity: 'warning', diff --git a/src/content-linter/tests/unit/octicon-aria-labels.ts b/src/content-linter/tests/unit/octicon-aria-labels.ts deleted file mode 100644 index aa4f03239a0a..000000000000 --- a/src/content-linter/tests/unit/octicon-aria-labels.ts +++ /dev/null @@ -1,155 +0,0 @@ -import { describe, expect, test } from 'vitest' -import { octiconAriaLabels } from '../../lib/linting-rules/octicon-aria-labels' - -interface ErrorInfo { - lineNumber: number - detail?: string - context?: string - range?: [number, number] - fixInfo?: any // Matches RuleErrorCallback signature - fixInfo structure varies by rule -} - -describe('octicon-aria-labels', () => { - const rule = octiconAriaLabels - - // Helper to create onError callback that captures errors - function createErrorCollector() { - const errors: ErrorInfo[] = [] - // Using any because the actual rule implementation calls onError with an object, - // not individual parameters as defined in RuleErrorCallback - const onError = (errorInfo: any) => { - errors.push(errorInfo) - } - return { errors, onError } - } - - test('detects octicon without aria-label', () => { - const { errors, onError } = createErrorCollector() - - const content = ['This is a test with an octicon:', '{% octicon "alert" %}', 'Some more text.'] - - rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError) - - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(2) - expect(errors[0].detail).toContain('aria-label=alert') - expect(errors[0].fixInfo.insertText).toContain('aria-label="alert"') - }) - - test('ignores octicons with aria-label', () => { - const { errors, onError } = createErrorCollector() - - const content = [ - 'This is a test with a proper octicon:', - '{% octicon "alert" aria-label="alert" %}', - 'Some more text.', - ] - - rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError) - - expect(errors.length).toBe(0) - }) - - test('detects multiple octicons without aria-label', () => { - const { errors, onError } = createErrorCollector() - - const content = [ - 'This is a test with multiple octicons:', - '{% octicon "alert" %}', - 'Some text in between.', - '{% octicon "check" %}', - 'More text.', - ] - - rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError) - - expect(errors.length).toBe(2) - expect(errors[0].lineNumber).toBe(2) - expect(errors[0].detail).toContain('aria-label=alert') - expect(errors[1].lineNumber).toBe(4) - expect(errors[1].detail).toContain('aria-label=check') - }) - - test('ignores non-octicon liquid tags', () => { - const { errors, onError } = createErrorCollector() - - const content = [ - 'This is a test with non-octicon tags:', - '{% data foo.bar %}', - '{% ifversion fpt %}', - 'Some text.', - '{% endif %}', - ] - - rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError) - - expect(errors.length).toBe(0) - }) - - test('suggests correct fix for octicon with other attributes', () => { - const { errors, onError } = createErrorCollector() - - const content = [ - 'This is a test with an octicon with other attributes:', - '{% octicon "plus" aria-hidden="true" class="foo" %}', - 'Some more text.', - ] - - rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError) - - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(2) - expect(errors[0].fixInfo.insertText).toContain('aria-label="plus"') - expect(errors[0].fixInfo.insertText).toContain('aria-hidden="true"') - expect(errors[0].fixInfo.insertText).toContain('class="foo"') - }) - - test('handles octicons with unusual spacing', () => { - const { errors, onError } = createErrorCollector() - - const content = [ - 'This is a test with unusual spacing:', - '{% octicon "x" %}', - 'Some more text.', - ] - - rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError) - - expect(errors.length).toBe(1) - expect(errors[0].lineNumber).toBe(2) - expect(errors[0].detail).toContain('aria-label=x') - }) - - test('handles octicons split across multiple lines', () => { - const { errors, onError } = createErrorCollector() - - const content = [ - 'This is a test with a multi-line octicon:', - '{% octicon "chevron-down"', - ' class="dropdown-menu-icon"', - '%}', - 'Some more text.', - ] - - rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError) - - expect(errors.length).toBe(1) - expect(errors[0].detail).toContain('aria-label=chevron-down') - }) - - test('falls back to "icon" when octicon name cannot be determined', () => { - const { errors, onError } = createErrorCollector() - - const content = [ - 'This is a test with a malformed octicon:', - '{% octicon variable %}', - 'Some more text.', - ] - - rule.function({ name: 'test.md', lines: content, frontMatterLines: [] }, onError) - - expect(errors.length).toBe(1) - expect(errors[0].detail).toContain('aria-label=icon') - expect(errors[0].fixInfo.insertText).toContain('aria-label="icon"') - }) -}) diff --git a/src/content-render/liquid/octicon.ts b/src/content-render/liquid/octicon.ts index c8ceaa3a447d..47d5a0ee8674 100644 --- a/src/content-render/liquid/octicon.ts +++ b/src/content-render/liquid/octicon.ts @@ -24,7 +24,10 @@ const SyntaxHelp = 'Syntax Error in tag \'octicon\' - Valid syntax: octicon " * {% octicon "check" width="64" aria-label="Example label" %} */ const Octicon: LiquidTag = { @@ -70,6 +73,13 @@ const Octicon: LiquidTag = { throw new Error(`Octicon ${this.icon} does not exist`) } + // Auto-generate aria-label if not provided + // Replace non-alphanumeric characters with spaces and append " icon" + if (!this.options['aria-label']) { + const defaultLabel = `${this.icon.toLowerCase().replace(/[^a-z0-9]+/gi, ' ')} icon` + this.options['aria-label'] = defaultLabel + } + const result: string = octicons[this.icon].toSVG(this.options) return result }, diff --git a/src/content-render/tests/octicon.ts b/src/content-render/tests/octicon.ts index e17b1088df78..7c6387c46a49 100644 --- a/src/content-render/tests/octicon.ts +++ b/src/content-render/tests/octicon.ts @@ -42,4 +42,34 @@ describe('octicon tag', () => { 'Octicon pizza-patrol does not exist', ) }) + + test('auto-generates aria-label when not provided', async () => { + const actual = await renderContent('{% octicon "check" %}') + expect(actual).toContain(' { + const actual = await renderContent('{% octicon "git-branch" %}') + expect(actual).toContain(' { + const actual = await renderContent('{% octicon "check" aria-label="Supported" %}') + expect(actual).toContain(' { + const actual = await renderContent('{% octicon "filter" width="32" %}') + expect(actual).toContain(' Date: Wed, 5 Nov 2025 13:48:48 -0600 Subject: [PATCH 5/6] bump actions/setup-node to v6 (#58363) --- .github/actions/node-npm-setup/action.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/actions/node-npm-setup/action.yml b/.github/actions/node-npm-setup/action.yml index fcc88e3d401e..5f488d7d935e 100644 --- a/.github/actions/node-npm-setup/action.yml +++ b/.github/actions/node-npm-setup/action.yml @@ -17,7 +17,7 @@ runs: key: ${{ runner.os }}-node_modules-${{ hashFiles('package*.json') }}-${{ hashFiles('.github/actions/node-npm-setup/action.yml') }} - name: Setup Node.js - uses: actions/setup-node@60edb5dd545a775178f52524783378180af0d1f8 # v4.0.2 + uses: actions/setup-node@2028fbc5c25fe9cf00d9f06a71cc4710d4507903 # v6.0.0 with: node-version-file: 'package.json' cache: npm From 9bc51e529eb805006176cfa277cbf3aa566607a3 Mon Sep 17 00:00:00 2001 From: Joe Clark <31087804+jc-clark@users.noreply.github.com> Date: Wed, 5 Nov 2025 13:11:11 -0800 Subject: [PATCH 6/6] Update dynamic Copilot prompts for screen reader accessibility (#58351) --- src/content-render/lib/prompt-id.ts | 10 ++ src/content-render/liquid/prompt.ts | 8 +- src/content-render/tests/prompt-id.ts | 98 ++++++++++++++++++++ src/content-render/unified/code-header.ts | 17 +++- src/content-render/unified/copilot-prompt.ts | 19 +++- 5 files changed, 143 insertions(+), 9 deletions(-) create mode 100644 src/content-render/lib/prompt-id.ts create mode 100644 src/content-render/tests/prompt-id.ts diff --git a/src/content-render/lib/prompt-id.ts b/src/content-render/lib/prompt-id.ts new file mode 100644 index 000000000000..1001533dfbde --- /dev/null +++ b/src/content-render/lib/prompt-id.ts @@ -0,0 +1,10 @@ +import murmur from 'imurmurhash' + +/** + * Generate a deterministic ID for a prompt based on its content. + * Uses MurmurHash to create a unique ID that remains consistent across renders, + * avoiding hydration mismatches in the client. + */ +export function generatePromptId(promptContent: string): string { + return murmur('prompt').hash(promptContent).result().toString() +} diff --git a/src/content-render/liquid/prompt.ts b/src/content-render/liquid/prompt.ts index b63b6c9b3e56..4a2c666d0657 100644 --- a/src/content-render/liquid/prompt.ts +++ b/src/content-render/liquid/prompt.ts @@ -2,6 +2,7 @@ // Defines {% prompt %}…{% endprompt %} to wrap its content in and append the Copilot icon. import octicons from '@primer/octicons' +import { generatePromptId } from '../lib/prompt-id' interface LiquidTag { type: 'block' @@ -30,10 +31,13 @@ export const Prompt: LiquidTag = { // Render the inner Markdown, wrap in , then append the SVG *render(scope: any): Generator { const content = yield this.liquid.renderer.renderTemplates(this.templates, scope) + const contentString = String(content) // build a URL with the prompt text encoded as query parameter - const promptParam: string = encodeURIComponent(content as string) + const promptParam: string = encodeURIComponent(contentString) const href: string = `https://github.com/copilot?prompt=${promptParam}` - return `${content}${octicons.copilot.toSVG()}` + // Use murmur hash for deterministic ID (avoids hydration mismatch) + const promptId: string = generatePromptId(contentString) + return `${content}${octicons.copilot.toSVG()}` }, } diff --git a/src/content-render/tests/prompt-id.ts b/src/content-render/tests/prompt-id.ts new file mode 100644 index 000000000000..ce7441e8832b --- /dev/null +++ b/src/content-render/tests/prompt-id.ts @@ -0,0 +1,98 @@ +import { describe, expect, test } from 'vitest' +import { generatePromptId } from '@/content-render/lib/prompt-id' + +describe('generatePromptId', () => { + test('generates consistent IDs for same content', () => { + const content = 'example prompt text' + const id1 = generatePromptId(content) + const id2 = generatePromptId(content) + expect(id1).toBe(id2) + }) + + test('generates different IDs for different content', () => { + const id1 = generatePromptId('prompt one') + const id2 = generatePromptId('prompt two') + expect(id1).not.toBe(id2) + }) + + test('generates numeric string IDs', () => { + const id = generatePromptId('test prompt') + expect(typeof id).toBe('string') + expect(Number.isNaN(Number(id))).toBe(false) + }) + + test('handles empty strings', () => { + const id = generatePromptId('') + expect(typeof id).toBe('string') + expect(id.length).toBeGreaterThan(0) + }) + + test('handles special characters', () => { + const id1 = generatePromptId('prompt with\nnewlines') + const id2 = generatePromptId('prompt with\ttabs') + const id3 = generatePromptId('prompt with "quotes"') + expect(typeof id1).toBe('string') + expect(typeof id2).toBe('string') + expect(typeof id3).toBe('string') + expect(id1).not.toBe(id2) + expect(id2).not.toBe(id3) + }) + + test('generates deterministic IDs (regression test)', () => { + // These specific values ensure the hash function remains consistent + expect(generatePromptId('hello world')).toBe('1730621824') + expect(generatePromptId('test')).toBe('4180565944') + }) + + test('handles prompts with code context (ref pattern)', () => { + // When ref= is used, the prompt includes referenced code + prompt text separated by newline + const codeContext = + 'function logPersonAge(name, age, revealAge) {\n if (revealAge) {\n console.log(name);\n }\n}' + const promptText = 'Improve the variable names in this function' + const combinedPrompt = `${codeContext}\n${promptText}` + + const id = generatePromptId(combinedPrompt) + expect(typeof id).toBe('string') + expect(id.length).toBeGreaterThan(0) + + // Should be different from just the prompt text alone + expect(id).not.toBe(generatePromptId(promptText)) + }) + + test('handles very long prompts', () => { + // Real-world prompts can include entire code blocks (100+ lines) + const longCode = 'x\n'.repeat(500) // 500 lines + const id = generatePromptId(longCode) + expect(typeof id).toBe('string') + expect(id.length).toBeGreaterThan(0) + }) + + test('handles prompts with backticks and template literals', () => { + // Prompts often include inline code with backticks + const prompt = "In JavaScript I'd write: `The ${numCats === 1 ? 'cat is' : 'cats are'} hungry.`" + const id = generatePromptId(prompt) + expect(typeof id).toBe('string') + expect(id.length).toBeGreaterThan(0) + }) + + test('handles prompts with placeholders', () => { + // Content uses placeholders like NEW-LANGUAGE, OWNER/REPOSITORY + const id1 = generatePromptId('What is NEW-LANGUAGE best suited for?') + const id2 = generatePromptId('In OWNER/REPOSITORY, create a feature request') + expect(id1).not.toBe(id2) + expect(typeof id1).toBe('string') + expect(typeof id2).toBe('string') + }) + + test('handles unicode and international characters', () => { + // May encounter non-ASCII characters in prompts + const id1 = generatePromptId('Explique-moi le code en français') + const id2 = generatePromptId('コードを説明してください') + const id3 = generatePromptId('Объясните этот код') + expect(typeof id1).toBe('string') + expect(typeof id2).toBe('string') + expect(typeof id3).toBe('string') + expect(id1).not.toBe(id2) + expect(id2).not.toBe(id3) + }) +}) diff --git a/src/content-render/unified/code-header.ts b/src/content-render/unified/code-header.ts index 444c2f23616e..f9e0c13743e3 100644 --- a/src/content-render/unified/code-header.ts +++ b/src/content-render/unified/code-header.ts @@ -12,6 +12,7 @@ import { parse } from 'parse5' import { fromParse5 } from 'hast-util-from-parse5' import murmur from 'imurmurhash' import { getPrompt } from './copilot-prompt' +import { generatePromptId } from '../lib/prompt-id' import type { Element } from 'hast' interface LanguageConfig { @@ -52,10 +53,18 @@ function wrapCodeExample(node: any, tree: any): Element { const code: string = node.children[0].children[0].value const subnav = null // getSubnav() lives in annotate.ts, not needed for normal code blocks - const prompt = getPrompt(node, tree, code) // returns null if there's no prompt + const hasPrompt: boolean = Boolean(getPreMeta(node).prompt) + const promptResult = hasPrompt ? getPrompt(node, tree, code) : null const hasCopy: boolean = Boolean(getPreMeta(node).copy) // defaults to true - const headerHast = header(lang, code, subnav, prompt, hasCopy) + const headerHast = header( + lang, + code, + subnav, + promptResult?.element ?? null, + hasCopy, + promptResult?.promptContent, + ) return h('div', { className: 'code-example' }, [headerHast, node]) } @@ -66,6 +75,7 @@ export function header( subnav: Element | null = null, prompt: Element | null = null, hasCopy: boolean = true, + promptContent?: string, ): Element { const codeId: string = murmur('js-btn-copy').hash(code).result().toString() @@ -100,6 +110,9 @@ export function header( ) : null, h('pre', { hidden: true, 'data-clipboard': codeId }, code), + promptContent + ? h('pre', { hidden: true, id: generatePromptId(promptContent) }, promptContent) + : null, ], ) } diff --git a/src/content-render/unified/copilot-prompt.ts b/src/content-render/unified/copilot-prompt.ts index 1ce09ef679f8..19d03cbf57e5 100644 --- a/src/content-render/unified/copilot-prompt.ts +++ b/src/content-render/unified/copilot-prompt.ts @@ -8,27 +8,36 @@ import octicons from '@primer/octicons' import { parse } from 'parse5' import { fromParse5 } from 'hast-util-from-parse5' import { getPreMeta } from './code-header' +import { generatePromptId } from '../lib/prompt-id' -// Using any because node and tree are hast/unist AST nodes without proper TypeScript definitions -// node is a pre element from the AST, tree is the full document AST -// Returns a hast element node for the prompt button, or null if no prompt meta exists -export function getPrompt(node: any, tree: any, code: string): any { +// node and tree are hast/unist AST nodes without proper TypeScript definitions +// Returns an object with the prompt button element and the full prompt content +export function getPrompt( + node: any, + tree: any, + code: string, +): { element: any; promptContent: string } | null { const hasPrompt = Boolean(getPreMeta(node).prompt) if (!hasPrompt) return null const { promptContent, ariaLabel } = buildPromptData(node, tree, code) const promptLink = `https://github.com/copilot?prompt=${encodeURIComponent(promptContent.trim())}` + // Use murmur hash for deterministic ID (avoids hydration mismatch) + const promptId: string = generatePromptId(promptContent) - return h( + const element = h( 'a', { href: promptLink, target: '_blank', class: ['btn', 'btn-sm', 'mr-1', 'tooltipped', 'tooltipped-nw', 'no-underline'], 'aria-label': ariaLabel, + 'aria-describedby': promptId, }, copilotIcon(), ) + + return { element, promptContent } } // Using any because node and tree are hast/unist AST nodes without proper TypeScript definitions