Publishing
Publishing a workspace means turning pending changelog fragments into version bumps, recording what shipped as git tags, and pushing packages to Hex in dependency order with workspace path dependencies rewritten to real Hex requirements. Trellis covers each step, and every step is idempotent, so re-running a partially failed release is safe.
trellis release pr [--base <branch>] [--branch <branch>]trellis tag plan [--json]trellis tag create [--push] [--github-release]trellis publish <pkg | --tag <tag> | --all-untagged> [--dry-run]trellis lockfile refresh [--package <pkg>]Tag format, path-dep rewriting, and Hex retry policy are configured under
[tools.trellis.publish]:
[tools.trellis.publish]exact_tag_format = "{name}-v{version}" # lat_core-v1.2.0series_tag_format = "{name}-v{series}" # lat_core-v1.2, lat_cli-v0.4# Which tags a release maintains, one entry per tag. "exact" keeps the whole# version and is immutable; "major" (v1) and "minor" (v1.2) truncate it and# move. Default ["exact"].package_tags = ["exact"]# Per-package overrides, keyed by a member-path glob.package_tags_overrides = { "packages/lat_cli" = ["exact", "major", "minor"] }# How a path dep is rewritten to a Hex requirement at publish time, from the# dependency's current version X.Y.Z:# minor → ">= X.Y.Z and < (X+1).0.0" (default)# patch → ">= X.Y.Z and < X.(Y+1).0"# exact → "== X.Y.Z"path_dep_requirement = "minor"retry = { attempts = 5, initial_delay = "30s", multiplier = 2 }
repository_tag_package = "lat_cli" # anchor packagerepository_tag_format = "repo-v{series}" # repo-v0.4# All three repository keys are required together, or omitted together.repository_tags = ["major", "minor"] # repo-v0 and repo-v0.4The release PR
Section titled “The release PR”trellis release pr turns pending fragments into a release pull request: it
runs version apply on a release branch (default release/pending), commits
the bumps, force-pushes (so the branch is regenerated on each run), and
creates or updates the PR via the GitHub API. It needs a token: GITHUB_TOKEN
(ambient in GitHub Actions), GH_TOKEN, or a logged-in gh CLI as the
fallback (gh auth token). The body carries the bump table
and each package’s new CHANGELOG section. It requires a clean working tree
and is a no-op when there are no fragments.
tag plan lists the tags the current versions call for and don’t have yet.
tag create reconciles them in topological order; --push pushes them, and
--github-release (which implies --push and needs the same GitHub token
as release pr) also creates a GitHub Release per exact tag, with the
matching CHANGELOG section as the body. --dry-run reports every tag, push,
and release it would perform without doing any of them.
Each entry in a package’s package_tags falls into one of two lifecycles:
- Exact tags (
{name}-v{version}) are immutable. They are created once, fetched when origin already has them, and never rewritten. Local and remote disagreeing about what one names is an error, not something to reconcile. - Series tags (
{name}-v{series}) move. Releasing a new version in the series force-moves its tag to the release commit and force-pushes it, so consumers can tracklat_cli-v0.4and follow the series instead of chasing patch tags.trellis pingives git-dependency consumers a reviewable way to do exactly that. The signal is the package’s manifest version stored at the tag, so a commit that does not release the package, such as another package’s release or a docs change, leaves its series tags alone. The series is derived from the version; see series tags. - Repository tags are independent of any package’s list. Trellis moves the configured tag only when the anchor package’s manifest version differs from the version at that tag. A series change creates a new tag and leaves the prior series tag intact.
$ trellis tag planlat_core: 1.2.0 needs tag lat_core-v1.2.0lat_cli: 0.4.3 needs tag lat_cli-v0.4.3lat_cli: 0.4.3 moves tag lat_cli-v0.4
$ trellis tag create --github-releasetagged lat_core-v1.2.0pushed lat_core-v1.2.0tagged lat_cli-v0.4.3pushed lat_cli-v0.4.3moved lat_cli-v0.4force-pushed lat_cli-v0.4--github-release skips every moving tag. A release attached
to a moving tag would silently retarget on the next release, so only immutable
per-version tags carry one.
With no exact entry a package has no per-version tags at all, so there is no
tag to trigger a publish from: run trellis publish --all-untagged on
release-PR merge instead. Passing a series tag to publish --tag is an error,
because it names a package but no particular version.
Repository tags identify repository snapshots rather than package
releases, so neither publish --tag nor ci tag-package resolves them.
Publishing to Hex
Section titled “Publishing to Hex”publish selects members whose release
lifecycle is hex. A named package
and --tag <tag> both refuse a workspace or git_only package, and
--all-untagged only ever considers hex members. It runs, per package and
in dependency order:
- Idempotency check. One Hex API query; versions already published are skipped.
- Validation.
gleam format --check,gleam build --warnings-as-errors, andgleam test, run against the original manifest with path deps intact. - Path-dep rewrite. Computed from the graph, never hand-listed: each
workspace path dependency becomes the Hex requirement derived from that
dep’s current version, per
path_dep_requirement. The version map only ever holdshexmembers, so a path dependency on agit_onlyorworkspacemember fails the rewrite rather than publishing something Hex could never resolve. - Publish.
gleam publish --yes. - Restore. The original
gleam.tomlcomes back even when publishing fails; the repo never shows rewritten files.
Every Hex-touching step runs under the configured retry backoff policy.
# Publish the package a pushed tag points at:trellis publish --tag "$GITHUB_REF_NAME"
# Or publish everything whose version isn't on Hex yet, in one run:trellis publish --all-untagged
# See what would be published (and rewritten) without doing it:trellis publish --all-untagged --dry-run--tag lat_core-v1.2.0 resolves a pushed tag to its package and refuses to
publish if the tag version doesn’t match gleam.toml. --all-untagged
publishes everything not yet on Hex, enabling a single publish run per
release instead of one per tag.
Dev-only path deps to a non-hex package are left alone, because Hex doesn’t
ship dev dependencies. A regular [dependencies] path dep to a package whose
lifecycle is less capable than its dependent’s refuses to publish, and
doctor catches that boundary on every PR; see Release
lifecycle.
Lockfiles
Section titled “Lockfiles”lockfile refresh --package <pkg> scopes gleam deps download to one package,
with retry. Refreshing the whole workspace at once is what gets a runner
rate-limited by Hex.
Two release shapes
Section titled “Two release shapes”Both of these flows are supported; the difference is policy, not tooling:
| Shape | Flow |
|---|---|
| Tags as trigger | Release PR merges → tag create --push → each pushed tag triggers a workflow running publish --tag "$GITHUB_REF_NAME". |
| Tags as record | Release PR merges → publish --all-untagged (one run, topologically ordered) → tag create --github-release records what shipped. |
Ready-to-paste workflows for both are in the CI recipes.