Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 50 additions & 2 deletions branch-suggestion/scripts/common/__tests__/match-branches.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,13 @@ describe('generateBranchCandidates', () => {
it('should generate candidates for patch release', () => {
const parsed = { major: 5, minor: 8, patch: 1 };
const candidates = generateBranchCandidates(parsed);
expect(candidates).toEqual(['release-5.8', 'release-5', 'master']);
expect(candidates).toEqual(['release-5.8.1', 'release-5.8', 'release-5', 'master']);
});

it('should generate candidates for minor release', () => {
const parsed = { major: 5, minor: 8, patch: 0 };
const candidates = generateBranchCandidates(parsed);
expect(candidates).toEqual(['release-5.8', 'release-5', 'master']);
expect(candidates).toEqual(['release-5.8.0', 'release-5.8', 'release-5', 'master']);
});

it('should generate candidates for major release', () => {
Expand Down Expand Up @@ -91,6 +91,11 @@ describe('getBranchPriority', () => {
expect(priority).toBe('required');
});

it('should return required for exact patch version branch', () => {
const priority = getBranchPriority('release-5.8.1', { parsed: { major: 5, minor: 8, patch: 1 } });
expect(priority).toBe('required');
});

it('should return required for patch release branch', () => {
const priority = getBranchPriority('release-5.8', { parsed: { major: 5, minor: 8, patch: 1 } });
expect(priority).toBe('required');
Expand All @@ -113,6 +118,15 @@ describe('getBranchReason', () => {
expect(reason).toContain('Main development branch');
});

it('should return reason for exact patch version branch', () => {
const reason = getBranchReason('release-5.8.1', {
name: '5.8.1',
parsed: { major: 5, minor: 8, patch: 1 }
});
expect(reason).toContain('Exact version branch');
expect(reason).toContain('5.8.1');
});

it('should return reason for patch release', () => {
const reason = getBranchReason('release-5.8', {
name: '5.8.1',
Expand Down Expand Up @@ -192,4 +206,38 @@ describe('matchBranches', () => {
expect(results[0].branches).toHaveLength(1);
expect(results[0].branches[0].branch).toBe('master');
});

it('should prefer exact version match when available', () => {
const fixVersions = [
{ name: '5.10.1', parsed: { major: 5, minor: 10, patch: 1, component: [] } }
];
const repoBranches = [
{ name: 'master' },
{ name: 'release-5.10' },
{ name: 'release-5.10.1' }
];

const results = matchBranches(fixVersions, repoBranches);
expect(results[0].branches).toHaveLength(3);
expect(results[0].branches[0].branch).toBe('release-5.10.1');
expect(results[0].branches[0].priority).toBe('required');
expect(results[0].branches[1].branch).toBe('release-5.10');
expect(results[0].branches[1].priority).toBe('required');
expect(results[0].branches[2].branch).toBe('master');
});

it('should fallback to minor version when exact match not available', () => {
const fixVersions = [
{ name: '5.10.1', parsed: { major: 5, minor: 10, patch: 1, component: [] } }
];
const repoBranches = [
{ name: 'master' },
{ name: 'release-5.10' }
];

const results = matchBranches(fixVersions, repoBranches);
expect(results[0].branches).toHaveLength(2);
expect(results[0].branches[0].branch).toBe('release-5.10');
expect(results[0].branches[0].priority).toBe('required');
});
});
52 changes: 45 additions & 7 deletions branch-suggestion/scripts/common/match-branches.js
Original file line number Diff line number Diff line change
Expand Up @@ -20,17 +20,22 @@
const { major, minor, patch } = parsedVersion;
const candidates = [];

// For patch releases (X.Y.Z where Z > 0): need release-X.Y
// First priority: exact match for the full version (X.Y.Z)
if (patch !== null && minor !== null) {
candidates.push(`release-${major}.${minor}.${patch}`);
}

// Second priority: minor version branch (X.Y) for patch releases
if (patch !== null && patch > 0 && minor !== null) {
candidates.push(`release-${major}.${minor}`);
}

// For minor releases (X.Y.0 or X.Y): need release-X.Y
// Third priority: minor version branch for minor releases (X.Y.0 or X.Y)
if (minor !== null) {
candidates.push(`release-${major}.${minor}`);
}

Check warning on line 37 in branch-suggestion/scripts/common/match-branches.js

View check run for this annotation

probelabs / Visor: architecture

architecture Issue

The logic to add a minor version branch (e.g., `release-5.8`) to the list of candidates is duplicated. The condition on line 36 (`if (minor !== null)`) is a superset of the condition on line 31 (`if (patch !== null && patch > 0 && minor !== null)`), causing the same branch to be potentially added twice. While `new Set()` correctly de-duplicates the final list, the code can be simplified to avoid this redundancy.
Raw output
Combine the two `if` statements into a single one that adds the minor version branch. The logic for both patch and minor releases can be covered by a single check for `minor !== null`.

Check warning on line 37 in branch-suggestion/scripts/common/match-branches.js

View check run for this annotation

probelabs / Visor: quality

logic Issue

The logic to add a minor version branch candidate (e.g., `release-5.8`) is duplicated. The condition `patch !== null && patch > 0 && minor !== null` is a subset of the subsequent condition `minor !== null`. This makes the first `if` block redundant, as the second block handles all necessary cases. While `new Set()` correctly deduplicates the candidates, the code is unnecessarily complex and can be simplified.
Raw output
Consolidate the logic into a single condition to improve clarity and remove redundancy. The block checking for `patch > 0` can be removed entirely as it is covered by the subsequent check.
// For any version: might need release-X (major version branch)
// Fourth priority: major version branch (X)
if (major !== null) {
candidates.push(`release-${major}`);
}
Expand Down Expand Up @@ -133,6 +138,12 @@
return 'Main development branch - ensures fix is in all future releases';
}

// Exact patch version match (e.g., release-5.10.1)
if (patch !== null && branch === `release-${major}.${minor}.${patch}`) {
return `Exact version branch for ${fixVersion.name} - specific patch release`;
}

// Minor version branch (e.g., release-5.10)
if (branch === `release-${major}.${minor}`) {
if (patch > 0) {
return `Minor version branch for ${major}.${minor}.x patches - required for creating ${fixVersion.name}`;
Expand All @@ -141,6 +152,7 @@
}
}

// Major version branch (e.g., release-5)
if (branch === `release-${major}`) {
return `Major version branch for all ${major}.x releases`;
}
Expand All @@ -162,6 +174,11 @@
return 'required';
}

// Exact patch version match is required (e.g., release-5.10.1 for version 5.10.1)
if (patch !== null && branch === `release-${major}.${minor}.${patch}`) {
return 'required';
}

// For patch releases (5.8.1), release-5.8 is required
if (patch > 0 && branch === `release-${major}.${minor}`) {
return 'required';
Expand Down Expand Up @@ -233,10 +250,31 @@
lines.push('---');
lines.push('');
lines.push('### 📋 Workflow');
lines.push('1. Merge this PR to `master` first');
lines.push('2. After merging, comment on the **merged PR** with `/release to <branch>` to cherry-pick to release branches');
lines.push('3. Example: `/release to release-5.8`');
lines.push('4. The bot will automatically create a backport PR to the specified release branch');
lines.push('');
lines.push('1. **Merge this PR to `master` first**');
lines.push('');

// Collect all non-master branches that need cherry-picking using Set for O(1) operations
const releaseBranchesSet = new Set();
for (const result of matchResults) {
for (const branch of result.branches) {
if (branch.branch !== 'master') {
releaseBranchesSet.add(branch.branch);
}
}
}
const releaseBranches = Array.from(releaseBranchesSet);

if (releaseBranches.length > 0) {
lines.push('2. **Cherry-pick to release branches** by commenting on the **merged PR**:');
lines.push('');

Check warning on line 270 in branch-suggestion/scripts/common/match-branches.js

View check run for this annotation

probelabs / Visor: security

security Issue

The script generates bot commands by embedding raw branch names from the repository. These branch names are not validated or sanitized. A maliciously crafted branch name containing special characters (e.g., newlines, semicolons, or other shell metacharacters) could potentially lead to command injection in the downstream release bot that processes these commands from PR comments.
Raw output
Validate branch names against a strict regular expression before embedding them in the command string. For example, you could enforce a pattern like `/^release-\d+\.\d+(\.\d+)?$/` for release branches and explicitly check for `master`. This will prevent unexpected characters from being passed to the release bot, mitigating the risk of injection attacks.
for (const branch of releaseBranches) {
lines.push(` - \`/release to ${branch}\``);
}
lines.push('');
lines.push('3. **Automated backport** - The bot will automatically create backport PRs to the specified release branches');
}

lines.push('');
lines.push('<!-- branch-suggestions -->');

Expand Down