mirror of
https://github.com/crewAIInc/crewAI.git
synced 2026-09-20 01:55:38 +00:00
* ci: require an open issue for first-time contributor PRs Gate anyone who is not a returning contributor, and allow the PR only when a closing keyword points at an open issue in this repo. * ci: accept any open issue mention for first-timer PRs Drop the closing-keyword regex so #123, owner/repo#N, or an issue URL is enough when that issue is open. * ci: ignore foreign owner/repo#N in first-timer issue gate Bare #123 no longer matches the suffix of other/repo#123, so an open local issue cannot keep that PR open.
122 lines
4.1 KiB
YAML
122 lines
4.1 KiB
YAML
name: First-time contributor issue required
|
|
|
|
on:
|
|
pull_request_target:
|
|
types: [opened, edited, reopened]
|
|
|
|
permissions:
|
|
pull-requests: write
|
|
issues: read
|
|
|
|
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"(?<![\w./-])#(\d+)\b"),
|
|
re.compile(rf"{re.escape(owner)}/{re.escape(name)}#(\d+)\b"),
|
|
re.compile(
|
|
rf"https://github\.com/{re.escape(owner)}/{re.escape(name)}/issues/(\d+)\b"
|
|
),
|
|
)
|
|
|
|
def gh_json(*args: str) -> 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", "close", pr_number, "--repo", repo],
|
|
check=True,
|
|
)
|
|
PY
|