From 842751fd69bad0a1cc03525574272edea0986aab Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 24 Sep 2026 16:21:15 -0500 Subject: [PATCH 1/2] Prepare Release workflow and towncrier changelog, as in anyplotlib - .github/workflows/prepare_release.yml (run from the Actions tab): bumps the version in pyproject.toml, builds CHANGES.rst from upcoming_changes/ with towncrier and opens a Release PR. Publishing a GitHub release still runs publish.yaml, which uploads to PyPI (its trusted publisher is unchanged). - Versions become major.minor.patch (5.3b6 -> 5.3.0b7, the same version under PEP 440); deapi/version.py, the protocol version sent to DE-Server, is left alone. - upcoming_changes/README.rst explains the fragment types and naming. --- .github/workflows/prepare_release.yml | 192 ++++++++++++++++++ CHANGES.rst | 7 + pyproject.toml | 47 +++++ .../+prepare-release.maintenance.rst | 1 + upcoming_changes/.gitkeep | 0 upcoming_changes/README.rst | 92 +++++++++ 6 files changed, 339 insertions(+) create mode 100644 .github/workflows/prepare_release.yml create mode 100644 upcoming_changes/+prepare-release.maintenance.rst create mode 100644 upcoming_changes/.gitkeep create mode 100644 upcoming_changes/README.rst diff --git a/.github/workflows/prepare_release.yml b/.github/workflows/prepare_release.yml new file mode 100644 index 00000000..17934129 --- /dev/null +++ b/.github/workflows/prepare_release.yml @@ -0,0 +1,192 @@ +name: Prepare Release + +# Run manually from the Actions tab. +# Opens a PR that bumps the version in pyproject.toml and builds CHANGES.rst from +# the towncrier fragments in upcoming_changes/. After merging it, create the GitHub +# Release by hand (tag v); publishing the release triggers publish.yaml, +# which uploads to PyPI. +on: + workflow_dispatch: + inputs: + bump: + description: "Version component to bump" + required: true + type: choice + options: + - pre-release # increments the bN counter on the current base version (5.3b6 -> 5.3.0b7) + - finalize # drop the bN suffix: release the current beta's base as stable (5.3b6 -> 5.3.0) + - minor + - bugfix + - major + beta: + description: "Mark as beta pre-release (adds bN suffix). Ignored for 'pre-release' (always beta) and 'finalize' (always stable)." + required: false + type: boolean + default: false + +permissions: + contents: write # push branch + pull-requests: write # open PR + +jobs: + prepare: + name: Prepare release PR + runs-on: ubuntu-latest + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up uv + uses: astral-sh/setup-uv@v5 + with: + python-version: "3.13" + enable-cache: true + + # ── Compute the new version ────────────────────────────────────────── + - name: Compute new version + id: version + env: + BUMP: ${{ inputs.bump }} + IS_BETA: ${{ inputs.beta }} + run: | + CURRENT=$(grep '^version = ' pyproject.toml | sed 's/version = "\(.*\)"/\1/') + export CURRENT_VERSION="$CURRENT" + + NEW_VERSION=$(python3 - <<'PYEOF' + import re, os + + current = os.environ["CURRENT_VERSION"] + bump = os.environ["BUMP"] + is_beta = os.environ["IS_BETA"].lower() == "true" + + # deapi's versions have had no patch component (5.3b6); read it as 0 and + # always write major.minor.patch from here on (5.3b6 == 5.3.0b6 in PEP 440). + m = re.match(r"^(\d+)\.(\d+)(?:\.(\d+))?(?:b(\d+))?$", current) + if not m: + raise SystemExit(f"cannot parse the current version {current!r}") + major = int(m.group(1)) + minor = int(m.group(2)) + patch = int(m.group(3) or 0) + beta_n = int(m.group(4)) if m.group(4) else None + on_beta = beta_n is not None + + if bump == "finalize": + # Release the current beta's base as stable: just drop the bN + # suffix, keep major.minor.patch. e.g. 5.3.0b7 -> 5.3.0. + if not on_beta: + raise SystemExit( + f"'finalize' requires a beta base version, but current " + f"version {current!r} has no bN suffix. Use minor / bugfix " + f"/ major to start a new release instead." + ) + is_beta = False + elif bump == "pre-release": + # Keep the same base; just walk the beta counter forward. + is_beta = True + beta_n = (beta_n or 0) + 1 + elif on_beta: + # We are on a beta of the NEXT release (e.g. 5.3.0b7). The base + # major.minor.patch is that upcoming version, so minor/bugfix/major + # would skip it entirely. The common intent from a beta is + # 'finalize', so steer the user there rather than guess. + raise SystemExit( + f"Current version {current!r} is a beta of the upcoming " + f"{major}.{minor}.{patch} release. To ship it, use bump=" + f"'finalize' (-> {major}.{minor}.{patch}). A '{bump}' bump from " + f"a beta would skip {major}.{minor}.{patch} entirely; " + f"that is almost never intended." + ) + elif bump == "major": + major, minor, patch = major + 1, 0, 0 + elif bump == "minor": + minor, patch = minor + 1, 0 + elif bump == "bugfix": + patch += 1 + + if is_beta: + if bump != "pre-release": + beta_n = 1 # fresh beta series for the new base + print(f"{major}.{minor}.{patch}b{beta_n}", end="") + else: + print(f"{major}.{minor}.{patch}", end="") + PYEOF + ) + + # Derive is_beta from the COMPUTED version (ends in bN?), not the raw input. + if [[ "$NEW_VERSION" =~ b[0-9]+$ ]]; then IS_BETA_OUT=true; else IS_BETA_OUT=false; fi + + echo "new_version=$NEW_VERSION" >> "$GITHUB_OUTPUT" + echo "tag=v$NEW_VERSION" >> "$GITHUB_OUTPUT" + echo "branch=release/v$NEW_VERSION" >> "$GITHUB_OUTPUT" + echo "is_beta=$IS_BETA_OUT" >> "$GITHUB_OUTPUT" + echo "Bumping (${{ inputs.bump }}): $CURRENT → $NEW_VERSION" + + # ── Bump version strings ───────────────────────────────────────────── + # Only the package version: deapi/version.py holds the client's protocol + # version (sent to DE-Server), which does not follow package releases. + - name: Bump version in pyproject.toml + run: | + sed -i 's/^version = ".*"/version = "${{ steps.version.outputs.new_version }}"/' pyproject.toml + + # ── Build changelog ────────────────────────────────────────────────── + - name: Build changelog with towncrier + run: | + FRAGMENT_COUNT=$(find upcoming_changes -maxdepth 1 -name "*.rst" \ + ! -name "README.rst" | wc -l) + if [ "$FRAGMENT_COUNT" -eq 0 ]; then + echo "⚠ No news fragments found — skipping towncrier (CHANGES.rst unchanged)." + else + uvx towncrier build --yes --version "${{ steps.version.outputs.new_version }}" + fi + + # ── Commit and push ────────────────────────────────────────────────── + - name: Configure git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Commit release changes + run: | + git checkout -b "${{ steps.version.outputs.branch }}" + + # Stage the version bump, updated changelog, and consumed fragments. + git add pyproject.toml CHANGES.rst + git add -A upcoming_changes/ # stages deleted fragment files + + git commit -m "chore: prepare release ${{ steps.version.outputs.tag }}" + git push origin "${{ steps.version.outputs.branch }}" + + # ── Open pull request ──────────────────────────────────────────────── + - name: Open pull request + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + TAG: ${{ steps.version.outputs.tag }} + BRANCH: ${{ steps.version.outputs.branch }} + IS_BETA: ${{ steps.version.outputs.is_beta }} + run: | + if [ "$IS_BETA" = "true" ]; then PRE="tick **Set as a pre-release**"; else PRE="leave **Set as a pre-release** unticked"; fi + gh pr create \ + --title "Release ${TAG}" \ + --base main \ + --head "${BRANCH}" \ + --body "## Release ${TAG} + + > Auto-generated by the **Prepare Release** workflow. + + ### What changed + - Version bumped to \`${TAG#v}\` in \`pyproject.toml\` + - \`CHANGES.rst\` updated from the towncrier fragments in \`upcoming_changes/\` + + ### Review checklist + - [ ] \`CHANGES.rst\` reads well — edit the text directly in this PR if needed + - [ ] The version in \`pyproject.toml\` is correct + - [ ] CI passes + + ### After merging + Create the GitHub Release by hand: **Releases → Draft a new release**, tag + \`${TAG}\` on \`main\`, paste this version's section of \`CHANGES.rst\` as the + notes, ${PRE}, and publish. Publishing it runs \`publish.yaml\`, which uploads + to PyPI." diff --git a/CHANGES.rst b/CHANGES.rst index dbd2701d..e643aa7f 100644 --- a/CHANGES.rst +++ b/CHANGES.rst @@ -4,6 +4,13 @@ Changelog ********* This document describes the changes in the DEAPI library. + +Changes are filed as fragment files in ``upcoming_changes/`` and assembled into this +file by `towncrier `_ when a release is prepared +(see ``upcoming_changes/README.rst``). + +.. towncrier release notes start + 5.3.beta6 ========= - Fixed invalid property errors and bugs in set_binning diff --git a/pyproject.toml b/pyproject.toml index 14c72e4d..70a19de9 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -77,3 +77,50 @@ Source = "https://github.com/directelectron/deapi" [project.scripts] pydeserver = "deapi.simulated_server.initialize_server:main" de_service = "deapi.service.de_service:main" + +[tool.towncrier] +package = "deapi" +package_dir = "." +directory = "upcoming_changes" +filename = "CHANGES.rst" +start_string = ".. towncrier release notes start\n" +issue_format = "`#{issue} `_" +title_format = "{version} ({project_date})" +underlines = ["=", "-", "~"] + +# Behaviour changes lead the release notes: they are the entries a reader +# upgrading has to act on, and they are easy to miss filed under "Bug Fixes". +[[tool.towncrier.type]] +directory = "api_change" +name = "API and Behaviour Changes" +showcontent = true + +[[tool.towncrier.type]] +directory = "new_feature" +name = "New Features" +showcontent = true + +[[tool.towncrier.type]] +directory = "bugfix" +name = "Bug Fixes" +showcontent = true + +[[tool.towncrier.type]] +directory = "deprecation" +name = "Deprecations" +showcontent = true + +[[tool.towncrier.type]] +directory = "removal" +name = "Removals" +showcontent = true + +[[tool.towncrier.type]] +directory = "doc" +name = "Documentation" +showcontent = true + +[[tool.towncrier.type]] +directory = "maintenance" +name = "Maintenance" +showcontent = true diff --git a/upcoming_changes/+prepare-release.maintenance.rst b/upcoming_changes/+prepare-release.maintenance.rst new file mode 100644 index 00000000..4cb8da07 --- /dev/null +++ b/upcoming_changes/+prepare-release.maintenance.rst @@ -0,0 +1 @@ +Releases are prepared by the **Prepare Release** workflow, and the changelog is assembled from towncrier fragments in ``upcoming_changes/``, as in the other Direct Electron Python packages. diff --git a/upcoming_changes/.gitkeep b/upcoming_changes/.gitkeep new file mode 100644 index 00000000..e69de29b diff --git a/upcoming_changes/README.rst b/upcoming_changes/README.rst new file mode 100644 index 00000000..a4863ff5 --- /dev/null +++ b/upcoming_changes/README.rst @@ -0,0 +1,92 @@ +Filing Change Log Entries +========================= + +deapi uses `towncrier `_ to manage its +changelog. When you open a pull request that should appear in the next release +notes, add a short news **fragment file** to this directory as part of that PR. + +Naming convention +----------------- + +Each fragment is a plain ``.rst`` file named:: + + {PR_number}.{type}.rst + +where ``{PR_number}`` is the GitHub pull-request number and ``{type}`` is one +of the types below. + +If a change has no natural PR number (e.g. work batched on a long-lived +feature branch), name the file ``+{slug}.{type}.rst`` — the leading ``+`` +marks it as an "orphan" fragment so towncrier omits the issue link. Without +it, the slug is rendered as a broken PR link in the changelog (this bit the +0.2.0 notes). + +================= ============================================================== +Type Use when … +================= ============================================================== +``api_change`` Existing behaviour changed in a way a user has to act on — + a signature, a default, or a gesture that now does something + different. Use this even when the change is a *fix*: what + matters to a reader upgrading is that the old behaviour is + gone, and that is easy to miss under ``bugfix``. +``new_feature`` A user-visible capability has been added. +``bugfix`` A bug has been fixed. +``deprecation`` Something is deprecated and will be removed in a future release. +``removal`` A previously deprecated API has been removed. +``doc`` Documentation improved without any code change. +``maintenance`` Internal / infrastructure change invisible to end users. +================= ============================================================== + +Content guidelines +------------------ + +* **One sentence per file**, written in the **past tense**, from a user's + perspective. +* Cross-reference the relevant class or function with a Sphinx role where + it adds value. +* Do **not** include the PR number in the sentence body — towncrier appends + the link automatically. + +Examples +-------- + +``123.new_feature.rst``:: + + Added :meth:`~deapi.Client.get_virtual_image_buffer` for reading virtual + images while a scan is still running. + +``124.bugfix.rst``:: + + Fixed :meth:`~deapi.Client.set_binning` failing to set hardware binning to 1. + +``125.deprecation.rst``:: + + Deprecated ``Client.SetProperty``; use ``client[name] = value`` instead. + ``SetProperty`` will be removed in a future release. + +``126.removal.rst``:: + + Removed ``Client.GetImage``, which was deprecated since 5.2. + +``127.doc.rst``:: + + Added an example that scans a region of interest with an XY array. + +``128.maintenance.rst``:: + + Moved the test workflow to ``uv``. + +Previewing the changelog locally +--------------------------------- + +See what the next release notes would look like **without** modifying any +files or consuming any fragments:: + + uvx towncrier build --draft --version 5.x.0 + +To actually build the changelog (done automatically by the +**Prepare Release** workflow — do not run this by hand unless you know what +you are doing):: + + uvx towncrier build --yes --version 5.x.0 + From 426630e4111b1b4b045a9e4f2cff76a9a3c349a8 Mon Sep 17 00:00:00 2001 From: Carter Francis Date: Thu, 24 Sep 2026 16:21:39 -0500 Subject: [PATCH 2/2] Name the fragment after its PR --- .../{+prepare-release.maintenance.rst => 61.maintenance.rst} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename upcoming_changes/{+prepare-release.maintenance.rst => 61.maintenance.rst} (100%) diff --git a/upcoming_changes/+prepare-release.maintenance.rst b/upcoming_changes/61.maintenance.rst similarity index 100% rename from upcoming_changes/+prepare-release.maintenance.rst rename to upcoming_changes/61.maintenance.rst