Skip to content

Commit

Permalink
feat: refactor into libraries. add join flag. support multiple pr jobs
Browse files Browse the repository at this point in the history
  • Loading branch information
petey committed Oct 11, 2017
1 parent c25225b commit a7f91c3
Show file tree
Hide file tree
Showing 10 changed files with 351 additions and 245 deletions.
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,7 @@ Code licensed under the BSD 3-Clause license. See LICENSE file for terms.
[license-image]: https://img.shields.io/npm/l/screwdriver-workflow-parser.svg
[issues-image]: https://img.shields.io/github/issues/screwdriver-cd/workflow-parser.svg
[issues-url]: https://github.com/screwdriver-cd/workflow-parser/issues
[status-image]: https://cd.screwdriver.cd/pipelines/pipelineid/badge
[status-url]: https://cd.screwdriver.cd/pipelines/pipelineid
[status-image]: https://cd.screwdriver.cd/pipelines/352/badge
[status-url]: https://cd.screwdriver.cd/pipelines/352
[daviddm-image]: https://david-dm.org/screwdriver-cd/workflow-parser.svg?theme=shields.io
[daviddm-url]: https://david-dm.org/screwdriver-cd/workflow-parser
119 changes: 2 additions & 117 deletions index.js
Original file line number Diff line number Diff line change
@@ -1,121 +1,6 @@
'use strict';

/**
* Get the list of nodes for the graph
* @method calculateNodes
* @param {Object} jobs Hash of job configs
* @return {Array} List of nodes (jobs)
*/
const calculateNodes = (jobs) => {
const nodes = [];

Object.keys(jobs).forEach((j) => {
nodes.push({ name: j });
});

return nodes;
};

/**
* Get all the edges of the directed graph from a legacy workflow config
* @method calculateLegacyEdges
* @param {Array} workflow List of all jobs in the workflow excluding "main"
* @return {Array} List of edge objects { src, dest }
*/
const calculateLegacyEdges = (workflow) => {
// In legacy-mode "main" is always required to exist, and be the target of commit and pr
const edges = [
{ src: '~pr', dest: 'main' },
{ src: '~commit', dest: 'main' }
];

// Legacy-mode workflows are always linear, starting with main
let src = 'main';

workflow.forEach((dest) => {
edges.push({ src, dest });
src = dest;
});

return edges;
};

/**
* Calculate edges of directed graph based on "requires" property of jobs
* @method calculateEdges
* @param {Object} jobs Hash of job configurations
* @return {Array} List of graph edges { src, dest }
*/
const calculateEdges = (jobs) => {
const edges = [];

Object.keys(jobs).forEach((j) => {
const job = jobs[j];
const dest = j;

if (Array.isArray(job.requires)) {
job.requires.forEach((src) => {
edges.push({ src, dest });
});
}
});

return edges;
};

/**
* Given a pipeline config, return a directed graph configuration that describes the workflow
* @method getWorkflow
* @param {Object} obj
* @param {Object} obj.config A pipeline config
* @param {Object} obj.config.jobs Hash of job configs
* @param {Array} [obj.config.workflow] Legacy workflow config
* @param {Boolean} [obj.useLegacy] Flag to process legacy workflows
* @return {Object} List of nodes and edges { nodes, edges }
*/
const getWorkflow = ({ config: pipelineConfig, useLegacy = false }) => {
const jobConfig = pipelineConfig.jobs;
let edges = [];

if (!jobConfig) {
throw new Error('No Job config provided');
}

const hasRequiresConfig = Object.keys(jobConfig)
.some(j => Array.isArray(jobConfig[j].requires));

if (useLegacy && !hasRequiresConfig) {
// Work out whether there is a user defined workflow,
// or if we use the order of jobs defined in jobConfig
const workflow = Array.isArray(pipelineConfig.workflow) ?
pipelineConfig.workflow :
Object.keys(jobConfig).filter(j => j !== 'main'); // remove main since that is a hard dependency

edges = calculateLegacyEdges(workflow);
} else {
edges = calculateEdges(jobConfig);
}

return { nodes: calculateNodes(jobConfig), edges };
};

/**
* Calculate the next jobs to execute, given a workflow and a trigger job
* @method getNextJobs
* @param {Object} workflow Directed graph representation of workflow
* @param {String} trigger Name of event that triggers jobs (~pr, ~commit, JobName)
* @return {Array} List of job names
*/
const getNextJobs = (workflow, trigger) => {
const jobs = new Set();

workflow.edges.forEach((edge) => {
if (edge.src === trigger) {
jobs.add(edge.dest);
}
});

return Array.from(jobs);
};
const getWorkflow = require('./lib/getWorkflow');
const getNextJobs = require('./lib/getNextJobs');

module.exports = { getWorkflow, getNextJobs };
33 changes: 33 additions & 0 deletions lib/getNextJobs.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
'use strict';

