From f846b3fdee22afc98740074026c6374403087ca6 Mon Sep 17 00:00:00 2001 From: Campbell Pool Date: Fri, 19 Jun 2026 12:18:59 -0700 Subject: [PATCH] feat(artifact): unblock v4+ on self-hosted GHES when ACTIONS_RESULTS_URL is set The current isGhes() check returns true for any GitHub Server URL that isn't github.com, *.ghe.com, or *.localhost. When isGhes() returns true, client.ts throws GHESNotSupportedError on every upload, download, and list call - so the artifact-v4+ family is completely blocked on self-hosted GHES instances. This made sense at v4 launch when the artifact-storage-v2 backend was github.com-only. GHES 3.13 added the v2 backend, and 3.16+ has full production support, but the hostname-based gate doesn't reflect that: it continues to throw on any GHES regardless of release. The runtime already exposes a clean signal for whether v2 is reachable: the runner sets ACTIONS_RESULTS_URL exactly when the storage backend is available. The artifact client's own getResultsServiceUrl() in the same package throws if ACTIONS_RESULTS_URL is unset, so it's already a precondition for the upload/download/list paths to succeed. Treating that env var as the authority on "is the v2 backend reachable here?" unblocks GHES 3.13+ without changing behaviour anywhere else. Compatibility: - github.com / *.ghe.com / *.localhost: hostname check returns isGhes()=false. No change. - GHES <3.13 (no v2 backend, ACTIONS_RESULTS_URL unset): hostname is GHES-shaped, env var is missing -> isGhes()=true -> existing GHESNotSupportedError. No change. - GHES 3.13+ (v2 backend present, ACTIONS_RESULTS_URL set): hostname is GHES-shaped, env var IS set -> isGhes()=false -> upload / download / list proceed normally. This is the unblock. Tests added in packages/artifact/__tests__/config.test.ts cover both new branches; the existing 'enterprise hostname' test is kept and now explicitly verifies the unset-env-var case. --- packages/artifact/__tests__/config.test.ts | 19 ++++++++++++++++++- .../artifact/src/internal/shared/config.ts | 16 +++++++++++++++- 2 files changed, 33 insertions(+), 2 deletions(-) diff --git a/packages/artifact/__tests__/config.test.ts b/packages/artifact/__tests__/config.test.ts index 3d14ace2b2..7d3be228e0 100644 --- a/packages/artifact/__tests__/config.test.ts +++ b/packages/artifact/__tests__/config.test.ts @@ -15,6 +15,11 @@ beforeEach(() => { }) describe('isGhes', () => { + beforeEach(() => { + delete process.env.GITHUB_SERVER_URL + delete process.env.ACTIONS_RESULTS_URL + }) + it('should return false when the request domain is github.com', () => { process.env.GITHUB_SERVER_URL = 'https://github.com' expect(config.isGhes()).toBe(false) @@ -35,10 +40,22 @@ describe('isGhes', () => { expect(config.isGhes()).toBe(false) }) - it('should return false when the request domain is specific to an enterprise', () => { + it('should return true on a self-hosted enterprise hostname when ACTIONS_RESULTS_URL is unset', () => { process.env.GITHUB_SERVER_URL = 'https://my-enterprise.github.com' expect(config.isGhes()).toBe(true) }) + + it('should return false on a self-hosted enterprise hostname when ACTIONS_RESULTS_URL is set (v2 backend available)', () => { + process.env.GITHUB_SERVER_URL = 'https://my-enterprise.github.com' + process.env.ACTIONS_RESULTS_URL = 'https://results.example.com/' + expect(config.isGhes()).toBe(false) + }) + + it('should ignore ACTIONS_RESULTS_URL on github.com (already returns false there)', () => { + process.env.GITHUB_SERVER_URL = 'https://github.com' + process.env.ACTIONS_RESULTS_URL = 'https://results.example.com/' + expect(config.isGhes()).toBe(false) + }) }) describe('uploadChunkTimeoutEnv', () => { diff --git a/packages/artifact/src/internal/shared/config.ts b/packages/artifact/src/internal/shared/config.ts index 0a275fd5ee..2991f69f8e 100644 --- a/packages/artifact/src/internal/shared/config.ts +++ b/packages/artifact/src/internal/shared/config.ts @@ -34,7 +34,21 @@ export function isGhes(): boolean { const isGheHost = hostname.endsWith('.GHE.COM') const isLocalHost = hostname.endsWith('.LOCALHOST') - return !isGitHubHost && !isGheHost && !isLocalHost + const hostnameLooksLikeGhes = + !isGitHubHost && !isGheHost && !isLocalHost + + // GHES 3.13+ ships the artifact-storage-v2 backend that the v4+ + // artifact client requires. The runner exposes the backend's + // presence via ACTIONS_RESULTS_URL (which getResultsServiceUrl() + // already requires for upload/download/list to function). When + // that env var is set we know the backend is reachable, so an + // unrecognised hostname does not need to short-circuit into a + // GHESNotSupportedError. + if (hostnameLooksLikeGhes && process.env['ACTIONS_RESULTS_URL']) { + return false + } + + return hostnameLooksLikeGhes } export function getGitHubWorkspaceDir(): string {