github-local 0.0.3b4586__tar.gz → 0.0.5b4611__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.
Files changed (33) hide show
  1. {github_local-0.0.3b4586/python_packages/github_local.egg-info → github_local-0.0.5b4611}/PKG-INFO +3 -1
  2. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/pyproject.toml +1 -1
  3. github_local-0.0.5b4611/python_packages/github_local/src/github_run_conclusion_enum.py +14 -0
  4. github_local-0.0.5b4611/python_packages/github_local/src/github_run_rerun.py +36 -0
  5. github_local-0.0.5b4611/python_packages/github_local/tests/unit/github_run_rerun_unit_test.py +75 -0
  6. {github_local-0.0.3b4586 → github_local-0.0.5b4611/python_packages/github_local.egg-info}/PKG-INFO +3 -1
  7. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local.egg-info/SOURCES.txt +3 -3
  8. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/setup.py +4 -2
  9. github_local-0.0.3b4586/python_packages/github_local/src/github_failed_job_reply.py +0 -10
  10. github_local-0.0.3b4586/python_packages/github_local/src/handle_github_failed_job.py +0 -62
  11. github_local-0.0.3b4586/python_packages/github_local/tests/unit/handle_github_failed_job_unit_test.py +0 -66
  12. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/.coveragerc +0 -0
  13. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/.flake8 +0 -0
  14. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/README.md +0 -0
  15. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/pytest.ini +0 -0
  16. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/conftest.py +0 -0
  17. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/__init__.py +0 -0
  18. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/src/__init__.py +0 -0
  19. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/src/classify_github_actions_failure.py +0 -0
  20. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/src/constants_github_local.py +0 -0
  21. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/src/github_actions_failure.py +0 -0
  22. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/src/github_actions_failure_category.py +0 -0
  23. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/src/github_local.py +0 -0
  24. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/tests/__init__.py +0 -0
  25. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/tests/unit/__init__.py +0 -0
  26. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/tests/unit/classify_github_actions_failure_unit_test.py +0 -0
  27. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/tests/unit/conftest.py +0 -0
  28. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local/tests/unit/github_local_unit_test.py +0 -0
  29. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local.egg-info/dependency_links.txt +0 -0
  30. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local.egg-info/requires.txt +0 -0
  31. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/python_packages/github_local.egg-info/top_level.txt +0 -0
  32. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/requirements.txt +0 -0
  33. {github_local-0.0.3b4586 → github_local-0.0.5b4611}/setup.cfg +0 -0
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-local
3
- Version: 0.0.3b4586
3
+ Version: 0.0.5b4611
4
4
  Summary: PyPI github-local Python Package owned by Circlez.ai
5
5
  Home-page: https://github.com/circles-zone/github-local-python-package
6
6
  Author: Circles
7
7
  Author-email: info@circlez.ai
8
+ License: MIT
8
9
  Classifier: Programming Language :: Python :: 3
9
10
  Classifier: Operating System :: OS Independent
10
11
  Description-Content-Type: text/markdown
@@ -16,6 +17,7 @@ Dynamic: classifier
16
17
  Dynamic: description
17
18
  Dynamic: description-content-type
18
19
  Dynamic: home-page
20
+ Dynamic: license
19
21
  Dynamic: requires-dist
20
22
  Dynamic: summary
21
23
 
@@ -22,7 +22,7 @@ pythonpath = ["."]
22
22
  name = "github-local"
23
23
  # After the b (beta) comes the Jira Work Item ID (sdk-local Makec.ps1 updates it)
24
24
  # I believe we are still using the version from setup.py and not from here until Potery will work
25
- version = "0.0.3b4586" # https://pypi.org/project/github-local/
25
+ version = "0.0.5b4611" # https://pypi.org/project/github-local/
26
26
  description = "github-local Python Package"
27
27
  readme = "README.md"
