Publishing simple_module_python
This repo publishes 13 Python packages to PyPI and 3 JS packages to npm in one lockstep version bump. Releases are driven entirely from GitHub Actions — no tokens live on your laptop.
- Python packages (
simple_module_*) → pypi.org - JS packages (
@simple-module-py/*) → npmjs.com - Auth: OIDC Trusted Publishing on both PyPI and npm — no long-lived tokens
- Entry point: Actions →
release→ Run workflow
TL;DR — already set up? Cut a release in 3 clicks
- Ensure
mainis green (make lint && make test). The workflow re-runs the whole PR suite against the dispatched commit and refuses to build if it is red. - Actions → release → Run workflow → pick bump (
patch/minor/major), leave version blank → Run. - After it finishes, write release notes on the auto-created
vX.Y.Ztag on GitHub.
For the very first time, or if any of the above is unfamiliar, keep reading.
First-time setup (once per registry account)
You need to set up Trusted Publisher entries on PyPI and npm before running the workflow. These entries tell each registry: "trust OIDC tokens minted by this exact GitHub Actions workflow." No tokens are exchanged — the registry validates the token's GitHub-issued claims at publish time.
1. PyPI
For each of the 13 published Python project names, on pypi.org:
- Log in as the owner account (
antosubash). - Go to Your account → Publishing (or click "Add a new pending publisher" if the project doesn't exist yet).
- Fill in:
- PyPI Project Name: the exact name, e.g.
simple_module_core - Owner:
antosubash - Repository name:
simple_module_python - Workflow name:
release.yml - Environment name:
pypi
- PyPI Project Name: the exact name, e.g.
- Save.
Repeat for every project in this list:
simple_module_cli
simple_module_core
simple_module_db
simple_module_hosting
simple_module_test
simple_module_auth
simple_module_background_tasks
simple_module_dashboard
simple_module_feature_flags
simple_module_file_storage
simple_module_permissions
simple_module_settings
simple_module_users(simple_module_audit_log and simple_module_keycloak live in the workspace and get version-bumped, but are not in the publish-pypi matrix — they are not published to PyPI.)
Pending publishers: if a project doesn't exist on PyPI yet, "pending publisher" is the right flow — you're reserving the project name and wiring up auth in one step. The first successful publish creates the project and promotes the pending publisher to a real one.
2. npm
npm publishes also use OIDC Trusted Publishing (no NPM_TOKEN secret). On npmjs.com:
Sign in as the owner.
Create the
@simple-module-pyorganization (Settings → "Create a new organization"). This is a one-time step.For each of the 3 npm packages (
@simple-module-py/ui,/i18n,/tsconfig), open the package's Settings → Trusted Publisher page and add a GitHub Actions publisher:- Repository:
antosubash/simple_module_python - Workflow filename:
release.yml - Environment:
npm
For a brand-new scoped package that doesn't exist yet, create it with an initial manual
npm publish(or reserve it) before the Trusted Publisher entry can be attached.- Repository:
3. GitHub Environments
In the repo's Settings → Environments → New environment, create two environments — they match the environment: fields used by the release workflow jobs:
pypinpm
No secrets or variables are needed on either environment — both publish via OIDC. Optionally, add a deployment-protection rule requiring a manual approval on both so every release gets a human click before it goes live.
4. Branch protection
The release workflow pushes a version-bump commit + tag directly to main. If branch protection blocks bots, pick one:
- Add
github-actions[bot]to the allowed-pushers list on themainbranch protection rule, or - Create a fine-grained PAT scoped to this repo's
contents: write, store it as repo secretRELEASE_PUSH_TOKEN— the workflow uses it automatically if present.
Cutting a release
Step 1. Confirm main is green
make lint
make testBoth should pass locally. CI should be green on every PR into main.
Step 2. Pick a version
Every workspace package bumps in lockstep to the same version — scripts/bump_version.py walks every framework/*, modules/*, and packages/* manifest (so the two unpublished modules, audit_log and keycloak, stay in sync too). Only the 13 PyPI + 3 npm packages are then published. We follow a relaxed SemVer during the 0.x phase:
| Situation | Input |
|---|---|
| Bug fix, docs, internal refactor | bump: patch |
| New feature, no breaking changes | bump: minor |
| Breaking change (post-1.0) | bump: major |
| Pre-release rehearsal or specific number | version: e.g. 0.2.0rc1 |
The workflow derives the next version from the current framework/core/pyproject.toml version when you pick a bump level. If you set version explicitly, it overrides the bump choice. Version strings must match ^[0-9]+\.[0-9]+\.[0-9]+([.-]?(a|b|rc|alpha|beta)[0-9]*)?$.
Step 3. Run the release
- Go to Actions → release → Run workflow.
- bump:
patch(default),minor, ormajor. Leave version blank to use it. - version: optional — set to an explicit number (e.g.
0.2.0rc1) to override the bump. - Run workflow.
What happens:
resolvecomputes the final version and shares it with every downstream job.checkscallspr.yml(workflow_call) on the dispatched commit, so thepr-checksaggregate must be green.buildneeds it: nothing is built or published from a red commit.buildrewrites every version, runsuv build --all-packages(a wheel + sdist for every buildable workspace package) plus the 3 npm tarballs, and uploads them as artifacts.publish-pypifans out 13 parallel jobs (the matrix package list), each filtering the artifact set down to one package and publishing its wheel+sdist pair via OIDC Trusted Publishing.publish-npmfans out 3 parallel jobs publishing@simple-module-py/*tarballs via OIDC Trusted Publishing. Each job verifies the tarball'spackage.jsonis actually scoped to@simple-module-py/*before invokingnpm publish.finalizere-applies the bump (the shared.github/actions/bump-versionaction, also used bybuild), cuts the CHANGELOG, commitsrelease: vX.Y.Z, tags it, and pushes both.
Expected wall time: 5–8 minutes.
Step 4. CHANGELOG and GitHub Release notes
finalize runs scripts/cut_changelog.py X.Y.Z before the bump commit: ## [Unreleased] becomes ## [X.Y.Z] - YYYY-MM-DD, a fresh empty ## [Unreleased] is inserted above it and the compare links are updated. Re-running is a no-op once the section exists. Keep [Unreleased] current in every user-visible PR; whatever is there at release time is the release's notes.
The GitHub Release is created with --notes-file from that dated section (scripts/cut_changelog.py X.Y.Z --notes), falling back to --generate-notes only when the section is empty.
To edit the body afterwards, open the release at Releases after the workflow finishes:
gh release view vX.Y.Z --json body
gh release edit vX.Y.Z --notes-file - < notes.mdPackage version vs framework API version
Two version numbers exist on purpose:
- Package version (
0.0.x, lockstep). Everysimple_module_*wheel and everypackage.jsonunderframework/,modules/andpackages/carries the same number, enforced byscripts/bump_version.py(--checkverifies it). The root andhost/pyproject.tomlare not published and are not bumped. - Framework API version (
FRAMEWORK_API_VERSIONinsimple_module_core/versioning.py, currently1.0.0). It versions the module contract (ModuleBase, the registries, the event bus,create_module_base, the model mixins), independent of release cadence. Modules declare a compatible range inModuleMeta.requires_framework(PEP 440, e.g.">=1.0,<2.0") and boot fails on a mismatch. Bump its major only for a breaking contract change.
A test (framework/cli/tests/test_template_requires_framework.py) pins that the scaffolded module template's requires_framework contains FRAMEWORK_API_VERSION.
Local pre-flight (optional but recommended)
Before running the workflow, you can rehearse the whole pipeline offline. Nothing leaves your machine:
# 1. Check every workspace package is currently at 0.0.1 (or your expected base)
uv run python scripts/bump_version.py 0.0.1 --check
# 2. Dry-run the bump (writes nothing, shows what would change)
uv run python scripts/bump_version.py 0.0.2 --dry-run
# 3. Actually bump (commit on a throwaway branch if you want to keep it)
uv run python scripts/bump_version.py 0.0.2
npm install --package-lock-only
# 4. Validate metadata + READMEs
uv run python scripts/check_metadata.py
uv run python scripts/check_readmes.py
# 5. Build everything
rm -rf dist-py dist-npm
uv build --all-packages --out-dir dist-py
mkdir -p dist-npm && for p in packages/*/; do npm pack "$p" --pack-destination dist-npm; done
# 6. Sanity-check contents
ls dist-py/ | wc -l # expect 30 (a wheel + sdist for each of the 15 buildable packages; only 13 are later published)
ls dist-npm/ | wc -l # expect 3
# 7. Revert (if you don't actually want to release)
git reset --hard HEAD~1If step 5 fails for any package, the workflow will fail the same way — fix it before dispatching.
Troubleshooting
Workflow fails at "Trusted publisher not configured"
The PyPI project's Trusted Publisher entry is missing or mismatched. Common causes:
- Wrong workflow filename (must be exactly
release.yml, not the full path) - Wrong environment name (must be exactly
pypi) - Repository owner typo
Fix the entry on the registry, re-run the failed job.
npm publish fails with 401/403 or a weird git error
401/403→ the package's npm Trusted Publisher entry is missing or mismatched (wrong repo, workflow filename, or environment), or thenpmGitHub environment /id-token: writepermission isn't wired up. Fix the entry on npm and re-run. (npm publishes via OIDC now — there is noNPM_TOKENsecret to rotate.)git ls-remote/Permission denied (publickey)→ npm is mis-parsing the tarball path. This is guarded for in the workflow (the path is prefixed with./and the package name is verified against@simple-module-py/<pkg>). If you see it again, the guard has regressed.
git push fails in "Commit, tag, push" step
Branch protection is blocking github-actions[bot]. Add the RELEASE_PUSH_TOKEN secret (see First-time setup → Branch protection) and re-dispatch the workflow. The token is consumed automatically when present.
PyPI publishes succeeded, npm publishes failed (partial release)
PyPI is immutable — you cannot re-upload 0.0.2 under any circumstance. Options:
- Yank the bad PyPI versions via the PyPI project UI (doesn't delete, but hides them from
pip install). - Unpublish the good npm versions within 72 hours:
npm unpublish @simple-module-py/<pkg>@0.0.2for each. - Fix the root cause.
- Bump to
0.0.3and re-run.
A new module was added — how do I include it in releases?
- Version-bumping is automatic —
scripts/bump_version.pydiscovers anymodules/*/pyproject.toml, so a new module is picked up with no edit. - Add it to
.github/workflows/release.ymlunderpublish-pypi→strategy.matrix.package(only matrix packages are published; that's howaudit_log/keycloakstay unpublished). - Create the PyPI Trusted Publisher entry for its project name.
- Add a substantive README (
check_readmes.pywill fail otherwise).
scripts/check_metadata.py and scripts/check_readmes.py run in make lint and will tell you what's missing.
I need to rotate or recover the owner account
Trusted Publishing is tied to the GitHub repo, not any personal account — so both PyPI and npm account handovers are the usual account-transfer flow at each registry, not a code change. The Trusted Publisher entries stay valid as long as the repo, workflow filename, and environment names are unchanged. Update the "Project names" section of this doc afterward.
Reference — what's published where
| Registry | Package | Source |
|---|---|---|
| PyPI | simple_module_cli | framework/cli/ — ships the smpy / simple-module CLI |
| PyPI | simple_module_core | framework/core/ |
| PyPI | simple_module_db | framework/db/ |
| PyPI | simple_module_hosting | framework/hosting/ — ships the sm-host CLI |
| PyPI | simple_module_test | framework/testing/ — pytest plugin |
| PyPI | simple_module_auth | modules/auth/ |
| PyPI | simple_module_background_tasks | modules/background_tasks/ |
| PyPI | simple_module_dashboard | modules/dashboard/ |
| PyPI | simple_module_feature_flags | modules/feature_flags/ |
| PyPI | simple_module_file_storage | modules/file_storage/ |
| PyPI | simple_module_permissions | modules/permissions/ |
| PyPI | simple_module_settings | modules/settings/ |
| PyPI | simple_module_users | modules/users/ |
| npm | @simple-module-py/ui | packages/ui/ |
| npm | @simple-module-py/i18n | packages/i18n/ |
| npm | @simple-module-py/tsconfig | packages/tsconfig/ |
Questions
File an issue: https://github.com/antosubash/simple_module_python/issues