[8.0.x] Improve GitHub release workflow

This commit is contained in:
Bruno Oliveira 2024-01-03 20:14:48 -03:00 committed by pytest bot
parent 838151638e
commit 72eb1b7ad1
9 changed files with 72 additions and 74 deletions

View File

@ -82,9 +82,14 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
pip install --upgrade tox pip install --upgrade tox
- name: Publish GitHub release notes - name: Generate release notes
env:
GH_RELEASE_NOTES_TOKEN: ${{ github.token }}
run: | run: |
sudo apt-get install pandoc sudo apt-get install pandoc
tox -e publish-gh-release-notes tox -e generate-gh-release-notes -- ${{ github.event.inputs.version }} scripts/latest-release-notes.md
- name: Publish GitHub Release
uses: softprops/action-gh-release@v1
with:
body_path: scripts/latest-release-notes.md
files: dist/*
tag_name: ${{ github.event.inputs.version }}

View File

@ -59,7 +59,7 @@ repos:
rev: v1.8.0 rev: v1.8.0
hooks: hooks:
- id: mypy - id: mypy
files: ^(src/|testing/) files: ^(src/|testing/|scripts/)
args: [] args: []
additional_dependencies: additional_dependencies:
- iniconfig>=1.1.0 - iniconfig>=1.1.0
@ -67,6 +67,7 @@ repos:
- packaging - packaging
- tomli - tomli
- types-pkg_resources - types-pkg_resources
- types-tabulate
# for mypy running on python>=3.11 since exceptiongroup is only a dependency # for mypy running on python>=3.11 since exceptiongroup is only a dependency
# on <3.11 # on <3.11
- exceptiongroup>=1.0.0rc8 - exceptiongroup>=1.0.0rc8

1
scripts/.gitignore vendored Normal file
View File

@ -0,0 +1 @@
latest-release-notes.md

View File

@ -1,3 +1,4 @@
# mypy: disallow-untyped-defs
""" """
Script used to publish GitHub release notes extracted from CHANGELOG.rst. Script used to publish GitHub release notes extracted from CHANGELOG.rst.
@ -19,27 +20,19 @@ The script also requires ``pandoc`` to be previously installed in the system.
Requires Python3.6+. Requires Python3.6+.
""" """
import os
import re import re
import sys import sys
from pathlib import Path from pathlib import Path
from typing import Sequence
import github3
import pypandoc import pypandoc
def publish_github_release(slug, token, tag_name, body): def parse_changelog(tag_name: str) -> str:
github = github3.login(token=token)
owner, repo = slug.split("/")
repo = github.repository(owner, repo)
return repo.create_release(tag_name=tag_name, body=body)
def parse_changelog(tag_name):
p = Path(__file__).parent.parent / "doc/en/changelog.rst" p = Path(__file__).parent.parent / "doc/en/changelog.rst"
changelog_lines = p.read_text(encoding="UTF-8").splitlines() changelog_lines = p.read_text(encoding="UTF-8").splitlines()
title_regex = re.compile(r"pytest (\d\.\d+\.\d+) \(\d{4}-\d{2}-\d{2}\)") title_regex = re.compile(r"pytest (\d\.\d+\.\d+\w*) \(\d{4}-\d{2}-\d{2}\)")
consuming_version = False consuming_version = False
version_lines = [] version_lines = []
for line in changelog_lines: for line in changelog_lines:
@ -57,43 +50,26 @@ def parse_changelog(tag_name):
return "\n".join(version_lines) return "\n".join(version_lines)
def convert_rst_to_md(text): def convert_rst_to_md(text: str) -> str:
return pypandoc.convert_text( result = pypandoc.convert_text(
text, "md", format="rst", extra_args=["--wrap=preserve"] text, "md", format="rst", extra_args=["--wrap=preserve"]
) )
assert isinstance(result, str), repr(result)
return result
def main(argv): def main(argv: Sequence[str]) -> int:
if len(argv) > 1: if len(argv) != 3:
tag_name = argv[1] print("Usage: generate-gh-release-notes VERSION FILE")
else: return 2
tag_name = os.environ.get("GITHUB_REF")
if not tag_name:
print("tag_name not given and $GITHUB_REF not set", file=sys.stderr)
return 1
if tag_name.startswith("refs/tags/"):
tag_name = tag_name[len("refs/tags/") :]
token = os.environ.get("GH_RELEASE_NOTES_TOKEN") version, filename = argv[1:3]
if not token: print(f"Generating GitHub release notes for version {version}")
print("GH_RELEASE_NOTES_TOKEN not set", file=sys.stderr) rst_body = parse_changelog(version)
return 1
slug = os.environ.get("GITHUB_REPOSITORY")
if not slug:
print("GITHUB_REPOSITORY not set", file=sys.stderr)
return 1
rst_body = parse_changelog(tag_name)
md_body = convert_rst_to_md(rst_body) md_body = convert_rst_to_md(rst_body)
if not publish_github_release(slug, token, tag_name, md_body): Path(filename).write_text(md_body, encoding="UTF-8")
print("Could not publish release notes:", file=sys.stderr)
print(md_body, file=sys.stderr)
return 5
print() print()
print(f"Release notes for {tag_name} published successfully:") print(f"Done: {filename}")
print(f"https://github.com/{slug}/releases/tag/{tag_name}")
print() print()
return 0 return 0

View File

@ -1,3 +1,4 @@
# mypy: disallow-untyped-defs
""" """
This script is part of the pytest release process which is triggered manually in the Actions This script is part of the pytest release process which is triggered manually in the Actions
tab of the repository. tab of the repository.

View File

@ -1,3 +1,4 @@
# mypy: disallow-untyped-defs
"""Invoke development tasks.""" """Invoke development tasks."""
import argparse import argparse
import os import os
@ -10,15 +11,15 @@ from colorama import Fore
from colorama import init from colorama import init
def announce(version, template_name, doc_version): def announce(version: str, template_name: str, doc_version: str) -> None:
"""Generates a new release announcement entry in the docs.""" """Generates a new release announcement entry in the docs."""
# Get our list of authors # Get our list of authors
stdout = check_output(["git", "describe", "--abbrev=0", "--tags"]) stdout = check_output(["git", "describe", "--abbrev=0", "--tags"], encoding="UTF-8")
stdout = stdout.decode("utf-8")
last_version = stdout.strip() last_version = stdout.strip()
stdout = check_output(["git", "log", f"{last_version}..HEAD", "--format=%aN"]) stdout = check_output(
stdout = stdout.decode("utf-8") ["git", "log", f"{last_version}..HEAD", "--format=%aN"], encoding="UTF-8"
)
contributors = { contributors = {
name name
@ -61,7 +62,7 @@ def announce(version, template_name, doc_version):
check_call(["git", "add", str(target)]) check_call(["git", "add", str(target)])
def regen(version): def regen(version: str) -> None:
"""Call regendoc tool to update examples and pytest output in the docs.""" """Call regendoc tool to update examples and pytest output in the docs."""
print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs") print(f"{Fore.CYAN}[generate.regen] {Fore.RESET}Updating docs")
check_call( check_call(
@ -70,7 +71,7 @@ def regen(version):
) )
def fix_formatting(): def fix_formatting() -> None:
"""Runs pre-commit in all files to ensure they are formatted correctly""" """Runs pre-commit in all files to ensure they are formatted correctly"""
print( print(
f"{Fore.CYAN}[generate.fix linting] {Fore.RESET}Fixing formatting using pre-commit" f"{Fore.CYAN}[generate.fix linting] {Fore.RESET}Fixing formatting using pre-commit"
@ -78,13 +79,15 @@ def fix_formatting():
call(["pre-commit", "run", "--all-files"]) call(["pre-commit", "run", "--all-files"])
def check_links(): def check_links() -> None:
"""Runs sphinx-build to check links""" """Runs sphinx-build to check links"""
print(f"{Fore.CYAN}[generate.check_links] {Fore.RESET}Checking links") print(f"{Fore.CYAN}[generate.check_links] {Fore.RESET}Checking links")
check_call(["tox", "-e", "docs-checklinks"]) check_call(["tox", "-e", "docs-checklinks"])
def pre_release(version, template_name, doc_version, *, skip_check_links): def pre_release(
version: str, template_name: str, doc_version: str, *, skip_check_links: bool
) -> None:
"""Generates new docs, release announcements and creates a local tag.""" """Generates new docs, release announcements and creates a local tag."""
announce(version, template_name, doc_version) announce(version, template_name, doc_version)
regen(version) regen(version)
@ -102,12 +105,12 @@ def pre_release(version, template_name, doc_version, *, skip_check_links):
print("Please push your branch and open a PR.") print("Please push your branch and open a PR.")
def changelog(version, write_out=False): def changelog(version: str, write_out: bool = False) -> None:
addopts = [] if write_out else ["--draft"] addopts = [] if write_out else ["--draft"]
check_call(["towncrier", "--yes", "--version", version] + addopts) check_call(["towncrier", "--yes", "--version", version] + addopts)
def main(): def main() -> None:
init(autoreset=True) init(autoreset=True)
parser = argparse.ArgumentParser() parser = argparse.ArgumentParser()
parser.add_argument("version", help="Release version") parser.add_argument("version", help="Release version")

View File

@ -1,11 +1,12 @@
# mypy: disallow-untyped-defs
import sys import sys
from subprocess import call from subprocess import call
def main(): def main() -> int:
""" """
Platform agnostic wrapper script for towncrier. Platform-agnostic wrapper script for towncrier.
Fixes the issue (#7251) where windows users are unable to natively run tox -e docs to build pytest docs. Fixes the issue (#7251) where Windows users are unable to natively run tox -e docs to build pytest docs.
""" """
with open( with open(
"doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8" "doc/en/_changelog_towncrier_draft.rst", "w", encoding="utf-8"

View File

@ -1,8 +1,13 @@
# mypy: disallow-untyped-defs
import datetime import datetime
import pathlib import pathlib
import re import re
from textwrap import dedent from textwrap import dedent
from textwrap import indent from textwrap import indent
from typing import Any
from typing import Iterable
from typing import Iterator
from typing import TypedDict
import packaging.version import packaging.version
import platformdirs import platformdirs
@ -109,7 +114,17 @@ def pytest_plugin_projects_from_pypi(session: CachedSession) -> dict[str, int]:
} }
def iter_plugins(): class PluginInfo(TypedDict):
"""Relevant information about a plugin to generate the summary."""
name: str
summary: str
last_release: str
status: str
requires: str
def iter_plugins() -> Iterator[PluginInfo]:
session = get_session() session = get_session()
name_2_serial = pytest_plugin_projects_from_pypi(session) name_2_serial = pytest_plugin_projects_from_pypi(session)
@ -136,7 +151,7 @@ def iter_plugins():
requires = requirement requires = requirement
break break
def version_sort_key(version_string): def version_sort_key(version_string: str) -> Any:
""" """
Return the sort key for the given version string Return the sort key for the given version string
returned by the API. returned by the API.
@ -162,20 +177,20 @@ def iter_plugins():
yield { yield {
"name": name, "name": name,
"summary": summary.strip(), "summary": summary.strip(),
"last release": last_release, "last_release": last_release,
"status": status, "status": status,
"requires": requires, "requires": requires,
} }
def plugin_definitions(plugins): def plugin_definitions(plugins: Iterable[PluginInfo]) -> Iterator[str]:
"""Return RST for the plugin list that fits better on a vertical page.""" """Return RST for the plugin list that fits better on a vertical page."""
for plugin in plugins: for plugin in plugins:
yield dedent( yield dedent(
f""" f"""
{plugin['name']} {plugin['name']}
*last release*: {plugin["last release"]}, *last release*: {plugin["last_release"]},
*status*: {plugin["status"]}, *status*: {plugin["status"]},
*requires*: {plugin["requires"]} *requires*: {plugin["requires"]}
@ -184,7 +199,7 @@ def plugin_definitions(plugins):
) )
def main(): def main() -> None:
plugins = [*iter_plugins()] plugins = [*iter_plugins()]
reference_dir = pathlib.Path("doc", "en", "reference") reference_dir = pathlib.Path("doc", "en", "reference")

11
tox.ini
View File

@ -177,18 +177,13 @@ passenv = {[testenv:release]passenv}
deps = {[testenv:release]deps} deps = {[testenv:release]deps}
commands = python scripts/prepare-release-pr.py {posargs} commands = python scripts/prepare-release-pr.py {posargs}
[testenv:publish-gh-release-notes] [testenv:generate-gh-release-notes]
description = create GitHub release after deployment description = generate release notes that can be published as GitHub Release
basepython = python3 basepython = python3
usedevelop = True usedevelop = True
passenv =
GH_RELEASE_NOTES_TOKEN
GITHUB_REF
GITHUB_REPOSITORY
deps = deps =
github3.py
pypandoc pypandoc
commands = python scripts/publish-gh-release-notes.py {posargs} commands = python scripts/generate-gh-release-notes.py {posargs}
[flake8] [flake8]
max-line-length = 120 max-line-length = 120