github-local 0.0.3b3827.post12__tar.gz → 0.0.3b4575__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 (27) hide show
  1. {github_local-0.0.3b3827.post12/python_packages/github_local.egg-info → github_local-0.0.3b4575}/PKG-INFO +1 -1
  2. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/pyproject.toml +1 -1
  3. github_local-0.0.3b4575/python_packages/github_local/src/classify_github_actions_failure.py +70 -0
  4. github_local-0.0.3b4575/python_packages/github_local/src/github_actions_failure.py +13 -0
  5. github_local-0.0.3b4575/python_packages/github_local/src/github_actions_failure_category.py +23 -0
  6. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local/src/github_local.py +2 -1
  7. github_local-0.0.3b4575/python_packages/github_local/tests/unit/classify_github_actions_failure_unit_test.py +69 -0
  8. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local/tests/unit/conftest.py +2 -1
  9. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local/tests/unit/github_local_unit_test.py +5 -4
  10. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575/python_packages/github_local.egg-info}/PKG-INFO +1 -1
  11. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local.egg-info/SOURCES.txt +4 -0
  12. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/setup.py +1 -1
  13. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/.coveragerc +0 -0
  14. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/.flake8 +0 -0
  15. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/README.md +0 -0
  16. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/pytest.ini +0 -0
  17. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/conftest.py +0 -0
  18. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local/__init__.py +0 -0
  19. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local/src/__init__.py +0 -0
  20. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local/src/constants_github_local.py +0 -0
  21. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local/tests/__init__.py +0 -0
  22. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local/tests/unit/__init__.py +0 -0
  23. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local.egg-info/dependency_links.txt +0 -0
  24. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local.egg-info/requires.txt +0 -0
  25. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/python_packages/github_local.egg-info/top_level.txt +0 -0
  26. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/requirements.txt +0 -0
  27. {github_local-0.0.3b3827.post12 → github_local-0.0.3b4575}/setup.cfg +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-local
3
- Version: 0.0.3b3827.post12
3
+ Version: 0.0.3b4575
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
@@ -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.3b3827.post12" # https://pypi.org/project/github-local/
25
+ version = "0.0.3b4575" # https://pypi.org/project/github-local/
26
26
  description = "github-local Python Package"
27
27
  readme = "README.md"
