`.
+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.
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)
+ })
+ })
})
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()
+ })
+})
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
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. |