From d2764aa47ef0c9bd7c337308ea93d682f4544891 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 23 Oct 2026 13:50:35 +0200 Subject: [PATCH 1/3] chore(ci): remove PR review reminders Co-Authored-By: GPT-6 --- .github/workflows/pr-review-reminder.yml | 37 --- scripts/pr-review-reminder.mjs | 321 ----------------------- 2 files changed, 358 deletions(-) delete mode 100644 .github/workflows/pr-review-reminder.yml delete mode 100644 scripts/pr-review-reminder.mjs diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml deleted file mode 100644 index e601f859d2c4..000000000000 --- a/.github/workflows/pr-review-reminder.yml +++ /dev/null @@ -1,37 +0,0 @@ -name: 'PR: Review Reminder' - -on: - workflow_dispatch: - schedule: - # Run on weekdays at 10:00 AM UTC. No new reminders can fire on weekends because - # Saturday/Sunday are never counted as business days. - - cron: '0 10 * * 1-5' - -# pulls.* list + listRequestedReviewers + createComment on PRs → pull-requests: write -# repos.listCollaborators (outside) → Metadata read on the token (see GitHub App permission map) -# checkout → contents: read -permissions: - contents: read - pull-requests: write - -concurrency: - group: ${{ github.workflow }} - cancel-in-progress: false - -jobs: - remind-reviewers: - # `schedule` has no `repository` on github.event; forks must be skipped only for workflow_dispatch. - if: github.event_name == 'schedule' || github.event.repository.fork != true - runs-on: namespace-profile-sentry-javascript-small - steps: - - name: Checkout repository - uses: actions/checkout@v7 - - - name: Remind pending reviewers - uses: actions/github-script@v9 - with: - script: | - const { default: run } = await import( - `${process.env.GITHUB_WORKSPACE}/scripts/pr-review-reminder.mjs` - ); - await run({ github, context, core }); diff --git a/scripts/pr-review-reminder.mjs b/scripts/pr-review-reminder.mjs deleted file mode 100644 index e4886bde47ed..000000000000 --- a/scripts/pr-review-reminder.mjs +++ /dev/null @@ -1,321 +0,0 @@ -/** - * PR Review Reminder script. - * - * Posts reminder comments on open PRs whose requested reviewers have not - * responded within 2 business days. Re-nags every 2 business days thereafter - * until the review is submitted (or the request is removed). - * - * PRs are skipped (no reminder) when they are drafts, opened by a bot, already - * approved, or have an outstanding "changes requested" review (the PR is then - * waiting on the author, not the reviewers). - * - * @mentions are narrowed as follows: - * - Individual users: not [outside collaborators](https://docs.github.com/en/organizations/managing-outside-collaborators) - * on this repo (via `repos.listCollaborators` with `affiliation: outside` — repo-scoped, no extra token). - * - Team reviewers: only org teams in `SDK_TEAM_SLUGS` (by slug). - * - * Business days exclude weekends and a small set of recurring public holidays - * (same calendar date each year) for US, CA, and AT. - * - * Intended to be called from a GitHub Actions workflow via actions/github-script: - * - * const { default: run } = await import( - * `${process.env.GITHUB_WORKSPACE}/scripts/pr-review-reminder.mjs` - * ); - * await run({ github, context, core }); - */ - -// Team @mentions only for these slugs. Individuals are filtered using outside-collaborator list (see below). -const SDK_TEAM_SLUGS = new Set([ - 'team-javascript-sdks', - 'team-javascript-sdks-framework', - 'team-javascript-sdks-browser', - 'team-javascript-sdks-server', -]); - -// --------------------------------------------------------------------------- -// Outside collaborators (repo API — works with default GITHUB_TOKEN). -// Org members with access via teams or default permissions are not listed here. -// --------------------------------------------------------------------------- - -async function loadOutsideCollaboratorLogins(github, owner, repo, core) { - try { - const users = await github.paginate(github.rest.repos.listCollaborators, { - owner, - repo, - affiliation: 'outside', - per_page: 100, - }); - return new Set(users.map(u => u.login)); - } catch (e) { - const status = e.response?.status; - core.warning( - `Could not list outside collaborators for ${owner}/${repo} (${status ? `HTTP ${status}` : 'no status'}): ${e.message}. ` + - 'Skipping @mentions for individual reviewers (team reminders unchanged).', - ); - return null; - } -} - -// --------------------------------------------------------------------------- -// Recurring public holidays (month–day in UTC, same date every year). -// A calendar day counts as a holiday if it appears in any country list. -// --------------------------------------------------------------------------- - -const RECURRING_PUBLIC_HOLIDAYS_AT = [ - '01-01', - '01-06', - '05-01', - '08-15', - '10-26', - '11-01', - '12-08', - '12-24', - '12-25', - '12-26', - '12-31', -]; - -const RECURRING_PUBLIC_HOLIDAYS_CA = ['01-01', '07-01', '09-30', '11-11', '12-24', '12-25', '12-26', '12-31']; - -const RECURRING_PUBLIC_HOLIDAYS_US = ['01-01', '06-19', '07-04', '11-11', '12-24', '12-25', '12-26', '12-31']; - -const RECURRING_PUBLIC_HOLIDAY_MM_DD = new Set([ - ...RECURRING_PUBLIC_HOLIDAYS_AT, - ...RECURRING_PUBLIC_HOLIDAYS_CA, - ...RECURRING_PUBLIC_HOLIDAYS_US, -]); - -function monthDayUTC(date) { - const m = String(date.getUTCMonth() + 1).padStart(2, '0'); - const d = String(date.getUTCDate()).padStart(2, '0'); - return `${m}-${d}`; -} - -// --------------------------------------------------------------------------- -// Business-day counter. -// Counts fully-elapsed business days (Mon–Fri, not a public holiday) between -// requestedAt and now. "Fully elapsed" means the day has completely passed, -// so today is not included — giving the reviewer the rest of today to respond. -// -// Example: review requested Friday → elapsed complete days include Sat, Sun, -// Mon, Tue, … The first two business days are Mon and Tue, so the reminder -// fires on Wednesday morning. That gives the reviewer all of Monday and -// Tuesday to respond. -// --------------------------------------------------------------------------- - -function countElapsedBusinessDays(requestedAt, now) { - // Walk from the day after the request up to (but not including) today. - const start = new Date(requestedAt); - start.setUTCHours(0, 0, 0, 0); - start.setUTCDate(start.getUTCDate() + 1); - - const todayUTC = new Date(now); - todayUTC.setUTCHours(0, 0, 0, 0); - - let count = 0; - const cursor = new Date(start); - while (cursor < todayUTC) { - const dow = cursor.getUTCDay(); // 0 = Sun, 6 = Sat - if (dow !== 0 && dow !== 6) { - if (!RECURRING_PUBLIC_HOLIDAY_MM_DD.has(monthDayUTC(cursor))) { - count++; - } - } - cursor.setUTCDate(cursor.getUTCDate() + 1); - } - return count; -} - -// --------------------------------------------------------------------------- -// Reminder marker helpers -// --------------------------------------------------------------------------- - -// Returns a unique HTML comment marker for a reviewer key (login or "team:slug"). -// Used for precise per-reviewer deduplication in existing comments. -function reminderMarker(key) { - return ``; -} - -// --------------------------------------------------------------------------- -// Main entry point -// --------------------------------------------------------------------------- - -export default async function run({ github, context, core }) { - const { owner, repo } = context.repo; - const now = new Date(); - - core.info(`Using ${RECURRING_PUBLIC_HOLIDAY_MM_DD.size} recurring public holiday month–day values (US/CA/AT union)`); - - const outsideCollaboratorLogins = await loadOutsideCollaboratorLogins(github, owner, repo, core); - if (outsideCollaboratorLogins) { - core.info(`Excluding ${outsideCollaboratorLogins.size} outside collaborator login(s) from individual @mentions`); - } - - // --------------------------------------------------------------------------- - // Main loop - // --------------------------------------------------------------------------- - - // Fetch all open PRs - const prs = await github.paginate(github.rest.pulls.list, { - owner, - repo, - state: 'open', - per_page: 100, - }); - - core.info(`Found ${prs.length} open PRs`); - - for (const pr of prs) { - // Skip draft PRs and PRs opened by bots - if (pr.draft) continue; - if (pr.user?.type === 'Bot') continue; - - // Get currently requested reviewers (only those who haven't reviewed yet — - // GitHub automatically removes a reviewer from this list once they submit a review) - const { data: requested } = await github.rest.pulls.listRequestedReviewers({ - owner, - repo, - pull_number: pr.number, - }); - - const pendingReviewers = requested.users; // individual users - const pendingTeams = requested.teams; // team reviewers - if (pendingReviewers.length === 0 && pendingTeams.length === 0) continue; - - const reviews = await github.paginate(github.rest.pulls.listReviews, { - owner, - repo, - pull_number: pr.number, - per_page: 100, - }); - - // Skip if the PR already has at least one approval — no need to nudge remaining reviewers. - const hasApproval = reviews.some(r => r.state === 'APPROVED'); - if (hasApproval) continue; - - // Skip if any reviewer has an *outstanding* "changes requested" review — the ball is then in - // the author's court, not the reviewers', so nagging the reviewers is noise. - // - // A reviewer who is re-requested after requesting changes reappears in listRequestedReviewers - // (i.e. is back in `pendingReviewers`). Their earlier change-request is superseded by that new - // pending request, so it must NOT suppress reminders — otherwise we'd never nudge a reviewer - // the author has explicitly asked to take another look. We therefore ignore change-requests - // from currently-pending reviewers. (Dismissed reviews report state `DISMISSED`, so they no - // longer count either.) - const pendingReviewerLogins = new Set(pendingReviewers.map(u => u.login)); - const hasOutstandingChangesRequested = reviews.some( - r => r.state === 'CHANGES_REQUESTED' && !pendingReviewerLogins.has(r.user?.login), - ); - if (hasOutstandingChangesRequested) continue; - - // Fetch the PR timeline to determine when each review was (last) requested - const timeline = await github.paginate(github.rest.issues.listEventsForTimeline, { - owner, - repo, - issue_number: pr.number, - per_page: 100, - }); - - // Fetch existing comments so we can detect previous reminders - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number: pr.number, - per_page: 100, - }); - - const botComments = comments.filter(c => c.user?.login === 'github-actions[bot]'); - - // Returns the date of the most recent reminder comment that contains the given marker, - // or null if no such comment exists. - function latestReminderDate(key) { - const marker = reminderMarker(key); - const matches = botComments - .filter(c => c.body.includes(marker)) - .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - return matches.length > 0 ? new Date(matches[0].created_at) : null; - } - - // Returns true if a reminder is due for a reviewer/team: - // - The "anchor" is the later of: the review-request date, or the last - // reminder we already posted for this reviewer. This means the - // 2-business-day clock restarts after every reminder (re-nagging), and - // also resets when a new push re-requests the review. - // - A reminder fires when ≥ 2 full business days have elapsed since the anchor. - function needsReminder(requestedAt, key) { - const lastReminded = latestReminderDate(key); - const anchor = lastReminded && lastReminded > requestedAt ? lastReminded : requestedAt; - return countElapsedBusinessDays(anchor, now) >= 2; - } - - // Collect overdue individual reviewers - const toRemind = []; // { key, mention } - - for (const reviewer of pendingReviewers) { - const requestEvents = timeline - .filter(e => e.event === 'review_requested' && e.requested_reviewer?.login === reviewer.login) - .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - - if (requestEvents.length === 0) { - core.warning( - `PR #${pr.number}: pending reviewer @${reviewer.login} has no matching review_requested timeline event; skipping reminder for them.`, - ); - continue; - } - - const requestedAt = new Date(requestEvents[0].created_at); - if (!needsReminder(requestedAt, reviewer.login)) continue; - - if (outsideCollaboratorLogins === null) { - continue; - } - if (outsideCollaboratorLogins.has(reviewer.login)) { - continue; - } - - toRemind.push({ key: reviewer.login, mention: `@${reviewer.login}` }); - } - - // Collect overdue team reviewers - for (const team of pendingTeams) { - if (!SDK_TEAM_SLUGS.has(team.slug)) { - continue; - } - - const requestEvents = timeline - .filter(e => e.event === 'review_requested' && e.requested_team?.slug === team.slug) - .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - - if (requestEvents.length === 0) { - core.warning( - `PR #${pr.number}: pending team reviewer @${owner}/${team.slug} has no matching review_requested timeline event; skipping reminder for them.`, - ); - continue; - } - - const requestedAt = new Date(requestEvents[0].created_at); - const key = `team:${team.slug}`; - if (!needsReminder(requestedAt, key)) continue; - - toRemind.push({ key, mention: `@${owner}/${team.slug}` }); - } - - if (toRemind.length === 0) continue; - - // Build a single comment that includes per-reviewer markers (for precise dedup - // on subsequent runs) and @-mentions all overdue reviewers/teams. - const markers = toRemind.map(({ key }) => reminderMarker(key)).join('\n'); - const mentions = toRemind.map(({ mention }) => mention).join(', '); - const body = `${markers}\n👋 ${mentions} — Please review this PR when you get a chance!`; - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pr.number, - body, - }); - - core.info(`Posted review reminder on PR #${pr.number} for: ${mentions}`); - } -} From a8342a5e46f9f6664475caee36b41ca4f178cc2e Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 23 Oct 2026 13:59:49 +0200 Subject: [PATCH 2/3] chore(ci): remove browser flaky-test detector workflow Co-Authored-By: GPT-6 --- .github/workflows/flaky-test-detector.yml | 71 ----------------------- 1 file changed, 71 deletions(-) delete mode 100644 .github/workflows/flaky-test-detector.yml diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml deleted file mode 100644 index fbde86cf1a0f..000000000000 --- a/.github/workflows/flaky-test-detector.yml +++ /dev/null @@ -1,71 +0,0 @@ -name: 'CI: Detect flaky tests' -on: - workflow_dispatch: - pull_request: - paths: - - 'dev-packages/browser-integration-tests/suites/**/test.ts' - branches-ignore: - - master - -env: - HEAD_COMMIT: ${{ github.event.inputs.commit || github.sha }} - -# Cancel in progress workflows on pull_requests. -# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value -concurrency: - group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} - cancel-in-progress: true - -jobs: - flaky-detector: - runs-on: namespace-profile-sentry-javascript-large - timeout-minutes: 60 - name: 'Check tests for flakiness' - # Also skip if PR is from master -> develop - if: ${{ github.base_ref != 'master' && github.ref != 'refs/heads/master' }} - steps: - - name: Check out current branch - uses: actions/checkout@v7 - - name: Install pnpm - uses: ./.github/actions/install-pnpm - - name: Set up Node - uses: actions/setup-node@v7 - with: - node-version-file: 'package.json' - cache: 'pnpm' - - - name: Install dependencies - run: pnpm install --frozen-lockfile - - - name: Build packages - run: pnpm run build - - - name: Install Playwright - uses: ./.github/actions/install-playwright - with: - browsers: 'chromium' - cwd: dev-packages/browser-integration-tests - - - name: Determine changed tests - uses: dorny/paths-filter@v4.0.3 - id: changed - with: - list-files: json - filters: | - browser_integration: dev-packages/browser-integration-tests/suites/**/test.ts - - - name: Detect flaky tests - id: test - run: pnpm run test:detect-flaky - working-directory: dev-packages/browser-integration-tests - env: - CHANGED_TEST_PATHS: ${{ steps.changed.outputs.browser_integration_files }} - TEST_RUN_COUNT: 'AUTO' - - - name: Upload Playwright Traces - uses: actions/upload-artifact@v7 - if: failure() && steps.test.outcome == 'failure' - with: - name: playwright-test-results - path: dev-packages/browser-integration-tests/test-results - retention-days: 5 From de19748f9eb5f2619af82d3a02d607aaaa698d19 Mon Sep 17 00:00:00 2001 From: Nicolas Hrubec Date: Fri, 23 Oct 2026 14:02:20 +0200 Subject: [PATCH 3/3] chore(ci): keep flaky-test removal in a separate PR Co-Authored-By: GPT-6 --- .github/workflows/flaky-test-detector.yml | 71 +++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 .github/workflows/flaky-test-detector.yml diff --git a/.github/workflows/flaky-test-detector.yml b/.github/workflows/flaky-test-detector.yml new file mode 100644 index 000000000000..fbde86cf1a0f --- /dev/null +++ b/.github/workflows/flaky-test-detector.yml @@ -0,0 +1,71 @@ +name: 'CI: Detect flaky tests' +on: + workflow_dispatch: + pull_request: + paths: + - 'dev-packages/browser-integration-tests/suites/**/test.ts' + branches-ignore: + - master + +env: + HEAD_COMMIT: ${{ github.event.inputs.commit || github.sha }} + +# Cancel in progress workflows on pull_requests. +# https://docs.github.com/en/actions/using-jobs/using-concurrency#example-using-a-fallback-value +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.run_id }} + cancel-in-progress: true + +jobs: + flaky-detector: + runs-on: namespace-profile-sentry-javascript-large + timeout-minutes: 60 + name: 'Check tests for flakiness' + # Also skip if PR is from master -> develop + if: ${{ github.base_ref != 'master' && github.ref != 'refs/heads/master' }} + steps: + - name: Check out current branch + uses: actions/checkout@v7 + - name: Install pnpm + uses: ./.github/actions/install-pnpm + - name: Set up Node + uses: actions/setup-node@v7 + with: + node-version-file: 'package.json' + cache: 'pnpm' + + - name: Install dependencies + run: pnpm install --frozen-lockfile + + - name: Build packages + run: pnpm run build + + - name: Install Playwright + uses: ./.github/actions/install-playwright + with: + browsers: 'chromium' + cwd: dev-packages/browser-integration-tests + + - name: Determine changed tests + uses: dorny/paths-filter@v4.0.3 + id: changed + with: + list-files: json + filters: | + browser_integration: dev-packages/browser-integration-tests/suites/**/test.ts + + - name: Detect flaky tests + id: test + run: pnpm run test:detect-flaky + working-directory: dev-packages/browser-integration-tests + env: + CHANGED_TEST_PATHS: ${{ steps.changed.outputs.browser_integration_files }} + TEST_RUN_COUNT: 'AUTO' + + - name: Upload Playwright Traces + uses: actions/upload-artifact@v7 + if: failure() && steps.test.outcome == 'failure' + with: + name: playwright-test-results + path: dev-packages/browser-integration-tests/test-results + retention-days: 5