reci.recipe_checks

Whole-recipe checks that need the recipe text and the config, not the step graph.

Shared by the compiler (which refuses to emit a workflow with these problems) and the validation rules (which report them), so both always agree.

Module Attributes

SETUP_JOB_ID

The generated job that exports config values; recipe jobs may not use the id when config is supplied.

Functions

as_expression(text)

An if: value as a ${{ }} expression: GitHub evaluates it either way.

config_names(text)

The config keys referenced in text's ``${{ .

config_references(recipe)

(location, key, needs_context_available) for each ${{ config.key }}.

expression_texts(text)

The body of each ${{ }} in text, string literals blanked.

find_recipe_problems(recipe, config)

Problems that would make the compiled workflow invalid.

serialize_config_value(value)

A config value as the text of a $GITHUB_OUTPUT entry.

sub_config_refs(text, repl)

Substitute every config.<key> inside a ``${{ .

unsupported_config_refs(text)

The uses of the config context in text that reci cannot rewrite.

Classes

RecipeProblem(rule_id, message[, location])

One problem found by find_recipe_problems().

class reci.recipe_checks.RecipeProblem(rule_id, message, location=None)[source]

Bases: object

One problem found by find_recipe_problems().

reci.recipe_checks.SETUP_JOB_ID = 'setup'

The generated job that exports config values; recipe jobs may not use the id when config is supplied.

reci.recipe_checks.as_expression(text)[source]

An if: value as a ${{ }} expression: GitHub evaluates it either way.

Return type:

str

>>> as_expression("config.flag"), as_expression("${{ config.flag }}")
('${{ config.flag }}', '${{ config.flag }}')
reci.recipe_checks.config_names(text)[source]

The config keys referenced in text’s ${{ ... }} expressions, in order.

Return type:

list[str]

>>> config_names("${{ config.a }} ${{ !config.b && 'config.c' }} config.d")
['a', 'b']
reci.recipe_checks.config_references(recipe)[source]

(location, key, needs_context_available) for each ${{ config.key }}.

Duplicates within one location are reported once.

Return type:

list[tuple[str, str, bool]]

reci.recipe_checks.expression_texts(text)[source]

The body of each ${{ }} in text, string literals blanked.

A literal survives only as the index of a bracket access (steps['p']), so a name inside a message or comparison ('steps.p.outputs.v') is never taken for a reference.

Return type:

Iterator[str]

>>> list(expression_texts("${{ steps['p'].outputs.v == 'steps.q.outputs.w' }} x"))
[" steps['p'].outputs.v == '' "]
reci.recipe_checks.find_recipe_problems(recipe, config)[source]

Problems that would make the compiled workflow invalid.

  • CONF004 — ${{ config.X }} where X is not a config key (the rewritten needs.setup.outputs.X would be empty or the setup job absent), or that sits where the needs context is unavailable.

  • DAG006 — needs: naming a job that does not exist (setup exists only when config is supplied).

  • CONF005 — a config key that is not a valid output id, or a string value holding characters the setup job cannot export unchanged.

  • DAG007 — a recipe job named setup while config is supplied: it would replace the generated setup job.

Return type:

list[RecipeProblem]

reci.recipe_checks.serialize_config_value(value)[source]

A config value as the text of a $GITHUB_OUTPUT entry.

Lists, mappings and tuples become JSON (dates inside them as ISO text); a boolean true / false; a number as the author wrote it (a TOML or YAML 3.10 stays 3.10, not 3.1). None, bytes and other objects have no faithful text and raise TypeError.

Return type:

str

>>> serialize_config_value({"d": datetime.date(2024, 1, 2), "t": (1, 2)})
'{"d": "2024-01-02", "t": [1, 2]}'
>>> serialize_config_value(True), serialize_config_value(7)
('true', '7')
>>> serialize_config_value(None)
Traceback (most recent call last):
...
TypeError: None (null) has no text form; give the key a value or remove it
reci.recipe_checks.sub_config_refs(text, repl)[source]

Substitute every config.<key> inside a ${{ ... }} in text.

repl receives the match (group(1) is the key). Only expression code is touched, never string literals inside an expression ('config.json') and never text outside ${{ }} (cat config.json). An expression that changes is re-emitted as ${{ <code> }}; the others are left verbatim.

Return type:

str

>>> sub_config_refs("${{ fromJson(config.v) }} cat config.json",
...                 lambda m: "NEEDS." + m.group(1))
'${{ fromJson(NEEDS.v) }} cat config.json'
reci.recipe_checks.unsupported_config_refs(text)[source]

The uses of the config context in text that reci cannot rewrite.

Only config.<key> maps to a setup-job output. Nested access (config.x.y), bracket access (config['x']) and the whole context (toJSON(config), config.*) have no counterpart, and would reach GitHub unrewritten, where config is no context.

Return type:

list[str]

>>> unsupported_config_refs("${{ config.a }} ${{ config.b.c }} ${{ config['d'] }}")
['config.b.c', 'config[']
>>> unsupported_config_refs("${{ toJSON(config) }} ${{ 'config' }} ${{ config.a }}")
['config']