CI & Distribution
After building your lexicon (see Package & Publish), you need to wire it into the monorepo’s CI pipeline, smoke tests, and publish workflow. The chant dev onboard command automates most of this.
Quick start
Section titled “Quick start”chant dev onboard <name>This patches 6 files (packages/core/src/cli/commands/onboard.ts:305):
| File | What it adds |
|---|---|
package.json (root) | @intentius/chant-lexicon-<name>: "workspace:*" dependency |
tsconfig.json (root) | paths mappings for the package and its subpaths (chant #1614). The whole-repo typecheck runs moduleResolution: node, which never reads exports maps, so a lexicon without this entry fails the check job as soon as an example imports it |
.github/workflows/chant.yml | Prepack lines in check/test jobs + validate step |
.github/workflows/publish.yml | Prepack line in test job. No publish step is needed; scripts/publish-packages.sh enumerates the workspace |
test/Dockerfile.smoke | <name> in the for lex in ... lexicon list of the workspace smoke test |
test/Dockerfile.smoke-npm | <name> in both for lex in ... lexicon lists of the npm tarball smoke test |
The command is idempotent. A second run changes nothing and reports each file as already covered.
Manual steps after onboard
Section titled “Manual steps after onboard”-
Create an example workspace
npm workspace resolution requires at least one dependent for each package. Without an example,
import("@intentius/chant-lexicon-<name>")will fail frompackages/core.lexicons/<name>/examples/basic-deployment/├── package.json└── src/└── infra.tsThe
package.jsonmust depend on your lexicon:{"name": "<name>-basic-deployment","version": "0.0.1","type": "module","private": true,"scripts": {"build": "chant build src --lexicon <name>","lint": "chant lint src"},"dependencies": {"@intentius/chant": "workspace:*","@intentius/chant-lexicon-<name>": "workspace:*"}}Add the example directory to the root
package.jsonworkspaces array:"workspaces": ["packages/*","lexicons/*","lexicons/<name>/examples/*"] -
Add smoke tests to
test/integration.shFollow the pattern of the existing AWS/GitLab/K8s sections. Each lexicon’s smoke tests should cover:
- Build a fixture project
- Build with
--outputand--format yaml - Lint (with diagnostics check)
- Lint with
--format json - List entities
- Doctor check
- MCP server initialize + tools/list
- LSP server initialize
chant init --lexicon <name>(skill install, build, lint)
-
Run
npm installto update workspace links -
Verify locally before pushing:
Terminal window # Unit testsnpx vitest run lexicons/<name># Full test suitenpx vitest run# Docker smoke testsjust smokeThe Docker smoke images (
test/Dockerfile.smoke,test/Dockerfile.smoke-npm) and the per-lexicon sections intest/integration.shrun only locally. No CI job builds them. Thesmoke-npmjob in.github/workflows/chant.ymlis gatedif: falseon purpose (chant #1687), because a Docker build per push is too expensive. A new lexicon’s integration section therefore executes for the first time when you runjust smokeby hand. Do that before the lexicon’s first release.
First npm publish
Section titled “First npm publish”Releases go out through just release, which bumps every package in lockstep,
commits, and pushes the tag the workflow actually listens for:
just release patch # or: minor / majorTo move a single lexicon without a full release:
just release-lexicon <name> patchThe workflow triggers on chant-v* and lexicon-*-v* tags. A bare v<version>
tag matches neither and silently does nothing.
Publishing itself is scripts/publish-packages.sh. It enumerates every
non-private workspace package, so a new lexicon is picked up with no workflow
edit. A package already at its version on the registry is skipped, which makes
re-running the workflow a safe way to recover a partial release.
Version coupling
Section titled “Version coupling”chant init --lexicon <name> generates a package.json depending on the core
version it was run from (packages/core/src/cli/commands/init.ts:101):
"@intentius/chant": "^0.57.0","@intentius/chant-lexicon-<name>": "^0.57.0"Under 0.x semver a caret does not float across minors: ^0.57.0 resolves to
>=0.57.0 <0.58.0. A lexicon published at a different minor from core is
unreachable from a scaffolded project. Every package in the monorepo is versioned
together, which is what just release does; bump them in lockstep before
tagging.
Why the root dependency exists
Section titled “Why the root dependency exists”chant dev onboard adds your lexicon to the root package.json dependencies so
that packages/core can dynamically import it:
import(`@intentius/chant-lexicon-${name}`)Resolution does not actually depend on that entry. The root
workspaces: ["lexicons/*"] glob symlinks every lexicon into node_modules
whether or not it is listed, which is why three lexicons sat unlisted for
months without anything breaking. The entry is an explicit declaration of what
core may import, not the mechanism that makes the import work.
It is kept complete anyway, and
packages/core/src/codegen/lexicon-wiring.test.ts fails if a lexicon is
missing from it — a hand-maintained list with no gate is how this drifted in
the first place.
Why prepack steps are needed in Docker
Section titled “Why prepack steps are needed in Docker”Generated files (lexicons/<name>/src/generated/) are gitignored. When Docker builds from a clean checkout, these files don’t exist. The prepack script runs the generation pipeline to produce them before any tests or builds.
Generating a pipeline from the component graph
Section titled “Generating a pipeline from the component graph”A CI-provider lexicon — gitlab, github, forgejo — can turn a discovered
component graph into pipeline YAML, behind
chant build --components --generate <name>. Core owns the generic half
(discovery, dependency waves, job ordering) and dispatches to the lexicon for
the emit:
generateComponentPipeline: (components, options) => generateGitlabPipeline(components, options),You receive DriverComponent[] plus an optional ComponentPipelineOptions
(env, runCommand, extraScript, beforeScript, image, variables) and
return a ComponentPipelineResult. That result carries the YAML alongside the
wave-ordered stages, the jobs in emit order, and the env you defaulted to,
so core can report what it generated without re-parsing it. All three types are
in packages/core/src/lexicon.ts:441.
generateOpPipeline is the cron-triggered sibling for scheduled Ops (chant
#927), with the same “CI providers only” rule. It receives ScheduledOpSpec[]
rather than components, and each spec can carry three things the options block
cannot, because they are per Op: setup, an ordered list of { uses, with?, env? } / { run, env? } steps emitted ahead of the beforeScript lines,
permissions, a map your generator merges additively over whatever the
finding mode already grants (chant #2242), and environment, a { name, url? } naming the forge deployment environment the job runs in (chant #2257). A
provider with no equivalent degrades by name at build time rather than
dropping the option: gitlab refuses a uses entry outright, and refuses every
additive scope except id-token: write, which it maps onto GitLab’s own
id_tokens: declaration (chant #2256); forgejo emits the steps and drops the
permissions the way it already drops the mode’s own.
environment is the one of the three where a silent drop would be worst,
because the option exists so a human can hold a job. A provider without
environments therefore both drops the key and says so in the file it
generates; forgejo writes a header comment naming the environment and stating
that nothing on the forge enforces it. GitLab does have environments, and maps
the option onto its own environment: key. If your provider has the concept,
emit it and let the protection rules stay where they live, in the provider’s
own configuration. If it does not, be loud.
Each job is a thin trigger: it runs the component’s deploy command. The deploy logic stays in the component, not in the generated YAML — the whole point is that a cross-cutting change is one edit to the generator rather than an edit per pipeline file.
Omit this member entirely unless your lexicon is a CI provider. It is the only member with a hard “this kind of lexicon only” rule.
CLI reference
Section titled “CLI reference”chant dev onboard <name> Patch CI, Dockerfiles, and workflows for a new lexiconThe command takes no meaningful flags. --verbose parses but changes nothing
today. After patching, it prints the remaining manual steps and then runs chant dev check-lexicon against lexicons/<name> if that directory exists, so you see
the completeness report without a second command.