From 7ed9d1a3060d3a605833ace6dc6bd5a039074544 Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:18:59 +0000 Subject: [PATCH 1/8] Fix two es-es Liquid corruptions breaking rendering (#63395) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../lib/correct-translation-content.ts | 42 +++++++++++++ .../tests/correct-translation-content.ts | 59 +++++++++++++++++++ 2 files changed, 101 insertions(+) diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index 4458abb464f1..bf13ef209f31 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -159,6 +159,48 @@ export function correctTranslatedContentStrings( ) } + // data/reusables/gated-features/dependabot-custom-auto-triage-rules.md (es): + // the translator duplicated the opening `{%- ifversion fpt %}` block in + // place of the `{%- elsif ghec %}` that should follow it, so the tag is + // never closed (`tag {%- ifversion fpt %} not closed`) and the `ghes` + // branch below is unreachable. English source has three branches: `fpt`, + // `elsif ghec`, `elsif ghes`. Restore the second branch's opener from + // `ifversion fpt` to `elsif ghec` — this string is unique to this file. + // Scoped path check supports both `dottedPath` (production reusable + // rendering via get-data.ts) and `relativePath` (count-translation- + // corruptions.ts validation path). + if ( + context.code === 'es' && + (context.dottedPath === 'reusables.gated-features.dependabot-custom-auto-triage-rules' || + context.relativePath?.endsWith( + 'data/reusables/gated-features/dependabot-custom-auto-triage-rules.md', + )) + ) { + content = content.replace( + /(están disponibles en repositorios públicos y en cualquier repositorio propiedad de una organización en \{% data variables\.product\.prodname_team %\} con \[\{% data variables\.product\.prodname_GH_code_security %\}\]\(\/get-started\/learning-about-github\/about-github-advanced-security\) habilitado\.\n\n)\{%- ifversion fpt %\}( \{% data variables\.dependabot\.custom_rules_caps %\} para \{% data variables\.product\.prodname_dependabot_alerts %\} están disponibles en repositorios públicos y en cualquier repositorio propiedad de una organización en \{% data variables\.product\.prodname_team %\} o )/, + '$1{%- elsif ghec %}$2', + ) + } + + // data/reusables/actions/service-container-host-runner.md (es): the + // translator dropped the `{%- endif %}` that closes the + // `{% ifversion not ghes %}` conditional, leaving the tag never closed. + // English: `...the {% ifversion not ghes %} {% data + // variables.product.prodname_dotcom %}-hosted {%- endif %} runner...`. + // Every other translated language keeps the closing tag; only es dropped + // it. Restore it right before " runner" / "ejecutor", matching the + // English tag placement. + if ( + context.code === 'es' && + (context.dottedPath === 'reusables.actions.service-container-host-runner' || + context.relativePath?.endsWith('data/reusables/actions/service-container-host-runner.md')) + ) { + content = content.replace( + /(En el ejemplo se usa el ejecutor hospedado en \{% data variables\.product\.prodname_dotcom %\} `ubuntu-latest` \{% ifversion not ghes %\}) (como host de Docker\.)/, + '$1 {%- endif %} $2', + ) + } + // --- Per-language fixes (es, ja, pt, zh, ru, fr, ko, de) --- if (context.code === 'es') { diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts index 58ea5c187610..d9ace41b121f 100644 --- a/src/languages/tests/correct-translation-content.ts +++ b/src/languages/tests/correct-translation-content.ts @@ -3465,4 +3465,63 @@ Para más información, consulta "[AUTOTITLE](/path)". expect(fix(fixed, 'ko')).toBe(fixed) }) }) + + // ─── dependabot-custom-auto-triage-rules.md per-file fix ──────────── + // The translator duplicated the opening `{%- ifversion fpt %}` block in + // place of the `{%- elsif ghec %}` that should follow it, leaving the + // tag never closed and the `ghes` branch unreachable. + describe('es: dependabot-custom-auto-triage-rules.md per-file fix', () => { + const context = { + relativePath: 'data/reusables/gated-features/dependabot-custom-auto-triage-rules.md', + code: 'es', + } + + test('restores the duplicated ifversion opener to elsif ghec', () => { + const broken = + '{%- ifversion fpt %} {% data variables.dependabot.custom_rules_caps %} para {% data variables.product.prodname_dependabot_alerts %} están disponibles en repositorios públicos y en cualquier repositorio propiedad de una organización en {% data variables.product.prodname_team %} con [{% data variables.product.prodname_GH_code_security %}](/get-started/learning-about-github/about-github-advanced-security) habilitado.\n\n{%- ifversion fpt %} {% data variables.dependabot.custom_rules_caps %} para {% data variables.product.prodname_dependabot_alerts %} están disponibles en repositorios públicos y en cualquier repositorio propiedad de una organización en {% data variables.product.prodname_team %} o {% data variables.product.prodname_enterprise %} con [{% data variables.product.prodname_GH_code_security %}](/get-started/learning-about-github/about-github-advanced-security) habilitado.\n\n{%- elsif ghes %} {% data variables.dependabot.custom_rules_caps %} para {% data variables.product.prodname_dependabot_alerts %} están disponibles para los repositorios propiedad de la organización con [{% data variables.product.prodname_GH_code_security %}](/get-started/learning-about-github/about-github-advanced-security) habilitado.\n\n{% endif %}' + const fixed = + '{%- ifversion fpt %} {% data variables.dependabot.custom_rules_caps %} para {% data variables.product.prodname_dependabot_alerts %} están disponibles en repositorios públicos y en cualquier repositorio propiedad de una organización en {% data variables.product.prodname_team %} con [{% data variables.product.prodname_GH_code_security %}](/get-started/learning-about-github/about-github-advanced-security) habilitado.\n\n{%- elsif ghec %} {% data variables.dependabot.custom_rules_caps %} para {% data variables.product.prodname_dependabot_alerts %} están disponibles en repositorios públicos y en cualquier repositorio propiedad de una organización en {% data variables.product.prodname_team %} o {% data variables.product.prodname_enterprise %} con [{% data variables.product.prodname_GH_code_security %}](/get-started/learning-about-github/about-github-advanced-security) habilitado.\n\n{%- elsif ghes %} {% data variables.dependabot.custom_rules_caps %} para {% data variables.product.prodname_dependabot_alerts %} están disponibles para los repositorios propiedad de la organización con [{% data variables.product.prodname_GH_code_security %}](/get-started/learning-about-github/about-github-advanced-security) habilitado.\n\n{% endif %}' + expect(correctTranslatedContentStrings(broken, '', context)).toBe(fixed) + // idempotent: the fix only matches the broken form + expect(correctTranslatedContentStrings(fixed, '', context)).toBe(fixed) + }) + + test('does not touch other languages or other files', () => { + const broken = + '{%- ifversion fpt %} {% data variables.dependabot.custom_rules_caps %} para {% data variables.product.prodname_dependabot_alerts %} están disponibles en repositorios públicos y en cualquier repositorio propiedad de una organización en {% data variables.product.prodname_team %} con [{% data variables.product.prodname_GH_code_security %}](/get-started/learning-about-github/about-github-advanced-security) habilitado.\n\n{%- ifversion fpt %} {% data variables.dependabot.custom_rules_caps %} para {% data variables.product.prodname_dependabot_alerts %} están disponibles en repositorios públicos y en cualquier repositorio propiedad de una organización en {% data variables.product.prodname_team %} o ' + expect(correctTranslatedContentStrings(broken, '', { ...context, code: 'pt' })).toBe(broken) + expect( + correctTranslatedContentStrings(broken, '', { ...context, relativePath: 'other.md' }), + ).toBe(broken) + }) + }) + + // ─── service-container-host-runner.md per-file fix ────────────────── + // The translator dropped the `{%- endif %}` that closes the + // `{% ifversion not ghes %}` conditional. + describe('es: service-container-host-runner.md per-file fix', () => { + const context = { + relativePath: 'data/reusables/actions/service-container-host-runner.md', + code: 'es', + } + + test('restores the dropped endif before the runner mention', () => { + const broken = + 'En el ejemplo se usa el ejecutor hospedado en {% data variables.product.prodname_dotcom %} `ubuntu-latest` {% ifversion not ghes %} como host de Docker.' + const fixed = + 'En el ejemplo se usa el ejecutor hospedado en {% data variables.product.prodname_dotcom %} `ubuntu-latest` {% ifversion not ghes %} {%- endif %} como host de Docker.' + expect(correctTranslatedContentStrings(broken, '', context)).toBe(fixed) + // idempotent: the fix only matches the broken form + expect(correctTranslatedContentStrings(fixed, '', context)).toBe(fixed) + }) + + test('does not touch other languages or other files', () => { + const broken = + 'En el ejemplo se usa el ejecutor hospedado en {% data variables.product.prodname_dotcom %} `ubuntu-latest` {% ifversion not ghes %} como host de Docker.' + expect(correctTranslatedContentStrings(broken, '', { ...context, code: 'pt' })).toBe(broken) + expect( + correctTranslatedContentStrings(broken, '', { ...context, relativePath: 'other.md' }), + ).toBe(broken) + }) + }) }) From 9a4201e68710a892b6e16efcf62f235623ce68ab Mon Sep 17 00:00:00 2001 From: docs-bot <77750099+docs-bot@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:29:17 +0000 Subject: [PATCH 2/8] Delete orphaned files (2026-09-21-16-31) (#63374) Co-authored-by: Sarita Iyer <66540150+saritai@users.noreply.github.com> --- .../copilot/windows-terminal-dropdown.png | Bin 29558 -> 0 bytes .../copilot/windows-terminal-settings.png | Bin 49400 -> 0 bytes .../help/copilot/xcode-chat-about-file.png | Bin 20434 -> 0 bytes .../actions/actions-policies-preview-note.md | 2 -- .../workflow-execution-protections-body.md | 29 ------------------ ...flow-execution-protections-preview-note.md | 2 -- data/reusables/copilot/create-js-file.md | 1 - data/reusables/copilot/quickstart-intro.md | 1 - .../copilot/quickstart-nextsteps1.md | 3 -- .../copilot/quickstart-nextsteps2.md | 3 -- data/reusables/copilot/quickstart-signup.md | 20 ------------ .../copilot/windows-terminal-access-denied.md | 1 - 12 files changed, 62 deletions(-) delete mode 100644 assets/images/help/copilot/windows-terminal-dropdown.png delete mode 100644 assets/images/help/copilot/windows-terminal-settings.png delete mode 100644 assets/images/help/copilot/xcode-chat-about-file.png delete mode 100644 data/reusables/actions/actions-policies-preview-note.md delete mode 100644 data/reusables/actions/workflow-execution-protections-body.md delete mode 100644 data/reusables/actions/workflow-execution-protections-preview-note.md delete mode 100644 data/reusables/copilot/create-js-file.md delete mode 100644 data/reusables/copilot/quickstart-intro.md delete mode 100644 data/reusables/copilot/quickstart-nextsteps1.md delete mode 100644 data/reusables/copilot/quickstart-nextsteps2.md delete mode 100644 data/reusables/copilot/quickstart-signup.md delete mode 100644 data/reusables/copilot/windows-terminal-access-denied.md diff --git a/assets/images/help/copilot/windows-terminal-dropdown.png b/assets/images/help/copilot/windows-terminal-dropdown.png deleted file mode 100644 index d05fc16346493864957bb00377ab80facd8e8237..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 29558 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

$1Zx);y+wx?;3EF?ES6 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 X*UmJXCGV2)AD6wh^qIVKCNC<+k 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

