flowpactworkflow contracts

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.

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).

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.
  • 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