Skip to content

Adds additional support for Github enterprise usecases #548

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 9 commits into
base: main
Choose a base branch
from
Open
4 changes: 4 additions & 0 deletions action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -102,6 +102,10 @@ inputs:
notice, warning and error as annotation type. For more information look here:
https://docs.github.com/en/actions/using-workflows/workflow-commands-for-github-actions#setting-a-notice-message
default: warning
USE_GH_PAGES_HTML_URL:
description: >
If true, will use the GitHub Pages URL for the coverage report instead of the raw URL or an htmlpreview.github.io link.
default: false
VERBOSE:
description: >
Deprecated, see https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging
Expand Down
11 changes: 10 additions & 1 deletion coverage_comment/activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,21 @@ class ActivityNotFound(Exception):
def find_activity(
event_name: str,
is_default_branch: bool,
event_action: str | None = None,
) -> str:
"""Find the activity to perform based on the event type and payload."""
if event_name == "workflow_run":
return "post_comment"

if (event_name == "push" and is_default_branch) or event_name == "schedule":
if (
(event_name == "push" and is_default_branch)
or (
event_name == "pull_request"
and event_action == "merged"
and is_default_branch
)
or event_name == "schedule"
):
return "save_coverage_data_files"

if event_name not in {"pull_request", "push"}:
Expand Down
34 changes: 34 additions & 0 deletions coverage_comment/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import pathlib
import sys
import zipfile
from urllib.parse import urlparse

from coverage_comment import github_client, log

Expand Down Expand Up @@ -46,6 +47,39 @@ def get_repository_info(
)


def extract_github_host(api_url: str) -> str:
"""
Extracts the base GitHub web host URL from a GitHub API URL.

Args:
api_url: The GitHub API URL (e.g., 'https://api.github.com/...',
'https://my-ghe.company.com/api/v3/...').

Returns:
The base GitHub web host URL (e.g., 'https://github.com',
'https://my-ghe.company.com').
"""
parsed_url = urlparse(api_url)
scheme = parsed_url.scheme
netloc = parsed_url.netloc # This includes the domain and potentially the port

# Special case for GitHub.com API
if netloc == "api.github.com":
host_domain = "github.com"
# Special case for GitHub.com with port (less common but good practice)
elif netloc.startswith("api.github.com:"):
# Remove 'api.' prefix but keep the port
host_domain = netloc.replace("api.", "", 1)
Comment on lines +66 to +72
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
# Special case for GitHub.com API
if netloc == "api.github.com":
host_domain = "github.com"
# Special case for GitHub.com with port (less common but good practice)
elif netloc.startswith("api.github.com:"):
# Remove 'api.' prefix but keep the port
host_domain = netloc.replace("api.", "", 1)
# Special case for GitHub.com API (including possible port)
if re.match(r"api\.github\.com(:|$)", netloc):
# Remove 'api.' prefix but keep the port
host_domain = netloc.removeprefix("api.")

# General case for GitHub Enterprise (netloc is already the host:port)
else:
host_domain = netloc

# Reconstruct the host URL
host_url = f"{scheme}://{host_domain}"

return host_url


