Configuration reference
Use this reference to look up a key or configure releases and changelogs. For workspace setup, package discovery, and task exclusions, start with the configuration guide.
Find a setting
Section titled “Find a setting”| Key | Required | What it does |
|---|---|---|
members |
no | Literal directories and wildcard patterns relative to the workspace root. Entries containing *, ?, or [ use repository-aware wildcard discovery; only matching directories with a gleam.toml become members. When omitted, every non-gitignored gleam.toml in the repository (outside build/) marks a member. An explicitly empty list is an error. |
exclude.<task> |
no | Member-path globs omitted from that built-in or custom task. The exclusion still applies when a package is named explicitly. |
exclude.@release |
no | The shared package set omitted from changelog, versioning, tagging, publishing, and release CI. The @ prefix is reserved for special keys like this one, so it can never collide with a task name — task names may not start with @. |
exclude.@members |
no | Directories removed from workspace membership entirely — never parsed, graphed, or touched by any command. Useful for committed test fixtures that auto-discovery would otherwise sweep in; also filters explicit members globs. |
tasks.<name> |
no | Custom tasks for trellis run. A task with a built-in’s name (build, test, …) overrides it. needs_deps = true downloads dependencies first. |
publish.package_tags |
no | Which tags a release maintains per package, one entry per tag: exact (the whole version — v1.2.3), major (v1), minor (v1.2). Default: ["exact"]. Must not be empty. |
publish.package_tags_overrides |
no | Per-package overrides: a map of member-path glob to tag list. A member matched by globs resolving to different lists is an error; matches agreeing on the same list are fine. |
publish.exact_tag_format |
no | Template the exact level substitutes into; {name} and {version}. Default: {name}-v{version}. |
publish.series_tag_format |
no | Template every series level substitutes into; must contain {name} and {series}. Default: {name}-v{series}. Use the repository tag keys below for a repository-wide tag. |
publish.repository_tag_package |
with the other two | Anchor package whose manifest version drives one repository-wide moving tag. Must be releasable. |
publish.repository_tag_format |
with the other two | Repository tag template; must contain {series} and no {name}. |
publish.repository_tags |
with the other two | Which repository tags to maintain, from the same vocabulary minus exact. Required alongside repository_tag_package and repository_tag_format; declare all three or none. |
publish.path_dep_requirement |
no | How a workspace path dependency becomes a Hex requirement at publish time, from the dependency’s current version X.Y.Z: minor (default, >= X.Y.Z and < (X+1).0.0), patch (>= X.Y.Z and < X.(Y+1).0), or exact (== X.Y.Z). |
publish.retry |
no | Backoff for Hex rate limits: { attempts, initial_delay, multiplier }. Defaults: 5 attempts, 30s, multiplier 2. |
publish.lifecycle.default |
no | Release lifecycle for a member matched by no packages glob and no legacy exclude.@release glob: workspace, git_only, or hex (default). |
publish.lifecycle.packages |
no | Per-package lifecycle overrides: a map of member-path glob to lifecycle. Takes precedence over exclude.@release. A member matched by globs resolving to different lifecycles is an error; matches agreeing on the same lifecycle are fine. |
changelog.dir |
no | Where fragments and batched version sections live. Default: .changes. |
changelog.kinds |
no | Change kinds and the version bump each implies. The largest bump among a package’s unreleased fragments wins. Replacing the list replaces it entirely. |
changelog.categories |
no | A second grouping axis, rendered above the kind headings. Carries no version bump. Empty by default, which switches the axis off. |
changelog.uncategorized_label |
no | Heading for entries naming no category, rendered last. Default: Other. Read only when categories is set. |
changelog.*_format |
no | Minijinja templates for the rendered changelog: header_format, version_format, category_format, kind_format, change_format. |
changelog.dependency_kind |
no | Kind used for the entries generated when a workspace dependency bumps. Must name one of kinds. |
changelog.dependency_body |
no | Minijinja template for one such entry’s body. Context: dependency, dependency_version, package. |
changelog.strictness |
no | How changelog check treats a changed releasable package with no unreleased fragment: error (default, fails the run), warn, or off. An invalid fragment fails at every setting. |
doctor.shared_dependencies |
no | How doctor treats packages disagreeing on a shared external dependency: warn (default), error, or off. |
Release lifecycle
Section titled “Release lifecycle”exclude.@release is a single on/off switch: a package either participates in
the full release pipeline or is invisible to all of it. Real monorepos often
have packages in between: versioned and tagged in git, but never meant for
Hex, or not ready to release at all yet. publish.lifecycle resolves each
member to one of three states:
| Lifecycle | Changelog/version | Git tags/releases | Hex publish |
|---|---|---|---|
workspace |
no | no | no |
git_only |
yes | yes | no |
hex (default) |
yes | yes | yes |
[tools.trellis.publish.lifecycle]default = "hex"packages = { "packages/experimental/**" = "workspace", "packages/providers/**" = "git_only" }default is the lifecycle for a member matched by no packages glob and no
legacy exclude.@release glob. packages is a map of member-path glob to
lifecycle; a member matched by globs resolving to different lifecycles is a
doctor error, but matching several globs that agree is fine. That’s how a
directory-wide glob and a narrower one inside it can both claim a member.
Resolution order, so a workspace can adopt publish.lifecycle incrementally
alongside an existing exclude.@release:
- Start from
publish.lifecycle.default. - Apply the legacy
exclude.@releasemapping toworkspace, when matched. - Apply an explicit
publish.lifecycle.packagesrule, when matched — this overrides both of the above, which is what lets a package graduate fromworkspacetogit_onlytohexover time without moving directories or rewritingexclude.@release.
--releasable (on list, ci matrix, and elsewhere) still means git_only
or hex — the set that changelog, version, and tag commands operate on.
publish alone needs the finer distinction: it selects hex members only,
and the path-dependency rewrite it computes at publish time only ever
substitutes versions of hex members, so a hex package referencing a
git_only or workspace runtime path dependency fails safely instead of
publishing something unresolvable.
A dependency must be at least as capable as its dependent. hex may
depend only on hex; git_only may depend on git_only or hex; workspace
may depend on anything. doctor’s release_boundary check enforces this for
runtime ([dependencies]) path deps only. A dev-only path dep never ships in
any distribution, so it is exempt regardless of lifecycle.
package_tags lists the tags a release maintains for each package, one entry
per tag. An entry names how much of the version the tag keeps:
| Level | 0.10.3 |
1.2.3 |
Lifecycle |
|---|---|---|---|
exact (default) |
lat_cli-v0.10.3 |
lat_cli-v1.2.3 |
immutable — written once, never rewritten |
major |
lat_cli-v0 |
lat_cli-v1 |
moving — re-pointed at each release in the series |
minor |
lat_cli-v0.10 |
lat_cli-v1.2 |
moving |
The moving ones are what let a consumer track lat_cli-v0.10 once instead of
chasing 0.10.1, 0.10.2, 0.10.3. exact substitutes into
exact_tag_format, the series levels into series_tag_format — the entry name
tells you which. A prerelease belongs to no series, so it gets an exact tag
and moves nothing.
package_tags sets the workspace default and package_tags_overrides sets it
per package, as globs matched against member paths:
[tools.trellis.publish]package_tags = ["exact"] # workspace defaultpackage_tags_overrides = { "packages/lat_cli" = ["exact", "major", "minor"] }Releasing lat_cli twice then leaves the exact tags where they were and moves
the series tags onto the newest release:
$ trellis tag create --push # releasing 0.4.2tagged lat_cli-v0.4.2pushed lat_cli-v0.4.2tagged lat_cli-v0tagged lat_cli-v0.4
$ trellis tag create --push # releasing 0.4.3tagged lat_cli-v0.4.3pushed lat_cli-v0.4.3moved lat_cli-v0moved lat_cli-v0.4force-pushed lat_cli-v0.4A series tag moves only when its own package releases. A commit that does not change the package’s version — another package’s release, a docs change — leaves its tags alone.
Entries name levels rather than templates deliberately. The closed vocabulary
is what keeps a series tag invertible — trellis ci tag-package splits
series_tag_format on {series} to recover the package a pushed tag names —
and what makes it impossible for two entries to collide at a version nobody
has released yet. Free-form templates could pass a uniqueness check at today’s
versions and still write one tag over another later, and series tags are
force-pushed.
Keep {name} in series_tag_format; omitting it is a configuration error.
See Repository tags to configure one shared series tag.
Series tags are the one ref trellis rewrites — they are force-moved and force-pushed on every release. That is also why they never carry a GitHub Release: the release would silently retarget on the next move. See Publishing for how the two lifecycles differ.
Repository tags
Section titled “Repository tags”For a repository consumed through Gleam 1.18 git path dependencies, configure one first-class repository tag rather than overloading the package format:
[tools.trellis.publish]repository_tag_package = "vestibule"repository_tag_format = "v{series}"# Required alongside the other two: the repository tag's levels are stated,# not inferred from package_tags.repository_tags = ["major", "minor"]The anchor package determines both the series and whether the tag moves.
Trellis compares the anchor’s manifest version at HEAD with its manifest
version at the existing repository tag. Changes to other packages do not move
the tag, and package tags do not affect that decision. A
series transition creates the new tag while preserving the old one;
prereleases create no repository tag.
[dependencies]vestibule = { git = "https://github.com/example/monorepo.git", ref = "v0.4", path = "packages/vestibule" }Repository tags are mutable repository metadata. They are force-pushed,
never receive GitHub Releases, and never resolve through publish --tag or
ci tag-package.
A {name}-less series_tag_format is invalid. Restore {name} in the
package tag format and declare the repository tag explicitly:
[tools.trellis.publish]series_tag_format = "v{series}"series_tag_format = "{name}-v{series}"repository_tag_package = "vestibule"repository_tag_format = "v{series}"One difference is deliberate: the repository tag is repository metadata, so
unlike the old shared package tag it does not resolve through ci tag-package
or publish --tag. A single-package repository that routed CI on its
{name}-less tag should keep a {name}-ful package series tag for that and
use the repository tag only for git path-dep consumers.
Changelog configuration
Section titled “Changelog configuration”The changelog engine is native, so there is no second tool to install and no
config file to keep in sync. Changes live as TOML fragments in
.changes/unreleased/; rendering is controlled by small minijinja templates,
each with a context drawn from name, version, date, tag, series,
category, kind, and body:
[tools.trellis.changelog]header_format = "# {{ name }} changelog" # defaultversion_format = "## v{{ version }} - {{ date }}" # defaultkind_format = "### {{ kind }}" # defaultchange_format = "- {{ body }}" # default
# The default kinds, in full. Setting `kinds` replaces the whole list, so# copy this before trimming it — a kind you drop becomes an invalid fragment.kinds = [ { label = "Initial Release", bump = "major" }, { label = "Breaking", bump = "major" }, { label = "Removed", bump = "major" }, { label = "Added", bump = "minor" }, { label = "Changed", bump = "minor" }, { label = "Deprecated", bump = "minor" }, { label = "Fixed", bump = "patch" }, { label = "Performance", bump = "patch" }, { label = "Security", bump = "patch" }, { label = "Dependencies", bump = "patch" },]
# When a package bumps, its workspace dependents bump too and get a generated# entry saying why. Those entries are ordinary entries of one configured kind.dependency_kind = "Dependencies" # defaultdependency_body = "Updated {{ dependency }} to {{ dependency_version }}" # defaultdependency_kind must name one of kinds; that kind’s bump is what a package
bumps by when a dependency bump is the only reason it is being released. If
you replace the default kinds list, include a kind for it or point
dependency_kind at one of yours. Trellis refuses to load otherwise rather
than drop the entries silently.
Categories
Section titled “Categories”A kind says how big a change is. For a package made of several distinct parts —
the subcommands of a CLI, say — readers also want to know which part changed,
and that question has nothing to do with the version bump. categories is a
second vocabulary for exactly that, grouping entries one level above the
kind headings:
[tools.trellis.changelog]categories = ["build", "publish", "doctor"]category_format = "### {{ category }}" # defaultuncategorized_label = "Other" # default## v1.3.0 - 2026-07-11
### build
#### Added
- A `--watch` flag
### publish
#### Fixed
- Retry on a 429 from Hex
### Other
#### Dependencies
- Updated lat_core to 1.3.0A fragment opts in by naming one:
package = "lat_cli"kind = "Added"category = "build"body = "A `--watch` flag"The details worth knowing:
- Categories are opt-in and off by default. With none configured, a version section is a flat list of kind headings.
- A category must be one of
categories, exactly as a kind must be one ofkinds. An unknown one is an invalid fragment:doctorreports it andversionrefuses. - The category stays optional on each fragment. Entries naming none — along
with the generated dependency entries, which belong to no single part of a
package — render last under
uncategorized_label. - Kind headings drop to
####while the axis is on, since categories now occupy###. Settingkind_formatyourself overrides that. Keep both below##, which the release notes extractor reads as the start of a version section. - Empty categories are skipped, so a release touching one area shows one heading.
Each package’s CHANGELOG.md is a generated file: the source of truth is the
version sections stored under .changes/<package>/, and version apply
reassembles the changelog from them, newest first. A package that already had
a changelog before adopting trellis keeps it — see
adopting an existing changelog.
Shared dependency agreement
Section titled “Shared dependency agreement”Nothing outside trellis notices when packages drift apart on an external
dependency they share: lat_core requiring gleam_stdlib >= 0.44.0 while
lat_cli requires >= 0.60.0. doctor catches it:
$ trellis doctorwarning: packages disagree on `gleam_stdlib`: `>= 0.44.0` (lat_core) vs`>= 0.60.0` (lat_cli). Requirements are compared as written, so whitespacecountsRequirements are compared as written, never parsed as ranges, so >= 1.0
and >=1.0 read as divergent. Path dependencies are out of scope, since they
carry no requirement to agree on, and lockfile drift already covers them.
Divergence is sometimes intended, so this warns rather than failing by default:
[tools.trellis.doctor]shared_dependencies = "error" # warn (default), error, or offThere is no doctor --fix for it: which requirement to unify on is a judgment
call.
Removed and unrecognized keys
Section titled “Removed and unrecognized keys”Every key above is snake_case, matching gleam.toml’s own settings and Gleam
itself. Through v0.7.0 they were kebab-case. Those spellings are now errors
that name the replacement:
$ trellis doctorerror: in gleam.toml: `publish.series-tag-format` was removed; use`publish.series_tag_format`A key that is not recognized in any spelling is also a warning, since it may belong to a newer trellis than the one CI has pinned:
$ trellis doctorwarning: [tools.trellis] key `publish.tag_frmat` is not recognized and is beingignored; it may belong to a newer trellisThe free-form tables — exclude, tasks, publish.package_tags_overrides,
and publish.lifecycle.packages — take keys you choose, hyphens and all, and
are never reported.
After changing a setting, verify the workspace with
trellis doctor.