va;R^NPsOq2n)hIsi(&`g__?+#v=;pt#o+Fj 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 diff --git a/assets/images/help/copilot/xcode-chat-about-file.png b/assets/images/help/copilot/xcode-chat-about-file.png deleted file mode 100644 index dc02b3ab5e58a17328c1b4961a51109aa31b300f..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 20434 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 WHfC_wqW2s>Ky#?gRh;qdxc>qdWb|JE diff --git a/data/reusables/actions/actions-policies-preview-note.md b/data/reusables/actions/actions-policies-preview-note.md deleted file mode 100644 index c664959ab9f9..000000000000 --- a/data/reusables/actions/actions-policies-preview-note.md +++ /dev/null @@ -1,2 +0,0 @@ -> [!NOTE] -> Actions policies are in {% data variables.release-phases.public_preview %} and subject to change. diff --git a/data/reusables/actions/workflow-execution-protections-body.md b/data/reusables/actions/workflow-execution-protections-body.md deleted file mode 100644 index 4e401b368684..000000000000 --- a/data/reusables/actions/workflow-execution-protections-body.md +++ /dev/null @@ -1,29 +0,0 @@ -## About workflow execution protections - -Workflow execution protections let you define an allow list that controls who can trigger {% data variables.product.prodname_actions %} workflows and which events are permitted to run them. Previously, a workflow ran based on the workflow file in the commit that triggered it, and an attacker with repository access could modify that file to run malicious code. Workflow execution protections close that gap. Administrators define the rules, and {% data variables.product.prodname_actions %} evaluates them before a workflow runs, so an unauthorized actor or event never reaches execution. - -Workflow execution protections are available at the enterprise, organization, and repository levels. - -## Backed by rulesets - -Workflow execution protections are built on the {% data variables.product.github %} rulesets framework, so the targeting you already know from rulesets works here too. You can apply protections with rulesets and scope them to specific repositories using repository custom properties. This means you can enforce broad protections from one place rather than configuring each workflow file individually. For more information about rulesets, see [AUTOTITLE](/repositories/configuring-branches-and-merges-in-your-repository/managing-rulesets/about-rulesets). - -You can also use evaluate mode to run your rules without enforcing them. Evaluate mode shows you exactly what a rule would block before you enforce it, so you can roll out policies without breaking existing workflows. - -## Available rules - -Event and actor are the first two rules, and {% data variables.product.github %} plans to add more rules over time. - -* **Actor rules** control who can trigger workflows, including individual users, repository roles such as Read, Maintain, and Admin, {% data variables.product.prodname_github_apps %}, {% data variables.product.prodname_copilot_short %}, and {% data variables.product.prodname_dependabot %}. -* **Event rules** control which events are permitted, such as `push`, `pull_request`, `pull_request_target`, and `workflow_dispatch`. - -By default, every user with write access to a repository can trigger workflows. Actor rules let you separate who contributes code from who runs your CI, so you can grant a contributor write access without granting them the ability to execute workflows. - -## Stop common attacker techniques - -Workflow execution protections disrupt several real-world attack patterns: - -* **Poisoned pipeline execution from pull requests.** Restrict or prohibit `pull_request_target`, including in public repositories where it is most often exploited. -* **Manual-trigger abuse.** Limit `workflow_dispatch` to maintainers so untrusted identities cannot start workflows. -* **Untrusted-actor execution.** Block low-trust identities from triggering workflows entirely. -* **Misconfiguration exploitation.** Apply central policy that overrides any single misconfigured workflow file. diff --git a/data/reusables/actions/workflow-execution-protections-preview-note.md b/data/reusables/actions/workflow-execution-protections-preview-note.md deleted file mode 100644 index 59ec2e5edf65..000000000000 --- a/data/reusables/actions/workflow-execution-protections-preview-note.md +++ /dev/null @@ -1,2 +0,0 @@ -> [!NOTE] -> Workflow execution protections are in {% data variables.release-phases.public_preview %} and subject to change. diff --git a/data/reusables/copilot/create-js-file.md b/data/reusables/copilot/create-js-file.md deleted file mode 100644 index 38946eaa5804..000000000000 --- a/data/reusables/copilot/create-js-file.md +++ /dev/null @@ -1 +0,0 @@ -1. In {% data variables.product.prodname_vscode %}, create a new JavaScript (_*.js_) file. diff --git a/data/reusables/copilot/quickstart-intro.md b/data/reusables/copilot/quickstart-intro.md deleted file mode 100644 index d22b7c59422a..000000000000 --- a/data/reusables/copilot/quickstart-intro.md +++ /dev/null @@ -1 +0,0 @@ -{% data variables.product.prodname_copilot %} provides coding suggestions as you type in your editor. You can also ask {% data variables.product.prodname_copilot_short %} coding-related questions, such as how best to code something, how to fix a bug, or how someone else's code works. For full details of what {% data variables.product.prodname_copilot_short %} can do, see [AUTOTITLE](/copilot/get-started/about-github-copilot). diff --git a/data/reusables/copilot/quickstart-nextsteps1.md b/data/reusables/copilot/quickstart-nextsteps1.md deleted file mode 100644 index 007bf85ca64a..000000000000 --- a/data/reusables/copilot/quickstart-nextsteps1.md +++ /dev/null @@ -1,3 +0,0 @@ -* **Find out more about {% data variables.product.prodname_copilot_short %} inline suggestions** - See [AUTOTITLE](/copilot/how-tos/get-code-suggestions/get-ide-code-suggestions). -* **Find out more about {% data variables.copilot.copilot_chat %}** - See [AUTOTITLE](/copilot/how-tos/chat-with-copilot/chat-in-ide){% ifversion fpt %}.{% endif %}{% ifversion ghec %} and [AUTOTITLE](/copilot/how-tos/copilot-on-github/chat-with-copilot/chat-in-github).{% endif %} -* **Learn how to write effective prompts** - See [AUTOTITLE](/copilot/concepts/prompting/prompt-engineering). diff --git a/data/reusables/copilot/quickstart-nextsteps2.md b/data/reusables/copilot/quickstart-nextsteps2.md deleted file mode 100644 index af96f2c440df..000000000000 --- a/data/reusables/copilot/quickstart-nextsteps2.md +++ /dev/null @@ -1,3 +0,0 @@ -* **Use {% data variables.product.prodname_copilot_short %} on your mobile device** - See [AUTOTITLE](/copilot/how-tos/copilot-on-github/chat-with-copilot/chat-in-mobile). -* **Use {% data variables.product.prodname_copilot_short %} on the command line** - See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/about-copilot-cli). -* **Configure {% data variables.product.prodname_copilot_short %} in your editor** - You can enable or disable {% data variables.product.prodname_copilot %} from within your editor, and create your own preferred keyboard shortcuts for {% data variables.product.prodname_copilot_short %}. See [AUTOTITLE](/copilot/how-tos/configure-personal-settings/configure-in-ide). diff --git a/data/reusables/copilot/quickstart-signup.md b/data/reusables/copilot/quickstart-signup.md deleted file mode 100644 index 1b3eed5828d3..000000000000 --- a/data/reusables/copilot/quickstart-signup.md +++ /dev/null @@ -1,20 +0,0 @@ -{% ifversion fpt %} - -## Sign up for {% data variables.product.prodname_copilot %} - -Get started for free {% octicon "link-external" height:16 aria-label="link-external" %} - -To use {% data variables.product.prodname_copilot_short %}, you’ll need a personal {% data variables.product.github %} account with access to a {% data variables.product.prodname_copilot_short %} plan. You can: - -* Start with {% data variables.copilot.copilot_free_short %} to explore limited features without subscribing to a plan. -* Upgrade to {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, or {% data variables.copilot.copilot_max_short %} to unlock more features, models, and request limits. - -For more information about the different plans for {% data variables.product.prodname_copilot %}, see [AUTOTITLE](/copilot/get-started/plans). - -{% elsif ghec %} - -## Prerequisites - -To use {% data variables.product.prodname_copilot %} you must have been assigned a seat by your organization as part of a {% data variables.copilot.copilot_for_business %} or {% data variables.copilot.copilot_enterprise %} subscription, or you must have signed up for {% data variables.copilot.copilot_pro %} on your personal {% data variables.product.prodname_dotcom %} account. - -{% endif %} diff --git a/data/reusables/copilot/windows-terminal-access-denied.md b/data/reusables/copilot/windows-terminal-access-denied.md deleted file mode 100644 index 5e73a2b0bd7b..000000000000 --- a/data/reusables/copilot/windows-terminal-access-denied.md +++ /dev/null @@ -1 +0,0 @@ -If you have access to {% data variables.product.prodname_copilot %} via your organization{% ifversion ghec %} or enterprise{% endif %}, you won't be able to use {% data variables.product.prodname_copilot %} in {% data variables.product.prodname_windows_terminal %} if your organization owner{% ifversion ghec %} or enterprise administrator{% endif %} has disabled {% data variables.copilot.copilot_cli %}. See [AUTOTITLE](/copilot/how-tos/administer-copilot/manage-for-organization/manage-policies). From ad7af5d5af64e7cbc8ca60de7bf0d0b620f65407 Mon Sep 17 00:00:00 2001 From: Sunbrye Ly <56200261+sunbrye@users.noreply.github.com> Date: Tue, 22 Sep 2026 17:50:28 +0000 Subject: [PATCH 3/8] Tented Model 0068 (#63363) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: a0d1944c-489f-4025-a3a1-f3da8576e7e7 --- content/copilot/reference/ai-models/model-hosting.md | 1 + content/copilot/reference/ai-models/supported-models.md | 2 ++ .../copilot/copilot-cloud-agent-non-auto-models.md | 1 + data/tables/copilot/model-comparison.yml | 5 +++++ data/tables/copilot/model-release-status.yml | 4 ++++ data/tables/copilot/model-supported-clients.yml | 9 +++++++++ data/tables/copilot/model-supported-plans.yml | 7 +++++++ data/tables/copilot/models-and-pricing.yml | 9 +++++++++ data/variables/copilot.yml | 1 + 9 files changed, 39 insertions(+) diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md index 763da511cea6..9d85d345cdc0 100644 --- a/content/copilot/reference/ai-models/model-hosting.md +++ b/content/copilot/reference/ai-models/model-hosting.md @@ -48,6 +48,7 @@ Used for: * {% data variables.copilot.copilot_claude_opus_48 %} * {% data variables.copilot.copilot_claude_opus_48_fast %} * {% data variables.copilot.copilot_claude_opus_5 %} +* {% data variables.copilot.copilot_claude_opus_55 %} * {% data variables.copilot.copilot_claude_fable_5 %} * {% data variables.copilot.copilot_claude_fable_51 %} diff --git a/content/copilot/reference/ai-models/supported-models.md b/content/copilot/reference/ai-models/supported-models.md index 92aace8cb465..a57b9c0784d2 100644 --- a/content/copilot/reference/ai-models/supported-models.md +++ b/content/copilot/reference/ai-models/supported-models.md @@ -82,6 +82,7 @@ Choosing a larger context window or higher reasoning will impact {% data variabl | {% 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_opus_5 %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | +| {% data variables.copilot.copilot_claude_opus_55 %} | {% 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" %} | @@ -136,6 +137,7 @@ Some {% data variables.product.prodname_copilot_short %} models require minimum | {% data variables.copilot.copilot_gpt_6_astra %} | `1.136.1` | `17.14.19` | TBD | TBD | TBD | | {% data variables.copilot.copilot_claude_opus_48 %} | `v1.118` | `17.14.6` | TBD | TBD | TBD | | {% data variables.copilot.copilot_claude_opus_5 %} | `v1.128.0` | `17.14.22` | TBD | TBD | TBD | +| {% data variables.copilot.copilot_claude_opus_55 %} | TBD | `17.14.6` | TBD | TBD | TBD | | {% data variables.copilot.copilot_claude_sonnet_5 %} | `v1.124` | `17.14.6` | TBD | TBD | TBD | | {% data variables.copilot.copilot_claude_fable_5 %} | `v1.124` | `17.14.6` | TBD | TBD | TBD | | {% data variables.copilot.copilot_claude_fable_51 %} | TBD | TBD | TBD | TBD | TBD | diff --git a/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md b/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md index 4bfb531c1f27..f3f562dfd0c6 100644 --- a/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md +++ b/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md @@ -1,5 +1,6 @@ * {% data variables.copilot.copilot_claude_opus_47 %} * {% data variables.copilot.copilot_claude_opus_5 %} +* {% data variables.copilot.copilot_claude_opus_55 %} * {% data variables.copilot.copilot_claude_haiku_45 %} * {% data variables.copilot.copilot_gemini_35_flash %} * {% data variables.copilot.copilot_gemini_36_flash %} diff --git a/data/tables/copilot/model-comparison.yml b/data/tables/copilot/model-comparison.yml index 68941899fcbc..2a1bcd294249 100644 --- a/data/tables/copilot/model-comparison.yml +++ b/data/tables/copilot/model-comparison.yml @@ -89,6 +89,11 @@ excels_at: Complex problem-solving challenges, sophisticated reasoning further_reading: '[Claude Opus 5 model card](https://www-cdn.anthropic.com/c5fbac3f0b1280a933ebd26d3cb8bb9f5bdeaf48/Claude%20Opus%205%20System%20Card.pdf)' +- name: Claude Opus 5.5 + task_area: Long-running agentic coding and knowledge work + excels_at: Efficient multistep tasks, error recovery, and collaboration + further_reading: '[Claude Opus 5.5 model card](https://www-cdn.anthropic.com/fc1b44717c85dc068bc6ba5024219938094694bd/Claude%20Opus%205.5%20System%20Card.pdf)' + - name: Claude Sonnet 4.6 task_area: General-purpose coding and agent tasks excels_at: Complex problem-solving challenges, sophisticated reasoning diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml index 59ff828adc18..5ec81bb8abd1 100644 --- a/data/tables/copilot/model-release-status.yml +++ b/data/tables/copilot/model-release-status.yml @@ -85,6 +85,10 @@ provider: 'Anthropic' release_status: 'GA' +- name: 'Claude Opus 5.5' + provider: 'Anthropic' + release_status: 'GA' + - name: 'Claude Sonnet 4.6' provider: 'Anthropic' release_status: 'GA' diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml index 4080c6fb272e..14166ac75fe4 100644 --- a/data/tables/copilot/model-supported-clients.yml +++ b/data/tables/copilot/model-supported-clients.yml @@ -59,6 +59,15 @@ xcode: true jetbrains: true +- name: Claude Opus 5.5 + dotcom: true + cli: true + vscode: true + vs: true + eclipse: true + xcode: true + jetbrains: true + - name: Claude Fable 5 dotcom: true cli: true diff --git a/data/tables/copilot/model-supported-plans.yml b/data/tables/copilot/model-supported-plans.yml index a5438a3d20c6..11ff89823063 100644 --- a/data/tables/copilot/model-supported-plans.yml +++ b/data/tables/copilot/model-supported-plans.yml @@ -47,6 +47,13 @@ business: true enterprise: true +- name: Claude Opus 5.5 + pro: false + pro_plus: true + max: true + business: true + enterprise: true + - name: Claude Fable 5 pro: false pro_plus: true diff --git a/data/tables/copilot/models-and-pricing.yml b/data/tables/copilot/models-and-pricing.yml index 37ec3e1c233c..b0fad13a067d 100644 --- a/data/tables/copilot/models-and-pricing.yml +++ b/data/tables/copilot/models-and-pricing.yml @@ -251,6 +251,15 @@ output: $25.00 cache_write: $6.25 +- model: Claude Opus 5.5 + provider: anthropic + release_status: GA + category: Powerful + input: $4.00 + cached_input: $0.20 + output: $20.00 + cache_write: $5.00 + - model: Claude Sonnet 5 provider: anthropic release_status: GA diff --git a/data/variables/copilot.yml b/data/variables/copilot.yml index 14c251abee68..752ebcf593dd 100644 --- a/data/variables/copilot.yml +++ b/data/variables/copilot.yml @@ -191,6 +191,7 @@ copilot_claude_opus_47: 'Claude Opus 4.7' copilot_claude_opus_48: 'Claude Opus 4.8' copilot_claude_opus_48_fast: 'Claude Opus 4.8 (fast mode) (preview)' copilot_claude_opus_5: 'Claude Opus 5' +copilot_claude_opus_55: 'Claude Opus 5.5' copilot_claude_fable_5: 'Claude Fable 5' copilot_claude_fable_51: 'Claude Fable 5.1' copilot_claude_sonnet: 'Claude Sonnet' From b22cce5ede6f959ea32b979ee0d1aceff39a31e3 Mon Sep 17 00:00:00 2001 From: Laura Coursen Date: Tue, 22 Sep 2026 18:11:13 +0000 Subject: [PATCH 4/8] Add rewrite-only guardrail to always-on content instructions (#63025) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: aa7516ab-403e-454c-8cfa-67b1c86b919c Copilot-Session: 47b893b9-9c89-498d-b1f2-c46e81d5c17e --- .github/instructions/content-guidelines.instructions.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/.github/instructions/content-guidelines.instructions.md b/.github/instructions/content-guidelines.instructions.md index 4effecc5042a..b788503764db 100644 --- a/.github/instructions/content-guidelines.instructions.md +++ b/.github/instructions/content-guidelines.instructions.md @@ -19,6 +19,8 @@ The strategic priority is simplification: create less content and remove content * Would a typical internet user figure this out on their own by exploring the UI? * Is the information presented at the moment the reader actually needs it? +Look for content to remove whether or not you were asked to, and don't add non-essential content in the first place. Before you cut, name the reader who would fail without it and how they would fail. If you cannot name a specific failure, cut it. + ## Give opinionated, actionable guidance This applies whenever you give the reader advice or present ways to accomplish a task. @@ -72,4 +74,4 @@ These apply to the prose in an article (intros and explanatory text), not to str * Write for the one reader scenario the article targets, for example a particular deployment configuration (GHEC with EMUs vs. Classic) or a particular type of reader (an open source maintainer vs. an enterprise developer). When the article has a content design plan, target the audience it identifies rather than inventing one; for small edits without a plan, follow the audience the existing article is clearly written for. Do not branch content to serve multiple audiences; readers in other scenarios can adapt the guidance. The exception is version differences: when in-article `{% ifversion %}` branching is genuinely required (see the versioning rules in `content.instructions.md`), it is not a scannability violation. * Ruthlessly minimize links. Only link when you actively want most readers to follow it in the ideal scenario. No "just in case" links. Links that build a logical user journey are exactly the kind to keep, for example a Prerequisites link that sends the reader to setup they need first, or a Next steps link that points them to the natural follow-on task. * Ruthlessly minimize alerts (notes, tips, warnings): more than one per article should be exceptional, and crowding several into one section is worse than spreading them out. Keep each to 1-2 sentences. Don't open an article or section with an alert unless the reader needs it before the surrounding content. Prefer folding a useful alert into the prose over deleting it, but first apply this test: if the reader must actually notice it to use the page correctly, keep it as an alert (don't fold or count it), since folding defeats its purpose. This covers, for example, critical warnings, plan or availability constraints, public preview notices, and cues that orient the reader to how the page works or which content applies to them. -* Prefer short sentences and paragraphs, generous white space, and formatting like bold and tables to highlight key information. Use a table only for genuinely complex data that belongs in a tabular format; do not add a table that repeats information already stated more clearly in prose. +* Prefer short sentences and paragraphs, generous white space, and formatting like bold and tables to highlight key information. Keep each sentence to one idea: where a sentence carries two, split it into two sentences rather than trimming it. Use a table only for genuinely complex data that belongs in a tabular format; do not add a table that repeats information already stated more clearly in prose. From 9b8c3b3460279d3de253fc2655b42d54fbdcae6b Mon Sep 17 00:00:00 2001 From: "release-controller[bot]" <110195724+release-controller[bot]@users.noreply.github.com> Date: Tue, 22 Sep 2026 18:26:52 +0000 Subject: [PATCH 5/8] Patch release notes for GitHub Enterprise Server (#63343) Co-authored-by: Release-Controller Co-authored-by: Felix Guntrip Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: Devin Dooley --- .../enterprise-server/3-17/21.yml | 78 +++++++++++ .../enterprise-server/3-18/15.yml | 106 +++++++++++++++ .../enterprise-server/3-19/12.yml | 110 +++++++++++++++ .../enterprise-server/3-20/8.yml | 113 ++++++++++++++++ .../enterprise-server/3-21/6.yml | 127 ++++++++++++++++++ .../enterprise-server/3-22/1.yml | 125 +++++++++++++++++ 6 files changed, 659 insertions(+) create mode 100644 data/release-notes/enterprise-server/3-17/21.yml create mode 100644 data/release-notes/enterprise-server/3-18/15.yml create mode 100644 data/release-notes/enterprise-server/3-19/12.yml create mode 100644 data/release-notes/enterprise-server/3-20/8.yml create mode 100644 data/release-notes/enterprise-server/3-21/6.yml create mode 100644 data/release-notes/enterprise-server/3-22/1.yml diff --git a/data/release-notes/enterprise-server/3-17/21.yml b/data/release-notes/enterprise-server/3-17/21.yml new file mode 100644 index 000000000000..2676a1edb045 --- /dev/null +++ b/data/release-notes/enterprise-server/3-17/21.yml @@ -0,0 +1,78 @@ +date: '2026-09-22' +sections: + security_fixes: + - | + **CRITICAL**: An attacker with network access to a GitHub Enterprise Server instance could supply a notebook viewer URL specifying an explicit port to reach internal services co-located on the appliance. Although response bodies were not returned, response timing could act as an oracle that allowed instance secrets to be extracted. These secrets could be used to obtain remote code execution on the appliance. Exploitation was unauthenticated when private mode was disabled and required any authenticated user when private mode was enabled. GitHub Enterprise Server now rejects notebook viewer URLs that specify an explicit port. GitHub has requested CVE ID [CVE-2026-77987](https://www.cve.org/cverecord?id=CVE-2026-77987) for this vulnerability. This vulnerability was reported through the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An unauthenticated attacker could cause a GitHub Enterprise Server instance to send crafted outbound requests to an attacker-controlled host via an unauthenticated Manage API endpoint that parsed an attacker-supplied cluster configuration. An attacker positioned to intercept the outbound request could capture a replayable management bearer token. High-availability deployments were not affected. GitHub has requested [CVE-2026-18730](https://www.cve.org/cverecord?id=CVE-2026-18730) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An authenticated attacker could execute malicious scripts in another user's browser by posting crafted Markdown content, such as an issue comment, that injected arbitrary HTML attributes into the rendered page. Successful exploitation could allow the attacker to read data visible to the victim, perform state-changing actions as the victim, exfiltrate data, and propagate the payload using the victim's write access. This was possible because the Markdown rendering pipeline rewrote quote characters in already-sanitized HTML without re-sanitizing the result. GitHub has requested CVE ID [CVE-2026-77912](https://www.cve.org/cverecord?id=CVE-2026-77912) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **MEDIUM**: An attacker could read the raw diff or patch of pull requests in private repositories they were not authorized to access, by creating a repository and pull request whose name and number matched the target. GitHub has requested CVE ID [CVE-2026-75101](https://www.cve.org/cverecord?id=CVE-2026-75101) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + An attacker could corrupt an in-progress package upload in GitHub Packages by obtaining the identifiers for that upload, because the registry did not verify the users permissions before continuing or completing an upload. GitHub has addressed this by validating authorization before any changes are made to an upload. + bugs: + - | + Dependabot alerts could remain open after the vulnerable dependency had been upgraded. Manifest files containing invalid UTF-8 byte sequences caused dependency graph parsing to fail, which prevented affected alerts from moving to a fixed state. + - | + During `ghe-config-apply`, a process that was intended to check that all pending jobs had passed health checks was silently skipped. + - | + GHES administrators running instances with high-write workloads experienced increased Redis memory use and latency because Redis created periodic RDB snapshots in addition to its configured AOF persistence. + - | + On instances whose user data disk was initialized before the ownership fix in ghe-storage-init, `/var/log/github-backup` remained owned by root:root, so the admin user could not write to it and ghe-backup failed immediately with a permission-denied error. + - | + The `ghe-cluster-support-bundle` `-x` short option required an argument to run. + - | + On AWS and Azure, `ghe-setup-network` now applies explicitly configured IPv6 networking without enabling IPv6 during IPv4-only network updates. The `-4` and `-6` options now disable the selected address family, and explicit IPv4 and IPv6 network configuration is preserved across feature upgrades. + - | + Administrators running backups with the backup service could see a successful exit code even when rsync failed to back up repositories and gists. The backup service now reports these failures correctly. + - | + On an instance with security overview enabled, reconciliation could repeatedly retry when background job queues were busy, causing high CPU usage and delays across the appliance. + changes: + - | + During a live upgrade, the retained copy of a table left after an online schema migration is now removed in a single operation rather than by a gradual, row-by-row deletion, reducing database load and reclaiming disk space more quickly. + 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 [AUTOTITLE](/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 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`. + - | + 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 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. diff --git a/data/release-notes/enterprise-server/3-18/15.yml b/data/release-notes/enterprise-server/3-18/15.yml new file mode 100644 index 000000000000..d193c45bc6b9 --- /dev/null +++ b/data/release-notes/enterprise-server/3-18/15.yml @@ -0,0 +1,106 @@ +date: '2026-09-22' +sections: + security_fixes: + - | + **CRITICAL**: An attacker with network access to a GitHub Enterprise Server instance could supply a notebook viewer URL specifying an explicit port to reach internal services co-located on the appliance. Although response bodies were not returned, response timing could act as an oracle that allowed instance secrets to be extracted. These secrets could be used to obtain remote code execution on the appliance. Exploitation was unauthenticated when private mode was disabled and required any authenticated user when private mode was enabled. GitHub Enterprise Server now rejects notebook viewer URLs that specify an explicit port. GitHub has requested CVE ID [CVE-2026-77987](https://www.cve.org/cverecord?id=CVE-2026-77987) for this vulnerability. This vulnerability was reported through the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An unauthenticated attacker could cause a GitHub Enterprise Server instance to send crafted outbound requests to an attacker-controlled host via an unauthenticated Manage API endpoint that parsed an attacker-supplied cluster configuration. An attacker positioned to intercept the outbound request could capture a replayable management bearer token. High-availability deployments were not affected. GitHub has requested [CVE-2026-18730](https://www.cve.org/cverecord?id=CVE-2026-18730) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An authenticated attacker could execute malicious scripts in another user's browser by posting crafted Markdown content, such as an issue comment, that injected arbitrary HTML attributes into the rendered page. Successful exploitation could allow the attacker to read data visible to the victim, perform state-changing actions as the victim, exfiltrate data, and propagate the payload using the victim's write access. This was possible because the Markdown rendering pipeline rewrote quote characters in already-sanitized HTML without re-sanitizing the result. GitHub has requested CVE ID [CVE-2026-77912](https://www.cve.org/cverecord?id=CVE-2026-77912) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **MEDIUM**: An attacker could read the raw diff or patch of pull requests in private repositories they were not authorized to access, by creating a repository and pull request whose name and number matched the target. GitHub has requested CVE ID [CVE-2026-75101](https://www.cve.org/cverecord?id=CVE-2026-75101) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + Server reports under `/stafftools/reports/` were accessible using a legacy non-expiring token. If you were accessing these reports programmatically using this legacy approach, you will need to switch to using a {% data variables.product.pat_v1 %} with the `site_admin` scope. + - | + An attacker could corrupt an in-progress package upload in GitHub Packages by obtaining the identifiers for that upload, because the registry did not verify the users permissions before continuing or completing an upload. GitHub has addressed this by validating authorization before any changes are made to an upload. + bugs: + - | + Dependabot alerts could remain open after the vulnerable dependency had been upgraded. Manifest files containing invalid UTF-8 byte sequences caused dependency graph parsing to fail, which prevented affected alerts from moving to a fixed state. + - | + During `ghe-config-apply`, a process that was intended to check that all pending jobs had passed health checks was silently skipped. + - | + GHES administrators running instances with high-write workloads experienced increased Redis memory use and latency because Redis created periodic RDB snapshots in addition to its configured AOF persistence. + - | + Customers with the OpenTelemetry observability stack enabled experienced a loss of metrics after reboot because some exporter services failed to start due to a dependency on the data disk being mounted. + - | + On instances whose user data disk was initialized before the ownership fix in ghe-storage-init, `/var/log/github-backup` remained owned by root:root, so the admin user could not write to it and ghe-backup failed immediately with a permission-denied error. + - | + The `ghe-cluster-support-bundle` `-x` short option required an argument to run. + - | + Administrators experienced a broken SSH command when they ran `ghe-ssl-ca-certificate-install` on a high-availability instance with replication enabled. + - | + On AWS and Azure, `ghe-setup-network` now applies explicitly configured IPv6 networking without enabling IPv6 during IPv4-only network updates. The `-4` and `-6` options now disable the selected address family, and explicit IPv4 and IPv6 network configuration is preserved across feature upgrades. + - | + Administrators running backups with the backup service could see a successful exit code even when rsync failed to back up repositories and gists. The backup service now reports these failures correctly. + - | + On an instance with suspended users, organization member counts on the enterprise organizations page and the organization people page included suspended users. This caused the displayed count to be higher than the number of members visible in the list. + - | + The first user created via SAML-JIT during first-run bootstrap on a fresh GHES appliance was not added as an Enterprise owner. + - | + On an instance with security overview enabled, reconciliation could repeatedly retry when background job queues were busy, causing high CPU usage and delays across the appliance. + - | + Enterprise owners would sometimes experience timeouts when loading the "Notifications preferences" section on the "Verified & approved domains" page. + - | + Advanced security configuration pages would not load at all if there was a timeout affecting certain subsections. + - | + The User contributions backfill job would process the full repository set per user, and not just those with contributions, leading to an excessively high `user_contributions_backfill` queue backlog and contributing to high resource consumption when large batches of user changes occur. + - | + Administrators on instances with many organizations experienced security configurations failing to apply to repositories. Fanout processing repeatedly retried without applying repository updates. + - | + Administrators experienced errors when loading license and security configuration pages on instances with large GitHub Advanced Security usage datasets. + changes: + - | + Enterprise Live Migrations skip branch protection rules by default because only part of each rule can be migrated. Partial migration of branch protection rules is opt in with `ghe-config app.elm-exporter.migrate-protected-branches true`. + - | + During a live upgrade, the retained copy of a table left after an online schema migration is now removed in a single operation rather than by a gradual, row-by-row deletion, reducing database load and reclaiming disk space more quickly. + - | + To avoid data processing delays on instances with a large number of internal message topics, administrators can configure the network timeouts used during topic discovery via `app.github.topic-checker-connect-timeout-sec` and `app.github.topic-checker-socket-timeout-sec`. The default timeout has been increased from 10 to 30 seconds, and values between 1 and 300 seconds are accepted. + 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 [AUTOTITLE](/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 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`. + - | + 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 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. + - | + 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. diff --git a/data/release-notes/enterprise-server/3-19/12.yml b/data/release-notes/enterprise-server/3-19/12.yml new file mode 100644 index 000000000000..b17079f46fb9 --- /dev/null +++ b/data/release-notes/enterprise-server/3-19/12.yml @@ -0,0 +1,110 @@ +date: '2026-09-22' +sections: + security_fixes: + - | + **CRITICAL**: An attacker with network access to a GitHub Enterprise Server instance could supply a notebook viewer URL specifying an explicit port to reach internal services co-located on the appliance. Although response bodies were not returned, response timing could act as an oracle that allowed instance secrets to be extracted. These secrets could be used to obtain remote code execution on the appliance. Exploitation was unauthenticated when private mode was disabled and required any authenticated user when private mode was enabled. GitHub Enterprise Server now rejects notebook viewer URLs that specify an explicit port. GitHub has requested CVE ID [CVE-2026-77987](https://www.cve.org/cverecord?id=CVE-2026-77987) for this vulnerability. This vulnerability was reported through the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An unauthenticated attacker could cause a GitHub Enterprise Server instance to send crafted outbound requests to an attacker-controlled host via an unauthenticated Manage API endpoint that parsed an attacker-supplied cluster configuration. An attacker positioned to intercept the outbound request could capture a replayable management bearer token. High-availability deployments were not affected. GitHub has requested [CVE-2026-18730](https://www.cve.org/cverecord?id=CVE-2026-18730) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An authenticated attacker could execute malicious scripts in another user's browser by posting crafted Markdown content, such as an issue comment, that injected arbitrary HTML attributes into the rendered page. Successful exploitation could allow the attacker to read data visible to the victim, perform state-changing actions as the victim, exfiltrate data, and propagate the payload using the victim's write access. This was possible because the Markdown rendering pipeline rewrote quote characters in already-sanitized HTML without re-sanitizing the result. GitHub has requested CVE ID [CVE-2026-77912](https://www.cve.org/cverecord?id=CVE-2026-77912) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **MEDIUM**: An attacker could read the raw diff or patch of pull requests in private repositories they were not authorized to access, by creating a repository and pull request whose name and number matched the target. GitHub has requested CVE ID [CVE-2026-75101](https://www.cve.org/cverecord?id=CVE-2026-75101) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + Server reports under `/stafftools/reports/` were accessible using a legacy non-expiring token. If you were accessing these reports programmatically using this legacy approach, you will need to switch to using a {% data variables.product.pat_v1 %} with the `site_admin` scope. + - | + An attacker could corrupt an in-progress package upload in GitHub Packages by obtaining the identifiers for that upload, because the registry did not verify the users permissions before continuing or completing an upload. GitHub has addressed this by validating authorization before any changes are made to an upload. + bugs: + - | + Dependabot alerts could remain open after the vulnerable dependency had been upgraded. Manifest files containing invalid UTF-8 byte sequences caused dependency graph parsing to fail, which prevented affected alerts from moving to a fixed state. + - | + GHES administrators running instances with high-write workloads experienced increased Redis memory use and latency because Redis created periodic RDB snapshots in addition to its configured AOF persistence. + - | + Customers with the OpenTelemetry observability stack enabled experienced a loss of metrics after reboot because some exporter services failed to start due to a dependency on the data disk being mounted. + - | + On instances whose user data disk was initialized before the ownership fix in ghe-storage-init, `/var/log/github-backup` remained owned by root:root, so the admin user could not write to it and ghe-backup failed immediately with a permission-denied error. + - | + Kafka-lite sessions in Grafana dashboard showed empty panels. + - | + Users experienced slow Git LFS uploads over high-latency network connections, because the instances HAProxy frontend advertised a fixed HTTP/2 initial receive window that throttled each stream to roughly one 64 KiB window per round trip. Administrators can now tune this window with `ghe-config core.haproxy-h2-fe-initial-window-size `, using a value between 65536 and 67108864 (64 KiB-64 MiB) inclusive, to improve throughput for large uploads. The setting is opt-in and disabled by default. + - | + The `ghe-cluster-support-bundle` `-x` short option required an argument to run. + - | + Administrators experienced a broken SSH command when they ran `ghe-ssl-ca-certificate-install` on a high-availability instance with replication enabled. + - | + On AWS and Azure, `ghe-setup-network` now applies explicitly configured IPv6 networking without enabling IPv6 during IPv4-only network updates. The `-4` and `-6` options now disable the selected address family, and explicit IPv4 and IPv6 network configuration is preserved across feature upgrades. + - | + Administrators running backups with the backup service could see a successful exit code even when rsync failed to back up repositories and gists. The backup service now reports these failures correctly. + - | + On an instance with suspended users, organization member counts on the enterprise organizations page and the organization people page included suspended users. This caused the displayed count to be higher than the number of members visible in the list. + - | + On an instance with security overview enabled, reconciliation could repeatedly retry when background job queues were busy, causing high CPU usage and delays across the appliance. + - | + The first user created via SAML-JIT during first-run bootstrap on a fresh GHES appliance was not added as an Enterprise owner. + - | + On the secret scanning metrics page for a large enough enterprise, the page could time out. + - | + Enterprise owners would sometimes experience timeouts when loading the "Notifications preferences" section on the "Verified & approved domains" page. + - | + Administrators who deleted a user that belonged to an organization received a 500 error, even though the user was successfully removed. Previously, the deletion attempted to enqueue a job to a Copilot Spaces queue that does not exist on GitHub Enterprise Server, causing the request to fail. + - | + Advanced security configuration pages would not load at all if there was a timeout affecting certain subsections. + - | + The User contributions backfill job would process the full repository set per user, and not just those with contributions, leading to an excessively high `user_contributions_backfill` queue backlog and contributing to high resource consumption when large batches of user changes occur. + - | + Administrators on instances with many organizations experienced security configurations failing to apply to repositories. Fanout processing repeatedly retried without applying repository updates. + - | + GitHub Advanced Security configuration pages would time out too quickly. + - | + On an instance using Redis 7, AOF (append-only file) recovery did not detect or repair corruption in Redis data volumes. This occurred because recovery only checked the legacy single-file AOF path and did not account for Redis 7s multi-part AOF format, which uses a manifest to track data across multiple files. + - | + Administrators experienced errors when loading license and security configuration pages on instances with large GitHub Advanced Security usage datasets. + changes: + - | + Enterprise Live Migrations skip branch protection rules by default because only part of each rule can be migrated. Partial migration of branch protection rules is opt in with `ghe-config app.elm-exporter.migrate-protected-branches true`. + - | + During a live upgrade, the retained copy of a table left after an online schema migration is now removed in a single operation rather than by a gradual, row-by-row deletion, reducing database load and reclaiming disk space more quickly. + - | + To avoid data processing delays on instances with a large number of internal message topics, administrators can configure the network timeouts used during topic discovery via `app.github.topic-checker-connect-timeout-sec` and `app.github.topic-checker-socket-timeout-sec`. The default timeout has been increased from 10 to 30 seconds, and values between 1 and 300 seconds are accepted. + 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 [AUTOTITLE](/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. + - | + 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. + - | + The setting to define private registries at the organization level for code scanning is only available if Dependabot is also enabled for the instance. + - | + 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. diff --git a/data/release-notes/enterprise-server/3-20/8.yml b/data/release-notes/enterprise-server/3-20/8.yml new file mode 100644 index 000000000000..ec3d8f5c550f --- /dev/null +++ b/data/release-notes/enterprise-server/3-20/8.yml @@ -0,0 +1,113 @@ +date: '2026-09-22' +sections: + features: + - | + Site administrators can configure MySQL temporary table and heap table size limits using `ghe-config` to work around a known MySQL optimizer issue that can cause queries to use disk-based temporary tables unnecessarily. To adjust the temporary table size, run `ghe-config mysql.tmp-table-size ` followed by `ghe-config-apply`. + security_fixes: + - | + **CRITICAL**: An attacker with network access to a GitHub Enterprise Server instance could supply a notebook viewer URL specifying an explicit port to reach internal services co-located on the appliance. Although response bodies were not returned, response timing could act as an oracle that allowed instance secrets to be extracted. These secrets could be used to obtain remote code execution on the appliance. Exploitation was unauthenticated when private mode was disabled and required any authenticated user when private mode was enabled. GitHub Enterprise Server now rejects notebook viewer URLs that specify an explicit port. GitHub has requested CVE ID [CVE-2026-77987](https://www.cve.org/cverecord?id=CVE-2026-77987) for this vulnerability. This vulnerability was reported through the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An unauthenticated attacker could cause a GitHub Enterprise Server instance to send crafted outbound requests to an attacker-controlled host via an unauthenticated Manage API endpoint that parsed an attacker-supplied cluster configuration. An attacker positioned to intercept the outbound request could capture a replayable management bearer token. High-availability deployments were not affected. GitHub has requested [CVE-2026-18730](https://www.cve.org/cverecord?id=CVE-2026-18730) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An authenticated attacker could execute malicious scripts in another user's browser by posting crafted Markdown content, such as an issue comment, that injected arbitrary HTML attributes into the rendered page. Successful exploitation could allow the attacker to read data visible to the victim, perform state-changing actions as the victim, exfiltrate data, and propagate the payload using the victim's write access. This was possible because the Markdown rendering pipeline rewrote quote characters in already-sanitized HTML without re-sanitizing the result. GitHub has requested CVE ID [CVE-2026-77912](https://www.cve.org/cverecord?id=CVE-2026-77912) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **MEDIUM**: An attacker could read the raw diff or patch of pull requests in private repositories they were not authorized to access, by creating a repository and pull request whose name and number matched the target. GitHub has requested CVE ID [CVE-2026-75101](https://www.cve.org/cverecord?id=CVE-2026-75101) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + Server reports under `/stafftools/reports/` were accessible using a legacy non-expiring token. If you were accessing these reports programmatically using this legacy approach, you will need to switch to using a {% data variables.product.pat_v1 %} with the `site_admin` scope. + - | + An attacker could corrupt an in-progress package upload in GitHub Packages by obtaining the identifiers for that upload, because the registry did not verify the users permissions before continuing or completing an upload. GitHub has addressed this by validating authorization before any changes are made to an upload. + bugs: + - | + Dependabot alerts could remain open after the vulnerable dependency had been upgraded. Manifest files containing invalid UTF-8 byte sequences caused dependency graph parsing to fail, which prevented affected alerts from moving to a fixed state. + - | + GHES administrators running instances with high-write workloads experienced increased Redis memory use and latency because Redis created periodic RDB snapshots in addition to its configured AOF persistence. + - | + Customers with the OpenTelemetry observability stack enabled experienced a loss of metrics after reboot because some exporter services failed to start due to a dependency on the data disk being mounted. + - | + On instances whose user data disk was initialized before the ownership fix in ghe-storage-init, `/var/log/github-backup` remained owned by root:root, so the admin user could not write to it and ghe-backup failed immediately with a permission-denied error. + - | + Management Console users other than the root site administrator received an HTTP 401 error when they clicked "Backup Now" or attempted to delete (prune) a backup on the Backups page, even though the controls were displayed to them. These authenticated operators can now start and prune backups successfully. + - | + Users experienced slow Git LFS uploads over high-latency network connections, because the instances HAProxy frontend advertised a fixed HTTP/2 initial receive window that throttled each stream to roughly one 64 KiB window per round trip. Administrators can now tune this window with `ghe-config core.haproxy-h2-fe-initial-window-size `, using a value between 65536 and 67108864 (64 KiB–64 MiB) inclusive, to improve throughput for large uploads. The setting is opt-in and disabled by default. + - | + Kafka-lite sessions in Grafana dashboard showed empty panels. + - | + The `ghe-cluster-support-bundle` `-x` short option required an argument to run. + - | + Administrators experienced a broken SSH command when they ran `ghe-ssl-ca-certificate-install` on a high-availability instance with replication enabled. + - | + On AWS and Azure, `ghe-setup-network` now applies explicitly configured IPv6 networking without enabling IPv6 during IPv4-only network updates. The `-4` and `-6` options now disable the selected address family, and explicit IPv4 and IPv6 network configuration is preserved across feature upgrades. + - | + Administrators running backups with the backup service could see a successful exit code even when rsync failed to back up repositories and gists. The backup service now reports these failures correctly. + - | + On an instance with suspended users, organization member counts on the enterprise organizations page and the organization people page included suspended users. This caused the displayed count to be higher than the number of members visible in the list. + - | + On an instance with security overview enabled, reconciliation could repeatedly retry when background job queues were busy, causing high CPU usage and delays across the appliance. + - | + The first user created via SAML-JIT during first-run bootstrap on a fresh GHES appliance was not added as an Enterprise owner. + - | + Enterprise Managed Users could remain absent from enterprise membership listings and the `enterprise.members` GraphQL response after being reinstated. + - | + On the secret scanning metrics page for a large enough enterprise, the page could time out. + - | + Enterprise owners would sometimes experience timeouts when loading the "Notifications preferences" section on the "Verified & approved domains" page. + - | + Administrators who deleted a user that belonged to an organization received a 500 error, even though the user was successfully removed. Previously, the deletion attempted to enqueue a job to a Copilot Spaces queue that does not exist on GitHub Enterprise Server, causing the request to fail. + - | + Advanced security configuration pages would not load at all if there was a timeout affecting certain subsections. + - | + The User contributions backfill job would process the full repository set per user, and not just those with contributions, leading to an excessively high `user_contributions_backfill` queue backlog and contributing to high resource consumption when large batches of user changes occur. + - | + Administrators on instances with many organizations experienced security configurations failing to apply to repositories. Fanout processing repeatedly retried without applying repository updates. + - | + GitHub Advanced Security configuration pages would time out too quickly. + - | + On an instance using Redis 7, AOF (append-only file) recovery did not detect or repair corruption in Redis data volumes. This occurred because recovery only checked the legacy single-file AOF path and did not account for Redis 7s multi-part AOF format, which uses a manifest to track data across multiple files. + - | + Administrators experienced errors when loading license and security configuration pages on instances with large GitHub Advanced Security usage datasets. + changes: + - | + Enterprise Live Migrations skip branch protection rules by default because only part of each rule can be migrated. Partial migration of branch protection rules is opt in with `ghe-config app.elm-exporter.migrate-protected-branches true`. + - | + Administrators can limit the CPU consumed by MySQL backup dumps by setting a CPU quota with `ghe-config backup.cpu-quota`. Provide a systemd `CPUQuota` value, such as `200%` to allow up to two cores. The setting is empty by default, which leaves backup behavior unchanged. The cap is applied on the node where each dump runs, including remote cluster and replica nodes. + - | + During a live upgrade, the retained copy of a table left after an online schema migration is now removed in a single operation rather than by a gradual, row-by-row deletion, reducing database load and reclaiming disk space more quickly. + - | + To avoid data processing delays on instances with a large number of internal message topics, administrators can configure the network timeouts used during topic discovery via `app.github.topic-checker-connect-timeout-sec` and `app.github.topic-checker-socket-timeout-sec`. The default timeout has been increased from 10 to 30 seconds, and values between 1 and 300 seconds are accepted. + 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 [AUTOTITLE](/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`. + - | + _When restoring an instance with `ghe-restore` while the replication controller is enabled, the storage directory is not restored._ diff --git a/data/release-notes/enterprise-server/3-21/6.yml b/data/release-notes/enterprise-server/3-21/6.yml new file mode 100644 index 000000000000..a23950cbb99f --- /dev/null +++ b/data/release-notes/enterprise-server/3-21/6.yml @@ -0,0 +1,127 @@ +date: '2026-09-22' +sections: + features: + - | + Site administrators can configure MySQL temporary table and heap table size limits using `ghe-config` to work around a known MySQL optimizer issue that can cause queries to use disk-based temporary tables unnecessarily. To adjust the temporary table size, run `ghe-config mysql.tmp-table-size ` followed by `ghe-config-apply`. + security_fixes: + - | + **CRITICAL**: An attacker with network access to a GitHub Enterprise Server instance could supply a notebook viewer URL specifying an explicit port to reach internal services co-located on the appliance. Although response bodies were not returned, response timing could act as an oracle that allowed instance secrets to be extracted. These secrets could be used to obtain remote code execution on the appliance. Exploitation was unauthenticated when private mode was disabled and required any authenticated user when private mode was enabled. GitHub Enterprise Server now rejects notebook viewer URLs that specify an explicit port. GitHub has requested CVE ID [CVE-2026-77987](https://www.cve.org/cverecord?id=CVE-2026-77987) for this vulnerability. This vulnerability was reported through the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An unauthenticated attacker could cause a GitHub Enterprise Server instance to send crafted outbound requests to an attacker-controlled host via an unauthenticated Manage API endpoint that parsed an attacker-supplied cluster configuration. An attacker positioned to intercept the outbound request could capture a replayable management bearer token. High-availability deployments were not affected. GitHub has requested [CVE-2026-18730](https://www.cve.org/cverecord?id=CVE-2026-18730) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An authenticated attacker could execute malicious scripts in another user's browser by posting crafted Markdown content, such as an issue comment, that injected arbitrary HTML attributes into the rendered page. Successful exploitation could allow the attacker to read data visible to the victim, perform state-changing actions as the victim, exfiltrate data, and propagate the payload using the victim's write access. This was possible because the Markdown rendering pipeline rewrote quote characters in already-sanitized HTML without re-sanitizing the result. GitHub has requested CVE ID [CVE-2026-77912](https://www.cve.org/cverecord?id=CVE-2026-77912) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **MEDIUM**: An attacker could read the raw diff or patch of pull requests in private repositories they were not authorized to access, by creating a repository and pull request whose name and number matched the target. GitHub has requested CVE ID [CVE-2026-75101](https://www.cve.org/cverecord?id=CVE-2026-75101) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + Server reports under `/stafftools/reports/` were accessible using a legacy non-expiring token. If you were accessing these reports programmatically using this legacy approach, you will need to switch to using a {% data variables.product.pat_v1 %} with the `site_admin` scope. + - | + Upgraded the Notebooks container base image from Ubuntu Focal (20.04) to Noble (24.04) for continued security support. + - | + An attacker could corrupt an in-progress package upload in GitHub Packages by obtaining the identifiers for that upload, because the registry did not verify the users permissions before continuing or completing an upload. GitHub has addressed this by validating authorization before any changes are made to an upload. + bugs: + - | + Dependabot could stop running scheduled version updates after upgrade. + - | + GHES administrators running instances with high-write workloads experienced increased Redis memory use and latency because Redis created periodic RDB snapshots in addition to its configured AOF persistence. + - | + Customers with the OpenTelemetry observability stack enabled experienced a loss of metrics after reboot because some exporter services failed to start due to a dependency on the data disk being mounted. + - | + Upgrade for HA with scale-out nodes could fail with HAProxy exceeding FD limit. + - | + On instances whose user data disk was initialized before the ownership fix in ghe-storage-init, `/var/log/github-backup` remained owned by root:root, so the admin user could not write to it and ghe-backup failed immediately with a permission-denied error. + - | + Management Console users other than the root site administrator received an HTTP 401 error when they clicked "Backup Now" or attempted to delete (prune) a backup on the Backups page, even though the controls were displayed to them. These authenticated operators can now start and prune backups successfully. + - | + Users experienced slow Git LFS uploads over high-latency network connections, because the instances HAProxy frontend advertised a fixed HTTP/2 initial receive window that throttled each stream to roughly one 64 KiB window per round trip. Administrators can now tune this window with `ghe-config core.haproxy-h2-fe-initial-window-size `, using a value between 65536 and 67108864 (64 KiB-64 MiB) inclusive, to improve throughput for large uploads. The setting is opt-in and disabled by default. + - | + Kafka-lite sessions in Grafana dashboard showed empty panels. + - | + Configured standalone restores could omit storage objects when Replication Controller changed storage topology before the restore completed. Replication Controller is now paused during this restore window. + - | + The `ghe-cluster-support-bundle` `-x` short option required an argument to run. + - | + On AWS and Azure, `ghe-setup-network` now applies explicitly configured IPv6 networking without enabling IPv6 during IPv4-only network updates. The `-4` and `-6` options now disable the selected address family, and explicit IPv4 and IPv6 network configuration is preserved across feature upgrades. + - | + Administrators running backups with the backup service could see a successful exit code even when rsync failed to back up repositories and gists. The backup service now reports these failures correctly. + - | + On an instance with suspended users, organization member counts on the enterprise organizations page and the organization people page included suspended users. This caused the displayed count to be higher than the number of members visible in the list. + - | + On an instance with security overview enabled, reconciliation could repeatedly retry when background job queues were busy, causing high CPU usage and delays across the appliance. + - | + The first user created via SAML-JIT during first-run bootstrap on a fresh GHES appliance was not added as an Enterprise owner. + - | + Enterprise Managed Users could remain absent from enterprise membership listings and the `enterprise.members` GraphQL response after being reinstated. + - | + On the secret scanning metrics page for a large enough enterprise, the page could time out. + - | + Enterprise owners would sometimes experience timeouts when loading the "Notifications preferences" section on the "Verified & approved domains" page. + - | + Administrators who deleted a user that belonged to an organization received a 500 error, even though the user was successfully removed. Previously, the deletion attempted to enqueue a job to a Copilot Spaces queue that does not exist on GitHub Enterprise Server, causing the request to fail. + - | + Advanced security configuration pages would not load at all if there was a timeout affecting certain subsections. + - | + The User contributions backfill job would process the full repository set per user, and not just those with contributions, leading to an excessively high `user_contributions_backfill` queue backlog and contributing to high resource consumption when large batches of user changes occur. + - | + Administrators on instances with many organizations experienced security configurations failing to apply to repositories. Fanout processing repeatedly retried without applying repository updates. + - | + GitHub Advanced Security configuration pages would time out too quickly. + - | + On instances using the Noble-based container image, users received errors when using auto-merge because GitHub Enterprise Server could not read the contents of the `git` and `gitro` home directories. + - | + On an instance using Redis 7, AOF (append-only file) recovery did not detect or repair corruption in Redis data volumes. This occurred because recovery only checked the legacy single-file AOF path and did not account for Redis 7s multi-part AOF format, which uses a manifest to track data across multiple files. + - | + When an administrator ran a configured restore that changed the appliances identity (UUID), the restore could skip the storage tier and still report success, resulting in an incomplete restore of repository data. + changes: + - | + Enterprise Live Migrations skip branch protection rules by default because only part of each rule can be migrated. Partial migration of branch protection rules is opt in with `ghe-config app.elm-exporter.migrate-protected-branches true`. + - | + Administrators can limit the CPU consumed by MySQL backup dumps by setting a CPU quota with `ghe-config backup.cpu-quota`. Provide a systemd `CPUQuota` value, such as `200%` to allow up to two cores. The setting is empty by default, which leaves backup behavior unchanged. The cap is applied on the node where each dump runs, including remote cluster and replica nodes. + - | + During a live upgrade, the retained copy of a table left after an online schema migration is now removed in a single operation rather than by a gradual, row-by-row deletion, reducing database load and reclaiming disk space more quickly. + - | + To avoid data processing delays on instances with a large number of internal message topics, administrators can configure the network timeouts used during topic discovery via `app.github.topic-checker-connect-timeout-sec` and `app.github.topic-checker-socket-timeout-sec`. The default timeout has been increased from 10 to 30 seconds, and values between 1 and 300 seconds are accepted. + 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](/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`. + - | + In some cases, pull requests using auto-merge or merge queue may not merge automatically until mergeability is recalculated. + - | + After upgrading to GHES 3.21, scheduled Dependabot version updates may stop running for pre-existing configurations. If you have already upgraded and want to trigger scheduled version updates, save a change to each affected repository’s `.github/dependabot.yml` file. + - | + In clustered GitHub Enterprise Server environments, administrators may find that nodes that remain in the cluster but no longer have the `git-server`, `pages-server`, or `storage-server` role remain online and eligible to vote in the corresponding replication tables. Review role-specific replication state and contact GitHub Support if stale rows affect replication. + - | + _When restoring an instance with `ghe-restore` while the replication controller is enabled, the storage directory is not restored._ + - | + _On a newly booted {% data variables.product.prodname_ghe_server %} instance, the merge box on a newly created pull request can stay on "Checking for the ability to merge automatically" and not show the merge status. If encountered, refreshing the page shows the correct merge status._ diff --git a/data/release-notes/enterprise-server/3-22/1.yml b/data/release-notes/enterprise-server/3-22/1.yml new file mode 100644 index 000000000000..8ad970f32781 --- /dev/null +++ b/data/release-notes/enterprise-server/3-22/1.yml @@ -0,0 +1,125 @@ +date: '2026-09-22' +sections: + features: + - | + Site administrators can configure MySQL temporary table and heap table size limits using `ghe-config` to work around a known MySQL optimizer issue that can cause queries to use disk-based temporary tables unnecessarily. To adjust the temporary table size, run `ghe-config mysql.tmp-table-size ` followed by `ghe-config-apply`. + security_fixes: + - | + **CRITICAL**: An attacker with network access to a GitHub Enterprise Server instance could supply a notebook viewer URL specifying an explicit port to reach internal services co-located on the appliance. Although response bodies were not returned, response timing could act as an oracle that allowed instance secrets to be extracted. These secrets could be used to obtain remote code execution on the appliance. Exploitation was unauthenticated when private mode was disabled and required any authenticated user when private mode was enabled. GitHub Enterprise Server now rejects notebook viewer URLs that specify an explicit port. GitHub has requested CVE ID [CVE-2026-77987](https://www.cve.org/cverecord?id=CVE-2026-77987) for this vulnerability. This vulnerability was reported through the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + **HIGH**: An authenticated attacker could execute malicious scripts in another user's browser by posting crafted Markdown content, such as an issue comment, that injected arbitrary HTML attributes into the rendered page. Successful exploitation could allow the attacker to read data visible to the victim, perform state-changing actions as the victim, exfiltrate data, and propagate the payload using the victim's write access. This was possible because the Markdown rendering pipeline rewrote quote characters in already-sanitized HTML without re-sanitizing the result. GitHub has requested CVE ID [CVE-2026-77912](https://www.cve.org/cverecord?id=CVE-2026-77912) for this vulnerability, which was reported via the [GitHub Bug Bounty program](https://bounty.github.com/). + - | + Server reports under `/stafftools/reports/` were accessible using a legacy non-expiring token. If you were accessing these reports programmatically using this legacy approach, you will need to switch to using a {% data variables.product.pat_v1 %} with the `site_admin` scope. + - | + An attacker could corrupt an in-progress package upload in GitHub Packages by obtaining the identifiers for that upload, because the registry did not verify the users permissions before continuing or completing an upload. GitHub has addressed this by validating authorization before any changes are made to an upload. + bugs: + - | + Fixes a bug where Dependabot stopped running scheduled version updates after upgrade + - | + GHES administrators running instances with high-write workloads experienced increased Redis memory use and latency because Redis created periodic RDB snapshots in addition to its configured AOF persistence. + - | + Customers with the OpenTelemetry observability stack enabled experienced a loss of metrics after reboot because some exporter services failed to start due to a dependency on the data disk being mounted. + - | + Upgrade for HA with scale-out nodes could fail with HAProxy exceeding FD limit. + - | + On instances whose user data disk was initialized before the ownership fix in ghe-storage-init, `/var/log/github-backup` remained owned by root:root, so the admin user could not write to it and ghe-backup failed immediately with a permission-denied error. + - | + Management Console users other than the root site administrator received an HTTP 401 error when they clicked "Backup Now" or attempted to delete (prune) a backup on the Backups page, even though the controls were displayed to them. These authenticated operators can now start and prune backups successfully. + - | + Users experienced slow Git LFS uploads over high-latency network connections, because the instances HAProxy frontend advertised a fixed HTTP/2 initial receive window that throttled each stream to roughly one 64 KiB window per round trip. Administrators can now tune this window with `ghe-config core.haproxy-h2-fe-initial-window-size `, using a value between 65536 and 67108864 (64 KiB-64 MiB) inclusive, to improve throughput for large uploads. The setting is opt-in and disabled by default. + - | + Kafka-lite sessions in Grafana dashboard showed empty panels. + - | + Configured standalone restores could omit storage objects when Replication Controller changed storage topology before the restore completed. Replication Controller is now paused during this restore window. + - | + The `ghe-cluster-support-bundle` `-x` short option required an argument to run. + - | + On AWS and Azure, `ghe-setup-network` now applies explicitly configured IPv6 networking without enabling IPv6 during IPv4-only network updates. The `-4` and `-6` options now disable the selected address family, and explicit IPv4 and IPv6 network configuration is preserved across feature upgrades. + - | + Administrators running backups with the backup service could see a successful exit code even when rsync failed to back up repositories and gists. The backup service now reports these failures correctly. + - | + On an instance with suspended users, organization member counts on the enterprise organizations page and the organization people page included suspended users. This caused the displayed count to be higher than the number of members visible in the list. + - | + On an instance with security overview enabled, reconciliation could repeatedly retry when background job queues were busy, causing high CPU usage and delays across the appliance. + - | + The first user created via SAML-JIT during first-run bootstrap on a fresh GHES appliance was not added as an Enterprise owner. + - | + Enterprise Managed Users could remain absent from enterprise membership listings and the `enterprise.members` GraphQL response after being reinstated. + - | + On the secret scanning metrics page for a large enough enterprise, the page could time out. + - | + Enterprise owners would sometimes experience timeouts when loading the "Notifications preferences" section on the "Verified & approved domains" page. + - | + Administrators who deleted a user that belonged to an organization received a 500 error, even though the user was successfully removed. Previously, the deletion attempted to enqueue a job to a Copilot Spaces queue that does not exist on GitHub Enterprise Server, causing the request to fail. + - | + Advanced security configuration pages would not load at all if there was a timeout affecting certain subsections. + - | + The User contributions backfill job would process the full repository set per user, and not just those with contributions, leading to an excessively high `user_contributions_backfill` queue backlog and contributing to high resource consumption when large batches of user changes occur. + - | + Administrators on instances with many organizations experienced security configurations failing to apply to repositories. Fanout processing repeatedly retried without applying repository updates. + - | + GitHub Advanced Security configuration pages would time out too quickly. + - | + On instances using the Noble-based container image, users received errors when using auto-merge because GitHub Enterprise Server could not read the contents of the `git` and `gitro` home directories. + - | + On an instance using Redis 7, AOF (append-only file) recovery did not detect or repair corruption in Redis data volumes. This occurred because recovery only checked the legacy single-file AOF path and did not account for Redis 7s multi-part AOF format, which uses a manifest to track data across multiple files. + - | + When an administrator ran a configured restore that changed the appliances identity (UUID), the restore could skip the storage tier and still report success, resulting in an incomplete restore of repository data. + changes: + - | + To avoid data processing delays on instances with a large number of internal message topics, administrators can configure the network timeouts used during topic discovery via `app.github.topic-checker-connect-timeout-sec` and `app.github.topic-checker-socket-timeout-sec`. The default timeout has been increased from 10 to 30 seconds, and values between 1 and 300 seconds are accepted. + - | + Enterprise Live Migrations skip branch protection rules by default because only part of each rule can be migrated. Partial migration of branch protection rules is opt in with `ghe-config app.elm-exporter.migrate-protected-branches true`. + - | + Administrators can limit the CPU consumed by MySQL backup dumps by setting a CPU quota with `ghe-config backup.cpu-quota`. Provide a systemd `CPUQuota` value, such as `200%` to allow up to two cores. The setting is empty by default, which leaves backup behavior unchanged. The cap is applied on the node where each dump runs, including remote cluster and replica nodes. + - | + Upgrade (.pkg) and hotpatch (.hpkg) packages are smaller, reducing the time spent downloading a package and verifying its signature during an upgrade. + - | + Site administrators can ensure that secret scanning uses processor features available on their GHES instance because secret scanning compiles its pattern-matching databases on the instance instead of using prebuilt databases. The first scan of each type after an upgrade or restart can take a few seconds longer while the databases are prepared. + 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](/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`. + - | + After creating a new branch via the `New branch` button, the `/branches` page doesn't automatically show the new branch. The page requires a manual refresh before the new branch appears + - | + In some cases, pull requests using auto-merge or merge queue may not merge automatically until mergeability is recalculated. + - | + After upgrading to GHES 3.21, scheduled Dependabot version updates may stop running for pre-existing configurations. If you have already upgraded and want to trigger scheduled version updates, save a change to each affected repository’s `.github/dependabot.yml` file. + - | + In clustered GitHub Enterprise Server environments, administrators may find that nodes that remain in the cluster but no longer have the `git-server`, `pages-server`, or `storage-server` role remain online and eligible to vote in the corresponding replication tables. Review role-specific replication state and contact GitHub Support if stale rows affect replication. + - | + _When restoring an instance with `ghe-restore` while the replication controller is enabled, the storage directory is not restored._ + - | + _On a newly booted {% data variables.product.prodname_ghe_server %} instance, the merge box on a newly created pull request can stay on "Checking for the ability to merge automatically" and not show the merge status. If encountered, refreshing the page shows the correct merge status._ From d1153b57c92b9039614042773d49cf3185f3a184 Mon Sep 17 00:00:00 2001 From: Sunbrye Ly <56200261+sunbrye@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:09:05 +0000 Subject: [PATCH 6/8] Add GPT-6 Sol and GPT-6 Luna (#63391) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../reference/ai-models/model-hosting.md | 2 + .../reference/ai-models/supported-models.md | 4 ++ .../copilot-billing/models-and-pricing.md | 2 +- .../copilot-cloud-agent-non-auto-models.md | 2 + data/tables/copilot/model-comparison.yml | 12 ++++- data/tables/copilot/model-release-status.yml | 8 ++++ .../copilot/model-supported-clients.yml | 18 ++++++++ data/tables/copilot/model-supported-plans.yml | 14 ++++++ data/tables/copilot/models-and-pricing.yml | 44 +++++++++++++++++++ data/variables/copilot.yml | 2 + 10 files changed, 106 insertions(+), 2 deletions(-) diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md index 9d85d345cdc0..b8d8c3502c71 100644 --- a/content/copilot/reference/ai-models/model-hosting.md +++ b/content/copilot/reference/ai-models/model-hosting.md @@ -28,6 +28,8 @@ Used for: * {% data variables.copilot.copilot_gpt_56_sol %} * {% data variables.copilot.copilot_gpt_56_terra %} * {% data variables.copilot.copilot_gpt_6_astra %} +* {% data variables.copilot.copilot_gpt_6_luna %} +* {% data variables.copilot.copilot_gpt_6_sol %} These models are hosted by OpenAI and {% data variables.product.github %}'s Azure infrastructure. diff --git a/content/copilot/reference/ai-models/supported-models.md b/content/copilot/reference/ai-models/supported-models.md index a57b9c0784d2..e18f9c2166ef 100644 --- a/content/copilot/reference/ai-models/supported-models.md +++ b/content/copilot/reference/ai-models/supported-models.md @@ -94,6 +94,8 @@ Choosing a larger context window or higher reasoning will impact {% data variabl | {% data variables.copilot.copilot_gpt_56_sol %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | | {% data variables.copilot.copilot_gpt_56_terra %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | | {% data variables.copilot.copilot_gpt_6_astra %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | +| {% data variables.copilot.copilot_gpt_6_luna %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | +| {% data variables.copilot.copilot_gpt_6_sol %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | | {% data variables.copilot.copilot_kimi_k3 %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% endrowheaders %} @@ -135,6 +137,8 @@ Some {% data variables.product.prodname_copilot_short %} models require minimum | {% data variables.copilot.copilot_gpt_56_sol %} | `1.128.0` | TBD | TBD | TBD | TBD | | {% data variables.copilot.copilot_gpt_56_terra %} | `1.128.0` | TBD | TBD | TBD | TBD | | {% data variables.copilot.copilot_gpt_6_astra %} | `1.136.1` | `17.14.19` | TBD | TBD | TBD | +| {% data variables.copilot.copilot_gpt_6_luna %} | TBD | `17.14.19` | TBD | TBD | TBD | +| {% data variables.copilot.copilot_gpt_6_sol %} | TBD | `17.14.19` | TBD | TBD | TBD | | {% data variables.copilot.copilot_claude_opus_48 %} | `v1.118` | `17.14.6` | TBD | TBD | TBD | | {% data variables.copilot.copilot_claude_opus_5 %} | `v1.128.0` | `17.14.22` | TBD | TBD | TBD | | {% data variables.copilot.copilot_claude_opus_55 %} | TBD | `17.14.6` | TBD | TBD | TBD | diff --git a/content/copilot/reference/copilot-billing/models-and-pricing.md b/content/copilot/reference/copilot-billing/models-and-pricing.md index 835b6680274d..c316385ca524 100644 --- a/content/copilot/reference/copilot-billing/models-and-pricing.md +++ b/content/copilot/reference/copilot-billing/models-and-pricing.md @@ -33,7 +33,7 @@ All prices are **per 1 million tokens**. {% data reusables.copilot.extended-context-pricing %} -{% data variables.copilot.copilot_gpt_56_sol %}, {% data variables.copilot.copilot_gpt_56_terra %}, {% data variables.copilot.copilot_gpt_56_luna %}, and {% data variables.copilot.copilot_gpt_6_astra %} include a cache write cost in addition to cached input. Earlier OpenAI models have no cache write cost. +{% data variables.copilot.copilot_gpt_56_sol %}, {% data variables.copilot.copilot_gpt_56_terra %}, {% data variables.copilot.copilot_gpt_56_luna %}, {% data variables.copilot.copilot_gpt_6_astra %}, {% data variables.copilot.copilot_gpt_6_luna %}, and {% data variables.copilot.copilot_gpt_6_sol %} include a cache write cost in addition to cached input. Earlier OpenAI models have no cache write cost. | Model | Release status | Category | Tier | Threshold (input tokens) | Input | Cached input | Cache write | Output | | --- | --- | --- | --- | --- | ---: | ---: | ---: | ---: | diff --git a/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md b/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md index f3f562dfd0c6..be79f8ae57ec 100644 --- a/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md +++ b/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md @@ -11,6 +11,8 @@ * {% data variables.copilot.copilot_gpt_56_sol %} * {% data variables.copilot.copilot_gpt_56_terra %} * {% data variables.copilot.copilot_gpt_6_astra %} +* {% data variables.copilot.copilot_gpt_6_luna %} +* {% data variables.copilot.copilot_gpt_6_sol %} * {% data variables.copilot.copilot_grok_45 %} * {% data variables.copilot.copilot_grok_46 %} * {% data variables.copilot.copilot_grok_47 %} diff --git a/data/tables/copilot/model-comparison.yml b/data/tables/copilot/model-comparison.yml index 2a1bcd294249..37d8fc3448e4 100644 --- a/data/tables/copilot/model-comparison.yml +++ b/data/tables/copilot/model-comparison.yml @@ -53,6 +53,16 @@ excels_at: Long-horizon coding tasks with continuous planning, batched diagnosis and verification, and independent result confirmation further_reading: '[GPT-6 Astra model card](https://deploymentsafety.openai.com/gpt-6-astra/gpt-6-astra.pdf)' +- name: GPT-6 Luna + task_area: Fast help with simple or repetitive tasks + excels_at: Quick, cost-efficient responses for smaller, faster tasks + further_reading: 'Coming soon' + +- name: GPT-6 Sol + task_area: Interactive and agentic coding + excels_at: All-round development tasks that benefit from careful, multistep validation + further_reading: 'Coming soon' + # Anthropic - name: Claude Fable 5 task_area: Long-horizon, autonomous coding and knowledge-work @@ -145,7 +155,7 @@ - name: Grok 4.7 task_area: Agentic coding and complex, multistep workflows excels_at: Complex problem-solving challenges, sophisticated reasoning - further_reading: 'Coming soon' + further_reading: '[Grok 4.7 model card](https://media.x.ai/v1/website/4p7card-5eccc980.pdf)' # Other providers (alphabetized by model name) - name: Qwen2.5 diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml index 5ec81bb8abd1..e83b05141cab 100644 --- a/data/tables/copilot/model-release-status.yml +++ b/data/tables/copilot/model-release-status.yml @@ -55,6 +55,14 @@ provider: 'OpenAI' release_status: 'GA' +- name: 'GPT-6 Luna' + provider: 'OpenAI' + release_status: 'GA' + +- name: 'GPT-6 Sol' + provider: 'OpenAI' + release_status: 'GA' + # Anthropic models - name: 'Claude Fable 5' diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml index 14166ac75fe4..2d2807e3a9fc 100644 --- a/data/tables/copilot/model-supported-clients.yml +++ b/data/tables/copilot/model-supported-clients.yml @@ -239,6 +239,24 @@ xcode: true jetbrains: true +- name: GPT-6 Luna + dotcom: true + cli: true + vscode: true + vs: true + eclipse: true + xcode: true + jetbrains: true + +- name: GPT-6 Sol + dotcom: true + cli: true + vscode: true + vs: true + eclipse: true + xcode: true + jetbrains: true + - name: Grok 4.5 dotcom: false cli: true diff --git a/data/tables/copilot/model-supported-plans.yml b/data/tables/copilot/model-supported-plans.yml index 11ff89823063..32887443a1a2 100644 --- a/data/tables/copilot/model-supported-plans.yml +++ b/data/tables/copilot/model-supported-plans.yml @@ -180,6 +180,20 @@ business: true enterprise: true +- name: GPT-6 Luna + pro: true + pro_plus: true + max: true + business: true + enterprise: true + +- name: GPT-6 Sol + pro: false + pro_plus: true + max: true + business: true + enterprise: true + - name: Grok 4.5 pro: true pro_plus: true diff --git a/data/tables/copilot/models-and-pricing.yml b/data/tables/copilot/models-and-pricing.yml index b0fad13a067d..b68d523a6e4e 100644 --- a/data/tables/copilot/models-and-pricing.yml +++ b/data/tables/copilot/models-and-pricing.yml @@ -196,6 +196,50 @@ output: $75.00 cache_write: $25.00 +- model: GPT-6 Luna + provider: openai + release_status: GA + category: Lightweight + threshold: '≤ 272K' + tier: Default + input: $0.10 + cached_input: $0.01 + output: $0.50 + cache_write: $0.125 + +- model: GPT-6 Luna + provider: openai + release_status: GA + category: Lightweight + threshold: '> 272K' + tier: 'Long context' + input: $0.20 + cached_input: $0.02 + output: $0.75 + cache_write: $0.25 + +- model: GPT-6 Sol + provider: openai + release_status: GA + category: Powerful + threshold: '≤ 272K' + tier: Default + input: $2.00 + cached_input: $0.20 + output: $10.00 + cache_write: $2.50 + +- model: GPT-6 Sol + provider: openai + release_status: GA + category: Powerful + threshold: '> 272K' + tier: 'Long context' + input: $4.00 + cached_input: $0.40 + output: $15.00 + cache_write: $5.00 + # Anthropic - model: Claude Haiku 4.5 provider: anthropic diff --git a/data/variables/copilot.yml b/data/variables/copilot.yml index 752ebcf593dd..996425dd1e0a 100644 --- a/data/variables/copilot.yml +++ b/data/variables/copilot.yml @@ -234,6 +234,8 @@ copilot_gpt_56_luna: 'GPT-5.6 Luna' copilot_gpt_56_sol: 'GPT-5.6 Sol' copilot_gpt_56_terra: 'GPT-5.6 Terra' copilot_gpt_6_astra: 'GPT-6 Astra' +copilot_gpt_6_luna: 'GPT-6 Luna' +copilot_gpt_6_sol: 'GPT-6 Sol' # OpenAI 'o' series: copilot_o3: 'o3' copilot_o4_mini: 'o4-mini' From 0a6b6c668b0627abc75572005f31eff46bf9f1e5 Mon Sep 17 00:00:00 2001 From: Laura Coursen Date: Tue, 22 Sep 2026 19:14:06 +0000 Subject: [PATCH 7/8] Add screenshot guidance to always-on content instructions (#63127) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 47b893b9-9c89-498d-b1f2-c46e81d5c17e Copilot-Session: 78be3fdf-d901-472a-8ad5-d5c984eac81f --- .github/instructions/content-guidelines.instructions.md | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/instructions/content-guidelines.instructions.md b/.github/instructions/content-guidelines.instructions.md index b788503764db..984856faeef9 100644 --- a/.github/instructions/content-guidelines.instructions.md +++ b/.github/instructions/content-guidelines.instructions.md @@ -74,4 +74,5 @@ These apply to the prose in an article (intros and explanatory text), not to str * Write for the one reader scenario the article targets, for example a particular deployment configuration (GHEC with EMUs vs. Classic) or a particular type of reader (an open source maintainer vs. an enterprise developer). When the article has a content design plan, target the audience it identifies rather than inventing one; for small edits without a plan, follow the audience the existing article is clearly written for. Do not branch content to serve multiple audiences; readers in other scenarios can adapt the guidance. The exception is version differences: when in-article `{% ifversion %}` branching is genuinely required (see the versioning rules in `content.instructions.md`), it is not a scannability violation. * Ruthlessly minimize links. Only link when you actively want most readers to follow it in the ideal scenario. No "just in case" links. Links that build a logical user journey are exactly the kind to keep, for example a Prerequisites link that sends the reader to setup they need first, or a Next steps link that points them to the natural follow-on task. * Ruthlessly minimize alerts (notes, tips, warnings): more than one per article should be exceptional, and crowding several into one section is worse than spreading them out. Keep each to 1-2 sentences. Don't open an article or section with an alert unless the reader needs it before the surrounding content. Prefer folding a useful alert into the prose over deleting it, but first apply this test: if the reader must actually notice it to use the page correctly, keep it as an alert (don't fold or count it), since folding defeats its purpose. This covers, for example, critical warnings, plan or availability constraints, public preview notices, and cues that orient the reader to how the page works or which content applies to them. +* Ruthlessly minimize screenshots. Only add one, or ask a writer to add one, when a UI element is genuinely hard to find: small, hidden until something is opened, or ambiguous among competing options. A mention of screenshots in an issue or PR description is not a reason. * Prefer short sentences and paragraphs, generous white space, and formatting like bold and tables to highlight key information. Keep each sentence to one idea: where a sentence carries two, split it into two sentences rather than trimming it. Use a table only for genuinely complex data that belongs in a tabular format; do not add a table that repeats information already stated more clearly in prose. From 9e9e018bd2f63716cc0a178f2b5f5c85052c6187 Mon Sep 17 00:00:00 2001 From: Sunbrye Ly <56200261+sunbrye@users.noreply.github.com> Date: Tue, 22 Sep 2026 19:56:20 +0000 Subject: [PATCH 8/8] Preserve redirect_from entries across Copilot SDK docs syncs (#62619) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 2cf57dde-5ea8-4f4a-89c9-b748ce3b5028 Copilot-Session: b513525a-1c7a-446d-b4d2-d296250dd9ee Copilot-Session: a8e2667d-7f41-4341-b1ca-58c0a42d3a73 Copilot-Session: 860ffa15-aef4-4655-a9cb-f4e04cc36886 --- .github/workflows/sync-sdk-docs.yml | 40 +- .../tests/render-changed-and-deleted-files.ts | 13 +- .../sync-sdk-docs/normalize-sdk-docs.ts | 23 +- .../sync-sdk-docs/preserve-redirects.ts | 515 ++++++++++++++++ .../sync-sdk-docs/strip-hidden-blocks.ts | 4 +- .../tests/sync-sdk-docs-preserve-redirects.ts | 577 ++++++++++++++++++ 6 files changed, 1150 insertions(+), 22 deletions(-) create mode 100644 src/workflows/sync-sdk-docs/preserve-redirects.ts create mode 100644 src/workflows/tests/sync-sdk-docs-preserve-redirects.ts diff --git a/.github/workflows/sync-sdk-docs.yml b/.github/workflows/sync-sdk-docs.yml index 82a1b2d3a1b4..92796217453e 100644 --- a/.github/workflows/sync-sdk-docs.yml +++ b/.github/workflows/sync-sdk-docs.yml @@ -50,6 +50,26 @@ jobs: - name: Checkout docs-internal uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + # `preserve-redirects.ts` reads the pre-sync state from `git HEAD` to learn + # which URLs are currently live. That is only a valid baseline when HEAD is + # the published branch. `pull_request` runs are safe because they never + # publish, but a `workflow_dispatch` from another branch would publish while + # comparing against that branch's tree, writing stale redirects into a real + # PR. Fail early rather than let the run reach the push step. + - name: Verify publishing runs start from the default branch + if: github.event_name != 'pull_request' && inputs.dry_run != 'true' + run: | + if [ "$GITHUB_REF_NAME" != "$DEFAULT_BRANCH" ]; then + echo "::error::This run would push and open a PR, but it started from" \ + "'$GITHUB_REF_NAME' rather than the default branch '$DEFAULT_BRANCH'." \ + "HEAD is the baseline for redirect preservation, so syncing from another" \ + "branch can drop or invent redirects. Re-run from '$DEFAULT_BRANCH', or" \ + "use the dry_run input to test from a branch." + exit 1 + fi + env: + DEFAULT_BRANCH: ${{ github.event.repository.default_branch }} + - name: Fetch SDK docs from copilot-sdk env: GH_TOKEN: ${{ secrets.DOCS_BOT_PAT_BASE }} @@ -91,6 +111,22 @@ jobs: --content-dir content \ --sdk-docs-dir "$SDK_DOCS_TARGET" + - name: Preserve redirects + run: | + # `--git-ref HEAD` is the record of which URLs are currently live. On + # publishing runs a preceding step has verified HEAD is the default + # branch, and the sync branch is only created later with `checkout -B`. + # On `pull_request` runs HEAD is the merge commit instead, which is fine + # because those runs are dry-run only. + # + # A removed page that needs a redirect decision should still produce a + # PR, because the PR is where that decision gets made and committed. The + # script still writes the at-risk URLs and a copy-pasteable + # `redirect_from` block to the run summary either way. + npx tsx src/workflows/sync-sdk-docs/preserve-redirects.ts \ + --sdk-docs-dir "$SDK_DOCS_TARGET" \ + --git-ref HEAD + - name: Convert Mermaid diagrams to PNG env: PUPPETEER_CHROMIUM_REVISION: '' @@ -165,7 +201,7 @@ jobs: ${{ env.ASSETS_TARGET }} retention-days: 7 - # --- Push and PR (only on schedule/dispatch, not dry-run, and changes exist) --- + # --- Push and PR (only on dispatch, not dry-run, and changes exist) --- - name: Commit and push if: >- env.has_changes == 'true' @@ -212,7 +248,7 @@ jobs: - Normalizes code fence languages and list formatting > [!NOTE] - > This PR is auto-generated. Do not edit it directly — make changes in the [copilot-sdk docs](https://github.com/github/copilot-sdk/tree/main/docs) instead. + > This PR is auto-generated. Do not edit it directly — make changes in the [copilot-sdk docs](https://github.com/github/copilot-sdk/tree/main/docs) instead. Exception: \`redirect_from\` cannot come from upstream, so add redirects here. --- _Generated by the [sync-sdk-docs workflow run]($GITHUB_SERVER_URL/$GITHUB_REPOSITORY/actions/runs/$GITHUB_RUN_ID)._" diff --git a/src/content-render/tests/render-changed-and-deleted-files.ts b/src/content-render/tests/render-changed-and-deleted-files.ts index d856d92227cd..617089e59ea4 100644 --- a/src/content-render/tests/render-changed-and-deleted-files.ts +++ b/src/content-render/tests/render-changed-and-deleted-files.ts @@ -44,21 +44,12 @@ const EMPTY = Symbol('EMPTY') const pageList = await loadPages(undefined, ['en']) -const SDK_DOCS_PATH = 'content/copilot/how-tos/copilot-sdk/' - function getChangedContentFiles() { const deleted = new Set([...getDeletedContentFiles(), ...getRenamedOldContentFiles()]) - return getContentFiles(process.env.CHANGED_FILES).filter( - (f) => !deleted.has(f) && !f.startsWith(SDK_DOCS_PATH), - ) + return getContentFiles(process.env.CHANGED_FILES).filter((f) => !deleted.has(f)) } function getDeletedContentFiles() { - return getContentFiles(process.env.DELETED_FILES).filter((file) => { - // Auto-generated SDK docs are managed by the sync-sdk-docs pipeline, - // which deletes and recreates pages when the source repo restructures. - // These deletions are expected and don't need redirects. - return !file.startsWith(SDK_DOCS_PATH) - }) + return getContentFiles(process.env.DELETED_FILES) } // Parse `RENAMED_FILES` from tj-actions/changed-files `all_old_new_renamed_files` diff --git a/src/workflows/sync-sdk-docs/normalize-sdk-docs.ts b/src/workflows/sync-sdk-docs/normalize-sdk-docs.ts index a4a1ec29566f..57901202dc25 100644 --- a/src/workflows/sync-sdk-docs/normalize-sdk-docs.ts +++ b/src/workflows/sync-sdk-docs/normalize-sdk-docs.ts @@ -30,7 +30,7 @@ import path from 'node:path' import { parseArgs } from 'node:util' import matter from '@gr2m/gray-matter' -import { stripHiddenBlocks } from './strip-hidden-blocks' +import { stripHiddenBlocks, nextFenceState, type OpenFence } from './strip-hidden-blocks' // Parse CLI arguments const { values: args } = parseArgs({ @@ -819,18 +819,19 @@ function convertDetailsToCodetabs(filePath: string): void { const lines = raw.split('\n') const result: string[] = [] let changed = false - let inCodeBlock = false + let openFence: OpenFence | null = null let i = 0 while (i < lines.length) { const line = lines[i] - // Track code fences to avoid matching

inside code blocks - if (/^\s*```/.test(line)) { - inCodeBlock = !inCodeBlock - } + // A bare toggle counts any ``` line as a delimiter, so a fenced content + // line such as ```
flips the state mid-block. That used to + // self-correct only because a stalled cursor re-toggled the same line. + // Now that every line is visited once, track fences the CommonMark way. + openFence = nextFenceState(line, openFence) - if (inCodeBlock || !/]/.test(line)) { + if (openFence || !/]/.test(line)) { result.push(line) i++ continue @@ -862,6 +863,14 @@ function convertDetailsToCodetabs(filePath: string): void { // Only convert groups of 2+ blocks if (group.length < 2) { + // When the first block fails to parse, `i` never moved — which happens + // for an inline `
` mention in prose, since fence tracking does + // not cover code spans. Step over the line so the loop can't stall. + if (i === groupStartLine) { + result.push(lines[i]) + i++ + continue + } // Emit original lines unchanged for (let j = groupStartLine; j < i; j++) { result.push(lines[j]) diff --git a/src/workflows/sync-sdk-docs/preserve-redirects.ts b/src/workflows/sync-sdk-docs/preserve-redirects.ts new file mode 100644 index 000000000000..1bf56ad9e983 --- /dev/null +++ b/src/workflows/sync-sdk-docs/preserve-redirects.ts @@ -0,0 +1,515 @@ +#!/usr/bin/env node + +/** + * Preserves and generates `redirect_from` frontmatter for synced Copilot SDK docs. + * + * The sync workflow deletes the SDK content directory and rebuilds it from the + * upstream repo on every run. Upstream markdown has no `redirect_from`, and the + * normalizer builds frontmatter from scratch, so every redirect previously added + * in docs-internal is silently dropped. Each sync since the May 2026 restructure + * has needed a manual "restore redirects" commit to avoid shipping live 404s. + * + * This script runs after normalization and reconciles the rebuilt tree against + * the pre-sync state recorded in git: + * + * - Preserve: redirects on a page that still exists are merged back in. + * - Generate: when a page disappears (renamed or moved upstream), its URL — + * plus any redirects it had accumulated — are transferred to its successor, + * so redirect chains are never broken. + * + * The script only ever adds redirects. It never removes one, so a redirect added + * by hand in docs-internal survives indefinitely. + * + * Usage: + * npx tsx preserve-redirects.ts --sdk-docs-dir [--git-ref HEAD] [--fail-on-unresolved] + */ + +import fs from 'node:fs' +import path from 'node:path' +import { execFileSync } from 'node:child_process' +import { parseArgs } from 'node:util' +import matter from '@gr2m/gray-matter' + +/** + * Convert a repo-relative content path to the URL docs.github.com serves it at. + * + * `content/copilot/how-tos/copilot-sdk/features/mcp.md` -> `/copilot/how-tos/copilot-sdk/features/mcp` + * `content/copilot/how-tos/copilot-sdk/auth/index.md` -> `/copilot/how-tos/copilot-sdk/auth` + */ +export function contentPathToUrl(repoRelativePath: string): string { + const withoutPrefix = repoRelativePath + .replace(/\\/g, '/') + .replace(/^content\//, '') + .replace(/\.md$/, '') + const withoutIndex = withoutPrefix.replace(/(^|\/)index$/, '') + return `/${withoutIndex}`.replace(/\/$/, '') || '/' +} + +/** Read `redirect_from` from a frontmatter blob, tolerating string or array form. */ +export function readRedirects(data: Record): string[] { + const raw = data.redirect_from + if (!raw) return [] + const list = Array.isArray(raw) ? raw : [raw] + return list.filter((entry): entry is string => typeof entry === 'string') +} + +/** + * Merge redirect lists, preserving first-seen order and dropping duplicates and + * trailing slashes. `redirect-orphans` fails the build on a trailing slash. + */ +export function mergeRedirects(...lists: string[][]): string[] { + const seen = new Set() + const merged: string[] = [] + for (const entry of lists.flat()) { + const normalized = entry.trim().replace(/\/+$/, '') + if (!normalized || seen.has(normalized)) continue + seen.add(normalized) + merged.push(normalized) + } + return merged +} + +/** + * The key a page is matched on when looking for its successor. + * + * An `index.md` identifies a directory rather than a page, so matching it on + * its basename would pair unrelated directories. Those match on the parent + * directory name instead. + */ +export function successorKey(repoPath: string): { key: string; reason: string } { + const basename = path.basename(repoPath) + return basename === 'index.md' + ? { key: `dir:${path.basename(path.dirname(repoPath))}`, reason: 'directory name' } + : { key: `file:${basename}`, reason: 'file name' } +} + +/** + * Suggest a candidate successor for a page that no longer exists. + * + * Upstream restructures move files between directories but rarely rename the + * file itself, so an unambiguous name match is a useful hint. It is only a + * hint: matching names are not evidence that one page replaced another, so the + * result is reported for a human to confirm and is never written automatically. + * + * The key must identify exactly one page on *both* sides. Requiring uniqueness + * among `removedPaths` as well as `currentPaths` stops two removed pages that + * share a basename from both being pointed at the same survivor. + */ +export function findSuccessor( + removedPath: string, + currentPaths: string[], + removedPaths: string[], +): { path: string; reason: string } | null { + const { key, reason } = successorKey(removedPath) + + // Ambiguous on the removed side: several pages disappeared under this name, + // so no single one of them can claim the survivor. + if (removedPaths.filter((p) => successorKey(p).key === key).length !== 1) return null + + const matches = currentPaths.filter((p) => successorKey(p).key === key) + return matches.length === 1 ? { path: matches[0], reason } : null +} + +/** + * List the .md files present under a directory at a given git ref. + * + * `git ls-tree` exits 0 with no output when the ref is valid but the path is + * absent, so an empty list genuinely means "nothing there yet" (the first sync). + * A throw therefore means the ref itself could not be read, which must fail the + * run rather than be mistaken for a first sync — silently treating a broken + * baseline as empty would drop every redirect in the tree. + */ +function listFilesAtRef(repoRoot: string, ref: string, dirRelativeToRoot: string): string[] { + let out: string + try { + out = execFileSync('git', ['ls-tree', '-r', '--name-only', ref, '--', dirRelativeToRoot], { + encoding: 'utf8', + cwd: repoRoot, + }) + } catch (error) { + throw new Error( + `Could not read the baseline tree at ref '${ref}'. Refusing to continue, because ` + + `treating an unreadable baseline as empty would silently drop every redirect ` + + `under ${dirRelativeToRoot}.\n ${(error as Error).message}`, + ) + } + return out + .split('\n') + .map((line) => line.trim()) + .filter((line) => line.endsWith('.md')) +} + +/** + * Read a file's contents at a given git ref. + * + * Callers only ask for paths that `listFilesAtRef` just reported at this same + * ref, so a failure here is a real error, not a missing file. + */ +function readFileAtRef(repoRoot: string, ref: string, repoRelativePath: string): string { + try { + return execFileSync('git', ['show', `${ref}:${repoRelativePath}`], { + encoding: 'utf8', + cwd: repoRoot, + maxBuffer: 20 * 1024 * 1024, + }) + } catch (error) { + throw new Error( + `Could not read '${repoRelativePath}' at ref '${ref}', although it is listed there. ` + + `Refusing to continue, because skipping it would silently drop its redirects.\n ` + + `${(error as Error).message}`, + ) + } +} + +/** Recursively collect .md files from the working tree. */ +function getAllMarkdownFiles(dir: string): string[] { + const results: string[] = [] + for (const entry of fs.readdirSync(dir, { withFileTypes: true })) { + const fullPath = path.join(dir, entry.name) + if (entry.isDirectory()) { + results.push(...getAllMarkdownFiles(fullPath)) + } else if (entry.isFile() && entry.name.endsWith('.md')) { + results.push(fullPath) + } + } + return results +} + +type PreSyncPage = { + url: string + redirects: string[] +} + +/** + * Insert or replace the `redirect_from` block in a raw frontmatter string. + * + * The block is edited as text rather than re-serialized from a parsed object. + * Round-tripping through YAML rewraps long values — the `intro` field in + * particular — which would bury the redirect change in unrelated reflow noise + * on every sync. Editing the lines directly leaves every other byte untouched. + * + * The block is placed just before `contentType` to match how these files are + * already written, falling back to the end of the frontmatter. + */ +export function upsertRedirectBlock(rawFrontmatter: string, redirects: string[]): string { + const lines = rawFrontmatter.split('\n') + const isListItem = (line: string | undefined) => line !== undefined && /^\s+-\s/.test(line) + const kept: string[] = [] + + // Drop any existing redirect_from, in block form or inline form. + for (let i = 0; i < lines.length; i++) { + const line = lines[i] + + if (/^redirect_from:\s*$/.test(line)) { + // Consume the indented list that follows. A blank line is only part of + // the block if another list item comes after it; otherwise it belongs to + // whatever follows and must be preserved. + let j = i + 1 + while (j < lines.length) { + if (isListItem(lines[j])) { + j++ + continue + } + if (lines[j].trim() === '') { + let k = j + while (k < lines.length && lines[k].trim() === '') k++ + if (isListItem(lines[k])) { + j = k + continue + } + } + break + } + i = j - 1 + continue + } + + if (/^redirect_from:\s*\S/.test(line)) continue + kept.push(line) + } + + if (redirects.length === 0) return kept.join('\n') + + const block = ['redirect_from:', ...redirects.map((url) => ` - ${url}`)] + const contentTypeIndex = kept.findIndex((line) => /^contentType:/.test(line)) + const insertAt = contentTypeIndex === -1 ? kept.length : contentTypeIndex + + kept.splice(insertAt, 0, ...block) + return kept.join('\n') +} + +/** + * Rewrite a file's `redirect_from` in place. Returns true if the file changed. + */ +function writeRedirects(absolutePath: string, redirects: string[]): boolean { + const raw = fs.readFileSync(absolutePath, 'utf8') + const match = raw.match(/^(---\r?\n)([\s\S]*?)(\r?\n---\r?\n)([\s\S]*)$/) + if (!match) { + throw new Error( + `Cannot add redirects to '${absolutePath}' because it has no frontmatter block. ` + + `Refusing to continue, because skipping the write would silently drop the ` + + `redirects meant for this page:\n ${redirects.join('\n ')}`, + ) + } + const [, open, frontmatterText, close, body] = match + const updated = upsertRedirectBlock(frontmatterText, redirects) + if (updated === frontmatterText) return false + fs.writeFileSync(absolutePath, `${open}${updated}${close}${body}`, 'utf8') + return true +} + +function main() { + const { values: args } = parseArgs({ + options: { + 'sdk-docs-dir': { type: 'string' }, + 'git-ref': { type: 'string', default: 'HEAD' }, + 'fail-on-unresolved': { type: 'boolean', default: false }, + }, + }) + + const sdkDocsDirArg = args['sdk-docs-dir'] + if (!sdkDocsDirArg) { + console.error('Missing required argument: --sdk-docs-dir') + process.exit(1) + } + + const resolvedArg = path.resolve(sdkDocsDirArg) + if (!fs.existsSync(resolvedArg)) { + console.error(`SDK docs directory not found: ${resolvedArg}`) + process.exit(1) + } + + const sdkDocsDir = fs.realpathSync(resolvedArg) + const gitRef = args['git-ref'] as string + const failOnUnresolved = args['fail-on-unresolved'] as boolean + + // Resolve the root from the docs directory so the script works against any + // checkout, not just the process's current working directory. Both sides are + // canonicalized so a symlinked path (macOS /var -> /private/var) still yields + // a correct relative path. + const repoRoot = fs.realpathSync( + path.resolve( + execFileSync('git', ['rev-parse', '--show-toplevel'], { + encoding: 'utf8', + cwd: sdkDocsDir, + }).trim(), + ), + ) + + const sdkDirRelative = path.relative(repoRoot, sdkDocsDir).replace(/\\/g, '/') + + // 1. Record the pre-sync state from git. + const preSyncPaths = listFilesAtRef(repoRoot, gitRef, sdkDirRelative) + const preSyncPages = new Map() + for (const repoPath of preSyncPaths) { + const raw = readFileAtRef(repoRoot, gitRef, repoPath) + let data: Record + try { + data = matter(raw).data as Record + } catch (error) { + throw new Error( + `Could not parse the frontmatter of '${repoPath}' at ref '${gitRef}'. Refusing to ` + + `continue, because treating it as empty would silently drop any redirects it ` + + `carries.\n ${(error as Error).message}`, + ) + } + preSyncPages.set(repoPath, { + url: contentPathToUrl(repoPath), + redirects: readRedirects(data), + }) + } + + if (preSyncPaths.length === 0) { + console.log(`No SDK docs exist at ${gitRef} yet — nothing to preserve.`) + return + } + + // 2. Read the post-sync working tree. + const currentRepoPaths = getAllMarkdownFiles(sdkDocsDir).map((p) => + path.relative(repoRoot, p).replace(/\\/g, '/'), + ) + const currentRepoPathSet = new Set(currentRepoPaths) + const currentUrls = new Set(currentRepoPaths.map(contentPathToUrl)) + + // Several files can resolve to one URL (`guide.md` and `guide/index.md` both + // serve `.../guide`), so the reverse mapping is one-to-many. + const currentPathsByUrl = new Map() + for (const repoPath of currentRepoPaths) { + const url = contentPathToUrl(repoPath) + currentPathsByUrl.set(url, [...(currentPathsByUrl.get(url) ?? []), repoPath]) + } + + // Redirects to add, keyed by the repo-relative path of the page receiving them. + const additions = new Map() + const addFor = (repoPath: string, urls: string[]) => { + additions.set(repoPath, mergeRedirects(additions.get(repoPath) ?? [], urls)) + } + + // 3. Preserve redirects for pages that survived the sync at the same path. + let preservedPages = 0 + for (const repoPath of currentRepoPaths) { + const before = preSyncPages.get(repoPath) + if (!before || before.redirects.length === 0) continue + addFor(repoPath, before.redirects) + preservedPages++ + } + + const allRemoved = [...preSyncPages.keys()].filter((p) => !currentRepoPathSet.has(p)) + + // 4. A page can lose its file while keeping its URL, because `guide.md` and + // `guide/index.md` serve the same URL. The URL itself stays live, so nothing + // 404s and no successor guess is needed — but the redirects it inherited are + // still stranded, since the file now serving that URL has never carried them. + // Transfer those by URL identity rather than by inference. + const needSuccessor: string[] = [] + let reshaped = 0 + for (const removedPath of allRemoved) { + const before = preSyncPages.get(removedPath)! + const servingPaths = currentPathsByUrl.get(before.url) + if (!servingPaths) { + needSuccessor.push(removedPath) + continue + } + // `before.url` is deliberately not carried over: it is the URL these files + // already serve, so adding it would create a self-redirect. + if (before.redirects.length === 0) continue + if (servingPaths.length > 1) { + throw new Error( + `The URL '${before.url}' is served by more than one file after the sync ` + + `(${servingPaths.join(', ')}), so there is no single place to move the ` + + `redirects that '${removedPath}' was carrying:\n ${before.redirects.join('\n ')}`, + ) + } + addFor(servingPaths[0], before.redirects) + reshaped++ + console.log(` RESHAPED: ${before.url} still served by ${servingPaths[0]}, redirects moved`) + } + + // 5. Pages that lost their URL outright need a human decision. + // + // A same-named page elsewhere in the tree is reported as a candidate but is + // never written. Matching names is not evidence of succession, and a redirect + // aimed at the wrong live page is worse than a 404 because nothing catches + // it: `render-changed-and-deleted-files` asserts the old URL resolves, but + // never checks where it lands. + const unresolved: { repoPath: string; urls: string[]; candidate: string | null }[] = [] + for (const removedPath of needSuccessor) { + const before = preSyncPages.get(removedPath)! + const successor = findSuccessor(removedPath, currentRepoPaths, needSuccessor) + unresolved.push({ + repoPath: removedPath, + // Every URL here 404s, not just the page's own: the redirects it carried + // have no other home either. + urls: [before.url, ...before.redirects], + candidate: successor ? contentPathToUrl(successor.path) : null, + }) + } + + // 6. Write the merged frontmatter back. + let written = 0 + let addedEntries = 0 + for (const [repoPath, incoming] of additions) { + const absolutePath = path.join(repoRoot, repoPath) + if (!fs.existsSync(absolutePath)) { + throw new Error( + `Expected to add redirects to '${repoPath}', but it is not on disk. Refusing to ` + + `continue, because skipping the write would silently drop these redirects:\n ` + + `${incoming.join('\n ')}`, + ) + } + + const existing = readRedirects(matter.read(absolutePath).data as Record) + const selfUrl = contentPathToUrl(repoPath) + + const merged = mergeRedirects(existing, incoming).filter((url) => { + // A page must never redirect to itself. + if (url === selfUrl) return false + // `redirect-orphans` fails if a live page's URL is another page's + // redirect_from. Keep entries we already had so this stays additive, and + // let that test flag any pre-existing conflict. + if (currentUrls.has(url) && !existing.includes(url)) { + console.log(` SKIP (live page): ${url} would shadow an existing page`) + return false + } + return true + }) + + if (merged.length === existing.length && merged.every((v, i) => v === existing[i])) continue + + if (writeRedirects(absolutePath, merged)) { + addedEntries += merged.length - existing.length + written++ + } + } + + const lostUrlCount = unresolved.reduce((n, entry) => n + entry.urls.length, 0) + + console.log('\n--- Redirect preservation summary ---') + console.log(` Pages kept in place with redirects: ${preservedPages}`) + console.log(` Pages reshaped but same URL: ${reshaped}`) + console.log(` Files rewritten: ${written}`) + console.log(` Redirect entries added: ${addedEntries}`) + + if (unresolved.length > 0) { + console.log( + `\n Removed pages needing a redirect decision (${unresolved.length} pages, ${lostUrlCount} URLs):`, + ) + for (const entry of unresolved) { + console.log(` was ${entry.repoPath}`) + for (const url of entry.urls) console.log(` ${url}`) + if (entry.candidate) console.log(` possible replacement: ${entry.candidate}`) + } + console.log( + '\n Add the URLs above to the `redirect_from` of whichever page replaced\n' + + ' them, or confirm they were never published. Any "possible replacement"\n' + + ' is a same-name match only and has not been verified.', + ) + + // Surface this in the Actions run summary. Buried log output is how the + // earlier 404s went unnoticed until they reached production. + if (process.env.GITHUB_STEP_SUMMARY) { + const summary = [ + `### Copilot SDK docs sync: ${lostUrlCount} URLs need a redirect decision`, + '', + 'These pages were removed upstream. The URLs below will 404 once this sync', + 'merges unless they are added to whichever page replaced them. Each entry lists', + 'the removed page followed by every URL that depended on it, including redirects', + 'it had inherited from earlier renames.', + '', + ...unresolved.flatMap((entry) => [ + `- \`${entry.repoPath}\``, + ...entry.urls.map((url) => ` - \`${url}\``), + ...(entry.candidate + ? [` - Possible replacement (same name only, **unverified**): \`${entry.candidate}\``] + : []), + ]), + '', + 'To fix, add the URLs to the replacement page:', + '', + '```yaml', + 'redirect_from:', + ...unresolved.flatMap((entry) => entry.urls.map((url) => ` - ${url}`)), + '```', + '', + 'A same-name match is only a suggestion. Redirecting to the wrong page is not', + 'caught by any test, so confirm each target before adding it.', + '', + ].join('\n') + fs.appendFileSync(process.env.GITHUB_STEP_SUMMARY, summary, 'utf8') + } + + if (failOnUnresolved) process.exit(1) + } +} + +// Only run when executed directly, so the helpers above stay unit-testable. +if (process.argv[1] && path.resolve(process.argv[1]) === path.resolve(import.meta.filename)) { + try { + main() + } catch (error) { + // Every throw in this script marks a case where continuing would silently + // drop redirects, so failing the sync is the intended outcome. + console.error(`\nRedirect preservation failed.\n\n${(error as Error).message}\n`) + process.exit(1) + } +} diff --git a/src/workflows/sync-sdk-docs/strip-hidden-blocks.ts b/src/workflows/sync-sdk-docs/strip-hidden-blocks.ts index ce3424e1c7ce..261cf9b5383c 100644 --- a/src/workflows/sync-sdk-docs/strip-hidden-blocks.ts +++ b/src/workflows/sync-sdk-docs/strip-hidden-blocks.ts @@ -38,7 +38,7 @@ const HIDDEN_CLOSE = /^\s*/i // indented at most 3 spaces, and the run of backticks or tildes may exceed 3. const FENCE = /^ {0,3}(`{3,}|~{3,})(.*)$/ -interface OpenFence { +export interface OpenFence { char: string length: number } @@ -50,7 +50,7 @@ interface OpenFence { * long, and carry no info string. Tracking the length matters because a * four-backtick fence can legally contain a three-backtick line as content. */ -function nextFenceState(line: string, open: OpenFence | null): OpenFence | null { +export function nextFenceState(line: string, open: OpenFence | null): OpenFence | null { const match = FENCE.exec(line) if (!match) return open diff --git a/src/workflows/tests/sync-sdk-docs-preserve-redirects.ts b/src/workflows/tests/sync-sdk-docs-preserve-redirects.ts new file mode 100644 index 000000000000..c2fb5b0ad9f4 --- /dev/null +++ b/src/workflows/tests/sync-sdk-docs-preserve-redirects.ts @@ -0,0 +1,577 @@ +import fs from 'node:fs' +import os from 'node:os' +import path from 'node:path' +import { execFileSync } from 'node:child_process' + +import { describe, expect, test, beforeAll, afterAll } from 'vitest' + +import { + contentPathToUrl, + readRedirects, + mergeRedirects, + findSuccessor, + upsertRedirectBlock, +} from '@/workflows/sync-sdk-docs/preserve-redirects' + +const SCRIPT = path.join(process.cwd(), 'src/workflows/sync-sdk-docs/preserve-redirects.ts') +const SDK_DIR = 'content/copilot/how-tos/copilot-sdk' +const STEP_SUMMARY_FILE = 'step-summary.md' + +/** + * Every invocation of the script must go through this helper. The script + * appends its unresolved-removal warning to whatever `GITHUB_STEP_SUMMARY` + * points at, so a child that inherited the real one would write this suite's + * synthetic warnings into the actual Actions job summary and raise a false + * operational alert. Pinning it to a per-fixture file both prevents that and + * makes the summary assertable via `readStepSummary`. + */ +const runScript = (cwd: string, args: string[] = []) => + execFileSync('npx', ['tsx', SCRIPT, ...args], { + cwd, + encoding: 'utf8', + env: { ...process.env, GITHUB_STEP_SUMMARY: path.join(cwd, STEP_SUMMARY_FILE) }, + }) + +const readStepSummary = (cwd: string) => { + const file = path.join(cwd, STEP_SUMMARY_FILE) + return fs.existsSync(file) ? fs.readFileSync(file, 'utf8') : '' +} + +describe('contentPathToUrl', () => { + test('strips the content prefix and .md extension', () => { + expect(contentPathToUrl(`${SDK_DIR}/features/mcp.md`)).toBe( + '/copilot/how-tos/copilot-sdk/features/mcp', + ) + }) + + test('collapses index.md to its directory', () => { + expect(contentPathToUrl(`${SDK_DIR}/auth/index.md`)).toBe('/copilot/how-tos/copilot-sdk/auth') + }) + + test('does not strip "index" from a longer filename', () => { + expect(contentPathToUrl(`${SDK_DIR}/reindex.md`)).toBe('/copilot/how-tos/copilot-sdk/reindex') + }) +}) + +describe('readRedirects', () => { + test('returns an empty list when absent', () => { + expect(readRedirects({})).toEqual([]) + }) + + test('accepts a bare string as well as an array', () => { + expect(readRedirects({ redirect_from: '/old' })).toEqual(['/old']) + expect(readRedirects({ redirect_from: ['/a', '/b'] })).toEqual(['/a', '/b']) + }) + + test('ignores non-string entries', () => { + expect(readRedirects({ redirect_from: ['/a', 42, null] })).toEqual(['/a']) + }) +}) + +describe('mergeRedirects', () => { + test('dedupes while preserving first-seen order', () => { + expect(mergeRedirects(['/a', '/b'], ['/b', '/c'])).toEqual(['/a', '/b', '/c']) + }) + + test('strips trailing slashes, which redirect-orphans rejects', () => { + expect(mergeRedirects(['/a/'])).toEqual(['/a']) + }) + + test('drops empty entries', () => { + expect(mergeRedirects(['', ' ', '/a'])).toEqual(['/a']) + }) +}) + +describe('findSuccessor', () => { + test('matches a moved page by its unique filename', () => { + const successor = findSuccessor( + `${SDK_DIR}/old/mcp.md`, + [`${SDK_DIR}/features/mcp.md`, `${SDK_DIR}/features/skills.md`], + [`${SDK_DIR}/old/mcp.md`], + ) + expect(successor?.path).toBe(`${SDK_DIR}/features/mcp.md`) + }) + + test('refuses to guess when the filename is ambiguous', () => { + expect( + findSuccessor( + `${SDK_DIR}/old/mcp.md`, + [`${SDK_DIR}/features/mcp.md`, `${SDK_DIR}/setup/mcp.md`], + [`${SDK_DIR}/old/mcp.md`], + ), + ).toBeNull() + }) + + test('refuses a many-to-one match when two removed pages share a filename', () => { + const currentPaths = [`${SDK_DIR}/features/mcp.md`] + const removedPaths = [`${SDK_DIR}/old/mcp.md`, `${SDK_DIR}/legacy/mcp.md`] + + // Neither removal may claim the single survivor: at most one of them is its + // real predecessor, so assigning both would invent a wrong redirect. + for (const removed of removedPaths) { + expect(findSuccessor(removed, currentPaths, removedPaths)).toBeNull() + } + }) + + test('refuses a many-to-one match when two removed directories share a name', () => { + const currentPaths = [`${SDK_DIR}/hooks/index.md`] + const removedPaths = [`${SDK_DIR}/a/hooks/index.md`, `${SDK_DIR}/b/hooks/index.md`] + + for (const removed of removedPaths) { + expect(findSuccessor(removed, currentPaths, removedPaths)).toBeNull() + } + }) + + test('matches index.md on its directory name, not the filename', () => { + const successor = findSuccessor( + `${SDK_DIR}/use-hooks/index.md`, + [`${SDK_DIR}/hooks/index.md`, `${SDK_DIR}/use-hooks/index.md`], + [`${SDK_DIR}/use-hooks/index.md`], + ) + expect(successor?.path).toBe(`${SDK_DIR}/use-hooks/index.md`) + }) + + test('does not pair unrelated directories through their index.md', () => { + expect( + findSuccessor( + `${SDK_DIR}/auth/index.md`, + [`${SDK_DIR}/features/index.md`, `${SDK_DIR}/setup/index.md`], + [`${SDK_DIR}/auth/index.md`], + ), + ).toBeNull() + }) + + test('does not confuse an index.md with a same-named page', () => { + // `hooks/index.md` and `hooks.md` are different keys, so a removed + // directory index must not be matched to a page called hooks.md. + expect( + findSuccessor( + `${SDK_DIR}/hooks/index.md`, + [`${SDK_DIR}/features/hooks.md`], + [`${SDK_DIR}/hooks/index.md`], + ), + ).toBeNull() + }) + + test('returns null when nothing matches', () => { + expect( + findSuccessor(`${SDK_DIR}/gone.md`, [`${SDK_DIR}/features/mcp.md`], [`${SDK_DIR}/gone.md`]), + ).toBeNull() + }) +}) + +describe('upsertRedirectBlock', () => { + const frontmatter = [ + 'title: Example', + 'intro: >-', + ' A long intro that YAML would rewrap if the file were re-serialized', + ' instead of edited as text.', + 'versions:', + " fpt: '*'", + 'contentType: how-tos', + ].join('\n') + + test('inserts the block directly before contentType', () => { + expect(upsertRedirectBlock(frontmatter, ['/old']).split('\n')).toEqual([ + 'title: Example', + 'intro: >-', + ' A long intro that YAML would rewrap if the file were re-serialized', + ' instead of edited as text.', + 'versions:', + " fpt: '*'", + 'redirect_from:', + ' - /old', + 'contentType: how-tos', + ]) + }) + + test('leaves every other line byte-identical', () => { + const result = upsertRedirectBlock(frontmatter, ['/old']) + for (const line of frontmatter.split('\n')) { + expect(result).toContain(line) + } + }) + + test('replaces an existing block rather than duplicating it', () => { + const withBlock = upsertRedirectBlock(frontmatter, ['/one']) + const replaced = upsertRedirectBlock(withBlock, ['/one', '/two']) + expect(replaced.match(/redirect_from:/g)).toHaveLength(1) + expect(replaced).toContain(' - /one') + expect(replaced).toContain(' - /two') + }) + + test('replaces the inline form too', () => { + const inline = 'title: Example\nredirect_from: /solo\ncontentType: how-tos' + const result = upsertRedirectBlock(inline, ['/solo', '/extra']) + expect(result.match(/redirect_from/g)).toHaveLength(1) + expect(result).toContain(' - /extra') + }) + + test('appends when there is no contentType key', () => { + const result = upsertRedirectBlock('title: Example', ['/old']) + expect(result).toBe('title: Example\nredirect_from:\n - /old') + }) + + test('removes the block when given no redirects', () => { + const withBlock = upsertRedirectBlock(frontmatter, ['/one']) + expect(upsertRedirectBlock(withBlock, [])).toBe(frontmatter) + }) + + test('consumes blank lines inside a hand-edited block', () => { + const messy = [ + 'title: Example', + 'redirect_from:', + ' - /one', + '', + ' - /two', + 'contentType: how-tos', + ].join('\n') + expect(upsertRedirectBlock(messy, ['/one', '/two'])).toBe( + ['title: Example', 'redirect_from:', ' - /one', ' - /two', 'contentType: how-tos'].join( + '\n', + ), + ) + }) + + test('keeps a blank line that separates the block from the next key', () => { + const spaced = [ + 'title: Example', + 'redirect_from:', + ' - /one', + '', + 'contentType: how-tos', + ].join('\n') + expect(upsertRedirectBlock(spaced, ['/one'])).toBe( + ['title: Example', '', 'redirect_from:', ' - /one', 'contentType: how-tos'].join('\n'), + ) + }) + + test('ignores keys that merely contain redirect_from', () => { + const decoy = 'title: Example\nold_redirect_from: /decoy\ncontentType: how-tos' + expect(upsertRedirectBlock(decoy, ['/real'])).toContain('old_redirect_from: /decoy') + }) +}) + +/** + * End-to-end runs against a throwaway git repo. The script reconciles the + * working tree against a git ref, so a real commit is the only honest fixture. + */ +describe('preserve-redirects end to end', () => { + let repo: string + + const git = (...args: string[]) => execFileSync('git', args, { cwd: repo, encoding: 'utf8' }) + + const write = (relativePath: string, contents: string) => { + const full = path.join(repo, relativePath) + fs.mkdirSync(path.dirname(full), { recursive: true }) + fs.writeFileSync(full, contents, 'utf8') + } + + const read = (relativePath: string) => fs.readFileSync(path.join(repo, relativePath), 'utf8') + + const page = (title: string, redirects?: string[]) => + [ + '---', + `title: ${title}`, + 'versions:', + " fpt: '*'", + ...(redirects ? ['redirect_from:', ...redirects.map((r) => ` - ${r}`)] : []), + 'contentType: how-tos', + '---', + '', + 'Body text.', + '', + ].join('\n') + + const run = (extraArgs: string[] = []) => + runScript(repo, ['--sdk-docs-dir', path.join(repo, SDK_DIR), ...extraArgs]) + + const stepSummary = () => readStepSummary(repo) + + beforeAll(() => { + repo = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-redirects-')) + git('init', '--initial-branch=main') + git('config', 'user.email', 'test@example.com') + git('config', 'user.name', 'Test') + + // Pre-sync state: a page carrying a hand-added redirect, a page that will be + // moved, a directory index that will be renamed, and a page left untouched. + write(`${SDK_DIR}/features/mcp.md`, page('MCP', ['/copilot/how-tos/copilot-sdk/old-mcp'])) + write(`${SDK_DIR}/features/moving.md`, page('Moving', ['/copilot/how-tos/copilot-sdk/ancient'])) + write(`${SDK_DIR}/use-hooks/index.md`, page('Hooks')) + write(`${SDK_DIR}/features/stable.md`, page('Stable')) + git('add', '-A') + git('commit', '-m', 'pre-sync state') + }) + + afterAll(() => { + if (repo) fs.rmSync(repo, { recursive: true, force: true }) + }) + + test('restores redirects the sync would have dropped, and reports moves', () => { + // Simulate the sync: wipe the tree and rebuild it without any redirect_from, + // moving one page and renaming one directory along the way. + fs.rmSync(path.join(repo, SDK_DIR), { recursive: true, force: true }) + write(`${SDK_DIR}/features/mcp.md`, page('MCP')) + write(`${SDK_DIR}/setup/moving.md`, page('Moving')) + write(`${SDK_DIR}/use-hooks/index.md`, page('Hooks')) + write(`${SDK_DIR}/features/stable.md`, page('Stable')) + + const output = run() + + // 1. A redirect on a page that kept its path is put back. + expect(read(`${SDK_DIR}/features/mcp.md`)).toContain('/copilot/how-tos/copilot-sdk/old-mcp') + + // 2. A moved page is reported for a human, never auto-redirected: a matching + // filename is not proof that one page replaced another. Both the page's + // own URL and the older redirect it had inherited must be listed, or a + // human fixing the obvious one would still strand the chain. + expect(output).toContain('/copilot/how-tos/copilot-sdk/features/moving') + expect(output).toContain('/copilot/how-tos/copilot-sdk/ancient') + expect(output).toContain('possible replacement: /copilot/how-tos/copilot-sdk/setup/moving') + expect(read(`${SDK_DIR}/setup/moving.md`)).not.toContain('redirect_from') + + // 3. A page that never had redirects is left alone. + expect(read(`${SDK_DIR}/features/stable.md`)).not.toContain('redirect_from') + }) + + test('is idempotent — a second run changes nothing', () => { + const before = [ + read(`${SDK_DIR}/features/mcp.md`), + read(`${SDK_DIR}/setup/moving.md`), + read(`${SDK_DIR}/features/stable.md`), + ] + + run() + + expect([ + read(`${SDK_DIR}/features/mcp.md`), + read(`${SDK_DIR}/setup/moving.md`), + read(`${SDK_DIR}/features/stable.md`), + ]).toEqual(before) + }) + + test('never points a page at its own URL', () => { + for (const relativePath of [`${SDK_DIR}/features/mcp.md`, `${SDK_DIR}/setup/moving.md`]) { + const selfUrl = contentPathToUrl(relativePath) + expect(read(relativePath)).not.toContain(`- ${selfUrl}\n`) + } + }) + + test('does not reflow unrelated frontmatter', () => { + // A long `intro` is the field most likely to be rewrapped by a YAML + // round-trip, which would swamp the real change in every sync diff. + const longIntro = + 'This intro is deliberately far longer than the eighty column default that ' + + 'js-yaml wraps folded scalars at, so any re-serialization would be obvious.' + + const isolated = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-redirects-reflow-')) + const igit = (...args: string[]) => + execFileSync('git', args, { cwd: isolated, encoding: 'utf8' }) + const target = path.join(isolated, SDK_DIR, 'features/stable.md') + + const build = (redirect: boolean) => + [ + '---', + 'title: Stable', + `intro: ${longIntro}`, + ...(redirect ? ['redirect_from:', ' - /copilot/how-tos/copilot-sdk/legacy-stable'] : []), + 'contentType: how-tos', + '---', + '', + 'Body.', + '', + ].join('\n') + + try { + igit('init', '--initial-branch=main') + igit('config', 'user.email', 'test@example.com') + igit('config', 'user.name', 'Test') + fs.mkdirSync(path.dirname(target), { recursive: true }) + + fs.writeFileSync(target, build(true), 'utf8') + igit('add', '-A') + igit('commit', '-m', 'pre-sync state') + const expected = fs.readFileSync(target, 'utf8') + + // A sync rebuilds the frontmatter without the redirect. + fs.writeFileSync(target, build(false), 'utf8') + + runScript(isolated, ['--sdk-docs-dir', path.join(isolated, SDK_DIR)]) + + expect(fs.readFileSync(target, 'utf8')).toBe(expected) + expect(fs.readFileSync(target, 'utf8')).toContain(`intro: ${longIntro}`) + } finally { + fs.rmSync(isolated, { recursive: true, force: true }) + } + }) + + test('reports an ambiguous removal instead of guessing', () => { + git('add', '-A') + git('commit', '-m', 'sync result') + + // `gone.md` disappears with no plausible successor. + write(`${SDK_DIR}/features/gone.md`, page('Gone')) + git('add', '-A') + git('commit', '-m', 'add page that will vanish') + fs.rmSync(path.join(repo, `${SDK_DIR}/features/gone.md`)) + + const output = run() + expect(output).toContain('needing a redirect decision') + expect(output).toContain('/copilot/how-tos/copilot-sdk/features/gone') + }) + + test('exits non-zero on an unresolved removal when asked to', () => { + expect(() => run(['--fail-on-unresolved'])).toThrow() + }) + + test('writes the unresolved warning to the step summary it was given', () => { + // Self-contained: clear the file, trigger its own unresolved run, then read + // it back, rather than depending on a previous test having written it. + const file = path.join(repo, 'step-summary.md') + fs.rmSync(file, { force: true }) + + write(`${SDK_DIR}/features/vanishing.md`, page('Vanishing', ['/copilot/older-vanishing'])) + git('add', '-A') + git('commit', '-m', 'add page that will vanish') + fs.rmSync(path.join(repo, `${SDK_DIR}/features/vanishing.md`)) + + run() + + // Guards the env redirect in `run`: without it these synthetic warnings + // would be appended to the real Actions job summary during CI. + const summary = stepSummary() + expect(summary).toContain('need a redirect decision') + expect(summary).toContain('/copilot/how-tos/copilot-sdk/features/vanishing') + // The inherited redirect is at risk too, so it must be reported, not just + // the removed page's own URL. + expect(summary).toContain('/copilot/older-vanishing') + }) + + test('fails loudly when the baseline ref cannot be read', () => { + // Previously a failed `git ls-tree` was indistinguishable from a first sync, + // so the run reported "nothing to preserve" and exited 0 — dropping every + // redirect in the tree without a single warning. + let message = '' + try { + run(['--git-ref', 'refs/heads/no-such-ref']) + throw new Error('expected the script to fail') + } catch (error) { + message = `${(error as Error).message}${(error as { stderr?: string }).stderr ?? ''}` + } + expect(message).toContain('Could not read the baseline tree') + expect(message).not.toContain('nothing to preserve') + }) + + test('carries inherited redirects when a page keeps its URL but changes file', () => { + // `guide.md` becoming `guide/index.md` keeps the URL live, so nothing 404s + // and no successor guess is needed — but the redirects the old file had + // inherited would be stranded unless they are moved by URL identity. + const isolated = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-redirects-reshape-')) + const igit = (...args: string[]) => + execFileSync('git', args, { cwd: isolated, encoding: 'utf8' }) + const flat = path.join(isolated, SDK_DIR, 'features/guide.md') + const nested = path.join(isolated, SDK_DIR, 'features/guide/index.md') + + try { + igit('init', '--initial-branch=main') + igit('config', 'user.email', 'test@example.com') + igit('config', 'user.name', 'Test') + fs.mkdirSync(path.dirname(flat), { recursive: true }) + fs.writeFileSync(flat, page('Guide', ['/copilot/ancient-guide']), 'utf8') + igit('add', '-A') + igit('commit', '-m', 'pre-sync state') + + fs.rmSync(flat) + fs.mkdirSync(path.dirname(nested), { recursive: true }) + fs.writeFileSync(nested, page('Guide'), 'utf8') + + runScript(isolated, ['--sdk-docs-dir', path.join(isolated, SDK_DIR), '--fail-on-unresolved']) + + const result = fs.readFileSync(nested, 'utf8') + expect(result).toContain('/copilot/ancient-guide') + // The URL the file already serves must not be added to its own page. + expect(result).not.toContain('- /copilot/how-tos/copilot-sdk/features/guide\n') + } finally { + fs.rmSync(isolated, { recursive: true, force: true }) + } + }) + + test('exits cleanly when the ref is valid but the SDK directory is absent', () => { + // The first ever sync. This is the one empty baseline that is legitimate, + // and it must stay distinguishable from a baseline that could not be read. + const isolated = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-redirects-first-')) + const igit = (...args: string[]) => + execFileSync('git', args, { cwd: isolated, encoding: 'utf8' }) + + try { + igit('init', '--initial-branch=main') + igit('config', 'user.email', 'test@example.com') + igit('config', 'user.name', 'Test') + fs.writeFileSync(path.join(isolated, 'README.md'), '# Repo\n', 'utf8') + igit('add', '-A') + igit('commit', '-m', 'repo without SDK docs') + + // The sync has just created the tree for the first time. + const target = path.join(isolated, SDK_DIR, 'features/new.md') + fs.mkdirSync(path.dirname(target), { recursive: true }) + fs.writeFileSync(target, page('New'), 'utf8') + + const output = runScript(isolated, [ + '--sdk-docs-dir', + path.join(isolated, SDK_DIR), + '--fail-on-unresolved', + ]) + expect(output).toContain('nothing to preserve') + } finally { + fs.rmSync(isolated, { recursive: true, force: true }) + } + }) + + test('fails loudly when the page receiving redirects has no frontmatter', () => { + const isolated = fs.mkdtempSync(path.join(os.tmpdir(), 'sdk-redirects-nofm-')) + const igit = (...args: string[]) => + execFileSync('git', args, { cwd: isolated, encoding: 'utf8' }) + const target = path.join(isolated, SDK_DIR, 'features/stable.md') + + try { + igit('init', '--initial-branch=main') + igit('config', 'user.email', 'test@example.com') + igit('config', 'user.name', 'Test') + fs.mkdirSync(path.dirname(target), { recursive: true }) + + fs.writeFileSync( + target, + [ + '---', + 'title: Stable', + 'redirect_from:', + ' - /copilot/legacy', + '---', + '', + 'Body.', + '', + ].join('\n'), + 'utf8', + ) + igit('add', '-A') + igit('commit', '-m', 'pre-sync state') + + // The sync rewrites the page without any frontmatter at all, so there is + // nowhere to put the redirect back. + fs.writeFileSync(target, 'Body only, no frontmatter.\n', 'utf8') + + let message = '' + try { + runScript(isolated, ['--sdk-docs-dir', path.join(isolated, SDK_DIR)]) + throw new Error('expected the script to fail') + } catch (error) { + message = `${(error as Error).message}${(error as { stderr?: string }).stderr ?? ''}` + } + expect(message).toContain('no frontmatter block') + expect(message).toContain('/copilot/legacy') + } finally { + fs.rmSync(isolated, { recursive: true, force: true }) + } + }) +})