Automate release by comment notifications (#6823)
* Automate release by comment notifications * Only run if @pytestbot is mentioned
This commit is contained in:
parent
92767fec51
commit
15e1dd0f87
|
@ -0,0 +1,28 @@
|
|||
# part of our release process, see `release-on-comment.py`
|
||||
name: release on comment
|
||||
|
||||
on:
|
||||
issues:
|
||||
types: [opened, edited]
|
||||
issue_comment:
|
||||
types: [created, edited]
|
||||
|
||||
jobs:
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
if: (github.event.comment && contains(github.event.comment.body, '@pytestbot please')) || (github.event.issue && contains(github.event.issue.body, '@pytestbot please'))
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v1
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v1
|
||||
with:
|
||||
python-version: "3.8"
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
python -m pip install --upgrade pip
|
||||
pip install --upgrade setuptools tox
|
||||
- name: Prepare release
|
||||
run: |
|
||||
tox -e release-on-comment -- $GITHUB_EVENT_PATH ${{ secrets.chatops }}
|
|
@ -5,6 +5,24 @@ Our current policy for releasing is to aim for a bug-fix release every few weeks
|
|||
is to get fixes and new features out instead of trying to cram a ton of features into a release and by consequence
|
||||
taking a lot of time to make a new one.
|
||||
|
||||
Preparing: Automatic Method
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
We have developed an automated workflow for releases, that uses GitHub workflows and is triggered
|
||||
by opening an issue or issuing a comment one.
|
||||
|
||||
The comment must be in the form::
|
||||
|
||||
@pytestbot please prepare release from BRANCH
|
||||
|
||||
Where ``BRANCH`` is ``master`` or one of the maintenance branches.
|
||||
|
||||
After that, the workflow should publish a PR and notify that it has done so as a comment
|
||||
in the original issue.
|
||||
|
||||
Preparing: Manual Method
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. important::
|
||||
|
||||
pytest releases must be prepared on **Linux** because the docs and examples expect
|
||||
|
@ -27,6 +45,12 @@ To release a version ``MAJOR.MINOR.PATCH``, follow these steps:
|
|||
|
||||
#. Open a PR for the ``release-MAJOR.MINOR.PATCH`` branch targeting ``MAJOR.MINOR.x``.
|
||||
|
||||
|
||||
Releasing
|
||||
~~~~~~~~~
|
||||
|
||||
Both automatic and manual processes described above follow the same steps from this point onward.
|
||||
|
||||
#. After all tests pass and the PR has been approved, tag the release commit
|
||||
in the ``MAJOR.MINOR.x`` branch and push it. This will publish to PyPI::
|
||||
|
||||
|
|
|
@ -0,0 +1,213 @@
|
|||
"""
|
||||
This script is part of the pytest release process which is triggered by comments
|
||||
in issues.
|
||||
|
||||
This script is started by the `prepare_release.yml` workflow, which is triggered by two comment
|
||||
related events:
|
||||
|
||||
* https://help.github.com/en/actions/reference/events-that-trigger-workflows#issue-comment-event-issue_comment
|
||||
* https://help.github.com/en/actions/reference/events-that-trigger-workflows#issues-event-issues
|
||||
|
||||
This script receives the payload and a secrets on the command line.
|
||||
|
||||
The payload must contain a comment with a phrase matching this regular expression:
|
||||
|
||||
@pytestbot please prepare release from <branch name>
|
||||
|
||||
Then the appropriate version will be obtained based on the given branch name:
|
||||
|
||||
* a feature or bug fix release from master (based if there are features in the current changelog
|
||||
folder)
|
||||
* a bug fix from a maintenance branch
|
||||
|
||||
After that, it will create a release using the `release` tox environment, and push a new PR.
|
||||
|
||||
**Secret**: currently the secret is defined in the @pytestbot account, which the core maintainers
|
||||
have access to. There we created a new secret named `chatops` with write access to the repository.
|
||||
"""
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from subprocess import check_call
|
||||
from subprocess import check_output
|
||||
from textwrap import dedent
|
||||
from typing import Dict
|
||||
from typing import Optional
|
||||
from typing import Tuple
|
||||
|
||||
from colorama import Fore
|
||||
from colorama import init
|
||||
from github3.repos import Repository
|
||||
|
||||
|
||||
class InvalidFeatureRelease(Exception):
|
||||
pass
|
||||
|
||||
|
||||
SLUG = "pytest-dev/pytest"
|
||||
|
||||
PR_BODY = """\
|
||||
Created automatically from {comment_url}.
|
||||
|
||||
Once all builds pass and it has been **approved** by one or more maintainers, the build
|
||||
can be released by pushing a tag `{version}` to this repository.
|
||||
"""
|
||||
|
||||
|
||||
def login(token: str) -> Repository:
|
||||
import github3
|
||||
|
||||
github = github3.login(token=token)
|
||||
owner, repo = SLUG.split("/")
|
||||
return github.repository(owner, repo)
|
||||
|
||||
|
||||
def get_comment_data(payload: Dict) -> str:
|
||||
if "comment" in payload:
|
||||
return payload["comment"]
|
||||
else:
|
||||
return payload["issue"]
|
||||
|
||||
|
||||
def validate_and_get_issue_comment_payload(
|
||||
issue_payload_path: Optional[Path],
|
||||
) -> Tuple[str, str]:
|
||||
payload = json.loads(issue_payload_path.read_text(encoding="UTF-8"))
|
||||
body = get_comment_data(payload)["body"]
|
||||
m = re.match(r"@pytestbot please prepare release from ([\w\-_\.]+)", body)
|
||||
if m:
|
||||
base_branch = m.group(1)
|
||||
else:
|
||||
base_branch = None
|
||||
return payload, base_branch
|
||||
|
||||
|
||||
def print_and_exit(msg) -> None:
|
||||
print(msg)
|
||||
raise SystemExit(1)
|
||||
|
||||
|
||||
def trigger_release(payload_path: Path, token: str) -> None:
|
||||
payload, base_branch = validate_and_get_issue_comment_payload(payload_path)
|
||||
if base_branch is None:
|
||||
url = get_comment_data(payload)["html_url"]
|
||||
print_and_exit(
|
||||
f"Comment {Fore.CYAN}{url}{Fore.RESET} did not match the trigger command."
|
||||
)
|
||||
print()
|
||||
print(f"Precessing release for branch {Fore.CYAN}{base_branch}")
|
||||
|
||||
repo = login(token)
|
||||
|
||||
issue_number = payload["issue"]["number"]
|
||||
issue = repo.issue(issue_number)
|
||||
|
||||
check_call(["git", "checkout", f"origin/{base_branch}"])
|
||||
print("DEBUG:", check_output(["git", "rev-parse", "HEAD"]))
|
||||
|
||||
try:
|
||||
version = find_next_version(base_branch)
|
||||
except InvalidFeatureRelease as e:
|
||||
issue.create_comment(str(e))
|
||||
print_and_exit(f"{Fore.RED}{e}")
|
||||
|
||||
try:
|
||||
print(f"Version: {Fore.CYAN}{version}")
|
||||
|
||||
release_branch = f"release-{version}"
|
||||
|
||||
check_call(["git", "config", "user.name", "pytest bot"])
|
||||
check_call(["git", "config", "user.email", "pytestbot@gmail.com"])
|
||||
|
||||
check_call(["git", "checkout", "-b", release_branch, f"origin/{base_branch}"])
|
||||
|
||||
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} created.")
|
||||
|
||||
check_call([sys.executable, "scripts/release.py", version])
|
||||
|
||||
oauth_url = f"https://{token}:x-oauth-basic@github.com/{SLUG}.git"
|
||||
check_call(["git", "push", oauth_url, f"HEAD:{release_branch}", "--force"])
|
||||
print(f"Branch {Fore.CYAN}{release_branch}{Fore.RESET} pushed.")
|
||||
|
||||
body = PR_BODY.format(
|
||||
comment_url=get_comment_data(payload)["html_url"], version=version
|
||||
)
|
||||
pr = repo.create_pull(
|
||||
f"Prepare release {version}",
|
||||
base=base_branch,
|
||||
head=release_branch,
|
||||
body=body,
|
||||
)
|
||||
print(f"Pull request {Fore.CYAN}{pr.url}{Fore.RESET} created.")
|
||||
|
||||
comment = issue.create_comment(
|
||||
f"As requested, opened a PR for release `{version}`: #{pr.number}."
|
||||
)
|
||||
print(f"Notified in original comment {Fore.CYAN}{comment.url}{Fore.RESET}.")
|
||||
|
||||
print(f"{Fore.GREEN}Success.")
|
||||
except Exception as e:
|
||||
link = f"https://github.com/{SLUG}/actions/runs/{os.environ['GITHUB_RUN_ID']}"
|
||||
issue.create_comment(
|
||||
dedent(
|
||||
f"""
|
||||
Sorry, the request to prepare release `{version}` from {base_branch} failed with:
|
||||
|
||||
```
|
||||
{e}
|
||||
```
|
||||
|
||||
See: {link}.
|
||||
"""
|
||||
)
|
||||
)
|
||||
print_and_exit(f"{Fore.RED}{e}")
|
||||
|
||||
|
||||
def find_next_version(base_branch: str) -> str:
|
||||
output = check_output(["git", "tag"], encoding="UTF-8")
|
||||
valid_versions = []
|
||||
for v in output.splitlines():
|
||||
m = re.match(r"\d.\d.\d+$", v.strip())
|
||||
if m:
|
||||
valid_versions.append(tuple(int(x) for x in v.split(".")))
|
||||
|
||||
valid_versions.sort()
|
||||
last_version = valid_versions[-1]
|
||||
|
||||
changelog = Path("changelog")
|
||||
|
||||
features = list(changelog.glob("*.feature.rst"))
|
||||
breaking = list(changelog.glob("*.breaking.rst"))
|
||||
is_feature_release = features or breaking
|
||||
|
||||
if is_feature_release and base_branch != "master":
|
||||
msg = dedent(
|
||||
f"""
|
||||
Found features or breaking changes in `{base_branch}`, and feature releases can only be
|
||||
created from `master`.":
|
||||
"""
|
||||
)
|
||||
msg += "\n".join(f"* `{x.name}`" for x in sorted(features + breaking))
|
||||
raise InvalidFeatureRelease(msg)
|
||||
|
||||
if is_feature_release:
|
||||
return f"{last_version[0]}.{last_version[1] + 1}.0"
|
||||
else:
|
||||
return f"{last_version[0]}.{last_version[1]}.{last_version[2] + 1}"
|
||||
|
||||
|
||||
def main() -> None:
|
||||
init(autoreset=True)
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("payload")
|
||||
parser.add_argument("token")
|
||||
options = parser.parse_args()
|
||||
trigger_release(Path(options.payload), options.token)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
9
tox.ini
9
tox.ini
|
@ -129,12 +129,19 @@ usedevelop = True
|
|||
passenv = *
|
||||
deps =
|
||||
colorama
|
||||
gitpython
|
||||
github3.py
|
||||
pre-commit>=1.11.0
|
||||
wheel
|
||||
towncrier
|
||||
commands = python scripts/release.py {posargs}
|
||||
|
||||
[testenv:release-on-comment]
|
||||
decription = do a release from a comment on GitHub
|
||||
usedevelop = {[testenv:release]usedevelop}
|
||||
passenv = {[testenv:release]passenv}
|
||||
deps = {[testenv:release]deps}
|
||||
commands = python scripts/release-on-comment.py {posargs}
|
||||
|
||||
[testenv:publish-gh-release-notes]
|
||||
description = create GitHub release after deployment
|
||||
basepython = python3
|
||||
|
|
Loading…
Reference in New Issue