28
28
  authors = [
@@ -0,0 +1,14 @@
1
+ from enum import Enum
2
+
3
+
4
+ class GitHubRunConclusionEnum(Enum):
5
+ """The conclusion of a completed GitHub Actions run, as the GitHub REST API returns it"""
6
+ SUCCESS = "success"
7
+ FAILURE = "failure"
8
+ CANCELLED = "cancelled"
9
+ SKIPPED = "skipped"
10
+ TIMED_OUT = "timed_out"
11
+ ACTION_REQUIRED = "action_required"
12
+ NEUTRAL = "neutral"
13
+ STALE = "stale"
14
+ STARTUP_FAILURE = "startup_failure"
@@ -0,0 +1,36 @@
1
+ import requests
2
+ from python_sdk_remote.utilities import our_get_env
3
+
4
+ from .github_run_conclusion_enum import GitHubRunConclusionEnum
5
+
6
+ DEFAULT_GITHUB_API_URL = "https://api.github.com"
7
+ REQUEST_TIMEOUT_SECONDS = 30
8
+ COMPLETED_STATUS = "completed"
9
+
10
+
11
+ class GitHubRunRerun:
12
+ """Re-runs the failed jobs of a GitHub Actions run - the automated fix of a failed run"""
13
+
14
+ def __init__(self, token: str | None = None, api_url: str | None = None,
15
+ session: requests.Session | None = None):
16
+ self.token = token or our_get_env("GITHUB_TOKEN", raise_if_empty=True)
17
+ self.api_url = (api_url or our_get_env("GITHUB_API_URL", DEFAULT_GITHUB_API_URL)).rstrip("/")
18
+ self.session = session or requests.Session()
19
+
20
+ def _headers(self) -> dict[str, str]:
21
+ headers = {"Authorization": f"Bearer {self.token}", "Accept": "application/vnd.github+json"}
22
+ return headers
23
+
24
+ def rerun_failed_jobs(self, repo: str, run_id: int) -> None:
25
+ response = self.session.post(f"{self.api_url}/repos/{repo}/actions/runs/{run_id}/rerun-failed-jobs",
26
+ headers=self._headers(), timeout=REQUEST_TIMEOUT_SECONDS)
27
+ response.raise_for_status()
28
+
29
+ def get_conclusion(self, repo: str, run_id: int) -> GitHubRunConclusionEnum | None:
30
+ """None while the run is not completed"""
31
+ response = self.session.get(f"{self.api_url}/repos/{repo}/actions/runs/{run_id}",
32
+ headers=self._headers(), timeout=REQUEST_TIMEOUT_SECONDS)
33
+ response.raise_for_status()
34
+ run = response.json()
35
+ conclusion = GitHubRunConclusionEnum(run["conclusion"]) if run["status"] == COMPLETED_STATUS else None
36
+ return conclusion
@@ -0,0 +1,75 @@
1
+ from http import HTTPStatus
2
+ from unittest.mock import MagicMock
3
+
4
+ import pytest
5
+ import requests
6
+ from python_sdk_remote.utilities import OurEnvironmentVariableException
7
+
8
+ from github_local.src.github_run_conclusion_enum import GitHubRunConclusionEnum
9
+ from github_local.src.github_run_rerun import GitHubRunRerun
10
+
11
+ API_URL = "https://api.github.test"
12
+ REPO = "circles-zone/sdk-local"
13
+
14
+
15
+ def response(status_code: HTTPStatus = HTTPStatus.CREATED, json_result: dict | None = None) -> MagicMock:
16
+ result = MagicMock(status_code=status_code)
17
+ result.json.return_value = json_result
18
+ result.raise_for_status.side_effect = requests.HTTPError(str(status_code)) if status_code >= HTTPStatus.BAD_REQUEST else None
19
+ return result
20
+
21
+
22
+ @pytest.fixture(autouse=True)
23
+ def environment(monkeypatch):
24
+ monkeypatch.setenv("GITHUB_TOKEN", "token")
25
+ monkeypatch.setenv("GITHUB_API_URL", API_URL)
26
+
27
+
28
+ def test_re_runs_the_failed_jobs_of_the_run():
29
+ session = MagicMock()
30
+ session.post.return_value = response()
31
+
32
+ GitHubRunRerun(session=session).rerun_failed_jobs(REPO, 41918691683)
33
+
34
+ assert session.post.call_args.args[0] == f"{API_URL}/repos/{REPO}/actions/runs/41918691683/rerun-failed-jobs"
35
+ assert session.post.call_args.kwargs["headers"]["Authorization"] == "Bearer token"
36
+
37
+
38
+ def test_re_run_raises_on_an_http_error():
39
+ session = MagicMock()
40
+ session.post.return_value = response(HTTPStatus.FORBIDDEN)
41
+
42
+ with pytest.raises(requests.HTTPError):
43
+ GitHubRunRerun(session=session).rerun_failed_jobs(REPO, 1)
44
+
45
+
46
+ def test_the_conclusion_is_none_while_the_run_is_in_progress():
47
+ session = MagicMock()
48
+ session.get.return_value = response(HTTPStatus.OK, {"status": "in_progress", "conclusion": None})
49
+
50
+ assert GitHubRunRerun(session=session).get_conclusion(REPO, 1) is None
51
+ assert session.get.call_args.args[0] == f"{API_URL}/repos/{REPO}/actions/runs/1"
52
+
53
+
54
+ def test_the_conclusion_of_a_completed_run_is_an_enum():
55
+ session = MagicMock()
56
+ session.get.return_value = response(HTTPStatus.OK, {"status": "completed", "conclusion": "success"})
57
+
58
+ assert GitHubRunRerun(session=session).get_conclusion(REPO, 1) is GitHubRunConclusionEnum.SUCCESS
59
+
60
+
61
+ def test_explicit_token_and_api_url_win():
62
+ session = MagicMock()
63
+ session.post.return_value = response()
64
+
65
+ GitHubRunRerun(token="explicit", api_url="https://ghe.test/", session=session).rerun_failed_jobs("o/r", 2)
66
+
67
+ assert session.post.call_args.args[0] == "https://ghe.test/repos/o/r/actions/runs/2/rerun-failed-jobs"
68
+ assert session.post.call_args.kwargs["headers"]["Authorization"] == "Bearer explicit"
69
+
70
+
71
+ def test_a_missing_token_raises(monkeypatch):
72
+ monkeypatch.setenv("GITHUB_TOKEN", "")
73
+
74
+ with pytest.raises(OurEnvironmentVariableException):
75
+ GitHubRunRerun()
@@ -1,10 +1,11 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-local
3
- Version: 0.0.3b4586
3
+ Version: 0.0.5b4611
4
4
  Summary: PyPI github-local Python Package owned by Circlez.ai
5
5
  Home-page: https://github.com/circles-zone/github-local-python-package
6
6
  Author: Circles
7
7
  Author-email: info@circlez.ai
8
+ License: MIT
8
9
  Classifier: Programming Language :: Python :: 3
9
10
  Classifier: Operating System :: OS Independent
10
11
  Description-Content-Type: text/markdown
@@ -16,6 +17,7 @@ Dynamic: classifier
16
17
  Dynamic: description
17
18
  Dynamic: description-content-type
18
19
  Dynamic: home-page
20
+ Dynamic: license
19
21
  Dynamic: requires-dist
20
22
  Dynamic: summary
21
23
 
@@ -17,12 +17,12 @@ python_packages/github_local/src/classify_github_actions_failure.py
17
17
  python_packages/github_local/src/constants_github_local.py
18
18
  python_packages/github_local/src/github_actions_failure.py
19
19
  python_packages/github_local/src/github_actions_failure_category.py
20
- python_packages/github_local/src/github_failed_job_reply.py
21
20
  python_packages/github_local/src/github_local.py
22
- python_packages/github_local/src/handle_github_failed_job.py
21
+ python_packages/github_local/src/github_run_conclusion_enum.py
22
+ python_packages/github_local/src/github_run_rerun.py
23
23
  python_packages/github_local/tests/__init__.py
24
24
  python_packages/github_local/tests/unit/__init__.py
25
25
  python_packages/github_local/tests/unit/classify_github_actions_failure_unit_test.py
26
26
  python_packages/github_local/tests/unit/conftest.py
27
27
  python_packages/github_local/tests/unit/github_local_unit_test.py
28
- python_packages/github_local/tests/unit/handle_github_failed_job_unit_test.py
28
+ python_packages/github_local/tests/unit/github_run_rerun_unit_test.py
@@ -24,6 +24,7 @@ package_dir = PACKAGE_NAME.replace("-", "_")
24
24
  PACKAGE_DESCRIPTION = f"PyPI {PACKAGE_NAME} Python Package owned by Circlez.ai"
25
25
 
26
26
  setuptools.setup(
27
+ license='MIT',
27
28
  name=PACKAGE_NAME,
28
29
  # Increase this number every time you make a change
29
30
  # want to publish. After 0.0.9 switch to 0.0.10 and not 0.1.0
@@ -32,7 +33,7 @@ setuptools.setup(
32
33
  # Updating the b{{JiraWorkItemId}} done automatically by makec.ps1 / make_py.ps1
33
34
  # According to GitFlow, in the Feature Branch, after the letter 'b' comes the
34
35
  # Jira Work Item ID, i.e., 0.0.1b2432. Semantic Version simver.org
35
- version='0.0.3b4586', # https://pypi.org/project/github-local#History
36
+ version="0.0.5b4611", # https://pypi.org/project/github-local#History
36
37
  author="Circles",
37
38
  author_email="info@circlez.ai",
38
39
  description=f"{PACKAGE_DESCRIPTION}",
@@ -60,7 +61,7 @@ setuptools.setup(
60
61
  "Programming Language :: Python :: 3",
61
62
  # https://packaging.python.org/en/latest/guides/writing-pyproject-toml/#license
62
63
  # TODO Fix all repos, especially the Python templates
63
- # "License :: MIT AND (Apache-2.0 OR BSD-2-Clause)",
64
+ #
64
65
  "Operating System :: OS Independent",
65
66
  ],
66
67
  # Do not add packages needed only for tests, i.e., packages for get_test_xxx(
@@ -71,3 +72,4 @@ setuptools.setup(
71
72
  'requests',
72
73
  ]
73
74
  )
75
+
@@ -1,10 +0,0 @@
1
- from dataclasses import dataclass
2
-
3
- from .github_actions_failure_category import GitHubActionsFailureCategory
4
-
5
-
6
- @dataclass(frozen=True)
7
- class GitHubFailedJobReply:
8
- channel_message: str
9
- category: GitHubActionsFailureCategory
10
- is_rerun_safe: bool
@@ -1,62 +0,0 @@
1
- """A failed GitHub Actions job (GitHub webhook workflow_job event): classify it and tell the team in one Slack message,
2
- so nobody has to open the log to know whether it is their code, a runner or a missing secret."""
3
- from collections.abc import Callable
4
- from typing import Any
5
-
6
- from .classify_github_actions_failure import classify_github_actions_failure, main_error
7
- from .github_actions_failure import GitHubActionsFailure
8
- from .github_actions_failure_category import GitHubActionsFailureCategory, is_rerun_safe
9
- from .github_failed_job_reply import GitHubFailedJobReply
10
-
11
- WORKFLOW_JOB_EVENT = "workflow_job"
12
- NEXT_STEP = {
13
- GitHubActionsFailureCategory.RUNNER: "The runner, not the code: rerun the job (another runner if it repeats).",
14
- GitHubActionsFailureCategory.CONFIG_SECRET: "A secret / variable / permission is missing or wrong.",
15
- GitHubActionsFailureCategory.PROCESS_GATE: "A company rule stopped it on purpose - see the error.",
16
- GitHubActionsFailureCategory.SECURITY_DEPENDENCY: "A dependency or a security alert - update the package.",
17
- GitHubActionsFailureCategory.TEST_FAILURE: "A test failed - the commit author please check.",
18
- GitHubActionsFailureCategory.LINT_TYPE: "Lint / type check - the commit author please fix.",
19
- GitHubActionsFailureCategory.PUBLISH_DEPLOY: "Build and tests passed; publishing or deploying failed.",
20
- GitHubActionsFailureCategory.INVALID_WORKFLOW: "The workflow file itself is invalid.",
21
- GitHubActionsFailureCategory.UNKNOWN: "Could not tell from the log.",
22
- }
23
- MESSAGE = (":red_circle: *{repo_name}* / {workflow_name} / {job_name} failed - `{category}`\n"
24
- "{next_step}\n> {main_error}\n{html_url}")
25
- MAXIMUM_ERROR_LENGTH = 300
26
-
27
-
28
- class HandleGitHubFailedJob:
29
- def __init__(self, get_job_log: Callable[[str, int], str]):
30
- """get_job_log(repo full name, job id) -> the job log text, e.g. from the GitHub REST API"""
31
- self.get_job_log = get_job_log
32
-
33
- @staticmethod
34
- def is_event(event_name: str, payload: dict[str, Any]) -> bool:
35
- workflow_job = payload.get("workflow_job") or {}
36
- is_failed_job = (event_name == WORKFLOW_JOB_EVENT and payload.get("action") == "completed"
37
- and workflow_job.get("conclusion") == "failure")
38
- return is_failed_job
39
-
40
- def handle(self, payload: dict[str, Any]) -> GitHubFailedJobReply:
41
- workflow_job = payload["workflow_job"]
42
- repo_full_name = payload["repository"]["full_name"]
43
- try:
44
- log = self.get_job_log(repo_full_name, workflow_job["id"])
45
- except Exception:
46
- # The message still goes out: the category is then UNKNOWN
47
- log = ""
48
- failed_step_name = next((step.get("name") for step in workflow_job.get("steps") or []
49
- if step.get("conclusion") == "failure"), None)
50
- failure = GitHubActionsFailure(repo_name=payload["repository"]["name"],
51
- workflow_name=workflow_job.get("workflow_name") or "",
52
- job_name=workflow_job.get("name"), failed_step_name=failed_step_name,
53
- runner_name=workflow_job.get("runner_name"), log=log)
54
- category = classify_github_actions_failure(failure)
55
- error = main_error(log)[:MAXIMUM_ERROR_LENGTH] or f"Failed step: {failed_step_name or 'unknown'}"
56
- channel_message = MESSAGE.format(repo_name=failure.repo_name, workflow_name=failure.workflow_name,
57
- job_name=failure.job_name, category=category.value,
58
- next_step=NEXT_STEP[category], main_error=error,
59
- html_url=workflow_job.get("html_url", ""))
60
- reply = GitHubFailedJobReply(channel_message=channel_message, category=category,
61
- is_rerun_safe=is_rerun_safe(category))
62
- return reply
@@ -1,66 +0,0 @@
1
- import pytest
2
-
3
- from github_local.src.github_actions_failure_category import GitHubActionsFailureCategory
4
- from github_local.src.handle_github_failed_job import HandleGitHubFailedJob
5
-
6
- JOB_URL = "https://github.com/circles-zone/sdk-local/actions/runs/1/job/2"
7
- RUNNER_LOG = "Run tests\n##[error]No space left on device\n##[error]Process completed with exit code 1."
8
-
9
-
10
- def workflow_job_payload(conclusion: str = "failure", action: str = "completed") -> dict:
11
- payload = {"action": action,
12
- "repository": {"name": "sdk-local", "full_name": "circles-zone/sdk-local"},
13
- "workflow_job": {"id": 2, "conclusion": conclusion, "name": "build", "workflow_name": "Play1",
14
- "runner_name": "runner-1", "html_url": JOB_URL,
15
- "steps": [{"name": "Checkout", "conclusion": "success"},
16
- {"name": "Run tests", "conclusion": "failure"}]}}
17
- return payload
18
-
19
-
20
- @pytest.mark.parametrize("event_name, payload, is_event", [
21
- ("workflow_job", workflow_job_payload(), True),
22
- ("workflow_job", workflow_job_payload(conclusion="success"), False),
23
- ("workflow_job", workflow_job_payload(action="in_progress"), False),
24
- ("push", workflow_job_payload(), False),
25
- ("workflow_job", {"action": "completed"}, False),
26
- ])
27
- def test_is_event_is_a_completed_failed_job(event_name, payload, is_event):
28
- assert HandleGitHubFailedJob.is_event(event_name, payload) is is_event
29
-
30
-
31
- def test_a_runner_failure_says_to_rerun_and_links_the_job():
32
- job_logs: list[tuple[str, int]] = []
33
-
34
- def get_job_log(repo_full_name: str, job_id: int) -> str:
35
- job_logs.append((repo_full_name, job_id))
36
- return RUNNER_LOG
37
-
38
- reply = HandleGitHubFailedJob(get_job_log).handle(workflow_job_payload())
39
-
40
- assert job_logs == [("circles-zone/sdk-local", 2)]
41
- assert reply.category == GitHubActionsFailureCategory.RUNNER
42
- assert reply.is_rerun_safe
43
- assert reply.channel_message.startswith(":red_circle: *sdk-local* / Play1 / build failed - `runner`\n")
44
- assert "rerun the job" in reply.channel_message
45
- assert "> No space left on device" in reply.channel_message
46
- assert reply.channel_message.endswith(JOB_URL)
47
-
48
-
49
- def test_without_a_log_the_message_names_the_failed_step():
50
- def get_job_log(repo_full_name: str, job_id: int) -> str:
51
- raise RuntimeError("Not Found")
52
-
53
- reply = HandleGitHubFailedJob(get_job_log).handle(workflow_job_payload())
54
-
55
- assert reply.category == GitHubActionsFailureCategory.UNKNOWN
56
- assert not reply.is_rerun_safe
57
- assert "> Failed step: Run tests" in reply.channel_message
58
-
59
-
60
- def test_a_job_without_steps_still_gets_a_message():
61
- payload = workflow_job_payload()
62
- del payload["workflow_job"]["steps"]
63
-
64
- reply = HandleGitHubFailedJob(lambda repo_full_name, job_id: "").handle(payload)
65
-
66
- assert "> Failed step: unknown" in reply.channel_message