28
28
  authors = [
@@ -0,0 +1,70 @@
1
+ import re
2
+ from http import HTTPStatus
3
+
4
+ from .github_actions_failure import GitHubActionsFailure
5
+ from .github_actions_failure_category import GitHubActionsFailureCategory
6
+
7
+ ERROR_MARKER = "##[error]"
8
+ GENERIC_ERROR_PATTERN = re.compile(r"Process completed with exit code|The process '.*' failed with exit code")
9
+ LINES_BEFORE_ERROR = 40
10
+ LINES_AFTER_ERROR = 10
11
+
12
+ UNAUTHORIZED = f"{HTTPStatus.UNAUTHORIZED.value} {HTTPStatus.UNAUTHORIZED.phrase}"
13
+ FORBIDDEN = f"{HTTPStatus.FORBIDDEN.value} {HTTPStatus.FORBIDDEN.phrase}"
14
+
15
+ # The first matching rule wins. The rules come from 1,959 failed runs in circles-zone (30 days to 2026-10-25).
16
+ RULES = [
17
+ (GitHubActionsFailureCategory.RUNNER,
18
+ r"no space left|runner.*(lost|shutdown|offline)|lost communication|command not found|"
19
+ r"cannot connect to the docker daemon|The operation was canceled"),
20
+ (GitHubActionsFailureCategory.CONFIG_SECRET,
21
+ r"(token|secret|credential|api[_ ]?key|password|deploy_key)\w*( and \w+)? (is |are )?"
22
+ rf"(not found|missing|invalid|expired|empty)|OurEnvironmentVariableException|Bad credentials|"
23
+ rf"{UNAUTHORIZED}|{FORBIDDEN}|ExpiredToken|Input required and not supplied: token|"
24
+ r"fatal: repository '.*' not found"),
25
+ (GitHubActionsFailureCategory.PROCESS_GATE,
26
+ r"Files were changed since Makec ran|Makec report is (invalid|missing)|Makec failed in local|"
27
+ r"Branch name for BU-|does not start with a BU-|is already Done - it must be an OPEN|"
28
+ r"no unit tests / system tests|check-jira-work-item|Not Fully Green"),
29
+ (GitHubActionsFailureCategory.SECURITY_DEPENDENCY,
30
+ r"Dependabot|No matching distribution|Could not find a version|ResolutionImpossible|npm ERR!|"
31
+ r"Cannot find module|ModuleNotFoundError"),
32
+ (GitHubActionsFailureCategory.TEST_FAILURE,
33
+ r"(^|\])\s*\[-\] |(?-i:\bFAILED\b)|AssertionError|Tests? failed|[1-9]\d* failed"),
34
+ (GitHubActionsFailureCategory.LINT_TYPE,
35
+ r"PSAvoid|PSUse|flake8|mypy|error TS\d+|\bE\d{3} |\bF\d{3} "),
36
+ (GitHubActionsFailureCategory.PUBLISH_DEPLOY,
37
+ r"PublishPowerShell failed|Serverless deploy failed|Docker build failed|Publish.*failed"),
38
+ ]
39
+ COMPILED_RULES = [(category, re.compile(pattern, re.IGNORECASE | re.MULTILINE)) for category, pattern in RULES]
40
+
41
+
42
+ def log_excerpt(log: str) -> str:
43
+ """The lines around the first error - the end of a job log is only its cleanup"""
44
+ lines = log.splitlines()
45
+ error_indexes = [index for index, line in enumerate(lines) if ERROR_MARKER in line]
46
+ if error_indexes:
47
+ first_error = error_indexes[0]
48
+ excerpt_lines = lines[max(0, first_error - LINES_BEFORE_ERROR):first_error + LINES_AFTER_ERROR]
49
+ else:
50
+ excerpt_lines = lines[-LINES_BEFORE_ERROR:]
51
+ excerpt = "\n".join(excerpt_lines)
52
+ return excerpt
53
+
54
+
55
+ def main_error(log: str) -> str:
56
+ """The first specific error line ("Process completed with exit code 1" says nothing)"""
57
+ errors = [line.split(ERROR_MARKER, 1)[1].strip() for line in log.splitlines() if ERROR_MARKER in line]
58
+ specific_errors = [error for error in errors if not GENERIC_ERROR_PATTERN.search(error)]
59
+ error = (specific_errors or errors or [""])[0]
60
+ return error
61
+
62
+
63
+ def classify_github_actions_failure(failure: GitHubActionsFailure) -> GitHubActionsFailureCategory:
64
+ if not failure.has_jobs:
65
+ category = GitHubActionsFailureCategory.INVALID_WORKFLOW
66
+ else:
67
+ excerpt = log_excerpt(failure.log)
68
+ category = next((rule_category for rule_category, pattern in COMPILED_RULES if pattern.search(excerpt)),
69
+ GitHubActionsFailureCategory.UNKNOWN)
70
+ return category
@@ -0,0 +1,13 @@
1
+ from dataclasses import dataclass
2
+
3
+
4
+ @dataclass(frozen=True)
5
+ class GitHubActionsFailure:
6
+ """A failed GitHub Actions job, as the GitHub webhook / REST-API reports it"""
7
+ repo_name: str
8
+ workflow_name: str
9
+ job_name: str | None = None
10
+ failed_step_name: str | None = None
11
+ runner_name: str | None = None
12
+ log: str = ""
13
+ has_jobs: bool = True
@@ -0,0 +1,23 @@
1
+ from enum import Enum
2
+
3
+
4
+ class GitHubActionsFailureCategory(Enum):
5
+ """Why a GitHub Actions job failed - decides what to do with it (rerun, notify, open a Jira Work Item)"""
6
+ INVALID_WORKFLOW = "invalid_workflow"
7
+ RUNNER = "runner"
8
+ CONFIG_SECRET = "config_secret"
9
+ PROCESS_GATE = "process_gate"
10
+ SECURITY_DEPENDENCY = "security_dependency"
11
+ TEST_FAILURE = "test_failure"
12
+ LINT_TYPE = "lint_type"
13
+ PUBLISH_DEPLOY = "publish_deploy"
14
+ UNKNOWN = "unknown"
15
+
16
+
17
+ # A rerun of the same commit (maybe on another runner) can pass only when the code is not the cause.
18
+ RERUN_SAFE_CATEGORIES = frozenset({GitHubActionsFailureCategory.RUNNER})
19
+
20
+
21
+ def is_rerun_safe(category: GitHubActionsFailureCategory) -> bool:
22
+ rerun_safe = category in RERUN_SAFE_CATEGORIES
23
+ return rerun_safe
@@ -1,3 +1,4 @@
1
+ from http import HTTPStatus
1
2
  import re
2
3
  from datetime import datetime, timezone
3
4
  from typing import Any, Optional
@@ -79,7 +80,7 @@ class GitHubLocal:
79
80
  timeout=GITHUB_REQUEST_TIMEOUT_SECONDS)
80
81
  except requests.RequestException as exception:
81
82
  raise GitHubLocalException(f"GitHub is unreachable: {exception}")
82
- if response.status_code == 404 and is_not_found_empty:
83
+ if response.status_code == HTTPStatus.NOT_FOUND and is_not_found_empty:
83
84
  response_json: Any = []
84
85
  elif not response.ok:
