Add Open External Contributor PRs and Issues to PyTorch Org Project 136 #134
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Add Open External Contributor PRs and Issues to PyTorch Org Project 136 | |
| on: | |
| workflow_dispatch: | |
| schedule: | |
| # GitHub Actions cron uses UTC. These run at: | |
| # - 14:00 UTC -> 08:00 CST (UTC-6) | |
| # - 19:00 UTC -> 13:00 CST (UTC-6) | |
| - cron: "0 14 * * *" | |
| - cron: "0 19 * * *" | |
| pull_request: | |
| paths: | |
| - .github/workflows/add-unanswered-to-project.yml | |
| jobs: | |
| add_to_project: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Add open issues and open, non-draft PRs to org project (excluding certain authors and bots) | |
| uses: actions/github-script@v7 | |
| with: | |
| github-token: ${{ secrets.ET_EXT_CONTRIB }} | |
| script: | | |
| const projectId = "PVT_kwDOAUB9vs4A_PUL"; // PyTorch org project 136 | |
| const owner = 'pytorch'; | |
| const repo = 'executorch'; | |
| // List of authors to exclude | |
| const excludedAuthors = new Set([ | |
| "nil-is-all", "tanvirislam-meta", "cbilgin", "kimishpatel", "psiddh", "digantdesai", "SS-JIA", "ahmtox", "mcr229", | |
| "shoumikhin", "manuelcandales", "metascroy", "cccclai", "rohansjoshi", "kirklandsign", "abhinaykukkadapu", | |
| "JacobSzwejbka", "Conarnar", "rascani", "lucylq", "larryliu0820", "BujSet", "Gasoonjia", "Juntian777", "guangy10", | |
| "jackzhxng", "GregoryComer", "leafs1", "swolchok", "mergennachin", "tarun292", "byjlw", "jathu", "Jack-Khuu", "georgehong", | |
| "zhenyan-zhang-meta", "silverguo", "harishs88ss", "AlannaBurke", "dbort", "huydhn", "mcremon-meta", "trivedivivek", | |
| "angelayi", "helunwencser", "hsharma35", "zhxchen17", "iseeyuan", "svekars", "nathanaelsee", "dulinriley", | |
| "jerryzh168", "cmodi-meta", "bigfootjon", "sxu", "ydwu4", "Riandy", "tugsbayasgalan", "bsoyluoglu", "yangw-dev", | |
| "YIWENX14", "namanahuja", "yushangdi", "limintang", "pianpwk", "viveknayakatmeta", "andreanicastro", "JakeStevens", | |
| "gmagogsfm", "zonglinpeng", "eigen-k", "derekxu", "salilsdesai", "skrtskrtfb", "pssrawat", "r-barnes", | |
| "kalpit-meta-1", "Will-MingLun-Li", "KapJI", "piyengar", "j-bahr", "BoyuanFeng", "fgasperij", "DariusHolmgren", | |
| "sammarden-meta", "kushrast", "meta-emilian", "Rittzz", "jeanschmidt", "copyrightly", "mikekgfb", "vmpuri", | |
| "zonglinpengmeta", "maggiemoss", "aorenste", "hoangminhle98", "Solumin", "meyering", "rchen152", "AishwaryaSivaraman", | |
| "migeed-z", "ebgraham", "Esteb37", "nausicaasnow", "Camyll", "ezyang", "huiyujie", "dltn", "cjhopman", "blackm00n", | |
| "agunapal", "SamGondelman", "Ninja91", "ivayloen", "DrJessop", "rodrigos01meta", "akrieger", "cmt0", "yiming0416", | |
| "ethansfng", "ThomasJannaud", "nirvanagth", "marcinkwiatkowski", "3l1", "omerjerk", "nitish2112", "yipjustin", | |
| "ejnguyen", "andrewor14", "phaiting", "mgiordy", "LeeOHzzZ", "adicatana", "Polyomino", "ezrilow", "navsud", | |
| "michaelmaitland", "RahulC7", "seyeong-han", "thdusdl1219", "jaejunku", "felixweilbach", "apullin", "trviv", "junluan01", | |
| "mvartani-meta", "abeakkas", "elpdumont", "corporateshark", "bdemirb", "GeorgeTzoupis", "AdithyaReddy9", "drinkmorewaterr", | |
| "YifanShenSZ", "RdoubleA", "Olivia-liu", "Abhi-hpp", "Vysarat","azad-meta", "junpi", "pytorchbot", "pytorchmergebot", "pytorchupdatebot", | |
| "facebook-github-bot", "app/dependabot", "Erik-Lundell", "zingo", "AdrianLundell", "oscarandersson8218", "per", | |
| "Sebastian-Larsson", "SaoirseARM", "robell", "mansnils", "martinlsm", "freddan80", "YufengShi-dudu", "tom-arm", "perheld", | |
| "Jerry-Ge", "gggekov", "fumchin", "wwwind", "benkli01", "Tessil", "maddun01", "Michiel-Olieslagers", "armwaheed", "agrima1304", | |
| "emmakujala", "annietllnd", "MatthiasHertel80", "AlexTawseArm", "jmahbs", "morgolock", "Christoffer-JL", "ArmRyan", "xingguo01", | |
| "tgonzalezorlandoarm", "chizkiyahu", "sarah-blades", "itsMarco-G", "usamahz", "haowhsu-quic", "shewu-quic", "winskuo-quic", | |
| "chunit-quic", "DannyYuyang-quic", "chuntl", "thchenqti", "jethroqti", "chenweng-quic", "qti-horodnic", "qti-mmadhava", "quic-boyuc", | |
| "cymbalrush", "DenisVieriu97", "billmguo", "StrycekSimon", "jirioc", "robert-kalmar", "skywall", "MartinPavella", "roman-janik-nxp", | |
| "novak-vaclav", "neuropilot-captain", "dijopaul", "cad-rlc", "cad-audio", "ynimmaga", "daniil-lyakhov", "emmanuel-ferdman", | |
| "cavusmustafa", "anzr299", "suryasidd", "Jiseong-oh", "alexdean08", | |
| // explicitly include the dependabot bot login seen in PRs | |
| "dependabot[bot]" | |
| ]); | |
| // List of organization logins (lowercased) to exclude members of | |
| const excludedOrgs = new Set([ | |
| "meta", "facebook", "pytorch", "arm", "apple", "qualcomm", "nxp", "mediatek", "cadence", "intel", "samsung", | |
| "@meta", "@facebook", "@pytorch", "@arm", "@apple", "@qualcomm", "@nxp", "@mediatek", "@cadence", "@intel", "@samsung" | |
| ]); | |
| // Labels on PRs to exclude from being added to the project | |
| const excludedPrLabels = new Set(["fb-exported", "meta-exported"]); | |
| // Simple cache for user -> boolean (member of excluded org) | |
| const orgsCache = new Map(); | |
| const companyCache = new Map(); | |
| async function isMemberOfExcludedOrg(user) { | |
| if (!user || !user.login) return false; | |
| const login = user.login; | |
| if (orgsCache.has(login)) return orgsCache.get(login); | |
| try { | |
| const response = await github.rest.orgs.listForUser({ username: login }); | |
| const orgs = response && response.data ? response.data : []; | |
| const isMember = orgs.some(o => o && o.login && excludedOrgs.has(o.login.toLowerCase())); | |
| orgsCache.set(login, isMember); | |
| return isMember; | |
| } catch (error) { | |
| // If checking orgs fails (rate limit, permissions etc.), assume not a member to avoid false positives. | |
| console.log(`Error checking orgs for ${login}: ${error.message}`); | |
| orgsCache.set(login, false); | |
| return false; | |
| } | |
| } | |
| function isBotOrExcluded(user) { | |
| if (!user) return false; | |
| // GitHub sometimes marks bots with user.type === "Bot" | |
| if (user.type && user.type.toLowerCase() === "bot") return true; | |
| // Some bots use logins that end with [bot], e.g. dependabot[bot] | |
| if (user.login && user.login.endsWith("[bot]")) return true; | |
| // Explicit excluded list | |
| if (excludedAuthors.has(user.login)) return true; | |
| return false; | |
| } | |
| function hasExcludedLabel(item) { | |
| if (!item || !item.labels) return false; | |
| return item.labels.some(l => l && l.name && excludedPrLabels.has(l.name.toLowerCase())); | |
| } | |
| async function isMemberOfExcludedCompany(user) { | |
| if (!user || !user.login) return false; | |
| const login = user.login; | |
| if (companyCache.has(login)) return companyCache.get(login); | |
| try { | |
| const response = await github.rest.users.getByUsername({ username: login }); | |
| const company = response && response.data && response.data.company ? response.data.company : ""; | |
| const isMember = company && excludedOrgs.has(company.toLowerCase()); | |
| companyCache.set(login, isMember); | |
| return isMember; | |
| } catch (error) { | |
| // If checking company fails (rate limit, permissions etc.), assume not a member to avoid false positives. | |
| console.log(`Error checking company for ${login}: ${error.message}`); | |
| companyCache.set(login, false); | |
| return false; | |
| } | |
| } | |
| async function addItem(contentId, type, number) { | |
| try { | |
| await github.graphql(` | |
| mutation { | |
| addProjectV2ItemById(input: {projectId: "${projectId}", contentId: "${contentId}"}) { | |
| item { id } | |
| } | |
| } | |
| `); | |
| console.log(`Added ${type} #${number} to project`); | |
| } catch (error) { | |
| if (error.message && error.message.includes("A project item already exists for this content")) { | |
| // Ignore if already exists | |
| console.log(`${type} #${number} already in project`); | |
| } else { | |
| console.log(`Error adding ${type} #${number}: ${error.message}`); | |
| } | |
| } | |
| } | |
| try { | |
| // Add open issues (not PRs) excluding by author/org/bots | |
| const issues = await github.paginate( | |
| github.rest.issues.listForRepo, | |
| { | |
| owner, | |
| repo, | |
| state: 'open', | |
| filter: 'all' | |
| } | |
| ); | |
| for (const issue of issues) { | |
| if (issue.pull_request) { | |
| console.log(`Skipping PR #${issue.number} (listed in issues)`); | |
| continue; | |
| } | |
| if (isBotOrExcluded(issue.user)) { | |
| console.log(`Skipping issue #${issue.number} by ${issue.user && issue.user.login} (excluded author/bot)`); | |
| continue; | |
| } | |
| if (await isMemberOfExcludedOrg(issue.user)) { | |
| console.log(`Skipping issue #${issue.number} by ${issue.user && issue.user.login} (member of excluded org)`); | |
| continue; | |
| } | |
| if (await isMemberOfExcludedCompany(issue.user)) { | |
| console.log(`Skipping issue #${issue.number} by ${issue.user && issue.user.login} (member of excluded org)`); | |
| continue; | |
| } | |
| await addItem(issue.node_id, 'issue', issue.number); | |
| } | |
| // Add open, non-draft PRs (regardless of review state), excluding by author/org/bots/labels | |
| const prs = await github.paginate( | |
| github.rest.pulls.list, | |
| { | |
| owner, | |
| repo, | |
| state: 'open', | |
| } | |
| ); | |
| for (const pr of prs) { | |
| if (pr.draft) { | |
| console.log(`Skipping PR #${pr.number} (draft)`); | |
| continue; | |
| } | |
| if (hasExcludedLabel(pr)) { | |
| console.log(`Skipping PR #${pr.number} (has excluded label)`); | |
| continue; | |
| } | |
| if (isBotOrExcluded(pr.user)) { | |
| console.log(`Skipping PR #${pr.number} by ${pr.user && pr.user.login} (excluded author/bot)`); | |
| continue; | |
| } | |
| if (await isMemberOfExcludedOrg(pr.user)) { | |
| console.log(`Skipping PR #${pr.number} by ${pr.user && pr.user.login} (member of excluded org)`); | |
| continue; | |
| } | |
| if (await isMemberOfExcludedCompany(pr.user)) { | |
| console.log(`Skipping PR #${pr.number} by ${pr.user && pr.user.login} (member of excluded org)`); | |
| continue; | |
| } | |
| await addItem(pr.node_id, 'pr', pr.number); | |
| } | |
| } catch (error) { | |
| core.setFailed(`Workflow failed: ${error.message}`); | |
| } |