Build on flowpact
Run flowpact under your organization's policies — a copy of the action, a plain run step, your own action or a wrapper — with the interfaces that stay stable, organization defaults and rules, and keeping the version current.
Organizations that cannot use a third-party action, or that want their own defaults, rules and reports, can run flowpact their way without forking it: copy the action, run the CLI in a step, write your own action on the API, or write a composite action. The organization owns the policy (rules, when to fail, where reports go); flowpact stays unmodified, verified, and updated with a version bump.
What is stable
Build on these. A release that changes one in an incompatible way says so under Breaking changes in its release notes, with upgrade notes. Before 1.0, that can happen in a minor release (0.8 → 0.9), never in a patch release.
| Interface | Reference |
|---|---|
| CLI commands, flags and exit codes | CLI |
| JSON report | /schemas/report/v1.json; an incompatible change makes it v2, and meta.schemas.report says which a report follows |
| SARIF | SARIF 2.1.0, with stable fingerprints in partialFingerprints["flowpact/v1"] |
| Annotations | --format github: GitHub workflow commands |
| Config and contracts | /schemas/config/v1.json, /schemas/contract/v1.json |
| Rule codes and names | Rules; configs, overrides and --only refer to them |
| Plugin rules | The rule object in Custom rules |
| GitHub Action inputs and outputs | GitHub Action |
API: the functions, their options, results and errors (import { lint } from 'flowpact') | API |
Not stable: the terminal output and the wording of messages and Markdown reports, which are written for people (parse
JSON or SARIF instead); the types of the data-flow graph a plugin rule reads through ctx.index; the @flowpact/*
packages inside the repository; and @ciatlas/core, the workflow model they are built on. None of them is published
(use the API).
Copy the action
The flowpact action is a self-contained JavaScript action (node24): it uses no other actions and installs nothing
when it runs. Each release has it as an archive,
flowpact-action-X.Y.Z.tar.gz: action.yml, the bundle, its license notices and SBOM. Copy it into a repository of
your organization, where your action policy allows it:
gh release download vX.Y.Z --repo ciatlas/flowpact --pattern 'flowpact-action-X.Y.Z.tar.gz'
gh attestation verify flowpact-action-X.Y.Z.tar.gz --repo ciatlas/flowpact
tar -xzf flowpact-action-X.Y.Z.tar.gz -C acme-flowpact # a clone of a new repositoryGive the copy a repository of its own: the archive has flowpact's LICENSE at its root, which stays with the copy and
would replace a LICENSE already there.
Add your defaults and rules next to it, and make them the defaults of
its base-config and plugin inputs (0.10.0 and later): change the default: '' those two inputs already have in
action.yml. Relative paths are read from the action's own directory:
acme/flowpact
├── action.yml # flowpact's, with the two defaults below
├── LICENSE # flowpact's
├── packages/action/dist/ # the bundle, as released
├── acme.base.yml
└── rules/acme.mjs base-config:
description: …
required: false
- default: ''
+ default: acme.base.yml
plugin:
description: …
required: false
- default: ''
+ default: rules/acme.mjsRepositories then use uses: acme/flowpact@<commit-sha> with every input of the
GitHub Action. In a private or internal repository, first allow that under Settings → Actions → General
→ Access. An empty base-config or plugin still means your defaults, but a workflow can set them to something else,
and its plugin replaces your list rather than adding to it (list rules/acme.mjs with the workflow's own plugins).
To enforce the policy, require the workflow centrally (below). To update, copy the next
archive the same way and keep the two defaults. The action
reaches the network only in impact mode (a git fetch of the base commit) and to upload
regenerated contracts, which GitHub Enterprise Server does not support (set upload-contracts: false there).
Run the CLI in a step
A run: step is not an action, so no action policy applies to it. The CLI is bundled with everything it needs and
installs nothing; it needs only Node.js 22 or later, which GitHub-hosted runners have:
jobs:
flowpact:
runs-on: ubuntu-latest
permissions:
contents: read
steps:
- uses: actions/checkout@v7 # pin to a commit SHA
- name: flowpact
env:
GH_TOKEN: ${{ github.token }}
FLOWPACT_VERSION: 0.12.3
run: |
gh release download "v$FLOWPACT_VERSION" --repo ciatlas/flowpact \
--pattern "flowpact-$FLOWPACT_VERSION.tgz" --dir "$RUNNER_TEMP"
gh attestation verify "$RUNNER_TEMP/flowpact-$FLOWPACT_VERSION.tgz" --repo ciatlas/flowpact
tar -xzf "$RUNNER_TEMP/flowpact-$FLOWPACT_VERSION.tgz" -C "$RUNNER_TEMP"
node "$RUNNER_TEMP/package/dist/index.js" lint --format github -o markdown:"$GITHUB_STEP_SUMMARY"The step refuses to run a file whose provenance does not verify: gh attestation verify fails it, with HTTP 404: Not Found for a file that GitHub holds no attestation for. Without network
access at run time, verify the tarball once and commit the extracted package/ folder to an internal repository as
tools/flowpact/ (its own package.json keeps the code an ES module whatever package.json the repository has); the
step is then node tools/flowpact/dist/index.js lint …. Add --base-config and --plugin for your defaults and rules.
On GitHub Enterprise Cloud, an organization
ruleset
can require such a workflow in every repository, whichever of these ways it uses; the workflow must run on
pull_request, pull_request_target or merge_group, and its repository must allow access from the others.
Write your own action
A JavaScript action of your own can run flowpact through its API: lint(), check() and the other
commands as functions, which return the report and render the annotations, the job summary and SARIF. It applies your
defaults and rules like the CLI does (baseConfig, plugins), with the inputs and reports you choose, and its bundle
installs nothing when it runs. Write your own action has a complete one.
A composite action
An internal repository with a composite action that installs a pinned flowpact and runs it with your defaults. It uses
actions/setup-node and installs from npm when it runs, so where your policy allows only your organization's actions or
blocks network access, copy the action, run the CLI in a step or
write your own action instead.
acme/flowpact-action
├── action.yml
├── package.json # flowpact, pinned to an exact version
├── package-lock.json # and its integrity hash
├── acme.base.yml # optional: organization defaults (below)
└── rules/acme.mjs # optional: organization rules (below)Create package.json and the lockfile with npm install --save-exact flowpact@0.12. The flowpact package has no
dependencies of its own: everything it runs is bundled, with its license notices and an SBOM next to it.
name: Acme flowpact
description: flowpact with Acme's rules and reports
inputs:
command:
description: lint, or check when the repository commits contracts
default: lint
runs:
using: composite
steps:
- uses: actions/setup-node@v7 # pin to a commit SHA
with:
node-version: 22
- name: Install flowpact
shell: bash
run: npm ci --prefix "$GITHUB_ACTION_PATH" --ignore-scripts
- name: Run flowpact
shell: bash
env:
COMMAND: ${{ inputs.command }}
# The repository's own plugins run its code: not on events that run a fork's code with secrets.
NO_PLUGINS: ${{ contains(fromJSON('["pull_request_target", "workflow_run"]'), github.event_name) && '--no-plugins' || '' }}
run: >-
"$GITHUB_ACTION_PATH/node_modules/.bin/flowpact" "$COMMAND"
--format github
--base-config "$GITHUB_ACTION_PATH/acme.base.yml"
--plugin "$GITHUB_ACTION_PATH/rules/acme.mjs"
$NO_PLUGINS
-o markdown:"$GITHUB_STEP_SUMMARY"
-o flowpact.sarifFrom one run, this gives:
- the organization's defaults and rules, under each repository's own config;
- an annotation on the file and line of each finding, in the run and the pull request (
--format github); - the Markdown report in the job summary (
-o markdown:"$GITHUB_STEP_SUMMARY"); flowpact.sarif, forgithub/codeql-action/upload-sarifin the calling job or in the wrapper;- a failed step when there are errors (exit code
1); add--fail-on warningor--fail-on neverto change that.
Repositories use it like any action, after actions/checkout. With --root (a repository in a subdirectory of the
workspace), the paths in annotations, SARIF and Markdown links stay relative to the repository.
Without access to the npm registry, install the npm package from a release instead: each
GitHub release has flowpact-X.Y.Z.tgz with build provenance.
Verify it, commit it to the wrapper and depend on the file
(npm install --save-exact ./flowpact-X.Y.Z.tgz).
Organization defaults
A base config holds settings for every repository, and each repository's own config
goes on top of it. Pass it with the action's base-config input, the CLI's --base-config or the API's baseConfig:
rules:
FP105: error # stricter than flowpact's default
needs-without-data: info # an opt-in rule, on for everyone
prod-deploy-needs-approval: error
ignore:
- .github/workflows/generated/
impact:
declaredBy: labels # Acme declares release impact with labelsrules,limitsandimpactsettings merge key by key, and the repository's value wins: a repository can still setFP105: warning.ignoreandgeneratedlists add up.- Settings that name files of one repository (
overrides,matrixShapes,repository,impact.publish) andpluginsare config errors in a base config. - In impact mode, the base config also applies under the baseline's config.
Repositories keep a config that works on its own: locally and in the editor, without the wrapper, flowpact uses the repository's config and its defaults.
Organization rules
Write your rules as plugins and ship them with your copy of the action or your wrapper, such as
rules/acme.mjs with the prod-deploy-needs-approval rule from Custom rules:
- The action's
plugininput, the CLI's--plugin <file>and the API'spluginsload a plugin in addition to those the repository's config lists (the input reads relative paths from the action's directory, the flag and the API from the working directory; all take several). - The repository's own plugins run the repository's code, so the action skips them on untrusted events
(
plugins: auto), the CLI with--no-pluginsand the API withrepositoryPlugins: false. Your rules keep running: the action skips apluginfile only when it is in the checkout. - The base config sets how strict each rule is, and a repository can change that under
rules:or accept a finding with an override, like for built-in rules. Where the rule is not loaded (plainflowpact lint, the editor), those entries are ignored with a warning.
rules:
FP105: warning # this repository wins over the base config
overrides:
- rule: prod-deploy-needs-approval
target: .github/workflows/deploy.yml#jobs.hotfix
reason: Hotfix deploys are approved in the incident channel (INC-142)
expires: 2026-12-31
owner: '@acme/sre'Reading the results
For a wrapper that decides by itself, run with --fail-on never and read the JSON report:
flowpact lint --fail-on never --format json > flowpact.json
jq -r '.findings[] | "\(.severity) \(.code) \(.loc.file):\(.loc.line)"' flowpact.json
jq '.summary.errors' flowpact.json- Exit codes:
0no findings at or above--fail-on,1findings,2usage or config error,3internal error. fingerprintstays the same across unrelated edits, so a finding can be tracked over time or compared with a baseline.symbolnames what the finding is about; it is thetargetof an override.- Logs go to stderr and never mix with the report on stdout.
Keeping it current
Renovate updates the wrapper's npm dependency, and pinned action references wherever flowpact's own action is used:
{
"extends": ["config:recommended", "helpers:pinGitHubActionDigests"],
"packageRules": [
{
"matchPackageNames": ["flowpact", "ciatlas/flowpact"],
"groupName": "flowpact",
"minimumReleaseAge": "3 days"
}
]
}minimumReleaseAgewaits a few days after a release before proposing it.- Before 1.0, read the Breaking changes section of a minor update before merging it; patch updates can be merged
automatically (
"matchUpdateTypes": ["patch"], "automerge": true). npm audit signaturesin the wrapper's CI checks that the installed version was published from this repository's release workflow.- Dependabot works the same way, with the
npmandgithub-actionsecosystems. - Your own action gets the same update of its npm dependency; rebuild its bundle with it.
- A copy of the action or a CLI run in a step is updated by copying or
downloading the next release the same way, verified again; for the step, that is the
FLOWPACT_VERSIONline.
Why not extends
Tools such as ESLint let a config extend another file. flowpact's base config comes from the command line instead, for two reasons:
- A repository's config that extends a file in the wrapper, or in a private package, points at something that is not
there when a developer runs
flowpact lintor opens the editor, so the config would fail everywhere but in CI. Passed by the wrapper, the base config applies where the wrapper runs, and the repository's config still works alone. - The organization applies its defaults: a repository cannot leave them out by deleting a line.
Why not a fork
A fork that changes flowpact's code has to be rebuilt, re-verified and merged on every release, and its bundle no longer matches the signed release. A wrapper keeps flowpact unmodified and adds what your organization needs around it. If it needs something flowpact does not offer — a flag, an output, an extension point — open an issue.