CI & Distribution
Show the whole prompt
Read https://intentius.io/chant/lexicon-authoring/ci-integration/.
In the chant repository, run `npx tsx packages/core/src/cli/main.ts dev onboard <name>` for the lexicon I name, then `npm install`. Do the manual steps the page lists after onboard, including an example that imports the lexicon.
Run `npx vitest run lexicons/<name>` and `npx vitest run --project unit`, and open a pull request with the result.
Leave `just smoke`, `just release`, `just release-lexicon` and the first npm publish to me; list them in the pull request description.
Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.Optional: hand this page to your coding agent
Read https://intentius.io/chant/lexicon-authoring/ci-integration/.In the chant repository, run `npx tsx packages/core/src/cli/main.ts dev onboard <name>` for the lexicon I name, then `npm install`. Do the manual steps the page lists after onboard, including an example that imports the lexicon.Run `npx vitest run lexicons/<name>` and `npx vitest run --project unit`, and open a pull request with the result.Leave `just smoke`, `just release`, `just release-lexicon` and the first npm publish to me; list them in the pull request description.Never deploy to a real cloud account or shared environment (with `chant run`, `chant components` or a platform's own CLI), approve (a pull request review, `chant approve` or `chant run approve`), override a policy denial (`chant approve policy-override`), or merge; never cut or publish a release (`chant release cut --yes` or `chant release publish`), push or delete a `ci/*` tag (`chant ci tick` pushes them), or change the value of an existing flag; never touch `.chant/allowed_signers` or `chant/lifecycle`.After building your lexicon (see Package & Publish), wire it into the monorepo’s CI, smoke tests and publish workflow. chant dev onboard does most of this.
Quick start
Section titled “Quick start”chant dev onboard <name>This patches up to six files (packages/core/src/cli/commands/onboard.ts):
| File | What it adds |
|---|---|
package.json (root) | @intentius/chant-lexicon-<name>: "workspace:*" dependency |
tsconfig.json (root) | paths mappings for the package and its subpaths, written ./lexicons/<name>/... like the existing entries (chant #1614, #3227). 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. With no baseUrl, a path without the leading ./ makes tsx’s tsconfig loader throw. scripts/tsconfig-lexicon-paths.test.ts fails when a lexicon directory has no entry or an entry names a removed one, and npm run generate:tsconfig-paths rewrites the block (chant #2376) |
.github/workflows/chant.yml | Prepack lines in any check/test run: block that lists them, and a validate step |
test/Dockerfile.smoke | <name> in the for lex in ... lexicon list of the workspace smoke test |
test/smoke-npm-lexicons.txt | <name> on its own line. The npm tarball smoke (test/Dockerfile.smoke-npm) and smoke.sh both read this list |
Onboard leaves some files alone and names them in its “Not patched” output:
.github/workflows/publish.ymlhas no per-lexicon lines to patch.scripts/ci-lexicon-artifacts.shbuilds every workspace lexicon andscripts/publish-packages.shpublishes them, so a new lexicon needs no edit.test/Dockerfile.smoke-npmreadstest/smoke-npm-lexicons.txt, which is the file onboard patches.test/smoke-e2e-lexicons.txtis opt-in. It lists the lexicons an e2e example uses and the lexicons they depend on, so add yours there by hand when an example needs it.
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># The unit suite, the one a change merges onnpx vitest run --project unit# Docker smoke testsjust smokeThe Docker smoke images (
test/Dockerfile.smoke,test/Dockerfile.smoke-npm) and the per-lexicon sections intest/integration.shnever run on a push or a pull request (chant #1687), because a Docker build per change costs more than it catches. They run only when a person starts them:just smokeat a terminal, which asks you to typerun, or thesmoke-npmjob of.github/workflows/large-suites.yml, aworkflow_dispatchworkflow. A new lexicon’s integration section therefore executes for the first time when someone runsjust smoke. Do that before the lexicon’s first release.
First npm publish
Section titled “First npm publish”Releases go out through just release, which cuts chant with its own release
command (#4040):
just release patch # or: minor / majorjust release minor --dry-run # run the cut without --yes, change nothingjust release patch <commit> # release a named commit on mainThe recipe picks the newest green commit on main, not main’s HEAD, and runs
chant release cut <version> --at <commit> from release/, the workspace
member npm. No version-bump commit is made: the version is the release’s
label, one past the highest version in any package.json at the commit or in
a chant-v* or release/npm/* tag. The cut pushes chant/lifecycle, and the
recipe pushes the release/npm/<version> tag.
just release-when-green runs the steps around it
(#4218). For main’s head,
or a named commit, it starts each required ci.green phase that has no check
run on the commit (release-suites.yml, and chant.yml when it takes a
dispatch; a commit behind the head goes through chant ci run --at), waits on
the check runs and stops on the first one that fails, starts the tick once
every phase passed, waits for ci/green/<sha>, runs just release, and waits
on publish.yml at the new tag. A run finishing, a job failing or a tag
appearing ends each wait; nothing times out into a verdict. A commit that
already has a release/npm/* tag is reported and left alone.
just release-when-green patch # main's headjust release-when-green patch <commit> # a named commit on mainjust release-when-green patch --dry-run # say what it would start; dispatch and cut nothingOn that tag, publish.yml’s release-publish job runs
scripts/release-pack.sh <version>. It sets every package to the version, with
the @intentius/* ranges at ^<version>, packs each one into .release/packs/,
and puts the package.json files back. Then chant release publish <version> --yes publishes the tarballs release/chant.config.ts names, core first,
straight to latest (stage: false). A new lexicon needs a line in that list,
and scripts/release-member.test.ts fails until it has one.
just release-legacy is the path before #4040. It commits the version bump on
top of the green commit, tags it chant-v<version>, and merges the tag back
into main. It stays until two releases have gone through just release. The
gate and untag jobs below belong to that path.
The publish workflow’s test job is the CI gate. Instead of running the suite
again, scripts/publish-verify-ci.sh looks up the chant run through the API.
A bump commit that changes only version fields is checked through its parent,
the green commit the recipe picked; any other commit needs a green run of its
own. Because the tag push also starts that run, the gate
polls until the run finishes rather than failing on a queued one. A successful
run lets the release through. The gate fails when every run has completed
without success, or when no run has appeared after 5 minutes.
Beside the gate, the audit job runs scripts/audit-trusted-publishers.sh,
which asks npm for an OIDC token for every publishable package. It fails when
any package has no working trusted-publisher record, including a package that
is not on npm yet, and publish waits for it, so a missing record stops the
release before any package ships. The run summary lists the packages to fix.
When the job gets no id-token from GitHub, as on a fork, the audit can check
nothing; it prints a warning and passes.
When the gate, the audit or the publish fails, the untag job runs
scripts/release-untag.sh. If none of the packages the release bumped is on
npm, it deletes the tag, so a tag exists only for a release that reached npm.
After an audit failure that is always the case: add the missing records, then
push the tag again at the same commit.
The tag survives two cases:
- Part of the release reached npm. Deleting the tag then would leave npm ahead
of git with no tag, so
untagkeeps it, fails, and its summary lists which packages published and which did not. Fix the cause and rungh run rerun <publish-run-id> --failed; packages already on npm are skipped. - The
chantrun is still going after 45 minutes. The gate ends with outcometimeoutand nothing is published. Wait for that run to go green, then re-run the gate and the publish withgh run rerun <publish-run-id> --failed.
To 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. By default it publishes to latest in dependency order, core first. 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.
Setting the repository variable CHANT_STAGED_PUBLISH to 1 switches on
staged publishing. Every package is published under the next dist-tag, the
run waits until each one can be installed, and then it moves latest, core
first and the lexicons after it. Until latest moves, npm install with a
version range keeps resolving the previous release. If anything fails before
the tag move, latest stays where it was, and a re-run only moves the tags.
Staged publishing needs every package’s trusted-publisher record on npmjs.com
to have “Allow npm dist-tag” turned on, including a new package’s. Without it,
that package’s latest move fails and the run fails with the setting named.
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 when it packs every package at the
release’s version.
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 works without 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 declares what core may import.
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 it drifted before.
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 or 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, say so in the generated file.
Each job is a thin trigger: it runs the component’s deploy command. The deploy logic stays in the component, so a cross-cutting change is one edit to the generator rather than one 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.