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 — 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 tag with the matching CHANGELOG section as the
body.
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 pinlat_cli-v0.4and follow the series instead of chasing patch tags. The signal is the package’s manifest version stored at the tag, so a commit that does not release the package — another package’s release, 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
— 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 — --package <name> and --tag <tag> refuse a workspace or
git_only package by name, 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,gleam 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 — 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.