diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md index c4a6046e5dca..55b148b0c10e 100644 --- a/.github/PULL_REQUEST_TEMPLATE.md +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -26,6 +26,7 @@ Fixes # (issue) [documentation guidelines](https://github.com/huggingface/diffusers/tree/main/docs), and [here are tips on formatting docstrings](https://github.com/huggingface/diffusers/tree/main/docs#writing-source-documentation). - [ ] Did you write any new necessary tests? +- [ ] Are you the author (or part of the team) of the model/pipeline (only applicable for model/pipeline related PRs)? ## Who can review? diff --git a/.github/workflows/pr_link_issue_reminder.yml b/.github/workflows/pr_link_issue_reminder.yml index 13d35f3ed2ef..b2de62f1b890 100644 --- a/.github/workflows/pr_link_issue_reminder.yml +++ b/.github/workflows/pr_link_issue_reminder.yml @@ -6,8 +6,10 @@ on: workflow_dispatch: jobs: - remind_or_close: - name: Remind or close PRs without a linked issue + remind: + # Reminds external contributors to link an issue. PRs from maintainers, users + # with write/admin access, and collaborators are skipped by the script. + name: Remind external contributors to link an issue if: github.repository == 'huggingface/diffusers' runs-on: ubuntu-22.04 permissions: diff --git a/utils/remind_link_issue.py b/utils/remind_link_issue.py index d4e6aa9d50c1..97ce8a4374b2 100644 --- a/utils/remind_link_issue.py +++ b/utils/remind_link_issue.py @@ -21,10 +21,13 @@ - If a PR is not linked and no prior reminder is present, the script posts a single friendly reminder comment. - PRs labeled `no-issue-needed` and bot-authored PRs are skipped. +- PRs authored by maintainers, users with write (or admin) access, and collaborators + are skipped; the reminder only targets external contributors. """ import logging import os +import re from datetime import datetime, timedelta, timezone import requests @@ -37,6 +40,20 @@ REMINDER_MARKER = "" BYPASS_LABELS = {"no-issue-needed"} LOOKBACK_DAYS = 2 +# Collaborator permission levels that mark a PR author as a maintainer / writer / +# collaborator. Authors with any of these are skipped (the reminder is only for +# external contributors). +PRIVILEGED_PERMISSIONS = {"admin", "write", "maintain", "triage"} + +# `author_association` values that mark the author as a maintainer / collaborator. +# These are available on the PR payload without needing extra token scopes. +PRIVILEGED_ASSOCIATIONS = {"OWNER", "MEMBER", "COLLABORATOR"} + +# A PR authored by the model/pipeline's own team does not need to link an issue. +# Matches a checked task-list item for the corresponding PR template checkbox. +AUTHOR_CHECKBOX_PATTERN = re.compile( + r"-\s*\[\s*[xX]\s*\]\s*Are you the author \(or part of the team\) of the model/pipeline" +) CONTRIBUTION_GUIDE_URL = "https://huggingface.co/docs/diffusers/main/en/conceptual/contribution#coding-with-ai-agents" GRAPHQL_URL = "https://api.github.com/graphql" @@ -68,10 +85,31 @@ def has_linked_issue(token, owner, name, number): return data["repository"]["pullRequest"]["closingIssuesReferences"]["totalCount"] > 0 +def author_checkbox_checked(pr): + return bool(AUTHOR_CHECKBOX_PATTERN.search(pr.body or "")) + + def has_existing_reminder(pr): return any(REMINDER_MARKER in (c.body or "") for c in pr.get_issue_comments()) +def is_privileged_author(repo, pr, author): + """Return True if the author is a maintainer, has write/admin access, or is a collaborator.""" + # `author_association` is on the PR payload and needs no extra token scope. + association = (pr.raw_data or {}).get("author_association") + if association in PRIVILEGED_ASSOCIATIONS: + return True + # Fall back to the collaborator-permission API to catch writers/collaborators + # whose association is reported as CONTRIBUTOR/NONE on this particular PR. + try: + permission = repo.get_collaborator_permission(author) + except Exception as e: + # A 404 here means the user is not a collaborator at all (external contributor). + logger.info("Could not resolve permission for @%s, treating as external: %s", author, e) + return False + return permission in PRIVILEGED_PERMISSIONS + + def reminder_body(author): return ( f"{REMINDER_MARKER}\n" @@ -109,9 +147,13 @@ def main(): author = pr.user.login if not author or author.endswith("[bot]") or pr.user.type == "Bot": continue + if is_privileged_author(repo, pr, author): + continue labels = {label.name for label in pr.labels} if labels & BYPASS_LABELS: continue + if author_checkbox_checked(pr): + continue if has_linked_issue(token, owner, name, pr.number): continue if has_existing_reminder(pr):