flowpact

Configuration

Configure severities, limits, ignored paths, overrides, matrix shapes, generated files and plugins in .github/flowpact/flowpact.config.yml, and share defaults across repositories with a base config.

flowpact works without configuration. To change behaviour, create .github/flowpact/flowpact.config.yml (or pass --config <file>):

.github/flowpact/flowpact.config.yml
# yaml-language-server: $schema=https://ciatlas.github.io/flowpact/schemas/config/v1.json
version: 1

# owner/repo of this repository, so `uses: owner/repo/.github/workflows/x.yml@ref` resolves locally.
# Detected from GITHUB_REPOSITORY or .git/config when omitted.
repository: acme/platform

rules:
  # by code or by name; values: error | warning | info | off
  FP104: error
  secrets-inherit: off
  needs-without-data: info   # opt-in rule

limits:
  nestingDepth: 10   # FP602: workflows in one call chain, counting the top-level workflow
  maxInputs: 30      # FP605: workflow_call inputs before suggesting to group them

ignore:
  - .github/workflows/experimental/   # path prefix
  - .github/workflows/legacy-*.yml    # glob (* within a segment, ** across)

overrides:
  - rule: unused-input
    target: .github/workflows/pipeline.yml#inputs.legacy-flag
    reason: Read by the external release dispatcher; remove after the migration (JIRA-123)
    expires: 2026-12-31
    owner: '@platform-team'

matrixShapes:
  .github/workflows/tests.yml#e2e:
    keys: [os, browser]

generated:
  include: [.github/workflows/rendered-*.yml]   # generated, but without a header comment
  exclude: [.github/workflows/docs.yml]         # written by hand, despite its header

plugins:
  - ./ci/flowpact-rules.mjs

The config is validated strictly: unknown keys and invalid values are reported with their path (exit code 2). An unknown code with the built-in prefix (FP and a digit, such as FP999) or a typo of a loaded rule is an error too, with a suggestion; other unknown rule names are ignored with a warning, since they may belong to a plugin that is not loaded.

Editor autocomplete

The schema is published at /schemas/config/v1.json. The comment on the first line of the example enables completion, hover docs and validation in editors that use the YAML language server (VS Code with the Red Hat YAML extension, JetBrains IDEs, Neovim):

# yaml-language-server: $schema=https://ciatlas.github.io/flowpact/schemas/config/v1.json

Overrides

rules: and ignore: change a rule or a path everywhere. An override accepts one specific finding, with a reason that stays next to it in the repository:

overrides:
  - rule: unused-input                                        # code (FP104) or name
    target: .github/workflows/pipeline.yml#inputs.legacy-flag  # the symbol the finding is about
    reason: Read by the external release dispatcher; remove after the migration (JIRA-123)
    expires: 2026-12-31                                       # optional, YYYY-MM-DD
    owner: '@platform-team'                                   # optional

  - rule: remote-unverified
    file: .github/workflows/release.yml    # every finding of this rule in this file
    reason: Calls into acme/shared-workflows are verified in that repository
KeyRequiredDescription
ruleyesRule code or name. See Rules that are not loaded for names no loaded rule has.
targettarget or fileThe finding's symbol id. * matches within a path segment, ** across segments, e.g. .github/workflows/*.yml#inputs.legacy-*.
filetarget or fileThe file the finding is reported in: a path, a path prefix or a glob.
reasonyesWhy the finding is accepted (at least 10 characters).
expiresnoYYYY-MM-DD (a UTC date). The override is honored through the end of that day in UTC; from the next day the finding is reported again.
ownernoWho owns the exception, shown in expiry messages.

Either target or file is required, so an override cannot silence a rule everywhere (use rules: for that). When both are set, both must match.

Finding the symbol. Every finding has a symbol in JSON output — copy it into target:

flowpact lint --format json | jq -r '.findings[] | "\(.code) \(.symbol)"'

What happens to accepted findings. They are removed from the report and from the error/warning/info counts, so they do not fail the build. They are not lost: JSON output lists them under suppressed, each with the override that accepted it (index, reason, expires, owner), and summary.suppressed counts them.

Keeping overrides honest. Three rules check the overrides themselves (they run after overrides are applied, so they cannot be overridden):

