SDK Changelog Generation
Generate changelogs for SDK pod packages following the monorepo GitFlow.
When to use this skill
Applies to SDK pod packages whose paths are owned by .github/teams/sdk.json.
Use when:
- Preparing a release for any SDK pod package
- User asks to generate changelog
- User asks to create human-readable/presentable changelog
- User asks to generate CHANGELOG_LLM.md
- User invokes
/qv-sdk-changelog
Workflow
Every step is mandatory. Do not ask the user whether to do CHANGELOG_LLM.md or
NOTICE — they are part of this skill and always run.
Step 1: Identify Target Package
If the user doesn't specify, ask which SDK pod package they want to generate a changelog for.
Package slugs match git tags (sdk, cli, ai-sdk-provider, opencode-plugin, openclaw-plugin, …). Directory resolution (including plugins/*) is in scripts/sdk/package-paths.cjs.
Working branch (when cutting from a release line): use
chore/<pkg>-<x.y.z>-changelog (e.g. chore/sdk-0.17.0-changelog). Do not
name the head release-* — org pushes to release-* run Release Merge Guard
against the pushed ref (not the PR base). The release cut itself must be
three-part release-<pkg>-x.y.z. Full rules live in
qv-sdk-pr-create → "Release PR branch naming".
Step 2: Fetch Tags and Resolve Base
Tags live on the upstream remote (tetherto/qvac), not the contributor's fork.
The script fetches from upstream first, falling back to origin.
Full-history requirement (fail-stop): discovery is
git log <base>..HEAD -- <packagePath>. Before generating:
git rev-parse --is-shallow-repositorymust befalse(elsegit fetch --unshallow/ re-clone without--depth, then stop).- Base must be an ancestor of
HEAD(git merge-base --is-ancestor <base> HEAD); otherwise check out the release tip / package tag first.
The generator enforces both checks and exits non-zero on failure.
Run git tag --list "<package>-v*" --sort=-v:refname to check for existing version tags.
- If tags exist: the script auto-detects the release type from
package.jsonversion:- Minor/major release (version ends in
.0, e.g.0.9.0): uses the latest.0tag as base (e.g.sdk-v0.8.0), skipping patch tags - Patch release (version ends in non-zero patch, e.g.
0.8.4): uses the absolute latest tag as base (e.g.sdk-v0.8.3)
- Minor/major release (version ends in
- If no tags: ask the user for
--base-commitand--base-version(migration scenario)
Why this matters: patches ship on separate release branches and get backmerged into main.
Using the latest patch tag as base for a minor release would miss all PRs that landed on main
between the previous minor release and the last backmerge. The correct base for a minor release
is the previous minor's .0 tag.
Step 3: Generate Raw Changelog
All SDK pod packages use the same command:
node scripts/sdk/generate-changelog-sdk-pod.cjs --package=<name>
With migration flags:
node scripts/sdk/generate-changelog-sdk-pod.cjs --package=<name> --base-commit=<sha> --base-version=<version>
The script automatically excludes:
- PRs tagged
[skiplog]. - Backmerge PRs (subjects starting with
BackmergeorMerge release …). Backmerges merge a release branch back into main; their content is already documented in the release branch's own changelog, so listing them here is noise. - PRs whose title fails the SDK PR-format validator (these are warned, not silently dropped — fix the title and re-run, or surface to the PR author).
For [mod] PRs, the script extracts the Added/Updated/Removed model lists
from the PR body and renders them as indented continuation lines beneath the
bullet in CHANGELOG.md (each section on its own line — never inline as one
giant row). The same filtered lists are written to models.md.
The extractor applies two policies (in this order):
- Companion entries are dropped. Companions are auxiliary files that ship
alongside a primary model but aren't independently usable — vocab files,
lexicons, raw data shards, metadata blobs. The filter recognises constant
suffixes (
*_LEX,*_VOCAB,*_DATA,*_METADATA) and any free-form description containing the word "companion". Only first-class models reach the changelog. - Entry-count suffixes are stripped.
(N entries)/(N entries — short note)decorations are removed from the displayed text — readers can follow themodels.mdlink for exact counts.
After both filters, each section is trimmed to MAX_INLINE_MODELS (currently
5) entries, with (and N more) for the remainder. Example:
- Regenerate model registry. (see PR [#123](...)) - See [model changes](./models.md)
Added: NMT_Q0F16, NMT_Q4_0 (and 12 more)
Removed: MARIAN_OPUS_*
If after filtering a section is empty, it's omitted. If all sections are empty the bullet emits with no continuation lines.
When writing the human-readable CHANGELOG_LLM.md (Step 4), apply the same
"no informational value" rule manually: skip backmerges, automated bumps, and any
entry whose subject would just repeat what a previous release already said. For
the Models section, mirror the script's policy — keep it concise in the body
(highlight the most notable adds/removes) and defer the full constant list to
the ### Added / ### Removed blocks at the bottom.
Step 4: Generate CHANGELOG_LLM.md (mandatory)
Always run this step. Do not ask the user — it's part of the skill.
After raw changelog files exist, generate the human-readable version at
packages/<package>/changelog/<version>/CHANGELOG_LLM.md.
See references/changelog-llm-format.md for the format guide.
After writing the file, re-run the raw generator (or rebuild the root aggregate) so
packages/<package>/CHANGELOG.md picks up the new CHANGELOG_LLM.md (the aggregator
prefers it over CHANGELOG.md). Easiest way: re-run the script from Step 3 — it's idempotent.
Format the generated markdown (mandatory). CHANGELOG_LLM.md is authored by
hand here, so it is the file most likely to carry markdown formatting issues that a
committed-file format check would later reject. Every SDK pod package uses prettier
(format = prettier --check ., format:fix = prettier --write .). Run the check
scoped to the changelog output so any issue surfaces now:
cd packages/<package>
bunx prettier --check "changelog/**/*.md" "CHANGELOG.md"
If it reports problems, fix them — bunx prettier --write on the same paths, or hand-edit —
and re-run the check until it passes clean. Do this before moving on so the release commit
carries only prettier-clean markdown.
Downstream rendering note: the docs site reads CHANGELOG_LLM.md
verbatim and inlines it under a ### @qvac/<pkg> subsection of the
minor series page (one permanent v<X.Y>.x.mdx per minor line — see
docs/website/docs-workflow.md). Each headline you write becomes a
section header on the public docs site (with two levels of demotion to
fit the nesting), so phrase them as standalone reader-facing prose, not
internal categories. Keep headings emoji-free (e.g. ## Breaking Changes, not ## 💥 Breaking Changes) — emoji prefixes leak verbatim
into the public headers; the only allowed emoji is the 📦 **NPM:**
line. See the format guide for the full rule.
Step 5: Generate announcement-post.txt (mandatory)
Always run this step after Step 4. It produces a Slack-ready copy-paste post at
packages/<package>/changelog/<version>/announcement-post.txt.
The file is gitignored (packages/*/changelog/*/announcement-post.txt) — it's a
local working artifact, not a committed deliverable. Never git add it.
node scripts/sdk/generate-changelog-sdk-pod.cjs --package=<name> --generate-announcement-post
The script emits the short Slack template — header + three links + optional breaking-changes block + footer. Per-section bullet lists are intentionally omitted; readers follow the full-changelog link for the detail.
Layout:
:qvac: SDK <version> :rocket: NPM Public releaseheader.- NPM, GitHub release, and full-changelog tree links.
:warning: Breaking Changessection with link tobreaking.md— emitted only whenbreaking.mdexists in the version folder (i.e. at least one PR carries the[bc]tag). Detected by file presence, not by parsing CHANGELOG.md.- Footer:
Thanks to everyone on QVAC team :green_heart: :qvac: :green_heart:.
If the post needs hand-tuning (e.g. a custom note for a specific release), edit the file directly. It's gitignored, so changes won't pollute the diff.
Step 6: Update NOTICE file for the target package
After Step 5 completes, run notice-generate for the same --package to ensure
its NOTICE file reflects any dependency changes in the release:
source .env
node .agents/skills/qv-notice-generate/scripts/generate-notice.js <package-name>
Do NOT commit the announcement post (gitignored) and let the user review the rest before committing.
See .agents/skills/qv-notice-generate/SKILL.md for full details.
Step 7: Sync lockstep clients (only when --package=sdk)
@qvac/sdk and tetherto-qvac-sdk release in lockstep at the
@qvac/inference version anchor. Every sdk release must stamp that anchor into
sdk and regenerate the Python
client (SDK_VERSION and other _generated/ outputs). Skip this step for any
other --package value.
Read and follow .agents/skills/qv-sdk-lockstep-sync/SKILL.md (Steps 1–3).
Short form:
node .agents/skills/qv-sdk-lockstep-sync/scripts/sync-sdk-pod.mjs
cd packages/sdk-python
.venv/bin/python3 scripts/generate.py
.venv/bin/python3 scripts/generate.py --check
Include sdk-python generated updates in the release commit. The Python
client does not get its own changelog — history lives in packages/sdk/CHANGELOG.md.
Step 8: Generate site docs (only when --package=sdk)
Generate the documentation-site API reference and release notes for the new
version in the same working tree, so the changelog PR also carries the docs
update. This replaces the old standalone docs-release.yml workflow (which
opened a second, separate docs PR). Skip this step entirely for any other
--package value — only the SDK release drives the versioned docs site.
Generation is deterministic: it runs the existing docs/website scripts
(TypeDoc + Nunjucks render + verbatim CHANGELOG_LLM.md inlining). No LLM is
involved in producing the API reference or release notes here — Step 4 already
authored CHANGELOG_LLM.md, and this step only renders it into the site.
Prerequisites:
docs/websitedependencies installed (cd docs/website && npm install).SDK_PATHset indocs/website/.envpointing at the SDK package root (packages/sdk, the directory containingindex.tsandtsconfig.json). Copydocs/website/.env.exampleto.envif it doesn't exist yet.CHANGELOG_REPO_ROOTdefaults to the repo root, so no override is needed when running inside the monorepo.
1. Generate the API reference + release notes (auto-detects minor vs patch):
cd docs/website
bun run scripts/release-version.ts <version> --force-extract
This is the exact command the old workflow ran. The dispatcher reads the
version and forwards to the minor (X.Y.0: generate the new series' MDX at
reference/{api,release-notes}/v<X.Y>.x.mdx, rewrite both index.mdx shims
to <include> the new series file, and rotate the managed alias block in
public/_redirects so the new v<X.Y>.x URL 301s to the shim canonical)
or patch (X.Y.Z, Z >= 1: insert the ## vX.Y.Z section into the target
series' v<X.Y>.x.mdx; for patch-latest, also mirror the refreshed
description onto the release-notes shim) orchestrator. It writes only:
docs/website/content/docs/reference/api/**(API summary MDX)docs/website/content/docs/reference/release-notes/**(release notes MDX)docs/website/src/lib/versions.ts(version-switcher manifest)docs/website/public/_redirects(minor only — the managed# ==== BEGIN latest-series alias (managed) ====block; patches never touch this file)
2. Verify the site still builds (mandatory):
cd docs/website
npm run build
A clean build confirms nothing on the website broke. Treat a build failure as fail-stop: surface the error and do NOT proceed to commit until it's fixed.
Staging follows the same convention as the other steps. Like every other
step, this one only generates files — it never runs git add or git commit.
The three surfaces above are part of the release commit (same as Step 7's
lockstep-client files: "Include … in the release commit"), and every
generation/build byproduct is gitignored — exactly like Step 5's
announcement-post.txt — so a normal git status review shows only the
committable files. Let the user review before committing. Generated + gitignored
byproducts (do not git add them):
docs/website/scripts/api-docs/api-data.json(written byrelease-version.ts)docs/website/.next/,.source/,out/,dist/(fromnpm run build)docs/website/next-env.d.tspackages/sdk/dist/(from theprebuild:examplesbuild step)
See docs/website/docs-workflow.md for the full pipeline reference.
CLI Parameters
| Flag | Required | Description |
|---|---|---|
--package |
Yes | Package name (e.g., sdk) |
--base-commit |
No | Initial commit SHA for migration (overrides tag lookup) |
--base-version |
No | Version label for base commit (display only) |
--release-type |
No | minor or patch (auto-detected from package.json version) |
--dry-run |
No | Preview output without writing files |
--update-root-changelog |
No | Rebuild only the root aggregate packages/<pkg>/CHANGELOG.md |
--generate-announcement-post |
No | Generate announcement-post.txt for the package's current version |
--version |
No | Override version when used with --generate-announcement-post |
Output
Generates changelog files in packages/<package>/changelog/<version>/:
CHANGELOG.md- Main changelogbreaking.md- Breaking changes detail (if[bc]PRs)api.md- API changes detail (if[api]PRs)models.md- Model changes (if[mod]PRs)CHANGELOG_LLM.md- Human-readable version (always generated, see Step 4)announcement-post.txt- Slack copy-paste post (always generated, see Step 5, gitignored — never commit)
Additionally:
packages/<package>/CHANGELOG.md– Aggregated changelog containing all versions (newest → oldest), preferringCHANGELOG_LLM.md(human-readable) from each version folder when available, falling back toCHANGELOG.md
When --package=sdk, Step 8 also generates the documentation-site surfaces
(commit these alongside the changelog):
docs/website/content/docs/reference/api/**– API reference MDXdocs/website/content/docs/reference/release-notes/**– Release notes MDXdocs/website/src/lib/versions.ts– Version-switcher manifestdocs/website/public/_redirects– minor releases only — the managed latest-series alias block (delimited by# ==== BEGIN latest-series alias (managed) ====markers). Patch releases never touch this file.
Tag Format
Tags follow the pattern: <package>-v<x.y.z> and are created on upstream (not the fork).
Examples:
sdk-v0.8.0(minor — used as base for next minor release)sdk-v0.8.1(patch — used as base for next patch release)rag-v2.0.0
Quality Checklist
Before completing:
- Correct package identified
- Working head (if branched for the release PR) is
chore/<pkg>-<x.y.z>-changelog, notrelease-* - Clone is not shallow (
git rev-parse --is-shallow-repository→false) - Base reference resolved (tag or
--base-commit) and is an ancestor ofHEAD - PRs scoped to package path only
- Changelog files written to correct version directory
- CHANGELOG_LLM.md generated (mandatory) and follows format guide
- Generated markdown is prettier-clean (
prettier --checkon the changelog output passes) - announcement-post.txt generated (mandatory, gitignored)
- NOTICE file updated for the target package
- When
--package=sdk:qv-sdk-lockstep-syncrun (sdk-python), pythongenerate.py --checkpassing - When
--package=sdk: site docs generated viarelease-version.ts,npm run buildpassed, andgit statusshows onlyreference/api/**,reference/release-notes/**,src/lib/versions.ts(andpublic/_redirectson minor releases — the managed latest-series alias block) as committable docs changes (byproducts gitignored) - Root CHANGELOG.md rebuilt from all version folders (and picks up CHANGELOG_LLM.md)
- Versions sorted in descending semver order
- No duplicated versions
- Root file is deterministic (fully regenerated)
References
- SDK pod ownership:
.github/teams/sdk.json - GitFlow and PR format:
docs/gitflow.md - LLM changelog format: references/changelog-llm-format.md
- NOTICE generation:
.agents/skills/qv-notice-generate/SKILL.md - sdk lockstep clients:
.agents/skills/qv-sdk-lockstep-sync/SKILL.md - Docs site pipeline (Step 8):
docs/website/docs-workflow.md - Release PR branch naming (org
release-*push / Merge Guard):.agents/skills/qv-sdk-pr-create/SKILL.md