flowpactworkflow contracts

Configuration

Configure severities, limits, ignored paths, overrides, matrix shapes and plugins in .github/flowpact/flowpact.config.yml.

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://rumankazi.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]

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

The config is validated strictly: unknown keys, unknown rule codes or names, and invalid values are reported with their path (exit code 2); misspelled rule codes and names get a suggestion.

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://rumankazi.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. An unknown rule is a config error.
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.

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.

When plugins are skipped, rules: and overrides: entries for rules that are not loaded are ignored for that run and listed in a warning. A name that starts with FP or is close to a built-in rule (unused-inptu) is still a config error, so a typo is never hidden.

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).
impact.publishlist of paths or globsreusable workflows (except _-prefixed files) and the root action.ymlUnits other repositories use (impact mode).
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