Skip to content

CLI Publishing

club publish is a native drop-in replacement for dart pub publish. It shares the same flag set, the same exit codes, and runs the same 25 client-side validators — but adds one critical superpower:

You do not need publish_to: in your pubspec.yaml.

This unlocks publishing packages that live inside larger projects (where modifying pubspec.yaml is not an option) without losing any of the safety checks dart pub publish provides.

Quick start

Terminal window
$ cd path/to/your/package
$ club publish
Publishing my_package 1.2.0 from /Users/me/code/my_package
Target server: myclub.birju.dev (auto-selected (only logged-in server))
Building package archive...
Created /tmp/club-publish-...tar.gz (12.4 KiB, 18 files)
Running 25 validators...
All validators passed.
Publish my_package 1.2.0 to myclub.birju.dev? [Y/n] y
Uploading to myclub.birju.dev...
Successfully uploaded my_package version 1.2.0.

Flags

The core flag set mirrors dart pub publish (matched against the upstream LishCommand), plus club-specific additions (--server, --version, --enhanced, --from-git, --ref, --auto).

FlagShortDescription
--directory <path>-CRun as if <path> is the package directory. Useful for monorepos.
--dry-run-nValidate without uploading. Exits non-zero on errors (and warnings unless --ignore-warnings).
--force-fSkip confirmation prompts. Required in CI when there is no TTY.
--skip-validationBypass all 25 client-side validators. The server still validates.
--ignore-warningsWith --dry-run only: do not exit non-zero on warnings.
--to-archive <path>Write the produced .tar.gz to <path> instead of uploading.
--from-archive <path>Upload an existing .tar.gz instead of building one. Implies --skip-validation.
--server <host>-sOverride publish_to and the auto-picker. Accepts a bare host (myclub.birju.dev) or a full URL. Must be a server you have logged in to with club login.
--version <semver>Override the version being published. Rewrites the version inside the tarball’s pubspec.yaml without modifying source files. Must be valid semver.
--enhanced-eClub extras on top of dart pub parity: stricter size limit, dart analyze --fatal-warnings, git deps as errors, extended leak patterns, DevTools config.yaml content checks, every file-case collision reported.
--from-git <url>Clone a git repository and publish it as a package. Accepts an https or SSH git URL. The repo is cloned under ~/.club/clones/ and removed after a successful publish. See club publish --from-git.
--ref <ref>Git branch, tag, or commit to check out. Only valid with --from-git. Defaults to the remote default branch.
--autoSwitch into multi-package mode for monorepos. See club publish --auto.

How the target server is resolved

club publish resolves the target server with this priority — only the first match is used:

  1. --server <host> flag (highest priority). The server must be in your logged-in credentials, otherwise the command exits with a hint to run club login. Accepts a bare host (myclub.birju.dev) or a full URL — both forms map to the same credential.
  2. publish_to: in pubspec.yaml, when present and not none. The URL must be in your logged-in credentials.
  3. Auto-pick from logged-in servers:
    • 1 server: used automatically (with confirmation, unless --force).
    • 2+ servers: an arrow-key TUI picker appears (in CI, this fails with a clear error directing you to use --server).
    • 0 servers: exits with a hint to run club login <host>.

This means you can publish a package whose pubspec.yaml has either no publish_to: at all, or publish_to: none, without modifying it.

Examples

Terminal window
# Pubspec has publish_to set → that server is used.
club publish
# Pubspec has no publish_to, you have one login → that one is used.
club publish
# Pubspec has no publish_to, you have multiple logins → arrow-key picker.
club publish
# Force a specific server regardless of pubspec.
club publish --server internal.myclub.birju.dev
# Try a publish without uploading.
club publish --dry-run
# Build the archive locally for inspection.
club publish --to-archive ./pkg.tar.gz
# Upload an archive that was built earlier.
club publish --from-archive ./pkg.tar.gz --force

Dependency resolution

Before the validators run, club publish resolves the package’s dependencies. It does this in isolation: the archive it just built is unpacked into a scratch directory, resolution: workspace is dropped from the unpacked copy, and pub get runs there. The archive that gets uploaded is untouched (resolution: workspace is normal in a published pubspec and consumers ignore it).

