conductor

CI task system
git clone git://git.finwo.net/app/conductor
Log | Files | Refs | README | LICENSE

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 });