diff --git a/lib/modules/datasource/docker/index.spec.ts b/lib/modules/datasource/docker/index.spec.ts index 5049f640894baeba92b891d006ad029a4225cf54..48d38b1cca8f328cc6d99cf1d141be2f6bb45dd8 100644 --- a/lib/modules/datasource/docker/index.spec.ts +++ b/lib/modules/datasource/docker/index.spec.ts @@ -608,7 +608,7 @@ describe('modules/datasource/docker/index', () => { registryUrls: ['https://registry.company.com'], }; const res = await getPkgReleases(config); - expect(res.releases).toHaveLength(1); + expect(res?.releases).toHaveLength(1); }); it('uses custom registry in depName', async () => { @@ -627,7 +627,7 @@ describe('modules/datasource/docker/index', () => { datasource: DockerDatasource.id, depName: 'registry.company.com/node', }); - expect(res.releases).toHaveLength(1); + expect(res?.releases).toHaveLength(1); }); it('uses quay api', async () => { @@ -652,7 +652,7 @@ describe('modules/datasource/docker/index', () => { registryUrls: ['https://quay.io'], }; const res = await getPkgReleases(config); - expect(res.releases).toHaveLength(1); + expect(res?.releases).toHaveLength(1); }); it('uses quay api and test error', async () => { @@ -988,7 +988,7 @@ describe('modules/datasource/docker/index', () => { datasource: DockerDatasource.id, depName: 'node', }); - expect(res.releases).toHaveLength(1); + expect(res?.releases).toHaveLength(1); }); it('adds library/ prefix for Docker Hub (explicit)', async () => { @@ -1016,7 +1016,7 @@ describe('modules/datasource/docker/index', () => { datasource: DockerDatasource.id, depName: 'docker.io/node', }); - expect(res.releases).toHaveLength(1); + expect(res?.releases).toHaveLength(1); }); it('adds no library/ prefix for other registries', async () => { @@ -1042,14 +1042,14 @@ describe('modules/datasource/docker/index', () => { datasource: DockerDatasource.id, depName: 'k8s.gcr.io/kubernetes-dashboard-amd64', }); - expect(res.releases).toHaveLength(1); + expect(res?.releases).toHaveLength(1); }); it('returns null on error', async () => { httpMock .scope(baseUrl) .get('/my/node/tags/list?n=10000') - .reply(200, null) + .reply(200) .get('/my/node/tags/list?n=10000') .replyWithError('error'); const res = await getPkgReleases({ @@ -1442,5 +1442,62 @@ describe('modules/datasource/docker/index', () => { releases: [], }); }); + + it('supports ghcr', async () => { + hostRules.find.mockResolvedValue({} as never); + httpMock + .scope('https://ghcr.io/v2', { + badheaders: ['authorization'], + }) + .get('/') + .twice() + .reply(401, '', { + 'www-authenticate': + 'Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:user/image:pull', + }) + .get('/visualon/drone-git/tags/list?n=10000') + .reply(401, '', { + 'www-authenticate': + 'Bearer realm="https://ghcr.io/token",service="ghcr.io",scope="repository:visualon/drone-git:pull"', + }); + httpMock + .scope('https://ghcr.io') + .get('/token?service=ghcr.io&scope=repository:visualon/drone-git:pull') + .times(3) + .reply(200, { token: 'abc' }); + httpMock + .scope('https://ghcr.io/v2', { + reqheaders: { + authorization: 'Bearer abc', + }, + }) + .get('/visualon/drone-git/tags/list?n=10000') + .reply(200, { tags: ['latest', '1.0.0'] }) + .get('/visualon/drone-git/manifests/latest') + .reply(200, { + schemaVersion: 2, + mediaType: MediaType.manifestV2, + config: { digest: 'some-config-digest' }, + }) + .get('/visualon/drone-git/blobs/some-config-digest') + .reply(200, { + config: { + Labels: { + 'org.opencontainers.image.source': + 'https://github.com/visualon/drone-git', + }, + }, + }); + + const res = await getPkgReleases({ + datasource: DockerDatasource.id, + depName: 'ghcr.io/visualon/drone-git', + }); + expect(res).toStrictEqual({ + registryUrl: 'https://ghcr.io', + sourceUrl: 'https://github.com/visualon/drone-git', + releases: [{ version: '1.0.0' }], + }); + }); }); }); diff --git a/lib/modules/datasource/docker/index.ts b/lib/modules/datasource/docker/index.ts index 54e2e5fcae051cd98b92110465290b057ab18cdb..def48cc64fa71e746865e37393d8fad8d1b08219 100644 --- a/lib/modules/datasource/docker/index.ts +++ b/lib/modules/datasource/docker/index.ts @@ -136,7 +136,11 @@ export async function getAuthHeaders( } let scope = `repository:${dockerRepository}:pull`; - if (is.string(authenticateHeader.params.scope)) { + // repo isn't known to server yet, so causing wrong scope `repository:user/image:pull` + if ( + is.string(authenticateHeader.params.scope) && + !apiCheckUrl.endsWith('/v2/') + ) { scope = authenticateHeader.params.scope; } diff --git a/test/http-mock.ts b/test/http-mock.ts index f17942374f10c01dca858ac068d7b4f371c385d9..ded50e9e5b8237e62f2067612c0bfd5429765881 100644 --- a/test/http-mock.ts +++ b/test/http-mock.ts @@ -49,12 +49,12 @@ export function clear(throwOnPending = true): void { const missing = missingLog; requestLog = []; missingLog = []; - if (!isDone && throwOnPending) { - throw new Error(`Pending mocks!\n * ${pending.join('\n * ')}`); - } if (missing.length && throwOnPending) { throw new Error(`Missing mocks!\n * ${missing.join('\n * ')}`); } + if (!isDone && throwOnPending) { + throw new Error(`Pending mocks!\n * ${pending.join('\n * ')}`); + } } export function scope(basePath: BasePath, options?: nock.Options): nock.Scope {