[JENKINS-49050] Add support for sequential parallel stages (#1784)

* add support for sequential parallel stages

* update sequential stages unit test

* fix bugs

* fix pipeline graph while pipeline is running
This commit is contained in:
Nicolae Pascu 2018-08-10 13:43:36 +10:00 committed by GitHub
parent 9cd5d47167
commit 5e1fed1828
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 93 additions and 76 deletions

View File

@ -91,6 +91,19 @@ function convertJenkinsNodeDetails(jenkinsNode, isCompleted, skewMillis = 0) {
return converted;
}
function buildSequentialStages(originalNodes, convertedNodes, sequentialNodeKey, currentNode) {
const nextSequentialNodeId = originalNodes[sequentialNodeKey].edges[0] ? originalNodes[sequentialNodeKey].edges[0].id : '';
currentNode.isSequential = true;
if (nextSequentialNodeId) {
if (originalNodes[sequentialNodeKey].edges.length && originalNodes[nextSequentialNodeId].firstParent == currentNode.id) {
currentNode.nextSibling = convertedNodes[nextSequentialNodeId];
buildSequentialStages(originalNodes, convertedNodes, currentNode.nextSibling.id, currentNode.nextSibling);
}
}
}
/**
* Convert the graph results of a run as reported by Jenkins into the model required by the PipelineGraph component
*
@ -109,7 +122,6 @@ export function convertJenkinsNodeGraph(jenkinsGraph, isCompleted, skewMillis) {
const edgeCountToNode = {}; // id => int
// const edgeCountFromNode = {}; // id => int
let firstNode = undefined;
// Convert the basic details of nodes, and index them by id
jenkinsGraph.forEach(jenkinsNode => {
const convertedNode = convertJenkinsNodeDetails(jenkinsNode, isCompleted, skewMillis);
@ -138,7 +150,9 @@ export function convertJenkinsNodeGraph(jenkinsGraph, isCompleted, skewMillis) {
// Follow the graph and build our results
let currentNode = firstNode;
while (currentNode) {
results.push(currentNode);
if (!currentNode.isSequential) {
results.push(currentNode);
}
let nextNode = null;
const originalNode = originalNodeForId[currentNode.id];
@ -150,6 +164,7 @@ export function convertJenkinsNodeGraph(jenkinsGraph, isCompleted, skewMillis) {
if (edges.length === 1 && parallelNodes.length === 0) {
// Single following (sibling) node
nextNode = convertedNodeForId[edges[0].id];
} else if (parallelNodes.length > 0) {
// Multiple following nodes are child nodes (parallel branch) not siblings
@ -165,6 +180,18 @@ export function convertJenkinsNodeGraph(jenkinsGraph, isCompleted, skewMillis) {
for (const branchNode of branchNodes) {
const branchNodeEdges = originalNodeForId[branchNode.id].edges || [];
Object.keys(convertedNodeForId).map((key, index) => {
//Check if this stage contains sequential stages and if so, replace it with the first one in the sequence
if (originalNodeForId[key].firstParent === branchNode.id) {
currentNode.children[0] = convertedNodeForId[key];
if (originalNodeForId[key].edges[0]) {
buildSequentialStages(originalNodeForId, convertedNodeForId, key, currentNode.children[0]);
}
}
});
if (branchNodeEdges.length > 0) {
// Should only be 0 at end of pipeline or bad input data
const followingNode = convertedNodeForId[branchNodeEdges[0].id];
@ -246,7 +273,6 @@ export default class PipelineRunGraph extends Component {
}
const id = this.props.selectedStage.id;
let selectedStage = null;
// Find selected stage by id
@ -255,9 +281,15 @@ export default class PipelineRunGraph extends Component {
selectedStage = topStage;
} else {
for (const child of topStage.children) {
if (child.id === id) {
selectedStage = child;
break;
let currentStage = child;
while (currentStage) {
if (currentStage.id === id) {
selectedStage = currentStage;
break;
}
currentStage = currentStage.nextSibling;
}
}
}

View File

@ -323,12 +323,16 @@ export default class Pipeline extends Component {
let nodeRestartId = this.pager.currentNode.restartable ? this.pager.currentNode.id : '';
let nodeRestartTitle = this.pager.currentNode.restartable ? title : '';
if (this.pager.currentNode.restartable == false && this.pager.currentNode.type == 'PARALLEL') {
const currentNodeParent = this.pager.nodes.data.model.filter(node => node.id == this.pager.currentNode.parent)[0];
if (this.pager.currentNode.restartable == false) {
let currentNodeParent = this.pager.nodes.data.model.filter(node => node.id == this.pager.currentNode.firstParent)[0];
if (currentNodeParent.restartable) {
nodeRestartId = currentNodeParent.id;
nodeRestartTitle = currentNodeParent.title;
while (currentNodeParent) {
if (currentNodeParent && currentNodeParent.restartable) {
nodeRestartId = currentNodeParent.id;
nodeRestartTitle = currentNodeParent.title;
break;
}
currentNodeParent = this.pager.nodes.data.model.filter(node => node.id == currentNodeParent.firstParent)[0];
}
}

View File

@ -4,6 +4,8 @@ import { logging } from '@jenkins-cd/blueocean-core-js';
import { prefixIfNeeded } from '../../urls/prefixIfNeeded';
import { KaraokeApi } from '../../index';
import { convertJenkinsNodeGraph } from '../../../PipelineRunGraph';
const logger = logging.logger('io.jenkins.blueocean.dashboard.karaoke.Pager.Pipeline');
/**
@ -111,7 +113,9 @@ export class PipelinePager {
} else {
// Actually we should only come here on a not running job
logger.debug('Actually we should only come here on a not running job');
const lastNode = logData.data.model[logData.data.model.length - 1];
const convertedNodes = convertJenkinsNodeGraph(logData.data.model, result.isFinished, 0);
const lastNode = logData.data.model.filter(node => node.id === convertedNodes[convertedNodes.length - 1].id)[0];
this.currentNode = lastNode;
}
this.currentStepsUrl = prefixIfNeeded(this.currentNode._links.steps.href);

View File

@ -66,6 +66,7 @@ export const getNodesInformation = nodes => {
logUrl: hasLogs ? logActions[0]._links.self.href : undefined,
isParallel,
parent,
firstParent: item.firstParent || undefined,
isRunning,
isCompleted,
computedResult,

View File

@ -572,7 +572,7 @@ export class PipelineGraph extends Component {
*/
stageIsSelected(stage?: StageInfo) {
const { selectedStage } = this.state;
return selectedStage && selectedStage === stage;
return selectedStage && stage && selectedStage.id === stage.id;
}
/**
@ -585,16 +585,18 @@ export class PipelineGraph extends Component {
if (children && selectedStage) {
for (const childStage of children) {
let testee = childStage;
while (testee) {
if (testee === selectedStage) {
let currentStage = childStage;
while (currentStage) {
if (currentStage.id === selectedStage.id) {
return true;
}
testee = testee.nextSibling;
currentStage = currentStage.nextSibling;
}
}
}
}
return false;
}

View File

@ -219,7 +219,7 @@ function createSmallLabels(columns: Array<NodeColumn>) {
for (const row of column.rows) {
for (const node of row) {
// We add small labels to parallel nodes only so skip others
if (!node.stage || node.stage.type !== 'PARALLEL') {
if (!node.stage || (node.stage.type !== 'PARALLEL' && node.stage.isSequential !== true)) {
continue;
}
const label: LabelInfo = {

View File

@ -11,17 +11,21 @@ const validResultValues = StatusIndicator.validResultValues;
// Data creation helpers Lifted from stories
let __id = 1111;
function makeNode(name, children = [], state = validResultValues.not_built, type='STAGE', completePercent) {
completePercent = completePercent || ((state == validResultValues.running) ? Math.floor(Math.random() * 60 + 20) : 50);
function makeNode(name, children = [], state = validResultValues.not_built, type = 'STAGE', completePercent) {
completePercent = completePercent || (state == validResultValues.running ? Math.floor(Math.random() * 60 + 20) : 50);
const id = __id++;
return {name, children, state, completePercent, id, type};
return { name, children, state, completePercent, id, type };
}
function makeSequence(...stages) {
for (let i = 0; i < stages.length - 1; i++) {
stages[i].nextSibling = stages[i + 1];
stages[i].isSequential = true;
}
//also mark the last node in the sequence as sequential
stages[stages.length - 1].isSequential = true;
return stages[0]; // The model only needs the first in a sequence
}
@ -50,7 +54,7 @@ function assertRow(row, ...nodesParams) {
}
}
function assertLabel(labels, text, x,y) {
function assertLabel(labels, text, x, y) {
const label = labels.find(label => label.text === text);
assert.ok(label, `label ${text} exists`);
assert.equal(label.x, x, `label ${text} x`);
@ -67,12 +71,11 @@ function assertConnection(connections, sourceName, destinationName) {
}
}
assert.fail(0,1,`could not find ${sourceName} --> ${destinationName} connection`);
assert.fail(0, 1, `could not find ${sourceName} --> ${destinationName} connection`);
}
describe('PipelineGraph', () => {
describe('layoutGraph', () => {
it('gracefully handles a Stage with null children', () => {
const stagesNullChildren = require('../data/pipeline-graph/stages-with-null-children.json');
const { nodeColumns } = layoutGraph(stagesNullChildren, defaultLayout);
@ -96,23 +99,20 @@ describe('PipelineGraph', () => {
]),
makeNode('Skizzled', [], validResultValues.skipped),
makeNode('Foshizzle', [], validResultValues.skipped),
makeNode('Dev', [
makeNode('US-East', [], validResultValues.success, 'PARALLEL'),
makeNode('US-West', [], validResultValues.success, 'PARALLEL'),
makeNode('APAC', [], validResultValues.success, 'PARALLEL'),
], validResultValues.success),
makeNode(
'Dev',
[
makeNode('US-East', [], validResultValues.success, 'PARALLEL'),
makeNode('US-West', [], validResultValues.success, 'PARALLEL'),
makeNode('APAC', [], validResultValues.success, 'PARALLEL'),
],
validResultValues.success
),
makeNode('Staging', [], validResultValues.skipped),
makeNode('Production'),
];
const {
nodeColumns,
connections,
bigLabels,
smallLabels,
measuredWidth,
measuredHeight,
} = layoutGraph(stages, defaultLayout);
const { nodeColumns, connections, bigLabels, smallLabels, measuredWidth, measuredHeight } = layoutGraph(stages, defaultLayout);
// Basic stuff
@ -269,30 +269,20 @@ describe('PipelineGraph', () => {
it('lays out a multi-stage parallel graph', () => {
const stages = [
makeNode("Alpha"),
makeNode("Bravo", [
makeNode("Echo", [], validResultValues.not_built, 'PARALLEL'),
makeNode('Alpha'),
makeNode('Bravo', [
makeNode('Echo', [], validResultValues.not_built, 'PARALLEL'),
makeSequence(
makeNode("Foxtrot", [], validResultValues.not_built, 'PARALLEL'),
makeNode("Golf", [], validResultValues.not_built, 'PARALLEL'),
makeNode("Hotel", [], validResultValues.not_built, 'PARALLEL'),
makeNode('Foxtrot', [], validResultValues.not_built, 'STAGE'),
makeNode('Golf', [], validResultValues.not_built, 'STAGE'),
makeNode('Hotel', [], validResultValues.not_built, 'STAGE')
),
makeSequence(
makeNode("India", [], validResultValues.not_built, 'PARALLEL'),
makeNode("Juliet", [], validResultValues.not_built, 'PARALLEL'),
)
makeSequence(makeNode('India', [], validResultValues.not_built, 'STAGE'), makeNode('Juliet', [], validResultValues.not_built, 'STAGE')),
]),
makeNode("Charlie"),
makeNode('Charlie'),
];
const {
nodeColumns,
connections,
bigLabels,
smallLabels,
measuredWidth,
measuredHeight,
} = layoutGraph(stages, defaultLayout);
const { nodeColumns, connections, bigLabels, smallLabels, measuredWidth, measuredHeight } = layoutGraph(stages, defaultLayout);
// Basic stuff
@ -327,15 +317,8 @@ describe('PipelineGraph', () => {
assert.equal(col.topStage.name, 'Bravo', 'top stage name');
assert.equal(3, col.rows.length);
assertSingleNodeRow(col.rows[0], 'Echo', 384, 55);
assertRow(col.rows[1],
['Foxtrot', 264, 125],
['Golf', 384, 125],
['Hotel', 504, 125],
);
assertRow(col.rows[2],
['India', 324, 195],
['Juliet', 444, 195],
);
assertRow(col.rows[1], ['Foxtrot', 264, 125], ['Golf', 384, 125], ['Hotel', 504, 125]);
assertRow(col.rows[2], ['India', 324, 195], ['Juliet', 444, 195]);
// Col 3
col = nodeColumns[3];
@ -381,20 +364,11 @@ describe('PipelineGraph', () => {
it('lays out a single node parallel graph', () => {
const stages = [
makeNode('Build', [], validResultValues.success),
makeNode('Test', [
makeNode('JUnit', [], validResultValues.success, 'PARALLEL'),
]),
makeNode('Test', [makeNode('JUnit', [], validResultValues.success, 'PARALLEL')]),
makeNode('Deploy'),
];
const {
nodeColumns,
connections,
bigLabels,
smallLabels,
measuredWidth,
measuredHeight,
} = layoutGraph(stages, defaultLayout);
const { nodeColumns, connections, bigLabels, smallLabels, measuredWidth, measuredHeight } = layoutGraph(stages, defaultLayout);
// Basic stuff