diff --git a/lib/getWorkflow.js b/lib/getWorkflow.js index 520637a..4371ad1 100644 --- a/lib/getWorkflow.js +++ b/lib/getWorkflow.js @@ -1,5 +1,13 @@ 'use strict'; +/** + * Remove the ~ prefix for logical OR on select node names. + * @method filterNodeName + * @param {String} name A Node Name, e.g. foo, ~foo, ~pr, ~commit, ~sd@1234:foo + * @return {String} A filtered node name, e.g. foo, foo, ~pr, ~commit, ~sd@1234:foo + */ +const filterNodeName = name => (/^~(pr|commit|sd@)/.test(name) ? name : name.replace('~', '')); + /** * Get the list of nodes for the graph * @method calculateNodes @@ -7,16 +15,16 @@ * @return {Array} List of nodes (jobs) */ const calculateNodes = (jobs) => { - const nodes = [ - { name: '~pr' }, - { name: '~commit' } - ]; + const nodes = new Set(['~pr', '~commit']); Object.keys(jobs).forEach((name) => { - nodes.push({ name }); + nodes.add(name); + if (Array.isArray(jobs[name].requires)) { + jobs[name].requires.forEach(n => nodes.add(filterNodeName(n))); + } }); - return nodes; + return [...nodes].map(name => ({ name })); }; /** @@ -57,12 +65,12 @@ const calculateEdges = (jobs) => { 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; + const specialTriggers = new Set(job.requires.filter(name => name.charAt(0) === '~')); + const normalTriggers = new Set(job.requires.filter(name => name.charAt(0) !== '~')); + const isJoin = normalTriggers.size > 1; specialTriggers.forEach((src) => { - edges.push({ src, dest }); + edges.push({ src: filterNodeName(src), dest }); }); normalTriggers.forEach((src) => { diff --git a/test/data/expected-external.json b/test/data/expected-external.json new file mode 100644 index 0000000..70d913a --- /dev/null +++ b/test/data/expected-external.json @@ -0,0 +1,17 @@ +{ + "nodes": [ + { "name": "~pr" }, + { "name": "~commit" }, + { "name": "main" }, + { "name": "foo" }, + { "name": "bar" }, + { "name": "~sd@1234:foo" } + ], + "edges": [ + { "src": "~pr", "dest": "main" }, + { "src": "~commit", "dest": "main" }, + { "src": "main", "dest": "foo" }, + { "src": "~sd@1234:foo", "dest": "bar" }, + { "src": "foo", "dest": "bar" } + ] +} diff --git a/test/data/requires-workflow-exttrigger.json b/test/data/requires-workflow-exttrigger.json new file mode 100644 index 0000000..e2b512c --- /dev/null +++ b/test/data/requires-workflow-exttrigger.json @@ -0,0 +1,7 @@ +{ + "jobs": { + "main": { "requires": ["~pr", "~commit"] }, + "foo": { "requires": ["main"] }, + "bar": { "requires": ["foo", "~sd@1234:foo"] } + } +} diff --git a/test/lib/getWorkflow.test.js b/test/lib/getWorkflow.test.js index 80b9496..7c16971 100644 --- a/test/lib/getWorkflow.test.js +++ b/test/lib/getWorkflow.test.js @@ -13,8 +13,11 @@ const LEGACY_AND_REQUIRES_WORKFLOW = Object.assign({}, REQUIRES_WORKFLOW); LEGACY_WITH_WORKFLOW.workflow = ['foo', 'bar']; +const EXTERNAL_TRIGGER = require('../data/requires-workflow-exttrigger'); + const EXPECTED_OUTPUT = require('../data/expected-output'); const NO_EDGES = Object.assign({}, EXPECTED_OUTPUT); +const EXPECTED_EXTERNAL = require('../data/expected-external'); NO_EDGES.edges = []; @@ -34,6 +37,9 @@ describe('getWorkflow', () => { assert.deepEqual(getWorkflow(REQUIRES_WORKFLOW, { useLegacy: true }), EXPECTED_OUTPUT, 'requires-style workflow'); + assert.deepEqual(getWorkflow(EXTERNAL_TRIGGER, { + useLegacy: true + }), EXPECTED_EXTERNAL, 'requires-style workflow with external trigger'); assert.deepEqual(getWorkflow(LEGACY_AND_REQUIRES_WORKFLOW, { useLegacy: true }), EXPECTED_OUTPUT, 'both legacy and non-legacy workflows'); @@ -51,6 +57,8 @@ describe('getWorkflow', () => { EXPECTED_OUTPUT, 'requires-style workflow'); assert.deepEqual(getWorkflow(LEGACY_AND_REQUIRES_WORKFLOW), EXPECTED_OUTPUT, 'both legacy and non-legacy workflows'); + assert.deepEqual(getWorkflow(EXTERNAL_TRIGGER), + EXPECTED_EXTERNAL, 'requires-style workflow with external trigger'); }); it('should handle detatched jobs', () => { @@ -67,6 +75,93 @@ describe('getWorkflow', () => { }); }); + it('should handle logical OR requires', () => { + const result = getWorkflow({ + jobs: { + foo: { requires: ['~commit'] }, + A: { requires: ['foo'] }, + B: { requires: ['foo'] }, + C: { requires: ['~A', '~B', '~sd@1234:foo'] } + } + }); + + assert.deepEqual(result, { + nodes: [ + { name: '~pr' }, + { name: '~commit' }, + { name: 'foo' }, + { name: 'A' }, + { name: 'B' }, + { name: 'C' }, + { name: '~sd@1234:foo' } + ], + edges: [ + { src: '~commit', dest: 'foo' }, + { src: 'foo', dest: 'A' }, + { src: 'foo', dest: 'B' }, + { src: 'A', dest: 'C' }, + { src: 'B', dest: 'C' }, + { src: '~sd@1234:foo', dest: 'C' } + ] + }); + }); + + it('should handle logical OR and logial AND requires', () => { + const result = getWorkflow({ + jobs: { + foo: { requires: ['~commit'] }, + A: { requires: ['foo'] }, + B: { requires: ['foo'] }, + C: { requires: ['~A', '~B', 'D', 'E'] }, + D: {}, + E: {} + } + }); + + assert.deepEqual(result, { + nodes: [ + { name: '~pr' }, + { name: '~commit' }, + { name: 'foo' }, + { name: 'A' }, + { name: 'B' }, + { name: 'C' }, + { name: 'D' }, + { name: 'E' } + ], + edges: [ + { src: '~commit', dest: 'foo' }, + { src: 'foo', dest: 'A' }, + { src: 'foo', dest: 'B' }, + { src: 'A', dest: 'C' }, + { src: 'B', dest: 'C' }, + { src: 'D', dest: 'C', join: true }, + { src: 'E', dest: 'C', join: true } + ] + }); + }); + + it('should dedupe requires', () => { + const result = getWorkflow({ + jobs: { + foo: { requires: ['A', 'A', 'A'] }, + A: {} + } + }); + + assert.deepEqual(result, { + nodes: [ + { name: '~pr' }, + { name: '~commit' }, + { name: 'foo' }, + { name: 'A' } + ], + edges: [ + { src: 'A', dest: 'foo' } + ] + }); + }); + it('should handle joins', () => { const result = getWorkflow({ jobs: {