diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index c8be00f..d2a9297 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -12,10 +12,17 @@ name: release # `pypi` job fails and everything before it still succeeds, so a tag never # leaves you with half a release and no artifacts. # -# `workflow_dispatch` runs the same pipeline against TestPyPI, which is how to -# validate a change to this file without spending a real version number. PyPI -# releases are effectively permanent: a version can be yanked but never -# replaced, so the dry run is worth the two minutes. +# A release can also be cut entirely from the Actions tab, without anyone +# pushing a tag from a laptop: +# +# Actions -> release -> Run workflow -> target: pypi +# +# That path creates the tag itself, at the commit it built and tested, so the +# tag can never point at something that was never verified. Choosing +# `testpypi` instead runs the identical pipeline against TestPyPI and creates +# no tag, which is how to validate a change to this file without spending a +# real version number. PyPI releases are effectively permanent: a version can +# be yanked but never replaced, so the dry run is worth the two minutes. on: push: @@ -23,7 +30,7 @@ on: workflow_dispatch: inputs: target: - description: "Where to publish" + description: "Where to publish (pypi also tags and cuts the release)" required: true default: testpypi type: choice @@ -168,10 +175,15 @@ jobs: github-release: name: cut the GitHub release needs: [build, pypi] - if: startsWith(github.ref, 'refs/tags/') + # Either a tag was pushed, or someone asked for a real release from the + # Actions tab. A TestPyPI dry run deliberately creates neither tag nor + # release -- it exists to rehearse, not to leave traces. + if: >- + startsWith(github.ref, 'refs/tags/') + || github.event.inputs.target == 'pypi' runs-on: ubuntu-latest permissions: - contents: write # create the release and attach the distributions + contents: write # create the tag, the release, and attach the artifacts steps: - uses: actions/download-artifact@v4 with: @@ -180,6 +192,12 @@ jobs: - name: Create the release uses: softprops/action-gh-release@v2 with: + # Named rather than inherited from the ref, so the dispatch path + # creates `v` at this commit. The release API creates the + # tag when it does not exist, and the build job has already refused + # to get here if the version were inconsistent anywhere. + tag_name: v${{ needs.build.outputs.version }} + target_commitish: ${{ github.sha }} name: jupyddl ${{ needs.build.outputs.version }} body_path: release-notes.md files: dist/* diff --git a/docs/RELEASING.md b/docs/RELEASING.md index 77390c5..381f2b7 100644 --- a/docs/RELEASING.md +++ b/docs/RELEASING.md @@ -1,13 +1,27 @@ # Releasing jupyddl -Releases are cut by pushing a tag. `.github/workflows/release.yml` builds, -verifies, publishes to PyPI and creates the GitHub Release from the changelog. +`.github/workflows/release.yml` builds, verifies, publishes to PyPI and +creates the GitHub Release from the changelog. There are two ways to start it. + +**From the Actions tab** — no local git, nothing depending on one person's +laptop: + +> **Actions → release → Run workflow → target: `pypi`** + +That path creates the tag itself, at the commit it just built and tested, so +the tag cannot end up pointing at something that was never verified. + +**Or by pushing a tag**, if you prefer: ```bash git tag -a v2.3.0 -m "jupyddl 2.3.0" git push origin v2.3.0 ``` +Both run the same checks and produce the same artifacts. Choosing `testpypi` +from the Actions tab rehearses everything and deliberately creates neither a +tag nor a release. + ## One-time setup on PyPI (a human has to do this) The workflow authenticates with **Trusted Publishing** (OIDC), so there is no @@ -55,7 +69,8 @@ number is spent either way. Two minutes on TestPyPI is cheap next to that. ## Cutting a release -1. **Land everything on `main`** and confirm CI is green. +1. **Land everything on `main`** and confirm CI is green. Steps 2-5 prepare the + commit; step 6 is either the Actions button above or a tag push. 2. **Bump the version in two places** — they are checked against each other and against the tag, and a mismatch fails the build rather than publishing a surprise: @@ -69,7 +84,8 @@ number is spent either way. Two minutes on TestPyPI is cheap next to that. python tools/build_web.py ``` 5. Commit, push, merge. -6. **Tag the merge commit** and push the tag. +6. **Cut it**: *Actions → release → Run workflow → `pypi`*, or tag the merge + commit and push the tag. ## What the workflow refuses to do