diff --git a/.changeset/fix-github-url-parsing.md b/.changeset/fix-github-url-parsing.md new file mode 100644 index 000000000..9cfee20b0 --- /dev/null +++ b/.changeset/fix-github-url-parsing.md @@ -0,0 +1,5 @@ +--- +"@asyncapi/cli": patch +--- + +Fix GitHub URL parsing for branch names containing slashes and file extension detection for multi-dot filenames diff --git a/src/apps/cli/commands/new/file.ts b/src/apps/cli/commands/new/file.ts index de61abc5d..03dcf5565 100644 --- a/src/apps/cli/commands/new/file.ts +++ b/src/apps/cli/commands/new/file.ts @@ -175,7 +175,7 @@ export default class NewFile extends Command { if (!fileName.includes('.')) { fileNameToWriteToDisk = `${fileName}.yaml`; } else { - const extension = fileName.split('.')[1]; + const extension = fileName.split('.').pop() ?? ''; if (extension === 'yml' || extension === 'yaml' || extension === 'json') { fileNameToWriteToDisk = fileName; diff --git a/src/domains/models/SpecificationFile.ts b/src/domains/models/SpecificationFile.ts index 5370f6e67..cbb19c13e 100644 --- a/src/domains/models/SpecificationFile.ts +++ b/src/domains/models/SpecificationFile.ts @@ -237,7 +237,7 @@ export async function fileExists(name: string): Promise { return true; } - const extension = name.split('.')[1]; + const extension = name.split('.').pop() ?? ''; const allowedExtenstion = ['yml', 'yaml', 'json']; diff --git a/src/domains/services/validation.service.ts b/src/domains/services/validation.service.ts index 11ac45fc4..d52233cbe 100644 --- a/src/domains/services/validation.service.ts +++ b/src/domains/services/validation.service.ts @@ -69,12 +69,30 @@ const convertGitHubWebUrl = (url: string): string => { const urlWithoutFragment = url.split('#')[0]; // Handle GitHub web URLs like: https://github.com/owner/repo/blob/branch/path + // Branch names can contain slashes (e.g. feature/new-validation) // eslint-disable-next-line no-useless-escape - const githubWebPattern = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/([^\/]+)\/(.+)$/; + const githubWebPattern = /^https:\/\/github\.com\/([^\/]+)\/([^\/]+)\/blob\/(.+)$/; const match = urlWithoutFragment.match(githubWebPattern); if (match) { - const [, owner, repo, branch, filePath] = match; + const [, owner, repo, branchAndPath] = match; + // Split on the first '/' after the branch name segment to separate branch from file path. + // Since branch names can contain '/', we need to find where the branch ends and the file path begins. + // We try progressively longer prefixes until the GitHub API confirms a valid path. + // Simple heuristic: try each possible split point from left to right. + const segments = branchAndPath.split('/'); + // Try each possible split: branch = segments[0..i], filePath = segments[i+1..] + for (let i = 0; i < segments.length - 1; i++) { + const branch = segments.slice(0, i + 1).join('/'); + const filePath = segments.slice(i + 1).join('/'); + // If the filePath contains a dot (likely a file), use this split + if (filePath.includes('.')) { + return `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`; + } + } + // Fallback: treat everything after the first segment as filePath + const branch = segments[0]; + const filePath = segments.slice(1).join('/'); return `https://api.github.com/repos/${owner}/${repo}/contents/${filePath}?ref=${branch}`; } diff --git a/test/unit/services/validation.service.test.ts b/test/unit/services/validation.service.test.ts index 401381bfe..c680412df 100644 --- a/test/unit/services/validation.service.test.ts +++ b/test/unit/services/validation.service.test.ts @@ -244,8 +244,8 @@ describe('ValidationService', () => { }; const result = await validationService.validateDocument(specFile, options); - // The validation succeeds means the validation command is successfully executed it is independent whether - // the document is valid or not + // The validation succeeds means the validation command is successfully executed it is independent whether + // the document is valid or not expect(result.success).to.equal(true); if (result.success) { expect(result.data).to.have.property('status'); @@ -253,5 +253,34 @@ describe('ValidationService', () => { expect(result.data?.diagnostics).to.be.an('array'); } }); + + it('should correctly parse GitHub URLs with slash-based branch names', async () => { + const specWithSlashBranch = `{ + "asyncapi": "2.6.0", + "info": { "title": "Test", "version": "1.0.0" }, + "channels": { + "user/event": { + "publish": { + "message": { + "payload": { + "$ref": "https://github.com/private-org/private-repo/blob/feature/new-validation/schema.yaml#/payload" + } + } + } + } + } + }`; + const specFile = new Specification(specWithSlashBranch); + const options = { 'diagnostics-format': 'stylish' as const }; + + const result = await validationService.validateDocument(specFile, options); + expect(result.success).to.equal(true); + if (result.success) { + const invalidRefDiagnostic = result.data?.diagnostics?.find((d: any) => d.code === 'invalid-ref'); + // eslint-disable-next-line no-unused-expressions + expect(invalidRefDiagnostic).to.exist; + expect(invalidRefDiagnostic?.message).to.include('feature/new-validation'); + } + }); }); });