CodeSeverityReported when
FP901 override-expirederrorThe expires date has passed. The findings it accepted are reported again.
FP902 override-unusedwarningThe override matches no finding — the problem was fixed, or the target is misspelled.
FP903 override-expiring-sooninfoIt expires within 14 days and still matches findings.

Overrides apply to every rule, including the contract rules — but an intended interface change is better recorded by regenerating the contracts than by overriding FP803.

Matrix shapes

A matrix computed at runtime (matrix: ${{ fromJSON(needs.setup.outputs.matrix) }}) cannot be expanded statically, so by default flowpact reports it as unverifiable (FP403) and does not check its matrix.* reads. If you know which keys the generated matrix has, declare them:

matrixShapes:
  # <workflow path>#<job id>
  .github/workflows/tests.yml#e2e:
    keys: [os, browser, shard]

flowpact then checks every matrix.* read in that job against the declared keys and reports unknown ones as FP404 (for example matrix.brower); FP403 is no longer reported for the job. The values of the keys stay unknown, so per-combination checks (FP401, FP402) do not apply. The declaration is used only for jobs whose matrix (or include) is actually computed at runtime.

Generated files

Some workflows are written by tools rather than people: GitHub Agentic Workflows compiles *.md files into *.lock.yml workflows, and some repositories render their workflows from templates. Editing such a file does not fix anything — the next generation overwrites it — so flowpact still checks how it calls other units and how other units call it, but does not report on its internals.

A workflow or action file counts as generated when its name ends in .lock.yml, or when the comment block at the top of the file (within its first 10 lines) says so in a generator's words:

  • DO NOT EDIT or DO NOT MODIFY in capitals (DO NOT EDIT., DO NOT EDIT MANUALLY), or do not edit this file, do not edit manually, do not edit by hand;
  • @generated;
  • This file was generated, This workflow is automatically generated, This is an auto-generated file;
  • a comment that starts with Generated by <tool>, Generated from <file>, Code generated by <tool> or Auto-generated file.
.github/workflows/triage.lock.yml
# This file was automatically generated by gh-aw. DO NOT EDIT.
name: Triage

A comment that only talks about generated things does not count: # Generated docs are published by this workflow, # Builds the autogenerated API client or # Please do not edit the deploy keys leave the file hand-written. flowpact lint -v lists the files it treats as generated, and why. When it guesses wrong, say so in the config; paths and globs match the file (.github/actions/x/action.yml) or, for an action, its directory:

generated:
  include:                                # generated, though nothing in the file says so
    - .github/workflows/rendered-*.yml
  exclude:                                # written by hand, whatever the header or name says
    - .github/workflows/docs.yml

A file listed under exclude is never treated as generated, even when it also matches include.

In a generated file, these rules are not reported — dead declarations and style advice whose only fix is an edit to the file:

CodeName
FP104unused-input
FP105optional-input-no-default-in-condition
FP203unused-secret
FP204secrets-inherit
FP303unused-output
FP304step-output-never-written
FP604needs-without-data
FP605large-interface
FP607unreferenced-reusable-workflow

Every other rule still applies: invalid YAML or expressions, schema errors, and every check of a call between units — a generated workflow that calls one of your reusable workflows without a required input (FP101), or a hand-written workflow that passes an unknown input to a generated one (FP102). Values that a hand-written unit receives from or hands to a generated one are traced as usual. To drop every finding in generated files, ignore them:

