Skip to content

Latest commit

 

History

90 Commits

Folders and files

NameName
Last commit message
Last commit date
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

trellis

Crates.io Version GitHub Release Date

A workspace CLI for Gleam monorepos. A trellis is the frame a lattice grows on.

Gleam has no native workspace concept: gleam build, gleam test, and gleam publish each operate on a single package directory. Multi-package repos end up hand-building workspace features out of bash loops, YAML glue, and duplicated config. Trellis replaces that glue with one binary that runs identically locally and in CI.

The design principle:

Configure nothing that can be derived. Verify anything that must be duplicated.

Everything trellis knows comes from one file format the ecosystem already uses: gleam.toml. The workspace root's manifest carries a [tools.trellis] table (member globs and options); each member's manifest supplies its name, version, and path dependencies. The dependency graph (topological order, publish order, change impact, path-dep rewrite maps) is computed, never declared.

Throughout, a package is one Gleam package, a directory with its own gleam.toml, and a member is a package belonging to the workspace. The second word appears where membership itself is the point, as in the members key and the @members exclusion.

See docs/DESIGN.md for the full design.

Status

The full rollout plan is implemented: the workspace model plus list, graph, info, run, exec, init, doctor, ci, changelog, version, release, tag, publish, lockfile, pin, and completions, with prebuilt release binaries for distribution.

Installation

Trellis ships as a single prebuilt binary, the same distribution model as just, changie, and ratchet, so it installs in CI in about a second with zero runtime dependencies. Releases are built and published by cargo-dist, with SLSA build provenance attestations.

Shell installer (Linux and macOS):

curl --proto '=https' --tlsv1.2 -LsSf https://github.com/tylerbutler/trellis/releases/latest/download/trellis-gleam-installer.sh | sh

PowerShell installer (Windows):

powershell -ExecutionPolicy Bypass -c "irm https://github.com/tylerbutler/trellis/releases/latest/download/trellis-gleam-installer.ps1 | iex"

Homebrew:

brew install tylerbutler/tap/trellis

mise / asdf (via the github backend), which is how a consuming workspace pins trellis in .tool-versions alongside its other tools:

mise use "github:tylerbutler/trellis@0.13.0"

From source: on crates.io the crate is trellis-gleam (trellis was taken), but the installed binary is named trellis either way:

cargo install trellis-gleam
# or straight from the repository:
cargo install --git https://github.com/tylerbutler/trellis

Prebuilt archives for every target are on the releases page. Pin a specific version in CI by replacing latest/download with download/v0.13.0 in the installer URL.

Shell completions

Add one line to your shell's startup file. bash, zsh, fish, powershell, and elvish are supported:

eval "$(trellis completions zsh)"

Completions are computed by the binary as you type, so they offer real package names, task names, and changelog kinds from the workspace you're in. Evaluate the snippet on startup rather than saving it to a file: it talks to trellis over an interface that can change between releases. See installation for the other shells and for man pages, which ship in the release archives under man/.

Update checks

Interactive commands print a one-line notice to stderr when a newer trellis has been published to crates.io. Successful checks are cached for a day, and the notice appears only after a fresh check. The check is best-effort, capped at a short timeout and silent on any error, so it never slows a command or changes its exit status. It runs only when stderr is a terminal, so scripts and structured output are never touched. It is additionally skipped in CI and when DO_NOT_TRACK or TRELLIS_NO_UPDATE_CHECK is set in the environment, and --no-update-check suppresses it for a single invocation.

Configuration

Configuration is optional. With no configuration at all, the git repository root is the workspace root and every non-gitignored gleam.toml (outside build/) marks a member. A fresh Gleam monorepo, or a single-package repo, works with zero setup.

When you need to configure something, a [tools.trellis] table in a gleam.toml marks the workspace root. There is no separate config file. The root manifest may be config-only, or a regular gleam package that also anchors the workspace. Every key is optional, including members: omit it to keep auto-discovering members from git while configuring everything else:

# gleam.toml at the repo root
[tools.trellis]
# Optional: pin membership to explicit globs instead of auto-discovery.
members = ["packages/*", "examples/*"]
# Exclusions are globbed against member paths and scoped by task. The
# reserved `@release` key covers changelog, versioning, tagging, and
# publishing; `@members` removes directories from membership entirely
# (e.g. committed test fixtures that auto-discovery would sweep in); the
# `@` prefix keeps them from ever colliding with a task name.
exclude = { docs = ["examples/*"], "@release" = ["examples/*"] }

# Custom tasks for `trellis run <name>`. Built-in verbs (build, test, check,
# format, docs, deps, clean) need no declaration.
[tools.trellis.tasks.lint]
command = "gleam run -m glinter"
needs_deps = true            # run `gleam deps download` first if not cached

[tools.trellis.publish]
# Which tags a release maintains for each package, one entry per tag:
#   exact → the whole version (v1.2.3), immutable   (the default)
#   major → v1, minor → v1.2, both moving, for consumers who pin a series
#           instead of chasing patch tags
package_tags = ["exact"]
# Per-package overrides, keyed by a member-path glob.
package_tags_overrides = { "packages/lat_cli" = ["exact", "major", "minor"] }
# `exact` substitutes into the first, every series level into the second.
# {version} and {series} are derived, never written by hand.
exact_tag_format = "{name}-v{version}"
series_tag_format = "{name}-v{series}"

# Optional repository-wide moving tag, independent of any package's list.
# All three keys are required together, or leave all three out.
repository_tag_package = "lat_cli"
repository_tag_format = "v{series}"
repository_tags = ["major", "minor"]

Path dependencies between members define the graph; cycles and path deps escaping the workspace are rejected, and a [tools.trellis] table in a member manifest is a doctor error (it would hijack root discovery).

