Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

BREAKING CHANGE: refactor into libraries. add join flag. support multiple pr jobs #2

Merged
merged 1 commit into from
Oct 12, 2017
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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