This is a deliberate divergence from dart pub publish, which resolves the whole workspace instead. Resolving what actually ships means:

  • The check answers the question that matters: does this package resolve for someone who installs it? A workspace resolve can pass because the root’s dependency_overrides quietly substitute local path: copies for the real hosted versions.
  • An unrelated dependency problem elsewhere in the workspace can’t block publishing a leaf package.
  • It works when the workspace root needs the Flutter SDK but the package being published does not, or the reverse. Pub escalates resolution to the workspace root, so dart pub get inside a Flutter workspace fails with Because <root> requires the Flutter SDK, version solving failed no matter which package you were trying to publish.

flutter pub get is used instead of dart pub get when the pubspec pulls in the Flutter SDK: any sdk: flutter entry in dependencies or dev_dependencies (flutter_test counts), or an environment: flutter: constraint.

Because a scratch directory has no pubspec.lock to fall back on, this step needs network access and a valid token for any club server the package depends on. Run club login first, or pass --skip-validation to skip resolution and validation together.

If the package still has path: or git: dependencies, the standalone resolve fails by design: consumers can’t reach them. Use club publish --auto or club prepare to rewrite them to hosted refs.

Validation

After resolution, club publish runs the same 25 validators as dart pub publish. Each validator mirrors the upstream implementation — see the inline doc comments in lib/src/publish/validators/ for the GitHub link to its upstream counterpart.

Validators are grouped by severity:

  • Errors block publish unconditionally. Pass --skip-validation to bypass (or fix the issue).
  • Warnings require interactive confirmation, or --force in CI.
  • Hints are informational only and never block.

The full list of validators (in run order):

  1. AnalyzeValidator — runs dart analyze against the resolved scratch copy of the archive (issues surface as warnings; --enhanced upgrades it to dart analyze --fatal-warnings).
  2. ChangelogValidator — CHANGELOG present and mentions current version.
  3. CompiledDartdocValidator — no doc/api/ or .ddc.js artifacts.
  4. DependencyValidator — no path/git deps; reasonable version constraints.
  5. DependencyOverrideValidator — non-dev deps not silently overridden.
  6. DeprecatedFieldsValidator — no transformers, web, author, authors.
  7. DevtoolsExtensionValidatorextension/devtools/ is well formed.
  8. DirectoryValidator — only standard top-level dirs.
  9. ExecutableValidatorbin/<name>.dart exists for every executable.
  10. FileCaseValidator — no case collisions.
  11. FlutterConstraintValidator — Flutter SDK constraint with upper bound.
  12. FlutterPluginFormatValidator — uses the federated plugin format.
  13. GitignoreValidator — no archived files are simultaneously gitignored.
  14. GitStatusValidator — git working tree is clean.
  15. LeakDetectionValidator — no AWS/Google/GitHub/Slack/PEM keys in files.
  16. LicenseValidator — LICENSE file present at the root.
  17. NameValidator — package name follows pub conventions.
  18. PubspecPresentValidatorpubspec.yaml is in the archive.
  19. PubspecFieldValidator — required fields present and well-formed.
  20. PubspecTypoValidator — top-level field names are not typos.
  21. ReadmeValidator — README file present.
  22. RelativeVersionValidator — new version greater than latest published.
  23. SdkConstraintValidator — Dart SDK constraint with upper bound.
  24. SizeValidator — archive < 100 MiB.
  25. StrictDependenciesValidatorlib/, bin/, hook/ only import declared deps.

CI usage

club publish is fully CI-friendly. Two rules to remember:

  1. Set CLUB_TOKEN as a secret. --server <host> plus CLUB_TOKEN is enough to authorise the run — no prior club login step is required. When the URL passed to --server is not in the local credentials file, the env-token is used directly.
  2. Pass --server <host> so the multi-server picker doesn’t fire. If your pubspec already has publish_to: set to a server you have logged in to (or have CLUB_TOKEN for), you can omit --server.

CI auto-detection: when CI, CONTINUOUS_INTEGRATION, or BUILD_NUMBER env vars are set (the convention every major CI provider follows), club publish 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.

.github/workflows/publish.yml
- name: Publish
run: club publish --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

.github/workflows/publish.yml
- name: Publish
run: club publish --server myclub.birju.dev
env:
CLUB_TOKEN: ${{ secrets.CLUB_TOKEN }}

