Skip to content

Commit dcdaef7

Browse files
committed
work
1 parent 9d434f2 commit dcdaef7

File tree

3 files changed

+167
-6
lines changed

3 files changed

+167
-6
lines changed

firebase-dataconnect/ci/calculate_github_issue_for_commenting.py

Lines changed: 86 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -17,31 +17,111 @@
1717
import argparse
1818
import logging
1919
import re
20+
import subprocess
2021
import typing
2122

22-
type ExitCode = int
23+
if typing.TYPE_CHECKING:
24+
from collections.abc import Iterable
2325

2426

2527
def main() -> None:
2628
args = parse_args()
2729
logging.basicConfig(format="%(message)s", level=logging.INFO)
2830

29-
logging.info("Extracting PR number from string: %s", args.github_ref)
30-
pr_number: int | None = pr_number_from_github_ref(args.github_ref)
31-
logging.info("Extracted PR number: %s", pr_number)
31+
github_issue = calculate_github_issue(
32+
github_event_name=args.github_event_name,
33+
github_issue_for_scheduled_run=args.github_issue_for_scheduled_run,
34+
github_ref=args.github_ref,
35+
github_repository=args.github_repository,
36+
pr_body_github_issue_key=args.pr_body_github_issue_key,
37+
)
38+
39+
logging.info(github_issue)
40+
41+
42+
def calculate_github_issue(
43+
github_event_name: str,
44+
github_issue_for_scheduled_run: int,
45+
github_ref: str,
46+
github_repository: str,
47+
pr_body_github_issue_key: str,
48+
) -> int | None:
49+
if github_event_name == "schedule":
50+
logging.info(
51+
"GitHub Event name is: %s; using GitHub Issue: %s",
52+
github_event_name,
53+
github_issue_for_scheduled_run,
54+
)
55+
return github_issue_for_scheduled_run
56+
57+
logging.info("Extracting PR number from string: %s", github_ref)
58+
pr_number: int | None = pr_number_from_github_ref(github_ref)
59+
60+
if pr_number is None:
61+
logging.info("No PR number extracted")
62+
return None
63+
64+
logging.info("PR number extracted: %s", pr_number)
65+
logging.info("Loading body text of PR: %s", pr_number)
66+
pr_body_text = load_pr_body(
67+
pr_number=pr_number,
68+
github_repository=github_repository,
69+
)
70+
71+
logging.info("Looking for GitHub Issue key in PR body text: %s=NNNN", pr_body_github_issue_key)
72+
github_issue = github_issue_from_pr_body(
73+
pr_body=pr_body_text,
74+
issue_key=pr_body_github_issue_key,
75+
)
76+
77+
if github_issue is None:
78+
logging.info("No GitHub Issue key found in PR body")
79+
return None
80+
81+
logging.info("Found GitHub Issue key in PR body: %s", github_issue)
82+
return github_issue
3283

3384

3485
def pr_number_from_github_ref(github_ref: str) -> int | None:
3586
match = re.fullmatch("refs/pull/([0-9]+)/merge", github_ref)
3687
return int(match.group(1)) if match else None
3788

3889

90+
def load_pr_body(pr_number: int, github_repository: str) -> str:
91+
gh_args = log_pr_body_gh_args(pr_number=pr_number, github_repository=github_repository)
92+
gh_args = tuple(gh_args)
93+
logging.info("Running command: %s", subprocess.list2cmdline(gh_args))
94+
return subprocess.check_output(gh_args, encoding="utf8", errors="replace") # noqa: S603
95+
96+
97+
def log_pr_body_gh_args(pr_number: int, github_repository: str) -> Iterable[str]:
98+
yield "gh"
99+
yield "issue"
100+
yield "view"
101+
yield str(pr_number)
102+
yield "--json"
103+
yield "body"
104+
yield "--jq"
105+
yield ".body"
106+
yield "-R"
107+
yield github_repository
108+
109+
110+
def github_issue_from_pr_body(pr_body: str, issue_key: str) -> int | None:
111+
expr = re.compile(r"\s*" + re.escape(issue_key) + r"\s*=\s*(\d+)\s*")
112+
for line in pr_body.splitlines():
113+
match = expr.fullmatch(line.strip())
114+
if match:
115+
return int(match.group(1))
116+
return None
117+
118+
39119
class ParsedArgs(typing.Protocol):
40120
github_ref: str
41121
github_repository: str
42122
github_event_name: str
43123
pr_body_github_issue_key: str
44-
github_issue_for_scheduled_run: str
124+
github_issue_for_scheduled_run: int
45125

46126

47127
def parse_args() -> ParsedArgs:
@@ -70,6 +150,7 @@ def parse_args() -> ParsedArgs:
70150
)
71151
arg_parser.add_argument(
72152
"--github-issue-for-scheduled-run",
153+
type=int,
73154
required=True,
74155
help="The GitHub Issue number to use for commenting when --github-event-name is 'schedule'",
75156
)

firebase-dataconnect/ci/calculate_github_issue_for_commenting_test.py

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,11 @@ def test_returns_number_from_valid_github_ref(self, number: int) -> None:
2727
github_ref = f"refs/pull/{number}/merge"
2828
assert sut.pr_number_from_github_ref(github_ref) == number
2929

