Skip to content

Commit 9735132

Browse files
daniel-anyaewjoachim
authored andcommitted
Adds additional support for GitHub Enterprise in coverage artifacts
Adds support for figuring out a github host from the `GITHUB_BASE_URL` config and using this value in generated coverage artifacts rather than hardcoding them to "github.com"
1 parent 14fb28a commit 9735132

File tree

6 files changed

+104
-33
lines changed

6 files changed

+104
-33
lines changed

coverage_comment/github.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
import pathlib
77
import sys
88
import zipfile
9+
from urllib.parse import urlparse
910

1011
from coverage_comment import github_client, log
1112

@@ -45,6 +46,41 @@ def get_repository_info(
4546
default_branch=response.default_branch, visibility=response.visibility
4647
)
4748

49+
def extract_github_host(api_url: str) -> str:
50+
"""
51+
Extracts the base GitHub web host URL from a GitHub API URL.
52+
53+
Args:
54+
api_url: The GitHub API URL (e.g., 'https://api.github.com/...',
55+
'https://my-ghe.company.com/api/v3/...').
56+
57+
Returns:
58+
The base GitHub web host URL (e.g., 'https://github.com',
59+
'https://my-ghe.company.com').
60+
"""
61+
try:
62+
parsed_url = urlparse(api_url)
63+
scheme = parsed_url.scheme
64+
netloc = parsed_url.netloc # This includes the domain and potentially the port
65+
66+
# Special case for GitHub.com API
67+
if netloc == 'api.github.com':
68+
host_domain = 'github.com'
69+
# Special case for GitHub.com with port (less common but good practice)
70+
elif netloc.startswith('api.github.com:'):
71+
# Remove 'api.' prefix but keep the port
72+
host_domain = netloc.replace('api.', '', 1)
73+
# General case for GitHub Enterprise (netloc is already the host:port)
74+
else:
75+
host_domain = netloc
76+
77+
# Reconstruct the host URL
78+
host_url = f"{scheme}://{host_domain}"
79+
80+
return host_url
81+
except Exception as e:
82+
log.error(f"Error parsing URL {api_url}: {e}")
83+
return ""
4884

