Skip to content

Commit

Permalink
feat: extend support to on-prem GitHub Enterprise Server (python-sema…
Browse files Browse the repository at this point in the history
…ntic-release#896)

* test(github): adjust init test to match the Enterprise Server api url

* feat(github): extend support to on-prem GitHub Enterprise Server

  Resolves: python-semantic-release#895
  • Loading branch information
codejedi365 authored Apr 20, 2024
1 parent ba3e326 commit 4fcb737
Show file tree
Hide file tree
Showing 3 changed files with 163 additions and 35 deletions.
113 changes: 97 additions & 16 deletions semantic_release/hvcs/github.py
Original file line number Diff line number Diff line change
Expand Up @@ -50,18 +50,28 @@ class Github(HvcsBase):
GitHub HVCS interface for interacting with GitHub repositories
This class supports the following products:
- GitHub Free, Pro, & Team
- GitHub Enterprise Cloud
This class does not support the following products:
- GitHub Enterprise Server (on-premises installations)
This interface does its best to detect which product is configured based
on the provided domain. If it is the official `github.com`, the default
domain, then it is considered as GitHub Enterprise Cloud which uses the
subdomain `api.github.com` for api communication.
If the provided domain is anything else, than it is assumed to be communicating
with an on-premise or 3rd-party maintained GitHub instance which matches with
the GitHub Enterprise Server product. The on-prem server product uses a
path prefix for handling api requests which is configured to be
`server.domain/api/v3` based on the documentation in April 2024.
"""

# TODO: Add support for GitHub Enterprise Server (on-premises installations)
# DEFAULT_ONPREM_API_PATH = "/api/v3"
DEFAULT_DOMAIN = "github.com"
DEFAULT_API_SUBDOMAIN_PREFIX = "api"
DEFAULT_API_DOMAIN = f"{DEFAULT_API_SUBDOMAIN_PREFIX}.{DEFAULT_DOMAIN}"
DEFAULT_API_PATH_CLOUD = "/" # no path prefix!
DEFAULT_API_PATH_ONPREM = "/api/v3"
DEFAULT_ENV_TOKEN_NAME = "GH_TOKEN" # noqa: S105

def __init__(
Expand Down Expand Up @@ -117,10 +127,15 @@ def __init__(
# infer from Domain url and prepend the default api subdomain
**{
**self.hvcs_domain._asdict(),
"host": f"{self.DEFAULT_API_SUBDOMAIN_PREFIX}.{self.hvcs_domain.host}",
"path": "",
"host": self.hvcs_domain.host,
"path": str(
PurePosixPath(
str.lstrip(self.hvcs_domain.path or "", "/") or "/",
self.DEFAULT_API_PATH_ONPREM.lstrip("/"),
)
),
}
).url
).url.rstrip("/")
)

if api_domain_parts.scheme == "http" and not allow_insecure:
Expand All @@ -138,16 +153,68 @@ def __init__(
"Only http and https are supported."
)

# Strip any auth, query or fragment from the domain
self.api_url = parse_url(
# As GitHub Enterprise Cloud and GitHub Enterprise Server (on-prem) have different api locations
# lets check what we have been given and set the api url accordingly
# NOTE: Github Server (on premise) uses a path prefix '/api/v3' for the api
# while GitHub Enterprise Cloud uses a separate subdomain as the base
is_github_cloud = bool(
self.hvcs_domain.url == f"https://{self.DEFAULT_DOMAIN}"
)

# Calculate out the api url that we expect for GitHub Cloud
default_cloud_api_url = parse_url(
Url(
scheme=api_domain_parts.scheme,
host=api_domain_parts.host,
port=api_domain_parts.port,
path=str(PurePosixPath(api_domain_parts.path or "/")),
# set api domain and append the default api path
**{
**self.hvcs_domain._asdict(),
"host": f"{self.DEFAULT_API_DOMAIN}",
"path": self.DEFAULT_API_PATH_CLOUD,
}
).url.rstrip("/")
)

if (
is_github_cloud
and hvcs_api_domain
and api_domain_parts.url not in default_cloud_api_url.url
):
# Api was provied but is not a subset of the expected one, raise an error
# we check for a subset because the user may not have provided the full api path
# but the correct domain. If they didn't, then we are erroring out here.
raise ValueError(
f"Invalid api domain {api_domain_parts.url} for GitHub Enterprise Cloud. "
f"Expected {default_cloud_api_url.url}."
)

# Set the api url to the default cloud one if we are on cloud, otherwise
# use the verified api domain for a on-prem server
self.api_url = (
default_cloud_api_url
if is_github_cloud
else parse_url(
# Strip any auth, query or fragment from the domain
Url(
scheme=api_domain_parts.scheme,
host=api_domain_parts.host,
port=api_domain_parts.port,
path=str(
PurePosixPath(
# pass any custom server prefix path but ensure we don't
# double up the api path in the case the user provided it
str.replace(
api_domain_parts.path or "",
self.DEFAULT_API_PATH_ONPREM,
"",
).lstrip("/")
or "/",
# apply the on-prem api path
self.DEFAULT_API_PATH_ONPREM.lstrip("/"),
)
),
).url.rstrip("/")
)
)

@lru_cache(maxsize=1)
def _get_repository_owner_and_name(self) -> tuple[str, str]:
# Github actions context
Expand Down Expand Up @@ -419,7 +486,7 @@ def _derive_url(
lambda x: x[1] is not None,
{
"auth": auth,
"path": str(PurePosixPath("/", path)),
"path": str(PurePosixPath("/", path.lstrip('/'))),
"query": query,
"fragment": fragment,
}.items(),
Expand All @@ -439,7 +506,14 @@ def create_server_url(
query: str | None = None,
fragment: str | None = None,
) -> str:
return self._derive_url(self.hvcs_domain, path, auth, query, fragment)
# Ensure any path prefix is transfered but not doubled up on the derived url
return self._derive_url(
self.hvcs_domain,
path=f"{self.hvcs_domain.path or ''}/{path.lstrip(self.hvcs_domain.path)}",
auth=auth,
query=query,
fragment=fragment,
)

def create_api_url(
self,
Expand All @@ -448,4 +522,11 @@ def create_api_url(
query: str | None = None,
fragment: str | None = None,
) -> str:
return self._derive_url(self.api_url, endpoint, auth, query, fragment)
# Ensure any api path prefix is transfered but not doubled up on the derived api url
return self._derive_url(
self.api_url,
path=f"{self.api_url.path or ''}/{endpoint.lstrip(self.api_url.path)}",
auth=auth,
query=query,
fragment=fragment,
)
4 changes: 1 addition & 3 deletions tests/command_line/test_changelog.py
Original file line number Diff line number Diff line change
Expand Up @@ -242,9 +242,7 @@ def test_changelog_post_to_release(
session.mount("https://", mock_adapter)

expected_request_url = "{api_url}/repos/{owner}/{repo_name}/releases".format(
# TODO: Fix as this is likely not correct given a custom domain and the
# use of GitHub which would be GitHub Enterprise Server which we don't yet support
api_url=f"https://api.{EXAMPLE_HVCS_DOMAIN}", # GitHub API URL
api_url=f"https://{EXAMPLE_HVCS_DOMAIN}/api/v3", # GitHub API URL
owner=EXAMPLE_REPO_OWNER,
repo_name=EXAMPLE_REPO_NAME,
)
Expand Down
81 changes: 65 additions & 16 deletions tests/unit/semantic_release/hvcs/test_github.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,12 +55,61 @@ def default_gh_client() -> Generator[Github, None, None]:
),
[
(
# Default values
# Default values (GitHub Enterprise Cloud)
{},
None,
None,
f"https://{Github.DEFAULT_DOMAIN}",
f"https://{Github.DEFAULT_API_DOMAIN}",
"https://github.com",
"https://api.github.com",
False,
),
(
# Explicitly set default values (GitHub Enterprise Cloud)
{},
Github.DEFAULT_DOMAIN,
Github.DEFAULT_API_DOMAIN,
"https://github.com",
"https://api.github.com",
False,
),
(
# Pull both locations from environment (GitHub Actions on Cloud)
{
"GITHUB_SERVER_URL": f"https://{Github.DEFAULT_DOMAIN}",
"GITHUB_API_URL": f"https://{Github.DEFAULT_API_DOMAIN}",
},
None,
None,
"https://github.com",
"https://api.github.com",
False,
),
(
# Explicitly set custom values with full api path
{},
EXAMPLE_HVCS_DOMAIN,
f"{EXAMPLE_HVCS_DOMAIN}{Github.DEFAULT_API_PATH_ONPREM}",
f"https://{EXAMPLE_HVCS_DOMAIN}",
f"https://{EXAMPLE_HVCS_DOMAIN}{Github.DEFAULT_API_PATH_ONPREM}",
False,
),
(
# Explicitly defined api as subdomain
# POSSIBLY WRONG ASSUMPTION of Api path for GitHub Enterprise Server (On Prem)
{},
f"https://{EXAMPLE_HVCS_DOMAIN}",
f"https://api.{EXAMPLE_HVCS_DOMAIN}",
f"https://{EXAMPLE_HVCS_DOMAIN}",
f"https://api.{EXAMPLE_HVCS_DOMAIN}{Github.DEFAULT_API_PATH_ONPREM}",
False,
),
(
# Custom domain with path prefix
{},
"special.custom.server/vcs",
None,
"https://special.custom.server/vcs",
"https://special.custom.server/vcs/api/v3",
False,
),
(
Expand All @@ -69,19 +118,19 @@ def default_gh_client() -> Generator[Github, None, None]:
None,
None,
"https://special.custom.server",
"https://api.special.custom.server",
"https://special.custom.server/api/v3",
False,
),
(
# Pull both locations from environment
# Pull both locations from environment (On-prem Actions Env)
{
"GITHUB_SERVER_URL": "https://special.custom.server/",
"GITHUB_API_URL": "https://api2.special.custom.server/",
"GITHUB_API_URL": "https://special.custom.server/api/v3",
},
None,
None,
"https://special.custom.server",
"https://api2.special.custom.server",
"https://special.custom.server/api/v3",
False,
),
(
Expand All @@ -91,25 +140,25 @@ def default_gh_client() -> Generator[Github, None, None]:
f"https://{EXAMPLE_HVCS_DOMAIN}",
None,
f"https://{EXAMPLE_HVCS_DOMAIN}",
f"https://api.{EXAMPLE_HVCS_DOMAIN}",
f"https://{EXAMPLE_HVCS_DOMAIN}/api/v3",
False,
),
(
# Ignore environment & use provided parameter value (ie from user config)
{"GITHUB_API_URL": "https://api.special.custom.server/"},
f"https://{EXAMPLE_HVCS_DOMAIN}",
f"https://api.{EXAMPLE_HVCS_DOMAIN}",
f"https://{EXAMPLE_HVCS_DOMAIN}/api/v3",
f"https://{EXAMPLE_HVCS_DOMAIN}",
f"https://api.{EXAMPLE_HVCS_DOMAIN}",
f"https://{EXAMPLE_HVCS_DOMAIN}/api/v3",
False,
),
(
# Allow insecure http connections explicitly
{},
f"http://{EXAMPLE_HVCS_DOMAIN}",
f"http://api.{EXAMPLE_HVCS_DOMAIN}",
f"http://{EXAMPLE_HVCS_DOMAIN}/api/v3",
f"http://{EXAMPLE_HVCS_DOMAIN}",
f"http://api.{EXAMPLE_HVCS_DOMAIN}",
f"http://{EXAMPLE_HVCS_DOMAIN}/api/v3",
True,
),
(
Expand All @@ -118,16 +167,16 @@ def default_gh_client() -> Generator[Github, None, None]:
f"http://{EXAMPLE_HVCS_DOMAIN}",
None,
f"http://{EXAMPLE_HVCS_DOMAIN}",
f"http://api.{EXAMPLE_HVCS_DOMAIN}",
f"http://{EXAMPLE_HVCS_DOMAIN}/api/v3",
True,
),
(
# Infer insecure connection from user configuration
{},
EXAMPLE_HVCS_DOMAIN,
f"api.{EXAMPLE_HVCS_DOMAIN}",
EXAMPLE_HVCS_DOMAIN,
f"http://{EXAMPLE_HVCS_DOMAIN}",
f"http://api.{EXAMPLE_HVCS_DOMAIN}",
f"http://{EXAMPLE_HVCS_DOMAIN}/api/v3",
True,
),
(
Expand All @@ -136,7 +185,7 @@ def default_gh_client() -> Generator[Github, None, None]:
EXAMPLE_HVCS_DOMAIN,
None,
f"http://{EXAMPLE_HVCS_DOMAIN}",
f"http://api.{EXAMPLE_HVCS_DOMAIN}",
f"http://{EXAMPLE_HVCS_DOMAIN}/api/v3",
True,
),
],
Expand Down

0 comments on commit 4fcb737

Please sign in to comment.