PyPI Package Publishing Guide
This guide explains how to publish qiongli to PyPI, as well as the complete workflow for routine version releases.
0) Prerequisites (One-time Setup)
0.1 PyPI Trusted Publisher
This project uses the Trusted Publisher mechanism for publishing (no manual API Token management needed).
- Log in to your account at pypi.org.
- If this is the first time publishing (the package does not exist on PyPI yet), go to the Publishing page. Under "Add a new pending publisher", fill in the following:
- PyPI Project Name:
qiongli - Owner:
<your-github-username> - Repository name:
qiongli - Workflow name:
publish-pypi.yml - Environment name:
pypi
- PyPI Project Name:
- If the package already exists, go to the package's Settings → Publishing → "Add a new publisher", and fill in the same details.
0.2 GitHub Environment
- Go to your GitHub repository Settings → Environments.
- Click "New environment" and name it
pypi. - (Optional) Add protection rules (e.g., only allow deployment from the
mainbranch, require approval, etc.).
0.3 TestPyPI Trusted Publisher
This repository includes a dedicated TestPyPI workflow: .github/workflows/publish-testpypi.yml.
- Log in to test.pypi.org.
- Go to Account settings → Publishing.
- Add a pending publisher (or add a publisher under the existing project) with:
- PyPI Project Name:
qiongli - Owner:
jxpeng98 - Repository name:
qiongli - Workflow name:
publish-testpypi.yml - Environment name:
testpypi
- PyPI Project Name:
- In GitHub repository Settings → Environments, create environment
testpypi.
1) Routine Publishing Workflow
1.1 Run the End-to-End Publish Automation
Recommended maintainer flow:
./scripts/release_automation.sh publish --version 0.2.0 --from-tag v0.1.0
./scripts/release_automation.sh publish --version 0.2.0b1 --from-tag v0.2.0publish is the canonical release entrypoint. It runs the full local and remote release loop:
- Normalize versions and run
release_ready.sh. - Commit release-prep files.
- Create and push the release tag.
- Let tag-triggered GitHub Actions publish to PyPI and npm.
- Wait for required branch workflows on the release commit:
CICheckout Install Check
- Wait for required tag publish workflows:
Publish to PyPIPublish to npm
- Run postflight with
--create-release, upload plugin artifacts, and write an acceptance receipt.
Routine production publishing must go through ./scripts/release_automation.sh publish. The production publish workflows are tag-triggered execution surfaces, not manual release entrypoints.
Use a stable version such as 0.2.0 or a beta version such as 0.2.0b1. The automation normalizes it into three synchronized forms:
| Layer | Stable | Beta |
|---|---|---|
| PyPI package | 0.2.0 | 0.2.0b1 |
| Skill metadata / registry | 0.2.0 | 0.2.0-beta.1 |
Portable skill VERSION / git tag | v0.2.0 | v0.2.0-beta.1 |
Package version format follows PEP 440, while skill metadata uses SemVer-compatible prerelease syntax. Currently the release tooling supports stable and beta only.
The default release smoke tier is intentionally conservative: builtin literature smoke + doctor. If you also want the heavier parallel / task-run profile-path checks before publishing, add --maintainer-smoke.
Optional full-cycle workflow harness:
python3 tooling/scripts/run_full_cycle_workflow_harness.py \
--fixture tests/fixtures/full_cycle_harness/clean_empirical \
--json-report /tmp/qiongli-full-cycle-harness.jsonThis is preview-only. It verifies stage gates, drift checks, and journal-fit readiness without launching local agents.
Optional subject runtime local-agent smoke
The default release smoke remains preview-first and does not launch local agents. Before a release candidate, maintainers can additionally verify the adaptive subject runtime with the deterministic checks and, when local runtime validation is desired, an opt-in real local-agent run:
uv run python tooling/scripts/run_subject_runtime_smoke.py --json
uv run python tooling/scripts/evaluate_subject_router.py --json
QIONGLI_SMOKE_RUN_AGENTS=1 \
uv run python tooling/scripts/run_subject_runtime_smoke.py \
--mode local-agent \
--case confirmed_finance_guidance_loaded \
--jsonThe local-agent command is opt-in because it launches local runtime agents. Run it only in an isolated environment before release when local runtime validation is desired. It verifies that confirmed subject guidance is loaded through .qiongli/guidance.d/subject-runtime.md, that a local guidance trace is written, and that Qiongli-visible paths remain inside the isolated smoke root.
Optional parallel multi-agent smoke is a heavier maintainer check and also uses the same environment opt-in:
QIONGLI_SMOKE_RUN_AGENTS=1 \
python3 tooling/scripts/smoke_multi_agent.py --run-parallelWithout QIONGLI_SMOKE_RUN_AGENTS=1, --run-parallel records a WARN case and does not launch the parallel Codex/Claude/Antigravity runtime path.
Release doc policy:
- stable releases must be summarized in
CHANGELOG.md; postflight turns that changelog section into GitHub Release notes with a release-category summary and download guide - beta / prerelease releases continue to use
tooling/release/<tag>.md
Beta channel policy:
- beta is optional; publish it when the release needs prerelease validation for release automation, package payloads, installers, package metadata, CI, or publish workflows
- routine docs, small fixes, and low-risk maintenance may publish directly as stable
- if stable publishes without a matching beta, npm
latestadvances while npmnextremains on the previous beta nextmeans "latest prerelease validation build", not "newer than latest stable"; do not publish a mechanical beta only to movenext
1.2 Dry Run / Split Phases
Use release_ready.sh when you want to prepare and verify locally without creating a tag:
./scripts/release_ready.sh --version 0.2.0
./scripts/release_ready.sh --version 0.2.0b1 --from-tag v0.2.0release_ready.sh runs version sync, strict validator, repository unit tests, release-tier smoke, release note evidence updates, package build checks, twine check, and wheel install smoke. It does not tag or push. Publish mode owns commit, branch push, the branch CI/check gate, tag push, tag publish wait, GitHub Release creation, plugin artifact upload, and acceptance receipt generation.
Subject runtime gate checks:
uv run python tooling/scripts/evaluate_subject_router.py --json
uv run python tooling/scripts/evaluate_subject_router.py \
--subject accounting \
--gate runtime-enabled \
--json
uv run python tooling/scripts/evaluate_subject_router.py \
--subject finance \
--gate runtime-enabled \
--json
uv run python tooling/scripts/evaluate_subject_router.py \
--subject economics \
--gate runtime-enabled \
--json
uv run python tooling/scripts/evaluate_subject_router.py \
--subject business \
--gate runtime-enabled \
--jsonThe default router evaluation and accounting, finance, economics, and business runtime-enabled gates must pass for release. Promotion-ready remains a pre-activation review gate for future eval-ready subjects; it should pass before those subjects move to a separate runtime activation PR.
Political economy, geoeconomics, and economics-accounting remain future eval-ready candidates.
For subject-scoped JSON, top-level case_count, metrics, and threshold_failures are filtered to the requested subject gate. Read subject_gate.eligible_for_eval_ready or subject_gate.eligible_for_runtime_promotion or subject_gate.eligible_for_runtime_enabled for the gate eligibility decision. Only eligible_for_runtime_enabled represents runtime activation eligibility. Treat the command exit code as authoritative: default eval exits 0; accounting, finance, economics, and business runtime-enabled checks should exit 0; and future candidate eval-ready checks still exit 0 when their fixture packs and metadata are ready for review.
For beta releases where GitHub Actions may exceed the default local wait window, extend the hard wait instead of using a soft publish gate:
./scripts/release_automation.sh publish --version 0.15.0b2 --from-tag v0.15.0-beta.1 --ci-timeout-seconds 2700Publish mode must verify CI and Checkout Install Check on the release-prep commit before it creates or pushes the release tag. Package registry publishing and GitHub Release creation happen only after the tag publish workflows pass. Soft CI mode remains available for manual post diagnostics, but not for routine publishing.
If you need manual split phases, they still exist:
./scripts/release_automation.sh pre --tag v0.2.0 --from-tag v0.1.0
./scripts/release_automation.sh post --tag v0.2.0 --create-release2) What Happens After The Tag Is Pushed
publish creates and pushes a tag whose format starts with v* and uses repo release syntax such as v0.2.0 or v0.2.0-beta.1. That triggers publish-pypi.yml, which:
- Checkout the code.
- Run
inject_project_toml.shin the checkout so the installed CLI knows its upstream default. - Materialize the release payload into
$RUNNER_TEMP/qiongli-dist. - Verify the release tag against the staged root.
python -m buildfrom the staged root to build the sdist and wheel.twine checkto validate staged package metadata.- Publish to PyPI using the Trusted Publisher mechanism.
The same tag also triggers publish-npm.yml, which validates the staged bundled npm package and publishes stable versions to latest or beta versions to next.
Postflight then waits for the release commit's CI and Checkout Install Check workflows and the tag's Publish to PyPI / Publish to npm workflows. If a required workflow is missing, the diagnostic includes the observed workflow names for that commit.
3) Local Verification (Manual / Optional)
If you want to run package checks outside release_ready.sh, use:
python scripts/materialize_distribution_payloads.py --target all --out /tmp/qiongli-dist --force
bash scripts/verify_release_tag_version.sh --root /tmp/qiongli-dist --tag <tag>
bash scripts/pypi_preflight.sh --root /tmp/qiongli-dist
bash scripts/npm_preflight.sh --root /tmp/qiongli-distEquivalent manual build steps:
# Install build tools
pip install build twine
# Inject upstream repo info
bash /tmp/qiongli-dist/scripts/inject_project_toml.sh
# Build
cd /tmp/qiongli-dist
python -m build
# Validate
twine check dist/*Local dry-run installation:
pip install dist/qiongli_installer-*.whl
qiongli --help
qiongli check --repo <owner>/<repo>4) Publishing to TestPyPI (Recommended Before Production)
Use the GitHub Actions workflow (manual trigger, no tag required):
- Open GitHub Actions.
- Select Publish to TestPyPI.
- Click Run workflow on the target branch.
The workflow will build, validate, and publish with Trusted Publishing to TestPyPI.
Install and verify from TestPyPI:
pip install --index-url https://test.pypi.org/simple/ qiongliRecommended order:
- Run Publish to TestPyPI and validate install/CLI behavior.
- After validation passes, push release tag
v*to trigger Publish to PyPI.
5) Complete Release Checklist
When cutting a release, follow these steps:
- [ ] Confirm all features are merged into
main. - [ ] Ensure CI is passing (Green
ci.yml). - [ ] Optional: run
./scripts/release_ready.sh --version <version>as a local dry run. - [ ] Run
./scripts/release_automation.sh publish --version <version> --from-tag <previous-tag>. - [ ] Confirm
Publish to PyPI,CI, andCheckout Install Checksucceeded on GitHub Actions. - [ ] Confirm postflight created or updated the GitHub Release and wrote
tooling/release/acceptance/<tag>-receipt.md. - [ ] Verify installation:
pipx install qiongli && rsk --help
6) Frequently Asked Questions (FAQ)
Q: I pushed a tag but Actions did not trigger?
Ensure the tag format starts with v (e.g., v0.1.0-beta.7) and that the .github/workflows/publish-pypi.yml workflow file is present on the main branch.
Q: PyPI publishing failed with "403 Forbidden"?
This is typically an issue with the Trusted Publisher setup:
- Make sure the workflow name on PyPI exactly matches
publish-pypi.yml. - Make sure the GitHub environment name exactly matches
pypi. - Verify the owner and repository names are correct.
Q: Upload failed because the version number already exists?
PyPI does not allow overwriting published versions. If you need to ship a fix, you must increment the version number (e.g., 0.1.0b7 → 0.1.0b8).
Q: Is TestPyPI auto-triggered?
No. publish-testpypi.yml is workflow_dispatch only (manual trigger), so it does not create extra tags. Production PyPI remains tag-triggered via publish-pypi.yml (v*).
Q: How do I recall/withdraw a published version?
You can "yank" a version from the PyPI project page (it will not be deleted from users who already installed it, but new pip install commands will skip a yanked version by default).