中文版:RELEASING_zh.md
QwenPaw ships four artifacts from a single version — the PyPI wheel, the Docker image, the desktop apps (Tauri, Windows + macOS) and the plugins bundle. They are published together by one orchestrated workflow so that a failure in any one of them blocks the whole release: you can never end up with, e.g., a web release that has no matching desktop build.
Orchestrator:
.github/workflows/release.yml. The older per-artifact workflows are kept as a fallback — see Rollback to the legacy flow.
- Create a draft GitHub Release (tag + notes). Do not click Publish.
- Actions → Release (unified) → Run workflow (leave
dry_runoff). - It builds + verifies everything; only if all of it passes does it publish all artifacts and flip the release to published.
- If anything fails, nothing is published and the draft is left untouched — fix and re-run.
release.yml runs in three phases:
- Resolve — finds the target draft (the
taginput, or auto-detects the single existing draft), resolves the draft'starget_commitishto a concrete SHA, and pins every downstream job to that SHA (so what is built == what is published). On a real release it also fails fast if the DashScope secret is missing. - Prepare (build + verify, publishes nothing) — in parallel:
build-wheel— build the Python wheel (with the bundled console).verify-web— pip-install, Docker health-check and install-script checks.build-desktop— build the Tauri Windows + macOS apps and run the install → launch → real-chat UI verification.build-plugins— pack the plugin bundle.
- Gate + Publish — every publish job
needsall prepare jobs, so a single failure above skips the entire publish phase. When all prepare jobs are green: publish to PyPI, push the multi-arch Docker image, attach the desktop installers to the release + upload them to OSS, publish plugins — then, as the last step, flip the draft to published (pinned to the built SHA). After publishing, it promotes the desktoplatest/updater, deploys the website (stable/post only — betas are skipped), and opens the Release Duty verification issue.
A full run is ~60–75 min, dominated by the desktop Tauri builds.
- Create the draft release
- UI: Releases → Draft a new release → set the tag + notes → Save draft (do not publish). For a pre-release, tick Set as a pre-release.
- CLI:
gh release create v2.0.0-beta.8 --draft --prerelease \ --target main --title "v2.0.0-beta.8" --notes "..."
- The tag should correspond to
src/qwenpaw/__version__.py(resolvevalidates this withpackagingnormalization and fails on a mismatch, e.g. tagv2.0.1-beta.1must match version2.0.1b1). - Prefer pinning the draft to a commit (
--target <sha>). If you use--target main, avoid merging tomainbetween creating the draft and running the workflow, otherwise the build uses the newermainHEAD.
- Run the workflow: Actions → Release (unified) → Run workflow on
main. Leavetagempty to auto-detect the single draft (or set it explicitly); leavedry_rununchecked. - Watch it: on success the release flips to published with all artifacts attached and a Release Duty issue is opened. On failure, see Troubleshooting.
The procedure is identical for all types — the type is inferred from the tag:
| Type | Example tag | Draft "pre-release"? | Docker tags | PyPI |
|---|---|---|---|---|
| beta / rc / alpha / dev | v2.0.0-beta.8 |
yes | <version> + pre (no latest) |
uploaded; treated as a pre-release by pip (--pre) |
| stable | v2.0.0 |
no | <version> + pre + latest |
normal |
| post | v2.0.0.post4 |
no | <version> + pre + latest |
post release |
Notes:
- Pre-release detection is tag-based: a tag containing
beta/alpha/rc/devis a pre-release (so use the-beta.Nform);stableand.postNtags also update the Dockerlatesttag. - The website (GitHub Pages,
qwenpaw.agentscope.io) is deployed only for stable and.postNreleases; pre-releases are skipped so the public site advertises only GA versions. - The desktop OSS
latestfiles and the Tauri auto-update manifest are currently updated for every release, including betas (this matches the previousdesktop-release.ymlbehavior and is unchanged here). Making the desktoplatest/updater stable-only is a possible future improvement.
Guarantee: the draft is flipped to published only after all publish jobs succeed. If anything fails, the release stays a draft.
| Situation | What happened | What to do |
|---|---|---|
| A prepare job fails (desktop / web verify / wheel / plugins) | All publish + finalize + duty-issue are skipped; nothing published; draft untouched |
Read the failed job's logs and fix (or re-run if flaky) → Re-run failed jobs, or re-run the workflow. No cleanup needed. |
| A publish job fails after the gate (e.g. Docker push fails after PyPI already uploaded) | finalize needs all publishes, so the draft is not flipped; but some artifacts may already be live |
Re-run failed jobs (already-succeeded jobs are not re-run; Docker re-push is idempotent, OSS uses --force) → the draft flips once they pass. If a published PyPI version is now taken and cannot be reused, cut a .postN instead. |
finalize fails |
All artifacts published but the release was not flipped | Re-run finalize, or manually gh release edit <tag> --draft=false --target <sha> (or click Publish). |
duty-issue fails |
Release is published; only the tracking issue is missing | Re-run the job, or dispatch release-duty.yml with the tag. Non-blocking. |
promote-desktop fails |
Release is published, but the desktop latest files / updater manifest / index were not refreshed (existing users' auto-updater does not see the new version yet; versioned downloads still work) |
Re-run the job — it is idempotent (ossutil cp --force). Non-blocking for first-install users. |
deploy-website fails (stable/post only) |
Release is published, but the public site (qwenpaw.agentscope.io) still shows the previous version |
Re-run the job, or manually dispatch deploy-website.yml (workflow_dispatch). Idempotent, non-blocking. |
| "Multiple draft releases found" | More than one draft exists | Re-run Run workflow with an explicit tag. |
| "No draft release found" / "not a draft" | No draft, or wrong tag | Create the draft / fix the tag, then re-run. |
| resolve rejects the tag (version mismatch) | The draft tag doesn't match src/qwenpaw/__version__.py |
Align the tag with the version (packaging-normalized, e.g. v2.0.1-beta.1 ↔ 2.0.1b1), then re-run. |
The pre-existing per-artifact workflows are intentionally retained. If the
orchestrator is broken, publish the release the old way — Publish the GitHub
Release (or gh release create ...), which triggers publish-pypi /
docker-release / desktop-release / plugins-release on release: published.
Warning: the legacy flow does not gate the web release on the desktop build (the very problem this orchestrator fixes), so use it only as an emergency fallback.
Run Release (unified) with dry_run: true to exercise the gate and the
draft→published flip without touching production: PyPI upload, Docker push
and OSS upload become no-ops, while the desktop build/verify, the draft flip and
the duty issue still run for real. On a fork this only affects the fork's own
release page.
Note: the desktop build's install → launch → chat UI verification still runs
under dry_run and needs the QWENPAW_DASHSCOPE_API_KEY secret — dry_run only
skips the resolve-stage fail-fast check, not the verification itself.