From df963a8ecdc4757c360c34c81e081561593447ff Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Thu, 14 May 2026 12:09:35 +0900 Subject: [PATCH 1/4] implement a bot to remind prs to link issues if not. --- .github/workflows/pr_link_issue_reminder.yml | 33 ++++ utils/remind_link_issue.py | 152 +++++++++++++++++++ 2 files changed, 185 insertions(+) create mode 100644 .github/workflows/pr_link_issue_reminder.yml create mode 100644 utils/remind_link_issue.py diff --git a/.github/workflows/pr_link_issue_reminder.yml b/.github/workflows/pr_link_issue_reminder.yml new file mode 100644 index 000000000000..051d2cd69746 --- /dev/null +++ b/.github/workflows/pr_link_issue_reminder.yml @@ -0,0 +1,33 @@ +name: PR Issue Link Reminder + +on: + schedule: + - cron: "30 7 * * *" + workflow_dispatch: + +jobs: + remind_or_close: + name: Remind or close PRs without a linked issue + if: github.repository == 'huggingface/diffusers' + runs-on: ubuntu-22.04 + permissions: + contents: read + pull-requests: write + issues: write + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + steps: + - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 + + - name: Setup Python + uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 + with: + python-version: 3.10 + + - name: Install requirements + run: | + pip install PyGithub requests + + - name: Run reminder script + run: | + python utils/remind_link_issue.py diff --git a/utils/remind_link_issue.py b/utils/remind_link_issue.py new file mode 100644 index 000000000000..f4e529f0dc99 --- /dev/null +++ b/utils/remind_link_issue.py @@ -0,0 +1,152 @@ +# Copyright 2025 The HuggingFace Team. All rights reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +""" +Script to remind PR authors to link an issue, and close PRs that ignore the reminders. + +Behavior: +- Scans open, non-draft PRs. +- A PR is considered "linked" if GitHub's GraphQL `closingIssuesReferences` returns > 0 + (covers both `Fixes #N` keywords in the body and issues linked via the GitHub UI). +- If a PR is not linked, the script posts up to 3 reminder comments spaced 7 days apart. +- If the 3rd reminder is older than 7 days and the PR is still not linked, the PR is closed. +- PRs labeled `no-issue-needed` and bot-authored PRs are skipped. +""" + +import os +from datetime import datetime, timedelta, timezone + +import requests +from github import Github + + +REPO = "huggingface/diffusers" +REMINDER_MARKER = "" +CLOSE_MARKER = "" +REMINDER_INTERVAL = timedelta(days=7) +MAX_REMINDERS = 3 +BYPASS_LABELS = {"no-issue-needed"} + +GRAPHQL_URL = "https://api.github.com/graphql" +GRAPHQL_QUERY = """ +query($owner: String!, $name: String!, $number: Int!) { + repository(owner: $owner, name: $name) { + pullRequest(number: $number) { + closingIssuesReferences(first: 1) { + totalCount + } + } + } +} +""" + + +def has_linked_issue(token, owner, name, number): + response = requests.post( + GRAPHQL_URL, + json={"query": GRAPHQL_QUERY, "variables": {"owner": owner, "name": name, "number": number}}, + headers={"Authorization": f"Bearer {token}"}, + timeout=30, + ) + response.raise_for_status() + payload = response.json() + return payload["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["totalCount"] > 0 + + +def reminder_history(pr): + reminders = [c for c in pr.get_issue_comments() if REMINDER_MARKER in (c.body or "")] + reminders.sort(key=lambda c: c.created_at) + return reminders + + +def reminder_body(author, count): + remaining = MAX_REMINDERS - count + lines = [ + REMINDER_MARKER, + f"Hi @{author}, this PR does not appear to link an issue it fixes. " + "If this PR addresses an existing issue, please add a closing keyword " + "(e.g. `Fixes #1234`) to the PR description so the issue is linked.", + "", + f"Reminder **{count}/{MAX_REMINDERS}**. ", + ] + if remaining > 0: + lines[-1] += ( + f"If no linked issue is added within {REMINDER_INTERVAL.days} days, " + f"you will receive {remaining} more reminder(s)." + ) + else: + lines[-1] += ( + f"This is the final reminder. If no linked issue is added within " + f"{REMINDER_INTERVAL.days} days, this PR will be closed automatically. " + "If this PR intentionally does not fix a tracked issue, a maintainer " + "can add the `no-issue-needed` label to bypass this check." + ) + return "\n".join(lines) + + +def close_body(author): + return ( + f"{CLOSE_MARKER}\n" + f"Closing this PR because @{author} did not add a linked issue after " + f"{MAX_REMINDERS} reminders spaced {REMINDER_INTERVAL.days} days apart. " + "Please reopen once the PR description references the issue it fixes " + "(e.g. `Fixes #1234`), or ask a maintainer to add the `no-issue-needed` " + "label if this PR is intentionally unrelated to a tracked issue." + ) + + +def aware(ts): + return ts if ts.tzinfo is not None else ts.replace(tzinfo=timezone.utc) + + +def main(): + token = os.environ["GITHUB_TOKEN"] + g = Github(token) + repo = g.get_repo(REPO) + owner, name = REPO.split("/", 1) + + now = datetime.now(timezone.utc) + + for pr in repo.get_pulls(state="open"): + if pr.draft: + continue + if pr.user is None: + continue + author = pr.user.login + if not author or author.endswith("[bot]") or pr.user.type == "Bot": + continue + labels = {label.name for label in pr.labels} + if labels & BYPASS_LABELS: + continue + if has_linked_issue(token, owner, name, pr.number): + continue + + reminders = reminder_history(pr) + count = len(reminders) + + if count == 0: + pr.create_issue_comment(reminder_body(author, 1)) + continue + + if now - aware(reminders[-1].created_at) < REMINDER_INTERVAL: + continue + + if count >= MAX_REMINDERS: + pr.create_issue_comment(close_body(author)) + pr.edit(state="closed") + else: + pr.create_issue_comment(reminder_body(author, count + 1)) + + +if __name__ == "__main__": + main() From cb40461998ad3dd8b5fcbd4a282aabdfb00a362e Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Thu, 14 May 2026 13:48:33 +0900 Subject: [PATCH 2/4] up --- .github/workflows/pr_link_issue_reminder.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pr_link_issue_reminder.yml b/.github/workflows/pr_link_issue_reminder.yml index 051d2cd69746..13d35f3ed2ef 100644 --- a/.github/workflows/pr_link_issue_reminder.yml +++ b/.github/workflows/pr_link_issue_reminder.yml @@ -22,7 +22,7 @@ jobs: - name: Setup Python uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6 with: - python-version: 3.10 + python-version: "3.10" - name: Install requirements run: | From 1552f704b4bcad0648f0f91fe308e648f7820103 Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Fri, 15 May 2026 10:17:59 +0900 Subject: [PATCH 3/4] remove auto-close and add contribution guide. --- utils/remind_link_issue.py | 39 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 26 deletions(-) diff --git a/utils/remind_link_issue.py b/utils/remind_link_issue.py index f4e529f0dc99..c766e790ff94 100644 --- a/utils/remind_link_issue.py +++ b/utils/remind_link_issue.py @@ -12,14 +12,13 @@ # See the License for the specific language governing permissions and # limitations under the License. """ -Script to remind PR authors to link an issue, and close PRs that ignore the reminders. +Script to remind PR authors to link an issue. Behavior: - Scans open, non-draft PRs. - A PR is considered "linked" if GitHub's GraphQL `closingIssuesReferences` returns > 0 (covers both `Fixes #N` keywords in the body and issues linked via the GitHub UI). - If a PR is not linked, the script posts up to 3 reminder comments spaced 7 days apart. -- If the 3rd reminder is older than 7 days and the PR is still not linked, the PR is closed. - PRs labeled `no-issue-needed` and bot-authored PRs are skipped. """ @@ -32,10 +31,10 @@ REPO = "huggingface/diffusers" REMINDER_MARKER = "" -CLOSE_MARKER = "" REMINDER_INTERVAL = timedelta(days=7) MAX_REMINDERS = 3 BYPASS_LABELS = {"no-issue-needed"} +CONTRIBUTION_GUIDE_URL = "https://huggingface.co/docs/diffusers/main/en/conceptual/contribution#coding-with-ai-agents" GRAPHQL_URL = "https://api.github.com/graphql" GRAPHQL_QUERY = """ @@ -75,36 +74,25 @@ def reminder_body(author, count): REMINDER_MARKER, f"Hi @{author}, this PR does not appear to link an issue it fixes. " "If this PR addresses an existing issue, please add a closing keyword " - "(e.g. `Fixes #1234`) to the PR description so the issue is linked.", + "(e.g. `Fixes #1234`) to the PR description so the issue is linked. " + f"See the [contribution guide]({CONTRIBUTION_GUIDE_URL}) for more details.", "", - f"Reminder **{count}/{MAX_REMINDERS}**. ", + f"Reminder **{count}/{MAX_REMINDERS}**.", ] if remaining > 0: lines[-1] += ( - f"If no linked issue is added within {REMINDER_INTERVAL.days} days, " + f" If no linked issue is added within {REMINDER_INTERVAL.days} days, " f"you will receive {remaining} more reminder(s)." ) else: lines[-1] += ( - f"This is the final reminder. If no linked issue is added within " - f"{REMINDER_INTERVAL.days} days, this PR will be closed automatically. " - "If this PR intentionally does not fix a tracked issue, a maintainer " - "can add the `no-issue-needed` label to bypass this check." + " This is the final reminder. If this PR intentionally does not fix " + "a tracked issue, a maintainer can add the `no-issue-needed` label " + "to bypass this check." ) return "\n".join(lines) -def close_body(author): - return ( - f"{CLOSE_MARKER}\n" - f"Closing this PR because @{author} did not add a linked issue after " - f"{MAX_REMINDERS} reminders spaced {REMINDER_INTERVAL.days} days apart. " - "Please reopen once the PR description references the issue it fixes " - "(e.g. `Fixes #1234`), or ask a maintainer to add the `no-issue-needed` " - "label if this PR is intentionally unrelated to a tracked issue." - ) - - def aware(ts): return ts if ts.tzinfo is not None else ts.replace(tzinfo=timezone.utc) @@ -138,14 +126,13 @@ def main(): pr.create_issue_comment(reminder_body(author, 1)) continue + if count >= MAX_REMINDERS: + continue + if now - aware(reminders[-1].created_at) < REMINDER_INTERVAL: continue - if count >= MAX_REMINDERS: - pr.create_issue_comment(close_body(author)) - pr.edit(state="closed") - else: - pr.create_issue_comment(reminder_body(author, count + 1)) + pr.create_issue_comment(reminder_body(author, count + 1)) if __name__ == "__main__": From f711ecee5380dd21231c5e23bb762490492102b2 Mon Sep 17 00:00:00 2001 From: sayakpaul Date: Fri, 15 May 2026 16:49:47 +0900 Subject: [PATCH 4/4] restrict to one comment --- utils/remind_link_issue.py | 63 +++++++++----------------------------- 1 file changed, 14 insertions(+), 49 deletions(-) diff --git a/utils/remind_link_issue.py b/utils/remind_link_issue.py index c766e790ff94..a5ebda3f449f 100644 --- a/utils/remind_link_issue.py +++ b/utils/remind_link_issue.py @@ -18,12 +18,12 @@ - Scans open, non-draft PRs. - A PR is considered "linked" if GitHub's GraphQL `closingIssuesReferences` returns > 0 (covers both `Fixes #N` keywords in the body and issues linked via the GitHub UI). -- If a PR is not linked, the script posts up to 3 reminder comments spaced 7 days apart. +- 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. """ import os -from datetime import datetime, timedelta, timezone import requests from github import Github @@ -31,8 +31,6 @@ REPO = "huggingface/diffusers" REMINDER_MARKER = "" -REMINDER_INTERVAL = timedelta(days=7) -MAX_REMINDERS = 3 BYPASS_LABELS = {"no-issue-needed"} CONTRIBUTION_GUIDE_URL = "https://huggingface.co/docs/diffusers/main/en/conceptual/contribution#coding-with-ai-agents" @@ -62,39 +60,20 @@ def has_linked_issue(token, owner, name, number): return payload["data"]["repository"]["pullRequest"]["closingIssuesReferences"]["totalCount"] > 0 -def reminder_history(pr): - reminders = [c for c in pr.get_issue_comments() if REMINDER_MARKER in (c.body or "")] - reminders.sort(key=lambda c: c.created_at) - return reminders +def has_existing_reminder(pr): + return any(REMINDER_MARKER in (c.body or "") for c in pr.get_issue_comments()) -def reminder_body(author, count): - remaining = MAX_REMINDERS - count - lines = [ - REMINDER_MARKER, - f"Hi @{author}, this PR does not appear to link an issue it fixes. " +def reminder_body(author): + return ( + f"{REMINDER_MARKER}\n" + f"Hi @{author}, thanks for the PR! It does not appear to link an issue it fixes. " "If this PR addresses an existing issue, please add a closing keyword " "(e.g. `Fixes #1234`) to the PR description so the issue is linked. " - f"See the [contribution guide]({CONTRIBUTION_GUIDE_URL}) for more details.", - "", - f"Reminder **{count}/{MAX_REMINDERS}**.", - ] - if remaining > 0: - lines[-1] += ( - f" If no linked issue is added within {REMINDER_INTERVAL.days} days, " - f"you will receive {remaining} more reminder(s)." - ) - else: - lines[-1] += ( - " This is the final reminder. If this PR intentionally does not fix " - "a tracked issue, a maintainer can add the `no-issue-needed` label " - "to bypass this check." - ) - return "\n".join(lines) - - -def aware(ts): - return ts if ts.tzinfo is not None else ts.replace(tzinfo=timezone.utc) + f"See the [contribution guide]({CONTRIBUTION_GUIDE_URL}) for more details. " + "If this PR intentionally does not fix a tracked issue, a maintainer can " + "add the `no-issue-needed` label to silence this reminder." + ) def main(): @@ -103,8 +82,6 @@ def main(): repo = g.get_repo(REPO) owner, name = REPO.split("/", 1) - now = datetime.now(timezone.utc) - for pr in repo.get_pulls(state="open"): if pr.draft: continue @@ -118,21 +95,9 @@ def main(): continue if has_linked_issue(token, owner, name, pr.number): continue - - reminders = reminder_history(pr) - count = len(reminders) - - if count == 0: - pr.create_issue_comment(reminder_body(author, 1)) + if has_existing_reminder(pr): continue - - if count >= MAX_REMINDERS: - continue - - if now - aware(reminders[-1].created_at) < REMINDER_INTERVAL: - continue - - pr.create_issue_comment(reminder_body(author, count + 1)) + pr.create_issue_comment(reminder_body(author)) if __name__ == "__main__":