only.test.js (5257B)
1 // test/only.test.js - restricting tasks to particular refs 2 3 import test from 'node:test'; 4 import assert from 'node:assert/strict'; 5 6 import { compilePipeline, refMatches, PipelineError } from '../src/lib/pipeline/index.js'; 7 8 const PIPELINE = ` 9 version: 1 10 defaults: 11 image: alpine:3 12 tasks: 13 build: 14 script: ['make'] 15 publish: 16 needs: [build] 17 only: 18 refs: [refs/heads/main, 'refs/tags/v*'] 19 script: ['make publish'] 20 `; 21 22 function names(text, ref) { 23 return compilePipeline(text, { ref }).tasks.map((j) => j.name).sort(); 24 } 25 26 test('a pattern without a star matches exactly', () => { 27 assert.ok(refMatches('refs/heads/main', 'refs/heads/main')); 28 assert.ok(!refMatches('refs/heads/main', 'refs/heads/main-2')); 29 assert.ok(!refMatches('refs/heads/main', 'refs/heads/mai')); 30 31 // A pattern is matched against the whole ref, so a branch name on its 32 // own is not enough. 33 assert.ok(!refMatches('main', 'refs/heads/main')); 34 }); 35 36 test('a star matches any job of characters', () => { 37 assert.ok(refMatches('refs/tags/v*', 'refs/tags/v1.2.0')); 38 assert.ok(refMatches('refs/tags/v*', 'refs/tags/v')); 39 assert.ok(!refMatches('refs/tags/v*', 'refs/heads/v1')); 40 assert.ok(refMatches('*', 'refs/heads/anything')); 41 assert.ok(refMatches('refs/*/main', 'refs/heads/main')); 42 }); 43 44 test('the rest of a pattern is literal, not a regular expression', () => { 45 // A dot is a dot. Were this compiled straight to a regular expression, 46 // v1.2.0 would also match v1x2x0. 47 assert.ok(refMatches('refs/tags/v1.2.0', 'refs/tags/v1.2.0')); 48 assert.ok(!refMatches('refs/tags/v1.2.0', 'refs/tags/v1x2x0')); 49 50 // And these would be quantifiers rather than characters. 51 assert.ok(refMatches('refs/heads/fix+', 'refs/heads/fix+')); 52 assert.ok(!refMatches('refs/heads/fix+', 'refs/heads/fixx')); 53 }); 54 55 test('a restricted task is left out of jobs for other refs', () => { 56 assert.deepEqual(names(PIPELINE, 'refs/heads/main'), ['build', 'publish']); 57 assert.deepEqual(names(PIPELINE, 'refs/tags/v1.2.0'), ['build', 'publish']); 58 assert.deepEqual(names(PIPELINE, 'refs/heads/feature'), ['build']); 59 }); 60 61 test('it is left out rather than recorded as skipped', () => { 62 // The scheduler reads a skipped task as a reason to fail the job, so a 63 // publish step that was never meant to run here must not appear at all. 64 const { tasks } = compilePipeline(PIPELINE, { ref: 'refs/heads/feature' }); 65 assert.ok(!tasks.some((j) => j.name === 'publish')); 66 }); 67 68 test('tasks depending on an excluded task are excluded too', () => { 69 const text = ` 70 version: 1 71 defaults: 72 image: alpine:3 73 tasks: 74 build: 75 script: ['make'] 76 publish: 77 needs: [build] 78 only: 79 refs: [refs/heads/main] 80 script: ['make publish'] 81 announce: 82 needs: [publish] 83 script: ['make announce'] 84 `; 85 // announce carries no rule of its own, but running it without publish 86 // would mean running it without something it declared it needed. 87 assert.deepEqual(names(text, 'refs/heads/feature'), ['build']); 88 assert.deepEqual(names(text, 'refs/heads/main'), ['announce', 'build', 'publish']); 89 }); 90 91 test('depth and ordering describe the job that will happen', () => { 92 const { tasks } = compilePipeline(PIPELINE, { ref: 'refs/heads/feature' }); 93 assert.equal(tasks.length, 1); 94 assert.equal(tasks[0].name, 'build'); 95 assert.equal(tasks[0].depth, 0); 96 }); 97 98 test('without a ref nothing is filtered, so a pipeline can still be validated', () => { 99 const { tasks } = compilePipeline(PIPELINE); 100 assert.deepEqual(tasks.map((j) => j.name).sort(), ['build', 'publish']); 101 }); 102 103 test('a restricted task cannot match a job that has no ref', () => { 104 assert.deepEqual(names(PIPELINE, ''), ['build']); 105 }); 106 107 test('a malformed rule is reported rather than ignored', () => { 108 const cases = [ 109 ['only: refs/heads/main', 'must be a mapping'], 110 ['only:\n branches: [main]', 'unknown key'], 111 ['only:\n refs: []', 'at least one'], 112 ['only: {}', 'needs a refs list'], 113 ]; 114 115 for (const [fragment, expected] of cases) { 116 const text = ` 117 version: 1 118 defaults: 119 image: alpine:3 120 tasks: 121 publish: 122 script: ['make publish'] 123 ${fragment} 124 `; 125 assert.throws( 126 () => compilePipeline(text, { ref: 'refs/heads/main' }), 127 (error) => { 128 assert.ok(error instanceof PipelineError, `${fragment}: expected PipelineError`); 129 const text = error.errors.map((e) => e.message).join('; '); 130 assert.match(text, new RegExp(expected), `${fragment}: got ${text}`); 131 return true; 132 }, 133 `expected ${JSON.stringify(fragment)} to be rejected`, 134 ); 135 } 136 }); 137 138 test('the rule is not inherited from defaults', () => { 139 // Inheriting it would restrict every task at once, which is a hard 140 // mistake to spot when the symptom is an empty job. 141 const text = ` 142 version: 1 143 defaults: 144 image: alpine:3 145 only: 146 refs: [refs/heads/main] 147 tasks: 148 build: 149 script: ['make'] 150 `; 151 assert.throws( 152 () => compilePipeline(text, { ref: 'refs/heads/main' }), 153 (error) => { 154 assert.ok(error instanceof PipelineError); 155 const message = error.errors.map((e) => `${e.path}: ${e.message}`).join('; '); 156 assert.match(message, /defaults\.only/, `expected defaults.only to be rejected, got ${message}`); 157 return true; 158 }, 159 ); 160 });