diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 25e0ec7814..4f6178c9ad 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -223,3 +223,66 @@ jobs: - name: Publish package run: uv publish --verbose --trusted-publishing always + + reset: + name: Draft develop reset PR + # runs after a release is published, opens a PR merging master back into develop + if: ${{ github.event_name == 'release' }} + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + defaults: + run: + shell: bash + steps: + + - name: Checkout master branch + uses: actions/checkout@v7 + with: + ref: master + fetch-depth: 0 + + - name: Setup Python + uses: astral-sh/setup-uv@v7 + with: + enable-cache: false + + - name: Install FloPy + run: uv sync --all-extras + + - name: Update version numbers + id: version + run: | + ver=$(cat version.txt) + next=$(uv run scripts/update_version.py --post-release) + echo "version=$ver" >> $GITHUB_OUTPUT + echo "next=$next" >> $GITHUB_OUTPUT + + - name: Format and lint Python files + run: | + uvx ruff format flopy/version.py + uvx ruff check --fix flopy/version.py + + - name: Draft pull request + env: + GITHUB_TOKEN: ${{ github.token }} + run: | + ver="${{ steps.version.outputs.version }}" + next="${{ steps.version.outputs.next }}" + branch="post-release-$ver-reset" + + git config core.sharedRepository true + git config user.name "github-actions[bot]" + git config user.email "41898282+github-actions[bot]@users.noreply.github.com" + git switch -c "$branch" + git add flopy/version.py version.txt CITATION.cff README.md docs/PyPI_release.md + git commit -m "ci(release): update version to $next" + git push origin "$branch" + + body=' + # Reset `develop` after release '$ver' + + Merge (do not squash) this pull request to bring `master` back into `develop` and set the development version to `'$next'`. + ' + gh pr create -B "develop" -H "$branch" --title "Reset develop after release $ver" --draft --body "$body" diff --git a/docs/make_release.md b/docs/make_release.md index 23b9e5ed62..b88512a9e1 100644 --- a/docs/make_release.md +++ b/docs/make_release.md @@ -96,24 +96,9 @@ For the Conda distribution, there [is a bot](https://github.com/regro-cf-autotic ### Reset the develop branch -Make a new branch from `master`: +Publishing the release triggers a final job to draft a pull request resetting `develop` from `master`. The job sets the development version for the next cycle with `scripts/update_version.py --post-release`: the next anticipated minor version, with a `.devN` segment where `N` is the patch number just released (e.g. `3.10.0.dev2` after `3.9.2`, `3.12.0.dev0` after `3.11.0`). The version number complies with [PEP 440](https://peps.python.org/pep-0440/). -```shell -git checkout master -git switch -c post-x.y.z-release-reset -``` - -Update the version number for the next development cycle: - -```shell -python scripts/update_version.py -v x.y.z.dev0 -``` - -The version number must comply with [PEP 440](https://peps.python.org/pep-0440/). - -Lint and format Python files: `ruff check .` and `ruff format .` from the project root. - -Create and merge (don't squash) a pull request from this branch into `develop`. +Review the draft PR and merge it — don't squash. Squashing removes the commit history from `develop` and causes it to diverge from `master`. ## Conda diff --git a/scripts/README.md b/scripts/README.md index 64b0536af3..12321ada7c 100644 --- a/scripts/README.md +++ b/scripts/README.md @@ -39,16 +39,21 @@ The `update_version.py` script can be used to update FloPy version numbers. Runn The script acquires a file lock before writing to files to make sure only one process edits the files at any given time and prevent desynchronization. -If the script is run with no arguments, the version number is not changed, but updated timestamps are written. To set the version number, use the `--version` (short `-v`) option, e.g.: +If the script is run with no arguments, the version number is not changed, but updated timestamps are written. In all cases the resulting version is printed to stdout. To set the version number, use the `--version` (short `-v`) option, e.g.: ```shell python scripts/update_version.py -v 3.3.6 ``` -To get the current version number, use the `--get` flag (short `-g`): +Two flags derive the version instead of taking it explicitly: + +- `--release` (short `-r`) strips the development segment (e.g. `.dev0`) from the current version, giving the version to release. +- `--post-release` (short `-p`) gives the development version for the next cycle: the next minor version, with a `.devN` segment where `N` is the patch number just released (e.g. `3.10.0.dev2` after `3.9.2`). + +To print the version without writing any changes, add `--dry-run`: ```shell -python scripts/update_version.py -g +python scripts/update_version.py --dry-run ``` -This simply returns the contents of `version.txt` and does not write any changes to the repository's files. +With no other flag this simply returns the contents of `version.txt`. diff --git a/scripts/update_version.py b/scripts/update_version.py index 30f36eef58..a8c9d33c13 100644 --- a/scripts/update_version.py +++ b/scripts/update_version.py @@ -1,5 +1,6 @@ import argparse import re +import sys import textwrap from datetime import datetime from pathlib import Path @@ -9,12 +10,13 @@ from packaging.version import Version _epilog = """\ -Update version information stored in version.txt in the project root, -as well as several other files in the repository. If --version is not -provided, the version number will not be changed. A file lock is held -to synchronize file access. The version tag must comply with standard -'..' format conventions for semantic versioning. -To show the version without changing anything, use --get (short -g). +Update version information stored in version.txt in the project root, as +well as several other files in the repository, and print the new version. +If none of --version, --release or --post-release is provided, the version +number is not changed. A file lock is held to synchronize file access. The +version tag must comply with standard '..' format +conventions for semantic versioning. To print the version without changing +anything, add --dry-run. """ _project_name = "flopy" _project_root_path = Path(__file__).parent.parent @@ -39,10 +41,27 @@ def split_nonnumeric(s): _current_version = Version(_version_txt_path.read_text().strip()) +def release_version() -> Version: + """Current version with any development segment (e.g. '.dev0') removed.""" + return Version(_current_version.base_version) + + +def post_release_version() -> Version: + """Development version for the next cycle, following a release. + + Targets the next anticipated minor version, with the development segment + set to the micro (patch) number of the version just released: e.g. after + 3.9.2 comes 3.10.0.dev2, and after 3.11.0 comes 3.12.0.dev0. The counter + marks how many releases into the series development has resumed. + """ + v = Version(_current_version.base_version) + return Version(f"{v.major}.{v.minor + 1}.0.dev{v.micro}") + + def update_version_txt(version: Version): with open(_version_txt_path, "w") as f: f.write(str(version)) - print(f"Updated {_version_txt_path} to version {version}") + print(f"Updated {_version_txt_path} to version {version}", file=sys.stderr) def update_version_py(timestamp: datetime, version: Version): @@ -53,7 +72,7 @@ def update_version_py(timestamp: datetime, version: Version): ) f.write(f'__version__ = "{version}"\n') f.close() - print(f"Updated {_version_py_path} to version {version}") + print(f"Updated {_version_py_path} to version {version}", file=sys.stderr) def get_software_citation(timestamp: datetime, version: Version): @@ -131,7 +150,7 @@ def update_readme_markdown(timestamp: datetime, version: Version): f.write(f"{line}\n") - print(f"Updated {fpth} to version {version}") + print(f"Updated {fpth} to version {version}", file=sys.stderr) def update_citation_cff(timestamp: datetime, version: Version): @@ -149,7 +168,7 @@ def update_citation_cff(timestamp: datetime, version: Version): citation, f, allow_unicode=True, default_flow_style=False, sort_keys=False ) - print(f"Updated {fpth} to version {version}") + print(f"Updated {fpth} to version {version}", file=sys.stderr) def update_pypi_release(timestamp: datetime, version: Version): @@ -166,7 +185,7 @@ def update_pypi_release(timestamp: datetime, version: Version): f.write(f"{line}\n") f.close() - print(f"Updated {fpth} to version {version}") + print(f"Updated {fpth} to version {version}", file=sys.stderr) def update_version( @@ -180,7 +199,7 @@ def update_version( version = ( version if version - else Version(previous.major, previous.minor, previous.micro) + else Version(f"{previous.major}.{previous.minor}.{previous.micro}") ) with lock: @@ -209,18 +228,42 @@ def update_version( help="Specify the release version", ) parser.add_argument( - "-g", - "--get", + "-r", + "--release", required=False, action="store_true", - help="Just get the current version number, no updates (defaults false)", + help=( + "Use the current development version with its development segment " + "(e.g. '.dev0') removed" + ), + ) + parser.add_argument( + "-p", + "--post-release", + required=False, + action="store_true", + help=( + "Use the development version for the next cycle: the next minor " + "version, with a development segment set to the released patch number" + ), + ) + parser.add_argument( + "--dry-run", + required=False, + action="store_true", + help="Print the version that would be written, and exit without writing", ) args = parser.parse_args() - if args.get: - print(_current_version) + if args.post_release: + version = post_release_version() + elif args.release: + version = release_version() + elif args.version: + version = Version(args.version) else: - update_version( - timestamp=datetime.now(), - version=(Version(args.version) if args.version else _current_version), - ) + version = _current_version + + if not args.dry_run: + update_version(timestamp=datetime.now(), version=version) + print(version)