github2gerrit 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl
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.
- github2gerrit/cli.py +161 -27
- github2gerrit/config.py +214 -2
- github2gerrit/core.py +274 -38
- github2gerrit/duplicate_detection.py +1 -1
- github2gerrit/github_api.py +11 -3
- github2gerrit/gitutils.py +22 -4
- github2gerrit/ssh_discovery.py +412 -0
- {github2gerrit-0.1.3.dist-info → github2gerrit-0.1.5.dist-info}/METADATA +104 -14
- github2gerrit-0.1.5.dist-info/RECORD +15 -0
- {github2gerrit-0.1.3.dist-info → github2gerrit-0.1.5.dist-info}/WHEEL +2 -1
- {github2gerrit-0.1.3.dist-info → github2gerrit-0.1.5.dist-info}/entry_points.txt +0 -3
- github2gerrit-0.1.5.dist-info/licenses/LICENSE +201 -0
- github2gerrit-0.1.5.dist-info/top_level.txt +1 -0
- github2gerrit-0.1.3.dist-info/RECORD +0 -12
github2gerrit/core.py
CHANGED
@@ -38,14 +38,6 @@ from dataclasses import dataclass
|
|
38
38
|
from pathlib import Path
|
39
39
|
from typing import Any
|
40
40
|
|
41
|
-
|
42
|
-
try:
|
43
|
-
from pygerrit2 import GerritRestAPI
|
44
|
-
from pygerrit2 import HTTPBasicAuth
|
45
|
-
except ImportError:
|
46
|
-
GerritRestAPI = None
|
47
|
-
HTTPBasicAuth = None
|
48
|
-
|
49
41
|
from .github_api import build_client
|
50
42
|
from .github_api import close_pr
|
51
43
|
from .github_api import create_pr_comment
|
@@ -67,9 +59,52 @@ from .models import GitHubContext
|
|
67
59
|
from .models import Inputs
|
68
60
|
|
69
61
|
|
62
|
+
try:
|
63
|
+
from pygerrit2 import GerritRestAPI
|
64
|
+
from pygerrit2 import HTTPBasicAuth
|
65
|
+
except ImportError:
|
66
|
+
GerritRestAPI = None
|
67
|
+
HTTPBasicAuth = None
|
68
|
+
|
69
|
+
try:
|
70
|
+
from .ssh_discovery import SSHDiscoveryError
|
71
|
+
from .ssh_discovery import auto_discover_gerrit_host_keys
|
72
|
+
except ImportError:
|
73
|
+
# Fallback if ssh_discovery module is not available
|
74
|
+
auto_discover_gerrit_host_keys = None # type: ignore[assignment]
|
75
|
+
SSHDiscoveryError = Exception # type: ignore[misc,assignment]
|
76
|
+
|
77
|
+
|
78
|
+
def _is_verbose_mode() -> bool:
|
79
|
+
"""Check if verbose mode is enabled via environment variable."""
|
80
|
+
return os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes")
|
81
|
+
|
82
|
+
|
83
|
+
def _log_exception_conditionally(
|
84
|
+
logger: logging.Logger, message: str, *args: Any
|
85
|
+
) -> None:
|
86
|
+
"""Log exception with traceback only if verbose mode is enabled."""
|
87
|
+
if _is_verbose_mode():
|
88
|
+
logger.exception(message, *args)
|
89
|
+
else:
|
90
|
+
logger.error(message, *args)
|
91
|
+
|
92
|
+
|
70
93
|
log = logging.getLogger("github2gerrit.core")
|
71
94
|
|
72
95
|
|
96
|
+
# Error message constants to comply with TRY003
|
97
|
+
_MSG_ISSUE_ID_MULTILINE = "Issue ID must be single line"
|
98
|
+
_MSG_MISSING_PR_CONTEXT = "missing PR context"
|
99
|
+
_MSG_BAD_REPOSITORY_CONTEXT = "bad repository context"
|
100
|
+
_MSG_MISSING_GERRIT_SERVER = "missing GERRIT_SERVER"
|
101
|
+
_MSG_MISSING_GERRIT_PROJECT = "missing GERRIT_PROJECT"
|
102
|
+
_MSG_PYGERRIT2_REQUIRED_REST = "pygerrit2 is required to query Gerrit REST API"
|
103
|
+
_MSG_PYGERRIT2_REQUIRED_AUTH = "pygerrit2 is required for HTTP authentication"
|
104
|
+
_MSG_PYGERRIT2_MISSING = "pygerrit2 missing"
|
105
|
+
_MSG_PYGERRIT2_AUTH_MISSING = "pygerrit2 auth missing"
|
106
|
+
|
107
|
+
|
73
108
|
def _insert_issue_id_into_commit_message(message: str, issue_id: str) -> str:
|
74
109
|
"""
|
75
110
|
Insert Issue ID into commit message after the first line.
|
@@ -87,10 +122,13 @@ def _insert_issue_id_into_commit_message(message: str, issue_id: str) -> str:
|
|
87
122
|
# Validate that Issue ID is a single line string
|
88
123
|
cleaned_issue_id = issue_id.strip()
|
89
124
|
if "\n" in cleaned_issue_id or "\r" in cleaned_issue_id:
|
90
|
-
raise ValueError(
|
125
|
+
raise ValueError(_MSG_ISSUE_ID_MULTILINE)
|
91
126
|
|
92
|
-
#
|
93
|
-
|
127
|
+
# Format as proper Issue-ID trailer
|
128
|
+
if cleaned_issue_id.startswith("Issue-ID:"):
|
129
|
+
issue_line = cleaned_issue_id
|
130
|
+
else:
|
131
|
+
issue_line = f"Issue-ID: {cleaned_issue_id}"
|
94
132
|
|
95
133
|
lines = message.splitlines()
|
96
134
|
if not lines:
|
@@ -232,7 +270,7 @@ class Orchestrator:
|
|
232
270
|
return SubmissionResult(
|
233
271
|
change_urls=[], change_numbers=[], commit_shas=[]
|
234
272
|
)
|
235
|
-
self._setup_ssh(inputs)
|
273
|
+
self._setup_ssh(inputs, gerrit)
|
236
274
|
|
237
275
|
if inputs.submit_single_commits:
|
238
276
|
prep = self._prepare_single_commits(inputs, gh, gerrit)
|
@@ -278,7 +316,7 @@ class Orchestrator:
|
|
278
316
|
|
279
317
|
def _guard_pull_request_context(self, gh: GitHubContext) -> None:
|
280
318
|
if gh.pr_number is None:
|
281
|
-
raise OrchestratorError(
|
319
|
+
raise OrchestratorError(_MSG_MISSING_PR_CONTEXT)
|
282
320
|
log.debug("PR context OK: #%s", gh.pr_number)
|
283
321
|
|
284
322
|
def _parse_gitreview_text(self, text: str) -> GerritInfo | None:
|
@@ -443,7 +481,7 @@ class Orchestrator:
|
|
443
481
|
# Fallback: use the repository name portion only.
|
444
482
|
repo_full = gh.repository
|
445
483
|
if not repo_full or "/" not in repo_full:
|
446
|
-
raise OrchestratorError(
|
484
|
+
raise OrchestratorError(_MSG_BAD_REPOSITORY_CONTEXT)
|
447
485
|
owner, name = repo_full.split("/", 1)
|
448
486
|
# Fallback: map all '-' to '/' for Gerrit path (e.g., 'my/repo/name')
|
449
487
|
gerrit_name = name.replace("-", "/")
|
@@ -463,7 +501,7 @@ class Orchestrator:
|
|
463
501
|
|
464
502
|
host = inputs.gerrit_server.strip()
|
465
503
|
if not host:
|
466
|
-
raise OrchestratorError(
|
504
|
+
raise OrchestratorError(_MSG_MISSING_GERRIT_SERVER)
|
467
505
|
port_s = inputs.gerrit_server_port.strip() or "29418"
|
468
506
|
try:
|
469
507
|
port = int(port_s)
|
@@ -483,13 +521,13 @@ class Orchestrator:
|
|
483
521
|
project,
|
484
522
|
)
|
485
523
|
else:
|
486
|
-
raise OrchestratorError(
|
524
|
+
raise OrchestratorError(_MSG_MISSING_GERRIT_PROJECT)
|
487
525
|
|
488
526
|
info = GerritInfo(host=host, port=port, project=project)
|
489
527
|
log.debug("Resolved Gerrit info: %s", info)
|
490
528
|
return info
|
491
529
|
|
492
|
-
def _setup_ssh(self, inputs: Inputs) -> None:
|
530
|
+
def _setup_ssh(self, inputs: Inputs, gerrit: GerritInfo) -> None:
|
493
531
|
"""Set up temporary SSH configuration for Gerrit access.
|
494
532
|
|
495
533
|
This method creates tool-specific SSH files in the workspace without
|
@@ -503,8 +541,46 @@ class Orchestrator:
|
|
503
541
|
|
504
542
|
Does not modify user files.
|
505
543
|
"""
|
506
|
-
if not inputs.gerrit_ssh_privkey_g2g
|
507
|
-
log.debug("SSH key
|
544
|
+
if not inputs.gerrit_ssh_privkey_g2g:
|
545
|
+
log.debug("SSH private key not provided, skipping SSH setup")
|
546
|
+
return
|
547
|
+
|
548
|
+
# Auto-discover host keys if not provided
|
549
|
+
effective_known_hosts = inputs.gerrit_known_hosts
|
550
|
+
if (
|
551
|
+
not effective_known_hosts
|
552
|
+
and auto_discover_gerrit_host_keys is not None
|
553
|
+
):
|
554
|
+
log.info(
|
555
|
+
"GERRIT_KNOWN_HOSTS not provided, attempting auto-discovery..."
|
556
|
+
)
|
557
|
+
try:
|
558
|
+
discovered_keys = auto_discover_gerrit_host_keys(
|
559
|
+
gerrit_hostname=gerrit.host,
|
560
|
+
gerrit_port=gerrit.port,
|
561
|
+
organization=inputs.organization,
|
562
|
+
save_to_config=True,
|
563
|
+
)
|
564
|
+
if discovered_keys:
|
565
|
+
effective_known_hosts = discovered_keys
|
566
|
+
log.info(
|
567
|
+
"Successfully auto-discovered SSH host keys for %s:%d",
|
568
|
+
gerrit.host,
|
569
|
+
gerrit.port,
|
570
|
+
)
|
571
|
+
else:
|
572
|
+
log.warning(
|
573
|
+
"Auto-discovery failed, SSH host key verification may "
|
574
|
+
"fail"
|
575
|
+
)
|
576
|
+
except Exception as exc:
|
577
|
+
log.warning("SSH host key auto-discovery failed: %s", exc)
|
578
|
+
|
579
|
+
if not effective_known_hosts:
|
580
|
+
log.debug(
|
581
|
+
"No SSH host keys available (manual or auto-discovered), "
|
582
|
+
"skipping SSH setup"
|
583
|
+
)
|
508
584
|
return
|
509
585
|
|
510
586
|
log.info("Setting up temporary SSH configuration for Gerrit")
|
@@ -526,7 +602,7 @@ class Orchestrator:
|
|
526
602
|
# Write known hosts to tool-specific location
|
527
603
|
known_hosts_path = tool_ssh_dir / "known_hosts"
|
528
604
|
with open(known_hosts_path, "w", encoding="utf-8") as f:
|
529
|
-
f.write(
|
605
|
+
f.write(effective_known_hosts.strip() + "\n")
|
530
606
|
known_hosts_path.chmod(0o644)
|
531
607
|
log.debug("Known hosts written to %s", known_hosts_path)
|
532
608
|
log.debug("Using isolated known_hosts to prevent user conflicts")
|
@@ -1081,6 +1157,7 @@ class Orchestrator:
|
|
1081
1157
|
repo.project_gerrit,
|
1082
1158
|
branch,
|
1083
1159
|
)
|
1160
|
+
log.debug("Starting git review push operation...")
|
1084
1161
|
if single_commits:
|
1085
1162
|
tmp_branch = os.getenv("G2G_TMP_BRANCH", "tmp_branch")
|
1086
1163
|
run_cmd(["git", "checkout", tmp_branch], cwd=self.workspace)
|
@@ -1113,9 +1190,19 @@ class Orchestrator:
|
|
1113
1190
|
if self._git_ssh_command
|
1114
1191
|
else None
|
1115
1192
|
)
|
1193
|
+
log.debug("Executing git review command: %s", " ".join(args))
|
1116
1194
|
run_cmd(args, cwd=self.workspace, env=env)
|
1195
|
+
log.info("Successfully pushed changes to Gerrit")
|
1117
1196
|
except CommandError as exc:
|
1118
|
-
|
1197
|
+
# Analyze the specific failure reason from git review output
|
1198
|
+
error_details = self._analyze_gerrit_push_failure(exc)
|
1199
|
+
_log_exception_conditionally(
|
1200
|
+
log, "Gerrit push failed: %s", error_details
|
1201
|
+
)
|
1202
|
+
msg = (
|
1203
|
+
f"Failed to push changes to Gerrit with git-review: "
|
1204
|
+
f"{error_details}"
|
1205
|
+
)
|
1119
1206
|
raise OrchestratorError(msg) from exc
|
1120
1207
|
# Cleanup temporary branch used during preparation
|
1121
1208
|
tmp_branch = (os.getenv("G2G_TMP_BRANCH", "") or "").strip()
|
@@ -1132,6 +1219,108 @@ class Orchestrator:
|
|
1132
1219
|
cwd=self.workspace,
|
1133
1220
|
)
|
1134
1221
|
|
1222
|
+
def _analyze_gerrit_push_failure(self, exc: CommandError) -> str:
|
1223
|
+
"""Analyze git review failure and provide helpful error message."""
|
1224
|
+
stdout = exc.stdout or ""
|
1225
|
+
stderr = exc.stderr or ""
|
1226
|
+
combined_output = f"{stdout}\n{stderr}"
|
1227
|
+
combined_lower = combined_output.lower()
|
1228
|
+
|
1229
|
+
# Check for SSH host key verification failures first
|
1230
|
+
if (
|
1231
|
+
"host key verification failed" in combined_lower
|
1232
|
+
or "no ed25519 host key is known" in combined_lower
|
1233
|
+
or "no rsa host key is known" in combined_lower
|
1234
|
+
or "no ecdsa host key is known" in combined_lower
|
1235
|
+
):
|
1236
|
+
return (
|
1237
|
+
"SSH host key verification failed. The GERRIT_KNOWN_HOSTS "
|
1238
|
+
"value is missing or contains an outdated host key for the "
|
1239
|
+
"Gerrit server. The tool will attempt to auto-discover "
|
1240
|
+
"host keys "
|
1241
|
+
"on the next run, or you can manually run "
|
1242
|
+
"'ssh-keyscan -p 29418 <gerrit-host>' "
|
1243
|
+
"to get the current host keys."
|
1244
|
+
)
|
1245
|
+
elif (
|
1246
|
+
"authenticity of host" in combined_lower
|
1247
|
+
and "can't be established" in combined_lower
|
1248
|
+
):
|
1249
|
+
return (
|
1250
|
+
"SSH host key unknown. The GERRIT_KNOWN_HOSTS value does not "
|
1251
|
+
"contain the host key for the Gerrit server. "
|
1252
|
+
"The tool will attempt "
|
1253
|
+
"to auto-discover host keys on the next run, or you can "
|
1254
|
+
"manually run "
|
1255
|
+
"'ssh-keyscan -p 29418 <gerrit-host>' to get the host keys."
|
1256
|
+
)
|
1257
|
+
# Check for specific SSH key issues before general permission denied
|
1258
|
+
elif (
|
1259
|
+
"key_load_public" in combined_lower
|
1260
|
+
and "invalid format" in combined_lower
|
1261
|
+
):
|
1262
|
+
return (
|
1263
|
+
"SSH key format is invalid. Check that the SSH private key "
|
1264
|
+
"is properly formatted."
|
1265
|
+
)
|
1266
|
+
elif "no matching host key type found" in combined_lower:
|
1267
|
+
return (
|
1268
|
+
"SSH key type not supported by server. The server may not "
|
1269
|
+
"accept this SSH key algorithm."
|
1270
|
+
)
|
1271
|
+
elif "authentication failed" in combined_lower:
|
1272
|
+
return (
|
1273
|
+
"SSH authentication failed - check SSH key, username, and "
|
1274
|
+
"server configuration"
|
1275
|
+
)
|
1276
|
+
# Check for connection timeout/refused before "could not read" check
|
1277
|
+
elif (
|
1278
|
+
"connection timed out" in combined_lower
|
1279
|
+
or "connection refused" in combined_lower
|
1280
|
+
):
|
1281
|
+
return (
|
1282
|
+
"Connection failed - check network connectivity and "
|
1283
|
+
"Gerrit server availability"
|
1284
|
+
)
|
1285
|
+
# Check for specific SSH publickey-only authentication failures
|
1286
|
+
elif "permission denied (publickey)" in combined_lower and not any(
|
1287
|
+
auth_method in combined_lower
|
1288
|
+
for auth_method in ["gssapi", "password", "keyboard"]
|
1289
|
+
):
|
1290
|
+
return (
|
1291
|
+
"SSH public key authentication failed. The SSH key may be "
|
1292
|
+
"invalid, not authorized for this user, or the wrong key type."
|
1293
|
+
)
|
1294
|
+
# Check for general SSH permission issues
|
1295
|
+
elif "permission denied" in combined_lower:
|
1296
|
+
return "SSH permission denied - check SSH key and user permissions"
|
1297
|
+
elif "could not read from remote repository" in combined_lower:
|
1298
|
+
return (
|
1299
|
+
"Could not read from remote repository - check SSH "
|
1300
|
+
"authentication and repository access permissions"
|
1301
|
+
)
|
1302
|
+
# Check for Gerrit-specific issues
|
1303
|
+
elif "missing issue-id" in combined_lower:
|
1304
|
+
return "Missing Issue-ID in commit message."
|
1305
|
+
elif "commit not associated to any issue" in combined_lower:
|
1306
|
+
return "Commit not associated to any issue."
|
1307
|
+
elif (
|
1308
|
+
"remote rejected" in combined_lower
|
1309
|
+
and "refs/for/" in combined_lower
|
1310
|
+
):
|
1311
|
+
# Extract specific rejection reason from output
|
1312
|
+
lines = combined_output.split("\n")
|
1313
|
+
for line in lines:
|
1314
|
+
if "! [remote rejected]" in line:
|
1315
|
+
# Extract the reason in parentheses
|
1316
|
+
if "(" in line and ")" in line:
|
1317
|
+
reason = line[line.find("(") + 1 : line.find(")")]
|
1318
|
+
return f"Gerrit rejected the push: {reason}"
|
1319
|
+
return f"Gerrit rejected the push: {line.strip()}"
|
1320
|
+
return "Gerrit rejected the push for an unknown reason"
|
1321
|
+
else:
|
1322
|
+
return f"Unknown error: {exc}"
|
1323
|
+
|
1135
1324
|
def _query_gerrit_for_results(
|
1136
1325
|
self,
|
1137
1326
|
*,
|
@@ -1154,14 +1343,10 @@ class Orchestrator:
|
|
1154
1343
|
)
|
1155
1344
|
http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
|
1156
1345
|
if GerritRestAPI is None:
|
1157
|
-
raise OrchestratorError(
|
1158
|
-
"pygerrit2 is required to query Gerrit REST API"
|
1159
|
-
)
|
1346
|
+
raise OrchestratorError(_MSG_PYGERRIT2_REQUIRED_REST)
|
1160
1347
|
if http_user and http_pass:
|
1161
1348
|
if HTTPBasicAuth is None:
|
1162
|
-
raise OrchestratorError(
|
1163
|
-
"pygerrit2 is required for HTTP authentication"
|
1164
|
-
)
|
1349
|
+
raise OrchestratorError(_MSG_PYGERRIT2_REQUIRED_AUTH)
|
1165
1350
|
rest = GerritRestAPI(
|
1166
1351
|
url=base_url, auth=HTTPBasicAuth(http_user, http_pass)
|
1167
1352
|
)
|
@@ -1352,6 +1537,19 @@ class Orchestrator:
|
|
1352
1537
|
if not commit_shas:
|
1353
1538
|
log.warning("No commit shas to comment on in Gerrit")
|
1354
1539
|
return
|
1540
|
+
|
1541
|
+
# Check if back-reference comments are disabled
|
1542
|
+
if os.getenv("G2G_SKIP_GERRIT_COMMENTS", "").lower() in (
|
1543
|
+
"true",
|
1544
|
+
"1",
|
1545
|
+
"yes",
|
1546
|
+
):
|
1547
|
+
log.info(
|
1548
|
+
"Skipping back-reference comments "
|
1549
|
+
"(G2G_SKIP_GERRIT_COMMENTS=true)"
|
1550
|
+
)
|
1551
|
+
return
|
1552
|
+
|
1355
1553
|
log.info("Adding back-reference comment in Gerrit")
|
1356
1554
|
user = os.getenv("GERRIT_SSH_USER_G2G", "")
|
1357
1555
|
server = gerrit.host
|
@@ -1368,12 +1566,24 @@ class Orchestrator:
|
|
1368
1566
|
continue
|
1369
1567
|
try:
|
1370
1568
|
log.debug("Executing SSH command for commit %s", csha)
|
1371
|
-
|
1569
|
+
# Build SSH command with our configured SSH options
|
1570
|
+
ssh_cmd = ["ssh", "-n", "-p", str(gerrit.port)]
|
1571
|
+
|
1572
|
+
# Add our SSH options if we have custom SSH config
|
1573
|
+
if self._git_ssh_command:
|
1574
|
+
# Extract SSH options from GIT_SSH_COMMAND
|
1575
|
+
# Format: "ssh -i /path/to/key -o Option=value ..."
|
1576
|
+
git_ssh_parts = self._git_ssh_command.split()
|
1577
|
+
if len(git_ssh_parts) > 1: # Skip the "ssh" part
|
1578
|
+
ssh_options = git_ssh_parts[1:]
|
1579
|
+
log.debug("Adding SSH options: %s", ssh_options)
|
1580
|
+
ssh_cmd.extend(ssh_options)
|
1581
|
+
else:
|
1582
|
+
log.debug("No custom SSH config, using default SSH options")
|
1583
|
+
|
1584
|
+
# Add the target and gerrit command
|
1585
|
+
ssh_cmd.extend(
|
1372
1586
|
[
|
1373
|
-
"ssh",
|
1374
|
-
"-n",
|
1375
|
-
"-p",
|
1376
|
-
str(gerrit.port),
|
1377
1587
|
f"{user}@{server}",
|
1378
1588
|
"gerrit",
|
1379
1589
|
"review",
|
@@ -1386,14 +1596,40 @@ class Orchestrator:
|
|
1386
1596
|
csha,
|
1387
1597
|
]
|
1388
1598
|
)
|
1599
|
+
|
1600
|
+
log.debug("Final SSH command: %s", " ".join(ssh_cmd))
|
1601
|
+
run_cmd(ssh_cmd, cwd=self.workspace)
|
1389
1602
|
log.info(
|
1390
1603
|
"Successfully added back-reference comment for %s: %s",
|
1391
1604
|
csha,
|
1392
1605
|
message,
|
1393
1606
|
)
|
1394
|
-
except
|
1395
|
-
log.
|
1396
|
-
"Failed to add back-reference comment for %s"
|
1607
|
+
except CommandError as exc:
|
1608
|
+
log.warning(
|
1609
|
+
"Failed to add back-reference comment for %s "
|
1610
|
+
"(non-fatal): %s",
|
1611
|
+
csha,
|
1612
|
+
exc,
|
1613
|
+
)
|
1614
|
+
if exc.stderr:
|
1615
|
+
log.debug("SSH stderr: %s", exc.stderr)
|
1616
|
+
if exc.stdout:
|
1617
|
+
log.debug("SSH stdout: %s", exc.stdout)
|
1618
|
+
log.info(
|
1619
|
+
"Back-reference comment failed but change was successfully "
|
1620
|
+
"submitted. You can set G2G_SKIP_GERRIT_COMMENTS=true to "
|
1621
|
+
"disable these comments."
|
1622
|
+
)
|
1623
|
+
# Continue processing - this is not a fatal error
|
1624
|
+
except Exception as exc:
|
1625
|
+
log.warning(
|
1626
|
+
"Failed to add back-reference comment for %s "
|
1627
|
+
"(non-fatal): %s",
|
1628
|
+
csha,
|
1629
|
+
exc,
|
1630
|
+
)
|
1631
|
+
log.debug(
|
1632
|
+
"Back-reference comment failure details:", exc_info=True
|
1397
1633
|
)
|
1398
1634
|
# Continue processing - this is not a fatal error
|
1399
1635
|
|
@@ -1591,15 +1827,15 @@ class Orchestrator:
|
|
1591
1827
|
def _build_client(url: str) -> Any:
|
1592
1828
|
if http_user and http_pass:
|
1593
1829
|
if GerritRestAPI is None:
|
1594
|
-
raise OrchestratorError(
|
1830
|
+
raise OrchestratorError(_MSG_PYGERRIT2_MISSING)
|
1595
1831
|
if HTTPBasicAuth is None:
|
1596
|
-
raise OrchestratorError(
|
1832
|
+
raise OrchestratorError(_MSG_PYGERRIT2_AUTH_MISSING)
|
1597
1833
|
return GerritRestAPI(
|
1598
1834
|
url=url, auth=HTTPBasicAuth(http_user, http_pass)
|
1599
1835
|
)
|
1600
1836
|
else:
|
1601
1837
|
if GerritRestAPI is None:
|
1602
|
-
raise OrchestratorError(
|
1838
|
+
raise OrchestratorError(_MSG_PYGERRIT2_MISSING)
|
1603
1839
|
return GerritRestAPI(url=url)
|
1604
1840
|
|
1605
1841
|
def _probe(url: str) -> None:
|
github2gerrit/github_api.py
CHANGED
@@ -30,6 +30,12 @@ from typing import TypeVar
|
|
30
30
|
from typing import cast
|
31
31
|
|
32
32
|
|
33
|
+
# Error message constants to comply with TRY003
|
34
|
+
_MSG_PYGITHUB_REQUIRED = "PyGithub required"
|
35
|
+
_MSG_MISSING_GITHUB_TOKEN = "missing GITHUB_TOKEN" # noqa: S105
|
36
|
+
_MSG_BAD_GITHUB_REPOSITORY = "bad GITHUB_REPOSITORY"
|
37
|
+
|
38
|
+
|
33
39
|
class GithubExceptionType(Exception):
|
34
40
|
pass
|
35
41
|
|
@@ -65,7 +71,7 @@ else:
|
|
65
71
|
|
66
72
|
class Github: # type: ignore[no-redef]
|
67
73
|
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
68
|
-
raise RuntimeError(
|
74
|
+
raise RuntimeError(_MSG_PYGITHUB_REQUIRED)
|
69
75
|
|
70
76
|
|
71
77
|
class GhIssueComment(Protocol):
|
@@ -123,6 +129,8 @@ def _getenv_str(name: str) -> str:
|
|
123
129
|
def _backoff_delay(attempt: int, base: float = 0.5, cap: float = 6.0) -> float:
|
124
130
|
# Exponential backoff with jitter; cap prevents unbounded waits.
|
125
131
|
delay: float = float(min(base * (2 ** max(0, attempt - 1)), cap))
|
132
|
+
# Using random.uniform for jitter is appropriate here - we only need
|
133
|
+
# pseudorandom distribution to avoid thundering herd, not crypto security
|
126
134
|
jitter: float = float(random.uniform(0.0, delay / 2.0)) # noqa: S311
|
127
135
|
return float(delay + jitter)
|
128
136
|
|
@@ -199,7 +207,7 @@ def build_client(token: str | None = None) -> GhClient:
|
|
199
207
|
"""
|
200
208
|
tok = token or _getenv_str("GITHUB_TOKEN")
|
201
209
|
if not tok:
|
202
|
-
raise ValueError(
|
210
|
+
raise ValueError(_MSG_MISSING_GITHUB_TOKEN)
|
203
211
|
# per_page improves pagination; adjust as needed.
|
204
212
|
base_url = _getenv_str("GITHUB_API_URL")
|
205
213
|
if not base_url:
|
@@ -240,7 +248,7 @@ def get_repo_from_env(client: GhClient) -> GhRepository:
|
|
240
248
|
"""Return the repository object based on GITHUB_REPOSITORY."""
|
241
249
|
full = _getenv_str("GITHUB_REPOSITORY")
|
242
250
|
if not full or "/" not in full:
|
243
|
-
raise ValueError(
|
251
|
+
raise ValueError(_MSG_BAD_GITHUB_REPOSITORY)
|
244
252
|
repo = client.get_repo(full)
|
245
253
|
return repo
|
246
254
|
|
github2gerrit/gitutils.py
CHANGED
@@ -20,6 +20,22 @@ from collections.abc import Mapping
|
|
20
20
|
from collections.abc import Sequence
|
21
21
|
from dataclasses import dataclass
|
22
22
|
from pathlib import Path
|
23
|
+
from typing import Any
|
24
|
+
|
25
|
+
|
26
|
+
def _is_verbose_mode() -> bool:
|
27
|
+
"""Check if verbose mode is enabled via environment variable."""
|
28
|
+
return os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes")
|
29
|
+
|
30
|
+
|
31
|
+
def _log_exception_conditionally(
|
32
|
+
logger: logging.Logger, message: str, *args: Any
|
33
|
+
) -> None:
|
34
|
+
"""Log exception with traceback only if verbose mode is enabled."""
|
35
|
+
if _is_verbose_mode():
|
36
|
+
logger.exception(message, *args)
|
37
|
+
else:
|
38
|
+
logger.error(message, *args)
|
23
39
|
|
24
40
|
|
25
41
|
__all__ = [
|
@@ -41,6 +57,9 @@ __all__ = [
|
|
41
57
|
"run_cmd_with_retries",
|
42
58
|
]
|
43
59
|
|
60
|
+
# Error message constants to comply with TRY003
|
61
|
+
_MSG_COMMIT_NO_MESSAGE = "Either message or message_file must be provided"
|
62
|
+
|
44
63
|
|
45
64
|
_LOGGER_NAME = "github2gerrit.git"
|
46
65
|
log = logging.getLogger(_LOGGER_NAME)
|
@@ -187,7 +206,7 @@ def run_cmd(
|
|
187
206
|
)
|
188
207
|
except subprocess.TimeoutExpired as exc:
|
189
208
|
msg = f"Command timed out: {cmd!r}"
|
190
|
-
log
|
209
|
+
_log_exception_conditionally(log, msg)
|
191
210
|
# TimeoutExpired carries 'output' and 'stderr' attributes,
|
192
211
|
# which may be bytes depending on invocation context.
|
193
212
|
out = getattr(exc, "output", None)
|
@@ -201,7 +220,7 @@ def run_cmd(
|
|
201
220
|
) from exc
|
202
221
|
except OSError as exc:
|
203
222
|
msg = f"Failed to execute command: {cmd!r} ({exc})"
|
204
|
-
log
|
223
|
+
_log_exception_conditionally(log, msg)
|
205
224
|
raise CommandError(msg, cmd=cmd) from exc
|
206
225
|
|
207
226
|
result = CommandResult(
|
@@ -258,7 +277,6 @@ def run_cmd_with_retries(
|
|
258
277
|
|
259
278
|
predicate = retry_on or _default_retry_on
|
260
279
|
attempt = 0
|
261
|
-
# removed unused variable 'last_res'
|
262
280
|
|
263
281
|
while True:
|
264
282
|
attempt += 1
|
@@ -472,7 +490,7 @@ def git_commit_new(
|
|
472
490
|
) -> None:
|
473
491
|
"""Create a new commit using message or message_file."""
|
474
492
|
if not message and not message_file:
|
475
|
-
raise ValueError(
|
493
|
+
raise ValueError(_MSG_COMMIT_NO_MESSAGE)
|
476
494
|
|
477
495
|
args: list[str] = ["commit"]
|
478
496
|
if signoff:
|