def download_artifact(
github: github_client.GitHub,
repository: str,
Expand Down
19 changes: 19 additions & 0 deletions coverage_comment/main.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
from __future__ import annotations

import functools
import json
import logging
import os
import sys
Expand Down Expand Up @@ -69,12 +70,23 @@
log.debug(f"Operating on {config.GITHUB_REF}")
gh = github_client.GitHub(session=github_session)
event_name = config.GITHUB_EVENT_NAME
event_path = config.GITHUB_EVENT_PATH
event_action = None

if event_path and os.path.exists(event_path):
with open(event_path) as event_file:
event_payload = json.load(event_file)
is_merged_pr_action = event_payload.get("pull_request", {}).get("merged", False)
if is_merged_pr_action:
event_action = "merged"

Check warning on line 81 in coverage_comment/main.py

View workflow job for this annotation

GitHub Actions / Run tests & display coverage

Missing coverage

Missing coverage on lines 77-81

repo_info = github.get_repository_info(
github=gh, repository=config.GITHUB_REPOSITORY
)
try:
activity = activity_module.find_activity(
event_name=event_name,
event_action=event_action,
is_default_branch=repo_info.is_default_branch(ref=config.GITHUB_REF),
)
except activity_module.ActivityNotFound:
Expand Down Expand Up @@ -176,6 +188,7 @@
max_files=config.MAX_FILES_IN_COMMENT,
minimum_green=config.MINIMUM_GREEN,
minimum_orange=config.MINIMUM_ORANGE,
github_host=github.extract_github_host(config.GITHUB_BASE_URL),
repo_name=config.GITHUB_REPOSITORY,
pr_number=config.GITHUB_PR_NUMBER,
base_template=template.read_template_file("comment.md.j2"),
Expand All @@ -195,6 +208,7 @@
max_files=None,
minimum_green=config.MINIMUM_GREEN,
minimum_orange=config.MINIMUM_ORANGE,
github_host=github.extract_github_host(config.GITHUB_BASE_URL),
repo_name=config.GITHUB_REPOSITORY,
pr_number=config.GITHUB_PR_NUMBER,
base_template=template.read_template_file("comment.md.j2"),
Expand Down Expand Up @@ -386,19 +400,24 @@
github_step_summary=config.GITHUB_STEP_SUMMARY,
)

