Skip to content

Commit eb9e147

Browse files
authored
Merge branch 'main' into cm-361-affiliation-registry-migration
2 parents 88a6a11 + 58ed2cd commit eb9e147

4 files changed

Lines changed: 169 additions & 4 deletions

File tree

services/apps/packages_worker/src/maven/__tests__/registry.test.ts

Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,11 @@
11
import { describe, expect, it } from 'vitest'
22

33
import {
4+
GRADLE_PLUGIN_PORTAL_BASE_URL,
5+
JITPACK_BASE_URL,
46
MAVEN_CENTRAL_BASE_URL,
7+
isJitpackNamespace,
8+
jitpackPageUrl,
59
resolveRegistryBaseUrl,
610
resolveRegistryPageUrl,
711
resolveRegistryPageUrlFromBase,
@@ -148,6 +152,22 @@ describe('resolveRegistryBaseUrl', () => {
148152
'https://repo.jenkins-ci.org/public',
149153
)
150154
})
155+
156+
it('returns Maven Central for io.github namespace (JitPack is a fallback, not primary)', () => {
157+
expect(resolveRegistryBaseUrl('io.github.resilience4j')).toBe(MAVEN_CENTRAL_BASE_URL)
158+
})
159+
160+
it('returns Maven Central for bare io.github', () => {
161+
expect(resolveRegistryBaseUrl('io.github')).toBe(MAVEN_CENTRAL_BASE_URL)
162+
})
163+
164+
it('returns Maven Central for com.github namespace (JitPack is a fallback, not primary)', () => {
165+
expect(resolveRegistryBaseUrl('com.github.ben-manes')).toBe(MAVEN_CENTRAL_BASE_URL)
166+
})
167+
168+
it('returns Maven Central for io.githubfoo (no dot boundary)', () => {
169+
expect(resolveRegistryBaseUrl('io.githubfoo')).toBe(MAVEN_CENTRAL_BASE_URL)
170+
})
151171
})
152172

153173
describe('resolveRegistryPageUrl', () => {
@@ -174,6 +194,18 @@ describe('resolveRegistryPageUrl', () => {
174194
'https://central.sonatype.com/artifact/org.apache.commons/commons-lang3',
175195
)
176196
})
197+
198+
it('returns Maven Central URL for io.github (Central is primary)', () => {
199+
expect(resolveRegistryPageUrl('io.github.resilience4j', 'resilience4j-core')).toBe(
200+
'https://central.sonatype.com/artifact/io.github.resilience4j/resilience4j-core',
201+
)
202+
})
203+
204+
it('returns Maven Central URL for com.github (Central is primary)', () => {
205+
expect(resolveRegistryPageUrl('com.github.ben-manes', 'caffeine')).toBe(
206+
'https://central.sonatype.com/artifact/com.github.ben-manes/caffeine',
207+
)
208+
})
177209
})
178210

179211
describe('resolveRegistryPageUrlFromBase', () => {
@@ -193,4 +225,65 @@ describe('resolveRegistryPageUrlFromBase', () => {
193225
resolveRegistryPageUrlFromBase('com.google.firebase', 'firebase-analytics', googleMavenUrl),
194226
).toBe('https://maven.google.com/web/index.html#com.google.firebase:firebase-analytics')
195227
})
228+
229+
it('returns Gradle Plugin Portal path when resolvedBaseUrl is Gradle Plugin Portal', () => {
230+
expect(
231+
resolveRegistryPageUrlFromBase(
232+
'io.github.trueangle',
233+
'gradle-plugin',
234+
GRADLE_PLUGIN_PORTAL_BASE_URL,
235+
),
236+
).toBe('https://plugins.gradle.org/m2/io/github/trueangle/gradle-plugin/')
237+
})
238+
239+
it('returns JitPack browse URL when resolvedBaseUrl is JitPack', () => {
240+
expect(
241+
resolveRegistryPageUrlFromBase('io.github.trueangle', 'gradle-plugin', JITPACK_BASE_URL),
242+
).toBe('https://jitpack.io/#trueangle/gradle-plugin')
243+
})
244+
})
245+
246+
describe('isJitpackNamespace', () => {
247+
it('returns true for io.github.* packages', () => {
248+
expect(isJitpackNamespace('io.github.resilience4j')).toBe(true)
249+
})
250+
251+
it('returns true for bare io.github', () => {
252+
expect(isJitpackNamespace('io.github')).toBe(true)
253+
})
254+
255+
it('returns true for com.github.* packages', () => {
256+
expect(isJitpackNamespace('com.github.ben-manes')).toBe(true)
257+
})
258+
259+
it('returns true for bare com.github', () => {
260+
expect(isJitpackNamespace('com.github')).toBe(true)
261+
})
262+
263+
it('returns false for io.githubfoo (no dot boundary)', () => {
264+
expect(isJitpackNamespace('io.githubfoo')).toBe(false)
265+
})
266+
267+
it('returns false for unrelated namespaces', () => {
268+
expect(isJitpackNamespace('org.apache.commons')).toBe(false)
269+
expect(isJitpackNamespace('io.jenkins.plugins')).toBe(false)
270+
})
271+
})
272+
273+
describe('jitpackPageUrl', () => {
274+
it('strips io.github. prefix from groupId', () => {
275+
expect(jitpackPageUrl('io.github.resilience4j', 'resilience4j-core')).toBe(
276+
'https://jitpack.io/#resilience4j/resilience4j-core',
277+
)
278+
})
279+
280+
it('strips com.github. prefix from groupId', () => {
281+
expect(jitpackPageUrl('com.github.ben-manes', 'caffeine')).toBe(
282+
'https://jitpack.io/#ben-manes/caffeine',
283+
)
284+
})
285+
286+
it('handles bare io.github with no sub-namespace', () => {
287+
expect(jitpackPageUrl('io.github', 'some-lib')).toBe('https://jitpack.io/#/some-lib')
288+
})
196289
})

