Repository: semantic-release/github (js)

Issue:
Missing success comments on issues
I have just noticed that `@semantic-release/github` does not comment on resolved issues. This has worked at some point in the past but seems to be broken for quite some time.

My initial suspicion is that we do not pass the correct GitHub URL to `issue-parser` at https://github.com/semantic-release/github/blob/master/lib/success.js#L56 and therefore cannot determine the correct org/repo tuple at https://github.com/semantic-release/github/blob/master/lib/success.js#L116.

Examples:

- https://github.com/semantic-release/gitlab/issues/704
- https://github.com/semantic-release/gitlab/issues/684

Pull request:
fix: introduce dedicated option for GitHub API endpoint
Server and API endpoints can be different. Some code parts, e.g. issue parsing, need the server endpoint and we should make this explicit. The existing `githubApiPathPrefix` option is not sufficient when the API is served from a different host (like `api.github.com`).

Fixes #827

:hammer_and_pick: with :heart: by [Siemens](https://opensource.siemens.com/)

Patch:
diff --git a/README.md b/README.md
index 69428aa..c222204 100644
--- a/README.md
+++ b/README.md
@@ -71,18 +71,20 @@ When using the _GITHUB_TOKEN_, the **minimum required permissions** are:
 
 ### Environment variables
 
-| Variable                                     | Description                                               |
-| -------------------------------------------- | --------------------------------------------------------- |
-| `GH_TOKEN` or `GITHUB_TOKEN`                 | **Required.** The token used to authenticate with GitHub. |
-| `GITHUB_API_URL` or `GH_URL` or `GITHUB_URL` | The GitHub Enterprise endpoint.                           |
-| `GH_PREFIX` or `GITHUB_PREFIX`               | The GitHub Enterprise API prefix.                         |
+| Variable                       | Description                                                         |
+| ------------------------------ | ------------------------------------------------------------------- |
+| `GITHUB_TOKEN` or `GH_TOKEN`   | **Required.** The token used to authenticate with GitHub.           |
+| `GITHUB_URL` or `GH_URL`       | The GitHub server endpoint.                                         |
+| `GITHUB_PREFIX` or `GH_PREFIX` | The GitHub API prefix, relative to `GITHUB_URL`.                    |
+| `GITHUB_API_URL`               | The GitHub API endpoint. Note that this overwrites `GITHUB_PREFIX`. |
 
 ### Options
 
 | Option                   | Description                                                                                                                                                                                            | Default                                                                                                                                              |
 | ------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
-| `githubUrl`              | The GitHub Enterprise endpoint.                                                                                                                                                                        | `GH_URL` or `GITHUB_URL` environment variable.                                                                                                       |
-| `githubApiPathPrefix`    | The GitHub Enterprise API prefix.                                                                                                                                                                      | `GH_PREFIX` or `GITHUB_PREFIX` environment variable.                                                                                                 |
+| `githubUrl`              | The GitHub server endpoint.                                                                                                                                                                            | `GH_URL` or `GITHUB_URL` environment variable.                                                                                                       |
+| `githubApiPathPrefix`    | The GitHub API prefix, relative to `githubUrl`.                                                                                                                                                        | `GH_PREFIX` or `GITHUB_PREFIX` environment variable.                                                                                                 |
+| `githubApiUrl`           | The GitHub API endpoint. Note that this overwrites `githubApiPathPrefix`.                                                                                                                              | `GITHUB_API_URL` environment variable.                                                                                                               |
 | `proxy`                  | The proxy to use to access the GitHub API. Set to `false` to disable usage of proxy. See [proxy](#proxy).                                                                                              | `HTTP_PROXY` environment variable.                                                                                                                   |
 | `assets`                 | An array of files to upload to the release. See [assets](#assets).                                                                                                                                     | -                                                                                                                                                    |
 | `successComment`         | The comment to add to each issue and pull request resolved by the release. Set to `false` to disable commenting on issues and pull requests. See [successComment](#successcomment).                    | `:tada: This issue has been resolved in version ${nextRelease.version} :tada:\n\nThe release is available on [GitHub release](<github_release_url>)` |
diff --git a/lib/add-channel.js b/lib/add-channel.js
index 65703a7..6069853 100644
--- a/lib/add-channel.js
+++ b/lib/add-channel.js
@@ -15,16 +15,15 @@ export default async function addChannel(pluginConfig, context, { Octokit }) {
     nextRelease: { name, gitTag, notes },
     logger,
   } = context;
-  const { githubToken, githubUrl, githubApiPathPrefix, proxy } = resolveConfig(
-    pluginConfig,
-    context,
-  );
+  const { githubToken, githubUrl, githubApiPathPrefix, githubApiUrl, proxy } =
+    resolveConfig(pluginConfig, context);
   const { owner, repo } = parseGithubUrl(repositoryUrl);
   const octokit = new Octokit(
     toOctokitOptions({
       githubToken,
       githubUrl,
       githubApiPathPrefix,
+      githubApiUrl,
       proxy,
     }),
   );
diff --git a/lib/fail.js b/lib/fail.js
index 6110928..9439b87 100644
--- a/lib/fail.js
+++ b/lib/fail.js
@@ -21,6 +21,7 @@ export default async function fail(pluginConfig, context, { Octokit }) {
     githubToken,
     githubUrl,
     githubApiPathPrefix,
+    githubApiUrl,
     proxy,
     failComment,
     failTitle,
@@ -32,7 +33,13 @@ export default async function fail(pluginConfig, context, { Octokit }) {
     logger.log("Skip issue creation.");
   } else {
     const octokit = new Octokit(
-      toOctokitOptions({ githubToken, githubUrl, githubApiPathPrefix, proxy }),
+      toOctokitOptions({
+        githubToken,
+        githubUrl,
+        githubApiPathPrefix,
+        githubApiUrl,
+        proxy,
+      }),
     );
     // In case the repo changed name, get the new `repo`/`owner` as the search API will not follow redirects
     const { data: repoData } = await octokit.request(
diff --git a/lib/octokit.js b/lib/octokit.js
index 798b575..d1d1baa 100644
--- a/lib/octokit.js
+++ b/lib/octokit.js
@@ -49,14 +49,16 @@ export const SemanticReleaseOctokit = Octokit.plugin(
 /* c8 ignore stop */
 
 /**
- * @param {{githubToken: string, proxy: any} | {githubUrl: string, githubApiPathPrefix: string, githubToken: string, proxy: any}} options
+ * @param {{githubToken: string, proxy: any} | {githubUrl: string, githubApiPathPrefix: string, githubApiUrl: string,githubToken: string, proxy: any}} options
  * @returns {{ auth: string, baseUrl?: string, request: { agent?: any } }}
  */
 export function toOctokitOptions(options) {
   const baseUrl =
-    "githubUrl" in options && options.githubUrl
-      ? urljoin(options.githubUrl, options.githubApiPathPrefix)
-      : undefined;
+    "githubApiUrl" in options && options.githubApiUrl
+      ? options.githubApiUrl
+      : "githubUrl" in options && options.githubUrl
+        ? urljoin(options.githubUrl, options.githubApiPathPrefix)
+        : undefined;
 
   const agent = options.proxy
     ? baseUrl && new URL(baseUrl).protocol.replace(":", "") === "http"
diff --git a/lib/publish.js b/lib/publish.js
index 6a5bdfc..b91c39d 100644
--- a/lib/publish.js
+++ b/lib/publish.js
@@ -26,6 +26,7 @@ export default async function publish(pluginConfig, context, { Octokit }) {
     githubToken,
     githubUrl,
     githubApiPathPrefix,
+    githubApiUrl,
     proxy,
     assets,
     draftRelease,
@@ -39,6 +40,7 @@ export default async function publish(pluginConfig, context, { Octokit }) {
       githubToken,
       githubUrl,
       githubApiPathPrefix,
+      githubApiUrl,
       proxy,
     }),
   );
diff --git a/lib/resolve-config.js b/lib/resolve-config.js
index 803f13a..b8081c8 100644
--- a/lib/resolve-config.js
+++ b/lib/resolve-config.js
@@ -3,6 +3,7 @@ import { isNil, castArray } from "lodash-es";
 export default function resolveConfig(
   {
     githubUrl,
+    githubApiUrl,
     githubApiPathPrefix,
     proxy,
     assets,
@@ -22,9 +23,10 @@ export default function resolveConfig(
 ) {
   return {
     githubToken: env.GH_TOKEN || env.GITHUB_TOKEN,
-    githubUrl: githubUrl || env.GITHUB_API_URL || env.GH_URL || env.GITHUB_URL,
+    githubUrl: githubUrl || env.GH_URL || env.GITHUB_URL,
     githubApiPathPrefix:
       githubApiPathPrefix || env.GH_PREFIX || env.GITHUB_PREFIX || "",
+    githubApiUrl: githubApiUrl || env.GITHUB_API_URL,
     proxy: isNil(proxy) ? env.http_proxy || env.HTTP_PROXY || false : proxy,
     assets: assets ? castArray(assets) : assets,
     successComment,
diff --git a/lib/success.js b/lib/success.js
index 75cca3a..8672990 100644
--- a/lib/success.js
+++ b/lib/success.js
@@ -27,6 +27,7 @@ export default async function success(pluginConfig, context, { Octokit }) {
     githubToken,
     githubUrl,
     githubApiPathPrefix,
+    githubApiUrl,
     proxy,
     successComment,
     failComment,
@@ -36,7 +37,13 @@ export default async function success(pluginConfig, context, { Octokit }) {
   } = resolveConfig(pluginConfig, context);
 
   const octokit = new Octokit(
-    toOctokitOptions({ githubToken, githubUrl, githubApiPathPrefix, proxy }),
+    toOctokitOptions({
+      githubToken,
+      githubUrl,
+      githubApiPathPrefix,
+      githubApiUrl,
+      proxy,
+    }),
   );
 
   // In case the repo changed name, get the new `repo`/`owner` as the search API will not follow redirects
diff --git a/lib/verify.js b/lib/verify.js
index 12a186c..b951cee 100644
--- a/lib/verify.js
+++ b/lib/verify.js
@@ -56,8 +56,14 @@ export default async function verify(pluginConfig, context, { Octokit }) {
     options: { repositoryUrl },
     logger,
   } = context;
-  const { githubToken, githubUrl, githubApiPathPrefix, proxy, ...options } =
-    resolveConfig(pluginConfig, context);
+  const {
+    githubToken,
+    githubUrl,
+    githubApiPathPrefix,
+    githubApiUrl,
+    proxy,
+    ...options
+  } = resolveConfig(pluginConfig, context);
 
   const errors = Object.entries({ ...options, proxy }).reduce(
     (errors, [option, value]) =>
@@ -70,7 +76,9 @@ export default async function verify(pluginConfig, context, { Octokit }) {
     [],
   );
 
-  if (githubUrl) {
+  if (githubApiUrl) {
+    logger.log("Verify GitHub authentication (%s)", githubApiUrl);
+  } else if (githubUrl) {
     logger.log(
       "Verify GitHub authentication (%s)",
       urlJoin(githubUrl, githubApiPathPrefix),
@@ -87,7 +95,13 @@ export default async function verify(pluginConfig, context, { Octokit }) {
     !errors.find(({ code }) => code === "EINVALIDPROXY")
   ) {
     const octokit = new Octokit(
-      toOctokitOptions({ githubToken, githubUrl, githubApiPathPrefix, proxy }),
+      toOctokitOptions({
+        githubToken,
+        githubUrl,
+        githubApiPathPrefix,
+        githubApiUrl,
+        proxy,
+      }),
     );
 
     // https://github.com/semantic-release/github/issues/182


Test patch:
diff --git a/test/verify.test.js b/test/verify.test.js
index 5d97ad9..9bac5d0 100644
--- a/test/verify.test.js
+++ b/test/verify.test.js
@@ -263,7 +263,7 @@ test("Verify package, token and repository with environment variables", async (t
   ]);
 });
 
-test("Verify package, token and repository access with alternative environment varialbes", async (t) => {
+test("Verify package, token and repository access with alternative environment variables", async (t) => {
   const owner = "test_user";
   const repo = "test_repo";
   const env = {
@@ -299,6 +299,79 @@ test("Verify package, token and repository access with alternative environment v
   t.true(fetch.done());
 });
 
+test("Verify package, token and repository access with custom API URL", async (t) => {
+  const owner = "test_user";
+  const repo = "test_repo";
+  const env = { GH_TOKEN: "github_token" };
+  const githubUrl = "https://othertesturl.com:9090";
+  const githubApiUrl = "https://api.othertesturl.com:9090";
+
+  const fetch = fetchMock
+    .sandbox()
+    .getOnce(`https://api.othertesturl.com:9090/repos/${owner}/${repo}`, {
+      permissions: { push: true },
+    });
+
+  await t.notThrowsAsync(
+    verify(
+      { githubUrl, githubApiUrl },
+      {
+        env,
+        options: { repositoryUrl: `github:${owner}/${repo}` },
+        logger: t.context.logger,
+      },
+      {
+        Octokit: TestOctokit.defaults((options) => ({
+          ...options,
+          request: { ...options.request, fetch },
+        })),
+      },
+    ),
+  );
+
+  t.true(fetch.done());
+  t.deepEqual(t.context.log.args[0], [
+    "Verify GitHub authentication (%s)",
+    "https://api.othertesturl.com:9090",
+  ]);
+});
+
+test("Verify package, token and repository access with API URL in environment variable", async (t) => {
+  const owner = "test_user";
+  const repo = "test_repo";
+  const env = {
+    GITHUB_URL: "https://othertesturl.com:443",
+    GITHUB_API_URL: "https://api.othertesturl.com:443",
+    GITHUB_TOKEN: "github_token",
+  };
+
+  const fetch = fetchMock
+    .sandbox()
+    .getOnce(`https://api.othertesturl.com:443/repos/${owner}/${repo}`, {
+      permissions: { push: true },
+    });
+
+  await t.notThrowsAsync(
+    verify(
+      {},
+      {
+        env,
+        options: {
+          repositoryUrl: `git@othertesturl.com:${owner}/${repo}.git`,
+        },
+        logger: t.context.logger,
+      },
+      {
+        Octokit: TestOctokit.defaults((options) => ({
+          ...options,
+          request: { ...options.request, fetch },
+        })),
+      },
+    ),
+  );
+  t.true(fetch.done());
+});
+
 test('Verify "proxy" is a String', async (t) => {
   const owner = "test_user";
   const repo = "test_repo";