CI flag matrix

Flag / env varWhen it’s neededWhat it controls
CLUB_TOKENAlwaysAuth. Shadows stored creds; no club login needed.
--server <host>Recommended in CISkips the multi-server picker; the env-token authorises this URL.
--forceOnly when CI auto-detection missesSkip the publish confirm. CI envs are auto-detected — you usually don’t need this.
--dry-runOptionalPlan + validators without uploading. Useful as a pre-merge gate.
--ignore-warningsOptional, with --dry-runDon’t exit non-zero on validator warnings.
--skip-validationDiscouragedBypasses the 25 validators. Server still validates.

CI behaviour reference

  • Server picker fails fast in non-interactive shells with an error pointing to --server.
  • Color output is auto-disabled when stdout is not a TTY (or when NO_COLOR is set, per no-color.org).
  • Exit codes match dart pub conventions:
    • 0 — success
    • 65 — validation failed (data error)
    • 66 — pubspec/package not found
    • 78 — config / server resolution / aborted

Comparing with dart pub publish

Aspectclub publishdart pub publish
Server resolution--server <host>publish_to → logged-in picker--server (deprecated) → publish_toPUB_HOSTED_URL
Works without publish_to❌ — refuses if none or missing
Picks from multiple logins✅ arrow-key TUI
ValidatorsAll 25, mirrored from upstreamAll 25
FlagsAll standard flags + --serverAll standard flags
Exit codesSame as upstream
Auth storage~/.config/club/credentials.json~/.config/dart/pub-tokens.json

Troubleshooting

Not logged in to <host>

Either --server or publish_to: points at a server you have not logged in to:

Terminal window
club login myclub.birju.dev

No club server is configured.

You have not run club login yet. Do that first.

Cannot prompt in a non-interactive shell.

You are in CI (or piping output) but the runner needs to confirm something. Pass --force (and/or --server <host> if there are multiple logins).

Found N error(s).

A validator produced a hard error. Read the message, fix the issue, or pass --skip-validation if you absolutely cannot fix it (the server will still validate).

dart analyze reported issues

Run dart analyze directly to see the issues, fix them, then retry. If you must publish anyway, --skip-validation bypasses the analyzer.

Dependency resolution failed.

The package did not resolve on its own. The pub output below the message names the cause. Common ones:

  • depends on <pkg> from path which doesn't exist — you still have a path: dependency. It resolves inside your workspace but not for anyone installing the package. Use club publish --auto or club prepare to rewrite it to a hosted ref.
  • could not find a package named <pkg> / an auth error — the package depends on a club server you are not logged in to. Run club login <host> and retry.
  • version solving failed on a hosted dep — the constraint genuinely cannot be satisfied from the server. Note that a workspace root’s dependency_overrides do not apply here, so a constraint you never actually exercised locally can surface at this point.

--skip-validation skips this step along with the validators.

Because <root> requires the Flutter SDK, version solving failed

You are on a club version older than 0.5.0. Publishing a package from a pub workspace whose root is a Flutter app used to run dart pub get at the workspace root, which cannot solve Flutter SDK dependencies. Upgrade, or pass --skip-validation on the old version.

Implementation notes

The publish flow is laid out as small, focused modules under packages/club_cli/lib/src/publish/ so future updates to dart pub publish can be mirrored cheaply:

publish/
├── publish_runner.dart # Orchestration (matches LishCommand.runProtected)
├── pubspec_reader.dart # Pubspec parsing (uses pubspec_parse package)
├── server_resolver.dart # The --server / publish_to / picker logic
├── workspace_resolver.dart # Pub workspace root discovery
├── tarball_builder.dart # tar + gzip (matches createTarGz)
├── archive_extractor.dart # Unpacks the archive into a scratch dir
├── isolated_resolution.dart # Standalone `pub get` on the unpacked archive
├── ignore_filter.dart # .pubignore + .gitignore handling
└── validators/
├── validator.dart # Base class + ValidationContext + Severity
├── runner.dart # Concurrent runner + report aggregation
└── *.dart # 25 validators, each linking to upstream source

Every file carries a doc comment with a link to the upstream Dart pub source it mirrors, making it straightforward to apply future SDK changes.