|
| 1 | +# Release process |
| 2 | + |
| 3 | +This document is the **operational** companion to [`.github/workflows/release.yml`](../.github/workflows/release.yml). The workflow is tag-triggered (`v*.*.*`); pushing such a tag builds wheel + sdist, attaches a `testBench-vX.Y.Z.zip`, creates a GitHub Release, and publishes to PyPI. **All three actions are partially or fully irreversible** — PyPI in particular does not allow re-publishing a version. Run through this checklist before tagging. |
| 4 | + |
| 5 | +## One-time setup |
| 6 | + |
| 7 | +These have to be done once per repository and persist across releases. |
| 8 | + |
| 9 | +### 1. GitHub Environment: `pypi` |
| 10 | + |
| 11 | +The release workflow's `pypi` job declares `environment: pypi`. That environment must exist on the repo, otherwise the OIDC token exchange with PyPI's Trusted Publisher endpoint fails. |
| 12 | + |
| 13 | +```bash |
| 14 | +# Create the empty environment (idempotent) |
| 15 | +gh api -X PUT repos/knowledgestack/ks-xlsx-parser/environments/pypi |
| 16 | +``` |
| 17 | + |
| 18 | +Optional but recommended after creation: open https://github.com/knowledgestack/ks-xlsx-parser/settings/environments/pypi and add a **required reviewer** so a tag push needs explicit approval before the PyPI publish step runs. This is a safety net — once a tag is pushed the release workflow auto-fires; a reviewer gate gives you one last "are you sure?" before the irreversible PyPI publish. |
| 19 | + |
| 20 | +### 2. PyPI Trusted Publisher binding |
| 21 | + |
| 22 | +This **cannot** be done via API or a PR — it requires logging into pypi.org as a maintainer of `ks-xlsx-parser`. |
| 23 | + |
| 24 | +1. Go to https://pypi.org/manage/project/ks-xlsx-parser/settings/publishing/ |
| 25 | +2. Click **Add a new publisher** → **GitHub** |
| 26 | +3. Fill in: |
| 27 | + - **PyPI Project Name:** `ks-xlsx-parser` |
| 28 | + - **Owner:** `knowledgestack` |
| 29 | + - **Repository:** `ks-xlsx-parser` |
| 30 | + - **Workflow filename:** `release.yml` |
| 31 | + - **Environment name:** `pypi` |
| 32 | +4. Save. |
| 33 | + |
| 34 | +Verify with: |
| 35 | + |
| 36 | +```bash |
| 37 | +# Should list any publishers tied to the project (requires you to be logged in) |
| 38 | +open "https://pypi.org/manage/project/ks-xlsx-parser/settings/publishing/" |
| 39 | +``` |
| 40 | + |
| 41 | +If you're spinning up a new project that doesn't exist on PyPI yet, the publisher has to be configured as a **pending publisher** under your account first. Same form, accessible at https://pypi.org/manage/account/publishing/. |
| 42 | + |
| 43 | +### 3. Branch protection on `main` |
| 44 | + |
| 45 | +CI on a PR validates {ubuntu, macOS} × Python {3.10, 3.11, 3.12} before merge. Add a branch protection rule on `main` requiring those status checks to pass before a PR is mergeable: |
| 46 | + |
| 47 | +```bash |
| 48 | +gh api -X PUT repos/knowledgestack/ks-xlsx-parser/branches/main/protection \ |
| 49 | + -F required_status_checks[strict]=true \ |
| 50 | + -F 'required_status_checks[contexts][]=tests (ubuntu-latest / py3.10)' \ |
| 51 | + -F 'required_status_checks[contexts][]=tests (ubuntu-latest / py3.11)' \ |
| 52 | + -F 'required_status_checks[contexts][]=tests (ubuntu-latest / py3.12)' \ |
| 53 | + -F 'required_status_checks[contexts][]=tests (macos-latest / py3.10)' \ |
| 54 | + -F 'required_status_checks[contexts][]=tests (macos-latest / py3.11)' \ |
| 55 | + -F 'required_status_checks[contexts][]=tests (macos-latest / py3.12)' \ |
| 56 | + -F 'required_status_checks[contexts][]=testBench round-trip (ubuntu / py3.12)' \ |
| 57 | + -F enforce_admins=false \ |
| 58 | + -F required_pull_request_reviews[required_approving_review_count]=1 \ |
| 59 | + -F restrictions= 2>/dev/null |
| 60 | +``` |
| 61 | + |
| 62 | +Or set in the UI: https://github.com/knowledgestack/ks-xlsx-parser/settings/branches |
| 63 | + |
| 64 | +## Per-release checklist |
| 65 | + |
| 66 | +For every new version `X.Y.Z`: |
| 67 | + |
| 68 | +1. **Decide the version number.** Follow [SemVer](https://semver.org/). Breaking API change → major bump. New feature, no breakage → minor. Bugfix only → patch. |
| 69 | +2. **Bump version in two places** (kept in sync to avoid drift): |
| 70 | + - `pyproject.toml` — `version = "X.Y.Z"` |
| 71 | + - `src/ks_xlsx_parser/__init__.py` — `__version__ = "X.Y.Z"` |
| 72 | +3. **Write the CHANGELOG entry** under a new `## [X.Y.Z] — YYYY-MM-DD` heading in [`CHANGELOG.md`](../CHANGELOG.md). Use the section labels documented at the top of that file (Added / Changed / Fixed / Performance / Docs / Internal / ⚠️ BREAKING). |
| 73 | +4. **(Optional but recommended) Write hand-curated release notes** at `docs/launch/RELEASE_NOTES_vX.Y.Z.md`. If present, the release workflow picks it up automatically as the GitHub Release body; otherwise GitHub auto-generates from commits. |
| 74 | +5. **Run `make test` locally** and verify all tests pass. |
| 75 | +6. **Open a PR**, wait for CI green on all matrix cells, get a review, merge to `main`. |
| 76 | +7. **Tag from `main`:** |
| 77 | + ```bash |
| 78 | + git checkout main && git pull |
| 79 | + git tag -a vX.Y.Z -m "vX.Y.Z — <short tagline>" |
| 80 | + git push origin vX.Y.Z |
| 81 | + ``` |
| 82 | +8. **Watch the workflow.** https://github.com/knowledgestack/ks-xlsx-parser/actions — the `Release` workflow should run `build` → `github-release` → `pypi`. If the `pypi` job is gated on a reviewer, approve it in the Actions UI. |
| 83 | +9. **Verify post-release:** |
| 84 | + - PyPI: https://pypi.org/project/ks-xlsx-parser/X.Y.Z/ resolves and `pip install ks-xlsx-parser==X.Y.Z` works in a fresh venv. |
| 85 | + - GitHub Release: https://github.com/knowledgestack/ks-xlsx-parser/releases/tag/vX.Y.Z shows the release notes + wheel + sdist + `testBench-vX.Y.Z.zip`. |
| 86 | + - The `[Unreleased]` heading at the top of `CHANGELOG.md` is reset to "Nothing yet" for the next cycle (manual; do this in a follow-up PR). |
| 87 | + |
| 88 | +## Common failure modes |
| 89 | + |
| 90 | +**`pypi` job fails with "trusted publisher" error.** The PyPI Trusted Publisher binding is missing or pointing at the wrong workflow / environment. Fix at pypi.org (step 2 above). The GitHub Release will already exist — once the binding is correct, re-run the failed job via `gh run rerun --failed`. |
| 91 | + |
| 92 | +**`pypi` job fails with "version already exists".** Someone published this version manually or a previous CI run partially succeeded. PyPI does not allow republishing. Bump to the next patch (`X.Y.Z+1`), update CHANGELOG / `RELEASE_NOTES`, delete the local tag, re-tag, force-delete the remote tag if it exists (`git push --delete origin vX.Y.Z`), and re-push. |
| 93 | + |
| 94 | +**CI fails on a matrix cell after a tag is pushed.** The tag-trigger workflow doesn't depend on CI — it runs in parallel. If CI is red, delete the tag (`git push --delete origin vX.Y.Z`), fix the issue on a PR, then re-tag from the merged fix. The GitHub Release artifact from the doomed run can be deleted via `gh release delete vX.Y.Z`. |
| 95 | + |
| 96 | +**Wheel doesn't include something it should.** Check `MANIFEST.in` / `pyproject.toml`'s `[tool.setuptools.packages.find]`. The `py.typed` marker, `*.pyi` stubs, and any data files (`.json`, `.xlsx` fixtures) only ship if explicitly declared. |
| 97 | + |
| 98 | +## Hotfix / yank |
| 99 | + |
| 100 | +If a published release has a critical bug: |
| 101 | + |
| 102 | +```bash |
| 103 | +# Yank from PyPI (hides from `pip install` but doesn't delete — required for cache-poisoning safety) |
| 104 | +# UI: https://pypi.org/manage/project/ks-xlsx-parser/release/X.Y.Z/ |
| 105 | + |
| 106 | +# Tag a hotfix |
| 107 | +git checkout main |
| 108 | +# … apply fix, bump to X.Y.Z+1, update CHANGELOG … |
| 109 | +git tag -a vX.Y.Z+1 -m "vX.Y.Z+1 — hotfix for <issue>" |
| 110 | +git push origin vX.Y.Z+1 |
| 111 | +``` |
| 112 | + |
| 113 | +Yanked versions remain installable if pinned explicitly; `pip install ks-xlsx-parser` without a version constraint skips them. This is the safe default for accidental release of broken code. |
| 114 | + |
| 115 | +## Why we use Trusted Publishing instead of an API token |
| 116 | + |
| 117 | +PyPI API tokens stored as repo secrets are forge-able by anyone with write access to the workflow file. Trusted Publishing uses GitHub OIDC: only a real run of the configured workflow on the configured environment can mint a publish token, and the token is short-lived. No long-lived secret lives in the repo. Trade-off: one-time PyPI-side setup (step 2) and the environment binding (step 1) are both required. |
0 commit comments