Plugins
A FlowWright plugin is a package that exports plain functions. There is no registry,
no lifecycle hooks, nothing to enable in pipeline.ts, and no flow plugin install.
You import a function and call it.
Everything a plugin produces goes through the same recording pass your own code does,
so flow explain shows no difference between a plugin's stages and hand-written ones.
If a helper doesn't fit, stop using it and write the stages yourself — that's the same
amount of code, just longer.
The seven packages
| Package | For |
|---|---|
plugin-std | Workspace housekeeping — clean, delete, write files |
plugin-git | Cloning, checking out, tagging, pushing |
plugin-cache | Wrapping a stage body in a content-keyed cache |
plugin-artifacts | Declaring build outputs |
plugin-docker | Building, tagging and pushing images |
plugin-node | A whole Node CI lifecycle |
plugin-python | A whole Python CI lifecycle |
Want X → use Y
| You want to | Use |
|---|---|
| Check out a repo at the start of a run | checkoutStage() — git |
Cache node_modules between runs | nodeStages() does it; standalone, cachedStage() — cache |
| Build and push an image | dockerStages() — docker |
| A standard install → lint → test → build for a Node repo | nodeStages() — node |
| The same for Python | pythonStages() — python |
Declare dist/ as an output | archiveStage() — artifacts — or just produces |
| Empty the workspace before a step | cleanWs() — std |
Two kinds of export
Every function in these packages is one of two shapes, and they're used in different places:
Step helpers are async functions you call inside a stage body. They record
steps, exactly like sh.
stage("Clean", async () => {
await cleanWs();
});
Stage builders are called at pipeline level and return StageDefinition[]
(or a single one) to spread into stages.
stages: [checkoutStage("git@github.com:acme/api.git"), ...nodeStages()];
Composing: group()
Plugin stage ids are fixed — install, lint, docker-build. Use two plugins that
both emit an install, or the same plugin twice, and they collide. group()
namespaces them:
import { pipeline, group } from "@flowwright/core";
import { nodeStages } from "@flowwright/plugin-node";
export default pipeline({
name: "monorepo",
stages: [...group("api", nodeStages()), ...group("web", nodeStages())],
});
That yields api-install, api-lint, …, web-install, and rewires each group's
internal needs. See Dependencies — and note that
group() namespaces ids only, not cache keys or produces paths.
Rejected option combinations
An option a configuration can't honor throws at plan time rather than silently doing nothing:
| Passed | Package | Why |
|---|---|---|
artifactPath with build disabled | node, python | nothing would be produced |
cache: true with install: false | node, python | there is nothing to cache |
cache: true with packageManager: "pip" | python | pip has no in-project venv |
push or login to dockerBuild() | docker | those build the other two stages |
name to nodeStages()/pythonStages() | node, python | only the *ServicePipeline wrappers read it |
Only an explicitly passed value counts. A default the configuration can't use is
narrowed quietly — pythonStages({ packageManager: "pip" }) doesn't throw just
because cache defaults to true.
Where the plugins differ
These are worth knowing before you assume the set is uniform. Each is only visible by comparison, which is why they're here rather than on the individual pages.
node and python are not symmetric
plugin-node | plugin-python | |
|---|---|---|
build default | true | false |
| Pin a toolchain version | nodeVersion | — |
| Version matrix | nodeVersions | — |
| Audit stage | audit | — |
artifactPath default | none | "dist" |
| How tools run | scripts, via <pm> run <script> | tools directly, or via uv run / poetry run |
Three cache-key formats, and node/python don't use plugin-cache
| Producer | Key format |
|---|---|
plugin-cache | <slug(path)>-<hash(keyFiles)> |
plugin-node install | <pm>-deps-<hash(lockfile)> |
plugin-python install | <pm>-deps-<hash(lockfile)> |
nodeStages() and pythonStages() call the @flowwright/core cache primitives
directly and hand-roll their key — they don't go through plugin-cache. The practical
consequence: a cachedStage({ path: "node_modules" }) and nodeStages() cache the
same directory under different keys, so they never share and never collide.
Four credential declaration styles
| Style | Used by |
|---|---|
{ id, as: "sshPrivateKey", env } | git checkoutStage |
{ id, as: "usernamePassword", usernameEnv, passwordEnv } | docker login |
{ id, as: "secretText", env } | python publish |
"npm-token" — a bare string | node publish |
All four are valid — see Credentials. The bare string is
the odd one out: it means secretText bound to the derived env name, which for
"npm-token" is NPM_TOKEN.
Only node and python bundle a whole pipeline
nodeServicePipeline() and pythonServicePipeline() return a complete
PipelineDefinition. Everything else gives you stages to compose.
Installing
Each package is published independently and takes @flowwright/core as a peer:
npm install --save-dev @flowwright/plugin-node
plugin-std pulls in plugin-git, since it re-exports the checkout helpers.