mirror of
https://github.com/go-gitea/gitea.git
synced 2026-08-22 02:10:51 +00:00
Backport #38980 The workflow graph decided which job rows belonged to the same matrix by parsing display names: it stripped a trailing `" (...)"` off `name` and grouped rows sharing the prefix. That guesses at a string the user controls, and it fails both ways. `jobparser` only appends the ` (<combination>)` suffix when `name:` contains no `${{ }}`, so a leg named `E2E on ${{ matrix.browser }}` never grouped, while two unrelated jobs `build (fast)` and `build (slow)` folded into one bogus matrix panel. Matrix legs already have a real identity: expansion clones one row per combination, all sharing the workflow's `JobID` and differing only in `Name`. Group on that instead, so a matrix is whatever the backend says it is. Matrix expansion state is keyed on the graph node id for the same reason. Closes https://github.com/go-gitea/gitea/issues/38975, though that report's own example already groups on main, since `explicit (${{ matrix.leg }})` interpolates to a name that still ends in a suffix. The interpolated shapes above are the broken ones. ## Screenshots: Before: <img width="1268" height="618" alt="image" src="https://github.com/user-attachments/assets/2d98dd1f-5454-423c-b610-8db920f1e99c" /> after: <img width="1145" height="607" alt="image" src="https://github.com/user-attachments/assets/25da1804-8386-4f5b-a4e3-f63380010907" /> _Assisted-by: Claude Code:claude-opus-5_ Co-authored-by: bircni <bircni@icloud.com> Co-authored-by: silverwind <me@silverwind.io>
This commit is contained in:
@@ -13,7 +13,6 @@ export type GraphNode = {
|
||||
level: number;
|
||||
displayHeight: number;
|
||||
jobs: ActionsJob[];
|
||||
matrixKey?: string;
|
||||
};
|
||||
|
||||
export type Edge = {
|
||||
@@ -88,10 +87,14 @@ function graphIdForJob(job: ActionsJob): string {
|
||||
return `job:${job.id}`;
|
||||
}
|
||||
|
||||
export function matrixKeyFromJobName(name: string): string | null {
|
||||
const idx = name.indexOf(' (');
|
||||
if (idx === -1) return null;
|
||||
return name.slice(0, idx).trim() || null;
|
||||
// matrix legs are named `<job name> (<combination>)`; a workflow-provided `name:` may not be
|
||||
function matrixLabel(matrixJobs: ActionsJob[], jobId: string): string {
|
||||
const prefixes = new Set(matrixJobs.map((job) => {
|
||||
const idx = job.name.indexOf(' (');
|
||||
return idx === -1 ? '' : job.name.slice(0, idx).trim();
|
||||
}));
|
||||
const [prefix] = prefixes;
|
||||
return prefixes.size === 1 && prefix ? prefix : jobId;
|
||||
}
|
||||
|
||||
export function boxBottom(node: GraphNode): number {
|
||||
@@ -251,7 +254,7 @@ type VisualGraphBuild = {
|
||||
|
||||
function buildVisualGraph(
|
||||
jobs: ActionsJob[],
|
||||
expandedMatrixKeys: ReadonlySet<string>,
|
||||
expandedMatrixNodeIds: ReadonlySet<string>,
|
||||
options: WorkflowGraphLayoutOptions,
|
||||
): VisualGraphBuild {
|
||||
const jobsByJobId = new Map<string, ActionsJob[]>();
|
||||
@@ -262,18 +265,8 @@ function buildVisualGraph(
|
||||
jobsByJobId.get(job.jobId)!.push(job);
|
||||
}
|
||||
|
||||
const matrixJobsByKey = new Map<string, ActionsJob[]>();
|
||||
for (const job of jobs) {
|
||||
// Matrix legs that call a reusable workflow are still one logical job (a single `uses:`
|
||||
// expanded over the matrix), so fold them into a matrix node like any other matrix job.
|
||||
const matrixKey = matrixKeyFromJobName(job.name);
|
||||
if (!matrixKey) continue;
|
||||
if (!matrixJobsByKey.has(matrixKey)) matrixJobsByKey.set(matrixKey, []);
|
||||
matrixJobsByKey.get(matrixKey)!.push(job);
|
||||
}
|
||||
for (const list of matrixJobsByKey.values()) {
|
||||
list.sort((a, b) => (jobIndexById.get(a.id) ?? 0) - (jobIndexById.get(b.id) ?? 0));
|
||||
}
|
||||
// legs of one matrix job share its `jobId`; their display names are free-form so cannot key them
|
||||
const isMatrixLeg = (job: ActionsJob): boolean => Boolean(job.jobId) && jobsByJobId.get(job.jobId)!.length > 1;
|
||||
|
||||
const directNeedsByJobId = buildDirectNeedsMap(jobs);
|
||||
const rawLevels = computeJobLevels(jobs);
|
||||
@@ -298,7 +291,7 @@ function buildVisualGraph(
|
||||
const groupsById = new Map<string, ActionsJob[]>();
|
||||
const groupCandidateBuckets = new Map<string, ActionsJob[]>();
|
||||
for (const job of jobs) {
|
||||
if (matrixKeyFromJobName(job.name)) continue;
|
||||
if (isMatrixLeg(job)) continue;
|
||||
// Reusable callers represent distinct workflow files — keep each as its own node so the
|
||||
// graph mirrors GitHub Actions, where every caller shows up as its own box even when
|
||||
// siblings share an identical (parents, children) dependency signature.
|
||||
@@ -319,36 +312,25 @@ function buildVisualGraph(
|
||||
}
|
||||
|
||||
const visualIdByJobId = new Map<number, string>();
|
||||
for (const job of jobs) {
|
||||
const matrixKey = matrixKeyFromJobName(job.name);
|
||||
// Symmetric with the matrix-bucket loop above (callers included).
|
||||
if (matrixKey && (matrixJobsByKey.get(matrixKey)?.length ?? 0) > 1) {
|
||||
visualIdByJobId.set(job.id, `matrix:${matrixKey}`);
|
||||
continue;
|
||||
}
|
||||
visualIdByJobId.set(job.id, groupedJobIds.get(job.id) || graphIdForJob(job));
|
||||
}
|
||||
|
||||
const emittedNodeIds = new Set<string>();
|
||||
const nodes: GraphNode[] = [];
|
||||
for (const job of jobs) {
|
||||
const visualId = visualIdByJobId.get(job.id);
|
||||
if (!visualId || emittedNodeIds.has(visualId)) continue;
|
||||
const matrixJobs = isMatrixLeg(job) ? jobsByJobId.get(job.jobId)! : null;
|
||||
const visualId = matrixJobs ? `matrix:${job.jobId}` : (groupedJobIds.get(job.id) || graphIdForJob(job));
|
||||
visualIdByJobId.set(job.id, visualId);
|
||||
if (emittedNodeIds.has(visualId)) continue;
|
||||
emittedNodeIds.add(visualId);
|
||||
|
||||
const matrixKey = matrixKeyFromJobName(job.name);
|
||||
if (matrixKey && visualId.startsWith('matrix:')) {
|
||||
const matrixJobs = matrixJobsByKey.get(matrixKey) || [];
|
||||
if (matrixJobs) {
|
||||
nodes.push({
|
||||
id: visualId,
|
||||
type: 'matrix',
|
||||
name: matrixKey,
|
||||
name: matrixLabel(matrixJobs, job.jobId),
|
||||
status: aggregateStatus(matrixJobs),
|
||||
duration: '',
|
||||
x: 0, y: 0, level: 0,
|
||||
displayHeight: matrixPanelHeight(matrixJobs.length, expandedMatrixKeys.has(matrixKey), options),
|
||||
displayHeight: matrixPanelHeight(matrixJobs.length, expandedMatrixNodeIds.has(visualId), options),
|
||||
jobs: matrixJobs,
|
||||
matrixKey,
|
||||
});
|
||||
continue;
|
||||
}
|
||||
@@ -539,11 +521,11 @@ function buildRoutedEdges(
|
||||
|
||||
export function createWorkflowGraphModel(
|
||||
jobs: ActionsJob[],
|
||||
expandedMatrixKeys: ReadonlySet<string> = new Set(),
|
||||
expandedMatrixNodeIds: ReadonlySet<string> = new Set(),
|
||||
partialOptions: Partial<WorkflowGraphLayoutOptions> = {},
|
||||
): WorkflowGraphModel {
|
||||
const options = {...defaultLayoutOptions, ...partialOptions};
|
||||
const {nodes, edges} = buildVisualGraph(jobs, expandedMatrixKeys, options);
|
||||
const {nodes, edges} = buildVisualGraph(jobs, expandedMatrixNodeIds, options);
|
||||
const nodesById = new Map(nodes.map((n) => [n.id, n]));
|
||||
const adjacency = buildNodeAdjacency(edges);
|
||||
assignNodeLevels(nodes, adjacency);
|
||||
|
||||
Reference in New Issue
Block a user