85
86
  raise GitHubLocalException(f"GitHub GET {path} failed with {response.status_code}: {response.text[:200]}")
@@ -0,0 +1,69 @@
1
+ import pytest
2
+
3
+ from github_local.src.classify_github_actions_failure import classify_github_actions_failure, log_excerpt, main_error
4
+ from github_local.src.github_actions_failure import GitHubActionsFailure
5
+ from github_local.src.github_actions_failure_category import GitHubActionsFailureCategory as Category, is_rerun_safe
6
+
7
+
8
+ def failure_with_log(log: str) -> GitHubActionsFailure:
9
+ failure = GitHubActionsFailure(repo_name="sdk-local", workflow_name="Build Publish SDK-Local", log=log)
10
+ return failure
11
+
12
+
13
+ # The error lines are real ones from failed runs in circles-zone.
14
+ @pytest.mark.parametrize("error_line, expected_category", [
15
+ ("/home/runner/_work/_temp/1.sh: line 3: jq: command not found", Category.RUNNER),
16
+ ("System.IO.IOException: No space left on device", Category.RUNNER),
17
+ ("API_TOKEN_GITHUB and SSH_DEPLOY_KEY are empty. Please fill one", Category.CONFIG_SECRET),
18
+ ("fatal: repository 'https://github.com/circles-zone/url-remote-python-package/' not found", Category.CONFIG_SECRET),
19
+ ("Files were changed since Makec ran: files other than makec_report.json5 are different", Category.PROCESS_GATE),
20
+ ("Input required and not supplied: token", Category.CONFIG_SECRET),
21
+ ("Makec report is missing: makec_report.json5 was not found in the repo", Category.PROCESS_GATE),
22
+ ("Jira Work Item BU-4163 is already Done - it must be an OPEN Work Item.", Category.PROCESS_GATE),
23
+ ("Dependabot alert #12 [critical] PyJWT (requirements-freeze.txt)", Category.SECURITY_DEPENDENCY),
24
+ ("[-] returns every TODO, not only the first few 12ms", Category.TEST_FAILURE),
25
+ ("FAILED tests/unit/parser_unit_test.py::test_parse - AssertionError", Category.TEST_FAILURE),
26
+ ("PSAvoidUsingConvertToSecureStringWithPlainText: File 'OurModule.Tests.ps1'", Category.LINT_TYPE),
27
+ ("Serverless deploy failed", Category.PUBLISH_DEPLOY),
28
+ ("Something nobody has seen before", Category.UNKNOWN),
29
+ ])
30
+ def test_classify_by_the_error_line(error_line: str, expected_category: Category):
31
+ log = f"2026-10-24T10:00:00.0000000Z Run the step\n##[error]{error_line}\n##[error]Process completed with exit code 1."
32
+
33
+ assert classify_github_actions_failure(failure_with_log(log)) == expected_category
34
+
35
+
36
+ def test_a_run_without_jobs_is_an_invalid_workflow_file():
37
+ failure = GitHubActionsFailure(repo_name="github-workflows", workflow_name=".github/workflows/smoke-tests.yml",
38
+ has_jobs=False)
39
+
40
+ assert classify_github_actions_failure(failure) == Category.INVALID_WORKFLOW
41
+
42
+
43
+ def test_the_cleanup_at_the_end_of_the_log_is_ignored():
44
+ log = "\n".join(["##[error]Serverless deploy failed"] + ["Cleaning up orphan processes"] * 100
45
+ + ["fatal: repository 'x' not found"])
46
+
47
+ assert classify_github_actions_failure(failure_with_log(log)) == Category.PUBLISH_DEPLOY
48
+
49
+
50
+ def test_log_excerpt_without_an_error_is_the_end_of_the_log():
51
+ log = "\n".join(f"line {number}" for number in range(100))
52
+
53
+ assert log_excerpt(log).splitlines() == [f"line {number}" for number in range(60, 100)]
54
+
55
+
56
+ def test_main_error_skips_the_generic_exit_code_line():
57
+ log = "##[error]Process completed with exit code 1.\n##[error]Serverless deploy failed"
58
+
59
+ assert main_error(log) == "Serverless deploy failed"
60
+
61
+
62
+ def test_main_error_falls_back_to_the_generic_line_and_to_empty():
63
+ assert main_error("##[error]Process completed with exit code 1.") == "Process completed with exit code 1."
64
+ assert main_error("no error marker") == ""
65
+
66
+
67
+ def test_only_runner_failures_are_rerun_safe():
68
+ assert is_rerun_safe(Category.RUNNER)
69
+ assert not is_rerun_safe(Category.TEST_FAILURE)
@@ -1,3 +1,4 @@
1
+ from http import HTTPStatus
1
2
  import json as json_module
2
3
  from dataclasses import dataclass
3
4
  from typing import Any, Optional, Union
