define-config pages
- define-config
- Step graphs
Step graphs
Let users write flows of steps in config, and check every outcome and loop with resolveGraph before your tool runs one.
A host tool, such as a release runner, knows a set of steps and what each one can end with. Its users write flows in config: which step comes after which outcome. resolveGraph turns those flows into a graph and reports every problem in it, so nothing runs on a flow that cannot finish.
Two inputs
The registry belongs to your tool. It lists each step and its outcomes, the words a step can end with, such as success and fail. The flows belong to the user's config. Each flow names its first step with $start, and each step entry says where an outcome leads.
import { digest, next, resolveGraph, walkOrder } from '@open-tomato/define-config';
const steps = {
check: { outcomes: ['success', 'fail'] },
merge: { outcomes: ['success', 'fail'] },
cleanup: { outcomes: ['success'] },
report: { outcomes: ['success'] },
};
const flows = {
release: {
$start: 'check',
check: { onSuccess: 'merge', onFail: 'report' },
merge: { onSuccess: 'cleanup', onFail: 'report' },
cleanup: {},
report: {},
},
};
const { graph, diagnostics } = resolveGraph(flows, steps);
console.log(diagnostics);
console.log(walkOrder(graph, 'release'));
console.log(next(graph, 'release.check', 'fail'));
console.log(digest(graph));
[]
[ "release.check", "release.merge", "release.cleanup", "release.report" ]
release.report
sha256:76f99fee2de12b18f79a7b5d0d04bbb717db3d45561f3cd4b977d1eaeb6db0ac
onSuccess is shorthand for on: { success: … }; onFail, onTrue and onFalse work the same way, and onChoice takes a map of outcome names to steps, for interactive steps. next answers the step an outcome leads to, which is what a runner asks after each step. digest gives a hash of the graph that stays the same as long as the graph does, so your tool can store it and notice when a user's flow changed.
What the check catches
Here a repair flow sends fix back to check without saying the loop is wanted, and gives fix an outcome it does not have:
import { resolveGraph } from '@open-tomato/define-config';
const steps = {
check: { outcomes: ['success', 'fail'] },
fix: { outcomes: ['success'] },
};
const { diagnostics } = resolveGraph(
{
repair: {
$start: 'check',
check: { onFail: 'fix' },
fix: { onSuccess: 'check', onFail: 'check' },
},
},
steps,
);
console.log(diagnostics.map((d) => `${d.code}: ${d.message}`));
[ "unknown-outcome: step \"fix\" declares no outcome \"fail\"; it declares \"success\"",
"cycle: the handler on \"success\" closes a cycle with the edge fix → check; the cycle is check → fix → check. Mark the edge `{ to: \"check\", repeat: true }` if the loop is wanted",
"cycle: the handler on \"fail\" closes a cycle with the edge fix → check; the cycle is check → fix → check. Mark the edge `{ to: \"check\", repeat: true }` if the loop is wanted"
]
A loop is allowed when its edge says so: onSuccess: { to: 'check', repeat: true }. Bounding how often it runs is your runner's job.
In 0.5.0 a misspelt handler key, such as onDone in place of onFail, raises no diagnostic and no type error. The step is left with no handler for that outcome.
Load flows from config files
Pass your registry to createLoader as steps, and the loader resolves the merged flows section into result.graph, with its diagnostics in the same list as the merge's and the schema's. A team layer can then define a flow, and a project can change one step of it with the merge rules from Layers and merge rules.
Coming, in an open spec (define-config#40, flow shorthands): a flow written as a plain list of step ids, such as ['pr/check', 'pr/merge', 'pr/delete-branch'], where each step's success leads to the next; one step of that list carrying its own onFail; and a step entry holding its own sub-steps. None of this is in 0.5.0.