Member entries containing *, ?, or [ are wildcard patterns. In a Git repository, wildcard discovery honors repository .gitignore files at every level and .git/info/exclude. It does not read global core.excludesFile rules, generic .ignore files, or automatically exclude hidden paths, so results do not depend on machine-local Git configuration. Outside a Git repository, Git ignore rules do not apply. Traversal follows symlinks but never enters .git, and only matching directories with a gleam.toml become members.

Entries without wildcard metacharacters are resolved directly, so an explicit literal path remains included even when Git ignores it. [tools.trellis.exclude] is a separate post-discovery filter: task and @release exclusions do not control traversal.

Commands

Every command works from anywhere inside the workspace (the root is found by walking up to the first gleam.toml with a [tools.trellis] table, like git or cargo — member manifests along the way are skipped). Without a [tools.trellis] table anywhere, the git repository root is the workspace root and members are auto-discovered.

Global flags

-C, --directory <DIR>    Run as if started in this directory
    --color <WHEN>       auto (default), always, or never
-q, --quiet              Drop the per-package stream and the summary table
-v, --verbose            Trace every command trellis shells out to, on stderr
    --no-update-check    Skip the release check for this invocation

These accept placement anywhere, before or after the subcommand.

--color auto follows the terminal and respects NO_COLOR (at any value) and CLICOLOR=0; always and never override that detection in either direction, so --color always survives a pipe. Live progress rows stay tied to terminal detection either way — forcing color into a pipe does not start drawing spinners into it.

-q and -v are mutually exclusive. -q changes nothing about exit codes: errors still go to stderr and a failing task still fails the command. -v writes a + trace to stderr for each gleam and git invocation, naming the directory it ran in, and for each GitHub API request as + METHOD url.

Exit codes

Every command uses the same four codes. Branch on 1 versus 3 to tell a workspace that has problems from a broken environment.

Code Meaning Examples
0 Success, no findings doctor found nothing; every task passed
1 Ran correctly, found problems doctor findings, missing changelog fragments, a failed task
2 Usage error Unknown flag, missing argument, bad subcommand
3 Trellis itself could not run Unparseable config, not a git repository, missing gleam, no GitHub token, Hex unreachable after retries

A failed task exits 1; trellis does not propagate the child's exit code. See Compatibility for what else the version number promises.

Introspection

trellis list [--json] [--since <ref>] [--with-dependents] [--releasable]
trellis graph [--format text|dot|mermaid|json]
trellis info <package> [--json]

list prints members in topological order, dependencies first. --since origin/main filters to packages owning changed files (committed, uncommitted, and untracked); --with-dependents adds the reverse-dependency closure. This is the primitive behind "only test what a PR touched."

JSON output

Every --json payload carries a schema field naming the payload and its major version ("schema": "trellis.list/1"), so a workflow can assert on the shape it was built against and detect a breaking change instead of silently producing wrong results. Fields may be added without a bump; renaming, removing, or retyping one bumps the major. Human-readable text output carries no such guarantee. trellis ci matrix and ci outputs are the two exceptions — their shapes are dictated by GitHub Actions.

Full details: docs/json-output.

Task running

trellis run <task> [pkgs...] [--since <ref>] [--with-dependents]
                   [--target erlang|javascript|all] [--strict] [--check]
                   [--serial] [--keep-going] [--jobs N] [--json]
trellis exec [pkgs...] [--since <ref>] [--serial] [--keep-going] [--json]
             -- <command...>

Scheduling is graph-parallel by default: a package runs as soon as its workspace dependencies have finished, up to --jobs N at once. Output is streamed with a pkg ▏ prefix and a summary table names any failures. In an interactive terminal, active packages remain visible as live progress rows and package names use stable, hash-derived colors. Pipes and CI receive plain text without terminal control sequences. --target all runs the task once per compile target. --serial runs one package at a time in dependency order.

--json reports per-package results — status, exit code, duration, and the command that failed — so a workflow can record what passed without parsing output written for a person. Package output moves to stderr so stdout carries nothing but the payload, and the summary table is replaced by it. This is what makes "only test what a PR touched" reportable as well as doable:

trellis run test --since origin/main --json | jq -r '.results[] | select(.status != "success") | .package'

Built-in tasks map 1:1 onto gleam verbs: build, test, check, format (--check variant), docs, deps, clean. A [tools.trellis.tasks] entry with the same name overrides a built-in. Any built-in or custom task can exclude member paths through its key in exclude; exclusions still apply when packages are named explicitly. For larger maps, use a table:

[tools.trellis.exclude]
docs = ["examples/*", "packages/internal-*"]
"@release" = ["examples/*"]

The reserved @release key defines the package set used by changelog, version, tag, and publish commands — read as the workspace state of the finer-grained release lifecycle described next — and @members removes directories from workspace membership entirely (in both auto-discovered and explicit-members modes). Special keys use a @ prefix so they can never collide with a task name — trellis rejects any task named with that prefix.

Release lifecycle

[tools.trellis.publish.lifecycle]
default = "hex"
packages = { "packages/experimental/**" = "workspace", "packages/providers/**" = "git_only" }

Each member resolves to one of three lifecycles: workspace (build and test only, with no changelog, version, tag, or publish), git_only (changelog, version, and git tags/releases, but never published to Hex), or hex (the full pipeline, and the default). default sets the workspace-wide fallback; packages overrides it per member, keyed by member-path glob. A member matched by globs that resolve to different lifecycles is a doctor error, but globs agreeing on the same lifecycle are fine. exclude.@release keeps resolving matching packages to workspace, but an explicit packages rule for the same member takes precedence, so a package can graduate from workspace to git_only to hex without moving directories.

--releasable (on list, ci matrix, and elsewhere) still means git_only or hex; publish narrows further, since only hex packages ever reach Hex. doctor additionally enforces that a runtime path dependency is at least as capable as its dependent: hex may depend only on hex, git_only may depend on git_only or hex, and workspace may depend on anything. Dev-only path deps are exempt, since they never ship in any distribution.

Changelog & versioning

trellis changelog new [--package <pkg>] --kind <kind> --body <text>
                      [--category <category>]
trellis changelog check --base <ref> [--head <ref>] [--format text|json|github]
                       [--strictness error|warn|off]
trellis version plan  [--bump <level>|<pkg>=<level>] [--set <pkg>=<version>]
                      [--pre <label>|none] [--json]
trellis version apply [--bump <level>|<pkg>=<level>] [--set <pkg>=<version>]
                      [--pre <label>|none] [--json]

The changelog engine is native, so there is no second tool to install and no config file to keep in sync. Changes are recorded as TOML fragments in .changes/unreleased/package, kind, body, and an optional category; changelog new writes one, non-interactively, which suits CI and agents as well as shells. changelog check maps a base...head diff to packages and fails if a changed releasable package has no unreleased fragment. How much that costs is set by changelog.strictness: error by default, warn to report without failing, off not to check (an invalid fragment fails either way). --format json emits the payload, including a markdown preview for a PR sticky comment; --format github emits the same facts as key=value lines for $GITHUB_OUTPUT, so a workflow can post that comment without a jq pipeline.

version plan computes each pending package's next version from its fragments' kinds (the largest bump wins; kinds and their bumps are configurable under [tools.trellis.changelog]). version apply renders each package's version section (minijinja templates, see below), stores it under .changes/<package>/, reassembles the package's CHANGELOG.md newest-first, bumps gleam.toml with a surgical TOML edit rather than a regex, and finally patches each member's manifest.toml so locked workspace-internal deps match. Zero Hex network calls throughout. Invalid fragments (unknown package or kind, empty body, unparseable TOML) are hard errors for plan/apply: silently dropping a change is the drift trellis exists to prevent.

A bump ripples to the bumping package's workspace dependents. When lat_core goes 1.2.0 → 1.3.0, every package that path-depends on it is released too — by a patch, and with a generated changelog entry saying why:

lat_core: 1.2.0 -> 1.3.0 (1 fragment(s))
lat_mid: 0.5.0 -> 0.5.1 (dependencies: lat_core)
lat_cli: 0.3.1 -> 0.3.2 (dependencies: lat_core, lat_mid)

A dependent needs no fragment of its own to ripple, and a package that has one keeps its own, larger bump. Ripples follow [dev-dependencies] path deps as well as [dependencies]. Packages excluded by @release never bump, and a ripple stops at one rather than skipping past it to its dependents. This is a correctness requirement rather than a convenience; why.

Overriding the derived version

The fragment kinds do not always determine the version you want. Three flags override them, accepted identically by plan and apply so an override is previewable before it is applied:

trellis version apply --bump major                 # whole plan
trellis version apply --bump lat_core=major        # one package
trellis version apply --set lat_core=1.0.0         # an exact version

--set beats a per-package --bump, which beats a workspace-wide one, which beats the derived level. Naming a package in both --bump and --set, naming one that isn't a releasable member, or pinning a version that isn't ahead of the current one are all errors before anything is written.

Prereleases

--pre <label> cuts a release candidate, and repeating it advances the counter within the same base version:

trellis version apply --set lat_core=1.0.0 --pre rc   # 1.0.0-rc.1
trellis version apply --pre rc                        # 1.0.0-rc.2
trellis version apply --pre none                      # 1.0.0

Fragments survive a prerelease. The candidate renders its changelog section, but the fragments behind it stay in .changes/unreleased/: they are still unreleased as far as 1.0.0 is concerned, and retiring them at rc.1 would leave the final release with nothing to say. --pre none promotes the candidate to its final version and consumes them. This means an entry appears twice in CHANGELOG.md: once under the RC that shipped it, once under the final. version --json reports fragments_retained so a workflow can tell the two cases apart.

A prerelease labels the whole plan, rippled dependents included, so the workspace moves as one coherent candidate. A package that only gained fragments after the RC was cut still bumps normally under --pre none, so a late arrival never blocks the promotion.

Once a package is at a prerelease, a plain version apply is an error rather than a silent bump to the next release; resolving the cycle has to be explicit. A prerelease belongs to no series, so it moves no series tag; exact tags apply as usual, and Hex accepts prerelease versions.

Rendering is controlled by minijinja templates in [tools.trellis.changelog], each with a small 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

# Generated ripple entries are ordinary entries of one configured kind, so
# they sort and render like any other. `dependency_kind` must name one of
# `kinds`; its bump is what a package bumps by when a dependency bump is the
# only reason it is being released.
dependency_kind = "Dependencies"                                  # default
dependency_body = "Updated {{ dependency }} to {{ dependency_version }}"  # default

kinds sets the change kinds and the bump each implies; setting it replaces the whole default list rather than adding to it, so copy the list before trimming it. A kind you drop turns every fragment naming it invalid. An optional categories list adds a second grouping axis above the kind headings, carrying no bump. Both are on configuration.

Each package's CHANGELOG.md is a generated file: the source of truth is the version sections under .changes/<package>/, and apply reassembles the changelog from them. A package that already had a changelog when it adopted trellis keeps it: on its first release, whatever sits below the header is captured verbatim as one section under .changes/<package>/, so regenerating preserves the history rather than replacing it. doctor reports this before release day, and doctor --fix does the capture up front.

Release & publish

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>]

