diff --git a/packages/squad-sdk/src/platform/detect.ts b/packages/squad-sdk/src/platform/detect.ts index 1d351b3a7..68824b866 100644 --- a/packages/squad-sdk/src/platform/detect.ts +++ b/packages/squad-sdk/src/platform/detect.ts @@ -27,14 +27,17 @@ export interface AzureDevOpsRemoteInfo { * git@github.com:owner/repo.git */ export function parseGitHubRemote(url: string): GitHubRemoteInfo | null { + // Repo capture allows dots (GitHub permits them, e.g. `foo/bar.baz`); the + // optional `\.git$` plus the non-greedy quantifier still strips a trailing + // `.git` correctly. // HTTPS: https://github.com/owner/repo.git - const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/.]+?)(?:\.git)?$/i); + const httpsMatch = url.match(/github\.com\/([^/]+)\/([^/]+?)(?:\.git)?$/i); if (httpsMatch) { return { owner: httpsMatch[1]!, repo: httpsMatch[2]! }; } // SSH: git@github.com:owner/repo.git - const sshMatch = url.match(/github\.com:([^/]+)\/([^/.]+?)(?:\.git)?$/i); + const sshMatch = url.match(/github\.com:([^/]+)\/([^/]+?)(?:\.git)?$/i); if (sshMatch) { return { owner: sshMatch[1]!, repo: sshMatch[2]! }; } @@ -51,10 +54,12 @@ export function parseGitHubRemote(url: string): GitHubRemoteInfo | null { * https://org.visualstudio.com/project/_git/repo */ export function parseAzureDevOpsRemote(url: string): AzureDevOpsRemoteInfo | null { + // Repo capture allows dots in repo names; the literal `/_git/` segment keeps + // the match anchored unambiguously. // HTTPS dev.azure.com: https://dev.azure.com/org/project/_git/repo // Also handles: https://org@dev.azure.com/org/project/_git/repo const devAzureHttps = url.match( - /dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/.]+?)(?:\.git)?$/i, + /dev\.azure\.com\/([^/]+)\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/i, ); if (devAzureHttps) { return { org: devAzureHttps[1]!, project: devAzureHttps[2]!, repo: devAzureHttps[3]! }; @@ -62,15 +67,17 @@ export function parseAzureDevOpsRemote(url: string): AzureDevOpsRemoteInfo | nul // SSH dev.azure.com: git@ssh.dev.azure.com:v3/org/project/repo const devAzureSsh = url.match( - /ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/.]+?)(?:\.git)?$/i, + /ssh\.dev\.azure\.com:v3\/([^/]+)\/([^/]+)\/([^/]+?)(?:\.git)?$/i, ); if (devAzureSsh) { return { org: devAzureSsh[1]!, project: devAzureSsh[2]!, repo: devAzureSsh[3]! }; } // Legacy visualstudio.com: https://org.visualstudio.com/project/_git/repo + // (Org subdomain keeps the no-dot constraint — the `.visualstudio.com` anchor + // requires it. Only the repo capture is widened.) const vsMatch = url.match( - /([^/.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/.]+?)(?:\.git)?$/i, + /([^/.]+)\.visualstudio\.com\/([^/]+)\/_git\/([^/]+?)(?:\.git)?$/i, ); if (vsMatch) { return { org: vsMatch[1]!, project: vsMatch[2]!, repo: vsMatch[3]! }; diff --git a/test/platform-adapter.test.ts b/test/platform-adapter.test.ts index c14748973..4e14989cc 100644 --- a/test/platform-adapter.test.ts +++ b/test/platform-adapter.test.ts @@ -96,6 +96,36 @@ describe('parseGitHubRemote', () => { // trailing slash is not standard git remote but shouldn't crash expect(parseGitHubRemote('https://github.com/owner/')).toBeNull(); }); + + // Regression: the repo capture used to disallow `.`, so any GitHub repo whose + // name contained a dot (which GitHub permits) failed to parse and bubbled up + // as `Could not detect platform: Could not parse GitHub remote URL: ...` in + // `squad watch`. See issue #1077. + it('parses HTTPS URL with dots in repo name', () => { + const result = parseGitHubRemote('https://github.com/myorg/JADE.xlighthousepipelines.git'); + expect(result).toEqual({ owner: 'myorg', repo: 'JADE.xlighthousepipelines' }); + }); + + it('parses HTTPS URL with dots in repo name and no .git suffix', () => { + const result = parseGitHubRemote('https://github.com/myorg/JADE.xlighthousepipelines'); + expect(result).toEqual({ owner: 'myorg', repo: 'JADE.xlighthousepipelines' }); + }); + + it('parses SSH URL with dots in repo name', () => { + const result = parseGitHubRemote('git@github.com:myorg/foo.bar.baz.git'); + expect(result).toEqual({ owner: 'myorg', repo: 'foo.bar.baz' }); + }); + + it('parses SSH URL with dots in repo name and no .git suffix', () => { + const result = parseGitHubRemote('git@github.com:myorg/foo.bar.baz'); + expect(result).toEqual({ owner: 'myorg', repo: 'foo.bar.baz' }); + }); + + it('parses repo named exactly ".github" (with leading dot before .git)', () => { + // owner/.github is a real convention for community-health files + const result = parseGitHubRemote('https://github.com/myorg/.github.git'); + expect(result).toEqual({ owner: 'myorg', repo: '.github' }); + }); }); // ─── Azure DevOps Remote Parsing ─────────────────────────────────────── @@ -152,6 +182,27 @@ describe('parseAzureDevOpsRemote', () => { const result = parseAzureDevOpsRemote('https://dev.azure.com/org/My-Project/_git/my-repo'); expect(result).toEqual({ org: 'org', project: 'My-Project', repo: 'my-repo' }); }); + + // Regression: see issue #1077 — repo capture used to disallow `.`. + it('parses HTTPS dev.azure.com URL with dots in repo name', () => { + const result = parseAzureDevOpsRemote('https://dev.azure.com/org/proj/_git/repo.with.dots'); + expect(result).toEqual({ org: 'org', project: 'proj', repo: 'repo.with.dots' }); + }); + + it('parses HTTPS dev.azure.com URL with dots in repo name and .git suffix', () => { + const result = parseAzureDevOpsRemote('https://dev.azure.com/org/proj/_git/repo.with.dots.git'); + expect(result).toEqual({ org: 'org', project: 'proj', repo: 'repo.with.dots' }); + }); + + it('parses SSH dev.azure.com URL with dots in repo name', () => { + const result = parseAzureDevOpsRemote('git@ssh.dev.azure.com:v3/org/proj/repo.with.dots.git'); + expect(result).toEqual({ org: 'org', project: 'proj', repo: 'repo.with.dots' }); + }); + + it('parses legacy visualstudio.com URL with dots in repo name', () => { + const result = parseAzureDevOpsRemote('https://contoso.visualstudio.com/proj/_git/repo.with.dots.git'); + expect(result).toEqual({ org: 'contoso', project: 'proj', repo: 'repo.with.dots' }); + }); }); // ─── WorkItem Type Shape ───────────────────────────────────────────────