/**
* Calculate the next jobs to execute, given a workflow and a trigger job
* @method getNextJobs
* @param {Object} workflow Directed graph representation of workflow
* @param {Object} obj
* @param {String} obj.trigger The triggering event (~pr, ~commit, jobName)
* @param {String} [obj.prNum] The PR number (required when ~pr trigger)
* @return {Array} List of job names
*/
const getNextJobs = (workflow, obj) => {
const jobs = new Set();

if (!obj || !obj.trigger) {
throw new Error('Must provide a trigger');
}

if (obj.trigger === '~pr' && !obj.prNum) {
throw new Error('Must provide a PR number with "~pr" trigger');
}

workflow.edges.forEach((edge) => {
if (edge.src === obj.trigger) {
// Make PR jobs PR-$num:$cloneJob (not sure how to better handle multiple PR jobs)
jobs.add(obj.trigger === '~pr' ? `PR-${obj.prNum}:${edge.dest}` : edge.dest);
}
});

return Array.from(jobs);
};

module.exports = getNextJobs;
119 changes: 119 additions & 0 deletions lib/getWorkflow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
'use strict';

/**
* Get the list of nodes for the graph
* @method calculateNodes
* @param {Object} jobs Hash of job configs
* @return {Array} List of nodes (jobs)
*/
const calculateNodes = (jobs) => {
const nodes = [
{ name: '~pr' },
{ name: '~commit' }
];

Object.keys(jobs).forEach((name) => {
nodes.push({ name });
});

return nodes;
};

/**
* Get all the edges of the directed graph from a legacy workflow config
* @method calculateLegacyEdges
* @param {Array} workflow List of all jobs in the workflow excluding "main"
* @return {Array} List of edge objects { src, dest }
*/
const calculateLegacyEdges = (workflow) => {
// In legacy-mode "main" is always required to exist, and be the target of commit and pr
const edges = [
{ src: '~pr', dest: 'main' },
{ src: '~commit', dest: 'main' }
];

// Legacy-mode workflows are always linear, starting with main
let src = 'main';

workflow.forEach((dest) => {
edges.push({ src, dest });
src = dest;
});

return edges;
};

/**
* Calculate edges of directed graph based on "requires" property of jobs
* @method calculateEdges
* @param {Object} jobs Hash of job configurations
* @return {Array} List of graph edges { src, dest }
*/
const calculateEdges = (jobs) => {
const edges = [];

Object.keys(jobs).forEach((j) => {
const job = jobs[j];
const dest = j;

if (Array.isArray(job.requires)) {
const specialTriggers = job.requires.filter(name => name.charAt(0) === '~');
const normalTriggers = job.requires.filter(name => name.charAt(0) !== '~');
const isJoin = normalTriggers.length > 1;

specialTriggers.forEach((src) => {
edges.push({ src, dest });
});

normalTriggers.forEach((src) => {
const obj = { src, dest };

if (isJoin) {
obj.join = true;
}

edges.push(obj);
});
}
});

return edges;
};

/**
* Given a pipeline config, return a directed graph configuration that describes the workflow
* @method getWorkflow
* @param {Object} obj
* @param {Object} obj.config A pipeline config
* @param {Object} obj.config.jobs Hash of job configs
* @param {Array} [obj.config.workflow] Legacy workflow config
* @param {Boolean} [obj.useLegacy] Flag to process legacy workflows
* @return {Object} List of nodes and edges { nodes, edges }
*/
const getWorkflow = ({ config: pipelineConfig, useLegacy = false }) => {
const jobConfig = pipelineConfig.jobs;
let edges = [];

if (!jobConfig) {
throw new Error('No Job config provided');
}

const hasRequiresConfig = Object.keys(jobConfig)
.some(j => Array.isArray(jobConfig[j].requires));

if (useLegacy && !hasRequiresConfig) {
// Work out whether there is a user defined workflow,
// or if we use the order of jobs defined in jobConfig
const workflow = Array.isArray(pipelineConfig.workflow) ?
pipelineConfig.workflow :
Object.keys(jobConfig).filter(j => j !== 'main'); // remove main since that is a hard dependency

edges = calculateLegacyEdges(workflow);
} else {
edges = calculateEdges(jobConfig);
}

return { nodes: calculateNodes(jobConfig), edges };
};

module.exports = getWorkflow;
15 changes: 15 additions & 0 deletions test/data/expected-output.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
{
"nodes": [
{ "name": "~pr" },
{ "name": "~commit" },
{ "name": "main" },
{ "name": "foo" },
{ "name": "bar" }
],
"edges": [
{ "src": "~pr", "dest": "main" },
{ "src": "~commit", "dest": "main" },
{ "src": "main", "dest": "foo" },
{ "src": "foo", "dest": "bar" }
]
}
7 changes: 7 additions & 0 deletions test/data/legacy-no-workflow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"jobs": {
"main": {},
"foo": {},
"bar": {}
}
}
7 changes: 7 additions & 0 deletions test/data/requires-workflow.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
{
"jobs": {
"main": { "requires": ["~pr", "~commit"] },
"foo": { "requires": ["main"] },
"bar": { "requires": ["foo"] }
}
}
Loading

0 comments on commit a7f91c3

Please sign in to comment.