club publish --auto
Default choice for releases. One command from picker to last package on the server. Source files never modified.
dart pub publish cannot publish a package that has path: dependencies
on other packages in the same repository. Validators reject path deps
because they cannot be resolved by anyone outside the workspace — the
package wouldn’t be installable.
For monorepos and pub workspaces, the standard fix is tedious: rewrite each pubspec by hand to use hosted references, publish in dependency order, then revert. Club ships two commands that automate this:
club prepare — rewrites the on-disk pubspecs in
topological order, but does not publish.club publish --auto — does the same rewriting
virtually in-memory and publishes every package in one run, with
source files left exactly as they were before.This guide is the end-to-end story.
club publish --auto
Default choice for releases. One command from picker to last package on the server. Source files never modified.
club prepare
Use when you want the rewrites to land on disk first — for review,
commit to a release branch, or because your CI publishes
package-by-package via dart pub publish rather than club’s flow.
Both commands share the same discovery, dependency graph, conflict resolution, and tree visualisation. They differ only in what they do with the planned rewrite:
prepare writes to disk.publish --auto keeps it in memory and uses it for the upload only.A monorepo with internal deps is modelled as a directed acyclic graph (DAG). The canonical example used throughout this guide:
┌─────────────────────┐ │ core_interface │ v1.0.0 (no internal deps) └──────────┬──────────┘ │ depended on by ▼ ┌─────────────────────┐ │ core │ v1.2.0 └──────────┬──────────┘ │ depended on by ▼ ┌──────┴──────┐ ▼ ▼ ┌─────────┐ ┌─────────┐ │ pkg_a │ │ pkg_b │ ◀ user-selected targets │ v0.5.0 │ │ v0.3.0 │ └─────────┘ └─────────┘
Cross-edges (not drawn): pkg_a → core_interface (also a direct dependency) pkg_b → core_interface (also a direct dependency)pkg_a and pkg_b are the leaf packages the developer wants on
the server. core is an internal dependency they share. core_interface
is a deeper internal dep that core, pkg_a, and pkg_b all consume.
Three levels, five edges total.
When you target pkg_a and pkg_b:
Walk the workspace, find every pubspec.yaml with a name: field.
Identify which discovered packages each target package depends on (transitively). The closure includes the targets and every internal dep.
Topologically sort the closure — leaves first, dependents last. In
the example above, that yields:
core_interface, core, pkg_a, pkg_b.
For each non-leaf package, rewrite its dependencies /
dev_dependencies entries that point at workspace members so they
become hosted references against the target server:
# beforecore_interface: path: ../core_interface
# aftercore_interface: hosted: https://club.example.com version: ^1.0.0Publish each package in topological order. core_interface first,
then core (which now references core_interface ^1.0.0 from the
server), then pkg_a and pkg_b.
Topological order is critical: by the time package N is uploaded, every
hosted dep it references is already on the server, so the validator’s
dart pub get step can resolve them.
Both path: deps and hosted-by-name deps are detected and
rewritten:
dependencies: core: path: ../coreDetected because the path resolves to another discovered package.
dependencies: core: ^1.0.0 # No `path:` — but pub workspace shadowing # resolves this locally.Detected because the dep name matches a discovered package. Pub workspaces shadow hosted deps with workspace members at resolve time, so the dep would resolve locally during development. The rewrite makes it explicit and pins the version.
dependencies: core: hosted: https://old.example.com version: ^1.0.0Detected because the dep name matches a discovered package. The
hosted: URL is corrected to the resolved target server during
rewrite — useful when the registry URL has changed.
dependency_overrides is intentionally never rewritten. That
section is local-only and should not appear in published pubspecs.
External path: dependencies (paths that resolve outside the discovered
set) are surfaced as a hard error before anything happens. That matches
the DependencyValidator behaviour in standard club publish.
Before any rewrite is computed, both commands query the server for every package’s current published versions. For each package whose local pubspec version is already on the server, you have three options:
| Option | What it does |
|---|---|
| Overwrite | Force-publish, replacing the existing version. The on-the-wire upload sets force=true. |
| Skip | Reuse the already-published version as-is. The skipped package is not rewritten/republished; dependents still rewrite to point at the existing published version. |
| Abort | Exit cleanly without publishing or writing anything. |
Without flags, both commands prompt per conflict via an arrow-key
picker. Pass --on-conflict <mode> to make the choice global:
# Force overwrite all conflicts (for example: re-publishing a known-good build)club publish --auto --on-conflict overwrite
# Reuse the existing version anywhere it's already on the serverclub publish --auto --on-conflict skip
# Fail fast if any version is already on the server (best for CI)club publish --auto --on-conflict abort
# Per-conflict prompt (default)club publish --auto --on-conflict promptThe two commands share the same dependency analysis. They differ in when and where the rewrite lands:
Source pubspec.yaml state ───────────────────────────────── before during after run
club publish --auto unchanged → unchanged → unchanged (rewrite is in-memory only; uploaded tarball carries it)
club prepare unchanged → modified → modified (rewrite is written to disk; committable artifact)
club prepare + manual revert unchanged → modified → unchanged (workflow: prepare, publish per package, then `git restore`)club publish --auto (recommended)One command, source files never touched, no commit churn:
$ club publish --auto pkg_a pkg_b🚀 club publish --auto ...Publish 4 packages to https://club.example.com? [y/N] y[1/4] Publishing core_interface … 🎉[2/4] Publishing core … 🎉[3/4] Publishing pkg_a … 🎉[4/4] Publishing pkg_b … 🎉
🎉 4 packages publishedThe rewrites happen entirely inside the uploaded tarballs:
dart pub get keeps resolving locally throughout because pub
workspace shadowing handles the on-disk path/hosted-by-name deps.club prepare then club publish per packageUse this when the rewritten pubspecs are an artifact you want to commit or review:
# 1. Rewrite the on-disk pubspecs in topological order.club prepare pkg_a pkg_b
# 2. Inspect the diff.git diff packages/
# 3. Commit (and tag, if you're cutting a release).git add packages/*/pubspec.yamlgit commit -m "Prepare release"
# 4. Publish each package — order doesn't matter at this point because# the pubspecs all reference hosted versions.cd packages/core_interface && club publish -fcd ../core && club publish -fcd ../pkg_a && club publish -fcd ../pkg_b && club publish -fYou can also use --auto against a workspace that has already been
prepared — it simply detects no rewrites are needed and runs the
publish loop in order.
Neither command bumps versions for you. Pick one of:
Edit version: in each package’s pubspec, commit, then run --auto.
Suitable for small monorepos where releases are infrequent.
Use a Git tag as the canonical version. In CI:
- name: Set versions from tag run: | VERSION="${GITHUB_REF_NAME#v}" for f in packages/*/pubspec.yaml; do sed -i "s/^version:.*/version: $VERSION/" "$f" done- name: Publish run: club publish --auto --force --on-conflict abortAll packages get the same version. --on-conflict abort ensures the
run fails if a version was forgotten.
Each package has an independent version, bumped per change. Use a
changeset tool (e.g.
melos) to manage the versions, then
club publish --auto to publish whichever packages had bumps.
--on-conflict skip is the right setting here — packages that
weren’t bumped will already be published, and skipping them is what
you want.
name: Releaseon: push: tags: ['v*']jobs: publish: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - uses: dart-lang/setup-dart@v1 - name: Install club CLI run: curl -fsSL https://club.birju.dev/install.sh | bash - name: Publish monorepo run: | club publish --auto \ --force \ --on-conflict abort \ --server myclub.birju.dev env: CLUB_TOKEN: ${{ secrets.CLUB_TOKEN }}publish: rules: - if: $CI_COMMIT_TAG image: dart:stable script: - curl -fsSL https://club.birju.dev/install.sh | bash - club publish --auto --force --on-conflict abort --server myclub.birju.dev variables: CLUB_TOKEN: $CLUB_TOKENBefore pushing, gate on a successful dry-run:
club publish --auto --dry-run --on-conflict abortExits zero if every package is publishable. Non-zero on:
abort mode)CLUB_TOKEN shadows any stored credential — set it from a CI secret
and you don’t need a separate club login step.
Both commands measure each package’s tarball before any write or upload happens. This catches the most common monorepo gotcha — a package whose archive exceeds the server’s size limit (default 100 MiB) — before the run gets halfway through.
The size shows up inline in the dependency tree:
▶ [3] pkg_a ◀ target 0.5.0 ✓ publish · 24.1 KiB depends on ↑ core ^1.2.0, core_interface ^1.0.0A package whose tarball would fail the size check is flagged here, with
no partial publish having taken place. Re-run with --no-tree to see
the same info as a flat table instead.
<name>: dependencies.<dep> is a path dependency pointing outside the workspaceA path: dep resolves to a directory that’s not in the discovered set.
Either move that target into the workspace, or replace the dep with a
hosted reference manually.
<name> has no version field in pubspec.yamlA package referenced as a rewrite target has no version: in its
pubspec. Add one — even private packages need a version for downstream
constraints to resolve.
Dependency cycle: a -> b -> aThe dep graph contains a cycle. Replace one of the path deps in the cycle with a hosted reference (or refactor the cycle out).
Version conflicts detected but stdin is non-interactiveYou ran in CI without --on-conflict. Pass --on-conflict <mode>
explicitly so the behaviour is deterministic.
--auto aborts immediately and prints the packages already on the
server. Re-running is safe:
--on-conflict skip to bypass the already-published packages
and pick up where the chain left off.--on-conflict overwrite if you want to re-publish them too.