Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add check for release file on PRs to master #126

Merged
merged 2 commits into from Aug 25, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 7 additions & 0 deletions .github/release-check-action/Dockerfile
@@ -0,0 +1,7 @@
FROM python:3.7-alpine

RUN pip install httpx

COPY . /action

ENTRYPOINT ["python", "/action/check.py"]
42 changes: 42 additions & 0 deletions .github/release-check-action/check.py
@@ -0,0 +1,42 @@
import json
import pathlib
import sys

from comment_templates import (
INVALID_RELEASE_FILE,
MISSING_RELEASE_FILE,
RELEASE_FILE_ADDED,
)
from config import GITHUB_EVENT_PATH, GITHUB_WORKSPACE, RELEASE_FILE_PATH
from github import add_or_edit_comment, update_labels
from release import InvalidReleaseFileError, get_release_info


with open(GITHUB_EVENT_PATH) as f:
event_data = json.load(f)


release_file = pathlib.Path(GITHUB_WORKSPACE) / RELEASE_FILE_PATH

exit_code = 0
release_info = None

if not release_file.exists():
print("release file does not exist")

exit_code = 1
comment = MISSING_RELEASE_FILE
else:
try:
release_info = get_release_info(release_file)

comment = RELEASE_FILE_ADDED.format(changelog_preview=release_info.changelog)
except InvalidReleaseFileError:
exit_code = 2
comment = INVALID_RELEASE_FILE


add_or_edit_comment(event_data, comment)
update_labels(event_data, release_info)

sys.exit(exit_code)
43 changes: 43 additions & 0 deletions .github/release-check-action/comment_templates.py
@@ -0,0 +1,43 @@
MISSING_RELEASE_FILE = """\
Hi, thanks for contributing to Strawberry 🍓!

We noticed that this PR is missing a `RELEASE.md` file. \
We use that to automatically do releases here on GitHub and, \
most importantly, to PyPI!

So as soon as this PR is merged, a release will be made 🚀.

Here's an example of `RELEASE.md`:

```markdown
Release type: patch

Description of the changes, ideally with some examples, if adding a new feature.
```

Release type can be one of patch, minor or major. We use [semver](https://semver.org/),\
so make sure to pick the appropriate type. If in doubt feel free to ask :)
"""

RELEASE_FILE_ADDED = """
Thanks for adding the `RELEASE.md` file!

![](https://media.giphy.com/media/xq1FxHkABwW7m/giphy.gif)

Here's a preview of the changelog:

```markdown
{changelog_preview}
```
"""

INVALID_RELEASE_FILE = """\
Thanks for adding the release file! Unfortunately it does seem to be \
invalid. Make sure it looks like the following example:

```markdown
Release type: patch

Description of the changes, ideally with some examples, if adding a new feature.
```
"""
8 changes: 8 additions & 0 deletions .github/release-check-action/config.py
@@ -0,0 +1,8 @@
import os


RELEASE_FILE_PATH = "RELEASE.md"
GITHUB_SHA = os.environ["GITHUB_SHA"]
GITHUB_EVENT_PATH = os.environ["GITHUB_EVENT_PATH"]
GITHUB_WORKSPACE = os.environ["GITHUB_WORKSPACE"]
GITHUB_TOKEN = os.environ.get("BOTBERRY_GITHUB_TOKEN", os.environ["GITHUB_TOKEN"])
112 changes: 112 additions & 0 deletions .github/release-check-action/github.py
@@ -0,0 +1,112 @@
import typing
from urllib.parse import urljoin

import httpx
from config import GITHUB_TOKEN
from release import ReleaseInfo


SIGNATURE = "<!-- action-check: release-file -->"


def is_release_check_comment(comment: dict) -> bool:
return (
comment["user"]["login"] in ["github-actions[bot]", "botberry"]
and SIGNATURE in comment["body"]
)


def get_comments_link(github_event_data: dict) -> str:
return github_event_data["pull_request"]["_links"]["comments"]["href"]


def get_labels_link(github_event_data: dict) -> str:
return urljoin(github_event_data["pull_request"]["issue_url"] + "/", "labels")


def get_comments(github_event_data: dict) -> typing.List[dict]:
comments_link = get_comments_link(github_event_data)

comments_request = httpx.get(comments_link)

return comments_request.json()


def add_or_edit_comment(github_event_data: dict, comment: str):
current_comments = get_comments(github_event_data)

previous_comment = next(
(comment for comment in current_comments if is_release_check_comment(comment)),
None,
)

method = httpx.patch if previous_comment else httpx.post
url = (
previous_comment["url"]
if previous_comment
else get_comments_link(github_event_data)
)

