diff --git a/assets/images/site/footer-divider.png b/assets/images/site/footer-divider.png new file mode 100644 index 000000000000..53fa765c696b Binary files /dev/null and b/assets/images/site/footer-divider.png differ diff --git a/assets/images/site/footer-divider.webp b/assets/images/site/footer-divider.webp new file mode 100644 index 000000000000..fc833eec8078 Binary files /dev/null and b/assets/images/site/footer-divider.webp differ diff --git a/content/actions/concepts/workflows-and-actions/dependency-caching.md b/content/actions/concepts/workflows-and-actions/dependency-caching.md index 6a51f5ce17d0..67ce1564c564 100644 --- a/content/actions/concepts/workflows-and-actions/dependency-caching.md +++ b/content/actions/concepts/workflows-and-actions/dependency-caching.md @@ -31,9 +31,9 @@ For more information on workflow run artifacts, see [AUTOTITLE](/actions/tutoria Caches are shared based on the branch or tag a workflow run uses, not on the identity of the workflow or job. See [AUTOTITLE](/actions/reference/workflows-and-actions/events-that-trigger-workflows) and the `GITHUB_REF` for the branch used for various workflow triggers. Any run that can read a cache restores its contents as-is, so you should treat restored files as untrusted input and never store secrets or other sensitive data in a cache. -Untrusted workflows can read sensitive cache contents, such as when a `pull_request` from a fork restores a cache. Poisoned caches can lead to code execution in trusted workflows. To limit the risk of cache poisoning, {% data variables.product.github %} gives workflows that run in response to low-trust triggers read-only access to caches in the default branch's scope. +Untrusted workflows can read sensitive cache contents, such as when a `pull_request` from a fork restores a cache. Poisoned caches can lead to code execution in trusted workflows. To limit the risk of cache poisoning, {% data variables.product.github %} gives workflows that run in response to low-trust triggers read-only access to caches in the default branch's scope.{% ifversion actions-cache-mode %} A workflow or job can override this read-only restriction by explicitly declaring a write-capable `cache-mode`, which reintroduces the cache-poisoning risk for that workflow.{% endif %} -For details on cache scope, access restrictions, and best practices for using caches securely, see [AUTOTITLE](/actions/reference/workflows-and-actions/dependency-caching#cache-access-for-low-trust-workflow-triggers). +For the trusted-versus-low-trust trigger breakdown and default cache behavior, see [AUTOTITLE](/actions/reference/workflows-and-actions/dependency-caching#cache-access-for-low-trust-workflow-triggers). For cache-specific security guidance, see [AUTOTITLE](/actions/reference/workflows-and-actions/dependency-caching#best-practices-for-using-caches-securely). ## Next steps diff --git a/content/actions/how-tos/reuse-automations/reuse-workflows.md b/content/actions/how-tos/reuse-automations/reuse-workflows.md index 6aa756020def..2049cbdef4a0 100644 --- a/content/actions/how-tos/reuse-automations/reuse-workflows.md +++ b/content/actions/how-tos/reuse-automations/reuse-workflows.md @@ -303,6 +303,22 @@ jobs: For more information on using job outputs, see [AUTOTITLE](/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idoutputs). If you want to share something other than a variable (e.g. a build artifact) between workflows, see [AUTOTITLE](/actions/tutorials/store-and-share-data). +{% ifversion actions-cache-mode %} + +## Controlling cache access in reusable workflows + +You can use the `cache-mode` key to grant a reusable workflow the least amount of {% data variables.product.prodname_actions %} cache access it needs. The value can be `read`, `write`, `write-only`, or `none`. If you omit `cache-mode`, a `read` or `write` default is used based on the trigger type. For the full syntax and the meaning of each value, see [AUTOTITLE](/actions/reference/workflows-and-actions/workflow-syntax#cache-mode). For trigger-dependent defaults, see [AUTOTITLE](/actions/reference/dependency-caching-reference#defaults). + +When a caller workflow calls a reusable workflow, `cache-mode` propagates to the called workflow. An explicit `cache-mode` on the calling job, or inherited from the caller workflow, limits the cache access the called workflow can request. + +If the calling job neither sets nor inherits an explicit `cache-mode`, the called workflow can explicitly request `write` even when the caller's low-trust trigger defaults to `read`. To cap a called workflow at read-only access, set `cache-mode: read` on the job that calls it. + +If a called workflow declares a `cache-mode` that requests access beyond this explicit limit, the run does not start and {% data variables.product.github %} reports a validation error. For example, a caller that allows at most `read` cannot call a workflow that declares `write`. Because `read` grants restore access and `write-only` grants save access, the two are non-overlapping capabilities, so a mismatch between them is also an over-request. For example, a `write-only` caller cannot call a workflow that declares `read`. + +For more information about cache access and the four modes, see [AUTOTITLE](/actions/reference/dependency-caching-reference#controlling-cache-access-with-cache-mode). + +{% endif %} + ## Monitoring which workflows are being used {% ifversion fpt or ghes %} diff --git a/content/actions/reference/security/securely-using-pull_request_target.md b/content/actions/reference/security/securely-using-pull_request_target.md index 1d03625565e1..c9ebc5ae8ce6 100644 --- a/content/actions/reference/security/securely-using-pull_request_target.md +++ b/content/actions/reference/security/securely-using-pull_request_target.md @@ -67,7 +67,7 @@ If you have confirmed you need `pull_request_target`, apply these controls to li * **Restrict secrets.** Confirm that the permissions set on the `GITHUB_TOKEN` have the least privileges and that only the necessary repository and organization secrets are used for the workflow. For more information, see [AUTOTITLE](/actions/tutorials/authenticate-with-github_token#modifying-the-permissions-for-the-github_token). -* **Understand the impact to caching.** To reduce the risk of cache poisoning, workflows triggered by `pull_request_target` have read-only access to the cache in the default branch's scope. These workflows can restore existing cache entries but cannot create or overwrite them, so they cannot affect the execution of other, unrelated, workflows through the shared cache. If such a workflow attempts to save a cache, the save fails but the step and the job continue, and the failure is reported as a warning in the workflow log. If your workflow needs to populate the cache, save it from a workflow that runs on a trusted trigger such as `push`. For more information, see [AUTOTITLE](/actions/reference/workflows-and-actions/dependency-caching#cache-access-for-low-trust-workflow-triggers). +* **Understand the impact to caching.** To reduce the risk of cache poisoning, workflows triggered by `pull_request_target` have read-only access to the cache in the default branch's scope. These workflows can restore existing cache entries but cannot create or overwrite them, so they cannot affect the execution of other, unrelated, workflows through the shared cache. If such a workflow attempts to save a cache, the save fails but the step and the job continue, and the failure is reported as a warning in the workflow log. If your workflow needs to populate the cache, save it from a workflow that runs on a trusted trigger such as `push`.{% ifversion actions-cache-mode %} A workflow or job can opt out of this read-only restriction by explicitly declaring a write-capable `cache-mode`, but doing so on a `pull_request_target` workflow reintroduces the cache-poisoning risk this restriction is designed to prevent.{% endif %} For more information, see [AUTOTITLE](/actions/reference/workflows-and-actions/dependency-caching#cache-access-for-low-trust-workflow-triggers). * **Ensure the underlying compute is isolated and ephemeral.** If self-hosted runners are used, you must confirm that the runner environment is properly restricted from internal resources and is not reused across {% data variables.product.prodname_actions %} runs. For more information, see [AUTOTITLE](/actions/reference/security/secure-use#hardening-for-self-hosted-runners). diff --git a/content/actions/reference/workflows-and-actions/dependency-caching.md b/content/actions/reference/workflows-and-actions/dependency-caching.md index fd89ca7b05e6..1ac52fcc12a0 100644 --- a/content/actions/reference/workflows-and-actions/dependency-caching.md +++ b/content/actions/reference/workflows-and-actions/dependency-caching.md @@ -274,7 +274,7 @@ To reduce this risk, only these workflow triggers can create or overwrite caches * `page_build` * `schedule` -Runs triggered by any other event that resolves to the default branch are given read-only access to caches in the default branch's scope. These runs can restore existing caches but cannot create or overwrite them. This includes triggers whose payload or initiating actor can be influenced by someone outside the repository, such as `pull_request_target`, `issue_comment`, and `workflow_run`. +Runs triggered by any other event that resolves to the default branch are given read-only access to caches in the default branch's scope. These runs can restore existing caches but cannot create or overwrite them. This includes triggers whose payload or initiating actor can be influenced by someone outside the repository, such as `pull_request_target`, `issue_comment`, and `workflow_run`.{% ifversion actions-cache-mode %} A repository can opt out of this restriction for a specific workflow or job by explicitly declaring a write-capable `cache-mode`. See [Bypassing the default untrusted-trigger cache restriction](#bypassing-the-default-untrusted-trigger-cache-restriction).{% endif %} The `pull_request` event is not affected. Caches created by a `pull_request` run are already scoped to the merge ref (`refs/pull/.../merge`) and cannot be written to the default branch's scope. For more information, see [Restrictions for accessing a cache](#restrictions-for-accessing-a-cache). @@ -282,6 +282,61 @@ When a run with read-only cache access tries to save a cache, the save fails but * To retain the performance benefits of caching on the default branch scope, ensure there is a trusted workflow that keeps the cache updated, for example a CI build triggered by a `push` to the default branch. Those cache entries can then be restored by workflows triggered by low-trust events such as `pull_request_target`. * In low-trust workflows, switch to a restore-only cache operation such as `actions/cache/restore` to make the intended cache usage clear and avoid the warning in the workflow run logs. +{% ifversion actions-cache-mode %} + +## Controlling cache access with `cache-mode` + +Use the `cache-mode` workflow key to grant jobs the least amount of cache access they need. You can set `cache-mode` at the workflow level, at the job level, or both. A job-level value overrides the workflow-level value for that job. For the syntax, see [AUTOTITLE](/actions/reference/workflows-and-actions/workflow-syntax#cache-mode). + +`cache-mode` controls the cache access granted to the job's token, and it is enforced with scoped cache tokens. The key accepts the following values. + +| Value | Restore caches | Save caches | +| ----- | -------------- | ----------- | +| `read` | Yes | No | +| `write` | Yes | Yes | +| `write-only` | No | Yes | +| `none` | No | No | + +If you omit `cache-mode`, a `read` or `write` default is used based on the trigger type. See [Defaults](#defaults) and [Bypassing the default untrusted-trigger cache restriction](#bypassing-the-default-untrusted-trigger-cache-restriction). + +### Defaults + +| Configuration | Trigger type | Effective access | +| --- | --- | --- | +| `cache-mode` omitted | Trusted | `write` | +| `cache-mode` omitted | Low-trust | `read` | +| `cache-mode: write` | Trusted or low-trust | `write` | +| `cache-mode: write-only` | Trusted or low-trust | `write-only` | +| `cache-mode: read` | Trusted or low-trust | `read` | +| `cache-mode: none` | Trusted or low-trust | `none` | + +For the trusted-versus-low-trust trigger breakdown, see [Cache access for low-trust workflow triggers](#cache-access-for-low-trust-workflow-triggers). + +The runner exposes the effective mode in the `ACTIONS_CACHE_MODE` environment variable, and the `actions/cache` action and the `@actions/cache` toolkit honor it. Restore is skipped when the mode does not allow reads (`none` or `write-only`), and save is skipped when the mode does not allow writes (`none` or `read`). When a cache operation is skipped because of the mode, the action logs an informational message and the step and run continue without failing. A skipped restore is treated as a cache miss; a skipped save is simply not performed. + +### Cache access in reusable workflows + +`cache-mode` propagates from a caller workflow to the reusable workflows it calls. An explicit `cache-mode` on the calling job, or inherited from the caller workflow, limits the cache access the called workflow can request. + +If the calling job neither sets nor inherits an explicit `cache-mode`, the called workflow can explicitly request `write` even when the caller's low-trust trigger defaults to `read`. To cap a called workflow at read-only access, set `cache-mode: read` on the job that calls it. + +If a called workflow declares a `cache-mode` that requests access beyond this explicit limit, the run does not start and {% data variables.product.github %} reports a validation error. For example, a caller that allows at most `read` cannot call a workflow that declares `write`. Because `read` and `write-only` grant different, non-overlapping capabilities, a mismatch between them is also an over-request. For example, a `write-only` caller cannot call a workflow that declares `read`. For more information about calling reusable workflows, see [AUTOTITLE](/actions/how-tos/reuse-automations/reuse-workflows#controlling-cache-access-in-reusable-workflows). + +### Bypassing the default untrusted-trigger cache restriction + +A job or workflow that explicitly declares `cache-mode: write` or `cache-mode: write-only` overrides the secure read-only default that would otherwise apply to a run triggered by a low-trust event. See [Cache access for low-trust workflow triggers](#cache-access-for-low-trust-workflow-triggers). + +> [!WARNING] +> Explicitly declaring `cache-mode: write` or `cache-mode: write-only` reintroduces the risk of cache-poisoning that the default untrusted-trigger read-only permissions are designed to prevent. If a workflow that runs on an untrusted trigger, such as `pull_request_target`, `issue_comment`, or `workflow_run`, declares a write-capable `cache-mode`, vulnerabilities or untrusted code execution in the workflow could then be used to save a cache. A more privileged workflow that later restores that cache could then execute attacker-controlled content. + +Before declaring a write-capable `cache-mode` on a workflow with a low-trust trigger, consider whether a narrower mitigation meets your needs instead: + * Keep the secure cache access restriction in effect by explicitly declaring `cache-mode: read` on the job, and have a trusted, `push`-triggered workflow maintain the cache instead. See [Cache access for low-trust workflow triggers](#cache-access-for-low-trust-workflow-triggers). + * Only declare a write-capable `cache-mode` on low-trust triggers for jobs that don't process untrusted input before writing to the cache. This includes code checked out from untrusted sources such as forks and pull requests. + * If you do override the secure default, treat the resulting cache as untrusted in every workflow that restores it, and avoid restoring it into a run that has write access to secrets or elevated permissions. + * Follow the guidance on [best practices for using caches securely](#best-practices-for-using-caches-securely). + +{% endif %} + ## Best practices for using caches securely Cache contents are not signed or verified, and any workflow run that can read a cache may extract its contents. Extracted caches may modify files that are subsequently executed in a workflow run, leading to malicious code execution. Follow these practices to reduce the security risk of using caches. diff --git a/content/actions/reference/workflows-and-actions/reusing-workflow-configurations.md b/content/actions/reference/workflows-and-actions/reusing-workflow-configurations.md index e86273424c6e..0210f05b1956 100644 --- a/content/actions/reference/workflows-and-actions/reusing-workflow-configurations.md +++ b/content/actions/reference/workflows-and-actions/reusing-workflow-configurations.md @@ -81,6 +81,9 @@ When you call a reusable workflow, you can only use the following keywords in th * [`jobs..if`](/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idif) * [`jobs..concurrency`](/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idconcurrency) * [`jobs..permissions`](/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idpermissions) +{%- ifversion actions-cache-mode %} +* [`jobs..cache-mode`](/actions/reference/workflows-and-actions/workflow-syntax#jobsjob_idcache-mode) +{%- endif %} > [!NOTE] > diff --git a/content/actions/reference/workflows-and-actions/workflow-syntax.md b/content/actions/reference/workflows-and-actions/workflow-syntax.md index 51a336d56b02..b7ffd6dab7c9 100644 --- a/content/actions/reference/workflows-and-actions/workflow-syntax.md +++ b/content/actions/reference/workflows-and-actions/workflow-syntax.md @@ -338,6 +338,46 @@ env: {% data reusables.actions.jobs.section-using-concurrency %} +{% ifversion actions-cache-mode %} + +## `cache-mode` + +Use `cache-mode` to control the level of {% data variables.product.prodname_actions %} cache access that jobs in the workflow are granted. Setting `cache-mode` at the top level applies to every job in the workflow, unless a job overrides it with [`jobs..cache-mode`](#jobsjob_idcache-mode). + +Access is enforced with scoped cache tokens, so a job cannot restore or save caches beyond the mode it is granted. `cache-mode` accepts the following values. + +| Value | Restore caches | Save caches | +| ----- | -------------- | ----------- | +| `read` | Yes | No | +| `write` | Yes | Yes | +| `write-only` | No | Yes | +| `none` | No | No | + +If you omit `cache-mode`, a `read` or `write` default is used based on the trigger type. For trigger-dependent effective defaults, see [AUTOTITLE](/actions/reference/dependency-caching-reference#defaults). + +> [!WARNING] +> Explicitly declaring `cache-mode: write` or `cache-mode: write-only` on low-trust triggers can bypass the secure default read-only cache restriction and reintroduce cache-poisoning risk. For guidance and mitigations, see [AUTOTITLE](/actions/reference/dependency-caching-reference#bypassing-the-default-untrusted-trigger-cache-restriction). + +When a cache operation is not permitted by the effective mode, the cache step logs an informational message and continues. The job and workflow do not fail. A skipped restore is treated as a cache miss; a skipped save is simply not performed. For more information, see [AUTOTITLE](/actions/reference/dependency-caching-reference#controlling-cache-access-with-cache-mode). + +### Example of `cache-mode` + +```yaml +cache-mode: read + +jobs: + build: + runs-on: ubuntu-latest + steps: + - uses: {% data reusables.actions.action-checkout %} + - uses: {% data reusables.actions.action-cache %} + with: + path: ~/.npm + key: {% raw %}npm-${{ hashFiles('**/package-lock.json') }}{% endraw %} +``` + +{% endif %} + ## `jobs` {% data reusables.actions.jobs.section-using-jobs-in-a-workflow %} @@ -408,6 +448,33 @@ env: {% data reusables.actions.jobs.section-using-concurrency-jobs %} +{% ifversion actions-cache-mode %} + +## `jobs..cache-mode` + +Use `jobs..cache-mode` to set the level of {% data variables.product.prodname_actions %} cache access for a single job. A value set here overrides any workflow-level [`cache-mode`](#cache-mode) for this job only. + +The accepted values are `read`, `write`, `write-only`, and `none`, with the same meanings as the top-level key. If neither the job nor the workflow sets `cache-mode`, a trigger-based default applies. For more information about each value, see [`cache-mode`](#cache-mode) and [AUTOTITLE](/actions/reference/dependency-caching-reference#defaults). + +> [!WARNING] +> Explicitly declaring `cache-mode: write` or `cache-mode: write-only` on low-trust triggers can bypass the secure default read-only cache restriction and reintroduce cache-poisoning risk. For guidance and mitigations, see [AUTOTITLE](/actions/reference/dependency-caching-reference#bypassing-the-default-untrusted-trigger-cache-restriction). + +You can also set `cache-mode` on a job that calls a reusable workflow to limit the cache access granted to the called workflow. For more information, see [AUTOTITLE](/actions/reference/workflows-and-actions/reusing-workflow-configurations#supported-keywords-for-jobs-that-call-a-reusable-workflow) and [AUTOTITLE](/actions/how-tos/reuse-automations/reuse-workflows#controlling-cache-access-in-reusable-workflows). + +### Example of `jobs..cache-mode` + +```yaml +jobs: + build: + runs-on: ubuntu-latest + cache-mode: write + test: + runs-on: ubuntu-latest + cache-mode: read +``` + +{% endif %} + ## `jobs..outputs` {% data reusables.actions.jobs.section-defining-outputs-for-jobs %} @@ -1572,5 +1639,3 @@ Path patterns must match the whole path, and start from the repository's root. | `'**/migrate-*.sql'` | A file with the prefix `migrate-` and suffix `.sql` anywhere in the repository. | `migrate-10909.sql`

`db/migrate-v1.0.sql`

`db/sept/migrate-v1.sql` | | `'*.md'`

`'!README.md'` | Using an exclamation mark (`!`) in front of a pattern negates it. When a file matches a pattern and also matches a negative pattern defined later in the file, the file will not be included. | `hello.md`

_Does not match_

`README.md`

`docs/hello.md` | | `'*.md'`

`'!README.md'`

`README*` | Patterns are checked sequentially. A pattern that negates a previous pattern will re-include file paths. | `hello.md`

`README.md`

`README.doc` | - - diff --git a/content/copilot/concepts/agents/copilot-cli/chronicle.md b/content/copilot/concepts/agents/copilot-cli/chronicle.md index 53c83885e5f5..ba1f7eee6889 100644 --- a/content/copilot/concepts/agents/copilot-cli/chronicle.md +++ b/content/copilot/concepts/agents/copilot-cli/chronicle.md @@ -86,7 +86,7 @@ If you use {% data variables.copilot.copilot_cli_short %} in {% data variables.j * **At the start of your day**: Run `/chronicle standup last 3 days` to generate a summary of recent work instead of reconstructing it manually. * **Periodically, to level up**: Run `/chronicle tips` every week or two to discover features and workflow improvements you might be missing. * **When {% data variables.product.prodname_copilot_short %} keeps making the same mistake**: Run `/chronicle improve` to identify the pattern and generate custom instructions that make the agent more effective for your project. -* **To understand your token usage**: Run `/chronicle cost tips` to see where tokens are going and get suggestions for reducing costs. +* **To understand your token usage**: Run `/chronicle cost-tips` to see where tokens are going and get suggestions for reducing costs. * **To search for a specific topic**: Run `/chronicle search KEYWORD` to find sessions containing a specific term or topic. Unlike free-form questions, this searches session content directly for keywords rather than interpreting your query semantically. * **To recall past work**: Ask a free-form question like "Have I worked on anything related to the payments API?" and {% data variables.product.prodname_copilot_short %} will search your history. * **To continue previous work**: Use `copilot --continue` or `copilot --resume` to pick up where you left off. diff --git a/content/copilot/how-tos/copilot-cli/set-up-copilot-cli/add-lsp-servers.md b/content/copilot/how-tos/copilot-cli/set-up-copilot-cli/add-lsp-servers.md index 3530dc658c3a..09ce7588aa6f 100644 --- a/content/copilot/how-tos/copilot-cli/set-up-copilot-cli/add-lsp-servers.md +++ b/content/copilot/how-tos/copilot-cli/set-up-copilot-cli/add-lsp-servers.md @@ -266,6 +266,7 @@ You can list and manage your LSP servers in an interactive CLI session using the | `/lsp` or `/lsp show` | Show the status of all configured LSP servers. | | `/lsp test SERVER-NAME` | Test whether a server starts correctly. | | `/lsp reload` | Reload LSP configurations from disk. | +| `/lsp logs` | Open the live LSP services panel to view server logs. | | `/lsp help` | Show `/lsp` command information. | ### Listing available LSP servers diff --git a/content/copilot/how-tos/copilot-cli/use-copilot-cli/browse-issues-prs-gists.md b/content/copilot/how-tos/copilot-cli/use-copilot-cli/browse-issues-prs-gists.md index 3d2c4db07bd5..287a281510d5 100644 --- a/content/copilot/how-tos/copilot-cli/use-copilot-cli/browse-issues-prs-gists.md +++ b/content/copilot/how-tos/copilot-cli/use-copilot-cli/browse-issues-prs-gists.md @@ -44,7 +44,7 @@ Tab switching is paused while another part of the CLI—such as the slash comman The **Issues**, **Pull requests**, and **Gists** tabs all use the same controls. Regardless of which of these tabs you're on: * Use the up and down arrow keys to highlight an item in the list. -* Use the left and right arrow keys to navigate between pages in a list. +* Use Shift+ and Shift+ to move to the previous or next page in a multi-page list. * Press Enter to display a detailed view of the highlighted item. Press Esc in the details view to return to the list. * Press o to open the highlighted item (or, in the detailed view, the current item) on {% data variables.product.prodname_dotcom_the_website %}. * Press c to insert a reference to the item into the prompt input area and jump back to the **Session** tab. @@ -141,7 +141,7 @@ The footer hint bar in the **Issues**, **Pull requests**, and **Gists** tabs sum | --- | --- | --- | | Tab / Shift+Tab | Any home tab | Switch to the next or previous home tab. | | /
j / k | List view | Highlight the next or previous item in a list. | -| /
l / h | List view | Display the next or previous page in a multi-page list. | +| Shift+ / Shift+ | List view | Display the next or previous page in a multi-page list. | | Enter | List view | Open the details view for the highlighted item. | | o | List view or details view | Open the highlighted item on {% data variables.product.prodname_dotcom_the_website %} in your browser. | | c | List view or details view | Insert a reference to the item into the prompt input area and jump back to the **Session** tab. | diff --git a/content/copilot/how-tos/copilot-cli/use-copilot-cli/chronicle.md b/content/copilot/how-tos/copilot-cli/use-copilot-cli/chronicle.md index 3702153baed8..e8b583570d94 100644 --- a/content/copilot/how-tos/copilot-cli/use-copilot-cli/chronicle.md +++ b/content/copilot/how-tos/copilot-cli/use-copilot-cli/chronicle.md @@ -99,7 +99,7 @@ When you type `/chronicle` without arguments, a picker is displayed that lets yo | ---------- | ----------- | | `standup` | Generate a standup report from your recent work. | | `tips` | Get personalized tips based on your usage patterns. | -| `cost tips` | Get feedback on your token spend and suggestions for reducing costs. | +| `cost-tips` | Get feedback on your token spend and suggestions for reducing costs. | | `search` | Search your session history for specific topics or keywords. | | `improve` | Suggest improvements to your {% data variables.product.prodname_copilot_short %} custom instructions file. | | `reindex` | Rebuild the local session store from your session history, and sync session data to your account. | @@ -160,12 +160,12 @@ You can focus the tips on a specific area by appending context after `/chronicle /chronicle tips for better prompting ``` -### `/chronicle cost tips` +### `/chronicle cost-tips` This analyzes your token usage across recent sessions to show where tokens are being spent and suggests ways to reduce costs. {% data variables.product.prodname_copilot_short %} looks at patterns like prompt length, tool call frequency, and continuation steps to identify opportunities for more efficient usage. ```copilot copy -/chronicle cost tips +/chronicle cost-tips ``` ### `/chronicle search` diff --git a/content/copilot/how-tos/copilot-cli/use-copilot-cli/voice-input.md b/content/copilot/how-tos/copilot-cli/use-copilot-cli/voice-input.md index 354528f219b7..b1ee6a496d02 100644 --- a/content/copilot/how-tos/copilot-cli/use-copilot-cli/voice-input.md +++ b/content/copilot/how-tos/copilot-cli/use-copilot-cli/voice-input.md @@ -14,10 +14,12 @@ docsTeamMetrics: Voice input lets you dictate prompts to {% data variables.copilot.copilot_cli_short %} by speaking into your microphone instead of typing on the keyboard. Transcription runs entirely on your local machine. Your audio is not sent over the network. The recognized text is inserted at the cursor position in the prompt input area, where you can edit it before submitting. -## Prerequisite +## Prerequisites You need a working microphone connected to your machine. By default, voice input uses your system's default microphone. +Voice input is supported on Windows (x64 and ARM64), Linux (x64), and macOS on Apple Silicon (ARM64). It is not available on Intel-based Macs. + ## Limitation {% data variables.copilot.copilot_cli_short %} offers a fixed set of speech-to-text models: a multilingual model, which is the default, plus a dedicated English model and a dedicated Spanish model. Only these models are listed in the voice models picker. diff --git a/content/copilot/how-tos/copilot-on-github/customize-copilot/configure-mcp-servers.md b/content/copilot/how-tos/copilot-on-github/customize-copilot/configure-mcp-servers.md index 3edc35457f0a..84738cde882e 100644 --- a/content/copilot/how-tos/copilot-on-github/customize-copilot/configure-mcp-servers.md +++ b/content/copilot/how-tos/copilot-on-github/customize-copilot/configure-mcp-servers.md @@ -87,6 +87,9 @@ The configuration object can contain the following keys: * `tools` (`string[]`): The tools from the MCP server to enable. You may be able to find a list of tools in the server's documentation, or in its code. We strongly recommend that you allowlist specific read-only tools, since the agent will be able to use these tools autonomously and will not ask you for approval first. You can also enable all tools by including `*` in the array. * `type` (`string`): {% data variables.copilot.copilot_cloud_agent %} accepts `"local"`, `"stdio"`, `"http"`, or `"sse"`. +> [!NOTE] +> For {% data variables.copilot.copilot_code-review_short %}, each tool in the MCP server's `tools/list` response must set `annotations.readOnlyHint` to `true` to allow {% data variables.product.prodname_copilot_short %} to use it. {% data variables.copilot.copilot_code-review_short %} excludes tools when this annotation is missing or set to `false`. For more information, see [Tool annotations in the MCP specification](https://modelcontextprotocol.io/specification/2025-11-25/schema#toolannotations-readonlyhint). + **Local MCP specific keys** * `command` (`string`): Required. The command to run to start the MCP server. * `args` (`string[]`): Required. The arguments to pass to the `command`. diff --git a/content/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site.md b/content/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site.md index 10ca0cfc10a4..21302821f3c2 100644 --- a/content/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site.md +++ b/content/pages/configuring-a-custom-domain-for-your-github-pages-site/managing-a-custom-domain-for-your-github-pages-site.md @@ -103,6 +103,8 @@ To set up an apex domain, such as `example.com`, you must configure a custom dom Navigate to your DNS provider and create a `CNAME` record for the `www` subdomain that points to your {% data variables.product.prodname_pages %} default domain. For example, if your site is located at `.github.io`, you should create a `CNAME` record that points `www.example.com` to `.github.io` Similarly, for an organization site located at `.github.io`, you should create a `CNAME` record that points `www.example.com` to `.github.io`. Ensure that the `CNAME` record points directly to `.github.io` or `.github.io` without including the repository name. +These `CNAME` record values are the same for both publicly and privately published {% data variables.product.prodname_pages %} sites. Privately published sites are available with {% data variables.product.prodname_ghe_cloud %}. + {% data reusables.pages.contact-dns-provider %} {% data reusables.pages.default-domain-information %} ## Configuring a subdomain diff --git a/content/pull-requests/how-tos/review-pull-requests/commenting-on-a-pull-request.md b/content/pull-requests/how-tos/review-pull-requests/commenting-on-a-pull-request.md index fd6daabe5d4d..2fe5c3ec5d13 100644 --- a/content/pull-requests/how-tos/review-pull-requests/commenting-on-a-pull-request.md +++ b/content/pull-requests/how-tos/review-pull-requests/commenting-on-a-pull-request.md @@ -52,7 +52,7 @@ To reply to an existing line or file comment, navigate to the comment on either * If you want to add **multiple comments**, click **Start a review**, then continue adding comments. - When you're finished, click {% ifversion fpt or ghec %}**Submit review** {% elsif ghes %}**Finish your review**{% endif %}, leave a summary of your review, and click **Submit review**. + When you're finished, click {% ifversion fpt or ghec %}**Submit review**{% elsif ghes %}**Finish your review**{% endif %}, leave a summary of your review, and click **Submit review**. Anyone watching the pull request or repository will receive a notification of your comments. Batching your comments avoids sending multiple notifications. diff --git a/data/features/actions-cache-mode.yml b/data/features/actions-cache-mode.yml new file mode 100644 index 000000000000..d505972bf131 --- /dev/null +++ b/data/features/actions-cache-mode.yml @@ -0,0 +1,6 @@ +# Documentation for the `cache-mode` workflow key, which controls least-privilege +# cache access for GitHub Actions jobs and reusable workflows. +# This feature is dotcom-only and is not available on GitHub Enterprise Server. +versions: + fpt: '*' + ghec: '*' diff --git a/data/reusables/copilot/policy/suggestions-code-enterprise-default.md b/data/reusables/copilot/policy/suggestions-code-enterprise-default.md index 381aa5d2dae4..7c6bca5e1bcc 100644 --- a/data/reusables/copilot/policy/suggestions-code-enterprise-default.md +++ b/data/reusables/copilot/policy/suggestions-code-enterprise-default.md @@ -1 +1 @@ -**Suggestions matching public code** is set to **Blocked** by default for {% data variables.copilot.copilot_business_short %} users. You can change this setting in the **Privacy** section of the {% data variables.product.prodname_copilot_short %} policy page. \ No newline at end of file +**Suggestions matching public code** is set to **Allowed** by default for {% data variables.copilot.copilot_business_short %} users. You can change this setting in the **Privacy** section of the {% data variables.product.prodname_copilot_short %} policy page. \ No newline at end of file diff --git a/data/ui.yml b/data/ui.yml index 7b4565bf4036..812d976a5b0d 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -93,7 +93,10 @@ search_results: n_results: '{n} results' search_validation_error: Validation error with search query filter: Filter - clear_filter: Clear + clear_all_filters: Clear all + show_filters: Show filters + hide_filters: Hide filters + filter_search_results: Filter search results homepage: explore_by_product: Explore by product @@ -131,7 +134,7 @@ support: ask_community: Ask the GitHub community survey: - able_to_find: Did you find what you needed? + able_to_find: Was this Doc helpful? yes: Yes no: No cancel: Cancel @@ -147,8 +150,8 @@ survey: server_error: Unable to process comment at the moment. Please try again. contribution_cta: - title: Help us make these docs great! - body: All GitHub docs are open source. See something that's wrong or unclear? Submit a pull request. + title: Help us make GitHub Docs great! + body: All Docs are open source. See something that's wrong or unclear? Submit a pull request. button: Make a contribution to_guidelines: Learn how to contribute @@ -314,6 +317,7 @@ webhooks: footer: support_heading: Help and support legal_heading: Legal + back_to_top: Back to top imprint: Imprint terms: Terms privacy: Privacy diff --git a/package-lock.json b/package-lock.json index e39deb56e96b..e2e0f203f109 100644 --- a/package-lock.json +++ b/package-lock.json @@ -30,7 +30,7 @@ "@primer/octicons-react": "^19.22.0", "@primer/primitives": "^11.3.1", "@primer/react": "38.19.0", - "@primer/react-brand": "^0.70.0", + "@primer/react-brand": "^0.74.0", "accept-language-parser": "^1.5.0", "ajv": "^8.18.0", "ajv-errors": "^3.0.0", @@ -3893,9 +3893,9 @@ } }, "node_modules/@primer/react-brand": { - "version": "0.70.0", - "resolved": "https://registry.npmjs.org/@primer/react-brand/-/react-brand-0.70.0.tgz", - "integrity": "sha512-6La/hIIbNp8OA3ZxUTzxRJmpr8Imr5NdMiSVjWJTGyEtIuEJ9d4VA9OL0qQkg5cdeq+GTr6qZuTHQQh8cAhG8g==", + "version": "0.74.0", + "resolved": "https://registry.npmjs.org/@primer/react-brand/-/react-brand-0.74.0.tgz", + "integrity": "sha512-bOgO48EBP7EYRE4/JSqrd33jj4islNBgCUNTs+DZqwCjsxLxlnzq8ezoeEQP2Pr1fpZcke7ImtPzU7h3VQuXLQ==", "license": "MIT", "dependencies": { "@oddbird/popover-polyfill": "0.5.2", diff --git a/package.json b/package.json index 95c12830688a..ec28fe159b59 100644 --- a/package.json +++ b/package.json @@ -193,7 +193,7 @@ "@primer/octicons-react": "^19.22.0", "@primer/primitives": "^11.3.1", "@primer/react": "38.19.0", - "@primer/react-brand": "^0.70.0", + "@primer/react-brand": "^0.74.0", "accept-language-parser": "^1.5.0", "ajv": "^8.18.0", "ajv-errors": "^3.0.0", diff --git a/src/events/components/Survey.tsx b/src/events/components/Survey.tsx index 624e717494bc..63ee60eb6664 100644 --- a/src/events/components/Survey.tsx +++ b/src/events/components/Survey.tsx @@ -1,7 +1,6 @@ import React, { useState, useRef, useEffect } from 'react' import cx from 'classnames' import { useRouter } from 'next/router' -import { ThumbsdownIcon, ThumbsupIcon } from '@primer/octicons-react' import { useTranslation } from '@/languages/components/useTranslation' import { Link } from '@/frame/components/Link' @@ -111,7 +110,7 @@ export const Survey = () => { data-testid="survey-form" aria-live="polite" > -

{t`able_to_find`}

+

{t`able_to_find`}

{/* Honeypot: token isn't a real field */} { )} htmlFor="survey-yes" > - {' '} {t`yes`} { )} htmlFor="survey-no" > - {' '} {t`no`} @@ -242,13 +233,15 @@ export const Survey = () => {

{t`feedback`}

)} - - {t`privacy_policy`} - + {(voteState || state === ViewState.END) && ( + + {t`privacy_policy`} + + )} ) } diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml index 7b4565bf4036..812d976a5b0d 100644 --- a/src/fixtures/fixtures/data/ui.yml +++ b/src/fixtures/fixtures/data/ui.yml @@ -93,7 +93,10 @@ search_results: n_results: '{n} results' search_validation_error: Validation error with search query filter: Filter - clear_filter: Clear + clear_all_filters: Clear all + show_filters: Show filters + hide_filters: Hide filters + filter_search_results: Filter search results homepage: explore_by_product: Explore by product @@ -131,7 +134,7 @@ support: ask_community: Ask the GitHub community survey: - able_to_find: Did you find what you needed? + able_to_find: Was this Doc helpful? yes: Yes no: No cancel: Cancel @@ -147,8 +150,8 @@ survey: server_error: Unable to process comment at the moment. Please try again. contribution_cta: - title: Help us make these docs great! - body: All GitHub docs are open source. See something that's wrong or unclear? Submit a pull request. + title: Help us make GitHub Docs great! + body: All Docs are open source. See something that's wrong or unclear? Submit a pull request. button: Make a contribution to_guidelines: Learn how to contribute @@ -314,6 +317,7 @@ webhooks: footer: support_heading: Help and support legal_heading: Legal + back_to_top: Back to top imprint: Imprint terms: Terms privacy: Privacy diff --git a/src/fixtures/tests/playwright-a11y.spec.ts b/src/fixtures/tests/playwright-a11y.spec.ts index 5aff5bc5bec9..0eacad14ed50 100644 --- a/src/fixtures/tests/playwright-a11y.spec.ts +++ b/src/fixtures/tests/playwright-a11y.spec.ts @@ -2,6 +2,8 @@ import { test, expect } from '@playwright/test' import AxeBuilder from '@axe-core/playwright' import { turnOffExperimentsInPage, turnOnExperimentsInPage } from '../helpers/turn-off-experiments' +const SEARCH_TESTS = !!process.env.ELASTICSEARCH_URL + const pages: { [key: string]: string } = { category: '/actions/category', codeAnnotations: '/get-started/markdown/code-annotations', @@ -45,3 +47,35 @@ for (const pageName of Object.keys(pages)) { }) }) } + +// The search facet filters collapse behind a "Show filters" disclosure below +// Primer Brand's `medium` breakpoint. The scans above run at the default desktop +// viewport, where that disclosure is display:none, so the expanded panel would +// otherwise never be scanned. +test.describe('search filters (narrow viewport)', () => { + // Without a local Elasticsearch the middleware proxies to production, so there are no + // aggregations, the disclosure never renders, and this would time out rather than skip + // — matching the guard every search test in playwright-rendering.spec.ts uses. + test.skip(!SEARCH_TESTS, 'No local Elasticsearch, no tests involving search') + + test('expanded filter disclosure passes axe', async ({ page }) => { + await page.setViewportSize({ width: 390, height: 820 }) + await page.goto('/search?query=foo') + await turnOffExperimentsInPage(page) + + const toggle = page.getByTestId('search-filter-toggle') + await expect(toggle).toBeVisible() + await expect(toggle).toHaveAttribute('aria-expanded', 'false') + + await toggle.click() + await expect(toggle).toHaveAttribute('aria-expanded', 'true') + // Scoped to the disclosure's own panel: a bare `fieldset` locator would hit strict + // mode the moment anything else on the page renders one. + const panelId = await toggle.getAttribute('aria-controls') + await expect(page.locator(`#${panelId} fieldset`)).toBeVisible() + + const accessibilityScanResults = await new AxeBuilder({ page }).analyze() + + expect(accessibilityScanResults.violations).toEqual([]) + }) +}) diff --git a/src/frame/components/DefaultLayout.module.scss b/src/frame/components/DefaultLayout.module.scss index 9d113e41e172..f6dea1f56509 100644 --- a/src/frame/components/DefaultLayout.module.scss +++ b/src/frame/components/DefaultLayout.module.scss @@ -1,9 +1,19 @@ @import "src/frame/stylesheets/breakpoint-xxl.scss"; +@import "@primer/react-brand/lib/design-tokens/scss/tokens/functional/size/breakpoints.scss"; .mainContent { scroll-margin-top: 5rem; } +// The search results page splits into rail + results at brand's `medium` +// breakpoint rather than waiting for `d-lg-flex` at 1012px, so the facet rail is +// available on tablets. Applied only on /search. +.searchColumns { + @media (min-width: $brand-breakpoint-medium) { + display: flex; + } +} + // When the inline mobile/tablet nav is open, it takes over the viewport — hide // the content column so the full-width rail isn't squeezed beside it. Above the // xxl breakpoint the rail is a fixed-width sibling, so content always shows. diff --git a/src/frame/components/DefaultLayout.tsx b/src/frame/components/DefaultLayout.tsx index 68e014aa4261..133e2f7b4467 100644 --- a/src/frame/components/DefaultLayout.tsx +++ b/src/frame/components/DefaultLayout.tsx @@ -10,9 +10,7 @@ import { SidebarCollapseProvider, useSidebarCollapsed, } from '@/frame/components/sidebar/SidebarCollapseContext' -import { LegalFooter } from '@/frame/components/page-footer/LegalFooter' -import { ScrollButton } from '@/frame/components/ui/ScrollButton' -import { SupportSection } from '@/frame/components/page-footer/SupportSection' +import { DocsFooter } from '@/frame/components/page-footer/DocsFooter' import { DeprecationBanner } from '@/versions/components/DeprecationBanner' import { RestBanner } from '@/rest/components/RestBanner' import { useMainContext } from '@/frame/components/context/MainContext' @@ -41,7 +39,7 @@ export const DefaultLayout = (props: Props) => { } = mainContext const xHost = mainContext.xHost const page = mainContext.page! - const { t } = useTranslation(['meta', 'scroll_button']) + const { t } = useTranslation('meta') const router = useRouter() const { languages } = useLanguages() @@ -234,20 +232,13 @@ export const DefaultLayout = (props: Props) => { {props.children} -
- - - -
+ ) : ( <> - {props.children} + {props.children} )} @@ -260,17 +251,32 @@ export const DefaultLayout = (props: Props) => { // collapsed; on mobile it shows inline (in the page flow, like desktop) only // when the nav is opened from the secondary bar. The content column (flex-1) // fills the row when the rail is absent. -type LayoutBodyProps = { children?: React.ReactNode; scrollToTopLabel: string } -const LayoutBody = ({ children, scrollToTopLabel }: LayoutBodyProps) => { +type LayoutBodyProps = { children?: React.ReactNode } +const LayoutBody = ({ children }: LayoutBodyProps) => { const { collapsed, mobileNavOpen } = useSidebarCollapsed() + const { currentProduct } = useMainContext() + // Matches SidebarNav's own gate rather than testing router.route. There are two search + // pages — src/pages/search.tsx and src/pages/[versionId]/search.tsx — so a route test + // for '/search' misses every versioned search URL, and this check would then disagree + // with SidebarNav about whether the rail is a facet rail. + const isSearchResultsPage = currentProduct?.id === 'search' return ( -
+ // `d-lg-flex` only goes side-by-side at 1012px. The search page's facet rail + // is meant to sit beside the results from brand's `medium` breakpoint, so it + // gets an earlier split of its own. Route-gated, so no other page moves. +
{/* `collapsed` is the desktop rail-collapse state (persisted). The inline mobile nav is independent, so still render the sidebar when it's open — otherwise opening the mobile nav while the desktop rail is collapsed hides the content column (contentHiddenForNav) with no drawer to show, - so the open nav displays a blank area instead of the doc tree. */} - {collapsed && !mobileNavOpen ? null : } + so the open nav displays a blank area instead of the doc tree. + + Search is exempt: the cookie is shared with the doc-tree rail, but the + search page has no toggle to undo it (DocsSecondaryBar returns null + there), so honouring it would strand the filters with no way back. */} + {collapsed && !mobileNavOpen && !isSearchResultsPage ? null : ( + + )} {/* Need to set an explicit height for sticky elements since we also set overflow to auto */}
{ {children} -
- - - -
+
) diff --git a/src/frame/components/page-footer/Contribution.tsx b/src/frame/components/page-footer/Contribution.tsx index 17875ec0e82a..5fdb93ab3243 100644 --- a/src/frame/components/page-footer/Contribution.tsx +++ b/src/frame/components/page-footer/Contribution.tsx @@ -1,5 +1,3 @@ -import { GitPullRequestIcon } from '@primer/octicons-react' - import { useMainContext } from '@/frame/components/context/MainContext' import { useTranslation } from '@/languages/components/useTranslation' @@ -11,19 +9,16 @@ export const Contribution = () => { ? `https://github.com/github/docs/blob/main/content/${relativePath}` : 'https://github.com/github/docs' + // Heading and body styling comes from the footer column rules in + // SupportSection.module.scss — the Docs 2026 design renders these as plain body + // text rather than a bold heading plus muted copy. return (
-

{t`title`}

-

{t`body`}

+

{t`title`}

+

{t`body`}

- {t`button`} -

- - {t`to_guidelines`} - -

) } diff --git a/src/frame/components/page-footer/DocsFooter.module.scss b/src/frame/components/page-footer/DocsFooter.module.scss new file mode 100644 index 000000000000..740cd196179c --- /dev/null +++ b/src/frame/components/page-footer/DocsFooter.module.scss @@ -0,0 +1,180 @@ +// In dark mode the footer sits on the repo's canvas (#0d1117) while brand's +// canvas-subtle comes from brand's own palette (#0f1511). Those are near-identical, +// so the controls would lose their fill and read as bare outlines. Use Primer's +// surfaces there instead — they're built to contrast with this background. Selector +// pattern follows octicon-table-optimization.scss. +@mixin footer-control-surfaces-dark { + --color-btn-bg: var(--color-canvas-subtle); + --color-btn-border: var(--color-border-default); + --color-btn-text: var(--color-fg-default); + --brand-footer-backToTop-bgColor-rest: var(--color-canvas-subtle); + // Brand's own dark border token is a different grey from Primer's, which would + // leave Back to top outlined differently from the three buttons beside it. + --brand-footer-backToTop-borderColor-rest: var(--color-border-default); +} + +.docsFooter { + // Brand defaults the footer to canvas-subtle (a grey panel); the design puts it on + // the page canvas so it reads as continuous with the article and the band above. + // Deliberately the *repo's* canvas, not brand's: brand's canvas-default is pure + // black in dark mode while these pages sit on #0d1117, which would make the footer + // a visibly darker slab. + --brand-footer-bg-color: var(--bgColor-default, var(--color-canvas-default)); + + // The design draws all four footer controls — Yes, No, Make a contribution and + // Back to top — on brand's subtle canvas with a subtle border (#f2f5f3 on #d2d9d4, + // which is exactly what these brand tokens resolve to). + // + // Yes/No and the contribution CTA are Primer `.btn`s, so set the variables they + // read rather than overriding the rules: the survey's selected states apply + // `color-bg-success-emphasis` / `color-bg-danger-emphasis`, which set + // background-color directly and must keep winning over the rest state. + --color-btn-bg: var(--brand-color-canvas-subtle); + --color-btn-border: var(--brand-color-border-subtle); + --color-btn-text: var(--brand-color-text-default); + + // Brand gives Back to top a near-transparent fill by default; the design gives it + // the same treatment as the other three. Its border already matches. + --brand-footer-backToTop-bgColor-rest: var(--brand-color-canvas-subtle); + + :global([data-color-mode="dark"]) & { + @include footer-control-surfaces-dark; + } + + @media (prefers-color-scheme: dark) { + :global([data-color-mode="auto"][data-dark-theme*="dark"]) & { + @include footer-control-surfaces-dark; + } + } + + // The band above supplies the visual break, and the design puts a 2px emphasized + // rule directly beneath it — on the footer's own top edge. Brand's default here is + // a much brighter green (scale-green-3), so this stays an explicit override. + border-top: 2px solid var(--brand-color-text-emphasized); + + // Brand's section padding is roughly double the design's. Targeted through markers + // we own rather than brand's hashed module classes, which change between releases. + // + // Top bar: 76px in the design, sized by the 44px back-to-top control plus 16px. + > div:has(.backToTop) > section { + padding-top: var(--base-size-16); + padding-bottom: var(--base-size-16); + } + + // Help region: the columns own their padding, so the section adds none. Without + // this the columns sit 32px below the rule that introduces them, and — because the + // rules between rows are column borders — those rules would stop short of the + // footer edges instead of spanning it as the design shows. The inline inset moves + // onto the columns in SupportSection.module.scss so content stays aligned with the + // top and bottom bars. + > div:has(.centerSlot) > section { + padding: 0; + } + + // Bottom bar: 76px in the design around a single 18px line. + > div:has(.bottomRow) > section { + padding-top: var(--base-size-28); + padding-bottom: var(--base-size-28); + } + + // `copyrightStatement` renders inside a brand that is a flex item of + // the bottom row. Let it fill the row so the copyright and links can sit at + // opposite ends, and drop the uppercasing the component applies to the whole

