diff options
author | Florian Bruhin <me@the-compiler.org> | 2023-08-17 20:05:24 +0200 |
---|---|---|
committer | GitHub <noreply@github.com> | 2023-08-17 20:05:24 +0200 |
commit | d5a8f22a2af9bee2976092f95a32e0c1b749665b (patch) | |
tree | 0690988f6da084f7615b20cd2fd825dfc0afd9dd | |
parent | a4d22e9bd9526bc9a8bd7f3abf7dabb214dded90 (diff) | |
parent | 8795b88d358deec34e3c3829f01eacded2f53a6a (diff) | |
download | qutebrowser-d5a8f22a2af9bee2976092f95a32e0c1b749665b.tar.gz qutebrowser-d5a8f22a2af9bee2976092f95a32e0c1b749665b.zip |
Merge pull request #7832 from qutebrowser/auto-releases
Releases on CI
-rw-r--r-- | .github/workflows/release.yml | 222 | ||||
-rw-r--r-- | doc/changelog.asciidoc | 7 | ||||
-rw-r--r-- | doc/contributing.asciidoc | 18 | ||||
-rw-r--r-- | misc/Makefile | 3 | ||||
-rwxr-xr-x | scripts/dev/build_release.py | 80 | ||||
-rw-r--r-- | scripts/dev/download_release.sh | 34 | ||||
-rw-r--r-- | scripts/dev/update_version.py | 72 | ||||
-rw-r--r-- | tox.ini | 8 |
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)) @@ -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 = * |