From 4c90b8fe783fbc04ddcefe4828cded2788c8e2d6 Mon Sep 17 00:00:00 2001
From: subatoi <32935794+subatoi@users.noreply.github.com>
Date: Tue, 13 Oct 2026 16:06:04 +0000
Subject: [PATCH 01/16] Clarify help wanted guidance for OS issues (#63833)
---
.github/CONTRIBUTING.md | 2 ++
.../about-contributing-to-github-docs.md | 11 ++++++++++-
2 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md
index 5719e174063d..b7cf0c300925 100644
--- a/.github/CONTRIBUTING.md
+++ b/.github/CONTRIBUTING.md
@@ -14,6 +14,8 @@ Use the table of contents icon
`.
+Update your translation clones to the latest `main`.
+
+Hide the search components so they don't get scraped into the static archive: in `src/frame/components/page-header/Header.tsx`, temporarily set the `SubdomainNavBar.Search` component's `className` to `"visually-hidden"`. Also wrap the `SearchOverlay` in `src/search/components/input/SearchOverlayContainer.tsx` in a `
`.
+
+Turn off analytics too. Archived pages keep running their scraped JavaScript, and `/api/events` rejects their events because the version is no longer supported. In `src/frame/lib/constants.ts`, temporarily set `ANALYTICS_ENABLED` to `false`. That stops requests to both `/api/events` and `collector.githubapp.com`.
Build, then scrape a few pages locally:
@@ -89,7 +93,7 @@ Serve the output over HTTP to review it. Opening the files directly doesn't work
python3 -m http.server 8080 --bind 127.0.0.1 -d tmpArchivalDir_{{ release-number }}/{{ release-number }}
```
-Confirm that a few pages in a couple of languages render, styles load, and the version dropdowns work. Offer to open the pages for the human in their browser.
+Confirm that a few pages in a couple of languages render, styles load, and the version dropdowns work. Also confirm that the browser's network tab shows no requests to `/api/events` or `collector.githubapp.com`, including after you click a tool picker or code tab. Offer to open the pages for the human in their browser.
🛑 **HUMAN**: Review the dry-run output before the full scrape.
@@ -103,10 +107,10 @@ Scrape every page. This takes 20-30 minutes and overwrites the dry-run output:
npm run deprecate-ghes-archive
```
-Revert the search component edits:
+Revert the search component and analytics edits:
```shell
-git checkout src/frame/components/page-header/Header.tsx src/search/components/input/SearchOverlayContainer.tsx
+git checkout src/frame/components/page-header/Header.tsx src/search/components/input/SearchOverlayContainer.tsx src/frame/lib/constants.ts
```
## Step 8: Publish the archive
@@ -265,8 +269,7 @@ To keep your clones elsewhere instead, clone the repositories below and map each
## Reference: Re-scraping a page or all pages
-Occasionally a change needs to land in an already-archived version. The archive script always scrapes the current oldest supported version, so check out the `enter
-ise-{{ release-number }}-deprecation` tag, which points at history from before the version left `supported`, and re-scrape with `npm run deprecate-ghes-archive`. To scrape a single page, use the `--page
` option, passing the path without a version or language prefix. Upload the new files to `github/docs-ghes-{{ release-number }}` for each language.
+Occasionally a change needs to land in an already-archived version. The archive script always scrapes the current oldest supported version, so check out the `enterprise-{{ release-number }}-deprecation` tag, which points at history from before the version left `supported`. Repeat the Step 6 search and analytics edits, run `npm run build`, and re-scrape with `npm run deprecate-ghes-archive`. To scrape a single page, use the `--page ` option, passing the path without a version or language prefix. Revert the edits as in Step 7, then upload the new files to `github/docs-ghes-{{ release-number }}` for each language.
Human: After uploading, purge the Fastly cache. From Okta, open Fastly, select `docs`, and click "Purge" then "Purge URL", or "Purge All" for a whole path. Enter the URL or path and do a soft purge.
From e2186dfca4d2b6a7ce61af94b7a8e1ffc2fa75e4 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 13 Oct 2026 16:16:26 +0000
Subject: [PATCH 04/16] Remove page_type from docs events (#63809)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ea1bcfd5-03bd-4b5f-adba-0bbd55a61815
---
src/events/components/events.ts | 1 -
src/events/components/hydro-analytics.ts | 4 ----
src/events/lib/schema.ts | 7 -------
src/events/tests/hydro-analytics.ts | 13 +------------
src/events/types.ts | 1 -
5 files changed, 1 insertion(+), 25 deletions(-)
diff --git a/src/events/components/events.ts b/src/events/components/events.ts
index f3dc57cf7c6d..e5a5d2fb02ce 100644
--- a/src/events/components/events.ts
+++ b/src/events/components/events.ts
@@ -115,7 +115,6 @@ export function sendEvent({
path_product: getMetaContent('path-product'),
path_article: getMetaContent('path-article'),
page_document_type: getMetaContent('page-document-type'),
- page_type: getMetaContent('page-type'),
content_type: getMetaContent('page-content-type'),
docs_team_metrics: getMetaContent('docs-team-metrics') || undefined,
status: Number(getMetaContent('status') || 0),
diff --git a/src/events/components/hydro-analytics.ts b/src/events/components/hydro-analytics.ts
index 065799d36ab1..a9c8d9a9078d 100644
--- a/src/events/components/hydro-analytics.ts
+++ b/src/events/components/hydro-analytics.ts
@@ -63,10 +63,6 @@ export function prepareData(body: Record): {
// BI dashboards expect react_app and marketing page_type for analytics_v0_page_view.
context.react_app = 'docs'
- // Preserve the docs page type because BI expects page_type to be marketing.
- if (context.page_type) {
- context.docs_page_type = context.page_type
- }
context.page_type = 'marketing'
return { type: typeof type === 'string' ? type : 'unknown', context }
diff --git a/src/events/lib/schema.ts b/src/events/lib/schema.ts
index ba1ecc998848..265a0462539c 100644
--- a/src/events/lib/schema.ts
+++ b/src/events/lib/schema.ts
@@ -96,13 +96,6 @@ const context = {
// Keep in sync with get-document-type.ts.
enum: ['homepage', 'early-access', 'product', 'category', 'subcategory', 'article'],
},
- page_type: {
- type: 'string',
- description: 'Optional page type from the content frontmatter.',
- // Keep in sync with the YAML frontmatter docs:
- // content/contributing/writing-for-github-docs/using-yaml-frontmatter.md.
- enum: ['overview', 'quick_start', 'tutorial', 'how_to', 'reference', 'rai'],
- },
content_type: {
type: 'string',
description: 'Optional content type from the content frontmatter (EDI content models).',
diff --git a/src/events/tests/hydro-analytics.ts b/src/events/tests/hydro-analytics.ts
index 2f00389bb67c..dd6b55d2657a 100644
--- a/src/events/tests/hydro-analytics.ts
+++ b/src/events/tests/hydro-analytics.ts
@@ -109,23 +109,12 @@ describe('prepareData', () => {
expect(result.context.react_app).toBe('docs')
})
- test('adds page_type field when not present', () => {
+ test('sets page_type to marketing', () => {
const body = {
type: 'page',
context: { event_id: '123' },
}
const result = prepareData(body)
expect(result.context.page_type).toBe('marketing')
- expect(result.context.docs_page_type).toBeUndefined()
- })
-
- test('preserves existing page_type as docs_page_type and sets page_type to marketing', () => {
- const body = {
- type: 'page',
- context: { event_id: '123', page_type: 'article' },
- }
- const result = prepareData(body)
- expect(result.context.page_type).toBe('marketing')
- expect(result.context.docs_page_type).toBe('article')
})
})
diff --git a/src/events/types.ts b/src/events/types.ts
index 8544824d2c6a..47929cced156 100644
--- a/src/events/types.ts
+++ b/src/events/types.ts
@@ -36,7 +36,6 @@ export type EventProps = {
path_product?: string
path_article: string
page_document_type: string
- page_type: string
content_type: string
docs_team_metrics?: string
status: number
From 1b87d052f4247983e0d22f34eb553a8c67f5213d Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 13 Oct 2026 16:16:31 +0000
Subject: [PATCH 05/16] Clamp negative exit_render_duration to 0 (#63811)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ea1bcfd5-03bd-4b5f-adba-0bbd55a61815
---
src/events/components/events.ts | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/src/events/components/events.ts b/src/events/components/events.ts
index e5a5d2fb02ce..0248b61992e1 100644
--- a/src/events/components/events.ts
+++ b/src/events/components/events.ts
@@ -224,7 +224,7 @@ function getPerformance() {
firstContentfulPaint: paint ? paint.startTime / 1000 : undefined,
domInteractive: nav ? nav.domInteractive / 1000 : undefined,
domComplete: nav ? nav.domComplete / 1000 : undefined,
- render: nav ? (nav.responseEnd - nav.requestStart) / 1000 : undefined,
+ render: nav ? Math.max(0, nav.responseEnd - nav.requestStart) / 1000 : undefined,
}
}
From ca2cb49a3c77795061da38aba942ea365e4800d3 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 13 Oct 2026 16:16:38 +0000
Subject: [PATCH 06/16] Send search_client 'unknown' when client_name is
missing (#63812)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: ea1bcfd5-03bd-4b5f-adba-0bbd55a61815
---
src/events/lib/schema.ts | 3 +-
.../lib/helpers/external-search-analytics.ts | 2 +-
src/search/tests/external-search-analytics.ts | 48 +++++++++++++++++++
3 files changed, 51 insertions(+), 2 deletions(-)
create mode 100644 src/search/tests/external-search-analytics.ts
diff --git a/src/events/lib/schema.ts b/src/events/lib/schema.ts
index 265a0462539c..27b2253255c9 100644
--- a/src/events/lib/schema.ts
+++ b/src/events/lib/schema.ts
@@ -408,7 +408,8 @@ const search = {
},
search_client: {
type: 'string',
- description: 'The client name identifier when the request is not from docs.github.com.',
+ description:
+ "The client name identifier when the request is not from docs.github.com, or 'unknown' when the request sends no client_name.",
},
},
}
diff --git a/src/search/lib/helpers/external-search-analytics.ts b/src/search/lib/helpers/external-search-analytics.ts
index c532d77b077f..bdbbafc28d70 100644
--- a/src/search/lib/helpers/external-search-analytics.ts
+++ b/src/search/lib/helpers/external-search-analytics.ts
@@ -73,7 +73,7 @@ export async function handleExternalSearchAnalytics(
},
search_query: 'REDACTED',
search_context: searchContext,
- search_client: client_name as string,
+ search_client: (client_name as string) || 'unknown',
},
}
diff --git a/src/search/tests/external-search-analytics.ts b/src/search/tests/external-search-analytics.ts
new file mode 100644
index 000000000000..484b60babd23
--- /dev/null
+++ b/src/search/tests/external-search-analytics.ts
@@ -0,0 +1,48 @@
+import { describe, test, expect, vi, beforeEach } from 'vitest'
+
+import { publish } from '@/events/lib/hydro'
+import { handleExternalSearchAnalytics } from '@/search/lib/helpers/external-search-analytics'
+import type { ExtendedRequest } from '@/types'
+
+vi.mock('@/events/lib/hydro', () => ({
+ publish: vi.fn(),
+}))
+
+function buildRequest(headers: Record, query: Record = {}) {
+ return { headers: { host: 'docs.github.com', ...headers }, query } as unknown as ExtendedRequest
+}
+
+function publishedClient() {
+ const payload = (publish as ReturnType).mock.calls[0][0]
+ return payload.value.search_client
+}
+
+describe('handleExternalSearchAnalytics', () => {
+ beforeEach(() => {
+ vi.mocked(publish).mockClear()
+ })
+
+ test('sends the client_name as search_client', async () => {
+ await handleExternalSearchAnalytics(
+ buildRequest({ 'user-agent': 'curl/8.0' }, { client_name: 'my-tool' }),
+ 'general-search',
+ )
+ expect(publishedClient()).toBe('my-tool')
+ })
+
+ test('sends unknown when a browser-like request has no client_name', async () => {
+ await handleExternalSearchAnalytics(
+ buildRequest({ 'user-agent': 'SomeAgent/1.0' }),
+ 'general-search',
+ )
+ expect(publishedClient()).toBe('unknown')
+ })
+
+ test('skips analytics for the docs.github.com frontend', async () => {
+ await handleExternalSearchAnalytics(
+ buildRequest({ 'sec-fetch-site': 'same-origin' }, { client_name: 'docs.github.com-client' }),
+ 'general-search',
+ )
+ expect(publish).not.toHaveBeenCalled()
+ })
+})
From b8b56d42fdc24bea66c4cbef842afdbed7e0c649 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 13 Oct 2026 16:16:44 +0000
Subject: [PATCH 07/16] Sample autogenerated page rendering on PRs that don't
touch pipelines (#63746)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Copilot-Session: 796e2742-a386-4cab-8cc3-be8fe189e974
---
.github/workflows/test.yml | 25 +++++++++++
src/automated-pipelines/README.md | 8 +++-
src/automated-pipelines/tests/rendering.ts | 50 +++++++++++++++++++++-
src/tests/SUITES.md | 2 +-
4 files changed, 81 insertions(+), 4 deletions(-)
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index cfcbd5b88aeb..a9f04b656d80 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -140,6 +140,29 @@ jobs:
if: ${{ matrix.name == 'content-linter' }}
run: echo "${{ steps.changed_files.outputs.all_modified_files }}" > get_diff_files.txt
+ - name: Check for automated pipeline changes
+ if: ${{ matrix.name == 'automated-pipelines' && github.event_name == 'pull_request' }}
+ id: pipeline_files
+ uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5
+ with:
+ # Count a rename as a delete plus an add, so moving a file out of these paths still matches.
+ output_renamed_files_as_deleted_and_added: true
+ # Generated content counts too, so sync PRs render every page.
+ files: |
+ .github/workflows/test.yml
+ package-lock.json
+ content/code-security/reference/code-scanning/codeql/codeql-cli-manual/**
+ content/graphql/**
+ content/rest/**
+ src/audit-logs/**
+ src/automated-pipelines/**
+ src/codeql-cli/**
+ src/github-apps/**
+ src/graphql/**
+ src/rest/**
+ src/secret-scanning/**
+ src/webhooks/**
+
- uses: ./.github/actions/cache-nextjs
if: ${{ env.START_VITEST_SERVER != 'false' }}
@@ -172,4 +195,6 @@ jobs:
# RUNNER_DEBUG enables timing and path logs when Actions reruns jobs with debug logging.
RUNNER_DEBUG: ${{ runner.debug }}
VITEST_FLAGS: ${{ matrix.name == 'article-api' && '--no-file-parallelism --maxWorkers=1' || '' }}
+ # PRs render pinned autogenerated pages unless they change pipeline code or output.
+ AUTOGENERATED_RENDER_ALL: ${{ matrix.name == 'automated-pipelines' && (github.event_name != 'pull_request' || steps.pipeline_files.outputs.any_modified == 'true') }}
run: npm test -- $VITEST_FLAGS src/${{ matrix.name }}/tests/
diff --git a/src/automated-pipelines/README.md b/src/automated-pipelines/README.md
index 3fbd97cf00d1..46e6e1564171 100644
--- a/src/automated-pipelines/README.md
+++ b/src/automated-pipelines/README.md
@@ -44,7 +44,7 @@ When creating a new pipeline, the source data that is being consumed may not hav
- Each pipeline typically requires a workflow to allow scheduling or manually running the pipeline. The workflow should be placed in the `.github/workflows` directory and named `sync-.ts`. Each workflow typically requires adding a manual run option and an input parameter to specify the source repo's branch to use.
- Each pipeline will need a `scripts` directory with (at minimum) a `scripts/sync.ts` file to run the pipeline.
- If the pipeline will contain structured data, you will need to add a `src//data` directory. The files inside the `data` directory are typically organized by version (e.g., `src/webhooks/data/fpt/*`).
-- Pipelines typically have tests specific to the pipeline that are placed in the `src//tests` directory. There is no need to add tests that render the page because all autogenerated pages are tested in `src/automated-pipelines/tests/rendering.ts`.
+- Pipelines typically have tests specific to the pipeline that are placed in the `src//tests` directory. There is no need to add tests that render the page because all autogenerated pages are tested in `src/automated-pipelines/tests/rendering.ts`. Add the new `src//**` path to the "Check for automated pipeline changes" step in `.github/workflows/test.yml` so PRs that change the pipeline render every page.
- If the pipeline uses a Next.js page component (e.g., `pages/**/*.tsx`), ensure there is a test that fails if that page component is moved or deleted.
## How to get help
@@ -175,6 +175,12 @@ All autogenerated pages tested in `src/automated-pipelines/tests/rendering.ts`:
- Links are valid
- Schema validation
+The merge queue and PRs that change pipeline code or generated REST, GraphQL, or CodeQL CLI content render every autogenerated page. Other PRs and local runs render the pages in `PINNED_PAGES` in `src/automated-pipelines/tests/rendering.ts`, and every page for generators not listed there. To render every page locally, run:
+
+```bash
+AUTOGENERATED_RENDER_ALL=true npm test -- src/automated-pipelines/tests/rendering.ts
+```
+
### Pipeline-specific tests
Each pipeline in `src//tests/` should test:
diff --git a/src/automated-pipelines/tests/rendering.ts b/src/automated-pipelines/tests/rendering.ts
index 9e729afc102f..470098f061be 100644
--- a/src/automated-pipelines/tests/rendering.ts
+++ b/src/automated-pipelines/tests/rendering.ts
@@ -9,6 +9,7 @@ import { get } from '@/tests/helpers/e2etest'
type Page = {
autogenerated?: string
fullPath: string
+ relativePath: string
permalinks: Array<{ href: string }>
versions: {
feature?: string | string[]
@@ -18,16 +19,61 @@ type Page = {
const pageList: Page[] = await loadPages(undefined, ['en'])
+// CI sets AUTOGENERATED_RENDER_ALL in the merge queue and on PRs that change pipeline code or output.
+// Otherwise, render a fixed set of pages per generator to keep local runs and PRs fast.
+const RENDER_ALL = process.env.AUTOGENERATED_RENDER_ALL === 'true'
+
+// Paths are relative to content/. Generators not listed here render every page.
+// Pick pages that use the generator's main page layout, not index pages.
+const PINNED_PAGES: Record = {
+ 'codeql-cli': [
+ 'code-security/reference/code-scanning/codeql/codeql-cli-manual/database-analyze.md',
+ 'code-security/reference/code-scanning/codeql/codeql-cli-manual/database-create.md',
+ 'code-security/reference/code-scanning/codeql/codeql-cli-manual/github-merge-results.md',
+ ],
+ 'github-apps': [
+ 'rest/authentication/endpoints-available-for-github-app-installation-access-tokens.md',
+ 'rest/authentication/permissions-required-for-fine-grained-personal-access-tokens.md',
+ 'rest/authentication/permissions-required-for-github-apps.md',
+ ],
+ graphql: [
+ 'graphql/overview/breaking-changes.md',
+ 'graphql/overview/changelog/2025.md',
+ 'graphql/reference/repos.md',
+ ],
+ rest: ['rest/actions/workflow-runs.md', 'rest/issues/issues.md', 'rest/repos/repos.md'],
+}
+
+function pagesToRender(generator: string, pages: Page[]): { render: Page[]; missing: string[] } {
+ const pinned = PINNED_PAGES[generator] ?? []
+ const byPath = new Map(pages.map((page) => [page.relativePath, page]))
+ // Check pins in full-render mode too, so a PR that moves a pinned page fails before merge.
+ const missing = pinned.filter((path) => !byPath.has(path))
+ if (RENDER_ALL || !pinned.length) return { render: pages, missing }
+ return { render: pinned.flatMap((path) => byPath.get(path) ?? []), missing }
+}
+
describe('autogenerated docs render', () => {
vi.setConfig({ testTimeout: 3 * 60 * 1000 })
const autogeneratedPages = pageList.filter((page: Page) => page.autogenerated)
+ const pagesByGenerator = Object.groupBy(autogeneratedPages, (page: Page) => page.autogenerated!)
+ // Include pinned generators with no pages left, so their stale pins still fail.
+ const generators = new Set([...Object.keys(pagesByGenerator), ...Object.keys(PINNED_PAGES)])
+ const generatorCases = [...generators].map(
+ (generator) => [generator, pagesByGenerator[generator] ?? []] as const,
+ )
- test('all automated pages', async () => {
+ test.each(generatorCases)('%s pages', async (generator, allPages) => {
+ const { render: pages, missing } = pagesToRender(generator, allPages)
+ expect(
+ missing,
+ `Pinned ${generator} pages not found. Update PINNED_PAGES in this file.`,
+ ).toEqual([])
// Each page must render with 200 OK and unique heading IDs.
const errors = (
await Promise.all(
- autogeneratedPages.map(async (page: Page) => {
+ pages.map(async (page: Page) => {
const url = page.permalinks[0].href
// Slow autogenerated pages get retries to avoid false-positive failures.
const res = await get(url, { retries: 3 })
diff --git a/src/tests/SUITES.md b/src/tests/SUITES.md
index d1a556bdfcd9..56c1e7ef28c9 100644
--- a/src/tests/SUITES.md
+++ b/src/tests/SUITES.md
@@ -22,7 +22,7 @@ Note: for build-gating suites (data-directory, content-linter, generated-referen
| article-api | Article API endpoints (`/api/pagelist*`, `/api/article/body`, `/api/article/meta`) behind hovercards, llms.txt, and markdown delivery, plus the transformers for autogenerated pages. | Local server with fixture ROOT; no Elasticsearch/GitHub/external network. | **Medium**: reliable (no external infra) but covers real user/API surfaces. Contained, not site-wide. |
| assets | Dynamic image processing (PNG to WebP, resizing), static asset cache headers keyed by checksum URL, and archived enterprise asset proxying. | Local Express server; checked-in fixture images; external calls mocked with nock. | **Low**: deterministic, mocked network, clear failures. |
| audit-logs | Audit log event structure, field merging, allowlist filtering, GHES version handling, category notes, and page rendering. | Unit tests run standalone; the rendering test needs the local server (getDOM). | **Medium**: unit tests reliable; the rendering test can flake. Contained to the audit-log reference. |
-| automated-pipelines | The pipeline that auto-generates/syncs content from external sources into markdown, plus autogenerated-page rendering and semver conversion. | Local server for the rendering check (retried HTTP calls); fixtures. | **Medium**: affects rendered docs; the rendering test's retried HTTP calls can flake. |
+| automated-pipelines | The pipeline that auto-generates/syncs content from external sources into markdown, plus autogenerated-page rendering and semver conversion. | Local server for the rendering check (retried HTTP calls); fixtures. Renders all autogenerated pages in the merge queue and on PRs that change pipeline code or generated content, a pinned set per generator otherwise. | **Medium**: affects rendered docs; the rendering test's retried HTTP calls can flake. |
| color-schemes | Theme cookie parsing and mapping between CSS and Primer React color modes, including bad-input handling. | None (pure-function unit tests). | **Low**: pure-function unit tests, no flake. |
| content-linter | Markdown/YAML lint compliance: links, frontmatter schema, Liquid syntax, code annotations, alt text, versioning, and data references. | Changed files (DIFF_FILE) or full repo; markdownlint + custom rules. | **Low**: deterministic; reds = real content problems in the PR. |
| content-render | The Markdown/Liquid to HTML rendering pipeline: templates, parsing, syntax highlighting, link rewriting, and content transforms. | In-process rendering (unified + LiquidJS); cheerio; no external services. | **High**: affects every page; reds = real rendering bugs site-wide. |
From 9014d3b4e7c11181a82ff56d597d1c542f80a7d6 Mon Sep 17 00:00:00 2001
From: docs-bot <77750099+docs-bot@users.noreply.github.com>
Date: Tue, 13 Oct 2026 16:43:06 +0000
Subject: [PATCH 08/16] Sync secret scanning data (#63835)
Co-authored-by: github-merge-queue <118344674+github-merge-queue@users.noreply.github.com>
---
src/secret-scanning/data/pattern-docs/fpt/public-docs.yml | 2 +-
src/secret-scanning/data/pattern-docs/ghec/public-docs.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml b/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml
index 11a68d0a98cd..18a1e827f22f 100644
--- a/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml
+++ b/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml
@@ -4648,7 +4648,7 @@
supportedSecret: Starlink Public API V2 Client Secret
secretType: starlink_public_api_v2_secret
isPublic: true
- isPrivateWithGhas: false
+ isPrivateWithGhas: true
hasPushProtection: false
hasValidityCheck: false
hasExtendedMetadata: false
diff --git a/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml b/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml
index 11a68d0a98cd..18a1e827f22f 100644
--- a/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml
+++ b/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml
@@ -4648,7 +4648,7 @@
supportedSecret: Starlink Public API V2 Client Secret
secretType: starlink_public_api_v2_secret
isPublic: true
- isPrivateWithGhas: false
+ isPrivateWithGhas: true
hasPushProtection: false
hasValidityCheck: false
hasExtendedMetadata: false
From 56fe58f16ef04c62ab2fc2a807c243fc58250be0 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 13 Oct 2026 17:07:32 +0000
Subject: [PATCH 09/16] Clarify personal-account deletion for restricted
accounts (#63789)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bot-digital-customer-success <121182491+bot-digital-customer-success@users.noreply.github.com>
Co-authored-by: Daniel Klemm
---
.../deleting-your-personal-account.md | 10 +++++++++-
1 file changed, 9 insertions(+), 1 deletion(-)
diff --git a/content/account-and-profile/how-tos/account-management/deleting-your-personal-account.md b/content/account-and-profile/how-tos/account-management/deleting-your-personal-account.md
index 9805376ec6e2..599e8851a808 100644
--- a/content/account-and-profile/how-tos/account-management/deleting-your-personal-account.md
+++ b/content/account-and-profile/how-tos/account-management/deleting-your-personal-account.md
@@ -1,6 +1,6 @@
---
title: Deleting your personal account
-intro: You can delete your personal account on {% data variables.location.product_location %} at any time.
+intro: You can delete your personal account on {% data variables.location.product_location %} from your account settings.
redirect_from:
- /articles/deleting-a-user-account
- /articles/deleting-your-user-account
@@ -46,6 +46,14 @@ Before you delete your personal account, make a copy of all repositories, privat
> [!WARNING]
> Once your personal account has been deleted, {% ifversion fpt or ghec %}{% data variables.product.company_short %}{% elsif ghes %}an enterprise owner{% endif %} cannot restore your content.
+{% ifversion fpt or ghec %}
+
+If your account is flagged, suspended, or otherwise restricted, **Delete your account** may be unavailable, disabled, or direct you to {% data variables.contact.github_support %} instead of completing deletion. Contact {% data variables.contact.contact_support_page %} to request access to the normal self-service deletion flow.
+
+{% data variables.contact.github_support %} does not delete your personal account on your behalf. Once access is restored, you must complete the steps below to delete your account.
+
+{% endif %}
+
{% data reusables.user-settings.access_settings %}
{% data reusables.user-settings.account_settings %}
1. At the bottom of the Account Settings page, under "Delete account", click **Delete your account**. Before you can delete your personal account:
From 6938b27b898d8b87ad54bdf1fb58f8e1536be5e5 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 13 Oct 2026 17:32:28 +0000
Subject: [PATCH 10/16] Clarify account locks after failed Copilot
authorization (#63676)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bot-digital-customer-success <121182491+bot-digital-customer-success@users.noreply.github.com>
Co-authored-by: Daniel Klemm
Co-authored-by: tecrogue
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
Co-authored-by: subatoi <32935794+subatoi@users.noreply.github.com>
Copilot-Session: 10db96b6-c67d-4aad-b79c-0475dbdec8e4
---
content/billing/how-tos/troubleshooting/locked-account.md | 6 +++++-
1 file changed, 5 insertions(+), 1 deletion(-)
diff --git a/content/billing/how-tos/troubleshooting/locked-account.md b/content/billing/how-tos/troubleshooting/locked-account.md
index 9477bacbb8ba..2dac7940bc65 100644
--- a/content/billing/how-tos/troubleshooting/locked-account.md
+++ b/content/billing/how-tos/troubleshooting/locked-account.md
@@ -26,6 +26,8 @@ category:
You can unlock and access your account by updating your account's payment method and resuming paid status. We do not ask you to pay for the time elapsed in locked mode.
+A failed authorization hold when you try to sign up for {% data variables.product.prodname_copilot_short %} or increase a budget can also lock your personal account billing and block {% data variables.product.prodname_actions %} or other free-tier access. This can happen even if you plan to stay on {% data variables.product.prodname_free_user %}, have removed the payment method, or see no active {% data variables.product.prodname_copilot_short %} plan or balance in the "Billing and licensing" section under your account's settings page.
+
You can downgrade your user account or organization to {% data variables.product.prodname_free_team %} to continue with the same advanced features in public repositories. For more information, see [AUTOTITLE](/billing/how-tos/manage-plan-and-licenses/downgrade-plan).
## Unlocking an account's features due to a declined payment
@@ -40,8 +42,10 @@ If the new billing information is approved, we will immediately charge you for t
When initiating a subscription or trial, we verify the entered payment method using an authorization hold. If the hold is unsuccessful, the chosen subscription or trial will not activate.
-If your account's advanced features are locked due to a failed authorization hold, you'll need to update your billing information to trigger a new authorization.
+If you want to continue with the chosen subscription, update your billing information to trigger a new authorization.
{% data reusables.billing.manage-payment-info %}
If the new authorization is successful, the account will automatically unlock and you will be able to proceed with the chosen subscription.
+
+If the "Billing and licensing" section under your account's settings page shows no active subscription, balance, or payment issue to resolve, contact {% data variables.contact.contact_support %} to request a review of the billing lock.
From 39d38fed389d2421720dabe087d805974e152348 Mon Sep 17 00:00:00 2001
From: Copilot <198982749+Copilot@users.noreply.github.com>
Date: Tue, 13 Oct 2026 17:32:41 +0000
Subject: [PATCH 11/16] Clarify access requirements for Copilot plan billing
(#63677)
Co-authored-by: copilot-swe-agent[bot] <198982749+Copilot@users.noreply.github.com>
Co-authored-by: bot-digital-customer-success <121182491+bot-digital-customer-success@users.noreply.github.com>
Co-authored-by: Jimmy <21030056+jimmyokami@users.noreply.github.com>
Co-authored-by: Daniel Klemm
Co-authored-by: subatoi <32935794+subatoi@users.noreply.github.com>
---
.../view-and-change-your-copilot-plan.md | 6 ++++--
1 file changed, 4 insertions(+), 2 deletions(-)
diff --git a/content/copilot/how-tos/manage-your-account/view-and-change-your-copilot-plan.md b/content/copilot/how-tos/manage-your-account/view-and-change-your-copilot-plan.md
index f6f004814218..4a2ded5a694f 100644
--- a/content/copilot/how-tos/manage-your-account/view-and-change-your-copilot-plan.md
+++ b/content/copilot/how-tos/manage-your-account/view-and-change-your-copilot-plan.md
@@ -25,9 +25,9 @@ category:
- Configure Copilot
---
-You can view your current plan details in your {% data variables.product.github %} account settings. From there, you can change or cancel your plan.
+You can view your current plan details in your {% data variables.product.github %} account settings. The billing and licensing steps described in this article apply only if you can sign in to the personal account billed for the plan.
-If you have access to {% data variables.product.prodname_copilot %} through an organization or enterprise, you will not be able to modify your plan.
+If you have access to {% data variables.product.prodname_copilot %} through an organization or enterprise, your plan can be modified by an organization or enterprise owner. See [AUTOTITLE](/enterprise-cloud@latest/copilot/how-tos/administer-copilot).
## Viewing your {% data variables.product.prodname_copilot_short %} plan
@@ -75,4 +75,6 @@ You can cancel your {% data variables.product.prodname_copilot_short %} plan at
1. In the "{% data variables.product.prodname_copilot %}" section, select the **Manage subscription** dropdown on the right and then click **Cancel subscription**.
1. Select **Cancel {% data variables.copilot.copilot_pro_short %}/{% data variables.copilot.copilot_pro_plus_short %}/{% data variables.copilot.copilot_max_short %}** to cancel your plan at the end of your current billing cycle.
+If you can't sign in to the personal account billed for your plan—for example, because you no longer have access to its work or school email or are blocked by account recovery—try the available account recovery methods. See [AUTOTITLE](/authentication/securing-your-account-with-two-factor-authentication-2fa/recovering-your-account-if-you-lose-your-2fa-credentials). If you still can't access the account or identify which account is being charged, contact {% data variables.contact.contact_support_page %} to request help stopping future {% data variables.product.prodname_copilot_short %} charges or to request removal of the payment method from the locked account.
+
{% data reusables.billing.billing-info %}
From 5196c55a5f50db5f2b540f3d28f90a45f0994800 Mon Sep 17 00:00:00 2001
From: Abhishek
Date: Tue, 13 Oct 2026 17:37:34 +0000
Subject: [PATCH 12/16] Update docs for org level anonymous private registry
configuration (#63808)
Co-authored-by: mc <42146119+mchammer01@users.noreply.github.com>
---
.../giving-org-access-private-registries.md | 20 +++++++++++++++++++
.../resolve-a-blocked-host.md | 8 ++++++--
.../dependabot-errors.md | 2 +-
.../org-private-registry-anonymous.yml | 6 ++++++
4 files changed, 33 insertions(+), 3 deletions(-)
create mode 100644 data/features/org-private-registry-anonymous.yml
diff --git a/content/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries.md b/content/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries.md
index e6b3567124dd..6b0abcfcc036 100644
--- a/content/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries.md
+++ b/content/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries.md
@@ -57,6 +57,9 @@ You need to be an **organization owner** to set up access to private registries
{% ifversion org-private-registry-oidc %}
* **OIDC (OpenID Connect)**: Use short-lived credentials from a cloud identity provider instead of storing long-lived secrets. When you select this option, choose a provider and fill in the provider-specific fields. For more information, see [Configuring OIDC authentication for a private registry](#configuring-oidc-authentication-for-a-private-registry).
{% endif %}
+ {% ifversion org-private-registry-anonymous %}
+ * **Anonymous**: Use this option for a registry that doesn't require authentication. No credentials are stored or sent to the registry. For more information, see [Configuring an anonymous private registry](#configuring-an-anonymous-private-registry).
+ {% endif %}
1. Define which repositories in the organization can access the private registry using these details: all, private and internal, or selected repositories only.
1. When you have finished defining the private registry, select **Add Registry** to save the registry information.
@@ -129,6 +132,23 @@ For more information about how OIDC works, see [AUTOTITLE](/actions/concepts/sec
{% endif %}
+{% ifversion org-private-registry-anonymous %}
+
+### Configuring an anonymous private registry
+
+Some registries, such as internal mirrors, don't require authentication. You can add these registries at the organization level with the **Anonymous** authentication method so that {% data variables.product.prodname_dependabot %} can use them without a token, password, or OIDC configuration.
+
+{% ifversion dependabot-egress-allowlist %}
+{% data variables.product.prodname_dependabot %} update jobs can only connect to hosts on an egress allowlist. Adding an anonymous registry allows {% data variables.product.prodname_dependabot %} to reach the registry's host for jobs in repositories that have access to the registry, without storing a secret or sending credentials. For more information, see [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host).
+{% endif %}
+
+When you select **Anonymous**, the credential fields are removed from the form. You still define the registry's **URL** and **Type**. You also choose which repositories can access the registry.
+
+
+After you create an anonymous registry, you can only update its URL and whether it replaces the default registry for the ecosystem. You can't change the authentication method of an existing registry. To switch to or from **Anonymous**, delete the registry and create a new one.
+
+{% endif %}
+
{% ifversion code-quality %}
## {% data variables.product.prodname_code_quality_short %} access to private registries
diff --git a/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md b/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md
index 47418f235691..2a73ac6bdbf3 100644
--- a/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md
+++ b/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md
@@ -15,7 +15,7 @@ This reduces the risk that a malicious dependency or build script can send repos
Most jobs are unaffected. The allowlist already covers public registries, mirrors, CDNs, and download hosts for supported ecosystems, as well as {% data variables.product.github %}'s infrastructure. Registries configured and referenced for a job are also allowed automatically.
-A registry counts as configured only when you declare it under the top-level `registries` key in your `dependabot.yml` file. Defining a registry solely in an ecosystem-native configuration file, such as `.npmrc`, `nuget.config`, `pip.conf`, or Maven's `settings.xml`, does not allow its host. The package manager reads the registry from that file and tries to reach it, but the proxy blocks the request.
+A registry counts as configured only when you declare it under the top-level `registries` key in your `dependabot.yml` file{% ifversion org-private-registry-anonymous %}, or when an organization owner adds it as an organization-level private registry{% endif %}. Defining a registry solely in an ecosystem-native configuration file, such as `.npmrc`, `nuget.config`, `pip.conf`, or Maven's `settings.xml`, does not allow its host. The package manager reads the registry from that file and tries to reach it, but the proxy blocks the request.
## Identifying a blocked request in job logs
@@ -42,10 +42,14 @@ Two independent sets of hosts are allowed for each job.
Declare the registry even if it allows anonymous access. It is the declaration in `dependabot.yml`, not the presence of credentials, that makes the host reachable.
+{% ifversion org-private-registry-anonymous %}
+Organization owners can also allow a registry's host for jobs across the organization by adding the registry as an organization-level private registry. For a registry that doesn't require authentication, use the **Anonymous** authentication method. No credentials are stored or sent. See [AUTOTITLE](/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries#configuring-an-anonymous-private-registry).
+{% endif %}
+
This split determines where a blocked host belongs.
* **A public registry or download host** that other users' jobs could legitimately need belongs in the defaults. See [Adding a host to the defaults](#adding-a-host-to-the-defaults).
-* **A private or organization-specific registry** belongs in your `dependabot.yml` file, never in the defaults. Configure it under `registries` so it is allowed only for the jobs that need it. See [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-access-to-private-registries).
+* **A private or organization-specific registry** belongs in your `dependabot.yml` file, never in the defaults. Configure it under `registries` so it is allowed only for the jobs that need it. See [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-access-to-private-registries).{% ifversion org-private-registry-anonymous %} Alternatively, an organization owner can add it as an organization-level private registry.{% endif %}
## Adding a host to the defaults
diff --git a/content/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors.md b/content/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors.md
index 15b2d9b01fd2..9e221577be40 100644
--- a/content/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors.md
+++ b/content/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors.md
@@ -289,7 +289,7 @@ Update jobs can only reach hosts on {% data variables.product.prodname_dependabo
**Resolution:** Choose the option that matches the blocked host:
-* For a private or organization-specific registry, define it under the top-level `registries` key in your `dependabot.yml` file. Reference it from the relevant `updates` entry to allow it for that job. Do this even if the registry allows anonymous access, and note that defining it only in a file such as `.npmrc` or `nuget.config` does not allow its host.
+* For a private or organization-specific registry, define it under the top-level `registries` key in your `dependabot.yml` file. Reference it from the relevant `updates` entry to allow it for that job. Do this even if the registry allows anonymous access, and note that defining it only in a file such as `.npmrc` or `nuget.config` does not allow its host.{% ifversion org-private-registry-anonymous %} Alternatively, an organization owner can add the registry at the organization level, using the **Anonymous** authentication method if the registry doesn't require authentication.{% endif %}
* For a public registry or download host, propose adding it to the default allowlist.
For more information, see [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host).
diff --git a/data/features/org-private-registry-anonymous.yml b/data/features/org-private-registry-anonymous.yml
new file mode 100644
index 000000000000..20dde8674933
--- /dev/null
+++ b/data/features/org-private-registry-anonymous.yml
@@ -0,0 +1,6 @@
+# Reference: github/releases#11330
+# Anonymous (unauthenticated) organization-level private registries used by Dependabot.
+versions:
+ fpt: '*'
+ ghec: '*'
+ ghes: '>=3.24'
From 421c88e3e10ca79067885126fdd880e5c55bdcd3 Mon Sep 17 00:00:00 2001
From: subatoi <32935794+subatoi@users.noreply.github.com>
Date: Tue, 13 Oct 2026 17:38:29 +0000
Subject: [PATCH 13/16] Close OS PRs that close issues without help wanted
(#63838)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../close-prs-without-help-wanted-issue.yml | 84 +++++++++++++++++++
1 file changed, 84 insertions(+)
create mode 100644 .github/workflows/close-prs-without-help-wanted-issue.yml
diff --git a/.github/workflows/close-prs-without-help-wanted-issue.yml b/.github/workflows/close-prs-without-help-wanted-issue.yml
new file mode 100644
index 000000000000..97368d91770a
--- /dev/null
+++ b/.github/workflows/close-prs-without-help-wanted-issue.yml
@@ -0,0 +1,84 @@
+name: Close PRs for issues without help wanted
+
+# docs-internal mirrors workflows to github/docs, so enforce this rule only in the public repo.
+on:
+ pull_request_target:
+ types: [opened, edited, reopened, synchronize]
+
+permissions:
+ contents: read
+ issues: write
+ pull-requests: write
+
+jobs:
+ close-without-help-wanted-issue:
+ if: >-
+ ${{
+ github.repository == 'github/docs' &&
+ github.event.pull_request.state == 'open'
+ }}
+ runs-on: ubuntu-latest
+ steps:
+ - name: Close the pull request if a linked issue lacks the help wanted label
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0
+ with:
+ script: |
+ const { owner, repo } = context.repo
+ const pull_number = context.payload.pull_request.number
+
+ const { repository } = await github.graphql(
+ `query($owner: String!, $repo: String!, $number: Int!) {
+ repository(owner: $owner, name: $repo) {
+ pullRequest(number: $number) {
+ state
+ closingIssuesReferences(first: 100) {
+ nodes {
+ labels(first: 100) {
+ nodes {
+ name
+ }
+ }
+ }
+ }
+ }
+ }
+ }`,
+ { owner, repo, number: pull_number },
+ )
+
+ const pullRequest = repository?.pullRequest
+ if (!pullRequest) {
+ throw new Error(`Pull request #${pull_number} was not found`)
+ }
+ if (pullRequest.state !== 'OPEN') {
+ console.log(`Pull request #${pull_number} is no longer open`)
+ return
+ }
+
+ const closingIssues = pullRequest.closingIssuesReferences.nodes
+ if (!closingIssues.length) {
+ console.log(`Pull request #${pull_number} has no closing issue references`)
+ return
+ }
+
+ const issuesWithoutHelpWanted = closingIssues.filter(issue =>
+ !issue.labels.nodes.some(label => label.name.toLowerCase() === 'help wanted'),
+ )
+ if (!issuesWithoutHelpWanted.length) {
+ console.log('Every linked issue has the help wanted label')
+ return
+ }
+
+ await github.rest.issues.createComment({
+ owner,
+ repo,
+ issue_number: pull_number,
+ body: "Please don't raise pull requests against an existing issue that another user has raised until it's been marked with a `help wanted` label. We need to properly assess all issues before we can accept community contributions, and determine whether it's something we need to fix internally, or how many articles are affected and what the scope of the fix will be. For now, we're going to close this pull request.",
+ })
+
+ await github.rest.pulls.update({
+ owner,
+ repo,
+ pull_number,
+ state: 'closed',
+ })
From d3855614d96acca078405f10c172c80334e84c71 Mon Sep 17 00:00:00 2001
From: subatoi <32935794+subatoi@users.noreply.github.com>
Date: Tue, 13 Oct 2026 18:23:35 +0000
Subject: [PATCH 14/16] Amend workflow to DM Hubbers that open OS issues
(#63832)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../confirm-internal-staff-work-in-docs.yml | 136 +++++++++++++++++-
1 file changed, 133 insertions(+), 3 deletions(-)
diff --git a/.github/workflows/confirm-internal-staff-work-in-docs.yml b/.github/workflows/confirm-internal-staff-work-in-docs.yml
index e224b5e8f897..1cfd8f94a24b 100644
--- a/.github/workflows/confirm-internal-staff-work-in-docs.yml
+++ b/.github/workflows/confirm-internal-staff-work-in-docs.yml
@@ -65,12 +65,123 @@ jobs:
core.setOutput('did_warn', 'true')
- - name: Send Slack notification if a GitHub employee who isn't on the Technical Content team opens an issue in public
- if: ${{ steps.membership_check.outputs.did_warn && github.repository == 'github/docs' }}
+ - name: Resolve Slack user ID from username
+ id: slack_user
+ if: ${{ steps.membership_check.outputs.did_warn == 'true' }}
+ uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3
+ env:
+ SLACK_BOT_TOKEN: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
+ with:
+ github-token: ${{ github.token }}
+ script: |
+ const githubLogin = context.payload.sender.login.toLowerCase();
+
+ core.info(`Looking up Slack user for @${githubLogin}...`);
+ core.setOutput('slack_user_id', '');
+ core.setOutput('slack_mention', `GitHub user \`${context.payload.sender.login}\``);
+
+ const token = process.env.SLACK_BOT_TOKEN;
+ if (!token) {
+ throw new Error('SLACK_DOCS_BOT_TOKEN is not configured.');
+ }
+
+ let cursor = '';
+ let pages = 0;
+ const matches = [];
+
+ do {
+ core.info(`Fetching Slack users.list page ${pages + 1}...`);
+ const url = new URL('https://slack.com/api/users.list');
+ url.searchParams.set('limit', '200');
+ if (cursor) {
+ url.searchParams.set('cursor', cursor);
+ }
+
+ const response = await fetch(url, {
+ headers: {
+ Authorization: `Bearer ${token}`,
+ },
+ });
+
+ if (!response.ok) {
+ throw new Error(`Slack users.list returned HTTP ${response.status}.`);
+ }
+
+ const result = await response.json();
+ if (!result.ok) {
+ throw new Error(`Slack users.list failed: ${result.error || 'unknown error'}.`);
+ }
+
+ if (!Array.isArray(result.members)) {
+ throw new Error('Slack users.list response did not include a members array.');
+ }
+
+ pages += 1;
+ matches.push(...result.members.filter((member) =>
+ !member.deleted &&
+ !member.is_bot &&
+ !member.is_app_user &&
+ typeof member.id === 'string' &&
+ typeof member.name === 'string' &&
+ member.name.toLowerCase() === githubLogin
+ ));
+
+ cursor = result.response_metadata?.next_cursor || '';
+ } while (cursor);
+
+ core.info(`Checked ${pages} pages of users.list. Found ${matches.length} match(es) for @${githubLogin}.`);
+ if (matches.length !== 1) {
+ core.warning(`Expected one Slack username matching @${githubLogin}; found ${matches.length}. No DM will be sent.`);
+ return;
+ }
+
+ core.info(`Will DM Slack user ${matches[0].id}.`);
+ core.setOutput('slack_user_id', matches[0].id);
+ core.setOutput('slack_mention', `<@${matches[0].id}>`);
+
+ - name: DM the author in Slack
+ id: slack_dm
+ if: ${{ steps.slack_user.outputs.slack_user_id != '' }}
+ uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
+ with:
+ method: chat.postMessage
+ token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
+ errors: true
+ payload: |
+ channel: ${{ steps.slack_user.outputs.slack_user_id }}
+ text: ${{ toJSON(format('You opened {0} publicly in github/docs instead of the private repo. An internal confirmation issue was created.', github.event.issue.html_url || github.event.pull_request.html_url)) }}
+
+ - name: Log Slack DM result
+ if: ${{ !cancelled() && steps.membership_check.outputs.did_warn == 'true' }}
+ env:
+ SLACK_USER_ID: ${{ steps.slack_user.outputs.slack_user_id }}
+ SLACK_LOOKUP_OUTCOME: ${{ steps.slack_user.outcome }}
+ SLACK_DM_OUTCOME: ${{ steps.slack_dm.outcome }}
+ SLACK_DM_OK: ${{ steps.slack_dm.outputs.ok }}
+ SLACK_DM_RESPONSE: ${{ steps.slack_dm.outputs.response }}
+ SLACK_DM_TS: ${{ steps.slack_dm.outputs.ts }}
+ run: |
+ echo "Slack user lookup outcome: ${SLACK_LOOKUP_OUTCOME:-not run}"
+ if [ -z "$SLACK_USER_ID" ]; then
+ echo "Slack DM skipped because no Slack user ID was resolved."
+ elif [ "$SLACK_DM_OK" = "true" ]; then
+ echo "Slack DM sent successfully (timestamp: ${SLACK_DM_TS:-unavailable})."
+ else
+ echo "Slack DM did not return success (step outcome: ${SLACK_DM_OUTCOME:-unknown})."
+ if [ -n "$SLACK_DM_RESPONSE" ]; then
+ printf '%s\n' "$SLACK_DM_RESPONSE" | jq -r 'if type == "object" and (.error | type) == "string" then "Slack API error: " + .error else "Slack API response did not include an error field." end'
+ else
+ echo "Slack DM API response was unavailable."
+ fi
+ fi
+
+ - name: Notify the docs-open-source channel
+ id: slack_channel_notification
+ if: ${{ !cancelled() && steps.membership_check.outputs.did_warn == 'true' }}
uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0
env:
- SLACK_MESSAGE: <@${{ github.actor }}> opened https://github.com/github/docs/issues/${{ github.event.number || github.event.issue.number }} publicly on the github/docs repo instead of a private repo. They have been notified via a new issue in the private repo to confirm this was intentional.
SLACK_CHANNEL_ID: ${{ secrets.DOCS_OPEN_SOURCE_SLACK_CHANNEL_ID }}
+ SLACK_MESSAGE: ${{ format('{0} opened {1} publicly in github/docs instead of the private repo. An internal confirmation issue was created.', steps.slack_user.outputs.slack_mention, github.event.issue.html_url || github.event.pull_request.html_url) }}
with:
method: chat.postMessage
token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
@@ -79,6 +190,25 @@ jobs:
channel: ${{ toJSON(env.SLACK_CHANNEL_ID) }}
text: ${{ toJSON(env.SLACK_MESSAGE) }}
+ - name: Log Slack channel notification result
+ if: ${{ !cancelled() && steps.membership_check.outputs.did_warn == 'true' }}
+ env:
+ SLACK_CHANNEL_OUTCOME: ${{ steps.slack_channel_notification.outcome }}
+ SLACK_CHANNEL_OK: ${{ steps.slack_channel_notification.outputs.ok }}
+ SLACK_CHANNEL_RESPONSE: ${{ steps.slack_channel_notification.outputs.response }}
+ SLACK_CHANNEL_TS: ${{ steps.slack_channel_notification.outputs.ts }}
+ run: |
+ if [ "$SLACK_CHANNEL_OK" = "true" ]; then
+ echo "Slack channel notification sent successfully (timestamp: ${SLACK_CHANNEL_TS:-unavailable})."
+ else
+ echo "Slack channel notification did not return success (step outcome: ${SLACK_CHANNEL_OUTCOME:-unknown})."
+ if [ -n "$SLACK_CHANNEL_RESPONSE" ]; then
+ printf '%s\n' "$SLACK_CHANNEL_RESPONSE" | jq -r 'if type == "object" and (.error | type) == "string" then "Slack API error: " + .error else "Slack API response did not include an error field." end'
+ else
+ echo "Slack channel API response was unavailable."
+ fi
+ fi
+
- name: Check out repo
if: ${{ failure() && github.event_name != 'pull_request_target' }}
uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
From 924579599358cb4235d5d40975ae6d905bc24554 Mon Sep 17 00:00:00 2001
From: Sunbrye Ly <56200261+sunbrye@users.noreply.github.com>
Date: Tue, 13 Oct 2026 18:34:45 +0000
Subject: [PATCH 15/16] [2026-10-13] Tented Model (#63795)
Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
---
.../copilot/reference/ai-models/model-hosting.md | 1 +
.../copilot/reference/ai-models/supported-models.md | 1 +
.../reference/copilot-billing/models-and-pricing.md | 6 ++++++
.../copilot/copilot-cloud-agent-non-auto-models.md | 1 +
data/tables/copilot/model-comparison.yml | 5 +++++
data/tables/copilot/model-release-status.yml | 4 ++++
data/tables/copilot/model-supported-clients.yml | 9 +++++++++
data/tables/copilot/model-supported-plans.yml | 7 +++++++
data/tables/copilot/models-and-pricing.yml | 13 +++++++++++++
data/variables/copilot.yml | 1 +
10 files changed, 48 insertions(+)
diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md
index f5e748dd2209..e8df32d98912 100644
--- a/content/copilot/reference/ai-models/model-hosting.md
+++ b/content/copilot/reference/ai-models/model-hosting.md
@@ -85,6 +85,7 @@ Used for:
* {% data variables.copilot.copilot_gemini_37_flash %}
* {% data variables.copilot.copilot_gemini_38_flash %}
+* {% data variables.copilot.copilot_gemini_4_argon %}
{% data variables.product.prodname_copilot %} uses {% data variables.copilot.copilot_gemini %} models hosted on Google Cloud Platform (GCP). When using {% data variables.copilot.copilot_gemini %} models, prompts and metadata are sent to GCP, which makes the [following data commitment](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance): _{% data variables.copilot.copilot_gemini %} doesn't use your prompts, or its responses, as data to train its models._
diff --git a/content/copilot/reference/ai-models/supported-models.md b/content/copilot/reference/ai-models/supported-models.md
index f1e653e48d9a..faac97de84c4 100644
--- a/content/copilot/reference/ai-models/supported-models.md
+++ b/content/copilot/reference/ai-models/supported-models.md
@@ -129,6 +129,7 @@ Some {% data variables.product.prodname_copilot_short %} models require minimum
|----------------------------------------------------------|----------------------------------------------|------------------------------------------|----------------|-------|---------|
| {% data variables.copilot.copilot_gemini_37_flash %} | `v1.128.0` | `17.14.22` or `18.1.0` | TBD | TBD | TBD |
| {% data variables.copilot.copilot_gemini_38_flash %} | TBD | `17.14.22` or `18.1.0` | TBD | TBD | TBD |
+| {% data variables.copilot.copilot_gemini_4_argon %} | TBD | `17.14.22` or `18.1.0` | TBD | TBD | TBD |
| {% data variables.copilot.copilot_gpt_53_codex %} | `v1.104.1` | `17.14.19` | `1.5.61` | `0.45.0` | `0.13.0` |
| {% data variables.copilot.copilot_gpt_54 %} | `v1.104.1` | `17.14.19` | `1.5.66` | `0.47.0` | `0.15.0` |
| {% data variables.copilot.copilot_gpt_54_mini %} | `v1.104.1` | `17.14.19` | `1.5.66` | `0.47.0` | `0.15.0` |
diff --git a/content/copilot/reference/copilot-billing/models-and-pricing.md b/content/copilot/reference/copilot-billing/models-and-pricing.md
index 0a4752ce7d2a..7a19e09b308a 100644
--- a/content/copilot/reference/copilot-billing/models-and-pricing.md
+++ b/content/copilot/reference/copilot-billing/models-and-pricing.md
@@ -121,3 +121,9 @@ You can view your current {% data variables.product.prodname_actions %} usage fo
{% data variables.copilot.copilot_pro_short %} and {% data variables.copilot.copilot_pro_plus_short %} subscribers on **existing annual billing plans** using the **request-based billing** model have different model multipliers. See [AUTOTITLE](/copilot/reference/copilot-billing/request-based-billing-legacy/model-multipliers-for-annual-plans).
[^gemini-flash-promo]: {% data variables.copilot.copilot_gemini_37_flash %} and {% data variables.copilot.copilot_gemini_38_flash %} are available at the promotional pricing of $0.75 per 1M input tokens, $0.075 per 1M cached input tokens, and $3.75 per 1M output tokens through December 31, 2026.
+
+
+
+[^gemini-4-argon-promo]: {% data variables.copilot.copilot_gemini_4_argon %} is available at the promotional pricing of $2.00 per 1M input tokens, $0.10 per 1M cached input tokens, and $10.00 per 1M output tokens until December 1, 2026.
+
+
diff --git a/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md b/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md
index b05ce787f8ba..f479e1385f96 100644
--- a/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md
+++ b/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md
@@ -5,6 +5,7 @@
* {% data variables.copilot.copilot_claude_haiku_55 %}
* {% data variables.copilot.copilot_gemini_37_flash %}
* {% data variables.copilot.copilot_gemini_38_flash %}
+* {% data variables.copilot.copilot_gemini_4_argon %}
* {% data variables.copilot.copilot_gpt_54_mini %}
* {% data variables.copilot.copilot_gpt_56_luna %}
* {% data variables.copilot.copilot_gpt_56_sol %}
diff --git a/data/tables/copilot/model-comparison.yml b/data/tables/copilot/model-comparison.yml
index c712c48ac382..3aee97f3c4a5 100644
--- a/data/tables/copilot/model-comparison.yml
+++ b/data/tables/copilot/model-comparison.yml
@@ -135,6 +135,11 @@
excels_at: Fast, reliable answers to lightweight coding questions
further_reading: '[Gemini 3.8 Flash model card](https://storage.googleapis.com/deepmind-media/Model-Cards/Gemini-3-8-Flash-Model-Card.pdf)'
+- name: Gemini 4 Argon
+ task_area: Complex coding, agentic, and reasoning tasks
+ excels_at: Thorough work that tests edge cases before finishing
+ further_reading: '[Gemini 4 Argon](https://storage.googleapis.com/deepmind-media/Model-Cards/Gemini-4-Argon-Model-Card.pdf)'
+
# Microsoft
- name: MAI-Code-1.1-Flash
task_area: General-purpose coding and writing, image understanding
diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml
index f5ee420cd1dc..83909bb9a955 100644
--- a/data/tables/copilot/model-release-status.yml
+++ b/data/tables/copilot/model-release-status.yml
@@ -123,6 +123,10 @@
provider: 'Google'
release_status: 'GA'
+- name: 'Gemini 4 Argon'
+ provider: 'Google'
+ release_status: 'GA'
+
# Microsoft models
- name: 'MAI-Code-1.1-Flash'
provider: 'Microsoft'
diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml
index 0f06d75c521a..bfc1627356d1 100644
--- a/data/tables/copilot/model-supported-clients.yml
+++ b/data/tables/copilot/model-supported-clients.yml
@@ -131,6 +131,15 @@
xcode: true
jetbrains: true
+- name: Gemini 4 Argon
+ dotcom: false
+ cli: true
+ vscode: true
+ vs: true
+ eclipse: true
+ xcode: true
+ jetbrains: true
+
- name: MAI-Code-1.1-Flash
dotcom: true
cli: true
diff --git a/data/tables/copilot/model-supported-plans.yml b/data/tables/copilot/model-supported-plans.yml
index 2718999edf55..473a4a1be282 100644
--- a/data/tables/copilot/model-supported-plans.yml
+++ b/data/tables/copilot/model-supported-plans.yml
@@ -103,6 +103,13 @@
business: true
enterprise: true
+- name: Gemini 4 Argon
+ pro: false
+ pro_plus: true
+ max: true
+ business: true
+ enterprise: true
+
- name: GPT-5 mini
pro: true
pro_plus: true
diff --git a/data/tables/copilot/models-and-pricing.yml b/data/tables/copilot/models-and-pricing.yml
index 7774c10a4b09..c5dd843f56fd 100644
--- a/data/tables/copilot/models-and-pricing.yml
+++ b/data/tables/copilot/models-and-pricing.yml
@@ -406,6 +406,19 @@
cached_input: $0.075
output: $3.75
+# Promotional pricing expires 2026-12-01. Google hasn't shared post-promo pricing yet.
+# When it does, update input, cached_input, and output below, then remove the
+# [^gemini-4-argon-promo] footnote here and in models-and-pricing.md.
+- model: 'Gemini 4 Argon[^gemini-4-argon-promo]'
+ provider: google
+ release_status: GA
+ category: Powerful
+ threshold: 'Not applicable'
+ tier: 'Default'
+ input: $2.00
+ cached_input: $0.10
+ output: $10.00
+
# xAI
- model: 'Grok 4.5'
provider: xai
diff --git a/data/variables/copilot.yml b/data/variables/copilot.yml
index 1cbda8e30a5d..feaaf7ebf5c9 100644
--- a/data/variables/copilot.yml
+++ b/data/variables/copilot.yml
@@ -211,6 +211,7 @@ copilot_gemini_3_pro: 'Gemini 3 Pro'
copilot_gemini_31_pro: 'Gemini 3.1 Pro'
copilot_gemini_37_flash: 'Gemini 3.7 Flash'
copilot_gemini_38_flash: 'Gemini 3.8 Flash'
+copilot_gemini_4_argon: 'Gemini 4 Argon'
# OpenAI GPT series:
copilot_gpt_4o: 'GPT-4o'
copilot_gpt_4o_mini: 'GPT-4o mini'
From 317933831a9190283c052c29908412e628ea66e4 Mon Sep 17 00:00:00 2001
From: "Hector A."
Date: Tue, 13 Oct 2026 18:51:39 +0000
Subject: [PATCH 16/16] Add translation corrections for es, zh, and ko Liquid
tags (#63831)
---
.../lib/correct-translation-content.ts | 20 ++++++
.../tests/correct-translation-content.ts | 72 +++++++++++++++++++
2 files changed, 92 insertions(+)
diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts
index 0256ef75e064..c97888abffaa 100644
--- a/src/languages/lib/correct-translation-content.ts
+++ b/src/languages/lib/correct-translation-content.ts
@@ -94,6 +94,13 @@ export function correctTranslatedContentStrings(
if (context.code === 'es') {
content = content.replace(/\{%:/g, '{%')
+ // The translator replaced the space after "data" with a dot, as in {% datos.variables.x %},
+ // or inserted a stray word, as in {% datos existente variables.x %}.
+ content = content.replace(
+ /\{%(-?)\s*datos(?:\.|\s+existente\s+)(variables|reusables)\./g,
+ '{%$1 data $2.',
+ )
+
content = content.replaceAll('{% advertencia %}', '{% warning %}')
content = content.replaceAll('{% siVersion ', '{% ifversion ')
content = content.replaceAll('{%- siVersion ', '{%- ifversion ')
@@ -671,6 +678,9 @@ export function correctTranslatedContentStrings(
}
if (context.code === 'zh') {
+ // Chinese "变量" ("variables") glued to "data" with no space.
+ // Also the space-separated form, as in {% data变量 x.y %}, which loses the dot.
+ content = content.replace(/\{%(-?)\s*data变量(?:\.|\s+)/g, '{%$1 data variables.')
content = content.replaceAll('{% 数据variables', '{% data variables')
content = content.replaceAll('{% 数据 variables', '{% data variables')
content = content.replaceAll('{%数据variables', '{% data variables')
@@ -1247,6 +1257,13 @@ export function correctTranslatedContentStrings(
}
if (context.code === 'ko') {
+ // Misspelled "data" keyword and translated IDE/app tag names
+ content = content.replaceAll('{% dada variables', '{% data variables')
+ content = content.replaceAll('{%- dada variables', '{%- data variables')
+ content = content.replaceAll('{% 이클립스 %}', '{% eclipse %}')
+ content = content.replaceAll('{%- 이클립스 %}', '{%- eclipse %}')
+ content = content.replaceAll('{% 데스크톱 %}', '{% desktop %}')
+ content = content.replaceAll('{%- 데스크톱 %}', '{%- desktop %}')
content = content.replaceAll('{% 경고 %}', '{% warning %}')
content = content.replaceAll('{% datda variables', '{% data variables')
content = content.replaceAll('{%- datda variables', '{%- data variables')
@@ -1611,6 +1628,9 @@ export function correctTranslatedContentStrings(
return match.replace(/[«»“”„]/g, '"').replace(/[‘’‚]/g, "'")
})
+ // The translator dropped the space between octicon and its quoted name.
+ content = content.replace(/\{%(-?)\s*octicon(?=")/g, '{%$1 octicon ')
+
// French « check » becomes " check " after quote normalization.
content = content.replace(
/\{%(-?)\s*octicon\s+"\s*([\w-]+)\s*"([^%]*?)(-?)%\}/g,
diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts
index e9e81b3b9c9c..7748ecf1fa5e 100644
--- a/src/languages/tests/correct-translation-content.ts
+++ b/src/languages/tests/correct-translation-content.ts
@@ -3405,4 +3405,76 @@ Para más información, consulta "[AUTOTITLE](/path)".
expect(fix(blank, 'fr')).toBe(blank)
})
})
+
+ describe('data keyword glued to variables or reusables', () => {
+ test('es: {% datos.variables. and a stray word between datos and variables', () => {
+ expect(fix('{% datos.variables.product.company_short %}', 'es')).toBe(
+ '{% data variables.product.company_short %}',
+ )
+ expect(fix('un {% datos existente variables.copilot.x %} de su', 'es')).toBe(
+ 'un {% data variables.copilot.x %} de su',
+ )
+ const ok = '{% data variables.product.company_short %}'
+ expect(fix(ok, 'es')).toBe(ok)
+ })
+
+ test('zh: {% data变量. becomes {% data variables.', () => {
+ expect(fix('{% data变量.dependabot.github_presets %} 是规则', 'zh')).toBe(
+ '{% data variables.dependabot.github_presets %} 是规则',
+ )
+ const ok = '{% data variables.dependabot.github_presets %}'
+ expect(fix(ok, 'zh')).toBe(ok)
+ })
+
+ test('zh: space-separated data变量 restores the dot', () => {
+ expect(fix('{% data变量 dependabot.github_presets %} 是规则', 'zh')).toBe(
+ '{% data variables.dependabot.github_presets %} 是规则',
+ )
+ expect(
+ fix(
+ '{% data变量 dependabot.github_presets %} 是规则',
+ 'zh',
+ '{% data variables.dependabot.github_presets %} are rules',
+ ),
+ ).toBe('{% data variables.dependabot.github_presets %} 是规则')
+ })
+
+ test('zh: space-separated data变量 with only whitespace-control data tags', () => {
+ expect(
+ fix(
+ '{%- data变量 dependabot.github_presets %} 是规则',
+ 'zh',
+ '{%- data variables.dependabot.github_presets %} are rules',
+ ),
+ ).toBe('{%- data variables.dependabot.github_presets %} 是规则')
+ })
+ })
+
+ describe('octicon missing space before quoted name', () => {
+ test('restores the space, including after curly-quote normalization', () => {
+ expect(fix('{% octicon“kebab-horizontal” aria-label=“图标” %}', 'zh')).toBe(
+ '{% octicon "kebab-horizontal" aria-label="图标" %}',
+ )
+ expect(fix('{% octicon"pulse" aria-hidden="true" %}', 'ja')).toBe(
+ '{% octicon "pulse" aria-hidden="true" %}',
+ )
+ })
+
+ test('leaves a correct octicon unchanged', () => {
+ const ok = '{% octicon "pulse" aria-hidden="true" %}'
+ expect(fix(ok, 'ja')).toBe(ok)
+ })
+ })
+
+ describe('Korean data keyword and tag names', () => {
+ test('fixes {%- dada and translated eclipse and desktop tags', () => {
+ expect(fix('{%- dada variables.product.prodname_actions %}', 'ko')).toBe(
+ '{%- data variables.product.prodname_actions %}',
+ )
+ expect(fix('{% 이클립스 %}x{% endeclipse %}', 'ko')).toBe('{% eclipse %}x{% endeclipse %}')
+ expect(fix('{%- 데스크톱 %}x{% enddesktop %}', 'ko')).toBe('{%- desktop %}x{% enddesktop %}')
+ const ok = '{% data variables.product.prodname_actions %}'
+ expect(fix(ok, 'ko')).toBe(ok)
+ })
+ })
})