diff --git a/client/src/app/components/pipeline/pipeline.component.spec.ts b/client/src/app/components/pipeline/pipeline.component.spec.ts index ea3a4d15e..0c59dcc5b 100644 --- a/client/src/app/components/pipeline/pipeline.component.spec.ts +++ b/client/src/app/components/pipeline/pipeline.component.spec.ts @@ -84,6 +84,80 @@ describe('PipelineComponent', () => { expect(logsLink?.getAttribute('href')).toContain('/repo/7/ci-cd/runs/42/logs'); }); + it('surfaces runs whose workflow is in no group under an "Ungrouped" fallback', async () => { + const groupedRun: WorkflowRunDto = { + id: 1, + workflowId: 5, + name: 'CI', + displayTitle: 'CI', + status: 'COMPLETED', + conclusion: 'SUCCESS', + htmlUrl: 'https://github.com/org/repo/actions/runs/1', + label: 'TEST', + createdAt: '2026-01-01T10:00:00Z', + updatedAt: '2026-01-01T10:01:00Z', + }; + const ungroupedRun: WorkflowRunDto = { + id: 2, + workflowId: 999, + name: 'Orphan Workflow', + displayTitle: 'Orphan Workflow', + status: 'COMPLETED', + conclusion: 'SUCCESS', + htmlUrl: 'https://github.com/org/repo/actions/runs/2', + label: 'NONE', + createdAt: '2026-01-01T10:00:00Z', + updatedAt: '2026-01-01T10:01:00Z', + }; + + fixture.componentRef.setInput('selector', { repositoryId: 7, pullRequestId: 11 }); + Object.defineProperty(component, 'branchQuery', { configurable: true, value: { isPending: () => false, data: () => [] } }); + Object.defineProperty(component, 'pullRequestQuery', { + configurable: true, + value: { isPending: () => false, data: () => [groupedRun, ungroupedRun] }, + }); + Object.defineProperty(component, 'groupsQuery', { + configurable: true, + value: { data: () => [{ id: 1, name: 'CI Group', memberships: [{ workflowId: 5, orderIndex: 0 }] }] }, + }); + + const groups = component.pipeline().groups; + const ciGroup = groups.find(g => g.name === 'CI Group'); + const ungrouped = groups.find(g => g.name === 'Ungrouped'); + expect(ciGroup?.workflows.map(w => w.id)).toEqual([1]); + expect(ungrouped).toBeTruthy(); + expect(ungrouped?.workflows.map(w => w.id)).toEqual([2]); + }); + + it('does not add an "Ungrouped" fallback when every run belongs to a group', async () => { + const groupedRun: WorkflowRunDto = { + id: 1, + workflowId: 5, + name: 'CI', + displayTitle: 'CI', + status: 'COMPLETED', + conclusion: 'SUCCESS', + htmlUrl: 'https://github.com/org/repo/actions/runs/1', + label: 'TEST', + createdAt: '2026-01-01T10:00:00Z', + updatedAt: '2026-01-01T10:01:00Z', + }; + + fixture.componentRef.setInput('selector', { repositoryId: 7, pullRequestId: 11 }); + Object.defineProperty(component, 'branchQuery', { configurable: true, value: { isPending: () => false, data: () => [] } }); + Object.defineProperty(component, 'pullRequestQuery', { + configurable: true, + value: { isPending: () => false, data: () => [groupedRun] }, + }); + Object.defineProperty(component, 'groupsQuery', { + configurable: true, + value: { data: () => [{ id: 1, name: 'CI Group', memberships: [{ workflowId: 5, orderIndex: 0 }] }] }, + }); + + const groups = component.pipeline().groups; + expect(groups.find(g => g.name === 'Ungrouped')).toBeUndefined(); + }); + it('does not render workflow logs icon link without write permission', async () => { const workflowRun: WorkflowRunDto = { id: 42, diff --git a/client/src/app/components/pipeline/pipeline.component.ts b/client/src/app/components/pipeline/pipeline.component.ts index 3815d0c4f..c2d336b80 100644 --- a/client/src/app/components/pipeline/pipeline.component.ts +++ b/client/src/app/components/pipeline/pipeline.component.ts @@ -107,6 +107,21 @@ export class PipelineComponent { }; }); + // Fallback bucket: surface runs whose workflow is not a member of ANY configured + // group so a newly added or renamed workflow (e.g. after a CI refactor that + // introduces a new orchestrator workflow) stays visible in the pipeline instead of + // silently disappearing until someone manually adds it to a group. + const groupedWorkflowIds = new Set(workflowGroups.flatMap(group => group.memberships?.map(membership => membership.workflowId) ?? [])); + const ungroupedRuns = workflowRuns.filter(run => run.workflowId === undefined || !groupedWorkflowIds.has(run.workflowId)); + if (ungroupedRuns.length > 0) { + groupedWorkflowsRuns.push({ + name: 'Ungrouped', + id: -1, + workflows: ungroupedRuns, + isLastWithWorkflows: false, + }); + } + let lastWithWorkflowsFound = false; // For loop in reverse to set isLastWithWorkflows in the correct order groupedWorkflowsRuns.reverse().forEach(group => {