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 }