JSON output
Every command that emits JSON carries a schema field naming the payload and
its major version:
{ "schema": "trellis.list/1", "packages": [ { "name": "lat_core", "version": "1.2.0", "path": "packages/lat_core", "lifecycle": "hex", "releasable": true, "dependencies": [], "dependents": ["lat_mid", "lat_cli"] } ]}An unrecognized schema means an unsupported version — assert on it.
What “stable” permits
Section titled “What “stable” permits”| Change | Allowed? |
|---|---|
| Adding a field | Yes, without a version bump — ignore fields you don’t know. |
| Adding an array element | Yes, per the command’s documented selection rules. |
Renaming, removing, or retyping a field; changing a documented enum’s values (kind, action, tag_kind) |
No — bumps the major (trellis.list/1 → /2). |
Not guaranteed: key order (parse as JSON) or whitespace (some commands pretty-print, some emit one line for shell-variable capture).
Human-readable output isn’t covered. It’s written for a terminal, is
TTY-dependent, and changes freely. Use --json if you’re parsing.
The payloads
Section titled “The payloads”| Command | Schema and top-level keys |
|---|---|
list --json |
trellis.list/1{schema, packages[]} |
info <pkg> --json |
trellis.info/1{schema, name, version, path, lifecycle, releasable, dependencies[], dependents[]} |
graph --format json |
trellis.graph/1{schema, nodes[], edges[]} |
run <task> --json |
trellis.run/1{schema, ok, task, target?, results[]} |
exec -- <cmd> --json |
trellis.exec/1{schema, ok, command[], results[]} |
changelog check --format json |
trellis.changelog_check/2{schema, ok, strictness, has_entries, needs_entry, invalid_fragments[], packages[], preview} |
version plan --json |
trellis.version_plan/1{schema, bumped[], fragments_retained} |
version apply --json |
trellis.version_apply/1{schema, bumped[], lockfiles[], adopted[], fragments_retained} |
tag plan --json |
trellis.tag_plan/2{schema, tags[]} |
ci tag-package --json |
trellis.ci_tag_package/1{schema, name, path, version, tag_kind, tag_version | tag_series} |
doctor --format json |
trellis.doctor/1{schema, ok, packages, configless, auto_members, findings[], fixes[], applied[], package_lifecycles[]} |
Notes:
list’spackages[]entries andinfo’s top level share a shape.lifecycleis the resolved release lifecycle (workspace,git_only, orhex);releasableis a derived boolean (lifecycle != workspace), kept for compatibility with the git_only+hex meaning--releasablehas always had.graphnodes also carry both.version plan/version applyshare thebumped[]shape (name,current,next,fragments,updated_dependencies[]).fragments_retainedis true only under--pre <label>.run/execshare theresults[]shape —package,path,status(success|failed|skipped),duration_ms. A failed entry also carriesexit_codeandcommand.duration_ms’s presence is stable, its value isn’t.ci tag-packagereportstag_kind: exactwithtag_version, ortag_kind: serieswithtag_series— the other key is absent, not null.tag planentries usekind: repository_seriesfor an anchored repository tag. The entry retains the anchor package’snameandversion; this kind is never returned by package-orientedci tag-package.changelog check’spreviewfield is always present and a string, but its Markdown prose can change without a version bump.needs_entryreports whether an entry is missing;okreports whatchangelog.strictnessmade of that, and matches the exit code.--jsonis a deprecated alias for--format json.
--json moves package output to stderr. run/exec normally stream
package output to stdout; under --json it moves to stderr so stdout
carries only the payload. -q suppresses the stream.
doctor findings
Section titled “doctor findings”package_lifecycles is an array of {name, lifecycle}, one per member in
workspace order, alongside the numeric packages count. doctor’s text
summary renders the same data as compact counts:
ok: 4 package(s) (1 workspace, 0 git_only, 3 hex), 0 warning(s).
findings[] carries one entry per problem:
{ "check": "lockfile_drift", "severity": "error", "message": "packages/lat_mid/manifest.toml locks `lat_core` at 1.1.0 …", "file": "packages/lat_mid/manifest.toml", "package": "lat_mid", "fixable": true}severity is error or warning (only error fails the run). file/
package are absent, not null, when a finding isn’t tied to one file. message
is prose, not stable — branch on check:
check |
Raised when |
|---|---|
member_glob |
a members glob is invalid, unreadable, or matches nothing |
package_manifest |
a package’s gleam.toml is missing, unparseable, carries a [tools.trellis] table, or repeats another package’s name |
path_dependency |
a path dependency escapes the workspace or names no package in it |
dependency_cycle |
the graph is cyclic |
workspace_config |
the root [tools.trellis] table is itself wrong |
exclusion_glob |
a task-exclusion, tag-mode-override, or publish.lifecycle.packages glob matches no member |
release_boundary |
a package’s runtime path dependency is less capable than it is (a hex package depending on git_only/workspace, or git_only on workspace) |
tag_collision |
two releasable packages produce the same tag |
lockfile_drift |
a manifest.toml locks a workspace-internal dep at a stale version |
changelog_missing |
a releasable package has no CHANGELOG.md |
changelog_unreadable |
a CHANGELOG.md exists but could not be read |
changelog_behind |
a package’s version is behind the newest one in its changelog |
changelog_adoption |
a package has pre-trellis changelog history the next release will adopt |
package_version |
a package’s version is not valid semver |
changelog_fragment |
an unreleased fragment does not parse, or names an unknown package or kind |
toolchain |
the gleam on PATH disagrees with the .tool-versions pin |
shared_dependency |
packages require different versions of the same external dependency |
New check values may appear without a bump — handle unrecognized ones by
reporting them, not dropping them.
--format github is not JSON. It emits workflow commands
(::error title=lockfile_drift,file=packages/lat_mid/manifest.toml::…) so
findings land on a PR’s Files tab. See CI recipes.
Two exceptions
Section titled “Two exceptions”ci matrix and ci outputs carry no schema — their shapes are
dictated by GitHub Actions, not trellis.
ci matrix emits one key, fed straight to strategy.matrix via
fromJSON():
{"include":[{"name":"lat_core","path":"packages/lat_core","version":"1.2.0"}]}A schema sibling here would be a bug: every top-level key besides
include becomes another matrix axis.
ci outputs emits key=value lines for $GITHUB_OUTPUT, each value a JSON
array of strings:
packages=["lat_core","lat_mid","lat_cli","lat_example"]projects=["lat_core","lat_mid","lat_cli","lat_example"]releasable=["lat_core","lat_mid","lat_cli"]version_files=["packages/lat_core/gleam.toml","packages/lat_mid/gleam.toml","packages/lat_cli/gleam.toml"]tags=["lat_core-v1.2.0","lat_mid-v0.5.1","lat_cli-v0.4.3"]series_tags=[]projects duplicates packages and is deprecated; it goes away at 1.0.
changelog check --format github emits the same style of lines — scalars
plain, arrays as JSON, and the multi-line preview in GitHub’s heredoc form.
It is the trellis.changelog_check/2 data in a shape the runner parses, not a
payload of its own, so it carries no schema either.
See CI recipes for how these are consumed.
Enforcement
Section titled “Enforcement”Every payload is snapshotted in trellis’s test suite, so a change to a stable shape fails CI before it can ship.