Skip to content

Commit

Permalink
Merge pull request #126 from strawberry-graphql/action-release-check
Browse files Browse the repository at this point in the history
Add check for release file on PRs to master
  • Loading branch information
patrick91 committed Aug 25, 2019
2 parents 0a22c05 + a77f7b3 commit 1af5873
Show file tree
Hide file tree
Showing 10 changed files with 281 additions and 6 deletions.
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

0 comments on commit 1af5873

Please sign in to comment.