@@ -453,37 +453,89 @@ export async function extractArtifact(groupId: string, artifactId: string, versi
453453// ─── SCM URL normalisation ───────────────────────────────────────────────────
454454
455455/**
456- * Converts the raw SCM URL from a POM (declared_repository_url) into a clean
457- * HTTPS repository URL suitable for storage as repository_url.
456+ * Known source-code-hosting hosts. A normalised repository_url is only produced
457+ * when the URL resolves to one of these — anything else (homepages, doc sites,
458+ * placeholders) yields null so it is never stored as a repository link.
459+ *
460+ * TODO(CM): host list pending product confirmation before rollout.
461+ */
462+ const SCM_HOSTS = new Set ( [
463+ 'github.com' ,
464+ 'gitlab.com' ,
465+ 'bitbucket.org' ,
466+ 'gitee.com' ,
467+ 'codeberg.org' ,
468+ ] )
469+
470+ /** Hosts whose owner/repo path is case-insensitive and should be lower-cased. */
471+ const CASE_INSENSITIVE_HOSTS = new Set ( [ 'github.com' , 'gitlab.com' ] )
472+
473+ /**
474+ * Converts the raw SCM URL from a POM (declared_repository_url) into a clean,
475+ * canonical `https://<host>/<owner>/<repo>` repository URL suitable for storage
476+ * as repository_url. Returns null when the input does not resolve to a real
477+ * repository on a known SCM host.
458478 *
459479 * Handles common Maven SCM URL forms:
460- * scm:git:git@github.com:owner/repo.git → https://github.com/owner/repo
461- * scm:git:https://github.com/owner/repo → https://github.com/owner/repo
462- * git://github.com/owner/repo.git → https://github.com/owner/repo
463- * https://github.com/owner/repo/tree/... → https://github.com/owner/repo
480+ * scm:git:git@github.com:owner/repo.git → https://github.com/owner/repo
481+ * scm:git:https://github.com/owner/repo → https://github.com/owner/repo
482+ * scm:git:github.com/owner/repo → https://github.com/owner/repo
483+ * github.com/owner/repo (no scheme) → https://github.com/owner/repo
484+ * git://github.com/owner/repo.git → https://github.com/owner/repo
485+ * http://github.com/owner/repo/tree/... → https://github.com/owner/repo
486+ *
487+ * Rejected (→ null): website-only URLs (https://meson.ai/), non-SCM hosts
488+ * (svn://…, http://source.android.com), placeholders (Private, ${scm-url}).
464489 */
465490export function normalizeScmUrl ( raw : string | null ) : string | null {
466491 if ( ! raw ) return null
467- let url = raw . trim ( )
492+ let s = raw . trim ( )
493+ if ( ! s ) return null
468494
469- // Strip scm:git: or scm: prefix
470- url = url . replace ( / ^ s c m : g i t : / i, '' ) . replace ( / ^ s c m : / i, '' )
495+ // Strip Maven scm:git: / scm: prefix
496+ s = s . replace ( / ^ s c m : g i t : / i, '' ) . replace ( / ^ s c m : / i, '' )
471497
472- // Convert SSH git@host :owner/repo → https://host/owner/repo
473- url = url . replace ( / ^ g i t @ ( [ ^ : ] + ) : ( . + ) $ / , 'https://$1/$2 ' )
498+ // git+https://… → https://…
499+ s = s . replace ( / ^ g i t \+ / , '' )
474500
475- // Convert git:// → https://
476- url = url . replace ( / ^ g i t : \/ \/ / , 'https://' )
501+ // SCP form git@host :owner/repo → https://host/owner/repo
502+ s = s . replace ( / ^ g i t @ ( [ ^ : / ] + ) : ( . + ) $ / , 'https://$1/$2 ' )
477503
478- // Strip trailing .git
479- url = url . replace ( / \. g i t $ / , '' )
504+ // ssh://git @host /… → https://host/…
505+ s = s . replace ( / ^ s s h : \/ \/ g i t @ ( [ ^ / ] + ) \/ / , 'https://$1/ ' )
480506
481- // Strip /tree/... or /blob/... path suffixes (keep only host + owner + repo)
482- url = url . replace ( / \/ ( t r e e | b l o b ) ( \/ . * ) ? $ / , '' )
507+ // git:// → https://, and upgrade http:// → https://
508+ s = s . replace ( / ^ g i t : \/ \/ / , 'https://' ) . replace ( / ^ h t t p : \/ \/ / , 'https:// ' )
483509
484- if ( ! url . startsWith ( 'https://' ) ) return null
510+ // No scheme at all (e.g. "github.com/owner/repo") → assume https
511+ if ( ! s . includes ( '://' ) ) s = `https://${ s } `
512+
513+ let parsed : URL
514+ try {
515+ parsed = new URL ( s )
516+ } catch {
517+ return null
518+ }
519+
520+ if ( parsed . protocol !== 'https:' ) return null
521+
522+ const host = parsed . hostname . toLowerCase ( ) . replace ( / ^ w w w \. / , '' )
523+ if ( ! SCM_HOSTS . has ( host ) ) return null
524+
525+ // Require at least owner + repo path segments
526+ const segments = parsed . pathname . split ( '/' ) . filter ( Boolean )
527+ if ( segments . length < 2 ) return null
528+
529+ let owner = segments [ 0 ]
530+ let name = segments [ 1 ] . replace ( / \. g i t $ / , '' )
531+ if ( ! owner || ! name ) return null
532+
533+ if ( CASE_INSENSITIVE_HOSTS . has ( host ) ) {
534+ owner = owner . toLowerCase ( )
535+ name = name . toLowerCase ( )
536+ }
485537
486- return url . replace ( / \/ $ / , '' )
538+ return `https:// ${ host } / ${ owner } / ${ name } `
487539}
488540
489541// ─── Private helpers ──────────────────────────────────────────────────────────
0 commit comments