@@ -22,7 +23,7 @@ class FakeHttp:
22
23
  self.calls: list[FakeHttpCall] = []
23
24
  self.responses_by_request: dict[tuple[str, str], list[Union[requests.Response, Exception]]] = {}
24
25
 
25
- def add(self, method: str, url: str, status: int = 200, json: Any = None,
26
+ def add(self, method: str, url: str, status: int = HTTPStatus.OK, json: Any = None,
26
27
  body: Union[str, Exception] = "") -> None:
27
28
  if isinstance(body, Exception):
28
29
  response: Union[requests.Response, Exception] = body
@@ -1,3 +1,4 @@
1
+ from http import HTTPStatus
1
2
  from datetime import datetime, timedelta, timezone
2
3
 
3
4
  import pytest
@@ -58,9 +59,9 @@ def test_get_keys_merged_to_dev_without_keys_calls_github_zero_times(fake_http,
58
59
 
59
60
 
60
61
  def test_search_not_found_is_an_error_and_not_an_empty_list(fake_http, github_local):
61
- fake_http.get(f"{API}/search/issues", status=404, json={"message": "Not Found"})
62
+ fake_http.get(f"{API}/search/issues", status=HTTPStatus.NOT_FOUND, json={"message": "Not Found"})
62
63
 
63
- with pytest.raises(GitHubLocalException, match="404"):
64
+ with pytest.raises(GitHubLocalException, match=str(HTTPStatus.NOT_FOUND.value)):
64
65
  github_local.get_keys_merged_to_dev(["BU-1"])
65
66
 
66
67
 
@@ -68,7 +69,7 @@ def test_get_branches_of_exactly_this_key_with_their_age(fake_http, github_local
68
69
  fake_http.get(f"{REFS}/feature/BU-1", json=[
69
70
  {"ref": "refs/heads/feature/BU-1-a-Yuval-Rotich", "object": {"sha": "abc"}},
70
71
  {"ref": "refs/heads/feature/BU-10-other", "object": {"sha": "def"}}])
71
- fake_http.get(f"{REFS}/BU-1", status=404)
72
+ fake_http.get(f"{REFS}/BU-1", status=HTTPStatus.NOT_FOUND)
72
73
  fake_http.get(f"{API}/repos/circles-zone/{REPO}/commits/abc",
73
74
  json={"commit": {"committer": {"date": commit_time(timedelta(days=3))}}})
74
75
 
@@ -86,7 +87,7 @@ def test_is_branch_of_key(branch_name, is_of_key):
86
87
  assert is_branch_of_key(branch_name, "BU-1") == is_of_key
87
88
 
88
89
 
89
- @pytest.mark.parametrize("status", [403, 429, 500])
90
+ @pytest.mark.parametrize("status", [HTTPStatus.FORBIDDEN, HTTPStatus.TOO_MANY_REQUESTS, HTTPStatus.INTERNAL_SERVER_ERROR])
90
91
  def test_github_errors_raise(fake_http, github_local, status):
91
92
  fake_http.get(f"{API}/search/issues", status=status, body="error")
92
93
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github-local
3
- Version: 0.0.3b3827.post12
3
+ Version: 0.0.3b4575
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
@@ -13,9 +13,13 @@ python_packages/github_local.egg-info/dependency_links.txt
13
13
  python_packages/github_local.egg-info/requires.txt
14
14
  python_packages/github_local.egg-info/top_level.txt
15
15
  python_packages/github_local/src/__init__.py
16
+ python_packages/github_local/src/classify_github_actions_failure.py
16
17
  python_packages/github_local/src/constants_github_local.py
18
+ python_packages/github_local/src/github_actions_failure.py
19
+ python_packages/github_local/src/github_actions_failure_category.py
17
20
  python_packages/github_local/src/github_local.py
18
21
  python_packages/github_local/tests/__init__.py
19
22
  python_packages/github_local/tests/unit/__init__.py
23
+ python_packages/github_local/tests/unit/classify_github_actions_failure_unit_test.py
20
24
  python_packages/github_local/tests/unit/conftest.py
21
25
  python_packages/github_local/tests/unit/github_local_unit_test.py
@@ -32,7 +32,7 @@ setuptools.setup(
32
32
  # Updating the b{{JiraWorkItemId}} done automatically by makec.ps1 / make_py.ps1
33
33
  # According to GitFlow, in the Feature Branch, after the letter 'b' comes the
34
34
  # Jira Work Item ID, i.e., 0.0.1b2432. Semantic Version simver.org
35
- version='0.0.3b3827.post12', # https://pypi.org/project/github-local#History
35
+ version='0.0.3b4575', # https://pypi.org/project/github-local#History
36
36
  author="Circles",
37
37
  author_email="info@circlez.ai",
38
38
  description=f"{PACKAGE_DESCRIPTION}",