From 540747633b40a5bea300ee82960901bff333f08f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 15:07:50 +0000 Subject: [PATCH 1/8] chore: add PR review reminder workflow Agent-Logs-Url: https://github.com/getsentry/sentry-javascript/sessions/279338b3-5e56-4760-9345-e9c86ecc3896 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- .github/workflows/pr-review-reminder.yml | 125 +++++++++++++++++++++++ 1 file changed, 125 insertions(+) create mode 100644 .github/workflows/pr-review-reminder.yml diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml new file mode 100644 index 000000000000..87a9bcdc4c56 --- /dev/null +++ b/.github/workflows/pr-review-reminder.yml @@ -0,0 +1,125 @@ +name: 'PR: Review Reminder' + +on: + workflow_dispatch: + schedule: + # Run every day at 10:00 AM UTC + - cron: '0 10 * * *' + +permissions: + pull-requests: write + issues: write + +concurrency: + group: ${{ github.workflow }} + cancel-in-progress: false + +jobs: + remind-reviewers: + runs-on: ubuntu-latest + steps: + - name: Remind pending reviewers + uses: actions/github-script@v7 + with: + script: | + const { owner, repo } = context.repo; + const now = new Date(); + const THRESHOLD_MS = 48 * 60 * 60 * 1000; // 48 hours + const REMINDER_MARKER = ''; + + // 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 + if (pr.draft) continue; + + // Get currently requested reviewers (only those who haven't reviewed yet) + const { data: requested } = await github.rest.pulls.listRequestedReviewers({ + owner, + repo, + pull_number: pr.number, + }); + + const pendingReviewers = requested.reviewers; + if (pendingReviewers.length === 0) continue; + + // Get timeline events to find when each review was requested + const timeline = await github.paginate(github.rest.issues.listEventsForTimeline, { + owner, + repo, + issue_number: pr.number, + per_page: 100, + }); + + // For each pending reviewer, find the most recent review_requested event + const overdueReviewers = []; + for (const reviewer of pendingReviewers) { + const reviewRequestEvents = 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 (reviewRequestEvents.length === 0) continue; + + const requestedAt = new Date(reviewRequestEvents[0].created_at); + const age = now - requestedAt; + + if (age >= THRESHOLD_MS) { + overdueReviewers.push(reviewer.login); + } + } + + if (overdueReviewers.length === 0) continue; + + // Fetch existing comments to avoid double-nagging the same reviewer + const comments = await github.paginate(github.rest.issues.listComments, { + owner, + repo, + issue_number: pr.number, + per_page: 100, + }); + + const reminderComments = comments.filter( + c => + c.user.login === 'github-actions[bot]' && c.body.includes(REMINDER_MARKER), + ); + + // Only nag reviewers who haven't been reminded yet on this PR + const alreadyReminded = new Set(); + for (const comment of reminderComments) { + for (const login of overdueReviewers) { + if (comment.body.includes(`@${login}`)) { + alreadyReminded.add(login); + } + } + } + + const newOverdueReviewers = overdueReviewers.filter( + login => !alreadyReminded.has(login), + ); + + if (newOverdueReviewers.length === 0) continue; + + const mentions = newOverdueReviewers.map(login => `@${login}`).join(', '); + const body = `${REMINDER_MARKER}\nπŸ‘‹ ${mentions} β€” you were requested to review this PR more than 48 hours ago. Could you please take a look when you get a chance? Thank you!`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr.number, + body, + }); + + core.info(`Posted review reminder on PR #${pr.number} for: ${mentions}`); + } From 994f774f7fb19362fcd6b105b6b2e7d2f2e3d64f Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 9 Apr 2026 17:35:04 +0000 Subject: [PATCH 2/8] improve pr-review-reminder: team reviewers, re-review reset, better dedup Agent-Logs-Url: https://github.com/getsentry/sentry-javascript/sessions/ebddacbf-8329-422a-8199-8b6914cd3645 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- .github/workflows/pr-review-reminder.yml | 116 ++++++++++++++--------- 1 file changed, 73 insertions(+), 43 deletions(-) diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml index 87a9bcdc4c56..23d138a6d595 100644 --- a/.github/workflows/pr-review-reminder.yml +++ b/.github/workflows/pr-review-reminder.yml @@ -25,7 +25,12 @@ jobs: const { owner, repo } = context.repo; const now = new Date(); const THRESHOLD_MS = 48 * 60 * 60 * 1000; // 48 hours - const REMINDER_MARKER = ''; + + // 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 ``; + } // Fetch all open PRs const prs = await github.paginate(github.rest.pulls.list, { @@ -38,20 +43,23 @@ jobs: core.info(`Found ${prs.length} open PRs`); for (const pr of prs) { - // Skip draft 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) + // 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.reviewers; - if (pendingReviewers.length === 0) continue; + const pendingReviewers = requested.reviewers; // individual users + const pendingTeams = requested.teams; // team reviewers + if (pendingReviewers.length === 0 && pendingTeams.length === 0) continue; - // Get timeline events to find when each review was requested + // Fetch the PR timeline to determine when each review was (last) requested const timeline = await github.paginate(github.rest.issues.listEventsForTimeline, { owner, repo, @@ -59,10 +67,42 @@ jobs: per_page: 100, }); - // For each pending reviewer, find the most recent review_requested event - const overdueReviewers = []; + // Fetch existing comments so we can suppress duplicate 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 review was requested more than 48 hours ago, AND + // - No reminder has been posted since the most recent review_requested event + // (this correctly resets the reminder when a new push re-requests the review) + function needsReminder(requestedAt, key) { + if (now - requestedAt < THRESHOLD_MS) return false; + const lastReminded = latestReminderDate(key); + if (lastReminded && lastReminded >= requestedAt) return false; + return true; + } + + // Collect overdue individual reviewers + const toRemind = []; // { key, mention } + for (const reviewer of pendingReviewers) { - const reviewRequestEvents = timeline + const requestEvents = timeline .filter( e => e.event === 'review_requested' && @@ -70,49 +110,39 @@ jobs: ) .sort((a, b) => new Date(b.created_at) - new Date(a.created_at)); - if (reviewRequestEvents.length === 0) continue; + if (requestEvents.length === 0) continue; - const requestedAt = new Date(reviewRequestEvents[0].created_at); - const age = now - requestedAt; + const requestedAt = new Date(requestEvents[0].created_at); + if (!needsReminder(requestedAt, reviewer.login)) continue; - if (age >= THRESHOLD_MS) { - overdueReviewers.push(reviewer.login); - } + toRemind.push({ key: reviewer.login, mention: `@${reviewer.login}` }); } - if (overdueReviewers.length === 0) continue; + // Collect overdue team reviewers + for (const team of pendingTeams) { + 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)); - // Fetch existing comments to avoid double-nagging the same reviewer - const comments = await github.paginate(github.rest.issues.listComments, { - owner, - repo, - issue_number: pr.number, - per_page: 100, - }); + if (requestEvents.length === 0) continue; - const reminderComments = comments.filter( - c => - c.user.login === 'github-actions[bot]' && c.body.includes(REMINDER_MARKER), - ); - - // Only nag reviewers who haven't been reminded yet on this PR - const alreadyReminded = new Set(); - for (const comment of reminderComments) { - for (const login of overdueReviewers) { - if (comment.body.includes(`@${login}`)) { - alreadyReminded.add(login); - } - } - } + const requestedAt = new Date(requestEvents[0].created_at); + const key = `team:${team.slug}`; + if (!needsReminder(requestedAt, key)) continue; - const newOverdueReviewers = overdueReviewers.filter( - login => !alreadyReminded.has(login), - ); + toRemind.push({ key, mention: `@${owner}/${team.slug}` }); + } - if (newOverdueReviewers.length === 0) continue; + if (toRemind.length === 0) continue; - const mentions = newOverdueReviewers.map(login => `@${login}`).join(', '); - const body = `${REMINDER_MARKER}\nπŸ‘‹ ${mentions} β€” you were requested to review this PR more than 48 hours ago. Could you please take a look when you get a chance? Thank you!`; + // 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} β€” you were requested to review this PR more than 48 hours ago. Could you please take a look when you get a chance? Thank you!`; await github.rest.issues.createComment({ owner, From 04055c161d67753f50384f9d34174395018f3eea Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:24:45 +0000 Subject: [PATCH 3/8] chore: business-day threshold + Nager.Date holiday API in review reminder Agent-Logs-Url: https://github.com/getsentry/sentry-javascript/sessions/ed7a9990-d8c7-47ed-b4e4-14c9a50cfac2 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- .github/workflows/pr-review-reminder.yml | 97 ++++++++++++++++++++++-- 1 file changed, 91 insertions(+), 6 deletions(-) diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml index 23d138a6d595..b457fa89b948 100644 --- a/.github/workflows/pr-review-reminder.yml +++ b/.github/workflows/pr-review-reminder.yml @@ -3,8 +3,9 @@ name: 'PR: Review Reminder' on: workflow_dispatch: schedule: - # Run every day at 10:00 AM UTC - - cron: '0 10 * * *' + # 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' permissions: pull-requests: write @@ -24,7 +25,87 @@ jobs: script: | const { owner, repo } = context.repo; const now = new Date(); - const THRESHOLD_MS = 48 * 60 * 60 * 1000; // 48 hours + + // --------------------------------------------------------------------------- + // Public holidays (US, Canada, Austria) via Nager.Date β€” free, no API key. + // See https://date.nager.at/ for documentation and supported countries. + // We fetch the current year and the previous year so that reviews requested + // in late December are handled correctly when the workflow runs in January. + // If the API is unreachable we fall back to weekday-only checking and warn. + // --------------------------------------------------------------------------- + const COUNTRY_CODES = ['US', 'CA', 'AT']; + + async function fetchHolidaysForYear(year) { + const dates = new Set(); + for (const cc of COUNTRY_CODES) { + try { + const resp = await fetch( + `https://date.nager.at/api/v3/PublicHolidays/${year}/${cc}`, + ); + if (!resp.ok) { + core.warning(`Nager.Date returned ${resp.status} for ${cc}/${year}`); + continue; + } + const holidays = await resp.json(); + for (const h of holidays) { + dates.add(h.date); // 'YYYY-MM-DD' + } + } catch (e) { + core.warning(`Failed to fetch holidays for ${cc}/${year}: ${e.message}`); + } + } + return dates; + } + + const currentYear = now.getUTCFullYear(); + const [currentYearHolidays, previousYearHolidays] = await Promise.all([ + fetchHolidaysForYear(currentYear), + fetchHolidaysForYear(currentYear - 1), + ]); + const publicHolidays = new Set([...currentYearHolidays, ...previousYearHolidays]); + + core.info( + `Loaded ${publicHolidays.size} public holiday dates for ${currentYear - 1}–${currentYear}`, + ); + + // --------------------------------------------------------------------------- + // 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) { + // 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) { + const dateStr = cursor.toISOString().slice(0, 10); + if (!publicHolidays.has(dateStr)) { + 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. @@ -32,6 +113,10 @@ jobs: return ``; } + // --------------------------------------------------------------------------- + // Main loop + // --------------------------------------------------------------------------- + // Fetch all open PRs const prs = await github.paginate(github.rest.pulls.list, { owner, @@ -88,11 +173,11 @@ jobs: } // Returns true if a reminder is due for a reviewer/team: - // - The review was requested more than 48 hours ago, AND + // - At least 2 business days have fully elapsed since the review was requested, AND // - No reminder has been posted since the most recent review_requested event // (this correctly resets the reminder when a new push re-requests the review) function needsReminder(requestedAt, key) { - if (now - requestedAt < THRESHOLD_MS) return false; + if (countElapsedBusinessDays(requestedAt) < 2) return false; const lastReminded = latestReminderDate(key); if (lastReminded && lastReminded >= requestedAt) return false; return true; @@ -142,7 +227,7 @@ jobs: // 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} β€” you were requested to review this PR more than 48 hours ago. Could you please take a look when you get a chance? Thank you!`; + const body = `${markers}\nπŸ‘‹ ${mentions} β€” you were requested to review this PR more than 2 business days ago. Could you please take a look when you get a chance? Thank you!`; await github.rest.issues.createComment({ owner, From 1bae9fe3a98fc54fcb67d7b7803a3878d3659b63 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 07:34:10 +0000 Subject: [PATCH 4/8] chore: add re-nagging every 2 business days to PR review reminder Agent-Logs-Url: https://github.com/getsentry/sentry-javascript/sessions/20ea6b6f-d865-4d7b-aff6-3a6f432844d2 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- .github/workflows/pr-review-reminder.yml | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml index b457fa89b948..9f21bc2e71d9 100644 --- a/.github/workflows/pr-review-reminder.yml +++ b/.github/workflows/pr-review-reminder.yml @@ -173,14 +173,16 @@ jobs: } // Returns true if a reminder is due for a reviewer/team: - // - At least 2 business days have fully elapsed since the review was requested, AND - // - No reminder has been posted since the most recent review_requested event - // (this correctly resets the reminder when a new push re-requests the review) + // - 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) { - if (countElapsedBusinessDays(requestedAt) < 2) return false; const lastReminded = latestReminderDate(key); - if (lastReminded && lastReminded >= requestedAt) return false; - return true; + const anchor = + lastReminded && lastReminded > requestedAt ? lastReminded : requestedAt; + return countElapsedBusinessDays(anchor) >= 2; } // Collect overdue individual reviewers @@ -227,7 +229,7 @@ jobs: // 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} β€” you were requested to review this PR more than 2 business days ago. Could you please take a look when you get a chance? Thank you!`; + const body = `${markers}\nπŸ‘‹ ${mentions} β€” a friendly reminder that your review on this PR is still pending. Could you please take a look when you get a chance? Thank you!`; await github.rest.issues.createComment({ owner, From b08add0a5b170ce52bd583b2bf0dc025024a55b8 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 09:07:10 +0000 Subject: [PATCH 5/8] refactor: extract review reminder script to scripts/pr-review-reminder.mjs Agent-Logs-Url: https://github.com/getsentry/sentry-javascript/sessions/0e6a9f7f-93f3-4278-8456-15bfc8715e39 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- .github/workflows/pr-review-reminder.yml | 222 +-------------------- scripts/pr-review-reminder.mjs | 234 +++++++++++++++++++++++ 2 files changed, 240 insertions(+), 216 deletions(-) create mode 100644 scripts/pr-review-reminder.mjs diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml index 9f21bc2e71d9..3e99ad563bd5 100644 --- a/.github/workflows/pr-review-reminder.yml +++ b/.github/workflows/pr-review-reminder.yml @@ -19,224 +19,14 @@ jobs: remind-reviewers: runs-on: ubuntu-latest steps: + - name: Checkout repository + uses: actions/checkout@v4 + - name: Remind pending reviewers uses: actions/github-script@v7 with: script: | - const { owner, repo } = context.repo; - const now = new Date(); - - // --------------------------------------------------------------------------- - // Public holidays (US, Canada, Austria) via Nager.Date β€” free, no API key. - // See https://date.nager.at/ for documentation and supported countries. - // We fetch the current year and the previous year so that reviews requested - // in late December are handled correctly when the workflow runs in January. - // If the API is unreachable we fall back to weekday-only checking and warn. - // --------------------------------------------------------------------------- - const COUNTRY_CODES = ['US', 'CA', 'AT']; - - async function fetchHolidaysForYear(year) { - const dates = new Set(); - for (const cc of COUNTRY_CODES) { - try { - const resp = await fetch( - `https://date.nager.at/api/v3/PublicHolidays/${year}/${cc}`, - ); - if (!resp.ok) { - core.warning(`Nager.Date returned ${resp.status} for ${cc}/${year}`); - continue; - } - const holidays = await resp.json(); - for (const h of holidays) { - dates.add(h.date); // 'YYYY-MM-DD' - } - } catch (e) { - core.warning(`Failed to fetch holidays for ${cc}/${year}: ${e.message}`); - } - } - return dates; - } - - const currentYear = now.getUTCFullYear(); - const [currentYearHolidays, previousYearHolidays] = await Promise.all([ - fetchHolidaysForYear(currentYear), - fetchHolidaysForYear(currentYear - 1), - ]); - const publicHolidays = new Set([...currentYearHolidays, ...previousYearHolidays]); - - core.info( - `Loaded ${publicHolidays.size} public holiday dates for ${currentYear - 1}–${currentYear}`, + const { default: run } = await import( + `${process.env.GITHUB_WORKSPACE}/scripts/pr-review-reminder.mjs` ); - - // --------------------------------------------------------------------------- - // 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) { - // 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) { - const dateStr = cursor.toISOString().slice(0, 10); - if (!publicHolidays.has(dateStr)) { - 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 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.reviewers; // individual users - const pendingTeams = requested.teams; // team reviewers - if (pendingReviewers.length === 0 && pendingTeams.length === 0) 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 suppress duplicate 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) >= 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) continue; - - const requestedAt = new Date(requestEvents[0].created_at); - if (!needsReminder(requestedAt, reviewer.login)) continue; - - toRemind.push({ key: reviewer.login, mention: `@${reviewer.login}` }); - } - - // Collect overdue team reviewers - for (const team of pendingTeams) { - 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) 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} β€” a friendly reminder that your review on this PR is still pending. Could you please take a look when you get a chance? Thank you!`; - - await github.rest.issues.createComment({ - owner, - repo, - issue_number: pr.number, - body, - }); - - core.info(`Posted review reminder on PR #${pr.number} for: ${mentions}`); - } + await run({ github, context, core }); diff --git a/scripts/pr-review-reminder.mjs b/scripts/pr-review-reminder.mjs new file mode 100644 index 000000000000..315485d90d2c --- /dev/null +++ b/scripts/pr-review-reminder.mjs @@ -0,0 +1,234 @@ +/** + * 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). + * + * Business days exclude weekends and public holidays for US, CA, and AT + * (fetched at runtime from the Nager.Date API). + * + * 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 }); + */ + +// --------------------------------------------------------------------------- +// Public holidays (US, Canada, Austria) via Nager.Date β€” free, no API key. +// See https://date.nager.at/ for documentation and supported countries. +// We fetch the current year and the previous year so that reviews requested +// in late December are handled correctly when the workflow runs in January. +// If the API is unreachable we fall back to weekday-only checking and warn. +// --------------------------------------------------------------------------- + +const COUNTRY_CODES = ['US', 'CA', 'AT']; + +async function fetchHolidaysForYear(year, core) { + const dates = new Set(); + for (const cc of COUNTRY_CODES) { + try { + const resp = await fetch(`https://date.nager.at/api/v3/PublicHolidays/${year}/${cc}`); + if (!resp.ok) { + core.warning(`Nager.Date returned ${resp.status} for ${cc}/${year}`); + continue; + } + const holidays = await resp.json(); + for (const h of holidays) { + dates.add(h.date); // 'YYYY-MM-DD' + } + } catch (e) { + core.warning(`Failed to fetch holidays for ${cc}/${year}: ${e.message}`); + } + } + return dates; +} + +// --------------------------------------------------------------------------- +// 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, publicHolidays) { + // 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) { + const dateStr = cursor.toISOString().slice(0, 10); + if (!publicHolidays.has(dateStr)) { + 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(); + + // Fetch public holidays + const currentYear = now.getUTCFullYear(); + const [currentYearHolidays, previousYearHolidays] = await Promise.all([ + fetchHolidaysForYear(currentYear, core), + fetchHolidaysForYear(currentYear - 1, core), + ]); + const publicHolidays = new Set([...currentYearHolidays, ...previousYearHolidays]); + + core.info(`Loaded ${publicHolidays.size} public holiday dates for ${currentYear - 1}–${currentYear}`); + + // --------------------------------------------------------------------------- + // 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.reviewers; // individual users + const pendingTeams = requested.teams; // team reviewers + if (pendingReviewers.length === 0 && pendingTeams.length === 0) 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, publicHolidays) >= 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) continue; + + const requestedAt = new Date(requestEvents[0].created_at); + if (!needsReminder(requestedAt, reviewer.login)) continue; + + toRemind.push({ key: reviewer.login, mention: `@${reviewer.login}` }); + } + + // Collect overdue team reviewers + for (const team of pendingTeams) { + 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) 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} β€” a friendly reminder that your review on this PR is still pending. Could you please take a look when you get a chance? Thank you!`; + + await github.rest.issues.createComment({ + owner, + repo, + issue_number: pr.number, + body, + }); + + core.info(`Posted review reminder on PR #${pr.number} for: ${mentions}`); + } +} From 91d8c62e8df104f77386cd2289d43fe64eac1f58 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 10 Apr 2026 11:46:39 +0200 Subject: [PATCH 6/8] ci: Harden PR review reminder workflow Filter individual @mentions via outside-collaborator list (no read:org PAT). Tighten GITHUB_TOKEN permissions; skip workflow_dispatch on forks; warn on timeline mismatches. Co-Authored-By: Claude Made-with: Cursor --- .github/workflows/pr-review-reminder.yml | 9 +++- scripts/pr-review-reminder.mjs | 68 +++++++++++++++++++++--- 2 files changed, 70 insertions(+), 7 deletions(-) diff --git a/.github/workflows/pr-review-reminder.yml b/.github/workflows/pr-review-reminder.yml index 3e99ad563bd5..3eda72221948 100644 --- a/.github/workflows/pr-review-reminder.yml +++ b/.github/workflows/pr-review-reminder.yml @@ -7,9 +7,14 @@ on: # Saturday/Sunday are never counted as business days. - cron: '0 10 * * 1-5' +# pulls.* list + listRequestedReviewers β†’ pull-requests: read +# issues timeline + comments + createComment β†’ issues: write +# repos.listCollaborators (outside) β†’ Metadata read on the token (see GitHub App permission map) +# checkout β†’ contents: read permissions: - pull-requests: write + contents: read issues: write + pull-requests: read concurrency: group: ${{ github.workflow }} @@ -17,6 +22,8 @@ concurrency: 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: ubuntu-latest steps: - name: Checkout repository diff --git a/scripts/pr-review-reminder.mjs b/scripts/pr-review-reminder.mjs index 315485d90d2c..8d1226266719 100644 --- a/scripts/pr-review-reminder.mjs +++ b/scripts/pr-review-reminder.mjs @@ -5,6 +5,11 @@ * responded within 2 business days. Re-nags every 2 business days thereafter * until the review is submitted (or the request is removed). * + * @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 the org team `team-javascript-sdks` (by slug). + * * Business days exclude weekends and public holidays for US, CA, and AT * (fetched at runtime from the Nager.Date API). * @@ -16,6 +21,33 @@ * await run({ github, context, core }); */ +// Team @mentions only for this slug. Individuals are filtered using outside-collaborator list (see below). +const SDK_TEAM_SLUG = 'team-javascript-sdks'; + +// --------------------------------------------------------------------------- +// 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; + } +} + // --------------------------------------------------------------------------- // Public holidays (US, Canada, Austria) via Nager.Date β€” free, no API key. // See https://date.nager.at/ for documentation and supported countries. @@ -110,6 +142,11 @@ export default async function run({ github, context, core }) { core.info(`Loaded ${publicHolidays.size} public holiday dates for ${currentYear - 1}–${currentYear}`); + const outsideCollaboratorLogins = await loadOutsideCollaboratorLogins(github, owner, repo, core); + if (outsideCollaboratorLogins) { + core.info(`Excluding ${outsideCollaboratorLogins.size} outside collaborator login(s) from individual @mentions`); + } + // --------------------------------------------------------------------------- // Main loop // --------------------------------------------------------------------------- @@ -186,26 +223,45 @@ export default async function run({ github, context, core }) { for (const reviewer of pendingReviewers) { const requestEvents = timeline - .filter( - e => e.event === 'review_requested' && e.requested_reviewer?.login === reviewer.login, - ) + .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) continue; + 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 (team.slug !== SDK_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) continue; + 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}`; @@ -220,7 +276,7 @@ export default async function run({ github, context, core }) { // 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} β€” a friendly reminder that your review on this PR is still pending. Could you please take a look when you get a chance? Thank you!`; + const body = `${markers}\nπŸ‘‹ ${mentions} β€” Please review this PR when you get a chance!`; await github.rest.issues.createComment({ owner, From 2d6c239c68c58c759f8c66481921fb858cea7834 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Fri, 10 Apr 2026 10:17:53 +0000 Subject: [PATCH 7/8] fix: use correct `users` property from listRequestedReviewers API response Agent-Logs-Url: https://github.com/getsentry/sentry-javascript/sessions/6863c3f4-212c-4c5c-9fad-7b4b27e47a52 Co-authored-by: Lms24 <8420481+Lms24@users.noreply.github.com> --- scripts/pr-review-reminder.mjs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/pr-review-reminder.mjs b/scripts/pr-review-reminder.mjs index 8d1226266719..5e264e236598 100644 --- a/scripts/pr-review-reminder.mjs +++ b/scripts/pr-review-reminder.mjs @@ -174,7 +174,7 @@ export default async function run({ github, context, core }) { pull_number: pr.number, }); - const pendingReviewers = requested.reviewers; // individual users + const pendingReviewers = requested.users; // individual users const pendingTeams = requested.teams; // team reviewers if (pendingReviewers.length === 0 && pendingTeams.length === 0) continue; From 91524ed3c9c3e4b4bdd223533bb72e13a88ff987 Mon Sep 17 00:00:00 2001 From: Lukas Stracke Date: Fri, 10 Apr 2026 17:21:56 +0200 Subject: [PATCH 8/8] replace holiday API call with static list --- scripts/pr-review-reminder.mjs | 76 ++++++++++++++++------------------ 1 file changed, 36 insertions(+), 40 deletions(-) diff --git a/scripts/pr-review-reminder.mjs b/scripts/pr-review-reminder.mjs index 5e264e236598..535f2d430331 100644 --- a/scripts/pr-review-reminder.mjs +++ b/scripts/pr-review-reminder.mjs @@ -10,8 +10,8 @@ * on this repo (via `repos.listCollaborators` with `affiliation: outside` β€” repo-scoped, no extra token). * - Team reviewers: only the org team `team-javascript-sdks` (by slug). * - * Business days exclude weekends and public holidays for US, CA, and AT - * (fetched at runtime from the Nager.Date API). + * 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: * @@ -49,33 +49,38 @@ async function loadOutsideCollaboratorLogins(github, owner, repo, core) { } // --------------------------------------------------------------------------- -// Public holidays (US, Canada, Austria) via Nager.Date β€” free, no API key. -// See https://date.nager.at/ for documentation and supported countries. -// We fetch the current year and the previous year so that reviews requested -// in late December are handled correctly when the workflow runs in January. -// If the API is unreachable we fall back to weekday-only checking and warn. +// 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 COUNTRY_CODES = ['US', 'CA', 'AT']; - -async function fetchHolidaysForYear(year, core) { - const dates = new Set(); - for (const cc of COUNTRY_CODES) { - try { - const resp = await fetch(`https://date.nager.at/api/v3/PublicHolidays/${year}/${cc}`); - if (!resp.ok) { - core.warning(`Nager.Date returned ${resp.status} for ${cc}/${year}`); - continue; - } - const holidays = await resp.json(); - for (const h of holidays) { - dates.add(h.date); // 'YYYY-MM-DD' - } - } catch (e) { - core.warning(`Failed to fetch holidays for ${cc}/${year}: ${e.message}`); - } - } - return dates; +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}`; } // --------------------------------------------------------------------------- @@ -90,7 +95,7 @@ async function fetchHolidaysForYear(year, core) { // Tuesday to respond. // --------------------------------------------------------------------------- -function countElapsedBusinessDays(requestedAt, now, publicHolidays) { +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); @@ -104,8 +109,7 @@ function countElapsedBusinessDays(requestedAt, now, publicHolidays) { while (cursor < todayUTC) { const dow = cursor.getUTCDay(); // 0 = Sun, 6 = Sat if (dow !== 0 && dow !== 6) { - const dateStr = cursor.toISOString().slice(0, 10); - if (!publicHolidays.has(dateStr)) { + if (!RECURRING_PUBLIC_HOLIDAY_MM_DD.has(monthDayUTC(cursor))) { count++; } } @@ -132,15 +136,7 @@ export default async function run({ github, context, core }) { const { owner, repo } = context.repo; const now = new Date(); - // Fetch public holidays - const currentYear = now.getUTCFullYear(); - const [currentYearHolidays, previousYearHolidays] = await Promise.all([ - fetchHolidaysForYear(currentYear, core), - fetchHolidaysForYear(currentYear - 1, core), - ]); - const publicHolidays = new Set([...currentYearHolidays, ...previousYearHolidays]); - - core.info(`Loaded ${publicHolidays.size} public holiday dates for ${currentYear - 1}–${currentYear}`); + 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) { @@ -215,7 +211,7 @@ export default async function run({ github, context, core }) { function needsReminder(requestedAt, key) { const lastReminded = latestReminderDate(key); const anchor = lastReminded && lastReminded > requestedAt ? lastReminded : requestedAt; - return countElapsedBusinessDays(anchor, now, publicHolidays) >= 2; + return countElapsedBusinessDays(anchor, now) >= 2; } // Collect overdue individual reviewers