This project publishes to PyPI as siglume-api-sdk and mirrors each release as a GitHub Release with attached wheel + sdist.
The sections below are the full checklist. Follow them top-to-bottom for every release.
packages/contracts/sdk/ inside the private monorepo is the public SDK mirror,
not a second source of truth.
- If you change public SDK docs, OpenAPI, examples, Python SDK, or TypeScript
SDK files under
packages/contracts/sdk/, you must sync the same change totaihei-05/siglume-api-sdkbefore the work is considered complete. - The private repo now carries a CI guard at
.github/workflows/public-sdk-sync.ymlthat clones the public repo and fails if the mirror drifts. It runs on SDK-tree pushes, pull requests, and a 6-hour schedule so public-first drift also gets caught. public-sync-ignore.txtandscripts/check_public_sdk_sync.pyare mirrored into the public repo root as well, so both repos carry the same file-level allowlist and the same local/manual drift checker. The GitHub Actions workflow itself stays private-repo only because public Actions runners cannot read the private monorepo mirror without extra secrets.- Run the same guard locally with:
# private monorepo
python packages/contracts/sdk/scripts/check_public_sdk_sync.py
# public siglume-api-sdk clone (requires auth to read the private repo)
python scripts/check_public_sdk_sync.py --peer-repo https://github.com/taihei-05/siglume.git- Intentional private-only files must be listed in
packages/contracts/sdk/public-sync-ignore.txt. If a file is not in that file-level allowlist, divergence is treated as a bug.
Before cutting a version tag:
-
mainis green (CI passing on Python 3.11 and 3.12) -
CHANGELOG.mdhas a new section## [X.Y.Z] -- YYYY-MM-DDwith Added / Changed / Deprecated / Removed / Fixed / Security sub-sections as appropriate - Version in
pyproject.toml([project].version) matches the tag you're about to cut -
release-notes/RELEASE_NOTES_vX.Y.Z.mdexists, written as the GitHub Release body (highlights, revenue model, quick start, what's next, honest note). Per-version release notes live underrelease-notes/;CHANGELOG.mdat the repo root remains the canonical consolidated history. -
examples/hello_price_compare.pyandexamples/x_publisher.pystill run end-to-end againstAppTestHarness -
docs/andREADME.mdare coherent with the shippedsiglume_api_sdk.pysurface (no references to removed symbols) - There is no stale
siglume_app_sdk/siglume-app-sdk/siglume-app-typesreference (grep -rnexpects zero matches)
From the repo root on main:
# Clean previous build output
rm -rf dist/ build/ siglume_api_sdk.egg-info/
# Build sdist + wheel (bash / WSL / macOS)
python3.11 -m pip install --upgrade build
python3.11 -m buildOn Windows PowerShell, use the py launcher instead:
Remove-Item -Recurse -Force dist, build, siglume_api_sdk.egg-info -ErrorAction SilentlyContinue
py -3.11 -m pip install --upgrade build
py -3.11 -m buildThis produces two files in dist/:
siglume_api_sdk-X.Y.Z-py3-none-any.whlsiglume_api_sdk-X.Y.Z.tar.gz
Inspect the sdist manifest (tar -tzf dist/*.tar.gz) — it should contain LICENSE, README.md, pyproject.toml, PKG-INFO, and the two siglume_api_sdk*.py modules. Nothing else.
There are three ways to publish. In decreasing order of recommendation:
- Option 0 (strongest): GitHub Actions Trusted Publisher (OIDC) — no token anywhere, just push a tag
- Option A: Per-release env vars — fallback only, no token left on disk
- Option B: Persistent
.pypirc— last-resort fallback only, accepts a longer-lived token on disk
The .github/workflows/release.yml in this repo is already configured to publish via OIDC when a tag matching v* is pushed. After one-time setup on the PyPI side, you never handle a token again.
One-time setup on PyPI:
- Sign in to https://pypi.org/manage/account/publishing/
- Add a pending publisher with:
- PyPI project name:
siglume-api-sdk - GitHub org:
taihei-05 - Repository name:
siglume-api-sdk - Workflow filename:
release.yml - Environment name:
pypi
- PyPI project name:
- Optional but recommended — create a GitHub environment:
- https://github.com/taihei-05/siglume-api-sdk/settings/environments/new
- Name:
pypi - Protection: required reviewer (yourself), deployment branch rule = protected branches only
- Once the pending publisher is registered, the next tag push triggers the workflow.
Every subsequent release:
# bash / WSL / macOS
git tag -a vX.Y.Z -m "Release vX.Y.Z — <headline>"
git push origin vX.Y.Z# PowerShell (Windows)
git tag -a vX.Y.Z -m "Release vX.Y.Z — <headline>"
git push origin vX.Y.ZThat's it. The GitHub Actions workflow:
- Verifies the tag matches
pyproject.tomlversion (fails loudly if not) - Builds sdist + wheel
- Runs
twine check - Publishes via OIDC with PEP 740 attestations
You can monitor the run at https://github.com/taihei-05/siglume-api-sdk/actions. If it fails, the workflow is rerunnable — fix the cause, re-push the tag (force if needed: git push --force origin vX.Y.Z).
Revoke the local .pypirc token once OIDC is live. It is no longer needed, and keeping it invites the attack surface the 5-agent review flagged.
Use this only for emergency bootstrapping or local publish testing when OIDC is unavailable. Get a project-scoped API token from https://pypi.org/manage/account/token/ (see SECURITY.md).
Two ways to pass the token — pick one:
$env:TWINE_USERNAME = "__token__"
$env:TWINE_PASSWORD = "pypi-..." # paste the project-scoped fallback token
py -3.11 -m twine upload dist\*
Remove-Item env:TWINE_USERNAME
Remove-Item env:TWINE_PASSWORDUse this if OIDC is unavailable and you do not want credentials on disk. Revoke the token immediately after the fallback upload.
Create ~/.pypirc (on Windows: %USERPROFILE%\.pypirc, e.g. D:\Users\<you>\.pypirc).
Important: do not put an inline # comment on the password line — configparser reads the whole line (including #) as the value, so twine would submit a bogus token and uploads would fail with auth errors. Put any comment on its own line above the password.
[distutils]
index-servers =
pypi
[pypi]
username = __token__
# paste the full pypi-... token on the line below, nothing else on that line
password = pypi-AgENdGVzdC5weXBpLm9yZwIk...After this, the fallback upload command is:
# bash / WSL / macOS
python3.11 -m twine upload dist/*# PowerShell (Windows)
py -3.11 -m twine upload dist\*Security tradeoff — read before choosing Option B: Persistent .pypirc
keeps a valid upload token on disk. Prefer OIDC, then env vars. If .pypirc is
unavoidable, keep it on a trusted local disk only, delete it immediately after
the fallback upload, and revoke the token.
On Windows PowerShell, twine upload can abort with UnicodeEncodeError: 'cp932' codec when rendering the progress bar. Prefix the command with UTF-8 env vars:
$env:PYTHONIOENCODING = "utf-8"
$env:PYTHONUTF8 = "1"
py -3.11 -m twine upload dist\*The upload fails silently if the encoding error fires before the HTTP request completes — always verify with the PyPI project page afterward (see sanity check below).
PyPI shows a token only at creation. If you've lost it:
- Check password managers (1Password / Bitwarden / Chrome) for
pypior the project name - Check Gmail for
from:noreply@pypi.org(recovery codes were emailed at 2FA setup) - Check mobile Authenticator apps (Google Authenticator / Authy / 1Password) for a
pypi.orgentry - Last resort: email
admin@pypi.orgwith account proof (takes 1-3 days)
Once logged in, delete the old token entry and create a fresh one — old and new can coexist, so no harm in generating a replacement.
Use annotated tags (with -a -m) so git log shows the release info clearly:
git tag -a vX.Y.Z -m "Release vX.Y.Z — <headline>"
git push origin vX.Y.Z
gh release create vX.Y.Z --title "vX.Y.Z — <headline>" --notes-file release-notes/RELEASE_NOTES_vX.Y.Z.md dist/*.whl dist/*.tar.gz(The gh release create form above attaches artifacts in one call — no separate gh release upload needed.)
Post-release sanity check:
curl -s https://pypi.org/simple/siglume-api-sdk/ | grep X.Y.Zreturns multiple matches (whl + sdist registered)pip install siglume-api-sdk==X.Y.Zin a clean venv succeedspython -c "import siglume_api_sdk; print(siglume_api_sdk.__name__)"printssiglume_api_sdk- The GitHub Release page shows the two asset files with the correct SHAs
The PyPI JSON API (https://pypi.org/pypi/siglume-api-sdk/json) can lag a few minutes for info.version to reflect the latest. The simple/ index updates immediately.
For bug fixes, repeat the full flow with the next patch version. Update:
pyproject.toml→ bump patch versionCHANGELOG.md→ add a new[X.Y.Z+1]section- Create
release-notes/RELEASE_NOTES_vX.Y.Z+1.md(can be terse for patch releases) - Rebuild + upload + tag + Release
Use .post when you need to ship a metadata-only change (typos in README.md, broken URL in pyproject.toml, etc.) without a code change:
siglume-api-sdk 0.1.0 # original release
siglume-api-sdk 0.1.0.post1 # docs-only fix
A .post release still increments the PyPI version, so users pip install -U can pick it up. Bump [project].version = "0.1.0.post1" and follow the same build/upload flow. Do not use .post for code changes — those go to the next patch release.
If a release is broken enough that it must be withdrawn, yank (don't delete — PyPI forbids deletion of a version once uploaded):
# bash / WSL / macOS
python3.11 -m twine yank --reason "broken: <one-line explanation>" siglume-api-sdk==X.Y.ZOn Windows PowerShell: py -3.11 -m twine yank --reason "broken: <one-line explanation>" siglume-api-sdk==X.Y.Z
Yanking makes the version invisible to pip install siglume-api-sdk resolution (it will skip the yanked version unless explicitly pinned), but anyone who already installed it keeps working. Follow up with a fixed release (X.Y.Z+1 or X.Y.Z.post1) ASAP.
Also: mark the yanked GitHub Release as "pre-release" and edit the body to start with ⚠️ YANKED -- see vA.B.C.
- Cut a branch off the broken tag:
git checkout -b hotfix/vX.Y.Z+1 vX.Y.Z - Apply the minimal fix + a regression test
- Yank the broken version first (above), then run the full release flow for
vX.Y.Z+1 - Merge the hotfix branch back to
main
Semantic versioning (https://semver.org):
- Major (X) — breaking API changes (rename, remove, signature change)
- Minor (Y) — additive (new modules, new manifest fields, new example)
- Patch (Z) — bug fix, no API surface change
.postN— docs / packaging metadata only
The project is 0.Y.Z (alpha) until the public surface stabilises. While under 0., minor bumps may include breaking changes; we'll call them out explicitly in CHANGELOG.md.