Skip to content

Add Open External Contributor PRs and Issues to PyTorch Org Project 136 #134

Add Open External Contributor PRs and Issues to PyTorch Org Project 136

Add Open External Contributor PRs and Issues to PyTorch Org Project 136 #134

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}`);
}