Skip to content

Commit e7ebec2

Browse files
committed
2 parents d9cf175 + b56de0c commit e7ebec2

4 files changed

Lines changed: 127 additions & 3 deletions

File tree

src/main/frontend/src/app/pages/release-graph/release-node.service.spec.ts

Lines changed: 49 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -191,6 +191,55 @@ describe('ReleaseNodeService', () => {
191191
expect(branchOrder).toEqual(['release/9.0', 'release/8.4']);
192192
});
193193

194+
it('should keep the latest LTS (major x.0) branch even if it has no nightly and is past support window', () => {
195+
const branches: Record<string, any> = {
196+
master: { id: 'b-master', name: MASTER_BRANCH_NAME },
197+
b90: { id: 'b-90', name: 'release/9.0' },
198+
b80: { id: 'b-80', name: 'release/8.0' },
199+
};
200+
201+
const releases: Release[] = [
202+
{
203+
id: 'master-nightly',
204+
name: 'v9.4.0-20251108.042330 (nightly)',
205+
publishedAt: new Date('2025-06-10T10:00:00Z'),
206+
lastScanned: new Date(),
207+
branch: branches['master'],
208+
tagName: 'release/9.4-nightly',
209+
},
210+
{
211+
id: '9.0-anchor',
212+
name: 'v9.0.0',
213+
publishedAt: new Date('2025-01-10T10:00:00Z'),
214+
lastScanned: new Date(),
215+
branch: branches['b90'],
216+
tagName: 'release/v9.0.0',
217+
},
218+
{
219+
id: '8.0-anchor',
220+
name: 'v8.0.0',
221+
publishedAt: new Date('2023-01-01T10:00:00Z'),
222+
lastScanned: new Date(),
223+
branch: branches['b80'],
224+
tagName: 'release/v8.0.0',
225+
},
226+
{
227+
id: '8.0-node-1',
228+
name: 'v8.0.1',
229+
publishedAt: new Date('2023-02-01T10:00:00Z'),
230+
lastScanned: new Date(),
231+
branch: branches['b80'],
232+
tagName: 'release/v8.0.1',
233+
},
234+
];
235+
236+
const structured = service.structureReleaseData(releases);
237+
const positionedMap = service.calculateReleaseCoordinates(structured);
238+
const branchOrder = [...positionedMap.keys()].filter((b) => b !== MASTER_BRANCH_NAME);
239+
240+
expect(branchOrder).toContain('release/8.0');
241+
});
242+
194243
it('should position master nodes on Y=0 axis', () => {
195244
const positionedMap = service.calculateReleaseCoordinates(structuredData);
196245
const masterNodes = positionedMap.get(MASTER_BRANCH_NAME)!;

src/main/frontend/src/app/pages/release-graph/release-node.service.ts

Lines changed: 27 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export class ReleaseNodeService {
6464

6565
private static readonly GITHUB_MASTER_BRANCH: string = 'master';
6666
private static readonly PIXELS_PER_QUARTER: number = 200;
67+
private static readonly KEEP_LATEST_LTS_COUNT: number = 3;
6768

6869
public timelineScale: TimelineScale | null = null;
6970

@@ -473,22 +474,44 @@ export class ReleaseNodeService {
473474
);
474475
}
475476

477+
/**
478+
* Returns the set of branch names for the N most recent major (x.0) version branches.
479+
* These are always kept visible regardless of support status or nightly activity.
480+
*/
481+
private findLatestMajorBranchNames(branchNames: string[]): Set<string> {
482+
const majorBranches = branchNames
483+
.map((name) => ({ name, version: this.getVersionFromBranchName(name) }))
484+
.filter(
485+
(item): item is { name: string; version: { major: number; minor: number } } =>
486+
item.version !== null && item.version.minor === 0,
487+
)
488+
.toSorted((a, b) => b.version.major - a.version.major)
489+
.slice(0, ReleaseNodeService.KEEP_LATEST_LTS_COUNT);
490+
491+
return new Set(majorBranches.map((b) => b.name));
492+
}
493+
476494
/**
477495
* Removes branches that are historically dead.
478496
* Logic:
479497
* 1. If it has an active Nightly -> KEEP (Active development).
480-
* 2. If NO Nightly -> Check the SUPPORT status of the BRANCH ROOT (the .0 release).
498+
* 2. The latest N major version (x.0) branches are always kept regardless of support status.
499+
* 3. If NO Nightly -> Check the SUPPORT status of the BRANCH ROOT (the .0 release).
481500
* If the .0 release is unsupported, the whole branch is considered historical/skipped,
482501
* even if a recent patch was released.
483502
*/
484503
private pruneHistoricalBranchesWithoutNightly(
485504
groupedByBranch: Map<string, (Release & { publishedAt: Date })[]>,
486505
): void {
506+
const protectedBranches = this.findLatestMajorBranchNames([...groupedByBranch.keys()]);
507+
487508
for (const [branchName, releases] of groupedByBranch.entries()) {
488509
if (branchName === ReleaseNodeService.GITHUB_MASTER_BRANCH) {
489510
continue;
490511
}
491512

513+
if (protectedBranches.has(branchName)) continue;
514+
492515
if (releases.length === 0) continue;
493516

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

712+
const protectedBranches = this.findLatestMajorBranchNames(branches.map(([name]) => name));
713+
689714
for (const [branchName, nodes] of branches) {
690-
if (nodes.every((n) => this.isUnsupported(n))) continue;
715+
if (!protectedBranches.has(branchName) && nodes.every((n) => this.isUnsupported(n))) continue;
691716

692717
const miniNode = masterNodes.find((n) => n.originalBranch === branchName && n.isMiniNode);
693718
if (!miniNode) continue;

src/main/frontend/src/app/pages/release-graph/release-skipped-versions/release-skipped-versions.spec.ts

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -178,6 +178,56 @@ describe('ReleaseSkippedVersions', () => {
178178
expect(component.releaseTree[2].version).toBe('v7.2.0');
179179
});
180180

181+
it('should group patch releases under the major (x.0.0) parent, not create a duplicate minor entry', () => {
182+
const skipNode: SkipNode = {
183+
id: 'skip-1',
184+
x: 100,
185+
y: 0,
186+
skippedCount: 1,
187+
skippedVersions: ['v9.0.0', 'v9.0.1', 'v9.0.2'],
188+
label: '1 skipped',
189+
};
190+
191+
const releases: Release[] = [
192+
{
193+
id: 'r1',
194+
name: 'v9.0.0',
195+
branch: { name: 'release/9.0' },
196+
tagName: '',
197+
publishedAt: new Date(),
198+
lastScanned: new Date(),
199+
},
200+
{
201+
id: 'r2',
202+
name: 'v9.0.1',
203+
branch: { name: 'release/9.0' },
204+
tagName: '',
205+
publishedAt: new Date(),
206+
lastScanned: new Date(),
207+
},
208+
{
209+
id: 'r3',
210+
name: 'v9.0.2',
211+
branch: { name: 'release/9.0' },
212+
tagName: '',
213+
publishedAt: new Date(),
214+
lastScanned: new Date(),
215+
},
216+
];
217+
218+
component.skipNode = skipNode;
219+
component.releases = releases;
220+
component.ngOnChanges({
221+
skipNode: { currentValue: skipNode, previousValue: null, firstChange: true, isFirstChange: () => true },
222+
releases: { currentValue: releases, previousValue: [], firstChange: true, isFirstChange: () => true },
223+
});
224+
225+
expect(component.releaseTree.length).toBe(1);
226+
expect(component.releaseTree[0].version).toBe('v9.0.0');
227+
expect(component.releaseTree[0].type).toBe('major');
228+
expect(component.releaseTree[0].patches.length).toBe(2);
229+
});
230+
181231
it('should handle releases without v prefix', () => {
182232
const skipNode: SkipNode = {
183233
id: 'skip-1',

src/main/frontend/src/app/pages/release-graph/release-skipped-versions/release-skipped-versions.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ export class ReleaseSkippedVersions implements OnChanges {
9494
): void {
9595
const prefixedReleaseName = release.name.startsWith('v') ? release.name : `v${release.name}`;
9696

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

9999
const displayVersion = prefixedReleaseName;
100100
const existingNode = releaseMap.get(mapKey);

0 commit comments

Comments
 (0)