Skip to content

Commit 066619f

Browse files
committed
chore(ci-issues): add junit upload CLI
Adds the cli `mergify ci-issues junit-upload` to upload JUnit xml reports to CI Issues. Fixes MRGFY-4339
1 parent 427ccc6 commit 066619f

File tree

9 files changed

+856
-4
lines changed

9 files changed

+856
-4
lines changed

mergify_cli/ci/__init__.py

Whitespace-only changes.

mergify_cli/ci/cli.py

+145
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,145 @@
1+
import json
2+
import os
3+
import pathlib
4+
import re
5+
import typing
6+
7+
import click
8+
9+
from mergify_cli import console
10+
from mergify_cli import utils
11+
from mergify_cli.ci import junit_upload as junit_upload_mod
12+
13+
14+
ci = click.Group(
15+
"ci",
16+
help="Mergify's CI related commands",
17+
)
18+
19+
20+
CIProviderT = typing.Literal["github_action", "circleci"]
21+
22+
23+
def get_ci_provider() -> CIProviderT | None:
24+
if os.getenv("GITHUB_ACTIONS") == "true":
25+
return "github_action"
26+
if os.getenv("CIRCLECI") == "true":
27+
return "circleci"
28+
return None
29+
30+
31+
def get_job_name() -> str | None:
32+
if get_ci_provider() == "github_action":
33+
return os.getenv("GITHUB_WORKFLOW")
34+
if get_ci_provider() == "circleci":
35+
return os.getenv("CIRCLE_JOB")
36+
37+
console.log("Error: failed to get the job's name from env", style="red")
38+
return None
39+
40+
41+
def get_head_sha() -> str | None:
42+
if get_ci_provider() == "github_action":
43+
if os.getenv("GITHUB_EVENT_NAME") == "pull_request":
44+
# NOTE(leo): we want the head sha of pull request
45+
event_raw_path = os.getenv("GITHUB_EVENT_PATH")
46+
if event_raw_path and (
47+
(event_path := pathlib.Path(event_raw_path)).is_file()
48+
):
49+
event = json.loads(event_path.read_bytes())
50+
return str(event.get("pull_request", {}).get("head", {}).get("sha"))
51+
return os.getenv("GITHUB_SHA")
52+
53+
if get_ci_provider() == "circleci":
54+
return os.getenv("CIRCLE_SHA1")
55+
56+
console.log("Error: failed to get the head SHA from env", style="red")
57+
return None
58+
59+
60+
def get_github_repository() -> str | None:
61+
if get_ci_provider() == "github_action":
62+
return os.getenv("GITHUB_REPOSITORY")
63+
if get_ci_provider() == "circleci":
64+
repository_url = os.getenv("CIRCLE_REPOSITORY_URL")
65+
if repository_url and (
66+
match := re.match(
67+
r"(https?://[\w.-]+/)?(?P<full_name>[\w.-]+/[\w.-]+)/?$",
68+
repository_url,
69+
)
70+
):
71+
return match.group("full_name")
72+
73+
console.log("Error: failed to get the GitHub repository from env", style="red")
74+
return None
75+
76+
77+
@ci.command(help="Upload JUnit XML reports")
78+
@click.option(
79+
"--api-url",
80+
"-u",
81+
help="URL of the Mergify API",
82+
required=True,
83+
envvar="MERGIFY_API_URL",
84+
default="https://api.mergify.com/",
85+
show_default=True,
86+
)
87+
@click.option(
88+
"--token",
89+
"-t",
90+
help="CI Issues Application Key",
91+
required=True,
92+
envvar="MERGIFY_TOKEN",
93+
)
94+
@click.option(
95+
"--repository",
96+
"-r",
97+
help="Repository full name (owner/repo)",
98+
required=True,
99+
default=get_github_repository,
100+
)
101+
@click.option(
102+
"--head-sha",
103+
"-s",
104+
help="Head SHA of the triggered job",
105+
required=True,
106+
default=get_head_sha,
107+
)
108+
@click.option(
109+
"--job-name",
110+
"-j",
111+
help="Job's name",
112+
required=True,
113+
default=get_job_name,
114+
)
115+
@click.option(
116+
"--provider",
117+
"-p",
118+
help="CI provider",
119+
default=get_ci_provider,
120+
)
121+
@click.argument(
122+
"files",
123+
nargs=-1,
124+
required=True,
125+
type=click.Path(exists=True, dir_okay=False),
126+
)
127+
@utils.run_with_asyncio
128+
async def junit_upload( # noqa: PLR0913, PLR0917
129+
api_url: str,
130+
token: str,
131+
repository: str,
132+
head_sha: str,
133+
job_name: str,
134+
provider: str | None,
135+
files: tuple[str, ...],
136+
) -> None:
137+
await junit_upload_mod.upload(
138+
api_url=api_url,
139+
token=token,
140+
repository=repository,
141+
head_sha=head_sha,
142+
job_name=job_name,
143+
provider=provider,
144+
files=files,
145+
)

