-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Showing
11 changed files
with
549 additions
and
6 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -127,3 +127,4 @@ dmypy.json | |
|
||
# Pyre type checker | ||
.pyre/ | ||
.idea/ |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,31 @@ | ||
repos: | ||
- repo: https://github.com/PyCQA/autoflake | ||
rev: v2.0.2 | ||
hooks: | ||
- id: autoflake | ||
name: autoflake | ||
args: ["--in-place", "--remove-unused-variables", "--remove-all-unused-imports"] | ||
language: python | ||
files: \.py$ | ||
- repo: https://github.com/pre-commit/pre-commit-hooks | ||
rev: v4.4.0 | ||
hooks: | ||
- id: trailing-whitespace | ||
- id: end-of-file-fixer | ||
- repo: https://github.com/asottile/reorder_python_imports | ||
rev: v3.9.0 | ||
hooks: | ||
- id: reorder-python-imports | ||
args: ['--application-directories=src:tests'] | ||
- repo: https://github.com/psf/black | ||
rev: 23.1.0 | ||
hooks: | ||
- id: black | ||
args: [--safe, --quiet] | ||
- repo: https://github.com/pre-commit/mirrors-mypy | ||
rev: v1.1.1 | ||
hooks: | ||
- id: mypy | ||
files: ^(src|tests) | ||
args: [] | ||
additional_dependencies: [pytest, types-mock, types-requests] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,2 +1,32 @@ | ||
# qa-checklist-action | ||
Action to make QA checks in PRs | ||
|
||
Action to make QA checks in PRs. | ||
|
||
When a pull request is opened or reopened, we check if the JIRA issue associated with the branch is of | ||
type `Story` -- if that's the case, we post a new comment containing a check-list of QA tasks that need | ||
to be done in order to merge the PR. | ||
|
||
To configure, add a new workflow in `.github/workflows`, configuring it like this: | ||
|
||
```yaml | ||
name: qa-checklist | ||
|
||
on: | ||
pull_request: | ||
types: [opened, reopened] | ||
|
||
|
||
jobs: | ||
qa-checklist: | ||
runs-on: ubuntu-latest | ||
|
||
steps: | ||
- uses: ESSS/qa-checklist-action@v1 | ||
with: | ||
# jira_url is optional, defaulting to the URL below. | ||
jira_url: https://eden.esss.co/jira | ||
jira_username: ${{ secrets.JIRA_BOT_USER }} | ||
jira_password: ${{ secrets.JIRA_BOT_PASSWORD }} | ||
github_token: ${{ secrets.EDEN_GITHUB_TOKEN }} | ||
ping_users: user1,user2 | ||
``` |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,60 @@ | ||
--- | ||
name: QA check list | ||
description: Verifies if the current branch needs additional QA, based on the issue type (Story). | ||
author: Bruno Oliveira | ||
branding: | ||
icon: package | ||
color: purple | ||
|
||
inputs: | ||
jira_url: | ||
description: JIRA URL to look for issues. | ||
required: false | ||
default: "https://eden.esss.co/jira" | ||
jira_username: | ||
description: JIRA user name. | ||
required: true | ||
jira_password: | ||
description: JIRA password. | ||
required: true | ||
github_token: | ||
description: Token for generic bot which will be used to post comments to PRs. | ||
required: true | ||
ping_users: | ||
description: List of usernames separated by `,` (without `@`), which will be pinged by the comment. | ||
required: true | ||
|
||
runs: | ||
using: composite | ||
steps: | ||
- uses: actions/setup-python@v4 | ||
id: python-qacl | ||
with: | ||
python-version: "3.x" | ||
update-environment: false | ||
|
||
- name: Create venv | ||
run: ${{ steps.python-qacl.outputs.python-path }} -Im venv /tmp/qacl | ||
shell: bash | ||
|
||
- name: Install dependencies | ||
run: > | ||
/tmp/qacl/bin/python | ||
-Im pip | ||
--disable-pip-version-check | ||
--no-python-version-warning | ||
install ${{ github.action_path }} | ||
shell: bash | ||
|
||
- run: > | ||
/tmp/qacl/bin/python | ||
${{ github.action_path }}/src/qa_checklist.py | ||
${{ github.head_ref }} | ||
${{ inputs.ping_users }} | ||
${{ inputs.github_token }} | ||
${{ inputs.jira_url }} | ||
${{ inputs.jira_username }} | ||
${{ inputs.jira_password }} | ||
${{ github.repository }} | ||
${{ github.event.number }} | ||
shell: bash |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,6 @@ | ||
[mypy] | ||
files = src | ||
ignore_missing_imports = True | ||
no_implicit_optional = True | ||
show_error_codes = True | ||
disallow_untyped_defs = True |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
[project] | ||
name = "qa-checklist-action" | ||
version = "0" # we're not an actual package. | ||
description = "QA checklist action: checks if a PR needs manual testing." | ||
authors = [{ name = "Bruno Oliveira", email = "[email protected]" }] | ||
dependencies = [ | ||
"github3.py", | ||
"requests", | ||
] | ||
license = { text = "MIT" } | ||
requires-python = ">=3.10" | ||
|
||
[project.optional-dependencies] | ||
test = [ | ||
"attrs", | ||
"pre-commit", | ||
"pytest", | ||
"pytest-datadir", | ||
"pytest-mock", | ||
"pytest-regressions", | ||
"responses", | ||
] |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,129 @@ | ||
import argparse | ||
import re | ||
import sys | ||
from typing import Sequence | ||
|
||
import github3 | ||
import requests | ||
|
||
|
||
def get_jira_issue_type( | ||
jira_url: str, issue_id: str, username: str, password: str | ||
) -> str: | ||
"""Gets the issue type of the given JIRA issue, as a string.""" | ||
auth = (username, password) | ||
resp = requests.get(jira_url + f"/rest/api/2/issue/{issue_id}", auth=auth) | ||
resp.raise_for_status() | ||
return resp.json()["fields"]["issuetype"]["name"] | ||
|
||
|
||
def post_qa_comment( | ||
*, | ||
ping_users: Sequence[str], | ||
reason: str, | ||
token: str, | ||
owner: str, | ||
repository: str, | ||
number: int, | ||
) -> None: | ||
"""Post a comment indicating that manual QA is required.""" | ||
gh = github3.login(token=token) | ||
pr = gh.pull_request(owner, repository, number) | ||
lines = [ | ||
"## QA Check ##", | ||
"", | ||
reason, | ||
"", | ||
"- [ ] Manual QA required.", | ||
"", | ||
"cc " + ", ".join(f"@{x}" for x in ping_users) + ".", | ||
] | ||
text = "\n".join(lines) | ||
pr.create_comment(text) | ||
|
||
|
||
def extract_issue_id(branch: str) -> str | None: | ||
""" | ||
Given a branch name, extract a JIRA issue id from there, or None if could not find a matching | ||
issue schema. | ||
""" | ||
if m := re.search(r"([A-Z]+-\d+)", branch): | ||
return m.group(1) | ||
else: | ||
return None | ||
|
||
|
||
def entry_point( | ||
*, | ||
branch: str, | ||
ping_users: Sequence[str], | ||
token: str, | ||
owner: str, | ||
repository: str, | ||
number: int, | ||
jira_url: str, | ||
jira_username: str, | ||
jira_password: str, | ||
) -> None: | ||
"""Main job of this action.""" | ||
issue_id = extract_issue_id(branch) | ||
print(f"Branch: {branch}") | ||
print(f"Issue: {issue_id}") | ||
if issue_id is None: | ||
print("Could not extract an issue from branch name, quitting...") | ||
return | ||
|
||
issue_type = get_jira_issue_type( | ||
jira_url=jira_url, | ||
issue_id=issue_id, | ||
username=jira_username, | ||
password=jira_password, | ||
) | ||
print(f"Issue type: {issue_type}") | ||
if issue_type != "Story": | ||
print("Not a Story, quitting...") | ||
return | ||
|
||
reason = ( | ||
f"Issue {issue_id} is of type {issue_type}, so we require manual QA to be done." | ||
) | ||
print(f"About to post to {owner}/{repository}, PR#{number}") | ||
post_qa_comment( | ||
ping_users=ping_users, | ||
reason=reason, | ||
token=token, | ||
owner=owner, | ||
repository=repository, | ||
number=number, | ||
) | ||
print("Posted successfully.") | ||
|
||
|
||
def main(argv: Sequence[str]) -> None: | ||
"""Main function, we extract all arguments and pass them along entry_point().""" | ||
parser = argparse.ArgumentParser() | ||
parser.add_argument("branch") | ||
parser.add_argument("ping_users") | ||
parser.add_argument("token") | ||
parser.add_argument("jira_url") | ||
parser.add_argument("jira_username") | ||
parser.add_argument("jira_password") | ||
parser.add_argument("slug") | ||
parser.add_argument("number", type=int) | ||
ns = parser.parse_args(argv[1:]) | ||
owner, repository = ns.slug.split("/") | ||
entry_point( | ||
branch=ns.branch, | ||
ping_users=tuple(x.strip() for x in ns.ping_users.split(",")), | ||
token=ns.token, | ||
owner=owner, | ||
repository=repository, | ||
number=ns.number, | ||
jira_url=ns.jira_url, | ||
jira_username=ns.jira_username, | ||
jira_password=ns.jira_password, | ||
) | ||
|
||
|
||
if __name__ == "__main__": | ||
main(sys.argv) |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,73 @@ | ||
import json | ||
from pathlib import Path | ||
|
||
import attr | ||
import github3 | ||
import responses | ||
from pytest_mock import MockerFixture | ||
from pytest_regressions.file_regression import FileRegressionFixture | ||
|
||
from qa_checklist import extract_issue_id | ||
from qa_checklist import get_jira_issue_type | ||
from qa_checklist import post_qa_comment | ||
|
||
|
||
@responses.activate | ||
def test_get_jira_issue_type(datadir: Path) -> None: | ||
response_data = json.loads((datadir / "response.json").read_text(encoding="UTF-8")) | ||
jira_url = "https://jira.com" | ||
issue_id = "SSRL-4612" | ||
|
||
responses.get( | ||
url=f"{jira_url}/rest/api/2/issue/{issue_id}", | ||
json=response_data, | ||
) | ||
|
||
issue_type = get_jira_issue_type( | ||
jira_url, issue_id, username="jira-bot", password="PASSWORD" | ||
) | ||
assert issue_type == "Story" | ||
|
||
|
||
def test_post_qa_comment( | ||
mocker: MockerFixture, file_regression: FileRegressionFixture | ||
) -> None: | ||
@attr.s(auto_attribs=True) | ||
class FakePullRequest: | ||
comment: str | None = None | ||
|
||
def create_comment(self, text: str) -> None: | ||
self.comment = text | ||
|
||
@attr.s(auto_attribs=True) | ||
class FakeGitHub: | ||
def __attrs_post_init__(self) -> None: | ||
self.pr = FakePullRequest() | ||
|
||
def pull_request( | ||
self, owner: str, repository: str, number: int | ||
) -> FakePullRequest: | ||
assert owner == "ESSS" | ||
assert repository == "alfasim" | ||
assert number == 105 | ||
return self.pr | ||
|
||
fake_gh = FakeGitHub() | ||
mocker.patch.object(github3, "login", return_value=fake_gh) | ||
|
||
post_qa_comment( | ||
ping_users=("user1", "user2"), | ||
reason="Because issue is of type Story.", | ||
token="GITHU_TOKEN", | ||
owner="ESSS", | ||
repository="alfasim", | ||
number=105, | ||
) | ||
assert fake_gh.pr.comment is not None | ||
file_regression.check(fake_gh.pr.comment) | ||
|
||
|
||
def test_extract_issue_id() -> None: | ||
assert extract_issue_id("fb-XP-1023-foobar") == "XP-1023" | ||
assert extract_issue_id("fb_XP-1023_foobar") == "XP-1023" | ||
assert extract_issue_id("fb_xp-1023_foobar") is None |
Oops, something went wrong.