diff --git a/__test__/utils.unit.test.ts b/__test__/utils.unit.test.ts index 2381b98e30..f967f38e67 100644 --- a/__test__/utils.unit.test.ts +++ b/__test__/utils.unit.test.ts @@ -2,6 +2,8 @@ import * as path from 'path' import * as utils from '../lib/utils' const originalGitHubWorkspace = process.env['GITHUB_WORKSPACE'] +const originalForgejoApiUrl = process.env['FORGEJO_API_URL'] +const originalGitHubApiUrl = process.env['GITHUB_API_URL'] describe('utils tests', () => { beforeAll(() => { @@ -15,6 +17,8 @@ describe('utils tests', () => { if (originalGitHubWorkspace) { process.env['GITHUB_WORKSPACE'] = originalGitHubWorkspace } + restoreEnvironmentVariable('FORGEJO_API_URL', originalForgejoApiUrl) + restoreEnvironmentVariable('GITHUB_API_URL', originalGitHubApiUrl) }) test('getStringAsArray splits string input by newlines and commas', async () => { @@ -69,6 +73,65 @@ describe('utils tests', () => { ) }) + test('determineApiBaseUrl uses the API URL supplied by the runner', async () => { + process.env['FORGEJO_API_URL'] = 'https://forgejo.example.com/api/v1/' + process.env['GITHUB_API_URL'] = 'https://forgejo.example.com/api/v1' + const probe = jest.fn() + + await expect( + utils.determineApiBaseUrl('forgejo.example.com', probe) + ).resolves.toEqual('https://forgejo.example.com/api/v1') + expect(probe).not.toHaveBeenCalled() + }) + + test('determineApiBaseUrl uses the GitHub API URL supplied by the runner', async () => { + delete process.env['FORGEJO_API_URL'] + process.env['GITHUB_API_URL'] = 'https://github.example.com/api/v3' + + await expect( + utils.determineApiBaseUrl('github.example.com') + ).resolves.toEqual('https://github.example.com/api/v3') + }) + + test('determineApiBaseUrl returns the public GitHub API URL', async () => { + delete process.env['FORGEJO_API_URL'] + delete process.env['GITHUB_API_URL'] + + await expect(utils.determineApiBaseUrl('github.com')).resolves.toEqual( + 'https://api.github.com' + ) + }) + + test('determineApiBaseUrl discovers the Forgejo API', async () => { + delete process.env['FORGEJO_API_URL'] + delete process.env['GITHUB_API_URL'] + const probe = jest.fn().mockResolvedValue({ + ok: true, + headers: {get: () => 'application/json; charset=utf-8'} + }) + + await expect( + utils.determineApiBaseUrl('forgejo.example.com', probe) + ).resolves.toEqual('https://forgejo.example.com/api/v1') + expect(probe).toHaveBeenCalledWith( + 'https://forgejo.example.com/api/v1/version', + {signal: expect.any(AbortSignal)} + ) + }) + + test('determineApiBaseUrl falls back to the GitHub Enterprise API', async () => { + delete process.env['FORGEJO_API_URL'] + delete process.env['GITHUB_API_URL'] + const probe = jest.fn().mockResolvedValue({ + ok: false, + headers: {get: () => 'application/json'} + }) + + await expect( + utils.determineApiBaseUrl('github.example.com', probe) + ).resolves.toEqual('https://github.example.com/api/v3') + }) + test('secondsSinceEpoch returns the number of seconds since the Epoch', async () => { const seconds = `${utils.secondsSinceEpoch()}` expect(seconds.length).toEqual(10) @@ -119,6 +182,17 @@ describe('utils tests', () => { }) }) +function restoreEnvironmentVariable( + name: string, + value: string | undefined +): void { + if (value === undefined) { + delete process.env[name] + } else { + process.env[name] = value + } +} + describe('retryWithBackoff', () => { const makeConsistencyError = () => { const error = new Error( diff --git a/dist/index.js b/dist/index.js index c06637d71d..497ab98397 100644 --- a/dist/index.js +++ b/dist/index.js @@ -405,8 +405,10 @@ function createPullRequest(inputs) { core.startGroup('Determining the base and head repositories'); const baseRemote = gitConfigHelper.getGitRemote(); // Init the GitHub clients - const ghBranch = new github_helper_1.GitHubHelper(baseRemote.hostname, inputs.branchToken); - const ghPull = new github_helper_1.GitHubHelper(baseRemote.hostname, inputs.token); + const apiUrl = yield utils.determineApiBaseUrl(baseRemote.hostname); + core.info(`Using API base URL: ${apiUrl}`); + const ghBranch = new github_helper_1.GitHubHelper(apiUrl, inputs.branchToken); + const ghPull = new github_helper_1.GitHubHelper(apiUrl, inputs.token); // Determine the head repository; the target for the pull request branch const branchRemoteName = inputs.pushToFork ? 'fork' : 'origin'; const branchRepository = inputs.pushToFork @@ -1384,17 +1386,12 @@ const ERROR_PR_REVIEW_TOKEN_SCOPE = 'Validation Failed: "Could not resolve to a const ERROR_PR_FORK_COLLAB = `Fork collab can't be granted by someone without permission`; const blobCreationLimit = (0, p_limit_1.default)(8); class GitHubHelper { - constructor(githubServerHostname, token) { + constructor(apiUrl, token) { const options = {}; if (token) { options.auth = `${token}`; } - if (githubServerHostname !== 'github.com') { - options.baseUrl = `https://${githubServerHostname}/api/v3`; - } - else { - options.baseUrl = 'https://api.github.com'; - } + options.baseUrl = apiUrl; options.throttle = octokit_client_1.throttleOptions; options.retry = octokit_client_1.retryOptions; this.octokit = new octokit_client_1.Octokit(options); @@ -1462,7 +1459,8 @@ class GitHubHelper { } catch (e) { const errorMessage = utils.getErrorMessage(e); - if (errorMessage.includes(ERROR_PR_ALREADY_EXISTS)) { + if (errorMessage.includes(ERROR_PR_ALREADY_EXISTS) || + (e instanceof request_error_1.RequestError && e.status === 409)) { core.info(`A pull request already exists for ${headBranch}`); } else if (errorMessage.includes(ERROR_PR_FORK_COLLAB)) { @@ -1927,6 +1925,7 @@ exports.getStringAsArray = getStringAsArray; exports.stripOrgPrefixFromTeams = stripOrgPrefixFromTeams; exports.getRepoPath = getRepoPath; exports.getRemoteUrl = getRemoteUrl; +exports.determineApiBaseUrl = determineApiBaseUrl; exports.secondsSinceEpoch = secondsSinceEpoch; exports.randomString = randomString; exports.parseDisplayNameEmail = parseDisplayNameEmail; @@ -1936,6 +1935,7 @@ exports.getErrorMessage = getErrorMessage; exports.retryWithBackoff = retryWithBackoff; const core = __importStar(__nccwpck_require__(7484)); const fs = __importStar(__nccwpck_require__(9896)); +const proxy_1 = __nccwpck_require__(3459); const path = __importStar(__nccwpck_require__(6928)); function getInputAsArray(name, options) { return getStringAsArray(core.getInput(name, options)); @@ -1973,6 +1973,32 @@ function getRemoteUrl(protocol, hostname, repository) { ? `https://${hostname}/${repository}` : `git@${hostname}:${repository}.git`; } +function determineApiBaseUrl(hostname_1) { + return __awaiter(this, arguments, void 0, function* (hostname, probe = proxy_1.fetch) { + var _a; + const apiUrl = process.env['FORGEJO_API_URL'] || process.env['GITHUB_API_URL']; + if (apiUrl) { + return apiUrl.replace(/\/$/, ''); + } + if (hostname === 'github.com') { + return 'https://api.github.com'; + } + const forgejoApiUrl = `https://${hostname}/api/v1`; + try { + const response = yield probe(`${forgejoApiUrl}/version`, { + signal: AbortSignal.timeout(5000) + }); + if (response.ok && + ((_a = response.headers.get('content-type')) === null || _a === void 0 ? void 0 : _a.includes('application/json'))) { + return forgejoApiUrl; + } + } + catch (error) { + core.debug(`API discovery failed: ${getErrorMessage(error)}`); + } + return `https://${hostname}/api/v3`; + }); +} function secondsSinceEpoch() { const now = new Date(); return Math.round(now.getTime() / 1000); diff --git a/src/create-pull-request.ts b/src/create-pull-request.ts index 2e3f0e0a29..cb39dd8ca1 100644 --- a/src/create-pull-request.ts +++ b/src/create-pull-request.ts @@ -51,8 +51,10 @@ export async function createPullRequest(inputs: Inputs): Promise { core.startGroup('Determining the base and head repositories') const baseRemote = gitConfigHelper.getGitRemote() // Init the GitHub clients - const ghBranch = new GitHubHelper(baseRemote.hostname, inputs.branchToken) - const ghPull = new GitHubHelper(baseRemote.hostname, inputs.token) + const apiUrl = await utils.determineApiBaseUrl(baseRemote.hostname) + core.info(`Using API base URL: ${apiUrl}`) + const ghBranch = new GitHubHelper(apiUrl, inputs.branchToken) + const ghPull = new GitHubHelper(apiUrl, inputs.token) // Determine the head repository; the target for the pull request branch const branchRemoteName = inputs.pushToFork ? 'fork' : 'origin' const branchRepository = inputs.pushToFork diff --git a/src/github-helper.ts b/src/github-helper.ts index 26ef65cb87..00298a795b 100644 --- a/src/github-helper.ts +++ b/src/github-helper.ts @@ -47,16 +47,12 @@ type TreeObject = { export class GitHubHelper { private octokit: InstanceType - constructor(githubServerHostname: string, token: string) { + constructor(apiUrl: string, token: string) { const options: OctokitOptions = {} if (token) { options.auth = `${token}` } - if (githubServerHostname !== 'github.com') { - options.baseUrl = `https://${githubServerHostname}/api/v3` - } else { - options.baseUrl = 'https://api.github.com' - } + options.baseUrl = apiUrl options.throttle = throttleOptions options.retry = retryOptions this.octokit = new Octokit(options) @@ -147,7 +143,10 @@ export class GitHubHelper { } } catch (e) { const errorMessage = utils.getErrorMessage(e) - if (errorMessage.includes(ERROR_PR_ALREADY_EXISTS)) { + if ( + errorMessage.includes(ERROR_PR_ALREADY_EXISTS) || + (e instanceof RequestError && e.status === 409) + ) { core.info(`A pull request already exists for ${headBranch}`) } else if (errorMessage.includes(ERROR_PR_FORK_COLLAB)) { core.warning( diff --git a/src/utils.ts b/src/utils.ts index a7f825d8b3..748a838a84 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,6 @@ import * as core from '@actions/core' import * as fs from 'fs' +import {fetch} from 'node-fetch-native/proxy' import * as path from 'path' export function getInputAsArray( @@ -51,6 +52,48 @@ export function getRemoteUrl( : `git@${hostname}:${repository}.git` } +interface ApiProbeResponse { + ok: boolean + headers: { + get(name: string): string | null + } +} + +type ApiProbe = ( + url: string, + options: {signal: AbortSignal} +) => Promise + +export async function determineApiBaseUrl( + hostname: string, + probe: ApiProbe = fetch +): Promise { + const apiUrl = process.env['FORGEJO_API_URL'] || process.env['GITHUB_API_URL'] + if (apiUrl) { + return apiUrl.replace(/\/$/, '') + } + if (hostname === 'github.com') { + return 'https://api.github.com' + } + + const forgejoApiUrl = `https://${hostname}/api/v1` + try { + const response = await probe(`${forgejoApiUrl}/version`, { + signal: AbortSignal.timeout(5000) + }) + if ( + response.ok && + response.headers.get('content-type')?.includes('application/json') + ) { + return forgejoApiUrl + } + } catch (error) { + core.debug(`API discovery failed: ${getErrorMessage(error)}`) + } + + return `https://${hostname}/api/v3` +} + export function secondsSinceEpoch(): number { const now = new Date() return Math.round(now.getTime() / 1000)