diff --git a/lib/entry-points.js b/lib/entry-points.js index 186a09106d..62b043f7b8 100644 --- a/lib/entry-points.js +++ b/lib/entry-points.js @@ -146756,15 +146756,19 @@ async function getGitHubVersionFromApi(apiClient, apiDetails) { if (parseGitHubUrl(apiDetails.url) === GITHUB_DOTCOM_URL) { return { type: "GitHub.com" /* DOTCOM */ }; } - const response = await apiClient.rest.meta.get(); - if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === void 0) { - return { type: "GitHub.com" /* DOTCOM */ }; - } - if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === "ghe.com") { - return { type: "GitHub Enterprise Cloud with data residency" /* GHEC_DR */ }; + try { + const response = await apiClient.rest.meta.get(); + if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === void 0) { + return { type: "GitHub.com" /* DOTCOM */ }; + } + if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === "ghe.com") { + return { type: "GitHub Enterprise Cloud with data residency" /* GHEC_DR */ }; + } + const version = response.headers[GITHUB_ENTERPRISE_VERSION_HEADER]; + return { type: "GitHub Enterprise Server" /* GHES */, version }; + } catch (err) { + throw wrapApiConfigurationError(err); } - const version = response.headers[GITHUB_ENTERPRISE_VERSION_HEADER]; - return { type: "GitHub Enterprise Server" /* GHES */, version }; } async function getGitHubVersion() { if (cachedGitHubVersion === void 0) { @@ -163041,7 +163045,8 @@ async function run4(startedAt) { try { const jobStatus2 = getOptionalInput("job-status"); restoreInputs(logger); - const gitHubVersion = await getGitHubVersion(); + config = await getConfig(getTemporaryDirectory(), logger); + const gitHubVersion = config?.gitHubVersion ?? await getGitHubVersion(); checkGitHubVersionInRange(gitHubVersion, logger); const repositoryNwo = getRepositoryNwo(); const features = initFeatures( @@ -163050,7 +163055,6 @@ async function run4(startedAt) { getTemporaryDirectory(), logger ); - config = await getConfig(getTemporaryDirectory(), logger); if (config === void 0) { logger.warning( "Debugging artifacts are unavailable since the 'init' Action failed before it could produce any." @@ -163073,7 +163077,7 @@ async function run4(startedAt) { } } } catch (unwrappedError) { - const error3 = wrapError(unwrappedError); + const error3 = wrapApiConfigurationError(wrapError(unwrappedError)); core23.setFailed(error3.message); const statusReportBase2 = await createStatusReportBase( "init-post" /* InitPost */, diff --git a/src/api-client.test.ts b/src/api-client.test.ts index ae8c6269b1..12fe819856 100644 --- a/src/api-client.test.ts +++ b/src/api-client.test.ts @@ -111,103 +111,115 @@ test.serial("getGitHubVersion for GHEC-DR", async (t) => { t.deepEqual({ type: util.GitHubVariant.GHEC_DR }, gheDotcom); }); -test.serial( - "wrapApiConfigurationError correctly wraps specific configuration errors", - (t) => { +test("wrapApiConfigurationError doesn't wrap errors it isn't supposed to", (t) => { + const unwrappedErrors = [ // We don't reclassify arbitrary errors - const arbitraryError = new Error("arbitrary error"); - let res = api.wrapApiConfigurationError(arbitraryError); - t.is(res, arbitraryError); + new Error("arbitrary error"), + // Same goes for arbitrary strings + "arbitrary error", + // If an HTTP error doesn't contain a specific error message, we don't wrap it. + new util.HTTPError("arbitrary HTTP error", 456), + ]; - // Same goes for arbitrary errors - const configError = new util.ConfigurationError("arbitrary error"); - res = api.wrapApiConfigurationError(configError); - t.is(res, configError); - - // If an HTTP error doesn't contain a specific error message, we don't - // wrap is an an API error. - const httpError = new util.HTTPError("arbitrary HTTP error", 456); - res = api.wrapApiConfigurationError(httpError); - t.is(res, httpError); - - // For other HTTP errors, we wrap them as Configuration errors if they contain - // specific error messages. - const httpNotFoundError = new util.HTTPError("commit not found", 404); - res = api.wrapApiConfigurationError(httpNotFoundError); - t.deepEqual(res, new util.ConfigurationError("commit not found")); - - const refNotFoundError = new util.HTTPError( - "ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest", - 404, - ); - res = api.wrapApiConfigurationError(refNotFoundError); - t.deepEqual( + for (const unwrappedError of unwrappedErrors) { + const res = api.wrapApiConfigurationError(unwrappedError); + t.is( res, - new util.ConfigurationError( - "ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest", - ), + unwrappedError, + `${util.getErrorMessage(unwrappedError)} should not be wrapped by wrapApiConfigurationError`, ); + } +}); - const apiRateLimitError = new util.HTTPError( - "API rate limit exceeded for installation", - 403, - ); - res = api.wrapApiConfigurationError(apiRateLimitError); - t.deepEqual( - res, - new util.ConfigurationError("API rate limit exceeded for installation"), - ); +test("wrapApiConfigurationError correctly wraps specific configuration errors", (t) => { + // For other HTTP errors, we wrap them as Configuration errors if they contain + // specific error messages. + const httpNotFoundError = new util.HTTPError("commit not found", 404); + const refNotFoundError = new util.HTTPError( + "ref 'refs/heads/jitsi' not found in this repository - https://docs.github.com/rest", + 404, + ); + const apiRateLimitError = new util.HTTPError( + "API rate limit exceeded for installation", + 403, + ); + const resourceNotAccessibleError = new util.HTTPError( + "Resource not accessible by integration", + 403, + ); + const errorsToWrap = [ + httpNotFoundError, + refNotFoundError, + apiRateLimitError, + resourceNotAccessibleError, + ]; - const tokenSuggestionMessage = - "Please check that your token is valid and has the required permissions: contents: read, security-events: write"; - const badCredentialsError = new util.HTTPError("Bad credentials", 401); - res = api.wrapApiConfigurationError(badCredentialsError); - t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage)); + for (const errorToWrap of errorsToWrap) { + const res = api.wrapApiConfigurationError(errorToWrap); + t.deepEqual(res, new util.ConfigurationError(errorToWrap.message)); + } +}); - const notFoundError = new util.HTTPError("Not Found", 404); - res = api.wrapApiConfigurationError(notFoundError); - t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage)); +test("wrapApiConfigurationError wraps token errors", async (t) => { + const tokenSuggestionMessage = + "Please check that your token is valid and has the required permissions: contents: read, security-events: write"; + const badCredentialsError = new util.HTTPError("Bad credentials", 401); + const notFoundError = new util.HTTPError("Not Found", 404); + const errorsToWrap = [badCredentialsError, notFoundError]; - const resourceNotAccessibleError = new util.HTTPError( - "Resource not accessible by integration", - 403, - ); - res = api.wrapApiConfigurationError(resourceNotAccessibleError); - t.deepEqual( - res, - new util.ConfigurationError("Resource not accessible by integration"), - ); + for (const errorToWrap of errorsToWrap) { + const res = api.wrapApiConfigurationError(errorToWrap); + t.deepEqual(res, new util.ConfigurationError(tokenSuggestionMessage)); + } +}); - // Enablement errors. - const enablementErrorMessages = [ - "Code Security must be enabled for this repository to use code scanning", - "Advanced Security must be enabled for this repository to use code scanning", - "Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.", - "Code quality is not enabled for this repository. Please enable code quality in the repository settings.", - ]; - const transforms = [ - (msg: string) => msg, - (msg: string) => msg.toLowerCase(), - (msg: string) => msg.toLocaleUpperCase(), - ]; +test("wrapApiConfigurationError wraps enablement errors", async (t) => { + // Enablement errors. + const enablementErrorMessages = [ + "Code Security must be enabled for this repository to use code scanning", + "Advanced Security must be enabled for this repository to use code scanning", + "Code Scanning is not enabled for this repository. Please enable code scanning in the repository settings.", + "Code quality is not enabled for this repository. Please enable code quality in the repository settings.", + ]; + const transforms = [ + (msg: string) => msg, + (msg: string) => msg.toLowerCase(), + (msg: string) => msg.toLocaleUpperCase(), + ]; - for (const enablementErrorMessage of enablementErrorMessages) { - for (const transform of transforms) { - const enablementError = new util.HTTPError( - transform(enablementErrorMessage), - 403, - ); - res = api.wrapApiConfigurationError(enablementError); - t.deepEqual( - res, - new util.ConfigurationError( - api.getFeatureEnablementError(enablementError.message), - ), - ); - } + for (const enablementErrorMessage of enablementErrorMessages) { + for (const transform of transforms) { + const enablementError = new util.HTTPError( + transform(enablementErrorMessage), + 403, + ); + const res = api.wrapApiConfigurationError(enablementError); + t.deepEqual( + res, + new util.ConfigurationError( + api.getFeatureEnablementError(enablementError.message), + ), + ); } - }, -); + } +}); + +test("wrapApiConfigurationError doesn't double-wrap errors", async (t) => { + // This test checks that errors don't get wrapped a second time if `wrapApiConfigurationError` + // is called on an error that was already wrapped by a previous call to `wrapApiConfigurationError`. + // Start by calling `wrapApiConfigurationError` on an unwrapped error that should be wrapped: + const unwrappedError = new util.HTTPError("commit not found", 404); + const wrappedError = api.wrapApiConfigurationError(unwrappedError); + + // Sanity-check that it was wrapped, as expected. + t.deepEqual( + wrappedError, + new util.ConfigurationError(unwrappedError.message), + ); + + // The result of the second call should be exactly `wrappedError`: + t.is(api.wrapApiConfigurationError(wrappedError), wrappedError); +}); test("getRegistryProxy - returns undefined if the proxy is not configured", async (t) => { const target = callee(api.getRegistryProxy).withArgs(); diff --git a/src/api-client.ts b/src/api-client.ts index e509f5eea0..de62f4db44 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -219,25 +219,31 @@ export async function getGitHubVersionFromApi( return { type: GitHubVariant.DOTCOM }; } - // Doesn't strictly have to be the meta endpoint as we're only - // using the response headers which are available on every request. - // - // See https://docs.github.com/en/rest/meta/meta#get-github-meta-information. - // eslint-disable-next-line @typescript-eslint/no-unsafe-call - const response = await apiClient.rest.meta.get(); - - // This happens on dotcom, although we expect to have already returned in that - // case. This can also serve as a fallback in cases we haven't foreseen. - if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === undefined) { - return { type: GitHubVariant.DOTCOM }; - } + try { + // Doesn't strictly have to be the meta endpoint as we're only + // using the response headers which are available on every request. + // + // See https://docs.github.com/en/rest/meta/meta#get-github-meta-information. + // eslint-disable-next-line @typescript-eslint/no-unsafe-call + const response = await apiClient.rest.meta.get(); + + // This happens on dotcom, although we expect to have already returned in that + // case. This can also serve as a fallback in cases we haven't foreseen. + if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === undefined) { + return { type: GitHubVariant.DOTCOM }; + } - if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === "ghe.com") { - return { type: GitHubVariant.GHEC_DR }; - } + if (response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] === "ghe.com") { + return { type: GitHubVariant.GHEC_DR }; + } - const version = response.headers[GITHUB_ENTERPRISE_VERSION_HEADER] as string; - return { type: GitHubVariant.GHES, version }; + const version = response.headers[ + GITHUB_ENTERPRISE_VERSION_HEADER + ] as string; + return { type: GitHubVariant.GHES, version }; + } catch (err) { + throw wrapApiConfigurationError(err); + } } /** @@ -415,7 +421,14 @@ export function getFeatureEnablementError(message: string): string { return `Please verify that the necessary features are enabled: ${message}`; } -export function wrapApiConfigurationError(e: unknown) { +/** + * Decides whether `e` is a known error returned by the GitHub API that we should + * classify as a `ConfigurationError`. + * + * @param e The error to classify. + * @returns Either `e` or a corresponding `ConfigurationError`. + */ +export function wrapApiConfigurationError(e: T): T | ConfigurationError { const httpError = asHTTPError(e); if (httpError !== undefined) { if ( diff --git a/src/init-action-post.ts b/src/init-action-post.ts index e3a68b0e57..a2bba6d7a4 100644 --- a/src/init-action-post.ts +++ b/src/init-action-post.ts @@ -12,7 +12,7 @@ import { getTemporaryDirectory, printDebugLogs, } from "./actions-util"; -import { getGitHubVersion } from "./api-client"; +import { getGitHubVersion, wrapApiConfigurationError } from "./api-client"; import { CachingKind } from "./caching-utils"; import { getCodeQL } from "./codeql"; import { type Config, getConfig } from "./config-utils"; @@ -64,7 +64,9 @@ async function run(startedAt: Date) { // Restore inputs from `init` Action. restoreInputs(logger); - const gitHubVersion = await getGitHubVersion(); + config = await getConfig(getTemporaryDirectory(), logger); + + const gitHubVersion = config?.gitHubVersion ?? (await getGitHubVersion()); checkGitHubVersionInRange(gitHubVersion, logger); const repositoryNwo = getRepositoryNwo(); @@ -75,7 +77,6 @@ async function run(startedAt: Date) { logger, ); - config = await getConfig(getTemporaryDirectory(), logger); if (config === undefined) { logger.warning( "Debugging artifacts are unavailable since the 'init' Action failed before it could produce any.", @@ -107,7 +108,7 @@ async function run(startedAt: Date) { } } } catch (unwrappedError) { - const error = wrapError(unwrappedError); + const error = wrapApiConfigurationError(wrapError(unwrappedError)); core.setFailed(error.message); const statusReportBase = await createStatusReportBase(