diff --git a/.github/actions/slack-alert/action.yml b/.github/actions/slack-alert/action.yml index e22e46d2f06e..5c5018d16c93 100644 --- a/.github/actions/slack-alert/action.yml +++ b/.github/actions/slack-alert/action.yml @@ -2,28 +2,27 @@ name: Send Slack notification if workflow fails description: Send Slack notification if workflow fails inputs: - slack_channel_id: - description: Slack channel ID - required: true slack_token: description: Slack token required: true + slack_channel_id: + description: Slack channel ID. Defaults to the docs-alerts channel (CG5MJHMB2). + default: CG5MJHMB2 # docs-alerts + required: false message: description: The message to send to Slack default: The last '${{ github.workflow }}' run failed. See ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }} required: false - color: - description: The color of the Slack message - default: failure - required: false runs: using: composite steps: - name: Send Slack notification if workflow fails - uses: someimportantcompany/github-actions-slack-message@a975b440de2bcef178d451cc70d4c1161b5a30cd + uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 with: - channel: ${{ inputs.slack_channel_id }} - bot-token: ${{ inputs.slack_token }} - color: ${{ inputs.color }} - text: ${{ inputs.message }} + method: chat.postMessage + token: ${{ inputs.slack_token }} + errors: true + payload: | + channel: ${{ toJSON(inputs.slack_channel_id) }} + text: ${{ toJSON(inputs.message) }} diff --git a/.github/workflows/benchmark-pages.yml b/.github/workflows/benchmark-pages.yml index 69e40cc8f2f2..2d7f4fc9acc3 100644 --- a/.github/workflows/benchmark-pages.yml +++ b/.github/workflows/benchmark-pages.yml @@ -161,7 +161,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/close-on-invalid-label.yaml b/.github/workflows/close-on-invalid-label.yaml index ec54378401d8..0e808effc1f9 100644 --- a/.github/workflows/close-on-invalid-label.yaml +++ b/.github/workflows/close-on-invalid-label.yaml @@ -44,5 +44,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'pull_request_target' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/codeql.yml b/.github/workflows/codeql.yml index dfe663247f56..70fcc75736f9 100644 --- a/.github/workflows/codeql.yml +++ b/.github/workflows/codeql.yml @@ -39,7 +39,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'pull_request' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/confirm-internal-staff-work-in-docs.yml b/.github/workflows/confirm-internal-staff-work-in-docs.yml index 8551eadf2970..039525edb066 100644 --- a/.github/workflows/confirm-internal-staff-work-in-docs.yml +++ b/.github/workflows/confirm-internal-staff-work-in-docs.yml @@ -75,11 +75,17 @@ jobs: - name: Send Slack notification if a GitHub employee who isn't on the docs team opens an issue in public if: ${{ steps.membership_check.outputs.did_warn && github.repository == 'github/docs' }} - uses: someimportantcompany/github-actions-slack-message@a975b440de2bcef178d451cc70d4c1161b5a30cd + uses: slackapi/slack-github-action@45a88b9581bfab2566dc881e2cd66d334e621e2c # v3.0.3 + 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 }} with: - channel: ${{ secrets.DOCS_OPEN_SOURCE_SLACK_CHANNEL_ID }} - bot-token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - text: <@${{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. + method: chat.postMessage + token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} + errors: true + payload: | + channel: ${{ toJSON(env.SLACK_CHANNEL_ID) }} + text: ${{ toJSON(env.SLACK_MESSAGE) }} - name: Check out repo if: ${{ failure() && github.event_name != 'pull_request_target' }} @@ -87,5 +93,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'pull_request_target' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/content-pipelines.yml b/.github/workflows/content-pipelines.yml index 9dde86a5f766..c33e1ce46856 100644 --- a/.github/workflows/content-pipelines.yml +++ b/.github/workflows/content-pipelines.yml @@ -190,7 +190,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/copy-api-issue-to-internal.yml b/.github/workflows/copy-api-issue-to-internal.yml index 09adbdb8f066..a76c18026330 100644 --- a/.github/workflows/copy-api-issue-to-internal.yml +++ b/.github/workflows/copy-api-issue-to-internal.yml @@ -77,5 +77,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' && github.repository == 'github/docs-internal' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/create-changelog-pr.yml b/.github/workflows/create-changelog-pr.yml index 2167ff74e9fb..324c2bea6a41 100644 --- a/.github/workflows/create-changelog-pr.yml +++ b/.github/workflows/create-changelog-pr.yml @@ -162,7 +162,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/delete-orphan-translation-files.yml b/.github/workflows/delete-orphan-translation-files.yml index 3b6e4f2f70b8..aa1757f1c51d 100644 --- a/.github/workflows/delete-orphan-translation-files.yml +++ b/.github/workflows/delete-orphan-translation-files.yml @@ -159,7 +159,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/docs-review-collect.yml b/.github/workflows/docs-review-collect.yml index 5ca21c43c169..2d6ff09f556c 100644 --- a/.github/workflows/docs-review-collect.yml +++ b/.github/workflows/docs-review-collect.yml @@ -45,7 +45,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/enterprise-dates.yml b/.github/workflows/enterprise-dates.yml index b56bd1f94fb1..b0fb9fe1f1ea 100644 --- a/.github/workflows/enterprise-dates.yml +++ b/.github/workflows/enterprise-dates.yml @@ -72,7 +72,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/enterprise-release-issue.yml b/.github/workflows/enterprise-release-issue.yml index cb40f2dd0184..faffb8e9c471 100644 --- a/.github/workflows/enterprise-release-issue.yml +++ b/.github/workflows/enterprise-release-issue.yml @@ -36,7 +36,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/index-autocomplete-search.yml b/.github/workflows/index-autocomplete-search.yml index f2510aa5c355..e30786b21de9 100644 --- a/.github/workflows/index-autocomplete-search.yml +++ b/.github/workflows/index-autocomplete-search.yml @@ -48,7 +48,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name == 'schedule' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/index-general-search.yml b/.github/workflows/index-general-search.yml index 6c0b43fd6fec..ef82ce76b22b 100644 --- a/.github/workflows/index-general-search.yml +++ b/.github/workflows/index-general-search.yml @@ -98,7 +98,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue if: ${{ failure() && github.event_name != 'workflow_dispatch' }} @@ -246,7 +245,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue @@ -385,15 +383,12 @@ jobs: if: ${{ steps.check-artifacts.outputs.has_artifacts == 'true' && fromJSON(steps.aggregate.outputs.result || '{"hasFailures":false}').hasFailures }} uses: ./.github/actions/slack-alert with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - color: warning message: ${{ fromJSON(steps.aggregate.outputs.result || '{"message":""}').message }} - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/keep-caches-warm.yml b/.github/workflows/keep-caches-warm.yml index 14a34fc8bea6..b65ffa81fba3 100644 --- a/.github/workflows/keep-caches-warm.yml +++ b/.github/workflows/keep-caches-warm.yml @@ -47,7 +47,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/link-check-external.yml b/.github/workflows/link-check-external.yml index 8b1cde812e64..25a0569c7af0 100644 --- a/.github/workflows/link-check-external.yml +++ b/.github/workflows/link-check-external.yml @@ -80,7 +80,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/link-check-github-github.yml b/.github/workflows/link-check-github-github.yml index d56a8c162a26..066a572d45d0 100644 --- a/.github/workflows/link-check-github-github.yml +++ b/.github/workflows/link-check-github-github.yml @@ -74,7 +74,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/link-check-internal.yml b/.github/workflows/link-check-internal.yml index 3bbbf0ca9d4a..614b0b2a83ad 100644 --- a/.github/workflows/link-check-internal.yml +++ b/.github/workflows/link-check-internal.yml @@ -60,7 +60,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue @@ -189,7 +188,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue @@ -256,7 +254,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/lint-entire-content-data-markdown.yml b/.github/workflows/lint-entire-content-data-markdown.yml index 32dd7a4755dd..81b51ea69476 100644 --- a/.github/workflows/lint-entire-content-data-markdown.yml +++ b/.github/workflows/lint-entire-content-data-markdown.yml @@ -46,7 +46,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/moda-allowed-ips.yml b/.github/workflows/moda-allowed-ips.yml index 20d0623fc06e..eefba673bca2 100644 --- a/.github/workflows/moda-allowed-ips.yml +++ b/.github/workflows/moda-allowed-ips.yml @@ -56,7 +56,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/move-reopened-issues-to-triage.yaml b/.github/workflows/move-reopened-issues-to-triage.yaml index 04b3e6e8c9c7..6c7c990d3ac0 100644 --- a/.github/workflows/move-reopened-issues-to-triage.yaml +++ b/.github/workflows/move-reopened-issues-to-triage.yaml @@ -49,5 +49,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/needs-sme-stale-check.yaml b/.github/workflows/needs-sme-stale-check.yaml index 589993d3d3ab..8aab4baaaff2 100644 --- a/.github/workflows/needs-sme-stale-check.yaml +++ b/.github/workflows/needs-sme-stale-check.yaml @@ -39,7 +39,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/needs-sme-workflow.yml b/.github/workflows/needs-sme-workflow.yml index 284ece93107a..e136add25d8a 100644 --- a/.github/workflows/needs-sme-workflow.yml +++ b/.github/workflows/needs-sme-workflow.yml @@ -33,7 +33,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'pull_request_target' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} add-pr-comment: @@ -54,5 +53,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'pull_request_target' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/no-response.yaml b/.github/workflows/no-response.yaml index 004db7603208..4139204e1353 100644 --- a/.github/workflows/no-response.yaml +++ b/.github/workflows/no-response.yaml @@ -61,7 +61,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/notify-about-deployment.yml b/.github/workflows/notify-about-deployment.yml index 0aa3dd0b97b4..c959eaaf049a 100644 --- a/.github/workflows/notify-about-deployment.yml +++ b/.github/workflows/notify-about-deployment.yml @@ -50,7 +50,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/orphaned-features-check.yml b/.github/workflows/orphaned-features-check.yml index abcd530e78a6..5689a95f764a 100644 --- a/.github/workflows/orphaned-features-check.yml +++ b/.github/workflows/orphaned-features-check.yml @@ -102,7 +102,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name == 'schedule' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/orphaned-files-check.yml b/.github/workflows/orphaned-files-check.yml index 48ac612e2fe7..09b604711ab6 100644 --- a/.github/workflows/orphaned-files-check.yml +++ b/.github/workflows/orphaned-files-check.yml @@ -110,7 +110,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name == 'schedule' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/os-ready-for-review.yml b/.github/workflows/os-ready-for-review.yml index 0d15785ecbaa..9fe64913e680 100644 --- a/.github/workflows/os-ready-for-review.yml +++ b/.github/workflows/os-ready-for-review.yml @@ -70,5 +70,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'pull_request_target' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/purge-fastly.yml b/.github/workflows/purge-fastly.yml index 9f5bd1bca7b1..18dc5847c24b 100644 --- a/.github/workflows/purge-fastly.yml +++ b/.github/workflows/purge-fastly.yml @@ -64,5 +64,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/repo-sync.yml b/.github/workflows/repo-sync.yml index c3b968e86940..79bb0b2366a6 100644 --- a/.github/workflows/repo-sync.yml +++ b/.github/workflows/repo-sync.yml @@ -191,7 +191,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml index a5326681e7df..192a2e24b284 100644 --- a/.github/workflows/stale.yml +++ b/.github/workflows/stale.yml @@ -42,7 +42,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/sync-audit-logs.yml b/.github/workflows/sync-audit-logs.yml index 036b8de8dcad..ecbe054c68c8 100644 --- a/.github/workflows/sync-audit-logs.yml +++ b/.github/workflows/sync-audit-logs.yml @@ -112,7 +112,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/sync-codeql-cli.yml b/.github/workflows/sync-codeql-cli.yml index d3351edb695f..2391d2e48add 100644 --- a/.github/workflows/sync-codeql-cli.yml +++ b/.github/workflows/sync-codeql-cli.yml @@ -121,5 +121,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/sync-graphql.yml b/.github/workflows/sync-graphql.yml index 338d9d9bfa89..095b971dc205 100644 --- a/.github/workflows/sync-graphql.yml +++ b/.github/workflows/sync-graphql.yml @@ -75,7 +75,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue @@ -92,9 +91,7 @@ jobs: uses: actions/checkout@8e8c483db84b4bee98b60c0593521ed34d9990e8 # v6.0.1 - uses: ./.github/actions/slack-alert with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - color: warning message: | ⚠️ GraphQL Sync found ${{ needs.update_graphql_files.outputs.ignored-count }} ignored change types: ${{ needs.update_graphql_files.outputs.ignored-types }} diff --git a/.github/workflows/sync-llms-txt.yml b/.github/workflows/sync-llms-txt.yml index b5cf8f2390e7..6c75b2456aa0 100644 --- a/.github/workflows/sync-llms-txt.yml +++ b/.github/workflows/sync-llms-txt.yml @@ -236,7 +236,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/sync-openapi.yml b/.github/workflows/sync-openapi.yml index 4d1762b6f651..4c1221cf36df 100644 --- a/.github/workflows/sync-openapi.yml +++ b/.github/workflows/sync-openapi.yml @@ -121,7 +121,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/sync-sdk-docs.yml b/.github/workflows/sync-sdk-docs.yml index c7ca9ef3f6ad..789da315dcea 100644 --- a/.github/workflows/sync-sdk-docs.yml +++ b/.github/workflows/sync-sdk-docs.yml @@ -241,7 +241,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/sync-secret-scanning.yml b/.github/workflows/sync-secret-scanning.yml index 68f068dde762..42080d486297 100644 --- a/.github/workflows/sync-secret-scanning.yml +++ b/.github/workflows/sync-secret-scanning.yml @@ -79,7 +79,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name != 'workflow_dispatch' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/triage-issue-comments.yml b/.github/workflows/triage-issue-comments.yml index 56921b3663ac..fa2efee04bc2 100644 --- a/.github/workflows/triage-issue-comments.yml +++ b/.github/workflows/triage-issue-comments.yml @@ -70,5 +70,4 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} diff --git a/.github/workflows/triage-stale-check.yml b/.github/workflows/triage-stale-check.yml index 4bd9eaa2565b..17e46b603d27 100644 --- a/.github/workflows/triage-stale-check.yml +++ b/.github/workflows/triage-stale-check.yml @@ -49,7 +49,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue @@ -85,7 +84,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/.github/workflows/validate-github-github-docs-urls.yml b/.github/workflows/validate-github-github-docs-urls.yml index 17192e7298f7..dc61365a69b3 100644 --- a/.github/workflows/validate-github-github-docs-urls.yml +++ b/.github/workflows/validate-github-github-docs-urls.yml @@ -124,7 +124,6 @@ jobs: - uses: ./.github/actions/slack-alert if: ${{ failure() && github.event_name == 'schedule' }} with: - slack_channel_id: ${{ secrets.DOCS_ALERTS_SLACK_CHANNEL_ID }} slack_token: ${{ secrets.SLACK_DOCS_BOT_TOKEN }} - uses: ./.github/actions/create-workflow-failure-issue diff --git a/CHANGELOG.md b/CHANGELOG.md index 0d75da7d7047..5e765fbd9da5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,16 @@ # Docs changelog +**4 June 2026** + +We consolidated all Copilot and code security responsible use articles into structured application cards. Previously, each feature had its own standalone transparency note with inconsistent formatting. Now there are four standardized cards covering Chat, Agents, Inline Suggestions, and Code Security AI features, all following the same template with numbered sections for overview, key terms, capabilities, intended uses, models, limitations, and more. + +* [Application card: GitHub Copilot Chat](https://docs.github.com/en/copilot/responsible-use/chat) +* [Application card: GitHub Copilot Agents](https://docs.github.com/en/copilot/responsible-use/agents) +* [Application card: GitHub Copilot Inline Suggestions](https://docs.github.com/en/copilot/responsible-use/inline-suggestions) +* [Application card: Security and code quality AI features](https://docs.github.com/en/code-security/responsible-use/security-and-quality-ai-features) + +
+ **28 May 2026** We published a new guide for teams that run the CodeQL CLI in their own CI/CD systems and want faster scans. The article covers two techniques that can reduce scan times: diff-informed analysis (report only alerts in changed lines) and overlay analysis (reuse a cached base database instead of rebuilding from scratch). diff --git a/content/graphql/reference/index.md b/content/graphql/reference/index.md index 6f37be1c4a6d..c19465d81286 100644 --- a/content/graphql/reference/index.md +++ b/content/graphql/reference/index.md @@ -11,6 +11,16 @@ redirect_from: - /graphql/reference/unions - /graphql/reference/input-objects - /graphql/reference/scalars + - /graphql/reference/audit-log + - /graphql/reference/billing + - /graphql/reference/code-scanning + - /graphql/reference/code-security + - /graphql/reference/codespaces + - /graphql/reference/collaborators + - /graphql/reference/interactions + - /graphql/reference/pages + - /graphql/reference/scim + - /graphql/reference/secret-scanning versions: fpt: '*' ghec: '*' diff --git a/content/issues/planning-and-tracking-with-projects/understanding-fields/about-issue-fields.md b/content/issues/planning-and-tracking-with-projects/understanding-fields/about-issue-fields.md index d38e83a990ee..c8eafe716170 100644 --- a/content/issues/planning-and-tracking-with-projects/understanding-fields/about-issue-fields.md +++ b/content/issues/planning-and-tracking-with-projects/understanding-fields/about-issue-fields.md @@ -13,8 +13,9 @@ category: Issue fields are organization-level fields that provide consistent, typed metadata across all repositories. Unlike project custom fields, issue fields are defined once at the organization level and are available on every issue and in every project across the organization. For more information on creating and managing issue fields, see [AUTOTITLE](/issues/tracking-your-work-with-issues/using-issues/managing-issue-fields-in-your-organization). -> [!NOTE] -> Issue fields are currently only supported in private projects. Issue fields are not available in public projects. +## Issue fields in public and internal projects + +Only fields with **Public** visibility appear in public and internal projects. Organization-only fields are hidden. For details on configuring field visibility and how visibility changes affect projects, see [AUTOTITLE](/issues/tracking-your-work-with-issues/using-issues/managing-issue-fields-in-your-organization#setting-field-visibility). ## Adding an issue field to a project diff --git a/content/issues/tracking-your-work-with-issues/using-issues/adding-and-managing-issue-fields.md b/content/issues/tracking-your-work-with-issues/using-issues/adding-and-managing-issue-fields.md index 6928837dc1d5..6be91e10ffb2 100644 --- a/content/issues/tracking-your-work-with-issues/using-issues/adding-and-managing-issue-fields.md +++ b/content/issues/tracking-your-work-with-issues/using-issues/adding-and-managing-issue-fields.md @@ -14,6 +14,9 @@ category: Issue fields appear in the right-hand sidebar of issues, alongside system fields like assignees, labels, and type. You can set values when creating or editing an issue. When you select an issue type while creating an issue, any fields pinned to that type automatically appear in the sidebar. +> [!NOTE] +> Issue fields are currently available on issues only. Pull requests do not support issue fields. + ## Setting a field value 1. Navigate to the issue you want to update. @@ -26,6 +29,9 @@ Issue fields appear in the right-hand sidebar of issues, alongside system fields * For **date** fields, use the date picker to select a date, or type the date directly. 1. Changes are saved automatically. +> [!NOTE] +> Issue fields cannot currently be pre-filled via URL query parameters or set through issue templates. To set field values, use the issue sidebar, projects, the API, or {% data variables.product.prodname_actions %}. + ## Editing a field value 1. Navigate to the issue. @@ -87,3 +93,20 @@ Issue fields have full REST and GraphQL API support. You can automate field mana * **Managing fields**: Create, update, and delete organization-level fields. See the [Organization issue fields REST API](/rest/orgs/issue-fields). * **Using fields**: Get, set, and clear field values on individual issues. See the [Issue field values REST API](/rest/issues/issue-field-values). * **GraphQL**: Issue field types and mutations are also available via GraphQL. See the [`IssueFields` union](/graphql/reference/issues#union-issuefields), [issue field objects](/graphql/reference/issues#object-issuefieldtext) (such as `IssueFieldText`, `IssueFieldSingleSelect`, `IssueFieldNumber`, and `IssueFieldDate`), and [mutations](/graphql/reference/issues#mutation-createissuefield) (such as `createIssueField`, `updateIssueField`, and `setIssueFieldValue`). + +## Automating with {% data variables.product.prodname_actions %} + +Issue field changes trigger webhook events on the `issues` event. You can use these as workflow triggers: + +* `field_added`: fires when a field value is set or updated. +* `field_removed`: fires when a field value is cleared. + +For example, to run a workflow whenever a field value changes: + +```yaml +on: + issues: + types: [field_added, field_removed] +``` + +The event payload includes the field name, type, value, and previous value. For more information, see [AUTOTITLE](/actions/reference/workflows-and-actions/events-that-trigger-workflows#issues). diff --git a/content/issues/tracking-your-work-with-issues/using-issues/managing-issue-fields-in-your-organization.md b/content/issues/tracking-your-work-with-issues/using-issues/managing-issue-fields-in-your-organization.md index a88c04b63e0f..4fe806c46743 100644 --- a/content/issues/tracking-your-work-with-issues/using-issues/managing-issue-fields-in-your-organization.md +++ b/content/issues/tracking-your-work-with-issues/using-issues/managing-issue-fields-in-your-organization.md @@ -34,8 +34,20 @@ When issue fields are enabled for your organization, four default fields are cre * **Start date** (date) * **Target date** (date) +These default fields are pinned to issue types as follows: + +| Field | No type | Bug | Task | Feature | +|-------|:-------:|:---:|:----:|:-------:| +| Priority | {% octicon "check" aria-label="Pinned" %} | {% octicon "check" aria-label="Pinned" %} | {% octicon "check" aria-label="Pinned" %} | {% octicon "check" aria-label="Pinned" %} | +| Effort | {% octicon "x" aria-label="Not pinned" %} | {% octicon "check" aria-label="Pinned" %} | {% octicon "check" aria-label="Pinned" %} | {% octicon "check" aria-label="Pinned" %} | +| Start date | {% octicon "x" aria-label="Not pinned" %} | {% octicon "x" aria-label="Not pinned" %} | {% octicon "x" aria-label="Not pinned" %} | {% octicon "check" aria-label="Pinned" %} | +| Target date | {% octicon "x" aria-label="Not pinned" %} | {% octicon "x" aria-label="Not pinned" %} | {% octicon "x" aria-label="Not pinned" %} | {% octicon "check" aria-label="Pinned" %} | + These default fields are fully customizable. You can edit their names, descriptions, and options, or delete them if they don't fit your workflow. +> [!TIP] +> You can rename options, change their colors, reorder them, or add new values to match your team's workflow. For example, you could change Effort options to T-shirt sizes (XS, S, M, L, XL). + ## Creating an issue field {% data reusables.profile.access_org %} @@ -73,6 +85,9 @@ When you delete an issue field, all values set on issues for that field are perm 1. To the right of the field you want to delete, click {% octicon "kebab-horizontal" aria-label="open field options" %}. 1. Click **Delete** and confirm the deletion. +> [!TIP] +> If you don't want to use issue fields, you can delete all default fields from your org settings. This removes them from all issues in your organization. You can re-create fields at any time. + ## Reordering issue fields The order of pinned fields is managed per issue type. The field order determines how fields appear in the issue sidebar and the issue creation modal. @@ -100,6 +115,8 @@ Pinned fields automatically appear in the issue sidebar based on the selected is > [!NOTE] > Fields must be pinned to at least one issue type, or to "Issues without a type", to appear in the issue sidebar. Fields that are not pinned to any type are only accessible via the **Add field** button or in projects. +If a field is not appearing on your issues, check that it is pinned to the relevant issue type or to "Issues without a type". Fields that are not pinned and have no value set are hidden from the issue sidebar. + ## Setting field visibility For organizations with public repositories, you can control whether each issue field is visible to everyone or only to organization members and collaborators. @@ -118,7 +135,22 @@ By default, all new and existing fields are set to "Organization only". Visibili ## Issue fields and projects -Issue fields are available in any project across your organization. For details on adding, removing, and editing issue fields in projects, see [AUTOTITLE](/issues/tracking-your-work-with-issues/using-issues/adding-and-managing-issue-fields#using-issue-fields-in-projects). +Issue fields are available in any project across your organization, including public and internal projects. For details on adding, removing, and editing issue fields in projects, see [AUTOTITLE](/issues/planning-and-tracking-with-projects/understanding-fields/about-issue-fields). + +### Visibility in public and internal projects + +Only fields with **Public** visibility are available in public and internal projects. Fields set to **Organization only** are not displayed. When adding fields to a public project, only public-visibility fields appear in the add-field dialog. + +If a field's visibility is changed from "Public" to "Organization only" while in use in a public project, the field is automatically removed from the project. To restore it, change the field's visibility back to "Public." + +### Migrating from project fields to issue fields + +If you already use project-level custom fields for metadata like priority or effort, you can adopt issue fields to centralize those values at the issue level. + +* Issue fields are the source of truth. The value lives on the issue and is consistent across all projects the issue belongs to. +* Project fields are scoped to a single project. The same issue can have different project field values in different projects. +* Both can coexist. You do not need to remove project fields immediately, but having both can cause confusion if they track the same concept (for example, two "Priority" fields). +* To migrate, create the equivalent issue field, then remove the project-level field from your project views when your team is ready. ### Field limits in projects @@ -129,6 +161,6 @@ Projects support up to 50 fields in total, and issue fields and system fields co | Resource | Limit | |----------|-------| | Issue fields per organization | 25 | -| Options per single-select field | 50 | +| Options per single-select field | 100 | | Pinned fields per issue type | 10 | | Total fields in a project (including issue fields and system fields) | 50 | diff --git a/data/reusables/dependabot/ip-allow-list-dependabot.md b/data/reusables/dependabot/ip-allow-list-dependabot.md index cfc76e19cf6b..f37a3b3784f7 100644 --- a/data/reusables/dependabot/ip-allow-list-dependabot.md +++ b/data/reusables/dependabot/ip-allow-list-dependabot.md @@ -1,7 +1,7 @@ -By default, dynamically provisioned {% data variables.product.github %}-hosted runners do not guarantee static IP addresses. This includes the runners that are used by default with {% data variables.product.prodname_dependabot %}. +{% data variables.product.prodname_dependabot %} is a first-party {% data variables.product.github %} App whose repository access is exempt from IP allow list restrictions. This means {% data variables.product.prodname_dependabot %} can read dependency files and create pull requests regardless of your IP allow list configuration. -If you use an IP allow list and {% data variables.product.prodname_dependabot %}, you must set up a self-hosted runner or enable {% data variables.product.prodname_dependabot %} for use with {% data variables.actions.hosted_runners %}. See [AUTOTITLE](/actions/concepts/runners/about-self-hosted-runners) and [AUTOTITLE](/code-security/dependabot/working-with-dependabot/about-dependabot-on-github-actions-runners#enabling-or-disabling-dependabot-on-larger-runners). +If {% data variables.product.prodname_dependabot %} jobs running on {% data variables.product.prodname_actions %} runners need to reach external resources that require predictable IP addresses (for example, private package registries behind a firewall), you should set up a self-hosted runner or configure {% data variables.actions.hosted_runners %} with a static IP address range. See [AUTOTITLE](/actions/concepts/runners/about-self-hosted-runners) and [AUTOTITLE](/code-security/dependabot/working-with-dependabot/about-dependabot-on-github-actions-runners#enabling-or-disabling-dependabot-on-larger-runners). -Additionally, to learn more about setting up a {% data variables.actions.hosted_runners %} with a static IP address configured, see [AUTOTITLE](/actions/concepts/runners/about-larger-runners). +Additionally, to learn more about configuring {% data variables.actions.hosted_runners %} with a static IP address range, see [AUTOTITLE](/actions/concepts/runners/about-larger-runners). To allow your self-hosted runners or {% data variables.actions.hosted_runners %} to communicate with {% data variables.product.github %}, add the IP address or IP address range of your runners to the IP allow list that you have configured for your enterprise. diff --git a/data/ui.yml b/data/ui.yml index 7d6c90dbe7a7..ca5e6b94ca15 100644 --- a/data/ui.yml +++ b/data/ui.yml @@ -273,6 +273,8 @@ secret_scanning: filter_all: All filter_yes: 'Yes' filter_no: 'No' + clear_filters: Clear filters + clear_filters_aria_label: Clear all filters and search showing_patterns: 'Showing {filtered} of {total} patterns' column_provider: Provider column_secret: Secret diff --git a/package.json b/package.json index 247113c0c334..543eb1028096 100644 --- a/package.json +++ b/package.json @@ -35,7 +35,7 @@ "deleted-assets-pr-comment": "tsx src/assets/scripts/deleted-assets-pr-comment.ts", "deleted-features-pr-comment": "tsx src/data-directory/scripts/deleted-features-pr-comment.ts", "deprecate-ghes": "tsx src/ghes-releases/scripts/deprecate/index.ts", - "deprecate-ghes-archive": "cross-env NODE_OPTIONS=--max-old-space-size=16384 tsx src/ghes-releases/scripts/deprecate/archive-version.ts", + "deprecate-ghes-archive": "cross-env NODE_ENV=production NODE_OPTIONS=--max-old-space-size=16384 tsx src/ghes-releases/scripts/deprecate/archive-version.ts", "dev": "cross-env npm start", "dev-toc": "tsx src/dev-toc/generate.ts", "enable-automerge": "tsx src/workflows/enable-automerge.ts", diff --git a/src/events/lib/schema.ts b/src/events/lib/schema.ts index 01cca0ab460f..89447ebdf4b0 100644 --- a/src/events/lib/schema.ts +++ b/src/events/lib/schema.ts @@ -646,6 +646,39 @@ const preference = { }, } +const tableInteraction = { + type: 'object', + additionalProperties: false, + required: ['type', 'context', 'table_interaction_name', 'table_interaction_type'], + properties: { + context, + type: { + type: 'string', + pattern: '^tableInteraction$', + }, + table_interaction_name: { + type: 'string', + description: + 'Identifier for the table being interacted with (e.g. "secret-scanning-patterns").', + }, + table_interaction_type: { + type: 'string', + enum: ['search', 'filter', 'sort', 'paginate', 'reset'], + description: 'The kind of interaction the user performed with the table.', + }, + table_interaction_field_name: { + type: 'string', + description: + 'The field/column the interaction targeted (e.g. "pushProtection"). Omitted for whole-table actions.', + }, + table_interaction_field_value: { + type: 'string', + description: + 'The value applied to the field (e.g. the filter value, search query, sort direction, or page number).', + }, + }, +} + const validation = { type: 'object', additionalProperties: false, @@ -682,6 +715,7 @@ export const schemas = { clipboard, print, preference, + tableInteraction, validation, } @@ -699,6 +733,7 @@ export const hydroNames = { clipboard: 'docs.v0.ClipboardEvent', print: 'docs.v0.PrintEvent', preference: 'docs.v0.PreferenceEvent', + tableInteraction: 'docs.v0.TableInteractionEvent', validation: 'docs.v0.ValidationEvent', } as Record diff --git a/src/events/tests/middleware.ts b/src/events/tests/middleware.ts index ebac4573d8c5..0dc9363197b5 100644 --- a/src/events/tests/middleware.ts +++ b/src/events/tests/middleware.ts @@ -210,4 +210,38 @@ describe('POST /events', () => { }) expect(statusCode).toBe(400) }) + + test('should accept a tableInteraction filter event', async () => { + const { statusCode } = await checkEvent({ + type: 'tableInteraction', + context: pageExample.context, + table_interaction_name: 'secret-scanning-patterns', + table_interaction_type: 'filter', + table_interaction_field_name: 'pushProtection', + table_interaction_field_value: 'yes', + }) + expect(statusCode).toBe(200) + }) + + test('should accept a tableInteraction event without optional fields', async () => { + const { statusCode } = await checkEvent({ + type: 'tableInteraction', + context: pageExample.context, + table_interaction_name: 'secret-scanning-patterns', + table_interaction_type: 'reset', + }) + expect(statusCode).toBe(200) + }) + + test('should reject a tableInteraction event with an invalid interaction type', async () => { + const { statusCode } = await checkEvent({ + type: 'tableInteraction', + context: pageExample.context, + table_interaction_name: 'secret-scanning-patterns', + table_interaction_type: 'not-a-valid-type', + table_interaction_field_name: 'pushProtection', + table_interaction_field_value: 'yes', + }) + expect(statusCode).toBe(400) + }) }) diff --git a/src/events/types.ts b/src/events/types.ts index 41a323daaf88..84c423e975da 100644 --- a/src/events/types.ts +++ b/src/events/types.ts @@ -12,6 +12,7 @@ export enum EventType { preference = 'preference', clipboard = 'clipboard', print = 'print', + tableInteraction = 'tableInteraction', } export type EventProps = { @@ -135,4 +136,10 @@ export type EventPropsByType = { survey_comment_language?: string survey_connected_event_id?: string } + [EventType.tableInteraction]: { + table_interaction_name: string + table_interaction_type: 'search' | 'filter' | 'sort' | 'paginate' | 'reset' + table_interaction_field_name?: string + table_interaction_field_value?: string + } } diff --git a/src/fixtures/fixtures/data/ui.yml b/src/fixtures/fixtures/data/ui.yml index 7d6c90dbe7a7..ca5e6b94ca15 100644 --- a/src/fixtures/fixtures/data/ui.yml +++ b/src/fixtures/fixtures/data/ui.yml @@ -273,6 +273,8 @@ secret_scanning: filter_all: All filter_yes: 'Yes' filter_no: 'No' + clear_filters: Clear filters + clear_filters_aria_label: Clear all filters and search showing_patterns: 'Showing {filtered} of {total} patterns' column_provider: Provider column_secret: Secret diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts index 3f83cb6787f8..c9dc604eacc0 100644 --- a/src/fixtures/tests/playwright-rendering.spec.ts +++ b/src/fixtures/tests/playwright-rendering.spec.ts @@ -1399,9 +1399,7 @@ test.describe('LandingArticleGridWithFilter component', () => { // Should show "no articles found" message as well const noResultsMessage = page.getByTestId('no-articles-message') await expect(noResultsMessage).toBeVisible() - await expect(page.locator('[aria-live="polite"][aria-atomic="true"]')).toHaveText( - 'No articles found matching your criteria.', - ) + await expect(noResultsMessage).toHaveText('No articles found matching your criteria.') }) test('responsive behavior on different screen sizes', async ({ page }) => { diff --git a/src/ghes-releases/lib/deprecation-steps.md b/src/ghes-releases/lib/deprecation-steps.md index c7a9cf2c5c00..bcf03f1507c7 100644 --- a/src/ghes-releases/lib/deprecation-steps.md +++ b/src/ghes-releases/lib/deprecation-steps.md @@ -7,321 +7,249 @@ labels: - workflow-generated --- -# Deprecation steps for GHES releases +# Deprecate a GitHub Enterprise Server release -The day after a GHES version's [deprecation date](https://github.com/github/docs-internal/tree/main/src/ghes-releases/lib/enterprise-dates.json), a banner on the docs will say: `This version was deprecated on .` This lets users know that the release is deprecated. However, until the release is fully deprecated, it will show up in the Versions dropdown on the docs.github.com site. +This issue is the runbook for fully deprecating a GHES version on docs.github.com. Fully deprecate means: scrape the version's docs into a static archive repository, remove all Markdown, YAML, and JSON content versioned for that release or lower from `github/docs-internal`, and deprecate its OpenAPI description in `github/github`. -When we fully deprecate the release, we remove all any content (YML, JSON, Markdown) versioned for that release or lower. Follow the steps in this issue to fully **deprecate** the docs. +Human: A coding agent such as Copilot CLI drives this runbook while a human supervises. Point your agent at this issue and work through it one step at a time. -**Note**: Each step below, except step 0, must be done in order. Only move on to the next step after successfully completing the previous step. +Throughout, `{{ release-number }}` stands for the version being deprecated. When this runbook is generated as a deprecation issue, the placeholder is filled in for you. -The following large repositories are used throughout this checklist, it may be useful to clone them before you begin: +- Do the steps in order. One at a time. Tell the human each time you start a new step. +- Only move on after the previous step succeeds. +- Keep process notes as you go. The final step uses them. +- A callout appears in the steps: 🛑 **HUMAN**. This means stop and get explicit human sign-off before continuing. These mark points where a mistake is expensive or hard to undo, or actions only a human can perform. Text on these lines are meant for the human to do; any text on a line that does not have the flag is for the agent. +- When a step is ambiguous or a tool misbehaves, look at the two most recent deprecation pull requests in `github/docs-internal` for how it was handled. Search for pull requests titled "Deprecate GitHub Enterprise Server", and read the content team's review comments so you can fix the same problems before requesting review. +- Clone `github/docs-internal` and `github/github` before you begin. -- `github/github` -- `github/docs-internal` +## Step 1: Confirm the deprecation date -## Step 0: Confirm the deprecation date +Look up `{{ release-number }}` in the [release date list](https://github.com/github/enterprise-releases/blob/master/releases.json), find the corresponding `prp` owner, and draft a short Slack message asking them to confirm the deprecation date. -Before beginning the deprecation, ensure the date of the deprecation is correctly defined: +🛑 **HUMAN**: Send a Slack message. If there is no `prp` owner, ask in #docs-content-enterprise or #ghes-releases instead. -1. Check that the deprecation date is correct by looking up the version you are deprecating in the [release date list](https://github.com/github/enterprise-releases/blob/master/releases.json) and finding the corresponding `prp` owner. Send them a slack message to confirm that the date is correct. If the date is being pushed out, you can ask the `prp` to update the date in the release date list. If the release date list does not get updated (it doesn't always) we have to prepare that our version of that file (`src/ghes-releases/lib/enterprise-dates.json`) will also be inaccurate. +* If the date is being pushed out, ask the `prp` to update the release date list, update this issue's target date, and pause until the new date arrives. +* The release date list is often not updated. If it isn't, our copy in `src/ghes-releases/lib/enterprise-dates.json` may also be wrong. You fix that in Step 6. - If there is no `prp` defined, reach out to our content friends for help in the #docs-content-enterprise or #ghes-releases Slack channel. +## Step 2: Remove the version from the github/docs-content release tracker -1. If this release is being pushed out, update the target date of this issue and you can wait to proceed with any futher steps. +In the `github/docs-content` repository, remove `{{ release-number }}` from the `options` list in [`release-tracking.yml`](https://github.com/github/docs-content/blob/main/.github/ISSUE_TEMPLATE/release-tracking.yml), ensure the list of versions matches the available versions, and open a pull request. -1. In the `docs-content` repo, remove the deprecated GHES version number from the `options` list in [`release-tracking.yml`](https://github.com/github/docs-content/blob/main/.github/ISSUE_TEMPLATE/release-tracking.yml). +🛑 **HUMAN**: Review the `github/docs-content` pull request. Acknowledge this will need to be merged in the next few days. -1. When the PR is approved, merge it in. +You can continue once the human reviews the `github/docs-content` pull request and acknowledges they are responsible for getting it merged. -
🤖 Copilot prompt for Step 0 +## Step 3: Clone the translation repositories -> I'm preparing to deprecate GHES VERSION_NUMBER. Read `src/ghes-releases/lib/enterprise-dates.json` in docs-internal and find the deprecation date for this version. Then draft a Slack message I can send to the PRP owner to confirm the date is correct. Also check `github/enterprise-releases/releases.json` and identify the PRP owner for this version. +The full scrape needs local clones of the eight translation repositories: -
- -## Step 1: Create the new archived repository - -All previously archived content lives in its own repository. For example, GHES 3.11 archived content is located in https://github.com/github/docs-ghes-3.11. - -1. Create a new repository that will store the scraped deprecated files: - - ```shell - npm run deprecate-ghes -- create-repo --version - ``` - - For example, to deprecate GHES 3.11, you would run: - - ```shell - npm run deprecate-ghes -- create-repo --version 3.11 - ``` - -1. From the new repository's home page, click the gear icon next to the "About" section and deselect the "Releases", "Packages", and "Deployments" checkboxes. Click "Save changes". - -
🤖 Copilot prompt for Step 1 - -> Run `npm run deprecate-ghes -- create-repo --version VERSION_NUMBER` and show me the output. If the script fails (API errors, permission issues, path problems), diagnose the error and suggest a fix. The `create-docs-ghes-version-repo.sh` script may have hardcoded paths or assumptions that need updating. - -
- -## Step 2: Dry run: Scrape the docs and archive the files - -**Note:** You may want to perform the following dry run steps on a new temporary branch that you can delete after the dry run is complete. - -1. If the release date documented in the [release date list](https://github.com/github/enterprise-releases/blob/master/releases.json) is incorrect or differs from what we have documented in `src/ghes-releases/lib/enterprise-dates.json`, update the date in `src/ghes-releases/lib/enterprise-dates.json` to the correct deprecation date before proceeding with the deprecation. A banner is displayed on each page with a version that will be deprecated soon. The banner uses the dates defined in `src/ghes-releases/lib/enterprise-dates.json`. - -1. Ensure you have local clones of the [translation repositories](#configuring-the-translation-repositories). - -1. Update all translation directories to the latest `main` branch. - -1. Hide search components temporarily while scraping docs by adding the `visually-hidden` class to the search components: - - **In `src/search/components/input/SearchBarButton.tsx`**, wrap the return statement content: - - ```javascript - return ( -
- {/* existing search button content */} -
- ); - ``` - - **In `src/search/components/input/SearchOverlayContainer.tsx`**, wrap the return statement content: - - ```javascript - if (isSearchOpen) { - return ( -
- -
- ); - } - ``` - -1. Ensure your build is up to date: - - ```shell - npm run build - ``` - -1. Do a dry run by scraping a small amount of files to test locally on your machine. This command does not overwrite the references to asset files so they will render on your machine. +```shell +npm run clone-translations +``` - ```shell - npm run deprecate-ghes-archive -- --dry-run --local-dev - ``` +This clones every language into `./translations/` inside your `github/docs-internal` checkout, which the scrape reads by default. No `.env` configuration is needed. It clones eight repositories and can take several minutes. To keep your clones elsewhere, use per-language `TRANSLATIONS_ROOT_*` variables instead, described in [the appendix](#reference-configuring-the-translation-repositories). -1. Navigate to the scraped files directory (`tmpArchivalDir_`) inside your docs-internal checkout. Open a few HTML files and ensure they render and drop-down pickers work correctly. +## Step 4: Create the archive repository -1. If the dry-run looks good, scrape all content files. This will take about 20-30 minutes. **Note:** This will overwrite the directory that was previously generated with new files. You can also create a specific output directory using the `--output` flag. +Each deprecated version's docs live in their own repository, for example `github/docs-ghes-3.11`. Create the new one: - ```shell - npm run deprecate-ghes-archive - ``` +```shell +npm run deprecate-ghes -- create-repo --version {{ release-number }} +``` -1. Revert changes to `src/search/components/input/SearchBarButton.tsx` and `src/search/components/input/SearchOverlayContainer.tsx`. +🛑 **HUMAN**: On the new repository's home page, click the gear next to "About" and clear the "Releases", "Packages", and "Deployments" checkboxes, then save. No public API covers these toggles. -1. Check in any change to `src/ghes-releases/lib/enterprise-dates.json`. +You can continue once the human has said they completed that step. -
🤖 Copilot prompt for Step 2 +## Step 5: Create the deprecation branch -> I'm doing a dry run of the GHES VERSION_NUMBER deprecation archive scrape. -> -> First, in `src/search/components/input/SearchBarButton.tsx`, wrap the return statement content in a `
` wrapper. Do the same in `src/search/components/input/SearchOverlayContainer.tsx` for the SearchOverlay component when `isSearchOpen` is true. -> -> Then run `npm run build` and show me the output. If the build succeeds, run `npm run deprecate-ghes-archive -- --dry-run --local-dev` and show me the output. Tell me if any errors occurred. -> -> After I've reviewed the dry run output, run the full scrape: `npm run deprecate-ghes-archive`. This will take 20-30 minutes. -> -> When the scrape completes, revert the search component changes: `git checkout src/search/components/input/SearchBarButton.tsx src/search/components/input/SearchOverlayContainer.tsx`. Verify the files are reverted. +Create the branch that holds every `github/docs-internal` change in this deprecation. Keep it through Step 13: -
+```shell +git checkout -b deprecate-{{ release-number }} +``` -## Step 3: Commit the scraped docs to the new repository +## Step 6: Fix the deprecation date if needed -1. Copy the scraped files from the `tmpArchivalDir_` directory in `docs-internal` over to the new `github/docs-ghes-` repository. +If the date in the [release date list](https://github.com/github/enterprise-releases/blob/master/releases.json) differs from `src/ghes-releases/lib/enterprise-dates.json`, update `enterprise-dates.json` to match and commit it on your branch. The pre-deprecation banner reads its dates from that file, so fix it before scraping. -1. Commit the files. A GitHub Pages build should automatically begin, creating the static site that serves these docs. +## Step 7: Dry-run the scrape -1. Preview a few pages, by navigating to the full URL checked into the repo. For example, for GHES 3.11, you can view `https://github.github.com/docs-ghes-3.11/en/enterprise-server@3.11/account-and-profile/managing-subscriptions-and-notifications-on-github/setting-up-notifications/about-notifications/index.html`. +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/search/components/input/SearchBarButton.tsx`, wrap the returned content in a `
`, and do the same for the `SearchOverlay` in `src/search/components/input/SearchOverlayContainer.tsx`. -1. Remove the `tmpArchivalDir_` directory from your `github/docs-internal` checkout. +Build, then scrape a few pages locally: -
🤖 Copilot prompt for Step 3 +```shell +npm run build +npm run deprecate-ghes-archive -- --dry-run --local-dev +``` -> Copy the scraped files from `tmpArchivalDir_` to the `github/docs-ghes-` repository. Commit all the files with the message "Archive GHES docs". Verify the commit succeeded and show me the file count. Then remove the `tmpArchivalDir_` directory from docs-internal. +Open a few HTML files in `tmpArchivalDir_{{ release-number }}` and confirm they render, styles load, and the version dropdowns work. -
+Offer to open the files for the human in their text editor or browser. -## Step 4: Deprecate the GHES release in docs-internal +🛑 **HUMAN**: Review the dry-run output before the full scrape. -1. In your `docs-internal` checkout, create a new branch: `git checkout -b deprecate-`. +You can continue after the human confirms they have reviewed the dry-run output. -1. In your `docs-internal` checkout, edit `src/versions/lib/enterprise-server-releases.ts` by removing the version number to be deprecated from the `supported` array and move it to the `deprecatedWithFunctionalRedirects` array. +## Step 8: Run the full scrape -1. Deprecate the automated pipelines data files (including audit logs, REST, GraphQL, webhooks, GitHub Apps, CodeQL CLI, and secret scanning): +Scrape every page. This takes 20-30 minutes and overwrites the dry-run output: - ```shell - npm run deprecate-ghes -- pipelines - ``` +```shell +npm run deprecate-ghes-archive +``` -1. Remove deprecated content files and update the versions frontmatter: +Revert the search component edits: - ```shell - npm run deprecate-ghes -- content - ``` +```shell +git checkout src/search/components/input/SearchBarButton.tsx src/search/components/input/SearchOverlayContainer.tsx +``` -1. Remove deprecated Liquid from content and data files. **Note:** The previous step to update content file frontmatter must have run successfully for this step to work because the updated frontmatter is used to determine file versions. +## Step 9: Publish the archive - ```shell - npm run lint-content -- --paths content data --rules liquid-ifversion-versions --fix - ``` +The scrape writes the publishable site to an inner directory, `tmpArchivalDir_{{ release-number }}/{{ release-number }}/`, which holds `en/`, the other language directories, and the redirect files. Copy the contents of that inner directory into the root of the `github/docs-ghes-{{ release-number }}` repository, so pages land at `/en/...` with no nested `{{ release-number }}/` directory. If you are unsure, please look at some previous `github/docs-ghes-*` repos for the right organization. -1. There are some `data/variables/*.yml` files that can't be autofixed. These will show up as errors. You can manually make the changes to these files. For example, this means open file data/variables/code-scanning and find the code_scanning_thread_model_support key. Edit the key’s value to remove the deprecated liquid: +🛑 **HUMAN**: Confirm the file count, organization, and contents. - ![Output from script that indicates manual fixes to variable files are needed](./variable-example.png) +After the human confirms, commit and push. GitHub Pages builds the static site automatically. Wait a few minutes. Preview a few pages at the full Pages URL, for example `https://github.github.com/docs-ghes-{{ release-number }}/en/enterprise-server@{{ release-number }}/get-started/index.html`, across a couple of languages. Then remove `tmpArchivalDir_{{ release-number }}` from `github/docs-internal`. -1. Deprecate any data files that are now empty, remove data resuables references that were deleted: +## Step 10: Remove the version from github/docs-internal - ```shell - npm run deprecate-ghes -- data - ``` +Back on your `deprecate-{{ release-number }}` branch, move the version out of the supported list. In `src/versions/lib/enterprise-server-releases.ts`, remove `'{{ release-number }}'` from `supported` and add it as the first element of `deprecatedWithFunctionalRedirects`. -1. Run the linter again to remove whitespace and check for any other errors: +Run the deprecation scripts in order. Each depends on the previous one succeeding: - ```shell - npm run lint-content -- --fix - ``` +```shell +npm run deprecate-ghes -- pipelines +npm run deprecate-ghes -- content +npm run lint-content -- --paths content data --rules liquid-ifversion-versions --fix +``` -1. Use VSCode find/replace to remove any remaining table pipes after liquid has been removed. For example lines that only contain 1 or two pipes: ` |` or ` | |`. You can use the following regexes: `^\|\s*\|$` and `^\s?\|\s?$`. +Some `data/variables/*.yml` files can't be autofixed and show as lint errors. Open each one, find the key named in the error, and remove the deprecated Liquid while keeping the content for supported versions. -1. Test the changes by running the site locally: +Then clean up empty data files and run the linter again: - ```shell - npm run start - ``` +```shell +npm run deprecate-ghes -- data +npm run lint-content -- --fix +``` -1. Poke around several deprecated pages by navigating to `docs.github.com/enterprise/`, and ensure that: +## Step 11: Clean up content artifacts - - Stylesheets are working properly - - Images are rendering properly - - The search functionality was disabled during scraping - - Look at any console errors to ensure that no new unexpected errors were introduced. You can look at previous errors by viewing a previously completed deprecation page. - - You should see a banner on the top of every deprecated page with the date that the version was deprecated. - - You should see a banner at the top of every page for the oldes currently supported version with the date that it will be deprecated in the ~3 months. +The content team flags issues on every deprecation, so fix them before requesting review: -1. If everything looks good, check in all changes and create a pull request. +* Collapse consecutive blank lines left behind by removed Liquid. Run `npm run deprecate-ghes -- collapse-blank-lines` to auto-fix any run of 2+ blank lines in the markdown files this deprecation changed. The linter doesn't catch these because MD012 is off. Use `--check` to list offenders without writing. +* Review each Liquid-removal site for a stray single blank line the codemod introduced, one at a time. The auto-fix above only touches 2+ blank lines, because a lone blank line is often legitimate (for example, before a nested sub-list), so a human must judge each one. +* Remove leftover table-pipe rows from removed Liquid, for example lines matching `^\|\s*\|$` or `^\s?\|\s?$`. +* Fix paragraphs missing a leading space, and any `ifversion` left in the wrong place after a reusable was removed. +* Revert codemod churn on autogenerated REST files. The codemod edits `content/rest/**` files marked `autogenerated: rest`, stripping the `# DO NOT MANUALLY EDIT` marker from their `versions:` block, rewriting `ghes: '>=X'` to `'*'`, and reflowing `intro: >-` scalars. The REST sync bot owns these files and will overwrite the edits, so restore the churned frontmatter to match `main` and keep only genuinely-removed pages. Alternatively, regenerate with `npm run sync-rest`, which needs a `github/github` checkout. +* Close up double spaces left in inline `ifversion` chains. When the codemod drops a version from an inline chain it can leave a double space, for example `upgrading to {% ifversion ghes = 3.17 %}3.17{% endif %} {% ifversion ghes = 3.18 %}3.18{% endif %} with caution.` Move the trailing space inside each conditional (`3.17 {% endif %}`) so only the matched branch renders one space. +* Prune empty `else` branches and dead conditionals. When the codemod removes the only content inside an `{% else %}`, often a now-unused reusable, delete the empty `{% else %}`. Also remove `{% ifversion ghes < %}` blocks, which are always false after the deprecation. +* Add redirects for content removed on every version. When a deprecation fully removes a feature, not just one version of it, there's no auto-generated redirect, so add redirects to the current docs by hand and repoint version-pinned entries in `src/fixtures/fixtures/rest-redirects.json` at the current location. Expect a large list. There's no helper for this yet. -1. Ensure that CI is passing or make any changes to content needed to get tests to pass. +The list in this step should increase after each deprecation to improve the output of this process and reduce human effort. -1. Add the PR to the [docs-content review board](https://github.com/orgs/github/projects/2936/views/2). +## Step 12: Fix CI the codemod doesn't touch -1. 🚢 Ship the change. +The deprecation scripts only rewrite `content/` and `data/`, so version references in tests and fixtures can break CI. Run: -
🤖 Copilot prompt for Step 4 — initial setup +```shell +npm test -- src/versions/tests src/redirects/tests +``` -> I'm deprecating GHES VERSION_NUMBER in docs-internal. In `src/versions/lib/enterprise-server-releases.ts`, remove `'VERSION_NUMBER'` from the `supported` array and add it as the first element of the `deprecatedWithFunctionalRedirects` array. Show me the diff. +Fix any failures. For example, when a deprecation fully removes content rather than just a version of it, redirect fixtures in `src/fixtures/fixtures/rest-redirects.json` may still point at the removed version; repoint them at the current location. -
+## Step 13: Review, smoke test, and open the pull request -
🤖 Copilot prompt for Step 4 — run deprecation scripts +🛑 **HUMAN**: Review the full diff before the smoke test. -> Run `npm run deprecate-ghes -- pipelines` and show me the output. If there are errors, diagnose and fix them. -> -> Then run `npm run deprecate-ghes -- content` and show me the output. If there are errors, diagnose and fix them. +After the human confirms they reviewed the full diff, smoke test locally with `npm run start` and visit a few pages at `docs.github.com/enterprise/{{ release-number }}`. Confirm stylesheets and images load, search is disabled, there are no new console errors, every deprecated page shows the "deprecated on " banner, and the new oldest supported version shows its upcoming-deprecation banner. -
+Commit, push, and open a draft pull request labelled `llm-generated`. Use this body so the reviewer has context and a way to feed findings back into the runbook, filling in the placeholders: -
🤖 Copilot prompt for Step 4 — lint and fix Liquid +````markdown +```markdown +Copilot generated this pull request. -> Run `npm run lint-content -- --paths content data --rules liquid-ifversion-versions --fix`. Some `data/variables/*.yml` files can't be auto-fixed and will show as errors. For each error, open the file, find the key mentioned in the error, and remove the deprecated Liquid conditional for GHES VERSION_NUMBER while preserving the content for supported versions. Show me each change you make. +This pull request deprecates GHES {{ release-number }} on docs.github.com. -
+## What this does -
🤖 Copilot prompt for Step 4 — clean up data and remaining issues +- Moves `{{ release-number }}` out of `supported` in `enterprise-server-releases.ts`. +- Runs the deprecation codemods over content and pipeline data. +- Removes the `{{ release-number }}` pipeline data and archives the version. +- {Note any content removed entirely, with redirect counts.} -> Run `npm run deprecate-ghes -- data` and show me the output. -> -> Then run `npm run lint-content -- --fix` to remove whitespace and check for other errors. -> -> Search the codebase for any remaining table pipe artifacts from removed Liquid conditionals. Look for lines matching `^\|\s*\|$` or `^\s?\|\s?$` across all content and data files. Remove any you find. -> -> Show me a summary of all changes made. +## For reviewers -
+[Step 11 of the runbook](https://github.com/github/docs-internal/blob/main/src/ghes-releases/lib/deprecation-steps.md#step-11-clean-up-content-artifacts) lists the content problems that recur. If you spot issues this pull request missed, please add to Step 11 so the next deprecation catches it. +``` +```` -
🤖 Copilot prompt for Step 4 — final validation +> [!NOTE] +> The `dont-delete-features` check can fail when a deprecation removes more than one feature file. It guards against translations still referencing deleted features. This is expected on deprecations that fully remove features, and the docs-bot "Delete orphaned features" automation cleans them up. Don't block the pull request on it. -> Run `npm run lint-content` and show me any remaining errors. For each error, fix it and show me the change. -> -> Then run `npm run test -- src/versions` to check version-related tests still pass. -> -> Summarize all changes made so far so I can review before committing. +Offer to open the pull request in the human's browser. -
+🛑 **HUMAN**: Review the draft pull request. Comment any changes needed. -## Step 5: Create a tag +Once the humans approves the pull request, mark as ready for review. Add the pull request to the [docs-content review board](https://github.com/orgs/github/projects/2936/views/2). -1. Create a new tag for the most recent commit on the `main` branch so that we can keep track of where in commit history we removed the GHES release. Create a tag called `enterprise--deprecation`. On your local, `git checkout main`, `git pull`, `git tag enterprise--deprecation`, then `git push --tags --no-verify`. +🛑 **HUMAN**: Get a content team review before merging the pull request. You should add any issues the content team finds to Step 11 of this runbook. -
🤖 Copilot prompt for Step 5 +You can proceed once the human acknowledges they will need to get a content team review and handle merging the pull request and adding any feedback. You do not need to wait for the pull request to get a content team review or to be merged. -> Run `git checkout main && git pull` then `git tag enterprise-VERSION_NUMBER-deprecation && git push --tags --no-verify`. Show me the output. +## Step 14: Tag the deprecation -
+Tag `main` so we can find where in history the version was removed. You can tag now. There's no need to wait for the deprecation pull request to merge, which matches the recent deprecations. -## Step 6: Deprecate the OpenAPI description in `github/github` +```shell +git checkout main && git pull +git tag enterprise-{{ release-number }}-deprecation +git push --tags --no-verify +``` -1. In `github/github`, edit the release's config file in `app/api/description/config/releases/`, and change `deprecated: false` to `deprecated: true`. +## Step 15: Deprecate the OpenAPI description in github/github -1. Open a new PR, and get the required code owner approvals. A docs-content team member can approve it for the docs team. +In `github/github`, edit `app/api/description/config/releases/ghes-{{ release-number }}.yaml` and change `deprecated: false` to `deprecated: true`. Open a pull request and get the required code owner approvals. A docs-content team member can approve for the docs team. -1. When the PR is approved, [deploy the `github/github` PR](https://thehub.github.com/epd/engineering/devops/deployment/deploying-dotcom/). If you haven't deployed a `github/github` PR before, work with someone that has -- the process isn't too involved depending on how you deploy, but there are a lot of details that can potentially be confusing as you can see from the documentation. +🛑 **HUMAN**: Once approved, [deploy the `github/github` pull request](https://thehub.github.com/epd/engineering/devops/deployment/deploying-dotcom/). If you haven't deployed `github/github` before, pair with someone who has. -
🤖 Copilot prompt for Step 6 +Continue on after the human acknowledges they will need to deploy the `github/github` pull request after the `github/docs-internal` pull request merges. -> In the `github/github` repository, find the config file for GHES VERSION_NUMBER in `app/api/description/config/releases/`. Change `deprecated: false` to `deprecated: true`. Show me the diff and open a PR with the title "Deprecate GHES VERSION_NUMBER OpenAPI description". +## Step 16: Capture process improvements -
+Keep notes throughout the deprecation of anything wrong, slow, or confusing: runbook bugs, tooling failures, manual workarounds, and content-team feedback. When you finish, open a separate pull request, not part of the deprecation pull request, that fixes this runbook and the deprecation tooling for the next release. Write it for the next deprecation's agent. -## Configuring the translation repositories +🛑 **HUMAN**: Review the process improvement pull request. -You can clone the translation repositories directly inside of your docs-internal checkout, but I'd recommend cloning them in a separate directory. For example, create a `translations` directory at the same level as your `docs-internal` directory. Inside of the `translations` directory, clone the following repoisitories (ensure this list includes all languages that we are supporting): +After the human approves the process improvement pull request, you can continue. -- [docs-internal.es-es](https://github.com/github/docs-internal.es-es) -- [docs-internal.ja-jp](https://github.com/github/docs-internal.ja-jp) -- [docs-internal.pt-br](https://github.com/github/docs-internal.pt-br) -- [docs-internal.zh-cn](https://github.com/github/docs-internal.zh-cn) -- [docs-internal.ru-ru](https://github.com/github/docs-internal.ru-ru) -- [docs-internal.fr-fr](https://github.com/github/docs-internal.fr-fr) -- [docs-internal.ko-kr](https://github.com/github/docs-internal.ko-kr) -- [docs-internal.de-de](https://github.com/github/docs-internal.de-de) +## Step 17: Summarize -To map the location of each translation repository, edit your `.env` file with the mapping. For example, if following the locations suggested above, your `.env` file might look like this: +Summarize the work completed in this workflow, and link to each of the pull requests with the next action needed from the human. -```shell -TRANSLATIONS=/Users/mona/repos/github-repos/translations -TRANSLATIONS_ROOT_ES_ES=${TRANSLATIONS}/docs-internal.es-es -TRANSLATIONS_ROOT_JA_JP=${TRANSLATIONS}/docs-internal.ja-jp -TRANSLATIONS_ROOT_PT_BR=${TRANSLATIONS}/docs-internal.pt-br -TRANSLATIONS_ROOT_ZH_CN=${TRANSLATIONS}/docs-internal.zh-cn -TRANSLATIONS_ROOT_RU_RU=${TRANSLATIONS}/docs-internal.ru-ru -TRANSLATIONS_ROOT_FR_FR=${TRANSLATIONS}/docs-internal.fr-fr -TRANSLATIONS_ROOT_KO_KR=${TRANSLATIONS}/docs-internal.ko-kr -TRANSLATIONS_ROOT_DE_DE=${TRANSLATIONS}/docs-internal.de-de -``` +## Reference: Configuring the translation repositories -## Re-scraping a page or all pages +`npm run clone-translations` from Step 3 is the simplest setup: it clones every language into `./translations/`, which the scrape reads by default with no extra configuration. -Occasionally, a change will need to be added to our archived enterprise versions. If this occurs, you can check out the `enterprise--release` branch and re-scrape the page or all pages using `npm run deprecate-ghes-archive`. To scrape a single page you can use the `—page ` option. +To keep your clones elsewhere instead, clone the repositories below and map each one with a `TRANSLATIONS_ROOT_*` variable in your `.env` file: -For each language, upload the new file to the `github/docs-ghes-` repo. +* `docs-internal.es-es` +* `docs-internal.ja-jp` +* `docs-internal.pt-br` +* `docs-internal.zh-cn` +* `docs-internal.ru-ru` +* `docs-internal.fr-fr` +* `docs-internal.ko-kr` +* `docs-internal.de-de` -After uploading the new files, you will need to purge the Fastly cache for the single page. From Okta, go to Fastly and select `docs`. Click `Purge` then `Purge URL`. If you need to purge a whole path, just do a `Purge All` +## Reference: Re-scraping a page or all pages -![The Fastly UI URL purge drop-down selector options.](/contributing/images/fastly_purge.jpg) +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. -Enter the URL or path and do a soft purge. +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. -![The Fastly UI purging guide.](/contributing/images/fastly_purge_url.jpg) +/cc @github/docs-engineering diff --git a/src/ghes-releases/lib/variable-example.png b/src/ghes-releases/lib/variable-example.png deleted file mode 100644 index ba7bc8f6068a..000000000000 Binary files a/src/ghes-releases/lib/variable-example.png and /dev/null differ diff --git a/src/ghes-releases/scripts/create-enterprise-issue.ts b/src/ghes-releases/scripts/create-enterprise-issue.ts index 9e48f4d5745a..33d194776220 100644 --- a/src/ghes-releases/scripts/create-enterprise-issue.ts +++ b/src/ghes-releases/scripts/create-enterprise-issue.ts @@ -103,12 +103,13 @@ async function createDeprecationIssue() { const issueTemplate = readFileSync('src/ghes-releases/lib/deprecation-steps.md', 'utf8') const { data, content } = matter(issueTemplate) const { title, labels } = data + const renderedContent = content.replaceAll('{{ release-number }}', oldestSupported) const body = `GHES ${oldestSupported} deprecation occurs on ${deprecationDate}. - \n${content} - '/cc @github/docs-engineering'` + +${renderedContent}` await createIssue( repo, - title.replace('{{ release-number }}', oldestSupported), + title.replaceAll('{{ release-number }}', oldestSupported), body, labels, oldestSupported, diff --git a/src/ghes-releases/scripts/deprecate/archive-version.ts b/src/ghes-releases/scripts/deprecate/archive-version.ts index 604247881eb7..179e2decacca 100755 --- a/src/ghes-releases/scripts/deprecate/archive-version.ts +++ b/src/ghes-releases/scripts/deprecate/archive-version.ts @@ -112,6 +112,8 @@ async function main() { } catch (err) { console.error('scraping error') console.error(err) + server.close(() => process.exit(1)) + return } fs.renameSync( @@ -127,11 +129,11 @@ async function main() { } else { console.log('🏁 Scraping a single page is complete') } - server.close() + server.close(() => process.exit(0)) }) .on('error', (err) => { console.log('error listening to port ', port, err) - server.close() + server.close(() => process.exit(1)) }) } diff --git a/src/ghes-releases/scripts/deprecate/collapse-blank-lines.ts b/src/ghes-releases/scripts/deprecate/collapse-blank-lines.ts new file mode 100644 index 000000000000..52815b0b91d8 --- /dev/null +++ b/src/ghes-releases/scripts/deprecate/collapse-blank-lines.ts @@ -0,0 +1,84 @@ +import fs from 'fs' +import { execSync } from 'child_process' + +// Removing deprecated Liquid conditionals leaves behind extra blank lines. +// The content team flags these every deprecation, and the MD012 linter rule +// is off so nothing catches them automatically. This collapses any run of +// two or more consecutive blank lines down to one, but only in the markdown +// files the deprecation actually changed. Single blank lines are left alone: +// removed Liquid can introduce one in a place where it doesn't belong, so a +// human still reviews each removal site one at a time. + +function getChangedMarkdownFiles(): string[] { + const commands = [ + 'git diff --name-only HEAD', + 'git diff --name-only --cached', + 'git diff --name-only origin/main...HEAD', + ] + const files = new Set() + for (const command of commands) { + let output = '' + try { + output = execSync(command, { encoding: 'utf8' }) + } catch { + // origin/main may not be fetched locally; skip that source. + continue + } + for (const line of output.split('\n')) { + const file = line.trim() + if (!file) continue + if (!file.endsWith('.md')) continue + if (!file.startsWith('content/') && !file.startsWith('data/')) continue + if (fs.existsSync(file)) files.add(file) + } + } + return [...files].sort() +} + +// Collapses any run of 2+ blank lines into a single blank line. +function collapse(contents: string): string { + const lines = contents.split('\n') + const result: string[] = [] + let blankRun = 0 + for (const line of lines) { + if (line.trim() === '') { + blankRun += 1 + if (blankRun <= 1) result.push('') + } else { + blankRun = 0 + result.push(line) + } + } + return result.join('\n') +} + +export function collapseBlankLines(options: { check?: boolean } = {}) { + const files = getChangedMarkdownFiles() + const offenders: string[] = [] + + for (const file of files) { + const contents = fs.readFileSync(file, 'utf8') + const collapsed = collapse(contents) + if (collapsed === contents) continue + offenders.push(file) + if (!options.check) { + fs.writeFileSync(file, collapsed) + console.log('Collapsed blank lines in: ', file) + } + } + + if (options.check) { + if (offenders.length) { + console.error('Found 2+ consecutive blank lines in:') + for (const file of offenders) console.error(` ${file}`) + console.error('Run `npm run deprecate-ghes -- collapse-blank-lines` to fix.') + process.exit(1) + } + console.log('No double blank lines found in changed markdown files.') + return + } + + if (!offenders.length) { + console.log('No double blank lines found in changed markdown files.') + } +} diff --git a/src/ghes-releases/scripts/deprecate/create-docs-ghes-version-repo.sh b/src/ghes-releases/scripts/deprecate/create-docs-ghes-version-repo.sh index eb8ec4c9d1a7..69ca2a54026d 100755 --- a/src/ghes-releases/scripts/deprecate/create-docs-ghes-version-repo.sh +++ b/src/ghes-releases/scripts/deprecate/create-docs-ghes-version-repo.sh @@ -46,7 +46,7 @@ mutation($repositoryId:ID!,$branch:String!,$requiredReviews:Int!) { }' -f repositoryId="$repositoryId" -f branch=main -F requiredReviews=1 --silent echo "--- Enable GitHub Pages, set source to main in root directory, and make the pages site public" gh api -X POST "/repos/github/docs-ghes-$version/pages" \ - -f "source[branch]=main" -f "source[path]=/" -f "public=true" --silent + -f "source[branch]=main" -f "source[path]=/" -F "public=true" --silent echo "--- Update custom properties" gh api --method PATCH /repos/github/docs-ghes-$version/properties/values \ -f "properties[][property_name]=ownership-name" \ diff --git a/src/ghes-releases/scripts/deprecate/index.ts b/src/ghes-releases/scripts/deprecate/index.ts index 4b9be2b51c89..b62542a2c179 100644 --- a/src/ghes-releases/scripts/deprecate/index.ts +++ b/src/ghes-releases/scripts/deprecate/index.ts @@ -3,31 +3,38 @@ import { execSync } from 'child_process' import { updateContentFiles } from '@/ghes-releases/scripts/deprecate/update-content' import { updateDataFiles } from '@/ghes-releases/scripts/deprecate/update-data' import { updateAutomatedConfigFiles } from '@/ghes-releases/scripts/deprecate/update-automated-pipelines' - -program.option('-f, --foo', 'enable some foo') +import { collapseBlankLines } from '@/ghes-releases/scripts/deprecate/collapse-blank-lines' program - .description('Update deprecated versions frontmatter and remove deprecated content files.') .command('content') + .description('Update deprecated versions frontmatter and remove deprecated content files.') .action(updateContentFiles) program + .command('data') .description( 'Update deprecated versions in data files, remove empty data files, and remove deleted reusables from content files.', ) - .command('data') .action(updateDataFiles) program + .command('pipelines') .description( 'Removes automated pipeline data files and updates the automated pipeline config files.', ) - .command('pipelines') .action(updateAutomatedConfigFiles) program + .command('collapse-blank-lines') + .description( + 'Collapse 2+ consecutive blank lines left by removed Liquid into one, in changed markdown files only. Pass --check to report without writing.', + ) + .option('--check', 'Report files with double blank lines and exit non-zero instead of fixing.') + .action((options) => collapseBlankLines({ check: options.check })) + +program + .command('create-repo') .description('Create new `github/docs-ghes-` repository.') - .command('repo') .option('-v, --version ', 'The GHES version to create the repo for.') .action((options) => { if (!options.version) { diff --git a/src/graphql/data/fpt/changelog.json b/src/graphql/data/fpt/changelog.json index 1989655bb1bd..9ed8a823e089 100644 --- a/src/graphql/data/fpt/changelog.json +++ b/src/graphql/data/fpt/changelog.json @@ -1,4 +1,17 @@ [ + { + "schemaChanges": [ + { + "title": "The GraphQL schema includes these changes:", + "changes": [ + "

Enum value THREADS was added to enum SocialAccountProvider

" + ] + } + ], + "previewChanges": [], + "upcomingChanges": [], + "date": "2026-06-09" + }, { "schemaChanges": [ { diff --git a/src/graphql/data/fpt/schema-users.json b/src/graphql/data/fpt/schema-users.json index d4cf542c879b..20b7c5244865 100644 --- a/src/graphql/data/fpt/schema-users.json +++ b/src/graphql/data/fpt/schema-users.json @@ -7536,6 +7536,10 @@ "name": "REDDIT", "description": "

Social news aggregation and discussion website.

" }, + { + "name": "THREADS", + "description": "

Microblogging social platform.

" + }, { "name": "TWITCH", "description": "

Live-streaming service.

" diff --git a/src/graphql/data/fpt/schema.docs.graphql b/src/graphql/data/fpt/schema.docs.graphql index 134049b6a71c..410d11a9f537 100644 --- a/src/graphql/data/fpt/schema.docs.graphql +++ b/src/graphql/data/fpt/schema.docs.graphql @@ -59330,6 +59330,11 @@ enum SocialAccountProvider @docsCategory(name: "users") { """ REDDIT + """ + Microblogging social platform. + """ + THREADS + """ Live-streaming service. """ diff --git a/src/graphql/data/ghec/schema-users.json b/src/graphql/data/ghec/schema-users.json index d4cf542c879b..20b7c5244865 100644 --- a/src/graphql/data/ghec/schema-users.json +++ b/src/graphql/data/ghec/schema-users.json @@ -7536,6 +7536,10 @@ "name": "REDDIT", "description": "

Social news aggregation and discussion website.

" }, + { + "name": "THREADS", + "description": "

Microblogging social platform.

" + }, { "name": "TWITCH", "description": "

Live-streaming service.

" diff --git a/src/graphql/data/ghec/schema.docs.graphql b/src/graphql/data/ghec/schema.docs.graphql index 134049b6a71c..410d11a9f537 100644 --- a/src/graphql/data/ghec/schema.docs.graphql +++ b/src/graphql/data/ghec/schema.docs.graphql @@ -59330,6 +59330,11 @@ enum SocialAccountProvider @docsCategory(name: "users") { """ REDDIT + """ + Microblogging social platform. + """ + THREADS + """ Live-streaming service. """ diff --git a/src/graphql/scripts/sync.ts b/src/graphql/scripts/sync.ts index 4d84455e9d06..2cb08cb86f0c 100755 --- a/src/graphql/scripts/sync.ts +++ b/src/graphql/scripts/sync.ts @@ -10,6 +10,8 @@ import processPreviews from './utils/process-previews' import processUpcomingChanges from './utils/process-upcoming-changes' import processSchemas from './utils/process-schemas' import { bucketSchemaByCategory, writeCategoryFiles } from './utils/bucket-by-category' +import { syncCategoryContentFiles, type CategoryPresence } from './utils/sync-category-content' +import { ALL_KIND_KEYS } from '@/graphql/lib/categories' import { prependDatedEntry, createChangelogEntry, @@ -65,6 +67,12 @@ if (!process.env.GITHUB_TOKEN) { const versionsToBuild = Object.keys(allVersions) +// Tracks, per category, the set of docs versions in which the category has at +// least one type. Populated inside the per-version loop and consumed after it +// to manage the per-category content pages. Declared before `main()` runs so +// the loop never reads it in the temporal dead zone. +const categoryPresence: CategoryPresence = new Map() + main() const allIgnoredChanges: IgnoredChange[] = [] @@ -145,6 +153,17 @@ async function main() { const perCategoryFiles = bucketSchemaByCategory(schemaJsonPerVersion) await writeCategoryFiles(path.join(graphqlStaticDir, graphqlVersion), perCategoryFiles) + // Record which categories have at least one type in this version so the + // content pages and their `versions` frontmatter can be managed after the + // loop. `version` is the docs version key (e.g. `enterprise-server@3.22`), + // which is the format `convertVersionsToFrontmatter` expects. + for (const [cat, bucket] of perCategoryFiles.entries()) { + const hasTypes = ALL_KIND_KEYS.some((kind) => (bucket[kind]?.length ?? 0) > 0) + if (!hasTypes) continue + if (!categoryPresence.has(cat)) categoryPresence.set(cat, new Set()) + categoryPresence.get(cat)!.add(version) + } + // 4. UPDATE CHANGELOG if (allVersions[version].nonEnterpriseDefault) { // The changelog is only built for free-pro-team@latest @@ -173,6 +192,11 @@ async function main() { } } + // Manage the per-category content pages (create new categories, delete + // emptied ones, narrow `versions` frontmatter) plus the reference index + // children and disappearance redirects, based on the presence collected above. + await syncCategoryContentFiles(categoryPresence) + // Ensure the YAML linter runs before checkinging in files execSync('npx prettier -w "**/*.{yml,yaml}"') diff --git a/src/graphql/scripts/utils/sync-category-content.ts b/src/graphql/scripts/utils/sync-category-content.ts new file mode 100644 index 000000000000..02a58d150c5d --- /dev/null +++ b/src/graphql/scripts/utils/sync-category-content.ts @@ -0,0 +1,177 @@ +import fs from 'fs/promises' +import path from 'path' +import walk from 'walk-sync' +import matter from '@gr2m/gray-matter' +import { isEqual } from 'lodash-es' + +import { + updateContentDirectory, + convertVersionsToFrontmatter, +} from '@/automated-pipelines/lib/update-markdown' +import { CATEGORIES, OTHER_CATEGORY, categoryTitle } from '@/graphql/lib/categories' + +// Default directory holding the per-category GraphQL reference content pages. +// Overridable via options for tests; production always uses this path. +const DEFAULT_CONTENT_DIR = path.join('content', 'graphql', 'reference') +// Value of the `autogenerated` frontmatter on managed category pages. The +// content-directory helper uses this to know which files it owns (and may +// therefore delete when a category empties). +const AUTOGENERATED_TYPE = 'graphql' +// Breadcrumb category the reference pages sit under in the sidebar. +const CATEGORY_BREADCRUMB = 'Explore the schema reference' + +// Maps a category slug to the set of docs version keys (e.g. +// `free-pro-team@latest`, `enterprise-server@3.22`) in which the category has +// at least one type. Built by sync.ts from the per-version buckets. +export type CategoryPresence = Map> + +const categoryUrlPath = (cat: string) => `/graphql/reference/${cat}` + +// Matches a bare category reference URL (no fragment), e.g. +// `/graphql/reference/code-scanning`. Kind pages like +// `/graphql/reference/queries` also match this shape but are filtered out +// because their slug is not in CATEGORIES. +const CATEGORY_URL_RE = /^\/graphql\/reference\/([a-z][a-z0-9-]*)$/ + +function isPresentInAnyVersion(presence: CategoryPresence, cat: string): boolean { + return (presence.get(cat)?.size ?? 0) > 0 +} + +// Read the `redirect_from` of every managed category page before the content +// helper potentially deletes those files, so redirect chains aren't lost when a +// category disappears. Returns a map of category slug -> redirect_from entries. +async function captureCategoryRedirects(contentDir: string): Promise> { + const captured = new Map() + let files: string[] = [] + try { + files = walk(contentDir, { + includeBasePath: true, + directories: false, + globs: ['**/*.md'], + ignore: ['**/index.md', '**/README.md'], + }) + } catch { + return captured + } + for (const file of files) { + try { + const { data } = matter(await fs.readFile(file, 'utf8')) + if (data.autogenerated !== AUTOGENERATED_TYPE) continue + const entries = normalizeRedirects(data.redirect_from) + if (entries.length > 0) captured.set(path.basename(file, '.md'), entries) + } catch { + // Unreadable/unparseable file; nothing to capture. + } + } + return captured +} + +function normalizeRedirects(value: unknown): string[] { + if (Array.isArray(value)) return value.filter((v): v is string => typeof v === 'string') + if (typeof value === 'string') return [value] + return [] +} + +// Build the `sourceContent` map the content-directory helper expects: +// `{ : { data: , content: } }`. Only categories +// that are non-empty in at least one version get a page; emptied categories are +// omitted so the helper deletes their stale files. +async function buildSourceContent(presence: CategoryPresence, contentDir: string) { + const sourceContent: Record; content: string }> = {} + for (const cat of CATEGORIES) { + const versionsSet = presence.get(cat) + if (!versionsSet || versionsSet.size === 0) continue + const versions = await convertVersionsToFrontmatter([...versionsSet]) + const title = categoryTitle(cat) + const file = path.join(contentDir, `${cat}.md`) + // For pages that already exist, the helper only refreshes `versions` and the + // autogenerated body, preserving any writer edits to title/intro/category. + // These values therefore only seed brand-new category pages. + sourceContent[file] = { + data: { + title, + shortTitle: title, + intro: `Reference documentation for GraphQL schema types in the ${title} category.`, + versions, + autogenerated: AUTOGENERATED_TYPE, + category: [CATEGORY_BREADCRUMB], + }, + content: '', + } + } + return sourceContent +} + +// Reconcile the reference index `redirect_from` so that a bare category URL +// redirects to the reference root when (and only when) that category is empty in +// every version. Categories present in at least one version must NOT have a +// redirect, otherwise a still-valid versioned page would be shadowed. +async function reconcileIndexRedirects( + presence: CategoryPresence, + capturedRedirects: Map, + indexFile: string, +): Promise { + let raw: string + try { + raw = await fs.readFile(indexFile, 'utf8') + } catch { + return + } + const { data, content } = matter(raw) + const existing = normalizeRedirects(data.redirect_from) + + // Drop redirects for managed categories that are now present (e.g. a category + // that previously emptied and has since come back). Leave kind-page redirects + // (queries, mutations, ...) and non-category redirects (/v4/reference) intact. + const next = existing.filter((entry) => { + const match = CATEGORY_URL_RE.exec(entry) + if (!match) return true + const cat = match[1] + if (!(CATEGORIES as readonly string[]).includes(cat)) return true + return !isPresentInAnyVersion(presence, cat) + }) + + // Add a root redirect for every managed category that is empty in all + // versions. `other` is always present (un-annotated types), so it never + // disappears, but guard against it defensively. + for (const cat of CATEGORIES) { + if (cat === OTHER_CATEGORY) continue + if (isPresentInAnyVersion(presence, cat)) continue + const url = categoryUrlPath(cat) + if (!next.includes(url)) next.push(url) + // Preserve any redirect_from the deleted category page carried so existing + // inbound redirect chains keep resolving. + for (const inherited of capturedRedirects.get(cat) ?? []) { + if (!next.includes(inherited)) next.push(inherited) + } + } + + if (isEqual(next, existing)) return + data.redirect_from = next + await fs.writeFile(indexFile, matter.stringify(content, data)) +} + +// Entry point used by sync.ts after it has bucketed every version. Creates, +// updates, and deletes the per-category content pages, refreshes the reference +// index children, and reconciles disappearance redirects. `contentDir` is +// overridable for tests; production uses the default reference directory. +export async function syncCategoryContentFiles( + presence: CategoryPresence, + options: { contentDir?: string } = {}, +): Promise { + const contentDir = options.contentDir ?? DEFAULT_CONTENT_DIR + const indexFile = path.join(contentDir, 'index.md') + const capturedRedirects = await captureCategoryRedirects(contentDir) + const sourceContent = await buildSourceContent(presence, contentDir) + + await updateContentDirectory({ + targetDirectory: contentDir, + sourceContent, + frontmatter: { + autogenerated: AUTOGENERATED_TYPE, + versions: { fpt: '*', ghec: '*', ghes: '*' }, + }, + }) + + await reconcileIndexRedirects(presence, capturedRedirects, indexFile) +} diff --git a/src/graphql/tests/sync-category-content.ts b/src/graphql/tests/sync-category-content.ts new file mode 100644 index 000000000000..ef3690219664 --- /dev/null +++ b/src/graphql/tests/sync-category-content.ts @@ -0,0 +1,173 @@ +import { tmpdir } from 'os' +import { mkdtemp, rm, mkdir, writeFile, readFile, readdir } from 'fs/promises' +import { existsSync } from 'fs' +import path from 'path' + +import { afterEach, beforeEach, describe, expect, test } from 'vitest' +import matter from '@gr2m/gray-matter' + +import { MARKDOWN_COMMENT } from '@/automated-pipelines/lib/update-markdown' +import { + syncCategoryContentFiles, + type CategoryPresence, +} from '../scripts/utils/sync-category-content' + +const FPT = 'free-pro-team@latest' +const GHEC = 'enterprise-cloud@latest' + +const REFERENCE_DIR = path.join('content', 'graphql', 'reference') + +// The full set of categories present in a steady-state fixture. Returned as a +// fresh Map each call so tests never share mutable state. +function steadyPresence(): CategoryPresence { + return new Map([ + ['actions', new Set([FPT, GHEC])], + ['sponsors', new Set([FPT])], + ['other', new Set([FPT, GHEC])], + ['code-scanning', new Set([FPT, GHEC])], + ]) +} + +// Write an autogenerated category page with the given versions frontmatter. +async function writeCategoryFile( + root: string, + cat: string, + versions: Record, + extra: Record = {}, +) { + const data = { + title: cat, + shortTitle: cat, + intro: `Reference documentation for GraphQL schema types in the ${cat} category.`, + versions, + autogenerated: 'graphql', + category: ['Explore the schema reference'], + ...extra, + } + await writeFile( + path.join(root, REFERENCE_DIR, `${cat}.md`), + matter.stringify(MARKDOWN_COMMENT, data), + ) +} + +async function writeIndex(root: string, children: string[], redirectFrom: string[]) { + const data = { + title: 'Reference', + redirect_from: redirectFrom, + versions: { fpt: '*', ghec: '*', ghes: '*' }, + children, + autogenerated: 'graphql', + } + await writeFile( + path.join(root, REFERENCE_DIR, 'index.md'), + matter.stringify(MARKDOWN_COMMENT, data), + ) +} + +async function readIndex(root: string) { + return matter(await readFile(path.join(root, REFERENCE_DIR, 'index.md'), 'utf8')) +} + +async function snapshotReferenceDir(root: string): Promise> { + const dir = path.join(root, REFERENCE_DIR) + const files = await readdir(dir) + const snapshot: Record = {} + for (const file of files.sort()) { + snapshot[file] = await readFile(path.join(dir, file), 'utf8') + } + return snapshot +} + +describe('syncCategoryContentFiles', () => { + let root: string + let contentDir: string + + beforeEach(async () => { + root = await mkdtemp(path.join(tmpdir(), 'graphql-category-content-')) + await mkdir(path.join(root, REFERENCE_DIR), { recursive: true }) + contentDir = path.join(root, REFERENCE_DIR) + }) + + afterEach(async () => { + await rm(root, { recursive: true, force: true }) + }) + + test('deletes emptied categories, narrows versions, and reconciles redirects', async () => { + await writeIndex( + root, + ['/actions', '/sponsors', '/code-scanning', '/other'], + ['/v4/reference', '/graphql/reference/queries'], + ) + await writeCategoryFile(root, 'actions', { fpt: '*', ghec: '*' }) + await writeCategoryFile(root, 'sponsors', { fpt: '*', ghec: '*' }) + await writeCategoryFile(root, 'code-scanning', { fpt: '*', ghec: '*' }) + await writeCategoryFile(root, 'other', { fpt: '*', ghec: '*' }) + + const presence = steadyPresence() + presence.delete('code-scanning') // emptied -> absent in all versions + + await syncCategoryContentFiles(presence, { contentDir }) + + // Emptied category file is deleted; populated ones remain. + expect(existsSync(path.join(root, REFERENCE_DIR, 'code-scanning.md'))).toBe(false) + expect(existsSync(path.join(root, REFERENCE_DIR, 'actions.md'))).toBe(true) + expect(existsSync(path.join(root, REFERENCE_DIR, 'other.md'))).toBe(true) + + // Versions are narrowed to where the category actually has types. + const sponsors = matter(await readFile(path.join(root, REFERENCE_DIR, 'sponsors.md'), 'utf8')) + expect(sponsors.data.versions).toEqual({ fpt: '*' }) + + const index = await readIndex(root) + // Sidebar children drop the emptied category. + expect(index.data.children).not.toContain('/code-scanning') + expect(index.data.children).toContain('/actions') + expect(index.data.children).toContain('/sponsors') + expect(index.data.children).toContain('/other') + + // Disappeared category gets a root redirect; unrelated redirects are kept; + // present categories are never redirected. + expect(index.data.redirect_from).toContain('/graphql/reference/code-scanning') + expect(index.data.redirect_from).toContain('/v4/reference') + expect(index.data.redirect_from).toContain('/graphql/reference/queries') + expect(index.data.redirect_from).not.toContain('/graphql/reference/actions') + }) + + test('recreates a reappearing category and removes its stale redirect', async () => { + // Seed the post-deletion state: code-scanning has no page and carries a + // disappearance redirect on the index. + await writeIndex( + root, + ['/actions', '/sponsors', '/other'], + ['/v4/reference', '/graphql/reference/code-scanning'], + ) + await writeCategoryFile(root, 'actions', { fpt: '*', ghec: '*' }) + await writeCategoryFile(root, 'sponsors', { fpt: '*' }) + await writeCategoryFile(root, 'other', { fpt: '*', ghec: '*' }) + + await syncCategoryContentFiles(steadyPresence(), { contentDir }) + + expect(existsSync(path.join(root, REFERENCE_DIR, 'code-scanning.md'))).toBe(true) + const index = await readIndex(root) + expect(index.data.children).toContain('/code-scanning') + expect(index.data.redirect_from).not.toContain('/graphql/reference/code-scanning') + // Unrelated redirects survive the reconciliation. + expect(index.data.redirect_from).toContain('/v4/reference') + }) + + test('is idempotent: a second sync run makes no further changes', async () => { + await writeIndex(root, ['/actions', '/sponsors', '/code-scanning', '/other'], ['/v4/reference']) + await writeCategoryFile(root, 'actions', { fpt: '*', ghec: '*' }) + await writeCategoryFile(root, 'sponsors', { fpt: '*' }) + await writeCategoryFile(root, 'code-scanning', { fpt: '*', ghec: '*' }) + await writeCategoryFile(root, 'other', { fpt: '*', ghec: '*' }) + + // First run establishes the canonical steady state for this presence. + await syncCategoryContentFiles(steadyPresence(), { contentDir }) + const before = await snapshotReferenceDir(root) + // Second run with identical input must be a no-op. + await syncCategoryContentFiles(steadyPresence(), { contentDir }) + const after = await snapshotReferenceDir(root) + + expect(after).toEqual(before) + }) +}) diff --git a/src/landings/components/shared/LandingArticleGridWithFilter.tsx b/src/landings/components/shared/LandingArticleGridWithFilter.tsx index 3e14d10406ce..cc216bbc8c42 100644 --- a/src/landings/components/shared/LandingArticleGridWithFilter.tsx +++ b/src/landings/components/shared/LandingArticleGridWithFilter.tsx @@ -1,6 +1,7 @@ import React, { useState, useRef, useEffect, useMemo } from 'react' import { TextInput, ActionMenu, ActionList, Token, Pagination } from '@primer/react' import { SearchIcon } from '@primer/octicons-react' +import { announce } from '@primer/live-region-element' import cx from 'classnames' import { Link } from '@/frame/components/Link' @@ -61,6 +62,7 @@ export const ArticleGrid = ({ const inputRef = useRef(null) const headingRef = useRef(null) + const statusTimerRef = useRef | null>(null) // Read filter state directly from query params const searchQuery = params['articles-filter'] || '' @@ -230,6 +232,25 @@ export const ArticleGrid = ({ prevPageRef.current = currentPage }, [currentPage]) + // Announce search/filter no-results to assistive technologies. + // Uses @primer/live-region-element which renders a web component + // with a shadow DOM on document.body — completely isolated from React's component + // tree. This avoids VoiceOver re-announcing the focused input when React re-renders + // cause DOM mutations near the TextInput. + const noArticlesFoundMessage = t('article_grid.no_articles_found') + useEffect(() => { + if (statusTimerRef.current) clearTimeout(statusTimerRef.current) + + if (filteredResults.length === 0) { + statusTimerRef.current = setTimeout(() => { + announce(noArticlesFoundMessage, { politeness: 'assertive' }) + }, 750) + } + + return () => { + if (statusTimerRef.current) clearTimeout(statusTimerRef.current) + } + }, [filteredResults.length, searchQuery, selectedCategory, noArticlesFoundMessage]) return (
{/* Filter and Search Controls */} @@ -294,14 +315,14 @@ export const ArticleGrid = ({ /> ))} {filteredResults.length === 0 && ( -
+ )} - -
- {filteredResults.length === 0 ? t('article_grid.no_articles_found') : ''} -
{/* Pagination */} diff --git a/src/secret-scanning/components/SecretScanningTable.tsx b/src/secret-scanning/components/SecretScanningTable.tsx index d942aa472c9d..b8818e93ad91 100644 --- a/src/secret-scanning/components/SecretScanningTable.tsx +++ b/src/secret-scanning/components/SecretScanningTable.tsx @@ -1,11 +1,32 @@ -import React, { useState, useMemo } from 'react' +import React, { useState, useMemo, useEffect, useRef, useCallback } from 'react' import { DataTable, Table } from '@primer/react/experimental' -import { TextInput, ActionMenu, ActionList, Pagination } from '@primer/react' +import { TextInput, ActionMenu, ActionList, Pagination, Button } from '@primer/react' +import debounce from 'lodash/debounce' import { useTranslation } from '@/languages/components/useTranslation' +import { sendEvent } from '@/events/components/events' +import { EventType } from '@/events/types' +import { sanitizeSearchQuery } from '@/search/lib/sanitize-search-query' import type { SecretScanningData } from '@/types' const PAGE_SIZE = 25 +// Identifies this table in the docs.v0.TableInteractionEvent analytics. +const TABLE_INTERACTION_NAME = 'secret-scanning-patterns' + +// Maps DataTable column ids to the canonical analytics field name so that a +// filter and a sort on the same column report the same +// table_interaction_field_name. Filter keys already use these canonical names. +const COLUMN_FIELD_NAMES: Record = { + provider: 'provider', + supportedSecret: 'secret', + isPublic: 'partnerAlert', + isPrivateWithGhas: 'userAlert', + hasPushProtection: 'pushProtection', + hasValidityCheck: 'validityCheck', + hasExtendedMetadata: 'metadata', + base64Supported: 'base64', +} + type SecretScanningRow = SecretScanningData & { id: string } type FilterState = { @@ -17,20 +38,81 @@ type FilterState = { base64: 'all' | 'yes' | 'no' } +type FilterKey = Exclude + +const DEFAULT_FILTERS: FilterState = { + search: '', + pushProtection: 'all', + validityCheck: 'all', + partnerAlert: 'all', + metadata: 'all', + base64: 'all', +} + +type TableInteractionType = 'search' | 'filter' | 'sort' | 'paginate' | 'reset' + export function SecretScanningTable({ data }: { data: SecretScanningData[] }) { const { t } = useTranslation('secret_scanning') - const [filters, setFilters] = useState({ - search: '', - pushProtection: 'all', - validityCheck: 'all', - partnerAlert: 'all', - metadata: 'all', - base64: 'all', - }) + const [filters, setFilters] = useState(DEFAULT_FILTERS) const [currentPage, setCurrentPage] = useState(1) const [sortColumn, setSortColumn] = useState(undefined) const [sortDirection, setSortDirection] = useState<'ASC' | 'DESC'>('ASC') + // Emit a TableInteractionEvent for analytics (github/docs-engineering#6593). + const trackInteraction = useCallback( + (interactionType: TableInteractionType, fieldName?: string, fieldValue?: string) => { + sendEvent({ + type: EventType.tableInteraction, + table_interaction_name: TABLE_INTERACTION_NAME, + table_interaction_type: interactionType, + table_interaction_field_name: fieldName, + table_interaction_field_value: fieldValue, + }) + }, + [], + ) + + // Debounce search tracking so we record the settled query, not every keystroke. + const debouncedTrackSearchRef = useRef | null>(null) + useEffect(() => { + debouncedTrackSearchRef.current = debounce((query: string) => { + // Sanitize before logging: users may paste a real secret into this + // table's search to check support, and the query is sent to analytics. + trackInteraction('search', 'search', sanitizeSearchQuery(query)) + }, 500) + return () => { + debouncedTrackSearchRef.current?.flush() + debouncedTrackSearchRef.current?.cancel() + } + }, [trackInteraction]) + + const handleFilterChange = useCallback( + (field: FilterKey, value: 'all' | 'yes' | 'no') => { + setFilters((f) => ({ ...f, [field]: value })) + setCurrentPage(1) + trackInteraction('filter', field, value) + }, + [trackInteraction], + ) + + const handleReset = useCallback(() => { + setFilters(DEFAULT_FILTERS) + setCurrentPage(1) + setSortColumn(undefined) + setSortDirection('ASC') + debouncedTrackSearchRef.current?.cancel() + trackInteraction('reset') + }, [trackInteraction]) + + const hasActiveFilters = + filters.search !== '' || + filters.pushProtection !== 'all' || + filters.validityCheck !== 'all' || + filters.partnerAlert !== 'all' || + filters.metadata !== 'all' || + filters.base64 !== 'all' || + sortColumn !== undefined + // Add stable IDs once based on original data order const dataWithIds: SecretScanningRow[] = useMemo(() => { return data.map((entry, i) => ({ ...entry, id: `${entry.secretType}-${i}` })) @@ -92,51 +174,49 @@ export function SecretScanningTable({ data }: { data: SecretScanningData[] }) { { - setFilters((f) => ({ ...f, pushProtection: v })) - setCurrentPage(1) - }} + onChange={(v) => handleFilterChange('pushProtection', v)} /> { - setFilters((f) => ({ ...f, validityCheck: v })) - setCurrentPage(1) - }} + onChange={(v) => handleFilterChange('validityCheck', v)} /> { - setFilters((f) => ({ ...f, partnerAlert: v })) - setCurrentPage(1) - }} + onChange={(v) => handleFilterChange('partnerAlert', v)} /> { - setFilters((f) => ({ ...f, metadata: v })) - setCurrentPage(1) - }} + onChange={(v) => handleFilterChange('metadata', v)} /> { - setFilters((f) => ({ ...f, base64: v })) - setCurrentPage(1) - }} + onChange={(v) => handleFilterChange('base64', v)} /> + {hasActiveFilters && ( + + )}
{ - setFilters((f) => ({ ...f, search: e.target.value })) + const value = e.target.value + setFilters((f) => ({ ...f, search: value })) setCurrentPage(1) + if (value.trim()) debouncedTrackSearchRef.current?.(value) + else debouncedTrackSearchRef.current?.cancel() }} />
@@ -168,6 +248,11 @@ export function SecretScanningTable({ data }: { data: SecretScanningData[] }) { setSortColumn(String(columnId)) setSortDirection(direction) setCurrentPage(1) + trackInteraction( + 'sort', + COLUMN_FIELD_NAMES[String(columnId)] ?? String(columnId), + direction, + ) }} columns={[ { @@ -280,7 +365,10 @@ export function SecretScanningTable({ data }: { data: SecretScanningData[] }) { aria-label={t('pagination_label')} pageCount={pageCount} currentPage={currentPage} - onPageChange={(_e: React.MouseEvent, page: number) => setCurrentPage(page)} + onPageChange={(_e: React.MouseEvent, page: number) => { + setCurrentPage(page) + trackInteraction('paginate', 'page', String(page)) + }} /> )}