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
BREAKING CHANGE: altered the signature for getWorkflow, getNextJobs
  • Loading branch information
petey committed Oct 12, 2017
1 parent c25225b commit 23d4c1e
Show file tree
Hide file tree
Showing 10 changed files with 359 additions and 245 deletions.
24 changes: 22 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,26 @@
npm install screwdriver-workflow-parser
```

```
const { getWorkflow, getNextJobs } = require('screwdriver-workflow-parser');
// Calculate the directed graph workflow from a pipeline config (and parse legacy workflows)
const workflowGraph = getWorkflow(pipelineConfig, { useLegacy: true });
/*
{
nodes: [{ name: '~pr'}, { name: '~commit'}, { name: 'main' }],
edges: [{ src: '~pr', dest: 'main'}, { src: '~commit', dest: 'main'}]
}
*/
// Get a list of job names to start as a result of a commit event, e.g. [ 'a', 'b' ]
const commitJobsToTrigger = getNextJobs(workflowGraph, { trigger: '~commit' });
// Get a list of job names to start as a result of a pull-request event, e.g. [ 'PR-123:a' ]
const prJobsToTrigger = getNextJobs(workflowGraph, { trigger: '~pr', prNum: 123 });
```

## Testing

```bash
Expand All @@ -25,7 +45,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} workflowGraph Directed graph representation of workflow
* @param {Object} config
* @param {String} config.trigger The triggering event (~pr, ~commit, jobName)
* @param {String} [config.prNum] The PR number (required when ~pr trigger)
* @return {Array} List of job names
*/
const getNextJobs = (workflowGraph, config) => {
const jobs = new Set();

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

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

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

return Array.from(jobs);
};

module.exports = getNextJobs;
123 changes: 123 additions & 0 deletions lib/getWorkflow.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
'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} pipelineConfig A Pipeline Config
* @param {Object} pipelineConfig.jobs Hash of job configs
* @param {Array} [pipelineConfig.workflow] Legacy workflow config
* @param {Object} [config] configuration object
* @param {Boolean} [config.useLegacy] Flag to process legacy workflows
* @return {Object} List of nodes and edges { nodes, edges }
*/
const getWorkflow = (pipelineConfig, config = { 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 (config.useLegacy && !hasRequiresConfig) {
// Work out whether there is a user defined workflow,
// or if we use the order of jobs defined in jobConfig
let workflow = Array.isArray(pipelineConfig.workflow) ?
pipelineConfig.workflow :
Object.keys(jobConfig);

// remove main since that is a hard dependency in legacy workflows
// main is already accounted for in calculateLegacyEdges
workflow = workflow.filter(j => j !== 'main');

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 23d4c1e

Please sign in to comment.