Skip to content

Publishing from Git

club publish --from-git extends club publish so you can publish a package from a git URL without cloning the repo yourself. In one command it:

  1. Clones the repository into ~/.club/clones/<host>/<org>/<repo>.
  2. Checks out a branch, tag, commit, or pull request (defaults to the remote default branch).
  3. Runs the standard publish flow on the clone.
  4. Deletes the clone on success.
  5. Keeps the clone on failure so a re-run reuses it via a fast hard reset, no re-clone.

Everything downstream of the clone is the unchanged single-package or --auto flow. The on-disk state of your current shell is never touched.

Quick start

Publish the default branch of a public repo:

Terminal window
club publish --from-git https://github.com/your-org/your-package

Publish a specific tag or branch:

Terminal window
club publish --from-git https://github.com/your-org/your-package --ref v1.2.0
club publish --from-git https://github.com/your-org/your-package --ref main

Publish the code on a GitHub pull request, as a prerelease:

Terminal window
club publish --from-git https://github.com/your-org/your-package/pull/2

Publish a package that lives in a subdirectory of the repo:

Terminal window
club publish --from-git https://github.com/your-org/your-monorepo -C packages/foo

Publish every package of a monorepo in topological order:

Terminal window
club publish --from-git https://github.com/your-org/your-monorepo --auto

Preview without uploading:

Terminal window
club publish --from-git https://github.com/your-org/your-package --dry-run

A typical successful run looks like:

Preparing git source ────────────────────────────────────────
repo: your-org/your-package (github.com)
cache: /home/me/.club/clones/github.com/your-org/your-package
cloning…
✓ cloned, checked out main (a1b2c3d) (1.4s)
📦 Publishing your_package 1.2.0
from /home/me/.club/clones/github.com/your-org/your-package
to myclub.birju.dev (auto-selected (only logged-in server))
Building package archive ────────────────────────────────────
/tmp/club-publish-1779431135919322.tar.gz
12.2 KiB, 16 files (42ms)
Resolving dependencies ──────────────────────────────────────
✓ Resolved (1.2s)
Running 25 validators ───────────────────────────────────────
✓ All validators passed. (642ms)
Uploading ───────────────────────────────────────────────────
Server: published your_package 1.2.0
(982ms)
┌──────────────────────────────────────────────────────┐
│ 🎉 your_package 1.2.0 published │
│ URL https://myclub.birju.dev/packages/your_package│
│ Size 12.2 KiB (16 files) │
└──────────────────────────────────────────────────────┘
Removing clone /home/me/.club/clones/github.com/your-org/your-package

How it works

  1. URL parsing. Both https://host/org/repo(.git) and SCP-style git@host:org/repo.git URLs are accepted. The host and repo path are mapped to a namespaced cache directory under ~/.club/clones/ so two repos with the same name from different orgs never collide. A GitHub pull request URL is reduced to its repository here, and the PR number is remembered for the next two steps.

  2. Clone or reuse (shallow). If the cache directory does not exist (or its origin remote does not match the requested URL, or the directory is not a healthy git work tree), Club does a fresh --depth 1 clone of just the ref being published, on a single branch. If the cache directory is a healthy match, Club reuses it and runs a shallow git fetch for the same ref. Either way the cache holds only the single commit being published, no history. A pull request is fetched from refs/pull/<n>/head into a local pr/<n> tracking branch, so from here on it behaves like any other branch.

  3. Checkout. Club force-checks-out the requested ref. Local branches are reset to the matching remote tip, tags and commits are checked out detached, and git clean -ffdx removes any untracked or ignored files (stale .dart_tool, lockfiles, build output). The working tree is pristine before the publish runs.

  4. Standard publish flow. From here on the run is identical to a regular club publish (or club publish --auto) executed in the clone directory: server resolution, version conflict pre-check, tarball build, standalone dependency resolution, the 25 validators, upload.

  5. Cleanup. After a successful publish the clone is deleted and any empty parent directories under ~/.club/clones/ are pruned. If the publish fails the clone is kept so a re-run can fast-path through step 2 with no re-clone.

