Running it in CI¶
Two commands check a corpus against its schema on every pull request, so a broken cross-reference fails CI. A corpus is one repository of knowledge records kept in git, and a record is one Markdown document in it, filed under a type. Both commands run from inside the corpus.
| Command | Fails when |
|---|---|
kac validate |
a record breaks a check the schema declares |
kac generate --check |
a generated file no longer matches the records and schema it was built from |
Checks is the page for adding a check. A corpus that ships an agent plugin adds
export and bundle beside the two, so a change that breaks either one fails the
pull request. Automation says what the checks are for.
Pin the tool first¶
Run the version the corpus was written against. Put kac in a tool manifest and commit it. Do this once, on your own
machine:
dotnet new tool-manifest
dotnet tool install KnowledgeAsCode.Tool
That writes .config/dotnet-tools.json with the version in it. CI restores from that file, so every machine runs the
same kac.
dotnet tool restore
dotnet tool run kac validate
Read-only permission¶
CI never commits. generate --check recomputes every generated file, lists the ones that differ, and exits 1. It
writes nothing. Give the job read-only permission. Run kac generate on your own machine and commit what it writes.
generated files are stale. These differ from the schema/frontmatter:
glossary/_index.md
run: kac generate
Checking out with git¶
Check out with git, not a tarball. kac lists a corpus with git ls-files, so .gitignore and the other exclude files
count. A working tree with no .git/ falls back to a directory walk, which obeys none of them. Every standard checkout
action is fine. A downloaded archive is not.
GitHub Actions¶
name: kac
on:
pull_request:
branches:
- main
# Nothing here writes back to the repository.
permissions:
contents: read
jobs:
validate:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v7
- uses: actions/setup-dotnet@v6
with:
dotnet-version: 10.0.x
- name: Install the pinned kac
run: dotnet tool restore
- name: Validate frontmatter, links and structure
run: dotnet tool run kac validate
- name: Check generated output is fresh
run: dotnet tool run kac generate --check
A tag is mutable, so pin each action to a commit where your organisation asks for it. This repository's own workflows pin that way and let Dependabot move them.
Azure Pipelines¶
trigger: none # PR validation comes from an Azure Repos branch policy
pr:
branches:
include:
- main
pool:
vmImage: ubuntu-latest
steps:
- task: UseDotNet@2
displayName: Install .NET 10 SDK
inputs:
packageType: sdk
version: 10.0.x
- script: dotnet tool restore
displayName: Install the pinned kac
- script: dotnet tool run kac validate
displayName: Validate frontmatter, links and structure
- script: dotnet tool run kac generate --check
displayName: Check generated output is fresh
Azure Repos ignores the pr: trigger in this file
Wire the pipeline up as a branch policy, or it never runs on a pull request and nothing says so.
Project settings, then Repositories, then your repository, then Policies, then Branch Policies, then main, then
Build Validation, then +. Select this pipeline and mark it Required.
The pr: block states the intent, and it works as written if the repository is ever mirrored to GitHub.
Restoring what a corpus consumes¶
A corpus that reads another corpus's records declares them in consumes:, and fetches them before anything is
validated:
dotnet tool run kac restore # each consumed corpus, into .imports/<shortcode>/
.imports/ is not committed, so a fresh checkout holds none of it. Run restore as the first step of
the job, exactly as a build restores packages.
A private feed needs a token, which the job reads from KAC_REGISTRY_TOKEN. In GitHub Actions the built-in
github.token reaches GitHub Packages in the same organisation, and the job needs packages: read.
A source: naming a folder rather than a registry needs no token, and the producer has to have packed into that folder
before this step runs. Two corpora in one repository take that form: build the producer's package first, in its own
directory, and the consumer reads it from the path its descriptor names.
Restore before you validate, not after. A declared import that is not on disk fails validate
naming this command. A corpus that consumes nothing skips the step, and standing alone is the ordinary case.
Building the plugin¶
A corpus that publishes an agent plugin runs two more commands, in this order:
dotnet tool run kac export # the corpus as data, into .dist/export/
dotnet tool run kac bundle # that export plus .plugin/, into .dist/plugin/
Each replaces its own directory under .dist/ and leaves the other alone, so a .gitignore listing .dist/ keeps both
out of the tree. Running them in CI proves the corpus still exports and still assembles. It publishes nothing. Pushing
the result anywhere is a separate job, and one that needs credentials this one should not have.
bundle validates nothing it assembles, so validate both the plugin and the marketplace above it. The
Claude Code CLI has to be on the runner first:
npm install -g @anthropic-ai/claude-code
claude plugin validate ./.dist/plugin --strict
claude plugin validate ./.dist --strict
Publishing the corpus as a package¶
A corpus another corpus consumes runs one more command, and it reads the same export:
dotnet tool run kac pack # the export, sealed into .dist/package/
pack writes a .nupkg, which is a zip a registry stores under the corpus name and its
content-version. Both GitHub Packages and Azure DevOps Artifacts take one. Nothing that reads the result needs a NuGet
client.
Run pack in the gate, where it proves the corpus can still be packaged. Push it from a separate job, because that job
needs a credential the gate should not hold.
A published version is never replaced, so pushing one the registry already holds fails rather than overwriting it. That
failure means a content-version somebody forgot to bump:
VERSION=$(jq -r '.contentVersion' .dist/export/manifest.json)
ID=$(jq -r '.corpus' .dist/export/manifest.json)
dotnet nuget push ".dist/package/$ID.$VERSION.nupkg" \
--source "https://nuget.pkg.github.com/OWNER/index.json" \
--api-key "$GITHUB_TOKEN"
The job needs packages: write in GitHub Actions. That block pushes and asks the registry nothing, so a re-run of a
corpus whose version has not moved fails on the push.
publish-corpus.yml
is the whole pipeline this repository publishes from. It asks the registry what it holds first and skips a version
already there, which is what keeps that re-run green.