+ // (the design uppercases only the copyright, not the links). + p:has(> .bottomRow) { + flex: 1; + // Inert while the bottom row is horizontal (flex-basis wins), but needed at + // narrow, where brand's bottom row turns into a column with align-items: + // flex-start and would otherwise shrink this to its content width. + width: 100%; + min-width: 0; + text-transform: none; + } +} + +.bottomRow { + display: flex; + flex-wrap: wrap; + align-items: center; + justify-content: space-between; + gap: var(--base-size-16); + width: 100%; +} + +.copyrightText { + text-transform: uppercase; +} + +// Narrow: the design stacks the legal links above the copyright, and gives each its +// own band separated by a rule that spans the footer. So the bottom section hands its +// padding to the two rows — the same move the help region makes — which lets the rule +// between them reach the edges while both rows stay inset and aligned with each other. +@media (max-width: 767px) { + .docsFooter > div:has(.bottomRow) > section { + padding-block: 0; + } + + .docsFooter > div:has(.bottomRow) > section > div { + padding-inline: 0; + } + + .bottomRow { + flex-direction: column; + align-items: stretch; + gap: 0; + } + + .legalLinks { + order: 1; + padding: var(--base-size-20); + } + + .copyrightText { + order: 2; + padding: var(--base-size-20); + border-top: 1px solid + var(--borderColor-muted, var(--color-border-muted, #d2d9d4)); + } +} + +.legalLinks { + display: flex; + flex-wrap: wrap; + align-items: center; + gap: var(--base-size-24); +} + +.legalLink { + // The design sets these apart from the muted monospace copyright: link blue, and + // the body font at the same size as the other footer link columns. + font-family: var(--brand-body-fontFamily, inherit); + font-size: 14px; + font-weight: 500; + text-transform: none; + // Brand's link token, not Primer's accent blue. There is no + // `--brand-color-text-link`; the real token is `-rest`, and naming it wrong meant + // this quietly fell back to Primer (#0969da) instead of brand (#0055d5). + color: var(--brand-color-text-link-rest); + + &:hover { + text-decoration: underline; + } + + // Pipe separators between links, per the design. Decorative, so a pseudo-element + // keeps them out of the accessibility tree and out of the copied text. + &:not(:first-child)::before { + content: ""; + display: inline-block; + width: 1px; + height: 10px; + margin-right: var(--base-size-24); + vertical-align: -1px; + background-color: var(--brand-color-border-subtle); + } + + // KO law requires the privacy statement link to be conspicuous. + &[data-conspicuous="true"] { + color: var(--fgColor-attention, var(--color-fg-attention, #9a6700)); + font-weight: 600; + } +} diff --git a/src/frame/components/page-footer/DocsFooter.tsx b/src/frame/components/page-footer/DocsFooter.tsx new file mode 100644 index 000000000000..995040c7429a --- /dev/null +++ b/src/frame/components/page-footer/DocsFooter.tsx @@ -0,0 +1,137 @@ +import { type MouseEvent } from 'react' +import { useRouter } from 'next/router' +import { MinimalFooter, Text } from '@primer/react-brand' +import cx from 'classnames' + +import { FooterDivider } from '@/frame/components/page-footer/FooterDivider' +import { SupportSection } from '@/frame/components/page-footer/SupportSection' +import { useTranslation } from '@/languages/components/useTranslation' + +import styles from './DocsFooter.module.scss' + +// The Docs 2026 site footer (Figma node 123-6013): decorative band, then brand +// MinimalFooter supplying the logomark + back-to-top row, the help region, and the +// legal/copyright strip. +// +// The design puts the legal links in the *bottom* row beside the copyright. +// MinimalFooter.Link children render in the top row instead, and the two rows live +// in separate DOM subtrees so no amount of CSS moves one into the other. Passing the +// links through `copyrightStatement` — which accepts a ReactElement and renders in +// the bottom row — gets the designed layout without overriding brand internals. +// It also sidesteps the component's hard cap of five links. +// +// Note `copyrightStatement` is rendered inside a , so everything here +// must be phrasing content: spans and anchors only, no lists or