reci.graph

Recipe DAG — computational graph built on graphlib.

RecipeGraph wraps graphlib.TopologicalSorter to model the two-level structure of GitHub Actions workflows: jobs containing sequential steps, with inter-job parallelism governed by needs: edges and data-flow dependencies.

Functions

find_cycle(deps)

A dependency cycle in {node: nodes it depends on}, or None.

Classes

ActionNode(step_id, job[, ref, action_spec, ...])

A single step in the recipe graph.

CrossJobEdge(source_job, source_step_id, ...)

Data-flow edge between steps in different jobs.

RecipeGraph()

The computational DAG of a recipe.

class reci.graph.ActionNode(step_id, job, ref=None, action_spec=None, with_=<factory>, bind=<factory>, if_=None, declared_outputs=<factory>, run=None, name=None, env=<factory>)[source]

Bases: object

A single step in the recipe graph.

Uses step_id + job as its identity (the graph keys these as "{job}.{step_id}" strings for TopologicalSorter).

property input_specs: dict[str, InputSpec]

steps.

Type:

Input specs from the action, or empty for run

property output_names: list[str]

All normalized output names this node produces.

property output_spellings: dict[str, str]

{normalized output name: spelling to emit} for this node.

Declared (recipe) outputs come first; the action’s own action.yml spelling wins when both name the same output.

class reci.graph.CrossJobEdge(source_job, source_step_id, source_output, target_job, target_step_id, target_input, alternatives=())[source]

Bases: object

Data-flow edge between steps in different jobs.

alternatives: tuple[str, ...] = ()

other jobs that also produce the output; the first producer in recipe order (source_job) is the one used

class reci.graph.RecipeGraph[source]

Bases: object

The computational DAG of a recipe.

Nodes are identified by "{job}.{step_id}" strings. The actual ActionNode objects are stored in _nodes.

execution_waves()[source]

Return parallel execution waves (topological generations).

Each wave is a tuple of node keys that can execute concurrently. Raises CycleError if the graph has cycles.

Return type:

list[tuple[str, ...]]

classmethod from_recipe(recipe, action_specs)[source]

Build a graph from a parsed recipe and fetched action specs.

action_specs is keyed by action ref (e.g. "actions/checkout@v4").

Return type:

RecipeGraph

job_needs(job_id)[source]

The job ids job_id lists in its explicit needs:.

Return type:

list[str]

upstream_output_names(node, *, other_jobs=True)[source]

Normalized output names node can be fed from.

Outputs of earlier steps in its own job, plus (unless other_jobs is false) outputs of any step in another job, which the compiler forwards through needs.

Return type:

set[str]

validate(config=None)[source]

Run all validation rules and return a ValidationReport.

Imported lazily to avoid circular imports (validation depends on graph).

reci.graph.find_cycle(deps)[source]

A dependency cycle in {node: nodes it depends on}, or None.

The cycle is returned as a closed path following the dependency direction: each node depends on the next, and the first node is repeated at the end.

Return type:

list[str] | None

>>> find_cycle({'a': ['b'], 'b': ['a']})
['a', 'b', 'a']
>>> find_cycle({'a': ['b'], 'b': ['c'], 'c': ['a']})
['a', 'b', 'c', 'a']
>>> find_cycle({'a': ['b'], 'b': []}) is None
True