fix(Github Node): Modify regex validation to support custom urls (#19076)

This commit is contained in:
Shireen Missi
2025-09-16 12:16:41 +02:00
committed by GitHub
parent 879e8f17ec
commit 3537a734f3
3 changed files with 265 additions and 8 deletions

View File

@@ -517,13 +517,13 @@ export class Github implements INodeType {
placeholder: 'e.g. https://github.com/n8n-io',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)',
regex: 'https:\\/\\/(?:[^/]+)\\/([-_0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)(?:.*)',
regex: 'https:\\/\\/([^/]+)\\/([-_0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid Github URL',
},
},
@@ -579,13 +579,13 @@ export class Github implements INodeType {
placeholder: 'e.g. https://github.com/n8n-io/n8n',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)',
regex: 'https:\\/\\/(?:[^/]+)\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)',
regex: 'https:\\/\\/([^/]+)\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid Github Repository URL',
},
},

View File

@@ -104,13 +104,13 @@ export class GithubTrigger implements INodeType {
placeholder: 'e.g. https://github.com/n8n-io',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)',
regex: 'https:\\/\\/(?:[^/]+)\\/([-_0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/github.com\\/([-_0-9a-zA-Z]+)(?:.*)',
regex: 'https:\\/\\/([^/]+)\\/([-_0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid Github URL',
},
},
@@ -158,13 +158,13 @@ export class GithubTrigger implements INodeType {
placeholder: 'e.g. https://github.com/n8n-io/n8n',
extractValue: {
type: 'regex',
regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)',
regex: 'https:\\/\\/(?:[^/]+)\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)',
},
validation: [
{
type: 'regex',
properties: {
regex: 'https:\\/\\/github.com\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)',
regex: 'https:\\/\\/([^/]+)\\/(?:[-_0-9a-zA-Z]+)\\/([-_.0-9a-zA-Z]+)(?:.*)',
errorMessage: 'Not a valid Github Repository URL',
},
},

View File

@@ -0,0 +1,257 @@
import { Github } from '../Github.node';
import { GithubTrigger } from '../GithubTrigger.node';
interface ValidationRule {
type: string;
properties: {
regex: string;
errorMessage: string;
};
}
describe('GitHub Node URL Pattern Tests', () => {
let githubNode: Github;
let githubTriggerNode: GithubTrigger;
const getOwnerUrlMode = () => {
const ownerParam = githubNode.description.properties.find((prop) => prop.name === 'owner');
return ownerParam?.modes?.find((mode) => mode.name === 'url');
};
const getRepositoryUrlMode = () => {
const repoParam = githubNode.description.properties.find((prop) => prop.name === 'repository');
return repoParam?.modes?.find((mode) => mode.name === 'url');
};
const getOwnerExtractRegex = () => {
const mode = getOwnerUrlMode();
return new RegExp(mode?.extractValue?.regex ?? '');
};
const getOwnerValidationRegex = () => {
const mode = getOwnerUrlMode();
const validation = mode?.validation?.[0] as ValidationRule;
return new RegExp(validation?.properties?.regex ?? '');
};
const getRepositoryExtractRegex = () => {
const mode = getRepositoryUrlMode();
return new RegExp(mode?.extractValue?.regex ?? '');
};
const getRepositoryValidationRegex = () => {
const mode = getRepositoryUrlMode();
const validation = mode?.validation?.[0] as ValidationRule;
return new RegExp(validation?.properties?.regex ?? '');
};
// Helper functions for GithubTrigger node
const getTriggerOwnerUrlMode = () => {
const ownerParam = githubTriggerNode.description.properties.find(
(prop) => prop.name === 'owner',
);
return ownerParam?.modes?.find((mode) => mode.name === 'url');
};
const getTriggerRepositoryUrlMode = () => {
const repoParam = githubTriggerNode.description.properties.find(
(prop) => prop.name === 'repository',
);
return repoParam?.modes?.find((mode) => mode.name === 'url');
};
const getTriggerOwnerExtractRegex = () => {
const mode = getTriggerOwnerUrlMode();
return new RegExp(mode?.extractValue?.regex ?? '');
};
const getTriggerOwnerValidationRegex = () => {
const mode = getTriggerOwnerUrlMode();
const validation = mode?.validation?.[0] as ValidationRule;
return new RegExp(validation?.properties?.regex ?? '');
};
const getTriggerRepositoryExtractRegex = () => {
const mode = getTriggerRepositoryUrlMode();
return new RegExp(mode?.extractValue?.regex ?? '');
};
const getTriggerRepositoryValidationRegex = () => {
const mode = getTriggerRepositoryUrlMode();
const validation = mode?.validation?.[0] as ValidationRule;
return new RegExp(validation?.properties?.regex ?? '');
};
beforeEach(() => {
githubNode = new Github();
githubTriggerNode = new GithubTrigger();
});
describe('GitHub Node Resource Locator Patterns', () => {
describe('Owner URL Pattern', () => {
it('should extract owner from github.com URL', () => {
const regex = getOwnerExtractRegex();
const url = 'https://github.com/n8n-io';
const match = url.match(regex);
expect(match?.[1]).toBe('n8n-io');
});
it('should extract owner from custom GitHub URL', () => {
const regex = getOwnerExtractRegex();
const url = 'https://github.company.com/acme-corp';
const match = url.match(regex);
expect(match?.[1]).toBe('acme-corp');
});
it('should validate github.com URL', () => {
const validationRegex = getOwnerValidationRegex();
const url = 'https://github.com/n8n-io';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate custom GitHub URL', () => {
const validationRegex = getOwnerValidationRegex();
const url = 'https://github.company.com/acme-corp';
expect(validationRegex.test(url)).toBe(true);
});
it('should reject invalid URLs', () => {
const validationRegex = getOwnerValidationRegex();
expect(validationRegex.test('not-a-url')).toBe(false);
expect(validationRegex.test('http://github.com/user')).toBe(false);
expect(validationRegex.test('https://')).toBe(false);
});
});
describe('Repository URL Pattern', () => {
it('should extract repository from github.com URL', () => {
const regex = getRepositoryExtractRegex();
const url = 'https://github.com/n8n-io/n8n';
const match = url.match(regex);
expect(match?.[1]).toBe('n8n');
});
it('should extract repository from custom GitHub URL', () => {
const regex = getRepositoryExtractRegex();
const url = 'https://github.company.com/acme-corp/my-repo';
const match = url.match(regex);
expect(match?.[1]).toBe('my-repo');
});
it('should validate github.com repository URL', () => {
const validationRegex = getRepositoryValidationRegex();
const url = 'https://github.com/n8n-io/n8n';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate custom GitHub repository URL', () => {
const validationRegex = getRepositoryValidationRegex();
const url = 'https://github.company.com/acme-corp/my-repo';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate URLs with additional paths', () => {
const validationRegex = getRepositoryValidationRegex();
expect(validationRegex.test('https://github.com/n8n-io/n8n/issues/123')).toBe(true);
expect(validationRegex.test('https://github.company.com/org/repo/pulls')).toBe(true);
});
it('should reject invalid repository URLs', () => {
const validationRegex = getRepositoryValidationRegex();
expect(validationRegex.test('https://github.com/user')).toBe(false);
expect(validationRegex.test('not-a-url')).toBe(false);
expect(validationRegex.test('https://')).toBe(false);
});
});
});
describe('GitHub Trigger Node Resource Locator Patterns', () => {
describe('Owner URL Pattern', () => {
it('should extract owner from github.com URL', () => {
const regex = getTriggerOwnerExtractRegex();
const url = 'https://github.com/n8n-io';
const match = url.match(regex);
expect(match?.[1]).toBe('n8n-io');
});
it('should extract owner from custom GitHub URL', () => {
const regex = getTriggerOwnerExtractRegex();
const url = 'https://github.company.com/my-org';
const match = url.match(regex);
expect(match?.[1]).toBe('my-org');
});
it('should validate github.com URL', () => {
const validationRegex = getTriggerOwnerValidationRegex();
const url = 'https://github.com/n8n-io';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate custom GitHub URL', () => {
const validationRegex = getTriggerOwnerValidationRegex();
const url = 'https://github.company.com/my-org';
expect(validationRegex.test(url)).toBe(true);
});
});
describe('Repository URL Pattern', () => {
it('should extract repository from github.com URL', () => {
const regex = getTriggerRepositoryExtractRegex();
const url = 'https://github.com/n8n-io/n8n';
const match = url.match(regex);
expect(match?.[1]).toBe('n8n');
});
it('should extract repository from custom GitHub URL', () => {
const regex = getTriggerRepositoryExtractRegex();
const url = 'https://github.company.com/my-org/my-repo';
const match = url.match(regex);
expect(match?.[1]).toBe('my-repo');
});
it('should validate github.com repository URL', () => {
const validationRegex = getTriggerRepositoryValidationRegex();
const url = 'https://github.com/n8n-io/n8n';
expect(validationRegex.test(url)).toBe(true);
});
it('should validate custom GitHub repository URL', () => {
const validationRegex = getTriggerRepositoryValidationRegex();
const url = 'https://github.company.com/my-org/my-repo';
expect(validationRegex.test(url)).toBe(true);
});
});
});
describe('URL Pattern Edge Cases', () => {
it('should handle URLs with subdomains', () => {
const ownerRegex = getOwnerExtractRegex();
const repoRegex = getRepositoryExtractRegex();
// Test complex custom URLs
expect('https://git.internal.company.com/dev-team'.match(ownerRegex)?.[1]).toBe('dev-team');
expect('https://github.acme.corp/engineering/backend-api'.match(repoRegex)?.[1]).toBe(
'backend-api',
);
});
it('should handle URLs with ports', () => {
const ownerRegex = getOwnerExtractRegex();
const repoRegex = getRepositoryExtractRegex();
// Test URLs with ports
expect('https://github.local:8080/testuser'.match(ownerRegex)?.[1]).toBe('testuser');
expect('https://git.company.com:443/org/project'.match(repoRegex)?.[1]).toBe('project');
});
it('should handle URLs with additional path segments', () => {
const ownerValidationRegex = getOwnerValidationRegex();
const repoValidationRegex = getRepositoryValidationRegex();
// Test URLs with extra paths
expect(ownerValidationRegex.test('https://github.com/user/settings')).toBe(true);
expect(repoValidationRegex.test('https://github.com/user/repo/issues/123')).toBe(true);
expect(repoValidationRegex.test('https://git.company.com/org/project/pulls')).toBe(true);
});
});
});