services/apps/packages_worker/src/maven/metadata.ts

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ import axios from 'axios'
1212
import { XMLParser } from 'fast-xml-parser'
1313

1414
import { isPrerelease } from './normalize'
15-
import { resolveRegistryBaseUrl } from './registry'
15+
import { JITPACK_BASE_URL, resolveRegistryBaseUrl } from './registry'
1616

1717
const REQUEST_TIMEOUT_MS = 10_000
1818
const MAX_RETRIES = 3
@@ -111,7 +111,12 @@ export async function resolveVersionsList(
111111
} catch (err) {
112112
if (axios.isAxiosError(err)) {
113113
const status = err.response?.status
114-
if (status === 404) return { kind: 'NOT_FOUND' }
114+
// 404 = standard not-found. 401 = JitPack-specific: returned for packages
115+
// that don't exist as public builds. Scoped to JitPack only — other
116+
// registries may return 401 for auth failures, which should not be
117+
// silently treated as a missing package.
118+
if (status === 404 || (status === 401 && url.startsWith(JITPACK_BASE_URL + '/')))
119+
return { kind: 'NOT_FOUND' }
115120
// 429 = explicit rate limit, 403 = CDN throttle (Maven Central uses both)
116121
if ((status === 429 || status === 403) && attempt < MAX_RETRIES) {
117122
const delay = RETRY_BASE_MS * 2 ** attempt + Math.random() * 500

services/apps/packages_worker/src/maven/registry.ts

Lines changed: 40 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,17 @@
99
*
1010
* All these repos expose the standard Maven repository layout, so the same
1111
* metadata.xml / POM fetch logic works — only the base URL changes.
12+
*
13+
* JitPack (io.github.*, com.github.*) is NOT listed here as a primary registry
14+
* because many well-known artifacts under those prefixes are published on Maven
15+
* Central (e.g. io.github.resilience4j, com.github.ben-manes:caffeine). Maven
16+
* Central is tried first; JitPack is used only as a fallback when Central returns
17+
* NOT_FOUND (see the enrichment loop).
1218
*/
1319

1420
export const MAVEN_CENTRAL_BASE_URL = 'https://repo1.maven.org/maven2'
21+
export const GRADLE_PLUGIN_PORTAL_BASE_URL = 'https://plugins.gradle.org/m2'
22+
export const JITPACK_BASE_URL = 'https://jitpack.io'
1523

1624
interface RegistryEntry {
1725
prefix: string
@@ -96,8 +104,8 @@ const ALTERNATIVE_REGISTRIES: RegistryEntry[] = [
96104
},
97105
{
98106
prefix: 'gradle.plugin',
99-
baseUrl: 'https://plugins.gradle.org/m2',
100-
pageUrl: (g, a) => `https://plugins.gradle.org/m2/${g.replace(/\./g, '/')}/${a}/`,
107+
baseUrl: GRADLE_PLUGIN_PORTAL_BASE_URL,
108+
pageUrl: (g, a) => `${GRADLE_PLUGIN_PORTAL_BASE_URL}/${g.replace(/\./g, '/')}/${a}/`,
101109
},
102110
{
103111
prefix: 'com.cloudbees.plugins',
@@ -166,6 +174,30 @@ export function isAlternativeRegistry(groupId: string): boolean {
166174
return findEntry(groupId) !== undefined
167175
}
168176

177+
/**
178+
* Returns true for io.github.* and com.github.* groupIds — the JitPack namespace
179+
* convention. Maven Central is tried first for these; JitPack is the fallback.
180+
*/
181+
export function isJitpackNamespace(groupId: string): boolean {
182+
return (
183+
groupId === 'io.github' ||
184+
groupId.startsWith('io.github.') ||
185+
groupId === 'com.github' ||
186+
groupId.startsWith('com.github.')
187+
)
188+
}
189+
190+
/** Returns a human-browsable JitPack URL for an io.github.* or com.github.* artifact. */
191+
export function jitpackPageUrl(groupId: string, artifactId: string): string {
192+
if (groupId.startsWith('io.github.')) {
193+
return `${JITPACK_BASE_URL}/#${groupId.slice('io.github.'.length)}/${artifactId}`
194+
}
195+
if (groupId.startsWith('com.github.')) {
196+
return `${JITPACK_BASE_URL}/#${groupId.slice('com.github.'.length)}/${artifactId}`
197+
}
198+
return `${JITPACK_BASE_URL}/#/${artifactId}`
199+
}
200+
169201
/**
170202
* Returns a human-browsable URL for the given artifact in its authoritative registry.
171203
*/
@@ -192,5 +224,11 @@ export function resolveRegistryPageUrlFromBase(
192224
if (isCentral) {
193225
return `https://central.sonatype.com/artifact/${groupId}/${artifactId}`
194226
}
227+
if (resolvedBaseUrl === GRADLE_PLUGIN_PORTAL_BASE_URL) {
228+
return `${GRADLE_PLUGIN_PORTAL_BASE_URL}/${groupId.replace(/\./g, '/')}/${artifactId}/`
229+
}
230+
if (resolvedBaseUrl === JITPACK_BASE_URL) {
231+
return jitpackPageUrl(groupId, artifactId)
232+
}
195233
return resolveRegistryPageUrl(groupId, artifactId)
196234
}

services/apps/packages_worker/src/maven/runMavenEnrichmentLoop.ts

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,8 +19,11 @@ import { extractArtifact, getPomCacheStats, normalizeScmUrl } from './extract'
1919
import { isMavenFetchError, resolveVersionsList } from './metadata'
2020
import { isPrerelease, parseRepoUrl } from './normalize'
2121
import {
22+
GRADLE_PLUGIN_PORTAL_BASE_URL,
23+
JITPACK_BASE_URL,
2224
MAVEN_CENTRAL_BASE_URL,
2325
isAlternativeRegistry,
26+
isJitpackNamespace,
2427
resolveRegistryBaseUrl,
2528
resolveRegistryPageUrl,
2629
resolveRegistryPageUrlFromBase,
@@ -141,6 +144,32 @@ async function processCriticalPackage(qx: QueryExecutor, pkg: PackageRow, forceF
141144
}
142145
}
143146

147+
// Second fallback: JitPack for io.github.* / com.github.* packages not found on
148+
// Maven Central. These namespaces are also used by well-known Central artifacts
149+
// (e.g. resilience4j, caffeine), so Central is tried first; JitPack is the fallback
150+
// for packages that genuinely live only on JitPack.
151+
if (isMavenFetchError(metadata) && metadata.kind === 'NOT_FOUND' && isJitpackNamespace(groupId)) {
152+
const jitpackMetadata = await resolveVersionsList(groupId, artifactId, JITPACK_BASE_URL)
153+
if (!isMavenFetchError(jitpackMetadata)) {
154+
log.info({ groupId, artifactId }, 'Not found on Maven Central — resolved via JitPack fallback')
155+
baseUrl = JITPACK_BASE_URL
156+
metadata = jitpackMetadata
157+
}
158+
}
159+
160+
// Third fallback: Gradle Plugin Portal. Plugins can be published under any groupId
161+
// (not just gradle.plugin.*), so this is worth trying for all NOT_FOUND packages.
162+
// Skip when GPP is already the primary registry (gradle.plugin.*) to avoid a
163+
// redundant second call to the same URL.
164+
if (isMavenFetchError(metadata) && metadata.kind === 'NOT_FOUND' && baseUrl !== GRADLE_PLUGIN_PORTAL_BASE_URL) {
165+
const gradlePortalMetadata = await resolveVersionsList(groupId, artifactId, GRADLE_PLUGIN_PORTAL_BASE_URL)
166+
if (!isMavenFetchError(gradlePortalMetadata)) {
167+
log.info({ groupId, artifactId }, 'Not found in primary/Central — resolved via Gradle Plugin Portal fallback')
168+
baseUrl = GRADLE_PLUGIN_PORTAL_BASE_URL
169+
metadata = gradlePortalMetadata
170+
}
171+
}
172+
144173
if (isMavenFetchError(metadata)) {
145174
if (metadata.kind === 'NOT_FOUND') {
146175
await upsertPackage(qx, {

0 commit comments

Comments
 (0)