Monorepos: --auto and subdirectories

--from-git composes with both ways club publish handles monorepos:

Publish every package in a workspace-style monorepo in topological order, with in-memory dependency rewrites, in a single command:

Terminal window
club publish --from-git https://github.com/your-org/your-monorepo --auto

The full --auto pipeline runs against the clone: discovery walks the cloned tree, the dependency graph and topo order are computed, internal path: deps are rewritten in memory, and every package is published in order. Target a specific subset by passing package names as positional args:

Terminal window
club publish --from-git https://github.com/your-org/your-monorepo \
--auto pkg_a pkg_b

Pull requests

Pass a GitHub pull request URL and Club publishes the code on that PR:

Terminal window
club publish --from-git https://github.com/your-org/your-package/pull/2

This is the review-before-merge case: get a contributor’s branch onto your private server so downstream projects can depend on it and try it for real, without merging first and without the PR author needing publish rights.

The derived version below is a default, not a constraint. The version step offers it and you can publish as anything else instead.

The version is derived, never the one in the pubspec

A PR is published as a prerelease of the package’s own version. If pubspec.yaml on the PR says 1.2.0, PR #2 publishes as:

1.2.0-pr2

The pubspec on disk is never modified. Only the copy inside the uploaded tarball carries the derived version, exactly like --version.

This matters because contributors almost never bump the version in a PR. Without the suffix, publishing PR #2 of a package sitting at 1.2.0 would overwrite the real released 1.2.0 with unreviewed code. The suffix makes that impossible.

It also means a PR build can never be picked up by accident. Under semver a prerelease sorts below its release, so a consumer with ^1.2.0 will not resolve 1.2.0-pr2. Depending on a PR build has to be deliberate:

dependencies:
your_package:
hosted: https://myclub.birju.dev
version: 1.2.0-pr2

How the suffix is applied in the less obvious cases:

Version in the PRPublished as
1.2.01.2.0-pr2
1.2.0-dev.31.2.0-dev.3.pr2
1.2.0+51.2.0-pr2+5

Validators run against the derived version, with one accommodation: the CHANGELOG check is satisfied by an entry for the version declared in the pubspec. Nobody writes a changelog entry for a version that only comes into existence at publish time.

Updating a PR build

The suffix is deliberately stable: it contains the PR number and nothing else, no commit hash. Every publish of PR #2 produces 1.2.0-pr2. When the author pushes new commits, re-publish the same version with --force:

Terminal window
club publish --from-git https://github.com/your-org/your-package/pull/2 --force

Consumers keep the constraint they already have and just re-resolve. The alternative, encoding the commit into the version, would leave a pile of dead 1.2.0-pr2.a1b2c3d versions nobody ever cleans up.

Accepted URL forms

Anything you can copy out of the browser address bar while looking at a PR works. Trailing tabs, query strings, and fragments are ignored:

https://github.com/your-org/your-package/pull/2
https://github.com/your-org/your-package/pull/2/files
https://github.com/your-org/your-package/pull/2/commits
https://github.com/your-org/your-package/pull/2#issuecomment-12345

Club resolves the PR head from refs/pull/<n>/head, which GitHub publishes on the repository itself. PRs opened from forks work with no extra configuration, and the fork does not have to be reachable.

Monorepos

A PR against a monorepo composes with --auto, and the whole stack is suffixed together:

Terminal window
club publish --from-git https://github.com/your-org/your-monorepo/pull/7 --auto

Every package in the closure publishes as X.Y.Z-pr7, and the rewritten internal constraints point at the prereleases too. If pkg_a depends on pkg_b, pkg_a is published with:

dependencies:
pkg_b:
hosted: https://myclub.birju.dev
version: ^1.2.0-pr7

So the published stack resolves against itself rather than falling back to the last released versions of its siblings. The rewrite preview printed before the upload shows the exact constraints, so you can check this before anything is sent.

Restrictions

CombinationResult
--ref with a PR URLRejected. The PR URL already selects the commit.
--version with a PR URLPublishes that version verbatim, with no -pr<n> suffix. See Choosing the version.
-C, --auto, --dry-run, --force, --serverAll work normally.

