From bfc96c05aa4d4c4b2b69328dfb96d36842fbbd33 Mon Sep 17 00:00:00 2001 From: Raffaele Di Fazio Date: Wed, 14 Oct 2026 15:02:21 +0000 Subject: [PATCH 1/6] Disable auto deployment in deployment.yaml (#63858) --- config/moda/deployment.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/config/moda/deployment.yaml b/config/moda/deployment.yaml index 925bb6bb633d..50b994ca27bc 100644 --- a/config/moda/deployment.yaml +++ b/config/moda/deployment.yaml @@ -196,7 +196,7 @@ environments: require_vault: true secret_environment: staging required_review_tasks: [] - auto_deploy: true + auto_deploy: false skip_auto_merge: true proxima: true resource_path: config/kubernetes/docs-internal-review-lab From 38ea45d43cebdb976b8aedb670196d89620d91d9 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 14 Oct 2026 15:27:46 +0000 Subject: [PATCH 2/6] Flush queued events on every tab hide (#63814) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: ea1bcfd5-03bd-4b5f-adba-0bbd55a61815 --- src/events/components/events.ts | 2 + .../tests/playwright-rendering.spec.ts | 61 +++++++++++++++++++ 2 files changed, 63 insertions(+) diff --git a/src/events/components/events.ts b/src/events/components/events.ts index 0248b61992e1..c00d32a1cce7 100644 --- a/src/events/components/events.ts +++ b/src/events/components/events.ts @@ -279,6 +279,8 @@ function initPageAndExitEvent() { document.addEventListener('visibilitychange', () => { if (document.visibilityState === 'hidden') { sendExit() + // sendExit only flushes the first time, so later hides still need a flush. + flushQueue() } else { flushQueue() } diff --git a/src/fixtures/tests/playwright-rendering.spec.ts b/src/fixtures/tests/playwright-rendering.spec.ts index 840d02190ad7..ec3ffd1405d0 100644 --- a/src/fixtures/tests/playwright-rendering.spec.ts +++ b/src/fixtures/tests/playwright-rendering.spec.ts @@ -1158,6 +1158,67 @@ test.describe('survey', () => { await page.getByRole('button', { name: 'Cancel' }).click() }) + test('events queued after the page was hidden once still send on the next hide', async ({ + page, + }) => { + const sentTypes: string[] = [] + + // Install the route before interacting with the page to avoid event races. + await page.route('**/api/events', (route, request) => { + const postData = request.postData() + if (postData) { + for (const eventBody of JSON.parse(postData)) sentTypes.push(eventBody.type) + } + route.fulfill({}) + }) + + await page.addInitScript(() => { + window.GHDOCSPLAYWRIGHT = 1 + }) + + await page.goto('/get-started/foo/for-playwright') + + const setVisibility = (state: 'hidden' | 'visible') => + page.evaluate((visibilityState) => { + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get() { + return visibilityState + }, + }) + document.dispatchEvent(new Event('visibilitychange')) + }, state) + + await setVisibility('hidden') + await setVisibility('visible') + await expect.poll(() => sentTypes).toContain('exit') + + // Vote and hide in one task so the 5 second batch timer can't flush the vote instead. + const typesSentOnSecondHide = await page.evaluate(async () => { + const beacons: Blob[] = [] + const originalSendBeacon = navigator.sendBeacon + navigator.sendBeacon = (url, data) => { + if (url === '/api/events') beacons.push(data as Blob) + return true + } + document.querySelector('[for=survey-yes]')!.click() + Object.defineProperty(document, 'visibilityState', { + configurable: true, + get() { + return 'hidden' + }, + }) + document.dispatchEvent(new Event('visibilitychange')) + navigator.sendBeacon = originalSendBeacon + const batches = await Promise.all(beacons.map((beacon) => beacon.text())) + return batches.flatMap((batch) => + JSON.parse(batch).map((eventBody: { type: string }) => eventBody.type), + ) + }) + + expect(typesSentOnSecondHide).toContain('survey') + }) + test('vote on one page, then go to another and it should reset', async ({ page }) => { // Install the route before interacting with the page to avoid event races. await page.route('**/api/events', (route) => { From 85efb7ad9550be52c3d27b6e03171a577bd17ef1 Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 14 Oct 2026 15:27:58 +0000 Subject: [PATCH 3/6] Remove the `pull_request_review` trigger from auto-close-dependencies (#63843) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: 8aca27fe-41b4-4a79-9f10-dd9021f3a735 --- .github/workflows/auto-close-dependencies.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/auto-close-dependencies.yml b/.github/workflows/auto-close-dependencies.yml index c0c6a4d5c104..a5fb729cbf88 100644 --- a/.github/workflows/auto-close-dependencies.yml +++ b/.github/workflows/auto-close-dependencies.yml @@ -9,10 +9,6 @@ on: - 'Gemfile*' - 'Dockerfile' - '.github/workflows/**' - pull_request_review: - types: - - edited - - submitted permissions: contents: write From 0f5ef70b7bfa59fa1c1c4957d7c13d2e8fc0c15d Mon Sep 17 00:00:00 2001 From: Kevin Heis Date: Wed, 14 Oct 2026 15:52:15 +0000 Subject: [PATCH 4/6] Skip lockfile churn check for Dependabot and docs-bot PRs (#63815) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> Copilot-Session: edc6793b-939e-4ad7-a298-0b9c964b3cb4 Copilot-Session: 8aca27fe-41b4-4a79-9f10-dd9021f3a735 --- .github/workflows/reviewers-docs-engineering.yml | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/.github/workflows/reviewers-docs-engineering.yml b/.github/workflows/reviewers-docs-engineering.yml index 03ba53d2ee49..d253c2553af4 100644 --- a/.github/workflows/reviewers-docs-engineering.yml +++ b/.github/workflows/reviewers-docs-engineering.yml @@ -51,10 +51,8 @@ jobs: # Detect PRs that only change package-lock.json. Contributors often create # cross-platform npm install churn while editing content. # Comment with reset instructions instead of adding them to the engineering review board. - # Dependabot security updates can change only the lockfile when package.json - # does not name the transitive dependency. - # Those PRs are intentional, so reset instructions would be wrong and - # keeping them off the board would leave them without engineering triage. + # Dependabot and docs-bot PRs can change only the lockfile on purpose, for example + # to fix a transitive dependency. Skip the check for them so they still get engineering triage. - name: Detect lockfile-only churn id: detect env: @@ -64,8 +62,8 @@ jobs: CHANGED_FILES: ${{ github.event.pull_request.changed_files }} REPO: ${{ github.repository }} run: | - if [ "$PR_AUTHOR" = "dependabot[bot]" ]; then - echo "Author is Dependabot; skipping lockfile churn detection." + if [ "$PR_AUTHOR" = "dependabot[bot]" ] || [ "$PR_AUTHOR" = "docs-bot" ]; then + echo "Author is $PR_AUTHOR; skipping lockfile churn detection." echo "lockfile_only=false" >> "$GITHUB_OUTPUT" exit 0 fi From c877d4459a294cec9aeb62a0911bf733988fadd1 Mon Sep 17 00:00:00 2001 From: "dependabot[bot]" <49699333+dependabot[bot]@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:27:53 +0000 Subject: [PATCH 5/6] Bump devcontainers/javascript-node from `02207aa` to `5ed0bf1` in /.devcontainer in the baseimages group across 1 directory (#63853) Signed-off-by: dependabot[bot] Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com> --- .devcontainer/Dockerfile | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.devcontainer/Dockerfile b/.devcontainer/Dockerfile index cd8a294621af..acbfe33253b2 100644 --- a/.devcontainer/Dockerfile +++ b/.devcontainer/Dockerfile @@ -1,4 +1,4 @@ # Update ARG and FROM together from the Node devcontainer image tags: # https://mcr.microsoft.com/en-us/product/devcontainers/javascript-node/tags ARG VARIANT=dev-24-bookworm -FROM mcr.microsoft.com/devcontainers/javascript-node:dev-24-bookworm@sha256:02207aa539c49a8714bd18ba482a05381a42ee4768ea19a39a4c58f2ae6f7644 +FROM mcr.microsoft.com/devcontainers/javascript-node:dev-24-bookworm@sha256:5ed0bf1411cffe8182ee6808477dd64c969a4ce78fee1b63e6a654d7deff889c From 55e4d16cc4f1668a75e78375e44c1ff817610b12 Mon Sep 17 00:00:00 2001 From: subatoi <32935794+subatoi@users.noreply.github.com> Date: Wed, 14 Oct 2026 16:48:48 +0000 Subject: [PATCH 6/6] Respect Slack API rate limits in confirmation workflow (#63855) Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../confirm-internal-staff-work-in-docs.yml | 121 ++++++++++++++---- 1 file changed, 95 insertions(+), 26 deletions(-) diff --git a/.github/workflows/confirm-internal-staff-work-in-docs.yml b/.github/workflows/confirm-internal-staff-work-in-docs.yml index 1cfd8f94a24b..62c06080aebe 100644 --- a/.github/workflows/confirm-internal-staff-work-in-docs.yml +++ b/.github/workflows/confirm-internal-staff-work-in-docs.yml @@ -75,6 +75,7 @@ jobs: github-token: ${{ github.token }} script: | const githubLogin = context.payload.sender.login.toLowerCase(); + const lookupStartedAt = Date.now(); core.info(`Looking up Slack user for @${githubLogin}...`); core.setOutput('slack_user_id', ''); @@ -85,59 +86,127 @@ jobs: throw new Error('SLACK_DOCS_BOT_TOKEN is not configured.'); } + const sleep = (milliseconds) => + new Promise((resolve) => setTimeout(resolve, milliseconds)); + const maxRetries = 5; + const minimumRequestIntervalMs = 3000; + let lastRequestAt = 0; + + async function fetchUsersPage(url, pageNumber) { + for (let attempt = 0; attempt <= maxRetries; attempt += 1) { + const spacingDelay = Math.max( + 0, + minimumRequestIntervalMs - (Date.now() - lastRequestAt), + ); + if (spacingDelay > 0) { + await sleep(spacingDelay); + } + + lastRequestAt = Date.now(); + const requestStartedAt = lastRequestAt; + const response = await fetch(url, { + headers: { + Authorization: `Bearer ${token}`, + }, + }); + const requestDurationMs = Date.now() - requestStartedAt; + + if (response.status === 429) { + if (attempt === maxRetries) { + throw new Error( + `Slack users.list page ${pageNumber} remained rate-limited after ${maxRetries} retries.`, + ); + } + + const retryAfterSeconds = Number(response.headers.get('retry-after')); + const retryDelayMs = + Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0 + ? retryAfterSeconds * 1000 + : Math.min(1000 * 2 ** attempt, 30000); + + core.warning( + `Slack users.list page ${pageNumber} was rate-limited (attempt ${attempt + 1}/${maxRetries + 1}); retrying in ${retryDelayMs} ms.`, + ); + await sleep(retryDelayMs); + continue; + } + + if (!response.ok) { + throw new Error( + `Slack users.list page ${pageNumber} returned HTTP ${response.status} after ${requestDurationMs} ms.`, + ); + } + + if (attempt > 0) { + core.info( + `Slack users.list page ${pageNumber} succeeded after ${attempt} retry attempt(s) in ${requestDurationMs} ms.`, + ); + } else { + core.info( + `Slack users.list page ${pageNumber} returned HTTP ${response.status} in ${requestDurationMs} ms.`, + ); + } + + return response.json(); + } + + throw new Error(`Slack users.list page ${pageNumber} request did not complete.`); + } + let cursor = ''; let pages = 0; - const matches = []; do { - core.info(`Fetching Slack users.list page ${pages + 1}...`); + const pageNumber = pages + 1; + core.info(`Fetching Slack users.list page ${pageNumber}...`); const url = new URL('https://slack.com/api/users.list'); url.searchParams.set('limit', '200'); if (cursor) { url.searchParams.set('cursor', cursor); } - const response = await fetch(url, { - headers: { - Authorization: `Bearer ${token}`, - }, - }); - - if (!response.ok) { - throw new Error(`Slack users.list returned HTTP ${response.status}.`); - } - - const result = await response.json(); + const result = await fetchUsersPage(url, pageNumber); if (!result.ok) { - throw new Error(`Slack users.list failed: ${result.error || 'unknown error'}.`); + throw new Error( + `Slack users.list page ${pageNumber} failed: ${result.error || 'unknown error'}.`, + ); } if (!Array.isArray(result.members)) { - throw new Error('Slack users.list response did not include a members array.'); + throw new Error( + `Slack users.list page ${pageNumber} did not include a members array.`, + ); } pages += 1; - matches.push(...result.members.filter((member) => + core.info(`Slack users.list page ${pages} contained ${result.members.length} member records.`); + const match = result.members.find((member) => !member.deleted && !member.is_bot && !member.is_app_user && typeof member.id === 'string' && typeof member.name === 'string' && member.name.toLowerCase() === githubLogin - )); + ); + + if (match) { + core.info( + `Found the matching Slack username on page ${pages} after ${Date.now() - lookupStartedAt} ms.`, + ); + core.setOutput('slack_user_id', match.id); + core.setOutput('slack_mention', `<@${match.id}>`); + return; + } cursor = result.response_metadata?.next_cursor || ''; + core.info( + `No matching Slack username on page ${pages}; ${cursor ? 'continuing pagination.' : 'no more pages.'}`, + ); } while (cursor); - core.info(`Checked ${pages} pages of users.list. Found ${matches.length} match(es) for @${githubLogin}.`); - if (matches.length !== 1) { - core.warning(`Expected one Slack username matching @${githubLogin}; found ${matches.length}. No DM will be sent.`); - return; - } - - core.info(`Will DM Slack user ${matches[0].id}.`); - core.setOutput('slack_user_id', matches[0].id); - core.setOutput('slack_mention', `<@${matches[0].id}>`); + core.warning( + `No active Slack username matched @${githubLogin} after ${pages} page(s) and ${Date.now() - lookupStartedAt} ms; no DM will be sent.`, + ); - name: DM the author in Slack id: slack_dm