conductor

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

only.js (2647B)


      1 // src/lib/pipeline/only.js - restricting tasks to particular refs
      2 //
      3 // A task carrying an `only` rule runs when the ref that triggered the run
      4 // matches one of its patterns, and is left out of the run entirely when it
      5 // does not. Left out rather than recorded as skipped, so a task that was
      6 // never meant to run on this branch does not show up in the graph at all.
      7 //
      8 // Anything that needs an excluded task is excluded with it. The
      9 // alternative, quietly dropping the dependency, would let a task run
     10 // without something it declared it needed, which is a worse surprise than
     11 // the task not running.
     12 //
     13 // An `after` edge is the other way round. It asks for ordering, not for
     14 // output, and a task that was never going to run has nothing to order
     15 // against, so the edge is dropped and the dependent stays in the run.
     16 
     17 import { transitiveDependents } from './dag.js';
     18 
     19 // Glob matching over a whole ref. Only * is special, standing for any run
     20 // of characters including none; everything else is literal. Deliberately
     21 // not a regular expression, since these come out of a repository and are
     22 // read by people who are not thinking about regular expression syntax.
     23 export function refMatches(pattern, ref) {
     24   if (typeof pattern !== 'string' || typeof ref !== 'string') return false;
     25   if (!pattern.includes('*')) return pattern === ref;
     26 
     27   const source = pattern
     28     .split('*')
     29     .map((part) => part.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))
     30     .join('.*');
     31   return new RegExp(`^${source}$`).test(ref);
     32 }
     33 
     34 export function taskRunsOnRef(task, ref) {
     35   if (!task.only) return true;
     36 
     37   // A run with no ref at all, which a manual or api trigger may produce,
     38   // cannot match a ref pattern. Restricted tasks stay out rather than
     39   // being handed a ref they were never written for.
     40   if (typeof ref !== 'string' || ref === '') return false;
     41 
     42   return task.only.refs.some((pattern) => refMatches(pattern, ref));
     43 }
     44 
     45 // The tasks that belong in a run for this ref, in their original order.
     46 export function selectForRef(tasks, ref) {
     47   const excluded = new Set(tasks.filter((task) => !taskRunsOnRef(task, ref)).map((task) => task.name));
     48   if (excluded.size === 0) return tasks;
     49 
     50   for (const name of transitiveDependents(tasks, [...excluded])) excluded.add(name);
     51 
     52   // Surviving after edges pointing into the excluded set are pruned rather
     53   // than left dangling, since everything downstream resolves them by name.
     54   return tasks
     55     .filter((task) => !excluded.has(task.name))
     56     .map((task) => (task.after?.some((name) => excluded.has(name))
     57       ? { ...task, after: task.after.filter((name) => !excluded.has(name)) }
     58       : task));
     59 }