mergify_cli/ci/junit_upload.py

+82
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,82 @@
1+
from collections import abc
2+
import contextlib
3+
import pathlib
4+
import typing
5+
6+
import httpx
7+
8+
from mergify_cli import console
9+
from mergify_cli import utils
10+
11+
12+
@contextlib.contextmanager
13+
def get_files_to_upload(
14+
files: tuple[str, ...],
15+
) -> abc.Generator[list[tuple[str, tuple[str, typing.BinaryIO, str]]], None, None]:
16+
files_to_upload: list[tuple[str, tuple[str, typing.BinaryIO, str]]] = []
17+
18+
for file in set(files):
19+
file_path = pathlib.Path(file)
20+
files_to_upload.append(
21+
("files", (file_path.name, file_path.open("rb"), "application/xml")),
22+
)
23+
24+
try:
25+
yield files_to_upload
26+
finally:
27+
for _, (_, opened_file, _) in files_to_upload:
28+
opened_file.close()
29+
30+
31+
async def raise_for_status(response: httpx.Response) -> None:
32+
if response.is_error:
33+
await response.aread()
34+
details = response.text or "<empty_response>"
35+
console.log(f"[red]Error details: {details}[/]")
36+
37+
response.raise_for_status()
38+
39+
40+
def get_ci_issues_client(
41+
api_url: str,
42+
token: str,
43+
) -> httpx.AsyncClient:
44+
return utils.get_http_client(
45+
api_url,
46+
headers={
47+
"Authorization": f"Bearer {token}",
48+
},
49+
event_hooks={
50+
"request": [],
51+
"response": [raise_for_status],
52+
},
53+
)
54+
55+
56+
async def upload( # noqa: PLR0913, PLR0917
57+
api_url: str,
58+
token: str,
59+
repository: str,
60+
head_sha: str,
61+
job_name: str,
62+
provider: str | None,
63+
files: tuple[str, ...],
64+
) -> None:
65+
form_data = {
66+
"head_sha": head_sha,
67+
"name": job_name,
68+
}
69+
if provider is not None:
70+
form_data["provider"] = provider
71+
72+
async with get_ci_issues_client(api_url, token) as client:
73+
with get_files_to_upload(files) as files_to_upload:
74+
response = await client.post(
75+
f"v1/repos/{repository}/ci_issues_upload",
76+
data=form_data,
77+
files=files_to_upload,
78+
)
79+
80+
gigid = response.json()["gigid"]
81+
console.log(f"::notice title=CI Issues report::CI_ISSUE_GIGID={gigid}")
82+
console.log("[green]:tada: File(s) uploaded[/]")

mergify_cli/cli.py

+2
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from mergify_cli import VERSION
2727
from mergify_cli import console
2828
from mergify_cli import utils
29+
from mergify_cli.ci import cli as ci_cli_mod
2930
from mergify_cli.stack import cli as stack_cli_mod
3031

3132

@@ -91,6 +92,7 @@ def cli(
9192

9293

9394
cli.add_command(stack_cli_mod.stack)
95+
cli.add_command(ci_cli_mod.ci)
9496

9597

9698
def main() -> None:

mergify_cli/tests/ci_issues/__init__.py

Whitespace-only changes.

0 commit comments

Comments
 (0)