Skip to content

Smart Publishing

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:

  1. --server <url> flag (highest priority)

    Must be a server you have logged in to with club login. Overrides publish_to: entirely. Example:

    Terminal window
    club publish --server https://club.example.com
  2. 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
  3. 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.

Examples by scenario

You run one club instance. publish_to: is absent from the pubspec.

Terminal window
$ club publish
Publishing my_package 1.2.0 from /path/to/my_package
Target server: https://club.example.com (auto-selected (only logged-in server))
...
Publish my_package 1.2.0 to https://club.example.com? [Y/n]

Flags

FlagShortDescription
--directory <path>-CRun as if <path> is the package directory.
--dry-run-nValidate without uploading.
--force-fSkip confirmation. Required in CI without a TTY.
--skip-validationBypass all client-side validators.
--ignore-warningsWith --dry-run: do not exit non-zero on warnings.
--to-archive <path>Write archive to file instead of uploading.
--from-archive <path>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>-sTarget 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.

Warnings (require confirmation)

Missing README/CHANGELOG, missing homepage/repository, uncommitted git changes, gitignored files in archive, deprecated pubspec fields, deprecated dependencies, detected credential leaks, legacy Flutter plugin format, asset path issues.

Hints (informational)

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:

  1. 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.).

  2. Colors auto-disable when stdout is not a TTY, or when NO_COLOR is set (per no-color.org), or when TERM=dumb.

  3. Use --force to skip all confirmations:

    CI step
    club login --key "$CLUB_TOKEN" https://club.example.com
    club publish --force

    Or skip club login entirely — the CLI also reads CLUB_TOKEN from the environment as a bearer-token override.

  4. Disambiguate with --server when the picker would fire:

    Terminal window
    club publish --force --server https://club.example.com
  5. Use --dry-run + --ignore-warnings as a pre-publish lint step:

    Terminal window
    club publish --dry-run --ignore-warnings
    # exits 0 on warnings, non-zero on errors

Exit codes

Match standard dart pub conventions (sysexits):

CodeMeaning
0Success
65Data error — validation failed or warnings in --dry-run
66No input — pubspec.yaml or archive not found
78Config — server resolution failed, user aborted, no TTY

Architecture

The publish flow is laid out as small focused modules under packages/club_cli/lib/src/publish/ so future dart pub SDK updates 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
├── tarball_builder.dart // tar + gzip (matches createTarGz)
├── 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 linking to the upstream Dart pub source it mirrors — for example:

/// Mirrors dart pub's
/// [`SizeValidator`](https://github.com/dart-lang/pub/blob/master/lib/src/validator/size.dart):
/// archives must be smaller than 100 MiB.

This makes it straightforward to review and apply future changes from the Dart SDK.

Publish flow (step by step)

  1. Parse flags. The PublishCommand reads argResults and constructs a PublishOptions value.

  2. 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).

  3. Read pubspec. readPubspec() uses the official pubspec_parse package so semantics match dart pub exactly.

  4. 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.

  5. Run validators concurrently. runAllValidators() fires all 25 validators in parallel via Future.wait and aggregates findings by severity.

  6. Confirm. If there are warnings and no --force, ask the user. In non-interactive shells, fail with a clear message instead of blocking.

  7. Upload. ClubClient.publishFile() runs the standard pub 3-step upload protocol (GET /api/packages/versions/new → multipart POST → follow redirect to newUploadFinish).

  8. Cleanup. Temp tarballs are deleted on success or failure.

Comparison: club publish vs dart pub publish

Aspectclub publishdart pub publish
Works without publish_toYes (core feature)No — refuses with none or missing
Multi-login pickerYes (arrow-key TUI)No
--server overrideYesDeprecated in dart pub
--version <semver> overrideYes
--from-archive uploadYes
Auth storage~/.config/club/credentials.json~/.config/dart/pub-tokens.json
Validators~25 (mirrors dart pub)~25
Standard flags1:1 match for common flags
Exit codesMatch dart pub
Underlying protocolPub spec v2 (same wire format)Pub spec v2