From 890b8f7ea3378685f7d6d9612ff584fc9ed17df0 Mon Sep 17 00:00:00 2001 From: Riccardo Cipolleschi Date: Tue, 8 Apr 2025 02:43:44 -0700 Subject: [PATCH] Automate the rotation of issue trage squad (#50526) Summary: Pull Request resolved: https://github.com/facebook/react-native/pull/50526 This change wants to automate the rotation of the issue triaging squad on Discord. This is taking us a few minutes every week to rotate the oncall. This change can save us some time. The configuration file format is supposed to be like this: ``` { "userMap": { "discord-username1": "discord-id1", "discord-username2": "discord-id2", "discord-username3": "discord-id3", "discord-username4": "discord-id4" }, "schedule": { "date1": ["discord-username1", "discord-username2"], "date2": ["discord-username3", "discord-username4"], } } ``` ## Changelog [Internal] - Automate the issue triage oncall rotation Reviewed By: cortinico Differential Revision: D72569435 fbshipit-source-id: 435c13350cf503e99302775674e78a20e328e68d --- .../__tests__/extractIssueOncalls-test.js | 83 +++++++++++++++++++ .../workflow-scripts/extractIssueOncalls.js | 61 ++++++++++++++ .github/workflows/monitor-new-issues.yml | 23 ++++- 3 files changed, 165 insertions(+), 2 deletions(-) create mode 100644 .github/workflow-scripts/__tests__/extractIssueOncalls-test.js create mode 100644 .github/workflow-scripts/extractIssueOncalls.js diff --git a/.github/workflow-scripts/__tests__/extractIssueOncalls-test.js b/.github/workflow-scripts/__tests__/extractIssueOncalls-test.js new file mode 100644 index 00000000000..7ad91ddc06a --- /dev/null +++ b/.github/workflow-scripts/__tests__/extractIssueOncalls-test.js @@ -0,0 +1,83 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +const {extractIssueOncalls} = require('../extractIssueOncalls'); + +const userMap = { + '@g': '1785', + '@c': '1781', + '@s': '1272', + '@d': '1332', + '@m': '9555', + '@p': '6097', + '@f': '7565', +}; + +const schedule = { + '2025-04-01': ['@m', '@f'], + '2025-04-08': ['@g', '@d'], +}; + +describe('extractIssueOncalls', () => { + beforeEach(() => { + jest.clearAllMocks(); + jest.useFakeTimers('modern'); + }); + + afterEach(() => { + jest.useRealTimers(); + }); + it('extracts m and f on 6 of April', () => { + jest.setSystemTime(new Date(2025, 3, 6)); + const oncalls = extractIssueOncalls(schedule, userMap); + expect(oncalls).toEqual([userMap['@m'], userMap['@f']]); + }); + + it('extracts m and f on 7 of April', () => { + jest.setSystemTime(new Date(2025, 3, 7)); + const oncalls = extractIssueOncalls(schedule, userMap); + expect(oncalls).toEqual([userMap['@m'], userMap['@f']]); + }); + + it('extracts g and d on 8 of April', () => { + jest.setSystemTime(new Date(2025, 3, 8)); + const oncalls = extractIssueOncalls(schedule, userMap); + expect(oncalls).toEqual([userMap['@g'], userMap['@d']]); + }); + + it('extracts g and d on 9 of April', () => { + jest.setSystemTime(new Date(2025, 3, 9)); + const oncalls = extractIssueOncalls(schedule, userMap); + expect(oncalls).toEqual([userMap['@g'], userMap['@d']]); + }); + + it('extracts g and d on 10 of April', () => { + jest.setSystemTime(new Date(2025, 3, 10)); + const oncalls = extractIssueOncalls(schedule, userMap); + expect(oncalls).toEqual([userMap['@g'], userMap['@d']]); + }); + + it('extracts g and d on 11 of April', () => { + jest.setSystemTime(new Date(2025, 3, 11)); + const oncalls = extractIssueOncalls(schedule, userMap); + expect(oncalls).toEqual([userMap['@g'], userMap['@d']]); + }); + + it('extracts g and d on 12 of April', () => { + jest.setSystemTime(new Date(2025, 3, 12)); + const oncalls = extractIssueOncalls(schedule, userMap); + expect(oncalls).toEqual([userMap['@g'], userMap['@d']]); + }); + + it('extracts g and d on 13 of April', () => { + jest.setSystemTime(new Date(2025, 3, 13)); + const oncalls = extractIssueOncalls(schedule, userMap); + expect(oncalls).toEqual([userMap['@g'], userMap['@d']]); + }); +}); diff --git a/.github/workflow-scripts/extractIssueOncalls.js b/.github/workflow-scripts/extractIssueOncalls.js new file mode 100644 index 00000000000..4db01d1f31e --- /dev/null +++ b/.github/workflow-scripts/extractIssueOncalls.js @@ -0,0 +1,61 @@ +/** + * Copyright (c) Meta Platforms, Inc. and affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + */ + +const fs = require('fs'); + +const MSEC_IN_DAY = 1000 * 60 * 60 * 24; + +function formatUsers(users) { + console.log(`${users[0]} ${users[1]}`.trim()); +} + +function extractUsersFromScheduleAndDate(schedule, userMap, date) { + const year = date.getFullYear(); + const month = date.getMonth() + 1; // 0 is January, 1 is February + const day = date.getDate(); + const dateStr = `${year}-${month < 10 ? `0${month}` : month}-${day < 10 ? `0${day}` : day}`; + const user1 = userMap[schedule[dateStr][0]]; + const user2 = userMap[schedule[dateStr][1]]; + return [user1, user2]; +} + +function main() { + const configuration = process.argv[2]; + const {userMap, schedule} = JSON.parse(configuration); + extractIssueOncalls(schedule, userMap); +} + +function extractIssueOncalls(schedule, userMap) { + const now = new Date(); + const dayOfTheWeek = now.getDay(); // 0 is Sunday, 1 is Monday, etc. + let users; + if (dayOfTheWeek === 2) { + // exact match in the schedule + users = extractUsersFromScheduleAndDate(schedule, userMap, now); + } else if (dayOfTheWeek < 2) { + // sunday + // go to the tuesday of the last week + const lastWeekTuesday = new Date(now - (5 + dayOfTheWeek) * MSEC_IN_DAY); + users = extractUsersFromScheduleAndDate(schedule, userMap, lastWeekTuesday); + } else if (dayOfTheWeek > 1) { + // go to the previous tuesday + const thisWeekTuesday = new Date(now - (dayOfTheWeek - 2) * MSEC_IN_DAY); + users = extractUsersFromScheduleAndDate(schedule, userMap, thisWeekTuesday); + } + formatUsers(users); + return users; +} + +if (require.main === module) { + void main(); +} + +module.exports = { + extractIssueOncalls, +}; diff --git a/.github/workflows/monitor-new-issues.yml b/.github/workflows/monitor-new-issues.yml index 632cf62f584..3c50a451f1b 100644 --- a/.github/workflows/monitor-new-issues.yml +++ b/.github/workflows/monitor-new-issues.yml @@ -5,14 +5,33 @@ on: - cron: "0 0,6,12,18 * * *" workflow_dispatch: +# Reminder for when we have to update the schedule (before Jan 2026): +# the secrets.ONCALL_SCHEDULE secret must be on a single line and must have all the `"` escaped as `\"`. +# Only a meta engineer can update it through the OSS internal portal. + jobs: monitor-issues: runs-on: ubuntu-latest steps: + - name: Checkout + uses: actions/checkout@v4 - name: Set up Node.js uses: actions/setup-node@v4 with: node-version: '20' + - name: Install dependencies + uses: ./.github/actions/yarn-install + - name: Extract next oncall + run: | + ONCALLS=$(node ./.github/workflow-scripts/extractIssueOncalls.js "${{ secrets.ONCALL_SCHEDULE }}") + ONCALL1=$(echo $ONCALLS | cut -d ' ' -f 1) + ONCALL2=$(echo $ONCALLS | cut -d ' ' -f 2) + echo "oncall1=$ONCALL1" >> $GITHUB_ENV + echo "oncall2=$ONCALL2" >> $GITHUB_ENV + - name: Print oncalls + run: | + echo "oncall1: ${{ env.oncall1 }}" + echo "oncall2: ${{ env.oncall2 }}" - name: Monitor New Issues uses: react-native-community/repo-monitor@v1.0.1 with: @@ -23,5 +42,5 @@ jobs: repo_owner: "facebook" repo_name: "react-native" discord_webhook_url: "${{ secrets.DISCORD_WEBHOOK_URL }}" - discord_id_type: "role" - discord_ids: "1339243367841927228" + discord_id_type: "user" + discord_ids: "${{ env.oncall1 }},${{ env.oncall2 }}"