diff --git a/.github/workflows/needs-sme-stale-check.yaml b/.github/workflows/needs-sme-stale-check.yaml
index 388708ebab91..589993d3d3ab 100644
--- a/.github/workflows/needs-sme-stale-check.yaml
+++ b/.github/workflows/needs-sme-stale-check.yaml
@@ -41,3 +41,8 @@ jobs:
with:
slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
+
+ - uses: ./.github/actions/create-workflow-failure-issue
+ if: ${{ failure() }}
+ with:
+ token: ${{ secrets.DOCS_BOT_PAT_BASE }}
diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml
index 451d668b10bd..004db7603208 100644
--- a/.github/workflows/no-response.yaml
+++ b/.github/workflows/no-response.yaml
@@ -63,3 +63,8 @@ jobs:
with:
slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
+
+ - uses: ./.github/actions/create-workflow-failure-issue
+ if: ${{ failure() }}
+ with:
+ token: ${{ secrets.DOCS_BOT_PAT_BASE }}
diff --git a/.github/workflows/notify-about-deployment.yml b/.github/workflows/notify-about-deployment.yml
index 30f8443978ed..e7fb384447b3 100644
--- a/.github/workflows/notify-about-deployment.yml
+++ b/.github/workflows/notify-about-deployment.yml
@@ -75,3 +75,8 @@ jobs:
with:
slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
+
+ - uses: ./.github/actions/create-workflow-failure-issue
+ if: ${{ failure() }}
+ with:
+ token: ${{ secrets.DOCS_BOT_PAT_BASE }}
diff --git a/.github/workflows/triage-stale-check.yml b/.github/workflows/triage-stale-check.yml
index 63e081fe0c06..4bd9eaa2565b 100644
--- a/.github/workflows/triage-stale-check.yml
+++ b/.github/workflows/triage-stale-check.yml
@@ -52,6 +52,11 @@ jobs:
slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
+ - uses: ./.github/actions/create-workflow-failure-issue
+ if: ${{ failure() }}
+ with:
+ token: ${{ secrets.DOCS_BOT_PAT_BASE }}
+
stale_staff:
name: Remind staff about PRs waiting for review
if: github.repository == 'github/docs'
@@ -82,3 +87,8 @@ jobs:
with:
slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }}
slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }}
+
+ - uses: ./.github/actions/create-workflow-failure-issue
+ if: ${{ failure() }}
+ with:
+ token: ${{ secrets.DOCS_BOT_PAT_BASE }}
diff --git a/content/code-security/reference/supply-chain-security/dependabot-options-reference.md b/content/code-security/reference/supply-chain-security/dependabot-options-reference.md
index 2b2d74225fd6..aea6d55c4831 100644
--- a/content/code-security/reference/supply-chain-security/dependabot-options-reference.md
+++ b/content/code-security/reference/supply-chain-security/dependabot-options-reference.md
@@ -572,7 +572,7 @@ Package manager | YAML value | Supported versions |
| pip | `pip` | 24.2 |
| pip-compile | `pip` | 7.5.3 |
| pipenv | `pip` | <= 2024.4.1 |
-| pnpm | `npm` | v7, v8
v9, v10 (version updates only) |
+| pnpm | `npm` | v7, v8, v9, v10 |
| poetry | `pip` | v2 |
| {% ifversion dependabot-pre-commit-support %} |
| pre-commit | `pre-commit` | Not applicable |
diff --git a/content/rest/about-the-rest-api/api-versions.md b/content/rest/about-the-rest-api/api-versions.md
index 519be84db7ca..068243fcd4bc 100644
--- a/content/rest/about-the-rest-api/api-versions.md
+++ b/content/rest/about-the-rest-api/api-versions.md
@@ -36,7 +36,7 @@ curl {% data reusables.rest-api.version-header %} https://api.github.com/zen
Requests without the `X-GitHub-Api-Version` header will default to use the `{{ defaultRestApiVersion }}` version.
-If you specify an API version that is no longer supported, you will receive a `400` error.
+If you specify an API version that is no longer supported, you will receive a `410 Gone` response.
## Upgrading to a new API version
@@ -46,12 +46,45 @@ When you update your integration to specify the new API version in the `X-GitHub
Once your integration is updated, test your integration to verify that it works with the new API version.
+## API version {% data variables.release-phases.closing_down %}
+
+API versions are supported for 24 months after a newer API version is released.
+
+While a version is within its support window but approaching {% data variables.release-phases.closing_down %}, {% data variables.product.github %} includes the following headers in API responses to help you prepare for migration:
+
+* `Deprecation` — The date when the API version will be {% data variables.release-phases.closing_down %}, formatted as an HTTP date per [RFC 7231](https://tools.ietf.org/html/rfc7231#section-7.1.1.1). For example: `Wed, 27 Nov 2019 14:34:29 GMT`.
+* `Sunset` — The date when the API version will be completely removed ({% data variables.release-phases.retired %}), after which requests will return a `410 Gone` response. Follows [RFC 8594](https://tools.ietf.org/html/rfc8594). For example: `Fri, 27 Nov 2020 14:34:29 GMT`.
+
+After the support window ends:
+
+* Requests that specify a {% data variables.release-phases.closing_down %} API version receive a `410 Gone` response.
+* Requests that do not specify an API version default to the next oldest supported version, not the {% data variables.release-phases.closing_down %} version. If you rely on unversioned requests, you may observe behavioral changes as older versions are removed from support.
+
+For more information on migrating to a newer API version, see [AUTOTITLE](/rest/about-the-rest-api/breaking-changes).
+
+## Exceptions to standard versioning
+
+In rare cases, {% data variables.product.github %} may make changes outside the normal API versioning cadence. These are exceptional interventions that do not alter the standard versioning guarantees for most integrators.
+
+### Security, availability, and reliability issues
+
+Critical security vulnerabilities, data exposure risks, or severe reliability issues may require changes outside the normal release schedule. {% data variables.product.github %} may release an unscheduled API version, backport fixes to supported versions, or in rare cases, introduce a breaking change to an existing version to protect users and platform integrity.
+
+{% data variables.product.github %} will communicate such changes through release notes, changelogs, and direct communication explaining what changed and why. Where feasible, advance notice will be provided. Immediate action may be taken without advance notice when required.
+
+### Low-usage services
+
+For certain services with very low usage, {% data variables.product.github %} may deprecate functionality outside the standard versioning process. In these cases, {% data variables.product.github %} will communicate the intent and reach out to affected integrators directly.
+
## Supported API versions
-The following REST API versions are currently supported:
+The following REST API versions are currently supported.
-{% for apiVersion in allVersions[currentVersion].apiVersions %}
-{{ apiVersion }}
-{% endfor %}
+| API version | End of support date |
+| --- | --- |
+{%- for apiVersion in allVersions[currentVersion].apiVersions %}
+{%- assign versionData = tables.rest-api-versions.versions[apiVersion] %}
+| `{{ apiVersion }}` | {{ versionData.end_of_support | default: "Not yet scheduled" }} |
+{%- endfor %}
You can also make an API request to get all of the supported API versions. For more information, see [AUTOTITLE](/rest/meta/meta#get-all-api-versions).
diff --git a/data/reusables/rest-api/about-api-versions.md b/data/reusables/rest-api/about-api-versions.md
index f6aece67bb61..729b78ec3bfc 100644
--- a/data/reusables/rest-api/about-api-versions.md
+++ b/data/reusables/rest-api/about-api-versions.md
@@ -1,6 +1,6 @@
The {% data variables.product.github %} REST API is versioned. The API version name is based on the date when the API version was released. For example, the API version `{{ initialRestVersioningReleaseDate }}` was released on {{ initialRestVersioningReleaseDateLong }}.
-Breaking changes are changes that can potentially break an integration. We will provide advance notice before releasing breaking changes. Breaking changes include:
+Breaking changes are changes that can potentially break an integration. Breaking changes will be released in a new API version. We will provide advance notice before releasing breaking changes. Breaking changes include:
* Removing an entire operation
* Removing or renaming a parameter
diff --git a/data/tables/rest-api-versions.yml b/data/tables/rest-api-versions.yml
new file mode 100644
index 000000000000..9de73b822984
--- /dev/null
+++ b/data/tables/rest-api-versions.yml
@@ -0,0 +1,8 @@
+# REST API calendar date versions and their end of support dates.
+# Rendering code lives in content/rest/about-the-rest-api/api-versions.md
+
+versions:
+ '2022-11-28':
+ end_of_support: 'March 10, 2028'
+ '2026-03-10':
+ end_of_support: ~
diff --git a/eslint.config.ts b/eslint.config.ts
index e56b53c9a0df..0d3ef1e2b087 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -234,7 +234,6 @@ export default [
'src/article-api/transformers/audit-logs-transformer.ts',
'src/article-api/transformers/rest-transformer.ts',
'src/codeql-cli/scripts/convert-markdown-for-docs.ts',
- 'src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts',
'src/content-linter/scripts/lint-content.ts',
'src/content-render/liquid/engine.ts',
@@ -260,7 +259,6 @@ export default [
'src/graphql/tests/validate-schema.ts',
'src/landings/components/CookBookFilter.tsx',
'src/landings/components/ProductGuidesContext.tsx',
- 'src/landings/components/ProductLandingContext.tsx',
'src/landings/components/SidebarProduct.tsx',
'src/landings/pages/home.tsx',
'src/landings/pages/product.tsx',
@@ -271,7 +269,6 @@ export default [
'src/links/scripts/check-github-github-links.ts',
'src/links/scripts/update-internal-links.ts',
'src/rest/components/get-rest-code-samples.ts',
- 'src/rest/lib/index.ts',
'src/rest/pages/category.tsx',
'src/rest/pages/subcategory.tsx',
'src/rest/scripts/utils/create-rest-examples.ts',
@@ -303,8 +300,6 @@ export default [
'src/types/github__markdownlint-github.d.ts',
'src/types/markdownlint-lib-rules.d.ts',
'src/types/markdownlint-rule-helpers.d.ts',
- 'src/types/markdownlint-rule-search-replace.d.ts',
- 'src/types/primer__octicons.d.ts',
],
rules: {
'@typescript-eslint/no-explicit-any': 'off',
diff --git a/package-lock.json b/package-lock.json
index c98c68f86116..fff13072d6f5 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -81,10 +81,10 @@
"micromark-extension-gfm": "^3.0.0",
"next": "^16.2.3",
"ora": "^9.3.0",
- "parse5": "7.1.2",
+ "parse5": "8.0.1",
"quick-lru": "7.0.1",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
"react-is": "^19.2.4",
"react-markdown": "^10.1.0",
"rehype-highlight": "^7.0.2",
@@ -137,8 +137,8 @@
"@types/lodash": "^4.17.24",
"@types/lodash-es": "4.17.12",
"@types/mdast": "^4.0.4",
- "@types/react": "18.3.20",
- "@types/react-dom": "^18.3.7",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "^19.2.3",
"@types/semver": "^7.7.1",
"@types/styled-components": "^5.1.36",
"@types/tcp-port-used": "1.0.4",
@@ -559,11 +559,12 @@
}
},
"node_modules/@babel/helper-annotate-as-pure": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz",
- "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==",
+ "version": "7.27.3",
+ "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz",
+ "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==",
+ "license": "MIT",
"dependencies": {
- "@babel/types": "^7.22.5"
+ "@babel/types": "^7.27.3"
},
"engines": {
"node": ">=6.9.0"
@@ -647,9 +648,10 @@
}
},
"node_modules/@babel/helper-plugin-utils": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.22.5.tgz",
- "integrity": "sha512-uLls06UVKgFG9QD4OeFYLEGteMIAa5kpTPcFL28yuCIIzsf6ZyKZMllKVOCZFhiZ5ptnwX4mtKdWCBE/uT4amg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.28.6.tgz",
+ "integrity": "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug==",
+ "license": "MIT",
"engines": {
"node": ">=6.9.0"
}
@@ -683,14 +685,14 @@
}
},
"node_modules/@babel/helpers": {
- "version": "7.28.6",
- "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz",
- "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==",
+ "version": "7.29.2",
+ "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.2.tgz",
+ "integrity": "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw==",
"license": "MIT",
"peer": true,
"dependencies": {
"@babel/template": "^7.28.6",
- "@babel/types": "^7.28.6"
+ "@babel/types": "^7.29.0"
},
"engines": {
"node": ">=6.9.0"
@@ -712,11 +714,12 @@
}
},
"node_modules/@babel/plugin-syntax-jsx": {
- "version": "7.22.5",
- "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.22.5.tgz",
- "integrity": "sha512-gvyP4hZrgrs/wWMaocvxZ44Hw0b3W8Pe+cMxc8V1ULQ07oh8VNbIRaoD1LRZVTvD+0nieDKjfgKg89sD7rrKrg==",
+ "version": "7.28.6",
+ "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.28.6.tgz",
+ "integrity": "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w==",
+ "license": "MIT",
"dependencies": {
- "@babel/helper-plugin-utils": "^7.22.5"
+ "@babel/helper-plugin-utils": "^7.28.6"
},
"engines": {
"node": ">=6.9.0"
@@ -4830,10 +4833,6 @@
"undici-types": "~7.16.0"
}
},
- "node_modules/@types/prop-types": {
- "version": "15.7.4",
- "license": "MIT"
- },
"node_modules/@types/qs": {
"version": "6.14.0",
"resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz",
@@ -4849,23 +4848,22 @@
"license": "MIT"
},
"node_modules/@types/react": {
- "version": "18.3.20",
- "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.20.tgz",
- "integrity": "sha512-IPaCZN7PShZK/3t6Q87pfTkRm6oLTd4vztyoj+cbHUF1g3FfVb2tFIL79uCRKEfv16AhqDMBywP2VW3KIZUvcg==",
+ "version": "19.2.14",
+ "resolved": "https://registry.npmjs.org/@types/react/-/react-19.2.14.tgz",
+ "integrity": "sha512-ilcTH/UniCkMdtexkoCN0bI7pMcJDvmQFPvuPvmEaYA/NSfFTAgdUSLAoVjaRJm7+6PvcM+q1zYOwS4wTYMF9w==",
"license": "MIT",
"dependencies": {
- "@types/prop-types": "*",
- "csstype": "^3.0.2"
+ "csstype": "^3.2.2"
}
},
"node_modules/@types/react-dom": {
- "version": "18.3.7",
- "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-18.3.7.tgz",
- "integrity": "sha512-MEe3UeoENYVFXzoXEWsvcpg6ZvlrFNlOQ7EOsvhI3CfAXwzPfO8Qwuxd40nepsYKqyyVQnTdEfv68q91yLcKrQ==",
+ "version": "19.2.3",
+ "resolved": "https://registry.npmjs.org/@types/react-dom/-/react-dom-19.2.3.tgz",
+ "integrity": "sha512-jp2L/eY6fn+KgVVQAOqYItbF0VY/YApe5Mz2F0aykSO8gx31bYCZyvSeYxCHKvzHG5eZjc+zyaS5BrBWya2+kQ==",
"devOptional": true,
"license": "MIT",
"peerDependencies": {
- "@types/react": "^18.0.0"
+ "@types/react": "^19.2.0"
}
},
"node_modules/@types/request": {
@@ -6080,6 +6078,7 @@
"version": "2.1.4",
"resolved": "https://registry.npmjs.org/babel-plugin-styled-components/-/babel-plugin-styled-components-2.1.4.tgz",
"integrity": "sha512-Xgp9g+A/cG47sUyRwwYxGM4bR/jDRg5N6it/8+HxCnbT5XNKSKDT9xm4oag/osgqjC2It/vH0yXsomOG6k558g==",
+ "license": "MIT",
"dependencies": {
"@babel/helper-annotate-as-pure": "^7.22.5",
"@babel/helper-module-imports": "^7.22.5",
@@ -9549,6 +9548,30 @@
"resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.0.tgz",
"integrity": "sha512-MFETx3tbTjE7Uk6vvnWINA/1iJ7LuMdO4fcq8UfF0pRbj01aGLduVvQcRyswuACJdpnHgg8E3rQLhaRdNEJS0w=="
},
+ "node_modules/hast-util-raw/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/hast-util-raw/node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/hast-util-to-html": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/hast-util-to-html/-/hast-util-to-html-9.0.0.tgz",
@@ -11243,16 +11266,6 @@
"url": "https://github.com/sponsors/wooorm"
}
},
- "node_modules/loose-envify": {
- "version": "1.4.0",
- "license": "MIT",
- "dependencies": {
- "js-tokens": "^3.0.0 || ^4.0.0"
- },
- "bin": {
- "loose-envify": "cli.js"
- }
- },
"node_modules/lowdb": {
"version": "7.0.1",
"resolved": "https://registry.npmjs.org/lowdb/-/lowdb-7.0.1.tgz",
@@ -13482,10 +13495,12 @@
}
},
"node_modules/parse5": {
- "version": "7.1.2",
+ "version": "8.0.1",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-8.0.1.tgz",
+ "integrity": "sha512-z1e/HMG90obSGeidlli3hj7cbocou0/wa5HacvI3ASx34PecNjNQeaHNo5WIZpWofN9kgkqV1q5YvXe3F0FoPw==",
"license": "MIT",
"dependencies": {
- "entities": "^4.4.0"
+ "entities": "^8.0.0"
},
"funding": {
"url": "https://github.com/inikulin/parse5?sponsor=1"
@@ -13504,6 +13519,30 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
+ "node_modules/parse5-htmlparser2-tree-adapter/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
+ "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
"node_modules/parse5-parser-stream": {
"version": "7.1.2",
"resolved": "https://registry.npmjs.org/parse5-parser-stream/-/parse5-parser-stream-7.1.2.tgz",
@@ -13516,8 +13555,10 @@
"url": "https://github.com/inikulin/parse5?sponsor=1"
}
},
- "node_modules/parse5/node_modules/entities": {
- "version": "4.4.0",
+ "node_modules/parse5-parser-stream/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
"license": "BSD-2-Clause",
"engines": {
"node": ">=0.12"
@@ -13526,6 +13567,30 @@
"url": "https://github.com/fb55/entities?sponsor=1"
}
},
+ "node_modules/parse5-parser-stream/node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/parse5/node_modules/entities": {
+ "version": "8.0.0",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-8.0.0.tgz",
+ "integrity": "sha512-zwfzJecQ/Uej6tusMqwAqU/6KL2XaB2VZ2Jg54Je6ahNBGNH6Ek6g3jjNCF0fG9EWQKGZNddNjU5F1ZQn/sBnA==",
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=20.19.0"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/parseurl": {
"version": "1.3.3",
"resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz",
@@ -13970,12 +14035,10 @@
}
},
"node_modules/react": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz",
- "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==",
- "dependencies": {
- "loose-envify": "^1.1.0"
- },
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react/-/react-19.2.5.tgz",
+ "integrity": "sha512-llUJLzz1zTUBrskt2pwZgLq59AemifIftw4aB7JxOqf1HY2FDaGDxgwpAPVzHU1kdWabH7FauP4i1oEeer2WCA==",
+ "license": "MIT",
"engines": {
"node": ">=0.10.0"
}
@@ -13990,15 +14053,15 @@
}
},
"node_modules/react-dom": {
- "version": "18.3.1",
- "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz",
- "integrity": "sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==",
+ "version": "19.2.5",
+ "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.2.5.tgz",
+ "integrity": "sha512-J5bAZz+DXMMwW/wV3xzKke59Af6CHY7G4uYLN1OvBcKEsWOs4pQExj86BBKamxl/Ik5bx9whOrvBlSDfWzgSag==",
+ "license": "MIT",
"dependencies": {
- "loose-envify": "^1.1.0",
- "scheduler": "^0.23.2"
+ "scheduler": "^0.27.0"
},
"peerDependencies": {
- "react": "^18.3.1"
+ "react": "^19.2.5"
}
},
"node_modules/react-intersection-observer": {
@@ -14695,12 +14758,10 @@
"integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg=="
},
"node_modules/scheduler": {
- "version": "0.23.2",
- "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz",
- "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==",
- "dependencies": {
- "loose-envify": "^1.1.0"
- }
+ "version": "0.27.0",
+ "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.27.0.tgz",
+ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==",
+ "license": "MIT"
},
"node_modules/scroll-anchoring": {
"version": "0.1.0",
@@ -17043,6 +17104,32 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
+ "node_modules/website-scraper/node_modules/parse5": {
+ "version": "7.3.0",
+ "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz",
+ "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==",
+ "dev": true,
+ "license": "MIT",
+ "dependencies": {
+ "entities": "^6.0.0"
+ },
+ "funding": {
+ "url": "https://github.com/inikulin/parse5?sponsor=1"
+ }
+ },
+ "node_modules/website-scraper/node_modules/parse5/node_modules/entities": {
+ "version": "6.0.1",
+ "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz",
+ "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==",
+ "dev": true,
+ "license": "BSD-2-Clause",
+ "engines": {
+ "node": ">=0.12"
+ },
+ "funding": {
+ "url": "https://github.com/fb55/entities?sponsor=1"
+ }
+ },
"node_modules/whatwg-encoding": {
"version": "3.1.1",
"resolved": "https://registry.npmjs.org/whatwg-encoding/-/whatwg-encoding-3.1.1.tgz",
diff --git a/package.json b/package.json
index d783b2822961..816e1bf326b8 100644
--- a/package.json
+++ b/package.json
@@ -241,10 +241,10 @@
"micromark-extension-gfm": "^3.0.0",
"next": "^16.2.3",
"ora": "^9.3.0",
- "parse5": "7.1.2",
+ "parse5": "8.0.1",
"quick-lru": "7.0.1",
- "react": "^18.3.1",
- "react-dom": "^18.3.1",
+ "react": "^19.2.5",
+ "react-dom": "^19.2.5",
"react-is": "^19.2.4",
"react-markdown": "^10.1.0",
"rehype-highlight": "^7.0.2",
@@ -297,8 +297,8 @@
"@types/lodash": "^4.17.24",
"@types/lodash-es": "4.17.12",
"@types/mdast": "^4.0.4",
- "@types/react": "18.3.20",
- "@types/react-dom": "^18.3.7",
+ "@types/react": "19.2.14",
+ "@types/react-dom": "^19.2.3",
"@types/semver": "^7.7.1",
"@types/styled-components": "^5.1.36",
"@types/tcp-port-used": "1.0.4",
diff --git a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts
index 664368357c86..68b7d225854f 100644
--- a/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts
+++ b/src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts
@@ -21,6 +21,64 @@ import {
import { oldestSupported } from '@/versions/lib/enterprise-server-releases'
import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
+// A liquidjs token, as exposed by getLiquidIfVersionTokens. liquidjs's TopLevelToken
+// type does not declare all of the runtime properties we rely on (begin/end, content,
+// contentRange, name), so we narrow it here.
+type LiquidConditionalToken = TopLevelToken & {
+ name: string
+ content: string
+ begin: number
+ end: number
+ contentRange: [number, number]
+}
+
+// Frontmatter `versions` declaration. May be a wildcard string ("*") or a record
+// keyed by short version names (fpt, ghec, ghes, feature, ...) with semver-range values.
+type VersionsObject = Record
+type FileVersionsFm = string | VersionsObject | undefined
+
+type CondTagAction = {
+ type: 'none' | 'delete' | 'all' | 'change'
+ name?: string
+ cond?: string
+ line?: unknown
+ lineNumbers?: unknown
+ length?: unknown
+ column?: unknown
+ content?: unknown
+}
+
+// Internal representation of an ifversion/elsif/else/endif tag that flows through
+// the rule. fileVersionsFmAll, versionsObj, featureVersionsObj, versionsObjAll, and
+// versions are populated for ifversion/elsif tags and may be absent on else/endif.
+// `action` is always populated by decorateCondTagItems before setLiquidErrors and
+// updateConditionals run.
+type CondTagItem = {
+ name: string
+ cond: string
+ begin: number
+ end: number
+ contentrange: [number, number]
+ fileVersionsFm: FileVersionsFm
+ fileVersionsFmAll: VersionsObject
+ fileVersions: string[]
+ parent?: CondTagItem
+ versionsObj: VersionsObject
+ featureVersionsObj?: VersionsObject
+ versionsObjAll: VersionsObject
+ versions: string[]
+ action: CondTagAction
+ // Cached error range (used by addError); never set by this rule but accepted by addError.
+ contentRange?: [number, number] | number[] | string | null
+}
+
+type DefaultProps = {
+ fileVersionsFm: FileVersionsFm
+ fileVersions: string[]
+ filename: string
+ parent: CondTagItem | undefined
+}
+
export const liquidIfversionVersions = {
names: ['GHD022', 'liquid-ifversion-versions'],
description:
@@ -33,14 +91,11 @@ export const liquidIfversionVersions = {
const fm = getFrontmatter(params.lines)
const content = fm ? getFrontmatterLines(params.lines).join('\n') : params.lines.join('\n')
- const fileVersionsFm = params.name.startsWith('data')
+ const fileVersionsFm: FileVersionsFm = params.name.startsWith('data')
? { ghec: '*', ghes: '*', fpt: '*' }
: fm
- ? (fm.versions as string | Record | undefined)
- : (getFrontmatter(params.frontMatterLines)?.versions as
- | string
- | Record
- | undefined)
+ ? (fm.versions as FileVersionsFm)
+ : (getFrontmatter(params.frontMatterLines)?.versions as FileVersionsFm)
if (!fileVersionsFm) return
// This will only contain valid (non-deprecated) and future versions
const fileVersions = getApplicableVersions(fileVersionsFm, '', {
@@ -48,16 +103,15 @@ export const liquidIfversionVersions = {
includeNextVersion: true,
})
- const tokens = getLiquidIfVersionTokens(content)
+ const tokens = getLiquidIfVersionTokens(content) as LiquidConditionalToken[]
// Array of arrays - each array entry is an array of items that
// make up a full if/elsif/else/endif statement.
// [ [ifversion, elsif, else, endif], [nested ifversion, elsif, else, endif] ]
- // Using any[] because these are complex dynamic objects with properties added at runtime
- const condStmtStack: any[] = []
+ const condStmtStack: CondTagItem[][] = []
// Tokens are in the order they are read in file, so we need to iterate
// through and group full if/elsif/else/endif statements together.
- const defaultProps = {
+ const defaultProps: DefaultProps = {
fileVersionsFm,
fileVersions,
filename: params.name,
@@ -74,27 +128,25 @@ export const liquidIfversionVersions = {
const condTagItem = await initTagObject(token, defaultProps)
condStmtStack.push([condTagItem])
} else if (token.name === 'elsif') {
- const condTagItems = condStmtStack.pop()
+ const condTagItems = condStmtStack.pop()!
const condTagItem = await initTagObject(token, defaultProps)
condTagItems.push(condTagItem)
condStmtStack.push(condTagItems)
} else if (token.name === 'else') {
- const condTagItems = condStmtStack.pop()
+ const condTagItems = condStmtStack.pop()!
const condTagItem = await initTagObject(token, defaultProps)
// The versions of an else tag are the set of file versions that are
// not supported by the previous ifversion or elsif tags.
const siblingVersions = condTagItems
- // Using any because condTagItems contains dynamic objects from initTagObject
- .filter((item: any) => item.name === 'ifversion' || item.name === 'elsif')
- .map((item: any) => item.versions)
+ .filter((item) => item.name === 'ifversion' || item.name === 'elsif')
+ .map((item) => item.versions)
.flat()
- // Using any because versions property is added dynamically to condTagItem
- ;(condTagItem as any).versions = difference(fileVersions, siblingVersions)
+ condTagItem.versions = difference(fileVersions, siblingVersions)
condTagItems.push(condTagItem)
condStmtStack.push(condTagItems)
} else if (token.name === 'endif') {
defaultProps.parent = undefined
- const condTagItems = condStmtStack.pop()
+ const condTagItems = condStmtStack.pop()!
const condTagItem = await initTagObject(token, defaultProps)
condTagItems.push(condTagItem)
decorateCondTagItems(condTagItems)
@@ -104,18 +156,21 @@ export const liquidIfversionVersions = {
},
}
-// Using any[] because condTagItems contains dynamic objects with properties added at runtime
-function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines: string[]) {
+function setLiquidErrors(condTagItems: CondTagItem[], onError: RuleErrorCallback, lines: string[]) {
for (let i = 0; i < condTagItems.length; i++) {
const item = condTagItems[i]
const tagNameNoCond = item.name === 'endif' || item.name === 'else'
const itemErrorName = tagNameNoCond ? item.name : `${item.name} ${item.cond}`
- if (item.action.type === 'delete') {
+ if (item.action?.type === 'delete') {
// There is no next stack item, the endif tag is alway the
// last in a conditional
const nextStackItem = item.name === 'endif' ? condTagItems[i].end : condTagItems[i + 1].begin
- const deleteItems = getContentDeleteData(condTagItems[i], nextStackItem, lines)
+ const deleteItems = getContentDeleteData(
+ condTagItems[i] as unknown as TopLevelToken,
+ nextStackItem,
+ lines,
+ )
for (const deleteItem of deleteItems) {
addError(
onError,
@@ -133,7 +188,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines:
}
}
- if (item.action.type === 'all') {
+ if (item.action?.type === 'all') {
// position is just the tag
const { lineNumber, column, length } = getPositionData(
{
@@ -158,7 +213,7 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines:
)
}
- if (item.action.type === 'change') {
+ if (item.action?.type === 'change') {
// position is just the inside of tag
const { lineNumber, column, length } = getPositionData(
{
@@ -186,9 +241,8 @@ function setLiquidErrors(condTagItems: any[], onError: RuleErrorCallback, lines:
}
}
-async function getApplicableVersionFromLiquidTag(conditionStr: string) {
- // Using Record because version object keys are dynamic (fpt, ghec, ghes, feature, etc.)
- const newConditionObject: Record = {}
+async function getApplicableVersionFromLiquidTag(conditionStr: string): Promise {
+ const newConditionObject: VersionsObject = {}
const condition = conditionStr.replace('not ', '')
const liquidTagVersions = condition.split(' or ').map((item) => item.trim())
for (const ver of liquidTagVersions) {
@@ -211,7 +265,7 @@ async function getApplicableVersionFromLiquidTag(conditionStr: string) {
// All actual uses have matching versions (e.g., "ghes and ghes > 3.19").
// If this edge case appears in the future, additional logic would be needed here.
if (!ands.every((and) => and.startsWith(firstAnd))) {
- return []
+ return {}
}
const andValues = []
let andVersion = ''
@@ -235,41 +289,56 @@ async function getApplicableVersionFromLiquidTag(conditionStr: string) {
doNotThrow: true,
includeNextVersion: true,
})
- return await convertVersionsToFrontmatter(difference(all, allApplicable))
+ return (await convertVersionsToFrontmatter(difference(all, allApplicable))) as VersionsObject
}
return newConditionObject
}
-// Using any for token and props because they come from markdownlint library without full type definitions
-async function initTagObject(token: any, props: any) {
- const condTagItem = {
+async function initTagObject(
+ token: LiquidConditionalToken,
+ props: DefaultProps,
+): Promise {
+ const fileVersionsFm = props.fileVersionsFm
+ // Normalize a wildcard string ('*') frontmatter `versions` value into the
+ // canonical all-versions object so downstream consumers (Object.keys, ghes /
+ // feature lookups) behave consistently. In practice no content file uses the
+ // string form today, but handling it keeps the rule type-safe and future-proof.
+ const fmObject: VersionsObject =
+ typeof fileVersionsFm === 'string'
+ ? { ghec: '*', ghes: '*', fpt: '*' }
+ : ((fileVersionsFm || {}) as VersionsObject)
+ const featureFromFm = fmObject.feature
+ const condTagItem: CondTagItem = {
name: token.name,
cond: token.content.replace(`${token.name} `, '').trim(),
begin: token.begin,
end: token.end,
contentrange: token.contentRange,
- fileVersionsFm: props.fileVersionsFm,
- fileVersionsFmAll: props.fileVersionsFm?.feature
+ fileVersionsFm,
+ fileVersionsFmAll: featureFromFm
? {
- ...props.fileVersionsFm.versions,
- ...getFeatureVersionsObject(props.fileVersionsFm.feature),
+ ...((fmObject as unknown as { versions?: VersionsObject }).versions || {}),
+ ...getFeatureVersionsObject(featureFromFm),
}
- : props.fileVersionsFm,
+ : fmObject,
fileVersions: props.fileVersions,
parent: props.parent,
+ versionsObj: {},
+ featureVersionsObj: undefined,
+ versionsObjAll: {},
+ versions: [],
+ action: { type: 'none' },
}
if (token.name === 'ifversion' || token.name === 'elsif') {
- // Using any because these properties (versionsObj, featureVersionsObj, versionsObjAll, versions)
- // are added dynamically to condTagItem and not part of its initial type definition
- ;(condTagItem as any).versionsObj = await getApplicableVersionFromLiquidTag(condTagItem.cond)
- ;(condTagItem as any).featureVersionsObj = (condTagItem as any).versionsObj.feature
- ? getFeatureVersionsObject((condTagItem as any).versionsObj.feature)
+ condTagItem.versionsObj = await getApplicableVersionFromLiquidTag(condTagItem.cond)
+ condTagItem.featureVersionsObj = condTagItem.versionsObj.feature
+ ? getFeatureVersionsObject(condTagItem.versionsObj.feature)
: undefined
- ;(condTagItem as any).versionsObjAll = {
- ...(condTagItem as any).versionsObj,
- ...(condTagItem as any).featureVersionsObj,
+ condTagItem.versionsObjAll = {
+ ...condTagItem.versionsObj,
+ ...condTagItem.featureVersionsObj,
}
- ;(condTagItem as any).versions = getApplicableVersions((condTagItem as any).versionsObj, '', {
+ condTagItem.versions = getApplicableVersions(condTagItem.versionsObj, '', {
doNotThrow: true,
includeNextVersion: true,
})
@@ -286,8 +355,7 @@ async function initTagObject(token: any, props: any) {
Then create flaws per stack item.
newCond
*/
-// Using any[] because condTagItems contains dynamic objects with action property added at runtime
-function decorateCondTagItems(condTagItems: any[]) {
+function decorateCondTagItems(condTagItems: CondTagItem[]) {
for (const item of condTagItems) {
item.action = {
type: 'none',
@@ -304,8 +372,7 @@ function decorateCondTagItems(condTagItems: any[]) {
return
}
-// Using any[] because condTagItems contains dynamic objects with various properties added at runtime
-function updateConditionals(condTagItems: any[]) {
+function updateConditionals(condTagItems: CondTagItem[]) {
// iterate through the ifversion, elsif, and else
// tags but NOT the endif tag. endif tags have
// no versions associated with them and are handled
@@ -317,7 +384,14 @@ function updateConditionals(condTagItems: any[]) {
// the liquid should always be removed regardless
// of whether it's a feature version or a nested
// condition.
- if (isAllVersions(item.featureVersionsObj || item.versionObj)) {
+ // NOTE: Original code referenced `item.versionObj` (no `s`), which was always
+ // undefined; preserved as-is to avoid changing runtime behavior in this PR.
+ if (
+ isAllVersions(
+ item.featureVersionsObj ||
+ ((item as unknown as { versionObj?: VersionsObject }).versionObj as VersionsObject),
+ )
+ ) {
processConditionals(item, condTagItems, i)
break
}
@@ -349,7 +423,9 @@ function updateConditionals(condTagItems: any[]) {
continue
}
}
- if (item.versionsObj?.feature || item.fileVersionsFm?.feature) break
+ const fileVersionsFmObject =
+ item.fileVersionsFm && typeof item.fileVersionsFm === 'object' ? item.fileVersionsFm : {}
+ if (item.versionsObj?.feature || fileVersionsFmObject.feature) break
// When the parent of a nested condition is a feature
// we don't want to assume that the feature versions
@@ -478,8 +554,11 @@ function updateConditionals(condTagItems: any[]) {
}
}
-// Using any for item and any[] for condTagItems because they contain dynamic objects with action property
-function processConditionals(item: any, condTagItems: any[], indexOfAllItem: number) {
+function processConditionals(
+ item: CondTagItem,
+ condTagItems: CondTagItem[],
+ indexOfAllItem: number,
+) {
item.action.type = 'all'
// if any tag in a statement is 'all', the
// remaining tags are obsolete.
diff --git a/src/data-directory/lib/data-schemas/tables/rest-api-versions.ts b/src/data-directory/lib/data-schemas/tables/rest-api-versions.ts
new file mode 100644
index 000000000000..046c02afe403
--- /dev/null
+++ b/src/data-directory/lib/data-schemas/tables/rest-api-versions.ts
@@ -0,0 +1,22 @@
+// This schema enforces the structure in data/tables/rest-api-versions.yml
+
+export default {
+ type: 'object',
+ additionalProperties: false,
+ required: ['versions'],
+ properties: {
+ versions: {
+ type: 'object',
+ additionalProperties: {
+ type: 'object',
+ additionalProperties: false,
+ required: ['end_of_support'],
+ properties: {
+ end_of_support: {
+ type: ['string', 'null'],
+ },
+ },
+ },
+ },
+ },
+}
diff --git a/src/frame/components/ui/BumpLink/BumpLink.tsx b/src/frame/components/ui/BumpLink/BumpLink.tsx
index 7e2f3454144d..a354495aae25 100644
--- a/src/frame/components/ui/BumpLink/BumpLink.tsx
+++ b/src/frame/components/ui/BumpLink/BumpLink.tsx
@@ -5,7 +5,7 @@ import styles from './BumpLink.module.scss'
export type BumpLinkPropsT = {
children?: ReactNode
- title: ReactElement | string
+ title: ReactElement<{ children?: ReactNode }> | string
href: string
as?: ElementType<{ className?: string; href: string }>
className?: string
diff --git a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx
index 3f3790287234..8a75efbf2a2b 100644
--- a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx
+++ b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx
@@ -1,4 +1,4 @@
-import { ReactNode } from 'react'
+import { memo, ReactNode } from 'react'
import type { JSX } from 'react'
import cx from 'classnames'
@@ -10,12 +10,15 @@ export type MarkdownContentPropsT = {
as?: keyof JSX.IntrinsicElements
}
-export const MarkdownContent = ({
+// Memoized so that re-renders of the parent (e.g. when ToolPicker/PlatformPicker
+// state updates) don't cause React 19 to re-apply `dangerouslySetInnerHTML` and
+// wipe out the inline `display` styles set imperatively by the pickers.
+export const MarkdownContent = memo(function MarkdownContent({
children,
as: Component = 'div',
className,
...restProps
-}: MarkdownContentPropsT) => {
+}: MarkdownContentPropsT) {
return (
)
-}
+})
diff --git a/src/frame/tests/server.ts b/src/frame/tests/server.ts
index 130ce22e771c..3e1fa0146af7 100644
--- a/src/frame/tests/server.ts
+++ b/src/frame/tests/server.ts
@@ -324,6 +324,12 @@ describe('server', () => {
expect(res.body).toMatch(/^# .+/)
})
+ test('.md URL without language prefix redirects to /en/ equivalent', async () => {
+ const res = await get('/get-started.md')
+ expect(res.statusCode).toBe(302)
+ expect(res.headers.location).toBe('/en/get-started.md')
+ })
+
test('/index.md redirects to the page without /index.md', async () => {
const res = await get('/en/get-started/index.md')
expect(res.statusCode).toBe(302)
diff --git a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json
index 0ac456e84042..ec3f990cc4cd 100644
--- a/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json
+++ b/src/github-apps/data/fpt-2022-11-28/fine-grained-pat-permissions.json
@@ -1076,6 +1076,102 @@
}
]
},
+ "organization_copilot_spaces": {
+ "title": "Copilot Spaces",
+ "displayTitle": "Organization permissions for \"Copilot Spaces\"",
+ "permissions": [
+ {
+ "category": "copilot-spaces",
+ "slug": "list-organization-copilot-spaces",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "list-resources-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-a-resource-from-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "write"
+ }
+ ]
+ },
"organization_copilot_agent_settings": {
"title": "Copilot agent settings",
"displayTitle": "Organization permissions for \"Copilot agent settings\"",
diff --git a/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json b/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json
index 9fb0e35ffdce..55279401e371 100644
--- a/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json
+++ b/src/github-apps/data/fpt-2022-11-28/server-to-server-permissions.json
@@ -1553,6 +1553,122 @@
}
]
},
+ "organization_copilot_spaces": {
+ "title": "Copilot Spaces",
+ "displayTitle": "Organization permissions for \"Copilot Spaces\"",
+ "permissions": [
+ {
+ "category": "copilot-spaces",
+ "slug": "list-organization-copilot-spaces",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "list-resources-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-a-resource-from-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ }
+ ]
+ },
"organization_copilot_agent_settings": {
"title": "Copilot agent settings",
"displayTitle": "Organization permissions for \"Copilot agent settings\"",
diff --git a/src/github-apps/data/fpt-2026-03-10/fine-grained-pat-permissions.json b/src/github-apps/data/fpt-2026-03-10/fine-grained-pat-permissions.json
index 0ac456e84042..ec3f990cc4cd 100644
--- a/src/github-apps/data/fpt-2026-03-10/fine-grained-pat-permissions.json
+++ b/src/github-apps/data/fpt-2026-03-10/fine-grained-pat-permissions.json
@@ -1076,6 +1076,102 @@
}
]
},
+ "organization_copilot_spaces": {
+ "title": "Copilot Spaces",
+ "displayTitle": "Organization permissions for \"Copilot Spaces\"",
+ "permissions": [
+ {
+ "category": "copilot-spaces",
+ "slug": "list-organization-copilot-spaces",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "list-resources-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-a-resource-from-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "write"
+ }
+ ]
+ },
"organization_copilot_agent_settings": {
"title": "Copilot agent settings",
"displayTitle": "Organization permissions for \"Copilot agent settings\"",
diff --git a/src/github-apps/data/fpt-2026-03-10/server-to-server-permissions.json b/src/github-apps/data/fpt-2026-03-10/server-to-server-permissions.json
index 9fb0e35ffdce..55279401e371 100644
--- a/src/github-apps/data/fpt-2026-03-10/server-to-server-permissions.json
+++ b/src/github-apps/data/fpt-2026-03-10/server-to-server-permissions.json
@@ -1553,6 +1553,122 @@
}
]
},
+ "organization_copilot_spaces": {
+ "title": "Copilot Spaces",
+ "displayTitle": "Organization permissions for \"Copilot Spaces\"",
+ "permissions": [
+ {
+ "category": "copilot-spaces",
+ "slug": "list-organization-copilot-spaces",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "list-resources-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-a-resource-from-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ }
+ ]
+ },
"organization_copilot_agent_settings": {
"title": "Copilot agent settings",
"displayTitle": "Organization permissions for \"Copilot agent settings\"",
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 4229083596da..10f9dd3a85ca 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
@@ -1112,6 +1112,102 @@
}
]
},
+ "organization_copilot_spaces": {
+ "title": "Copilot Spaces",
+ "displayTitle": "Organization permissions for \"Copilot Spaces\"",
+ "permissions": [
+ {
+ "category": "copilot-spaces",
+ "slug": "list-organization-copilot-spaces",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "list-resources-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-a-resource-from-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "write"
+ }
+ ]
+ },
"organization_copilot_agent_settings": {
"title": "Copilot agent settings",
"displayTitle": "Organization permissions for \"Copilot agent settings\"",
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 8977d37dcfd9..e49590ecc6a9 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
@@ -2255,6 +2255,122 @@
}
]
},
+ "organization_copilot_spaces": {
+ "title": "Copilot Spaces",
+ "displayTitle": "Organization permissions for \"Copilot Spaces\"",
+ "permissions": [
+ {
+ "category": "copilot-spaces",
+ "slug": "list-organization-copilot-spaces",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "list-resources-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-a-resource-from-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ }
+ ]
+ },
"organization_copilot_agent_settings": {
"title": "Copilot agent settings",
"displayTitle": "Organization permissions for \"Copilot agent settings\"",
diff --git a/src/github-apps/data/ghec-2026-03-10/fine-grained-pat-permissions.json b/src/github-apps/data/ghec-2026-03-10/fine-grained-pat-permissions.json
index 4229083596da..10f9dd3a85ca 100644
--- a/src/github-apps/data/ghec-2026-03-10/fine-grained-pat-permissions.json
+++ b/src/github-apps/data/ghec-2026-03-10/fine-grained-pat-permissions.json
@@ -1112,6 +1112,102 @@
}
]
},
+ "organization_copilot_spaces": {
+ "title": "Copilot Spaces",
+ "displayTitle": "Organization permissions for \"Copilot Spaces\"",
+ "permissions": [
+ {
+ "category": "copilot-spaces",
+ "slug": "list-organization-copilot-spaces",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "list-resources-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "read"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "write"
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-a-resource-from-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "additional-permissions": false,
+ "access": "write"
+ }
+ ]
+ },
"organization_copilot_agent_settings": {
"title": "Copilot agent settings",
"displayTitle": "Organization permissions for \"Copilot agent settings\"",
diff --git a/src/github-apps/data/ghec-2026-03-10/server-to-server-permissions.json b/src/github-apps/data/ghec-2026-03-10/server-to-server-permissions.json
index 8977d37dcfd9..e49590ecc6a9 100644
--- a/src/github-apps/data/ghec-2026-03-10/server-to-server-permissions.json
+++ b/src/github-apps/data/ghec-2026-03-10/server-to-server-permissions.json
@@ -2255,6 +2255,122 @@
}
]
},
+ "organization_copilot_spaces": {
+ "title": "Copilot Spaces",
+ "displayTitle": "Organization permissions for \"Copilot Spaces\"",
+ "permissions": [
+ {
+ "category": "copilot-spaces",
+ "slug": "list-organization-copilot-spaces",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-an-organization-copilot-space",
+ "subcategory": "copilot-spaces",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "list-resources-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "create-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "post",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "get-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "get",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "read",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "set-a-resource-for-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "put",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ },
+ {
+ "category": "copilot-spaces",
+ "slug": "delete-a-resource-from-an-organization-copilot-space",
+ "subcategory": "resources",
+ "verb": "delete",
+ "requestPath": "/orgs/{org}/copilot-spaces/{space_number}/resources/{space_resource_id}",
+ "access": "write",
+ "user-to-server": true,
+ "server-to-server": true,
+ "additional-permissions": false
+ }
+ ]
+ },
"organization_copilot_agent_settings": {
"title": "Copilot agent settings",
"displayTitle": "Organization permissions for \"Copilot agent settings\"",
diff --git a/src/landings/components/ProductLandingContext.tsx b/src/landings/components/ProductLandingContext.tsx
index 857e01c54190..35c435c77739 100644
--- a/src/landings/components/ProductLandingContext.tsx
+++ b/src/landings/components/ProductLandingContext.tsx
@@ -1,6 +1,7 @@
import { createContext, useContext } from 'react'
import pick from 'lodash/pick'
import type { SimpleTocItem } from '@/landings/types'
+import type { ExtendedRequest, FeaturedLinkExpanded } from '@/types'
export type FeaturedLink = {
title: string
href: string
@@ -69,17 +70,29 @@ export const useProductLandingContext = (): ProductLandingContextT => {
return context
}
-export const getFeaturedLinksFromReq = (req: any): Record> => {
+// Minimal request shape needed to extract featured links. We use a structural type
+// here because callers pass either an Express ExtendedRequest or a narrower
+// per-context request type defined alongside other landing-context helpers.
+type FeaturedLinksRequest = {
+ context?: {
+ featuredLinks?: Record | unknown
+ }
+}
+
+export const getFeaturedLinksFromReq = (
+ req: FeaturedLinksRequest,
+): Record> => {
+ const featuredLinks = (req.context?.featuredLinks || {}) as Record
return Object.fromEntries(
- Object.entries(req.context.featuredLinks || {}).map(([key, entries]) => {
+ Object.entries(featuredLinks).map(([key, entries]) => {
return [
key,
- ((entries as Array) || []).map((entry: any) => ({
+ ((entries as FeaturedLinkExpanded[]) || []).map((entry) => ({
href: entry.href,
title: entry.title,
- intro: entry.intro || null,
- authors: entry.page?.authors || [],
- fullTitle: entry.fullTitle || null,
+ intro: entry.intro,
+ authors: (entry.page as { authors?: string[] } | undefined)?.authors || [],
+ fullTitle: entry.fullTitle,
})),
]
}),
@@ -87,70 +100,89 @@ export const getFeaturedLinksFromReq = (req: any): Record => {
- const productTree = req.context.currentProductTree
- const page = req.context.page
+ const context = req.context
+ if (!context) {
+ throw new Error('"getProductLandingContextFromRequest" requires req.context')
+ }
+ const productTree = context.currentProductTree
+ if (!productTree) {
+ throw new Error('"getProductLandingContextFromRequest" requires req.context.currentProductTree')
+ }
+ const page = context.page
+ if (!page) {
+ throw new Error('"getProductLandingContextFromRequest" requires req.context.page')
+ }
const hasGuidesPage = (page.children || []).includes('/guides')
- const title = await page.renderProp('title', req.context, { textOnly: true })
- const shortTitle = (await page.renderProp('shortTitle', req.context, { textOnly: true })) || null
+ const title = await page.renderProp('title', context, { textOnly: true })
+ const shortTitle = (await page.renderProp('shortTitle', context, { textOnly: true })) || null
// This props is displayed on the product landing page as "Supported
// releases". So we omit, if there is one, the release candidate.
- const ghesReleases = (req.context.ghesReleases || []).filter((release: Release) => {
+ const ghesReleases = ((context.ghesReleases || []) as Release[]).filter((release) => {
return !release.isReleaseCandidate
})
return {
title,
- shortTitle,
- ...pick(page, ['introPlainText', 'beta_product', 'intro']),
- heroImage: page.heroImage || null,
+ shortTitle: shortTitle || '',
+ ...(pick(page as unknown as Record, [
+ 'introPlainText',
+ 'beta_product',
+ 'intro',
+ ]) as { introPlainText: string; beta_product: boolean; intro: string }),
+ heroImage: (page as { heroImage?: string }).heroImage,
hasGuidesPage,
product: {
href: productTree.href,
title: productTree.page.shortTitle || productTree.page.title,
},
- whatsNewChangelog: req.context.whatsNewChangelog || [],
- changelogUrl: req.context.changelogUrl || [],
- productCommunityExamples: req.context.productCommunityExamples || [],
+ whatsNewChangelog: context.whatsNewChangelog || [],
+ changelogUrl: context.changelogUrl,
+ productCommunityExamples: (context.productCommunityExamples ||
+ []) as ProductLandingContextT['productCommunityExamples'],
ghesReleases,
- productUserExamples: (req.context.productUserExamples || []).map(
- ({ user, description }: any) => ({
- username: user,
- description,
- }),
- ),
+ productUserExamples: (context.productUserExamples || []).map(({ user, description }) => ({
+ username: user as string,
+ description,
+ })),
- introLinks: page.introLinks || null,
+ introLinks:
+ ((page as { introLinks?: Record }).introLinks as
+ | Record
+ | undefined) || null,
featuredLinks: getFeaturedLinksFromReq(req),
- tocItems: req.context.tocItems || [],
+ tocItems: ((context as { tocItems?: SimpleTocItem[] }).tocItems || []) as SimpleTocItem[],
- featuredArticles: Object.entries(req.context.featuredLinks || [])
+ featuredArticles: Object.entries(context.featuredLinks || {})
.filter(([key]) => {
return key === 'startHere' || key === 'popular'
})
- .map(([key, links]: any) => {
+ .map(([key, links]) => {
+ const pageFeaturedLinks = (page.featuredLinks || {}) as unknown as Record
+ const tocLabels = ((context.site as { data?: { ui?: { toc?: Record } } })
+ ?.data?.ui?.toc || {}) as Record
return {
key,
label:
key === 'popular'
- ? req.context.page.featuredLinks[`${key}Heading`] || req.context.site.data.ui.toc[key]
- : req.context.site.data.ui.toc[key],
+ ? pageFeaturedLinks[`${key}Heading`] || tocLabels[key]
+ : tocLabels[key],
viewAllHref:
- key === 'startHere' && !req.context.currentCategory && hasGuidesPage
- ? `${req.context.currentPath}/guides`
+ key === 'startHere' && !context.currentCategory && hasGuidesPage
+ ? `${context.currentPath}/guides`
: '',
- articles: links.map((link: any) => {
+ articles: (links as FeaturedLinkExpanded[]).map((link) => {
return {
href: link.href,
title: link.title,
- intro: link.intro || null,
- authors: link.page?.authors || [],
- fullTitle: link.fullTitle || null,
+ intro: link.intro,
+ authors: (link.page as { authors?: string[] } | undefined)?.authors || [],
+ fullTitle: link.fullTitle,
}
}),
}
diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts
index 04ff3d6f1c8e..1df23bd1acd9 100644
--- a/src/languages/lib/correct-translation-content.ts
+++ b/src/languages/lib/correct-translation-content.ts
@@ -81,6 +81,26 @@ export function correctTranslatedContentStrings(
content = content.replaceAll('{% datos variables', '{% data variables')
content = content.replaceAll('{% de datos variables', '{% data variables')
content = content.replaceAll('{% datos reusables', '{% data reusables')
+ // `{% WORD de datos variables.` — extra Spanish word before "de datos variables"
+ // e.g. `{% uso de datos variables.` ("use of data variables") or
+ // `{% análisis de datos variables.` ("data analysis variables").
+ // Unicode-aware character class so accented translator words match.
+ content = content.replace(
+ /\{%(-?)\s*[\p{L}\p{M}]+\s+de datos (variables|reusables)\./gu,
+ '{%$1 data $2.',
+ )
+ // `{% de datos WORD variables.` — adjective inserted between "de datos" and path
+ // e.g. `{% de datos específico variables.` ("specific data variables")
+ content = content.replace(
+ /\{%(-?)\s*de datos [\p{L}\p{M}]+ (variables|reusables)\./gu,
+ '{%$1 data $2.',
+ )
+ // `{% WORD de variables.` — word + "de variables" (missing "datos" keyword)
+ // e.g. `{% alerta de variables.product.X %}` (alert of variables)
+ content = content.replace(
+ /\{%(-?)\s*[\p{L}\p{M}]+\s+de\s+(variables|reusables)\./gu,
+ '{%$1 data $2.',
+ )
content = content.replaceAll('{% data reutilizables.', '{% data reusables.')
// `{% datos reutilizables.` — fully translated "data reusables" path
content = content.replaceAll('{% datos reutilizables.', '{% data reusables.')
@@ -552,8 +572,11 @@ export function correctTranslatedContentStrings(
// `{% 行标题 %}` — "row headers" = rowheaders
content = content.replaceAll('{% 行标题 %}', '{% rowheaders %}')
content = content.replaceAll('{%- 行标题 %}', '{%- rowheaders %}')
- // `{% 数据变量.` — "data variables" = data variables
+ // `{% 数据变量.` — "data variables" = data variables (with space before)
content = content.replaceAll('{% 数据变量.', '{% data variables.')
+ // `{%数据变量.` — same but no space between `{%` and 数据变量 (e.g. `{%数据变量.enterprise.management_console%}`)
+ content = content.replaceAll('{%数据变量.', '{% data variables.')
+ content = content.replaceAll('{%-数据变量.', '{%- data variables.')
// `{% Windows 操作系统 %}` — "Windows OS" = windows platform tag
content = content.replaceAll('{% Windows 操作系统 %}', '{% windows %}')
content = content.replaceAll('{%- Windows 操作系统 %}', '{%- windows %}')
@@ -610,6 +633,9 @@ export function correctTranslatedContentStrings(
if (context.code === 'ru') {
content = content.replaceAll('[«AUTOTITLE»](', '[AUTOTITLE](')
content = content.replaceAll('[АВТОЗАГОЛОВОК](', '[AUTOTITLE](')
+ // `[{% autoTITLE](url)` — Liquid-embedded lowercase autotitle (translator lowercased
+ // the link anchor and wrapped it in Liquid tag syntax instead of plain `[AUTOTITLE](url)`)
+ content = content.replaceAll('[{% autoTITLE](', '[AUTOTITLE](')
content = content.replaceAll('{% данных variables', '{% data variables')
content = content.replaceAll('{% данных, variables', '{% data variables')
content = content.replaceAll('{% данными variables', '{% data variables')
@@ -1122,6 +1148,10 @@ export function correctTranslatedContentStrings(
content = content.replaceAll('{%- Datenvariablen.', '{%- data variables.')
content = content.replaceAll('{%-Daten variables', '{%- data variables')
content = content.replaceAll('{%-Daten-variables', '{%- data variables')
+ // `{%-DatenXxx variables` — compound "Daten..." word immediately after `{%-` (no space)
+ // e.g. `{%-Datenpaket variables.`, `{%-Dateninstanz variables.`, `{%-Dateneinstellungen variables.`
+ // The existing `{%- DatenXxx variables` rules (with space) don't catch the no-space variant.
+ content = content.replace(/\{%-(Daten[A-Za-z]+)\s+(variables|reusables)/g, '{%- data $2')
content = content.replaceAll('{%- ifversion fpt oder ghec %}', '{%- ifversion fpt or ghec %}')
content = content.replaceAll('{% ifversion fpt oder ghec %}', '{% ifversion fpt or ghec %}')
// Catch remaining "oder" between any plan names in ifversion/elsif/if tags
@@ -1138,6 +1168,15 @@ export function correctTranslatedContentStrings(
content = content.replaceAll('{% Tipp %}', '{% tip %}')
content = content.replaceAll('{%- Tipp %}', '{%- tip %}')
content = content.replaceAll('{%- Tipp -%}', '{%- tip -%}')
+ // `{% Codespaces %}` — translator capitalized the platform tag
+ content = content.replaceAll('{% Codespaces %}', '{% codespaces %}')
+ content = content.replaceAll('{%- Codespaces %}', '{%- codespaces %}')
+ // `{% Aufforderung %}` — German "Aufforderung" (prompt/instruction) = prompt
+ content = content.replaceAll('{% Aufforderung %}', '{% prompt %}')
+ content = content.replaceAll('{%- Aufforderung %}', '{%- prompt %}')
+ // `{% Endprompt %}` — mix of German "End" and English "prompt" = endprompt
+ content = content.replaceAll('{% Endprompt %}', '{% endprompt %}')
+ content = content.replaceAll('{%- Endprompt %}', '{%- endprompt %}')
// Translated for-loop keywords: `für VARNAME in COLLECTION`
content = content.replace(/\{%-? für (\w+) in /g, (match) => {
return match.replace('für', 'for')
diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts
index fb657cea1273..3afdf2bcf042 100644
--- a/src/languages/tests/correct-translation-content.ts
+++ b/src/languages/tests/correct-translation-content.ts
@@ -33,6 +33,30 @@ describe('correctTranslatedContentStrings', () => {
expect(fix('{% data reutilizables.foo.bar %}', 'es')).toBe('{% data reusables.foo.bar %}')
})
+ test('fixes extra Spanish word inserted around "de datos" and "de variables"', () => {
+ // `{% WORD de datos variables.` — leading translator word
+ expect(fix('{% uso de datos variables.product.github %}', 'es')).toBe(
+ '{% data variables.product.github %}',
+ )
+ // Unicode-aware: accented words must also match
+ expect(fix('{% análisis de datos variables.product.github %}', 'es')).toBe(
+ '{% data variables.product.github %}',
+ )
+ expect(fix('{%- uso de datos reusables.foo.bar %}', 'es')).toBe(
+ '{%- data reusables.foo.bar %}',
+ )
+
+ // `{% de datos WORD variables.` — adjective inserted after "de datos"
+ expect(fix('{% de datos específico variables.product.github %}', 'es')).toBe(
+ '{% data variables.product.github %}',
+ )
+
+ // `{% WORD de variables.` — missing "datos" keyword
+ expect(fix('{% alerta de variables.product.github %}', 'es')).toBe(
+ '{% data variables.product.github %}',
+ )
+ })
+
test('fixes translated comment keyword', () => {
expect(fix('{% comentario %}', 'es')).toBe('{% comment %}')
expect(fix('{%- comentario %}', 'es')).toBe('{%- comment %}')
@@ -502,6 +526,15 @@ describe('correctTranslatedContentStrings', () => {
test('fixes 数据变量 → data variables', () => {
expect(fix('{% 数据变量.product.github %}', 'zh')).toBe('{% data variables.product.github %}')
})
+
+ test('fixes 数据变量 with no leading space (`{%数据变量.`)', () => {
+ expect(fix('{%数据变量.enterprise.management_console%}', 'zh')).toBe(
+ '{% data variables.enterprise.management_console%}',
+ )
+ expect(fix('{%-数据变量.product.github %}', 'zh')).toBe(
+ '{%- data variables.product.github %}',
+ )
+ })
})
// ─── RUSSIAN (ru) ──────────────────────────────────────────────────
@@ -515,6 +548,10 @@ describe('correctTranslatedContentStrings', () => {
expect(fix('[АВТОЗАГОЛОВОК](/path/to/article)', 'ru')).toBe('[AUTOTITLE](/path/to/article)')
})
+ test('fixes Liquid-embedded lowercase autotitle anchor (`[{% autoTITLE](`)', () => {
+ expect(fix('[{% autoTITLE](/path/to/article)', 'ru')).toBe('[AUTOTITLE](/path/to/article)')
+ })
+
test('fixes translated data tag variants', () => {
expect(fix('{% данных variables.product.github %}', 'ru')).toBe(
'{% data variables.product.github %}',
@@ -983,6 +1020,27 @@ describe('correctTranslatedContentStrings', () => {
expect(fix('{%- Tipp -%}', 'de')).toBe('{%- tip -%}')
})
+ test('fixes capitalized Codespaces platform tag', () => {
+ expect(fix('{% Codespaces %}', 'de')).toBe('{% codespaces %}')
+ expect(fix('{%- Codespaces %}', 'de')).toBe('{%- codespaces %}')
+ })
+
+ test('fixes translated prompt/endprompt keywords', () => {
+ expect(fix('{% Aufforderung %}', 'de')).toBe('{% prompt %}')
+ expect(fix('{%- Aufforderung %}', 'de')).toBe('{%- prompt %}')
+ expect(fix('{% Endprompt %}', 'de')).toBe('{% endprompt %}')
+ expect(fix('{%- Endprompt %}', 'de')).toBe('{%- endprompt %}')
+ })
+
+ test('fixes `{%-DatenXxx variables` no-space compound German "Daten" tags', () => {
+ expect(fix('{%-Datenpaket variables.product.github %}', 'de')).toBe(
+ '{%- data variables.product.github %}',
+ )
+ expect(fix('{%-Dateneinstellungen reusables.foo.bar %}', 'de')).toBe(
+ '{%- data reusables.foo.bar %}',
+ )
+ })
+
test('fixes für → for in for-loops', () => {
expect(fix('{%- für version in tables.copilot.ides -%}', 'de')).toBe(
'{%- for version in tables.copilot.ides -%}',
diff --git a/src/redirects/middleware/handle-redirects.ts b/src/redirects/middleware/handle-redirects.ts
index c5f6b485ab98..c92bdec51c3f 100644
--- a/src/redirects/middleware/handle-redirects.ts
+++ b/src/redirects/middleware/handle-redirects.ts
@@ -107,8 +107,12 @@ export default function handleRedirects(req: ExtendedRequest, res: Response, nex
// But for example, a `/authentication/connecting-to-github-with-ssh`
// needs to become `/en/authentication/connecting-to-github-with-ssh`
const possibleRedirectTo = `/en${req.path}`
+ // Pages are keyed without .md, so strip it before lookup
+ const lookupPath = possibleRedirectTo.endsWith('.md')
+ ? possibleRedirectTo.replace(/\.md$/, '')
+ : possibleRedirectTo
if (!req.context.pages) throw new Error('req.context.pages not yet set')
- if (possibleRedirectTo in req.context.pages || isDeprecatedVersion(req.path)) {
+ if (lookupPath in req.context.pages || isDeprecatedVersion(req.path)) {
const language = getLanguage(req)
// Note, it's important to use `req.url` here and not `req.path`
@@ -127,7 +131,10 @@ export default function handleRedirects(req: ExtendedRequest, res: Response, nex
// do not redirect if the redirected page can't be found
if (
- !(req.context.pages[removeQueryParams(redirect)] || isDeprecatedVersion(req.path)) &&
+ !(
+ req.context.pages[removeQueryParams(redirect).replace(/\.md$/, '')] ||
+ isDeprecatedVersion(req.path)
+ ) &&
!redirect.includes('://')
) {
// display error on the page in development, but not in production
diff --git a/src/rest/lib/index.ts b/src/rest/lib/index.ts
index 2bba026573ea..89a652df4bd3 100644
--- a/src/rest/lib/index.ts
+++ b/src/rest/lib/index.ts
@@ -4,7 +4,7 @@ import path from 'path'
import QuickLRU from 'quick-lru'
import { brotliDecompress } from 'zlib'
import { promisify } from 'util'
-import { getAutomatedPageMiniTocItems } from '@/frame/lib/get-mini-toc-items'
+import { getAutomatedPageMiniTocItems, type MiniTocItem } from '@/frame/lib/get-mini-toc-items'
import { allVersions, getOpenApiVersion } from '@/versions/lib/all-versions'
import languages from '@/languages/lib/languages-server'
import type { Context } from '@/types'
@@ -19,7 +19,7 @@ export interface RestOperationCategory {
}
interface RestMiniTocData {
- restOperationsMiniTocItems: any[]
+ restOperationsMiniTocItems: MiniTocItem[]
}
/*
diff --git a/src/search/components/input/SearchBarButton.tsx b/src/search/components/input/SearchBarButton.tsx
index 528b5a0f1e11..804b9806c11d 100644
--- a/src/search/components/input/SearchBarButton.tsx
+++ b/src/search/components/input/SearchBarButton.tsx
@@ -12,7 +12,7 @@ type Props = {
isSearchOpen: boolean
setIsSearchOpen: (value: boolean) => void
params: QueryParams
- searchButtonRef: React.RefObject
+ searchButtonRef: React.RefObject
instanceId?: string
}
diff --git a/src/search/components/input/SearchOverlay.tsx b/src/search/components/input/SearchOverlay.tsx
index a1e42fa5aa87..c96fb2424f74 100644
--- a/src/search/components/input/SearchOverlay.tsx
+++ b/src/search/components/input/SearchOverlay.tsx
@@ -34,7 +34,7 @@ import styles from './SearchOverlay.module.scss'
type Props = {
searchOverlayOpen: boolean
- parentRef: RefObject
+ parentRef: RefObject
debug: boolean
onClose: () => void
params: {
diff --git a/src/search/components/input/SearchOverlayContainer.tsx b/src/search/components/input/SearchOverlayContainer.tsx
index cd504ca62b6f..84141391658c 100644
--- a/src/search/components/input/SearchOverlayContainer.tsx
+++ b/src/search/components/input/SearchOverlayContainer.tsx
@@ -7,7 +7,7 @@ type Props = {
setIsSearchOpen: (value: boolean) => void
params: QueryParams
updateParams: (updates: Partial) => void
- searchButtonRef: React.RefObject
+ searchButtonRef: React.RefObject
}
export function SearchOverlayContainer({
diff --git a/src/tools/components/InArticlePicker.tsx b/src/tools/components/InArticlePicker.tsx
index e60edd961a6d..ddc942e02d9b 100644
--- a/src/tools/components/InArticlePicker.tsx
+++ b/src/tools/components/InArticlePicker.tsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from 'react'
+import React, { useEffect, useLayoutEffect, useState } from 'react'
import Cookies from '@/frame/components/lib/cookies'
import { UnderlineNav } from '@primer/react'
import { sendEvent } from '@/events/components/events'
@@ -7,6 +7,8 @@ import { useRouter } from 'next/router'
import styles from './InArticlePicker.module.scss'
+const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
+
type Option = {
value: string
label: string
@@ -63,7 +65,11 @@ export const InArticlePicker = ({
const [asPathRoot, asPathQuery = ''] = router.asPath.split('#')[0].split('?')
- useEffect(() => {
+ // Use a layout effect so the DOM mutation (hiding non-matching .ghd-tool
+ // content) happens before the browser paints. With React 19's stricter
+ // effect timing, a regular useEffect could leave non-matching content
+ // visible on initial page load until after first paint.
+ useIsomorphicLayoutEffect(() => {
// This will make the hook run this callback on mount and on change.
// That's important because even though the user hasn't interacted
// and made an overriding choice, we still want to run this callback
diff --git a/src/types/markdownlint-rule-search-replace.d.ts b/src/types/markdownlint-rule-search-replace.d.ts
index 95ea43d99689..829b77306f82 100644
--- a/src/types/markdownlint-rule-search-replace.d.ts
+++ b/src/types/markdownlint-rule-search-replace.d.ts
@@ -1,11 +1,11 @@
declare module 'markdownlint-rule-search-replace' {
+ import type { RuleParams, RuleErrorCallback } from '@/content-linter/types'
+
const searchReplace: {
names: string[]
description: string
tags: string[]
- // Using any because this is a third-party library without proper TypeScript definitions
- // params contains markdownlint-specific data structures, onError is a callback function
- function: (params: any, onError: any) => void
+ function: (params: RuleParams, onError: RuleErrorCallback) => void
}
export default searchReplace
diff --git a/src/types/primer__octicons.d.ts b/src/types/primer__octicons.d.ts
index 0ba8c8041b82..0ab576361b89 100644
--- a/src/types/primer__octicons.d.ts
+++ b/src/types/primer__octicons.d.ts
@@ -6,7 +6,7 @@ declare module '@primer/octicons' {
'aria-hidden'?: string | boolean
class?: string
fill?: string
- [key: string]: any
+ [key: string]: string | number | boolean | undefined
}
interface Octicon {
diff --git a/src/webhooks/lib/index.ts b/src/webhooks/lib/index.ts
index dae7fd331078..9868ab4287a0 100644
--- a/src/webhooks/lib/index.ts
+++ b/src/webhooks/lib/index.ts
@@ -77,17 +77,22 @@ export async function getInitialPageWebhooks(version: string): Promise ({
+ ...bodyParam,
+ ...(bodyParam.childParamsGroups ? { childParamsGroups: [] } : {}),
+ })),
}
}
- initialWebhooks.push({ ...initialWebhook })
+ initialWebhooks.push(initialWebhook)
}
initialWebhooksCache.set(version, initialWebhooks)
return initialWebhooks
diff --git a/src/webhooks/lib/tests/index.ts b/src/webhooks/lib/tests/index.ts
new file mode 100644
index 000000000000..182d2043271d
--- /dev/null
+++ b/src/webhooks/lib/tests/index.ts
@@ -0,0 +1,59 @@
+import { describe, expect, it } from 'vitest'
+
+import { getInitialPageWebhooks, getWebhook } from '../index'
+
+// Use a version that's guaranteed to exist in the data directory.
+const VERSION = 'free-pro-team@latest'
+
+// Pick a webhook category whose data file has body parameters with
+// non-empty childParamsGroups so we can verify they survive the
+// initial-page stripping.
+const CATEGORY = 'projects_v2_item'
+
+describe('getInitialPageWebhooks does not corrupt the getWebhook cache', () => {
+ it('strips childParamsGroups from the initial-page data', async () => {
+ const initial = await getInitialPageWebhooks(VERSION)
+ const initialWebhook = initial.find((w) => w.name === CATEGORY)
+ expect(initialWebhook).toBeDefined()
+
+ for (const bp of initialWebhook!.data.bodyParameters ?? []) {
+ if (bp.childParamsGroups) {
+ expect(bp.childParamsGroups, `initial ${bp.name} should be stripped`).toHaveLength(0)
+ }
+ }
+ })
+
+ it('preserves childParamsGroups in the getWebhook cache after getInitialPageWebhooks runs', async () => {
+ // Seed the cache and record original childParamsGroups lengths.
+ const before = await getWebhook(VERSION, CATEGORY)
+ expect(before).toBeDefined()
+
+ const originalLengths: Record = {}
+ for (const [action, actionData] of Object.entries(before!)) {
+ for (const bp of actionData.bodyParameters ?? []) {
+ if (bp.childParamsGroups && bp.childParamsGroups.length > 0) {
+ originalLengths[`${action}.${bp.name}`] = bp.childParamsGroups.length
+ }
+ }
+ }
+ expect(Object.keys(originalLengths).length).toBeGreaterThan(0)
+
+ // This intentionally empties childParamsGroups for the initial page render.
+ await getInitialPageWebhooks(VERSION)
+
+ // getWebhook returns cached data — it must NOT have been mutated.
+ const after = await getWebhook(VERSION, CATEGORY)
+ expect(after).toBeDefined()
+
+ for (const [key, expectedLen] of Object.entries(originalLengths)) {
+ const [action, paramName] = key.split('.')
+ const bp = after![action]?.bodyParameters?.find(
+ (p: { name?: string }) => p.name === paramName,
+ )
+ expect(
+ bp?.childParamsGroups?.length,
+ `${key}.childParamsGroups should still have ${expectedLen} entries`,
+ ).toBe(expectedLen)
+ }
+ })
+})
diff --git a/src/workflows/tests/actions-workflows.ts b/src/workflows/tests/actions-workflows.ts
index e3d6d514f857..fd06ea4422a2 100644
--- a/src/workflows/tests/actions-workflows.ts
+++ b/src/workflows/tests/actions-workflows.ts
@@ -68,19 +68,44 @@ const allUsedActions = chain(workflows)
const scheduledWorkflows = workflows.filter(({ data }) => data.on.schedule)
-const alertWorkflows = workflows
- // Only include jobs running on docs-internal
- .filter(({ data }) =>
- Object.values(data.jobs)
- .map((job) => job.if)
- .toString()
- .includes('docs-internal'),
- )
- // Require slack alerts on workflows that aren't actively watched at time of run
- .filter(({ data }) => data.on.schedule || data.on.push || data.on.issues || data.on.issue_comment)
-// Not including
-// - premerge workflows: pull_request, pull_request_target, pull_request_review, merge_group
-// - adhoc workflows: workflow_dispatch, workflow_run, workflow_call, repository_dispatch
+// Triggers where a workflow runs without a human actively watching and
+// therefore needs explicit failure reporting (Slack + issue). Attended
+// triggers (pull_request*, workflow_dispatch, workflow_call, merge_group)
+// are intentionally excluded: the person who triggered the run sees the
+// result directly.
+//
+// `issues` and `issue_comment` are only considered unattended for jobs
+// running in docs-internal itself. When a job is scoped to the public
+// github/docs fork via `if: github.repository == 'github/docs'`, those
+// triggers fire from external reporters/commenters, and the issue or
+// comment itself is the natural failure surface — piling on automated
+// alert-issues there is duplicative and noisy.
+const ALWAYS_UNATTENDED_TRIGGERS = ['schedule', 'workflow_run', 'repository_dispatch', 'push']
+const DOCS_INTERNAL_ONLY_UNATTENDED_TRIGGERS = ['issues', 'issue_comment']
+
+function jobIsPublicDocsScoped(job: WorkflowJob): boolean {
+ return typeof job.if === 'string' && /github\.repository\s*==\s*['"]github\/docs['"]/.test(job.if)
+}
+
+function jobRequiresFailureAlerts(workflow: WorkflowMeta, job: WorkflowJob): boolean {
+ const triggers = workflow.data.on || {}
+ if (ALWAYS_UNATTENDED_TRIGGERS.some((t) => (triggers as Record)[t])) {
+ return true
+ }
+ if (
+ !jobIsPublicDocsScoped(job) &&
+ DOCS_INTERNAL_ONLY_UNATTENDED_TRIGGERS.some((t) => (triggers as Record)[t])
+ ) {
+ return true
+ }
+ return false
+}
+
+// Workflows where at least one job requires failure alerts — used to drive
+// the parameterised tests below. Per-job filtering happens inside each test.
+const alertWorkflows = workflows.filter(({ data }) =>
+ Object.values(data.jobs).some((job) => job.steps),
+)
// to generate list, console.log(new Set(workflows.map(({ data }) => Object.keys(data.on)).flat()))
const dailyWorkflows = scheduledWorkflows.filter(({ data }) =>
@@ -151,23 +176,22 @@ describe('GitHub Actions workflows', () => {
}
})
- test.each(alertWorkflows)(
- 'scheduled workflows slack alert on fail $filename',
- ({ filename, data }) => {
- for (const [name, job] of Object.entries(data.jobs)) {
- if (
- !job.steps.find((step: WorkflowStep) => step.uses === './.github/actions/slack-alert')
- ) {
- throw new Error(`Job ${filename} # ${name} missing slack alert on fail`)
- }
+ test.each(alertWorkflows)('unattended workflows slack alert on fail $filename', (workflow) => {
+ const { filename, data } = workflow
+ for (const [name, job] of Object.entries(data.jobs)) {
+ if (!jobRequiresFailureAlerts(workflow, job)) continue
+ if (!job.steps.find((step: WorkflowStep) => step.uses === './.github/actions/slack-alert')) {
+ throw new Error(`Job ${filename} # ${name} missing slack alert on fail`)
}
- },
- )
+ }
+ })
test.each(alertWorkflows)(
- 'scheduled workflows create failure issue on fail $filename',
- ({ filename, data }) => {
+ 'unattended workflows create failure issue on fail $filename',
+ (workflow) => {
+ const { filename, data } = workflow
for (const [name, job] of Object.entries(data.jobs)) {
+ if (!jobRequiresFailureAlerts(workflow, job)) continue
if (
!job.steps.find(
(step: WorkflowStep) => step.uses === './.github/actions/create-workflow-failure-issue',
@@ -181,8 +205,10 @@ describe('GitHub Actions workflows', () => {
test.each(alertWorkflows)(
'performs a checkout before calling composite action $filename',
- ({ filename, data }) => {
+ (workflow) => {
+ const { filename, data } = workflow
for (const [name, job] of Object.entries(data.jobs)) {
+ if (!jobRequiresFailureAlerts(workflow, job)) continue
if (!job.steps.find((step: WorkflowStep) => checkoutRegexp.test(step.uses || ''))) {
throw new Error(
`Job ${filename} # ${name} missing a checkout before calling the composite action`,