Skip to content

Commit 624d8ed

Browse files
committed
refactor: align the structure with npm
Signed-off-by: Umberto Sgueglia <usgueglia@contractor.linuxfoundation.org>
1 parent f99de94 commit 624d8ed

9 files changed

Lines changed: 457 additions & 273 deletions

File tree

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { describe, expect, it } from 'vitest'
2+
3+
import { normalizeScmUrl } from '../extract'
4+
import { isPrerelease, parseRepoUrl } from '../normalize'
5+
6+
describe('isPrerelease', () => {
7+
it('returns false for a stable version', () => {
8+
expect(isPrerelease('3.12.0')).toBe(false)
9+
})
10+
11+
it('detects SNAPSHOT', () => {
12+
expect(isPrerelease('1.0.0-SNAPSHOT')).toBe(true)
13+
})
14+
15+
it('detects alpha', () => {
16+
expect(isPrerelease('2.0.0-alpha')).toBe(true)
17+
expect(isPrerelease('2.0.0-ALPHA.1')).toBe(true)
18+
})
19+
20+
it('detects beta', () => {
21+
expect(isPrerelease('1.5.0-beta.2')).toBe(true)
22+
})
23+
24+
it('detects rc', () => {
25+
expect(isPrerelease('4.0.0-rc1')).toBe(true)
26+
expect(isPrerelease('4.0.0-RC.2')).toBe(true)
27+
})
28+
29+
it('detects milestone (m1, m10)', () => {
30+
expect(isPrerelease('5.3.0-m1')).toBe(true)
31+
expect(isPrerelease('5.3.0-M10')).toBe(true)
32+
})
33+
34+
it('returns false for versions with numbers that are not milestones', () => {
35+
expect(isPrerelease('1.2.3')).toBe(false)
36+
expect(isPrerelease('10.0.0')).toBe(false)
37+
})
38+
})
39+
40+
describe('parseRepoUrl', () => {
41+
it('identifies github.com', () => {
42+
expect(parseRepoUrl('https://github.com/apache/commons-lang')).toEqual({
43+
host: 'github',
44+
owner: 'apache',
45+
name: 'commons-lang',
46+
})
47+
})
48+
49+
it('identifies gitlab.com', () => {
50+
expect(parseRepoUrl('https://gitlab.com/owner/repo')).toEqual({
51+
host: 'gitlab',
52+
owner: 'owner',
53+
name: 'repo',
54+
})
55+
})
56+
57+
it('identifies bitbucket.org', () => {
58+
expect(parseRepoUrl('https://bitbucket.org/owner/repo')).toEqual({
59+
host: 'bitbucket',
60+
owner: 'owner',
61+
name: 'repo',
62+
})
63+
})
64+
65+
it('returns other for unknown hosts', () => {
66+
const result = parseRepoUrl('https://svn.example.com/repo')
67+
expect(result?.host).toBe('other')
68+
})
69+
70+
it('returns null for invalid URLs', () => {
71+
expect(parseRepoUrl('not-a-url')).toBeNull()
72+
})
73+
74+
it('handles URLs with no path segments', () => {
75+
const result = parseRepoUrl('https://github.com/')
76+
expect(result).toEqual({ host: 'github', owner: null, name: null })
77+
})
78+
})
79+
80+
describe('normalizeScmUrl', () => {
81+
it('returns null for null input', () => {
82+
expect(normalizeScmUrl(null)).toBeNull()
83+
})
84+
85+
it('strips scm:git: prefix', () => {
86+
expect(normalizeScmUrl('scm:git:https://github.com/apache/commons-lang')).toBe(
87+
'https://github.com/apache/commons-lang',
88+
)
89+
})
90+
91+
it('converts SSH git@ to https', () => {
92+
expect(normalizeScmUrl('git@github.com:apache/commons-lang.git')).toBe(
93+
'https://github.com/apache/commons-lang',
94+
)
95+
})
96+
97+
it('converts git:// to https://', () => {
98+
expect(normalizeScmUrl('git://github.com/apache/commons-lang.git')).toBe(
99+
'https://github.com/apache/commons-lang',
100+
)
101+
})
102+
103+
it('strips trailing .git', () => {
104+
expect(normalizeScmUrl('https://github.com/apache/commons-lang.git')).toBe(
105+
'https://github.com/apache/commons-lang',
106+
)
107+
})
108+
109+
it('strips /tree/... path suffix', () => {
110+
expect(normalizeScmUrl('https://github.com/apache/commons-lang/tree/master')).toBe(
111+
'https://github.com/apache/commons-lang',
112+
)
113+
})
114+
115+
it('strips trailing slash', () => {
116+
expect(normalizeScmUrl('https://github.com/apache/commons-lang/')).toBe(
117+
'https://github.com/apache/commons-lang',
118+
)
119+
})
120+
121+
it('handles combined scm:git: + SSH form', () => {
122+
expect(normalizeScmUrl('scm:git:git@github.com:apache/commons-lang.git')).toBe(
123+
'https://github.com/apache/commons-lang',
124+
)
125+
})
126+
127+
it('returns null for non-https result', () => {
128+
expect(normalizeScmUrl('svn://svn.apache.org/repos/commons-lang')).toBeNull()
129+
})
130+
})

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

