Skip to content

Commit 34f82ac

Browse files
committed
post_comment_for_job_results.py added
1 parent dcdaef7 commit 34f82ac

File tree

2 files changed

+191
-0
lines changed

2 files changed

+191
-0
lines changed
Lines changed: 188 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,188 @@
1+
# Copyright 2025 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import argparse
18+
import dataclasses
19+
import logging
20+
import pathlib
21+
import subprocess
22+
import tempfile
23+
import typing
24+
25+
if typing.TYPE_CHECKING:
26+
from collections.abc import Iterable, Sequence
27+
28+
29+
def main() -> None:
30+
args = parse_args()
31+
logging.basicConfig(format="%(message)s", level=logging.INFO)
32+
33+
message_lines = tuple(generate_message_lines(args))
34+
35+
issue_url = f"{args.github_repository_html_url}/issues/{args.github_issue}"
36+
logging.info("Posting the following comment to GitHub Issue %s", issue_url)
37+
for line in message_lines:
38+
logging.info(line)
39+
40+
message_bytes = "\n".join(message_lines).encode("utf8", errors="replace")
41+
with tempfile.TemporaryDirectory() as tempdir_path:
42+
message_file = pathlib.Path(tempdir_path) / "message_text.txt"
43+
message_file.write_bytes(message_bytes)
44+
post_github_issue_comment(
45+
issue_number=args.github_issue,
46+
body_file=message_file,
47+
github_repository=args.github_repository,
48+
)
49+
50+
51+
def generate_message_lines(data: ParsedArgs) -> Iterable[str]:
52+
yield f"Result of workflows at {data.github_sha}:"
53+
54+
for job_result in data.job_results:
55+
result_symbol = "✅" if job_result.result == "success" else "❌"
56+
yield f" - {job_result.job_id}: {result_symbol} {job_result.result}"
57+
58+
yield ""
59+
yield f"{data.github_repository_html_url}/actions/runs/{data.github_run_id}"
60+
61+
yield ""
62+
yield (
63+
f"run_id={data.github_run_id} "
64+
f"run_number={data.github_run_number} "
65+
f"run_attempt={data.github_run_attempt}"
66+
)
67+
68+
69+
def post_github_issue_comment(
70+
issue_number: int, body_file: pathlib.Path, github_repository: str
71+
) -> None:
72+
gh_args = post_issue_comment_gh_args(
73+
issue_number=issue_number, body_file=body_file, github_repository=github_repository
74+
)
75+
gh_args = tuple(gh_args)
76+
logging.info("Running command: %s", subprocess.list2cmdline(gh_args))
77+
subprocess.check_call(gh_args) # noqa: S603
78+
79+
80+
def post_issue_comment_gh_args(
81+
issue_number: int, body_file: pathlib.Path, github_repository: str
82+
) -> Iterable[str]:
83+
yield "gh"
84+
yield "issue"
85+
yield "comment"
86+
yield str(issue_number)
87+
yield "--body-file"
88+
yield str(body_file)
89+
yield "-R"
90+
yield github_repository
91+
92+
93+
@dataclasses.dataclass(frozen=True)
94+
class JobResult:
95+
job_id: str
96+
result: str
97+
98+
@classmethod
99+
def parse(cls, s: str) -> JobResult:
100+
colon_index = s.find(":")
101+
if colon_index < 0:
102+
raise ParseError(
103+
"no colon (:) character found in job result specfication, "
104+
"which is required to delimit the job ID from the job result"
105+
)
106+
job_id = s[:colon_index]
107+
job_result = s[colon_index + 1 :]
108+
return cls(job_id=job_id, result=job_result)
109+
110+
111+
class ParsedArgs(typing.Protocol):
112+
job_results: Sequence[JobResult]
113+
github_issue: int
114+
github_repository: str
115+
github_sha: str
116+
github_repository_html_url: str
117+
github_run_id: str
118+
github_run_number: str
119+
github_run_attempt: str
120+
121+
122+
class ParseError(Exception):
123+
pass
124+
125+
126+
def parse_args() -> ParsedArgs:
127+
arg_parser = argparse.ArgumentParser()
128+
arg_parser.add_argument(
129+
"job_results",
130+
nargs="+",
131+
help="The results of the jobs in question, of the form "
132+
"'job-id:${{ needs.job-id.result }}' where 'job-id' is the id of the corresponding job "
133+
"in the 'needs' section of the job.",
134+
)
135+
arg_parser.add_argument(
136+
"--github-issue",
137+
required=True,
138+
help="The GitHub Issue number to which to post a comment",
139+
)
140+
arg_parser.add_argument(
141+
"--github-repository",
142+
required=True,
143+
help="The value of ${{ github.repository }} in the workflow",
144+
)
145+
arg_parser.add_argument(
146+
"--github-sha",
147+
required=True,
148+
help="The value of ${{ github.sha }} in the workflow",
149+
)
150+
arg_parser.add_argument(
151+
"--github-repository-html-url",
152+
required=True,
153+
help="The value of ${{ github.event.repository.html_url }} in the workflow",
154+
)
155+
arg_parser.add_argument(
156+
"--github-run-id",
157+
required=True,
158+
help="The value of ${{ github.run_id }} in the workflow",
159+
)
160+
arg_parser.add_argument(
161+
"--github-run-number",
162+
required=True,
163+
help="The value of ${{ github.run_number }} in the workflow",
164+
)
165+
arg_parser.add_argument(
166+
"--github-run-attempt",
167+
required=True,
168+
help="The value of ${{ github.run_attempt }} in the workflow",
169+
)
170+
171+
parse_result = arg_parser.parse_args()
172+
173+
job_results: list[JobResult] = []
174+
for job_result_str in parse_result.job_results:
175+
try:
176+
job_result = JobResult.parse(job_result_str)
177+
except ParseError as e:
178+
arg_parser.error(f"invalid job result specification: {job_result_str} ({e})")
179+
typing.assert_never("the line above should have raised an exception")
180+
else:
181+
job_results.append(job_result)
182+
parse_result.job_results = tuple(job_results)
183+
184+
return typing.cast("ParsedArgs", parse_result)
185+
186+
187+
if __name__ == "__main__":
188+
main()

firebase-dataconnect/ci/pyproject.toml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,9 @@ ignore = [
2828
"D211", # no-blank-line-before-class
2929
"D212", # multi-line-summary-second-line
3030
"E501", # Line too long (will be fixed by the formatter)
31+
"EM101", # Exception must not use a string literal, assign to variable first
3132
"LOG015", # root-logger-call
33+
"TRY003", # Avoid specifying long messages outside the exception class
3234
]
3335

3436
[tool.ruff.lint.per-file-ignores]
@@ -42,3 +44,4 @@ quote-style = "double"
4244
indent-style = "space"
4345
skip-magic-trailing-comma = false
4446
docstring-code-format = true
47+
SUCCESS!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!

0 commit comments

Comments
 (0)