Skip to content

Commit f60851f

Browse files
committed
draft PR anchor
Signed-off-by: Effi-S <effi.szt@gmail.com>
1 parent 0101560 commit f60851f

1 file changed

Lines changed: 155 additions & 0 deletions

File tree

create_pr_from_issue.py

Lines changed: 155 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,155 @@
1+
#!/usr/bin/env python3
2+
"""Create a pull request from a GitHub issue using the ``ghapi`` package.
3+
4+
The PR inherits the issue's body (prefixed with ``Fixes #<issue>`` so GitHub
5+
links and auto-closes the issue), its labels, and its milestone, and is assigned
6+
to the authenticated user.
7+
8+
Requires the ``GITHUB_TOKEN`` (or ``GH_TOKEN``) environment variable.
9+
"""
10+
11+
12+
import argparse
13+
import asyncio
14+
import os
15+
from pathlib import Path
16+
import subprocess
17+
import sys
18+
19+
from ghapi.all import GhApi
20+
21+
DEFAULT_OWNER = "LFDT-Panurus"
22+
DEFAULT_REPO = "panurus"
23+
DEFAULT_BASE = "main"
24+
25+
26+
def subrun(*args, check=True, capture_output=True, text=True, **kwargs):
27+
proc = subprocess.run(*args, check=check,
28+
capture_output=capture_output, text=text, **kwargs)
29+
30+
if proc.stderr:
31+
print(proc.stderr)
32+
if proc.stdout:
33+
print(proc.stdout)
34+
35+
return proc
36+
37+
38+
def build_body(issue_body: str | None, issue_number: int) -> str:
39+
"""Prepend the ``Fixes #N`` linking line to the issue body."""
40+
fixes = f"Fixes #{issue_number}"
41+
body = (issue_body or "").strip()
42+
return f"{fixes}\n\n{body}" if body else fixes
43+
44+
45+
def create_branch(name: str, root: Path | None = None, remote: str = "origin"):
46+
subrun(["git", "branch", name],
47+
cwd=root or Path.cwd(), check=True)
48+
return name
49+
50+
51+
def add_dummy_commit(branch, root: Path | None = None):
52+
subrun(["git", "commit", "--allow-empty", "--only", "-m",
53+
"draft PR anchor", "-s", "--", ":/"],
54+
cwd=root or Path.cwd(),
55+
check=True,
56+
capture_output=True,
57+
text=True)
58+
59+
subrun(["git", "push", "origin", branch],
60+
check=True, cwd=root or Path.cwd(),
61+
capture_output=True, text=True)
62+
63+
64+
def set_upstream(name: str, root: Path | None = None, remote: str = "origin") -> str:
65+
66+
subrun(["git", "push", "--set-upstream", remote, name],
67+
cwd=root or Path.cwd(), check=True)
68+
return name
69+
70+
71+
def parse_args() -> argparse.Namespace:
72+
parser = argparse.ArgumentParser(description=__doc__)
73+
parser.add_argument("issue", type=int,
74+
help="Issue number to base the PR on")
75+
parser.add_argument("--owner", default=DEFAULT_OWNER,
76+
help=f"Repo owner (default: {DEFAULT_OWNER})")
77+
parser.add_argument("--repo", default=DEFAULT_REPO,
78+
help=f"Repo name (default: {DEFAULT_REPO})")
79+
parser.add_argument("--base", default=DEFAULT_BASE,
80+
help=f"Base branch to merge into (default: {DEFAULT_BASE})")
81+
parser.add_argument("--title", default=None,
82+
help="PR title (default: the issue's title)")
83+
84+
return parser.parse_args()
85+
86+
87+
def _get_token():
88+
return subprocess.run(["gh", "auth", "token"], capture_output=True, text=True).stdout.strip()
89+
90+
91+
def main(args) -> int:
92+
93+
token = os.environ.get("GITHUB_TOKEN") or os.environ.get(
94+
"GH_TOKEN", _get_token())
95+
if not token:
96+
print("error: set GITHUB_TOKEN (or GH_TOKEN) with repo scope", file=sys.stderr)
97+
return 1
98+
99+
api = GhApi(owner=args.owner, repo=args.repo, token=token)
100+
101+
me = asyncio.run(api.users.get_authenticated()).login
102+
103+
issue = asyncio.run(api.issues.get(
104+
owner=args.owner, repo=args.repo, issue_number=args.issue))
105+
106+
labels = [lbl.name for lbl in (issue.labels or [])]
107+
milestone = issue.milestone.number if issue.milestone else None
108+
title = args.title or issue.title
109+
110+
branch = f"fix-{issue.number}"
111+
# branch = create_branch(f"fix-{issue.number}")
112+
# print("Created Branch:", branch)
113+
114+
# print("Pushing Upstream branch:", branch)
115+
# set_upstream(branch)
116+
117+
print("Adding dummy commit to:", branch)
118+
add_dummy_commit(branch)
119+
120+
# # Create the PR.
121+
print("Creating Draft PR:")
122+
pr = asyncio.run(api.pulls.create(
123+
owner=args.owner,
124+
repo=args.repo,
125+
title=title,
126+
head=branch,
127+
base=args.base,
128+
body=build_body(issue.body, args.issue),
129+
draft=True,
130+
))
131+
132+
print(f"created PR #{pr.number}: {pr.html_url}")
133+
134+
# Labels, milestone and assignee live on the issue view of the PR.
135+
asyncio.run(api.issues.update(
136+
owner=args.owner,
137+
repo=args.repo,
138+
issue_number=pr.number,
139+
labels=labels,
140+
milestone=milestone,
141+
assignees=[me],
142+
))
143+
144+
print(f"assigned to @{me}")
145+
if labels:
146+
print(f"labels: {', '.join(labels)}")
147+
if milestone is not None:
148+
print(f"milestone: {issue.milestone.title} (#{milestone})")
149+
150+
return 0
151+
152+
153+
if __name__ == "__main__":
154+
args = parse_args()
155+
main(args)

0 commit comments

Comments
 (0)