club publish --auto
club publish --auto extends club publish with a
multi-package mode designed for Dart monorepos and pub workspaces. In one
command it:
- Discovers every publishable package in the workspace.
- Lets you pick one or more leaf targets (or accepts them as positional args).
- Computes the transitive closure of internal dependencies.
- Resolves each package’s local-vs-server version state.
- Rewrites every internal-dep entry to a hosted reference in memory.
- Publishes every package in topological order, in a single run.
The single most important guarantee:
Example workspace
Every example on this page uses the same hypothetical 4-package monorepo so the terminal output maps to a concrete dep graph:
┌─────────────────────┐ │ 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 and pkg_b also depend directly oncore_interface. `--auto` detects every internal-dep shape (path:,workspace-shadowed hosted-by-name, and explicit hosted) and rewritesthem all in memory.
Publish order (topological, leaves first): 1. core_interface → 2. core → 3. pkg_a + 4. pkg_bFilesystem layout:
my_monorepo/├── pubspec.yaml # workspace umbrella (skipped — no version)└── packages/ ├── core_interface/pubspec.yaml # name: core_interface version: 1.0.0 ├── core/pubspec.yaml # depends on core_interface ├── pkg_a/pubspec.yaml # depends on core, core_interface └── pkg_b/pubspec.yaml # depends on core, core_interfaceQuick start
From the workspace root, with no arguments:
club publish --autoThe CLI walks you through discovery, target selection (interactive multi-select picker), version conflict resolution, a full plan preview with tarball sizes, one final confirmation, then publishes every package in dependency order.
To skip the picker and target specific leaves:
club publish --auto pkg_a pkg_bTo preview the plan without uploading:
club publish --auto --dry-runA typical successful run looks like:
🚀 club publish --auto root: /Users/me/code/my_monorepo discovered 4 packages server: myclub.birju.dev (auto-selected (only logged-in server))
Measuring tarballs ────────────────────────────────────────── measuring core_interface… measuring core… measuring pkg_a… measuring pkg_b…
Publish stack (top-to-bottom) ─────────────────────────────── ▶ [1] core_interface 1.0.0 ✓ publish · 4.5 KiB depended on by → core, pkg_a, pkg_b
▶ [2] core 1.2.0 ✓ publish · 12.3 KiB depends on ↑ core_interface ^1.0.0 depended on by → pkg_a, pkg_b
▶ [3] pkg_a ◀ target 0.5.0 ✓ publish · 24.1 KiB depends on ↑ core ^1.2.0, core_interface ^1.0.0
▶ [4] pkg_b ◀ target 0.3.0 ✓ publish · 18.7 KiB depends on ↑ core ^1.2.0, core_interface ^1.0.0
Total: 4 packages · 149 files · 59.6 KiB
Pubspec rewrites (in-memory — source files unchanged) core — packages/core/pubspec.yaml dependencies.core_interface (path) → hosted ^1.0.0 pkg_a — packages/pkg_a/pubspec.yaml dependencies.core (path) → hosted ^1.2.0 dependencies.core_interface (path) → hosted ^1.0.0 pkg_b — packages/pkg_b/pubspec.yaml dependencies.core (hosted-by-name) → hosted ^1.2.0 dependencies.core_interface (hosted-by-name) → hosted ^1.0.0
Publish 4 packages to myclub.birju.dev? (source pubspec.yaml fileswill not be modified) [y/N] y
[1/4] Publishing core_interface ─────────────────────────────…validators run, tarball uploads…🎉 core_interface 1.0.0 published
[2/4] Publishing core ───────────────────────────────────────…🎉 core 1.2.0 published
[3/4] Publishing pkg_a ──────────────────────────────────────…🎉 pkg_a 0.5.0 published
[4/4] Publishing pkg_b ──────────────────────────────────────…🎉 pkg_b 0.3.0 published
┌──────────────────────────────────────────────────────────────┐ │ 🎉 4 packages published to myclub.birju.dev │ │ ✓ core_interface │ │ ✓ core │ │ ✓ pkg_a │ │ ✓ pkg_b │ └──────────────────────────────────────────────────────────────┘What it does (vs. plain club publish)
The standard club publish is a single-package flow. Running it on a
package with path: deps fails immediately — the DependencyValidator
rejects path deps because they cannot be resolved by anyone outside the
workspace.
--auto is a higher-level orchestrator that:
-
Runs the same prep pipeline as
club prepare— discovery, target selection, dependency graph, server resolution, conflict check, planning, tarball sizing. -
Computes per-package pubspec overrides in memory. For every package whose pubspec needs rewriting, the new content is built using
yaml_editand held in memory. -
Publishes each package in topological order. Calls the same
PublishRunnerthatclub publishuses, passing the override string so that:- The validators see the rewritten dep shape (no path-dep error).
- The tarball ships the rewritten pubspec.
- The on-disk
pubspec.yamlis never read.
-
Stops on the first failure. If any per-package upload fails, the chain aborts with a list of packages already published and a hint to re-run after fixing the failure.
The workspace’s dart pub get keeps resolving locally throughout the
run (because pub workspaces shadow hosted deps with workspace
members) — so you can build and test the workspace at any point during
or after a --auto run with no special steps.
Flags
--auto accepts the standard club publish flags plus the
multi-package controls:
| Flag | Description |
|---|---|
--auto | Switch into multi-package mode. Required to enable everything below. |
--dry-run, -n | Print the full plan and exit without publishing anything. Source files are not touched in dry-run either. |
--force, -f | Skip the final apply confirmation. Does not automatically force-publish over existing versions — that’s controlled by --on-conflict. |
--directory <path>, -C | Workspace root to discover from. Defaults to the current directory. |
--server <host>, -s | Target server (e.g. myclub.birju.dev). Accepts a full URL too. Same rules as club publish. |
--on-conflict <mode> | How to handle packages whose local version is already published. prompt (default) / overwrite / skip / abort. |
--tree <style> | Visual style for the dependency tree. stacked (default) or nested. |
--no-tree | Suppress the dependency tree section. |
--skip-validation | Bypass the 25 client-side validators per package. |
--ignore-warnings | With --dry-run: do not exit non-zero on warnings. |
--enhanced, -e | Club extras (stricter size limit, enhanced analyzer rules, extra leak patterns). Applied to every package in the chain. |
--version <semver> | Publish every package in the closure as this one version, instead of each using its own. Rewritten internal constraints point at it too, so the stack resolves against itself. |
The single-package flags --to-archive and --from-archive are not
compatible with --auto and produce a usage error if combined.
Positional arguments
Package names to target. When omitted, an interactive multi-select picker shows all discovered packages.
club publish --auto pkg_a pkg_bYou don’t have to list transitive deps — they’re pulled in automatically.
Version conflict resolution
Same model as club prepare.
For every package in the closure, --auto queries the server’s package
endpoint and detects when a local version is already published. You
have three options per conflict:
| Option | Behaviour |
|---|---|
| Overwrite | Force-publish, replacing the existing version. The upload is sent with force=true. |
| Skip | Reuse the already-published version. The skipped package is not uploaded; dependents still rewrite to point at the existing published version. |
| Abort | Exit cleanly without publishing anything. |
Use --on-conflict <mode> to make a global decision for every conflict
(useful in CI). Without the flag, --auto prompts you per conflict via
an arrow-key picker.
In-memory rewrites
--auto’s defining feature is that source files stay untouched.
This is achieved with a pubspecOverride plumbed through the publish
pipeline:
-
The CLI computes the rewritten
pubspec.yamlcontent in memory usingyaml_edit. Comments, formatting, and key order are preserved. -
PublishRunnerparses the override (instead of reading from disk) so validators see the post-rewrite dep shape — no path-dep error. -
TarballBuildersubstitutes the override bytes forpubspec.yamlwhile streaming the rest of the package files unchanged. The uploaded.tar.gzships the rewritten pubspec. -
The on-disk
pubspec.yamlis never opened, never written. Its file hash before and after the run is identical.
This means:
- The workspace stays usable for local development between publish runs.
- Your VCS doesn’t see uncommitted changes after a publish.
dart pub getcontinues to resolve workspace members locally because the on-disk pubspec still haspath:(or hosted-by-name) deps that pub workspace shadowing handles.
If you’d rather see the rewrites land on disk (for review, commit, or
debugging), use club prepare — it does the rewrite,
stops there, and lets dart pub publish or club publish run as
separate steps.
Tarball sizes
Tarballs are measured before the publish loop starts, using the in-memory pubspec override. The compressed size + file count is shown inline in the dependency tree so you spot oversized packages before any upload happens — same numbers the server’s archive-size limit checks against.
A package whose tarball would exceed the server limit (default 100 MiB) will be flagged here without any upload attempt being made.
Examples
$ club publish --autoSelect packages to prepare:❯ [x] pkg_a 0.5.0 [x] pkg_b 0.3.0 [ ] core 1.2.0 [ ] core_interface 1.0.0
Publish 4 packages to myclub.birju.dev? (source pubspec.yamlfiles will not be modified) [y/N] yclub publish --auto --dry-run pkg_a pkg_bPrints the dependency tree, the planned in-memory rewrites, the tarball sizes, and an ASCII summary box. Never publishes, never writes anything to disk.
club publish --auto \ --force \ --on-conflict overwrite \ pkg_a pkg_bSkip the apply confirmation. Force-publish every conflict.
club publish --auto \ --on-conflict skip \ pkg_aIf core_interface 1.0.0 is already on the server, don’t republish
it. Just rewrite + publish pkg_a (and any other non-skipped
packages in its closure) referencing the existing ^1.0.0.
club publish --auto \ --on-conflict abort \ --force \ pkg_a pkg_bPure CI mode: bail out the moment a conflict is detected. The CLI exits non-zero so the build fails — fix the version in source and re-run.
CI usage
club publish --auto is fully CI-friendly. Three rules to remember:
- Pass package names as positional args. Without args, the interactive multi-select picker fires; in a non-TTY shell the command exits with a clear hint to add positional args.
- Pin the conflict policy with
--on-conflict <abort|skip|overwrite>. Default isprompt, which throws fast withVersion conflicts detected but stdin is non-interactiveif a duplicate version turns up.abortis the safest CI default. - Set
CLUB_TOKENas a secret.--server <host>plusCLUB_TOKENis enough to authorise the run — no priorclub loginstep is required. When the URL passed to--serveris not in the local credentials file, the env-token is used directly.
CI auto-detection: when CI, CONTINUOUS_INTEGRATION, or BUILD_NUMBER
env vars are set (the convention every major CI provider follows),
--auto automatically skips the final publish confirmation. You
don’t need --force in those environments. Use --force only
when running in a non-TTY shell that’s not detected as CI.
- name: Publish monorepo run: | club publish --auto pkg_a pkg_b \ --on-conflict abort \ --server myclub.birju.dev env: CLUB_TOKEN: ${{ secrets.CLUB_TOKEN }}That’s the entire publish step. No club login, no --force, no
dart pub token add.
Other CI providers
- name: Publish monorepo run: | club publish --auto pkg_a pkg_b \ --on-conflict abort \ --server myclub.birju.dev env: CLUB_TOKEN: ${{ secrets.CLUB_TOKEN }}publish: rules: - if: $CI_COMMIT_TAG image: dart:stable script: - club publish --auto pkg_a pkg_b --on-conflict abort --server myclub.birju.dev variables: CLUB_TOKEN: $CLUB_TOKEN # masked CI variableWhen the runner doesn’t set the standard CI env vars, add
--force so the publish prompt is skipped:
CLUB_TOKEN="$pat" \club publish --auto pkg_a pkg_b \ --force \ --on-conflict abort \ --server myclub.birju.devCI flag matrix
| Flag / env var | When it’s needed | What it controls |
|---|---|---|
CLUB_TOKEN | Always | Auth. Shadows stored creds; no club login needed. |
--server <host> | Always (in CI) | Skips the multi-server picker; the env-token authorises this URL. |
| Positional args | Always (in CI) | Skips the target multi-select picker. |
--on-conflict abort | Recommended | Fail fast on duplicate versions. Default is prompt, which throws in non-TTY. |
--force | Only when CI auto-detection misses | Skip the publish confirm. CI envs are auto-detected — you usually don’t need this. |
--no-tree | Optional | Quieter CI logs. |
--dry-run | Optional | Plan + validators without uploading. Useful as a pre-merge gate. |
--skip-validation | Discouraged | Bypasses the per-package validators. Server still validates. |
Dependency cycles
Two packages can depend on each other. It is legal on pub.dev and it happens
in real monorepos: firebase_ui_auth and firebase_ui_oauth each declare the
other as a dependency in their released versions. Pub’s version solver has no
acyclic requirement, so --auto publishes such packages as a group rather
than refusing the workspace.
Cycle members are flagged up front and badged in the publish stack:
⚠ 1 dependency cycle in the closure: firebase_ui_auth ↔ firebase_ui_oauth● Members publish as a group. The standalone resolution check is deferred until every member is up, then verified.
▶ [1] firebase_ui_shared 1.6.0 ✓ publish · 7.7 KiB ▶ [2] firebase_ui_auth 3.2.0 ✓ publish · 275.0 KiB ⟳ cycle ▶ [3] firebase_ui_oauth 2.2.0 ✓ publish · 182.0 KiB ⟳ cycleWhy resolution is deferred
--auto normally resolves every package standalone before uploading it (see
In-memory rewrites), which is a stronger check than
dart pub publish performs. Inside a cycle no publish order can satisfy that
check: whichever member goes first names a sibling version that is not on the
server yet.
So the check is deferred rather than dropped. Cycle members show:
Resolving dependencies ────────────────────────────────────── Deferred (dependency cycle; verified once the group is up)dart analyze is deferred for the same reason: mid-cycle there is no resolved
package to analyse.
Once every member has uploaded, the loop is closed and both checks run for real against the published bytes:
Verifying deferred resolutions ────────────────────────────── the cycle is closed; resolving each standalone now ✓ all 2 cycle packages resolve standaloneIf verification fails, the exit code is 65 and the report says plainly that
the packages are already on the server. Uploads cannot be rolled back, so fix
the constraints and publish a new version of the cycle:
✗ 1 of 2 cycle packages do not resolve standalone: firebase_ui_auth.● These versions are already on the server and will stay there. Fix the constraints and publish a new version of the cycle.Cycles mean lockstep versioning
A cycle cannot be released one package at a time. Bumping one member forces a matching release of the other, because each names the other’s new version. That is a property of the dependency graph, not of club.
To break a cycle instead of living with it, move the shared contract that both sides reach for into a package they already both depend on, leaving the graph acyclic.
Failure handling
If a per-package publish fails mid-chain, --auto aborts immediately
and prints which packages were already on the server:
[3/4] Publishing pkg_a ──────────────────────────────────────✗ Publish of pkg_a failed (exit code 65). Aborting the chain.● Already published: core_interface, core. These remain on the server. Re-run after fixing pkg_a to finish the rest.Re-running --auto is safe:
- Already-published packages are detected as conflicts and you can
pass
--on-conflict skipto bypass them. - Or pass
--on-conflict overwriteto force-re-publish them too.
Exit codes
| Code | Meaning |
|---|---|
0 | Success — every package in the chain was published. |
65 | Data error — unknown target, validation failed, external path dep, self-dependency, a per-package upload failed, or a cycle member failed post-publish verification. |
66 | No input — no publishable packages discovered. |
78 | Config — server resolution failed, user aborted, conflict in non-interactive shell. |
| Other | Forwarded from the per-package PublishRunner if it failed with a non-standard code. |
Comparison: club publish vs club publish --auto
| Aspect | club publish | club publish --auto |
|---|---|---|
| Number of packages | One | All targets + transitive workspace deps |
| Path/workspace deps | Hard error (DependencyValidator) | Rewritten in-memory to hosted refs |
| Source pubspec.yaml | Untouched | Untouched |
| Topological order | N/A | Yes — leaves first |
| Dependency cycles | Not detected (single package) | Published as a group, verified after |
| Version conflict handling | --force is all-or-nothing | Per-package overwrite / skip / abort |
| Confirmation prompts | One per run | One per run (covers the whole chain) |
| Use case | Standalone packages | Monorepos, pub workspaces |