club publish is a fully native drop-in replacement for dart pub publish.
It runs the same style of client-side validators, accepts the same flags, and
produces the same archive format — but it removes the single biggest friction
point of publishing to a private server: you no longer need publish_to: in
your pubspec.yaml.
This is the feature that unlocks publishing packages whose pubspec cannot
be modified (e.g. subpackages inside a larger application, or vendored
code you do not own).
Why this exists
dart pub publish has a hard requirement: it reads publish_to: from your
pubspec.yaml and refuses to publish if it is missing or set to none.
That is deliberate — it prevents accidental uploads to pub.dev. But it
creates real friction for private-registry users:
Monorepos. A package inside a larger app repository often cannot
set publish_to because the same pubspec is also used in local
development.
Vendored packages. You may need to publish a fork of an upstream
package without patching its pubspec.
Multiple registries. Teams that run more than one club server
(e.g. staging + prod) want to choose per-publish.
club publish solves all three by moving the “where to publish” decision
out of the pubspec and into a resolver that consults your login state.
How server resolution works
When you run club publish, the CLI resolves the target server in this
exact priority order — only the first match wins:
--server <url> flag (highest priority)
Must be a server you have logged in to with club login. Overrides
publish_to: entirely. Example:
Terminal window
clubpublish--serverhttps://club.example.com
publish_to: in pubspec.yaml
Used when set to an http(s) URL (not none). Must also be a logged-in
server.
pubspec.yaml
publish_to: https://club.example.com
Auto-pick from logged-in servers
When --server is not given and publish_to is missing or none:
1 logged-in server → used automatically (with confirmation
prompt, unless --force).
2+ logged-in servers → arrow-key picker (↑/↓/Enter), or in a
non-interactive shell: exits with an error asking you to pass
--server.
0 logged-in servers → exits with a hint to run club login.
Upload an existing archive. Implies --skip-validation.
--version <semver>
Override the version written into the tarball (useful in CI that derives versions from tags). club-only.
--server <url>
-s
Target server override. club-only.
The --server and --version flags are club-specific; the rest mirror
dart pub publish.
The validators
club publish runs a family of ~25 client-side validators that mirror the
named validator classes in
dart-lang/pub
— when the upstream SDK changes, we can diff the source to apply updates.
Errors (block publish)
Invalid pubspec format, bad version, bad package name, missing
description, missing or bad SDK constraints, missing license,
path/git dependencies, lib/ imports from dev_dependencies,
duplicate version already on the server, pubspec size over limit.
Non-standard top-level directories, dependency overrides,
short/long descriptions, version regressions.
Bypass all validators with --skip-validation. The server still performs
its own validation regardless.
Skip validation with --from-archive
--from-archive <path> uploads a pre-built .tar.gz and implies
--skip-validation. This is useful when another step has already produced
and validated the archive (e.g. club publish --to-archive), and you want
the publish step to be a pure upload.
CI usage
The CLI is fully CI-friendly by design:
No interactive prompts when stdin is not a TTY. Instead of blocking
forever, club publish exits with a clear message pointing to the
flag you need (--force, --server, etc.).
Colors auto-disable when stdout is not a TTY, or when NO_COLOR
is set (per no-color.org), or when TERM=dumb.
This makes it straightforward to review and apply future changes from
the Dart SDK.
Publish flow (step by step)
Parse flags. The PublishCommand reads argResults and constructs a
PublishOptions value.
Resolve the target server.ServerResolver.resolve() applies the
priority order described above and returns a ResolvedServer with
the URL, token, and the source (cliFlag, pubspec, singleLogin,
or interactivePick).
Read pubspec.readPubspec() uses the official pubspec_parse
package so semantics match dart pub exactly.
Build or load the tarball.TarballBuilder walks the directory,
applies IgnoreFilter (.pubignore with .gitignore fallback),
streams entries through the tar package, and pipes to
dart:io’s gzip encoder.
Run validators concurrently.runAllValidators() fires all 25
validators in parallel via Future.wait and aggregates findings by
severity.
Confirm. If there are warnings and no --force, ask the user. In
non-interactive shells, fail with a clear message instead of blocking.
Upload.ClubClient.publishFile() runs the standard pub
3-step upload protocol (GET /api/packages/versions/new → multipart
POST → follow redirect to newUploadFinish).
Cleanup. Temp tarballs are deleted on success or failure.