From 97c573e4435eeef54c2c041c71da52cd9611306f Mon Sep 17 00:00:00 2001
From: Sunbrye Ly <56200261+sunbrye@users.noreply.github.com>
Date: Tue, 30 Jun 2026 11:23:09 -0700
Subject: [PATCH 01/21] Tented-model-0044 (#61857)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
content/copilot/reference/ai-models/model-hosting.md | 1 +
.../copilot/reference/ai-models/supported-models.md | 4 +++-
.../reference/copilot-billing/models-and-pricing.md | 2 ++
data/tables/copilot/model-comparison.yml | 5 +++++
data/tables/copilot/model-release-status.yml | 11 ++++++++---
data/tables/copilot/model-supported-clients.yml | 9 +++++++++
data/tables/copilot/model-supported-plans.yml | 7 +++++++
data/tables/copilot/models-and-pricing.yml | 9 +++++++++
data/variables/copilot.yml | 1 +
9 files changed, 45 insertions(+), 4 deletions(-)
diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md
index 8b537ba1b772..c6a859e065e3 100644
--- a/content/copilot/reference/ai-models/model-hosting.md
+++ b/content/copilot/reference/ai-models/model-hosting.md
@@ -50,6 +50,7 @@ Used for:
* {% data variables.copilot.copilot_claude_haiku_45 %}
* {% data variables.copilot.copilot_claude_sonnet_45 %}
* {% data variables.copilot.copilot_claude_sonnet_46 %}
+* {% data variables.copilot.copilot_claude_sonnet_5 %}
* {% data variables.copilot.copilot_claude_opus_45 %}
* {% data variables.copilot.copilot_claude_opus_46 %}
* {% data variables.copilot.copilot_claude_opus_47 %}
diff --git a/content/copilot/reference/ai-models/supported-models.md b/content/copilot/reference/ai-models/supported-models.md
index 470ee888960b..2672b81cbd49 100644
--- a/content/copilot/reference/ai-models/supported-models.md
+++ b/content/copilot/reference/ai-models/supported-models.md
@@ -83,6 +83,7 @@ Choosing a larger context window or higher reasoning will impact {% data variabl
| {% data variables.copilot.copilot_claude_opus_46 %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} |
| {% data variables.copilot.copilot_claude_opus_47 %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} |
| {% data variables.copilot.copilot_claude_opus_48 %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} |
+| {% data variables.copilot.copilot_claude_sonnet_5 %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} |
| {% data variables.copilot.copilot_claude_opus_48_fast %} | {% octicon "x" aria-label="Not supported" %} | {% octicon "check" aria-label="Supported" %} |
| {% data variables.copilot.copilot_claude_fable_5 %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} |
| {% data variables.copilot.copilot_gpt_53_codex %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} |
@@ -141,8 +142,9 @@ Some {% data variables.product.prodname_copilot_short %} models require minimum
| {% data variables.copilot.copilot_gpt_54_mini %} | `v1.104.1` and later | `17.14.19` and later | `1.5.66` and later | `0.47.0` and later | `0.15.0` and later |
| {% data variables.copilot.copilot_gpt_55 %} | `v1.117` and later | `17.14.19` and later | `1.5.66` and later | `0.47.0` and later | `0.15.0` and later |
| {% data variables.copilot.copilot_claude_opus_48 %} | `v1.118` and later | `17.14.6` and later | TBD | TBD | TBD |
+| {% data variables.copilot.copilot_claude_sonnet_5 %} | `v1.124` and later | `17.14.6` and later | TBD | TBD | TBD |
| {% data variables.copilot.copilot_claude_fable_5 %} | `v1.124` and later | `17.14.6` and later | TBD | TBD | TBD |
-| {% data variables.copilot.copilot_mai_code_1_flash %} | `v1.121` and later | Not available | Not available | Not available | Not available |
+| {% data variables.copilot.copilot_mai_code_1_flash %} | `v1.121` and later | TBD | TBD | TBD | TBD |
{% endrowheaders %}
diff --git a/content/copilot/reference/copilot-billing/models-and-pricing.md b/content/copilot/reference/copilot-billing/models-and-pricing.md
index 87b5c0f23e7b..63b94d004f25 100644
--- a/content/copilot/reference/copilot-billing/models-and-pricing.md
+++ b/content/copilot/reference/copilot-billing/models-and-pricing.md
@@ -97,3 +97,5 @@ You can view your current {% data variables.product.prodname_actions %} usage fo
## Model multipliers for annual {% data variables.copilot.copilot_pro_short %} and {% data variables.copilot.copilot_pro_plus_short %} subscribers
{% data variables.copilot.copilot_pro_short %} and {% data variables.copilot.copilot_pro_plus_short %} subscribers on **existing annual billing plans** using the **request-based billing** model have different model multipliers. See [AUTOTITLE](/copilot/reference/copilot-billing/model-multipliers-for-annual-plans).
+
+[^sonnet-5-promo]: {% data variables.copilot.copilot_claude_sonnet_5 %} is available at the promotional pricing of $2.00 per 1M input tokens, $0.20 per 1M cached input tokens, $2.50 per 1M cache write tokens, and $10.00 per 1M output tokens through August 31, 2026.
diff --git a/data/tables/copilot/model-comparison.yml b/data/tables/copilot/model-comparison.yml
index 0ab19e0b16db..109577762406 100644
--- a/data/tables/copilot/model-comparison.yml
+++ b/data/tables/copilot/model-comparison.yml
@@ -68,6 +68,11 @@
excels_at: Complex problem-solving challenges, sophisticated reasoning
further_reading: '[Claude Sonnet 4.6 model card](https://www-cdn.anthropic.com/78073f739564e986ff3e28522761a7a0b4484f84.pdf)'
+- name: Claude Sonnet 5
+ task_area: General-purpose coding and agent tasks
+ excels_at: Complex problem-solving challenges, sophisticated reasoning
+ further_reading: 'Not available'
+
# Google
- name: Gemini 2.5 Pro
task_area: Deep reasoning and debugging
diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml
index b291035bf752..9e05c227c2f7 100644
--- a/data/tables/copilot/model-release-status.yml
+++ b/data/tables/copilot/model-release-status.yml
@@ -40,6 +40,11 @@
release_status: 'GA'
# Anthropic models
+
+- name: 'Claude Fable 5'
+ provider: 'Anthropic'
+ release_status: 'GA'
+
- name: 'Claude Haiku 4.5'
provider: 'Anthropic'
release_status: 'GA'
@@ -64,15 +69,15 @@
provider: 'Anthropic'
release_status: 'GA'
-- name: 'Claude Fable 5'
+- name: 'Claude Sonnet 4.5'
provider: 'Anthropic'
release_status: 'GA'
-- name: 'Claude Sonnet 4.5'
+- name: 'Claude Sonnet 4.6'
provider: 'Anthropic'
release_status: 'GA'
-- name: 'Claude Sonnet 4.6'
+- name: 'Claude Sonnet 5'
provider: 'Anthropic'
release_status: 'GA'
diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml
index f5f9fbe6c10b..0a45d99d394c 100644
--- a/data/tables/copilot/model-supported-clients.yml
+++ b/data/tables/copilot/model-supported-clients.yml
@@ -95,6 +95,15 @@
xcode: false
jetbrains: false
+- name: Claude Sonnet 5
+ dotcom: true
+ cli: true
+ vscode: true
+ vs: true
+ eclipse: false
+ xcode: false
+ jetbrains: false
+
- name: Gemini 2.5 Pro
dotcom: false
cli: false
diff --git a/data/tables/copilot/model-supported-plans.yml b/data/tables/copilot/model-supported-plans.yml
index f3d3e95e7310..dd0c20882c86 100644
--- a/data/tables/copilot/model-supported-plans.yml
+++ b/data/tables/copilot/model-supported-plans.yml
@@ -75,6 +75,13 @@
business: true
enterprise: true
+- name: Claude Sonnet 5
+ pro: true
+ pro_plus: true
+ max: true
+ business: true
+ enterprise: true
+
- name: Gemini 2.5 Pro
pro: true
pro_plus: true
diff --git a/data/tables/copilot/models-and-pricing.yml b/data/tables/copilot/models-and-pricing.yml
index dd6592b523ea..81a37cde4281 100644
--- a/data/tables/copilot/models-and-pricing.yml
+++ b/data/tables/copilot/models-and-pricing.yml
@@ -171,6 +171,15 @@
output: $25.00
cache_write: $6.25
+- model: Claude Sonnet 5[^sonnet-5-promo]
+ provider: anthropic
+ release_status: GA
+ category: Versatile
+ input: $2.00
+ cached_input: $0.20
+ output: $10.00
+ cache_write: $2.50
+
- model: Claude Opus 4.8 (fast mode) (preview)
provider: anthropic
release_status: GA
diff --git a/data/variables/copilot.yml b/data/variables/copilot.yml
index 2e05c6c4ead3..ad3c0c7fbfff 100644
--- a/data/variables/copilot.yml
+++ b/data/variables/copilot.yml
@@ -202,6 +202,7 @@ copilot_claude_sonnet_37: 'Claude Sonnet 3.7'
copilot_claude_sonnet_40: 'Claude Sonnet 4'
copilot_claude_sonnet_45: 'Claude Sonnet 4.5'
copilot_claude_sonnet_46: 'Claude Sonnet 4.6'
+copilot_claude_sonnet_5: 'Claude Sonnet 5'
# Gemini:
copilot_gemini: 'Gemini'
copilot_gemini_flash: 'Gemini 2.0 Flash'
From c5d56d694e3ea07be7aa7e53186e45752af35120 Mon Sep 17 00:00:00 2001
From: Lokesh Gopu
Date: Tue, 30 Jun 2026 14:27:45 -0400
Subject: [PATCH 02/21] Document composite-action and conditional limitations
for background step keywords (#61978)
---
.../workflows-and-actions/workflow-syntax.md | 15 +++++++++++++++
1 file changed, 15 insertions(+)
diff --git a/content/actions/reference/workflows-and-actions/workflow-syntax.md b/content/actions/reference/workflows-and-actions/workflow-syntax.md
index 5ca629d02b49..dc693471414b 100644
--- a/content/actions/reference/workflows-and-actions/workflow-syntax.md
+++ b/content/actions/reference/workflows-and-actions/workflow-syntax.md
@@ -915,6 +915,9 @@ Outputs and environment changes from a background step are only available after
Use `background` when you need fine-grained control: starting a long-running process (like a server or database) that stays up while later steps run, referencing a specific step with [`wait`](#jobsjob_idstepswait) or [`cancel`](#jobsjob_idstepscancel), or interleaving background work with other steps. If you instead have a self-contained group of steps that should all finish before the job continues, [`parallel`](#jobsjob_idstepsparallel) is a more convenient shorthand.
+> [!NOTE]
+> You cannot use `background` on steps inside a composite action. A composite action can itself run as a background step, but it cannot declare background steps internally.
+
### Example: Running a step in the background
```yaml
@@ -937,6 +940,9 @@ Pauses the job until one or more background steps complete. A `wait` step perfor
After a `wait` step completes, the outputs of the referenced background steps become available to subsequent steps. If a referenced background step failed, the `wait` step fails too.
+> [!NOTE]
+> A `wait` step always runs and does not support the [`if`](#jobsjob_idstepsif) conditional.
+
### Example: Waiting for specific background steps
```yaml
@@ -967,6 +973,9 @@ Pauses the job until all active background steps complete. This is useful when s
The `wait-all` keyword takes no arguments.
+> [!NOTE]
+> A `wait-all` step always runs and does not support the [`if`](#jobsjob_idstepsif) conditional.
+
### Example: Waiting for all background steps
```yaml
@@ -992,6 +1001,9 @@ steps:
Gracefully terminates a running background step. The runner sends the step's process a termination signal (`SIGTERM`) so it can clean up, and forcibly stops it (`SIGKILL`) if it does not exit within a short grace period. The `cancel` keyword targets a single background step by its `id`.
+> [!NOTE]
+> A `cancel` step always runs and does not support the [`if`](#jobsjob_idstepsif) conditional.
+
### Example: Canceling a background step
```yaml
@@ -1016,6 +1028,9 @@ Use `parallel` when you have a self-contained group of steps that should all fin
Each step in the group is subject to the same 10-step concurrency limit as other background steps.
+> [!NOTE]
+> You cannot use `parallel` inside a composite action.
+
### Example: Running steps in parallel
```yaml
From 825353c0a4cda7ee88845dab85758139c1874065 Mon Sep 17 00:00:00 2001
From: docs-bot <77750099+docs-bot@users.noreply.github.com>
Date: Tue, 30 Jun 2026 11:41:46 -0700
Subject: [PATCH 03/21] Update OpenAPI Description (#61985)
Co-authored-by: Sunbrye Ly <56200261+sunbrye@users.noreply.github.com>
---
src/github-apps/lib/config.json | 2 +-
src/rest/data/fpt-2022-11-28/billing.json | 11 +++++++---
src/rest/data/fpt-2022-11-28/dependabot.json | 22 +++++++++++++++++++
src/rest/data/fpt-2026-03-10/billing.json | 11 +++++++---
src/rest/data/fpt-2026-03-10/dependabot.json | 22 +++++++++++++++++++
src/rest/data/ghec-2022-11-28/billing.json | 14 +++++++++---
src/rest/data/ghec-2022-11-28/dependabot.json | 22 +++++++++++++++++++
src/rest/data/ghec-2026-03-10/billing.json | 14 +++++++++---
src/rest/data/ghec-2026-03-10/dependabot.json | 22 +++++++++++++++++++
.../data/ghes-3.17-2022-11-28/dependabot.json | 22 +++++++++++++++++++
.../data/ghes-3.18-2022-11-28/dependabot.json | 22 +++++++++++++++++++
.../data/ghes-3.19-2022-11-28/dependabot.json | 22 +++++++++++++++++++
.../data/ghes-3.20-2022-11-28/dependabot.json | 22 +++++++++++++++++++
.../data/ghes-3.21-2022-11-28/dependabot.json | 22 +++++++++++++++++++
.../data/ghes-3.21-2026-03-10/dependabot.json | 22 +++++++++++++++++++
src/rest/lib/config.json | 2 +-
src/webhooks/lib/config.json | 2 +-
17 files changed, 261 insertions(+), 15 deletions(-)
diff --git a/src/github-apps/lib/config.json b/src/github-apps/lib/config.json
index 9702595163a7..a4ff5b8bfa26 100644
--- a/src/github-apps/lib/config.json
+++ b/src/github-apps/lib/config.json
@@ -60,5 +60,5 @@
"2022-11-28"
]
},
- "sha": "3c63b49e41d98449d7b0f7ed12ea40dc9839edd4"
+ "sha": "8d5eb57de9ea64dbcfeca8442934eaecb3817f12"
}
\ No newline at end of file
diff --git a/src/rest/data/fpt-2022-11-28/billing.json b/src/rest/data/fpt-2022-11-28/billing.json
index 4c0dbdedac09..efb16dedc234 100644
--- a/src/rest/data/fpt-2022-11-28/billing.json
+++ b/src/rest/data/fpt-2022-11-28/billing.json
@@ -37,7 +37,7 @@
},
{
"name": "scope",
- "description": "Filter budgets by scope type.
",
+ "description": "Filter budgets by scope type.
\n\norganization: Budgets scoped to the organization. \nrepository: Budgets scoped to a repository. \nmulti_user_customer: Universal budgets that apply to all users in the organization. \nuser: Budgets scoped to an individual user. \n ",
"in": "query",
"schema": {
"type": "string",
@@ -174,6 +174,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -367,6 +368,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -537,7 +539,7 @@
{
"type": "string",
"name": "budget_scope",
- "description": "The scope of the budget for this organization. Use 'organization' for org-level budgets or 'repository' for repo-specific budgets within the organization. user and multi_user_customer scopes are only supported when budget_product_sku is ai_credits or premium_requests.
",
+ "description": "The scope of the budget for this organization.
\n\norganization: Apply the budget to the organization. \nrepository: Apply the budget to a specific repository in the organization. \nmulti_user_customer: Apply a universal budget to all users in the organization. \nuser: Apply the budget to a single user in the organization. \n \nuser and multi_user_customer scopes are only supported when\nbudget_product_sku is ai_credits or premium_requests.
",
"enum": [
"organization",
"repository",
@@ -633,6 +635,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -814,6 +817,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -981,7 +985,7 @@
{
"type": "string",
"name": "budget_scope",
- "description": "The scope of the budget
",
+ "description": "The scope of the budget for this organization.
\n\norganization: Apply the budget to the organization. \nrepository: Apply the budget to a specific repository in the organization. \nmulti_user_customer: Apply a universal budget to all users in the organization. \nuser: Apply the budget to a single user in the organization. \n ",
"enum": [
"enterprise",
"organization",
@@ -1078,6 +1082,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
diff --git a/src/rest/data/fpt-2022-11-28/dependabot.json b/src/rest/data/fpt-2022-11-28/dependabot.json
index e8cd020187fb..e0b44716dab3 100644
--- a/src/rest/data/fpt-2022-11-28/dependabot.json
+++ b/src/rest/data/fpt-2022-11-28/dependabot.json
@@ -6326,6 +6326,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/fpt-2026-03-10/billing.json b/src/rest/data/fpt-2026-03-10/billing.json
index 4c0dbdedac09..efb16dedc234 100644
--- a/src/rest/data/fpt-2026-03-10/billing.json
+++ b/src/rest/data/fpt-2026-03-10/billing.json
@@ -37,7 +37,7 @@
},
{
"name": "scope",
- "description": "Filter budgets by scope type.
",
+ "description": "Filter budgets by scope type.
\n\norganization: Budgets scoped to the organization. \nrepository: Budgets scoped to a repository. \nmulti_user_customer: Universal budgets that apply to all users in the organization. \nuser: Budgets scoped to an individual user. \n ",
"in": "query",
"schema": {
"type": "string",
@@ -174,6 +174,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -367,6 +368,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -537,7 +539,7 @@
{
"type": "string",
"name": "budget_scope",
- "description": "The scope of the budget for this organization. Use 'organization' for org-level budgets or 'repository' for repo-specific budgets within the organization. user and multi_user_customer scopes are only supported when budget_product_sku is ai_credits or premium_requests.
",
+ "description": "The scope of the budget for this organization.
\n\norganization: Apply the budget to the organization. \nrepository: Apply the budget to a specific repository in the organization. \nmulti_user_customer: Apply a universal budget to all users in the organization. \nuser: Apply the budget to a single user in the organization. \n \nuser and multi_user_customer scopes are only supported when\nbudget_product_sku is ai_credits or premium_requests.
",
"enum": [
"organization",
"repository",
@@ -633,6 +635,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -814,6 +817,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -981,7 +985,7 @@
{
"type": "string",
"name": "budget_scope",
- "description": "The scope of the budget
",
+ "description": "The scope of the budget for this organization.
\n\norganization: Apply the budget to the organization. \nrepository: Apply the budget to a specific repository in the organization. \nmulti_user_customer: Apply a universal budget to all users in the organization. \nuser: Apply the budget to a single user in the organization. \n ",
"enum": [
"enterprise",
"organization",
@@ -1078,6 +1082,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
diff --git a/src/rest/data/fpt-2026-03-10/dependabot.json b/src/rest/data/fpt-2026-03-10/dependabot.json
index b416b0d69b9f..51b9f2d46e74 100644
--- a/src/rest/data/fpt-2026-03-10/dependabot.json
+++ b/src/rest/data/fpt-2026-03-10/dependabot.json
@@ -6214,6 +6214,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/ghec-2022-11-28/billing.json b/src/rest/data/ghec-2022-11-28/billing.json
index 43dbc93472f6..45f039f504e8 100644
--- a/src/rest/data/ghec-2022-11-28/billing.json
+++ b/src/rest/data/ghec-2022-11-28/billing.json
@@ -222,7 +222,7 @@
},
{
"name": "scope",
- "description": "Filter budgets by scope type.
",
+ "description": "Filter budgets by scope type.
\n\nenterprise: Budgets that apply to the entire enterprise. \norganization: Budgets scoped to an organization in the enterprise. \nrepository: Budgets scoped to a repository. \ncost_center: Budgets scoped to a cost center. \nmulti_user_customer: Universal budgets that apply to all users in the enterprise. \nmulti_user_cost_center: Universal budgets that apply to all users in a cost center. \nuser: Budgets scoped to an individual user. \n ",
"in": "query",
"schema": {
"type": "string",
@@ -232,6 +232,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
}
@@ -359,6 +360,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -552,6 +554,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -719,7 +722,7 @@
{
"type": "string",
"name": "budget_scope",
- "description": "The scope of the budget. user and multi_user_customer scopes are only supported when budget_product_sku is ai_credits or premium_requests.
",
+ "description": "The scope of the budget.
\n\nenterprise: Apply the budget to the entire enterprise. \norganization: Apply the budget to a specific organization in the enterprise. \nrepository: Apply the budget to a specific repository. \ncost_center: Apply the budget to a specific cost center. \nmulti_user_customer: Apply a universal budget to all users in the enterprise. \nmulti_user_cost_center: Apply a universal budget to all users in a cost center. \nuser: Apply the budget to a single user. \n \nuser, multi_user_customer, and multi_user_cost_center scopes are only supported when budget_product_sku is ai_credits or premium_requests.
",
"isRequired": true,
"enum": [
"enterprise",
@@ -727,6 +730,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -819,6 +823,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -996,6 +1001,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -1159,13 +1165,14 @@
{
"type": "string",
"name": "budget_scope",
- "description": "The scope of the budget
",
+ "description": "The scope of the budget.
\n\nenterprise: Apply the budget to the entire enterprise. \norganization: Apply the budget to a specific organization in the enterprise. \nrepository: Apply the budget to a specific repository. \ncost_center: Apply the budget to a specific cost center. \nmulti_user_customer: Apply a universal budget to all users in the enterprise. \nmulti_user_cost_center: Apply a universal budget to all users in a cost center. \nuser: Apply the budget to a single user. \n ",
"enum": [
"enterprise",
"organization",
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -1256,6 +1263,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
diff --git a/src/rest/data/ghec-2022-11-28/dependabot.json b/src/rest/data/ghec-2022-11-28/dependabot.json
index e4a5a83c43f7..ec10118be026 100644
--- a/src/rest/data/ghec-2022-11-28/dependabot.json
+++ b/src/rest/data/ghec-2022-11-28/dependabot.json
@@ -8032,6 +8032,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/ghec-2026-03-10/billing.json b/src/rest/data/ghec-2026-03-10/billing.json
index 43dbc93472f6..45f039f504e8 100644
--- a/src/rest/data/ghec-2026-03-10/billing.json
+++ b/src/rest/data/ghec-2026-03-10/billing.json
@@ -222,7 +222,7 @@
},
{
"name": "scope",
- "description": "Filter budgets by scope type.
",
+ "description": "Filter budgets by scope type.
\n\nenterprise: Budgets that apply to the entire enterprise. \norganization: Budgets scoped to an organization in the enterprise. \nrepository: Budgets scoped to a repository. \ncost_center: Budgets scoped to a cost center. \nmulti_user_customer: Universal budgets that apply to all users in the enterprise. \nmulti_user_cost_center: Universal budgets that apply to all users in a cost center. \nuser: Budgets scoped to an individual user. \n ",
"in": "query",
"schema": {
"type": "string",
@@ -232,6 +232,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
}
@@ -359,6 +360,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -552,6 +554,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -719,7 +722,7 @@
{
"type": "string",
"name": "budget_scope",
- "description": "The scope of the budget. user and multi_user_customer scopes are only supported when budget_product_sku is ai_credits or premium_requests.
",
+ "description": "The scope of the budget.
\n\nenterprise: Apply the budget to the entire enterprise. \norganization: Apply the budget to a specific organization in the enterprise. \nrepository: Apply the budget to a specific repository. \ncost_center: Apply the budget to a specific cost center. \nmulti_user_customer: Apply a universal budget to all users in the enterprise. \nmulti_user_cost_center: Apply a universal budget to all users in a cost center. \nuser: Apply the budget to a single user. \n \nuser, multi_user_customer, and multi_user_cost_center scopes are only supported when budget_product_sku is ai_credits or premium_requests.
",
"isRequired": true,
"enum": [
"enterprise",
@@ -727,6 +730,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -819,6 +823,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -996,6 +1001,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -1159,13 +1165,14 @@
{
"type": "string",
"name": "budget_scope",
- "description": "The scope of the budget
",
+ "description": "The scope of the budget.
\n\nenterprise: Apply the budget to the entire enterprise. \norganization: Apply the budget to a specific organization in the enterprise. \nrepository: Apply the budget to a specific repository. \ncost_center: Apply the budget to a specific cost center. \nmulti_user_customer: Apply a universal budget to all users in the enterprise. \nmulti_user_cost_center: Apply a universal budget to all users in a cost center. \nuser: Apply the budget to a single user. \n ",
"enum": [
"enterprise",
"organization",
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
@@ -1256,6 +1263,7 @@
"repository",
"cost_center",
"multi_user_customer",
+ "multi_user_cost_center",
"user"
]
},
diff --git a/src/rest/data/ghec-2026-03-10/dependabot.json b/src/rest/data/ghec-2026-03-10/dependabot.json
index f6ddb0deb78f..3b567d3f31e5 100644
--- a/src/rest/data/ghec-2026-03-10/dependabot.json
+++ b/src/rest/data/ghec-2026-03-10/dependabot.json
@@ -7920,6 +7920,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/ghes-3.17-2022-11-28/dependabot.json b/src/rest/data/ghes-3.17-2022-11-28/dependabot.json
index 4aa00f03b3da..b90a5b0b1b41 100644
--- a/src/rest/data/ghes-3.17-2022-11-28/dependabot.json
+++ b/src/rest/data/ghes-3.17-2022-11-28/dependabot.json
@@ -5688,6 +5688,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/ghes-3.18-2022-11-28/dependabot.json b/src/rest/data/ghes-3.18-2022-11-28/dependabot.json
index 4d49a7213412..97bc8c62b6b6 100644
--- a/src/rest/data/ghes-3.18-2022-11-28/dependabot.json
+++ b/src/rest/data/ghes-3.18-2022-11-28/dependabot.json
@@ -5688,6 +5688,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/ghes-3.19-2022-11-28/dependabot.json b/src/rest/data/ghes-3.19-2022-11-28/dependabot.json
index 24c1b6bd710c..daf32d6b83a6 100644
--- a/src/rest/data/ghes-3.19-2022-11-28/dependabot.json
+++ b/src/rest/data/ghes-3.19-2022-11-28/dependabot.json
@@ -6973,6 +6973,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/ghes-3.20-2022-11-28/dependabot.json b/src/rest/data/ghes-3.20-2022-11-28/dependabot.json
index af6dda8c04f9..2636ea8d0a79 100644
--- a/src/rest/data/ghes-3.20-2022-11-28/dependabot.json
+++ b/src/rest/data/ghes-3.20-2022-11-28/dependabot.json
@@ -7328,6 +7328,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/ghes-3.21-2022-11-28/dependabot.json b/src/rest/data/ghes-3.21-2022-11-28/dependabot.json
index 6638ca30ad0e..05d44a144804 100644
--- a/src/rest/data/ghes-3.21-2022-11-28/dependabot.json
+++ b/src/rest/data/ghes-3.21-2022-11-28/dependabot.json
@@ -7972,6 +7972,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/data/ghes-3.21-2026-03-10/dependabot.json b/src/rest/data/ghes-3.21-2026-03-10/dependabot.json
index 88b000a4cc9e..d455b79c370e 100644
--- a/src/rest/data/ghes-3.21-2026-03-10/dependabot.json
+++ b/src/rest/data/ghes-3.21-2026-03-10/dependabot.json
@@ -7860,6 +7860,28 @@
"type": "array of strings",
"name": "assignees",
"description": "Usernames to assign to this Dependabot Alert.\nPass one or more user logins to replace the set of assignees on this alert.\nSend an empty array ([]) to clear all assignees from the alert.\nTo assign an AI agent, include the bot login (for example, copilot-swe-agent[bot]).
"
+ },
+ {
+ "type": "object",
+ "name": "agent_assignment",
+ "description": "Parameters for AI agent assignment. Only used when an agent bot login is\nincluded in assignees. Ignored when no agent is being assigned.
",
+ "childParamsGroups": [
+ {
+ "type": "string",
+ "name": "custom_instructions",
+ "description": "Custom instructions for the agent.
"
+ },
+ {
+ "type": "string",
+ "name": "custom_agent",
+ "description": "A custom agent identifier.
"
+ },
+ {
+ "type": "string",
+ "name": "model",
+ "description": "The model to use for the agent.
"
+ }
+ ]
}
],
"descriptionHTML": "The authenticated user must have access to security alerts for the repository to use this endpoint. For more information, see \"Granting access to security alerts .\"
\nOAuth app tokens and personal access tokens (classic) need the security_events scope to use this endpoint. If this endpoint is only used with public repositories, the token can use the public_repo scope instead.
",
diff --git a/src/rest/lib/config.json b/src/rest/lib/config.json
index 6eaaa278c3fe..193cae090c80 100644
--- a/src/rest/lib/config.json
+++ b/src/rest/lib/config.json
@@ -47,5 +47,5 @@
]
}
},
- "sha": "3c63b49e41d98449d7b0f7ed12ea40dc9839edd4"
+ "sha": "8d5eb57de9ea64dbcfeca8442934eaecb3817f12"
}
\ No newline at end of file
diff --git a/src/webhooks/lib/config.json b/src/webhooks/lib/config.json
index 47bbaa8a9793..e8f6a2d2b73d 100644
--- a/src/webhooks/lib/config.json
+++ b/src/webhooks/lib/config.json
@@ -1,3 +1,3 @@
{
- "sha": "3c63b49e41d98449d7b0f7ed12ea40dc9839edd4"
+ "sha": "8d5eb57de9ea64dbcfeca8442934eaecb3817f12"
}
\ No newline at end of file
From cc45d7373c85e3d443626266821b559463d3afe9 Mon Sep 17 00:00:00 2001
From: Steve S
Date: Tue, 30 Jun 2026 15:01:48 -0400
Subject: [PATCH 04/21] fix(a11y): add role="listitem" to list-style-none
elements (#16815 follow-up) (#61980)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
eslint.config.ts | 12 +++++++----
src/frame/components/GenericError.tsx | 14 ++++++-------
.../components/page-footer/LegalFooter.tsx | 18 +++++++++--------
.../components/ProductSelectionCard.tsx | 2 +-
src/landings/components/TableOfContents.tsx | 20 ++++++++-----------
.../components/GHESReleaseNotes.tsx | 6 +++++-
6 files changed, 39 insertions(+), 33 deletions(-)
diff --git a/eslint.config.ts b/eslint.config.ts
index 0be658ea7314..babfc6ba6a35 100644
--- a/eslint.config.ts
+++ b/eslint.config.ts
@@ -232,9 +232,10 @@ export default [
},
// Allow role="list" on list-style:none elements in these components.
- // Chromium drops the implicit `list` role from the accessibility tree when
- // list-style:none is set, so NVDA/JAWS lose list semantics; role="list" restores
- // them and is not actually redundant here. See github/accessibility-audits#16815.
+ // Chromium drops the implicit `list`/`listitem` roles from the accessibility tree
+ // when list-style:none is set, so NVDA/JAWS lose list semantics and the item count;
+ // role="list" on the and role="listitem" on each restore them and are not
+ // actually redundant here. See github/accessibility-audits#16815.
{
files: [
'src/frame/components/ui/MiniTocs/MiniTocs.tsx',
@@ -245,7 +246,10 @@ export default [
'src/release-notes/components/GHESReleaseNotes.tsx',
],
rules: {
- 'jsx-a11y/no-redundant-roles': ['error', { nav: ['navigation'], ul: ['list'] }],
+ 'jsx-a11y/no-redundant-roles': [
+ 'error',
+ { nav: ['navigation'], ul: ['list'], li: ['listitem'] },
+ ],
},
},
diff --git a/src/frame/components/GenericError.tsx b/src/frame/components/GenericError.tsx
index a365375e4a57..15e3dd3a56c4 100644
--- a/src/frame/components/GenericError.tsx
+++ b/src/frame/components/GenericError.tsx
@@ -68,15 +68,15 @@ export const SimpleFooter = () => {
-
+
© {new Date().getFullYear()} GitHub, Inc.
-
+
Terms
-
+
{
Privacy{' '}
-
+
Status
-
+
Pricing
-
+
Expert services
-
+
Blog
diff --git a/src/frame/components/page-footer/LegalFooter.tsx b/src/frame/components/page-footer/LegalFooter.tsx
index a093b5479d48..d24186dbf561 100644
--- a/src/frame/components/page-footer/LegalFooter.tsx
+++ b/src/frame/components/page-footer/LegalFooter.tsx
@@ -12,10 +12,12 @@ export const LegalFooter = () => {
{t('legal_heading')}
{router.locale !== 'en' && {t('machine')}
}
- © {new Date().getFullYear()} GitHub, Inc.
+
+ © {new Date().getFullYear()} GitHub, Inc.
+
{/* In Germany, Austria, and Switzerland, the Impressum link is legally required. */}
{router.locale === 'de' && (
-
+
{
)}
-
+
{
{t('terms')}
-
+
{/* KO law requires link to privacy statement to be conspicuous */}
{
{t('privacy')}
-
+
{t('status')}
-
+
{t('pricing')}
-
+
{t('expert_services')}
-
+
{t('blog')}
diff --git a/src/landings/components/ProductSelectionCard.tsx b/src/landings/components/ProductSelectionCard.tsx
index ab8653da45eb..0173d6ae7ad4 100644
--- a/src/landings/components/ProductSelectionCard.tsx
+++ b/src/landings/components/ProductSelectionCard.tsx
@@ -80,7 +80,7 @@ export const ProductSelectionCard = ({ group }: ProductSelectionCardProps) => {
{group.children.map((product) => {
return (
-
+
{product.name}
{product.external && (
diff --git a/src/landings/components/TableOfContents.tsx b/src/landings/components/TableOfContents.tsx
index 2ecb62556359..32df313e810d 100644
--- a/src/landings/components/TableOfContents.tsx
+++ b/src/landings/components/TableOfContents.tsx
@@ -40,26 +40,22 @@ export const TableOfContents = (props: Props) => {
const { fullPath, title, childTocItems } = item
const filteredChildren = (childTocItems || []).filter(Boolean)
return (
-
+
{title}
+
+ , {index + 1} of {items.length}
+
{filteredChildren.length > 0 && (
{filteredChildren.map((childItem, childIndex) => (
-
+
{childItem.title}
+
+ , {childIndex + 1} of {filteredChildren.length}
+
))}
diff --git a/src/release-notes/components/GHESReleaseNotes.tsx b/src/release-notes/components/GHESReleaseNotes.tsx
index 4964b7f33b65..45d05594da76 100644
--- a/src/release-notes/components/GHESReleaseNotes.tsx
+++ b/src/release-notes/components/GHESReleaseNotes.tsx
@@ -32,7 +32,11 @@ export function GHESReleaseNotes({ context }: Props) {
{currentRelease.patches.map((patch) => {
return (
-
+
{patch.version}
From c24359f70fbf7b64f8d07c9d7cb6bfc74713ac96 Mon Sep 17 00:00:00 2001
From: "release-controller[bot]"
<110195724+release-controller[bot]@users.noreply.github.com>
Date: Tue, 30 Jun 2026 12:52:51 -0700
Subject: [PATCH 05/21] Patch release notes for GitHub Enterprise Server
(#61874)
Co-authored-by: Release-Controller
Co-authored-by: felix
Co-authored-by: Isaac Brown <101839405+isaacmbrown@users.noreply.github.com>
---
.../migrations/elm/about-live-migrations.md | 3 +-
content/migrations/elm/elm-cli-reference.md | 2 +-
.../migrations/elm/migrate-your-repository.md | 16 +--
.../elm/prepare-for-your-migration.md | 20 ++--
content/migrations/elm/troubleshooting.md | 4 +-
.../overview/about-locked-repositories.md | 4 +-
.../enterprise-server/3-17/17.yml | 90 ++++++++++++++++
.../enterprise-server/3-18/11.yml | 96 +++++++++++++++++
.../enterprise-server/3-19/8.yml | 100 ++++++++++++++++++
.../enterprise-server/3-20/4.yml | 92 ++++++++++++++++
.../enterprise-server/3-21/2.yml | 90 ++++++++++++++++
data/reusables/elm/locked-repo.md | 6 +-
12 files changed, 492 insertions(+), 31 deletions(-)
create mode 100644 data/release-notes/enterprise-server/3-17/17.yml
create mode 100644 data/release-notes/enterprise-server/3-18/11.yml
create mode 100644 data/release-notes/enterprise-server/3-19/8.yml
create mode 100644 data/release-notes/enterprise-server/3-20/4.yml
create mode 100644 data/release-notes/enterprise-server/3-21/2.yml
diff --git a/content/migrations/elm/about-live-migrations.md b/content/migrations/elm/about-live-migrations.md
index 4c149e55dbbf..906316c44faa 100644
--- a/content/migrations/elm/about-live-migrations.md
+++ b/content/migrations/elm/about-live-migrations.md
@@ -7,7 +7,6 @@ versions:
ghes: '*'
ghec: '*'
contentType: concepts
-product: '{% data reusables.elm.ghes-version-requirement %}'
---
{% data reusables.elm.preview-note %}
@@ -43,7 +42,7 @@ The high-level phases of a migration are:
1. **Creation**: The site admin runs CLI commands to create and start the migration, specifying the source repository and destination.
1. **Preflight checks**: The migration service verifies parameters, tokens, network connectivity, and repository configuration.
1. **Backfill**: The {% data variables.product.prodname_elm_short %} tool does an initial crawl to capture all repository data and sends it to the migration service on the destination platform. During the backfill phase, webhooks check for live updates to the repository as the migration continues.
-1. **Cutover**: The source repository is locked and any final live updates are sent to {% data variables.product.prodname_elm_short %}. This is the downtime period for developers.
+1. **Cutover**: The source repository is archived (made read-only) and any final live updates are sent to {% data variables.product.prodname_elm_short %}. This is the downtime period for developers.
1. **Completion**: The migration is finished. The site admin can check the data was migrated successfully.
1. **Follow-up**: An organization owner performs follow-up tasks on the destination enterprise, such as reconfiguring organization settings and reattributing activity to users.
diff --git a/content/migrations/elm/elm-cli-reference.md b/content/migrations/elm/elm-cli-reference.md
index b74b873f85b6..fa2ab60bc75f 100644
--- a/content/migrations/elm/elm-cli-reference.md
+++ b/content/migrations/elm/elm-cli-reference.md
@@ -20,7 +20,7 @@ contentType: reference
| `elm migration status --migration-id MIGRATION-ID` | Shows the status, progress, cutover readiness, and timing of a migration |
| `elm migration list` | Lists all migrations and their statuses |
| `elm migration cancel --migration-id MIGRATION-ID` | Cancels a migration in progress |
-| `elm migration cutover-to-destination --migration-id MIGRATION-ID` | Initiates the final cutover, locking the source repository and completing the migration |
+| `elm migration cutover-to-destination --migration-id MIGRATION-ID` | Initiates the final cutover, archiving the source repository and completing the migration |
Some of these commands can take additional options. See the later sections in this article.
diff --git a/content/migrations/elm/migrate-your-repository.md b/content/migrations/elm/migrate-your-repository.md
index a9441a56bbe8..10ba3bf63e90 100644
--- a/content/migrations/elm/migrate-your-repository.md
+++ b/content/migrations/elm/migrate-your-repository.md
@@ -7,7 +7,6 @@ versions:
ghes: '*'
ghec: '*'
contentType: how-tos
-product: '{% data reusables.elm.ghes-version-requirement %}'
permissions: 'Site administrators on {% data variables.product.prodname_ghe_server %} who are also enterprise owners on {% data variables.enterprise.data_residency_site %}.'
---
@@ -60,18 +59,7 @@ You must set some configuration on the {% data variables.product.prodname_ghe_se
| `secrets.elm-exporter.migration-target-token` | The access token you created for {% data variables.enterprise.data_residency_site %}. |
| `secrets.elm-exporter.source-token` | The access token you created for {% data variables.product.prodname_ghe_server %}. |
| `secrets.elm-exporter.source-user` | The username associated with the {% data variables.product.prodname_ghe_server %} token (for example: `ghe-admin`). |
-
-1. If you **don't** already have migrations enabled and blob storage configured on the instance, you can configure them now. You can check your existing settings in in "Migrations" section of the Management Console (`HOSTNAME/setup/settings`).
-
- You can use the following default values, which will not introduce any unexpected functionality.
-
- ```shell copy
- ghe-config app.migrations.enabled true
- ```
-
- ```shell copy
- ghe-config secrets.migrations.blob-storage-type local-storage
- ```
+ | `app.migrations.enabled` | If you don't already have migrations enabled on the instance, you must set this to `true`. |
1. Apply the configuration.
@@ -194,7 +182,7 @@ Tips:
## 7. Complete the migration
-When a migration is ready for cutover, you can complete the migration. The cutover process will lock the source repository, making it **permanently unavailable** for developers unless an administrator unlocks it.
+When a migration is ready for cutover, you can complete the migration. The cutover process will archive the source repository, making it **permanently read-only** unless a repository administrator unarchives it.
``` shell copy
elm migration cutover-to-destination --migration-id $MIGRATION_ID
diff --git a/content/migrations/elm/prepare-for-your-migration.md b/content/migrations/elm/prepare-for-your-migration.md
index 86d4eb38e42b..afafef08806d 100644
--- a/content/migrations/elm/prepare-for-your-migration.md
+++ b/content/migrations/elm/prepare-for-your-migration.md
@@ -13,18 +13,22 @@ contentType: concepts
## Is our {% data variables.product.prodname_ghe_server %} instance ready?
-{% data variables.product.prodname_elm_short %} has been backported to supported releases. To use it, you must upgrade to one of the following minor versions or later:
+{% data variables.product.prodname_elm_short %} is available in the latest patch releases for {% data variables.product.prodname_ghe_server %} 3.17 and later. This documentation assumes you are using the following patch version or later. The instructions may not work on earlier versions.
-* `3.21.0`
-* `3.20.2`
-* `3.19.6`
-* `3.18.9`
-* `3.17.15`
+
+
+* `3.21.2`
+* `3.20.4`
+* `3.19.8`
+* `3.18.11`
+* `3.17.17`
Your {% data variables.product.prodname_ghe_server %} instance must also:
* Use an **HTTPS** URL. HTTP URLs are not supported.
-* Have migrations enabled and blob storage configured. You can check these settings in the "Migrations" section of the Management Console. If you don't already have these settings configured, we will explain how to set them to default values in [AUTOTITLE](/migrations/elm/migrate-your-repository).
+* Allow outbound traffic to the destination of the migration.
+* Have migrations enabled in the "Migrations" section of the Management Console.
+* Be prepared for some additional load during the migration: repository archiving causes all issues and pull requests in the repository to be pulled from MySQL and reindexed in Elasticsearch.
## What will our destination organization look like?
@@ -65,7 +69,7 @@ After the migration, someone will need to perform some follow-up tasks on {% dat
Before you start, communicate with developers that:
-* The repository is moving to a new location. Users can continue to use the source repository during the migration until the operator begins the final cutover to the new location.
+* The repository is moving to a new location. Users can continue to use the source repository during the migration until the operator begins the final cutover to the new location. After cutover, the source repository will be archived, so it will be read-only unless it is unarchived by a repository administrator.
* While the migration is in progress, developers should avoid force pushes to the repository, because these will disrupt the Git history in a way that {% data variables.product.prodname_elm_short %} cannot resolve.
* Certain actions that developers perform during the migration process may not be reflected in the migrated repository. For details, see the unsupported actions in [AUTOTITLE](/migrations/elm/migrated-data-reference#events-included-in-live-updates).
diff --git a/content/migrations/elm/troubleshooting.md b/content/migrations/elm/troubleshooting.md
index 6448dd0cfadd..36d1afba4fad 100644
--- a/content/migrations/elm/troubleshooting.md
+++ b/content/migrations/elm/troubleshooting.md
@@ -22,7 +22,7 @@ If your migration encounters a problem, check the migration status with `elm mig
| **Exporting** | Data is being exported from the source | Monitor with `elm migration status` |
| **Processing** | Exported data is being imported to the destination | Monitor with `elm migration status` |
| **Ready for cutover** | The initial migration is complete and the migration is ready for cutover | When ready, run `elm migration cutover-to-destination` |
-| **Cutting over** | The source repository is locked and remaining changes are being applied to the destination | Monitor; the status will transition to **Completed** |
+| **Cutting over** | The source repository is archived and remaining changes are being applied to the destination | Monitor; the status will transition to **Completed** |
| **Completed** | The migration has finished successfully | Verify the destination repository and reclaim mannequins |
| **Failed** | The migration encountered an unrecoverable failure | Investigate the error (see below) |
| **Paused** | The migration is paused | Check the pause reason and resolve (see below) |
@@ -95,7 +95,7 @@ Failed resources are only shown after all automatic retries have been exhausted,
If the number and types of failed resources are acceptable, you can proceed with cutover. If not, abort the migration, resolve the underlying issue, then start a new migration.
-## Cutover failed and the source repository is locked
+## Cutover failed and the source repository is unavailable
{% data reusables.elm.locked-repo %}
diff --git a/content/migrations/overview/about-locked-repositories.md b/content/migrations/overview/about-locked-repositories.md
index b964b610d8dc..00e84121c0e1 100644
--- a/content/migrations/overview/about-locked-repositories.md
+++ b/content/migrations/overview/about-locked-repositories.md
@@ -35,7 +35,9 @@ While a migration is in progress, access to the destination repository is locked
For information about how to unlock repositories that were locked by {% data variables.product.prodname_importer_proper_name %}, see [AUTOTITLE](/migrations/using-github-enterprise-importer/completing-your-migration-with-github-enterprise-importer/troubleshooting-your-migration-with-github-enterprise-importer#locked-repositories).
-## Repositories locked by {% data variables.product.prodname_elm %}
+## Repositories archived by {% data variables.product.prodname_elm %}
+
+In the latest {% data variables.product.prodname_ghe_server %} releases, {% data variables.product.prodname_elm %} archives, rather than locks, the source repository. This keeps the repository available for read operations.
{% data reusables.elm.locked-repo %}
diff --git a/data/release-notes/enterprise-server/3-17/17.yml b/data/release-notes/enterprise-server/3-17/17.yml
new file mode 100644
index 000000000000..2d928414e322
--- /dev/null
+++ b/data/release-notes/enterprise-server/3-17/17.yml
@@ -0,0 +1,90 @@
+date: '2026-06-30'
+sections:
+ security_fixes:
+ - |
+ **HIGH**: Current configurations of the GitHub API access controls could allow an attacker to create issues in any public repository via a u2s token without requiring the underlying installation to have issues write permission. This therefore allows an attacker to impersonate the victim in public repositories by creating issues and commit comments.
+ - |
+ **MEDIUM**: An attacker with site administrator privileges could extract arbitrary data from the instance's database, including user password hashes, by exploiting a blind SQL injection vulnerability in the `dependenciesPrefers` argument of the `dependencyGraphManifests` GraphQL field. This vulnerability affected instances with the dependency graph enabled and was reported via the GitHub Bug Bounty program.
+ - |
+ **MEDIUM**: An attacker could gain unintended access to an organization's runner management by directing a user to authorize an OAuth app whose requested manage_runners:org scope was not displayed on the authorization consent screen. GitHub has requested CVE ID [CVE-2026-9106](https://www.cve.org/cverecord?id=CVE-2026-9106) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ **MEDIUM**: An authenticated GHES user could read source code from private repositories they did not have access to by supplying a cross-repository comparison range to the Copilot pull request description diff summary endpoint, which did not verify the user's permission to view the target repository. GitHub has requested CVE ID [CVE-2026-9132](https://www.cve.org/cverecord?id=CVE-2026-9132) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/).
+ - |
+ **MEDIUM**: An attacker could execute arbitrary JavaScript in a victim's browser on a GitHub Enterprise Server instance by creating a discussion in the Q\&A category with a crafted title that breaks out of the JSON-LD structured-data script block when a comment is marked as the answer. To mitigate this issue, GitHub has updated the rendering of JSON-LD structured data in discussions to properly escape user-controlled input. GitHub has requested CVE ID [CVE-2026-10585](https://www.cve.org/cverecord?id=CVE-2026-10585) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ GitHub has updated `dnsmasq` to fix the following vulnerabilities: CVE-2026-4891, CVE-2026-4890, CVE-2026-4892, CVE-2026-4893, CVE-2026-5172, CVE-2026-2291.
+ - |
+ Packages have been updated to the latest security versions.
+ bugs:
+ - |
+ After administrators updated a GitHub Enterprise Server instance's TLS certificate, GitHub Pages deployments failed because the necessary services did not restart when `ghe-config-apply` was run.
+ - |
+ Administrators could not generate support bundles on stateless high availability nodes because the `ghe-support-bundle` command failed when attempting to query Elasticsearch on nodes without the `elasticsearch-server` role.
+ - |
+ On an instance with Enterprise Live Migrations enabled, running `/usr/local/bin/elm` without arguments dropped into a degraded container shell where standard administrative commands were unavailable.
+ - |
+ The scheduled job that repairs search indexes could run long enough that its next execution interrupted the in-progress repair, causing the process to restart before completing. The job used a locking mechanism to prevent overlapping runs and was scheduled to run once per month, on the first Saturday, instead of weekly.
+ - |
+ On instances with multiple data disks configured, administrators who ran `ghe-restore` encountered an error message: `Runtime error (func=(main), adr=10): Divide by zero`.
+ - |
+ For administrators using the Backup Service to back up a high availability (HA) appliance from the replica with GitHub Actions enabled, MSSQL backups would fail with the error `This command can only be run on the primary mssql node.`
+ - |
+ When running `ghe-backup` on an instance configured with the Backup Service, the `data` directory inside the backups mountpoint (usually `/data/backup/data`) failed to be automatically created due to missing permissions.
+ - |
+ Users who created custom patterns for secret scanning could bypass the restriction on unbounded wildcards by wrapping them in capture groups, which could cause performance degradation during scans.
+ changes:
+ - |
+ To reduce response times for repositories with a large number of code scanning alerts, GitHub Enterprise Server uses an optimized query when counting alerts.
+ - |
+ Site administrators can include Enterprise Live Migrations (ELM) Exporter appliance data in scheduled backups using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ - |
+ Site administrators can restore Enterprise Live Migrations (ELM) Exporter appliance data using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ known_issues:
+ - |
+ During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading.
+ - |
+ During the validation phase of a configuration run, a `No such object` error may occur for the Notebook and Viewscreen services. This error can be ignored as the services should still correctly start.
+ - |
+ If the root site administrator is locked out of the Management Console after failed login attempts, the account does not unlock automatically after the defined lockout time. Someone with administrative SSH access to the instance must unlock the account using the administrative shell. For more information, see [Troubleshooting access to the Management Console](/enterprise-server@latest/admin/administering-your-instance/administering-your-instance-from-the-web-ui/troubleshooting-access-to-the-management-console#unlocking-the-root-site-administrator-account).
+ - |
+ On an instance with the HTTP `X-Forwarded-For` header configured for use behind a load balancer, all client IP addresses in the instance's audit log erroneously appear as 127.0.0.1.
+ - |
+ {% data reusables.release-notes.large-adoc-files-issue %}
+ - |
+ Admin stats REST API endpoints may time out on appliances with many users or repositories. Retrying the request until data is returned is advised.
+ - |
+ When following the steps for [Replacing the primary MySQL node](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-the-primary-mysql-node), step 14 (running `ghe-cluster-config-apply`) might fail with errors. If this occurs, re-running `ghe-cluster-config-apply` is expected to succeed.
+ - |
+ Running a config apply as part of the steps for [Replacing a node in an emergency](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-a-node-in-an-emergency) may fail with errors if the node being replaced is still reachable. If this occurs, shutdown the node and repeat the steps.
+ - |
+ {% data reusables.release-notes.2024-06-possible-frontend-5-minute-outage-during-hotpatch-upgrade %}
+ - |
+ When restoring data originally backed up from a 3.13 or later appliance version, the Elasticsearch indices need to be reindexed before some of the data will show up. This happens via a nightly scheduled job. It can also be forced by running `/usr/local/share/enterprise/ghe-es-search-repair`.
+ - |
+ An organization-level code scanning configuration page is displayed on instances that do not use GitHub Advanced Security or code scanning.
+ - |
+ When enabling automatic update checks for the first time in the Management Console, the status is not dynamically reflected until the "Updates" page is reloaded.
+ - |
+ When restoring from a backup snapshot, a large number of `mapper_parsing_exception` errors may be displayed.
+ - |
+ When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration.
+ - |
+ When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration.
+ - |
+ In a cluster, the host running restore requires access to the storage nodes via their private IPs.
+ - |
+ On an instance hosted on Azure, commenting on an issue via email meant the comment was not added to the issue.
+ - |
+ After a restore, existing outside collaborators cannot be added to repositories in a new organization. This issue can be resolved by running `/usr/local/share/enterprise/ghe-es-search-repair` on the appliance.
+ - |
+ After a geo-replica is promoted to be a primary by running `ghe-repl-promote`, the GitHub Actions workflow of a repository does not have any suggested workflows.
+ - |
+ Unexpected elements may appear in the UI on the repository overview page for locked repositories.
+ - |
+ When publishing npm packages in a workflow after restoring from a backup to GitHub Enterprise Server 3.13.5.gm4 or 3.14.2.gm3, you may encounter a `401 Unauthorized` error from the GitHub Packages service. This can happen if the restore is from an N-1 or N-2 version and the workflow targets the npm endpoint on the backup instance. To avoid this issue, ensure the access token is valid and includes the correct scopes for publishing to GitHub Packages.
+ - |
+ When applying an enterprise security configuration to all repositories (for example, enabling Secret Scanning or Code Scanning across all repositories), the system immediately enqueues enablement jobs for every organization in the enterprise simultaneously. For enterprises with a large number of repositories, this can result in significant system load and potential performance degradation. If you manage a large enterprise with many organizations and repositories, we recommend applying security configurations at the organization level rather than at the enterprise level in the UI. This allows you to enable security features incrementally and monitor system performance as you roll out changes.
+ - |
+ Git versions are mismatched between containers on the instance.
+ - |
+ Administrators upgrading to certain patch releases encountered a `Failed to generate an OIDC token` error during the `Update Servicing Resources` step when GitHub Actions was configured with Google Cloud Storage (GCS) or AWS S3 using OpenID Connect (OIDC) authentication. The upgrade was blocked and could not complete. To work around this issue, administrators could apply a manual patch by running `sudo sed -i.bak 's|ghe-actions-console -s -c Update-Service|ghe-actions-console -s --no-blob-creds -c Update-Service|g' /usr/local/bin/ghe-actions-update` followed by `ghe-config-apply`.
diff --git a/data/release-notes/enterprise-server/3-18/11.yml b/data/release-notes/enterprise-server/3-18/11.yml
new file mode 100644
index 000000000000..041505b860f7
--- /dev/null
+++ b/data/release-notes/enterprise-server/3-18/11.yml
@@ -0,0 +1,96 @@
+date: '2026-06-30'
+sections:
+ security_fixes:
+ - |
+ **HIGH**: Current configurations of the GitHub API access controls could allow an attacker to create issues in any public repository via a u2s token without requiring the underlying installation to have issues write permission. This therefore allows an attacker to impersonate the victim in public repositories by creating issues and commit comments.
+ - |
+ **MEDIUM**: An attacker with site administrator privileges could extract arbitrary data from the instance's database, including user password hashes, by exploiting a blind SQL injection vulnerability in the `dependenciesPrefers` argument of the `dependencyGraphManifests` GraphQL field. This vulnerability affected instances with the dependency graph enabled and was reported via the GitHub Bug Bounty program.
+ - |
+ **MEDIUM**: An attacker could gain unintended access to an organization's runner management by directing a user to authorize an OAuth app whose requested manage_runners:org scope was not displayed on the authorization consent screen. GitHub has requested CVE ID [CVE-2026-9106](https://www.cve.org/cverecord?id=CVE-2026-9106) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ **MEDIUM**: An authenticated GHES user could read source code from private repositories they did not have access to by supplying a cross-repository comparison range to the Copilot pull request description diff summary endpoint, which did not verify the user's permission to view the target repository. GitHub has requested CVE ID [CVE-2026-9132](https://www.cve.org/cverecord?id=CVE-2026-9132) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/).
+ - |
+ **MEDIUM**: An attacker who could execute code within a Copilot coding agent session, for example through a malicious dependency or repository setup steps, could use the agent's server-to-server token to create, modify, or delete releases and release assets in that repository via the REST API, exceeding the agent's documented push-only restriction. GitHub has requested CVE ID [CVE-2026-12373](https://www.cve.org/cverecord?id=CVE-2026-12373) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/).
+ - |
+ **MEDIUM**: An attacker could execute arbitrary JavaScript in a victim's browser on a GitHub Enterprise Server instance by creating a discussion in the Q\&A category with a crafted title that breaks out of the JSON-LD structured-data script block when a comment is marked as the answer. To mitigate this issue, GitHub has updated the rendering of JSON-LD structured data in discussions to properly escape user-controlled input. GitHub has requested CVE ID [CVE-2026-10585](https://www.cve.org/cverecord?id=CVE-2026-10585) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ GitHub has updated `dnsmasq` to fix the following vulnerabilities: CVE-2026-4891, CVE-2026-4890, CVE-2026-4892, CVE-2026-4893, CVE-2026-5172, CVE-2026-2291.
+ - |
+ Packages have been updated to the latest security versions.
+ bugs:
+ - |
+ Administrators experienced "Permission denied" errors when the `ghes-manage-agent` attempted to write logs under `/var/log/license-upgrade` during license upgrade restart tasks.
+ - |
+ After administrators updated a GitHub Enterprise Server instance's TLS certificate, GitHub Pages deployments failed because the necessary services did not restart when `ghe-config-apply` was run.
+ - |
+ On an instance with Enterprise Live Migrations enabled, running `/usr/local/bin/elm` without arguments dropped into a degraded container shell where standard administrative commands were unavailable.
+ - |
+ The scheduled job that repairs search indexes could run long enough that its next execution interrupted the in-progress repair, causing the process to restart before completing. The job used a locking mechanism to prevent overlapping runs and was scheduled to run once per month, on the first Saturday, instead of weekly.
+ - |
+ When a site administrator ran `ghe-remove-node` to evacuate and remove a node, orphaned configuration entries remained in node subsections such as `hostname.app.github`.
+ - |
+ On instances with multiple data disks configured, administrators who ran `ghe-restore` encountered an error message: `Runtime error (func=(main), adr=10): Divide by zero`.
+ - |
+ For administrators using the Backup Service to back up a high availability (HA) appliance from the replica with GitHub Actions enabled, MSSQL backups would fail with the error `This command can only be run on the primary mssql node.`
+ - |
+ When running `ghe-backup` on an instance configured with the Backup Service, the `data` directory inside the backups mountpoint (usually `/data/backup/data`) failed to be automatically created due to missing permissions.
+ - |
+ On instances processing webhooks, notifications, or other background events, the `github-stream-processors` Nomad job consumed increasing memory and processed events progressively more slowly the longer it ran, requiring site administrators to restart the job to restore baseline throughput.
+ - |
+ Administrators experienced gradual memory growth in long-running GitHub Enterprise Server background event processor services. Upgrading resets service-mapping context between messages, and administrators do not need to take any action after upgrading.
+ changes:
+ - |
+ To reduce response times for repositories with a large number of code scanning alerts, GitHub Enterprise Server uses an optimized query when counting alerts.
+ - |
+ Site administrators can include Enterprise Live Migrations (ELM) Exporter appliance data in scheduled backups using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ - |
+ Site administrators can restore Enterprise Live Migrations (ELM) Exporter appliance data using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ known_issues:
+ - |
+ During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading.
+ - |
+ During the validation phase of a configuration run, a `No such object` error may occur for the Notebook and Viewscreen services. This error can be ignored as the services should still correctly start.
+ - |
+ If the root site administrator is locked out of the Management Console after failed login attempts, the account does not unlock automatically after the defined lockout time. Someone with administrative SSH access to the instance must unlock the account using the administrative shell. For more information, see [Troubleshooting access to the Management Console](/enterprise-server@latest/admin/administering-your-instance/administering-your-instance-from-the-web-ui/troubleshooting-access-to-the-management-console#unlocking-the-root-site-administrator-account).
+ - |
+ On an instance with the HTTP `X-Forwarded-For` header configured for use behind a load balancer, all client IP addresses in the instance's audit log erroneously appear as 127.0.0.1.
+ - |
+ {% data reusables.release-notes.large-adoc-files-issue %}
+ - |
+ Admin stats REST API endpoints may time out on appliances with many users or repositories. Retrying the request until data is returned is advised.
+ - |
+ When following the steps for [Replacing the primary MySQL node](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-the-primary-mysql-node), step 14 (running `ghe-cluster-config-apply`) might fail with errors. If this occurs, re-running `ghe-cluster-config-apply` is expected to succeed.
+ - |
+ Running a config apply as part of the steps for [Replacing a node in an emergency](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-a-node-in-an-emergency) may fail with errors if the node being replaced is still reachable. If this occurs, shutdown the node and repeat the steps.
+ - |
+ {% data reusables.release-notes.2024-06-possible-frontend-5-minute-outage-during-hotpatch-upgrade %}
+ - |
+ When restoring data originally backed up from a 3.13 or later appliance version, the Elasticsearch indices need to be reindexed before some of the data will show up. This happens via a nightly scheduled job. It can also be forced by running `/usr/local/share/enterprise/ghe-es-search-repair`.
+ - |
+ An organization-level code scanning configuration page is displayed on instances that do not use GitHub Advanced Security or code scanning.
+ - |
+ When enabling automatic update checks for the first time in the Management Console, the status is not dynamically reflected until the "Updates" page is reloaded.
+ - |
+ When restoring from a backup snapshot, a large number of `mapper_parsing_exception` errors may be displayed.
+ - |
+ When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration.
+ - |
+ In a cluster, the host running restore requires access the storage nodes via their private IPs.
+ - |
+ On an instance hosted on Azure, commenting on an issue via email meant the comment was not added to the issue.
+ - |
+ After a restore, existing outside collaborators cannot be added to repositories in a new organization. This issue can be resolved by running `/usr/local/share/enterprise/ghe-es-search-repair` on the appliance.
+ - |
+ After a geo-replica is promoted to be a primary by running `ghe-repl-promote`, the GitHub Actions workflow of a repository does not have any suggested workflows.
+ - |
+ Unexpected elements may appear in the UI on the repository overview page for locked repositories.
+ - |
+ When publishing npm packages in a workflow after restoring from a backup to GitHub Enterprise Server 3.13.5.gm4 or 3.14.2.gm3, you may encounter a `401 Unauthorized` error from the GitHub Packages service. This can happen if the restore is from an N-1 or N-2 version and the workflow targets the npm endpoint on the backup instance. To avoid this issue, ensure the access token is valid and includes the correct scopes for publishing to GitHub Packages.
+ - |
+ The setting to define private registries at the organization level for code scanning is only available if Dependabot is also enabled for the instance.
+ - |
+ Custom NTP settings are removed during the upgrade process.
+ - |
+ When applying an enterprise security configuration to all repositories (for example, enabling Secret Scanning or Code Scanning across all repositories), the system immediately enqueues enablement jobs for every organization in the enterprise simultaneously. For enterprises with a large number of repositories, this can result in significant system load and potential performance degradation. If you manage a large enterprise with many organizations and repositories, we recommend applying security configurations at the organization level rather than at the enterprise level in the UI. This allows you to enable security features incrementally and monitor system performance as you roll out changes.
+ - |
+ Administrators upgrading to certain patch releases encountered a "Failed to generate an OIDC token" error during the `Update Servicing Resources` step when GitHub Actions was configured with Google Cloud Storage (GCS) or AWS S3 using OpenID Connect (OIDC) authentication. The upgrade was blocked and could not complete. To work around this issue, administrators could apply a manual patch by running `sudo sed -i.bak 's|ghe-actions-console -s -c Update-Service|ghe-actions-console -s --no-blob-creds -c Update-Service|g' /usr/local/bin/ghe-actions-update` followed by `ghe-config-apply`.
diff --git a/data/release-notes/enterprise-server/3-19/8.yml b/data/release-notes/enterprise-server/3-19/8.yml
new file mode 100644
index 000000000000..1cd1446aee4d
--- /dev/null
+++ b/data/release-notes/enterprise-server/3-19/8.yml
@@ -0,0 +1,100 @@
+date: '2026-06-30'
+sections:
+ security_fixes:
+ - |
+ **HIGH**: Current configurations of the GitHub API access controls could allow an attacker to create issues in any public repository via a u2s token without requiring the underlying installation to have issues write permission. This therefore allows an attacker to impersonate the victim in public repositories by creating issues and commit comments.
+ - |
+ **MEDIUM**: An attacker with site administrator privileges could extract arbitrary data from the instance's database, including user password hashes, by exploiting a blind SQL injection vulnerability in the `dependenciesPrefers` argument of the `dependencyGraphManifests` GraphQL field. This vulnerability affected instances with the dependency graph enabled and was reported via the GitHub Bug Bounty program.
+ - |
+ **MEDIUM**: An attacker could gain unintended access to an organization's runner management by directing a user to authorize an OAuth app whose requested manage_runners:org scope was not displayed on the authorization consent screen. GitHub has requested CVE ID [CVE-2026-9106](https://www.cve.org/cverecord?id=CVE-2026-9106) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ **MEDIUM**: An authenticated GHES user could read source code from private repositories they did not have access to by supplying a cross-repository comparison range to the Copilot pull request description diff summary endpoint, which did not verify the user's permission to view the target repository. GitHub has requested CVE ID [CVE-2026-9132](https://www.cve.org/cverecord?id=CVE-2026-9132) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/).
+ - |
+ **MEDIUM**: An attacker who could execute code within a Copilot coding agent session, for example through a malicious dependency or repository setup steps, could use the agent's server-to-server token to create, modify, or delete releases and release assets in that repository via the REST API, exceeding the agent's documented push-only restriction. GitHub has requested CVE ID [CVE-2026-12373](https://www.cve.org/cverecord?id=CVE-2026-12373) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/).
+ - |
+ **MEDIUM**: An attacker could execute arbitrary JavaScript in a victim's browser on a GitHub Enterprise Server instance by creating a discussion in the Q\&A category with a crafted title that breaks out of the JSON-LD structured-data script block when a comment is marked as the answer. To mitigate this issue, GitHub has updated the rendering of JSON-LD structured data in discussions to properly escape user-controlled input. GitHub has requested CVE ID [CVE-2026-10585](https://www.cve.org/cverecord?id=CVE-2026-10585) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ GitHub has updated `dnsmasq` to fix the following vulnerabilities: CVE-2026-4891, CVE-2026-4890, CVE-2026-4892, CVE-2026-4893, CVE-2026-5172, CVE-2026-2291.
+ - |
+ Packages have been updated to the latest security versions.
+ bugs:
+ - |
+ Administrators experienced "Permission denied" errors when the `ghes-manage-agent` attempted to write logs under `/var/log/license-upgrade` during license upgrade restart tasks.
+ - |
+ After administrators updated a GitHub Enterprise Server instance's TLS certificate, GitHub Pages deployments failed because the necessary services did not restart when `ghe-config-apply` was run.
+ - |
+ When uploading an invalid or expired license in the Management Console, the page would not correctly indicate the failure and restart the page without any additional information.
+ - |
+ On an instance with Enterprise Live Migrations enabled, running `/usr/local/bin/elm` without arguments dropped into a degraded container shell where standard administrative commands were unavailable.
+ - |
+ The scheduled job that repairs search indexes could run long enough that its next execution interrupted the in-progress repair, causing the process to restart before completing. The job used a locking mechanism to prevent overlapping runs and was scheduled to run once per month, on the first Saturday, instead of weekly.
+ - |
+ When a site administrator ran `ghe-remove-node` to evacuate and remove a node, orphaned configuration entries remained in node subsections such as `hostname.app.github`.
+ - |
+ On instances with multiple data disks configured, administrators who ran `ghe-restore` encountered an error message: `Runtime error (func=(main), adr=10): Divide by zero`.
+ - |
+ For administrators using the Backup Service to back up a high availability (HA) appliance from the replica with GitHub Actions enabled, MSSQL backups would fail with the error `This command can only be run on the primary mssql node.`
+ - |
+ When running `ghe-backup` on an instance configured with the Backup Service, the `data` directory inside the backups mountpoint (usually `/data/backup/data`) failed to be automatically created due to missing permissions.
+ - |
+ On instances processing webhooks, notifications, or other background events, the `github-stream-processors` Nomad job consumed increasing memory and processed events progressively more slowly the longer it ran, requiring site administrators to restart the job to restore baseline throughput.
+ - |
+ Administrators experienced gradual memory growth in long-running GitHub Enterprise Server background event processor services. Upgrading resets service-mapping context between messages, and administrators do not need to take any action after upgrading.
+ - |
+ On an instance with dependency graph enabled, dependency snapshot uploads via the dependency submission API occasionally failed with internal server errors.
+ - |
+ On instances without a GitHub Advanced Security license, the organization Dependabot dashboard in Security Overview returned a 404 error, even though Dependabot is available on those instances.
+ - |
+ On an instance with GitHub Code Security enabled, users could bypass code scanning merge protection when the scan had passed on an earlier commit but had not yet run on the latest commit of a pull request.
+ changes:
+ - |
+ To reduce response times for repositories with a large number of code scanning alerts, GitHub Enterprise Server uses an optimized query when counting alerts.
+ - |
+ Site administrators can include Enterprise Live Migrations (ELM) Exporter appliance data in scheduled backups using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ - |
+ Site administrators can restore Enterprise Live Migrations (ELM) Exporter appliance data using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ known_issues:
+ - |
+ During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading.
+ - |
+ During the validation phase of a configuration run, a `No such object` error may occur for the Notebook and Viewscreen services. This error can be ignored as the services should still correctly start.
+ - |
+ If the root site administrator is locked out of the Management Console after failed login attempts, the account does not unlock automatically after the defined lockout time. Someone with administrative SSH access to the instance must unlock the account using the administrative shell. For more information, see [Troubleshooting access to the Management Console](/enterprise-server@latest/admin/administering-your-instance/administering-your-instance-from-the-web-ui/troubleshooting-access-to-the-management-console#unlocking-the-root-site-administrator-account).
+ - |
+ {% data reusables.release-notes.large-adoc-files-issue %}
+ - |
+ Admin stats REST API endpoints may timeout on appliances with many users or repositories. Retrying the request until data is returned is advised.
+ - |
+ When following the steps for [Replacing the primary MySQL node](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-the-primary-mysql-node), step 14 (running `ghe-cluster-config-apply`) might fail with errors. If this occurs, re-running `ghe-cluster-config-apply` is expected to succeed.
+ - |
+ Running a config apply as part of the steps for [Replacing a node in an emergency](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-a-node-in-an-emergency) may fail with errors if the node being replaced is still reachable. If this occurs, shutdown the node and repeat the steps.
+ - |
+ {% data reusables.release-notes.2024-06-possible-frontend-5-minute-outage-during-hotpatch-upgrade %}
+ - |
+ When restoring data originally backed up from a 3.13 or greater appliance version, the Elasticsearch indices need to be reindexed before some of the data will show up. This happens via a nightly scheduled job. It can also be forced by running `/usr/local/share/enterprise/ghe-es-search-repair`.
+ - |
+ When enabling automatic update checks for the first time in the Management Console, the status is not dynamically reflected until the "Updates" page is reloaded.
+ - |
+ When restoring from a backup snapshot, a large number of `mapper_parsing_exception` errors may be displayed.
+ - |
+ When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration.
+ - |
+ In a cluster, the host running restore requires access the storage nodes via their private IPs.
+ - |
+ On an instance hosted on Azure, commenting on an issue via email means the comment is not added to the issue.
+ - |
+ After a restore, existing outside collaborators cannot be added to repositories in a new organization. This issue can be resolved by running `/usr/local/share/enterprise/ghe-es-search-repair` on the appliance.
+ - |
+ After a geo-replica is promoted to be a primary by running `ghe-repl-promote`, the GitHub Actions workflow of a repository does not have any suggested workflows.
+ - |
+ When publishing npm packages in a workflow after restoring from a backup to GitHub Enterprise Server 3.13.5.gm4 or 3.14.2.gm3, you may encounter a `401 Unauthorized` error from the GitHub Packages service. This can happen if the restore is from an N-1 or N-2 version and the workflow targets the npm endpoint on the backup instance. To avoid this issue, ensure the access token is valid and includes the correct scopes for publishing to GitHub Packages.
+ - |
+ The setting to define private registries at the organization level for code scanning is only available if Dependabot is also enabled for the instance.
+ - |
+ Upgrading or hotpatching to 3.19.1 may fail on nodes that have been continuously upgraded from versions older than 2021 (i.e. 2.17). If this issue occurs, you will see log entries prefixed with `invalid secret` in ghe-config.log. If you are running nodes from these older versions, it is recommended not to upgrade to 3.19.1.
+ - |
+ An issue in the Management Console means the Backups (Preview) and Updates tabs may fail to open and instead return an Internal Server Error. We recommend using the command line interface (CLI) for backups and updates.
+ - |
+ When applying an enterprise security configuration to all repositories (for example, enabling Secret Scanning or Code Scanning across all repositories), the system immediately enqueues enablement jobs for every organization in the enterprise simultaneously. For enterprises with a large number of repositories, this can result in significant system load and potential performance degradation. If you manage a large enterprise with many organizations and repositories, we recommend applying security configurations at the organization level rather than at the enterprise level in the UI. This allows you to enable security features incrementally and monitor system performance as you roll out changes.
+ - |
+ Administrators upgrading to certain patch releases encountered a "Failed to generate an OIDC token" error during the `Update Servicing Resources` step when GitHub Actions was configured with Google Cloud Storage (GCS) or AWS S3 using OpenID Connect (OIDC) authentication. The upgrade was blocked and could not complete. To work around this issue, administrators could apply a manual patch by running `sudo sed -i.bak 's|ghe-actions-console -s -c Update-Service|ghe-actions-console -s --no-blob-creds -c Update-Service|g' /usr/local/bin/ghe-actions-update` followed by `ghe-config-apply`.
diff --git a/data/release-notes/enterprise-server/3-20/4.yml b/data/release-notes/enterprise-server/3-20/4.yml
new file mode 100644
index 000000000000..62e5a66ab2ba
--- /dev/null
+++ b/data/release-notes/enterprise-server/3-20/4.yml
@@ -0,0 +1,92 @@
+date: '2026-06-30'
+sections:
+ security_fixes:
+ - |
+ **HIGH**: Current configurations of the GitHub API access controls could allow an attacker to create issues in any public repository via a u2s token without requiring the underlying installation to have issues write permission. This therefore allows an attacker to impersonate the victim in public repositories by creating issues and commit comments.
+ - |
+ **MEDIUM**: An attacker with site administrator privileges could extract arbitrary data from the instance's database, including user password hashes, by exploiting a blind SQL injection vulnerability in the `dependenciesPrefers` argument of the `dependencyGraphManifests` GraphQL field. This vulnerability affected instances with the dependency graph enabled and was reported via the GitHub Bug Bounty program.
+ - |
+ **MEDIUM**: An attacker could gain unintended access to an organization's runner management by directing a user to authorize an OAuth app whose requested manage_runners:org scope was not displayed on the authorization consent screen. GitHub has requested CVE ID [CVE-2026-9106](https://www.cve.org/cverecord?id=CVE-2026-9106) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ **MEDIUM**: An authenticated GHES user could read source code from private repositories they did not have access to by supplying a cross-repository comparison range to the Copilot pull request description diff summary endpoint, which did not verify the user's permission to view the target repository. GitHub has requested CVE ID [CVE-2026-9132](https://www.cve.org/cverecord?id=CVE-2026-9132) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/).
+ - |
+ **MEDIUM**: An attacker who could execute code within a Copilot coding agent session, for example through a malicious dependency or repository setup steps, could use the agent's server-to-server token to create, modify, or delete releases and release assets in that repository via the REST API, exceeding the agent's documented push-only restriction. GitHub has requested CVE ID [CVE-2026-12373](https://www.cve.org/cverecord?id=CVE-2026-12373) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/).
+ - |
+ **MEDIUM**: An attacker could execute arbitrary JavaScript in a victim's browser on a GitHub Enterprise Server instance by creating a discussion in the Q\&A category with a crafted title that breaks out of the JSON-LD structured-data script block when a comment is marked as the answer. To mitigate this issue, GitHub has updated the rendering of JSON-LD structured data in discussions to properly escape user-controlled input. GitHub has requested CVE ID [CVE-2026-10585](https://www.cve.org/cverecord?id=CVE-2026-10585) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ GitHub has updated `dnsmasq` to fix the following vulnerabilities: CVE-2026-4891, CVE-2026-4890, CVE-2026-4892, CVE-2026-4893, CVE-2026-5172, CVE-2026-2291.
+ - |
+ Packages have been updated to the latest security versions.
+ - |
+ Upgraded the Notebooks container base image from Ubuntu Focal (20.04) to Noble (24.04) for continued security support.
+ bugs:
+ - |
+ Administrators experienced "Permission denied" errors when the `ghes-manage-agent` attempted to write logs under `/var/log/license-upgrade` during license upgrade restart tasks.
+ - |
+ When uploading an invalid or expired license in the Management Console, the page would not correctly indicate the failure and restart the page without any additional information.
+ - |
+ After administrators updated a GitHub Enterprise Server instance's TLS certificate, GitHub Pages deployments failed because the necessary services did not restart when `ghe-config-apply` was run.
+ - |
+ On an instance with Enterprise Live Migrations enabled, running `/usr/local/bin/elm` without arguments dropped into a degraded container shell where standard administrative commands were unavailable.
+ - |
+ The scheduled job that repairs search indexes could run long enough that its next execution interrupted the in-progress repair, causing the process to restart before completing. The job used a locking mechanism to prevent overlapping runs and was scheduled to run once per month, on the first Saturday, instead of weekly.
+ - |
+ When a site administrator ran `ghe-remove-node` to evacuate and remove a node, orphaned configuration entries remained in node subsections such as `hostname.app.github`.
+ - |
+ Administrators who had upgraded to GHES 3.20 or later saw validation errors when running a `config-apply`. This is because the required backup service configuration was not populated in `github.conf` during the upgrade.
+ - |
+ On instances with multiple data disks configured, administrators who ran `ghe-restore` encountered an error message: `Runtime error (func=(main), adr=10): Divide by zero`.
+ - |
+ For administrators using the Backup Service to back up a high availability (HA) appliance from the replica with GitHub Actions enabled, MSSQL backups would fail with the error `This command can only be run on the primary mssql node.`
+ - |
+ When running `ghe-backup` on an instance configured with the Backup Service, the `data` directory inside the backups mountpoint (usually `/data/backup/data`) failed to be automatically created due to missing permissions.
+ - |
+ Administrators experienced gradual memory growth in long-running GitHub Enterprise Server background event processor services. Upgrading resets service-mapping context between messages, and administrators do not need to take any action after upgrading.
+ - |
+ Submitting a dependency snapshot via the REST API could return a 500 internal server error instead of a successful response.
+ - |
+ On instances without a GitHub Advanced Security license, the organization Dependabot dashboard in Security Overview returned a 404 error, even though Dependabot is available on those instances.
+ - |
+ On an instance with GitHub Code Security enabled, users could bypass code scanning merge protection when the scan had passed on an earlier commit but had not yet run on the latest commit of a pull request.
+ changes:
+ - |
+ To reduce response times for repositories with a large number of code scanning alerts, GitHub Enterprise Server uses an optimized query when counting alerts.
+ - |
+ Site administrators can include Enterprise Live Migrations (ELM) Exporter appliance data in scheduled backups using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ - |
+ Site administrators can restore Enterprise Live Migrations (ELM) Exporter appliance data using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ known_issues:
+ - |
+ During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading.
+ - |
+ During the validation phase of a configuration run, a `No such object` error may occur for the Notebook and Viewscreen services. This error can be ignored as the services should still correctly start.
+ - |
+ If the root site administrator is locked out of the Management Console after failed login attempts, the account does not unlock automatically after the defined lockout time. Someone with administrative SSH access to the instance must unlock the account using the administrative shell. For more information, see [Troubleshooting access to the Management Console](/enterprise-server@latest/admin/administering-your-instance/administering-your-instance-from-the-web-ui/troubleshooting-access-to-the-management-console#unlocking-the-root-site-administrator-account).
+ - |
+ {% data reusables.release-notes.large-adoc-files-issue %}
+ - |
+ Admin stats REST API endpoints may time out on appliances with many users or repositories. Retrying the request until data is returned is advised.
+ - |
+ When following the steps for [Replacing the primary MySQL node](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-the-primary-mysql-node), step 14 (running `ghe-cluster-config-apply`) might fail with errors. If this occurs, re-running `ghe-cluster-config-apply` is expected to succeed.
+ - |
+ Running a config apply as part of the steps for [Replacing a node in an emergency](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-a-node-in-an-emergency) may fail with errors if the node being replaced is still reachable. If this occurs, shutdown the node and repeat the steps.
+ - |
+ When restoring data originally backed up from a 3.13 or later appliance version, the Elasticsearch indices need to be reindexed before some of the data will show up. This happens via a nightly scheduled job. It can also be forced by running `/usr/local/share/enterprise/ghe-es-search-repair`.
+ - |
+ When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration.
+ - |
+ In a cluster, the host running restore requires access the storage nodes via their private IPs.
+ - |
+ On an instance hosted on Azure, commenting on an issue via email means the comment is not added to the issue.
+ - |
+ After a restore, existing outside collaborators cannot be added to repositories in a new organization. This issue can be resolved by running `/usr/local/share/enterprise/ghe-es-search-repair` on the appliance.
+ - |
+ After a geo-replica is promoted to be a primary by running `ghe-repl-promote`, the GitHub Actions workflow of a repository does not have any suggested workflows.
+ - |
+ When publishing npm packages in a workflow after restoring from a backup to GitHub Enterprise Server 3.13.5.gm4 or 3.14.2.gm3, you may encounter a `401 Unauthorized` error from the GitHub Packages service. This can happen if the restore is from an N-1 or N-2 version and the workflow targets the npm endpoint on the backup instance. To avoid this issue, ensure the access token is valid and includes the correct scopes for publishing to GitHub Packages.
+ - |
+ When applying an enterprise security configuration to all repositories (for example, enabling Secret Scanning or Code Scanning across all repositories), the system immediately enqueues enablement jobs for every organization in the enterprise simultaneously. For enterprises with a large number of repositories, this can result in significant system load and potential performance degradation. If you manage a large enterprise with many organizations and repositories, we recommend applying security configurations at the organization level rather than at the enterprise level in the UI. This allows you to enable security features incrementally and monitor system performance as you roll out changes.
+ - |
+ On instances with multiple Git storage nodes in a voting configuration, including cluster and geo-replication high availability topologies, upgrading may fail to correctly install Actions that ship with the new version. In some cases, previous versions of these Actions remain on the instance. To resolve this issue, run the following commands on the primary node: `ghe-config --unset 'app.actions.actions-repos-sha1sum'`, `ghe-config-apply`, and `/usr/local/share/enterprise/ghe-run-init-actions-graph`.
+ - |
+ Administrators upgrading to certain patch releases encountered a "Failed to generate an OIDC token" error during the `Update Servicing Resources` step when GitHub Actions was configured with Google Cloud Storage (GCS) or AWS S3 using OpenID Connect (OIDC) authentication. The upgrade was blocked and could not complete. To work around this issue, administrators could apply a manual patch by running `sudo sed -i.bak 's|ghe-actions-console -s -c Update-Service|ghe-actions-console -s --no-blob-creds -c Update-Service|g' /usr/local/bin/ghe-actions-update` followed by `ghe-config-apply`.
diff --git a/data/release-notes/enterprise-server/3-21/2.yml b/data/release-notes/enterprise-server/3-21/2.yml
new file mode 100644
index 000000000000..644c6c5d0860
--- /dev/null
+++ b/data/release-notes/enterprise-server/3-21/2.yml
@@ -0,0 +1,90 @@
+date: '2026-06-30'
+sections:
+ security_fixes:
+ - |
+ **HIGH**: Current configurations of the GitHub API access controls could allow an attacker to create issues in any public repository via a u2s token without requiring the underlying installation to have issues write permission. This therefore allows an attacker to impersonate the victim in public repositories by creating issues and commit comments.
+ - |
+ **MEDIUM**: An attacker with site administrator privileges could extract arbitrary data from the instance's database, including user password hashes, by exploiting a blind SQL injection vulnerability in the `dependenciesPrefers` argument of the `dependencyGraphManifests` GraphQL field. This vulnerability affected instances with the dependency graph enabled and was reported via the GitHub Bug Bounty program.
+ - |
+ **MEDIUM**: An attacker could gain unintended access to an organization's runner management by directing a user to authorize an OAuth app whose requested manage_runners:org scope was not displayed on the authorization consent screen. GitHub has requested CVE ID [CVE-2026-9106](https://www.cve.org/cverecord?id=CVE-2026-9106) for this vulnerability, which was reported via the [GitHub Bug Bounty](https://bounty.github.com/) program.
+ - |
+ **MEDIUM**: An attacker who could execute code within a Copilot coding agent session, for example through a malicious dependency or repository setup steps, could use the agent's server-to-server token to create, modify, or delete releases and release assets in that repository via the REST API, exceeding the agent's documented push-only restriction. GitHub has requested CVE ID [CVE-2026-12373](https://www.cve.org/cverecord?id=CVE-2026-12373) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/).
+ - |
+ GitHub has updated `dnsmasq` to fix the following vulnerabilities: CVE-2026-4891, CVE-2026-4890, CVE-2026-4892, CVE-2026-4893, CVE-2026-5172, CVE-2026-2291.
+ - |
+ Packages have been updated to the latest security versions.
+ bugs:
+ - |
+ After administrators updated a GitHub Enterprise Server instances TLS certificate, GitHub Pages deployments failed because the necessary services did not restart when `ghe-config-apply` was run.
+ - |
+ When uploading an invalid or expired license in the Management Console, the page would not correctly indicate the failure and restart the page without any additional information.
+ - |
+ On an instance with Enterprise Live Migrations enabled, running `/usr/local/bin/elm` without arguments dropped into a degraded container shell where standard administrative commands were unavailable.
+ - |
+ On instances with multiple data disks configured, administrators who ran `ghe-restore` encountered an error message: `Runtime error (func=(main), adr=10): Divide by zero`.
+ - |
+ The scheduled job that repairs search indexes could run long enough that its next execution interrupted the in-progress repair, causing the process to restart before completing. The job now uses a locking mechanism to prevent overlapping runs and is scheduled to run once per month, on the first Saturday, instead of weekly.
+ - |
+ When a site administrator ran `ghe-remove-node` to evacuate and remove a node, orphaned configuration entries remained in node subsections such as `hostname.app.github`.
+ - |
+ Administrators who had upgraded to GHES 3.20 or later saw validation errors when running a `config-apply`. This is because the required backup service configuration was not populated in `github.conf` during the upgrade.
+ - |
+ For administrators using the Backup Service to back up a high availability (HA) appliance from the replica with GitHub Actions enabled, MSSQL backups would fail with the error `This command can only be run on the primary mssql node.`
+ - |
+ When running `ghe-backup` on an instance configured with the Backup Service, the `data` directory inside the backups mountpoint (usually `/data/backup/data`) failed to be automatically created due to missing permissions.
+ - |
+ Administrators experienced gradual memory growth in long-running GitHub Enterprise Server background event processor services. Upgrading resets service-mapping context between messages, and administrators do not need to take any action after upgrading.
+ - |
+ On instances without a GitHub Advanced Security license, the organization Dependabot dashboard in Security Overview returned a 404 error, even though Dependabot is available on those instances.
+ - |
+ On an instance with GitHub Code Security enabled, users could bypass code scanning merge protection when the scan had passed on an earlier commit but had not yet run on the latest commit of a pull request.
+ - |
+ When a user viewed an issue or pull request containing image attachments, the images could appear broken after the page had been open or idle for several minutes, and reloading the page was required to display them again.
+ changes:
+ - |
+ The default `maxconn` (maximum connections) value for HAProxy has increased from 1,024 to 8,192 across all proxies. Previously, large customers reported surpassing the limit on high-traffic services.
+ - |
+ Administrators who forward GitHub Enterprise Server logs to external syslog systems can now configure syslog forwarding to use RFC 5424 formatting with microsecond-precision timestamps. This improves event ordering and correlation in SIEM and monitoring systems. To enable this format, run `ghe-config syslog.format RFC5424` and `ghe-config-apply`.
+ - |
+ To reduce response times for repositories with a large number of code scanning alerts, GitHub Enterprise Server uses an optimized query when counting alerts.
+ - |
+ Site administrators can include Enterprise Live Migrations (ELM) Exporter appliance data in scheduled backups using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ - |
+ Site administrators can restore Enterprise Live Migrations (ELM) Exporter appliance data using GitHub Enterprise Server Backup Service. For more information, see [AUTOTITLE](/admin/backing-up-and-restoring-your-instance).
+ known_issues:
+ - |
+ During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading.
+ - |
+ During the validation phase of a configuration run, a `No such object` error may occur for the Notebook and Viewscreen services. This error can be ignored as the services should still correctly start.
+ - |
+ If the root site administrator is locked out of the Management Console after failed login attempts, the account does not unlock automatically after the defined lockout time. Someone with administrative SSH access to the instance must unlock the account using the administrative shell. For more information, see [Troubleshooting access to the Management Console](/enterprise-server@latest/admin/administering-your-instance/administering-your-instance-from-the-web-ui/troubleshooting-access-to-the-management-console#unlocking-the-root-site-administrator-account).
+ - |
+ {% data reusables.release-notes.large-adoc-files-issue %}
+ - |
+ Admin stats REST API endpoints may timeout on appliances with many users or repositories. Retrying the request until data is returned is advised.
+ - |
+ When following the steps for [Replacing the primary MySQL node](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-the-primary-mysql-node), step 14 (running `ghe-cluster-config-apply`) might fail with errors. If this occurs, re-running `ghe-cluster-config-apply` is expected to succeed.
+ - |
+ Running a config apply as part of the steps for [Replacing a node in an emergency](/admin/monitoring-managing-and-updating-your-instance/configuring-clustering/replacing-a-cluster-node#replacing-a-node-in-an-emergency) may fail with errors if the node being replaced is still reachable. If this occurs, shutdown the node and repeat the steps.
+ - |
+ When restoring data originally backed up from a 3.13 or greater appliance version, the Elasticsearch indices need to be reindexed before some of the data will show up. This happens via a nightly scheduled job. It can also be forced by running `/usr/local/share/enterprise/ghe-es-search-repair`.
+ - |
+ When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration.
+ - |
+ When initializing a new GHES cluster, nodes with the `consul-server` role should be added to the cluster before adding additional nodes. Adding all nodes simultaneously creates a race condition between nomad server registration and nomad client registration.
+ - |
+ In a cluster, the host running restore requires access the storage nodes via their private IPs.
+ - |
+ On an instance hosted on Azure, commenting on an issue via email meant the comment was not added to the issue.
+ - |
+ After a restore, existing outside collaborators cannot be added to repositories in a new organization. This issue can be resolved by running `/usr/local/share/enterprise/ghe-es-search-repair` on the appliance.
+ - |
+ After a geo-replica is promoted to be a primary by running `ghe-repl-promote`, the actions workflow of a repository does not have any suggested workflows.
+ - |
+ When publishing npm packages in a workflow after restoring from a backup to GitHub Enterprise Server 3.13.5.gm4 or 3.14.2.gm3, you may encounter a `401 Unauthorized` error from the GitHub Packages service. This can happen if the restore is from an N-1 or N-2 version and the workflow targets the npm endpoint on the backup instance. To avoid this issue, ensure the access token is valid and includes the correct scopes for publishing to GitHub Packages.
+ - |
+ When applying an enterprise security configuration to all repositories (for example, enabling Secret Scanning or Code Scanning across all repositories), the system immediately enqueues enablement jobs for every organization in the enterprise simultaneously. For enterprises with a large number of repositories, this can result in significant system load and potential performance degradation. If you manage a large enterprise with many organizations and repositories, we recommend applying security configurations at the organization level rather than at the enterprise level in the UI. This allows you to enable security features incrementally and monitor system performance as you roll out changes.
+ - |
+ On instances with multiple Git storage nodes in a voting configuration, including cluster and geo-replication high availability topologies, upgrading may fail to correctly install Actions that ship with the new version. In some cases, previous versions of these Actions remain on the instance. To resolve this issue, run the following commands on the primary node: `ghe-config --unset 'app.actions.actions-repos-sha1sum'`, `ghe-config-apply`, and `/usr/local/share/enterprise/ghe-run-init-actions-graph`.
+ - |
+ Users attempting to clone repositories via HTTPS receive an "Internal Server Error" message, and the clone operation fails with a 500 error. This issue affects all deployment topologies including cluster, standalone, high availability, and geo-replication configurations.
diff --git a/data/reusables/elm/locked-repo.md b/data/reusables/elm/locked-repo.md
index 7cd9ef640042..4377959ca7f7 100644
--- a/data/reusables/elm/locked-repo.md
+++ b/data/reusables/elm/locked-repo.md
@@ -1,5 +1,5 @@
-If a cutover fails partway through, the source repository may remain locked or archived. This prevents developers from pushing to the source while the destination may still be incomplete.
+If a cutover fails after the source repository has been archived, the {% data variables.product.prodname_elm_short %} service will attempt to unarchive the repository. If this fails, a repository administrator can unarchive the repository. See [AUTOTITLE](/repositories/archiving-a-github-repository/archiving-repositories#unarchiving-a-repository).
-To unlock the source repository, a site administrator must unlock it from the {% data variables.product.prodname_ghe_server %} {% data variables.enterprise.management_console %}.
+Be aware that unarchiving a repository will cause additional load on the instance, as all issues and pull requests in the repository will be reindexed in Elasticsearch.
-After the source is unlocked, you can either retry cutover using `elm migration cutover-to-destination --migration-id MIGRATION-ID`, or abort the migration with `elm migration cancel --migration-id MIGRATION-ID` and start a new migration when you're ready.
+After the source repository is unarchived, you can either retry cutover using `elm migration cutover-to-destination --migration-id MIGRATION-ID`, or abort the migration with `elm migration cancel --migration-id MIGRATION-ID` and start a new migration when you're ready.
From bdf26953dd719dc3a3f4d8885716cd390110dd51 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 12:55:18 -0700
Subject: [PATCH 06/21] Guard renderedPageHast against undefined in
ArticleContext (#61969)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/frame/components/context/ArticleContext.tsx | 4 ++--
1 file changed, 2 insertions(+), 2 deletions(-)
diff --git a/src/frame/components/context/ArticleContext.tsx b/src/frame/components/context/ArticleContext.tsx
index 43dc59e2d3f2..2b84f53b0a95 100644
--- a/src/frame/components/context/ArticleContext.tsx
+++ b/src/frame/components/context/ArticleContext.tsx
@@ -17,7 +17,7 @@ export type ArticleContextT = {
intro: string
effectiveDate: string
renderedPage: string | JSX.Element[]
- renderedPageHast?: import('hast').Root
+ renderedPageHast?: import('hast').Root | null
miniTocItems: Array
permissions?: string
includesPlatformSpecificContent: boolean
@@ -99,7 +99,7 @@ export const getArticleContextFromRequest = (req: ContextRequest): ArticleContex
intro: page.intro,
effectiveDate,
renderedPage: (req.context.renderedPage as string) || '',
- renderedPageHast: req.context.renderedPageHast,
+ renderedPageHast: req.context.renderedPageHast ?? null,
miniTocItems: req.context.miniTocItems || [],
permissions: (page.permissions as string) || '',
includesPlatformSpecificContent: (page.includesPlatformSpecificContent as boolean) || false,
From 7792c0ac44dd29a59efabece0f2b95954f45e557 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 12:55:25 -0700
Subject: [PATCH 07/21] Fix github/github link check token and make PR link
check non-blocking (#61986)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/workflows/link-check-external.yml | 9 ---------
.github/workflows/link-check-github-github.yml | 3 +++
.github/workflows/link-check-internal.yml | 3 ---
.github/workflows/link-check-on-pr.yml | 10 +++++-----
src/links/lib/link-report.ts | 2 +-
5 files changed, 9 insertions(+), 18 deletions(-)
diff --git a/.github/workflows/link-check-external.yml b/.github/workflows/link-check-external.yml
index 3c9877844c6c..a8440699ae87 100644
--- a/.github/workflows/link-check-external.yml
+++ b/.github/workflows/link-check-external.yml
@@ -61,15 +61,6 @@ jobs:
run: |
if [ -f "artifacts/external-link-report.md" ]; then
echo "has_report=true" >> $GITHUB_OUTPUT
- # Prepend disclaimer banner
- tmp=$(mktemp)
- {
- echo "> [!NOTE]"
- echo "> **No action needed right now.** The link checker is being actively worked on and may produce false positives. You can safely ignore this report for now. We'll let you know when it's reliable."
- echo ""
- cat "artifacts/external-link-report.md"
- } > "$tmp"
- mv "$tmp" "artifacts/external-link-report.md"
else
echo "has_report=false" >> $GITHUB_OUTPUT
echo "No broken link report generated - all links valid!"
diff --git a/.github/workflows/link-check-github-github.yml b/.github/workflows/link-check-github-github.yml
index fcbc1c11f1ee..b48532edd83e 100644
--- a/.github/workflows/link-check-github-github.yml
+++ b/.github/workflows/link-check-github-github.yml
@@ -55,6 +55,9 @@ jobs:
curl --retry-connrefused --retry 5 -I http://localhost:4000/
- name: Run broken github/github link check
+ env:
+ # Needs a token with access to github/github; the app token is scoped to it above
+ GITHUB_TOKEN: ${{ steps.app-token.outputs.token }}
run: |
npm run check-github-github-links -- broken_github_github_links.md
diff --git a/.github/workflows/link-check-internal.yml b/.github/workflows/link-check-internal.yml
index aedda28a08ea..1d8a394e6bae 100644
--- a/.github/workflows/link-check-internal.yml
+++ b/.github/workflows/link-check-internal.yml
@@ -248,9 +248,6 @@ jobs:
# Combine all markdown reports
echo "# Internal Links Report" > combined-report.md
echo "" >> combined-report.md
- echo "> [!NOTE]" >> combined-report.md
- echo "> **No action needed right now.** The link checker is being actively worked on and may produce false positives. You can safely ignore this report for now. We'll let you know when it's reliable." >> combined-report.md
- echo "" >> combined-report.md
echo "Generated: $(date -u +'%Y-%m-%d %H:%M UTC')" >> combined-report.md
echo "[Action run](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }})" >> combined-report.md
echo "" >> combined-report.md
diff --git a/.github/workflows/link-check-on-pr.yml b/.github/workflows/link-check-on-pr.yml
index 904596143a8f..6d77c5627e6a 100644
--- a/.github/workflows/link-check-on-pr.yml
+++ b/.github/workflows/link-check-on-pr.yml
@@ -8,7 +8,7 @@ on:
workflow_dispatch:
# merge_group:
pull_request:
- types: [labeled, opened, synchronize, reopened]
+ types: [opened, synchronize, reopened]
permissions:
contents: read
@@ -24,9 +24,7 @@ jobs:
check-links:
name: Check links
runs-on: ubuntu-latest
- if: |
- (github.repository == 'github/docs-internal' || github.repository == 'github/docs') &&
- (github.event_name == 'workflow_dispatch' || contains(github.event.pull_request.labels.*.name, 'check-links'))
+ if: github.repository == 'github/docs-internal' || github.repository == 'github/docs'
steps:
- name: Checkout
uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1
@@ -61,12 +59,14 @@ jobs:
- name: Check links in changed files
if: steps.changed-files.outputs.any_changed == 'true'
+ # Work in progress: never fail the PR. The comment is informational only.
+ continue-on-error: true
env:
FILES_CHANGED: ${{ steps.changed-files.outputs.all_changed_files }}
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
ACTION_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
SHOULD_COMMENT: ${{ secrets.DOCS_BOT_APP_ID != '' }}
- FAIL_ON_FLAW: true
+ FAIL_ON_FLAW: false
ENABLED_LANGUAGES: en
run: npm run check-links-pr
diff --git a/src/links/lib/link-report.ts b/src/links/lib/link-report.ts
index 8204a67669b5..ec60910225bc 100644
--- a/src/links/lib/link-report.ts
+++ b/src/links/lib/link-report.ts
@@ -123,7 +123,7 @@ ${rows}`
prComment: (errors: GroupedBrokenLinks[], warnings: GroupedBrokenLinks[], actionUrl?: string) => {
const errorSection =
errors.length > 0
- ? `### ❌ ${errors.length} Broken Link${errors.length === 1 ? '' : 's'}
+ ? `### ⚠️ ${errors.length} Broken Link${errors.length === 1 ? '' : 's'}
${errors
.map((group) => {
From 3aa41b0345418952a31a358e27a3c5086f6868e3 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 13:22:49 -0700
Subject: [PATCH 08/21] Add enterprise-releases to GitHub App token scope for
GHES sync workflows (#61987)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.github/workflows/enterprise-dates.yml | 2 +-
.github/workflows/enterprise-release-issue.yml | 2 +-
2 files changed, 2 insertions(+), 2 deletions(-)
diff --git a/.github/workflows/enterprise-dates.yml b/.github/workflows/enterprise-dates.yml
index f12bf24642d6..047618db6539 100644
--- a/.github/workflows/enterprise-dates.yml
+++ b/.github/workflows/enterprise-dates.yml
@@ -32,7 +32,7 @@ jobs:
app-id: ${{ secrets.DOCS_BOT_APP_ID }}
private-key: ${{ secrets.DOCS_BOT_APP_PRIVATE_KEY }}
owner: github
- repositories: docs-internal,docs-engineering
+ repositories: docs-internal,docs-engineering,enterprise-releases
- uses: ./.github/actions/node-npm-setup
diff --git a/.github/workflows/enterprise-release-issue.yml b/.github/workflows/enterprise-release-issue.yml
index 4b9f59498cb2..5b95ca5864d8 100644
--- a/.github/workflows/enterprise-release-issue.yml
+++ b/.github/workflows/enterprise-release-issue.yml
@@ -27,7 +27,7 @@ jobs:
app-id: ${{ secrets.DOCS_BOT_APP_ID }}
private-key: ${{ secrets.DOCS_BOT_APP_PRIVATE_KEY }}
owner: github
- repositories: docs-content,docs-engineering
+ repositories: docs-content,docs-engineering,enterprise-releases
- uses: ./.github/actions/node-npm-setup
From bfd94cf8af12308a6c2f9c8797a3b34f3c872d71 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 13:35:06 -0700
Subject: [PATCH 09/21] Render REST reference HTML via RenderedHTML (#6619)
(#61958)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/rest/components/RestAuth.tsx | 3 ++-
src/rest/components/RestBanner.tsx | 16 ++++++++--------
src/rest/components/RestCodeSamples.tsx | 15 ++++++---------
src/rest/components/RestMethod.tsx | 3 ++-
src/rest/components/RestOperation.tsx | 6 ++++--
src/rest/components/RestStatusCodes.tsx | 3 ++-
6 files changed, 24 insertions(+), 22 deletions(-)
diff --git a/src/rest/components/RestAuth.tsx b/src/rest/components/RestAuth.tsx
index cb3c49875e40..0f15102cf675 100644
--- a/src/rest/components/RestAuth.tsx
+++ b/src/rest/components/RestAuth.tsx
@@ -4,6 +4,7 @@ import { useTranslation } from '@/languages/components/useTranslation'
import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion'
import { Link } from '@/frame/components/Link'
import { ProgAccessT } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
// Documentation paths may be moved around by content team in the future
const USER_TOKEN_PATH =
@@ -69,7 +70,7 @@ export function RestAuth({ progAccess, slug, operationTitle }: Props) {
function NoFineGrainedAccess({ basicAuth }: { basicAuth: boolean }) {
const { t } = useTranslation('rest_reference')
- if (basicAuth) return
+ if (basicAuth) return
return {t('no_fine_grained_access')}
}
diff --git a/src/rest/components/RestBanner.tsx b/src/rest/components/RestBanner.tsx
index 77841e75a552..4e864c23a3fb 100644
--- a/src/rest/components/RestBanner.tsx
+++ b/src/rest/components/RestBanner.tsx
@@ -6,6 +6,7 @@ import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion'
import { Link } from '@/frame/components/Link'
import { useMainContext } from '@/frame/components/context/MainContext'
import { useTranslation } from '@/languages/components/useTranslation'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
const restRepoDisplayPages = [
'branches',
@@ -78,14 +79,13 @@ export const RestBanner = () => {
className="container-xl mt-3 mx-auto p-responsive"
>
- {' '}
- {' '}
+
diff --git a/src/rest/components/RestCodeSamples.tsx b/src/rest/components/RestCodeSamples.tsx
index 8e9afc54c774..de4f8bba123d 100644
--- a/src/rest/components/RestCodeSamples.tsx
+++ b/src/rest/components/RestCodeSamples.tsx
@@ -22,6 +22,7 @@ import type { Operation, ExampleT } from './types'
import { ResponseKeys, CodeSampleKeys } from './types'
import { useVersion } from '@/versions/components/useVersion'
import { useMainContext } from '@/frame/components/context/MainContext'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
slug: string
@@ -141,10 +142,7 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
{isEnterpriseCloud && selectedLanguage === CodeSampleKeys.curl ? (
-
+
) : null}
@@ -228,12 +226,11 @@ export function RestCodeSamples({ operation, slug, heading }: Props) {
{/* Response section */}
-
+ html={displayedExample.response.description || t('response')}
+ />
{displayedExample.response.schema ? (
{verb}
-
+
)
}
diff --git a/src/rest/components/RestOperation.tsx b/src/rest/components/RestOperation.tsx
index cae9e76497f8..83dd0ba8a618 100644
--- a/src/rest/components/RestOperation.tsx
+++ b/src/rest/components/RestOperation.tsx
@@ -13,6 +13,7 @@ import { RestAuth } from './RestAuth'
import { Operation } from './types'
import styles from './RestOperation.module.scss'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
operation: Operation
@@ -71,9 +72,10 @@ export function RestOperation({ operation }: Props) {
-
@@ -30,7 +31,7 @@ export function RestStatusCodes({ statusCodes, slug, heading }: Props) {
{statusCode.httpStatusCode}
-
+
))}
From 85ae7bdbf8bfef9ee0c122c586ba5eb26eddddba Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 13:36:39 -0700
Subject: [PATCH 10/21] Render webhook reference HTML via RenderedHTML (#6619)
(#61959)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/webhooks/components/Webhook.tsx | 30 ++++++++++++++---------------
1 file changed, 15 insertions(+), 15 deletions(-)
diff --git a/src/webhooks/components/Webhook.tsx b/src/webhooks/components/Webhook.tsx
index 66d5863e57dc..8ee3f5efe42e 100644
--- a/src/webhooks/components/Webhook.tsx
+++ b/src/webhooks/components/Webhook.tsx
@@ -13,6 +13,7 @@ import { ParameterTable } from '@/automated-pipelines/components/parameter-table
import { HighlightedCode } from '@/frame/components/HighlightedCode'
import styles from './WebhookPayloadExample.module.scss'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
webhook: WebhookAction
@@ -162,11 +163,10 @@ export function Webhook({ webhook }: Props) {
{currentWebhookAction.category}
-
-
+
{currentWebhookAction.availability.map((availability) => {
@@ -179,13 +179,12 @@ export function Webhook({ webhook }: Props) {
)
})}
-
{error && (
@@ -219,10 +218,11 @@ export function Webhook({ webhook }: Props) {
)}
-
+ html={currentWebhookAction.descriptionHtml}
+ />
Date: Tue, 30 Jun 2026 14:43:18 -0700
Subject: [PATCH 11/21] Render graphql reference HTML via RenderedHTML (#6619)
(#61957)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/graphql/components/BreakingChanges.tsx | 5 ++--
src/graphql/components/Changelog.tsx | 7 +++---
src/graphql/components/Enum.tsx | 7 ++----
src/graphql/components/GraphqlItem.tsx | 10 +++-----
src/graphql/components/Interface.tsx | 7 ++----
src/graphql/components/Mutation.tsx | 3 ++-
src/graphql/components/Notice.tsx | 7 ++----
src/graphql/components/Object.tsx | 5 ++--
src/graphql/components/Query.tsx | 8 +++---
src/graphql/components/Table.tsx | 29 +++++-----------------
10 files changed, 31 insertions(+), 57 deletions(-)
diff --git a/src/graphql/components/BreakingChanges.tsx b/src/graphql/components/BreakingChanges.tsx
index e7ec306b734b..2b6b71058dd0 100644
--- a/src/graphql/components/BreakingChanges.tsx
+++ b/src/graphql/components/BreakingChanges.tsx
@@ -2,6 +2,7 @@ import React from 'react'
import cx from 'classnames'
import { HeadingLink } from '@/frame/components/article/HeadingLink'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
import { BreakingChangesT } from './types'
import styles from '@/frame/components/ui/MarkdownContent/MarkdownContent.module.scss'
@@ -39,10 +40,10 @@ export function BreakingChanges({ schema, headings }: Props) {
A change will be made to {item.location}.
Description:
-
+
- Reason:
+ Reason:
diff --git a/src/graphql/components/Changelog.tsx b/src/graphql/components/Changelog.tsx
index a6f1d13a6ecb..ca57a9735d3b 100644
--- a/src/graphql/components/Changelog.tsx
+++ b/src/graphql/components/Changelog.tsx
@@ -3,6 +3,7 @@ import cx from 'classnames'
import GithubSlugger from 'github-slugger'
import { HeadingLink } from '@/frame/components/article/HeadingLink'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
import { ChangelogItemT } from './types'
import styles from '@/frame/components/ui/MarkdownContent/MarkdownContent.module.scss'
@@ -47,7 +48,7 @@ export function Changelog({ changelogItems, years, currentYear }: Props) {
{change.title}
{change.changes.map((changeItem) => (
-
+
))}
@@ -57,7 +58,7 @@ export function Changelog({ changelogItems, years, currentYear }: Props) {
{change.title}
{change.changes.map((changeItem) => (
-
+
))}
@@ -67,7 +68,7 @@ export function Changelog({ changelogItems, years, currentYear }: Props) {
{change.title}
{change.changes.map((changeItem) => (
-
+
))}
diff --git a/src/graphql/components/Enum.tsx b/src/graphql/components/Enum.tsx
index 3ae0633f99d9..68d802f131ae 100644
--- a/src/graphql/components/Enum.tsx
+++ b/src/graphql/components/Enum.tsx
@@ -1,6 +1,7 @@
import { useTranslation } from '@/languages/components/useTranslation'
import { GraphqlItem } from './GraphqlItem'
import type { EnumT } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
item: EnumT
@@ -26,11 +27,7 @@ export function Enum({ item, headingLevel = 2 }: Props) {
{value.name}
-
+
))}
diff --git a/src/graphql/components/GraphqlItem.tsx b/src/graphql/components/GraphqlItem.tsx
index d7b9f59ebc6f..42c0f5361e5c 100644
--- a/src/graphql/components/GraphqlItem.tsx
+++ b/src/graphql/components/GraphqlItem.tsx
@@ -5,6 +5,7 @@ import type { GraphqlT } from './types'
import { Notice } from './Notice'
import type { SchemaKindKey } from '@/graphql/lib/categories'
import { KIND_LABELS, KIND_SLUG_PREFIX } from '@/graphql/lib/categories'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
item: GraphqlT
@@ -46,12 +47,7 @@ export function GraphqlItem({ item, heading, children, headingLevel = 2, kind }:
{kindLabel}
)}
-
+
{hasNotice && (
@@ -59,7 +55,7 @@ export function GraphqlItem({ item, heading, children, headingLevel = 2, kind }:
{item.isDeprecated && }
)}
- {heading &&
}
+ {heading &&
}
{children}
>
)
diff --git a/src/graphql/components/Interface.tsx b/src/graphql/components/Interface.tsx
index da2510d6832f..d47e82fed056 100644
--- a/src/graphql/components/Interface.tsx
+++ b/src/graphql/components/Interface.tsx
@@ -3,6 +3,7 @@ import { GraphqlItem, headingTag } from './GraphqlItem'
import { Table } from './Table'
import { useTranslation } from '@/languages/components/useTranslation'
import type { ObjectT, InterfaceT } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
item: InterfaceT
@@ -37,11 +38,7 @@ export function Interface({ item, objects, headingLevel = 2 }: Props) {
{item.fields && (
<>
-
+
>
)}
diff --git a/src/graphql/components/Mutation.tsx b/src/graphql/components/Mutation.tsx
index 7827e6804cfa..28576c70e262 100644
--- a/src/graphql/components/Mutation.tsx
+++ b/src/graphql/components/Mutation.tsx
@@ -4,6 +4,7 @@ import { Notice } from './Notice'
import { useTranslation } from '@/languages/components/useTranslation'
import { Table } from './Table'
import type { MutationT } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
import React from 'react'
type Props = {
@@ -35,7 +36,7 @@ export function Mutation({ item, headingLevel = 2 }: Props) {
{input.preview &&
}
{input.isDeprecated &&
}
-
+
))}
diff --git a/src/graphql/components/Notice.tsx b/src/graphql/components/Notice.tsx
index 154447a5a7af..48a69fbed1b6 100644
--- a/src/graphql/components/Notice.tsx
+++ b/src/graphql/components/Notice.tsx
@@ -2,6 +2,7 @@ import { Link } from '@/frame/components/Link'
import { Alert } from '@/frame/components/ui/Alert'
import { useTranslation } from '@/languages/components/useTranslation'
import type { GraphqlT } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
item: GraphqlT
@@ -25,11 +26,7 @@ export function Notice({ item, variant = 'preview' }: Props) {
{item.name} is deprecated.
-
+
) : null}
diff --git a/src/graphql/components/Object.tsx b/src/graphql/components/Object.tsx
index 4c1108ba1589..76f62de9a56b 100644
--- a/src/graphql/components/Object.tsx
+++ b/src/graphql/components/Object.tsx
@@ -3,6 +3,7 @@ import { GraphqlItem, headingTag } from './GraphqlItem'
import { Table } from './Table'
import { useTranslation } from '@/languages/components/useTranslation'
import type { ObjectT, ImplementsT } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
item: ObjectT
@@ -19,7 +20,7 @@ export function Object({ item, headingLevel = 2 }: Props) {
{item.implements && (
<>
-
+
{item.implements.map((implement: ImplementsT) => (
@@ -36,7 +37,7 @@ export function Object({ item, headingLevel = 2 }: Props) {
{item.fields && (
<>
-
+
>
)}
diff --git a/src/graphql/components/Query.tsx b/src/graphql/components/Query.tsx
index ed59b5665404..7aa29ec020fc 100644
--- a/src/graphql/components/Query.tsx
+++ b/src/graphql/components/Query.tsx
@@ -3,6 +3,7 @@ import { GraphqlItem, headingTag } from './GraphqlItem'
import { Table } from './Table'
import { useTranslation } from '@/languages/components/useTranslation'
import type { QueryT } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
item: QueryT
@@ -24,10 +25,9 @@ export function Query({ item, headingLevel = 2 }: Props) {
{item.args.length > 0 && (
<>
-
>
diff --git a/src/graphql/components/Table.tsx b/src/graphql/components/Table.tsx
index 3610795c5239..3377cce2b091 100644
--- a/src/graphql/components/Table.tsx
+++ b/src/graphql/components/Table.tsx
@@ -2,6 +2,7 @@ import { Link } from '@/frame/components/Link'
import { Notice } from './Notice'
import { useTranslation } from '@/languages/components/useTranslation'
import { FieldT } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
fields: FieldT[]
@@ -39,15 +40,7 @@ export function Table({ fields }: Props) {
- {field.description ? (
-
- ) : (
- 'N/A'
- )}
+ {field.description ? : 'N/A'}
{field.defaultValue !== undefined && (
The default value is {field.defaultValue.toString()}.
@@ -58,14 +51,10 @@ export function Table({ fields }: Props) {
{field.arguments && (
-
{field.arguments.map((argument, index) => (
)
- {
-
- }
+
{argument.defaultValue !== undefined && (
The default value is {argument.defaultValue.toString()}.
From f2abf8baba1d1bc07e52ddf385efa90d55ded6c8 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 14:43:21 -0700
Subject: [PATCH 12/21] Render landing-page HTML via RenderedHTML (#6619)
(#61960)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/landings/components/LandingSection.tsx | 5 ++---
src/landings/components/TableOfContents.tsx | 5 ++---
src/landings/components/shared/LandingCarousel.tsx | 8 ++------
src/landings/components/shared/LandingHero.tsx | 3 ++-
4 files changed, 8 insertions(+), 13 deletions(-)
diff --git a/src/landings/components/LandingSection.tsx b/src/landings/components/LandingSection.tsx
index 35902d4bda7b..9a8917afe19f 100644
--- a/src/landings/components/LandingSection.tsx
+++ b/src/landings/components/LandingSection.tsx
@@ -1,6 +1,7 @@
import React from 'react'
import cx from 'classnames'
import { HeadingLink } from '@/frame/components/article/HeadingLink'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
title?: string
@@ -18,9 +19,7 @@ export const LandingSection = ({ title, children, className, sectionLink, descri
{title}
)}
- {description && (
-
- )}
+ {description && }
{children}
diff --git a/src/landings/components/TableOfContents.tsx b/src/landings/components/TableOfContents.tsx
index 32df313e810d..ba8d5a909c3b 100644
--- a/src/landings/components/TableOfContents.tsx
+++ b/src/landings/components/TableOfContents.tsx
@@ -2,6 +2,7 @@ import React from 'react'
import { Link } from '@/frame/components/Link'
import type { TocItem } from '@/landings/types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
items: Array
@@ -27,9 +28,7 @@ export const TableOfContents = (props: Props) => {
{title}
- {intro && (
-
- )}
+ {intro && }
)
})}
diff --git a/src/landings/components/shared/LandingCarousel.tsx b/src/landings/components/shared/LandingCarousel.tsx
index b549b3e560eb..62419e76d265 100644
--- a/src/landings/components/shared/LandingCarousel.tsx
+++ b/src/landings/components/shared/LandingCarousel.tsx
@@ -6,6 +6,7 @@ import type { ResolvedArticle } from '@/types'
import { useTranslation } from '@/languages/components/useTranslation'
import { useVersion } from '@/versions/components/useVersion'
import styles from './LandingCarousel.module.scss'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type LandingCarouselProps = {
heading?: string
@@ -177,12 +178,7 @@ export const LandingCarousel = ({
{article.title}
-
+
))}
diff --git a/src/landings/components/shared/LandingHero.tsx b/src/landings/components/shared/LandingHero.tsx
index 25f45650a51c..148e2a2d4468 100644
--- a/src/landings/components/shared/LandingHero.tsx
+++ b/src/landings/components/shared/LandingHero.tsx
@@ -1,6 +1,7 @@
import { LinkExternalIcon } from '@primer/octicons-react'
import styles from './LandingHero.module.scss'
import { useTranslation } from '@/languages/components/useTranslation'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type LandingHeroProps = {
title: string
@@ -33,7 +34,7 @@ export const LandingHero = ({ title, intro, heroImage, introLinks }: LandingHero
{title}
{intro && (
)}
{(primaryAction || secondaryAction) && (
From a94e87468a3799bc696a1fa9b454b964d98721bb Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 14:43:24 -0700
Subject: [PATCH 13/21] Render GHES release-notes HTML via RenderedHTML (#6619)
(#61961)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../components/GHESReleaseNotePatch.tsx | 17 +++++------------
src/release-notes/components/PatchNotes.tsx | 5 +++--
2 files changed, 8 insertions(+), 14 deletions(-)
diff --git a/src/release-notes/components/GHESReleaseNotePatch.tsx b/src/release-notes/components/GHESReleaseNotePatch.tsx
index 9da1f480a82a..25a52ba80fb7 100644
--- a/src/release-notes/components/GHESReleaseNotePatch.tsx
+++ b/src/release-notes/components/GHESReleaseNotePatch.tsx
@@ -7,6 +7,7 @@ import { Link } from '@/frame/components/Link'
import { CurrentVersion, ReleaseNotePatch, GHESMessage } from './types'
import styles from './PatchNotes.module.scss'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
patch: ReleaseNotePatch
@@ -57,36 +58,28 @@ export function GHESReleaseNotePatch({
{patch.version !== latestPatch && currentVersion.currentRelease === latestRelease && (
- {' '}
+ {' '}
{t('notices.release_notes_use_latest')}
)}
{patch.version === latestPatch && currentVersion.currentRelease !== latestRelease && (
- {' '}
+ {' '}
{t('notices.release_notes_use_latest')}
)}
{patch.version !== latestPatch && currentVersion.currentRelease !== latestRelease && (
- {' '}
+ {' '}
{t('notices.release_notes_use_latest')}
)}
diff --git a/src/release-notes/components/PatchNotes.tsx b/src/release-notes/components/PatchNotes.tsx
index 7fead859162f..4d3e1dc5857b 100644
--- a/src/release-notes/components/PatchNotes.tsx
+++ b/src/release-notes/components/PatchNotes.tsx
@@ -1,6 +1,7 @@
import { slug } from 'github-slugger'
import { ReleaseNotePatch } from './types'
import { HeadingLink } from '@/frame/components/article/HeadingLink'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
const SectionToLabelMap: Record = {
features: 'Features',
@@ -32,7 +33,7 @@ export function PatchNotes({ patch }: Props) {
{sectionItems.map((item, i) => {
if (typeof item === 'string') {
- return
+ return
}
const headingSlug = item.heading ? slug(item.heading) : `heading${i}`
@@ -43,7 +44,7 @@ export function PatchNotes({ patch }: Props) {
{item.notes.map((note) => {
- return
+ return
})}
From 382e828f9d7c8a7c51b81532ab17ff749fb3fc47 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 14:43:27 -0700
Subject: [PATCH 14/21] Render automated-pipelines parameter HTML via
RenderedHTML (#6619) (#61962)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
.../components/parameter-table/ParameterRow.tsx | 3 ++-
1 file changed, 2 insertions(+), 1 deletion(-)
diff --git a/src/automated-pipelines/components/parameter-table/ParameterRow.tsx b/src/automated-pipelines/components/parameter-table/ParameterRow.tsx
index 67d959f0df85..823b8f361dc6 100644
--- a/src/automated-pipelines/components/parameter-table/ParameterRow.tsx
+++ b/src/automated-pipelines/components/parameter-table/ParameterRow.tsx
@@ -3,6 +3,7 @@ import cx from 'classnames'
import { useTranslation } from '@/languages/components/useTranslation'
import { ChildBodyParametersRows } from './ChildBodyParametersRows'
import type { ChildParameter } from './types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type Props = {
rowParams: ChildParameter
@@ -83,7 +84,7 @@ export function ParameterRow({
)
})}
diff --git a/src/frame/components/ui/Alert/Alert.tsx b/src/frame/components/ui/Alert/Alert.tsx
index ea6e20a7e2d5..497da298e148 100644
--- a/src/frame/components/ui/Alert/Alert.tsx
+++ b/src/frame/components/ui/Alert/Alert.tsx
@@ -3,6 +3,7 @@ import cx from 'classnames'
import styles from './Alert.module.scss'
import { InfoIcon, ReportIcon, AlertIcon, LightBulbIcon, StopIcon } from '@primer/octicons-react'
import { useTranslation } from '@/languages/components/useTranslation'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML'
const alertTypes = {
NOTE: { icon: InfoIcon, color: 'accent' },
@@ -32,7 +33,7 @@ export function Alert({ className, html, children, type = 'IMPORTANT' }: AlertPr
{createElement(alertTypes[type].icon, { size: 16, className: 'mr-2' })}
{t(type)}
- {html ?
: children}
+ {html ? : children}
)
}
diff --git a/src/frame/components/ui/Lead/Lead.tsx b/src/frame/components/ui/Lead/Lead.tsx
index 96b735db3da1..96a58a8eae7f 100644
--- a/src/frame/components/ui/Lead/Lead.tsx
+++ b/src/frame/components/ui/Lead/Lead.tsx
@@ -2,6 +2,7 @@ import { ReactNode } from 'react'
import type { JSX } from 'react'
import cx from 'classnames'
import styles from './Lead.module.scss'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML'
export type LeadPropsT = {
children: string | ReactNode
@@ -10,14 +11,13 @@ export type LeadPropsT = {
}
export function Lead({ children, className, as: Component = 'div', ...restProps }: LeadPropsT) {
- return (
-
- )
+ const sharedProps = {
+ className: cx('f2 color-fg-muted mb-3', styles.container, className),
+ 'data-container': 'lead',
+ ...restProps,
+ }
+ if (typeof children === 'string') {
+ return
+ }
+ return {children}
}
diff --git a/src/frame/components/ui/PermissionsStatement/PermissionsStatement.tsx b/src/frame/components/ui/PermissionsStatement/PermissionsStatement.tsx
index de59fb55faa2..86af182383e9 100644
--- a/src/frame/components/ui/PermissionsStatement/PermissionsStatement.tsx
+++ b/src/frame/components/ui/PermissionsStatement/PermissionsStatement.tsx
@@ -3,6 +3,7 @@ import { PersonIcon, BriefcaseIcon } from '@primer/octicons-react'
import { useTranslation } from '@/languages/components/useTranslation'
import styles from './PermissionsStatement.module.scss'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML'
type Props = {
product?: string
@@ -21,13 +22,13 @@ export function PermissionsStatement({ product, permissions }: Props) {
{permissions && (
)}
{product && (
)}
From 52577164f116c862ffcca77fa62c20b3b3d4993a Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 14:43:34 -0700
Subject: [PATCH 16/21] Render remaining single-use HTML via RenderedHTML
(#6619) (#61964)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/events/components/Survey.tsx | 6 ++----
src/journeys/components/JourneyTrackCard.tsx | 6 ++----
src/versions/components/DeprecationBanner.tsx | 9 +++------
3 files changed, 7 insertions(+), 14 deletions(-)
diff --git a/src/events/components/Survey.tsx b/src/events/components/Survey.tsx
index dfc9e9d9860d..624e717494bc 100644
--- a/src/events/components/Survey.tsx
+++ b/src/events/components/Survey.tsx
@@ -9,6 +9,7 @@ import { sendEvent } from '@/events/components/events'
import { EventType } from '../types'
import styles from './Survey.module.scss'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
enum ViewState {
START = 'START',
@@ -219,10 +220,7 @@ export const Survey = () => {
)}
-
+
{journey.alternativeNextStep && (
-
+
)}
diff --git a/src/versions/components/DeprecationBanner.tsx b/src/versions/components/DeprecationBanner.tsx
index 66b72cc5230a..48b1e7311ed4 100644
--- a/src/versions/components/DeprecationBanner.tsx
+++ b/src/versions/components/DeprecationBanner.tsx
@@ -5,6 +5,7 @@ import { Flash } from '@primer/react'
import cx from 'classnames'
import styles from './DeprecationBanner.module.scss'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
export const DeprecationBanner = () => {
const { data, enterpriseServerReleases } = useMainContext()
@@ -32,17 +33,13 @@ export const DeprecationBanner = () => {
- {' '}
+ {' '}
{enterpriseServerReleases.nextDeprecationDate}
.
{' '}
-
+
From 301802c347a3dccc8810eace99fd32b739503e9c Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 14:43:37 -0700
Subject: [PATCH 17/21] Render search HTML via RenderedHTML (#6619) (#61965)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
data/ui.yml | 2 +-
src/fixtures/fixtures/data/ui.yml | 2 +-
src/search/components/input/AskAIResults.tsx | 3 ++-
src/search/components/input/SearchOverlay.tsx | 6 ++++--
src/search/components/results/SearchResults.tsx | 10 +++++++---
5 files changed, 15 insertions(+), 8 deletions(-)
diff --git a/data/ui.yml b/data/ui.yml
index 4eba450206e8..3c0042112d20 100644
--- a/data/ui.yml
+++ b/data/ui.yml
@@ -54,7 +54,7 @@ search:
search_docs_with_query: Search docs for "{{query}}"
privacy_disclaimer: For product and service improvement purposes, the GitHub Docs team will retain questions and answers generated in the Docs search function. Please see the GitHub Privacy Statement to review how GitHub collects and uses your data.
ai:
- disclaimer: Copilot uses AI. Check for mistakes.
+ disclaimer: Copilot uses AI. Check for mistakes.
references: Copilot Sources
loading_status_message: Loading Copilot response...
done_loading_status_message: Done loading Copilot response
diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml
index 4eba450206e8..3c0042112d20 100644
--- a/src/fixtures/fixtures/data/ui.yml
+++ b/src/fixtures/fixtures/data/ui.yml
@@ -54,7 +54,7 @@ search:
search_docs_with_query: Search docs for "{{query}}"
privacy_disclaimer: For product and service improvement purposes, the GitHub Docs team will retain questions and answers generated in the Docs search function. Please see the GitHub Privacy Statement to review how GitHub collects and uses your data.
ai:
- disclaimer: Copilot uses AI. Check for mistakes.
+ disclaimer: Copilot uses AI. Check for mistakes.
references: Copilot Sources
loading_status_message: Loading Copilot response...
done_loading_status_message: Done loading Copilot response
diff --git a/src/search/components/input/AskAIResults.tsx b/src/search/components/input/AskAIResults.tsx
index 754a6d298534..eefd71c2d6be 100644
--- a/src/search/components/input/AskAIResults.tsx
+++ b/src/search/components/input/AskAIResults.tsx
@@ -24,6 +24,7 @@ import { generateAISearchLinksJson } from '../helpers/ai-search-links-json'
import { ASK_AI_EVENT_GROUP } from '@/events/components/event-groups'
import type { AIReference } from '../types'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
type AIQueryResultsProps = {
query: string
@@ -398,7 +399,7 @@ export function AskAIResults({
{!aiCouldNotAnswer && message !== '' ? (
-
+
) : null}
diff --git a/src/search/components/results/SearchResults.tsx b/src/search/components/results/SearchResults.tsx
index 828bee21b853..4e8e2803cf2a 100644
--- a/src/search/components/results/SearchResults.tsx
+++ b/src/search/components/results/SearchResults.tsx
@@ -15,6 +15,9 @@ import type { SearchQueryContentT } from '@/search/components/types'
import type { GeneralSearchHitWithoutIncludes, GeneralSearchResponse } from '@/search/types'
import type { estypes } from '@elastic/elasticsearch'
import { GENERAL_SEARCH_RESULTS } from '@/events/components/event-groups'
+import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
+import { renderHTMLString } from '@/frame/components/ui/RenderedHTML/render-html-string'
+import { markdownComponents } from '@/frame/components/ui/MarkdownContent/markdownComponents'
type Props = {
results: GeneralSearchResponse
@@ -118,7 +121,6 @@ function SearchResultHit({
{
sendEvent({
@@ -132,9 +134,11 @@ function SearchResultHit({
eventGroupId: eventGroupId.current,
})
}}
- >
+ >
+ {renderHTMLString(title, markdownComponents)}
+
- {content &&
}
+ {content &&
}
{debug && (
score: {hit.score} popularity:{' '}
From 6c0d9ea1d33683673d7468fc335a666bfaf45cb3 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 14:43:40 -0700
Subject: [PATCH 18/21] Render TOC-landing, automated, and REST page bodies
from hast (#6619) (#61966)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/automated-pipelines/components/AutomatedPage.tsx | 7 ++++++-
.../components/AutomatedPageContext.tsx | 2 ++
src/frame/components/context/TocLandingContext.tsx | 3 +++
src/landings/components/TocLanding.tsx | 7 +++++--
src/rest/components/RestReferencePage.tsx | 9 +++++++--
5 files changed, 23 insertions(+), 5 deletions(-)
diff --git a/src/automated-pipelines/components/AutomatedPage.tsx b/src/automated-pipelines/components/AutomatedPage.tsx
index 607eb67be157..8c3208d7b6e9 100644
--- a/src/automated-pipelines/components/AutomatedPage.tsx
+++ b/src/automated-pipelines/components/AutomatedPage.tsx
@@ -22,6 +22,7 @@ export const AutomatedPage = ({ children, rawChildren, fullWidth }: Props) => {
title,
intro,
renderedPage,
+ renderedPageHast,
miniTocItems,
product,
permissions,
@@ -33,7 +34,11 @@ export const AutomatedPage = ({ children, rawChildren, fullWidth }: Props) => {
const articleContents = (
- {renderedPage &&
{renderedPage} }
+ {(renderedPage || renderedPageHast) && (
+
+ {renderedPage}
+
+ )}
{children &&
{children} }
{rawChildren &&
{rawChildren}
}
diff --git a/src/automated-pipelines/components/AutomatedPageContext.tsx b/src/automated-pipelines/components/AutomatedPageContext.tsx
index 03fb1d944d91..7a5e5c9d189e 100644
--- a/src/automated-pipelines/components/AutomatedPageContext.tsx
+++ b/src/automated-pipelines/components/AutomatedPageContext.tsx
@@ -9,6 +9,7 @@ export type AutomatedPageContextT = {
title: string
intro: string
renderedPage: string | JSX.Element[]
+ renderedPageHast?: import('hast').Root | null
miniTocItems: Array
product?: string
permissions?: string
@@ -57,6 +58,7 @@ export const getAutomatedPageContextFromRequest = (
title: page.title,
intro: page.intro,
renderedPage,
+ renderedPageHast: context.renderedPageHast ?? null,
miniTocItems,
product: page.product ?? '',
permissions: page.permissions ?? page.rawPermissions ?? '',
diff --git a/src/frame/components/context/TocLandingContext.tsx b/src/frame/components/context/TocLandingContext.tsx
index 05a4b2a92706..7864f3bbbf5b 100644
--- a/src/frame/components/context/TocLandingContext.tsx
+++ b/src/frame/components/context/TocLandingContext.tsx
@@ -12,6 +12,7 @@ export type TocLandingContextT = {
variant?: 'compact' | 'expanded'
featuredLinks: Record>
renderedPage: string
+ renderedPageHast?: import('hast').Root | null
}
export const TocLandingContext = createContext(null)
@@ -34,6 +35,7 @@ interface ContextRequest {
genericTocFlat?: unknown[]
genericTocNested?: unknown[]
renderedPage?: string
+ renderedPageHast?: import('hast').Root | null
featuredLinks?: Record
[key: string]: unknown
}
@@ -52,5 +54,6 @@ export const getTocLandingContextFromRequest = (req: ContextRequest): TocLanding
variant: req.context.genericTocFlat ? 'expanded' : 'compact',
featuredLinks: getFeaturedLinksFromReq(req),
renderedPage: (req.context.renderedPage as string) || '',
+ renderedPageHast: req.context.renderedPageHast ?? null,
}
}
diff --git a/src/landings/components/TocLanding.tsx b/src/landings/components/TocLanding.tsx
index 67306e8b1a11..f1812f3cde87 100644
--- a/src/landings/components/TocLanding.tsx
+++ b/src/landings/components/TocLanding.tsx
@@ -26,6 +26,7 @@ export const TocLanding = () => {
variant,
featuredLinks,
renderedPage,
+ renderedPageHast,
permissions,
} = useTocLandingContext()
const { t } = useTranslation('toc')
@@ -69,9 +70,11 @@ export const TocLanding = () => {
)}
- {renderedPage && (
+ {(renderedPage || renderedPageHast) && (
- {renderedPage}
+
+ {renderedPage}
+
)}
diff --git a/src/rest/components/RestReferencePage.tsx b/src/rest/components/RestReferencePage.tsx
index 452588921d8b..d0988155aad2 100644
--- a/src/rest/components/RestReferencePage.tsx
+++ b/src/rest/components/RestReferencePage.tsx
@@ -16,7 +16,8 @@ export type StructuredContentT = {
}
export const RestReferencePage = ({ restOperations }: StructuredContentT) => {
- const { title, intro, renderedPage, permissions, product } = useAutomatedPageContext()
+ const { title, intro, renderedPage, renderedPageHast, permissions, product } =
+ useAutomatedPageContext()
// Scrollable code blocks in our REST API docs and elsewhere aren't accessible
// via keyboard navigation without setting tabindex="0". But we don't want to set
@@ -56,7 +57,11 @@ export const RestReferencePage = ({ restOperations }: StructuredContentT) => {
- {renderedPage && {renderedPage} }
+ {(renderedPage || renderedPageHast) && (
+
+ {renderedPage}
+
+ )}
{restOperations.length > 0 && (
{restOperations.map((operation) => (
From 26dcde1a0c4e5ba9f60c0c8c20ce5354754f8a22 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 14:43:44 -0700
Subject: [PATCH 19/21] Render the MarkdownContent string fallback
React-natively and drop the imperative enhancers (#6619) (#61967)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
---
src/frame/components/CodeTabs.tsx | 189 ------------------
src/frame/components/article/ArticlePage.tsx | 4 -
.../ui/MarkdownContent/MarkdownContent.tsx | 14 +-
src/tools/components/PlatformPicker.tsx | 41 +---
src/tools/components/ToolPicker.tsx | 46 +----
5 files changed, 16 insertions(+), 278 deletions(-)
delete mode 100644 src/frame/components/CodeTabs.tsx
diff --git a/src/frame/components/CodeTabs.tsx b/src/frame/components/CodeTabs.tsx
deleted file mode 100644
index c24951596b23..000000000000
--- a/src/frame/components/CodeTabs.tsx
+++ /dev/null
@@ -1,189 +0,0 @@
-import React, { useCallback, useEffect, useLayoutEffect, useState } from 'react'
-import { createPortal } from 'react-dom'
-import { useRouter } from 'next/router'
-import { UnderlineNav } from '@primer/react'
-
-import Cookies from '@/frame/components/lib/cookies'
-import { CODE_SAMPLE_LANGUAGE_COOKIE_NAME } from '@/frame/lib/constants'
-import { sendEvent } from '@/events/components/events'
-import { EventType } from '@/events/types'
-import { useTranslation } from '@/languages/components/useTranslation'
-
-// This component shares the code_sample_language_preferred cookie with the
-// REST API code sample tabs. The only overlapping language key is 'javascript'.
-// If a user selects a REST-only value like 'curl', code tabs gracefully fall
-// back to the first available language per group. This is intentional and
-// works the same in the other direction (SDK value in a REST context).
-
-type TabData = {
- key: string
- label: string
- panel: HTMLElement
-}
-
-type GroupData = {
- mountPoint: HTMLDivElement
- tabs: TabData[]
-}
-
-const useIsomorphicLayoutEffect = typeof window !== 'undefined' ? useLayoutEffect : useEffect
-
-function getPanels(container: HTMLElement): HTMLElement[] {
- return Array.from(container.children).filter(
- (child): child is HTMLElement =>
- child instanceof HTMLElement && child.classList.contains('ghd-codetab'),
- )
-}
-
-function sanitizeForId(value: string): string {
- return value.replace(/[^a-z0-9_-]+/gi, '-').replace(/(^-|-$)/g, '') || 'article'
-}
-
-function getActiveKey(tabs: TabData[], selectedLanguage: string): string {
- return tabs.some((tab) => tab.key === selectedLanguage) ? selectedLanguage : (tabs[0]?.key ?? '')
-}
-
-function updatePanelVisibility(groups: GroupData[], languageKey: string): void {
- for (const group of groups) {
- const activeKey = getActiveKey(group.tabs, languageKey)
- for (const tab of group.tabs) {
- const isActive = tab.key === activeKey
- tab.panel.hidden = !isActive
- tab.panel.classList.toggle('ghd-codetab-hidden', !isActive)
- }
- }
-}
-
-export const CodeTabs = () => {
- const router = useRouter()
- const { t } = useTranslation('code_tabs')
- const [groups, setGroups] = useState([])
- const [selectedLanguage, setSelectedLanguage] = useState('')
-
- useIsomorphicLayoutEffect(() => {
- const articleContents = document.getElementById('article-contents')
- if (!articleContents) {
- setGroups([])
- return
- }
-
- const containers = Array.from(articleContents.querySelectorAll('.ghd-codetabs'))
- if (!containers.length) {
- setGroups([])
- return
- }
-
- const pageId = sanitizeForId(router.asPath.split(/[?#]/)[0])
- const newGroups: GroupData[] = []
-
- for (const [groupIndex, container] of containers.entries()) {
- // Reset any previous enhancement before re-enhancing
- container.classList.remove('ghd-codetabs-enhanced')
- for (const el of container.querySelectorAll(':scope > .ghd-codetabs-nav')) {
- el.remove()
- }
- for (const panel of getPanels(container)) {
- panel.hidden = false
- panel.classList.remove('ghd-codetab-hidden')
- panel.removeAttribute('role')
- panel.removeAttribute('tabindex')
- panel.removeAttribute('aria-labelledby')
- panel.removeAttribute('id')
- }
-
- const panels = getPanels(container)
- if (!panels.length) continue
-
- const mountPoint = document.createElement('div')
- mountPoint.className = 'ghd-codetabs-nav'
- container.insertBefore(mountPoint, container.firstChild)
- container.classList.add('ghd-codetabs-enhanced')
-
- const tabs: TabData[] = panels
- .map((panel, panelIndex) => {
- const key = panel.dataset.lang
- const label = panel.dataset.label
- if (!key || !label) return null
-
- const panelId = `ghd-codetabs-${pageId}-${groupIndex}-panel-${panelIndex}`
- panel.id = panelId
- panel.setAttribute('role', 'tabpanel')
- panel.setAttribute('tabindex', '0')
-
- return { key, label, panel }
- })
- .filter((tab): tab is TabData => tab !== null)
-
- if (!tabs.length) continue
- newGroups.push({ mountPoint, tabs })
- }
-
- const cookieValue = Cookies.get(CODE_SAMPLE_LANGUAGE_COOKIE_NAME)
- const initialLang = cookieValue || (newGroups[0]?.tabs[0]?.key ?? '')
-
- // Apply initial visibility synchronously before browser paint
- updatePanelVisibility(newGroups, initialLang)
-
- setGroups(newGroups)
- setSelectedLanguage(initialLang)
-
- return () => {
- for (const group of newGroups) {
- group.mountPoint.remove()
- for (const tab of group.tabs) {
- tab.panel.hidden = false
- tab.panel.classList.remove('ghd-codetab-hidden')
- tab.panel.removeAttribute('role')
- tab.panel.removeAttribute('tabindex')
- tab.panel.removeAttribute('aria-labelledby')
- tab.panel.removeAttribute('id')
- }
- }
- }
- }, [router.asPath])
-
- const handleSelect = useCallback(
- (key: string) => {
- updatePanelVisibility(groups, key)
- setSelectedLanguage(key)
- Cookies.set(CODE_SAMPLE_LANGUAGE_COOKIE_NAME, key)
- sendEvent({
- type: EventType.preference,
- preference_name: 'code_language',
- preference_value: key,
- })
- },
- [groups],
- )
-
- if (!groups.length) return null
-
- return (
- <>
- {groups.map((group, groupIndex) =>
- createPortal(
-
- {group.tabs.map((tab) => {
- const activeKey = getActiveKey(group.tabs, selectedLanguage)
- return (
- {
- event.preventDefault()
- handleSelect(tab.key)
- }}
- >
- {tab.label}
-
- )
- })}
- ,
- group.mountPoint,
- `codetabs-${groupIndex}`,
- ),
- )}
- >
- )
-}
diff --git a/src/frame/components/article/ArticlePage.tsx b/src/frame/components/article/ArticlePage.tsx
index 68a6a826597e..c2c58c042e8b 100644
--- a/src/frame/components/article/ArticlePage.tsx
+++ b/src/frame/components/article/ArticlePage.tsx
@@ -17,7 +17,6 @@ import { RestRedirect } from '@/rest/components/RestRedirect'
import { Breadcrumbs } from '@/frame/components/page-header/Breadcrumbs'
import { LinkPreviewPopover } from '@/links/components/LinkPreviewPopover'
import { UtmPreserver } from '@/frame/components/UtmPreserver'
-import { CodeTabs } from '@/frame/components/CodeTabs'
import { JourneyTrackCard, JourneyTrackNav } from '@/journeys/components'
import { CopyMarkdownMenu } from './ViewMarkdownButton'
import { ExperimentContentSwap } from '@/events/components/experiments/ExperimentContentSwap'
@@ -103,9 +102,6 @@ export const ArticlePage = () => {
- {/* The imperative CodeTabs enhancer only runs for the string fallback
- path; on the hast path, CodeTabsGroup renders tabs React-natively. */}
- {!renderedPageHast && }
{isDev && }
{router.pathname.includes('/rest/') && }
{currentLayout === 'inline' ? (
diff --git a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx
index 77acd14a2abc..6ae4bd9dafe6 100644
--- a/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx
+++ b/src/frame/components/ui/MarkdownContent/MarkdownContent.tsx
@@ -6,6 +6,7 @@ import type { Root as HastRoot } from 'hast'
import cx from 'classnames'
import { markdownComponents } from './markdownComponents'
+import { renderHTMLString } from '@/frame/components/ui/RenderedHTML/render-html-string'
import styles from './MarkdownContent.module.scss'
export type MarkdownContentPropsT = {
@@ -15,9 +16,9 @@ export type MarkdownContentPropsT = {
as?: keyof JSX.IntrinsicElements
}
-// 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.
+// Memoized so that unrelated parent re-renders (e.g. ToolPicker/PlatformPicker
+// state updates) don't re-parse the HTML string and rebuild the body's element
+// tree, which is the expensive part of rendering an article.
export const MarkdownContent = memo(function MarkdownContent({
children,
hast,
@@ -25,12 +26,13 @@ export const MarkdownContent = memo(function MarkdownContent({
className,
...restProps
}: MarkdownContentPropsT) {
- // When a hast (HTML AST) tree is provided, render it as real React elements
- // instead of injecting an HTML string via dangerouslySetInnerHTML (#6619).
+ // Render trusted HTML as real React elements instead of injecting it as raw
+ // innerHTML (#6619). Prefer a hast (HTML AST) tree when provided; otherwise
+ // parse a rendered HTML string. Non-string children render as-is.
const childProps = hast
? { children: toJsxRuntime(hast, { Fragment, jsx, jsxs, components: markdownComponents }) }
: typeof children === 'string'
- ? { dangerouslySetInnerHTML: { __html: children } }
+ ? { children: renderHTMLString(children, markdownComponents) }
: { children }
return (
diff --git a/src/tools/components/PlatformPicker.tsx b/src/tools/components/PlatformPicker.tsx
index 4c95b7681711..b0ab8782712b 100644
--- a/src/tools/components/PlatformPicker.tsx
+++ b/src/tools/components/PlatformPicker.tsx
@@ -15,40 +15,8 @@ const platforms = [
// Nota bene: platform === os
-// Imperatively modify article content to show only the selected platform
-// find all platform-specific *block* elements and hide or show as appropriate
-// example: {% mac %} block content {% endmac %}
-function showPlatformSpecificContent(platform: string) {
- const markdowns = Array.from(document.querySelectorAll('.ghd-tool'))
- const platformMarkdowns = markdowns.filter((xel) =>
- platforms.some((platformValue) => xel.classList.contains(platformValue.value)),
- )
- for (const el of platformMarkdowns) {
- el.style.display = el.classList.contains(platform) ? '' : 'none'
-
- // hack: special handling for minitoc links -- we can't pass the tool classes
- // directly to the Primer NavList.Item generated , it gets passed down
- // to the child . So if we find an that has the tool class and its
- // parent is an , we hide/unhide that element as well.
- if (el.tagName === 'A' && el.parentElement && el.parentElement.tagName === 'LI') {
- el.parentElement.style.display = el.classList.contains(platform) ? '' : 'none'
- }
- }
-
- // find all platform-specific *inline* elements and hide or show as appropriate
- // example: inline content
- const platformEls = Array.from(
- document.querySelectorAll(
- platforms.map((platformOption) => `.platform-${platformOption.value}`).join(', '),
- ),
- )
- for (const el of platformEls) {
- el.style.display = el.classList.contains(`platform-${platform}`) ? '' : 'none'
- }
-}
-
export const PlatformPicker = () => {
- const { defaultPlatform, detectedPlatforms, renderedPageHast } = useArticleContext()
+ const { defaultPlatform, detectedPlatforms } = useArticleContext()
const { setPlatform } = useSelection()
const [defaultUA, setDefaultUA] = useState('')
@@ -78,11 +46,10 @@ export const PlatformPicker = () => {
cookieKey={OS_PREFERRED_COOKIE_NAME}
queryStringKey={platformQueryKey}
onValue={(value: string) => {
- // Drive visibility through React state on the hast path (#6619). Only the
- // string fallback (renderedPageHast undefined) still needs the imperative
- // DOM mutation, since that markup isn't React-owned.
+ // Visibility is driven by React state via ToggleableContent/MiniTocs
+ // (#6619); the article body is React-owned on both the hast and string
+ // paths, so no imperative DOM mutation is needed.
setPlatform(value)
- if (!renderedPageHast) showPlatformSpecificContent(value)
}}
preferenceName="os"
ariaLabel="Platform"
diff --git a/src/tools/components/ToolPicker.tsx b/src/tools/components/ToolPicker.tsx
index d9e465733e00..14a141a17924 100644
--- a/src/tools/components/ToolPicker.tsx
+++ b/src/tools/components/ToolPicker.tsx
@@ -1,5 +1,3 @@
-import { preserveAnchorNodePosition } from 'scroll-anchoring'
-
import { useArticleContext } from '@/frame/components/context/ArticleContext'
import { InArticlePicker } from './InArticlePicker'
import { useSelection } from './SelectionContext'
@@ -10,38 +8,6 @@ import { TOOL_PREFERRED_COOKIE_NAME } from '@/frame/lib/constants'
// Nota bene: tool === application
// Nota bene: picker === switcher
-// Imperatively modify article content to show only the selected tool
-// find all platform-specific *block* elements and hide or show as appropriate
-// example: {% webui %} block content {% endwebui %}
-function showToolSpecificContent(tool: string, supportedTools: Array) {
- const markdowns = Array.from(document.querySelectorAll('.ghd-tool'))
- const supportedMarkdowns = markdowns.filter((xel) =>
- supportedTools.some((toolName) => xel.classList.contains(toolName)),
- )
- for (const el of supportedMarkdowns) {
- el.style.display = el.classList.contains(tool) ? '' : 'none'
-
- // hack: special handling for minitoc links -- we can't pass the tool classes
- // directly to the Primer NavList.Item generated , it gets passed down
- // to the child . So if we find an that has the tool class and its
- // parent is an , we hide/unhide that element as well.
- if (el.tagName === 'A' && el.parentElement && el.parentElement.tagName === 'LI') {
- el.parentElement.style.display = el.classList.contains(tool) ? '' : 'none'
- }
- }
-
- // find all tool-specific *inline* elements and hide or show as appropriate
- // example: inline content
- const toolEls = Array.from(
- document.querySelectorAll(
- supportedTools.map((toolOption) => `.tool-${toolOption}`).join(', '),
- ),
- )
- for (const el of toolEls) {
- el.style.display = el.classList.contains(`tool-${tool}`) ? '' : 'none'
- }
-}
-
function getDefaultTool(defaultTool: string | undefined, detectedTools: Array): string {
// If there is a default tool and the tool is present on this page
if (defaultTool && detectedTools.includes(defaultTool)) return defaultTool
@@ -59,7 +25,7 @@ function getDefaultTool(defaultTool: string | undefined, detectedTools: Array {
// allTools comes from the ArticleContext which contains the list of tools available
- const { defaultTool, detectedTools, allTools, renderedPageHast } = useArticleContext()
+ const { defaultTool, detectedTools, allTools } = useArticleContext()
const { setTool } = useSelection()
if (!detectedTools.length) return null
@@ -74,14 +40,10 @@ export const ToolPicker = () => {
cookieKey={TOOL_PREFERRED_COOKIE_NAME}
queryStringKey={toolQueryKey}
onValue={(value: string) => {
- // Drive visibility through React state on the hast path (#6619). Only the
- // string fallback still needs the imperative DOM mutation.
+ // Visibility is driven by React state via ToggleableContent/MiniTocs
+ // (#6619); the article body is React-owned on both the hast and string
+ // paths, so no imperative DOM mutation is needed.
setTool(value)
- if (!renderedPageHast) {
- preserveAnchorNodePosition(document, () => {
- showToolSpecificContent(value, Object.keys(allTools))
- })
- }
}}
preferenceName="application"
ariaLabel="Tool"
From c1cab81c4062294b8e3e58edace729595462e186 Mon Sep 17 00:00:00 2001
From: Kevin Heis
Date: Tue, 30 Jun 2026 14:45:38 -0700
Subject: [PATCH 20/21] Delete stale PR link-checker comment when links are
clean (#61993)
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
---
src/links/scripts/check-links-pr.ts | 38 +++++++++++++++++++++++------
1 file changed, 30 insertions(+), 8 deletions(-)
diff --git a/src/links/scripts/check-links-pr.ts b/src/links/scripts/check-links-pr.ts
index d6257457b10f..cb216a0b0665 100644
--- a/src/links/scripts/check-links-pr.ts
+++ b/src/links/scripts/check-links-pr.ts
@@ -177,21 +177,35 @@ async function commentOnPR(brokenLinks: BrokenLink[], actionUrl?: string) {
const octokit = github()
const comment = generatePRComment(brokenLinks, { actionUrl })
- if (!comment) {
- console.log('No broken links to report')
- return
- }
-
- // Find existing comment
+ // Find any existing comment we previously posted (identified by the hidden marker)
+ const marker = ''
const { data: comments } = await octokit.rest.issues.listComments({
owner,
repo,
issue_number: pullNumber,
})
-
- const marker = ''
const existingComment = comments.find((c) => c.body?.includes(marker))
+ if (!comment) {
+ console.log('No broken links to report')
+ // Links are now clean: remove any stale comment from an earlier commit.
+ // Best-effort: a concurrent run may have already deleted it (404), and
+ // cleanup should never turn an otherwise-passing run into a failure.
+ if (existingComment) {
+ try {
+ await octokit.rest.issues.deleteComment({
+ owner,
+ repo,
+ comment_id: existingComment.id,
+ })
+ console.log(`Deleted stale PR comment: ${existingComment.id}`)
+ } catch (err) {
+ console.warn(`Could not delete stale PR comment ${existingComment.id}:`, err)
+ }
+ }
+ return
+ }
+
if (existingComment) {
await octokit.rest.issues.updateComment({
owner,
@@ -287,6 +301,14 @@ async function main() {
if (allBrokenLinks.length === 0 && allRedirectLinks.length === 0) {
console.log(chalk.green('✅ All links valid!'))
+ // Remove any stale comment posted on an earlier commit, now that links are clean
+ if (process.env.SHOULD_COMMENT === 'true') {
+ try {
+ await commentOnPR([], process.env.ACTION_RUN_URL)
+ } catch (err) {
+ console.warn('Could not clean up stale PR comment:', err)
+ }
+ }
process.exit(0)
}
From 4902e4ecd775cec4cd27433f85f7bd7a5c5680dc Mon Sep 17 00:00:00 2001
From: Joe Clark <31087804+jc-clark@users.noreply.github.com>
Date: Tue, 30 Jun 2026 15:56:09 -0700
Subject: [PATCH 21/21] Docs for 'Add AI credits per user to the usage metrics
API [GA]' (#61936)
---
.../reference/copilot-usage-metrics/copilot-usage-metrics.md | 3 ++-
.../copilot/reference/copilot-usage-metrics/example-schema.md | 1 +
2 files changed, 3 insertions(+), 1 deletion(-)
diff --git a/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md b/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md
index e8358e457d31..a8f8bae09aa2 100644
--- a/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md
+++ b/content/copilot/reference/copilot-usage-metrics/copilot-usage-metrics.md
@@ -67,7 +67,7 @@ These fields appear in the exported NDJSON reports and in the {% data variables.
Reports come in different shapes depending on their scope and granularity, so the fields available in a record depend on which report it comes from:
-* **Per-user reports** (`*-users-1-day` and `*-users-28-day`) contain one record per user, including `user_id`, `user_login`, the `used_*` indicators, and `ai_adoption_phase`. They do not contain active-user counts, `pull_requests`, or `totals_by_ai_adoption_phase`.
+* **Per-user reports** (`*-users-1-day` and `*-users-28-day`) contain one record per user, including `user_id`, `user_login`, `ai_credits_used`, the `used_*` indicators, and `ai_adoption_phase`. They do not contain active-user counts, `pull_requests`, or `totals_by_ai_adoption_phase`.
* **Aggregated reports** (`enterprise-1-day` and `org-1-day`) contain one aggregated record per enterprise or organization, including active-user counts, `pull_requests`, and `totals_by_ai_adoption_phase`. They do not contain `user_id`, `user_login`, or the `used_*` indicators.
* **28-day reports** (`enterprise-28-day` and `org-28-day`) wrap an array of daily aggregated records in a `day_totals` field, with the reporting window at the top level.
* **User-teams reports** (`*-user-teams-1-day`) map users to the teams they belong to, so you can construct team-level metrics.
@@ -96,6 +96,7 @@ Per-user reports contain one record per user for the reporting period. The 28-da
|:--|:--|:--|:--|
| `user_id` | `integer` | No | Unique identifier for the user. |
| `user_login` | `string` | No | {% data variables.product.github %} username for the user. |
+| `ai_credits_used` | `number` | No | Total AI credits consumed by the user in the reporting period. This field is included in per-user reports only and is not broken down by feature, model, or surface. This metric is for consumption analysis, not invoicing totals. |
| `user_initiated_interaction_count` | `integer` | No | Number of explicit prompts sent to {% data variables.product.prodname_copilot_short %}. Only counts messages or prompts actively sent to the model. Does **not** include opening the chat panel, switching modes (for example, ask, edit, plan, or agent), using keyboard shortcuts to open the inline UI, or making configuration changes. |
| `code_generation_activity_count` | `integer` | No | Number of distinct {% data variables.product.prodname_copilot_short %} output events generated. **Includes:** All generated content, including comments and docstrings. **Multiple blocks:** Each distinct code block from a single user prompt counts as a separate generation. **Note:** This metric is not directly comparable to `user_initiated_interaction_count`, since one prompt can produce multiple generations. |
| `code_acceptance_activity_count` | `integer` | No | Number of suggestions or code blocks accepted by users. **Counts:** All built-in accept actions, such as “apply to file,” “insert at cursor,” “insert into terminal,” and use of the **Copy** button. **Does not count:** Manual OS clipboard actions (for example, Ctrl +C ). **Granularity:** Each acceptance action increments the count once, regardless of how many code blocks were generated by the initial prompt. |
diff --git a/content/copilot/reference/copilot-usage-metrics/example-schema.md b/content/copilot/reference/copilot-usage-metrics/example-schema.md
index 2e7db8049773..f4050a182336 100644
--- a/content/copilot/reference/copilot-usage-metrics/example-schema.md
+++ b/content/copilot/reference/copilot-usage-metrics/example-schema.md
@@ -19,6 +19,7 @@ The following are example schemas for the user-level and enterprise-level data r
```json copy
[{
+ "ai_credits_used": 12.5,
"code_acceptance_activity_count": 1,
"code_generation_activity_count": 1,
"day": "2025-10-01",