release pr turns pending changelog 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 each run), and creates the PR through the GitHub REST API, or updates it when one is already open. The body carries the bump table and each package's new CHANGELOG section. Requires a clean working tree and a token from GITHUB_TOKEN (ambient in GitHub Actions), GH_TOKEN, or a logged-in gh CLI; 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, optionally pushing them and creating GitHub Releases — through the same API and token as release pr — with the matching CHANGELOG section as the body. release bootstrap is the same reconciliation without a version bump, for adopting trellis on a repository that already has the versions it wants but no tags.

package_tags lists the tags a release maintains per package, one entry per tag, each naming how much of the version it keeps: exact keeps the whole thing ({name}-v1.2.3), major truncates to one part ({name}-v1), minor to two ({name}-v1.2). It defaults to ["exact"], and package_tags_overrides sets it per package by member-path glob. Entries name levels rather than templates so that ci tag-package can resolve a pushed tag back to its package.

The level also picks the lifecycle. exact tags are immutable, created once and never rewritten, and substitute into exact_tag_format. Series levels move: releasing a new version in the series force-moves the tag to the release commit and force-pushes it, substituting into series_tag_format. A commit that does not change the package's version leaves its series tags where they are, so one package's release never drags another's forward. Because a moving tag names no particular version, it never carries a GitHub Release and publish --tag refuses it; ci tag-package still resolves it, and a workspace with no exact entry publishes with --all-untagged.

