Skip to content

Commit b020771

Browse files
authored
Merge pull request #46322 from github/repo-sync
Repo sync
2 parents 8b39dc4 + 55e4d16 commit b020771

7 files changed

Lines changed: 164 additions & 38 deletions

File tree

‎.devcontainer/Dockerfile‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# Update ARG and FROM together from the Node devcontainer image tags:
22
# https://mcr.microsoft.com/en-us/product/devcontainers/javascript-node/tags
33
ARG VARIANT=dev-24-bookworm
4-
FROM mcr.microsoft.com/devcontainers/javascript-node:dev-24-bookworm@sha256:02207aa539c49a8714bd18ba482a05381a42ee4768ea19a39a4c58f2ae6f7644
4+
FROM mcr.microsoft.com/devcontainers/javascript-node:dev-24-bookworm@sha256:5ed0bf1411cffe8182ee6808477dd64c969a4ce78fee1b63e6a654d7deff889c

‎.github/workflows/auto-close-dependencies.yml‎

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,6 @@ on:
99
- 'Gemfile*'
1010
- 'Dockerfile'
1111
- '.github/workflows/**'
12-
pull_request_review:
13-
types:
14-
- edited
15-
- submitted
1612

1713
permissions:
1814
contents: write

‎.github/workflows/confirm-internal-staff-work-in-docs.yml‎

