flowpactworkflow contracts

How it works

From YAML to a data-flow graph — how flowpact finds problems that span workflows and matrix combinations.

flowpact is a static analyzer. Nothing is executed and no GitHub API is called; everything comes from the files in your repository.

 YAML files ──▶ IR ──▶ expressions ──▶ graph ──▶ matrix expansion ──▶ contracts ──▶ rules ──▶ overrides ──▶ reporters
   (yaml +    (jobs,   (@actions/    (symbols,    (exact include/    (flowpact check)  (50 rules)  (reason,  (terminal, JSON,
 positions)   steps)  expressions)    edges)      exclude rules)                               expiry)  Markdown, SARIF)

1. Load and build the IR

Every workflow and local action is parsed with exact source positions into an intermediate representation: triggers, the workflow_call / workflow_dispatch interface (inputs with type/required/default, secrets, outputs), env at every scope, jobs (needs, if, strategy.matrix, uses, with, secrets / inherit, outputs) and steps (id, uses, with, env, run).

Files are also validated against GitHub's schema with @actions/workflow-parser — the parser behind GitHub's Actions language service — and reported as FP503.

Local references are followed until closure: uses: ./.github/workflows/x.yml, uses: ./path/to/action, and owner/repo/...@ref when it points at this same repository.

2. Parse every expression

Every ${{ }} (and every bare if: condition) is parsed with GitHub's @actions/expressions into an AST. flowpact extracts typed references such as inputs.config, needs.build.outputs['version'], steps.meta.outputs.tag, matrix.os or github.event.inputs.x, each with its own line and column.

3. Build the graph

References become edges between symbols with stable ids, for example .github/workflows/test.yml#inputs.config or .github/workflows/ci.yml#jobs.build.outputs.version:

  • with: on a reusable call or local action binds a caller expression to a callee input;
  • step output → job output → workflow_call output → the caller's needs.<job>.outputs.<name>;
  • secrets: inherit is expanded to the secrets the callee tree actually reads;
  • needs, calls and uses edges describe the structure.

flowpact trace is a walk over this graph, flowpact graph draws its call structure, and flowpact lint --dump-graph graph.json writes it out.

4. Expand matrices — the part that catches silent empties

Matrices are expanded with GitHub's exact rules: cartesian product of the dimensions, exclude partial matches, then each include entry is merged into every original combination it does not conflict with — or becomes a new combination. (The implementation is checked against GitHub's documented examples and against an independent reference implementation with property-based tests.)

Every with: value and every expression in the job is then evaluated once per combination with a partial evaluator that implements GitHub's operators and functions (||, &&, ==, format, contains, fromJSON, …). A missing matrix key evaluates to null and carries a taint. If that taint survives into a value — and is not neutralized by a fallback such as matrix.config || 'default' — the binding is reported with the exact combinations (FP401, FP402).

Values that depend on runtime state (github.sha, fromJSON(needs.setup.outputs.matrix)) evaluate to unknown and are never guessed at; runtime-computed matrices are reported as unverifiable (FP403) unless you declare their keys under matrixShapes.

5. Compare with the contracts (flowpact check)

In check mode, flowpact builds a contract for every workflow and local action from the graph — interface, calls, local action uses and consumers — and compares it with the committed file in .github/flowpact/. The comparison is semantic: each difference is classified as breaking or non-breaking, and missing, orphaned and unreadable contract files are recorded. flowpact generate uses the same comparison to decide what to write.

6. Run rules

Rules are small functions over the graph (and, in check mode, the contract comparison). Each finding carries a code, severity, message, primary location, related locations (the call chain, outermost first), affected combinations, the symbol id, why/fix text and a docs URL — plus a stable fingerprint that survives line shifts. Built-in rules and plugin rules run the same way.

Findings outside the paths you asked for, or matching ignore patterns, are dropped after analysis, so cross-file rules still see the whole picture.

7. Apply overrides

Each remaining finding is matched against the overrides in the config by rule and symbol or file. Accepted findings move to a separate suppressed list with the reason that accepted them. Then a last set of rules checks the overrides themselves: expired (FP901, the findings come back), unused (FP902) and expiring soon (FP903).

8. Report

The same result is rendered for the terminal, as JSON, Markdown or SARIF, and the exit code is derived from the counts and --fail-on.

What flowpact deliberately does not do

  • Guess runtime values. Anything that depends on events, previous job results or scripts is unknown, so flowpact stays quiet rather than produce false positives.
  • Analyze other repositories. Calls to reusable workflows in other repositories are reported as unverified (FP603).
  • Replace actionlint. actionlint checks single files in depth (shell scripts, action inputs of marketplace actions, runner labels). Run both — see Comparison.

On this page