diff --git a/.github/actions/node-npm-setup/action.yml b/.github/actions/node-npm-setup/action.yml
index e25ce75a1f94..da7c0e2c875e 100644
--- a/.github/actions/node-npm-setup/action.yml
+++ b/.github/actions/node-npm-setup/action.yml
@@ -20,6 +20,8 @@ runs:
uses: actions/setup-node@949feb2413d6458794dcd2491c4babbbce0c15c1 # v7.1.0
with:
node-version-file: 'package.json'
+ # Resolve from the Node manifest so CI uses the newest allowed version, not an older cached match.
+ check-latest: true
package-manager-cache: false
cache: ${{ steps.cache-node_modules.outputs.cache-hit != 'true' && 'npm' || '' }}
diff --git a/.github/workflows/changelog-agent.yml b/.github/workflows/changelog-agent.yml
index 1d02b16ef51d..e32621cb71a8 100644
--- a/.github/workflows/changelog-agent.yml
+++ b/.github/workflows/changelog-agent.yml
@@ -393,11 +393,22 @@ jobs:
fs.writeFileSync('prompt.txt', userPrompt);
core.setOutput('date_str', dateStr);
+ # Use a subdirectory: checkout would clear the workspace and the prompt files above.
+ - name: Check out package.json for the Node version
+ if: steps.check_parent.outputs.has_parent == 'true' && steps.check_existing.outputs.exists == 'false'
+ uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0
+ with:
+ path: node-version-source
+ sparse-checkout: /package.json
+ sparse-checkout-cone-mode: false
+ persist-credentials: false
+
- name: Set up Node.js for Copilot CLI
if: steps.check_parent.outputs.has_parent == 'true' && steps.check_existing.outputs.exists == 'false'
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
- node-version: 24
+ node-version-file: node-version-source/package.json
+ check-latest: true
- name: Install GitHub Copilot CLI
if: steps.check_parent.outputs.has_parent == 'true' && steps.check_existing.outputs.exists == 'false'
diff --git a/.github/workflows/package-lock-lint.yml b/.github/workflows/package-lock-lint.yml
index 43333e453355..516abe89df30 100644
--- a/.github/workflows/package-lock-lint.yml
+++ b/.github/workflows/package-lock-lint.yml
@@ -43,6 +43,7 @@ jobs:
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: 'package.json'
+ check-latest: true
cache: npm
- name: Run check
diff --git a/.github/workflows/ready-for-doc-review.yml b/.github/workflows/ready-for-doc-review.yml
index 552ecc7fade4..ab5613f1f064 100644
--- a/.github/workflows/ready-for-doc-review.yml
+++ b/.github/workflows/ready-for-doc-review.yml
@@ -29,6 +29,7 @@ jobs:
uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0
with:
node-version-file: 'package.json'
+ check-latest: true
cache: npm
- name: Install dependencies
diff --git a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-monitoring-your-instance.md b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-monitoring-your-instance.md
index 6b14648c7aa1..dd000f8c2988 100644
--- a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-monitoring-your-instance.md
+++ b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-monitoring-your-instance.md
@@ -10,12 +10,21 @@ category:
## Metrics
-OpenTelemetry metrics were introduced in {% data variables.product.prodname_ghe_server %} 3.18 and are the future foundation for monitoring your {% data variables.product.prodname_ghe_server %} instance.
-Collectd metrics will continue to be gathered by default, but we expect collectd metrics to be discontinued in a future release.
+OpenTelemetry metrics were introduced in {% data variables.product.prodname_ghe_server %} 3.18 and are the foundation for monitoring {% data variables.product.prodname_ghe_server %} instances.
+
+{% ifversion ghes < 3.23 %}
+
+New installations of version 3.19 and later use OpenTelemetry metrics by default. On version 3.18, and on instances upgraded from earlier releases, collectd metrics are still gathered by default. Collectd is retired in version 3.23, and upgrading to 3.23 disables it. For migration guidance for instances that still use collectd, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance).
+
+{% else %}
+
+Collectd is retired in version 3.23 and can no longer be run. Upgrading to 3.23 disables collectd, and OpenTelemetry metrics are the only metrics stack.
+
+{% endif %}
For more information about OpenTelemetry metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/about-opentelemetry-metrics).
-For more information about Collectd metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/collectd-metrics/about-collectd-metrics).
+For more information about collectd metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/collectd-metrics/about-collectd-metrics).
## Dashboards
diff --git a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-the-monitor-dashboards.md b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-the-monitor-dashboards.md
index a1e9c9ff5ce7..7832b7d2e803 100644
--- a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-the-monitor-dashboards.md
+++ b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-the-monitor-dashboards.md
@@ -52,12 +52,14 @@ Starting with {% data variables.product.prodname_ghe_server %} 3.18, you can cre
> [!IMPORTANT] Pre-built dashboards can be opened and edited for inspection, but changes cannot be saved. Create a copy of a dashboard to apply and retain any customizations.
-You can use OpenTelemetry and collectd metrics concurrently for external monitoring. During the {% data variables.release-phases.public_preview %}, collectd remains enabled and cannot be disabled.
-
For more information about OpenTelemetry metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/about-opentelemetry-metrics).
+{% ifversion ghes < 3.23 %}
+
For more information about collectd metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/collectd-metrics/about-collectd-metrics).
+{% endif %}
+
## About the metrics on the monitor dashboards
The monitor dashboards provide a comprehensive view of various metrics that help in understanding the performance and health of your {% data variables.product.prodname_ghe_server %} instance. These metrics are categorized into different sections to make it easier to analyze specific aspects of the system.
diff --git a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/configuring-opentelemetry-for-your-instance.md b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/configuring-opentelemetry-for-your-instance.md
index 7d76dd58077d..d9e8b7266919 100644
--- a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/configuring-opentelemetry-for-your-instance.md
+++ b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/configuring-opentelemetry-for-your-instance.md
@@ -25,13 +25,17 @@ category:
## Enabling OpenTelemetry metrics
+{% ifversion ghes < 3.23 %}
+
{% ifversion ghes > 3.18 %}
-OpenTelemetry metrics are enabled by default for **new installations** of {% data variables.product.prodname_ghe_server %} 3.19 and later. Upgrades to {% data variables.product.prodname_ghe_server %} 3.19 will still have `collectd` metrics enabled by default, but you can choose to switch to OpenTelemetry metrics.
+OpenTelemetry metrics are enabled by default for **new installations** of {% data variables.product.prodname_ghe_server %} 3.19 and later. Instances upgraded from earlier releases still have collectd metrics enabled by default. For the complete migration procedure for instances that still use collectd, including how to disable collectd, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance).
{% else %}
-OpenTelemetry metrics are disabled by default. You can enable them through the {% data variables.enterprise.management_console %} or command line.
+OpenTelemetry metrics are disabled by default. They can be enabled through the {% data variables.enterprise.management_console %} or command line.
+
+{% endif %}
### Using the {% data variables.enterprise.management_console %}
@@ -49,10 +53,14 @@ OpenTelemetry metrics are disabled by default. You can enable them through the {
```shell
ghe-config observability.metrics.next-enabled true
- ```
+ ```
{% data reusables.enterprise.apply-configuration %}
+{% else %}
+
+OpenTelemetry metrics are always enabled and are the only supported metrics stack. Collectd is retired in {% data variables.product.prodname_ghe_server %} 3.23 and later, and upgrading from an earlier version disables it. For information about post-upgrade checks and historical collectd data, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance).
+
{% endif %}
## Performance considerations
@@ -166,5 +174,5 @@ sudo journalctl -u victoriametrics -f
## Next steps
* To enable advanced monitoring dashboards, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/advanced-dashboards)
-* Exploring OpenTelemetry metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/using-grafana-to-analyze-opentelemetry-metrics).
+* To explore OpenTelemetry metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/using-grafana-to-analyze-opentelemetry-metrics)
* To set up external monitoring, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/setting-up-external-monitoring-with-opentelemetry).
diff --git a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/index.md b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/index.md
index 968337abe0bc..bcc4390f7322 100644
--- a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/index.md
+++ b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/index.md
@@ -5,6 +5,7 @@ versions:
feature: ghes-opentelemetry
children:
- /about-opentelemetry-metrics
+ - /migration-guidance
- /configuring-opentelemetry-for-your-instance
- /advanced-dashboards
- /using-grafana-to-analyze-opentelemetry-metrics
diff --git a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance.md b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance.md
new file mode 100644
index 000000000000..6f24022bfd3f
--- /dev/null
+++ b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance.md
@@ -0,0 +1,80 @@
+---
+title: Migrating from collectd to OpenTelemetry
+intro: This guide describes the migration of a {% data variables.product.prodname_ghe_server %} instance from collectd to the OpenTelemetry metrics stack.
+versions:
+ feature: ghes-opentelemetry
+shortTitle: Migration guidance
+contentType: how-tos
+category:
+ - Monitor and audit your enterprise
+---
+
+{% data variables.product.prodname_ghe_server %} is transitioning from the collectd metrics stack to OpenTelemetry. Collectd is retired in version 3.23, and upgrading to 3.23 disables it.
+
+## Migration timeline
+
+| Version | Default for new installations | Default for instances upgraded from earlier releases | Notes |
+| :- | :- | :- | :- |
+| 3.18 | Collectd | Collectd | OpenTelemetry is opt-in. |
+| 3.19 to 3.22 | OpenTelemetry | Collectd | Migration to OpenTelemetry is possible before upgrading to 3.23. |
+| 3.23 and later | OpenTelemetry | OpenTelemetry | Collectd is retired and cannot be used. The upgrade disables it. |
+
+## Preparing to migrate
+
+Before you migrate, complete the following preparations:
+
+* Test the migration on a staging instance before applying it in production.
+* Identify the monitoring integrations that depend on collectd metrics, such as external dashboards and alerts.
+* If you plan to run both metrics stacks during a transition period, review the caveats in [Running both metrics stacks](#running-both-metrics-stacks).
+
+## Migrating to OpenTelemetry
+
+You can migrate to OpenTelemetry on instances running version 3.19 to 3.22. On version 3.18, OpenTelemetry is opt-in, so you can also enable it there. We recommend migrating before you upgrade to 3.23, so that you can verify your monitoring setup while you can still use collectd.
+
+If you don't migrate, upgrading to version 3.23 or later does it for you: collectd is disabled and cannot be re-enabled, and OpenTelemetry metrics are enabled and become the only metrics stack.
+
+Migration requires enabling OpenTelemetry metrics and disabling collectd. Both settings are available through the {% data variables.enterprise.management_console %} or the command line.
+
+### Using the {% data variables.enterprise.management_console %}
+
+{% data reusables.enterprise_site_admin_settings.access-settings %}
+{% data reusables.enterprise_site_admin_settings.management-console %}
+
+1. In the monitoring settings, select **Enable OpenTelemetry**.
+1. Under **Metrics - Collectd**, select **Disable collectd metrics**.
+
+{% data reusables.enterprise_management_console.save-settings %}
+
+### Using the command line
+
+{% data reusables.enterprise_installation.ssh-into-instance %}
+1. To enable OpenTelemetry metrics and disable collectd, run the following commands.
+
+ ```shell
+ ghe-config observability.metrics.next-enabled true
+ ghe-config observability.metrics.legacy-disabled true
+ ```
+
+{% data reusables.enterprise.apply-configuration %}
+
+### Running both metrics stacks
+
+On version 3.18 to 3.22, running the collectd and OpenTelemetry stacks at the same time is possible but **not recommended**. This configuration has the following caveats:
+
+* Some metric names may differ to remain compatible with the collectd format.
+* Some OpenTelemetry metrics do not include labels.
+* Increased resource utilization to deliver metrics to both solutions.
+
+Collectd is retired in version 3.23, so running both stacks is not possible after upgrading.
+
+## Post-upgrade checks
+
+After the configuration run completes, whether you migrated manually or upgraded to version 3.23 or later:
+
+* Verify that the metrics are visible and collected in the **Monitoring** tab of the {% data variables.enterprise.management_console %}.
+* Update monitoring integrations that depend on collectd metrics. For information about connecting external tools, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/setting-up-external-monitoring-with-opentelemetry).
+* For information about analyzing metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/using-grafana-to-analyze-opentelemetry-metrics).
+
+### Viewing historical collectd data
+
+Since patches 3.22.3, 3.21.8, 3.20.10, and 3.19.14, the **Monitoring** tab includes collectd dashboards for viewing historical data. These dashboards display collectd metrics recorded up to the time the collectd stack was disabled.
diff --git a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/recommended-alert-thresholds.md b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/recommended-alert-thresholds.md
index c234855b7330..e545874d262a 100644
--- a/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/recommended-alert-thresholds.md
+++ b/content/admin/monitoring-and-managing-your-instance/monitoring-your-instance/recommended-alert-thresholds.md
@@ -24,8 +24,12 @@ You can configure external monitoring systems to alert you to storage, CPU, and
For more information about OpenTelemetry metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/about-opentelemetry-metrics).
+{% ifversion ghes < 3.23 %}
+
For more information about collectd metrics, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/collectd-metrics/about-collectd-metrics).
+{% endif %}
+
For more information about the monitor dashboards, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/about-the-monitor-dashboards).
## Monitoring storage
diff --git a/content/code-security/concepts/supply-chain-security/open-source-license-compliance.md b/content/code-security/concepts/supply-chain-security/open-source-license-compliance.md
index 45f27ac2b8b4..6d724533c0bd 100644
--- a/content/code-security/concepts/supply-chain-security/open-source-license-compliance.md
+++ b/content/code-security/concepts/supply-chain-security/open-source-license-compliance.md
@@ -36,6 +36,14 @@ If there is a ruleset in **Active** mode which uses the "Requires license compli
Additionally, a [branch protection rule](/repositories/configuring-branches-and-merges-in-your-repository/managing-protected-branches/managing-a-branch-protection-rule) that requires comment resolution before merging will track annotations from license checks, so even alerts generated by **Evaluate** rulesets will be subject to the protection.
+### Dependency snapshots
+
+Dependency snapshots describe a repository's dependencies at a specific commit. License compliance compares snapshots for the base and head commits of a pull request to identify added dependencies and evaluate their licenses against your policy.
+
+For each detector that submitted a snapshot for the base commit, the check waits for a snapshot from that detector for the exact head commit. The check resumes when all required snapshots arrive. For example, if a workflow or external system submits snapshots only for commits to the default branch, the head commit does not receive that detector's snapshot and the check remains waiting.
+
+Automatic dependency submission runs on both the base and head commits. When it submits the snapshots your repository needs, you do not need to add another detector. For more information about automatic dependency submission, see [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/submit-dependencies-automatically).
+
## Where results appear
If a dependency's license isn't in your policy, findings will appear in pull request annotations. The annotations do not automatically generate an exception request, because the developer could decide to modify their code to avoid the noncompliant dependency. If they do want to use the dependency, {% data variables.product.github %} will prompt the developer for more information, then send the closure request to the Enterprise Open Source License Managers, who have permission to modify the policy.
diff --git a/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-license-policies.md b/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-license-policies.md
index 586929943e71..dc0e5862139e 100644
--- a/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-license-policies.md
+++ b/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-license-policies.md
@@ -106,6 +106,14 @@ For more information about rulesets, see [AUTOTITLE](/repositories/configuring-b
If the ruleset is in **Active** mode, the pull request remains blocked until all license policy violations are resolved.
+## Troubleshooting checks waiting for dependency snapshots
+
+If a license compliance check reports `Waiting for dependency snapshots on head`, the pull request's head commit is missing one or more snapshots that were submitted for the base commit. This can happen when a workflow submits snapshots for the base branch but does not submit them for the pull request's head commit.
+
+The message can list detectors that submitted a snapshot for the base commit but not yet for the head commit. Review those detectors and the workflows or external systems that submit their snapshots. If you submit snapshots through your own workflow or the {% data variables.dependency-submission-api.name %}, configure it to also submit snapshots for the pull request's head commit. If automatic dependency submission is enabled and already submits the snapshots your repository needs, you do not need to add another detector. Adding another detector can increase the number of snapshots the check expects on the head commit.
+
+For more information about snapshot submission, see [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/secure-your-dependencies/use-dependency-submission-api).
+
## Managing license compliance requests and policy
### Review and handle exception requests
diff --git a/data/release-notes/enterprise-server/3-18/15.yml b/data/release-notes/enterprise-server/3-18/15.yml
index 9b7e274dc51c..ca805d3ffaec 100644
--- a/data/release-notes/enterprise-server/3-18/15.yml
+++ b/data/release-notes/enterprise-server/3-18/15.yml
@@ -84,8 +84,6 @@ sections:
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.
- |
diff --git a/data/release-notes/enterprise-server/3-18/16.yml b/data/release-notes/enterprise-server/3-18/16.yml
index f7827ae5ab16..aae46f8d36ec 100644
--- a/data/release-notes/enterprise-server/3-18/16.yml
+++ b/data/release-notes/enterprise-server/3-18/16.yml
@@ -20,6 +20,8 @@ sections:
- |
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:
+ - |
+ After a restart, an instance can remain unavailable with "Starting..." displayed in the web interface. To recover, an administrator can connect using administrative SSH and rerun `ghe-config-apply`. This provides temporary recovery but does not prevent the issue from recurring. This issue is resolved in {% data variables.product.prodname_ghe_server %} 3.21.0 and later. [Updated: 2026-10-30]
- |
During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading.
- |
diff --git a/data/release-notes/enterprise-server/3-19/12.yml b/data/release-notes/enterprise-server/3-19/12.yml
index 720035ec4ace..2f8ac4c746b2 100644
--- a/data/release-notes/enterprise-server/3-19/12.yml
+++ b/data/release-notes/enterprise-server/3-19/12.yml
@@ -90,8 +90,6 @@ sections:
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.
- |
diff --git a/data/release-notes/enterprise-server/3-19/13.yml b/data/release-notes/enterprise-server/3-19/13.yml
index 740600e85193..dde2a1b04851 100644
--- a/data/release-notes/enterprise-server/3-19/13.yml
+++ b/data/release-notes/enterprise-server/3-19/13.yml
@@ -28,6 +28,8 @@ sections:
- |
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:
+ - |
+ After a restart, an instance can remain unavailable with "Starting..." displayed in the web interface. To recover, an administrator can connect using administrative SSH and rerun `ghe-config-apply`. This provides temporary recovery but does not prevent the issue from recurring. This issue is resolved in {% data variables.product.prodname_ghe_server %} 3.21.0 and later. [Updated: 2026-10-30]
- |
During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading.
- |
diff --git a/data/release-notes/enterprise-server/3-20/8.yml b/data/release-notes/enterprise-server/3-20/8.yml
index 2c50abb16478..72eadf2898eb 100644
--- a/data/release-notes/enterprise-server/3-20/8.yml
+++ b/data/release-notes/enterprise-server/3-20/8.yml
@@ -93,8 +93,6 @@ sections:
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 scheduled job that runs on the first Saturday of each month. 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.
- |
diff --git a/data/release-notes/enterprise-server/3-20/9.yml b/data/release-notes/enterprise-server/3-20/9.yml
index 093a6d487c76..1e234cd9734a 100644
--- a/data/release-notes/enterprise-server/3-20/9.yml
+++ b/data/release-notes/enterprise-server/3-20/9.yml
@@ -35,6 +35,8 @@ sections:
- |
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:
+ - |
+ After a restart, an instance can remain unavailable with "Starting..." displayed in the web interface. To recover, an administrator can connect using administrative SSH and rerun `ghe-config-apply`. This provides temporary recovery but does not prevent the issue from recurring. This issue is resolved in {% data variables.product.prodname_ghe_server %} 3.21.0 and later. [Updated: 2026-10-30]
- |
During an upgrade of GitHub Enterprise Server, custom firewall rules are removed. If you use custom firewall rules, you must reapply them after upgrading.
- |
diff --git a/data/release-notes/enterprise-server/3-21/6.yml b/data/release-notes/enterprise-server/3-21/6.yml
index 7e988bc19621..bf24a813ede3 100644
--- a/data/release-notes/enterprise-server/3-21/6.yml
+++ b/data/release-notes/enterprise-server/3-21/6.yml
@@ -101,8 +101,6 @@ sections:
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 scheduled job that runs on the first Saturday of each month. 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.
- |
diff --git a/data/release-notes/enterprise-server/3-22/1.yml b/data/release-notes/enterprise-server/3-22/1.yml
index 1ddee99b1ad5..9c9a5e5e3b69 100644
--- a/data/release-notes/enterprise-server/3-22/1.yml
+++ b/data/release-notes/enterprise-server/3-22/1.yml
@@ -95,8 +95,6 @@ sections:
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 scheduled job that runs on the first Saturday of each month. 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.
- |
diff --git a/data/reusables/enterprise/collectd-metrics-future.md b/data/reusables/enterprise/collectd-metrics-future.md
index d7e0a8b1c0ff..0b7dfa895ff1 100644
--- a/data/reusables/enterprise/collectd-metrics-future.md
+++ b/data/reusables/enterprise/collectd-metrics-future.md
@@ -2,7 +2,7 @@
Collectd metrics are gathered by default and are fully supported in this release. OpenTelemetry metrics are the future foundation for monitoring, available from {% data variables.product.prodname_ghe_server %} 3.18 onward.
-> [!NOTE]
-> Collectd metrics are expected to be {% data variables.release-phases.closing_down %} and then {% data variables.release-phases.retired %} in a future release of {% data variables.product.prodname_ghe_server %}. We recommend that your long-term monitoring strategy includes a migration to OpenTelemetry metrics. See [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/about-opentelemetry-metrics).
+> [!IMPORTANT]
+> Collectd metrics are {% data variables.release-phases.retired %} in {% data variables.product.prodname_ghe_server %} 3.23. We recommend migrating to OpenTelemetry metrics before upgrading. See [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance).
{% endif %}
diff --git a/data/reusables/enterprise/opentelemetry-migration.md b/data/reusables/enterprise/opentelemetry-migration.md
index 1ebe5f2ce7ac..8308cfb1e798 100644
--- a/data/reusables/enterprise/opentelemetry-migration.md
+++ b/data/reusables/enterprise/opentelemetry-migration.md
@@ -1,7 +1,15 @@
{% ifversion ghes-opentelemetry %}
-OpenTelemetry metrics are the future foundation for monitoring, available from {% data variables.product.prodname_ghe_server %} 3.18 onward. This new monitoring stack provides enhanced observability and management capabilities compared to the legacy collectd system.
+OpenTelemetry metrics are the foundation for monitoring {% data variables.product.prodname_ghe_server %}, available from version 3.18 onward. This monitoring stack provides enhanced observability and management capabilities compared to the legacy collectd system.
-Collectd metrics continue to be gathered by default and remain fully supported in this release.
+{% ifversion ghes < 3.23 %}
+
+New installations of version 3.19 and later use OpenTelemetry metrics by default. On version 3.18, and on instances upgraded from earlier releases, collectd metrics are still gathered by default and remain supported in this release. Collectd is retired in version 3.23, and upgrading to 3.23 disables it. Migration to OpenTelemetry is recommended before upgrading instances that still use collectd. See [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance).
+
+{% else %}
+
+Collectd is retired in this release and can no longer be run. Upgrading to version 3.23 disables collectd, and OpenTelemetry metrics become the only metrics stack. For information about post-upgrade checks, see [AUTOTITLE](/admin/monitoring-and-managing-your-instance/monitoring-your-instance/opentelemetry-metrics/migration-guidance).
+
+{% endif %}
{% endif %}
diff --git a/next.config.ts b/next.config.ts
index 2ae56cb8e80b..dad105d8397e 100644
--- a/next.config.ts
+++ b/next.config.ts
@@ -40,9 +40,10 @@ const config: NextConfig = {
quietDeps: true,
silenceDeprecations: ['legacy-js-api', 'import', 'global-builtin', 'color-4-api'],
},
- // Don't use automatic Next.js logging in dev unless the log level is `debug` or higher
+ // Don't use automatic Next.js request logging unless the log level is `debug` or higher.
+ // Our own request logger already covers it. `false` would also stop `[browser]` console forwarding.
// See `src/observability/logger/README.md` for log levels
- logging: getLogLevelNumber() < 3 ? undefined : {},
+ logging: getLogLevelNumber() < 3 ? { incomingRequests: false } : {},
async rewrites() {
const DEFAULT_VERSION = 'free-pro-team@latest'
return productIds.map((productId) => {
@@ -101,6 +102,12 @@ const config: NextConfig = {
// the CDN marks the cached content as "fresh".
generateEtags: false,
+ experimental: {
+ // getServerSideProps returns rendered Markdown, so many pages exceed the 128 kB default.
+ // https://nextjs.org/docs/messages/large-page-data
+ largePageDataBytes: 1024 * 1024, // 1 MB
+ },
+
// Disable Next.js's in-memory data cache. We don't use ISR or cached
// fetch — all pages render via Express middleware with getServerSideProps.
// The default 50 MB cache is unnecessary overhead during a memory-leak
diff --git a/package-lock.json b/package-lock.json
index 59513645ecbd..f5d8c377e41d 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -86,7 +86,7 @@
"strip-ansi": "7.1.0",
"swr": "^2.4.0",
"tcp-port-used": "1.0.2",
- "tsx": "^4.21.0",
+ "tsx": "^4.23.15",
"unified": "^11.0.5",
"unist-util-find": "^3.0.0",
"unist-util-visit": "^5.1.0",
@@ -8926,6 +8926,7 @@
"version": "4.10.1",
"resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.10.1.tgz",
"integrity": "sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==",
+ "dev": true,
"license": "MIT",
"dependencies": {
"resolve-pkg-maps": "^1.0.0"
@@ -13365,6 +13366,7 @@
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz",
"integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==",
+ "dev": true,
"funding": {
"url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1"
}
@@ -14726,13 +14728,12 @@
"integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q=="
},
"node_modules/tsx": {
- "version": "4.21.0",
- "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz",
- "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==",
+ "version": "4.23.15",
+ "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.15.tgz",
+ "integrity": "sha512-Yiex1Ovn8z2xPpOWckIiysV1SSyRMY9BkLF++q0yKiDxCqRhosKfMg3janKkiLBwZ5c/YryloKwGZcrEmtwxKw==",
"license": "MIT",
"dependencies": {
- "esbuild": "~0.27.0",
- "get-tsconfig": "^4.7.5"
+ "esbuild": "~0.28.0"
},
"bin": {
"tsx": "dist/cli.mjs"
diff --git a/package.json b/package.json
index 2d382fc6f400..9ba51ad81cf8 100644
--- a/package.json
+++ b/package.json
@@ -245,7 +245,7 @@
"strip-ansi": "7.1.0",
"swr": "^2.4.0",
"tcp-port-used": "1.0.2",
- "tsx": "^4.21.0",
+ "tsx": "^4.23.15",
"unified": "^11.0.5",
"unist-util-find": "^3.0.0",
"unist-util-visit": "^5.1.0",
diff --git a/src/archives/middleware/archived-enterprise-versions.ts b/src/archives/middleware/archived-enterprise-versions.ts
index 6a4451cfdee6..2a24fd4cf2dc 100644
--- a/src/archives/middleware/archived-enterprise-versions.ts
+++ b/src/archives/middleware/archived-enterprise-versions.ts
@@ -278,8 +278,6 @@ export default async function archivedEnterpriseVersions(
versionSatisfiesRange(requestedVersion, `>=${firstReleaseStoredInBlobStorage}`) &&
versionSatisfiesRange(requestedVersion, `<=3.9`)
) {
- // Fastly sets x-host, and GLB removes x-forwarded-host.
- const host = req.get('x-host') || req.get('x-forwarded-host') || req.get('host')
const modifiedBody = body
.replaceAll(
`${OLD_AZURE_BLOB_ENTERPRISE_DIR}/${requestedVersion}/assets/cb-`,
@@ -287,7 +285,8 @@ export default async function archivedEnterpriseVersions(
)
.replaceAll(
`${OLD_AZURE_BLOB_ENTERPRISE_DIR}/${requestedVersion}/`,
- `${req.protocol}://${host}/enterprise-server@${requestedVersion}/`,
+ // Root-relative, so request headers never end up in the page.
+ `/enterprise-server@${requestedVersion}/`,
)
return res.send(modifiedBody)
diff --git a/src/archives/tests/deprecated-enterprise-versions.ts b/src/archives/tests/deprecated-enterprise-versions.ts
index 614cca0cef83..8e43625189b8 100644
--- a/src/archives/tests/deprecated-enterprise-versions.ts
+++ b/src/archives/tests/deprecated-enterprise-versions.ts
@@ -68,6 +68,23 @@ describe('enterprise deprecation', () => {
expect(res.headers['set-cookie']).toBeUndefined()
})
+ test('does not put request host headers into deprecated Enterprise pages ( 3.2-3.9 )', async () => {
+ const res = await get(
+ '/en/enterprise-server@3.5/admin/overview/about-github-enterprise-server',
+ {
+ headers: {
+ 'x-host': 'evil.example">',
+ 'x-forwarded-host': 'evil.example',
+ 'x-forwarded-proto': 'javascript',
+ },
+ },
+ )
+ expect(res.statusCode).toBe(200)
+ expect(res.body).not.toContain('evil.example')
+ expect(res.body).not.toContain('javascript://')
+ expect(res.body).toContain('src="/enterprise-server@3.5/_next/')
+ })
+
test('handles requests for deprecated Enterprise pages ( <2.13 )', async () => {
expect(enterpriseServerReleases.deprecated.includes('2.12')).toBe(true)
const { $, res } = await getDOM('/enterprise/2.12/user/articles/about-branches')
diff --git a/src/frame/lib/create-tree.ts b/src/frame/lib/create-tree.ts
index 507864b7eba1..90dfaecb746d 100644
--- a/src/frame/lib/create-tree.ts
+++ b/src/frame/lib/create-tree.ts
@@ -8,6 +8,9 @@ const logger = createLogger(import.meta.url)
const isProduction = process.env.NODE_ENV === 'production'
+// Development rebuilds the tree on every request, so each missing path warns once until it appears.
+const warnedMissingPaths = new Set()
+
export default async function createTree(
originalPath: string,
rootPath?: string,
@@ -41,12 +44,16 @@ export default async function createTree(
originalPath === 'content/early-access' ||
originalPath.startsWith('content/early-access/')
) {
- logger.warn(msg, { path: originalPath })
+ if (!warnedMissingPaths.has(originalPath)) {
+ warnedMissingPaths.add(originalPath)
+ logger.warn(msg, { path: originalPath })
+ }
return
}
throw new Error(msg)
}
}
+ warnedMissingPaths.delete(originalPath)
const relativePath = filepath.replace(`${basePath}/`, '')
diff --git a/src/frame/middleware/helmet.ts b/src/frame/middleware/helmet.ts
index dadb83ab637e..1d28b2489a72 100644
--- a/src/frame/middleware/helmet.ts
+++ b/src/frame/middleware/helmet.ts
@@ -30,7 +30,6 @@ const DEFAULT_OPTIONS = {
contentSecurityPolicy: {
directives: {
defaultSrc: ["'none'"],
- prefetchSrc: ["'self'"],
// Safari local development needs ws: for Next.js hot module reloading.
connectSrc: ["'self'", 'https://collector.githubapp.com', isDev && 'ws:'].filter(
Boolean,
diff --git a/src/frame/middleware/index.ts b/src/frame/middleware/index.ts
index 41aa501ce6a8..d911cc29babf 100644
--- a/src/frame/middleware/index.ts
+++ b/src/frame/middleware/index.ts
@@ -49,7 +49,7 @@ import features from '@/versions/middleware/features'
import productGroups from './context/product-groups'
import featuredLinks from '@/landings/middleware/featured-links'
import journeyTrack from '@/journeys/middleware/journey-track'
-import next from './next'
+import next, { renderNextDevEndpoints } from './next'
import renderPage from './render-page'
import assetPreprocessing from '@/assets/middleware/asset-preprocessing'
import archivedAssetRedirects from '@/archives/middleware/archived-asset-redirects'
@@ -160,6 +160,10 @@ export default function index(app: Express) {
)
}
+ if (process.env.NODE_ENV === 'development') {
+ app.use(renderNextDevEndpoints)
+ }
+
app.use(shielding)
app.use(handleNextDataPath)
diff --git a/src/frame/middleware/next.ts b/src/frame/middleware/next.ts
index a467ddffc588..f51c2c6ad4c8 100644
--- a/src/frame/middleware/next.ts
+++ b/src/frame/middleware/next.ts
@@ -20,4 +20,13 @@ function renderPageWithNext(req: ExtendedRequest, res: Response, nextFn: NextFun
return nextFn()
}
+// Dev overlay endpoints, like /__nextjs_original-stack-frames, read the raw body, so they run before express.json().
+export function renderNextDevEndpoints(req: ExtendedRequest, res: Response, nextFn: NextFunction) {
+ if (req.path.startsWith('/__nextjs_')) {
+ return nextHandleRequest(req, res)
+ }
+
+ return nextFn()
+}
+
export default renderPageWithNext
diff --git a/src/frame/start-server.ts b/src/frame/start-server.ts
index ec5f6620269e..30b415cf7aff 100644
--- a/src/frame/start-server.ts
+++ b/src/frame/start-server.ts
@@ -14,7 +14,7 @@ import createApp from './lib/app'
import warmServer from './lib/warm-server'
import { createLogger } from '@/observability/logger'
-dotenv.config()
+dotenv.config({ quiet: true })
checkNodeVersion()
diff --git a/src/frame/tests/create-tree.ts b/src/frame/tests/create-tree.ts
index c0ba7396c1f1..09d991165eda 100644
--- a/src/frame/tests/create-tree.ts
+++ b/src/frame/tests/create-tree.ts
@@ -1,11 +1,14 @@
import fs from 'fs/promises'
-import { describe, expect, test } from 'vitest'
+import { describe, expect, test, vi } from 'vitest'
import createTree from '@/frame/lib/create-tree'
import { setCategoryApplicableVersions } from '@/frame/lib/page-data'
import type { Page, UnversionedTree } from '@/types'
+const logger = vi.hoisted(() => ({ error: vi.fn(), warn: vi.fn(), info: vi.fn(), debug: vi.fn() }))
+vi.mock('@/observability/logger', () => ({ createLogger: () => logger }))
+
const contentDir = 'src/fixtures/fixtures/content'
function flattenPages(tree: UnversionedTree): Page[] {
@@ -55,6 +58,16 @@ describe('createTree', () => {
expect(reused.length).toBe(declared.length - 1)
})
+ test('warns once for missing early-access content across rebuilds', async () => {
+ const missingPath = 'content/early-access/does-not-exist'
+ logger.warn.mockClear()
+
+ expect(await createTree(missingPath)).toBeUndefined()
+ expect(await createTree(missingPath)).toBeUndefined()
+
+ expect(logger.warn).toHaveBeenCalledTimes(1)
+ })
+
test('stores the file mtime on the page', async () => {
const tree = (await createTree(contentDir)) as UnversionedTree
const { mtimeMs } = await fs.stat(tree.page.fullPath)
diff --git a/src/landings/components/shared/LandingCarousel.tsx b/src/landings/components/shared/LandingCarousel.tsx
index 8e9a821cc0dd..c72db2031cd9 100644
--- a/src/landings/components/shared/LandingCarousel.tsx
+++ b/src/landings/components/shared/LandingCarousel.tsx
@@ -5,7 +5,7 @@ import { Card } from '@primer/react-brand'
import cx from 'clsx'
import type { ResolvedArticle } from '@/types'
import { useTranslation } from '@/languages/components/useTranslation'
-import { useVersion } from '@/versions/components/useVersion'
+import { DEFAULT_VERSION, useVersion } from '@/versions/components/useVersion'
import styles from './LandingCarousel.module.scss'
import { RenderedHTML } from '@/frame/components/ui/RenderedHTML/RenderedHTML'
@@ -120,6 +120,7 @@ export const LandingCarousel = ({
}, 100)
}
+ const versionPrefix = currentVersion !== DEFAULT_VERSION ? `/${currentVersion}` : ''
const startIndex = currentPage * itemsPerView
const visibleItems = processedItems.slice(startIndex, startIndex + itemsPerView)
@@ -164,7 +165,7 @@ export const LandingCarousel = ({
{visibleItems.map((article: ResolvedArticle, index) => (
({
+ locale: 'en',
+ query: {} as Record,
+}))
+
+vi.mock('next/router', () => ({ useRouter: () => router }))
+vi.mock('@/languages/components/useTranslation', () => ({
+ useTranslation: () => ({ t: (key: string) => key }),
+}))
+// Primer packages import CSS files, which Node cannot load outside a bundler.
+vi.mock('@primer/react-brand', () => {
+ const passthrough = ({ children }: { children?: ReactNode }) =>
+ createElement('div', null, children)
+ const Card = ({ href, children }: { href: string; children?: ReactNode }) =>
+ createElement('a', { href }, children)
+ Card.Heading = passthrough
+ Card.Description = passthrough
+ return { Card }
+})
+vi.mock('@/frame/components/ui/RenderedHTML/RenderedHTML', () => ({ RenderedHTML: () => null }))
+
+const { LandingCarousel } = await import('@/landings/components/shared/LandingCarousel')
+
+function renderHrefs() {
+ const carouselArticles = [
+ { href: '/actions/quickstart', title: 'Quickstart', intro: '', category: [] },
+ ]
+ const $ = load(renderToStaticMarkup(createElement(LandingCarousel, { carouselArticles })))
+ return $('a')
+ .map((_, el) => $(el).attr('href'))
+ .get()
+}
+
+describe('LandingCarousel card links', () => {
+ beforeEach(() => {
+ router.locale = 'en'
+ router.query = {}
+ })
+
+ test('omits the default version', () => {
+ expect(renderHrefs()).toEqual(['/en/actions/quickstart'])
+ })
+
+ test('includes a non-default version', () => {
+ router.locale = 'ja'
+ router.query = { versionId: 'enterprise-server@3.16' }
+ expect(renderHrefs()).toEqual(['/ja/enterprise-server@3.16/actions/quickstart'])
+ })
+})
diff --git a/src/observability/logger/middleware/get-automatic-request-logger.ts b/src/observability/logger/middleware/get-automatic-request-logger.ts
index 5cef1caecd88..0fd1c527575f 100644
--- a/src/observability/logger/middleware/get-automatic-request-logger.ts
+++ b/src/observability/logger/middleware/get-automatic-request-logger.ts
@@ -44,8 +44,8 @@ export function getAutomaticRequestLogger() {
} else if (shouldEnableAutomaticDevLogging()) {
const logLevelNum = getLogLevelNumber()
- // Suppress /_next/ requests unless LOG_LEVEL is debug or more verbose.
- if (url?.startsWith('/_next/') && logLevelNum < 3) {
+ // Suppress /_next/ and /__nextjs_ requests unless LOG_LEVEL is debug or more verbose.
+ if ((url?.startsWith('/_next/') || url?.startsWith('/__nextjs_')) && logLevelNum < 3) {
return originalEnd.apply(this, args as Parameters)
}
diff --git a/src/observability/tests/get-automatic-request-logger.ts b/src/observability/tests/get-automatic-request-logger.ts
index 1204552d4591..a33ee21da31e 100644
--- a/src/observability/tests/get-automatic-request-logger.ts
+++ b/src/observability/tests/get-automatic-request-logger.ts
@@ -164,20 +164,23 @@ describe('getAutomaticRequestLogger', () => {
}
})
- it('should filter out _next requests unless debug level', async () => {
- vi.stubEnv('LOG_LEVEL', 'info') // info level = 2, debug = 3
+ it.each(['/_next/static/file.js', '/__nextjs_font/geist-latin.woff2'])(
+ 'should filter out %s unless debug level',
+ async (url) => {
+ vi.stubEnv('LOG_LEVEL', 'info') // info level = 2, debug = 3
- mockReq.url = '/_next/static/file.js'
- mockReq.originalUrl = '/_next/static/file.js'
+ mockReq.url = url
+ mockReq.originalUrl = url
- const middleware = getAutomaticRequestLogger()
- middleware(mockReq as Request, mockRes as Response, mockNext)
- ;(mockRes as MockResponseWithEnd).end()
+ const middleware = getAutomaticRequestLogger()
+ middleware(mockReq as Request, mockRes as Response, mockNext)
+ ;(mockRes as MockResponseWithEnd).end()
- await new Promise((resolve) => setTimeout(resolve, 20))
+ await new Promise((resolve) => setTimeout(resolve, 20))
- expect(consoleLogs).toHaveLength(0)
- })
+ expect(consoleLogs).toHaveLength(0)
+ },
+ )
it('should log _next requests when debug level is set', async () => {
vi.stubEnv('LOG_LEVEL', 'debug') // debug level = 3
diff --git a/src/pages/_document.tsx b/src/pages/_document.tsx
index 92d962b3d572..226a488cd35a 100644
--- a/src/pages/_document.tsx
+++ b/src/pages/_document.tsx
@@ -6,10 +6,13 @@ import { colorModeScript } from '@/color-schemes/lib/color-mode-script'
// MyDocument leaves SSR theme attributes at defaultCSSTheme so the HTML stays shared-cacheable.
// colorModeScript updates them from the color_mode cookie before the browser's first paint.
// colorModeScript injects executable JS, not content HTML, so RenderedHTML and hast do not apply.
+// Primer Brand sets smooth scrolling on . data-scroll-behavior lets Next jump instantly on
+// route changes and keep smooth scrolling for in-page anchors.
export default class MyDocument extends Document {
render() {
return (
{
).resolves.not.toThrow()
}
})
+
+ // PatchNotes uses each note as its React key.
+ test('has no duplicate notes in a list', () => {
+ for (const [key, section] of Object.entries(yamlContent.sections || {})) {
+ const lists = [section.filter((item) => typeof item === 'string')]
+ for (const item of section) {
+ if (typeof item !== 'string' && Array.isArray(item.notes)) lists.push(item.notes)
+ }
+ for (const notes of lists) {
+ const duplicates = notes.filter((note, i) => notes.indexOf(note) !== i)
+ expect(duplicates, `sections.${key} has duplicate notes`).toEqual([])
+ }
+ }
+ })
})
})