Lines changed: 95 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ jobs:
7575
github-token: ${{ github.token }}
7676
script: |
7777
const githubLogin = context.payload.sender.login.toLowerCase();
78+
const lookupStartedAt = Date.now();
7879
7980
core.info(`Looking up Slack user for @${githubLogin}...`);
8081
core.setOutput('slack_user_id', '');
@@ -85,59 +86,127 @@ jobs:
8586
throw new Error('SLACK_DOCS_BOT_TOKEN is not configured.');
8687
}
8788
89+
const sleep = (milliseconds) =>
90+
new Promise((resolve) => setTimeout(resolve, milliseconds));
91+
const maxRetries = 5;
92+
const minimumRequestIntervalMs = 3000;
93+
let lastRequestAt = 0;
94+
95+
async function fetchUsersPage(url, pageNumber) {
96+
for (let attempt = 0; attempt <= maxRetries; attempt += 1) {
97+
const spacingDelay = Math.max(
98+
0,
99+
minimumRequestIntervalMs - (Date.now() - lastRequestAt),
100+
);
101+
if (spacingDelay > 0) {
102+
await sleep(spacingDelay);
103+
}
104+
105+
lastRequestAt = Date.now();
106+
const requestStartedAt = lastRequestAt;
107+
const response = await fetch(url, {
108+
headers: {
109+
Authorization: `Bearer ${token}`,
110+
},
111+
});
112+
const requestDurationMs = Date.now() - requestStartedAt;
113+
114+
if (response.status === 429) {
115+
if (attempt === maxRetries) {
116+
throw new Error(
117+
`Slack users.list page ${pageNumber} remained rate-limited after ${maxRetries} retries.`,
118+
);
119+
}
120+
121+
const retryAfterSeconds = Number(response.headers.get('retry-after'));
122+
const retryDelayMs =
123+
Number.isFinite(retryAfterSeconds) && retryAfterSeconds > 0
124+
? retryAfterSeconds * 1000
125+
: Math.min(1000 * 2 ** attempt, 30000);
126+
127+
core.warning(
128+
`Slack users.list page ${pageNumber} was rate-limited (attempt ${attempt + 1}/${maxRetries + 1}); retrying in ${retryDelayMs} ms.`,
129+
);
130+
await sleep(retryDelayMs);
131+
continue;
132+
}
133+
134+
if (!response.ok) {
135+
throw new Error(
136+
`Slack users.list page ${pageNumber} returned HTTP ${response.status} after ${requestDurationMs} ms.`,
137+
);
138+
}
139+
140+
if (attempt > 0) {
141+
core.info(
142+
`Slack users.list page ${pageNumber} succeeded after ${attempt} retry attempt(s) in ${requestDurationMs} ms.`,
143+
);
144+
} else {
145+
core.info(
146+
`Slack users.list page ${pageNumber} returned HTTP ${response.status} in ${requestDurationMs} ms.`,
147+
);
148+
}
149+
150+
return response.json();
151+
}
152+
153+
throw new Error(`Slack users.list page ${pageNumber} request did not complete.`);
154+
}
155+
88156
let cursor = '';
89157
let pages = 0;
90-
const matches = [];
91158
92159
do {
93-
core.info(`Fetching Slack users.list page ${pages + 1}...`);
160+
const pageNumber = pages + 1;
161+
core.info(`Fetching Slack users.list page ${pageNumber}...`);
94162
const url = new URL('https://slack.com/api/users.list');
95163
url.searchParams.set('limit', '200');
96164
if (cursor) {
97165
url.searchParams.set('cursor', cursor);
98166
}
99167
100-
const response = await fetch(url, {
101-
headers: {
102-
Authorization: `Bearer ${token}`,
103-
},
104-
});
105-
106-
if (!response.ok) {
107-
throw new Error(`Slack users.list returned HTTP ${response.status}.`);
108-
}
109-
110-
const result = await response.json();
168+
const result = await fetchUsersPage(url, pageNumber);
111169
if (!result.ok) {
112-
throw new Error(`Slack users.list failed: ${result.error || 'unknown error'}.`);
170+
throw new Error(
171+
`Slack users.list page ${pageNumber} failed: ${result.error || 'unknown error'}.`,
172+
);
113173
}
114174
115175
if (!Array.isArray(result.members)) {
116-
throw new Error('Slack users.list response did not include a members array.');
176+
throw new Error(
177+
`Slack users.list page ${pageNumber} did not include a members array.`,
178+
);
117179
}
118180
119181
pages += 1;
120-
matches.push(...result.members.filter((member) =>
182+
core.info(`Slack users.list page ${pages} contained ${result.members.length} member records.`);
183+
const match = result.members.find((member) =>
121184
!member.deleted &&
122185
!member.is_bot &&
123186
!member.is_app_user &&
124187
typeof member.id === 'string' &&
125188
typeof member.name === 'string' &&
126189
member.name.toLowerCase() === githubLogin
127-
));
190+
);
191+
192+
if (match) {
193+
core.info(
194+
`Found the matching Slack username on page ${pages} after ${Date.now() - lookupStartedAt} ms.`,
195+
);
196+
core.setOutput('slack_user_id', match.id);
197+
core.setOutput('slack_mention', `<@${match.id}>`);
198+
return;
199+
}
128200
129201
cursor = result.response_metadata?.next_cursor || '';
202+
core.info(
203+
`No matching Slack username on page ${pages}; ${cursor ? 'continuing pagination.' : 'no more pages.'}`,
204+
);
130205
} while (cursor);
131206
132-
core.info(`Checked ${pages} pages of users.list. Found ${matches.length} match(es) for @${githubLogin}.`);
133-
if (matches.length !== 1) {
134-
core.warning(`Expected one Slack username matching @${githubLogin}; found ${matches.length}. No DM will be sent.`);
135-
return;
136-
}
137-
138-
core.info(`Will DM Slack user ${matches[0].id}.`);
139-
core.setOutput('slack_user_id', matches[0].id);
140-
core.setOutput('slack_mention', `<@${matches[0].id}>`);
207+
core.warning(
208+
`No active Slack username matched @${githubLogin} after ${pages} page(s) and ${Date.now() - lookupStartedAt} ms; no DM will be sent.`,
209+
);
141210
142211
- name: DM the author in Slack
143212
id: slack_dm

‎.github/workflows/reviewers-docs-engineering.yml‎

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,8 @@ jobs:
5151
# Detect PRs that only change package-lock.json. Contributors often create
5252
# cross-platform npm install churn while editing content.
5353
# Comment with reset instructions instead of adding them to the engineering review board.
54-
# Dependabot security updates can change only the lockfile when package.json
55-
# does not name the transitive dependency.
56-
# Those PRs are intentional, so reset instructions would be wrong and
57-
# keeping them off the board would leave them without engineering triage.
54+
# Dependabot and docs-bot PRs can change only the lockfile on purpose, for example
55+
# to fix a transitive dependency. Skip the check for them so they still get engineering triage.
5856
- name: Detect lockfile-only churn
5957
id: detect
6058
env:
@@ -64,8 +62,8 @@ jobs:
6462
CHANGED_FILES: ${{ github.event.pull_request.changed_files }}
6563
REPO: ${{ github.repository }}
6664
run: |
67-
if [ "$PR_AUTHOR" = "dependabot[bot]" ]; then
68-
echo "Author is Dependabot; skipping lockfile churn detection."
65+
if [ "$PR_AUTHOR" = "dependabot[bot]" ] || [ "$PR_AUTHOR" = "docs-bot" ]; then
66+
echo "Author is $PR_AUTHOR; skipping lockfile churn detection."
6967
echo "lockfile_only=false" >> "$GITHUB_OUTPUT"
7068
exit 0
7169
fi

