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
Original file line number Diff line number Diff line change
Expand Up @@ -191,6 +191,55 @@ describe('ReleaseNodeService', () => {
expect(branchOrder).toEqual(['release/9.0', 'release/8.4']);
});

it('should keep the latest LTS (major x.0) branch even if it has no nightly and is past support window', () => {
const branches: Record<string, any> = {
master: { id: 'b-master', name: MASTER_BRANCH_NAME },
b90: { id: 'b-90', name: 'release/9.0' },
b80: { id: 'b-80', name: 'release/8.0' },
};

const releases: Release[] = [
{
id: 'master-nightly',
name: 'v9.4.0-20251108.042330 (nightly)',
publishedAt: new Date('2025-06-10T10:00:00Z'),
lastScanned: new Date(),
branch: branches['master'],
tagName: 'release/9.4-nightly',
},
{
id: '9.0-anchor',
name: 'v9.0.0',
publishedAt: new Date('2025-01-10T10:00:00Z'),
lastScanned: new Date(),
branch: branches['b90'],
tagName: 'release/v9.0.0',
},
{
id: '8.0-anchor',
name: 'v8.0.0',
publishedAt: new Date('2023-01-01T10:00:00Z'),
lastScanned: new Date(),
branch: branches['b80'],
tagName: 'release/v8.0.0',
},
{
id: '8.0-node-1',
name: 'v8.0.1',
publishedAt: new Date('2023-02-01T10:00:00Z'),
lastScanned: new Date(),
branch: branches['b80'],
tagName: 'release/v8.0.1',
},
];

const structured = service.structureReleaseData(releases);
const positionedMap = service.calculateReleaseCoordinates(structured);
const branchOrder = [...positionedMap.keys()].filter((b) => b !== MASTER_BRANCH_NAME);

expect(branchOrder).toContain('release/8.0');
});

it('should position master nodes on Y=0 axis', () => {
const positionedMap = service.calculateReleaseCoordinates(structuredData);
const masterNodes = positionedMap.get(MASTER_BRANCH_NAME)!;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,6 +64,7 @@ export class ReleaseNodeService {

private static readonly GITHUB_MASTER_BRANCH: string = 'master';
private static readonly PIXELS_PER_QUARTER: number = 200;
private static readonly KEEP_LATEST_LTS_COUNT: number = 3;

public timelineScale: TimelineScale | null = null;

Expand Down Expand Up @@ -473,22 +474,44 @@ export class ReleaseNodeService {
);
}

/**
* Returns the set of branch names for the N most recent major (x.0) version branches.
* These are always kept visible regardless of support status or nightly activity.
*/
private findLatestMajorBranchNames(branchNames: string[]): Set<string> {
const majorBranches = branchNames
.map((name) => ({ name, version: this.getVersionFromBranchName(name) }))
.filter(
(item): item is { name: string; version: { major: number; minor: number } } =>
item.version !== null && item.version.minor === 0,
)
.toSorted((a, b) => b.version.major - a.version.major)
.slice(0, ReleaseNodeService.KEEP_LATEST_LTS_COUNT);

return new Set(majorBranches.map((b) => b.name));
}

/**
* Removes branches that are historically dead.
* Logic:
* 1. If it has an active Nightly -> KEEP (Active development).
* 2. If NO Nightly -> Check the SUPPORT status of the BRANCH ROOT (the .0 release).
* 2. The latest N major version (x.0) branches are always kept regardless of support status.
* 3. If NO Nightly -> Check the SUPPORT status of the BRANCH ROOT (the .0 release).
* If the .0 release is unsupported, the whole branch is considered historical/skipped,
* even if a recent patch was released.
*/
private pruneHistoricalBranchesWithoutNightly(
groupedByBranch: Map<string, (Release & { publishedAt: Date })[]>,
): void {
const protectedBranches = this.findLatestMajorBranchNames([...groupedByBranch.keys()]);

for (const [branchName, releases] of groupedByBranch.entries()) {
if (branchName === ReleaseNodeService.GITHUB_MASTER_BRANCH) {
continue;
}

if (protectedBranches.has(branchName)) continue;

if (releases.length === 0) continue;

const latestByDate = releases.reduce((previous, current) =>
Expand Down Expand Up @@ -686,8 +709,10 @@ export class ReleaseNodeService {
const Y_SPACING = 90;
let yLevel = 1;

const protectedBranches = this.findLatestMajorBranchNames(branches.map(([name]) => name));

for (const [branchName, nodes] of branches) {
if (nodes.every((n) => this.isUnsupported(n))) continue;
if (!protectedBranches.has(branchName) && nodes.every((n) => this.isUnsupported(n))) continue;

const miniNode = masterNodes.find((n) => n.originalBranch === branchName && n.isMiniNode);
if (!miniNode) continue;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -178,6 +178,56 @@ describe('ReleaseSkippedVersions', () => {
expect(component.releaseTree[2].version).toBe('v7.2.0');
});

it('should group patch releases under the major (x.0.0) parent, not create a duplicate minor entry', () => {
const skipNode: SkipNode = {
id: 'skip-1',
x: 100,
y: 0,
skippedCount: 1,
skippedVersions: ['v9.0.0', 'v9.0.1', 'v9.0.2'],
label: '1 skipped',
};

const releases: Release[] = [
{
id: 'r1',
name: 'v9.0.0',
branch: { name: 'release/9.0' },
tagName: '',
publishedAt: new Date(),
lastScanned: new Date(),
},
{
id: 'r2',
name: 'v9.0.1',
branch: { name: 'release/9.0' },
tagName: '',
publishedAt: new Date(),
lastScanned: new Date(),
},
{
id: 'r3',
name: 'v9.0.2',
branch: { name: 'release/9.0' },
tagName: '',
publishedAt: new Date(),
lastScanned: new Date(),
},
];

component.skipNode = skipNode;
component.releases = releases;
component.ngOnChanges({
skipNode: { currentValue: skipNode, previousValue: null, firstChange: true, isFirstChange: () => true },
releases: { currentValue: releases, previousValue: [], firstChange: true, isFirstChange: () => true },
});

expect(component.releaseTree.length).toBe(1);
expect(component.releaseTree[0].version).toBe('v9.0.0');
expect(component.releaseTree[0].type).toBe('major');
expect(component.releaseTree[0].patches.length).toBe(2);
});

it('should handle releases without v prefix', () => {
const skipNode: SkipNode = {
id: 'skip-1',
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -94,7 +94,7 @@ export class ReleaseSkippedVersions implements OnChanges {
): void {
const prefixedReleaseName = release.name.startsWith('v') ? release.name : `v${release.name}`;

const mapKey = info.type === 'minor' ? `v${info.major}.${info.minor}` : prefixedReleaseName;
const mapKey = `v${info.major}.${info.minor}`;

const displayVersion = prefixedReleaseName;
const existingNode = releaseMap.get(mapKey);
Expand Down
Loading