Choosing the version

Publishing from git is usually a republish of code you do not own, where the version in the source pubspec is not the one you want to occupy on your server. So --from-git asks:

Version ─────────────────────────────────────────────────────
detected: 0.0.8-pr2 (PR #2)
Publish as [0.0.8-pr2]:

Press Enter to take the detected version. Type anything else and that becomes the published version, used verbatim: entering 2.0.0 while publishing PR #2 publishes 2.0.0, not 2.0.0-pr2. The suffix is the default, not a rule imposed on a version you chose yourself.

The value is checked as semver before anything is published, and a typo just re-asks rather than losing the run:

Publish as [0.0.8-pr2]: v2.0
Not valid semver: "v2.0". Try 1.2.3, 1.2.3-beta.1, or 1.2.3+build5.
Publish as [0.0.8-pr2]:

The step is skipped, and the detected version used, whenever there is nobody to ask or the answer is already known:

SituationWhy
--version was passedThe answer is already given.
--forceMeans “stop asking me things”, the same as for every other prompt.
CI, or a non-interactive shellNobody to ask.

--version is the non-interactive equivalent, and is validated the same way before any cloning happens:

Terminal window
club publish --from-git https://github.com/your-org/your-package/pull/2 \
--version 2.0.0

With --auto

In a monorepo there is no single detected version to offer, so the step asks whether to publish the whole stack as one version:

Version ─────────────────────────────────────────────────────
every package publishes with its own version
suffixed for PR #7, e.g. 1.2.0-pr7
enter a version to publish the whole stack as that version instead
Publish all packages as:

Enter nothing and each package keeps its own version. Enter 2.0.0 and every package in the closure publishes as 2.0.0, with the rewritten internal constraints pointing at it:

dependencies:
pkg_b:
hosted: https://myclub.birju.dev
version: ^2.0.0

Reuse and the clone cache

~/.club/clones/<host>/<org>/<repo> is treated as a managed cache, not a persistent checkout. Two rules govern it:

OutcomeWhat happens to the clone
Publish succeededClone is deleted. Empty parent dirs under ~/.club/clones/ are pruned.
Publish failedClone is kept so the next run can hard-reset and re-checkout instead of cloning from scratch.

A re-run after failure is foolproof: regardless of what was committed, modified, or left behind in the working tree, Club fetches the latest refs, force-checks-out the requested ref (resetting any local divergence), and runs git clean -ffdx to wipe untracked and ignored files. There is no way for stale state to leak into a publish.

The reuse path is also safe across different repos: each one has its own namespaced directory, and a directory pointing at a different remote is wiped and re-cloned automatically.

Cleaning the cache manually

The cache is just a directory. Safe to delete by hand at any time:

Terminal window
rm -rf ~/.club/clones

The next --from-git run will recreate any directories it needs.

URL formats

FormExample
HTTPShttps://github.com/your-org/your-package
HTTPS with .githttps://github.com/your-org/your-package.git
SSH (SCP-style)git@github.com:your-org/your-package.git
SSH (URL-style)ssh://git@github.com/your-org/your-package.git
Nested groupshttps://gitlab.com/group/subgroup/repo.git
GitHub pull requesthttps://github.com/your-org/your-package/pull/2

A pull request URL is normalised to the plain repository URL before cloning, so PR and branch publishes of the same repo share one cache directory. See Pull requests.

Nested groups (GitLab subgroups, Gitea organisations with sub-orgs, etc.) map naturally onto nested cache directories:

~/.club/clones/gitlab.com/group/subgroup/repo

Private repositories

Authentication is delegated to your local git setup. Whatever lets git clone succeed in a regular terminal (SSH key in ssh-agent, an HTTPS credential helper, an OAuth token, a ~/.netrc entry) is what Club uses. No --from-git-specific auth flags exist.

Flags

--from-git accepts every standard club publish flag, with two additions and one restriction.

FlagDescription
--from-git <url>Git URL to clone and publish from. HTTPS, SSH (SCP-style or URL-style), and GitHub pull request URLs are all accepted.
--ref <ref>Branch, tag, or full commit SHA to check out. Defaults to the remote default branch. Only valid with --from-git, and not with a PR URL.
--directory <path>, -CWhen --from-git is set, resolved relative to the clone root. Useful for publishing a single package in a monorepo without --auto.
--autoRun multi-package discovery + topological publish over the clone. See club publish --auto.
--dry-run, -nClone (or reuse), validate, and bail before uploading. The clone is still removed on success.
--force, -fSkip confirmation prompts (including the version step), force-publish existing versions. Required to re-publish an updated PR.
--version <semver>Publish as this version, skipping the version step. Applies to every package under --auto. Validated before cloning.
--ref without --from-gitRejected with a config error.
--ref with a PR URLRejected with a config error. The PR URL already selects the commit.
--from-archiveNot compatible with --from-git (the archive is already built; there is nothing to clone).

All other club publish flags (--server, --enhanced, --skip-validation, --to-archive, the --auto flags --on-conflict, --tree, --no-tree) behave exactly as in the standard flow.

Examples

Publish exactly what was released as v1.4.0 upstream:

Terminal window
club publish --from-git https://github.com/upstream/cool_package \
--ref v1.4.0

CI usage

--from-git is CI-friendly. The non-TTY rules from club publish and club publish --auto apply unchanged. The one extra consideration is git auth: the CI runner needs to be able to git clone the URL you pass.

.github/workflows/mirror.yml
- name: Mirror upstream release to private club server
run: |
club publish --from-git https://github.com/upstream/cool_package \
--ref ${{ github.event.release.tag_name }} \
--server myclub.birju.dev \
--on-conflict abort
env:
CLUB_TOKEN: ${{ secrets.CLUB_TOKEN }}

For private source repos, configure your CI’s standard git credential mechanism (SSH key, deploy token, OAuth token), then --from-git works with no additional flags.

The cache lives under $HOME on the runner. If your CI persists $HOME across job runs, expect the fast reuse path to kick in on subsequent invocations of the same repo.

Exit codes

CodeMeaning
0Success. Clone was removed.
65Data error from the publish flow (validation failed, version conflict without --force, etc.). Clone is kept.
66No input (e.g. missing pubspec.yaml at the resolved directory). Clone is kept.
69Git failure (git not on PATH, unknown ref, unknown pull request, network error, repo not accessible). Clone is kept if it was created.
78Config error (--ref without --from-git, --ref or --version with a PR URL, --from-git combined with --from-archive, a non-GitHub pull request URL, etc.). No clone happens.

When the clone is kept, the CLI prints the cache path and a hint that a re-run will reuse it.

Limitations

  • Authentication for the source repo is the user’s responsibility. Whatever lets git clone <url> succeed is what makes --from-git succeed. Club does not prompt for git credentials.
  • Git submodules are not initialised. If the package being published depends on submodule content at build time, run git clone --recurse-submodules into a regular checkout and use the plain club publish flow instead.
  • Commit refs must be full 40-char SHAs. When --ref is a commit (rather than a branch or tag), pass the complete SHA. Abbreviated SHAs are rejected by every git remote and cannot be fetched shallowly. Branch and tag names accept their usual form.
  • Commit refs need server-side SHA fetch support. Fetching a specific commit by SHA requires the remote’s uploadpack.allowAnySHA1InWant to be enabled. GitHub, GitLab, and Gitea enable this by default; some self-hosted servers may need a config change. Branch/tag refs work everywhere.
  • Pull request URLs are GitHub-only. Other forges expose their own PR refs, so they are still reachable through --ref (for example --ref refs/merge-requests/4/head on GitLab), but without the automatic -pr<n> version. See Pull requests.
  • A PR is published from its head, not its merge result. Club fetches refs/pull/<n>/head, which is what the author pushed. It does not use GitHub’s synthesized merge commit, which does not exist when the PR has conflicts and would otherwise make the published contents depend on the state of the base branch.