From 98ca9e99228128274c257141d160c73bd2731ffb Mon Sep 17 00:00:00 2001 From: Burak Yigit Kaya Date: Tue, 13 Jan 2026 00:57:51 +0000 Subject: [PATCH] fix(docker): Add GITHUB_API_TOKEN and x-access-token fallbacks for ghcr.io When authenticating to ghcr.io, add fallback support for: - GITHUB_API_TOKEN as password when GITHUB_TOKEN is not set - x-access-token as username when GITHUB_ACTOR is empty or not set Also fix credential fallback consistency: - Use || instead of ?? for DOCKER_USERNAME/PASSWORD fallback - This ensures empty strings are treated as 'not set' consistently This enables ghcr.io authentication with GitHub App tokens, which use x-access-token as the username. The getsentry/publish workflow uses GITHUB_API_TOKEN for the release bot token, so this fallback allows Docker targets to authenticate to ghcr.io without additional configuration. --- src/targets/__tests__/docker.test.ts | 87 ++++++++++++++++++++++++++++ src/targets/docker.ts | 11 ++-- 2 files changed, 94 insertions(+), 4 deletions(-) diff --git a/src/targets/__tests__/docker.test.ts b/src/targets/__tests__/docker.test.ts index cc4012ea..2fa52f66 100644 --- a/src/targets/__tests__/docker.test.ts +++ b/src/targets/__tests__/docker.test.ts @@ -344,6 +344,7 @@ describe('DockerTarget', () => { delete process.env.DOCKER_GCR_IO_PASSWORD; delete process.env.GITHUB_ACTOR; delete process.env.GITHUB_TOKEN; + delete process.env.GITHUB_API_TOKEN; }); afterAll(() => { @@ -465,6 +466,73 @@ describe('DockerTarget', () => { expect(target.dockerConfig.target.credentials!.password).toBe('github-token'); }); + it('falls back to GITHUB_API_TOKEN for ghcr.io when GITHUB_TOKEN is not set', () => { + process.env.GITHUB_ACTOR = 'github-actor'; + process.env.GITHUB_API_TOKEN = 'github-api-token'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('github-actor'); + expect(target.dockerConfig.target.credentials!.password).toBe('github-api-token'); + }); + + it('falls back to x-access-token username for ghcr.io when GITHUB_ACTOR is not set', () => { + process.env.GITHUB_TOKEN = 'github-token'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('x-access-token'); + expect(target.dockerConfig.target.credentials!.password).toBe('github-token'); + }); + + it('falls back to x-access-token username for ghcr.io when GITHUB_ACTOR is empty', () => { + process.env.GITHUB_ACTOR = ''; + process.env.GITHUB_TOKEN = 'github-token'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('x-access-token'); + expect(target.dockerConfig.target.credentials!.password).toBe('github-token'); + }); + + it('uses x-access-token and GITHUB_API_TOKEN for ghcr.io (app token scenario)', () => { + // This simulates the getsentry/publish workflow with release bot token + process.env.GITHUB_API_TOKEN = 'release-bot-token'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'ghcr.io/org/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('x-access-token'); + expect(target.dockerConfig.target.credentials!.password).toBe('release-bot-token'); + }); + it('uses default DOCKER_* env vars for Docker Hub', () => { process.env.DOCKER_USERNAME = 'dockerhub-user'; process.env.DOCKER_PASSWORD = 'dockerhub-pass'; @@ -536,6 +604,25 @@ describe('DockerTarget', () => { expect(target.dockerConfig.target.credentials!.password).toBe('default-pass'); }); + it('falls back to DOCKER_* when registry-specific vars are empty strings', () => { + process.env.DOCKER_GCR_IO_USERNAME = ''; + process.env.DOCKER_GCR_IO_PASSWORD = ''; + process.env.DOCKER_USERNAME = 'default-user'; + process.env.DOCKER_PASSWORD = 'default-pass'; + + const target = new DockerTarget( + { + name: 'docker', + source: 'ghcr.io/org/image', + target: 'gcr.io/project/image', + }, + new NoneArtifactProvider() + ); + + expect(target.dockerConfig.target.credentials!.username).toBe('default-user'); + expect(target.dockerConfig.target.credentials!.password).toBe('default-pass'); + }); + it('throws when no credentials are available', () => { expect( () => diff --git a/src/targets/docker.ts b/src/targets/docker.ts index 6d773514..a4e950f4 100644 --- a/src/targets/docker.ts +++ b/src/targets/docker.ts @@ -331,15 +331,18 @@ export class DockerTarget extends BaseTarget { // GHCR defaults: use GitHub Actions built-in env vars // GITHUB_ACTOR and GITHUB_TOKEN are available by default in GitHub Actions // See: https://docs.github.com/en/actions/reference/workflows-and-actions/variables - username = username ?? process.env.GITHUB_ACTOR; - password = password ?? process.env.GITHUB_TOKEN; + // GITHUB_API_TOKEN is used by getsentry/publish workflow with release bot token + // x-access-token works with GitHub App installation tokens and PATs + username = username || process.env.GITHUB_ACTOR || 'x-access-token'; + password = password || process.env.GITHUB_TOKEN || process.env.GITHUB_API_TOKEN; } } // 3. Fallback to defaults (only for target registry, not for source) + // Use || to treat empty strings as "not set" (consistent with ghcr.io logic above) if (useDefaultFallback) { - username = username ?? process.env.DOCKER_USERNAME; - password = password ?? process.env.DOCKER_PASSWORD; + username = username || process.env.DOCKER_USERNAME; + password = password || process.env.DOCKER_PASSWORD; } }