chant build
Synopsis
Section titled “Synopsis”chant build [path] [flags]chant build [path] --components --generate <lexicon> [--env <name>] [--promote-to <env> [--allow-skip]] [-o <file>]Description
Section titled “Description”chant build loads the project at the given path, parses all TypeScript source files, evaluates resource definitions, and serializes the output.
It reaches no network. Every schema it needs is generated into the lexicon packages ahead of time, and a guard test runs the build over every example project with the socket layer replaced by a throwing stub. See Network Egress.
| Flag | Type | Default | Description |
|---|---|---|---|
-o, --output | string | stdout | Write output to file |
-f, --format | string | json | Output format: json or yaml |
-d, --lexicon | string | all | Build only the specified lexicon (e.g. aws, gitlab) |
--lexicon-output <lexicon>=<path> | string | — | Repeatable. Write that lexicon’s output to its own file. See One file per lexicon |
-w, --watch | bool | false | Watch for file changes and rebuild |
--fold / --no-fold | bool | true | Fold source files statically instead of importing and running them. On by default since #1134; --no-fold runs every file instead. Folding falls back to running a file, per file, for anything outside the fold subset — summarized as fold: N files folded, M ran, with the per-file [fold:fold] / [fold:run] <reason> lines under --verbose |
-v, --verbose | bool | false | List every resolved build parameter and every per-file fold decision, and print a success line with the resource count. Without it each group is one summary line |
--sandbox | bool | false | Run run-fallback source files (or every file, without --fold) together, isolated, in one sandboxed child process instead of in-process (opt-in). No filesystem write, no process spawn, no worker threads, no ambient environment visible to project source; network egress is not blocked — see Sandboxed Execution |
--param <name=value> | string | — | Bind a declared build-time parameter (chant.config.ts’s buildParams) to a value; repeatable. See Build-Time Parameters |
--params-file <path> | string | — | JSON file of { "name": value } build-time parameter values; second precedence, after --param |
--env <name> | string | — | Active environment. Sets CHANT_ENV so env-aware source re-evaluates for that environment, and selects the organizational policy that applies. Must be declared under environments in chant.config.ts when that list exists |
--components --generate <lexicon> | string | — | Generate mode. Synthesize a CI pipeline from the discovered *.component.ts declarations instead of running a lexicon build. See Generate mode |
--allow-skip | boolean | false | With --promote-to. Let the promote job skip a step of the member’s promotion order. The generated job runs promote with --allow-skip. |
--promote-to <env> | string | — | Generate mode only. Add a job that promotes the deployed releases to <env> after every component job succeeds. Without --components --generate <lexicon> the command fails rather than ignoring the flag. See Promote job |
--root-only | bool | false | At the root of a declared workspace, build the root project alone instead of refusing. See At a workspace root |
--fold-rank [path] | bool or string | off | After a folded build, print the run-fallback blockers ranked by how many files each one keeps from folding. With a path, also write the ranking in collapsed-stack format to that file for a flame-graph viewer. No-op with --no-fold |
# Build from current directorychant build
# Build from specific directorychant build ./infra
# Output to filechant build --output stack.json
# YAML outputchant build --format yaml --output stack.yaml
# Watch modechant build --watch
# Import and run every file instead of folding (pre-#1134 behavior)chant build --no-fold
# Isolate run-fallback files in a sandboxed child process (opt-in)chant build --sandbox
# Bind a declared build-time parameter (repeatable)chant build --param tier=production --param env=staging
# Or from a JSON filechant build --params-file ./params.prod.json
# Re-evaluate env-aware source for one environmentchant build --env prod
# Rank what keeps files from folding; also export a flame-graph inputchant build --fold-rankchant build --fold-rank ./fold-blockers.collapsed
# Generate a CI pipeline from components instead of building resourceschant build --components --generate github --env staging --output .github/workflows/components.ymlFold vs run
Section titled “Fold vs run”chant build reduces each file’s AST directly to the resource spec by default, with no module execution. A file outside the supported subset (see Folded vs Run) is imported and run instead, per file. You write resources the same way in both modes.
Pass --no-fold to import and run every file instead, which is the pre-#1134 behavior. The same switch is settable project-wide:
export default { build: { fold: false },};An explicit --fold/--no-fold flag always wins over build.fold when both are present.
What a build prints
Section titled “What a build prints”Output goes to stdout. Everything else goes to stderr, in this order: a one-line count of resolved build parameters, a one-line fold summary, then any warnings and errors, so the lines you can act on are at the bottom.
ℹ 23 build parameters resolved (4 from env, 19 default); --verbose to listℹ fold: 8 files folded, 13 ran (--verbose for reasons)⚠ [apiDeployment] ...A file that ran instead of folding is expected for source that computes values in functions, and the build output is the same either way. Pass --verbose to see each [param] name = value (source) line and each [fold:fold] / [fold:run] <reason> line.
Sandboxed execution
Section titled “Sandboxed execution”--sandbox runs every run-fallback file for the build together, isolated, in one child process with the filesystem, process-spawn, worker-thread, and environment access locked down. Sandboxed Execution lists what that isolates and what it leaves open: Node cannot block network egress, a documented residual risk with its own deployment guidance. It composes with --fold: the run-fallback remainder is what gets sandboxed.
With --sandbox, no project source executes in the CLI’s process at all. That is slightly stricter than folding alone: folding a SomeComposite({...}) call or a new SomeType({...}) has to invoke the real factory or constructor, so when those are imported from a project file rather than a lexicon package, the file falls back to the sandboxed run path instead of folding. Sandboxed Execution reports what that costs across the example corpus.
export default { build: { fold: true, sandbox: true },};The --sandbox flag always wins over build.sandbox when both are present.
Generate mode: CI pipelines from components
Section titled “Generate mode: CI pipelines from components”--components --generate <lexicon> switches the build to the component discovery path. It reads every *.component.ts under the path, orders them by dependsOn into waves, and asks the named lexicon’s generateComponentPipeline to render those waves as CI stages and jobs. The github, gitlab and forgejo lexicons implement it; naming any other lexicon fails with a message saying so.
A component whose enabled is false under the bound parameters gets no job, and no other job needs it. These are the components chant run --components all runs, and a component that depends on a disabled one keeps its job with that dependency dropped.
A components-only project need not declare a lexicon plugin at all. Lexicons are loaded best-effort here, so a project whose chant.config.ts lists none still generates.
--env <name> is threaded into the generated pipeline as the environment it deploys, and --param / --params-file bind build-time parameters the same way they do for a resource build. With --format json the result is { stages, jobs, yaml, env } on stdout, where env is the generator’s own resolved environment (present whenever the generator returns one), so a consumer reads it here instead of parsing the YAML. The same stages, jobs and env are available as graph IR through chant graph --components --format ir --projection <lexicon>.
Promote job
Section titled “Promote job”--promote-to <env> adds one job to the pipeline that runs chant components promote --from <env> --to <promote-to> once every component job has succeeded. The promote job promotes the releases this pipeline run recorded in <env>, not the latest ones there, which another pipeline may have recorded while this one ran.
Each component job whose deploy has a publish step runs with --digest-file <component>.digest, which writes the digest of the release the run recorded. Components with no publish step record no release and are not promoted. On GitHub and Forgejo a Record <component> digest step writes that digest to the job output digest, and the promote job passes --digest "<component>=${{ needs.<job>.outputs.digest }}" for each such component. On GitLab the digest file joins the job’s artifacts: and the promote job passes --digest "<component>=$(cut -d= -f2- <component>.digest)".
When a component job records no release (under --no-release-record, say) its digest is empty, and the promote refuses it instead of promoting something else. Generation fails when no component has a publish step, since there would be nothing to promote.
A promote publishes from the build archive on disk, and the promote job runs on its own runner, so the archive travels with the pipeline too:
- Each component job uploads the files its
docker-build,zip-packageandjvm-buildsteps wrote tointo. On GitHub and Forgejo that is anactions/upload-artifactstep named<component>-archive, and the promote job downloads each one back to the same path. On GitLab the files join the job’sartifacts:, and the promote job, in apromotestage, receives them throughneeds:. - The
beforeScriptandextraScriptoptions apply to the promote job as they do to the component jobs, andpromoteCommandreplaces its command. The--digestpins are appended topromoteCommandas well. - An
intothe generator cannot read as a plain path, such as a wiring reference or an$env.value, fails generation and names the component, because the file it names is only known at run time.
When the member’s chant.config.ts declares environments, the build checks the promotion before it writes anything. A --promote-to more than one step after --env in that order fails with the promote-skips-environment code and a fix line, since the job would be refused when it ran. Pass --allow-skip to generate it anyway, and the job runs chant components promote --from <env> --to <promote-to> --allow-skip. With no declared environments nothing is checked.
Without --promote-to the generated pipeline has no promote job. It uploads no archives and writes no digest files either.
--promote-to is refused anywhere else, including a plain chant build --promote-to prod, with an error naming the flags it needs.
chant build --components --generate github --env staging --promote-to prod --output .github/workflows/components.ymlPull-request pipeline
Section titled “Pull-request pipeline”--pr-loop generates a pipeline with two jobs instead of one per component. plan runs on each pull request and posts the plan of the components the change reaches. record runs on each push to the target branch, plans the merged commit and writes the merge record; it applies nothing (#4168). With --apply-on-merge the job on merge is apply instead, which applies the plan once a reviewer approved it (apply on merge). With --apply-before-merge a push still records, and an apply job applies an open pull request’s head on request: a /chant apply comment on GitHub and Forgejo, a target-branch pipeline with CHANT_APPLY_PR set on GitLab (apply before merge, #4301). It takes no --apply-on-merge, --wave-gate or --pr-build. --gate <name> renames the gate from pr-apply, and --branch <name> names the target branch (default main, or the project’s default branch on GitLab). --events-json makes both jobs stream their run events and keep them in the report artifact, and --report-url <url> makes both copy their reports and events to an s3:// or file:// URL (#4007). --pr-build adds a step before pr-plan and before the job on merge that runs chant build --env <env> and then chant lint, and fails the job on an error. A project with a cfn-deploy step needs it, since the plan and the apply read the built template (#4088). --pr-build-command '<line>' runs that line in place of the build, for a project whose templates come from several builds, and turns --pr-build on. Generated CI for pull requests walks through it on each forge.
chant build --components --generate github --pr-loop --env prod --output .github/workflows/chant-pr.ymlWith --wave-gate <name> and --apply-on-merge beside it, the apply gets a gate per wave (#4003). The single apply job becomes apply-wave-1 to apply-wave-<n>, one per wave of the component graph, each after the one before it. Each runs pr-apply --wave-gate <name> --wave <k>, which plans its wave after the earlier waves applied. A wave whose plan the pull request’s approved plan covers applies under that approval, and one whose plan moved waits at <name>-wave-<k>; see a gate per wave on merge. --wave-gate without --apply-on-merge is refused, since a merge applies nothing to gate. --canary and --base are refused beside --pr-loop, and so is --promote-to. Inside a workspace member it generates that member’s loop, written to chant-pr.<member>.<env> in the forge’s directory; see inside a workspace member.
The job image
Section titled “The job image”Every generated job runs in node:22 unless --image <ref> names another image. --before-script <line> adds a shell line that every job runs after the checkout and before its command. Repeat it for more lines; they run in the order given. Use it to install what the image lacks. chant is one of those things: no default image has it.
chant build --components --generate forgejo --pr-loop --env prod \ --before-script 'npm ci --no-audit --no-fund' \ --before-script 'echo "$PWD/node_modules/.bin" >> "$GITHUB_PATH"' \ --output .forgejo/workflows/chant-pr.ymlThe pull-request pipeline installs OpenTofu itself when the image is the default one. With --image, the image brings its Terraform binary.
Inside a workspace member
Section titled “Inside a workspace member”When the project sits in a member of a workspace, the pipeline belongs to that member. chant finds the member from the chant.workspace.json above the project. The pipeline keeps the triggers it has outside a workspace, and a deploy pipeline never gains a push or pull_request trigger. What changes is where its jobs run and what they are called:
- On GitHub and Forgejo,
run:steps start in the member’s directory throughdefaults.run.working-directory. Artifact upload and download paths move under that directory to match. The only trigger isworkflow_dispatch, which takes nopaths:filter. - On GitLab each job name starts with the member’s name, and each script starts with
cd <member dir>. Each job also gets arules: changes:entry for the member’s directory and the pipeline file, so a pipeline that the forge starts for a change in another member skips it. GitLab’schangeshas no exclusions, so a member at the workspace root (.) gets nochangesrule. - The workflow’s name is
chant-components.<member>.<env>.
Without --output the pipeline is written to the forge’s directory at the repository root, in a file named after the member and the environment, so two members never write the same file:
| Lexicon | File |
|---|---|
github | .github/workflows/chant-<member>-<env>.yml |
forgejo | .forgejo/workflows/chant-<member>-<env>.yml |
gitlab | .gitlab/ci/chant-<member>-<env>.gitlab-ci.yml, which the root .gitlab-ci.yml includes with include: local: .gitlab/ci/*.gitlab-ci.yml |
Its first line is chant’s generated-file marker. The marker names the command that rewrites the file, which runs in the member’s directory. The member lists the file in .chant/generated.json in its own directory, and each entry holds that command and the environment. This list is interim, and moves into the member’s generated list in chant.workspace.json with #2541. Forge files at the repository root are the one kind of generated file a member may own outside its own directory, and only one member may own each of them.
With --format json the result also carries member and file, and nothing is written. A project in an example group, or in a directory no member holds, gets the plain pipeline. So does every project outside a workspace, whose output is unchanged.
cd services/apichant build --components --generate github --env staging# wrote .github/workflows/chant-api-staging.yml for member apiScheduled Ops have their own generator command, chant run --generate <provider>. It writes one pipeline file per Op, and each file carries the generated-file marker and the command that regenerates it:
chant build --components --generate github --env staging --output .github/workflows/components.ymlchant run --generate githubBuild-time parameters
Section titled “Build-time parameters”One file per lexicon
Section titled “One file per lexicon”A build over several lexicons writes them to one output, with non-JSON documents such as YAML appended one after another. That is wrong for a project that declares both a collector and a Prometheus config, which are separate files read by separate programs. List every lexicon the source uses in chant.config.ts (a lexicon missing from the config prints No serializer found for lexicon ...), then give each its own path:
chant build observability/ \ --lexicon-output otel=observability/otel-collector.yml \ --lexicon-output prometheus=observability/prometheus.ymlEach named lexicon is built into its path, and the format follows that file’s extension. Lexicons you do not name are written together to -o <file> when you pass it, and skipped otherwise. --lexicon-output cannot be combined with --lexicon or --watch, and a lexicon that has no entities in the project makes its build fail with the usual empty-output error.
--param name=value (repeatable) and --params-file <path> bind declared build-time parameters (buildParams in chant.config.ts) so source can read params.<name> (import { params } from "@intentius/chant/params") instead of process.env. Unlike the AWS lexicon’s deploy-time Parameter(), a build-time parameter resolves before synthesis, so its value can change which resources are produced at all. An unknown name, a missing required value, or a value outside a declared enum is reported as a build error naming the parameter, never a thrown error from inside project source. The resolution is summarized on one line (23 build parameters resolved (4 from env, 19 default)), listed one per line as [param] name = value (source) under --verbose, and recorded on the build result for provenance. See Build-Time Parameters for precedence and why it folds to a literal.
In a workspace member, --env <name> also reads the feature flags that back the member’s parameters, and records each such value with the source flag:<name> (counted as from flags in the summary line). Where the environment has a gate, --param, --params-file and an env mapping can’t set a flag-backed parameter, and the build fails naming it (#4024).
At a workspace root
Section titled “At a workspace root”When the project chant build acts on is the root of a declared workspace, the build refuses with the code WSP000 and exits 1 (#2537, ws-023). The declaration moves its members out of the root project, so a root build would silently cover less than it did before the file existed. The message says how many members and example projects left, and points to chant workspace build, which builds each member with its own chant.
error: WSP000: this is the root of workspace acme (chant.workspace.json), which moves 3 members and 1 example project out of the root project. Run chant workspace build to build each member with its own toolchain, or pass --root-only (or set rootOnly: true in chant.config) to build the root project alone.--root-only, or rootOnly: true in the root’s chant.config.ts, builds the root project on its own. Member directories and example-group matches are left out of discovery, in addition to the project’s own exclude globs. A build inside a member’s directory or an example project is never refused, and a project with no chant.workspace.json is unaffected.
Exit Codes
Section titled “Exit Codes”| Code | Meaning |
|---|---|
| 0 | Success |
| 1 | Errors found, or WSP000 at a workspace root |