flowpact

Custom rules

The rule API and code format — how built-in rules are defined and how organization-specific rules fit in.

Every rule — built-in or not — is a single definition. That definition is the source for the runtime message, the why/fix text and docs URL printed with each finding, and (for built-in rules) flowpact explain and the generated rule pages on this site.

ci/flowpact-rules.mjs
/** Jobs that deploy to production must depend on the approval job. */
const prodNeedsApproval = {
  code: 'ACME601',
  name: 'prod-deploy-needs-approval',
  category: 'structure',
  defaultSeverity: 'error',
  docsUrl: 'https://wiki.acme.dev/ci/ACME601',
  docs: {
    summary: 'Jobs that deploy to production must depend on the approval job.',
    why: 'Production deploys without the approval gate bypass change management.',
    fix: 'Add `approve` to the job’s `needs:`.',
  },
  check(ctx) {
    for (const wf of ctx.index.project.workflows.values()) {
      for (const job of Object.values(wf.jobs)) {
        const deploysProd = job.with.environment?.value === 'production';
        if (deploysProd && !job.needs.some((n) => n.id === 'approve')) {
          ctx.report({
            message: `jobs.${job.id} deploys to production without needing "approve"`,
            loc: job.loc,
            symbol: `${wf.path}#jobs.${job.id}`,
          });
        }
      }
    }
  },
};

export default [prodNeedsApproval];

A rule is a plain object, so a plugin needs no imports. Built-in rules are written the same way in TypeScript, wrapped in defineRule(...) for type checking. A plugin written in TypeScript gets the RuleDefinition type from the flowpact package (see TypeScript in the API).

Besides message, loc and symbol, ctx.report takes fix (advice for this finding instead of the rule's) and severity, which reports one finding below the rule's severity. It can only lower it: a finding never exceeds the severity the rule is configured with.

Two optional fields shape where and how a rule speaks:

  • generatedFiles: 'skip' drops the rule's findings located in generated files (gh-aw *.lock.yml, a DO NOT EDIT header). Use it for rules about a file's own internals, such as dead declarations or style; the default, 'report', suits checks of calls between units.
  • docs.scope describes what the rule deliberately leaves out. It appears in flowpact explain (and on the built-in rule pages), but not with each finding.

Codes

Codes have the form <PREFIX><category digit><two digits>:

DigitCategoryDigitCategory
1inputs6structure
2secrets7hygiene
3outputs8contracts
4matrix9config
5expressions

The registry enforces the format and that the digit matches the declared category, rejects duplicate codes and names, reserves the FP prefix for built-in rules, and requires third-party rules to bring their own docsUrl.

What a rule can use

ctx.index is the data-flow graph:

  • ctx.index.units(), project.workflows, project.actions — the IR with exact locations;
  • callSites, callersOf(path), actionUses, usersOf(path) — who calls whom;
  • usagesOf(symbol) — every place a symbol is read, with the location of the reference and where the value flows next;
  • ctx.matrix(unit, job) — the expanded matrix (combinations, keys, exactness);
  • ctx.config — the resolved configuration;
  • ctx.contracts — the contract comparison (only in flowpact check).

ctx.report() takes the message, loc and optional symbol, related locations, combos and an instance-specific fix. A milder variant of the problem can pass severity: 'warning' or 'info': it lowers that finding below the rule's severity, never raises it, and a lower severity set in the config still wins.

Loading plugins

List the module under plugins: in the config:

.github/flowpact/flowpact.config.yml
plugins:
  - ./ci/flowpact-rules.mjs

rules:
  prod-deploy-needs-approval: warning   # plugin rules are configured like built-in ones
  • Paths are relative to the repository root; the module is loaded with Node's import(), so it must be JavaScript (.mjs, or .js in an ESM package).
  • It default-exports a rule or an array of rules, or exports them as rules.
  • The CLI also loads plugins given with --plugin <file> (relative to the working directory, repeatable), and the action those in its plugin input (relative to the action's directory), such as organization rules shipped with a copy of the action. --no-plugins and the action's plugins input skip the plugins the config lists, but not those.
  • flowpact lint, flowpact check and the GitHub Action run plugin rules next to the built-in ones, so they can be configured under rules:, accepted with overrides and selected with --only.
  • A plugin that is missing, fails to load, exports no rules, or registers an invalid rule (wrong code format, FP prefix, no docsUrl, duplicate code or name) stops the run with exit code 2.

Each finding links to the rule's docsUrl, so point it at your own documentation.

On this page