github_host = github.extract_github_host(config.GITHUB_BASE_URL)
url_getter = functools.partial(
storage.get_raw_file_url,
github_host=github_host,
is_public=is_public,
repository=config.GITHUB_REPOSITORY,
branch=config.FINAL_COVERAGE_DATA_BRANCH,
)
readme_url = storage.get_repo_file_url(
github_host=github_host,
branch=config.FINAL_COVERAGE_DATA_BRANCH,
repository=config.GITHUB_REPOSITORY,
)
html_report_url = storage.get_html_report_url(
github_host=github_host,
branch=config.FINAL_COVERAGE_DATA_BRANCH,
repository=config.GITHUB_REPOSITORY,
use_gh_pages_html_url=config.USE_GH_PAGES_HTML_URL,
)
readme_file, log_message = communication.get_readme_and_log(
is_public=is_public,
Expand Down
2 changes: 2 additions & 0 deletions coverage_comment/settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,7 @@ class Config:
# (from https://docs.github.com/en/actions/learn-github-actions/variables#default-environment-variables )
GITHUB_REF: str
GITHUB_EVENT_NAME: str
GITHUB_EVENT_PATH: pathlib.Path | None = None
GITHUB_PR_RUN_ID: int | None
GITHUB_STEP_SUMMARY: pathlib.Path
COMMENT_TEMPLATE: str | None = None
Expand All @@ -62,6 +63,7 @@ class Config:
ANNOTATE_MISSING_LINES: bool = False
ANNOTATION_TYPE: str = "warning"
MAX_FILES_IN_COMMENT: int = 25
USE_GH_PAGES_HTML_URL: bool = False
VERBOSE: bool = False
# Only for debugging, not exposed in the action:
FORCE_WORKFLOW_RUN: bool = False
Expand Down
51 changes: 41 additions & 10 deletions coverage_comment/storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -132,16 +132,17 @@ def get_datafile_contents(


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

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


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


def get_html_report_url(repository: str, branch: str) -> str:
def get_html_report_url(
github_host: str,
repository: str,
branch: str,
use_gh_pages_html_url: bool = False,
) -> str:
"""
Computes the URL for an HTML report:
- If use_gh_pages_html_url is True:
* GitHub.com => https://<user_or_org>.github.io/<repo>/<pages_path>
* GitHub Enterprise => https://<host>/pages/<user_or_org>/<repo>/<pages_path>
- If use_gh_pages_html_url is False:
* GitHub.com => https://htmlpreview.github.io/?<readme_url>
* GitHub Enterprise => <readme_url>
"""
html_report_path = "htmlcov/index.html"
readme_url = get_repo_file_url(
repository=repository, branch=branch, path="/htmlcov/index.html"
github_host, repository=repository, branch=branch, path=html_report_path
)
return f"https://htmlpreview.github.io/?{readme_url}"

if github_host.endswith("github.com"):
if use_gh_pages_html_url:
user, repo = repository.split("/", 1)
return f"https://{user}.github.io/{repo}/{html_report_path}"
else:
return f"https://htmlpreview.github.io/?{readme_url}"
else:
# Assume GitHub Enterprise
if use_gh_pages_html_url:
return f"{github_host}/pages/{repository}/{html_report_path}"

# Always fallback to the raw readme_url
return readme_url
6 changes: 4 additions & 2 deletions coverage_comment/template.py
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,7 @@ def get_comment_markdown(
count_files: int,
minimum_green: decimal.Decimal,
minimum_orange: decimal.Decimal,
github_host: str,
repo_name: str,
pr_number: int,
base_template: str,
Expand All @@ -148,7 +149,7 @@ def get_comment_markdown(
env.filters["pluralize"] = pluralize
env.filters["compact"] = compact
env.filters["file_url"] = functools.partial(
get_file_url, repo_name=repo_name, pr_number=pr_number
get_file_url, github_host=github_host, repo_name=repo_name, pr_number=pr_number
)
env.filters["get_badge_color"] = functools.partial(
badge.get_badge_color,
Expand Down Expand Up @@ -304,11 +305,12 @@ def get_file_url(
filename: pathlib.Path,
lines: tuple[int, int] | None = None,
*,
github_host: str,
repo_name: str,
pr_number: int,
) -> str:
# 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
s = f"https://github.com/{repo_name}/pull/{pr_number}/files#diff-{hashlib.sha256(str(filename).encode('utf-8')).hexdigest()}"
s = f"{github_host}/{repo_name}/pull/{pr_number}/files#diff-{hashlib.sha256(str(filename).encode('utf-8')).hexdigest()}"

if lines is not None:
# R stands for Right side of the diff. But since we generate these links for new code we only need the right side.
Expand Down
4 changes: 2 additions & 2 deletions dev-env
Original file line number Diff line number Diff line change
Expand Up @@ -47,7 +47,7 @@ function create-repo(){
repo_dirname=$(basename ${GITHUB_REPOSITORY})
mv "${repo_dirname}/"{*,.*} .
rmdir "${repo_dirname}"
git pull --ff-only origin master
git pull --ff-only origin main
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch !

}

function delete-repo(){
Expand Down Expand Up @@ -142,7 +142,7 @@ function help(){
echo " coverage_comment" >&2
echo " Launch the action locally (no argument)" >&2
echo " pytest" >&2
echo " Launch the the tests on the example repo (generates the coverage data that the action uses)" >&2
echo " Launch the tests on the example repo (generates the coverage data that the action uses)" >&2
echo "" >&2

echo "Change configuration:" >&2
Expand Down
21 changes: 21 additions & 0 deletions tests/integration/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,27 @@ def test_get_repository_info(gh, session):
assert info == github.RepositoryInfo(default_branch="baz", visibility="public")


@pytest.mark.parametrize(
"api_url, expected",
[
("https://api.github.com/repos/foo/bar", "https://github.com"),
("https://api.github.com:8080/repos/foo/bar", "https://github.com:8080"),
("https://api.github.com/repos/foo/bar/issues", "https://github.com"),
(
"https://my-ghe.company.com/api/v3/repos/foo/bar",
"https://my-ghe.company.com",
),
(
"https://my-ghe.company.com/api/v3/repos/foo/bar/issues",
"https://my-ghe.company.com",
),
],
)
def test_extract_github_host(api_url, expected):
result = github.extract_github_host(api_url=api_url)
assert result == expected


def test_download_artifact(gh, session, zip_bytes):
artifacts = [
{"name": "bar", "id": 456},
Expand Down
20 changes: 12 additions & 8 deletions tests/unit/test_activity.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,18 +6,22 @@


@pytest.mark.parametrize(
"event_name, is_default_branch, expected_activity",
"event_name, event_action, is_default_branch, expected_activity",
[
("workflow_run", True, "post_comment"),
("push", True, "save_coverage_data_files"),
("push", False, "process_pr"),
("pull_request", True, "process_pr"),
("pull_request", False, "process_pr"),
("workflow_run", None, True, "post_comment"),
("push", None, True, "save_coverage_data_files"),
("push", None, False, "process_pr"),
("pull_request", "merged", True, "save_coverage_data_files"),
("pull_request", None, True, "process_pr"),
("pull_request", None, False, "process_pr"),
("schedule", None, False, "save_coverage_data_files"),
],
)
def test_find_activity(event_name, is_default_branch, expected_activity):
def test_find_activity(event_name, event_action, is_default_branch, expected_activity):
result = activity.find_activity(
event_name=event_name, is_default_branch=is_default_branch
event_name=event_name,
event_action=event_action,
is_default_branch=is_default_branch,
)
assert result == expected_activity

Expand Down
5 changes: 5 additions & 0 deletions tests/unit/test_settings.py
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ def test_config__from_environ__ok():
"GITHUB_REF": "master",
"GITHUB_OUTPUT": "foo.txt",
"GITHUB_EVENT_NAME": "pull",
"GITHUB_EVENT_PATH": pathlib.Path("test_event_path"),
"GITHUB_PR_RUN_ID": "123",
"GITHUB_STEP_SUMMARY": "step_summary",
"COMMENT_ARTIFACT_NAME": "baz",
Expand All @@ -56,6 +57,7 @@ def test_config__from_environ__ok():
GITHUB_REF="master",
GITHUB_OUTPUT=pathlib.Path("foo.txt"),
GITHUB_EVENT_NAME="pull",
GITHUB_EVENT_PATH=pathlib.Path("test_event_path"),
GITHUB_PR_RUN_ID=123,
GITHUB_STEP_SUMMARY=pathlib.Path("step_summary"),
COMMENT_ARTIFACT_NAME="baz",
Expand All @@ -82,6 +84,7 @@ def test_config__verbose_deprecated(get_logs):
"GITHUB_REPOSITORY": "owner/repo",
"GITHUB_REF": "master",
"GITHUB_EVENT_NAME": "pull",
"GITHUB_EVENT_PATH": pathlib.Path("test_event_path"),
"GITHUB_PR_RUN_ID": "123",
"GITHUB_STEP_SUMMARY": "step_summary",
"VERBOSE": "true",
Expand All @@ -92,6 +95,7 @@ def test_config__verbose_deprecated(get_logs):
GITHUB_REPOSITORY="owner/repo",
GITHUB_REF="master",
GITHUB_EVENT_NAME="pull",
GITHUB_EVENT_PATH=pathlib.Path("test_event_path"),
GITHUB_PR_RUN_ID=123,
GITHUB_STEP_SUMMARY=pathlib.Path("step_summary"),
VERBOSE=False,
Expand All @@ -107,6 +111,7 @@ def config():
"GITHUB_REPOSITORY": "owner/repo",
"GITHUB_REF": "master",
"GITHUB_EVENT_NAME": "pull",
"GITHUB_EVENT_PATH": pathlib.Path("test_event_path"),
"GITHUB_PR_RUN_ID": 123,
"GITHUB_STEP_SUMMARY": pathlib.Path("step_summary"),
"COMMENT_ARTIFACT_NAME": "baz",
Expand Down
Loading
Loading