30+
@hypothesis.given(number=st.integers(min_value=0, max_value=10000))
31+
def test_ignores_leading_zeroes(self, number: int) -> None:
32+
github_ref = f"refs/pull/0{number}/merge"
33+
assert sut.pr_number_from_github_ref(github_ref) == number
34+
3035
@hypothesis.given(invalid_github_ref=st.text())
3136
def test_returns_none_on_random_input(self, invalid_github_ref: str) -> None:
3237
assert sut.pr_number_from_github_ref(invalid_github_ref) is None
@@ -35,6 +40,7 @@ def test_returns_none_on_random_input(self, invalid_github_ref: str) -> None:
3540
"invalid_number",
3641
[
3742
"",
43+
"-1",
3844
"123a",
3945
"a123",
4046
"12a34",
@@ -80,3 +86,77 @@ def test_returns_none_on_invalid_number(self, invalid_number: str) -> None:
8086
)
8187
def test_returns_none_on_malformed_ref(self, malformed_ref: str) -> None:
8288
assert sut.pr_number_from_github_ref(malformed_ref) is None
89+
90+
91+
class Test_github_issue_from_pr_body:
92+
@hypothesis.given(number=st.integers(min_value=0, max_value=10000))
93+
def test_returns_number(self, number: int) -> None:
94+
text = f"zzyzx={number}"
95+
assert sut.github_issue_from_pr_body(text, "zzyzx") == number
96+
97+
@hypothesis.given(number=st.integers(min_value=0, max_value=10000))
98+
def test_ignores_leading_zeroes(self, number: int) -> None:
99+
text = f"zzyzx=0{number}"
100+
assert sut.github_issue_from_pr_body(text, "zzyzx") == number
101+
102+
@hypothesis.given(number=st.integers(min_value=0, max_value=10000))
103+
def test_ignores_whitespace(self, number: int) -> None:
104+
text = f" zzyzx = {number} "
105+
assert sut.github_issue_from_pr_body(text, "zzyzx") == number
106+
107+
@hypothesis.given(
108+
number1=st.integers(min_value=0, max_value=10000),
109+
number2=st.integers(min_value=0, max_value=10000),
110+
)
111+
def test_does_not_ignore_whitespace_in_key(self, number1: int, number2: int) -> None:
112+
text = f"zzyzx={number1}\n z z y z x = {number2} "
113+
assert sut.github_issue_from_pr_body(text, "z z y z x") == number2
114+
115+
@hypothesis.given(
116+
number1=st.integers(min_value=0, max_value=10000),
117+
number2=st.integers(min_value=0, max_value=10000),
118+
)
119+
def test_returns_first_number_ignoring_second(self, number1: int, number2: int) -> None:
120+
text = f"zzyzx={number1}\nzzyzx={number2}"
121+
assert sut.github_issue_from_pr_body(text, "zzyzx") == number1
122+
123+
@hypothesis.given(number=st.integers(min_value=0, max_value=10000))
124+
def test_returns_first_valid_number_ignoring_invalid(self, number: int) -> None:
125+
text = f"zzyzx=12X34\nzzyzx={number}"
126+
assert sut.github_issue_from_pr_body(text, "zzyzx") == number
127+
128+
@hypothesis.given(number=st.integers(min_value=0, max_value=10000))
129+
def test_returns_number_amidst_other_lines(self, number: int) -> None:
130+
text = f"line 1\nline 2\nzzyzx={number}\nline 3"
131+
assert sut.github_issue_from_pr_body(text, "zzyzx") == number
132+
133+
@hypothesis.given(number=st.integers(min_value=0, max_value=10000))
134+
def test_returns_escapes_regex_special_chars_in_key(self, number: int) -> None:
135+
text = f"*+={number}"
136+
assert sut.github_issue_from_pr_body(text, "*+") == number
137+
138+
@pytest.mark.parametrize(
139+
"text",
140+
[
141+
"",
142+
"asdf",
143+
"zzyzx=",
144+
"=zzyzx",
145+
"zzyzx=a",
146+
"zzyzx=-1",
147+
"zzyzx=a123",
148+
"zzyzx=123a",
149+
"zzyzx=1.2",
150+
"a zzyzx=1234",
151+
"zzyzx=1234 a",
152+
pytest.param(
153+
"zzyzx=1234",
154+
marks=pytest.mark.xfail(
155+
reason="make sure that the test would otherwise pass on valid text",
156+
strict=True,
157+
),
158+
),
159+
],
160+
)
161+
def test_returns_none_when_key_not_found_or_cannot_parse_int(self, text: str) -> None:
162+
assert sut.github_issue_from_pr_body(text, "zzyzx") is None

firebase-dataconnect/ci/pyproject.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -27,8 +27,8 @@ ignore = [
2727
"D203", # incorrect-blank-line-before-class
2828
"D211", # no-blank-line-before-class
2929
"D212", # multi-line-summary-second-line
30+
"E501", # Line too long (will be fixed by the formatter)
3031
"LOG015", # root-logger-call
31-
"T201", # print` found
3232
]
3333

3434
[tool.ruff.lint.per-file-ignores]

0 commit comments

Comments
 (0)