FerrFlow now publishes your packages to registries declaratively. Instead of wiring a postPublish shell hook that shells out to cargo publish, npm publish, or docker buildx, you describe the intent in ferrflow.json and FerrFlow runs it after the GitHub Release is created — per package, idempotently, and crash-resume safe.

Six publisher kinds are supported:

  • cargocargo publish to crates.io or a private registry, with allowDirty and noVerify.
  • npmnpm publish with a scoped, throwaway .npmrc so your project's .npmrc is never touched.
  • dockerdocker buildx build --push with multi-arch ({version}/{major}/{minor}/latest tag templates), and optional sign: sigstore (cosign).
  • helmhelm package + helm push to an OCI registry.
  • github-release-asset — attach a sidecar file (SBOM, signature, …) to the release with --clobber.
  • webhook — POST to Slack/Discord/your own endpoint with {name}, {version}, {tag} and {env:NAME} interpolation.
{
  "workspace": {
    "registries": {
      "kellnr": { "tokenEnv": "CARGO_REGISTRIES_KELLNR_TOKEN" }
    }
  },
  "package": [{
    "name": "my-crate",
    "path": "crates/my-crate",
    "publishers": [
      { "kind": "cargo", "registry": "kellnr", "noVerify": true }
    ]
  }]
}

Registry tokens are referenced by env-var name (tokenEnv) — the token value never lives in the config file, so ferrflow.json stays safe to commit. Every command publisher also takes an args array, a verbatim escape hatch for flags FerrFlow doesn't model natively (--locked, --provenance, --build-arg, …).

Publishers are idempotent: an already-published version is detected and skipped, so a re-run after a partial failure picks up exactly where it left off. The block is additive — your existing postPublish hooks keep working while you migrate. See the publishers documentation for the full reference.