diff --git a/__tests__/main.test.ts b/__tests__/main.test.ts index a36a22c5e..32d401b21 100644 --- a/__tests__/main.test.ts +++ b/__tests__/main.test.ts @@ -911,14 +911,15 @@ describe('getPackagesCredential', () => { expect(cred).toEqual({ type: 'rubygems_server', host: 'rubygems.pkg.github.com', - token: 'test-actor:test-token' + token: 'test-actor:test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential', () => { + it('preserves an explicit credential using a URL', () => { const existingCred: Credential = { type: 'rubygems_server', - host: 'rubygems.pkg.github.com', + url: 'https://RUBYGEMS.PKG.GITHUB.COM/', token: 'some-other-actor:some-other-token' } const details = createJobDetails('bundler', {[experimentName]: true}, [ @@ -939,14 +940,15 @@ describe('getPackagesCredential', () => { type: 'docker_registry', registry: 'ghcr.io', username: 'test-actor', - password: 'test-token' + password: 'test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential', () => { + it('preserves an explicit credential using a host', () => { const existingCred: Credential = { type: 'docker_registry', - registry: 'ghcr.io', + host: 'GHCR.IO/', username: 'some-other-actor', password: 'some-other-token' } @@ -970,14 +972,15 @@ describe('getPackagesCredential', () => { type: 'docker_registry', registry: 'ghcr.io', username: 'test-actor', - password: 'test-token' + password: 'test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential', () => { + it('creates a credential alongside an explicit path-scoped URL', () => { const existingCred: Credential = { type: 'docker_registry', - registry: 'ghcr.io', + url: 'https://GHCR.IO/other-owner/', username: 'some-other-actor', password: 'some-other-token' } @@ -987,7 +990,13 @@ describe('getPackagesCredential', () => { [existingCred] ) const cred = getPackagesCredential(details, 'test-actor') - expect(cred).toBeNull() + expect(cred).toEqual({ + type: 'docker_registry', + registry: 'ghcr.io', + username: 'test-actor', + password: 'test-token', + 'proxy-only': true + }) }) }) }) @@ -1003,14 +1012,15 @@ describe('getPackagesCredential', () => { type: 'docker_registry', registry: 'ghcr.io', username: 'test-actor', - password: 'test-token' + password: 'test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential', () => { + it('preserves an explicit credential using a registry', () => { const existingCred: Credential = { type: 'docker_registry', - registry: 'ghcr.io', + registry: 'GHCR.IO/', username: 'some-other-actor', password: 'some-other-token' } @@ -1032,16 +1042,17 @@ describe('getPackagesCredential', () => { const cred = getPackagesCredential(details, 'test-actor') expect(cred).toEqual({ type: 'maven_repository', - url: 'https://maven.pkg.github.com/test-org', + host: 'maven.pkg.github.com', username: 'test-actor', - password: 'test-token' + password: 'test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential with no trailing slash', () => { + it('creates a credential alongside an explicit cross-owner URL', () => { const existingCred: Credential = { type: 'maven_repository', - url: 'https://maven.pkg.github.com/TEST-ORG', + url: 'https://MAVEN.PKG.GITHUB.COM/OTHER-ORG', username: 'some-other-actor', password: 'some-other-token' } @@ -1049,13 +1060,19 @@ describe('getPackagesCredential', () => { existingCred ]) const cred = getPackagesCredential(details, 'test-actor') - expect(cred).toBeNull() + expect(cred).toEqual({ + type: 'maven_repository', + host: 'maven.pkg.github.com', + username: 'test-actor', + password: 'test-token', + 'proxy-only': true + }) }) - it('does not create a duplicate credential with a trailing slash', () => { + it('creates a credential alongside an explicit path-scoped registry', () => { const existingCred: Credential = { type: 'maven_repository', - url: 'https://maven.pkg.github.com/TEST-ORG/', + registry: 'MAVEN.PKG.GITHUB.COM/OTHER-ORG/', username: 'some-other-actor', password: 'some-other-token' } @@ -1063,7 +1080,13 @@ describe('getPackagesCredential', () => { existingCred ]) const cred = getPackagesCredential(details, 'test-actor') - expect(cred).toBeNull() + expect(cred).toEqual({ + type: 'maven_repository', + host: 'maven.pkg.github.com', + username: 'test-actor', + password: 'test-token', + 'proxy-only': true + }) }) }) }) @@ -1075,16 +1098,17 @@ describe('getPackagesCredential', () => { const cred = getPackagesCredential(details, 'test-actor') expect(cred).toEqual({ type: 'maven_repository', - url: 'https://maven.pkg.github.com/test-org', + host: 'maven.pkg.github.com', username: 'test-actor', - password: 'test-token' + password: 'test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential', () => { + it('preserves an explicit host-wide credential', () => { const existingCred: Credential = { type: 'maven_repository', - url: 'https://maven.pkg.github.com/TEST-ORG', + host: 'MAVEN.PKG.GITHUB.COM/', username: 'some-other-actor', password: 'some-other-token' } @@ -1107,14 +1131,15 @@ describe('getPackagesCredential', () => { expect(cred).toEqual({ type: 'npm_registry', registry: 'npm.pkg.github.com', - token: 'test-actor:test-token' + token: 'test-actor:test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential', () => { + it('preserves an explicit credential using a URL', () => { const existingCred: Credential = { type: 'npm_registry', - registry: 'npm.pkg.github.com', + url: 'https://NPM.PKG.GITHUB.COM/', token: 'some-other-actor:some-other-token' } const details = createJobDetails( @@ -1125,6 +1150,26 @@ describe('getPackagesCredential', () => { const cred = getPackagesCredential(details, 'test-actor') expect(cred).toBeNull() }) + + it('creates a credential alongside an explicit path-scoped URL', () => { + const existingCred: Credential = { + type: 'npm_registry', + url: 'https://NPM.PKG.GITHUB.COM/some-path/', + token: 'some-other-actor:some-other-token' + } + const details = createJobDetails( + 'npm_and_yarn', + {[experimentName]: true}, + [existingCred] + ) + const cred = getPackagesCredential(details, 'test-actor') + expect(cred).toEqual({ + type: 'npm_registry', + registry: 'npm.pkg.github.com', + token: 'test-actor:test-token', + 'proxy-only': true + }) + }) }) }) @@ -1136,14 +1181,15 @@ describe('getPackagesCredential', () => { expect(cred).toEqual({ type: 'npm_registry', registry: 'npm.pkg.github.com', - token: 'test-actor:test-token' + token: 'test-actor:test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential', () => { + it('preserves an explicit credential using a registry', () => { const existingCred: Credential = { type: 'npm_registry', - registry: 'npm.pkg.github.com', + registry: 'NPM.PKG.GITHUB.COM/', token: 'some-other-actor:some-other-token' } const details = createJobDetails('bun', {[experimentName]: true}, [ @@ -1187,9 +1233,10 @@ describe('getPackagesCredential', () => { const cred = getPackagesCredential(details, 'test-actor') expect(cred).toEqual({ type: 'nuget_feed', - url: 'https://nuget.pkg.github.com/test-org/index.json', + host: 'nuget.pkg.github.com', username: 'test-actor', - password: 'test-token' + password: 'test-token', + 'proxy-only': true }) }) @@ -1200,16 +1247,17 @@ describe('getPackagesCredential', () => { const cred = getPackagesCredential(details, 'test-actor') expect(cred).toEqual({ type: 'nuget_feed', - url: 'https://nuget.pkg.github.com/test-org/index.json', + host: 'nuget.pkg.github.com', username: 'test-actor', - password: 'test-token' + password: 'test-token', + 'proxy-only': true }) }) - it('does not create a duplicate credential', () => { + it('creates a credential alongside an explicit cross-owner URL', () => { const existingCred: Credential = { type: 'nuget_feed', - url: 'https://nuget.pkg.github.com/TEST-ORG/index.json', + url: 'https://NUGET.PKG.GITHUB.COM/OTHER-ORG/index.json/', username: 'some-other-actor', password: 'some-other-token' } @@ -1217,7 +1265,13 @@ describe('getPackagesCredential', () => { existingCred ]) const cred = getPackagesCredential(details, 'test-actor') - expect(cred).toBeNull() + expect(cred).toEqual({ + type: 'nuget_feed', + host: 'nuget.pkg.github.com', + username: 'test-actor', + password: 'test-token', + 'proxy-only': true + }) }) }) }) diff --git a/__tests__/proxy.test.ts b/__tests__/proxy.test.ts index 67a332bfc..209cce224 100644 --- a/__tests__/proxy.test.ts +++ b/__tests__/proxy.test.ts @@ -1,7 +1,7 @@ import * as core from '@actions/core' import Docker, {Container, Network} from 'dockerode' import {PassThrough} from 'node:stream' -import {ApiClient, JobDetails} from '../src/api-client' +import {ApiClient, Credential, JobDetails} from '../src/api-client' import {ContainerService} from '../src/container-service' import {Proxy, ProxyBuilder} from '../src/proxy' import {Updater} from '../src/updater' @@ -22,7 +22,8 @@ async function buildProxyWithStopError( stopError: Error, containerRemoveError?: Error, containerRemoveOverride?: jest.Mock, - experiments: object = {} + experiments: object = {}, + credentials: Credential[] = [] ): Promise { const containerRemove = containerRemoveOverride ?? @@ -63,7 +64,7 @@ async function buildProxyWithStopError( 1, 'job-token', 'https://dependabot-api.example.com', - [] + credentials ) return { @@ -145,6 +146,42 @@ describe('Proxy config', () => { storeInput.mockRestore() } }) + + it('forwards proxy-only credentials in full', async () => { + const credentials: Credential[] = [ + { + type: 'nuget_feed', + host: 'nuget.pkg.github.com', + username: 'dependabot[bot]', + password: 'github-token', + 'proxy-only': true + } + ] + const storeInput = jest + .spyOn(ContainerService, 'storeInput') + .mockResolvedValue(undefined) + + try { + await buildProxyWithStopError( + alreadyStoppedError(), + undefined, + undefined, + {}, + credentials + ) + + expect(storeInput).toHaveBeenCalledWith( + 'config.json', + '/', + expect.anything(), + expect.objectContaining({ + all_credentials: credentials + }) + ) + } finally { + storeInput.mockRestore() + } + }) }) describe('Proxy environment', () => { diff --git a/__tests__/updater.test.ts b/__tests__/updater.test.ts index f0e9d025c..936fc564c 100644 --- a/__tests__/updater.test.ts +++ b/__tests__/updater.test.ts @@ -132,6 +132,50 @@ describe('Updater', () => { expect(jest.mocked(ContainerService).run.mock.calls).toHaveLength(0) expect(mockProxy.shutdown.mock.calls).toHaveLength(1) }) + + it('omits proxy-only credentials from metadata but passes them to the proxy', async () => { + const proxyRun = jest.spyOn(ProxyBuilder.prototype, 'run') + const jobDetails = {...mockJobDetails} + const credentials = [ + { + type: 'git_source', + host: 'github.com', + username: 'user', + password: 'pass' + }, + { + type: 'maven_repository', + host: 'maven.pkg.github.com', + username: 'dependabot[bot]', + password: 'github-token', + 'proxy-only': true + } + ] + const updaterWithProxyOnlyCredential = new Updater( + 'MOCK_UPDATER_IMAGE_NAME', + 'MOCK_PROXY_IMAGE_NAME', + mockApiClient, + jobDetails, + credentials + ) + + expect(jobDetails['credentials-metadata']).toEqual([ + { + type: 'git_source', + host: 'github.com' + } + ]) + + mockApiClient.getJobToken.mockReturnValueOnce('job-token') + await updaterWithProxyOnlyCredential.runUpdater() + + expect(proxyRun).toHaveBeenCalledWith( + mockApiClient.params.jobId, + 'job-token', + mockApiClient.params.dependabotApiUrl, + credentials + ) + }) }) describe('when the updater container fails', () => { diff --git a/dist/main.js b/dist/main.js index b0e3fec87..a901f9bc2 100644 --- a/dist/main.js +++ b/dist/main.js @@ -100514,7 +100514,7 @@ var Updater = class { const unique = /* @__PURE__ */ new Set(); const result = []; for (const credential of this.credentials) { - if (credential.type === "jit_access") { + if (credential.type === "jit_access" || credential["proxy-only"] === true) { continue; } const obj = { type: credential.type }; @@ -100799,14 +100799,39 @@ function getPackagesCredential(jobDetails, actor) { credential = getNuGetPackagesCredential(jobDetails, actor, githubToken); break; } + if (credential !== null) { + credential["proxy-only"] = true; + } return credential; } +function hasCredentialForHost(jobDetails, type, host) { + return jobDetails["credentials-metadata"].some( + (credential) => credential.type === type && (normalizeCredentialHost(credential.host) === host || [credential.url, credential.registry].some( + (value) => isHostWideCredentialLocation(value, host) + )) + ); +} +function normalizeCredentialHost(value) { + if (!value) { + return null; + } + return value.toLowerCase().replace(/\/+$/, ""); +} +function isHostWideCredentialLocation(value, host) { + if (!value) { + return false; + } + try { + const url = value.includes("://") ? value : `https://${value}`; + const parsedUrl = new URL(url); + return parsedUrl.hostname.toLowerCase() === host && parsedUrl.pathname.replace(/\/+$/, "") === ""; + } catch { + return false; + } +} function getRubyGemsPackagesCredential(jobDetails, actor, githubToken) { const host = "rubygems.pkg.github.com"; - const existingIndex = jobDetails["credentials-metadata"].findIndex( - (c) => c.type === "rubygems_server" && (c.host || "").toLowerCase() === host - ); - if (existingIndex !== -1) { + if (hasCredentialForHost(jobDetails, "rubygems_server", host)) { return null; } return { @@ -100817,10 +100842,7 @@ function getRubyGemsPackagesCredential(jobDetails, actor, githubToken) { } function getDockerPackagesCredential(jobDetails, actor, githubToken) { const registry = "ghcr.io"; - const existingIndex = jobDetails["credentials-metadata"].findIndex( - (c) => c.type === "docker_registry" && (c.registry || "").toLowerCase() === registry - ); - if (existingIndex !== -1) { + if (hasCredentialForHost(jobDetails, "docker_registry", registry)) { return null; } return { @@ -100831,26 +100853,20 @@ function getDockerPackagesCredential(jobDetails, actor, githubToken) { }; } function getMavenPackagesCredential(jobDetails, actor, githubToken) { - const url = `https://maven.pkg.github.com/${jobDetails.source.repo.split("/")[0]}`; - const existingIndex = jobDetails["credentials-metadata"].findIndex( - (c) => c.type === "maven_repository" && (c.url || "").toLowerCase().replace(/\/$/, "") === url.toLowerCase() - ); - if (existingIndex !== -1) { + const host = "maven.pkg.github.com"; + if (hasCredentialForHost(jobDetails, "maven_repository", host)) { return null; } return { type: "maven_repository", - url, + host, username: actor, password: githubToken }; } function getNpmPackagesCredential(jobDetails, actor, githubToken) { const registry = "npm.pkg.github.com"; - const existingIndex = jobDetails["credentials-metadata"].findIndex( - (c) => c.type === "npm_registry" && (c.registry || "").toLowerCase() === registry - ); - if (existingIndex !== -1) { + if (hasCredentialForHost(jobDetails, "npm_registry", registry)) { return null; } return { @@ -100860,17 +100876,13 @@ function getNpmPackagesCredential(jobDetails, actor, githubToken) { }; } function getNuGetPackagesCredential(jobDetails, actor, githubToken) { - const orgName = jobDetails.source.repo.split("/")[0]; - const feedUrl = `https://nuget.pkg.github.com/${orgName}/index.json`; - const existingIndex = jobDetails["credentials-metadata"].findIndex( - (c) => c.type === "nuget_feed" && (c.url || "").toLowerCase() === feedUrl.toLowerCase() - ); - if (existingIndex !== -1) { + const host = "nuget.pkg.github.com"; + if (hasCredentialForHost(jobDetails, "nuget_feed", host)) { return null; } return { type: "nuget_feed", - url: feedUrl, + host, username: actor, password: githubToken }; diff --git a/src/api-client.ts b/src/api-client.ts index 155fb1c49..d1aa0a5a6 100644 --- a/src/api-client.ts +++ b/src/api-client.ts @@ -51,6 +51,7 @@ export type Credential = { 'auth-key'?: string 'tenant-id'?: string 'client-id'?: string + 'proxy-only'?: boolean } export type Metric = { diff --git a/src/main.ts b/src/main.ts index 5efe85bcc..d35aa9ecc 100644 --- a/src/main.ts +++ b/src/main.ts @@ -261,19 +261,63 @@ export function getPackagesCredential( break } + if (credential !== null) { + credential['proxy-only'] = true + } + return credential } +function hasCredentialForHost( + jobDetails: JobDetails, + type: string, + host: string +): boolean { + return jobDetails['credentials-metadata'].some( + credential => + credential.type === type && + (normalizeCredentialHost(credential.host) === host || + [credential.url, credential.registry].some(value => + isHostWideCredentialLocation(value, host) + )) + ) +} + +function normalizeCredentialHost(value: string | undefined): string | null { + if (!value) { + return null + } + + return value.toLowerCase().replace(/\/+$/, '') +} + +function isHostWideCredentialLocation( + value: string | undefined, + host: string +): boolean { + if (!value) { + return false + } + + try { + const url = value.includes('://') ? value : `https://${value}` + const parsedUrl = new URL(url) + return ( + parsedUrl.hostname.toLowerCase() === host && + parsedUrl.pathname.replace(/\/+$/, '') === '' + ) + } catch { + return false + } +} + function getRubyGemsPackagesCredential( jobDetails: JobDetails, actor: string, githubToken: string ): Credential | null { const host = 'rubygems.pkg.github.com' - const existingIndex = jobDetails['credentials-metadata'].findIndex( - c => c.type === 'rubygems_server' && (c.host || '').toLowerCase() === host - ) - if (existingIndex !== -1) { + if (hasCredentialForHost(jobDetails, 'rubygems_server', host)) { return null } @@ -291,12 +335,7 @@ function getDockerPackagesCredential( githubToken: string ): Credential | null { const registry = 'ghcr.io' - const existingIndex = jobDetails['credentials-metadata'].findIndex( - c => - c.type === 'docker_registry' && - (c.registry || '').toLowerCase() === registry - ) - if (existingIndex !== -1) { + if (hasCredentialForHost(jobDetails, 'docker_registry', registry)) { return null } @@ -314,20 +353,15 @@ function getMavenPackagesCredential( actor: string, githubToken: string ): Credential | null { - const url = `https://maven.pkg.github.com/${jobDetails.source.repo.split('/')[0]}` - const existingIndex = jobDetails['credentials-metadata'].findIndex( - c => - c.type === 'maven_repository' && - (c.url || '').toLowerCase().replace(/\/$/, '') === url.toLowerCase() - ) - if (existingIndex !== -1) { + const host = 'maven.pkg.github.com' + if (hasCredentialForHost(jobDetails, 'maven_repository', host)) { return null } - // proxy expects `url`, `username`, and `password` fields + // proxy expects `host`, `username`, and `password` fields return { type: 'maven_repository', - url, + host, username: actor, password: githubToken } @@ -339,11 +373,7 @@ function getNpmPackagesCredential( githubToken: string ): Credential | null { const registry = 'npm.pkg.github.com' - const existingIndex = jobDetails['credentials-metadata'].findIndex( - c => - c.type === 'npm_registry' && (c.registry || '').toLowerCase() === registry - ) - if (existingIndex !== -1) { + if (hasCredentialForHost(jobDetails, 'npm_registry', registry)) { return null } @@ -360,21 +390,15 @@ function getNuGetPackagesCredential( actor: string, githubToken: string ): Credential | null { - const orgName = jobDetails.source.repo.split('/')[0] - const feedUrl = `https://nuget.pkg.github.com/${orgName}/index.json` - const existingIndex = jobDetails['credentials-metadata'].findIndex( - c => - c.type === 'nuget_feed' && - (c.url || '').toLowerCase() === feedUrl.toLowerCase() - ) - if (existingIndex !== -1) { + const host = 'nuget.pkg.github.com' + if (hasCredentialForHost(jobDetails, 'nuget_feed', host)) { return null } - // proxy expects `url` and allows either `token` or `username` and `password` fields + // proxy expects `host` and allows either `token` or `username` and `password` fields return { type: 'nuget_feed', - url: feedUrl, + host, username: actor, password: githubToken } diff --git a/src/updater.ts b/src/updater.ts index 06597175b..e7750f163 100644 --- a/src/updater.ts +++ b/src/updater.ts @@ -68,7 +68,10 @@ export class Updater { const unique: Set = new Set() const result: Credential[] = [] for (const credential of this.credentials) { - if (credential.type === 'jit_access') { + if ( + credential.type === 'jit_access' || + credential['proxy-only'] === true + ) { continue }