request = method(
url,
headers={"Authorization": f"token {GITHUB_TOKEN}"},
json={"body": comment + SIGNATURE},
)

if request.status_code >= 400:
print(request.text)
print(request.status_code)


def update_labels(github_event_data: dict, release_info: typing.Optional[ReleaseInfo]):
labels_to_add = {"bot:has-release-file"}
labels_to_remove: typing.Set[str] = set()

new_release_label = None

if release_info is None:
labels_to_remove = labels_to_add
labels_to_add = set()
else:
new_release_label = f"bot:release-type-{release_info.change_type.value}"
labels_to_add.add(new_release_label)

labels_url = get_labels_link(github_event_data)

current_labels_url_by_name = {
label["name"]: label["url"]
for label in github_event_data["pull_request"]["labels"]
}

current_labels = set(current_labels_url_by_name.keys())

release_labels_to_remove = [
label
for label in current_labels
if label.startswith("bot:release-type-") and label != new_release_label
]
labels_to_remove.update(release_labels_to_remove)

print("current_labels", current_labels, "labels_to_remove", labels_to_remove)

if not current_labels.issuperset(labels_to_add):
request = httpx.post(
labels_url,
headers={"Authorization": f"token {GITHUB_TOKEN}"},
json={"labels": list(labels_to_add)},
)

if request.status_code >= 400:
print(request.text)
print(request.status_code)

if current_labels.issuperset(labels_to_remove):
for label in labels_to_remove:
request = httpx.delete(
current_labels_url_by_name[label],
headers={"Authorization": f"token {GITHUB_TOKEN}"},
)

if request.status_code >= 400:
print(request.text)
print(request.status_code)
40 changes: 40 additions & 0 deletions .github/release-check-action/release.py
@@ -0,0 +1,40 @@
import re
from enum import Enum
from pathlib import Path

import dataclasses


RELEASE_TYPE_REGEX = re.compile(r"^[Rr]elease [Tt]ype: (major|minor|patch)$")


class InvalidReleaseFileError(Exception):
pass


class ChangeType(Enum):
MAJOR = "major"
MINOR = "minor"
PATCH = "patch"


@dataclasses.dataclass
class ReleaseInfo:
change_type: ChangeType
changelog: str


# TODO: remove duplication when we migrate our deployer to GitHub Actions
def get_release_info(file_path: Path) -> ReleaseInfo:
with file_path.open("r") as f:
line = f.readline()
match = RELEASE_TYPE_REGEX.match(line)

if not match:
raise InvalidReleaseFileError()

change_type_key = match.group(1)
change_type = ChangeType[change_type_key.upper()]
changelog = "".join([l for l in f.readlines()]).strip()

return ReleaseInfo(change_type, changelog)
19 changes: 19 additions & 0 deletions .github/workflows/release-check.yml
@@ -0,0 +1,19 @@
name: Release file check

on:
pull_request:
branches:
- master

jobs:
release-file-check:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v1

- name: Release file check
uses: ./.github/release-check-action
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
BOTBERRY_GITHUB_TOKEN: ${{ secrets.BOTBERRY_GITHUB_TOKEN }}
2 changes: 1 addition & 1 deletion .isort.cfg
Expand Up @@ -11,5 +11,5 @@ known_pytest=pytest
known_future_library=future
known_standard_library=types,requests
default_section=THIRDPARTY
known_third_party = base,click,github_release,graphql,hupper,pygments,starlette,uvicorn
known_third_party = base,click,comment_templates,config,github,github_release,graphql,httpx,hupper,pygments,release,starlette,uvicorn
sections=FUTURE,STDLIB,PYTEST,DJANGO,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
4 changes: 4 additions & 0 deletions RELEASE.md
@@ -0,0 +1,4 @@
Release type: patch

This release doesn't add any feature or fixes, it only introduces a GitHub
Action to let people know how to add a RELEASE.md file when submitting a PR.
10 changes: 5 additions & 5 deletions scripts/base.py
@@ -1,9 +1,9 @@
import re
import subprocess
import sys
import re

from pathlib import Path


ROOT = Path(
subprocess.check_output(["git", "rev-parse", "--show-toplevel"])
.decode("ascii")
Expand Down Expand Up @@ -49,15 +49,15 @@ def get_project_version():
match = VERSION_REGEX.match(line)

if match:
return match.group('version')
return match.group("version")

return None


def get_release_info():
def get_release_info(file_path=RELEASE_FILE):
RELEASE_TYPE_REGEX = re.compile(r"^[Rr]elease [Tt]ype: (major|minor|patch)$")

with open(RELEASE_FILE, "r") as f:
with open(file_path, "r") as f:
line = f.readline()
match = RELEASE_TYPE_REGEX.match(line)

Expand Down