name: First-time contributor issue required on: pull_request_target: types: [opened, edited, reopened] permissions: pull-requests: write issues: write concurrency: group: ftc-require-issue-${{ github.event.pull_request.number }} cancel-in-progress: true jobs: require-issue: # Allow-list returning contributors. FIRST_TIMER / FIRST_TIME_CONTRIBUTOR # are often NONE on pull_request_target at opened time, which skipped the # previous deny-list and left first-timer PRs open. if: > github.event.pull_request.user.type != 'Bot' && !contains(fromJSON('["MEMBER","OWNER","COLLABORATOR","CONTRIBUTOR"]'), github.event.pull_request.author_association) runs-on: ubuntu-latest steps: - name: Require an open issue env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} REPO: ${{ github.repository }} AUTHOR_ASSOCIATION: ${{ github.event.pull_request.author_association }} run: | python3 << 'PY' import json import os import re import subprocess import sys repo = os.environ["REPO"] pr_number = os.environ["PR_NUMBER"] owner, name = repo.split("/", 1) print( "author_association=", os.environ.get("AUTHOR_ASSOCIATION", ""), sep="", ) patterns = ( re.compile(r"(? dict: return json.loads( subprocess.check_output(["gh", *args], text=True) ) def is_open_repo_issue(number: int) -> bool: result = subprocess.run( ["gh", "api", f"repos/{repo}/issues/{number}"], capture_output=True, text=True, ) if result.returncode != 0: stderr = result.stderr or "" if "404" in stderr or "Not Found" in stderr: return False raise RuntimeError( f"GitHub API error looking up #{number}: {stderr}" ) payload = json.loads(result.stdout) if "pull_request" in payload: return False return (payload.get("state") or "").lower() == "open" pr = gh_json( "pr", "view", pr_number, "--repo", repo, "--json", "title,body,state" ) text = f"{pr.get('title') or ''}\n{pr.get('body') or ''}" candidates = { int(match) for pattern in patterns for match in pattern.findall(text) } if any(is_open_repo_issue(number) for number in sorted(candidates)): sys.exit(0) if (pr.get("state") or "").upper() == "CLOSED": sys.exit(0) comment = f"""Thanks for the pull request. First-time contributors need an associated open issue before we can review a PR. 1. Open an issue with a [template](https://github.com/{repo}/issues/new/choose), or pick an existing open one. 2. Open a new PR (or reopen this one) whose title or body mentions that issue, for example `#123`. See the [contributing guide](https://github.com/{repo}/blob/main/.github/CONTRIBUTING.md). """ subprocess.run( [ "gh", "pr", "comment", pr_number, "--repo", repo, "--body", comment, ], check=True, ) subprocess.run( [ "gh", "pr", "edit", pr_number, "--repo", repo, "--add-label", "needs-issue", ], check=True, ) subprocess.run( ["gh", "pr", "close", pr_number, "--repo", repo], check=True, ) PY