diff --git a/.github/workflows/purge-fastly.yml b/.github/workflows/purge-fastly.yml index 558817cb55df..9f5bd1bca7b1 100644 --- a/.github/workflows/purge-fastly.yml +++ b/.github/workflows/purge-fastly.yml @@ -39,6 +39,7 @@ jobs: - uses: ./.github/actions/node-npm-setup - name: Wait for production to match build number + if: github.event_name != 'workflow_dispatch' run: | needs=$(git rev-parse HEAD) start_time=$(date +%s) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3a42d77f0ea6..8faa174523df 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # Docs changelog +**21 April 2026** + +We recently published two new guides for organizations adopting Copilot cloud agent. + +* Pre-enablement: [Building guardrails for GitHub Copilot cloud agent](https://docs.github.com/en/enterprise-cloud@latest/copilot/tutorials/cloud-agent/build-guardrails) helps you expand built-in protections to create a secure environment for agents to operate in. +* Post-enablement: [Giving GitHub Copilot cloud agent access to resources in your organization](https://docs.github.com/en/enterprise-cloud@latest/copilot/tutorials/cloud-agent/give-access-to-resources) helps you get more out of Copilot by giving it access to MCP servers and internal packages, while promoting secure, consistent practices. + +
+ **15 April 2026** LSP servers greatly improve Copilot CLI's ability to work with your code. For example, when Copilot renames a symbol throughout a project it can do this more quickly and reliably if it has access to an LSP server for the language the code is written in. diff --git a/assets/images/help/copilot/cloud-agent/open-agent-session-in-copilot-cli.png b/assets/images/help/copilot/cloud-agent/open-agent-session-in-copilot-cli.png deleted file mode 100644 index 4feaa802831e..000000000000 Binary files a/assets/images/help/copilot/cloud-agent/open-agent-session-in-copilot-cli.png and /dev/null differ diff --git a/config/kubernetes/production/deployments/webapp.yaml b/config/kubernetes/production/deployments/webapp.yaml index 85c6dbba4a7c..7b366e61bfee 100644 --- a/config/kubernetes/production/deployments/webapp.yaml +++ b/config/kubernetes/production/deployments/webapp.yaml @@ -40,18 +40,18 @@ spec: image: docs-internal resources: requests: - cpu: 1250m + cpu: 1500m # Absolute minimum to start app is 1000m # Node is single-threaded but we want more CPUs # for OS and image resizing, and other binary executions # Better to increase replicas or memory than CPU - memory: 8.0Gi + memory: 5000Mi # Absolute minimum to start app is 4500Mi # Would increase with more pages, versions, or languages supported # The additional memory helps during traffic surges limits: cpu: 8000m - memory: 16.0Gi + memory: 14.0Gi ports: - name: http containerPort: 4000 diff --git a/content/billing/how-tos/set-up-budgets.md b/content/billing/how-tos/set-up-budgets.md index 29c4a65e7a2c..950b8589665a 100644 --- a/content/billing/how-tos/set-up-budgets.md +++ b/content/billing/how-tos/set-up-budgets.md @@ -136,3 +136,4 @@ You can edit or delete a budget at any time, but you cannot change the scope of 1. Navigate to the "Budgets and alerts" view. See [Viewing budgets](#viewing-budgets). 1. In the list of budgets, click {% octicon "kebab-horizontal" aria-label="View actions" %} next to the budget you want to edit, and click **{% octicon "pencil" aria-hidden="true" aria-label="pencil" %} Edit** or **{% octicon "trash" aria-hidden="true" aria-label="trash" %} Delete**. 1. Follow the prompts. + diff --git a/content/codespaces/troubleshooting/troubleshooting-included-usage.md b/content/codespaces/troubleshooting/troubleshooting-included-usage.md index 0aa4ac320c34..2becdafc129b 100644 --- a/content/codespaces/troubleshooting/troubleshooting-included-usage.md +++ b/content/codespaces/troubleshooting/troubleshooting-included-usage.md @@ -106,3 +106,4 @@ If the dev container for the current codespace was built from the default image, Alternatively, you can check which repositories have prebuilds by reviewing a usage report. See [Understanding your {% data variables.product.prodname_codespaces %} usage](#understanding-your-codespaces-usage) above. * Storage of containers built from the default dev container image for codespaces is free of charge and does not reduce your included storage. You can therefore avoid your storage allowance being consumed by your dev container by using the default image in your dev container configuration, rather than specifying a more specialized image. See [AUTOTITLE](/codespaces/setting-up-your-project-for-codespaces/adding-a-dev-container-configuration/introduction-to-dev-containers#using-the-default-dev-container-configuration) and [Storage usage for your base dev container](#storage-usage-for-your-base-dev-container) above. + diff --git a/content/copilot/concepts/agents/copilot-cli/cancel-and-roll-back.md b/content/copilot/concepts/agents/copilot-cli/cancel-and-roll-back.md new file mode 100644 index 000000000000..e25d30058598 --- /dev/null +++ b/content/copilot/concepts/agents/copilot-cli/cancel-and-roll-back.md @@ -0,0 +1,75 @@ +--- +title: Canceling a {% data variables.copilot.copilot_cli %} operation and rolling back changes +shortTitle: Cancel and roll back +intro: 'Find out about the different ways to cancel an active {% data variables.product.prodname_copilot_short %} operation, and how to roll back changes made during a session if the result isn''t what you expected.' +versions: + feature: copilot +contentType: concepts +docsTeamMetrics: + - copilot-cli +category: + - Learn about Copilot # Copilot discovery page + - Learn about Copilot CLI # Copilot CLI bespoke page +--- + +## Introduction + +When you work in an interactive {% data variables.copilot.copilot_cli_short %} session, you can press Esc or Ctrl+C to control what {% data variables.product.prodname_copilot_short %} is doing. Both keypresses can cancel operations, but they work slightly differently: + +* Ctrl+C immediately stops the current operation. +* A single Esc keypress gives you more gradual control—letting you dismiss dialogs, clear queued prompts, or cancel an operation in stages. + +If {% data variables.product.prodname_copilot_short %} has already made changes and you want to undo them, you can roll back your workspace to a previous point in the session. {% data variables.copilot.copilot_cli_short %} takes a snapshot of your workspace state each time you enter a prompt, and this allows you to rewind to an earlier state by pressing Esc twice when {% data variables.product.prodname_copilot_short %} is idle and the input area is empty. + +## What pressing Esc does in different situations + +Pressing Esc once performs different actions depending on the current state of the session: + +| Current state | What pressing Esc does | +| ------------- | ------------------------ | +| {% data variables.product.prodname_copilot_short %} is active with no queued prompts. | Cancels the running operation. | +| {% data variables.product.prodname_copilot_short %} is active and there are queued prompts. | Clears the queued prompts without stopping the current operation. | +| A dialog, overlay, or picker is open. | Closes the dialog, overlay, or picker. | +| {% data variables.product.prodname_copilot_short %} is idle. | Shows a brief reminder that pressing Esc again quickly will open the rewind picker. See [Rolling back changes](#rolling-back-changes). | + +## When to use Esc instead of Ctrl+C + +The main difference between these two ways of canceling an operation is that Esc is designed for gradual, targeted intervention, while Ctrl+C is a hard stop. + +Use Esc when you want to interact with {% data variables.product.prodname_copilot_short %} without necessarily ending the current operation. For example, if a permission dialog appears and you want to deny that specific request, pressing Esc dismisses the dialog and {% data variables.product.prodname_copilot_short %} continues working—it just won't use the tool you denied. Similarly, if you've queued follow-up prompts and want to cancel them without interrupting the work already in progress, Esc clears the queue while the current operation keeps running. Pressing Esc only cancels the operation outright if there are no dialogs open and no queued prompts to clear first. + +Use Ctrl+C when you want to stop everything at once. It immediately cancels the active operation and clears any queued prompts in a single keypress. Any file write that is already in progress will complete—files are not left corrupted mid-write—but any remaining planned changes are abandoned. Pressing Ctrl+C a second time within two seconds, when the input area is empty, exits the session entirely. + +As a rule of thumb, use Esc when you want to intervene selectively, and Ctrl+C when you want to stop and start over. + +## Rolling back changes + +While {% data variables.product.prodname_copilot_short %} is inactive and there is no text in the input area, you can press Esc twice to display a list of points in your current session that you can roll back to. Each point corresponds to a snapshot of your workspace that was taken immediately before {% data variables.product.prodname_copilot_short %} started working on the prompt shown in the list. + +For full details of how to use the double Esc keypress to roll back changes made during a session, see [AUTOTITLE](/copilot/how-tos/copilot-cli/roll-back-changes). + +> [!WARNING] +> {% data reusables.copilot.copilot-cli.cli-rewind-warning %} + +### What happens when you roll back + +When you select a snapshot from the rewind picker, the following actions occur: + +1. **Git state is restored.** The repository is checked out to the Git commit and branch recorded in the snapshot. +1. **Untracked files are cleaned.** Files that did not exist at the time of the snapshot are removed. +1. **Modified files are restored.** Files that were changed after the snapshot are reverted to their backed-up state, including permissions and staging state. +1. **Session history is truncated.** The conversation is rewound to the point where the selected snapshot was taken. All messages and tool calls that occurred after that point are removed from the session. +1. **Snapshots are removed.** The selected snapshot and all snapshots after it are permanently deleted. Only snapshots from earlier conversation steps remain available for future rewinds. +1. **Rollback confirmed.** After the rollback, {% data variables.product.prodname_copilot_short %} displays a message indicating how many files were restored. +1. **Your prompt is restored.** The prompt associated with the selected snapshot is placed in the input area. + +### Changes that can't be rolled back + +Rewind is unavailable in the following situations: + +* **Files over 10 MB.** Individual files larger than 10 MB are skipped during snapshot creation. Changes to these files are not restored during a rollback. +* **More than 500 changed files.** If more than 500 files were changed during a single step of a CLI conversation, a snapshot is not created for that step. You will not be able to roll back changes made in that step. Earlier snapshots are unaffected. + +## Further reading + +* [AUTOTITLE](/copilot/reference/copilot-cli-reference/cli-command-reference) diff --git a/content/copilot/concepts/agents/copilot-cli/index.md b/content/copilot/concepts/agents/copilot-cli/index.md index a78a8fa3a4be..4fb6f7c59556 100644 --- a/content/copilot/concepts/agents/copilot-cli/index.md +++ b/content/copilot/concepts/agents/copilot-cli/index.md @@ -8,6 +8,7 @@ versions: children: - /about-copilot-cli - /comparing-cli-features + - /cancel-and-roll-back - /about-remote-access - /about-custom-agents - /about-cli-plugins diff --git a/content/copilot/concepts/billing/billing-for-individuals.md b/content/copilot/concepts/billing/billing-for-individuals.md index 8298273da757..978a61160367 100644 --- a/content/copilot/concepts/billing/billing-for-individuals.md +++ b/content/copilot/concepts/billing/billing-for-individuals.md @@ -16,6 +16,9 @@ category: - Learn about Copilot --- +> [!IMPORTANT] +> {% data reusables.copilot.plans.individual-plans-paused %} If you hit unexpected limits as a result of these changes, you can cancel your Pro or Pro+ subscription and you will not be charged for April usage. Please reach out to [GitHub support](https://support.github.com/) between April 20 and May 20, 2026, for a refund. + ## Pricing for {% data variables.copilot.copilot_pro_short %} and {% data variables.copilot.copilot_pro_plus_short %} {% data variables.product.github %} offers two paid plans for individuals: {% data variables.copilot.copilot_pro_short %} and {% data variables.copilot.copilot_pro_plus_short %}. diff --git a/content/copilot/concepts/billing/copilot-requests.md b/content/copilot/concepts/billing/copilot-requests.md index 95ee6ac915a3..d99260de91c6 100644 --- a/content/copilot/concepts/billing/copilot-requests.md +++ b/content/copilot/concepts/billing/copilot-requests.md @@ -107,6 +107,5 @@ If you use **{% data variables.copilot.copilot_free_short %}**, you have access Premium request usage is based on the model’s multiplier and the feature you’re using. For example: -* **Using {% data variables.copilot.copilot_claude_opus_45 %} in {% data variables.copilot.copilot_chat_short %}**: With a 3× multiplier, one interaction counts as 3 premium requests. * **Using {% data variables.copilot.copilot_gpt_5_mini %} on {% data variables.copilot.copilot_free_short %}**: Each interaction counts as 1 premium request. * **Using {% data variables.copilot.copilot_gpt_5_mini %} on a paid plan**: No premium requests are consumed. diff --git a/content/copilot/concepts/billing/individual-plans.md b/content/copilot/concepts/billing/individual-plans.md index 0ba222517989..5dc3b44098fe 100644 --- a/content/copilot/concepts/billing/individual-plans.md +++ b/content/copilot/concepts/billing/individual-plans.md @@ -16,6 +16,9 @@ category: - Learn about Copilot --- +> [!IMPORTANT] +> {% data reusables.copilot.plans.individual-plans-paused %} + {% data variables.product.company_short %} offers three {% data variables.product.prodname_copilot_short %} plans for individual developers, as well as a dedicated student offering, each designed to meet different needs based on your coding habits, interest in AI models, and desired level of flexibility. You can choose from the following plans. diff --git a/content/copilot/concepts/billing/premium-request-management.md b/content/copilot/concepts/billing/premium-request-management.md index 8ca3ab83a2b5..a4bdca941b2b 100644 --- a/content/copilot/concepts/billing/premium-request-management.md +++ b/content/copilot/concepts/billing/premium-request-management.md @@ -11,6 +11,9 @@ category: - Manage Copilot for a team --- +> [!IMPORTANT] +> **Starting April 20, 2026**, {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, and student plans have tighter usage limits: Session limits and weekly (7 day) limits. For more information, see [AUTOTITLE](/copilot/concepts/usage-limits). + ## Management methods The best way to control budgets for premium requests in an enterprise is to create cost centers and scope one or more budgets to each center. diff --git a/content/copilot/concepts/index.md b/content/copilot/concepts/index.md index 05d356b2ac66..7c60f1e6517f 100644 --- a/content/copilot/concepts/index.md +++ b/content/copilot/concepts/index.md @@ -14,7 +14,7 @@ children: - /context - /tools - /auto-model-selection - - /rate-limits + - /usage-limits - /billing - /about-enterprise-accounts-for-copilot-business - /policies diff --git a/content/copilot/concepts/rate-limits.md b/content/copilot/concepts/rate-limits.md deleted file mode 100644 index e5c369da617e..000000000000 --- a/content/copilot/concepts/rate-limits.md +++ /dev/null @@ -1,44 +0,0 @@ ---- -title: Rate limits for GitHub Copilot -shortTitle: Rate limits -allowTitleToDifferFromFilename: true -intro: 'Learn about {% data variables.product.prodname_copilot %} rate limits and what to do if you are rate limited.' -versions: - feature: copilot -redirect_from: - - /copilot/troubleshooting-github-copilot/rate-limits-for-github-copilot - - /copilot/how-tos/troubleshoot/rate-limits-for-github-copilot -contentType: concepts -category: - - Learn about Copilot ---- - -Rate limiting is a mechanism used to control the number of requests a user or application can make in a given time period. {% data variables.product.github %} uses rate limits to ensure everyone has fair access to {% data variables.product.prodname_copilot %} and to protect against abuse. - -When you hit a rate limit, you may temporarily lose access to certain {% data variables.product.prodname_copilot %} features or models, and you’ll see an error message informing you that you’ve been rate limited. - -## Why does {% data variables.product.github %} use rate limits? - -{% data variables.product.github %} enforces rate limits for several reasons. - -* **Capacity:** There is a limited amount of computing power available to serve all {% data variables.product.prodname_copilot_short %} users. Rate limiting helps prevent the system from being overloaded. -* **High usage:** Popular features and models may receive bursts of requests. Rate limits ensure no single user or group can monopolize these resources. -* **Fairness:** Rate limits ensure that all users have equitable access to {% data variables.product.prodname_copilot_short %}. -* **Abuse mitigation:** Without rate limits, malicious actors could exploit {% data variables.product.prodname_copilot_short %}, leading to degraded service for everyone or even denial of service. - -## Types of rate limits -* **Limits for overall service reliability:** Temporary protections that {% data variables.product.github %} applies to keep {% data variables.product.prodname_copilot %} reliable and fair for everyone. You may see these represented as **global** or **weekly** rate limits. -* **Limits for specific models or model family capacity:** Plan-based limits that reflect the {% data variables.product.prodname_copilot %} usage you've consumed for a particular model or model family. - -## What to do if you are rate limited - -If you receive a rate limit error when using {% data variables.product.prodname_copilot_short %}, you should: - -* **Wait and try again.** Rate limits are temporary. Often, waiting a short period and trying again resolves the issue. -* **Check your usage.** If you’re making frequent or automated requests (for example, rapid-fire completions or large-scale usage), consider adjusting your usage pattern. -* **Change your model.** Select models may have stricter rate limits due to limited capacity. -* **Switch to {% data variables.copilot.copilot_auto_model_selection %}.** If you hit a **weekly** rate limit, you can continue using {% data variables.product.prodname_copilot_short %} with {% data variables.copilot.copilot_auto_model_selection_short_cap_a %} until exhausting your premium requests. -* **Upgrade your plan.** If you are on an individual {% data variables.product.prodname_copilot_short %} plan, upgrading your plan will allow for additional usage. -* **Contact Support.** If you’re repeatedly rate limited and believe it’s impacting legitimate use, contact {% data variables.contact.contact_support_page %} for assistance. - ->[!NOTE] Service-level rate limits should not affect typical {% data variables.product.prodname_copilot_short %} usage. However, if you’re heavily using select models, you may encounter rate limits more frequently. diff --git a/content/copilot/concepts/usage-limits.md b/content/copilot/concepts/usage-limits.md new file mode 100644 index 000000000000..931fb40471dd --- /dev/null +++ b/content/copilot/concepts/usage-limits.md @@ -0,0 +1,55 @@ +--- +title: Usage limits for GitHub Copilot +shortTitle: Usage limits +allowTitleToDifferFromFilename: true +intro: 'Learn about {% data variables.product.prodname_copilot %} usage limits and what to do if you hit a limit.' +versions: + feature: copilot +redirect_from: + - /copilot/troubleshooting-github-copilot/rate-limits-for-github-copilot + - /copilot/how-tos/troubleshoot/rate-limits-for-github-copilot + - /copilot/concepts/rate-limits +contentType: concepts +category: + - Learn about Copilot +--- + +Rate limiting is a mechanism used to control the number of requests a user or application can make in a given time period. {% data variables.product.github %} uses rate limits to ensure everyone has fair access to {% data variables.product.prodname_copilot %} and to protect against abuse. + +## Why does {% data variables.product.github %} use rate limits? + +{% data variables.product.github %} enforces rate limits for several reasons. + +* **Capacity:** There is a limited amount of computing power available to serve all {% data variables.product.prodname_copilot_short %} users. Rate limiting helps prevent the system from being overloaded. +* **High usage:** Popular features and models may receive bursts of requests. Rate limits ensure no single user or group can monopolize these resources. +* **Fairness:** Rate limits ensure that all users have equitable access to {% data variables.product.prodname_copilot_short %}. +* **Abuse mitigation:** Without rate limits, malicious actors could exploit {% data variables.product.prodname_copilot_short %}, leading to degraded service for everyone or even denial of service. + +## Types of usage limits + +{% data variables.product.prodname_copilot %} has two limits: a **session** and a **weekly (7-day) limit**. + +* **Session limit.** If you hit the session limit, you must wait until it resets before you can resume using {% data variables.product.prodname_copilot_short %}. +* **Weekly limit.** This limit caps the total number of tokens you can consume during a 7-day period. If you hit a weekly limit and you have {% data variables.product.prodname_prus %} remaining, you can continue using {% data variables.product.prodname_copilot_short %} with {% data variables.copilot.copilot_auto_model_selection_short_cap_a %}. Model choice will be re-enabled when the weekly period resets. + +## What you will see when approaching a limit + +{% data variables.product.prodname_vscode_shortname %} and {% data variables.copilot.copilot_cli %} both display a warning when you are approaching a limit. These indicators are designed to help you avoid hitting a limit unexpectedly. + +## What to do if you approach a limit + +If you are approaching a limit, the following steps can help reduce the chances of hitting it. + +* **Use a model with a smaller multiplier for simpler tasks.** The larger the multiplier, the faster you will reach the limit. +* **Use plan mode.** In {% data variables.product.prodname_vscode_shortname %} and {% data variables.copilot.copilot_cli_short %}, plan mode can improve task efficiency and task success, reducing overall token consumption. +* **Reduce parallel workflows.** Parallelized tools result in higher token consumption. Use them sparingly if you are nearing your limits. +* **Upgrade your plan.** If you are on a {% data variables.copilot.copilot_pro_short %} plan, upgrading to {% data variables.copilot.copilot_pro_plus_short %} provides significantly higher usage limits. + +## What to do if you hit a limit + +If you receive a usage limit error when using {% data variables.product.prodname_copilot_short %}, you should: + +* **Wait until your limit time resets.** +* **Switch to {% data variables.copilot.copilot_auto_model_selection %}.** If you hit a **weekly** usage limit, you can continue using {% data variables.product.prodname_copilot_short %} with {% data variables.copilot.copilot_auto_model_selection_short_cap_a %} until exhausting your premium requests. +* **Upgrade your plan.** If you are on an individual {% data variables.product.prodname_copilot_short %} plan, upgrading your plan will allow for additional usage. +* **Contact Support.** If you repeatedly hit usage limits and believe it’s impacting legitimate use, contact {% data variables.contact.contact_support_page %} for assistance. diff --git a/content/copilot/get-started/plans.md b/content/copilot/get-started/plans.md index 5e54f6424166..bed5bf5be565 100644 --- a/content/copilot/get-started/plans.md +++ b/content/copilot/get-started/plans.md @@ -13,6 +13,9 @@ category: - Learn about Copilot --- +> [!IMPORTANT] +> {% data reusables.copilot.plans.individual-plans-paused %} + {% data variables.product.company_short %} offers several plans for {% data variables.product.prodname_copilot %}, depending on your needs and whether you're using {% data variables.product.prodname_copilot_short %} as an individual or as part of an organization or enterprise. * **{% data variables.copilot.copilot_free %}** is available to individual developers who don't have access to {% data variables.product.prodname_copilot_short %} through an organization or enterprise. This free plan includes limited access to select {% data variables.product.prodname_copilot_short %} features, allowing you to try AI-powered coding assistance at no cost. @@ -41,6 +44,9 @@ For more information, see [AUTOTITLE](/copilot/about-github-copilot/github-copil Start using {% data variables.product.prodname_copilot_short %} by signing up for the plan that best fits your needs. +> [!IMPORTANT] +> {% data reusables.copilot.plans.individual-plans-paused %} + * **{% data variables.copilot.copilot_free_short %}** — Try {% data variables.product.prodname_copilot_short %} with limited features and requests. [Start using {% data variables.copilot.copilot_free_short %}](https://github.com/copilot?ref_product=copilot&ref_type=engagement&ref_style=text&ref_plan=free). * **{% data variables.copilot.copilot_student %}** — Get access to {% data variables.product.prodname_copilot_short %}'s premium features for free. [Access {% data variables.copilot.copilot_student %}](/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-students). diff --git a/content/copilot/get-started/what-is-github-copilot.md b/content/copilot/get-started/what-is-github-copilot.md index 163b13395a66..597f0f0a12d7 100644 --- a/content/copilot/get-started/what-is-github-copilot.md +++ b/content/copilot/get-started/what-is-github-copilot.md @@ -64,8 +64,11 @@ You can start using {% data variables.product.prodname_copilot_short %} in sever ### Individuals +>[!IMPORTANT] +> {% data reusables.copilot.plans.individual-plans-paused %} + * **Try {% data variables.product.prodname_copilot_short %} for free.** Use {% data variables.copilot.copilot_free_short %} to explore core features with no paid plan required. -* **Subscribe to a paid plan.** Upgrade to {% data variables.copilot.copilot_pro_short %} or {% data variables.copilot.copilot_pro_plus_short %} for full access to premium features and more generous usage limits. +* **Subscribe to a paid plan.** Upgrade to {% data variables.copilot.copilot_pro_short %} or {% data variables.copilot.copilot_pro_plus_short %} for full access to premium features and more generous usage limits. * **Get free access if you're eligible.** Students, teachers, and open source maintainers may qualify for access to premium features at no cost. See [AUTOTITLE](/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-students) and [AUTOTITLE](/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-teachers-and-os-maintainers). * **Request access from your organization.** If your organization or enterprise has a {% data variables.product.prodname_copilot %} plan, you can request access by going to [https://github.com/settings/copilot](https://github.com/settings/copilot) and request access under "Get {% data variables.product.prodname_copilot_short %} from an organization." diff --git a/content/copilot/how-tos/copilot-cli/index.md b/content/copilot/how-tos/copilot-cli/index.md index 101102e16629..bb40b56de0c1 100644 --- a/content/copilot/how-tos/copilot-cli/index.md +++ b/content/copilot/how-tos/copilot-cli/index.md @@ -25,9 +25,11 @@ children: - /use-copilot-cli-agents - /administer-copilot-cli-for-your-enterprise - /speed-up-task-completion + - /roll-back-changes - /chronicle - /content/copilot/concepts/agents/copilot-cli/about-copilot-cli - /content/copilot/concepts/agents/copilot-cli/comparing-cli-features + - /content/copilot/concepts/agents/copilot-cli/cancel-and-roll-back - /content/copilot/concepts/agents/about-agent-skills - /content/copilot/concepts/agents/copilot-cli/about-cli-plugins - /content/copilot/concepts/agents/copilot-cli/autopilot diff --git a/content/copilot/how-tos/copilot-cli/roll-back-changes.md b/content/copilot/how-tos/copilot-cli/roll-back-changes.md new file mode 100644 index 000000000000..a7c47cfd1920 --- /dev/null +++ b/content/copilot/how-tos/copilot-cli/roll-back-changes.md @@ -0,0 +1,70 @@ +--- +title: Rolling back changes made during a {% data variables.copilot.copilot_cli %} session +shortTitle: Roll back changes +intro: 'Rewind your {% data variables.copilot.copilot_cli_short %} session to a previous prompt to undo changes and restore your repository to a previous state.' +versions: + feature: copilot +contentType: how-tos +category: + - Author and optimize with Copilot # Copilot discovery page + - Build with Copilot CLI # Copilot CLI bespoke page +docsTeamMetrics: + - copilot-cli +--- + +## Introduction + +When you work in an interactive {% data variables.copilot.copilot_cli_short %} session, {% data variables.product.prodname_copilot_short %} can make changes to files, run shell commands, and modify your repository. If the result isn't what you expected, you can rewind to a previous point in the session to undo those changes. + +When you enter a prompt, the first thing {% data variables.copilot.copilot_cli_short %} does is take a snapshot of your workspace state. This snapshot allows you to roll back to that point in the session if you need to. You can trigger a rewind by pressing Esc twice, or by using the `/undo` slash command. + +This article explains how to roll back changes. For more conceptual information about rewinding to an earlier point in a session, see [AUTOTITLE](/copilot/concepts/agents/copilot-cli/cancel-and-roll-back). + +## Prerequisites + +* **You must be working in a Git repository with at least one commit.** {% data variables.copilot.copilot_cli_short %} uses Git operations to track and restore workspace state. +* **A snapshot must exist.** Snapshots are created automatically at the start of each of your interactions with {% data variables.product.prodname_copilot_short %} in a CLI session. You can't roll back changes made before your first prompt in a session, or to the repository state for a step where snapshot creation was skipped, see [Changes that can't be rolled back](/copilot/concepts/agents/copilot-cli/cancel-and-roll-back#changes-that-cant-be-rolled-back). + +## Rolling back with a double Esc keypress + +> [!WARNING] +> * {% data reusables.copilot.copilot-cli.cli-rewind-warning %} +> * Rewinding cannot be undone. Once you roll back to a snapshot, all snapshots and session history after that point are permanently removed. + +When {% data variables.product.prodname_copilot_short %} has finished responding to a prompt you've entered: + +1. Make sure the input area is empty. If there's text in the input area, pressing Esc twice in quick succession clears the text. +1. Press Esc twice in quick succession to open the rewind picker. + + The rewind picker lists the available snapshots for the current session, with the most recent first. The ten most recent snapshots are displayed. If there are more than ten snapshots available you can use the arrow key to scroll down through earlier snapshots. + + For each snapshot, the beginning of the prompt you entered is shown, with an indication of how long ago you submitted it. + +1. Choose a snapshot to roll back to. This will return you to the state of the repository when you entered the associated prompt. + + > [!NOTE] + > The repository is rolled back to its state immediately before {% data variables.product.prodname_copilot_short %} started working on the prompt, not immediately after it finished working on the prompt. + + The prompt you selected is shown in the input area, so you can edit and resubmit it, if required. + +## Rolling back with the `/undo` slash command + +The `/undo` slash command, and its alias `/rewind`, provide an alternative way of opening the rewind picker. + +Both commands produce the same result that you get by pressing Esc twice when {% data variables.product.prodname_copilot_short %} is idle and there is no text in the input area. + +## Verifying the rollback + +After rolling back, you can use Git commands to verify the state of your repository and confirm that it matches your expectations. + +Typing `!` allows you to run shell commands directly from the {% data variables.copilot.copilot_cli_short %} input prompt, so you don't need to exit the CLI to check the repository state. + +| To do this | Enter this command | +| ---------- | ------------------------ | +| Check which files show as modified, staged, or untracked. | `! git status` | +| Show the SHA and commit message of the current commit. | `! git log --oneline -1` | +| Review the unstaged changes. | `! git diff` | + +## Further reading + +* [AUTOTITLE](/copilot/reference/copilot-cli-reference/cli-command-reference) diff --git a/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-students.md b/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-students.md index 8239efa7d134..b4849ab5a293 100644 --- a/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-students.md +++ b/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-students.md @@ -12,6 +12,9 @@ category: - Configure Copilot --- +> [!IMPORTANT] +> {% data reusables.copilot.plans.individual-plans-paused %} + ## About {% data variables.copilot.copilot_student_short %} access Verified students on {% data variables.product.prodname_education %} get **free access to {% data variables.product.prodname_copilot_short %}'s premium features**. See [AUTOTITLE](/copilot/concepts/billing/individual-plans#github-copilot-student). diff --git a/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-teachers-and-os-maintainers.md b/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-teachers-and-os-maintainers.md index b375cdced1c7..8dd9133ac32f 100644 --- a/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-teachers-and-os-maintainers.md +++ b/content/copilot/how-tos/copilot-on-github/set-up-copilot/enable-copilot/set-up-for-teachers-and-os-maintainers.md @@ -17,6 +17,9 @@ category: - Configure Copilot --- +> [!IMPORTANT] +> {% data reusables.copilot.plans.individual-plans-paused %} + ## About free {% data variables.copilot.copilot_pro %} access There are two ways to qualify for free access to {% data variables.copilot.copilot_pro_short %}: diff --git a/content/copilot/how-tos/copilot-sdk/troubleshooting/sdk-and-cli-compatibility.md b/content/copilot/how-tos/copilot-sdk/troubleshooting/sdk-and-cli-compatibility.md index 5f10da7276ea..c83b556c9cbc 100644 --- a/content/copilot/how-tos/copilot-sdk/troubleshooting/sdk-and-cli-compatibility.md +++ b/content/copilot/how-tos/copilot-sdk/troubleshooting/sdk-and-cli-compatibility.md @@ -118,7 +118,6 @@ contentType: how-tos | Screen reader mode | `--screen-reader` | Accessibility | | Rich diff rendering | `--plain-diff` | Terminal rendering | | Startup banner | `--banner` | Visual element | -| Streamer mode | `/streamer-mode` | TUI display mode | | Alternate screen buffer | `--alt-screen`, `--no-alt-screen` | Terminal rendering | | Mouse support | `--mouse`, `--no-mouse` | Terminal input | | **Path/Permission Shortcuts** | | | diff --git a/content/copilot/how-tos/manage-and-track-spending/monitor-premium-requests.md b/content/copilot/how-tos/manage-and-track-spending/monitor-premium-requests.md index 903a381895a9..64504493655c 100644 --- a/content/copilot/how-tos/manage-and-track-spending/monitor-premium-requests.md +++ b/content/copilot/how-tos/manage-and-track-spending/monitor-premium-requests.md @@ -37,6 +37,9 @@ For information about viewing premium request usage for an organization or enter ### Viewing usage in your IDE +> [!IMPORTANT] +> **Starting April 20, 2026**, {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, and student plans have tighter usage limits: Session limits and weekly (7 day) limits. {% data variables.product.prodname_vscode_shortname %} and {% data variables.copilot.copilot_cli %} both display your available usage when you are approaching a limit. See [AUTOTITLE](/copilot/concepts/usage-limits). + If you're using {% data variables.product.prodname_copilot_short %} in an editor, you can view your usage directly in the editor. For example, in {% data variables.product.prodname_vscode %}, you can view information about features included in your plan, your progress towards any limits on your plan, and the date your allowance resets. You can access usage information in the following IDEs. diff --git a/content/copilot/how-tos/manage-your-account/get-started-with-a-copilot-plan.md b/content/copilot/how-tos/manage-your-account/get-started-with-a-copilot-plan.md index 72017e789d83..0dee1488eb47 100644 --- a/content/copilot/how-tos/manage-your-account/get-started-with-a-copilot-plan.md +++ b/content/copilot/how-tos/manage-your-account/get-started-with-a-copilot-plan.md @@ -82,6 +82,9 @@ If you’re already using {% data variables.copilot.copilot_free_short %} and re ## Subscribing to {% data variables.copilot.copilot_pro_short %} or {% data variables.copilot.copilot_pro_plus_short %} +> [!IMPORTANT] +> {% data reusables.copilot.plans.individual-plans-paused %}. However, existing {% data variables.product.prodname_copilot_short %} plans can still be upgraded. + You can subscribe to {% data variables.copilot.copilot_pro_short %} or {% data variables.copilot.copilot_pro_plus_short %} at any time to unlock advanced AI features, higher usage limits, and access to additional models. > [!TIP] Not sure which plan to choose? For a side-by-side comparison, see [AUTOTITLE](/copilot/managing-copilot/managing-copilot-as-an-individual-subscriber/getting-started-with-copilot-on-your-personal-account/about-individual-copilot-plans-and-benefits). diff --git a/content/copilot/how-tos/manage-your-account/view-and-change-your-copilot-plan.md b/content/copilot/how-tos/manage-your-account/view-and-change-your-copilot-plan.md index a2cfe83aed00..9133c57bbd3c 100644 --- a/content/copilot/how-tos/manage-your-account/view-and-change-your-copilot-plan.md +++ b/content/copilot/how-tos/manage-your-account/view-and-change-your-copilot-plan.md @@ -25,6 +25,9 @@ category: - Configure Copilot --- +> [!IMPORTANT] +> **Starting April 20, 2026**, new sign-ups for {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, and student plans are temporarily paused. However, existing {% data variables.product.prodname_copilot_short %} plans can still be upgraded, downgraded, or canceled. + You can view your current plan details in your {% data variables.product.github %} account settings. From there, you can change or cancel your plan. If you have access to {% data variables.product.prodname_copilot %} through an organization{% ifversion ghec %} or enterprise{% endif %}, you will not be able to modify your plan. diff --git a/content/copilot/how-tos/use-copilot-agents/cloud-agent/customize-the-agent-environment.md b/content/copilot/how-tos/use-copilot-agents/cloud-agent/customize-the-agent-environment.md index 32516b39d1ef..345cfa573aa7 100644 --- a/content/copilot/how-tos/use-copilot-agents/cloud-agent/customize-the-agent-environment.md +++ b/content/copilot/how-tos/use-copilot-agents/cloud-agent/customize-the-agent-environment.md @@ -13,7 +13,7 @@ redirect_from: - /copilot/how-tos/agents/copilot-coding-agent/customize-the-agent-environment - /copilot/how-tos/agents/coding-agent/customize-the-agent-environment contentType: how-tos -category: +category: - Configure Copilot --- @@ -110,28 +110,7 @@ In its ephemeral development environment, {% data variables.product.prodname_cop You can use a Copilot setup steps file to deterministically install tools or dependencies before {% data variables.product.prodname_copilot_short %} starts work. To do this, add `steps` to the `copilot-setup-steps` job: -```yaml -# ... - -jobs: - copilot-setup-steps: - # ... - - # You can define any steps you want, and they will run before the agent starts. - # If you do not check out your code, Copilot will do this for you. - steps: - - name: Checkout code - uses: {% data reusables.actions.action-checkout %} - - - name: Set up Node.js - uses: {% data reusables.actions.action-setup-node %} - with: - node-version: "20" - cache: "npm" - - - name: Install JavaScript dependencies - run: npm ci -``` +{% data reusables.copilot.cloud-agent.install-dependencies %} ## Upgrading to larger {% data variables.product.prodname_dotcom %}-hosted {% data variables.product.prodname_actions %} runners @@ -158,14 +137,14 @@ By default, {% data variables.product.prodname_copilot_short %} works in a stand ## Using self-hosted {% data variables.product.prodname_actions %} runners -You can run {% data variables.copilot.copilot_cloud_agent %} on self-hosted runners. You may want to do this to match how you run CI/CD workflows on {% data variables.product.prodname_actions %}, or to give {% data variables.product.prodname_copilot_short %} access to internal resources on your network. +You can run {% data variables.copilot.copilot_cloud_agent %} on self-hosted runners. You may want to do this to match how you run CI/CD workflows on {% data variables.product.prodname_actions %}, or to give {% data variables.product.prodname_copilot_short %} access to internal resources on your network. We recommend that you only use {% data variables.copilot.copilot_cloud_agent %} with ephemeral, single-use runners that are not reused for multiple jobs. Most customers set this up using ARC (Actions Runner Controller) or the {% data variables.product.prodname_actions %} Runner Scale Set Client. For more information, see [AUTOTITLE](/actions/reference/runners/self-hosted-runners#supported-autoscaling-solutions). > [!NOTE] > {% data variables.copilot.copilot_cloud_agent %} is only compatible with Ubuntu x64 and Windows 64-bit runners. Runners with macOS or other operating systems are not supported. -1. Configure network security controls for your {% data variables.product.prodname_actions %} runners to ensure that {% data variables.copilot.copilot_cloud_agent %} does not have open access to your network or the public internet. +1. Configure network security controls for your {% data variables.product.prodname_actions %} runners to ensure that {% data variables.copilot.copilot_cloud_agent %} does not have open access to your network or the public internet. You must configure your firewall to allow connections to the [standard hosts required for {% data variables.product.prodname_actions %} self-hosted runners](/actions/reference/runners/self-hosted-runners#accessible-domains-by-function), plus the following hosts: @@ -193,7 +172,7 @@ We recommend that you only use {% data variables.copilot.copilot_cloud_agent %} ## Switching {% data variables.product.prodname_copilot_short %} to a Windows development environment -By default, {% data variables.product.prodname_copilot_short %} uses an Ubuntu Linux-based development environment. +By default, {% data variables.product.prodname_copilot_short %} uses an Ubuntu Linux-based development environment. You may want to use a Windows development environment if you're building software for Windows or your repository uses a Windows-based toolchain so {% data variables.product.prodname_copilot_short %} can build your project, run tests and validate its work. diff --git a/content/copilot/how-tos/use-copilot-agents/cloud-agent/extend-cloud-agent-with-mcp.md b/content/copilot/how-tos/use-copilot-agents/cloud-agent/extend-cloud-agent-with-mcp.md index 2266a05e7389..74dd25e29fca 100644 --- a/content/copilot/how-tos/use-copilot-agents/cloud-agent/extend-cloud-agent-with-mcp.md +++ b/content/copilot/how-tos/use-copilot-agents/cloud-agent/extend-cloud-agent-with-mcp.md @@ -129,7 +129,6 @@ The [Sentry MCP server](https://github.com/getsentry/sentry-mcp) gives {% data v // We can use the $SENTRY_HOST environment variable which is passed to // the server because of the `env` value below. "args": ["@sentry/mcp-server@latest", "--host=$SENTRY_HOST"], - "tools": ["get_issue_details", "get_issue_summary"], "env": { // We can specify an environment variable value as a string... "SENTRY_HOST": "https://contoso.sentry.io", diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md index ed9d8835654c..ddfefd63b928 100644 --- a/content/copilot/reference/ai-models/model-hosting.md +++ b/content/copilot/reference/ai-models/model-hosting.md @@ -25,6 +25,7 @@ Used for: * {% data variables.copilot.copilot_gpt_53_codex %} * {% data variables.copilot.copilot_gpt_54 %} * {% data variables.copilot.copilot_gpt_54_mini %} +* {% data variables.copilot.copilot_gpt_54_nano %} These models are hosted by OpenAI and {% data variables.product.github %}'s Azure infrastructure. diff --git a/content/copilot/reference/copilot-billing/license-changes.md b/content/copilot/reference/copilot-billing/license-changes.md index 310aea2fde05..24f7cfcc6eee 100644 --- a/content/copilot/reference/copilot-billing/license-changes.md +++ b/content/copilot/reference/copilot-billing/license-changes.md @@ -10,6 +10,9 @@ allowTitleToDifferFromFilename: true contentType: reference --- +> [!IMPORTANT] +> **Starting April 20, 2026**, new sign-ups for {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, and student plans are temporarily paused. However, existing {% data variables.product.prodname_copilot_short %} plans can still be upgraded, downgraded, or canceled. If you hit unexpected limits as a result of these changes, you can cancel your Pro or Pro+ subscription and you will not be charged for April usage. Please reach out to [GitHub support](https://support.github.com/) between April 20 and May 20, 2026, for a refund. + {% data variables.product.prodname_copilot_short %} follows the same billing rules as other license-based products on {% data variables.product.company_short %}. For the general concepts, see: diff --git a/content/copilot/reference/copilot-cli-reference/cli-command-reference.md b/content/copilot/reference/copilot-cli-reference/cli-command-reference.md index b5cf739dba0b..e5d7181108e3 100644 --- a/content/copilot/reference/copilot-cli-reference/cli-command-reference.md +++ b/content/copilot/reference/copilot-cli-reference/cli-command-reference.md @@ -18,27 +18,63 @@ docsTeamMetrics: | Command | Purpose | |------------------------|----------------------------------------------------| -| `copilot` | Launch the interactive user interface. | -| `copilot help [topic]` | Display help information. Help topics include: `config`, `commands`, `environment`, `logging`, `permissions`, and `providers`. | +| `copilot` | Launch the interactive user interface. | +| `copilot help [TOPIC]` | Display help information. Help topics include: `config`, `commands`, `environment`, `logging`, `monitoring`, `permissions`, and `providers`. | | `copilot init` | Initialize {% data variables.product.prodname_copilot_short %} custom instructions for this repository. | -| `copilot update` | Download and install the latest version. | -| `copilot version` | Display version information and check for updates. | | `copilot login` | Authenticate with {% data variables.product.prodname_copilot_short %} via the OAuth device flow. Accepts `--host HOST` to specify the {% data variables.product.github %} host URL (default: `https://github.com`). | -| `copilot logout` | Sign out of {% data variables.product.github %} and remove stored credentials. | -| `copilot plugin` | Manage plugins and plugin marketplaces. | +| `copilot login` [OPTION] | Authenticate with {% data variables.product.prodname_copilot_short %} via the OAuth device flow. See [`copilot login` options](#copilot-login-options). | | `copilot mcp` | Manage MCP server configurations from the command line. | +| `copilot plugin` | Manage plugins and plugin marketplaces. | +| `copilot update` | Download and install the latest version. | +| `copilot version` | Display version information and check for updates. | + +### `copilot login` options + +| Option | Purpose | +|-------------------------|-----------------------------------------------------------------------------------------------| +| `--host HOST` | {% data variables.product.github %} host URL (default: `https://github.com`). Use this to authenticate with a {% data variables.product.prodname_ghe_cloud %} instance that uses data residency (for example, `https://example.ghe.com`). | +| `--config-dir PATH` | Set the configuration directory (default: `~/.copilot`). | + +The default authentication mode is a web-based browser flow. After completion, an authentication token is stored securely in the system credential store. If a credential store is not found, the token is stored in a plain text config file under `~/.copilot/`. + +Alternatively, {% data variables.copilot.copilot_cli_short %} will use an authentication token found in environment variables. The following are checked in order of precedence: `COPILOT_GITHUB_TOKEN`, `GH_TOKEN`, `GITHUB_TOKEN`. This method is most suitable for headless use such as automation. + +Supported token types include {% data variables.product.pat_v2_plural %} (v2 PATs) with the "Copilot Requests" permission, OAuth tokens from the {% data variables.product.prodname_copilot_short %} CLI app, and OAuth tokens from the {% data variables.product.prodname_cli %} (`gh`) app. Classic {% data variables.product.pat_generic_plural %} (`ghp_`) are not supported. + +**Examples:** + +```shell +# Authenticate with github.com +copilot login + +# Authenticate with GitHub Enterprise Cloud (data residency) +copilot login --host https://example.ghe.com + +# Use a fine-grained PAT via environment variable +COPILOT_GITHUB_TOKEN=github_pat_... copilot +``` ## Global shortcuts in the interactive interface | Shortcut | Purpose | |-------------------------------------|---------------------------------------| | `@ FILENAME` | Include file contents in the context. | -| Ctrl+X then `/` | After you have started typing a prompt, this allows you to run a slash command—for example, if you want to change the model without having to retype your prompt. | -| Esc | Cancel the current operation. | +| `# NUMBER` | Include a {% data variables.product.github %} issue or pull request in the context. | | `! COMMAND` | Execute a command in your local shell, bypassing {% data variables.product.prodname_copilot_short %}. | +| `?` | Open quick help (on an empty prompt). | +| Esc | Cancel the current operation. | | Ctrl+C | Cancel operation / clear input. Press twice to exit. | | Ctrl+D | Shutdown. | +| Ctrl+G | Edit the prompt in an external editor (`$EDITOR`). | | Ctrl+L | Clear the screen. | +| Ctrl+Enter or Ctrl+Q | Queue a message to send while the agent is busy. | +| Ctrl+R | Reverse search through command history. | +| Ctrl+V | Paste from clipboard as an attachment. | +| Ctrl+X then `/` | After you have started typing a prompt, this allows you to run a slash command—for example, if you want to change the model without having to retype your prompt. | +| Ctrl+X then `e` | Edit the prompt in an external editor (`$EDITOR`). | +| Ctrl+X then `o` | Open the most recent link from the timeline. | +| Ctrl+Z | Suspend the process to the background (Unix). | +| Shift+Enter or Option+Enter (Mac) / Alt+Enter (Windows/Linux) | Insert a newline in the input. | | Shift+Tab | Cycle between standard, plan, and autopilot mode. | ## Timeline shortcuts in the interactive interface @@ -48,6 +84,7 @@ docsTeamMetrics: | Ctrl+O | While there is nothing in the prompt input, this expands recent items in {% data variables.product.prodname_copilot_short %}'s response timeline to show more details. | | Ctrl+E | While there is nothing in the prompt input, this expands all items in {% data variables.product.prodname_copilot_short %}'s response timeline. | | Ctrl+T | Expand/collapse display of reasoning in responses. | +| Page Up/Page Down | Scroll the timeline up or down by one page. | ## Navigation shortcuts in the interactive interface @@ -57,50 +94,48 @@ docsTeamMetrics: | Ctrl+B | Move to the previous character. | | Ctrl+E | Move to end of the line (when typing). | | Ctrl+F | Move to the next character. | -| Ctrl+G | Edit the prompt in an external editor. | | Ctrl+H | Delete the previous character. | | Ctrl+K | Delete from cursor to end of the line. If the cursor is at the end of the line, delete the line break. | | Ctrl+U | Delete from cursor to beginning of the line. | | Ctrl+W | Delete the previous word. | -| Home | Move to the start of the current line. | -| End | Move to the end of the current line. | -| Ctrl+Home | Move to the start of the text. | -| Ctrl+End | Move to the end of the text. | -| Meta+/ | Move the cursor by a word. | +| Home | Move to the start of the text. | +| End | Move to the end of the text. | +| Alt+/ (Windows/Linux)
Option+/ (Mac) | Move the cursor by a word. | | / | Navigate the command history. | - ## Slash commands in the interactive interface | Command | Purpose | |-----------------------------------------------------|---------| | `/add-dir PATH` | Add a directory to the allowed list for file access. | | `/agent` | Browse and select from available agents (if any). See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/about-custom-agents). | -| `/allow-all`, `/yolo` | Enable all permissions (tools, paths, and URLs). | -| `/changelog [SUMMARIZE] [VERSION]` | Display the CLI changelog with an optional AI-generated summary. | -| `/clear [PROMPT]`, `/new [PROMPT]` | Start a new conversation. | +| `/ask QUESTION` | Ask a quick side question without adding to the conversation history. {% data reusables.copilot.experimental %} | +| `/allow-all [on\|off\|show]`, `/yolo [on\|off\|show]` | Enable all permissions (tools, paths, and URLs). | +| `/changelog [summarize] [VERSION\|last N\|since VERSION]`, `/release-notes [summarize] [VERSION\|last N\|since VERSION]` | Display the CLI changelog. Optionally specify a version, a count of recent releases, or a starting version. Add the keyword `summarize` for an AI-generated summary. | +| `/chronicle ` | Session history tools and insights. {% data reusables.copilot.experimental %} | +| `/clear [PROMPT]`, `/new [PROMPT]`, `/reset [PROMPT]` | Start a new conversation. | | `/compact` | Summarize the conversation history to reduce context window usage. See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/context-management#compaction). | | `/context` | Show the context window token usage and visualization. See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/context-management#checking-your-context-usage). | | `/copy` | Copy the last response to the clipboard. | | `/cwd`, `/cd [PATH]` | Change the working directory or display the current directory. | | `/delegate [PROMPT]` | Delegate changes to a remote repository with an AI-generated pull request. See [AUTOTITLE](/copilot/how-tos/copilot-cli/use-copilot-cli-agents/delegate-tasks-to-cca). | | `/diff` | Review the changes made in the current directory. | +| `/env` | Show loaded environment details (instructions, MCP servers, skills, agents, plugins, LSPs, extensions). | | `/exit`, `/quit` | Exit the CLI. | | `/experimental [on\|off\|show]` | Toggle, set, or show experimental features. | -| `/feedback` | Provide feedback about the CLI. | +| `/feedback`, `/bug` | Provide feedback about the CLI. | | `/fleet [PROMPT]` | Enable parallel subagent execution of parts of a task. See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/fleet). | | `/help` | Show the help for interactive commands. | | `/ide` | Connect to an IDE workspace. See [AUTOTITLE](/copilot/how-tos/copilot-cli/connecting-vs-code#managing-the-connection-with-the-ide-slash-command). | | `/init` | Initialize {% data variables.product.prodname_copilot_short %} custom instructions and agentic features for this repository. See [Project initialization for {% data variables.product.prodname_copilot_short %}](#project-initialization-for-copilot). | | `/instructions` | View and toggle custom instruction files. | -| `/keep-alive [on\|busy\|NUMBERm\|NUMBERh]` | Prevent the machine from going to sleep: while a CLI session is active, while the agent is busy, or for a defined length of time. | +| `/keep-alive [on\|busy\|NUMBERm\|NUMBERh]` | Prevent the machine from going to sleep: while a CLI session is active, while the agent is busy, or for a defined length of time. {% data reusables.copilot.experimental %} | | `/list-dirs` | Display all of the directories for which file access has been allowed. | | `/login` | Log in to {% data variables.product.prodname_copilot_short %}. | | `/logout` | Log out of {% data variables.product.prodname_copilot_short %}. | | `/lsp [show\|test\|reload\|help] [SERVER-NAME]` | Manage the language server configuration. | | `/mcp [show\|add\|edit\|delete\|disable\|enable\|auth\|reload] [SERVER-NAME]` | Manage the MCP server configuration. See [AUTOTITLE](/copilot/how-tos/copilot-cli/customize-copilot/add-mcp-servers#managing-mcp-servers). | | `/model`, `/models [MODEL]` | Select the AI model you want to use. | -| `/on-air`, `/streamer-mode` | Toggle streamer mode (hides preview model names). | | `/plan [PROMPT]` | Create an implementation plan before coding. | | `/plugin [marketplace\|install\|uninstall\|update\|list] [ARGS...]` | Manage plugins and plugin marketplaces. See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/about-cli-plugins). | | `/pr [view\|create\|fix\|auto]` | Operate on pull requests for the current branch. | @@ -109,16 +144,17 @@ docsTeamMetrics: | `/research TOPIC` | Run a deep research investigation using {% data variables.product.github %} search and web sources. See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/research). | | `/reset-allowed-tools` | Reset the list of allowed tools. | | `/restart` | Restart the CLI, preserving the current session. | -| `/resume [SESSION-ID]` | Switch to a different session by choosing from a list (optionally specify a session ID). | +| `/resume [SESSION-ID]`, `/continue [SESSION-ID]` | Switch to a different session by choosing from a list (optionally specify a session ID). | | `/review [PROMPT]` | Run the code review agent to analyze changes. See [AUTOTITLE](/copilot/how-tos/copilot-cli/use-copilot-cli-agents/agentic-code-review). | -| `/session [checkpoints [n]\|files\|plan\|rename NAME]` | Show session information and a workspace summary. Use the subcommands for details. | -| `/share [file\|gist] [session\|research] [PATH]` | Share the session to a Markdown file or {% data variables.product.github %} gist. | +| `/session [checkpoints [n]\|files\|plan\|rename NAME]`, `/sessions [checkpoints [n]\|files\|plan\|rename NAME]` | Show session information and a workspace summary. Use the subcommands for details. | +| `/share [file\|html\|gist] [session\|research] [PATH]`, `/export [file\|html\|gist] [session\|research] [PATH]` | Share the session to a Markdown file, interactive HTML file, or {% data variables.product.github %} gist. | | `/skills [list\|info\|add\|remove\|reload] [ARGS...]` | Manage skills for enhanced capabilities. See [AUTOTITLE](/copilot/how-tos/copilot-cli/customize-copilot/create-skills). | +| `/statusline`, `/footer` | Configure which items appear in the status line. | | `/tasks` | View and manage background tasks (subagents and shell sessions). | | `/terminal-setup` | Configure the terminal for multiline input support (Shift+Enter and Ctrl+Enter). | -| `/theme [show\|set\|list] [auto\|THEME-ID]` | View or configure the terminal theme. | +| `/theme [default\|dim\|high-contrast\|colorblind]` | View or set the color mode. | | `/undo`, `/rewind` | Rewind the last turn and revert file changes. | -| `/update` | Update the CLI to the latest version. | +| `/update`, `/upgrade` | Update the CLI to the latest version. | | `/usage` | Display session usage metrics and statistics. | | `/user [show\|list\|switch]` | Manage the current {% data variables.product.github %} user. | | `/version` | Display version information and check for updates. | @@ -129,7 +165,6 @@ For a complete list of available slash commands enter `/help` in the CLI's inter | Option | Purpose | |------------------------------------|------------------------------------------| -| `--acp` | Start the Agent Client Protocol server. | | `--add-dir=PATH` | Add a directory to the allowed list for file access (can be used multiple times). | | `--add-github-mcp-tool=TOOL` | Add a tool to enable for the {% data variables.product.github %} MCP server, instead of the default CLI subset (can be used multiple times). Use `*` for all tools. | | `--add-github-mcp-toolset=TOOLSET` | Add a toolset to enable for the {% data variables.product.github %} MCP server, instead of the default CLI subset (can be used multiple times). Use `all` for all toolsets. | @@ -143,15 +178,15 @@ For a complete list of available slash commands enter `/help` in the CLI's inter | `--allow-url=URL ...` | Allow access to specific URLs or domains. For multiple URLs, use a quoted, comma-separated list. | | `--autopilot` | Enable autopilot continuation in prompt mode. See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/autopilot). | | `--available-tools=TOOL ...` | Only these tools will be available to the model. For multiple tools, use a quoted, comma-separated list. See [AUTOTITLE](/copilot/how-tos/copilot-cli/allowing-tools). | -| `--banner` | Show the startup banner. | +| `--banner`, `--no-banner` | Show or hide the startup banner. | | `--bash-env` | Enable `BASH_ENV` support for bash shells. | | `--config-dir=PATH` | Set the configuration directory (default: `~/.copilot`). | +| `--connect[=SESSION-ID]` | Connect directly to a remote session (optionally specify a session ID or task ID). Conflicts with `--resume` and `--continue`. | | `--continue` | Resume the most recent session. | | `--deny-tool=TOOL ...` | Tools the CLI does not have permission to use. Will not prompt for permission. For multiple tools, use a quoted, comma-separated list. | | `--deny-url=URL ...` | Deny access to specific URLs or domains, takes precedence over `--allow-url`. For multiple URLs, use a quoted, comma-separated list. | | `--disable-builtin-mcps` | Disable all built-in MCP servers (currently: `github-mcp-server`). | | `--disable-mcp-server=SERVER-NAME` | Disable a specific MCP server (can be used multiple times). | -| `--disable-parallel-tools-execution` | Disable parallel execution of tools (LLM can still make parallel tool calls, but they will be executed sequentially). | | `--disallow-temp-dir` | Prevent automatic access to the system temporary directory. | | `--effort=LEVEL`, `--reasoning-effort=LEVEL` | Set the reasoning effort level (`low`, `medium`, `high`). | | `--enable-all-github-mcp-tools` | Enable all {% data variables.product.github %} MCP server tools, instead of the default CLI subset. Overrides the `--add-github-mcp-toolset` and `--add-github-mcp-tool` options. | @@ -164,7 +199,7 @@ For a complete list of available slash commands enter `/help` in the CLI's inter | `--log-level=LEVEL` | Set the log level (choices: `none`, `error`, `warning`, `info`, `debug`, `all`, `default`). | | `--max-autopilot-continues=COUNT` | Maximum number of continuation messages in autopilot mode (default: unlimited). See [AUTOTITLE](/copilot/concepts/agents/copilot-cli/autopilot). | | `--mode=MODE` | Set the initial agent mode (choices: `interactive`, `plan`, `autopilot`). Cannot be combined with `--autopilot` or `--plan`. | -| `--model=MODEL` | Set the AI model you want to use. | +| `--model=MODEL` | Set the AI model you want to use. Pass `auto` to let {% data variables.product.prodname_copilot_short %} pick the best available model automatically. | | `--mouse[=VALUE]` | Enable mouse support in alt screen mode. VALUE can be `on` (default) or `off`. When enabled, the CLI captures mouse events in alt screen mode—scroll wheel, clicks, etc. When disabled, the terminal's native mouse behavior is preserved. Once set the setting is persisted by being written to your configuration file.| | `--no-ask-user` | Disable the `ask_user` tool (the agent works autonomously without asking questions). | | `--no-auto-update` | Disable downloading CLI updates automatically. | @@ -180,7 +215,7 @@ For a complete list of available slash commands enter `/help` in the CLI's inter | `--plain-diff` | Disable rich diff rendering (syntax highlighting via the diff tool specified by your git config). | | `--plugin-dir=DIRECTORY` | Load a plugin from a local directory (can be used multiple times). | | `--remote` | Enable remote access to this session from {% data variables.product.prodname_dotcom_the_website %} and {% data variables.product.prodname_mobile %}. See [AUTOTITLE](/copilot/how-tos/copilot-cli/steer-remotely). | -| `--resume=SESSION-ID` | Resume a previous interactive session by choosing from a list (optionally specify a session ID). | +| `--resume=SESSION-ID` | Resume a previous interactive session by choosing from a list (optionally specify a session ID or unique prefix of 7+ hex characters). | | `-s`, `--silent` | Output only the agent response (without usage statistics), useful for scripting with `-p`. | | `--screen-reader` | Enable screen reader optimizations. | | `--secret-env-vars=VAR ...` | Redact an environment variable from shell and MCP server environments (can be used multiple times). For multiple variables, use a quoted, comma-separated list. The values in the `GITHUB_TOKEN` and `COPILOT_GITHUB_TOKEN` environment variables are redacted from output by default. | @@ -193,11 +228,11 @@ For a complete list of available slash commands enter `/help` in the CLI's inter For a complete list of commands and options, run `copilot help`. > [!NOTE] -> The `--remote` and `--no-remote` options require the remote sessions feature to be available on your account. +> The `--remote`, `--no-remote`, and `--connect` options require the remote sessions feature to be available on your account. ## Tool availability values -The `--available-tools` and `--excluded-tools` options support the following values for specifying tools: +The `--available-tools` and `--excluded-tools` options support these values: ### Shell tools @@ -222,7 +257,7 @@ The `--available-tools` and `--excluded-tools` options support the following val | Tool name | Description | |---|---| -| `task` | Run sub-agents | +| `task` | Run subagents | | `read_agent` | Check background agent status | | `list_agents` | List available agents | @@ -235,15 +270,6 @@ The `--available-tools` and `--excluded-tools` options support the following val | `web_fetch` | Fetch and parse web content | | `skill` | Invoke custom skills | | `ask_user` | Ask the user a question | -| `report_intent` | Report what the agent plans to do | -| `show_file` | Display a file prominently | -| `fetch_copilot_cli_documentation` | Look up CLI documentation | -| `update_todo` | Update task checklist | -| `store_memory` | Persist facts across sessions | -| `task_complete` | Signal task is done (autopilot only) | -| `exit_plan_mode` | Exit plan mode | -| `sql` | Query session data (experimental) | -| `lsp` | Language server refactoring (experimental) | ## Tool permission patterns @@ -293,7 +319,6 @@ copilot --allow-tool='MyMCP' | `USE_BUILTIN_RIPGREP` | Set to `false` to use the system ripgrep instead of the bundled version. | | `PLAIN_DIFF` | Set to `true` to disable rich diff rendering. | | `COLORFGBG` | Fallback for dark/light terminal background detection. | -| `COPILOT_CLI_ENABLED_FEATURE_FLAGS` | Comma-separated list of feature flags to enable (for example, `"SOME_FEATURE,SOME_OTHER_FEATURE"`). | ## Configuration file settings @@ -309,7 +334,7 @@ Settings cascade from user to repository to local, with more specific scopes ove | Key | Type | Default | Description | |-----|------|---------|-------------| -| `allowed_urls` | `string[]` | `[]` | URLs or domains allowed without prompting. | +| `allowedUrls` | `string[]` | `[]` | URLs or domains allowed without prompting. | | `autoUpdate` | `boolean` | `true` | Automatically download CLI updates. | | `banner` | `"always"` \| `"once"` \| `"never"` | `"once"` | Animated banner display frequency. | | `bashEnv` | `boolean` | `false` | Enable `BASH_ENV` support for bash shells. | @@ -317,36 +342,41 @@ Settings cascade from user to repository to local, with more specific scopes ove | `compactPaste` | `boolean` | `true` | Collapse large pastes into compact tokens. | | `custom_agents.default_local_only` | `boolean` | `false` | Only use local custom agents. | | `denied_urls` | `string[]` | `[]` | URLs or domains blocked (takes precedence over `allowed_urls`). | -| `enabledFeatureFlags` | `object` | — | Enable or disable individual feature flags. Keys are flag names; values are `true` (enable) or `false` (explicitly disable). Takes precedence over the legacy `feature_flags.enabled` array format. See [Feature flag reference](#feature-flag-reference). | | `experimental` | `boolean` | `false` | Enable experimental features. | | `includeCoAuthoredBy` | `boolean` | `true` | Add a `Co-authored-by` trailer to git commits made by the agent. | | `companyAnnouncements` | `string[]` | `[]` | Custom messages shown randomly on startup. | | `logLevel` | `"none"` \| `"error"` \| `"warning"` \| `"info"` \| `"debug"` \| `"all"` \| `"default"` | `"default"` | Logging verbosity. | -| `model` | `string` | varies | AI model to use (see the `/model` command). | +| `model` | `string` | varies | AI model to use (see the `/model` command). Set to `"auto"` to let {% data variables.product.prodname_copilot_short %} pick the best available model automatically. | | `powershellFlags` | `string[]` | `["-NoProfile", "-NoLogo"]` | Flags passed to PowerShell (`pwsh`) on startup. Windows only. | | `effortLevel` | `string` | `"medium"` | Reasoning effort level for extended thinking (e.g., `"low"`, `"medium"`, `"high"`, `"xhigh"`). Higher levels use more compute. | +| `mergeStrategy` | `"rebase"` \| `"merge"` | — | Conflict resolution strategy for `/pr fix conflicts`. When set to `"rebase"`, conflicts are resolved by rebasing onto the base branch. When set to `"merge"`, the base branch is merged into the feature branch. If not configured, a picker dialog is shown. | | `renderMarkdown` | `boolean` | `true` | Render Markdown in terminal output. | | `screenReader` | `boolean` | `false` | Enable screen reader optimizations. | | `stream` | `boolean` | `true` | Enable streaming responses. | | `storeTokenPlaintext` | `boolean` | `false` | Store authentication tokens in plain text in the configuration file when no system keychain is available. | | `streamerMode` | `boolean` | `false` | Hide preview model names and quota details (useful when demonstrating {% data variables.copilot.copilot_cli_short %}). | | `theme` | `"auto"` \| `"dark"` \| `"light"` | `"auto"` | Terminal color theme. | -| `trusted_folders` | `string[]` | `[]` | Folders with pre-granted file access. | | `mouse` | `boolean` | `true` | Enable mouse support in alt screen mode. | | `respectGitignore` | `boolean` | `true` | Exclude gitignored files from the `@` file picker. | | `disableAllHooks` | `boolean` | `false` | Disable all hooks. | | `hooks` | `object` | — | Inline user-level hook definitions. | +| `copyOnSelect` | `boolean` | `true` (macOS), `false` (other) | Automatically copy mouse-selected text to the system clipboard in alt screen mode. | +| `statusLine` | `object` | — | Custom status line display. `type`: must be `"command"`. `command`: path to a script that receives session JSON on stdin and prints status content to stdout. `padding`: optional left-padding spaces. | +| `suppress_init_folders` | `string[]` | `[]` | Folders where the `/init` suggestion has been dismissed. Managed automatically by `/init suppress`. | | `updateTerminalTitle` | `boolean` | `true` | Show the current intent in the terminal title. | ### Repository settings (`.github/copilot/settings.json`) -Repository settings apply to everyone who works in the repository. Only a subset of settings is supported at the repository level. Unsupported keys are ignored. +Repository settings apply to everyone who works in the repository. Only the keys listed in the following table are supported at the repository level. Any other keys—including keys that are valid in the user configuration file—are silently ignored. | Key | Type | Merge behavior | Description | |-----|------|---------------|-------------| | `companyAnnouncements` | `string[]` | Replaced—repository takes precedence | Messages shown randomly on startup. | +| `disableAllHooks` | `boolean` | Repository takes precedence | Disable all hooks. | | `enabledPlugins` | `Record` | Merged—repository overrides user for same key | Declarative plugin auto-install. | | `extraKnownMarketplaces` | `Record` | Merged—repository overrides user for same key | Plugin marketplaces available in this repository. | +| `hooks` | `object` | Concatenated—repository hooks run after user hooks | Hook definitions scoped to this repository. See [Hooks reference](#hooks-reference). | +| `mergeStrategy` | `"rebase"` \| `"merge"` | Repository takes precedence | Conflict resolution strategy for `/pr fix conflicts`. | ### Local settings (`.github/copilot/settings.local.json`) @@ -830,7 +860,7 @@ The `notification` hook fires asynchronously when the CLI emits a system notific |------|---------------| | `shell_completed` | A background (async) shell command finishes | | `shell_detached_completed` | A detached shell session completes | -| `agent_completed` | A background sub-agent finishes (completed or failed) | +| `agent_completed` | A background subagent finishes (completed or failed) | | `agent_idle` | A background agent finishes a turn and enters idle state (waiting for `write_agent`) | | `permission_prompt` | The agent requests permission to execute a tool | | `elicitation_dialog` | The agent requests additional information from the user | @@ -970,6 +1000,27 @@ MCP servers are loaded from multiple sources, each with a different trust level. All MCP tool invocations require explicit permission. This applies even to read-only operations on external services. +### Enterprise MCP allowlist + +{% data variables.product.prodname_enterprise %} organizations can enforce an allowlist of permitted MCP servers. When active, the CLI evaluates each non-default server against the enterprise policy before connecting. + +When a {% data variables.product.prodname_enterprise %} registry policy is detected (or the `MCP_ENTERPRISE_ALLOWLIST` experimental feature flag is enabled), the CLI: + +1. Computes a fingerprint for each configured non-default server based on its command, arguments, and remote URL. +1. Sends the fingerprints to the enterprise allowlist evaluate endpoint. +1. Allows only servers whose fingerprints are approved; all others are blocked with a message naming the enterprise. + +This check is fail-closed: if the evaluate endpoint is unreachable or returns an error, non-default servers are blocked until the policy can be verified. + +When a server is blocked by an enterprise allowlist, the CLI displays: + +```text +MCP server "SERVER-NAME" was blocked by your enterprise "ENTERPRISE-NAME". +Contact your enterprise administrator to add this server to the allowlist. +``` + +Built-in default servers are always exempt from allowlist enforcement. + ### Migrating from `.vscode/mcp.json` If your project uses `.vscode/mcp.json` (VS Code's MCP configuration format), migrate to `.mcp.json` for {% data variables.copilot.copilot_cli %}. The migration remaps the `servers` key to `mcpServers`. @@ -1034,7 +1085,7 @@ Custom agents are specialized AI agents defined in Markdown files. The filename | Agent | Default model | Description | |-------|--------------|-------------| | `code-review` | claude-sonnet-4.5 | High signal-to-noise code review. Analyzes diffs for bugs, security issues, and logic errors. | -| `rubber-duck` | complementary model | Use a complementary model to provide a constructive critique of proposals, designs, implementations, or tests. Identifies weak points and suggests improvements. Only available in experimental mode. | +| `rubber-duck` | complementary model | Use a complementary model to provide a constructive critique of proposals, designs, implementations, or tests. Identifies weak points and suggests improvements. {% data reusables.copilot.experimental %} | | `explore` | claude-haiku-4.5 | Fast codebase exploration. Searches files, reads code, and answers questions. Returns focused answers under 300 words. Safe to run in parallel. | | `general-purpose` | claude-sonnet-4.5 | Full-capability agent for complex multi-step tasks. Runs in a separate context window. | | `research` | claude-sonnet-4.6 | Deep research agent. Generates a report based on information in your codebase, in relevant repositories, and on the web. | @@ -1047,7 +1098,7 @@ Custom agents are specialized AI agents defined in Markdown files. The filename | `description` | string | Yes | Description shown in the agent list and `task` tool. | | `infer` | boolean | No | Allow auto-delegation by the main agent. Default: `true`. | | `mcp-servers` | object | No | MCP servers to connect. Uses the same schema as `~/.copilot/mcp-config.json`. | -| `model` | string | No | AI model for this agent. When unset, inherits the outer agent's model. | +| `model` | string | No | AI model for this agent. When unset, inherits the outer agent's model. When the session model is set to `Auto` (server-selected), subagents always inherit the resolved session model regardless of this field. | | `name` | string | No | Display name. Defaults to the filename. | | `tools` | string[] | No | Tools available to the agent. Default: `["*"]` (all tools). | @@ -1258,39 +1309,6 @@ When content capture is enabled, the following attributes are populated. | `gen_ai.tool.call.arguments` | Tool input arguments | | `gen_ai.tool.call.result` | Tool output | -## Feature flag reference - -Feature flags enable functionality that is not yet generally available. You can enable or disable individual flags in three ways: - -* **Environment variable**: Set `COPILOT_CLI_ENABLED_FEATURE_FLAGS` to a comma-separated list of flag names (for example, `"SOME_FEATURE,SOME_OTHER_FEATURE"`). -* **Slash command**: Use `/experimental on` in an interactive session to enable all experimental-tier flags. -* **Configuration file**: Add an `enabledFeatureFlags` object to `~/.copilot/config.json`. Set a flag to `true` to enable it or `false` to explicitly disable a flag that would otherwise be enabled by your tier. - -```json -{ - "enabledFeatureFlags": { - "SOME_FEATURE": true, - "SOME_OTHER_FEATURE": false - } -} -``` - -> [!NOTE] -> The legacy `feature_flags.enabled` array format is still supported as a fallback, but `enabledFeatureFlags` takes precedence when both are present. - -| Flag | Tier | Description | -|------|------|-------------| -| `RUBBER_DUCK_AGENT` | experimental | Rubber-duck subagent for adversarial feedback on code and designs | -| `BACKGROUND_SESSIONS` | experimental | Multiple concurrent sessions with background management | -| `MULTI_TURN_AGENTS` | experimental | Multi-turn subagent message passing via `write_agent` | -| `EXTENSIONS` | experimental | Programmatic extensions with custom tools and hooks | -| `QUEUED_COMMANDS` | staff-or-experimental | Queue commands with Ctrl+Enter while the agent runs | -| `PERSISTED_PERMISSIONS` | staff-or-experimental | Persist tool permissions across sessions per location | -| `SESSION_STORE` | staff-or-experimental | SQLite-based session store for cross-session history | -| `COMPUTER_USE` | staff | Built-in computer use MCP server (screen capture and mouse/keyboard control) | -| `copilot-feature-agentic-memory` | on | Persistent memory tools across sessions | -| `COPILOT_SWE_AGENT_BACKGROUND_AGENTS` | on | Background agent task execution | - ## Further reading * [AUTOTITLE](/copilot/how-tos/copilot-cli) diff --git a/content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md b/content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md index ca501b9c5a83..eb1e976572ab 100644 --- a/content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md +++ b/content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md @@ -50,7 +50,7 @@ Common settings include: | Key | Type | Description | |-----|------|-------------| -| `model` | string | AI model to use (e.g., `"gpt-5.2"`, `"claude-sonnet-4.6"`) | +| `model` | string | AI model to use (for example, `"gpt-5.2"`, `"claude-sonnet-4.6"`). Set to `Auto` to let {% data variables.product.prodname_copilot_short %} pick the best available model automatically. | | `effortLevel` | string | Reasoning effort level for models that support it | | `theme` | string | Color theme: `"auto"`, `"dark"`, or `"light"` | | `mouse` | boolean | Enable mouse support in alt screen mode (default: `true`) | @@ -61,9 +61,8 @@ Common settings include: | `stream` | boolean | Stream responses token by token (default: `true`) | | `includeCoAuthoredBy` | boolean | Add Co-authored-by to agent-created commits (default: `true`) | | `respectGitignore` | boolean | Exclude gitignored files from the `@` file picker (default: `true`) | -| `trusted_folders` | string[] | Folders where read/execute permission has been granted | -| `allowed_urls` | string[] | URLs or domains allowed without prompting | -| `denied_urls` | string[] | URLs or domains that are always denied | +| `allowedUrls` | string[] | URLs or domains allowed without prompting | +| `deniedUrls` | string[] | URLs or domains that are always denied | | `logLevel` | string | Log verbosity: `"none"`, `"error"`, `"warning"`, `"info"`, `"debug"`, `"all"`, or `"default"` (default: `"default"`) | | `disableAllHooks` | boolean | Disable all hooks (default: `false`) | | `hooks` | object | Inline user-level hook definitions | diff --git a/content/copilot/reference/review-excluded-files.md b/content/copilot/reference/review-excluded-files.md index 854f260d1b23..be94825f4416 100644 --- a/content/copilot/reference/review-excluded-files.md +++ b/content/copilot/reference/review-excluded-files.md @@ -79,6 +79,9 @@ Files matching these patterns are also excluded: * `**/*.map` * `**/out/**/*` * `**/vendor/**/*` -* `**/bin/**/*` * `**/generated/**/*` * `**/generated-sources/**/*` +* `**/bin/**/*` + + > [!NOTE] + > Rust files matching `**/bin/**/*.rs` _are_ included for review. diff --git a/content/copilot/tutorials/cloud-agent/give-access-to-resources.md b/content/copilot/tutorials/cloud-agent/give-access-to-resources.md new file mode 100644 index 000000000000..a5ecda353f19 --- /dev/null +++ b/content/copilot/tutorials/cloud-agent/give-access-to-resources.md @@ -0,0 +1,104 @@ +--- +title: 'Giving GitHub Copilot cloud agent access to resources in your organization' +shortTitle: 'Give access to resources' +intro: 'Get more out of {% data variables.product.prodname_copilot_short %} by giving it access to approved MCP servers and internal packages.' +versions: + feature: copilot +contentType: tutorials +category: + - Manage Copilot for a team + - Roll Copilot out at scale +--- + +{% data variables.copilot.copilot_cloud_agent %} can connect to MCP servers, use private packages, and access external services, but only if your organization's repositories are configured to allow it. + +Although much of the configuration below is done at the repository level, organization owners have control over which resources are in scope and who can configure access to them. + +## Example scenario + +Your organization uses Sentry to track bugs in your Node app. New exceptions are raised as issues on {% data variables.product.github %}, and your developers want to assign these issues to {% data variables.product.prodname_copilot_short %}. + +You want {% data variables.product.prodname_copilot_short %} to: + +* Connect to the Sentry MCP server so it can access details on your Sentry instance +* Install dependencies, including private packages hosted on {% data variables.product.github %}, to build your app and run tests +* Follow your organization's conventions for error-handling + +## Storing secrets securely + +By default, the scope of {% data variables.product.prodname_copilot_short %}'s authentication token is limited to the repository where it's running. This means that {% data variables.product.prodname_copilot_short %} won't be able to authenticate to external systems or access private, organization-scoped packages. + +Repository administrators should add variables and secrets that {% data variables.product.prodname_copilot_short %} requires to a dedicated `copilot` {% data variables.product.prodname_actions %} environment. {% data variables.product.prodname_copilot_short %} can access this data in its setup and task execution. It won't be able to access variables or secrets outside this environment, such as organization-wide {% data variables.product.prodname_actions %} secrets. + +### Example: Save a secret + +A repository administrator saves an authentication token for the organization's Sentry instance. + +1. Go to the **Environments** section of the repository settings. +1. Create a new environment called `copilot`. +1. Save an access token for your Sentry instance in an environment secret called `COPILOT_MCP_SENTRY_ACCESS_TOKEN`. + +> [!TIP] We don't need to save a token for our private {% data variables.product.prodname_registry %} registry, which we'll access using the standard {% data variables.product.prodname_actions %} `GITHUB_TOKEN`. However, you would want to save an authentication token if you were using an external package registry. + +## Configuring access to MCP servers + +Organization and enterprise owners can set a policy to allow users to configure access to MCP servers. If this policy is enabled, users can configure MCP servers for {% data variables.copilot.copilot_cloud_agent %} in repository settings or in custom agent profiles. For organization-wide consistency, we recommend creating **custom agent profiles** at the organization or enterprise level. + +A session using a custom agent has access to MCP servers configured in **both** the repository settings and the agent profile. However, the more use cases you cover with organization-wide custom agents, the less users will need to configure ad hoc access to MCP servers in repository settings. + +We recommend browsing the [{% data variables.product.github %} MCP Registry](https://github.com/mcp) to find trusted, highly rated options. + +### Example: Create a custom agent + +An organization owner creates a custom agent profile for the Sentry agent. It has access to the Sentry MCP server and custom instructions for the organization's error-handling conventions. + +1. Create a repository called `.github-private` in your organization. Optionally, an enterprise owner can set this repository as the source for all custom agents in the enterprise. +1. In the repository, add an `agent.md` file with a profile like the following. This includes configuration for the MCP server, which references the secret we saved. + + ``` text + --- + name: sentry-error-fixer + description: Proposed fixes for exception issues raised from Sentry + mcp-servers: + sentry: + type: 'local' + command: 'npx' + args: ['@sentry/mcp-server@latest'] + env: + SENTRY_ACCESS_TOKEN: {% raw %}${{ secrets.COPILOT_MCP_SENTRY_ACCESS_TOKEN }}{% endraw %} + --- + + You are an error resolution specialist. When you're assigned an issue created by our Sentry integration, check for error details and stack traces using the MCP server, then propose a fix. + + Make sure you check that your proposed fix works by building the site with `npm run build` and running the test suite in `npm test`. + ``` + +1. When developers assign an issue to {% data variables.product.prodname_copilot_short %}, they can select the custom agent from a dropdown. + +## Installing private packages + +The best way to give {% data variables.product.prodname_copilot_short %} access to a project's dependencies is to install them in a `copilot-setup-steps.yml` workflow file. This file defines how the environment is set up before {% data variables.product.prodname_copilot_short %} starts working. + +To allow the workflow to pull your private, organization-scoped packages, you will update the package settings to make sure that the repository's `GITHUB_TOKEN` has access to the package. This is more secure than using a long-lived {% data variables.product.pat_generic %} with organization permissions. + +### Example: Install Node dependencies + +A developer creates a workflow to install the Node dependencies defined in a repository's `package-lock.json` file. This includes private, organization-scoped packages hosted on {% data variables.product.github %}. + +1. The developer creates a `copilot-setup-steps.yml` file in the repository. +1. They add steps for installing the project's dependencies. For example: + + {% data reusables.copilot.cloud-agent.install-dependencies %} + +1. An organization administrator ensures that the repository has access to the organization's private packages by granting access to the repository in each package's settings. See [AUTOTITLE](/packages/learn-github-packages/configuring-a-packages-access-control-and-visibility#github-actions-access-for-packages-scoped-to-organizations). + +>[!TIP] If you need to access packages that are hosted internally within your corporate network, you may need to run {% data variables.copilot.copilot_cloud_agent %} on self-hosted {% data variables.product.prodname_actions %} runners. + +## Controlling who can configure these settings + +Now you have seen how access to resources is controlled at the repository and organization levels, consider how much scope you want to give users to manage these settings. + +1. **Choose which repositories have access** to {% data variables.copilot.copilot_cloud_agent %}. If you're concerned about a specific repository, you can block it for all users. +1. **Consider who gets admin access** to these repositories. You can control this at the organization level by creating a team with the **All-repository admin** custom role. These users will be able to manage configuration _settings_, such as MCP configuration and `copilot` environments, in every repository. +1. **Use rulesets and CODEOWNERS files** to control edits of configuration _files_, such as `copilot-setup-steps.yml`, which anyone with write access can edit by default. +1. **Review the default firewall**. The firewall doesn't affect connections to MCP servers or setup steps in `copilot-setup-steps.yml`, but it does limit {% data variables.product.prodname_copilot_short %}'s access to the Internet during task execution. See [AUTOTITLE](/copilot/how-tos/use-copilot-agents/cloud-agent/customize-the-agent-firewall). diff --git a/content/copilot/tutorials/cloud-agent/index.md b/content/copilot/tutorials/cloud-agent/index.md index 7b2be160db42..1a564b7b0ba7 100644 --- a/content/copilot/tutorials/cloud-agent/index.md +++ b/content/copilot/tutorials/cloud-agent/index.md @@ -10,6 +10,7 @@ children: - /pilot-cloud-agent - /improve-a-project - /build-guardrails + - /give-access-to-resources contentType: tutorials redirect_from: - /copilot/tutorials/coding-agent diff --git a/content/graphql/overview/changelog/2017.md b/content/graphql/overview/changelog/2017.md new file mode 100644 index 000000000000..051944633796 --- /dev/null +++ b/content/graphql/overview/changelog/2017.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2017" +shortTitle: "2017" +intro: 'GraphQL schema changes from 2017.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2018.md b/content/graphql/overview/changelog/2018.md new file mode 100644 index 000000000000..5721027b0a7b --- /dev/null +++ b/content/graphql/overview/changelog/2018.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2018" +shortTitle: "2018" +intro: 'GraphQL schema changes from 2018.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2019.md b/content/graphql/overview/changelog/2019.md new file mode 100644 index 000000000000..a250cfcef489 --- /dev/null +++ b/content/graphql/overview/changelog/2019.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2019" +shortTitle: "2019" +intro: 'GraphQL schema changes from 2019.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2020.md b/content/graphql/overview/changelog/2020.md new file mode 100644 index 000000000000..3faaadc2f372 --- /dev/null +++ b/content/graphql/overview/changelog/2020.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2020" +shortTitle: "2020" +intro: 'GraphQL schema changes from 2020.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2021.md b/content/graphql/overview/changelog/2021.md new file mode 100644 index 000000000000..af29ea40ab20 --- /dev/null +++ b/content/graphql/overview/changelog/2021.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2021" +shortTitle: "2021" +intro: 'GraphQL schema changes from 2021.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2022.md b/content/graphql/overview/changelog/2022.md new file mode 100644 index 000000000000..dd936ddd9473 --- /dev/null +++ b/content/graphql/overview/changelog/2022.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2022" +shortTitle: "2022" +intro: 'GraphQL schema changes from 2022.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2023.md b/content/graphql/overview/changelog/2023.md new file mode 100644 index 000000000000..6b11d2b2d463 --- /dev/null +++ b/content/graphql/overview/changelog/2023.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2023" +shortTitle: "2023" +intro: 'GraphQL schema changes from 2023.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2024.md b/content/graphql/overview/changelog/2024.md new file mode 100644 index 000000000000..9d56b809ddbc --- /dev/null +++ b/content/graphql/overview/changelog/2024.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2024" +shortTitle: "2024" +intro: 'GraphQL schema changes from 2024.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2025.md b/content/graphql/overview/changelog/2025.md new file mode 100644 index 000000000000..afc659fbc082 --- /dev/null +++ b/content/graphql/overview/changelog/2025.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2025" +shortTitle: "2025" +intro: 'GraphQL schema changes from 2025.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog/2026.md b/content/graphql/overview/changelog/2026.md new file mode 100644 index 000000000000..5f0ba0a8d496 --- /dev/null +++ b/content/graphql/overview/changelog/2026.md @@ -0,0 +1,8 @@ +--- +title: "GraphQL changelog for 2026" +shortTitle: "2026" +intro: 'GraphQL schema changes from 2026.' +versions: + fpt: '*' +autogenerated: graphql +--- diff --git a/content/graphql/overview/changelog.md b/content/graphql/overview/changelog/index.md similarity index 85% rename from content/graphql/overview/changelog.md rename to content/graphql/overview/changelog/index.md index 11f8acc59920..12470b307848 100644 --- a/content/graphql/overview/changelog.md +++ b/content/graphql/overview/changelog/index.md @@ -6,6 +6,17 @@ redirect_from: versions: fpt: '*' autogenerated: graphql +children: + - /2026 + - /2025 + - /2024 + - /2023 + - /2022 + - /2021 + - /2020 + - /2019 + - /2018 + - /2017 category: - Understand API changes and limits --- diff --git a/content/issues/planning-and-tracking-with-projects/managing-your-project/managing-visibility-of-your-projects.md b/content/issues/planning-and-tracking-with-projects/managing-your-project/managing-visibility-of-your-projects.md index 2e3c475bfa3e..73bacc82a493 100644 --- a/content/issues/planning-and-tracking-with-projects/managing-your-project/managing-visibility-of-your-projects.md +++ b/content/issues/planning-and-tracking-with-projects/managing-your-project/managing-visibility-of-your-projects.md @@ -39,3 +39,4 @@ Project admins can also manage write and admin access to their project and contr ## Further reading * [Allowing project visibility changes in your organization](/organizations/managing-organization-settings/allowing-project-visibility-changes-in-your-organization) + diff --git a/content/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/using-query-parameters-to-create-a-pull-request.md b/content/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/using-query-parameters-to-create-a-pull-request.md index 79a98e51c40b..c1aab59703a0 100644 --- a/content/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/using-query-parameters-to-create-a-pull-request.md +++ b/content/pull-requests/collaborating-with-pull-requests/proposing-changes-to-your-work-with-pull-requests/using-query-parameters-to-create-a-pull-request.md @@ -27,3 +27,4 @@ Query parameter | Example `assignees` | `https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&assignees=octocat` creates a pull request and assigns it to @octocat. `projects` | `https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&title=Bug+fix&projects=octo-org/1` creates a pull request with the title "Bug fix" and adds it to the organization's project 1. `template` | `https://github.com/octo-org/octo-repo/compare/main...my-branch?quick_pull=1&template=issue_template.md` creates a pull request with a template in the pull request body. The `template` query parameter works with templates stored in a `PULL_REQUEST_TEMPLATE` subdirectory within the root, `docs/` or `.github/` directory in a repository. For more information, see [AUTOTITLE](/communities/using-templates-to-encourage-useful-issues-and-pull-requests). + diff --git a/content/repositories/managing-your-repositorys-settings-and-features/repository-access-and-collaboration/maintaining-ownership-continuity-of-your-personal-accounts-repositories.md b/content/repositories/managing-your-repositorys-settings-and-features/repository-access-and-collaboration/maintaining-ownership-continuity-of-your-personal-accounts-repositories.md index 466fe9a0f133..9559243d60a9 100644 --- a/content/repositories/managing-your-repositorys-settings-and-features/repository-access-and-collaboration/maintaining-ownership-continuity-of-your-personal-accounts-repositories.md +++ b/content/repositories/managing-your-repositorys-settings-and-features/repository-access-and-collaboration/maintaining-ownership-continuity-of-your-personal-accounts-repositories.md @@ -30,4 +30,4 @@ The person you invite to be your successor must have a {% data variables.product 1. Click **Add successor**. {% data reusables.user-settings.sudo-mode-popup %} -The user you've invited will be listed as "Pending" until they agree to become your successor. +The user you've invited will be listed as "Pending" until they agree to become your successor. For more information about what happens after a successor is appointed, see [About successors](/account-and-profile/setting-up-and-managing-your-personal-account-on-github/managing-access-to-your-personal-repositories/about-successors). \ No newline at end of file diff --git a/content/site-policy/acceptable-use-policies/github-acceptable-use-policies.md b/content/site-policy/acceptable-use-policies/github-acceptable-use-policies.md index f096c344b909..5100dcce69b9 100644 --- a/content/site-policy/acceptable-use-policies/github-acceptable-use-policies.md +++ b/content/site-policy/acceptable-use-policies/github-acceptable-use-policies.md @@ -126,3 +126,4 @@ We will interpret our policies and resolve disputes in favor of protecting users **Enforcement.** GitHub retains full discretion to [take action](/site-policy/github-terms/github-community-guidelines#what-happens-if-someone-violates-githubs-policies) in response to a violation of these policies, including account suspension, account [termination](/site-policy/github-terms/github-terms-of-service#3-github-may-terminate), or [removal](/site-policy/github-terms/github-terms-of-service#2-github-may-remove-content) of content. Please also see our [Community Guidelines](/site-policy/github-terms/github-community-guidelines) for actions you can take if something or someone offends you. **Reinstatement and appeal.** If your content or account has been disabled or restricted and you seek reinstatement or wish to appeal, please see our [Appeal and Reinstatement page](/site-policy/acceptable-use-policies/github-appeal-and-reinstatement) for information about the process and use our [Appeal and Reinstatement form](https://support.github.com/contact/reinstatement) to submit a request. + diff --git a/content/support/contacting-github-support/creating-a-support-ticket.md b/content/support/contacting-github-support/creating-a-support-ticket.md index ff0a6b40bbf7..16d2c70bf5e9 100644 --- a/content/support/contacting-github-support/creating-a-support-ticket.md +++ b/content/support/contacting-github-support/creating-a-support-ticket.md @@ -78,13 +78,15 @@ Especially for tickets with {% data variables.product.support_ticket_priority_ur ## Creating a support ticket{% ifversion ghes %} using the {% data variables.contact.enterprise_portal %}{% endif %} -> [!NOTE] -> Before you submit a ticket, you have the option of using {% data variables.copilot.copilot_in_support %} to receive an immediate response to your question. If {% data variables.copilot.copilot_in_support %} is unable to resolve your issue, you can continue submitting your ticket. For more information, see [AUTOTITLE](/support/learning-about-github-support/about-copilot-in-github-support). +Before you submit a ticket, you have the option of using {% data variables.copilot.copilot_in_support %} to receive an immediate response to your question. If {% data variables.copilot.copilot_in_support %} is unable to resolve your issue, you can continue submitting your ticket. For more information, see [AUTOTITLE](/support/learning-about-github-support/about-copilot-in-github-support). + +> [!IMPORTANT] +> If your enterprise uses {% data variables.enterprise.data_residency_short %}, you must sign in with your {% data variables.enterprise.data_residency_site %} account when creating a support ticket about your data-resident enterprise. Otherwise, you will be asked to open a new ticket using your {% data variables.enterprise.data_residency_site %} account. 1. Navigate to the {% data variables.contact.contact_landing_page_portal %} and choose one of the following options: * To sign in with your {% data variables.product.github %} account, click **Sign in with {% data variables.product.github %}**. - * If your organization uses {% data variables.enterprise.data_residency_short %} or {% data variables.enterprise.prodname_managed_users %} (accounts ending in `.ghe`): + * If your enterprise uses {% data variables.enterprise.data_residency_short %} (you will use a domain like `{% data variables.enterprise.data_residency_example_domain %}`): 1. In the sign-in dialog, click **Sign in to your .ghe account**. 1. Enter your enterprise or tenant name (provided by your administrator) and continue. 1. Complete the SAML authentication process as prompted. This will sign you in to your .ghe enterprise-managed account. diff --git a/data/reusables/apps/deprecating_auth_with_query_parameters.md b/data/reusables/apps/deprecating_auth_with_query_parameters.md index d3c93c1aaac4..0cfe35b1b941 100644 --- a/data/reusables/apps/deprecating_auth_with_query_parameters.md +++ b/data/reusables/apps/deprecating_auth_with_query_parameters.md @@ -2,3 +2,4 @@ > **{% data variables.release-phases.retired_caps %} Notice:** Authenticating to the {% data variables.product.prodname_dotcom %} API is no longer accessible using query parameters. Authenticating to the API should be done with [HTTP basic authentication](/rest/overview/authenticating-to-the-rest-api#using-basic-authentication). For more information, including scheduled brownouts, see the [blog post](https://developer.github.com/changes/2020-02-10-deprecating-auth-through-query-param/). {% ifversion ghes %}> > Authentication to the API using query parameters while available is no longer supported due to security concerns. Instead we recommend integrators move their access token, `client_id`, or `client_secret` in the header. {% data variables.product.prodname_dotcom %} will announce the removal of authentication by query parameters with advanced notice. {% endif %} + diff --git a/data/reusables/copilot/anthropic-claude-agent-models.md b/data/reusables/copilot/anthropic-claude-agent-models.md index 54abccdbe82a..59de6fee6a05 100644 --- a/data/reusables/copilot/anthropic-claude-agent-models.md +++ b/data/reusables/copilot/anthropic-claude-agent-models.md @@ -1,4 +1,5 @@ * {% data variables.copilot.copilot_claude_opus_45 %} * {% data variables.copilot.copilot_claude_opus_46 %} +* {% data variables.copilot.copilot_claude_opus_47 %} * {% data variables.copilot.copilot_claude_sonnet_45 %} * {% data variables.copilot.copilot_claude_sonnet_46 %} diff --git a/data/reusables/copilot/cloud-agent/install-dependencies.md b/data/reusables/copilot/cloud-agent/install-dependencies.md new file mode 100644 index 000000000000..d2e4545621b3 --- /dev/null +++ b/data/reusables/copilot/cloud-agent/install-dependencies.md @@ -0,0 +1,22 @@ +```yaml +# ... + +jobs: + copilot-setup-steps: + # ... + + # You can define any steps you want, and they will run before the agent starts. + # If you do not check out your code, Copilot will do this for you. + steps: + - name: Checkout code + uses: {% data reusables.actions.action-checkout %} + + - name: Set up Node.js + uses: {% data reusables.actions.action-setup-node %} + with: + node-version: "20" + cache: "npm" + + - name: Install JavaScript dependencies + run: npm ci +``` diff --git a/data/reusables/copilot/copilot-cli/cli-rewind-warning.md b/data/reusables/copilot/copilot-cli/cli-rewind-warning.md new file mode 100644 index 000000000000..0218b807e97d --- /dev/null +++ b/data/reusables/copilot/copilot-cli/cli-rewind-warning.md @@ -0,0 +1 @@ +Rewinding restores your entire workspace to the state it was in at the selected snapshot. This reverts all changes made after that point—not only changes made by {% data variables.product.prodname_copilot_short %}, but also any manual edits, and changes resulting from shell commands. Any new files created in the workspace after the snapshot was taken are deleted, irrespective of their Git status. 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 a2334571aa66..817aa7a58ab0 100644 --- a/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md +++ b/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md @@ -1,4 +1,2 @@ -* {% data variables.copilot.copilot_claude_opus_45 %} -* {% data variables.copilot.copilot_claude_opus_46 %} * {% data variables.copilot.copilot_claude_opus_47 %} * {% data variables.copilot.copilot_gpt_52_codex %} diff --git a/data/reusables/copilot/experimental.md b/data/reusables/copilot/experimental.md new file mode 100644 index 000000000000..817b67d556ec --- /dev/null +++ b/data/reusables/copilot/experimental.md @@ -0,0 +1 @@ +Only available in experimental mode. diff --git a/data/reusables/copilot/openai-codex-agent-models.md b/data/reusables/copilot/openai-codex-agent-models.md index 4300434f86fe..641914462714 100644 --- a/data/reusables/copilot/openai-codex-agent-models.md +++ b/data/reusables/copilot/openai-codex-agent-models.md @@ -1,3 +1,4 @@ * {% data variables.copilot.copilot_gpt_52_codex %} * {% data variables.copilot.copilot_gpt_53_codex %} * {% data variables.copilot.copilot_gpt_54 %} +* {% data variables.copilot.copilot_gpt_54_nano %} diff --git a/data/reusables/copilot/plans/individual-plans-paused.md b/data/reusables/copilot/plans/individual-plans-paused.md new file mode 100644 index 000000000000..2177a65e356a --- /dev/null +++ b/data/reusables/copilot/plans/individual-plans-paused.md @@ -0,0 +1 @@ +**Starting April 20, 2026**, new sign-ups for {% data variables.copilot.copilot_pro_short %}, {% data variables.copilot.copilot_pro_plus_short %}, and student plans are temporarily paused. diff --git a/data/reusables/copilot/quickstart-signup.md b/data/reusables/copilot/quickstart-signup.md index 3d417b9718a0..c3cad1fe33ca 100644 --- a/data/reusables/copilot/quickstart-signup.md +++ b/data/reusables/copilot/quickstart-signup.md @@ -9,6 +9,9 @@ To use {% data variables.product.prodname_copilot_short %}, you’ll need a pers * 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 %} or {% data variables.copilot.copilot_pro_plus_short %} to unlock more features, models, and request limits. + >[!IMPORTANT] + > {% data reusables.copilot.plans.individual-plans-paused %} + For more information about the different plans for {% data variables.product.prodname_copilot %}, see [AUTOTITLE](/copilot/about-github-copilot/subscription-plans-for-github-copilot). {% elsif ghec %} diff --git a/data/reusables/copilot/trial-period.md b/data/reusables/copilot/trial-period.md deleted file mode 100644 index 64bb6b746dce..000000000000 --- a/data/reusables/copilot/trial-period.md +++ /dev/null @@ -1 +0,0 @@ -30 diff --git a/data/reusables/enterprise-accounts/emu-cap-validates.md b/data/reusables/enterprise-accounts/emu-cap-validates.md index 3e778ec339f7..951acb4df252 100644 --- a/data/reusables/enterprise-accounts/emu-cap-validates.md +++ b/data/reusables/enterprise-accounts/emu-cap-validates.md @@ -1 +1,2 @@ When your enterprise uses OIDC SSO, {% data variables.product.prodname_dotcom %} will automatically use your IdP's conditional access policy (CAP) IP conditions to validate interactions with {% data variables.product.prodname_dotcom %} when members use the web UI or change IP addresses, and for each authentication with a {% data variables.product.pat_generic %} or SSH key associated with a user account. + diff --git a/data/reusables/user-settings/user-api.md b/data/reusables/user-settings/user-api.md index a3826508906d..359ab2db663c 100644 --- a/data/reusables/user-settings/user-api.md +++ b/data/reusables/user-settings/user-api.md @@ -1 +1,2 @@ If a request URL does not include a `{username}` parameter then the response will be for the signed-in user (and you must pass [authentication information](/rest/overview/authenticating-to-the-rest-api) with your request). Additional private information, such as whether a user has two-factor authentication enabled, is included when authenticated through{% ifversion ghes %} Basic Authentication or{% endif %} OAuth with the `user` scope. + diff --git a/data/tables/copilot/model-comparison.yml b/data/tables/copilot/model-comparison.yml index 72f8bdeaec47..0d6446016cb7 100644 --- a/data/tables/copilot/model-comparison.yml +++ b/data/tables/copilot/model-comparison.yml @@ -48,16 +48,6 @@ excels_at: Fast, reliable answers to lightweight coding questions further_reading: '[Claude Haiku 4.5 model card](https://assets.anthropic.com/m/99128ddd009bdcb/Claude-Haiku-4-5-System-Card.pdf)' -- name: Claude Opus 4.5 - task_area: Deep reasoning and debugging - excels_at: Complex problem-solving challenges, sophisticated reasoning - further_reading: '[Claude Opus 4.5 model card](https://assets.anthropic.com/m/64823ba7485345a7/Claude-Opus-4-5-System-Card.pdf)' - -- name: Claude Opus 4.6 - task_area: Deep reasoning and debugging - excels_at: Complex problem-solving challenges, sophisticated reasoning - further_reading: '[Claude Opus 4.6 model card](https://www-cdn.anthropic.com/14e4fb01875d2a69f646fa5e574dea2b1c0ff7b5.pdf)' - - name: Claude Opus 4.6 (fast mode) (preview) task_area: Deep reasoning and debugging excels_at: Complex problem-solving challenges, sophisticated reasoning diff --git a/data/tables/copilot/model-multipliers.yml b/data/tables/copilot/model-multipliers.yml index 0c833be0d54d..403102ef1188 100644 --- a/data/tables/copilot/model-multipliers.yml +++ b/data/tables/copilot/model-multipliers.yml @@ -85,6 +85,10 @@ multiplier_paid: 0.33 multiplier_free: Not applicable +- name: GPT-5.4 nano + multiplier_paid: 0.25 + multiplier_free: Not applicable + - name: Grok Code Fast 1 multiplier_paid: 0.25 multiplier_free: 1 diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml index 9777d57224b8..51891c307f3f 100644 --- a/data/tables/copilot/model-release-status.yml +++ b/data/tables/copilot/model-release-status.yml @@ -67,6 +67,13 @@ ask_mode: true edit_mode: true +- name: 'GPT-5.4 nano' + provider: 'OpenAI' + release_status: 'GA' + agent_mode: true + ask_mode: true + edit_mode: true + # Anthropic models - name: 'Claude Haiku 4.5' provider: 'Anthropic' diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml index bf16d623fbd9..32a6c0f50631 100644 --- a/data/tables/copilot/model-supported-clients.yml +++ b/data/tables/copilot/model-supported-clients.yml @@ -176,6 +176,15 @@ xcode: true jetbrains: true +- name: GPT-5.4 nano + dotcom: false + cli: false + vscode: false + vs: false + eclipse: false + xcode: false + jetbrains: false + - name: Grok Code Fast 1 dotcom: true cli: false diff --git a/data/tables/copilot/model-supported-plans.yml b/data/tables/copilot/model-supported-plans.yml index 4f40eb6ebb1a..9a5d72b62d34 100644 --- a/data/tables/copilot/model-supported-plans.yml +++ b/data/tables/copilot/model-supported-plans.yml @@ -24,16 +24,16 @@ - name: Claude Opus 4.5 free: false student: false - pro: true - pro_plus: true + pro: false + pro_plus: false business: true enterprise: true - name: Claude Opus 4.6 free: false student: false - pro: true - pro_plus: true + pro: false + pro_plus: false business: true enterprise: true @@ -41,7 +41,7 @@ free: false student: false pro: false - pro_plus: true + pro_plus: false business: false enterprise: true @@ -157,6 +157,14 @@ business: true enterprise: true +- name: GPT-5.4 nano + free: false + student: true + pro: true + pro_plus: true + business: true + enterprise: true + - name: Grok Code Fast 1 free: true student: true diff --git a/data/variables/copilot.yml b/data/variables/copilot.yml index 169918dd50fc..d51dcb12497a 100644 --- a/data/variables/copilot.yml +++ b/data/variables/copilot.yml @@ -175,6 +175,7 @@ copilot_gpt_52_codex: 'GPT-5.2-Codex' copilot_gpt_53_codex: 'GPT-5.3-Codex' copilot_gpt_54: 'GPT-5.4' copilot_gpt_54_mini: 'GPT-5.4 mini' +copilot_gpt_54_nano: 'GPT-5.4 nano' # OpenAI 'o' series: copilot_o3: 'o3' copilot_o4_mini: 'o4-mini' diff --git a/eslint.config.ts b/eslint.config.ts index 5010779df4fe..961aaa8bcde3 100644 --- a/eslint.config.ts +++ b/eslint.config.ts @@ -204,32 +204,17 @@ export default [ 'src/article-api/transformers/audit-logs-transformer.ts', 'src/article-api/transformers/rest-transformer.ts', 'src/codeql-cli/scripts/convert-markdown-for-docs.ts', - 'src/content-linter/lib/init-test.ts', - 'src/content-linter/lib/linting-rules/code-annotations.ts', - 'src/content-linter/lib/linting-rules/index.ts', - 'src/content-linter/lib/linting-rules/journey-tracks-liquid.ts', 'src/content-linter/lib/linting-rules/liquid-ifversion-versions.ts', - 'src/content-linter/lib/linting-rules/liquid-versioning.ts', - 'src/content-linter/lib/linting-rules/third-party-action-pinning.ts', 'src/content-linter/scripts/lint-content.ts', - 'src/content-linter/scripts/pretty-print-results.ts', - 'src/content-linter/style/base.ts', - 'src/content-linter/tests/integration/lint-cli.ts', - 'src/content-linter/tests/lint-files.ts', - 'src/content-linter/tests/lint-frontmatter-links.ts', - 'src/content-linter/tests/unit/table-column-integrity-simple.ts', + 'src/content-render/liquid/engine.ts', 'src/content-render/liquid/index.ts', 'src/content-render/scripts/liquid-tags.ts', 'src/content-render/scripts/move-content.ts', - 'src/content-render/tests/link-error-line-numbers.ts', 'src/content-render/unified/annotate.ts', 'src/content-render/unified/index.ts', 'src/data-directory/lib/get-data.ts', - 'src/early-access/scripts/migrate-early-access-product.ts', - 'src/fixtures/tests/categories-and-subcategory.ts', 'src/fixtures/tests/guides.ts', - 'src/fixtures/tests/translations.ts', 'src/frame/components/context/MainContext.tsx', 'src/frame/lib/create-tree.ts', 'src/frame/lib/frontmatter.ts', @@ -290,7 +275,6 @@ export default [ 'src/types/markdownlint-rule-helpers.d.ts', 'src/types/markdownlint-rule-search-replace.d.ts', 'src/types/primer__octicons.d.ts', - 'src/workflows/projects.ts', ], rules: { '@typescript-eslint/no-explicit-any': 'off', diff --git a/src/article-api/templates/graphql-changelog.template.md b/src/article-api/templates/graphql-changelog.template.md index b1461930710f..3ecffbd95e33 100644 --- a/src/article-api/templates/graphql-changelog.template.md +++ b/src/article-api/templates/graphql-changelog.template.md @@ -4,6 +4,8 @@ {{ manualContent }} +{% for navItem in yearNavItems %}{% if navItem.isCurrent %}**{{ navItem.year }}**{% else %}[{{ navItem.year }}]({{ navItem.year }}){% endif %}{% unless forloop.last %} · {% endunless %}{% endfor %} + {% for item in changelogItems %} ## Schema changes for {{ item.date }} diff --git a/src/article-api/tests/graphql-transformer.ts b/src/article-api/tests/graphql-transformer.ts index d418bec102cd..fc0a49502bf2 100644 --- a/src/article-api/tests/graphql-transformer.ts +++ b/src/article-api/tests/graphql-transformer.ts @@ -233,7 +233,7 @@ describe('GraphQL transformer', { timeout: 10000 }, () => { }) describe('Overview pages', () => { - test('changelog page renders with changes', async () => { + test('changelog index page renders with latest year changes', async () => { const res = await getCached('/en/graphql/overview/changelog') expect(res.statusCode).toBe(200) @@ -250,16 +250,38 @@ describe('GraphQL transformer', { timeout: 10000 }, () => { 'Breaking changes include changes that will break existing queries', ) - // Check for date-based changelog sections - expect(res.body).toContain('## Schema changes for 2025-11-30') + // Index page shows latest year (2026) entries only + expect(res.body).toContain('## Schema changes for 2026-') // Check for change items expect(res.body).toContain('### The GraphQL schema includes these changes:') - expect(res.body).toContain('Type SuggestedReviewerActor was added') + + // Should NOT contain entries from other years + expect(res.body).not.toContain('## Schema changes for 2025-') + + // Check for year navigation + expect(res.body).toContain('2026') + expect(res.body).toContain('2025') + }) + + test('changelog year page renders with that year only', async () => { + const res = await getCached('/en/graphql/overview/changelog/2025') + expect(res.statusCode).toBe(200) + + // Check for year-specific heading + expect(res.body).toContain('# GraphQL changelog for 2025') + + // Check for date-based changelog sections from 2025 + expect(res.body).toContain('## Schema changes for 2025-') + + // Should NOT contain entries from other years + expect(res.body).not.toContain('## Schema changes for 2026-') + expect(res.body).not.toContain('## Schema changes for 2024-') }) test('changelog removes HTML tags from changes', async () => { - const res = await getCached('/en/graphql/overview/changelog') + // Use a year page that has the specific test data + const res = await getCached('/en/graphql/overview/changelog/2025') expect(res.statusCode).toBe(200) // Check that HTML tags are removed diff --git a/src/article-api/transformers/graphql-changelog-transformer.ts b/src/article-api/transformers/graphql-changelog-transformer.ts index 0e3b504cbce0..53d764cbdb09 100644 --- a/src/article-api/transformers/graphql-changelog-transformer.ts +++ b/src/article-api/transformers/graphql-changelog-transformer.ts @@ -22,9 +22,22 @@ export class GraphQLChangelogTransformer implements PageTransformer { async transform(page: Page, _pathname: string, context: Context): Promise { const currentVersion = context.currentVersion! - const { getGraphqlChangelog } = await import('@/graphql/lib/index') + const { getGraphqlChangelogByYear, getGraphqlChangelogYears } = + await import('@/graphql/lib/index') - const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[] + // Determine if this is a year-specific page + const yearMatch = page.relativePath.match(/changelog\/(\d{4})\.md$/) + const year = yearMatch ? Number(yearMatch[1]) : null + const years = getGraphqlChangelogYears(currentVersion) + + let schema: ChangelogItemT[] + if (year) { + schema = getGraphqlChangelogByYear(currentVersion, year) as ChangelogItemT[] + } else { + // Index page: show only the latest year + const latestYear = years[0] + schema = getGraphqlChangelogByYear(currentVersion, latestYear) as ChangelogItemT[] + } const intro = page.intro ? await page.renderProp('intro', context, { textOnly: true }) : '' const manualContent = await extractManualContent(page, context) @@ -51,11 +64,19 @@ export class GraphQLChangelogTransformer implements PageTransformer { } }) + // Build year navigation links + const displayYear = year || years[0] + const yearNavItems = years.map((y) => ({ + year: y, + isCurrent: y === displayYear, + })) + const templateData: Record = { pageTitle: page.title, pageIntro: intro, manualContent, changelogItems, + yearNavItems, } const templateContent = loadTemplate(this.templateName) diff --git a/src/content-linter/lib/init-test.ts b/src/content-linter/lib/init-test.ts index f5eb91e05d45..6fcd819f5634 100644 --- a/src/content-linter/lib/init-test.ts +++ b/src/content-linter/lib/init-test.ts @@ -23,7 +23,9 @@ export async function runRule( } const testOptions: Partial = { - customRules: [module as any], + customRules: [ + module as unknown as NonNullable>[number], + ], config: { ...defaultConfig, ...testConfig }, } if (strings) testOptions.strings = strings diff --git a/src/content-linter/lib/linting-rules/code-annotations.ts b/src/content-linter/lib/linting-rules/code-annotations.ts index 4226d8ed2070..df9f090c0a5c 100644 --- a/src/content-linter/lib/linting-rules/code-annotations.ts +++ b/src/content-linter/lib/linting-rules/code-annotations.ts @@ -5,7 +5,7 @@ import type { RuleParams, RuleErrorCallback, MarkdownToken } from '@/content-lin interface Frontmatter { layout?: string - [key: string]: any + [key: string]: unknown } export const codeAnnotations = { diff --git a/src/content-linter/lib/linting-rules/index.ts b/src/content-linter/lib/linting-rules/index.ts index 56b641849e14..a9232fae65c2 100644 --- a/src/content-linter/lib/linting-rules/index.ts +++ b/src/content-linter/lib/linting-rules/index.ts @@ -58,13 +58,10 @@ import { raiAppCardStructure } from '@/content-linter/lib/linting-rules/rai-app- import { frontmatterContentType } from '@/content-linter/lib/linting-rules/frontmatter-content-type' import { frontmatterDocsTeamMetrics } from '@/content-linter/lib/linting-rules/frontmatter-docs-team-metrics' -// Using any type because @github/markdownlint-github doesn't provide TypeScript declarations -// The elements in the array have a 'names' property that contains rule identifiers -const noDefaultAltText = markdownlintGitHub.find((elem: any) => +const noDefaultAltText = markdownlintGitHub.find((elem: { names: string[] }) => elem.names.includes('no-default-alt-text'), ) -// Using any type because @github/markdownlint-github doesn't provide TypeScript declarations -const noGenericLinkText = markdownlintGitHub.find((elem: any) => +const noGenericLinkText = markdownlintGitHub.find((elem: { names: string[] }) => elem.names.includes('no-generic-link-text'), ) diff --git a/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts b/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts index bb602fc1c14d..f3a3dfd547c9 100644 --- a/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts +++ b/src/content-linter/lib/linting-rules/journey-tracks-liquid.ts @@ -9,8 +9,7 @@ export const journeyTracksLiquid = { description: 'Journey track properties must use valid Liquid syntax', tags: ['frontmatter', 'journey-tracks', 'liquid'], function: (params: RuleParams, onError: RuleErrorCallback) => { - // Using any for frontmatter as it's a dynamic YAML object with varying properties - const fm: any = getFrontmatter(params.lines) + const fm: Record = getFrontmatter(params.lines) as Record if (!fm || !fm.journeyTracks || !Array.isArray(fm.journeyTracks)) return if (!fm.layout || fm.layout !== 'journey-landing') return @@ -23,7 +22,7 @@ export const journeyTracksLiquid = { : 1 for (let trackIndex = 0; trackIndex < fm.journeyTracks.length; trackIndex++) { - const track: any = fm.journeyTracks[trackIndex] + const track = (fm.journeyTracks as Array>)[trackIndex] // Try to find the line number for this specific journey track so we can use that for the error // line number. Getting the exact line number is probably more work than it's worth for this // particular rule. @@ -62,11 +61,11 @@ export const journeyTracksLiquid = { if (prop.value && typeof prop.value === 'string') { try { liquid.parse(prop.value) - } catch (error: any) { + } catch (error: unknown) { addError( onError, trackLineNumber, - `Invalid Liquid syntax in journey track ${prop.name} (track ${trackIndex + 1}): ${error.message}`, + `Invalid Liquid syntax in journey track ${prop.name} (track ${trackIndex + 1}): ${error instanceof Error ? error.message : String(error)}`, prop.value, ) } @@ -84,11 +83,11 @@ export const journeyTracksLiquid = { if ('href' in guideObj && typeof guideObj.href === 'string') { try { liquid.parse(guideObj.href) - } catch (error: any) { + } catch (error: unknown) { addError( onError, trackLineNumber, - `Invalid Liquid syntax in journey track guide href (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`, + `Invalid Liquid syntax in journey track guide href (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error instanceof Error ? error.message : String(error)}`, guideObj.href, ) } @@ -101,11 +100,11 @@ export const journeyTracksLiquid = { ) { try { liquid.parse(guideObj.alternativeNextStep) - } catch (error: any) { + } catch (error: unknown) { addError( onError, trackLineNumber, - `Invalid Liquid syntax in journey track guide alternativeNextStep (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error.message}`, + `Invalid Liquid syntax in journey track guide alternativeNextStep (track ${trackIndex + 1}, guide ${guideIndex + 1}): ${error instanceof Error ? error.message : String(error)}`, guideObj.alternativeNextStep, ) } diff --git a/src/content-linter/lib/linting-rules/liquid-versioning.ts b/src/content-linter/lib/linting-rules/liquid-versioning.ts index a9062aa04243..bebbba7bdfa8 100644 --- a/src/content-linter/lib/linting-rules/liquid-versioning.ts +++ b/src/content-linter/lib/linting-rules/liquid-versioning.ts @@ -169,9 +169,8 @@ function validateIfversionConditionals(cond: string, possibleVersionNames: Set = {} if (part in allFeatures) { for (const [shortName, version] of Object.entries(allFeatures[part].versions)) { - // Using 'as any' for recursive getVersionsObject call because it can return either - // a string or a nested Record, but we flatten it to string for this context const versionOperator: string = version in allFeatures - ? (getVersionsObject(version, allFeatures) as any) + ? Object.values(getVersionsObject(version, allFeatures))[0] || '*' : (version as string) if (shortName in versions) { versions[shortName] = lowestVersion(versionOperator, versions[shortName]) diff --git a/src/content-linter/lib/linting-rules/third-party-action-pinning.ts b/src/content-linter/lib/linting-rules/third-party-action-pinning.ts index 45cb3424099c..cc8d927dcbb9 100644 --- a/src/content-linter/lib/linting-rules/third-party-action-pinning.ts +++ b/src/content-linter/lib/linting-rules/third-party-action-pinning.ts @@ -14,18 +14,18 @@ const firstPartyPrefixes = ['actions/', './.github/actions/', 'github/', 'octo-o interface WorkflowStep { uses?: string - [key: string]: any + [key: string]: unknown } interface WorkflowJob { steps?: WorkflowStep[] - [key: string]: any + [key: string]: unknown } interface WorkflowYaml { jobs?: Record steps?: WorkflowStep[] - [key: string]: any + [key: string]: unknown } export const thirdPartyActionPinning: Rule = { diff --git a/src/content-linter/scripts/pretty-print-results.ts b/src/content-linter/scripts/pretty-print-results.ts index 3c0a8124a095..bd758d0d949f 100644 --- a/src/content-linter/scripts/pretty-print-results.ts +++ b/src/content-linter/scripts/pretty-print-results.ts @@ -152,8 +152,8 @@ function chalkFunColors(text: string): string { .map((char) => { const color = shuffledColors[colorIndex] colorIndex = (colorIndex + 1) % shuffledColors.length - // Chalk's TypeScript types don't support dynamic property access, but these are valid color methods - return (chalk as any)[color](char) + const colorFn = chalk[color] as (text: string) => string + return colorFn(char) }) .join('') } diff --git a/src/content-linter/style/base.ts b/src/content-linter/style/base.ts index 600a65017754..bdb7dc725869 100644 --- a/src/content-linter/style/base.ts +++ b/src/content-linter/style/base.ts @@ -9,7 +9,7 @@ type RuleConfig = { severity: 'error' | 'warning' 'partial-markdown-files': boolean 'yml-files': boolean - [key: string]: any + [key: string]: unknown } type BaseConfig = { diff --git a/src/content-linter/tests/integration/lint-cli.ts b/src/content-linter/tests/integration/lint-cli.ts index 4e464014ba49..c089f08df799 100644 --- a/src/content-linter/tests/integration/lint-cli.ts +++ b/src/content-linter/tests/integration/lint-cli.ts @@ -52,9 +52,10 @@ describe('Content Linter CLI Integration Tests', { timeout: 30000 }, () => { stdio: 'pipe', timeout: 10000, // 10 second timeout }) - } catch (error: any) { - output = error.stdout + error.stderr - exitCode = error.status || 1 + } catch (error: unknown) { + const execError = error as { stdout?: string; stderr?: string; status?: number } + output = (execError.stdout || '') + (execError.stderr || '') + exitCode = execError.status || 1 } return { output, exitCode } diff --git a/src/content-linter/tests/lint-files.ts b/src/content-linter/tests/lint-files.ts index c700fc7cb08b..cc3509a3e722 100755 --- a/src/content-linter/tests/lint-files.ts +++ b/src/content-linter/tests/lint-files.ts @@ -200,9 +200,15 @@ function formatLinkError(message: string, links: string[]) { // Returns `content` if its a string, or `content.description` if it can. // Used for getting the nested `description` key in glossary files. // Using any because content can be string | { description: string } | other YAML structures -function getContent(content: any) { +function getContent(content: unknown) { if (typeof content === 'string') return content - if (typeof content.description === 'string') return content.description + if ( + content && + typeof content === 'object' && + 'description' in content && + typeof (content as { description: unknown }).description === 'string' + ) + return (content as { description: string }).description return null } @@ -240,198 +246,200 @@ if (ymlToLint.length === 0) { } else { describe('lint yaml content', () => { if (ymlToLint.length < 1) return - describe.each(ymlToLint)('%s', (yamlRelPath: any, yamlAbsPath: any) => { - // Using any because Vitest's describe.each doesn't properly infer tuple types - let dictionary: any // YAML structure varies by file type (variables, glossaries, features) - let isEarlyAccess: boolean - let fileContents: string - // This variable is used to determine if the file was parsed successfully. - // When `yaml.load()` fails to parse the file, it is overwritten with the error message. - // `false` is intentionally chosen since `null` and `undefined` are valid return values. - let dictionaryError: any = false // Can be false, Error, or other error types - - beforeAll(async () => { - fileContents = await fs.readFile(yamlAbsPath, 'utf8') - try { - dictionary = yaml.load(fileContents, { filename: yamlRelPath }) - } catch (error) { - dictionaryError = error - } - - isEarlyAccess = yamlRelPath.split('/').includes('early-access') - }) - - test('it can be parsed as a single yaml document', () => { - expect(dictionaryError).toBe(false) - }) - - test('placeholder string is not present in any yaml files', () => { - const matches = fileContents.match(placeholderRegex) || [] - const errorMessage = ` - Found ${matches.length} placeholder string '${placeholder}'! Please update all placeholders. - ` - expect(matches.length, errorMessage).toBe(0) - }) - - test('relative URLs must start with "/"', async () => { - const matches = [] - - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = contentStr.match(relativeArticleLinkRegex) || [] - if (valMatches.length > 0) { - matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + describe.each(ymlToLint)( + '%s', + (yamlRelPath: string | undefined, yamlAbsPath: string | undefined) => { + let dictionary: unknown // YAML structure varies by file type (variables, glossaries, features) + let isEarlyAccess: boolean + let fileContents: string + // This variable is used to determine if the file was parsed successfully. + // When `yaml.load()` fails to parse the file, it is overwritten with the error message. + // `false` is intentionally chosen since `null` and `undefined` are valid return values. + let dictionaryError: unknown = false + + beforeAll(async () => { + fileContents = await fs.readFile(yamlAbsPath!, 'utf8') + try { + dictionary = yaml.load(fileContents, { filename: yamlRelPath }) + } catch (error) { + dictionaryError = error } - } - const errorMessage = formatLinkError(relativeArticleLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + isEarlyAccess = yamlRelPath!.split('/').includes('early-access') + }) + + test('it can be parsed as a single yaml document', () => { + expect(dictionaryError).toBe(false) + }) - test('must not leak Early Access doc URLs', async () => { - // Only execute for docs that are NOT Early Access - if (!isEarlyAccess) { + test('placeholder string is not present in any yaml files', () => { + const matches = fileContents.match(placeholderRegex) || [] + const errorMessage = ` + Found ${matches.length} placeholder string '${placeholder}'! Please update all placeholders. + ` + expect(matches.length, errorMessage).toBe(0) + }) + + test('relative URLs must start with "/"', async () => { const matches = [] - for (const [key, content] of Object.entries(dictionary)) { + for (const [key, content] of Object.entries(dictionary as Record)) { const contentStr = getContent(content) if (!contentStr) continue - const valMatches = contentStr.match(earlyAccessLinkRegex) || [] + const valMatches = contentStr.match(relativeArticleLinkRegex) || [] if (valMatches.length > 0) { matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) } } - const errorMessage = formatLinkError(earlyAccessLinkErrorText, matches) + const errorMessage = formatLinkError(relativeArticleLinkErrorText, matches) expect(matches.length, errorMessage).toBe(0) - } - }) + }) + + test('must not leak Early Access doc URLs', async () => { + // Only execute for docs that are NOT Early Access + if (!isEarlyAccess) { + const matches = [] + + for (const [key, content] of Object.entries(dictionary as Record)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(earlyAccessLinkRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + } + } + + const errorMessage = formatLinkError(earlyAccessLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + } + }) + + test('must not leak Early Access image URLs', async () => { + // Only execute for docs that are NOT Early Access + if (!isEarlyAccess) { + const matches = [] + + for (const [key, content] of Object.entries(dictionary as Record)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(earlyAccessImageRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + } + } + + const errorMessage = formatLinkError(earlyAccessImageErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + } + }) - test('must not leak Early Access image URLs', async () => { - // Only execute for docs that are NOT Early Access - if (!isEarlyAccess) { + test('must have correctly formatted Early Access image URLs', async () => { + // Execute for ALL docs (not just Early Access) to ensure non-EA docs + // are not leaking incorrectly formatted EA image URLs const matches = [] - for (const [key, content] of Object.entries(dictionary)) { + for (const [key, content] of Object.entries(dictionary as Record)) { const contentStr = getContent(content) if (!contentStr) continue - const valMatches = contentStr.match(earlyAccessImageRegex) || [] + const valMatches = contentStr.match(badEarlyAccessImageRegex) || [] if (valMatches.length > 0) { matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) } } - const errorMessage = formatLinkError(earlyAccessImageErrorText, matches) + const errorMessage = formatLinkError(badEarlyAccessImageErrorText, matches) expect(matches.length, errorMessage).toBe(0) - } - }) - - test('must have correctly formatted Early Access image URLs', async () => { - // Execute for ALL docs (not just Early Access) to ensure non-EA docs - // are not leaking incorrectly formatted EA image URLs - const matches = [] - - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = contentStr.match(badEarlyAccessImageRegex) || [] - if (valMatches.length > 0) { - matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + }) + + test('URLs must not contain a hard-coded language code', async () => { + const matches = [] + + for (const [key, content] of Object.entries(dictionary as Record)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(languageLinkRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + } } - } - const errorMessage = formatLinkError(badEarlyAccessImageErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(languageLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('URLs must not contain a hard-coded language code', async () => { - const matches = [] + test('URLs must not contain a hard-coded version number', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = contentStr.match(languageLinkRegex) || [] - if (valMatches.length > 0) { - matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + for (const [key, content] of Object.entries(dictionary as Record)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(versionLinkRegEx) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + } } - } - const errorMessage = formatLinkError(languageLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(versionLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('URLs must not contain a hard-coded version number', async () => { - const matches = [] + test('URLs must not contain a hard-coded domain name', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = contentStr.match(versionLinkRegEx) || [] - if (valMatches.length > 0) { - matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + for (const [key, content] of Object.entries(dictionary as Record)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(domainLinkRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + } } - } - const errorMessage = formatLinkError(versionLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(domainLinkErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('URLs must not contain a hard-coded domain name', async () => { - const matches = [] + test('does not use old site.data variable syntax', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = contentStr.match(domainLinkRegex) || [] - if (valMatches.length > 0) { - matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) - } - } - - const errorMessage = formatLinkError(domainLinkErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) - - test('does not use old site.data variable syntax', async () => { - const matches = [] - - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = contentStr.match(oldVariableRegex) || [] - if (valMatches.length > 0) { - matches.push( - ...valMatches.map((match: string) => { - const example = match.replace( - /{{\s*?site\.data\.([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)+)\s*?}}/g, - '{% data $1 %}', - ) - return `Key "${key}": ${match} => ${example}` - }), - ) + for (const [key, content] of Object.entries(dictionary as Record)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(oldVariableRegex) || [] + if (valMatches.length > 0) { + matches.push( + ...valMatches.map((match: string) => { + const example = match.replace( + /{{\s*?site\.data\.([a-zA-Z0-9-_]+(?:\.[a-zA-Z0-9-_]+)+)\s*?}}/g, + '{% data $1 %}', + ) + return `Key "${key}": ${match} => ${example}` + }), + ) + } } - } - const errorMessage = formatLinkError(oldVariableErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) + const errorMessage = formatLinkError(oldVariableErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) - test('does not use old octicon variable syntax', async () => { - const matches = [] + test('does not use old octicon variable syntax', async () => { + const matches = [] - for (const [key, content] of Object.entries(dictionary)) { - const contentStr = getContent(content) - if (!contentStr) continue - const valMatches = contentStr.match(oldOcticonRegex) || [] - if (valMatches.length > 0) { - matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + for (const [key, content] of Object.entries(dictionary as Record)) { + const contentStr = getContent(content) + if (!contentStr) continue + const valMatches = contentStr.match(oldOcticonRegex) || [] + if (valMatches.length > 0) { + matches.push(...valMatches.map((match: string) => `Key "${key}": ${match}`)) + } } - } - const errorMessage = formatLinkError(oldOcticonErrorText, matches) - expect(matches.length, errorMessage).toBe(0) - }) - }) + const errorMessage = formatLinkError(oldOcticonErrorText, matches) + expect(matches.length, errorMessage).toBe(0) + }) + }, + ) }) } diff --git a/src/content-linter/tests/lint-frontmatter-links.ts b/src/content-linter/tests/lint-frontmatter-links.ts index 07b4b1e352af..f13643ecad07 100644 --- a/src/content-linter/tests/lint-frontmatter-links.ts +++ b/src/content-linter/tests/lint-frontmatter-links.ts @@ -12,9 +12,11 @@ const liquidElsif = /{%\s*elsif/ const containsLiquidElseIf = (text: string) => liquidElsif.test(text) describe('front matter', () => { - // Using any type for page because it comes from loadPages which returns dynamic page objects with varying properties - // Using any[] for trouble because the error objects have different shapes depending on the validation that failed - function makeCustomErrorMessage(page: any, trouble: any[], key: string) { + function makeCustomErrorMessage( + page: { relativePath: string }, + trouble: Array<{ warning?: boolean; uri?: string; index?: number; redirects?: string }>, + key: string, + ) { let customErrorMessage = `In the front matter of ${page.relativePath} ` if (trouble.length > 0) { if (trouble.length === 1) { @@ -22,8 +24,7 @@ describe('front matter', () => { } else { customErrorMessage += `there are ${trouble.length} .${key} front matter entries that are not correct.` } - // Using any type because trouble array contains objects with varying error properties - const nonWarnings = trouble.filter((t: any) => !t.warning) + const nonWarnings = trouble.filter((t) => !t.warning) for (const { uri, index, redirects: redirectTo } of nonWarnings) { customErrorMessage += `\nindex: ${index} URI: ${uri}` if (redirectTo) { @@ -32,8 +33,7 @@ describe('front matter', () => { customErrorMessage += '\tPage not found' } } - // Using any type because trouble array contains objects with varying error properties - if (trouble.find((t: any) => t.redirects)) { + if (trouble.find((t) => t.redirects)) { customErrorMessage += `\n\nNOTE! To automatically fix the redirects run this command:\n` customErrorMessage += `\n\t./src/links/scripts/update-internal-links.ts content/${page.relativePath}\n\n` } @@ -59,7 +59,7 @@ describe('front matter', () => { ...links .filter((link) => link.href) .map((link, i) => checkURL(link.href, i, redirectsContext)) - .filter(Boolean), + .filter((item): item is NonNullable => Boolean(item)), ) } @@ -89,7 +89,7 @@ describe('front matter', () => { // Ignore those too. .filter((uri) => !uri.includes('https://')) .map((uri, i) => checkURL(uri, i, redirectsContext)) - .filter(Boolean), + .filter((item): item is NonNullable => Boolean(item)), ) } const customErrorMessage = makeCustomErrorMessage(page, trouble, 'introLinks') diff --git a/src/content-linter/tests/unit/table-column-integrity-simple.ts b/src/content-linter/tests/unit/table-column-integrity-simple.ts index 4980fce6f999..dcbb9947a0d7 100644 --- a/src/content-linter/tests/unit/table-column-integrity-simple.ts +++ b/src/content-linter/tests/unit/table-column-integrity-simple.ts @@ -22,8 +22,10 @@ describe(tableColumnIntegrity.names.join(' - '), () => { const errors = result.markdown expect(errors.length).toBe(1) expect(errors[0].lineNumber).toBe(3) - if ((errors[0] as any).detail) { - expect((errors[0] as any).detail).toContain('Table row has 3 columns but header has 2') + if ((errors[0] as unknown as { detail?: string }).detail) { + expect((errors[0] as unknown as { detail?: string }).detail).toContain( + 'Table row has 3 columns but header has 2', + ) } else if (errors[0].errorDetail) { expect(errors[0].errorDetail).toContain('Table row has 3 columns but header has 2') } else { @@ -38,8 +40,10 @@ describe(tableColumnIntegrity.names.join(' - '), () => { const errors = result.markdown expect(errors.length).toBe(1) expect(errors[0].lineNumber).toBe(3) - if ((errors[0] as any).detail) { - expect((errors[0] as any).detail).toContain('Table row has 2 columns but header has 3') + if ((errors[0] as unknown as { detail?: string }).detail) { + expect((errors[0] as unknown as { detail?: string }).detail).toContain( + 'Table row has 2 columns but header has 3', + ) } else if (errors[0].errorDetail) { expect(errors[0].errorDetail).toContain('Table row has 2 columns but header has 3') } else { diff --git a/src/content-pipelines/config.yml b/src/content-pipelines/config.yml index 2ba9ae316698..01d357911c3c 100644 --- a/src/content-pipelines/config.yml +++ b/src/content-pipelines/config.yml @@ -24,7 +24,10 @@ copilot-cli: - content/copilot/reference/copilot-cli-reference/cli-plugin-reference.md - content/copilot/reference/copilot-cli-reference/acp-server.md - content/copilot/reference/copilot-cli-reference/cli-config-dir-reference.md - exclusions: [] + exclusions: + - All mentions of, or information directly relating to, feature flags + - enabledFeatureFlags configuration setting + - COPILOT_CLI_ENABLED_FEATURE_FLAGS environment variable content-mapping: | cli-plugin-reference.md covers only plugin-specific content. All other CLI topics (hooks, MCP, skills, agents, permissions, etc.) belong in cli-command-reference.md even when they mention plugins. diff --git a/src/content-pipelines/state/copilot-cli.sha b/src/content-pipelines/state/copilot-cli.sha index 3813f23c6c28..960e9f298976 100644 --- a/src/content-pipelines/state/copilot-cli.sha +++ b/src/content-pipelines/state/copilot-cli.sha @@ -1 +1 @@ -584832db507d47454ed410791e2d0c53468bea20 +aaf6f363050eb7ad4b98542f7389b76f4e29f5b8 diff --git a/src/content-render/tests/link-error-line-numbers.ts b/src/content-render/tests/link-error-line-numbers.ts index 5ee9a839b842..4c1d9255e421 100644 --- a/src/content-render/tests/link-error-line-numbers.ts +++ b/src/content-render/tests/link-error-line-numbers.ts @@ -1,12 +1,12 @@ import { describe, expect, test, beforeEach, afterEach } from 'vitest' import { renderContent } from '@/content-render/index' import { TitleFromAutotitleError } from '@/content-render/unified/rewrite-local-links' -import type { Context } from '@/types' +import type { Context, Page } from '@/types' describe('link error line numbers', () => { - let fs: any // Dynamic import of fs module for mocking in tests - let originalReadFileSync: any // Storing original fs.readFileSync for restoration after test - let originalExistsSync: any // Storing original fs.existsSync for restoration after test + let fs: { default: typeof import('fs') } + let originalReadFileSync: typeof import('fs').readFileSync + let originalExistsSync: typeof import('fs').existsSync let mockContext: Context beforeEach(async () => { @@ -21,11 +21,11 @@ describe('link error line numbers', () => { mockContext = { currentLanguage: 'en', currentVersion: 'free-pro-team@latest', - pages: {} as any, - redirects: {} as any, + pages: {} as unknown as Record, + redirects: {} as Record, page: { fullPath: '/fake/test-file.md', - } as any, + } as unknown as Context['page'], } }) @@ -50,7 +50,7 @@ Here is a broken link: [AUTOTITLE](/nonexistent/page). More content here.` - fs.default.readFileSync = () => template + fs.default.readFileSync = (() => template) as unknown as typeof fs.default.readFileSync try { await renderContent(template, mockContext) @@ -70,7 +70,7 @@ More content here.` test('reports correct line numbers with different frontmatter sizes', async () => { mockContext.page = { fullPath: '/fake/test-file-2.md', - } as any + } as unknown as Context['page'] // Test with more extensive frontmatter const template = `--- @@ -92,7 +92,7 @@ Some introductory text here. Content with a [AUTOTITLE](/another/nonexistent/page) link.` - fs.default.readFileSync = () => template + fs.default.readFileSync = (() => template) as unknown as typeof fs.default.readFileSync try { await renderContent(template, mockContext) @@ -107,7 +107,7 @@ Content with a [AUTOTITLE](/another/nonexistent/page) link.` test('handles files without frontmatter correctly', async () => { mockContext.page = { fullPath: '/fake/no-frontmatter.md', - } as any + } as unknown as Context['page'] // Test content without frontmatter const template = `# Simple Title @@ -116,7 +116,7 @@ This is content without frontmatter. Here is a broken link: [AUTOTITLE](/missing/page).` - fs.default.readFileSync = () => template + fs.default.readFileSync = (() => template) as unknown as typeof fs.default.readFileSync try { await renderContent(template, mockContext) @@ -131,7 +131,7 @@ Here is a broken link: [AUTOTITLE](/missing/page).` test('error message format is improved', async () => { mockContext.page = { fullPath: '/fake/message-test.md', - } as any + } as unknown as Context['page'] const template = `--- title: Message Test @@ -140,7 +140,7 @@ title: Message Test [AUTOTITLE](/test/broken/link) ` - fs.default.readFileSync = () => template + fs.default.readFileSync = (() => template) as unknown as typeof fs.default.readFileSync try { await renderContent(template, mockContext) diff --git a/src/early-access/scripts/migrate-early-access-product.ts b/src/early-access/scripts/migrate-early-access-product.ts index d4117215b190..07102aa21611 100644 --- a/src/early-access/scripts/migrate-early-access-product.ts +++ b/src/early-access/scripts/migrate-early-access-product.ts @@ -198,19 +198,18 @@ function moveVariable(dataRef: string): void { } } - const variableFileContent: Record = yaml.load( + const variableFileContent: Record = yaml.load( fs.readFileSync(oldVariableFinalPath, 'utf8'), - ) as Record - const value: any = variableFileContent[variableKey] + ) as Record + const value: unknown = variableFileContent[variableKey] // If the variable file already exists, add the key/value pair. if (fs.existsSync(nonAltPath)) { - const content: Record = yaml.load(fs.readFileSync(nonAltPath, 'utf8')) as Record< - string, - any - > + const content: Record = yaml.load( + fs.readFileSync(nonAltPath, 'utf8'), + ) as Record if (!content[variableKey]) { - const newString = `\n\n${variableKey}: ${value}` + const newString = `\n\n${variableKey}: ${String(value)}` fs.appendFileSync(nonAltPath, newString) } } else { diff --git a/src/fixtures/fixtures/content/graphql/overview/changelog/2024.md b/src/fixtures/fixtures/content/graphql/overview/changelog/2024.md new file mode 100644 index 000000000000..260addef2fe3 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/overview/changelog/2024.md @@ -0,0 +1,10 @@ +--- +title: "GraphQL changelog for 2024" +shortTitle: "2024" +intro: 'GraphQL schema changes from 2024.' +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- diff --git a/src/fixtures/fixtures/content/graphql/overview/changelog/2025.md b/src/fixtures/fixtures/content/graphql/overview/changelog/2025.md new file mode 100644 index 000000000000..7352eb823c78 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/overview/changelog/2025.md @@ -0,0 +1,10 @@ +--- +title: "GraphQL changelog for 2025" +shortTitle: "2025" +intro: 'GraphQL schema changes from 2025.' +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- diff --git a/src/fixtures/fixtures/content/graphql/overview/changelog/2026.md b/src/fixtures/fixtures/content/graphql/overview/changelog/2026.md new file mode 100644 index 000000000000..d663bd9fcdc2 --- /dev/null +++ b/src/fixtures/fixtures/content/graphql/overview/changelog/2026.md @@ -0,0 +1,10 @@ +--- +title: "GraphQL changelog for 2026" +shortTitle: "2026" +intro: 'GraphQL schema changes from 2026.' +versions: + fpt: '*' + ghec: '*' + ghes: '*' +autogenerated: graphql +--- diff --git a/src/fixtures/fixtures/content/graphql/overview/changelog.md b/src/fixtures/fixtures/content/graphql/overview/changelog/index.md similarity index 79% rename from src/fixtures/fixtures/content/graphql/overview/changelog.md rename to src/fixtures/fixtures/content/graphql/overview/changelog/index.md index 248d15f70c0f..d93fa0f07b86 100644 --- a/src/fixtures/fixtures/content/graphql/overview/changelog.md +++ b/src/fixtures/fixtures/content/graphql/overview/changelog/index.md @@ -6,8 +6,12 @@ versions: ghec: '*' ghes: '*' autogenerated: graphql +children: + - /2026 + - /2025 + - /2024 --- Breaking changes include changes that will break existing queries or could affect the runtime behavior of clients. For a list of breaking changes and when they will occur, see our breaking changes log. - \ No newline at end of file + diff --git a/src/fixtures/helpers/turn-off-experiments.ts b/src/fixtures/helpers/turn-off-experiments.ts index cd4065eb7968..cfb9547b4e2e 100644 --- a/src/fixtures/helpers/turn-off-experiments.ts +++ b/src/fixtures/helpers/turn-off-experiments.ts @@ -44,18 +44,3 @@ export function turnOffExperimentsBeforeEach(test: typeof Test) { await turnOffExperimentsInPage(page) }) } - -export async function dismissCTAPopover(page: Page) { - // Set the CTA popover to permanently dismissed in localStorage - await page.evaluate(() => { - localStorage.setItem( - 'ctaPopoverState', - JSON.stringify({ - dismissedCount: 0, - lastDismissedAt: null, - permanentlyDismissed: true, - }), - ) - }) - await page.reload() -} diff --git a/src/fixtures/tests/categories-and-subcategory.ts b/src/fixtures/tests/categories-and-subcategory.ts index 01fb0a629124..fcbf6375987d 100644 --- a/src/fixtures/tests/categories-and-subcategory.ts +++ b/src/fixtures/tests/categories-and-subcategory.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest' import type { CheerioAPI } from 'cheerio' +import type { Element } from 'domhandler' import { getDOM, head } from '@/tests/helpers/e2etest' @@ -12,13 +13,13 @@ describe('subcategories', () => { const links = $('[data-testid=table-of-contents] a[href]') expect(links.length).toBeGreaterThan(0) // They all have the same prefix - const hrefs = links.map((i: number, el: any) => $(el).attr('href')).get() + const hrefs = links.map((i: number, el: Element) => $(el).attr('href')).get() expect( hrefs.every((href: string) => href.startsWith('/en/get-started/start-your-journey/')), ).toBeTruthy() // The all resolve to a 200 OK without redirects const responses = await Promise.all(hrefs.map((href: string) => head(href))) - expect(responses.every((r: any) => r.statusCode === 200)).toBeTruthy() + expect(responses.every((r: { statusCode: number }) => r.statusCode === 200)).toBeTruthy() }) test('actions/category/subcategory subcategory has its articles intro', async () => { @@ -27,7 +28,7 @@ describe('subcategories', () => { expect(lead).toMatch("Here's the intro for HubGit Actions.") const links = $('[data-testid=table-of-contents] a[href]') - const hrefs = links.map((i: number, el: any) => $(el).attr('href')).get() + const hrefs = links.map((i: number, el: Element) => $(el).attr('href')).get() expect(hrefs.every((href: string) => href.startsWith('/en/actions/category/'))).toBeTruthy() const firstArticleH2 = $('[data-testid=table-of-contents] h2').first() @@ -50,10 +51,10 @@ describe('categories', () => { const links = $('[data-testid=table-of-contents] a[href]') expect(links.length).toBeGreaterThan(0) // They all have the same prefix - const hrefs = links.map((i: number, el: any) => $(el).attr('href')).get() + const hrefs = links.map((i: number, el: Element) => $(el).attr('href')).get() expect(hrefs.every((href: string) => href.startsWith('/en/actions/category/'))).toBeTruthy() // The all resolve to a 200 OK without redirects const responses = await Promise.all(hrefs.map((href: string) => head(href))) - expect(responses.every((r: any) => r.statusCode === 200)).toBeTruthy() + expect(responses.every((r: { statusCode: number }) => r.statusCode === 200)).toBeTruthy() }) }) diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts index 6ae060d97614..b99559b46009 100644 --- a/src/fixtures/tests/playwright-rendering.spec.ts +++ b/src/fixtures/tests/playwright-rendering.spec.ts @@ -1,6 +1,6 @@ import dotenv from 'dotenv' import { test, expect } from '@playwright/test' -import { turnOffExperimentsInPage, dismissCTAPopover } from '../helpers/turn-off-experiments' +import { turnOffExperimentsInPage } from '../helpers/turn-off-experiments' import { HOVERCARDS_ENABLED, ANALYTICS_ENABLED } from '../../frame/lib/constants' // This exists for the benefit of local testing. @@ -22,7 +22,6 @@ test('view home page', async ({ page }) => { test('logo link keeps current version', async ({ page }) => { await page.goto('/enterprise-cloud@latest') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // Basically clicking into any page that isn't the home page for this version. await page.getByTestId('product').getByRole('link', { name: 'Get started' }).click() await expect(page).toHaveURL(/\/en\/enterprise-cloud@latest\/get-started/) @@ -58,7 +57,6 @@ test('do a search from home page and click on "Foo" page', async ({ page }) => { await page.goto('/') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // Use the search overlay await page.locator('[data-testid="search"]:visible').click() @@ -84,7 +82,6 @@ test('open search, and perform a general search', async ({ page }) => { await page.goto('/') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('serve playwright') @@ -182,7 +179,6 @@ test('search from enterprise-cloud and filter by top-level Fooing', async ({ pag await page.goto('/enterprise-cloud@latest') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // Use the search overlay await page.locator('[data-testid="search"]:visible').click() @@ -213,7 +209,6 @@ test.describe('platform picker', () => { test('switch operating systems', async ({ page }) => { await page.goto('/get-started/liquid/platform-specific') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await page.getByTestId('platform-picker').getByRole('link', { name: 'Mac' }).click() await expect(page).toHaveURL(/\?platform=mac/) @@ -230,7 +225,6 @@ test.describe('platform picker', () => { // default platform set to windows in fixture fronmatter await page.goto('/get-started/liquid/platform-specific') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Macintosh until 1999' }), ).not.toBeVisible() @@ -249,7 +243,6 @@ test.describe('platform picker', () => { test('remember last clicked OS', async ({ page }) => { await page.goto('/get-started/liquid/platform-specific') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await page.getByTestId('platform-picker').getByRole('link', { name: 'Windows' }).click() // Return and now the cookie should start us off on Windows again @@ -263,7 +256,6 @@ test.describe('tool picker', () => { test('switch tools', async ({ page }) => { await page.goto('/get-started/liquid/tool-specific') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await page.getByTestId('tool-picker').getByRole('link', { name: 'GitHub CLI' }).click() await expect(page).toHaveURL(/\?tool=cli/) @@ -289,7 +281,6 @@ test.describe('tool picker', () => { test('remember last clicked tool', async ({ page }) => { await page.goto('/get-started/liquid/tool-specific') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await page.getByTestId('tool-picker').getByRole('link', { name: 'Web browser' }).click() // Return and now the cookie should start us off with Web UI content again @@ -303,7 +294,6 @@ test.describe('tool picker', () => { // default tool set to webui in fixture frontmatter await page.goto('/get-started/liquid/tool-specific') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await expect( page.getByTestId('minitoc').getByRole('link', { name: 'Webui section' }), ).toBeVisible() @@ -353,7 +343,6 @@ test.describe('hover cards', () => { test('hover over link', async ({ page }) => { await page.goto('/pages/quickstart') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // hover over a link and check for intro content from hovercard await page @@ -416,7 +405,6 @@ test.describe('hover cards', () => { test('use keyboard shortcut to open hover card', async ({ page }) => { await page.goto('/pages/quickstart') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // Simply putting focus on the link should not open the hovercard await page @@ -449,7 +437,6 @@ test.describe('hover cards', () => { test('able to use Esc to close hovercard', async ({ page }) => { await page.goto('/pages/quickstart') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // hover over a link and check for intro content from hovercard await page @@ -598,7 +585,6 @@ test.describe('test nav at different viewports', () => { }) await page.goto('/get-started/foo/bar') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // header sign-up button is not visible await expect(page.getByTestId('header-signup')).not.toBeVisible() @@ -635,7 +621,6 @@ test.describe('test nav at different viewports', () => { }) await page.goto('/get-started/foo/bar') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // Use the search overlay await page.locator('[data-testid="mobile-search-button"]:visible').click() @@ -660,7 +645,6 @@ test.describe('test nav at different viewports', () => { }) await page.goto('/get-started/foo/bar') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) // Use the search overlay await page.locator('[data-testid="mobile-search-button"]:visible').click() @@ -921,7 +905,6 @@ test('open search, and ask Copilot (Ask AI) a question', async ({ page }) => { await page.goto('/') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('How do I create a Repository?') @@ -964,7 +947,6 @@ test('open search, Ask AI returns 400 error and shows general search results', a await page.goto('/') await turnOffExperimentsInPage(page) - await dismissCTAPopover(page) await page.locator('[data-testid="search"]:visible').click() await page.getByTestId('overlay-search-input').fill('foo') diff --git a/src/fixtures/tests/translations.ts b/src/fixtures/tests/translations.ts index 0682eecfcd71..a35fa3e346c5 100644 --- a/src/fixtures/tests/translations.ts +++ b/src/fixtures/tests/translations.ts @@ -1,5 +1,6 @@ import { describe, expect, test } from 'vitest' -import type { CheerioAPI } from 'cheerio' +import type { Cheerio, CheerioAPI } from 'cheerio' +import type { Element } from 'domhandler' import { TRANSLATIONS_FIXTURE_ROOT } from '@/frame/lib/constants' import { getDOM, head } from '@/tests/helpers/e2etest' @@ -19,14 +20,14 @@ describe('translations', () => { const links = $('[data-testid=product] a[href]') const hrefs = links - .filter((i: number, link: any) => { + .filter((i: number, link: Element) => { const href = $(link).attr('href') return href !== undefined && href.startsWith('/') }) - .map((i: number, link: any) => $(link)) + .map((i: number, link: Element) => $(link)) .get() const linkTexts = Object.fromEntries( - hrefs.map(($link: any) => [$link.attr('href'), $link.text()]), + hrefs.map(($link: Cheerio) => [$link.attr('href'), $link.text()]), ) expect(linkTexts['/ja/get-started']).toBe('はじめに') }) @@ -40,11 +41,11 @@ describe('translations', () => { test('internal links get prefixed with /ja', async () => { const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/link-rewriting') const links = $('#article-contents a[href]') - const jaLinks = links.filter((i: number, element: any) => { + const jaLinks = links.filter((i: number, element: Element) => { const href = $(element).attr('href') return href !== undefined && href.startsWith('/ja') }) - const enLinks = links.filter((i: number, element: any) => { + const enLinks = links.filter((i: number, element: Element) => { const href = $(element).attr('href') return href !== undefined && href.startsWith('/en') }) @@ -55,7 +56,7 @@ describe('translations', () => { test('internal links with AUTOTITLE resolves', async () => { const $: CheerioAPI = await getDOM('/ja/get-started/foo/autotitling') const links = $('#article-contents a[href]') - links.each((i: number, element: any) => { + links.each((i: number, element: Element) => { if ($(element).attr('href')?.includes('/ja/get-started/start-your-journey/hello-world')) { expect($(element).text()).toBe('こんにちは World') } @@ -73,7 +74,7 @@ describe('translations', () => { expect(paragraph).toMatch('mention of HubGit in Liquid') const tds = $('#article-contents td') - .map((i: number, element: any) => $(element).text()) + .map((i: number, element: Element) => $(element).text()) .get() expect(tds.length).toBe(2) expect(tds[1]).toBe('Not') @@ -88,7 +89,7 @@ describe('translations', () => { expect(paragraph).toMatch('mention of HubGit Enterprise Server in Liquid') const tds = $('#article-contents td') - .map((i: number, element: any) => $(element).text()) + .map((i: number, element: Element) => $(element).text()) .get() expect(tds.length).toBe(2) expect(tds[1]).toBe('Present') @@ -98,7 +99,7 @@ describe('translations', () => { test('automatic correction of bad AUTOTITLE in reusables', async () => { const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/hello-world') const links = $('#article-contents a[href]') - const texts = links.map((i: number, element: any) => $(element).text()).get() + const texts = links.map((i: number, element: Element) => $(element).text()).get() // That Japanese page uses AUTOTITLE links. Both in the main `.md` file // but also inside a reusable. // E.g. `["AUTOTITLE](/get-started/start-your-journey/hello-world)."` @@ -129,11 +130,11 @@ describe('translations', () => { const $: CheerioAPI = await getDOM('/ja/get-started/start-your-journey/hello-world') const links = $('#article-contents a[href]') const texts = links - .filter((i: number, element: any) => { + .filter((i: number, element: Element) => { const href = $(element).attr('href') return href !== undefined && href.includes('get-started/foo/bar') }) - .map((i: number, element: any) => $(element).text()) + .map((i: number, element: Element) => $(element).text()) .get() // Check that the text contains the essential parts rather than exact spacing const foundBarLink = texts.find( diff --git a/src/frame/components/context/CTAContext.tsx b/src/frame/components/context/CTAContext.tsx deleted file mode 100644 index 8f033c0a5cf8..000000000000 --- a/src/frame/components/context/CTAContext.tsx +++ /dev/null @@ -1,108 +0,0 @@ -// Context to keep track of a call to action (e.g. popover introducing a new feature) -// The state of the CTA will be stored in local storage, so it will persist across page reloads -// If `dismiss` is called, the CTA will not be shown again -import { - createContext, - useCallback, - useContext, - useEffect, - useState, - PropsWithChildren, -} from 'react' - -type CTAPopoverState = { - isOpen: boolean - initializeCTA: () => void // Call to "open" the CTA if it's not already been dismissed by the user - dismiss: () => void // Call to "close" the CTA and store the dismissal in local storage. Will be shown again after 24 hours for a max of 3 times - permanentDismiss: () => void // Call to permanently dismiss the CTA and store the dismissal in local storage -} - -type StoredValue = { - dismissedCount: number - lastDismissedAt: number | null - permanentlyDismissed: boolean -} - -const CTAPopoverContext = createContext(undefined) - -const STORAGE_KEY = 'ctaPopoverState' -const MAX_DISMISSES = 3 -const HIDE_CTA_FOR_MS = 24 * 60 * 60 * 1000 // Every 24 hours we show the CTA again, unless permanently dismissed - -const shouldHide = (): boolean => { - if (typeof window === 'undefined') return false // SSR guard - try { - const raw = localStorage.getItem(STORAGE_KEY) - if (!raw) return false - const parsed = JSON.parse(raw) as StoredValue - if (parsed.permanentlyDismissed) return true - if (parsed.dismissedCount >= MAX_DISMISSES) return true - if (parsed.lastDismissedAt && Date.now() - parsed.lastDismissedAt < HIDE_CTA_FOR_MS) return true - return false - } catch { - return false // corruption / quota / disabled storage - } -} - -const readStored = (): StoredValue => { - const emptyValue = { dismissedCount: 0, lastDismissedAt: null, permanentlyDismissed: false } - try { - const raw = localStorage.getItem(STORAGE_KEY) - if (!raw) { - return emptyValue - } - return JSON.parse(raw) as StoredValue - } catch { - return emptyValue // corruption / quota / disabled storage - } -} - -const writeStored = (v: StoredValue) => { - try { - localStorage.setItem(STORAGE_KEY, JSON.stringify(v)) - } catch { - /* ignore */ - } -} - -export function CTAPopoverProvider({ children }: PropsWithChildren) { - // We start closed because we might only want to "turn on" the CTA if an experiment is active - const [isOpen, setIsOpen] = useState(false) - - const dismiss = useCallback(() => { - const stored = readStored() - writeStored({ - ...stored, - dismissedCount: stored.dismissedCount + 1, - lastDismissedAt: Date.now(), - }) - setIsOpen(false) - }, []) - - const permanentDismiss = useCallback(() => { - const stored = readStored() - writeStored({ ...stored, permanentlyDismissed: true }) - setIsOpen(false) - }, []) - - const initializeCTA = useCallback(() => { - setIsOpen(!shouldHide()) - }, []) - - // Wrap in a useEffect to avoid a hydration mismatch (SSR guard) - useEffect(() => { - setIsOpen(!shouldHide()) - }, []) - - return ( - - {children} - - ) -} - -export const useCTAPopoverContext = () => { - const ctx = useContext(CTAPopoverContext) - if (!ctx) throw new Error('useCTAPopoverContext must be used inside ') - return ctx -} diff --git a/src/frame/components/page-header/Header.tsx b/src/frame/components/page-header/Header.tsx index 92b69ac35fda..b4ef9d70faac 100644 --- a/src/frame/components/page-header/Header.tsx +++ b/src/frame/components/page-header/Header.tsx @@ -19,7 +19,6 @@ import { HeaderSearchAndWidgets } from './HeaderSearchAndWidgets' import { useInnerWindowWidth } from './hooks/useInnerWindowWidth' import { useMultiQueryParams } from '@/search/components/hooks/useMultiQueryParams' import { SearchOverlayContainer } from '@/search/components/input/SearchOverlayContainer' -import { useCTAPopoverContext } from '@/frame/components/context/CTAContext' import { useSearchOverlayContext } from '@/search/components/context/SearchOverlayContext' import styles from './Header.module.scss' @@ -45,7 +44,6 @@ export const Header = () => { const returnFocusRef = useRef(null) const searchButtonRefLarge = useRef(null) const searchButtonRefSmall = useRef(null) - const { initializeCTA } = useCTAPopoverContext() const { isSearchOpen, setIsSearchOpen } = useSearchOverlayContext() // The lg breakpoint (1012px) determines which search button is visible. @@ -74,9 +72,6 @@ export const Header = () => { /> ) - // Initialize the CTA(s) - initializeCTA() - useEffect(() => { function onScroll() { setScroll(window.scrollY > 10) diff --git a/src/frame/pages/app.tsx b/src/frame/pages/app.tsx index 5286bdc70310..4fd98004f740 100644 --- a/src/frame/pages/app.tsx +++ b/src/frame/pages/app.tsx @@ -17,7 +17,6 @@ import { } from '@/languages/components/LanguagesContext' import { useTheme } from '@/color-schemes/components/useTheme' import { SharedUIContextProvider } from '@/frame/components/context/SharedUIContext' -import { CTAPopoverProvider } from '@/frame/components/context/CTAContext' import { ClientSideHashFocus } from '@/frame/components/ClientSideHashFocus' import type { ExtendedRequest } from '@/types' @@ -146,10 +145,8 @@ const MyApp = ({ Component, pageProps, languagesContext, stagingName }: MyAppPro > - - - - + + diff --git a/src/frame/start-server.ts b/src/frame/start-server.ts index fca1713301e4..68bf88385ad2 100644 --- a/src/frame/start-server.ts +++ b/src/frame/start-server.ts @@ -65,9 +65,33 @@ async function startServer() { process.once('SIGTERM', () => { logger.info('Received SIGTERM, beginning graceful shutdown', { pid: process.pid, port }) + + // Force-close idle keep-alive sockets so server.close() doesn't hang + // waiting for them to disconnect naturally. + try { + server.closeIdleConnections() + } catch (err) { + logger.warn('closeIdleConnections failed (server may not be running)', { error: err }) + } + server.close(() => { logger.info('HTTP server closed') }) + + // If in-flight requests haven't drained within 25s, force exit. + // Kubernetes sends SIGKILL at terminationGracePeriodSeconds (60s), + // but the deploy controller may time out before that if an old pod + // stays in "Terminating" state too long. The preStop hook sleeps 5s, + // so 25s here keeps total shutdown well under the 60s grace period. + setTimeout(() => { + logger.warn('Graceful shutdown timed out, forcing exit') + try { + server.closeAllConnections() + } catch (err) { + logger.warn('closeAllConnections failed (server may not be running)', { error: err }) + } + process.exit(0) + }, 25_000).unref() }) return server diff --git a/src/graphql/components/Changelog.tsx b/src/graphql/components/Changelog.tsx index cc1d06777803..a6f1d13a6ecb 100644 --- a/src/graphql/components/Changelog.tsx +++ b/src/graphql/components/Changelog.tsx @@ -8,9 +8,29 @@ import styles from '@/frame/components/ui/MarkdownContent/MarkdownContent.module type Props = { changelogItems: ChangelogItemT[] + years?: number[] + currentYear?: number } -export function Changelog({ changelogItems }: Props) { +function YearNav({ years, currentYear }: { years: number[]; currentYear: number }) { + return ( + + ) +} + +export function Changelog({ changelogItems, years, currentYear }: Props) { const slugger = new GithubSlugger() const changes = changelogItems.map((item, index) => { @@ -45,14 +65,21 @@ export function Changelog({ changelogItems }: Props) { {(item.upcomingChanges || []).map((change, changeIndex) => (

