summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorFlorian Bruhin <me@the-compiler.org>2023-08-17 20:05:24 +0200
committerGitHub <noreply@github.com>2023-08-17 20:05:24 +0200
commitd5a8f22a2af9bee2976092f95a32e0c1b749665b (patch)
tree0690988f6da084f7615b20cd2fd825dfc0afd9dd
parenta4d22e9bd9526bc9a8bd7f3abf7dabb214dded90 (diff)
parent8795b88d358deec34e3c3829f01eacded2f53a6a (diff)
downloadqutebrowser-d5a8f22a2af9bee2976092f95a32e0c1b749665b.tar.gz
qutebrowser-d5a8f22a2af9bee2976092f95a32e0c1b749665b.zip
Merge pull request #7832 from qutebrowser/auto-releases
Releases on CI
-rw-r--r--.github/workflows/release.yml222
-rw-r--r--doc/changelog.asciidoc7
-rw-r--r--doc/contributing.asciidoc18
-rw-r--r--misc/Makefile3
-rwxr-xr-xscripts/dev/build_release.py80
-rw-r--r--scripts/dev/download_release.sh34
-rw-r--r--scripts/dev/update_version.py72
-rw-r--r--tox.ini8
8 files changed, 367 insertions, 77 deletions
diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml
new file mode 100644
index 000000000..65dec84e8
--- /dev/null
+++ b/.github/workflows/release.yml
@@ -0,0 +1,222 @@
+name: Release
+
+on:
+ workflow_dispatch:
+ inputs:
+ release_type:
+ description: 'Release type'
+ required: true
+ default: 'patch'
+ type: choice
+ options:
+ - 'patch'
+ - 'minor'
+ - 'major'
+ # FIXME do we want a possibility to do prereleases here?
+ python_version:
+ description: 'Python version'
+ required: true
+ default: '3.11'
+ type: choice
+ options:
+ - '3.8'
+ - '3.9'
+ - '3.10'
+ - '3.11'
+jobs:
+ prepare:
+ runs-on: ubuntu-20.04
+ timeout-minutes: 5
+ outputs:
+ version: ${{ steps.bump.outputs.version }}
+ release_id: ${{ steps.create-release.outputs.id }}
+ permissions:
+ contents: write # To push release commit/tag
+ steps:
+ - name: Find release branch
+ uses: actions/github-script@v6
+ id: find-branch
+ with:
+ script: |
+ if (context.payload.inputs.release_type != 'patch') {
+ return 'main';
+ }
+ const branches = await github.paginate(github.rest.repos.listBranches, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ });
+ const branch_names = branches.map(branch => branch.name);
+ console.log(`branches: ${branch_names}`);
+ const release_branches = branch_names.filter(branch => branch.match(/^v\d+\.\d+\.x$/));
+ if (release_branches.length === 0) {
+ core.setFailed('No release branch found!');
+ return '';
+ }
+ console.log(`release_branches: ${release_branches}`);
+ // Get newest release branch (biggest version number)
+ const sorted = release_branches.sort((a, b) => a.localeCompare(b, undefined, { numeric: true, sensitivity: 'base' }));
+ console.log(`sorted: ${sorted}`);
+ return sorted.at(-1);
+ result-encoding: string
+ - uses: actions/checkout@v3
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ # Doesn't really matter what we prepare the release with, but let's
+ # use the same version for consistency.
+ python-version: ${{ github.event.inputs.python_version }}
+ - name: Install dependencies
+ run: |
+ python -m pip install -U pip
+ python -m pip install -U -r misc/requirements/requirements-tox.txt
+ - name: Configure git
+ run: |
+ git config --global user.name "qutebrowser bot"
+ git config --global user.email "bot@qutebrowser.org"
+ - name: Switch to release branch
+ uses: actions/checkout@v3
+ with:
+ ref: ${{ steps.find-branch.outputs.result }}
+ - name: Import GPG Key
+ run: |
+ gpg --import <<< "${{ secrets.QUTEBROWSER_BOT_GPGKEY }}"
+ - name: Bump version
+ id: bump
+ run: "tox -e update-version -- ${{ github.event.inputs.release_type }}"
+ - name: Check milestone
+ uses: actions/github-script@v6
+ with:
+ script: |
+ const milestones = await github.paginate(github.rest.issues.listMilestones, {
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ });
+ const names = milestones.map(milestone => milestone.title);
+ console.log(`milestones: ${names}`);
+
+ const milestone = milestones.find(milestone => milestone.title === "v${{ steps.bump.outputs.version }}");
+ if (milestone !== undefined) {
+ core.setFailed(`Found open milestone ${milestone.title} with ${milestone.open_issues} open and ${milestone.closed_issues} closed issues!`);
+ }
+ - name: Push release commit/tag
+ run: |
+ git push origin ${{ steps.find-branch.outputs.result }}
+ git push origin v${{ steps.bump.outputs.version }}
+ - name: Cherry-pick release commit
+ if: ${{ github.event.inputs.release_type == 'patch' }}
+ run: |
+ git checkout main
+ git cherry-pick -x v${{ steps.bump.outputs.version }}
+ git push origin main
+ git checkout v${{ steps.bump.outputs.version_x }}
+ - name: Create release branch
+ if: ${{ github.event.inputs.release_type != 'patch' }}
+ run: |
+ git checkout -b v${{ steps.bump.outputs.version_x }}
+ git push --set-upstream origin v${{ steps.bump.outputs.version_x }}
+ - name: Create GitHub draft release
+ id: create-release
+ uses: softprops/action-gh-release@v1
+ with:
+ tag_name: v${{ steps.bump.outputs.version }}
+ draft: true
+ body: "*Release artifacts for this release are currently being uploaded...*"
+ release:
+ strategy:
+ matrix:
+ include:
+ - os: macos-11
+ - os: windows-2019
+ - os: ubuntu-20.04
+ runs-on: "${{ matrix.os }}"
+ timeout-minutes: 45
+ needs: [prepare]
+ permissions:
+ contents: write # To upload release artifacts
+ steps:
+ - uses: actions/checkout@v3
+ with:
+ ref: v${{ needs.prepare.outputs.version }}
+ - name: Set up Python
+ uses: actions/setup-python@v4
+ with:
+ python-version: ${{ github.event.inputs.python_version }}
+ - name: Import GPG Key
+ if: ${{ startsWith(matrix.os, 'ubuntu-') }}
+ run: |
+ gpg --import <<< "${{ secrets.QUTEBROWSER_BOT_GPGKEY }}"
+ # Needed because of the following import chain:
+ # - scripts/dev/build_release.py
+ # - scripts/dev/update_3rdparty.py
+ # - scripts/dictcli.py
+ # - qutebrowser/browser/webengine/spell.py
+ # - utils.message -> utils.usertypes -> utils.qtutils -> qt.gui
+ # - PyQt6.QtGui
+ # Some additional packages are needed for a2x to build manpage
+ - name: Install apt dependencies
+ if: ${{ startsWith(matrix.os, 'ubuntu-') }}
+ run: |
+ sudo apt-get update
+ sudo apt-get install --no-install-recommends libegl1-mesa libxml2-utils docbook-xml xsltproc docbook-xsl
+ - name: Install dependencies
+ run: |
+ python -m pip install -U pip
+ python -m pip install -U -r misc/requirements/requirements-tox.txt
+ # FIXME consider switching to trusted publishers:
+ # https://blog.pypi.org/posts/2023-04-20-introducing-trusted-publishers/
+ - name: Build and upload release
+ run: "tox -e build-release -- --upload --no-confirm"
+ env:
+ TWINE_USERNAME: __token__
+ TWINE_PASSWORD: ${{ secrets.QUTEBROWSER_BOT_PYPI_TOKEN }}
+ GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
+ finalize:
+ runs-on: ubuntu-20.04
+ timeout-minutes: 5
+ needs: [prepare, release]
+ permissions:
+ contents: write # To change release
+ steps:
+ - name: Publish final release
+ uses: actions/github-script@v6
+ with:
+ script: |
+ await github.rest.repos.updateRelease({
+ owner: context.repo.owner,
+ repo: context.repo.repo,
+ release_id: "${{ needs.prepare.outputs.release_id }}",
+ draft: false,
+ body: "Check the [changelog](https://github.com/qutebrowser/qutebrowser/blob/master/doc/changelog.asciidoc) for changes in this release.",
+ })
+ irc:
+ timeout-minutes: 2
+ continue-on-error: true
+ runs-on: ubuntu-20.04
+ needs: [prepare, release, finalize]
+ if: "${{ always() }}"
+ steps:
+ - name: Send success IRC notification
+ uses: Gottox/irc-message-action@v2
+ if: "${{ needs.finalize.result == 'success' }}"
+ with:
+ server: irc.libera.chat
+ channel: '#qutebrowser-bots'
+ nickname: qutebrowser-bot
+ message: "[${{ github.workflow }}] \u00033Success:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})"
+ - name: Send main channel IRC notification
+ uses: Gottox/irc-message-action@v2
+ if: "${{ needs.finalize.result == 'success' && github.repository == 'qutebrowser/qutebrowser' }}"
+ with:
+ server: irc.libera.chat
+ channel: '#qutebrowser'
+ nickname: qutebrowser-bot
+ message: "qutebrowser v${{ needs.prepare.outputs.version }} has been released! https://github.com/${{ github.repository }}/releases/tag/v${{ needs.prepare.outputs.version }}"
+ - name: Send non-success IRC notification
+ uses: Gottox/irc-message-action@v2
+ if: "${{ needs.finalize.result != 'success' }}"
+ with:
+ server: irc.libera.chat
+ channel: '#qutebrowser-bots'
+ nickname: qutebrowser-bot
+ message: "[${{ github.workflow }}] \u00034FAIL:\u0003 ${{ github.ref }} https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }} (@${{ github.actor }})\n
+ prepare: ${{ needs.prepare.result }}, release: ${{ needs.release.result}}, finalize: ${{ needs.finalize.result }}"
diff --git a/doc/changelog.asciidoc b/doc/changelog.asciidoc
index 30fbfb8fd..53931f8b7 100644
--- a/doc/changelog.asciidoc
+++ b/doc/changelog.asciidoc
@@ -38,6 +38,13 @@ Major changes
* `run-with-count` -> `cmd-run-with-count`
The old names continue to work for the time being, but are deprecated and
show a warning.
+- Releases are now automated on CI, and GPG signed by
+ `qutebrowser bot <bot@qutebrowser.org>`, fingerprint
+ `27F3 BB4F C217 EECB 8585 78AE EF7E E4D0 3969 0B7B`.
+ The key is available as follows:
+ * On https://qutebrowser.org/pubkey.gpg
+ * Via keys.openpgp.org
+ * Via WKD for bot@qutebrowser.org
Added
~~~~~
diff --git a/doc/contributing.asciidoc b/doc/contributing.asciidoc
index aa0c7516a..0be2655c5 100644
--- a/doc/contributing.asciidoc
+++ b/doc/contributing.asciidoc
@@ -758,18 +758,30 @@ qutebrowser release
* Make sure there are no unstaged changes and the tests are green.
* Make sure all issues with the related milestone are closed.
+* Mark the https://github.com/qutebrowser/qutebrowser/milestones[milestone] as closed.
* Consider updating the completions for `content.headers.user_agent` in `configdata.yml`.
* Minor release: Consider updating some files from main:
- `misc/requirements/` and `requirements.txt`
- `scripts/`
-* Make sure Python is up-to-date on build machines.
-* Mark the milestone at https://github.com/qutebrowser/qutebrowser/milestones as closed.
-* Update changelog in main branch
+* Update changelog in main branch and ensure the correct version number has `(unreleased)`
* If necessary: Update changelog in release branch from main.
+
+**Automatic release via GitHub Actions (starting with v3.0.0):**
+
+* Double check Python version in `.github/workflows/release.yml`
+* Run the `release` workflow on the `main` branch, e.g. via `gh workflow run release -f release_type=major` (`release_type` can be `major`, `minor` or `patch`; you can also override `python_version`)
+
+**Manual release:**
+
+* Make sure Python is up-to-date on build machines.
* Run `./.venv/bin/python3 scripts/dev/update_version.py {major,minor,patch}`.
* Run the printed instructions accordingly.
+
+**Post release:**
+
* Update `qutebrowser-git` PKGBUILD if dependencies/install changed.
* Add unreleased future versions to changelog
* Update IRC topic
* Announce to qutebrowser and qutebrowser-announce mailinglist.
* Post announcement mail to subreddit
+* Post on the website formerly known as Twitter
diff --git a/misc/Makefile b/misc/Makefile
index 62294ba61..39a7e005f 100644
--- a/misc/Makefile
+++ b/misc/Makefile
@@ -4,6 +4,7 @@ ICONSIZES = 16 24 32 48 64 128 256 512
DATAROOTDIR = $(PREFIX)/share
DATADIR ?= $(DATAROOTDIR)
MANDIR ?= $(DATAROOTDIR)/man
+A2X ?= a2x
ifdef DESTDIR
SETUPTOOLSOPTS = --root="$(DESTDIR)"
@@ -14,7 +15,7 @@ all: man
man: doc/qutebrowser.1
doc/qutebrowser.1: doc/qutebrowser.1.asciidoc
- a2x -f manpage $<
+ $(A2X) -f manpage $<
install: man
$(PYTHON) setup.py install --prefix="$(PREFIX)" --optimize=1 $(SETUPTOOLSOPTS)
diff --git a/scripts/dev/build_release.py b/scripts/dev/build_release.py
index 55b3f5f1c..65eef720c 100755
--- a/scripts/dev/build_release.py
+++ b/scripts/dev/build_release.py
@@ -400,13 +400,11 @@ def build_windows(
utils.print_title("Updating VersionInfo file")
gen_versioninfo.main()
- artifacts = [
- _build_windows_single(
- skip_packaging=skip_packaging,
- debug=debug,
- qt5=qt5,
- ),
- ]
+ artifacts = _build_windows_single(
+ skip_packaging=skip_packaging,
+ debug=debug,
+ qt5=qt5,
+ )
return artifacts
@@ -434,6 +432,8 @@ def _package_windows_single(
name_parts.append('debug')
if qt5:
name_parts.append('qt5')
+
+ name_parts.append('amd64') # FIXME:qt6 temporary until new installer
name = '-'.join(name_parts) + '.exe'
artifacts.append(Artifact(
@@ -518,9 +518,17 @@ def build_sdist() -> List[Artifact]:
def test_makefile() -> None:
"""Make sure the Makefile works correctly."""
utils.print_title("Testing makefile")
+ a2x_path = pathlib.Path(sys.executable).parent / 'a2x'
+ assert a2x_path.exists(), a2x_path
with tempfile.TemporaryDirectory() as tmpdir:
- subprocess.run(['make', '-f', 'misc/Makefile',
- f'DESTDIR={tmpdir}', 'install'], check=True)
+ subprocess.run(
+ [
+ 'make', '-f', 'misc/Makefile',
+ f'DESTDIR={tmpdir}', f'A2X={a2x_path}',
+ 'install'
+ ],
+ check=True,
+ )
def read_github_token(
@@ -531,6 +539,9 @@ def read_github_token(
if arg_token is not None:
return arg_token
+ if "GITHUB_TOKEN" in os.environ:
+ return os.environ["GITHUB_TOKEN"]
+
token_path = pathlib.Path.home() / '.gh_token'
if not token_path.exists():
if optional:
@@ -544,13 +555,19 @@ def read_github_token(
return token
-def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
+def github_upload(
+ artifacts: List[Artifact],
+ tag: str,
+ gh_token: str,
+ experimental: bool,
+) -> None:
"""Upload the given artifacts to GitHub.
Args:
artifacts: A list of Artifacts to upload.
tag: The name of the release tag
gh_token: The GitHub token to use
+ experimental: Upload to the experiments repo
"""
# pylint: disable=broad-exception-raised
import github3
@@ -558,14 +575,20 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
utils.print_title("Uploading to github...")
gh = github3.login(token=gh_token)
- repo = gh.repository('qutebrowser', 'qutebrowser')
+
+ if experimental:
+ repo = gh.repository('qutebrowser', 'experiments')
+ else:
+ repo = gh.repository('qutebrowser', 'qutebrowser')
release = None # to satisfy pylint
for release in repo.releases():
if release.tag_name == tag:
break
else:
- raise Exception(f"No release found for {tag!r}!")
+ releases = ", ".join(r.tag_name for r in repo.releases())
+ raise Exception(
+ f"No release found for {tag!r} in {repo.full_name}, found: {releases}")
for artifact in artifacts:
while True:
@@ -575,6 +598,10 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
if asset.name == artifact.path.name]
if assets:
print(f"Assets already exist: {assets}")
+
+ if utils.ON_CI:
+ sys.exit(1)
+
print("Press enter to continue anyways or Ctrl-C to abort.")
input()
@@ -588,8 +615,13 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
)
except github3.exceptions.ConnectionError as e:
utils.print_error(f'Failed to upload: {e}')
- print("Press Enter to retry...", file=sys.stderr)
- input()
+ if utils.ON_CI:
+ print("Retrying in 30s...")
+ time.sleep(30)
+ else:
+ print("Press Enter to retry...", file=sys.stderr)
+ input()
+
print("Retrying!")
assets = [asset for asset in release.assets()
@@ -602,10 +634,16 @@ def github_upload(artifacts: List[Artifact], tag: str, gh_token: str) -> None:
break
-def pypi_upload(artifacts: List[Artifact]) -> None:
+def pypi_upload(artifacts: List[Artifact], experimental: bool) -> None:
"""Upload the given artifacts to PyPI using twine."""
+ # https://blog.pypi.org/posts/2023-05-23-removing-pgp/
+ artifacts = [a for a in artifacts if a.mimetype != 'application/pgp-signature']
+
utils.print_title("Uploading to PyPI...")
- run_twine('upload', artifacts)
+ if experimental:
+ run_twine('upload', artifacts, "-r", "testpypi")
+ else:
+ run_twine('upload', artifacts)
def twine_check(artifacts: List[Artifact]) -> None:
@@ -635,6 +673,8 @@ def main() -> None:
help="Build a debug build.")
parser.add_argument('--qt5', action='store_true', required=False,
help="Build against PyQt5")
+ parser.add_argument('--experimental', action='store_true', required=False,
+ help="Upload to experiments repo and test PyPI")
args = parser.parse_args()
utils.change_cwd()
@@ -647,6 +687,7 @@ def main() -> None:
gh_token = read_github_token(args.gh_token)
else:
gh_token = read_github_token(args.gh_token, optional=True)
+ assert not args.experimental # makes no sense without upload
if not misc_checks.check_git():
utils.print_error("Refusing to do a release with a dirty git tree")
@@ -680,14 +721,15 @@ def main() -> None:
if args.upload:
version_tag = f"v{qutebrowser.__version__}"
- if not args.no_confirm:
+ if not args.no_confirm and not utils.ON_CI:
utils.print_title(f"Press enter to release {version_tag}...")
input()
assert gh_token is not None
- github_upload(artifacts, version_tag, gh_token=gh_token)
+ github_upload(
+ artifacts, version_tag, gh_token=gh_token, experimental=args.experimental)
if upload_to_pypi:
- pypi_upload(artifacts)
+ pypi_upload(artifacts, experimental=args.experimental)
else:
print()
utils.print_title("Artifacts")
diff --git a/scripts/dev/download_release.sh b/scripts/dev/download_release.sh
deleted file mode 100644
index 207da21c8..000000000
--- a/scripts/dev/download_release.sh
+++ /dev/null
@@ -1,34 +0,0 @@
-#!/bin/bash
-set -e
-
-# This script downloads the given release from GitHub so we can mirror it on
-# qutebrowser.org.
-
-tmpdir=$(mktemp -d)
-oldpwd=$PWD
-
-if [[ $# != 1 ]]; then
- echo "Usage: $0 <version>" >&2
- exit 1
-fi
-
-cd "$tmpdir"
-mkdir windows
-
-base="https://github.com/qutebrowser/qutebrowser/releases/download/v$1"
-
-wget "$base/qutebrowser-$1.tar.gz"
-wget "$base/qutebrowser-$1.tar.gz.asc"
-wget "$base/qutebrowser-$1.dmg"
-wget "$base/qutebrowser_${1}-1_all.deb"
-
-cd windows
-wget "$base/qutebrowser-${1}-amd64.msi"
-wget "$base/qutebrowser-${1}-win32.msi"
-wget "$base/qutebrowser-${1}-windows-standalone-amd64.zip"
-wget "$base/qutebrowser-${1}-windows-standalone-win32.zip"
-
-dest="/srv/http/qutebrowser/releases/v$1"
-cd "$oldpwd"
-sudo mv "$tmpdir" "$dest"
-sudo chown -R http:http "$dest"
diff --git a/scripts/dev/update_version.py b/scripts/dev/update_version.py
index ec1550414..b0f48710e 100644
--- a/scripts/dev/update_version.py
+++ b/scripts/dev/update_version.py
@@ -8,6 +8,7 @@
"""Update version numbers using bump2version."""
+import re
import sys
import argparse
import os.path
@@ -19,6 +20,24 @@ sys.path.insert(0, os.path.join(os.path.dirname(__file__), os.pardir,
from scripts import utils
+class Error(Exception):
+ """Base class for exceptions in this module."""
+
+
+def verify_branch(version_leap):
+ """Check that we're on the correct git branch."""
+ proc = subprocess.run(
+ ['git', 'rev-parse', '--abbrev-ref', 'HEAD'],
+ check=True, capture_output=True, text=True)
+ branch = proc.stdout.strip()
+
+ if (
+ version_leap == 'patch' and not re.fullmatch(r'v\d+\.\d+\.x', branch) or
+ version_leap != 'patch' and branch != 'main'
+ ):
+ raise Error(f"Invalid branch for {version_leap} release: {branch}")
+
+
def bump_version(version_leap="patch"):
"""Update qutebrowser release version.
@@ -31,7 +50,11 @@ def bump_version(version_leap="patch"):
def show_commit():
- subprocess.run(['git', 'show'], check=True)
+ """Show the latest git commit."""
+ git_args = ['git', 'show']
+ if utils.ON_CI:
+ git_args.append("--color")
+ subprocess.run(git_args, check=True)
if __name__ == "__main__":
@@ -46,30 +69,39 @@ if __name__ == "__main__":
utils.change_cwd()
if not args.commands:
+ verify_branch(args.bump)
bump_version(args.bump)
show_commit()
import qutebrowser
version = qutebrowser.__version__
- x_version = '.'.join([str(p) for p in qutebrowser.__version_info__[:-1]] +
+ version_x = '.'.join([str(p) for p in qutebrowser.__version_info__[:-1]] +
['x'])
- print("Run the following commands to create a new release:")
- print("* git push origin; git push origin v{v}".format(v=version))
- if args.bump == 'patch':
- print("* git checkout main && git cherry-pick v{v} && "
- "git push origin".format(v=version))
+ if utils.ON_CI:
+ output_file = os.environ["GITHUB_OUTPUT"]
+ with open(output_file, "w", encoding="ascii") as f:
+ f.write(f"version={version}\n")
+ f.write(f"version_x={version_x}\n")
+
+ print(f"Outputs for {version} written to GitHub Actions output file")
else:
- print("* git branch v{x} v{v} && git push --set-upstream origin v{x}"
- .format(v=version, x=x_version))
- print("* Create new release via GitHub (required to upload release "
- "artifacts)")
- print("* Linux: git fetch && git checkout v{v} && "
- "tox -e build-release -- --upload"
- .format(v=version))
- print("* Windows: git fetch; git checkout v{v}; "
- "py -3.9 -m tox -e build-release -- --upload"
- .format(v=version))
- print("* macOS: git fetch && git checkout v{v} && "
- "tox -e build-release -- --upload"
- .format(v=version))
+ print("Run the following commands to create a new release:")
+ print("* git push origin; git push origin v{v}".format(v=version))
+ if args.bump == 'patch':
+ print("* git checkout main && git cherry-pick -x v{v} && "
+ "git push origin".format(v=version))
+ else:
+ print("* git branch v{x} v{v} && git push --set-upstream origin v{x}"
+ .format(v=version, x=version_x))
+ print("* Create new release via GitHub (required to upload release "
+ "artifacts)")
+ print("* Linux: git fetch && git checkout v{v} && "
+ "tox -e build-release -- --upload"
+ .format(v=version))
+ print("* Windows: git fetch; git checkout v{v}; "
+ "py -3.X -m tox -e build-release -- --upload"
+ .format(v=version))
+ print("* macOS: git fetch && git checkout v{v} && "
+ "tox -e build-release -- --upload"
+ .format(v=version))
diff --git a/tox.ini b/tox.ini
index 060dccaaa..06c96cdf1 100644
--- a/tox.ini
+++ b/tox.ini
@@ -267,6 +267,14 @@ deps =
commands =
{envpython} -m sphinx -jauto -W --color {posargs} {toxinidir}/doc/extapi/ {toxinidir}/doc/extapi/_build/
+[testenv:update-version]
+basepython = {env:PYTHON:python3}
+passenv =
+ GITHUB_OUTPUT
+ CI
+deps = -r{toxinidir}/misc/requirements/requirements-dev.txt
+commands = {envpython} scripts/dev/update_version.py {posargs}
+
[testenv:build-release{,-qt5}]
basepython = {env:PYTHON:python3}
passenv = *