‎config/moda/deployment.yaml‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -196,7 +196,7 @@ environments:
196196
require_vault: true
197197
secret_environment: staging
198198
required_review_tasks: []
199-
auto_deploy: true
199+
auto_deploy: false
200200
skip_auto_merge: true
201201
proxima: true
202202
resource_path: config/kubernetes/docs-internal-review-lab

‎src/events/components/events.ts‎

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -279,6 +279,8 @@ function initPageAndExitEvent() {
279279
document.addEventListener('visibilitychange', () => {
280280
if (document.visibilityState === 'hidden') {
281281
sendExit()
282+
// sendExit only flushes the first time, so later hides still need a flush.
283+
flushQueue()
282284
} else {
283285
flushQueue()
284286
}

‎src/fixtures/tests/playwright-rendering.spec.ts‎

Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1158,6 +1158,67 @@ test.describe('survey', () => {
11581158
await page.getByRole('button', { name: 'Cancel' }).click()
11591159
})
11601160

1161+
test('events queued after the page was hidden once still send on the next hide', async ({
1162+
page,
1163+
}) => {
1164+
const sentTypes: string[] = []
1165+
1166+
// Install the route before interacting with the page to avoid event races.
1167+
await page.route('**/api/events', (route, request) => {
1168+
const postData = request.postData()
1169+
if (postData) {
1170+
for (const eventBody of JSON.parse(postData)) sentTypes.push(eventBody.type)
1171+
}
1172+
route.fulfill({})
1173+
})
1174+
1175+
await page.addInitScript(() => {
1176+
window.GHDOCSPLAYWRIGHT = 1
1177+
})
1178+
1179+
await page.goto('/get-started/foo/for-playwright')
1180+
1181+
const setVisibility = (state: 'hidden' | 'visible') =>
1182+
page.evaluate((visibilityState) => {
1183+
Object.defineProperty(document, 'visibilityState', {
1184+
configurable: true,
1185+
get() {
1186+
return visibilityState
1187+
},
1188+
})
1189+
document.dispatchEvent(new Event('visibilitychange'))
1190+
}, state)
1191+
1192+
await setVisibility('hidden')
1193+
await setVisibility('visible')
1194+
await expect.poll(() => sentTypes).toContain('exit')
1195+
1196+
// Vote and hide in one task so the 5 second batch timer can't flush the vote instead.
1197+
const typesSentOnSecondHide = await page.evaluate(async () => {
1198+
const beacons: Blob[] = []
1199+
const originalSendBeacon = navigator.sendBeacon
1200+
navigator.sendBeacon = (url, data) => {
1201+
if (url === '/api/events') beacons.push(data as Blob)
1202+
return true
1203+
}
1204+
document.querySelector<HTMLElement>('[for=survey-yes]')!.click()
1205+
Object.defineProperty(document, 'visibilityState', {
1206+
configurable: true,
1207+
get() {
1208+
return 'hidden'
1209+
},
1210+
})
1211+
document.dispatchEvent(new Event('visibilitychange'))
1212+
navigator.sendBeacon = originalSendBeacon
1213+
const batches = await Promise.all(beacons.map((beacon) => beacon.text()))
1214+
return batches.flatMap((batch) =>
1215+
JSON.parse(batch).map((eventBody: { type: string }) => eventBody.type),
1216+
)
1217+
})
1218+
1219+
expect(typesSentOnSecondHide).toContain('survey')
1220+
})
1221+
11611222
test('vote on one page, then go to another and it should reset', async ({ page }) => {
11621223
// Install the route before interacting with the page to avoid event races.
11631224
await page.route('**/api/events', (route) => {

0 commit comments

Comments
 (0)