An optional repository tag is separate from every package's list. repository_tag_package is the anchor: trellis creates repository_tag_format for the anchor's stable series and moves it only when the anchor's manifest version differs from the version stored at that tag. Its repository_tags is stated rather than inherited from package_tags, and all three keys are required together — a partly-written repository tag is an error, not a default. A new series creates a new tag and leaves the old series intact. Repository tags are mutable, never get GitHub Releases, and cannot be passed to publish --tag or ci tag-package.

It replaces the older way of reaching a repository-wide tag: dropping {name} from series_tag_format. That form is deprecated and removed at 1.0, because without the discriminator every package's series tag matches every member and ci tag-package can no longer resolve one. doctor warns on it. To migrate, restore {name} in series_tag_format and declare the three repository tag keys above.

publish runs, per package and in dependency order: an idempotency check against the Hex API (already-published versions are skipped, so re-running a partially failed release is safe), validation (gleam format --check, build --warnings-as-errors, test), then a path-dep rewrite computed from the graph — each workspace path dep becomes the Hex requirement derived from that dep's current version (minor, patch, or exact, per path_dep_requirement) — followed by gleam publish --yes, and finally restoration of the original gleam.toml (the repo never shows rewritten files, even on failure). Every Hex-touching step runs under the configured [tools.trellis.publish] retry backoff policy. --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.

lockfile refresh scopes gleam deps download to one package, with retry. Refreshing the whole workspace at once is what gets a runner rate-limited by Hex. trellis ci tag-package <tag> resolves $GITHUB_REF_NAME to a package name for shell substitution.

Dependency pinning

trellis pin [pkgs...]            # resolve symbolic refs to SHAs, record the intent
trellis pin --update [pkgs...]   # re-resolve each recorded ref to its latest SHA
trellis pin --check [pkgs...]    # fail if a pinned SHA drifted from its tracked ref
trellis pin --unpin [pkgs...]    # restore the symbolic refs

A Gleam git dependency names a ref, and the two values you can give it are wrong in opposite ways. ref = "v0.4" is readable but re-resolves silently when the tag moves, and a bare SHA is reproducible but loses what to bump to. pin keeps both: the ref becomes the commit SHA and the ref it tracks moves into a # trellis:pin comment on the same line, the style ratchet uses for GitHub Actions:

vestibule = { git = "https://github.com/example/monorepo.git", ref = "93deb4c38d4b3f5848681ff7e9d59883db751c67", path = "packages/vestibule" } # trellis:pin v0.4

