Skip to content

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.

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.

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
gleam.toml
[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:

  1. Start from publish.lifecycle.default.
  2. Apply the legacy exclude.@release mapping to workspace, when matched.
  3. Apply an explicit publish.lifecycle.packages rule, when matched — this overrides both of the above, which is what lets a package graduate from workspace to git_only to hex over time without moving directories or rewriting exclude.@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:

gleam.toml
[tools.trellis.publish]
package_tags = ["exact"] # workspace default
package_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:

Terminal window
$ trellis tag create --push # releasing 0.4.2
tagged lat_cli-v0.4.2
pushed lat_cli-v0.4.2
tagged lat_cli-v0
tagged lat_cli-v0.4
$ trellis tag create --push # releasing 0.4.3
tagged lat_cli-v0.4.3
pushed lat_cli-v0.4.3
moved lat_cli-v0
moved lat_cli-v0.4
force-pushed lat_cli-v0.4

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

For a repository consumed through Gleam 1.18 git path dependencies, configure one first-class repository tag rather than overloading the package format:

gleam.toml
[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.

gleam.toml
[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:

gleam.toml
[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.

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" # default
version_format = "## v{{ version }} - {{ date }}" # default
kind_format = "### {{ kind }}" # default
change_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" # default
dependency_body = "Updated {{ dependency }} to {{ dependency_version }}" # default

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

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 }}" # default
uncategorized_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.0

A fragment opts in by naming one:

.changes/unreleased/lat_cli-a-watch-flag.toml
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 of kinds. An unknown one is an invalid fragment: doctor reports it and version refuses.
  • 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 ###. Setting kind_format yourself 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.

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:

Terminal window
$ trellis doctor
warning: packages disagree on `gleam_stdlib`: `>= 0.44.0` (lat_core) vs
`>= 0.60.0` (lat_cli). Requirements are compared as written, so whitespace
counts

Requirements 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 off

There is no doctor --fix for it: which requirement to unify on is a judgment call.

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:

Terminal window
$ trellis doctor
error: 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:

Terminal window
$ trellis doctor
warning: [tools.trellis] key `publish.tag_frmat` is not recognized and is being
ignored; it may belong to a newer trellis

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