{change.title}

- {change.changes.map((changeItem) => ( -
  • - ))} +
      + {change.changes.map((changeItem) => ( +
    • + ))} +
    ))} ) }) - return
    {changes}
    + return ( +
    + {years && currentYear && } + {changes} +
    + ) } diff --git a/src/graphql/lib/index.ts b/src/graphql/lib/index.ts index 572be9573c54..e6ebf5cc056b 100644 --- a/src/graphql/lib/index.ts +++ b/src/graphql/lib/index.ts @@ -50,6 +50,26 @@ export function getGraphqlChangelog(version: string): any { return changelog.get(graphqlVersion) } +/** + * Return changelog entries filtered by year. + */ +export function getGraphqlChangelogByYear(version: string, year: number): any[] { + const all = getGraphqlChangelog(version) as Array<{ date: string }> + return all.filter((entry) => entry.date.startsWith(String(year))) +} + +/** + * Return the distinct years present in the changelog, sorted descending (newest first). + */ +export function getGraphqlChangelogYears(version: string): number[] { + const all = getGraphqlChangelog(version) as Array<{ date: string }> + const years = new Set() + for (const entry of all) { + years.add(Number(entry.date.slice(0, 4))) + } + return [...years].sort((a, b) => b - a) +} + // Using any for return type as the breaking changes structure is dynamically loaded from JSON export function getGraphqlBreakingChanges(version: string): any { const graphqlVersion: string = getGraphqlVersion(version) diff --git a/src/graphql/pages/changelog-year.tsx b/src/graphql/pages/changelog-year.tsx new file mode 100644 index 000000000000..79df6419c61b --- /dev/null +++ b/src/graphql/pages/changelog-year.tsx @@ -0,0 +1,74 @@ +import { GetServerSideProps } from 'next' +import type { ExtendedRequest } from '@/types' +import type { ServerResponse } from 'http' + +import { MainContextT, MainContext, getMainContext } from '@/frame/components/context/MainContext' +import { AutomatedPage } from '@/automated-pipelines/components/AutomatedPage' +import { + AutomatedPageContext, + AutomatedPageContextT, + getAutomatedPageContextFromRequest, +} from '@/automated-pipelines/components/AutomatedPageContext' +import { Changelog } from '@/graphql/components/Changelog' +import { ChangelogItemT } from '@/graphql/components/types' +import { stripParagraphWrappers } from '@/graphql/pages/changelog' + +type Props = { + mainContext: MainContextT + schema: ChangelogItemT[] + automatedPageContext: AutomatedPageContextT + years: number[] + currentYear: number +} + +export default function GraphqlChangelogYear({ + mainContext, + schema, + automatedPageContext, + years, + currentYear, +}: Props) { + const content = + return ( + + + {content} + + + ) +} + +export const getServerSideProps: GetServerSideProps = async (context) => { + const { getGraphqlChangelogByYear, getGraphqlChangelogYears } = + await import('@/graphql/lib/index') + const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items') + + const req = context.req as unknown as ExtendedRequest + const res = context.res as unknown as ServerResponse + const currentVersion = context.query.versionId as string + const year = Number(context.query.year) + + const years = getGraphqlChangelogYears(currentVersion) + if (!years.includes(year)) { + return { notFound: true } + } + + const schema = getGraphqlChangelogByYear(currentVersion, year) as ChangelogItemT[] + + const automatedPageContext = getAutomatedPageContextFromRequest(req) + const titles = schema.map((item) => `Schema changes for ${item.date}`) + const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context!, 2) + automatedPageContext.miniTocItems.push(...changelogMiniTocItems) + + stripParagraphWrappers(schema) + + return { + props: { + mainContext: await getMainContext(req, res), + automatedPageContext, + schema, + years, + currentYear: year, + }, + } +} diff --git a/src/graphql/pages/changelog.tsx b/src/graphql/pages/changelog.tsx index 8dd1ce67fa7d..46bc884988a1 100644 --- a/src/graphql/pages/changelog.tsx +++ b/src/graphql/pages/changelog.tsx @@ -16,10 +16,18 @@ type Props = { mainContext: MainContextT schema: ChangelogItemT[] automatedPageContext: AutomatedPageContextT + years: number[] + currentYear: number } -export default function GraphqlChangelog({ mainContext, schema, automatedPageContext }: Props) { - const content = +export default function GraphqlChangelog({ + mainContext, + schema, + automatedPageContext, + years, + currentYear, +}: Props) { + const content = return ( @@ -30,34 +38,41 @@ export default function GraphqlChangelog({ mainContext, schema, automatedPageCon } export const getServerSideProps: GetServerSideProps = async (context) => { - const { getGraphqlChangelog } = await import('@/graphql/lib/index') + const { getGraphqlChangelogByYear, getGraphqlChangelogYears } = + await import('@/graphql/lib/index') const { getAutomatedPageMiniTocItems } = await import('@/frame/lib/get-mini-toc-items') const req = context.req as unknown as ExtendedRequest const res = context.res as unknown as ServerResponse const currentVersion = context.query.versionId as string - const schema = getGraphqlChangelog(currentVersion) as ChangelogItemT[] + const years = getGraphqlChangelogYears(currentVersion) + const currentYear = years[0] + const schema = getGraphqlChangelogByYear(currentVersion, currentYear) as ChangelogItemT[] if (!schema) throw new Error('No graphql free-pro-team changelog schema found.') - // Gets the miniTocItems in the article context. At this point it will only - // include miniTocItems that exist in Markdown pages in - // content/graphql/reference/* + const automatedPageContext = getAutomatedPageContextFromRequest(req) const titles = schema.map((item) => `Schema changes for ${item.date}`) const changelogMiniTocItems = await getAutomatedPageMiniTocItems(titles, req.context!, 2) - // Update the existing context to include the miniTocItems from GraphQL automatedPageContext.miniTocItems.push(...changelogMiniTocItems) - // All groups in the schema have a change.changes array of strings that are - // all the HTML output from a Markdown conversion. E.g. - // `

    Field filename was added to object type IssueTemplate

    ` - // Change these to just be the inside of the

    tag. - // `Field filename was added to object type IssueTemplate` - // This makes the serialized state data smaller and it makes it possible - // to render it as... - // - //

  • Field filename was added to object type IssueTemplate
  • - // - // ...without the additional

    . + stripParagraphWrappers(schema) + + return { + props: { + mainContext: await getMainContext(req, res), + automatedPageContext, + schema, + years, + currentYear, + }, + } +} + +/** + * Strip wrapping `

    ` tags from HTML change descriptions to allow + * rendering as `

  • ` content without nested block elements. + */ +export function stripParagraphWrappers(schema: ChangelogItemT[]) { for (const item of schema) { for (const group of [item.schemaChanges, item.previewChanges, item.upcomingChanges]) { for (const change of group) { @@ -68,12 +83,4 @@ export const getServerSideProps: GetServerSideProps = async (context) => } } } - - return { - props: { - mainContext: await getMainContext(req, res), - automatedPageContext, - schema, - }, - } } diff --git a/src/graphql/scripts/build-changelog.ts b/src/graphql/scripts/build-changelog.ts index e594f77d64d2..11acb66f209b 100644 --- a/src/graphql/scripts/build-changelog.ts +++ b/src/graphql/scripts/build-changelog.ts @@ -1,6 +1,7 @@ import { diff, ChangeType, Change } from '@graphql-inspector/core' import { loadSchema } from '@graphql-tools/load' import fs from 'fs' +import nodePath from 'path' import { renderContent } from '@/content-render/index' interface UpcomingChange { @@ -75,6 +76,43 @@ export function prependDatedEntry(changelogEntry: ChangelogEntry, targetPath: st previousChangelog.unshift(changelogEntry) // rewrite the updated changelog fs.writeFileSync(targetPath, JSON.stringify(previousChangelog, null, 2)) + + // Ensure a content page exists for this entry's year + const year = todayString.slice(0, 4) + ensureYearPage(year) +} + +const DEFAULT_CHANGELOG_CONTENT_DIR = nodePath.join('content', 'graphql', 'overview', 'changelog') + +/** + * If a year-specific content page doesn't exist yet (e.g. 2027.md), + * create it and prepend it to the children list in index.md. + */ +export function ensureYearPage( + year: string, + contentDir: string = DEFAULT_CHANGELOG_CONTENT_DIR, +): void { + const yearPagePath = nodePath.join(contentDir, `${year}.md`) + if (fs.existsSync(yearPagePath)) return + + const yearPage = [ + '---', + `title: "GraphQL changelog for ${year}"`, + `shortTitle: "${year}"`, + `intro: 'GraphQL schema changes from ${year}.'`, + 'versions:', + " fpt: '*'", + 'autogenerated: graphql', + '---', + '', + ].join('\n') + fs.writeFileSync(yearPagePath, yearPage) + + // Prepend the new year to children in index.md + const indexPath = nodePath.join(contentDir, 'index.md') + const indexContent = fs.readFileSync(indexPath, 'utf8') + const updated = indexContent.replace(/^(children:\n)/m, `$1 - /${year}\n`) + fs.writeFileSync(indexPath, updated) } /** @@ -359,4 +397,10 @@ export function getIgnoredChangesSummary(): IgnoredChangesSummary | null { return summary } -export default { createChangelogEntry, cleanPreviewTitle, previewAnchor, prependDatedEntry } +export default { + createChangelogEntry, + cleanPreviewTitle, + previewAnchor, + prependDatedEntry, + ensureYearPage, +} diff --git a/src/graphql/tests/build-changelog.ts b/src/graphql/tests/build-changelog.ts index f5c131468bad..139520b572dc 100644 --- a/src/graphql/tests/build-changelog.ts +++ b/src/graphql/tests/build-changelog.ts @@ -9,6 +9,7 @@ import { cleanPreviewTitle, previewAnchor, prependDatedEntry, + ensureYearPage, getLastIgnoredChanges, getIgnoredChangesSummary, type ChangelogEntry, @@ -265,6 +266,48 @@ describe('updating the changelog file', () => { }) }) +describe('ensureYearPage', () => { + const tmpDir = 'src/graphql/tests/fixtures/tmp-changelog' + + afterEach(async () => { + await fs.rm(tmpDir, { recursive: true, force: true }) + }) + + test('creates a new year page and updates index.md children', async () => { + await fs.mkdir(tmpDir, { recursive: true }) + await fs.writeFile( + `${tmpDir}/index.md`, + ['---', 'title: Changelog', 'children:', ' - /2026', ' - /2025', '---', ''].join('\n'), + ) + + ensureYearPage('2027', tmpDir) + + const yearPage = await fs.readFile(`${tmpDir}/2027.md`, 'utf8') + expect(yearPage).toContain('title: "GraphQL changelog for 2027"') + expect(yearPage).toContain('shortTitle: "2027"') + expect(yearPage).toContain('autogenerated: graphql') + + const indexContent = await fs.readFile(`${tmpDir}/index.md`, 'utf8') + expect(indexContent).toContain(' - /2027\n - /2026') + }) + + test('is a no-op when the year page already exists', async () => { + await fs.mkdir(tmpDir, { recursive: true }) + const indexContent = ['---', 'children:', ' - /2026', '---', ''].join('\n') + await fs.writeFile(`${tmpDir}/index.md`, indexContent) + await fs.writeFile(`${tmpDir}/2026.md`, '---\ntitle: existing\n---\n') + + ensureYearPage('2026', tmpDir) + + // Should not modify the existing file + const yearPage = await fs.readFile(`${tmpDir}/2026.md`, 'utf8') + expect(yearPage).toContain('title: existing') + // index.md should be unchanged + const updatedIndex = await fs.readFile(`${tmpDir}/index.md`, 'utf8') + expect(updatedIndex).toBe(indexContent) + }) +}) + describe('ignored changes tracking', () => { test('tracks ignored change types', async () => { const oldSchemaString = ` diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index fca1adefd0f9..2e1eb3fed434 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -26,6 +26,10 @@ export function correctTranslatedContentStrings( // Remove colon prefix on Liquid tags: `{%:` → `{%` content = content.replace(/\{%:/g, '{%') + // `{% siVersion X %}` — Spanish "si" (if) fused with "Version" = ifversion + content = content.replaceAll('{% siVersion ', '{% ifversion ') + content = content.replaceAll('{%- siVersion ', '{%- ifversion ') + content = content.replaceAll('{% vulnerables variables.', '{% data variables.') content = content.replaceAll('{% datos variables', '{% data variables') content = content.replaceAll('{% de datos variables', '{% data variables') @@ -235,9 +239,17 @@ export function correctTranslatedContentStrings( } if (context.code === 'pt') { + // `{%–` — en-dash (U+2013) used instead of hyphen in `{%-` trim modifier + content = content.replaceAll('{%–', '{%-') + content = content.replaceAll('{% dados variables', '{% data variables') content = content.replaceAll('{% de dados variables', '{% data variables') content = content.replaceAll('{% dados reusables', '{% data reusables') + // `{% dadosvariables` / `{% datavariables` — no space between "dados"/"data" and "variables" + content = content.replaceAll('{% dadosvariables', '{% data variables') + content = content.replaceAll('{%- dadosvariables', '{%- data variables') + content = content.replaceAll('{% datavariables', '{% data variables') + content = content.replaceAll('{%- datavariables', '{%- data variables') // Fully translated reusables path: `{% dados reutilizáveis.X.Y %}` → `{% data reusables.X.Y %}` content = content.replaceAll('{% dados reutilizáveis.', '{% data reusables.') // Translated path segment inside reusables path: `repositórios` → `repositories` @@ -292,6 +304,9 @@ export function correctTranslatedContentStrings( if (context.code === 'zh') { content = content.replaceAll('{% 数据variables', '{% data variables') content = content.replaceAll('{% 数据 variables', '{% data variables') + // `{%数据variables` — no space between `{%` and 数据 (data) + content = content.replaceAll('{%数据variables', '{% data variables') + content = content.replaceAll('{%数据 variables', '{% data variables') // Order matters: the more specific `s.` variant must run first to // avoid the broader rule producing a double-s (`reusabless`). content = content.replaceAll('{% 数据可重用s.', '{% data reusables.') @@ -467,6 +482,12 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% données variables', '{% data variables') content = content.replaceAll('{% données réutilisables.', '{% data reusables.') content = content.replaceAll('{% variables de données.', '{% data variables.') + // `{% de données variables.` — preposition "de" prepended to "données variables" + content = content.replaceAll('{% de données variables.', '{% data variables.') + content = content.replaceAll('{%- de données variables.', '{%- data variables.') + // `{% de data variables.` — partially-corrected form (données already fixed to data) + content = content.replaceAll('{% de data variables.', '{% data variables.') + content = content.replaceAll('{%- de data variables.', '{%- data variables.') content = content.replaceAll('{% autre %}', '{% else %}') content = content.replaceAll('{%- autre %}', '{%- else %}') content = content.replaceAll('{% brut %}', '{% raw %}') @@ -571,6 +592,10 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% 옥티콘 ', '{% octicon ') content = content.replaceAll('{%- 옥티콘 ', '{%- octicon ') + // `{% data Variables.` — capital V in "Variables" (Korean translator capitalised the word) + content = content.replaceAll('{% data Variables.', '{% data variables.') + content = content.replaceAll('{%- data Variables.', '{%- data variables.') + // Korean translation of github-glossary.md content = content.replaceAll('{{ 용어집.term }}', '{{ glossary.term }}') // `{% 데이터 재사용.` — Korean translation of "data reusables" path @@ -662,8 +687,23 @@ export function correctTranslatedContentStrings( // `{% Endnotiz %}` — "end note" = endnote content = content.replaceAll('{% Endnotiz %}', '{% endnote %}') content = content.replaceAll('{%- Endnotiz %}', '{%- endnote %}') + // `{% data-variables.` — hyphen used instead of space between "data" and "variables" + content = content.replaceAll('{% data-variables.', '{% data variables.') + content = content.replaceAll('{%- data-variables.', '{%- data variables.') + // `{%- Datenworkflow variables.` — compound "Datenworkflow" (data workflow) = data + content = content.replaceAll('{%- Datenworkflow variables.', '{%- data variables.') + content = content.replaceAll('{% Datenworkflow variables.', '{% data variables.') + // `{% ifec ` — truncated/corrupted form of "ifversion" + content = content.replaceAll('{% ifec ', '{% ifversion ') + content = content.replaceAll('{%- ifec ', '{%- ifversion ') + // `{% andere %}` / `{%- andere %}` — German "andere" (other) = else + content = content.replaceAll('{% andere %}', '{% else %}') + content = content.replaceAll('{%- andere %}', '{%- else %}') // `{% Dateninstanz` — "data instance" = data content = content.replaceAll('{% Dateninstanz ', '{% data ') + // `{% Datenauflistung ` — "data listing" (compound) = data + content = content.replaceAll('{% Datenauflistung ', '{% data ') + content = content.replaceAll('{%- Datenauflistung ', '{%- data ') // `{% ifversion-Sicherheitskonfigurationen %}` — hyphenated compound content = content.replaceAll( '{% ifversion-Sicherheitskonfigurationen %}', @@ -731,6 +771,20 @@ export function correctTranslatedContentStrings( content = content.replaceAll('{% dada variables', '{% data variables') content = content.replaceAll('{% % data', '{% data') + // Leading dot in `{% data` paths: `{% data .variables.X %}` / `{% data .reusables.X %}` + // — translator inserted a stray dot. Affects ja, pt, zh. + content = content.replaceAll('{% data .variables.', '{% data variables.') + content = content.replaceAll('{%- data .variables.', '{%- data variables.') + content = content.replaceAll('{% data .reusables.', '{% data reusables.') + content = content.replaceAll('{%- data .reusables.', '{%- data reusables.') + + // Singular "variable" / "reusable" in `{% data` paths: + // `{% data variable.product.X %}` → `{% data variables.product.X %}` (es, zh) + content = content.replaceAll('{% data variable.', '{% data variables.') + content = content.replaceAll('{%- data variable.', '{%- data variables.') + content = content.replaceAll('{% data reusable.', '{% data reusables.') + content = content.replaceAll('{%- data reusable.', '{%- data reusables.') + // Double-quote corruption in href attributes content = content.replace(/href=""https:\/\//g, 'href="https://') diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts index 308391c5bb5b..26f5382ff528 100644 --- a/src/languages/tests/correct-translation-content.ts +++ b/src/languages/tests/correct-translation-content.ts @@ -131,6 +131,11 @@ describe('correctTranslatedContentStrings', () => { '{% data reusables.profile.access_org %}', ) }) + + test('fixes siVersion → ifversion', () => { + expect(fix('{% siVersion productos-ghas %}', 'es')).toBe('{% ifversion productos-ghas %}') + expect(fix('{%- siVersion productos-ghas %}', 'es')).toBe('{%- ifversion productos-ghas %}') + }) }) // ─── JAPANESE (ja) ────────────────────────────────────────────────── @@ -318,6 +323,26 @@ describe('correctTranslatedContentStrings', () => { ) }) + test('fixes en-dash in trim modifier', () => { + // `{%–` — en-dash (U+2013) used instead of hyphen in `{%-` trim modifier + expect(fix('{%– ifversion projects-v1 %}', 'pt')).toBe('{%- ifversion projects-v1 %}') + expect(fix('{%– endif %}', 'pt')).toBe('{%- endif %}') + }) + + test('fixes datavariables / dadosvariables (no space)', () => { + // `{% datavariables` — no space between "data" and "variables" (post-translation) + expect(fix('{% datavariables.product.github %}', 'pt')).toBe( + '{% data variables.product.github %}', + ) + expect(fix('{%- datavariables.product.github %}', 'pt')).toBe( + '{%- data variables.product.github %}', + ) + // `{% dadosvariables` — Portuguese "dados" fused with "variables" + expect(fix('{% dadosvariables.product.github %}', 'pt')).toBe( + '{% data variables.product.github %}', + ) + }) + test('fixes translated else variants', () => { expect(fix('{% senão %}', 'pt')).toBe('{% else %}') expect(fix('{%- senão %}', 'pt')).toBe('{%- else %}') @@ -434,6 +459,13 @@ describe('correctTranslatedContentStrings', () => { '{% data variables.product.github %}', ) expect(fix('{% 数据可重用s.foo %}', 'zh')).toBe('{% data reusables.foo %}') + // No space between `{%` and 数据 + expect(fix('{%数据variables.product.github%}', 'zh')).toBe( + '{% data variables.product.github%}', + ) + expect(fix('{%数据 variables.product.github%}', 'zh')).toBe( + '{% data variables.product.github%}', + ) }) test('fixes translated else and raw', () => { @@ -687,6 +719,14 @@ describe('correctTranslatedContentStrings', () => { '{% data variables.product.github %}', ) expect(fix('{% données reusables.foo %}', 'fr')).toBe('{% data reusables.foo %}') + // `{% de données variables.` — preposition "de" prepended + expect(fix('{% de données variables.product.github %}', 'fr')).toBe( + '{% data variables.product.github %}', + ) + // `{% de data variables.` — partially-corrected form + expect(fix('{% de data variables.product.github %}', 'fr')).toBe( + '{% data variables.product.github %}', + ) }) test('fixes translated else', () => { @@ -889,6 +929,15 @@ describe('correctTranslatedContentStrings', () => { expect(fix('{% 주석 끝 %}', 'ko')).toBe('{% endnote %}') expect(fix('{%- 주석 끝 %}', 'ko')).toBe('{%- endnote %}') }) + + test('fixes capitalized Variables → data variables', () => { + expect(fix('{% data Variables.product.github %}', 'ko')).toBe( + '{% data variables.product.github %}', + ) + expect(fix('{%- data Variables.product.github %}', 'ko')).toBe( + '{%- data variables.product.github %}', + ) + }) }) // ─── GERMAN (de) ────────────────────────────────────────────────── @@ -1088,6 +1137,44 @@ describe('correctTranslatedContentStrings', () => { '{% ifversion enterprise-installed-apps %}', ) }) + + test('fixes data-variables (hyphen instead of space)', () => { + expect(fix('{% data-variables.product.github %}', 'de')).toBe( + '{% data variables.product.github %}', + ) + expect(fix('{%- data-variables.product.github %}', 'de')).toBe( + '{%- data variables.product.github %}', + ) + }) + + test('fixes Datenworkflow variables → data variables', () => { + expect(fix('{%- Datenworkflow variables.product.prodname_actions %}', 'de')).toBe( + '{%- data variables.product.prodname_actions %}', + ) + expect(fix('{% Datenworkflow variables.product.prodname_actions %}', 'de')).toBe( + '{% data variables.product.prodname_actions %}', + ) + }) + + test('fixes ifec → ifversion', () => { + expect(fix('{% ifec ghec %}', 'de')).toBe('{% ifversion ghec %}') + expect(fix('{%- ifec ghec %}', 'de')).toBe('{%- ifversion ghec %}') + }) + + test('fixes andere → else', () => { + expect(fix('{% andere %}', 'de')).toBe('{% else %}') + expect(fix('{%- andere %}', 'de')).toBe('{%- else %}') + }) + + test('fixes Datenauflistung → data', () => { + // `{% Datenauflistung variables.X %}` — "data listing" compound = data + expect(fix('{% Datenauflistung variables.product.github %}', 'de')).toBe( + '{% data variables.product.github %}', + ) + expect(fix('{%- Datenauflistung variables.product.github %}', 'de')).toBe( + '{%- data variables.product.github %}', + ) + }) }) describe('Generic fixes (all languages)', () => { @@ -1108,6 +1195,28 @@ describe('correctTranslatedContentStrings', () => { ) }) + test('fixes leading dot in {% data paths', () => { + // `{% data .variables.X %}` — translator inserted a stray dot + expect(fix('{% data .variables.product.prodname_ghe_server %}', 'ja')).toBe( + '{% data variables.product.prodname_ghe_server %}', + ) + expect(fix('{%- data .variables.product.github %}', 'pt')).toBe( + '{%- data variables.product.github %}', + ) + expect(fix('{% data .reusables.foo.bar %}', 'zh')).toBe('{% data reusables.foo.bar %}') + }) + + test('fixes singular variable / reusable in {% data paths', () => { + // `{% data variable.product.X %}` (singular) → `{% data variables.product.X %}` + expect(fix('{% data variable.product.prodname_container_registry %}', 'zh')).toBe( + '{% data variables.product.prodname_container_registry %}', + ) + expect(fix('{%- data variable.product.github %}', 'es')).toBe( + '{%- data variables.product.github %}', + ) + expect(fix('{% data reusable.foo.bar %}', 'fr')).toBe('{% data reusables.foo.bar %}') + }) + test('fixes capitalized platform tags across all languages', () => { expect(fix('{% Windows %}', 'zh')).toBe('{% windows %}') expect(fix('{% Eclipse %}', 'zh')).toBe('{% eclipse %}') diff --git a/src/pages/[versionId]/graphql/overview/changelog/[year].tsx b/src/pages/[versionId]/graphql/overview/changelog/[year].tsx new file mode 100644 index 000000000000..b3bfdb624f0e --- /dev/null +++ b/src/pages/[versionId]/graphql/overview/changelog/[year].tsx @@ -0,0 +1 @@ +export { default, getServerSideProps } from '@/graphql/pages/changelog-year' diff --git a/src/pages/[versionId]/graphql/overview/changelog.tsx b/src/pages/[versionId]/graphql/overview/changelog/index.tsx similarity index 100% rename from src/pages/[versionId]/graphql/overview/changelog.tsx rename to src/pages/[versionId]/graphql/overview/changelog/index.tsx diff --git a/src/search/components/input/AskAIResults.tsx b/src/search/components/input/AskAIResults.tsx index ec248e622264..4df1b247c23d 100644 --- a/src/search/components/input/AskAIResults.tsx +++ b/src/search/components/input/AskAIResults.tsx @@ -22,7 +22,6 @@ import { sendEvent, uuidv4 } from '@/events/components/events' import { EventType } from '@/events/types' import { generateAISearchLinksJson } from '../helpers/ai-search-links-json' import { ASK_AI_EVENT_GROUP } from '@/events/components/event-groups' -import { useCTAPopoverContext } from '@/frame/components/context/CTAContext' import type { AIReference } from '../types' @@ -83,7 +82,6 @@ export function AskAIResults({ aiCouldNotAnswer: boolean connectedEventId?: string }>('ai-query-cache', 1000, 7) - const { isOpen: isCTAOpen, permanentDismiss: permanentlyDismissCTA } = useCTAPopoverContext() let copyUrl = `` if (window?.location?.href) { @@ -145,12 +143,6 @@ export function AskAIResults({ setResponseLoading(true) disclaimerRef.current?.focus() - // We permanently dismiss the CTA after performing an AI Search because the - // user has tried it and doesn't require additional CTA prompting to try it - if (isCTAOpen) { - permanentlyDismissCTA() - } - const cachedData = getItem(query, version, router.locale || 'en') if (cachedData) { setMessage(cachedData.message) diff --git a/src/workflows/projects.ts b/src/workflows/projects.ts index 398b9e1924b8..88c24b987539 100644 --- a/src/workflows/projects.ts +++ b/src/workflows/projects.ts @@ -2,10 +2,68 @@ import { graphql } from '@octokit/graphql' // Shared functions for managing projects (memex) +export interface ProjectV2FieldNode { + name: string + id: string + options?: Array<{ name: string; id: string }> +} + +export interface ProjectV2Data { + organization: { + projectV2: { + id: string + fields: { + nodes: ProjectV2FieldNode[] + } + } + } +} + +interface TeamMemberData { + organization: { + team: { + members: { + nodes: Array<{ login: string }> + } + } + } +} + +interface OrgMemberData { + user: { + organization: { name: string } | null + } +} + +interface MutationResult { + [key: string]: { item: { id: string } } +} + +export interface FileNode { + path: string + additions: number + deletions: number +} + +export interface ItemData { + item: { + __typename: string + files: { + nodes: FileNode[] + } + author?: { + login: string + } + assignees?: { + nodes: Array<{ login: string }> + } + } +} + // Pull out the node ID of a project field -export function findFieldID(fieldName: string, data: Record) { +export function findFieldID(fieldName: string, data: ProjectV2Data) { const field = data.organization.projectV2.fields.nodes.find( - (fieldNode: Record) => fieldNode.name === fieldName, + (fieldNode) => fieldNode.name === fieldName, ) if (field && field.id) { @@ -19,18 +77,16 @@ export function findFieldID(fieldName: string, data: Record) { export function findSingleSelectID( singleSelectName: string, fieldName: string, - data: Record, + data: ProjectV2Data, ) { const field = data.organization.projectV2.fields.nodes.find( - (fieldData: Record) => fieldData.name === fieldName, + (fieldData) => fieldData.name === fieldName, ) if (!field) { throw new Error(`A field called "${fieldName}" was not found. Check if the field was renamed.`) } - const singleSelect = field.options.find( - (option: Record) => option.name === singleSelectName, - ) + const singleSelect = field.options?.find((option) => option.name === singleSelectName) if (singleSelect && singleSelect.id) { return singleSelect.id @@ -66,7 +122,7 @@ export async function addItemsToProject(items: string[], project: string) { } ` - const newItems: Record = await graphql(mutation, { + const newItems: MutationResult = await graphql(mutation, { project, headers: { authorization: `token ${process.env.TOKEN}`, @@ -98,7 +154,7 @@ export async function isDocsTeamMember(login: string) { return true } // Get all members of the docs team - const data: Record = await graphql( + const data: TeamMemberData = await graphql( ` query { organization(login: "github") { @@ -119,9 +175,7 @@ export async function isDocsTeamMember(login: string) { }, ) - const teamMembers = data.organization.team.members.nodes.map( - (entry: Record) => entry.login, - ) + const teamMembers = data.organization.team.members.nodes.map((entry) => entry.login) return teamMembers.includes(login) } @@ -129,7 +183,7 @@ export async function isDocsTeamMember(login: string) { // Given a GitHub login, returns a bool indicating // whether the login is part of the GitHub org export async function isGitHubOrgMember(login: string) { - const data: Record = await graphql( + const data: OrgMemberData = await graphql( ` query { user(login: "${login}") { @@ -302,13 +356,13 @@ export function generateUpdateProjectV2ItemFieldMutation({ } // Guess the affected docs sets based on the files that the PR changed -export function getFeature(data: Record) { +export function getFeature(data: ItemData) { // For issues, just use an empty string if (data.item.__typename !== 'PullRequest') { return '' } - const paths = data.item.files.nodes.map((node: Record) => node.path) + const paths = data.item.files.nodes.map((node) => node.path) // For docs and docs-internal and docs-early-access PRs, // determine the affected docs sets by looking at which @@ -364,7 +418,7 @@ export function getFeature(data: Record) { } // Guess the size of an item -export function getSize(data: Record) { +export function getSize(data: ItemData) { // We need to set something in case this is an issue, so just guesstimate small if (data.item.__typename !== 'PullRequest') { return 'S' @@ -374,7 +428,7 @@ export function getSize(data: Record) { if (process.env.REPO === 'github/github') { let numFiles = 0 let numChanges = 0 - for (const node of data.item.files.nodes as Record[]) { + for (const node of data.item.files.nodes) { if (node.path.startsWith('app/api/description')) { numFiles += 1 numChanges += node.additions @@ -394,7 +448,7 @@ export function getSize(data: Record) { // Otherwise, estimated the size based on all files let numFiles = 0 let numChanges = 0 - for (const node of data.item.files.nodes as Record[]) { + for (const node of data.item.files.nodes) { numFiles += 1 numChanges += node.additions numChanges += node.deletions diff --git a/src/workflows/ready-for-docs-review.ts b/src/workflows/ready-for-docs-review.ts index c032261fc94a..06757373302a 100644 --- a/src/workflows/ready-for-docs-review.ts +++ b/src/workflows/ready-for-docs-review.ts @@ -9,6 +9,8 @@ import { generateUpdateProjectV2ItemFieldMutation, getFeature, getSize, + type ProjectV2Data, + type ItemData, } from './projects' /** @@ -16,28 +18,25 @@ import { * @param data GraphQL response data containing PR information * @returns Object with isCopilotAuthor boolean and copilotAssignee string */ -function getCopilotAuthorInfo(data: Record): { +function getCopilotAuthorInfo(data: ItemData): { isCopilotAuthor: boolean copilotAssignee: string } { - const item = data.item as Record - const author = item.author as Record | undefined - const assigneesObj = item.assignees as Record | undefined + const item = data.item // Check if this is a Copilot-authored PR const isCopilotAuthor = !!( item.__typename === 'PullRequest' && - author && - author.login === 'copilot-swe-agent' + item.author && + item.author.login === 'copilot-swe-agent' ) // For Copilot PRs, find the appropriate assignee (excluding Copilot itself) let copilotAssignee = '' - if (isCopilotAuthor && assigneesObj && assigneesObj.nodes) { - const nodes = assigneesObj.nodes as Array> - const assigneeLogins = nodes - .map((assignee: Record) => assignee.login as string) - .filter((login: string) => login !== 'copilot-swe-agent') + if (isCopilotAuthor && item.assignees && item.assignees.nodes) { + const assigneeLogins = item.assignees.nodes + .map((assignee) => assignee.login) + .filter((login) => login !== 'copilot-swe-agent') // Use the first non-Copilot assignee copilotAssignee = assigneeLogins.length > 0 ? assigneeLogins[0] : '' @@ -71,7 +70,7 @@ function getAuthorFieldValue( async function run() { // Get info about the docs-content review board project - const data: Record = await graphql( + const data = (await graphql( ` query ($organization: String!, $projectNumber: Int!, $id: ID!) { organization(login: $organization) { @@ -125,12 +124,10 @@ async function run() { authorization: `token ${process.env.TOKEN}`, }, }, - ) + )) as ProjectV2Data & ItemData // Get the project ID - const organization = data.organization as Record - const projectV2 = organization.projectV2 as Record - const projectID = projectV2.id as string + const projectID = data.organization.projectV2.id // Get the ID of the fields that we want to populate const datePostedID = findFieldID('Date posted', data)