pbx-code-owners 1.0.0a4__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of pbx-code-owners might be problematic. Click here for more details.
- pbx_code_owners-1.0.0a4/PKG-INFO +9 -0
- pbx_code_owners-1.0.0a4/README.md +8 -0
- pbx_code_owners-1.0.0a4/code_owners/__init__.py +1 -0
- pbx_code_owners-1.0.0a4/code_owners/config.py +17 -0
- pbx_code_owners-1.0.0a4/code_owners/parser.py +80 -0
- pbx_code_owners-1.0.0a4/pbx_code_owners.egg-info/PKG-INFO +9 -0
- pbx_code_owners-1.0.0a4/pbx_code_owners.egg-info/SOURCES.txt +13 -0
- pbx_code_owners-1.0.0a4/pbx_code_owners.egg-info/dependency_links.txt +1 -0
- pbx_code_owners-1.0.0a4/pbx_code_owners.egg-info/entry_points.txt +2 -0
- pbx_code_owners-1.0.0a4/pbx_code_owners.egg-info/requires.txt +2 -0
- pbx_code_owners-1.0.0a4/pbx_code_owners.egg-info/top_level.txt +1 -0
- pbx_code_owners-1.0.0a4/setup.cfg +25 -0
- pbx_code_owners-1.0.0a4/setup.py +23 -0
- pbx_code_owners-1.0.0a4/tests/tests.py +101 -0
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
# Sample GitLab Project
|
|
2
|
+
|
|
3
|
+
This sample project shows how a project in GitLab looks for demonstration purposes. It contains issues, merge requests and Markdown files in many branches,
|
|
4
|
+
named and filled with lorem ipsum.
|
|
5
|
+
|
|
6
|
+
You can look around to get an idea how to structure your project and, when done, you can safely delete this project.
|
|
7
|
+
|
|
8
|
+
[Learn more about creating GitLab projects.](https://docs.gitlab.com/ee/gitlab-basics/create-project.html)
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
from .parser import main # noqa: F401
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import os
|
|
2
|
+
from pathlib import Path
|
|
3
|
+
|
|
4
|
+
DEFAULT_BRANCH = os.environ.get("DEFAULT_BRANCH", "master")
|
|
5
|
+
CODEOWNERS_FILE = Path(os.getcwd()) / "codeowners.yml"
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
CI_SERVER_URL = os.environ["CI_SERVER_URL"]
|
|
9
|
+
CI_PROJECT_ID = os.environ["CI_PROJECT_ID"]
|
|
10
|
+
CI_MERGE_REQUEST_IID = os.environ["CI_MERGE_REQUEST_IID"]
|
|
11
|
+
GITLAB_TOKEN = os.environ["MR_QA_GUARD_TOKEN"]
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
GITLAB_DISCUSSIONS_URL = (
|
|
15
|
+
f"{CI_SERVER_URL}/api/v4/projects/{CI_PROJECT_ID}"
|
|
16
|
+
f"/merge_requests/{CI_MERGE_REQUEST_IID}/discussions"
|
|
17
|
+
)
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import subprocess # nosec
|
|
2
|
+
|
|
3
|
+
import requests
|
|
4
|
+
import yaml
|
|
5
|
+
|
|
6
|
+
from code_owners import config
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def load_codeowners() -> dict:
|
|
10
|
+
with open(config.CODEOWNERS_FILE) as file:
|
|
11
|
+
return yaml.safe_load(file)
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
def get_changed_paths() -> list[str]:
|
|
15
|
+
subprocess.run(["git", "fetch", "origin", "master:master"]) # nosec
|
|
16
|
+
git_diff = subprocess.run( # nosec
|
|
17
|
+
["git", "diff", "--name-only", config.DEFAULT_BRANCH], stdout=subprocess.PIPE
|
|
18
|
+
)
|
|
19
|
+
return git_diff.stdout.decode().splitlines()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def match_any_path(sub_paths: list[str], base_paths: list[str]) -> bool:
|
|
23
|
+
# TODO: implement wildcards
|
|
24
|
+
matched_paths = (
|
|
25
|
+
sub_path
|
|
26
|
+
for sub_path in sub_paths
|
|
27
|
+
for base_path in base_paths
|
|
28
|
+
if sub_path.startswith(base_path)
|
|
29
|
+
)
|
|
30
|
+
try:
|
|
31
|
+
next(matched_paths)
|
|
32
|
+
except StopIteration:
|
|
33
|
+
return False
|
|
34
|
+
return True
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def discussion_exists(content: str) -> bool:
|
|
38
|
+
url = config.GITLAB_DISCUSSIONS_URL
|
|
39
|
+
headers = {"PRIVATE-TOKEN": config.GITLAB_TOKEN}
|
|
40
|
+
resp = requests.get(url, headers=headers, timeout=(3, 30))
|
|
41
|
+
resp.raise_for_status()
|
|
42
|
+
discussions = resp.json()
|
|
43
|
+
filtered_discussions = (
|
|
44
|
+
discussion
|
|
45
|
+
for discussion in discussions
|
|
46
|
+
if discussion["notes"][0]["body"] == content
|
|
47
|
+
)
|
|
48
|
+
try:
|
|
49
|
+
next(filtered_discussions)
|
|
50
|
+
except StopIteration:
|
|
51
|
+
return False
|
|
52
|
+
return True
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
def notify_owner(owner: str, section_name: str) -> None:
|
|
56
|
+
content = f'Section "{section_name}" requires {owner} approval'
|
|
57
|
+
if discussion_exists(content):
|
|
58
|
+
return
|
|
59
|
+
|
|
60
|
+
url = config.GITLAB_DISCUSSIONS_URL
|
|
61
|
+
headers = {"PRIVATE-TOKEN": config.GITLAB_TOKEN}
|
|
62
|
+
resp = requests.post(url, headers=headers, json={"body": content}, timeout=(3, 30))
|
|
63
|
+
resp.raise_for_status()
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def main() -> None:
|
|
67
|
+
codeowners = load_codeowners()
|
|
68
|
+
changed_paths = get_changed_paths()
|
|
69
|
+
if not changed_paths:
|
|
70
|
+
print("No changes, checking codeowners stopped")
|
|
71
|
+
return
|
|
72
|
+
|
|
73
|
+
for section in codeowners["sections"]:
|
|
74
|
+
if "." in section["paths"] or match_any_path(changed_paths, section["paths"]):
|
|
75
|
+
for owner in section["owners"]:
|
|
76
|
+
notify_owner(owner, section["name"])
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
if __name__ == "__main__":
|
|
80
|
+
main()
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
setup.cfg
|
|
3
|
+
setup.py
|
|
4
|
+
code_owners/__init__.py
|
|
5
|
+
code_owners/config.py
|
|
6
|
+
code_owners/parser.py
|
|
7
|
+
pbx_code_owners.egg-info/PKG-INFO
|
|
8
|
+
pbx_code_owners.egg-info/SOURCES.txt
|
|
9
|
+
pbx_code_owners.egg-info/dependency_links.txt
|
|
10
|
+
pbx_code_owners.egg-info/entry_points.txt
|
|
11
|
+
pbx_code_owners.egg-info/requires.txt
|
|
12
|
+
pbx_code_owners.egg-info/top_level.txt
|
|
13
|
+
tests/tests.py
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
code_owners
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
[bumpversion]
|
|
2
|
+
current_version = 1.0.0
|
|
3
|
+
commit = True
|
|
4
|
+
tag = True
|
|
5
|
+
|
|
6
|
+
[bumpversion:file:setup.py]
|
|
7
|
+
|
|
8
|
+
[coverage:run]
|
|
9
|
+
include = code_owners/*
|
|
10
|
+
omit = */tests.py,*/tests/*.py
|
|
11
|
+
|
|
12
|
+
[flake8]
|
|
13
|
+
exclude = .git,__pycache
|
|
14
|
+
max-line-length = 96
|
|
15
|
+
|
|
16
|
+
[isort]
|
|
17
|
+
profile = black
|
|
18
|
+
default_section = THIRDPARTY
|
|
19
|
+
known_first_party = code_owners
|
|
20
|
+
sections = FUTURE,STDLIB,THIRDPARTY,FIRSTPARTY,LOCALFOLDER
|
|
21
|
+
|
|
22
|
+
[egg_info]
|
|
23
|
+
tag_build =
|
|
24
|
+
tag_date = 0
|
|
25
|
+
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
from setuptools import find_packages, setup
|
|
2
|
+
|
|
3
|
+
setup(
|
|
4
|
+
name="pbx-code-owners",
|
|
5
|
+
version="1.0.0a4",
|
|
6
|
+
packages=find_packages(exclude=["tests*"]),
|
|
7
|
+
url="https://getprintbox.com/",
|
|
8
|
+
license="",
|
|
9
|
+
author="",
|
|
10
|
+
author_email="",
|
|
11
|
+
description="",
|
|
12
|
+
install_requires=[
|
|
13
|
+
"pyyaml",
|
|
14
|
+
"requests",
|
|
15
|
+
],
|
|
16
|
+
extras_require={},
|
|
17
|
+
python_requires=">=3.8",
|
|
18
|
+
entry_points={
|
|
19
|
+
"console_scripts": [
|
|
20
|
+
"pbx-code-owners=code_owners:main",
|
|
21
|
+
],
|
|
22
|
+
},
|
|
23
|
+
)
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
from unittest.mock import ANY, MagicMock, patch
|
|
2
|
+
|
|
3
|
+
from parameterized import parameterized
|
|
4
|
+
|
|
5
|
+
from code_owners.parser import discussion_exists, main, notify_owner
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
@parameterized.expand(
|
|
9
|
+
[
|
|
10
|
+
("french_owner", "peugeot"),
|
|
11
|
+
("polish_owner", "peugeot_too"),
|
|
12
|
+
("polish_owner_too", "bmw"),
|
|
13
|
+
]
|
|
14
|
+
)
|
|
15
|
+
@patch("code_owners.parser.requests.post")
|
|
16
|
+
@patch("code_owners.config.GITLAB_DISCUSSIONS_URL", "https://gitlab.test")
|
|
17
|
+
@patch("code_owners.config.GITLAB_TOKEN", "secret_token")
|
|
18
|
+
def test_notify(owner: str, section_name: str, post_mock: MagicMock):
|
|
19
|
+
with patch("code_owners.parser.discussion_exists", return_value=False):
|
|
20
|
+
notify_owner(owner, section_name)
|
|
21
|
+
expected_message = f'Section "{section_name}" requires {owner} approval'
|
|
22
|
+
post_mock.assert_called_once_with(
|
|
23
|
+
"https://gitlab.test",
|
|
24
|
+
headers={"PRIVATE-TOKEN": "secret_token"},
|
|
25
|
+
json={"body": expected_message},
|
|
26
|
+
timeout=ANY,
|
|
27
|
+
)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
@parameterized.expand(
|
|
31
|
+
[
|
|
32
|
+
("LGTM! :>", True),
|
|
33
|
+
("CTZG", False),
|
|
34
|
+
("Dzieen bobry", False),
|
|
35
|
+
("Dzieen bobry *beaver_emoji*", True),
|
|
36
|
+
]
|
|
37
|
+
)
|
|
38
|
+
def test_discussion_exists(searched_content: str, expeted_existence: bool):
|
|
39
|
+
mocked_reply = [
|
|
40
|
+
{
|
|
41
|
+
"id": "105a81c457103110851c7906d6b4424876ec1482",
|
|
42
|
+
"notes": [{"body": "Hi, have a nice day! :>"}],
|
|
43
|
+
},
|
|
44
|
+
{
|
|
45
|
+
"id": "9472da155271a85072741b27b9364860abc4add1",
|
|
46
|
+
"notes": [{"body": "Dzieen bobry *beaver_emoji*"}],
|
|
47
|
+
},
|
|
48
|
+
{
|
|
49
|
+
"id": "b4374de03ff500a8a790fe4c993e1c890a821589",
|
|
50
|
+
"notes": [{"body": "LGTM! :>"}],
|
|
51
|
+
},
|
|
52
|
+
{
|
|
53
|
+
"id": "6c10c938ddc3471c9075072d47d1d8d5938a0aee",
|
|
54
|
+
"notes": [{"body": 'Section "beer" requires @bald_man approval'}],
|
|
55
|
+
},
|
|
56
|
+
]
|
|
57
|
+
|
|
58
|
+
with patch("code_owners.parser.requests.get") as get_mock:
|
|
59
|
+
get_mock.return_value.json.return_value = mocked_reply
|
|
60
|
+
assert discussion_exists(searched_content) == expeted_existence
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def test_code_owners_match():
|
|
64
|
+
code_owners = {
|
|
65
|
+
"sections": [
|
|
66
|
+
{"name": "Code owners", "owners": ["@user_owner"], "paths": ["."]},
|
|
67
|
+
{"name": "tests", "owners": ["@user_a"], "paths": ["tests/"]},
|
|
68
|
+
{"name": "config", "owners": ["@user_b"], "paths": ["config/config.py"]},
|
|
69
|
+
{
|
|
70
|
+
"name": "compose",
|
|
71
|
+
"owners": ["@user_c", "@user_d"],
|
|
72
|
+
"paths": ["docker/compose.yaml"],
|
|
73
|
+
},
|
|
74
|
+
{"name": "dependencies", "owners": ["@user_a"], "paths": ["requirements/"]},
|
|
75
|
+
]
|
|
76
|
+
}
|
|
77
|
+
changed_paths = [
|
|
78
|
+
"path/to/file.py",
|
|
79
|
+
"urls.py",
|
|
80
|
+
"tests/tests.py",
|
|
81
|
+
"config/config-dev.py",
|
|
82
|
+
"docker/compose.yaml",
|
|
83
|
+
"requirements/requirements.txt",
|
|
84
|
+
]
|
|
85
|
+
|
|
86
|
+
with patch(
|
|
87
|
+
"code_owners.parser.get_changed_paths", return_value=changed_paths
|
|
88
|
+
), patch("code_owners.parser.load_codeowners", return_value=code_owners), patch(
|
|
89
|
+
"code_owners.parser.notify_owner"
|
|
90
|
+
) as notify_owner_mock:
|
|
91
|
+
main()
|
|
92
|
+
|
|
93
|
+
assert notify_owner_mock.call_count == 5
|
|
94
|
+
for owner, section in (
|
|
95
|
+
("@user_owner", "Code owners"),
|
|
96
|
+
("@user_a", "tests"),
|
|
97
|
+
("@user_c", "compose"),
|
|
98
|
+
("@user_d", "compose"),
|
|
99
|
+
("@user_a", "dependencies"),
|
|
100
|
+
):
|
|
101
|
+
notify_owner_mock.assert_any_call(owner, section)
|