diff --git a/.github/CONTRIBUTING.md b/.github/CONTRIBUTING.md index 5719e174063d..b7cf0c300925 100644 --- a/.github/CONTRIBUTING.md +++ b/.github/CONTRIBUTING.md @@ -14,6 +14,8 @@ Use the table of contents icon Table of contents icon + !member.deleted && + !member.is_bot && + !member.is_app_user && + typeof member.id === 'string' && + typeof member.name === 'string' && + member.name.toLowerCase() === githubLogin + )); + + cursor = result.response_metadata?.next_cursor || ''; + } while (cursor); + + core.info(`Checked ${pages} pages of users.list. Found ${matches.length} match(es) for @${githubLogin}.`); + if (matches.length !== 1) { + core.warning(`Expected one Slack username matching @${githubLogin}; found ${matches.length}. No DM will be sent.`); + return; + } + + core.info(`Will DM Slack user ${matches[0].id}.`); + core.setOutput('slack_user_id', matches[0].id); + core.setOutput('slack_mention', `<@${matches[0].id}>`); + + - name: DM the author in Slack + id: slack_dm + if: ${{ steps.slack_user.outputs.slack_user_id != '' }} + uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 + with: + method: chat.postMessage + token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ steps.slack_user.outputs.slack_user_id }} + text: ${{ toJSON(format('You opened {0} publicly in github/docs instead of the private repo. An internal confirmation issue was created.', github.event.issue.html_url || github.event.pull_request.html_url)) }} + + - name: Log Slack DM result + if: ${{ !cancelled() && steps.membership_check.outputs.did_warn == 'true' }} + env: + SLACK_USER_ID: ${{ steps.slack_user.outputs.slack_user_id }} + SLACK_LOOKUP_OUTCOME: ${{ steps.slack_user.outcome }} + SLACK_DM_OUTCOME: ${{ steps.slack_dm.outcome }} + SLACK_DM_OK: ${{ steps.slack_dm.outputs.ok }} + SLACK_DM_RESPONSE: ${{ steps.slack_dm.outputs.response }} + SLACK_DM_TS: ${{ steps.slack_dm.outputs.ts }} + run: | + echo "Slack user lookup outcome: ${SLACK_LOOKUP_OUTCOME:-not run}" + if [ -z "$SLACK_USER_ID" ]; then + echo "Slack DM skipped because no Slack user ID was resolved." + elif [ "$SLACK_DM_OK" = "true" ]; then + echo "Slack DM sent successfully (timestamp: ${SLACK_DM_TS:-unavailable})." + else + echo "Slack DM did not return success (step outcome: ${SLACK_DM_OUTCOME:-unknown})." + if [ -n "$SLACK_DM_RESPONSE" ]; then + printf '%s\n' "$SLACK_DM_RESPONSE" | jq -r 'if type == "object" and (.error | type) == "string" then "Slack API error: " + .error else "Slack API response did not include an error field." end' + else + echo "Slack DM API response was unavailable." + fi + fi + + - name: Notify the docs-open-source channel + id: slack_channel_notification + if: ${{ !cancelled() && steps.membership_check.outputs.did_warn == 'true' }} uses: slackapi/slack-github-action@dcb1066f776dd043e64d0e8ba94ca15cc7e1875d # v4.0.0 env: - SLACK_MESSAGE: <@${{ github.actor }}> opened https://github.com/github/docs/issues/${{ github.event.number || github.event.issue.number }} publicly on the github/docs repo instead of a private repo. They have been notified via a new issue in the private repo to confirm this was intentional. SLACK_CHANNEL_ID: ${{ secrets.DOCS_OPEN_SOURCE_SLACK_CHANNEL_ID }} + SLACK_MESSAGE: ${{ format('{0} opened {1} publicly in github/docs instead of the private repo. An internal confirmation issue was created.', steps.slack_user.outputs.slack_mention, github.event.issue.html_url || github.event.pull_request.html_url) }} with: method: chat.postMessage token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} @@ -79,6 +190,25 @@ jobs: channel: ${{ toJSON(env.SLACK_CHANNEL_ID) }} text: ${{ toJSON(env.SLACK_MESSAGE) }} + - name: Log Slack channel notification result + if: ${{ !cancelled() && steps.membership_check.outputs.did_warn == 'true' }} + env: + SLACK_CHANNEL_OUTCOME: ${{ steps.slack_channel_notification.outcome }} + SLACK_CHANNEL_OK: ${{ steps.slack_channel_notification.outputs.ok }} + SLACK_CHANNEL_RESPONSE: ${{ steps.slack_channel_notification.outputs.response }} + SLACK_CHANNEL_TS: ${{ steps.slack_channel_notification.outputs.ts }} + run: | + if [ "$SLACK_CHANNEL_OK" = "true" ]; then + echo "Slack channel notification sent successfully (timestamp: ${SLACK_CHANNEL_TS:-unavailable})." + else + echo "Slack channel notification did not return success (step outcome: ${SLACK_CHANNEL_OUTCOME:-unknown})." + if [ -n "$SLACK_CHANNEL_RESPONSE" ]; then + printf '%s\n' "$SLACK_CHANNEL_RESPONSE" | jq -r 'if type == "object" and (.error | type) == "string" then "Slack API error: " + .error else "Slack API response did not include an error field." end' + else + echo "Slack channel API response was unavailable." + fi + fi + - name: Check out repo if: ${{ failure() && github.event_name != 'pull_request_target' }} uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 diff --git a/.github/workflows/sync-sdk-docs.yml b/.github/workflows/sync-sdk-docs.yml index 31394c19c172..21587446765b 100644 --- a/.github/workflows/sync-sdk-docs.yml +++ b/.github/workflows/sync-sdk-docs.yml @@ -261,12 +261,12 @@ jobs: - uses: ./.github/actions/create-workflow-failure-issue id: create-failure-issue - if: ${{ failure() && github.event_name != 'workflow_dispatch' }} + if: ${{ failure() && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request' }} with: token: ${{ secrets.DOCS_BOT_PAT_BASE }} - uses: ./.github/actions/slack-alert - if: ${{ failure() && github.event_name != 'workflow_dispatch' }} + if: ${{ failure() && github.event_name != 'workflow_dispatch' && github.event_name != 'pull_request' }} with: slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} issue_url: ${{ steps.create-failure-issue.outputs.issue_url }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index cfcbd5b88aeb..a9f04b656d80 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -140,6 +140,29 @@ jobs: if: ${{ matrix.name == 'content-linter' }} run: echo "${{ steps.changed_files.outputs.all_modified_files }}" > get_diff_files.txt + - name: Check for automated pipeline changes + if: ${{ matrix.name == 'automated-pipelines' && github.event_name == 'pull_request' }} + id: pipeline_files + uses: tj-actions/changed-files@22103cc46bda19c2b464ffe86db46df6922fd323 # v47.0.5 + with: + # Count a rename as a delete plus an add, so moving a file out of these paths still matches. + output_renamed_files_as_deleted_and_added: true + # Generated content counts too, so sync PRs render every page. + files: | + .github/workflows/test.yml + package-lock.json + content/code-security/reference/code-scanning/codeql/codeql-cli-manual/** + content/graphql/** + content/rest/** + src/audit-logs/** + src/automated-pipelines/** + src/codeql-cli/** + src/github-apps/** + src/graphql/** + src/rest/** + src/secret-scanning/** + src/webhooks/** + - uses: ./.github/actions/cache-nextjs if: ${{ env.START_VITEST_SERVER != 'false' }} @@ -172,4 +195,6 @@ jobs: # RUNNER_DEBUG enables timing and path logs when Actions reruns jobs with debug logging. RUNNER_DEBUG: ${{ runner.debug }} VITEST_FLAGS: ${{ matrix.name == 'article-api' && '--no-file-parallelism --maxWorkers=1' || '' }} + # PRs render pinned autogenerated pages unless they change pipeline code or output. + AUTOGENERATED_RENDER_ALL: ${{ matrix.name == 'automated-pipelines' && (github.event_name != 'pull_request' || steps.pipeline_files.outputs.any_modified == 'true') }} run: npm test -- $VITEST_FLAGS src/${{ matrix.name }}/tests/ diff --git a/content/account-and-profile/how-tos/account-management/deleting-your-personal-account.md b/content/account-and-profile/how-tos/account-management/deleting-your-personal-account.md index 9805376ec6e2..599e8851a808 100644 --- a/content/account-and-profile/how-tos/account-management/deleting-your-personal-account.md +++ b/content/account-and-profile/how-tos/account-management/deleting-your-personal-account.md @@ -1,6 +1,6 @@ --- title: Deleting your personal account -intro: You can delete your personal account on {% data variables.location.product_location %} at any time. +intro: You can delete your personal account on {% data variables.location.product_location %} from your account settings. redirect_from: - /articles/deleting-a-user-account - /articles/deleting-your-user-account @@ -46,6 +46,14 @@ Before you delete your personal account, make a copy of all repositories, privat > [!WARNING] > Once your personal account has been deleted, {% ifversion fpt or ghec %}{% data variables.product.company_short %}{% elsif ghes %}an enterprise owner{% endif %} cannot restore your content. +{% ifversion fpt or ghec %} + +If your account is flagged, suspended, or otherwise restricted, **Delete your account** may be unavailable, disabled, or direct you to {% data variables.contact.github_support %} instead of completing deletion. Contact {% data variables.contact.contact_support_page %} to request access to the normal self-service deletion flow. + +{% data variables.contact.github_support %} does not delete your personal account on your behalf. Once access is restored, you must complete the steps below to delete your account. + +{% endif %} + {% data reusables.user-settings.access_settings %} {% data reusables.user-settings.account_settings %} 1. At the bottom of the Account Settings page, under "Delete account", click **Delete your account**. Before you can delete your personal account: diff --git a/content/billing/how-tos/troubleshooting/locked-account.md b/content/billing/how-tos/troubleshooting/locked-account.md index 9477bacbb8ba..2dac7940bc65 100644 --- a/content/billing/how-tos/troubleshooting/locked-account.md +++ b/content/billing/how-tos/troubleshooting/locked-account.md @@ -26,6 +26,8 @@ category: You can unlock and access your account by updating your account's payment method and resuming paid status. We do not ask you to pay for the time elapsed in locked mode. +A failed authorization hold when you try to sign up for {% data variables.product.prodname_copilot_short %} or increase a budget can also lock your personal account billing and block {% data variables.product.prodname_actions %} or other free-tier access. This can happen even if you plan to stay on {% data variables.product.prodname_free_user %}, have removed the payment method, or see no active {% data variables.product.prodname_copilot_short %} plan or balance in the "Billing and licensing" section under your account's settings page. + You can downgrade your user account or organization to {% data variables.product.prodname_free_team %} to continue with the same advanced features in public repositories. For more information, see [AUTOTITLE](/billing/how-tos/manage-plan-and-licenses/downgrade-plan). ## Unlocking an account's features due to a declined payment @@ -40,8 +42,10 @@ If the new billing information is approved, we will immediately charge you for t When initiating a subscription or trial, we verify the entered payment method using an authorization hold. If the hold is unsuccessful, the chosen subscription or trial will not activate. -If your account's advanced features are locked due to a failed authorization hold, you'll need to update your billing information to trigger a new authorization. +If you want to continue with the chosen subscription, update your billing information to trigger a new authorization. {% data reusables.billing.manage-payment-info %} If the new authorization is successful, the account will automatically unlock and you will be able to proceed with the chosen subscription. + +If the "Billing and licensing" section under your account's settings page shows no active subscription, balance, or payment issue to resolve, contact {% data variables.contact.contact_support %} to request a review of the billing lock. diff --git a/content/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries.md b/content/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries.md index e6b3567124dd..6b0abcfcc036 100644 --- a/content/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries.md +++ b/content/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries.md @@ -57,6 +57,9 @@ You need to be an **organization owner** to set up access to private registries {% ifversion org-private-registry-oidc %} * **OIDC (OpenID Connect)**: Use short-lived credentials from a cloud identity provider instead of storing long-lived secrets. When you select this option, choose a provider and fill in the provider-specific fields. For more information, see [Configuring OIDC authentication for a private registry](#configuring-oidc-authentication-for-a-private-registry). {% endif %} + {% ifversion org-private-registry-anonymous %} + * **Anonymous**: Use this option for a registry that doesn't require authentication. No credentials are stored or sent to the registry. For more information, see [Configuring an anonymous private registry](#configuring-an-anonymous-private-registry). + {% endif %} 1. Define which repositories in the organization can access the private registry using these details: all, private and internal, or selected repositories only. 1. When you have finished defining the private registry, select **Add Registry** to save the registry information. @@ -129,6 +132,23 @@ For more information about how OIDC works, see [AUTOTITLE](/actions/concepts/sec {% endif %} +{% ifversion org-private-registry-anonymous %} + +### Configuring an anonymous private registry + +Some registries, such as internal mirrors, don't require authentication. You can add these registries at the organization level with the **Anonymous** authentication method so that {% data variables.product.prodname_dependabot %} can use them without a token, password, or OIDC configuration. + +{% ifversion dependabot-egress-allowlist %} +{% data variables.product.prodname_dependabot %} update jobs can only connect to hosts on an egress allowlist. Adding an anonymous registry allows {% data variables.product.prodname_dependabot %} to reach the registry's host for jobs in repositories that have access to the registry, without storing a secret or sending credentials. For more information, see [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host). +{% endif %} + +When you select **Anonymous**, the credential fields are removed from the form. You still define the registry's **URL** and **Type**. You also choose which repositories can access the registry. + + +After you create an anonymous registry, you can only update its URL and whether it replaces the default registry for the ecosystem. You can't change the authentication method of an existing registry. To switch to or from **Anonymous**, delete the registry and create a new one. + +{% endif %} + {% ifversion code-quality %} ## {% data variables.product.prodname_code_quality_short %} access to private registries diff --git a/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md b/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md index 47418f235691..2a73ac6bdbf3 100644 --- a/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md +++ b/content/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host.md @@ -15,7 +15,7 @@ This reduces the risk that a malicious dependency or build script can send repos Most jobs are unaffected. The allowlist already covers public registries, mirrors, CDNs, and download hosts for supported ecosystems, as well as {% data variables.product.github %}'s infrastructure. Registries configured and referenced for a job are also allowed automatically. -A registry counts as configured only when you declare it under the top-level `registries` key in your `dependabot.yml` file. Defining a registry solely in an ecosystem-native configuration file, such as `.npmrc`, `nuget.config`, `pip.conf`, or Maven's `settings.xml`, does not allow its host. The package manager reads the registry from that file and tries to reach it, but the proxy blocks the request. +A registry counts as configured only when you declare it under the top-level `registries` key in your `dependabot.yml` file{% ifversion org-private-registry-anonymous %}, or when an organization owner adds it as an organization-level private registry{% endif %}. Defining a registry solely in an ecosystem-native configuration file, such as `.npmrc`, `nuget.config`, `pip.conf`, or Maven's `settings.xml`, does not allow its host. The package manager reads the registry from that file and tries to reach it, but the proxy blocks the request. ## Identifying a blocked request in job logs @@ -42,10 +42,14 @@ Two independent sets of hosts are allowed for each job. Declare the registry even if it allows anonymous access. It is the declaration in `dependabot.yml`, not the presence of credentials, that makes the host reachable. +{% ifversion org-private-registry-anonymous %} +Organization owners can also allow a registry's host for jobs across the organization by adding the registry as an organization-level private registry. For a registry that doesn't require authentication, use the **Anonymous** authentication method. No credentials are stored or sent. See [AUTOTITLE](/code-security/how-tos/secure-at-scale/configure-organization-security/manage-usage-and-access/giving-org-access-private-registries#configuring-an-anonymous-private-registry). +{% endif %} + This split determines where a blocked host belongs. * **A public registry or download host** that other users' jobs could legitimately need belongs in the defaults. See [Adding a host to the defaults](#adding-a-host-to-the-defaults). -* **A private or organization-specific registry** belongs in your `dependabot.yml` file, never in the defaults. Configure it under `registries` so it is allowed only for the jobs that need it. See [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-access-to-private-registries). +* **A private or organization-specific registry** belongs in your `dependabot.yml` file, never in the defaults. Configure it under `registries` so it is allowed only for the jobs that need it. See [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/configure-access-to-private-registries).{% ifversion org-private-registry-anonymous %} Alternatively, an organization owner can add it as an organization-level private registry.{% endif %} ## Adding a host to the defaults diff --git a/content/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors.md b/content/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors.md index 15b2d9b01fd2..9e221577be40 100644 --- a/content/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors.md +++ b/content/code-security/reference/supply-chain-security/troubleshoot-dependabot/dependabot-errors.md @@ -289,7 +289,7 @@ Update jobs can only reach hosts on {% data variables.product.prodname_dependabo **Resolution:** Choose the option that matches the blocked host: -* For a private or organization-specific registry, define it under the top-level `registries` key in your `dependabot.yml` file. Reference it from the relevant `updates` entry to allow it for that job. Do this even if the registry allows anonymous access, and note that defining it only in a file such as `.npmrc` or `nuget.config` does not allow its host. +* For a private or organization-specific registry, define it under the top-level `registries` key in your `dependabot.yml` file. Reference it from the relevant `updates` entry to allow it for that job. Do this even if the registry allows anonymous access, and note that defining it only in a file such as `.npmrc` or `nuget.config` does not allow its host.{% ifversion org-private-registry-anonymous %} Alternatively, an organization owner can add the registry at the organization level, using the **Anonymous** authentication method if the registry doesn't require authentication.{% endif %} * For a public registry or download host, propose adding it to the default allowlist. For more information, see [AUTOTITLE](/code-security/how-tos/secure-your-supply-chain/manage-your-dependency-security/resolve-a-blocked-host). diff --git a/content/contributing/collaborating-on-github-docs/about-contributing-to-github-docs.md b/content/contributing/collaborating-on-github-docs/about-contributing-to-github-docs.md index ad8f27b44898..61a92c8958fd 100644 --- a/content/contributing/collaborating-on-github-docs/about-contributing-to-github-docs.md +++ b/content/contributing/collaborating-on-github-docs/about-contributing-to-github-docs.md @@ -16,12 +16,21 @@ The documentation repository is the place to discuss and collaborate on the docu ## Issues +> [!NOTE] +> For larger updates, we highly recommend that you open an issue first, to give us the opportunity to review your proposal before you go ahead and raise a pull request. + [Issues](/issues/tracking-your-work-with-issues/learning-about-issues/about-issues) are used to track tasks that contributors can help with. If an issue has a `triage` label, we haven't reviewed it yet, and you shouldn't begin work on it. If you've found something in the documentation content, or something about the docs.github.com website, that should be updated, search the open issues to see if someone else has reported the same thing. If it's something new, open an issue using a [template](https://github.com/github/docs/issues/new/choose). We'll use the issue to have a conversation about the problem you'd like to be fixed. +### Working on other contributors' issues + > [!NOTE] -> For larger updates, we highly recommend that you open an issue first, to give us the opportunity to review your proposal before you go ahead and raise a pull request. +> We will close any pull requests raised against existing issues without a `help wanted` label. + +To make sure all of our contributors have the best experience, please don't raise any pull requests against an existing issue that another user has raised until it's been marked with a `help wanted` label. We need to properly assess all issues before we can accept community contributions, and determine whether it's something we need to fix internally, or how many articles are affected and what the scope of the fix will be. + +You can still raise a pull request and an issue at the same time, if you're confident about the nature of your contribution. ## Pull requests diff --git a/content/copilot/how-tos/administer-copilot/manage-for-enterprise/use-managed-settings/deploy-managed-settings.md b/content/copilot/how-tos/administer-copilot/manage-for-enterprise/use-managed-settings/deploy-managed-settings.md index 14ee09afbbd1..80348595b516 100644 --- a/content/copilot/how-tos/administer-copilot/manage-for-enterprise/use-managed-settings/deploy-managed-settings.md +++ b/content/copilot/how-tos/administer-copilot/manage-for-enterprise/use-managed-settings/deploy-managed-settings.md @@ -91,6 +91,14 @@ Place `{% data variables.copilot.managed_setting_file %}` in the following locat 1. For {% data variables.copilot.copilot_cli_short %} on macOS and Linux, make the file a regular file owned by `root`, and ensure it is not group-writable or world-writable. Do not use a symbolic link. The CLI rejects files that do not meet these requirements. 1. Ask users to restart supported clients so the updated policy is loaded at startup. +## Requiring fresh server-managed settings on startup + +Use `forceRemoteSettingsRefresh` when a time-sensitive policy change must reach supported clients before {% data variables.product.prodname_copilot_short %} starts. The setting is supported in {% data variables.copilot.copilot_cli_short %}, the {% data variables.copilot.github_copilot_app %}, and {% data variables.product.prodname_vscode_shortname %}. + +MDM-managed and file-based delivery can apply this requirement before the client signs in for the first time. A server-managed value begins requiring refreshes on subsequent startups after the client has received it once. + +For client-specific behavior, value formats, and limitations, see [`forceRemoteSettingsRefresh`](/copilot/reference/enterprise-administrators/enterprise-managed-settings#forceremotesettingsrefresh) in the enterprise managed settings reference. + ## Precedence of deployment methods When multiple settings sources are present, settings earlier in this list take precedence over settings later in the list: 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 f6f004814218..4a2ded5a694f 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,9 +25,9 @@ category: - Configure Copilot --- -You can view your current plan details in your {% data variables.product.github %} account settings. From there, you can change or cancel your plan. +You can view your current plan details in your {% data variables.product.github %} account settings. The billing and licensing steps described in this article apply only if you can sign in to the personal account billed for the plan. -If you have access to {% data variables.product.prodname_copilot %} through an organization or enterprise, you will not be able to modify your plan. +If you have access to {% data variables.product.prodname_copilot %} through an organization or enterprise, your plan can be modified by an organization or enterprise owner. See [AUTOTITLE](/enterprise-cloud@latest/copilot/how-tos/administer-copilot). ## Viewing your {% data variables.product.prodname_copilot_short %} plan @@ -75,4 +75,6 @@ You can cancel your {% data variables.product.prodname_copilot_short %} plan at 1. In the "{% data variables.product.prodname_copilot %}" section, select the **Manage subscription** dropdown on the right and then click **Cancel subscription**. 1. Select **Cancel {% data variables.copilot.copilot_pro_short %}/{% data variables.copilot.copilot_pro_plus_short %}/{% data variables.copilot.copilot_max_short %}** to cancel your plan at the end of your current billing cycle. +If you can't sign in to the personal account billed for your plan—for example, because you no longer have access to its work or school email or are blocked by account recovery—try the available account recovery methods. See [AUTOTITLE](/authentication/securing-your-account-with-two-factor-authentication-2fa/recovering-your-account-if-you-lose-your-2fa-credentials). If you still can't access the account or identify which account is being charged, contact {% data variables.contact.contact_support_page %} to request help stopping future {% data variables.product.prodname_copilot_short %} charges or to request removal of the payment method from the locked account. + {% data reusables.billing.billing-info %} diff --git a/content/copilot/reference/ai-models/model-hosting.md b/content/copilot/reference/ai-models/model-hosting.md index f5e748dd2209..e8df32d98912 100644 --- a/content/copilot/reference/ai-models/model-hosting.md +++ b/content/copilot/reference/ai-models/model-hosting.md @@ -85,6 +85,7 @@ Used for: * {% data variables.copilot.copilot_gemini_37_flash %} * {% data variables.copilot.copilot_gemini_38_flash %} +* {% data variables.copilot.copilot_gemini_4_argon %} {% data variables.product.prodname_copilot %} uses {% data variables.copilot.copilot_gemini %} models hosted on Google Cloud Platform (GCP). When using {% data variables.copilot.copilot_gemini %} models, prompts and metadata are sent to GCP, which makes the [following data commitment](https://cloud.google.com/vertex-ai/generative-ai/docs/data-governance): _{% data variables.copilot.copilot_gemini %} doesn't use your prompts, or its responses, as data to train its models._ diff --git a/content/copilot/reference/ai-models/supported-models.md b/content/copilot/reference/ai-models/supported-models.md index f1e653e48d9a..faac97de84c4 100644 --- a/content/copilot/reference/ai-models/supported-models.md +++ b/content/copilot/reference/ai-models/supported-models.md @@ -129,6 +129,7 @@ Some {% data variables.product.prodname_copilot_short %} models require minimum |----------------------------------------------------------|----------------------------------------------|------------------------------------------|----------------|-------|---------| | {% data variables.copilot.copilot_gemini_37_flash %} | `v1.128.0` | `17.14.22` or `18.1.0` | TBD | TBD | TBD | | {% data variables.copilot.copilot_gemini_38_flash %} | TBD | `17.14.22` or `18.1.0` | TBD | TBD | TBD | +| {% data variables.copilot.copilot_gemini_4_argon %} | TBD | `17.14.22` or `18.1.0` | TBD | TBD | TBD | | {% data variables.copilot.copilot_gpt_53_codex %} | `v1.104.1` | `17.14.19` | `1.5.61` | `0.45.0` | `0.13.0` | | {% data variables.copilot.copilot_gpt_54 %} | `v1.104.1` | `17.14.19` | `1.5.66` | `0.47.0` | `0.15.0` | | {% data variables.copilot.copilot_gpt_54_mini %} | `v1.104.1` | `17.14.19` | `1.5.66` | `0.47.0` | `0.15.0` | diff --git a/content/copilot/reference/copilot-billing/models-and-pricing.md b/content/copilot/reference/copilot-billing/models-and-pricing.md index 0a4752ce7d2a..7a19e09b308a 100644 --- a/content/copilot/reference/copilot-billing/models-and-pricing.md +++ b/content/copilot/reference/copilot-billing/models-and-pricing.md @@ -121,3 +121,9 @@ You can view your current {% data variables.product.prodname_actions %} usage fo {% data variables.copilot.copilot_pro_short %} and {% data variables.copilot.copilot_pro_plus_short %} subscribers on **existing annual billing plans** using the **request-based billing** model have different model multipliers. See [AUTOTITLE](/copilot/reference/copilot-billing/request-based-billing-legacy/model-multipliers-for-annual-plans). [^gemini-flash-promo]: {% data variables.copilot.copilot_gemini_37_flash %} and {% data variables.copilot.copilot_gemini_38_flash %} are available at the promotional pricing of $0.75 per 1M input tokens, $0.075 per 1M cached input tokens, and $3.75 per 1M output tokens through December 31, 2026. + + + +[^gemini-4-argon-promo]: {% data variables.copilot.copilot_gemini_4_argon %} is available at the promotional pricing of $2.00 per 1M input tokens, $0.10 per 1M cached input tokens, and $10.00 per 1M output tokens until December 1, 2026. + + 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 3decacba7eb7..d1501b328dfb 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 @@ -665,7 +665,7 @@ Only the following keys are supported in MDM managed settings. | `enabledPlugins` | Enable or disable specific plugins | | `extraKnownMarketplaces` | Add trusted plugin marketplaces | | `forceLoginOrgs` | Pin sign-in to an approved set of {% data variables.product.github %} organizations (an array of organization logins, matched case-insensitively). {% data variables.product.prodname_copilot_short %} only runs for an account belonging to at least one listed organization; a personal account, an account that belongs only to some other enterprise, or BYOK/API-key authentication is refused with an actionable error. Set an empty array to turn the pin off without deleting the key. Deploy this key through the device channel (MDM plist/registry, or `managed-settings.json`) since it must be able to redirect a developer's first sign-in—the server-managed channel only reaches accounts that have already authenticated into the organization. This key fails closed: an unusable value, or a managed policy that can't be read on a known-managed device, blocks all sign-in until fixed. | -| `forceRemoteSettingsRefresh` | Require a fresh server-managed settings fetch on startup, even when a fresh cached policy exists. The cached entry is still kept as a fallback if the fetch fails. The device (MDM) value takes precedence over a cached server value. | +| `forceRemoteSettingsRefresh` | Require a fresh server-managed settings fetch on startup, even when a fresh cached policy exists. Cached policy cannot satisfy the freshness requirement. If the fetch fails, affected operations remain restricted until current policy is confirmed. The device (MDM) value takes precedence over a cached server value. | | `model` | Set a default model for all users (overridden by the `--model` flag or a resumed-session model). `effortLevel` and `contextTier` set alongside `model` apply the same managed reasoning effort and context tier as the corresponding [repository settings](#repository-settings-githubcopilotsettingsjson) keys, but only when the managed model supports explicit effort/context options. | | `permissions` | Set managed permissions, including `disableBypassPermissionsMode`, `deny` / `ask` / `allow` rule arrays, and the `limitTo` domain boundary array. See [Managed permission rules](#managed-permission-rules) and [Domain boundary (`limitTo`)](#domain-boundary-limitto). | | `policyHelper` | Register an executable that supplies the lowest-priority managed-settings layer. Fields: `path` (required), plus optional `args`, `timeoutMs`, and `refreshIntervalMs`. If both a device (MDM) and a server policy register a `policyHelper`, the device registration wins. | diff --git a/content/copilot/reference/enterprise-administrators/enterprise-managed-settings.md b/content/copilot/reference/enterprise-administrators/enterprise-managed-settings.md index 66e85a9a570c..ce80a422175f 100644 --- a/content/copilot/reference/enterprise-administrators/enterprise-managed-settings.md +++ b/content/copilot/reference/enterprise-administrators/enterprise-managed-settings.md @@ -32,9 +32,10 @@ For instructions on creating the file, see [AUTOTITLE](/copilot/how-tos/administ | `strictKnownMarketplaces` | Restricts plugin installation to explicitly listed marketplaces | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | | `telemetry` | Configures OpenTelemetry export, routing {% data variables.product.prodname_copilot_short %} usage data to a collector of your choice | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "x" aria-label="Not supported" %} | {% octicon "check" aria-label="Supported" %} | | `remoteControl` | Restricts whether sessions hosted on this device can be remotely controlled, based on the controlling client's SSO authorization status for the listed organizations. Doesn't affect the user's ability to remotely control sessions hosted on other devices | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "x" aria-label="Not supported" %} | {% octicon "x" aria-label="Not supported" %} | +| `forceRemoteSettingsRefresh` | Requires a fresh server-managed settings fetch on client startup | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "x" aria-label="Not supported" %} | {% octicon "x" aria-label="Not supported" %} | | `allowedMcpServers` | Defines an allowlist of MCP servers permitted to run. Any server not matched is blocked. Omit to allow all servers, subject to any deny rules | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "x" aria-label="Not supported" %} | {% octicon "check" aria-label="Supported" %} | | `deniedMcpServers` | Defines MCP servers that are unconditionally blocked, even if they also match an entry in `allowedMcpServers` | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} | {% octicon "x" aria-label="Not supported" %} | {% octicon "check" aria-label="Supported" %} | -| `sandbox` | Enforces minimum local sandbox restrictions for command execution, filesystem and network access, credentials, and local MCP and LSP servers | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} (Agent Host, 1.138.0+) | {% octicon "check" aria-label="Supported" %} | {% octicon "x" aria-label="Not supported" %} | {% octicon "x" aria-label="Not supported" %} | +| `sandbox` | Enforces minimum local sandbox restrictions for command execution, filesystem and network access, credentials, and local MCP and LSP servers. Subject to minimum client version requirements. | {% octicon "check" aria-label="Supported" %} | {% octicon "check" aria-label="Supported" %} (Agent Host, 1.138.0+) | {% octicon "check" aria-label="Supported" %} | {% octicon "x" aria-label="Not supported" %} | {% octicon "x" aria-label="Not supported" %} | {% endrowheaders %} @@ -239,11 +240,11 @@ For example, a team settings file can set `"autoTier": "intelligence"`. If the t ## forceRemoteSettingsRefresh -Requires a fresh download of server-managed settings at startup in {% data variables.copilot.copilot_cli_short %} and {% data variables.product.prodname_vscode_shortname %}. Set `forceRemoteSettingsRefresh` to `true` to enable this requirement. +Requires a fresh download of server-managed settings at startup in {% data variables.copilot.copilot_cli_short %}, {% data variables.product.prodname_vscode_shortname %}, and the {% data variables.copilot.github_copilot_app %}. Set `forceRemoteSettingsRefresh` to `true` to enable this requirement. -In {% data variables.copilot.copilot_cli_short %}, this skips the normal one-hour cache and prevents fallback to an older cached policy if the download fails. An unsuccessful refresh leaves the policy unconfirmed, so affected operations are restricted. In {% data variables.product.prodname_vscode_shortname %}, {% data variables.product.prodname_copilot_short %} features are blocked until the required refresh succeeds. Users therefore need a network connection when starting the client. +In {% data variables.copilot.copilot_cli_short %} and the {% data variables.copilot.github_copilot_app %}, this skips the normal one-hour cache and prevents fallback to an older cached policy if the download fails. An unsuccessful refresh leaves the policy unconfirmed, so affected operations are restricted. In {% data variables.product.prodname_vscode_shortname %}, {% data variables.product.prodname_copilot_short %} features are blocked until the required refresh succeeds. Users therefore need a network connection when starting the client. -For the requirement to apply from a device's first startup, deliver it through device management or `{% data variables.copilot.managed_setting_file %}`. A server-delivered value can require refreshes on subsequent startups after the client has received it. Policy helper scripts cannot set this key. +For the requirement to apply from a device's first startup, deliver it through MDM-managed or file-based settings. A server-managed value can require refreshes on subsequent startups after the client has received it. Policy helper scripts cannot set this key. In `{% data variables.copilot.managed_setting_file %}`, use a JSON Boolean, for example `"forceRemoteSettingsRefresh": true`. When configuring {% data variables.copilot.copilot_cli_short %} through the Windows registry or macOS managed preferences, store this value as a string containing `true` or `false`. Do not use a DWORD or native Boolean. @@ -458,3 +459,13 @@ In {% data variables.copilot.copilot_cli_short %}, host rules and upstream proxi Configures macOS-specific Seatbelt options. * `keychainAccess`: `false` prevents sandboxed processes from accessing the macOS Keychain. + +### Minimum client version enforcement for `sandbox` + +When you add the `sandbox` key to managed settings, {% data variables.product.prodname_copilot_short %} enforces a local sandbox for all users in scope. These users must use a {% data variables.product.prodname_copilot_short %} client version that supports local sandbox. Until a user updates their client to at least the minimum supported version, their connections to {% data variables.product.prodname_copilot_short %} are blocked. + +| Supported client | Minimum version | +| --- | --- | +| VS Code | [1.141.0](https://github.com/microsoft/vscode/releases/tag/1.141.0) | +| Copilot CLI | [1.0.93](https://github.com/github/copilot-cli/releases/tag/v1.0.93) | +| GitHub Copilot App | [1.1.26](https://github.com/github/app/releases#release-v1.1.26) | diff --git a/data/features/org-private-registry-anonymous.yml b/data/features/org-private-registry-anonymous.yml new file mode 100644 index 000000000000..20dde8674933 --- /dev/null +++ b/data/features/org-private-registry-anonymous.yml @@ -0,0 +1,6 @@ +# Reference: github/releases#11330 +# Anonymous (unauthenticated) organization-level private registries used by Dependabot. +versions: + fpt: '*' + ghec: '*' + ghes: '>=3.24' 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 b05ce787f8ba..f479e1385f96 100644 --- a/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md +++ b/data/reusables/copilot/copilot-cloud-agent-non-auto-models.md @@ -5,6 +5,7 @@ * {% data variables.copilot.copilot_claude_haiku_55 %} * {% data variables.copilot.copilot_gemini_37_flash %} * {% data variables.copilot.copilot_gemini_38_flash %} +* {% data variables.copilot.copilot_gemini_4_argon %} * {% data variables.copilot.copilot_gpt_54_mini %} * {% data variables.copilot.copilot_gpt_56_luna %} * {% data variables.copilot.copilot_gpt_56_sol %} diff --git a/data/tables/copilot/model-comparison.yml b/data/tables/copilot/model-comparison.yml index c712c48ac382..3aee97f3c4a5 100644 --- a/data/tables/copilot/model-comparison.yml +++ b/data/tables/copilot/model-comparison.yml @@ -135,6 +135,11 @@ excels_at: Fast, reliable answers to lightweight coding questions further_reading: '[Gemini 3.8 Flash model card](https://storage.googleapis.com/deepmind-media/Model-Cards/Gemini-3-8-Flash-Model-Card.pdf)' +- name: Gemini 4 Argon + task_area: Complex coding, agentic, and reasoning tasks + excels_at: Thorough work that tests edge cases before finishing + further_reading: '[Gemini 4 Argon](https://storage.googleapis.com/deepmind-media/Model-Cards/Gemini-4-Argon-Model-Card.pdf)' + # Microsoft - name: MAI-Code-1.1-Flash task_area: General-purpose coding and writing, image understanding diff --git a/data/tables/copilot/model-release-status.yml b/data/tables/copilot/model-release-status.yml index f5ee420cd1dc..83909bb9a955 100644 --- a/data/tables/copilot/model-release-status.yml +++ b/data/tables/copilot/model-release-status.yml @@ -123,6 +123,10 @@ provider: 'Google' release_status: 'GA' +- name: 'Gemini 4 Argon' + provider: 'Google' + release_status: 'GA' + # Microsoft models - name: 'MAI-Code-1.1-Flash' provider: 'Microsoft' diff --git a/data/tables/copilot/model-supported-clients.yml b/data/tables/copilot/model-supported-clients.yml index 0f06d75c521a..bfc1627356d1 100644 --- a/data/tables/copilot/model-supported-clients.yml +++ b/data/tables/copilot/model-supported-clients.yml @@ -131,6 +131,15 @@ xcode: true jetbrains: true +- name: Gemini 4 Argon + dotcom: false + cli: true + vscode: true + vs: true + eclipse: true + xcode: true + jetbrains: true + - name: MAI-Code-1.1-Flash dotcom: true cli: true diff --git a/data/tables/copilot/model-supported-plans.yml b/data/tables/copilot/model-supported-plans.yml index 2718999edf55..473a4a1be282 100644 --- a/data/tables/copilot/model-supported-plans.yml +++ b/data/tables/copilot/model-supported-plans.yml @@ -103,6 +103,13 @@ business: true enterprise: true +- name: Gemini 4 Argon + pro: false + pro_plus: true + max: true + business: true + enterprise: true + - name: GPT-5 mini pro: true pro_plus: true diff --git a/data/tables/copilot/models-and-pricing.yml b/data/tables/copilot/models-and-pricing.yml index 7774c10a4b09..c5dd843f56fd 100644 --- a/data/tables/copilot/models-and-pricing.yml +++ b/data/tables/copilot/models-and-pricing.yml @@ -406,6 +406,19 @@ cached_input: $0.075 output: $3.75 +# Promotional pricing expires 2026-12-01. Google hasn't shared post-promo pricing yet. +# When it does, update input, cached_input, and output below, then remove the +# [^gemini-4-argon-promo] footnote here and in models-and-pricing.md. +- model: 'Gemini 4 Argon[^gemini-4-argon-promo]' + provider: google + release_status: GA + category: Powerful + threshold: 'Not applicable' + tier: 'Default' + input: $2.00 + cached_input: $0.10 + output: $10.00 + # xAI - model: 'Grok 4.5' provider: xai diff --git a/data/variables/copilot.yml b/data/variables/copilot.yml index 1cbda8e30a5d..feaaf7ebf5c9 100644 --- a/data/variables/copilot.yml +++ b/data/variables/copilot.yml @@ -211,6 +211,7 @@ copilot_gemini_3_pro: 'Gemini 3 Pro' copilot_gemini_31_pro: 'Gemini 3.1 Pro' copilot_gemini_37_flash: 'Gemini 3.7 Flash' copilot_gemini_38_flash: 'Gemini 3.8 Flash' +copilot_gemini_4_argon: 'Gemini 4 Argon' # OpenAI GPT series: copilot_gpt_4o: 'GPT-4o' copilot_gpt_4o_mini: 'GPT-4o mini' diff --git a/src/automated-pipelines/README.md b/src/automated-pipelines/README.md index 3fbd97cf00d1..46e6e1564171 100644 --- a/src/automated-pipelines/README.md +++ b/src/automated-pipelines/README.md @@ -44,7 +44,7 @@ When creating a new pipeline, the source data that is being consumed may not hav - Each pipeline typically requires a workflow to allow scheduling or manually running the pipeline. The workflow should be placed in the `.github/workflows` directory and named `sync-.ts`. Each workflow typically requires adding a manual run option and an input parameter to specify the source repo's branch to use. - Each pipeline will need a `scripts` directory with (at minimum) a `scripts/sync.ts` file to run the pipeline. - If the pipeline will contain structured data, you will need to add a `src//data` directory. The files inside the `data` directory are typically organized by version (e.g., `src/webhooks/data/fpt/*`). -- Pipelines typically have tests specific to the pipeline that are placed in the `src//tests` directory. There is no need to add tests that render the page because all autogenerated pages are tested in `src/automated-pipelines/tests/rendering.ts`. +- Pipelines typically have tests specific to the pipeline that are placed in the `src//tests` directory. There is no need to add tests that render the page because all autogenerated pages are tested in `src/automated-pipelines/tests/rendering.ts`. Add the new `src//**` path to the "Check for automated pipeline changes" step in `.github/workflows/test.yml` so PRs that change the pipeline render every page. - If the pipeline uses a Next.js page component (e.g., `pages/**/*.tsx`), ensure there is a test that fails if that page component is moved or deleted. ## How to get help @@ -175,6 +175,12 @@ All autogenerated pages tested in `src/automated-pipelines/tests/rendering.ts`: - Links are valid - Schema validation +The merge queue and PRs that change pipeline code or generated REST, GraphQL, or CodeQL CLI content render every autogenerated page. Other PRs and local runs render the pages in `PINNED_PAGES` in `src/automated-pipelines/tests/rendering.ts`, and every page for generators not listed there. To render every page locally, run: + +```bash +AUTOGENERATED_RENDER_ALL=true npm test -- src/automated-pipelines/tests/rendering.ts +``` + ### Pipeline-specific tests Each pipeline in `src//tests/` should test: diff --git a/src/automated-pipelines/tests/rendering.ts b/src/automated-pipelines/tests/rendering.ts index 9e729afc102f..470098f061be 100644 --- a/src/automated-pipelines/tests/rendering.ts +++ b/src/automated-pipelines/tests/rendering.ts @@ -9,6 +9,7 @@ import { get } from '@/tests/helpers/e2etest' type Page = { autogenerated?: string fullPath: string + relativePath: string permalinks: Array<{ href: string }> versions: { feature?: string | string[] @@ -18,16 +19,61 @@ type Page = { const pageList: Page[] = await loadPages(undefined, ['en']) +// CI sets AUTOGENERATED_RENDER_ALL in the merge queue and on PRs that change pipeline code or output. +// Otherwise, render a fixed set of pages per generator to keep local runs and PRs fast. +const RENDER_ALL = process.env.AUTOGENERATED_RENDER_ALL === 'true' + +// Paths are relative to content/. Generators not listed here render every page. +// Pick pages that use the generator's main page layout, not index pages. +const PINNED_PAGES: Record = { + 'codeql-cli': [ + 'code-security/reference/code-scanning/codeql/codeql-cli-manual/database-analyze.md', + 'code-security/reference/code-scanning/codeql/codeql-cli-manual/database-create.md', + 'code-security/reference/code-scanning/codeql/codeql-cli-manual/github-merge-results.md', + ], + 'github-apps': [ + 'rest/authentication/endpoints-available-for-github-app-installation-access-tokens.md', + 'rest/authentication/permissions-required-for-fine-grained-personal-access-tokens.md', + 'rest/authentication/permissions-required-for-github-apps.md', + ], + graphql: [ + 'graphql/overview/breaking-changes.md', + 'graphql/overview/changelog/2025.md', + 'graphql/reference/repos.md', + ], + rest: ['rest/actions/workflow-runs.md', 'rest/issues/issues.md', 'rest/repos/repos.md'], +} + +function pagesToRender(generator: string, pages: Page[]): { render: Page[]; missing: string[] } { + const pinned = PINNED_PAGES[generator] ?? [] + const byPath = new Map(pages.map((page) => [page.relativePath, page])) + // Check pins in full-render mode too, so a PR that moves a pinned page fails before merge. + const missing = pinned.filter((path) => !byPath.has(path)) + if (RENDER_ALL || !pinned.length) return { render: pages, missing } + return { render: pinned.flatMap((path) => byPath.get(path) ?? []), missing } +} + describe('autogenerated docs render', () => { vi.setConfig({ testTimeout: 3 * 60 * 1000 }) const autogeneratedPages = pageList.filter((page: Page) => page.autogenerated) + const pagesByGenerator = Object.groupBy(autogeneratedPages, (page: Page) => page.autogenerated!) + // Include pinned generators with no pages left, so their stale pins still fail. + const generators = new Set([...Object.keys(pagesByGenerator), ...Object.keys(PINNED_PAGES)]) + const generatorCases = [...generators].map( + (generator) => [generator, pagesByGenerator[generator] ?? []] as const, + ) - test('all automated pages', async () => { + test.each(generatorCases)('%s pages', async (generator, allPages) => { + const { render: pages, missing } = pagesToRender(generator, allPages) + expect( + missing, + `Pinned ${generator} pages not found. Update PINNED_PAGES in this file.`, + ).toEqual([]) // Each page must render with 200 OK and unique heading IDs. const errors = ( await Promise.all( - autogeneratedPages.map(async (page: Page) => { + pages.map(async (page: Page) => { const url = page.permalinks[0].href // Slow autogenerated pages get retries to avoid false-positive failures. const res = await get(url, { retries: 3 }) diff --git a/src/events/components/events.ts b/src/events/components/events.ts index cc36319bf818..0248b61992e1 100644 --- a/src/events/components/events.ts +++ b/src/events/components/events.ts @@ -115,7 +115,6 @@ export function sendEvent({ path_product: getMetaContent('path-product'), path_article: getMetaContent('path-article'), page_document_type: getMetaContent('page-document-type'), - page_type: getMetaContent('page-type'), content_type: getMetaContent('page-content-type'), docs_team_metrics: getMetaContent('docs-team-metrics') || undefined, status: Number(getMetaContent('status') || 0), @@ -156,7 +155,7 @@ export function sendEvent({ queueEvent(body) // Hydro handles cross-subdomain tracking. - sendHydroAnalyticsEvent(body) + if (ANALYTICS_ENABLED) sendHydroAnalyticsEvent(body) if (type === EventType.exit) { flushQueue() @@ -225,7 +224,7 @@ function getPerformance() { firstContentfulPaint: paint ? paint.startTime / 1000 : undefined, domInteractive: nav ? nav.domInteractive / 1000 : undefined, domComplete: nav ? nav.domComplete / 1000 : undefined, - render: nav ? (nav.responseEnd - nav.requestStart) / 1000 : undefined, + render: nav ? Math.max(0, nav.responseEnd - nav.requestStart) / 1000 : undefined, } } diff --git a/src/events/components/hydro-analytics.ts b/src/events/components/hydro-analytics.ts index 065799d36ab1..a9c8d9a9078d 100644 --- a/src/events/components/hydro-analytics.ts +++ b/src/events/components/hydro-analytics.ts @@ -63,10 +63,6 @@ export function prepareData(body: Record): { // BI dashboards expect react_app and marketing page_type for analytics_v0_page_view. context.react_app = 'docs' - // Preserve the docs page type because BI expects page_type to be marketing. - if (context.page_type) { - context.docs_page_type = context.page_type - } context.page_type = 'marketing' return { type: typeof type === 'string' ? type : 'unknown', context } diff --git a/src/events/lib/schema.ts b/src/events/lib/schema.ts index ba1ecc998848..27b2253255c9 100644 --- a/src/events/lib/schema.ts +++ b/src/events/lib/schema.ts @@ -96,13 +96,6 @@ const context = { // Keep in sync with get-document-type.ts. enum: ['homepage', 'early-access', 'product', 'category', 'subcategory', 'article'], }, - page_type: { - type: 'string', - description: 'Optional page type from the content frontmatter.', - // Keep in sync with the YAML frontmatter docs: - // content/contributing/writing-for-github-docs/using-yaml-frontmatter.md. - enum: ['overview', 'quick_start', 'tutorial', 'how_to', 'reference', 'rai'], - }, content_type: { type: 'string', description: 'Optional content type from the content frontmatter (EDI content models).', @@ -415,7 +408,8 @@ const search = { }, search_client: { type: 'string', - description: 'The client name identifier when the request is not from docs.github.com.', + description: + "The client name identifier when the request is not from docs.github.com, or 'unknown' when the request sends no client_name.", }, }, } diff --git a/src/events/tests/hydro-analytics.ts b/src/events/tests/hydro-analytics.ts index 2f00389bb67c..dd6b55d2657a 100644 --- a/src/events/tests/hydro-analytics.ts +++ b/src/events/tests/hydro-analytics.ts @@ -109,23 +109,12 @@ describe('prepareData', () => { expect(result.context.react_app).toBe('docs') }) - test('adds page_type field when not present', () => { + test('sets page_type to marketing', () => { const body = { type: 'page', context: { event_id: '123' }, } const result = prepareData(body) expect(result.context.page_type).toBe('marketing') - expect(result.context.docs_page_type).toBeUndefined() - }) - - test('preserves existing page_type as docs_page_type and sets page_type to marketing', () => { - const body = { - type: 'page', - context: { event_id: '123', page_type: 'article' }, - } - const result = prepareData(body) - expect(result.context.page_type).toBe('marketing') - expect(result.context.docs_page_type).toBe('article') }) }) diff --git a/src/events/types.ts b/src/events/types.ts index 8544824d2c6a..47929cced156 100644 --- a/src/events/types.ts +++ b/src/events/types.ts @@ -36,7 +36,6 @@ export type EventProps = { path_product?: string path_article: string page_document_type: string - page_type: string content_type: string docs_team_metrics?: string status: number diff --git a/src/ghes-releases/lib/deprecation-steps.md b/src/ghes-releases/lib/deprecation-steps.md index 5e56316b8274..d4bb172ef659 100644 --- a/src/ghes-releases/lib/deprecation-steps.md +++ b/src/ghes-releases/lib/deprecation-steps.md @@ -74,7 +74,11 @@ If the date in the [release date list](https://github.com/github/enterprise-rele ## Step 6: Dry-run the scrape -Update your translation clones to the latest `main`. Then hide the search components so they don't get scraped into the static archive: in `src/frame/components/page-header/Header.tsx`, temporarily set the `SubdomainNavBar.Search` component's `className` to `"visually-hidden"`. Also wrap the `SearchOverlay` in `src/search/components/input/SearchOverlayContainer.tsx` in a `
`. +Update your translation clones to the latest `main`. + +Hide the search components so they don't get scraped into the static archive: in `src/frame/components/page-header/Header.tsx`, temporarily set the `SubdomainNavBar.Search` component's `className` to `"visually-hidden"`. Also wrap the `SearchOverlay` in `src/search/components/input/SearchOverlayContainer.tsx` in a `
`. + +Turn off analytics too. Archived pages keep running their scraped JavaScript, and `/api/events` rejects their events because the version is no longer supported. In `src/frame/lib/constants.ts`, temporarily set `ANALYTICS_ENABLED` to `false`. That stops requests to both `/api/events` and `collector.githubapp.com`. Build, then scrape a few pages locally: @@ -89,7 +93,7 @@ Serve the output over HTTP to review it. Opening the files directly doesn't work python3 -m http.server 8080 --bind 127.0.0.1 -d tmpArchivalDir_{{ release-number }}/{{ release-number }} ``` -Confirm that a few pages in a couple of languages render, styles load, and the version dropdowns work. Offer to open the pages for the human in their browser. +Confirm that a few pages in a couple of languages render, styles load, and the version dropdowns work. Also confirm that the browser's network tab shows no requests to `/api/events` or `collector.githubapp.com`, including after you click a tool picker or code tab. Offer to open the pages for the human in their browser. 🛑 **HUMAN**: Review the dry-run output before the full scrape. @@ -103,10 +107,10 @@ Scrape every page. This takes 20-30 minutes and overwrites the dry-run output: npm run deprecate-ghes-archive ``` -Revert the search component edits: +Revert the search component and analytics edits: ```shell -git checkout src/frame/components/page-header/Header.tsx src/search/components/input/SearchOverlayContainer.tsx +git checkout src/frame/components/page-header/Header.tsx src/search/components/input/SearchOverlayContainer.tsx src/frame/lib/constants.ts ``` ## Step 8: Publish the archive @@ -265,8 +269,7 @@ To keep your clones elsewhere instead, clone the repositories below and map each ## Reference: Re-scraping a page or all pages -Occasionally a change needs to land in an already-archived version. The archive script always scrapes the current oldest supported version, so check out the `enter -ise-{{ release-number }}-deprecation` tag, which points at history from before the version left `supported`, and re-scrape with `npm run deprecate-ghes-archive`. To scrape a single page, use the `--page ` option, passing the path without a version or language prefix. Upload the new files to `github/docs-ghes-{{ release-number }}` for each language. +Occasionally a change needs to land in an already-archived version. The archive script always scrapes the current oldest supported version, so check out the `enterprise-{{ release-number }}-deprecation` tag, which points at history from before the version left `supported`. Repeat the Step 6 search and analytics edits, run `npm run build`, and re-scrape with `npm run deprecate-ghes-archive`. To scrape a single page, use the `--page ` option, passing the path without a version or language prefix. Revert the edits as in Step 7, then upload the new files to `github/docs-ghes-{{ release-number }}` for each language. Human: After uploading, purge the Fastly cache. From Okta, open Fastly, select `docs`, and click "Purge" then "Purge URL", or "Purge All" for a whole path. Enter the URL or path and do a soft purge. diff --git a/src/languages/lib/correct-translation-content.ts b/src/languages/lib/correct-translation-content.ts index 0256ef75e064..c97888abffaa 100644 --- a/src/languages/lib/correct-translation-content.ts +++ b/src/languages/lib/correct-translation-content.ts @@ -94,6 +94,13 @@ export function correctTranslatedContentStrings( if (context.code === 'es') { content = content.replace(/\{%:/g, '{%') + // The translator replaced the space after "data" with a dot, as in {% datos.variables.x %}, + // or inserted a stray word, as in {% datos existente variables.x %}. + content = content.replace( + /\{%(-?)\s*datos(?:\.|\s+existente\s+)(variables|reusables)\./g, + '{%$1 data $2.', + ) + content = content.replaceAll('{% advertencia %}', '{% warning %}') content = content.replaceAll('{% siVersion ', '{% ifversion ') content = content.replaceAll('{%- siVersion ', '{%- ifversion ') @@ -671,6 +678,9 @@ export function correctTranslatedContentStrings( } if (context.code === 'zh') { + // Chinese "变量" ("variables") glued to "data" with no space. + // Also the space-separated form, as in {% data变量 x.y %}, which loses the dot. + content = content.replace(/\{%(-?)\s*data变量(?:\.|\s+)/g, '{%$1 data variables.') content = content.replaceAll('{% 数据variables', '{% data variables') content = content.replaceAll('{% 数据 variables', '{% data variables') content = content.replaceAll('{%数据variables', '{% data variables') @@ -1247,6 +1257,13 @@ export function correctTranslatedContentStrings( } if (context.code === 'ko') { + // Misspelled "data" keyword and translated IDE/app tag names + content = content.replaceAll('{% dada variables', '{% data variables') + content = content.replaceAll('{%- dada variables', '{%- data variables') + content = content.replaceAll('{% 이클립스 %}', '{% eclipse %}') + content = content.replaceAll('{%- 이클립스 %}', '{%- eclipse %}') + content = content.replaceAll('{% 데스크톱 %}', '{% desktop %}') + content = content.replaceAll('{%- 데스크톱 %}', '{%- desktop %}') content = content.replaceAll('{% 경고 %}', '{% warning %}') content = content.replaceAll('{% datda variables', '{% data variables') content = content.replaceAll('{%- datda variables', '{%- data variables') @@ -1611,6 +1628,9 @@ export function correctTranslatedContentStrings( return match.replace(/[«»“”„]/g, '"').replace(/[‘’‚]/g, "'") }) + // The translator dropped the space between octicon and its quoted name. + content = content.replace(/\{%(-?)\s*octicon(?=")/g, '{%$1 octicon ') + // French « check » becomes " check " after quote normalization. content = content.replace( /\{%(-?)\s*octicon\s+"\s*([\w-]+)\s*"([^%]*?)(-?)%\}/g, diff --git a/src/languages/tests/correct-translation-content.ts b/src/languages/tests/correct-translation-content.ts index e9e81b3b9c9c..7748ecf1fa5e 100644 --- a/src/languages/tests/correct-translation-content.ts +++ b/src/languages/tests/correct-translation-content.ts @@ -3405,4 +3405,76 @@ Para más información, consulta "[AUTOTITLE](/path)". expect(fix(blank, 'fr')).toBe(blank) }) }) + + describe('data keyword glued to variables or reusables', () => { + test('es: {% datos.variables. and a stray word between datos and variables', () => { + expect(fix('{% datos.variables.product.company_short %}', 'es')).toBe( + '{% data variables.product.company_short %}', + ) + expect(fix('un {% datos existente variables.copilot.x %} de su', 'es')).toBe( + 'un {% data variables.copilot.x %} de su', + ) + const ok = '{% data variables.product.company_short %}' + expect(fix(ok, 'es')).toBe(ok) + }) + + test('zh: {% data变量. becomes {% data variables.', () => { + expect(fix('{% data变量.dependabot.github_presets %} 是规则', 'zh')).toBe( + '{% data variables.dependabot.github_presets %} 是规则', + ) + const ok = '{% data variables.dependabot.github_presets %}' + expect(fix(ok, 'zh')).toBe(ok) + }) + + test('zh: space-separated data变量 restores the dot', () => { + expect(fix('{% data变量 dependabot.github_presets %} 是规则', 'zh')).toBe( + '{% data variables.dependabot.github_presets %} 是规则', + ) + expect( + fix( + '{% data变量 dependabot.github_presets %} 是规则', + 'zh', + '{% data variables.dependabot.github_presets %} are rules', + ), + ).toBe('{% data variables.dependabot.github_presets %} 是规则') + }) + + test('zh: space-separated data变量 with only whitespace-control data tags', () => { + expect( + fix( + '{%- data变量 dependabot.github_presets %} 是规则', + 'zh', + '{%- data variables.dependabot.github_presets %} are rules', + ), + ).toBe('{%- data variables.dependabot.github_presets %} 是规则') + }) + }) + + describe('octicon missing space before quoted name', () => { + test('restores the space, including after curly-quote normalization', () => { + expect(fix('{% octicon“kebab-horizontal” aria-label=“图标” %}', 'zh')).toBe( + '{% octicon "kebab-horizontal" aria-label="图标" %}', + ) + expect(fix('{% octicon"pulse" aria-hidden="true" %}', 'ja')).toBe( + '{% octicon "pulse" aria-hidden="true" %}', + ) + }) + + test('leaves a correct octicon unchanged', () => { + const ok = '{% octicon "pulse" aria-hidden="true" %}' + expect(fix(ok, 'ja')).toBe(ok) + }) + }) + + describe('Korean data keyword and tag names', () => { + test('fixes {%- dada and translated eclipse and desktop tags', () => { + expect(fix('{%- dada variables.product.prodname_actions %}', 'ko')).toBe( + '{%- data variables.product.prodname_actions %}', + ) + expect(fix('{% 이클립스 %}x{% endeclipse %}', 'ko')).toBe('{% eclipse %}x{% endeclipse %}') + expect(fix('{%- 데스크톱 %}x{% enddesktop %}', 'ko')).toBe('{%- desktop %}x{% enddesktop %}') + const ok = '{% data variables.product.prodname_actions %}' + expect(fix(ok, 'ko')).toBe(ok) + }) + }) }) diff --git a/src/search/lib/helpers/external-search-analytics.ts b/src/search/lib/helpers/external-search-analytics.ts index c532d77b077f..bdbbafc28d70 100644 --- a/src/search/lib/helpers/external-search-analytics.ts +++ b/src/search/lib/helpers/external-search-analytics.ts @@ -73,7 +73,7 @@ export async function handleExternalSearchAnalytics( }, search_query: 'REDACTED', search_context: searchContext, - search_client: client_name as string, + search_client: (client_name as string) || 'unknown', }, } diff --git a/src/search/tests/external-search-analytics.ts b/src/search/tests/external-search-analytics.ts new file mode 100644 index 000000000000..484b60babd23 --- /dev/null +++ b/src/search/tests/external-search-analytics.ts @@ -0,0 +1,48 @@ +import { describe, test, expect, vi, beforeEach } from 'vitest' + +import { publish } from '@/events/lib/hydro' +import { handleExternalSearchAnalytics } from '@/search/lib/helpers/external-search-analytics' +import type { ExtendedRequest } from '@/types' + +vi.mock('@/events/lib/hydro', () => ({ + publish: vi.fn(), +})) + +function buildRequest(headers: Record, query: Record = {}) { + return { headers: { host: 'docs.github.com', ...headers }, query } as unknown as ExtendedRequest +} + +function publishedClient() { + const payload = (publish as ReturnType).mock.calls[0][0] + return payload.value.search_client +} + +describe('handleExternalSearchAnalytics', () => { + beforeEach(() => { + vi.mocked(publish).mockClear() + }) + + test('sends the client_name as search_client', async () => { + await handleExternalSearchAnalytics( + buildRequest({ 'user-agent': 'curl/8.0' }, { client_name: 'my-tool' }), + 'general-search', + ) + expect(publishedClient()).toBe('my-tool') + }) + + test('sends unknown when a browser-like request has no client_name', async () => { + await handleExternalSearchAnalytics( + buildRequest({ 'user-agent': 'SomeAgent/1.0' }), + 'general-search', + ) + expect(publishedClient()).toBe('unknown') + }) + + test('skips analytics for the docs.github.com frontend', async () => { + await handleExternalSearchAnalytics( + buildRequest({ 'sec-fetch-site': 'same-origin' }, { client_name: 'docs.github.com-client' }), + 'general-search', + ) + expect(publish).not.toHaveBeenCalled() + }) +}) diff --git a/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml b/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml index 11a68d0a98cd..18a1e827f22f 100644 --- a/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml +++ b/src/secret-scanning/data/pattern-docs/fpt/public-docs.yml @@ -4648,7 +4648,7 @@ supportedSecret: Starlink Public API V2 Client Secret secretType: starlink_public_api_v2_secret isPublic: true - isPrivateWithGhas: false + isPrivateWithGhas: true hasPushProtection: false hasValidityCheck: false hasExtendedMetadata: false diff --git a/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml b/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml index 11a68d0a98cd..18a1e827f22f 100644 --- a/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml +++ b/src/secret-scanning/data/pattern-docs/ghec/public-docs.yml @@ -4648,7 +4648,7 @@ supportedSecret: Starlink Public API V2 Client Secret secretType: starlink_public_api_v2_secret isPublic: true - isPrivateWithGhas: false + isPrivateWithGhas: true hasPushProtection: false hasValidityCheck: false hasExtendedMetadata: false diff --git a/src/tests/SUITES.md b/src/tests/SUITES.md index d1a556bdfcd9..56c1e7ef28c9 100644 --- a/src/tests/SUITES.md +++ b/src/tests/SUITES.md @@ -22,7 +22,7 @@ Note: for build-gating suites (data-directory, content-linter, generated-referen | article-api | Article API endpoints (`/api/pagelist*`, `/api/article/body`, `/api/article/meta`) behind hovercards, llms.txt, and markdown delivery, plus the transformers for autogenerated pages. | Local server with fixture ROOT; no Elasticsearch/GitHub/external network. | **Medium**: reliable (no external infra) but covers real user/API surfaces. Contained, not site-wide. | | assets | Dynamic image processing (PNG to WebP, resizing), static asset cache headers keyed by checksum URL, and archived enterprise asset proxying. | Local Express server; checked-in fixture images; external calls mocked with nock. | **Low**: deterministic, mocked network, clear failures. | | audit-logs | Audit log event structure, field merging, allowlist filtering, GHES version handling, category notes, and page rendering. | Unit tests run standalone; the rendering test needs the local server (getDOM). | **Medium**: unit tests reliable; the rendering test can flake. Contained to the audit-log reference. | -| automated-pipelines | The pipeline that auto-generates/syncs content from external sources into markdown, plus autogenerated-page rendering and semver conversion. | Local server for the rendering check (retried HTTP calls); fixtures. | **Medium**: affects rendered docs; the rendering test's retried HTTP calls can flake. | +| automated-pipelines | The pipeline that auto-generates/syncs content from external sources into markdown, plus autogenerated-page rendering and semver conversion. | Local server for the rendering check (retried HTTP calls); fixtures. Renders all autogenerated pages in the merge queue and on PRs that change pipeline code or generated content, a pinned set per generator otherwise. | **Medium**: affects rendered docs; the rendering test's retried HTTP calls can flake. | | color-schemes | Theme cookie parsing and mapping between CSS and Primer React color modes, including bad-input handling. | None (pure-function unit tests). | **Low**: pure-function unit tests, no flake. | | content-linter | Markdown/YAML lint compliance: links, frontmatter schema, Liquid syntax, code annotations, alt text, versioning, and data references. | Changed files (DIFF_FILE) or full repo; markdownlint + custom rules. | **Low**: deterministic; reds = real content problems in the PR. | | content-render | The Markdown/Liquid to HTML rendering pipeline: templates, parsing, syntax highlighting, link rewriting, and content transforms. | In-process rendering (unified + LiquidJS); cheerio; no external services. | **High**: affects every page; reds = real rendering bugs site-wide. |