Refs resolve through git ls-remote, so there is no clone and any host works, and manifest.toml is patched surgically. --update re-resolves each recorded ref, so following a moving tag becomes a reviewable diff instead of a silent re-resolution. --check fails when a pinned SHA is no longer reachable from its tracked ref, which means the ref was force-moved past it; a pin merely behind its ref is not drift. doctor runs the same check as an advisory warning, since re-pinning past a force-move is a supply-chain call --fix must not make.

Validation

trellis doctor [--fix] [--dry-run] [--format text|json|github]

Checks every workspace invariant and reports all problems at once: member globs resolve and parse, path deps stay inside the workspace, the graph is acyclic, task exclusion globs match real members, every package's runtime path deps are available at its release lifecycle, tag formats don't collide, manifest.toml locked versions match workspace-internal gleam.toml versions, no package's version is behind its CHANGELOG, and every unreleased changelog fragment parses and references a valid package, kind, and category. Two checks are advisory warnings rather than errors, because both need the network or the machine's own toolchain: pinned git dependency SHAs still reachable from their tracked refs, and a gleam on PATH matching the .tool-versions pin (enforcing toolchains stays mise/asdf's job). Non-zero exit on any error. Run it on every PR.

Two further checks cover things that must agree because they are duplicated:

Deprecated and unrecognized [tools.trellis] keys. Every trellis config key is snake_case. Through v0.7.0 they were kebab-case; the old spellings are still accepted, and each one is reported so a workspace can migrate at its own pace. A key that is not recognized in any spelling is also a warning, since it may belong to a newer trellis and erroring would make the workspace unloadable under a pinned older one.

Shared external dependencies. Members are checked for agreeing on the non-path dependencies they share — lat_core requiring gleam_stdlib >= 0.44.0 while lat_cli requires >= 0.60.0. Requirements are compared as written, never parsed as ranges, so whitespace counts. Divergence is sometimes intended, so it warns by default:

[tools.trellis.doctor]
shared_dependencies = "warn"   # or "error" to fail CI, "off" to skip

There is no --fix for it: picking a winner is a judgment call.

--fix applies the mechanical remedies (seed a missing CHANGELOG, patch stale locked versions) and re-checks; --dry-run lists them without writing. --format json emits each finding as {check, severity, message, file, package, fixable}, and --format github emits the same findings as workflow commands, so they annotate the changed lines in a PR instead of scrolling past in a log:

- run: trellis doctor --format github

Scaffolding

trellis init

init bootstraps the workspace itself: it writes a [tools.trellis] table into the repository root's gleam.toml, creating a config-only manifest if the root isn't a package, and finishes by running doctor.

The table it writes is nearly empty by design. Its presence is what marks the workspace root; members stay auto-discovered, so members is derivable and therefore not written. What it leaves instead are comments pointing at what can be configured — the part a reference page answers badly. init reports the members it discovered so you can see whether they need narrowing, and refuses if the repository is already a trellis workspace.

$ trellis init
created /repo/gleam.toml
members are auto-discovered; found 2:
  packages/a
  packages/b

CI glue

trellis ci matrix [--since <ref>] [--releasable]
trellis ci outputs

matrix emits a GitHub Actions strategy matrix ({"include":[{"name","path","version"},…]}); with --since it covers only affected packages, dependents included. outputs emits workspace facts as key=value lines for $GITHUB_OUTPUT:

- id: plan
  run: echo "matrix=$(trellis ci matrix --since origin/main)" >> "$GITHUB_OUTPUT"

Development

Standard Rust project: cargo test runs unit tests plus an end-to-end suite against the fixture workspace in tests/fixtures/. cargo fmt and cargo clippy --all-targets are enforced in CI.

assets/man/ and website/src/content/docs/docs/reference.md are generated from the clap definitions — regenerate both with just docs after changing any command, flag, or help string. cargo test fails if they're stale.

Contributor conventions — naming, changelog fragments, and how trellis itself is released — are in AGENTS.md.

License

MIT — see LICENSE.

About

A workspace management tool for multi-package Gleam projects.

Topics

Resources

Stars

17 stars

Watchers

0 watching

Forks

Releases

Packages

Used by

Contributors

Languages