Lines changed: 19 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -29,14 +29,23 @@ export interface MavenVersionsMetadata {
2929
releaseVersion: string | null
3030
}
3131

32+
export type MavenFetchError =
33+
| { kind: 'NOT_FOUND' }
34+
| { kind: 'RATE_LIMIT'; status: number }
35+
| { kind: 'TRANSIENT'; message: string }
36+
37+
export function isMavenFetchError(v: unknown): v is MavenFetchError {
38+
return typeof v === 'object' && v !== null && 'kind' in v
39+
}
40+
3241
async function sleep(ms: number): Promise<void> {
3342
return new Promise((r) => setTimeout(r, ms))
3443
}
3544

3645
export async function resolveVersionsList(
3746
groupId: string,
3847
artifactId: string,
39-
): Promise<MavenVersionsMetadata | null> {
48+
): Promise<MavenVersionsMetadata | MavenFetchError> {
4049
const groupPath = groupId.replace(/\./g, '/')
4150
const url = `${MAVEN_REPO}/${groupPath}/${artifactId}/maven-metadata.xml`
4251

@@ -61,25 +70,29 @@ export async function resolveVersionsList(
6170
return { versions, releaseVersion: release || latest || null }
6271
} catch (err) {
6372
if (axios.isAxiosError(err)) {
64-
if (err.response?.status === 404) return null
73+
const status = err.response?.status
74+
if (status === 404) return { kind: 'NOT_FOUND' }
6575
// 429 = explicit rate limit, 403 = CDN throttle (Maven Central uses both)
66-
if ((err.response?.status === 429 || err.response?.status === 403) && attempt < MAX_RETRIES) {
76+
if ((status === 429 || status === 403) && attempt < MAX_RETRIES) {
6777
const delay = RETRY_BASE_MS * 2 ** attempt + Math.random() * 500
6878
await sleep(delay)
6979
continue
7080
}
81+
if (status === 429 || status === 403) return { kind: 'RATE_LIMIT', status: status! }
7182
}
72-
throw err
83+
const message = err instanceof Error ? err.message : String(err)
84+
return { kind: 'TRANSIENT', message }
7385
}
7486
}
7587

76-
return null
88+
return { kind: 'RATE_LIMIT', status: 429 }
7789
}
7890

7991
export async function resolveLatestVersion(
8092
groupId: string,
8193
artifactId: string,
8294
): Promise<string | null> {
8395
const meta = await resolveVersionsList(groupId, artifactId)
84-
return meta?.releaseVersion ?? null
96+
if (isMavenFetchError(meta)) return null
97+
return meta.releaseVersion
8598
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
export function isPrerelease(version: string): boolean {
2+
return /-(SNAPSHOT|alpha|beta|rc|m\d+)/i.test(version)
3+
}
4+
5+
export function parseRepoUrl(url: string): { host: string; owner: string | null; name: string | null } | null {
6+
try {
7+
const parsed = new URL(url)
8+
const h = parsed.hostname.toLowerCase()
9+
let host: string
10+
if (h === 'github.com' || h.endsWith('.github.com')) host = 'github'
11+
else if (h === 'gitlab.com' || h.includes('gitlab')) host = 'gitlab'
12+
else if (h === 'bitbucket.org') host = 'bitbucket'
13+
else host = 'other'
14+
const parts = parsed.pathname.split('/').filter(Boolean)
15+
return { host, owner: parts[0] ?? null, name: parts[1] ?? null }
16+
} catch {
17+
return null
18+
}
19+
}

0 commit comments

Comments
 (0)