ignore:
  - .github/workflows/*.lock.yml

The report counts the findings it leaves out (summary.skippedInGenerated in --format json, and a line under the summary). An override written for one of them, from before flowpact skipped generated files, now matches nothing: FP902 says so, and the override can be deleted.

Plugins

Organization-specific rules can be loaded from JavaScript modules:

plugins:
  - ./ci/flowpact-rules.mjs

Paths are relative to the repository root. A module default-exports a rule or an array of rules (or exports them as rules). Plugin rules need their own code prefix and a docsUrl; see Custom rules for the rule format. Plugins are loaded by flowpact lint, flowpact check and flowpact generate, and by the GitHub Action.

Plugins run code from the repository

Loading a plugin executes it, with the permissions of whoever runs flowpact. Only run flowpact with plugins on code you trust. The GitHub Action skips plugins on pull_request_target and workflow_run events unless its plugins input is set to true; the CLI skips them with --no-plugins.

Rules that are not loaded

rules: and overrides: entries for a rule that is not loaded in a run are ignored for that run and listed in a warning. That happens when plugins are skipped, or when a repository configures an organization rule that only its organization's wrapper loads: the entry takes effect in the wrapper, and plain flowpact lint or the editor ignore it. What is still a config error, so a typo is not hidden:

  • a code with the built-in prefix (FP999, FP-101), or a built-in code with a character replaced (FPl01);
  • a code one edit from a loaded code with the same prefix (ACME610 beside a loaded ACME601);
  • another spelling of a loaded rule's name (unusedInput), or one at most two edits from it (unused-inptu).

A code with another prefix (AC201, DP201) or a name that adds a word to a loaded one (no-secrets-inherit) belongs to a plugin and is ignored with the warning.

Base config

A base config holds defaults for many repositories, such as an organization's. It is passed with the CLI's --base-config <file> or the action's base-config input, usually by an organization's copy of the action or wrapper, and each repository's own config goes on top of it:

acme.base.yml (in the wrapper)
rules:
  FP105: error             # stricter than the default everywhere
  needs-without-data: info # an opt-in rule, on for everyone
  acme-deploy-env: error   # an organization rule, loaded with --plugin
ignore:
  - .github/workflows/generated/
impact:
  declaredBy: labels
.github/flowpact/flowpact.config.yml (in one repository)
rules:
  FP105: warning           # this repository wins
ignore:
  - .github/workflows/legacy-*.yml
flowpact lint --base-config acme.base.yml
KeyIn a base configCombined with the repository's
rules, limits, impactallowedkey by key; the repository's value wins (impact.types and impact.labels entry by entry)
ignore, generatedallowedboth lists apply
repository, overrides, matrixShapes, impact.publishconfig errorthey name files of one repository
pluginsconfig errorplugins run code; organization rules load with the action's plugin input or --plugin

The repository's config still works on its own: run without the base config, as locally and in the editor, flowpact uses the repository's config and the defaults. The base config's path is relative to the working directory (--base-config) or to the action's own directory (the base-config input), and the banner shows it (config .github/flowpact/flowpact.config.yml · base acme.base.yml). A repository's config cannot point at a base config itself; see why.

Reference

KeyTypeDefaultDescription
$schemastring—JSON Schema reference for editors (the # yaml-language-server comment works too).
version11Config schema version.
repositoryowner/repodetectedLets same-repository owner/repo/...@ref references resolve locally.
rulesmap of code or name → error | warning | info | off{}Severity per rule.
limits.nestingDepthpositive integer10Maximum workflows in one call chain (FP602).
limits.maxInputspositive integer30workflow_call inputs before FP605.
ignorelist of paths or globs[]Findings in matching files are suppressed.
overrideslist of {rule, target?, file?, reason, expires?, owner?}[]Accepted findings, each with a reason and an optional expiry date (details).
matrixShapesmap of <workflow path>#<job id> → { keys: [...] }{}Declared keys of runtime-computed matrices (details).
generated.includelist of paths or globs[]Workflows and actions that are generated, even without a marker (details).
generated.excludelist of paths or globs[]Workflows and actions written by hand, even with a marker or a .lock.yml name; wins over include.
impact.publishlist of paths or globsreusable workflows (except _-prefixed files) and the root action.ymlUnits other repositories use (impact mode). Their outputs are not reported as unused (FP303); list actions in subdirectories (owner/repo/restore@v1) here too.
impact.declaredByexplicit | title | labelsexplicit when given, else titleThe authoritative source of the declared impact.
impact.labels{ major, minor, patch, none }semver:*Label names that declare each level.
impact.typesmap of type → levelfeat: minor, fix / perf: patchConventional Commits types; others declare none, ! declares major.
impact.uncertainwarn | failwarnWhether changes flowpact cannot fully resolve count towards the required impact.
pluginslist of paths[]JavaScript modules exporting extra rules (details).

On this page