This document describes how to publish new versions of the Prompty runtimes.
Both runtimes use tag-based releases: push a GPG-signed tag, CI builds/tests/publishes via OIDC (no secrets needed).
| Runtime | Registry | Tag format | CI workflow |
|---|---|---|---|
| Python | PyPI | python/{version} |
prompty-python.yml |
| TypeScript | npm | typescript/{version} |
prompty-ts-release.yml |
Those tags are produced with release-please, run locally by a maintainer (see below). The manual tag-and-push steps later in this document remain as a break-glass path.
Prompty ships seven runtimes independently via
release-please. There is no shared
version number — python/2.1.0 and rust/2.0.3 can coexist. Config lives in
release-please-config.json + .release-please-manifest.json at the repo root.
Why not a CI workflow? The
microsoftorg blocks the ActionsGITHUB_TOKENfrom creating pull requests, and a tag cut byGITHUB_TOKENdoes not trigger the tag-listening publish workflows. So there is norelease-please.yml— a maintainer runs the release-please CLI locally under their ownghidentity. The org permits real users to open PRs and push tags, so this needs no GitHub App, no PAT, and no org-owner approval. A tag pushed by a real user does trigger the publish workflow.
VS Code extension (
vscode/) is intentionally not managed by release-please — it uses the Marketplace pre-release channel (vscode/2.0.0-pre.N) and keeps its own pipeline.
conventional commit on main
│
▼
maintainer runs `release-please release-pr` ── opens/updates one "Release PR"
│ (locally, as themselves) per runtime with changes
▼ (title: "chore(<runtime>): release <ver>")
merge the Release PR ── bumps version files, writes CHANGELOG
│
▼
maintainer runs `release-please github-release` ── pushes a tag like `python/2.1.0`
│ + creates the GitHub Release
▼
prompty-<runtime>.yml ── existing publish workflow triggers on the tag and publishes
release-please only opens PRs and cuts tags; nothing publishes until a human
merges a Release PR and runs github-release. Commits route to a runtime by the
paths they touch; add a scope when ambiguous (feat(python):, fix(rust):).
Only feat, fix, perf, revert, deps trigger releases; feat!: /
BREAKING CHANGE: bumps major.
| Component | Path | release-type | Version source | Publish |
|---|---|---|---|---|
python |
runtime/python/prompty |
python |
prompty/_version.py (# x-release-please-version) |
✅ PyPI |
rust |
runtime/rust |
rust |
all workspace Cargo.toml (native) |
✅ crates |
typescript |
runtime/typescript/packages/core |
node |
all packages/*/package.json (node-workspace) |
✅ npm |
csharp |
runtime/csharp |
simple |
4 *.csproj <Version> (annotated) |
✅ NuGet |
java |
runtime/java |
simple |
build.gradle.kts version (annotated) |
check only |
go |
runtime/go/prompty |
go |
git tag only | check only |
swift |
runtime/swift/prompty |
simple |
git tag only | check only |
Tag format (tag-separator:"/", include-component-in-tag:true, include-v-in-tag:false)
reproduces the existing python/2.1.0 tags exactly — no publish workflow changed.
All runtimes were on 2.0.0-beta.N. The manifest is seeded at each runtime's last
beta with no prerelease mode, so the first Release PR graduates each runtime to a
clean 2.0.0 stable release; normal 2.0.1 / 2.1.0 / 3.0.0 bumps follow.
Run from a clone where gh auth status shows you authenticated with repo +
workflow scopes. The $(gh auth token) below passes your identity to the CLI.
CRITICAL — tag separator. The publish workflows trigger on slash tags (
python/2.*), so the release must be taggedpython/2.0.0, not the release-please defaultpython-2.0.0(hyphen). Two things control this, and both are required:
release-please-config.jsonsets"tag-separator": "/". This is the real knob. The schema field istag-separator; a top-levelseparatorkey is silently ignored (rootadditionalPropertiesdrops it), leaving the separatorundefined→ default-. This exact typo shipped once and producedpython-2.0.0/python-2.0.1despite the flags below. Verify withrelease-please debug-config … | grep tagSeparator— it must print'/', notundefined.- Always pass
--config-fileand--manifest-file. Without them release-please never loadsrelease-please-config.jsonat all and falls back to every default (including the-separator).
# 1. Open / update the Release PR (safe to re-run; add --dry-run to preview)
npx --yes release-please@16 release-pr \
--token="$(gh auth token)" --repo-url=microsoft/prompty --target-branch=main \
--config-file=release-please-config.json \
--manifest-file=.release-please-manifest.json
# 2. Merge the Release PR on GitHub (all publish checks must be green).
# 3. Cut the tag + GitHub Release, which triggers the publish workflow
npx --yes release-please@16 github-release \
--token="$(gh auth token)" --repo-url=microsoft/prompty --target-branch=main \
--config-file=release-please-config.json \
--manifest-file=.release-please-manifest.jsonIf github-release ever produces the wrong tag, remediate deterministically:
# delete the bad release + tag, recreate the correct one at the merge commit
gh release delete "python-2.0.0" --repo microsoft/prompty --yes --cleanup-tag
gh release create "python/2.0.0" --repo microsoft/prompty \
--target <merge-commit-sha> --title "python/2.0.0" --notes-file notes.mdA tag pushed this way (by you) triggers prompty-python.yml → PyPI publish via OIDC.
- typescript — confirm all four
packages/*/package.jsonversions and their^@prompty/coreranges bump (node-workspace); if a sibling is missed, register it as its own package in the config. - rust — confirm all workspace members +
path/versiondeps bump together. - csharp / java — confirm the annotated
<Version>/version =lines rewrite. - python — Release PR sets
_version.pyto2.0.0; the built wheel isprompty-2.0.0.
Run these exact steps locally before pushing tags. They mirror CI. If these pass locally, CI will pass. Do not skip this.
Enable the local hook once per clone so staged files are normalized through
.gitattributes and whitespace errors are blocked before commit:
git config core.hooksPath .githooksBefore releasing, confirm the repository has no whitespace errors or tracked CRLF files:
git diff --check
git ls-files --eol | grep 'w/crlf' # should print nothingcd runtime/typescript
# 1. Clean install from lockfile (exactly like CI)
rm -rf node_modules packages/*/node_modules packages/*/dist
# Windows: Remove-Item -Recurse -Force node_modules -EA 0; Get-ChildItem packages -Dir | % { Remove-Item -Recurse -Force "$_\node_modules","$_\dist" -EA 0 }
npm ci
# 2. Build all packages INCLUDING DTS declarations
npm run build
# 3. Run all tests
npm run test
# 4. Type-check
npm run lintWhy clean build matters:
tsupgenerates TypeScript declarations (DTS) using a stricter compiler mode than Vitest. Code can pass tests but fail DTS build. Always build from clean before tagging.
cd runtime/python/prompty
# 1. Lint
uv run ruff check .
# 2. Format check
uv run ruff format --check .
# 3. Run tests with coverage (exact CI command)
python -m pytest tests/ -q --tb=short --cov=prompty --cov-report=term --cov-report=json
# Windows venv: .venv\Scripts\python.exe -m pytest ...Python — edit runtime/python/prompty/prompty/_version.py:
VERSION = "2.0.0a4" # PEP 440: a=alpha, b=beta, rc=release candidateTypeScript — update all 4 packages + cross-references:
cd runtime/typescript
node -e "
const fs = require('fs');
const version = '2.0.0-alpha.4'; // <-- set your version here
for (const p of ['core','openai','foundry','anthropic']) {
const path = 'packages/' + p + '/package.json';
const pkg = JSON.parse(fs.readFileSync(path, 'utf8'));
const old = pkg.version;
pkg.version = version;
for (const depType of ['dependencies','devDependencies','peerDependencies']) {
if (!pkg[depType]) continue;
for (const [k,v] of Object.entries(pkg[depType])) {
if (k.startsWith('@prompty/') && v.includes(old)) {
pkg[depType][k] = v.replace(old, version);
}
}
}
fs.writeFileSync(path, JSON.stringify(pkg, null, 2) + '\n');
console.log(p + ': ' + pkg.version);
}
"See Pre-flight checklist above.
git add -A
git commit -S -m "chore: bump versions to {version}"
# Verify signature
git log --format="%h %G? %s" -1 # must show 'G'git push origin maingit tag -s "python/2.0.0a4" -m "Prompty Python 2.0.0a4
<release notes>"
git tag -s "typescript/2.0.0-alpha.4" -m "Prompty TypeScript 2.0.0-alpha.4
<release notes>"
git push origin "python/2.0.0a4" "typescript/2.0.0-alpha.4"https://github.com/microsoft/prompty/actions
# Python
pip install prompty==2.0.0a4
python -c "import prompty; print(prompty.__version__)"
# TypeScript
npm info @prompty/core versions --json | tail -5| Workflow | Node | OS | Purpose |
|---|---|---|---|
prompty-ts-check.yml |
22, 24 | ubuntu, windows | Test matrix |
prompty-ts-release.yml |
24 | ubuntu | Publish (npm 11+ for OIDC) |
| Workflow | Python | OS | Purpose |
|---|---|---|---|
prompty-python-check.yml |
3.11, 3.12, 3.13 | ubuntu | Test matrix |
prompty-python-check.yml |
3.11 | windows | Compat check |
prompty-python.yml |
3.11 | ubuntu | Publish to PyPI |
| Install | What you get |
|---|---|
pip install prompty |
Core only |
pip install prompty[openai] |
+ OpenAI provider |
pip install prompty[foundry] |
+ Microsoft Foundry provider |
pip install prompty[anthropic] |
+ Anthropic provider |
pip install prompty[jinja2] |
+ Jinja2 renderer |
pip install prompty[all] |
Everything |
| Package | What it is |
|---|---|
@prompty/core |
Loader, pipeline, types, tracing |
@prompty/openai |
OpenAI provider |
@prompty/foundry |
Microsoft Foundry provider |
@prompty/anthropic |
Anthropic provider |
The trusted publisher config on the registry must match exactly:
- Owner:
microsoft - Repository:
prompty - Workflow filename:
prompty-python.ymlorprompty-ts-release.yml - Environment: (leave blank)
tsup DTS is stricter than Vitest. Common fix: as unknown as Record<string, unknown>.
Always run npm run build from clean before tagging.
Known Node 22 bug (npm/cli#9151).
Publish workflow uses Node 24 which ships npm 11 natively. Do NOT add
npm install -g npm@latest to any workflow.
PowerShell doesn't support \ line continuation. Keep run: commands on a single line.
# Delete old tags
git tag -d "typescript/2.0.0-alpha.4"
git push origin :refs/tags/typescript/2.0.0-alpha.4
# Fix, commit, push
git push origin main
# Re-tag and push
git tag -s "typescript/2.0.0-alpha.4" -m "..."
git push origin "typescript/2.0.0-alpha.4"PyPI does not allow re-uploading the same version. If Python published but TS failed, only re-tag TS. If you must re-publish Python, bump to the next version.