4985
def download_artifact(
5086
github: github_client.GitHub,

coverage_comment/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -201,6 +201,7 @@ def process_pr(
201201
max_files=config.MAX_FILES_IN_COMMENT,
202202
minimum_green=config.MINIMUM_GREEN,
203203
minimum_orange=config.MINIMUM_ORANGE,
204+
github_host=github.extract_github_host(config.GITHUB_BASE_URL),
204205
repo_name=config.GITHUB_REPOSITORY,
205206
pr_number=config.GITHUB_PR_NUMBER,
206207
base_template=template.read_template_file("comment.md.j2"),
@@ -220,6 +221,7 @@ def process_pr(
220221
max_files=None,
221222
minimum_green=config.MINIMUM_GREEN,
222223
minimum_orange=config.MINIMUM_ORANGE,
224+
github_host=github.extract_github_host(config.GITHUB_BASE_URL),
223225
repo_name=config.GITHUB_REPOSITORY,
224226
pr_number=config.GITHUB_PR_NUMBER,
225227
base_template=template.read_template_file("comment.md.j2"),
@@ -411,17 +413,21 @@ def save_coverage_data_files(
411413
github_step_summary=config.GITHUB_STEP_SUMMARY,
412414
)
413415

416+
github_host = github.extract_github_host(config.GITHUB_BASE_URL)
414417
url_getter = functools.partial(
415418
storage.get_raw_file_url,
419+
github_host=github_host,
416420
is_public=is_public,
417421
repository=config.GITHUB_REPOSITORY,
418422
branch=config.FINAL_COVERAGE_DATA_BRANCH,
419423
)
420424
readme_url = storage.get_repo_file_url(
425+
github_host=github_host,
421426
branch=config.FINAL_COVERAGE_DATA_BRANCH,
422427
repository=config.GITHUB_REPOSITORY,
423428
)
424429
html_report_url = storage.get_html_report_url(
430+
github_host=github_host,
425431
branch=config.FINAL_COVERAGE_DATA_BRANCH,
426432
repository=config.GITHUB_REPOSITORY,
427433
)

coverage_comment/storage.py

Lines changed: 14 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -132,16 +132,17 @@ def get_datafile_contents(
132132

133133

134134
def get_raw_file_url(
135+
github_host: str,
135136
repository: str,
136137
branch: str,
137138
path: pathlib.Path,
138139
is_public: bool,
139140
):
140-
if not is_public:
141-
# If the repository is private, then the real links to raw.githubusercontents.com
142-
# will be short-lived. In this case, it's better to keep an URL that will
143-
# redirect to the correct URL just when asked.
144-
return f"https://github.com/{repository}/raw/{branch}/{path}"
141+
if (not is_public) or (not github_host.endswith("github.com")):
142+
# If the repository is private or hosted on a github enterprise instance,
143+
# then the real links to raw.githubusercontents.com will be short-lived.
144+
# In this case, it's better to keep an URL that will redirect to the correct URL just when asked.
145+
return f"{github_host}/{repository}/raw/{branch}/{path}"
145146

146147
# Otherwise, we can access the file directly. (shields.io doesn't like the
147148
# github.com domain)
@@ -154,7 +155,7 @@ def get_raw_file_url(
154155
# seconds.
155156

156157

157-
def get_repo_file_url(repository: str, branch: str, path: str = "/") -> str:
158+
def get_repo_file_url(github_host: str, repository: str, branch: str, path: str = "/") -> str:
158159
"""
159160
Computes the GitHub Web UI URL for a given path:
160161
If the path is empty or ends with a slash, it will be interpreted as a folder,
@@ -166,11 +167,14 @@ def get_repo_file_url(repository: str, branch: str, path: str = "/") -> str:
166167
# See test_get_repo_file_url for precise specifications
167168
path = "/" + path.lstrip("/")
168169
part = "tree" if path.endswith("/") else "blob"
169-
return f"https://github.com/{repository}/{part}/{branch}{path}".rstrip("/")
170+
return f"{github_host}/{repository}/{part}/{branch}{path}".rstrip("/")
170171

171172

172-
def get_html_report_url(repository: str, branch: str) -> str:
173+
def get_html_report_url(github_host: str, repository: str, branch: str) -> str:
173174
readme_url = get_repo_file_url(
174-
repository=repository, branch=branch, path="/htmlcov/index.html"
175+
github_host, repository=repository, branch=branch, path="/htmlcov/index.html"
175176
)
176-
return f"https://htmlpreview.github.io/?{readme_url}"
177+
if github_host.endswith("github.com"):
178+
return f"https://htmlpreview.github.io/?{readme_url}"
179+
return readme_url
180+

coverage_comment/template.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ def get_comment_markdown(
130130
count_files: int,
131131
minimum_green: decimal.Decimal,
132132
minimum_orange: decimal.Decimal,
133+
github_host: str,
133134
repo_name: str,
134135
pr_number: int,
135136
base_template: str,
@@ -148,7 +149,7 @@ def get_comment_markdown(
148149
env.filters["pluralize"] = pluralize
149150
env.filters["compact"] = compact
150151
env.filters["file_url"] = functools.partial(
151-
get_file_url, repo_name=repo_name, pr_number=pr_number
152+
get_file_url, github_host=github_host, repo_name=repo_name, pr_number=pr_number
152153
)
153154
env.filters["get_badge_color"] = functools.partial(
154155
badge.get_badge_color,
@@ -304,11 +305,12 @@ def get_file_url(
304305
filename: pathlib.Path,
305306
lines: tuple[int, int] | None = None,
306307
*,
308+
github_host: str,
307309
repo_name: str,
308310
pr_number: int,
309311
) -> str:
310312
# To link to a file in a PR, GitHub uses the link to the file overview combined with a SHA256 hash of the file path
311-
s = f"https://github.com/{repo_name}/pull/{pr_number}/files#diff-{hashlib.sha256(str(filename).encode('utf-8')).hexdigest()}"
313+
s = f"{github_host}/{repo_name}/pull/{pr_number}/files#diff-{hashlib.sha256(str(filename).encode('utf-8')).hexdigest()}"
312314

313315
if lines is not None:
314316
# R stands for Right side of the diff. But since we generate these links for new code we only need the right side.

tests/unit/test_storage.py

Lines changed: 34 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -154,14 +154,16 @@ def test_get_datafile_contents(gh, session):
154154

155155

156156
@pytest.mark.parametrize(
157-
"is_public, expected",
157+
"github_host, is_public, expected",
158158
[
159-
(False, "https://github.com/foo/bar/raw/baz/qux"),
160-
(True, "https://raw.githubusercontent.com/foo/bar/baz/qux"),
159+
("https://github.com", False, "https://github.com/foo/bar/raw/baz/qux"),
160+
("https://github.com", True, "https://raw.githubusercontent.com/foo/bar/baz/qux"),
161+
("https://github.mycompany.com", True, "https://github.mycompany.com/foo/bar/raw/baz/qux"),
161162
],
162163
)
163-
def test_get_raw_file_url(is_public, expected):
164+
def test_get_raw_file_url(github_host, is_public, expected):
164165
result = storage.get_raw_file_url(
166+
github_host=github_host,
165167
repository="foo/bar",
166168
branch="baz",
167169
path=pathlib.Path("qux"),
@@ -171,29 +173,40 @@ def test_get_raw_file_url(is_public, expected):
171173

172174

173175
@pytest.mark.parametrize(
174-
"path, expected",
176+
"github_host, path, expected",
175177
[
176-
("", "https://github.com/foo/bar/tree/baz"),
177-
("/", "https://github.com/foo/bar/tree/baz"),
178-
("qux", "https://github.com/foo/bar/blob/baz/qux"), # blob
179-
("qux/", "https://github.com/foo/bar/tree/baz/qux"),
180-
("/qux", "https://github.com/foo/bar/blob/baz/qux"), # blob
181-
("/qux/", "https://github.com/foo/bar/tree/baz/qux"),
178+
("https://github.com", "", "https://github.com/foo/bar/tree/baz"),
179+
("https://github.com", "/", "https://github.com/foo/bar/tree/baz"),
180+
("https://github.com", "qux", "https://github.com/foo/bar/blob/baz/qux"), # blob
181+
("https://github.com", "qux/", "https://github.com/foo/bar/tree/baz/qux"),
182+
("https://github.mycompany.com", "/qux", "https://github.mycompany.com/foo/bar/blob/baz/qux"), # blob
183+
("https://github.mycompany.com", "/qux/", "https://github.mycompany.com/foo/bar/tree/baz/qux"),
182184
],
183185
)
184-
def test_get_repo_file_url(path, expected):
185-
result = storage.get_repo_file_url(repository="foo/bar", branch="baz", path=path)
186+
def test_get_repo_file_url(github_host, path, expected):
187+
result = storage.get_repo_file_url(github_host=github_host, repository="foo/bar", branch="baz", path=path)
186188

187189
assert result == expected
188190

191+
@pytest.mark.parametrize(
192+
"github_host",
193+
[
194+
"https://github.com",
195+
"https://github.mycompany.com",
196+
],
197+
)
198+
def test_get_repo_file_url__no_path(github_host):
199+
result = storage.get_repo_file_url(github_host=github_host, repository="foo/bar", branch="baz")
189200

190-
def test_get_repo_file_url__no_path():
191-
result = storage.get_repo_file_url(repository="foo/bar", branch="baz")
192-
193-
assert result == "https://github.com/foo/bar/tree/baz"
194-
201+
assert result == f"{github_host}/foo/bar/tree/baz"
195202

196-
def test_get_html_report_url():
197-
result = storage.get_html_report_url(repository="foo/bar", branch="baz")
198-
expected = "https://htmlpreview.github.io/?https://github.com/foo/bar/blob/baz/htmlcov/index.html"
203+
@pytest.mark.parametrize(
204+
"github_host, expected",
205+
[
206+
("https://github.com", "https://htmlpreview.github.io/?https://github.com/foo/bar/blob/baz/htmlcov/index.html"),
207+
("https://github.mycompany.com", "https://github.mycompany.com/foo/bar/blob/baz/htmlcov/index.html"),
208+
],
209+
)
210+
def test_get_html_report_url(github_host, expected):
211+
result = storage.get_html_report_url(github_host=github_host, repository="foo/bar", branch="baz")
199212
assert result == expected

tests/unit/test_template.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ def test_get_comment_markdown(coverage_obj, diff_coverage_obj):
2727
minimum_green=decimal.Decimal("100"),
2828
minimum_orange=decimal.Decimal("70"),
2929
marker="<!-- foo -->",
30+
github_host="https://github.com",
3031
repo_name="org/repo",
3132
pr_number=1,
3233
base_template="""
@@ -66,6 +67,7 @@ def test_template(coverage_obj, diff_coverage_obj):
6667
files=files,
6768
count_files=total,
6869
max_files=25,
70+
github_host="https://github.com",
6971
repo_name="org/repo",
7072
pr_number=5,
7173
base_template=template.read_template_file("comment.md.j2"),
@@ -195,6 +197,7 @@ def test_template_full(make_coverage, make_coverage_and_diff):
195197
minimum_green=decimal.Decimal("100"),
196198
minimum_orange=decimal.Decimal("70"),
197199
marker="<!-- foo -->",
200+
github_host="https://github.com",
198201
repo_name="org/repo",
199202
pr_number=12,
200203
base_template=template.read_template_file("comment.md.j2"),
@@ -257,6 +260,7 @@ def test_template__no_previous(coverage_obj_no_branch, diff_coverage_obj):
257260
minimum_green=decimal.Decimal("100"),
258261
minimum_orange=decimal.Decimal("70"),
259262
marker="<!-- foo -->",
263+
github_host="https://github.com",
260264
repo_name="org/repo",
261265
pr_number=3,
262266
base_template=template.read_template_file("comment.md.j2"),
@@ -309,6 +313,7 @@ def test_template__max_files(coverage_obj_more_files, diff_coverage_obj_more_fil
309313
previous_coverage_rate=decimal.Decimal("0.92"),
310314
minimum_green=decimal.Decimal("79"),
311315
minimum_orange=decimal.Decimal("40"),
316+
github_host="https://github.com",
312317
repo_name="org/repo",
313318
pr_number=5,
314319
max_files=1,
@@ -340,6 +345,7 @@ def test_template__no_max_files(coverage_obj_more_files, diff_coverage_obj_more_
340345
previous_coverage_rate=decimal.Decimal("0.92"),
341346
minimum_green=decimal.Decimal("79"),
342347
minimum_orange=decimal.Decimal("40"),
348+
github_host="https://github.com",
343349
repo_name="org/repo",
344350
pr_number=5,
345351
max_files=None,
@@ -374,6 +380,7 @@ def test_template__no_files(coverage_obj, diff_coverage_obj_more_files):
374380
previous_coverage_rate=decimal.Decimal("0.92"),
375381
minimum_green=decimal.Decimal("79"),
376382
minimum_orange=decimal.Decimal("40"),
383+
github_host="https://github.com",
377384
repo_name="org/repo",
378385
pr_number=5,
379386
max_files=25,
@@ -412,6 +419,7 @@ def test_template__no_marker(coverage_obj, diff_coverage_obj):
412419
previous_coverage_rate=decimal.Decimal("0.92"),
413420
minimum_green=decimal.Decimal("100"),
414421
minimum_orange=decimal.Decimal("70"),
422+
github_host="https://github.com",
415423
repo_name="org/repo",
416424
pr_number=1,
417425
base_template=template.read_template_file("comment.md.j2"),
@@ -432,6 +440,7 @@ def test_template__broken_template(coverage_obj, diff_coverage_obj):
432440
previous_coverage_rate=decimal.Decimal("0.92"),
433441
minimum_green=decimal.Decimal("100"),
434442
minimum_orange=decimal.Decimal("70"),
443+
github_host="https://github.com",
435444
repo_name="org/repo",
436445
pr_number=1,
437446
base_template=template.read_template_file("comment.md.j2"),
@@ -496,6 +505,7 @@ def test_get_file_url(filepath, lines, expected):
496505
result = template.get_file_url(
497506
filename=filepath,
498507
lines=lines,
508+
github_host="https://github.com",
499509
repo_name="py-cov-action/python-coverage-comment-action",
500510
pr_number=33,
501511
)

0 commit comments

Comments
 (0)