Skip to content

Commit

Permalink
Implement of QA checks action
Browse files Browse the repository at this point in the history
ASIM-5216
  • Loading branch information
nicoddemus committed Apr 4, 2023
1 parent 31c8ecb commit bf3018e
Show file tree
Hide file tree
Showing 11 changed files with 549 additions and 6 deletions.
10 changes: 5 additions & 5 deletions .github/workflows/python-app.yml
Original file line number Diff line number Diff line change
@@ -1,16 +1,16 @@
name: tests
name: test

on:
push:
branches: [ "master" ]
pull_request:


permissions:
contents: read

jobs:
build:
test:

runs-on: ubuntu-latest

Expand All @@ -22,7 +22,7 @@ jobs:
python-version: "3.x"
- name: Install dependencies
run: |
python -m pip install -e .[test]
python -m pip install -e .[test]
- name: Test
run: |
pytest
pytest --color=yes
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -127,3 +127,4 @@ dmypy.json

# Pyre type checker
.pyre/
.idea/
31 changes: 31 additions & 0 deletions .pre-commit-config.yaml
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]
32 changes: 31 additions & 1 deletion README.md
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
```
60 changes: 60 additions & 0 deletions action.yml
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
6 changes: 6 additions & 0 deletions mypy.ini
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
22 changes: 22 additions & 0 deletions pyproject.toml
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",
]
129 changes: 129 additions & 0 deletions src/qa_checklist.py
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)
73 changes: 73 additions & 0 deletions tests/test_qa_checklist.py
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
Loading

0 comments on commit bf3018e

Please sign in to comment.