github2gerrit 0.1.4__py3-none-any.whl → 0.1.6__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 +196 -108
- github2gerrit/config.py +207 -5
- github2gerrit/core.py +542 -398
- github2gerrit/duplicate_detection.py +375 -193
- github2gerrit/gerrit_urls.py +256 -0
- github2gerrit/github_api.py +15 -20
- github2gerrit/gitutils.py +49 -13
- github2gerrit/models.py +1 -0
- github2gerrit/similarity.py +458 -0
- github2gerrit/ssh_discovery.py +365 -0
- {github2gerrit-0.1.4.dist-info → github2gerrit-0.1.6.dist-info}/METADATA +24 -25
- github2gerrit-0.1.6.dist-info/RECORD +17 -0
- github2gerrit-0.1.4.dist-info/RECORD +0 -14
- {github2gerrit-0.1.4.dist-info → github2gerrit-0.1.6.dist-info}/WHEEL +0 -0
- {github2gerrit-0.1.4.dist-info → github2gerrit-0.1.6.dist-info}/entry_points.txt +0 -0
- {github2gerrit-0.1.4.dist-info → github2gerrit-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {github2gerrit-0.1.4.dist-info → github2gerrit-0.1.6.dist-info}/top_level.txt +0 -0
github2gerrit/core.py
CHANGED
@@ -29,6 +29,7 @@ from __future__ import annotations
|
|
29
29
|
import logging
|
30
30
|
import os
|
31
31
|
import re
|
32
|
+
import shlex
|
32
33
|
import stat
|
33
34
|
import urllib.parse
|
34
35
|
import urllib.request
|
@@ -38,14 +39,7 @@ from dataclasses import dataclass
|
|
38
39
|
from pathlib import Path
|
39
40
|
from typing import Any
|
40
41
|
|
41
|
-
|
42
|
-
try:
|
43
|
-
from pygerrit2 import GerritRestAPI
|
44
|
-
from pygerrit2 import HTTPBasicAuth
|
45
|
-
except ImportError:
|
46
|
-
GerritRestAPI = None
|
47
|
-
HTTPBasicAuth = None
|
48
|
-
|
42
|
+
from .gerrit_urls import create_gerrit_url_builder
|
49
43
|
from .github_api import build_client
|
50
44
|
from .github_api import close_pr
|
51
45
|
from .github_api import create_pr_comment
|
@@ -67,9 +61,50 @@ from .models import GitHubContext
|
|
67
61
|
from .models import Inputs
|
68
62
|
|
69
63
|
|
64
|
+
try:
|
65
|
+
from pygerrit2 import GerritRestAPI
|
66
|
+
from pygerrit2 import HTTPBasicAuth
|
67
|
+
except ImportError:
|
68
|
+
GerritRestAPI = None
|
69
|
+
HTTPBasicAuth = None
|
70
|
+
|
71
|
+
try:
|
72
|
+
from .ssh_discovery import SSHDiscoveryError
|
73
|
+
from .ssh_discovery import auto_discover_gerrit_host_keys
|
74
|
+
except ImportError:
|
75
|
+
# Fallback if ssh_discovery module is not available
|
76
|
+
auto_discover_gerrit_host_keys = None # type: ignore[assignment]
|
77
|
+
SSHDiscoveryError = Exception # type: ignore[misc,assignment]
|
78
|
+
|
79
|
+
|
80
|
+
def _is_verbose_mode() -> bool:
|
81
|
+
"""Check if verbose mode is enabled via environment variable."""
|
82
|
+
return os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes")
|
83
|
+
|
84
|
+
|
85
|
+
def _log_exception_conditionally(logger: logging.Logger, message: str, *args: Any) -> 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,13 +122,10 @@ 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
127
|
# Format as proper Issue-ID trailer
|
93
|
-
if cleaned_issue_id.startswith("Issue-ID:"):
|
94
|
-
issue_line = cleaned_issue_id
|
95
|
-
else:
|
96
|
-
issue_line = f"Issue-ID: {cleaned_issue_id}"
|
128
|
+
issue_line = cleaned_issue_id if cleaned_issue_id.startswith("Issue-ID:") else f"Issue-ID: {cleaned_issue_id}"
|
97
129
|
|
98
130
|
lines = message.splitlines()
|
99
131
|
if not lines:
|
@@ -228,14 +260,34 @@ class Orchestrator:
|
|
228
260
|
|
229
261
|
if inputs.dry_run:
|
230
262
|
# Perform preflight validations and exit without making changes
|
231
|
-
self._dry_run_preflight(
|
232
|
-
gerrit=gerrit, inputs=inputs, gh=gh, repo=repo_names
|
233
|
-
)
|
263
|
+
self._dry_run_preflight(gerrit=gerrit, inputs=inputs, gh=gh, repo=repo_names)
|
234
264
|
log.info("Dry run complete; skipping write operations to Gerrit")
|
235
|
-
return SubmissionResult(
|
236
|
-
|
265
|
+
return SubmissionResult(change_urls=[], change_numbers=[], commit_shas=[])
|
266
|
+
self._setup_ssh(inputs, gerrit)
|
267
|
+
# Establish baseline non-interactive SSH/Git environment
|
268
|
+
# for all child processes
|
269
|
+
os.environ.update(self._ssh_env())
|
270
|
+
|
271
|
+
# Ensure commit/tag signing is disabled before any commit operations
|
272
|
+
# to avoid agent prompts
|
273
|
+
try:
|
274
|
+
git_config(
|
275
|
+
"commit.gpgsign",
|
276
|
+
"false",
|
277
|
+
global_=False,
|
278
|
+
cwd=self.workspace,
|
237
279
|
)
|
238
|
-
|
280
|
+
except GitError:
|
281
|
+
git_config("commit.gpgsign", "false", global_=True)
|
282
|
+
try:
|
283
|
+
git_config(
|
284
|
+
"tag.gpgsign",
|
285
|
+
"false",
|
286
|
+
global_=False,
|
287
|
+
cwd=self.workspace,
|
288
|
+
)
|
289
|
+
except GitError:
|
290
|
+
git_config("tag.gpgsign", "false", global_=True)
|
239
291
|
|
240
292
|
if inputs.submit_single_commits:
|
241
293
|
prep = self._prepare_single_commits(inputs, gh, gerrit)
|
@@ -281,7 +333,7 @@ class Orchestrator:
|
|
281
333
|
|
282
334
|
def _guard_pull_request_context(self, gh: GitHubContext) -> None:
|
283
335
|
if gh.pr_number is None:
|
284
|
-
raise OrchestratorError(
|
336
|
+
raise OrchestratorError(_MSG_MISSING_PR_CONTEXT)
|
285
337
|
log.debug("PR context OK: #%s", gh.pr_number)
|
286
338
|
|
287
339
|
def _parse_gitreview_text(self, text: str) -> GerritInfo | None:
|
@@ -319,13 +371,8 @@ class Orchestrator:
|
|
319
371
|
repo_obj: Any = get_repo_from_env(client)
|
320
372
|
# Prefer a specific ref when available; otherwise default branch
|
321
373
|
ref = os.getenv("GITHUB_HEAD_REF") or os.getenv("GITHUB_SHA")
|
322
|
-
if ref
|
323
|
-
|
324
|
-
else:
|
325
|
-
content = repo_obj.get_contents(".gitreview")
|
326
|
-
text_remote = (
|
327
|
-
getattr(content, "decoded_content", b"") or b""
|
328
|
-
).decode("utf-8")
|
374
|
+
content = repo_obj.get_contents(".gitreview", ref=ref) if ref else repo_obj.get_contents(".gitreview")
|
375
|
+
text_remote = (getattr(content, "decoded_content", b"") or b"").decode("utf-8")
|
329
376
|
info_remote = self._parse_gitreview_text(text_remote)
|
330
377
|
if info_remote:
|
331
378
|
log.debug("Parsed remote .gitreview: %s", info_remote)
|
@@ -335,14 +382,7 @@ class Orchestrator:
|
|
335
382
|
log.debug("Remote .gitreview not available: %s", exc)
|
336
383
|
# Attempt raw.githubusercontent.com as a fallback
|
337
384
|
try:
|
338
|
-
repo_full = (
|
339
|
-
(
|
340
|
-
gh.repository
|
341
|
-
if gh
|
342
|
-
else os.getenv("GITHUB_REPOSITORY", "")
|
343
|
-
)
|
344
|
-
or ""
|
345
|
-
).strip()
|
385
|
+
repo_full = ((gh.repository if gh else os.getenv("GITHUB_REPOSITORY", "")) or "").strip()
|
346
386
|
branches: list[str] = []
|
347
387
|
# Prefer PR head/base refs via GitHub API when running
|
348
388
|
# from a direct URL when a token is available
|
@@ -356,18 +396,8 @@ class Orchestrator:
|
|
356
396
|
client = build_client()
|
357
397
|
repo_obj = get_repo_from_env(client)
|
358
398
|
pr_obj = get_pull(repo_obj, int(gh.pr_number))
|
359
|
-
api_head = str(
|
360
|
-
|
361
|
-
getattr(pr_obj, "head", object()), "ref", ""
|
362
|
-
)
|
363
|
-
or ""
|
364
|
-
)
|
365
|
-
api_base = str(
|
366
|
-
getattr(
|
367
|
-
getattr(pr_obj, "base", object()), "ref", ""
|
368
|
-
)
|
369
|
-
or ""
|
370
|
-
)
|
399
|
+
api_head = str(getattr(getattr(pr_obj, "head", object()), "ref", "") or "")
|
400
|
+
api_base = str(getattr(getattr(pr_obj, "base", object()), "ref", "") or "")
|
371
401
|
if api_head:
|
372
402
|
branches.append(api_head)
|
373
403
|
if api_base:
|
@@ -387,15 +417,9 @@ class Orchestrator:
|
|
387
417
|
if not br or br in tried:
|
388
418
|
continue
|
389
419
|
tried.add(br)
|
390
|
-
url =
|
391
|
-
f"https://raw.githubusercontent.com/"
|
392
|
-
f"{repo_full}/refs/heads/{br}/.gitreview"
|
393
|
-
)
|
420
|
+
url = f"https://raw.githubusercontent.com/{repo_full}/refs/heads/{br}/.gitreview"
|
394
421
|
parsed = urllib.parse.urlparse(url)
|
395
|
-
if
|
396
|
-
parsed.scheme != "https"
|
397
|
-
or parsed.netloc != "raw.githubusercontent.com"
|
398
|
-
):
|
422
|
+
if parsed.scheme != "https" or parsed.netloc != "raw.githubusercontent.com":
|
399
423
|
continue
|
400
424
|
log.info("Fetching .gitreview via raw URL: %s", url)
|
401
425
|
with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
|
@@ -446,7 +470,7 @@ class Orchestrator:
|
|
446
470
|
# Fallback: use the repository name portion only.
|
447
471
|
repo_full = gh.repository
|
448
472
|
if not repo_full or "/" not in repo_full:
|
449
|
-
raise OrchestratorError(
|
473
|
+
raise OrchestratorError(_MSG_BAD_REPOSITORY_CONTEXT)
|
450
474
|
owner, name = repo_full.split("/", 1)
|
451
475
|
# Fallback: map all '-' to '/' for Gerrit path (e.g., 'my/repo/name')
|
452
476
|
gerrit_name = name.replace("-", "/")
|
@@ -466,7 +490,7 @@ class Orchestrator:
|
|
466
490
|
|
467
491
|
host = inputs.gerrit_server.strip()
|
468
492
|
if not host:
|
469
|
-
raise OrchestratorError(
|
493
|
+
raise OrchestratorError(_MSG_MISSING_GERRIT_SERVER)
|
470
494
|
port_s = inputs.gerrit_server_port.strip() or "29418"
|
471
495
|
try:
|
472
496
|
port = int(port_s)
|
@@ -486,13 +510,13 @@ class Orchestrator:
|
|
486
510
|
project,
|
487
511
|
)
|
488
512
|
else:
|
489
|
-
raise OrchestratorError(
|
513
|
+
raise OrchestratorError(_MSG_MISSING_GERRIT_PROJECT)
|
490
514
|
|
491
515
|
info = GerritInfo(host=host, port=port, project=project)
|
492
516
|
log.debug("Resolved Gerrit info: %s", info)
|
493
517
|
return info
|
494
518
|
|
495
|
-
def _setup_ssh(self, inputs: Inputs) -> None:
|
519
|
+
def _setup_ssh(self, inputs: Inputs, gerrit: GerritInfo) -> None:
|
496
520
|
"""Set up temporary SSH configuration for Gerrit access.
|
497
521
|
|
498
522
|
This method creates tool-specific SSH files in the workspace without
|
@@ -506,8 +530,35 @@ class Orchestrator:
|
|
506
530
|
|
507
531
|
Does not modify user files.
|
508
532
|
"""
|
509
|
-
if not inputs.gerrit_ssh_privkey_g2g
|
510
|
-
log.debug("SSH key
|
533
|
+
if not inputs.gerrit_ssh_privkey_g2g:
|
534
|
+
log.debug("SSH private key not provided, skipping SSH setup")
|
535
|
+
return
|
536
|
+
|
537
|
+
# Auto-discover host keys if not provided
|
538
|
+
effective_known_hosts = inputs.gerrit_known_hosts
|
539
|
+
if not effective_known_hosts and auto_discover_gerrit_host_keys is not None:
|
540
|
+
log.info("GERRIT_KNOWN_HOSTS not provided, attempting auto-discovery...")
|
541
|
+
try:
|
542
|
+
discovered_keys = auto_discover_gerrit_host_keys(
|
543
|
+
gerrit_hostname=gerrit.host,
|
544
|
+
gerrit_port=gerrit.port,
|
545
|
+
organization=inputs.organization,
|
546
|
+
save_to_config=True,
|
547
|
+
)
|
548
|
+
if discovered_keys:
|
549
|
+
effective_known_hosts = discovered_keys
|
550
|
+
log.info(
|
551
|
+
"Successfully auto-discovered SSH host keys for %s:%d",
|
552
|
+
gerrit.host,
|
553
|
+
gerrit.port,
|
554
|
+
)
|
555
|
+
else:
|
556
|
+
log.warning("Auto-discovery failed, SSH host key verification may fail")
|
557
|
+
except Exception as exc:
|
558
|
+
log.warning("SSH host key auto-discovery failed: %s", exc)
|
559
|
+
|
560
|
+
if not effective_known_hosts:
|
561
|
+
log.debug("No SSH host keys available (manual or auto-discovered), skipping SSH setup")
|
511
562
|
return
|
512
563
|
|
513
564
|
log.info("Setting up temporary SSH configuration for Gerrit")
|
@@ -529,7 +580,7 @@ class Orchestrator:
|
|
529
580
|
# Write known hosts to tool-specific location
|
530
581
|
known_hosts_path = tool_ssh_dir / "known_hosts"
|
531
582
|
with open(known_hosts_path, "w", encoding="utf-8") as f:
|
532
|
-
f.write(
|
583
|
+
f.write(effective_known_hosts.strip() + "\n")
|
533
584
|
known_hosts_path.chmod(0o644)
|
534
585
|
log.debug("Known hosts written to %s", known_hosts_path)
|
535
586
|
log.debug("Using isolated known_hosts to prevent user conflicts")
|
@@ -551,9 +602,13 @@ class Orchestrator:
|
|
551
602
|
|
552
603
|
# Build SSH command with strict options to prevent key scanning
|
553
604
|
ssh_options = [
|
605
|
+
"-F /dev/null",
|
554
606
|
f"-i {self._ssh_key_path}",
|
555
607
|
f"-o UserKnownHostsFile={self._ssh_known_hosts_path}",
|
556
608
|
"-o IdentitiesOnly=yes", # Critical: prevents SSH agent scanning
|
609
|
+
"-o IdentityAgent=none",
|
610
|
+
"-o BatchMode=yes",
|
611
|
+
"-o PreferredAuthentications=publickey",
|
557
612
|
"-o StrictHostKeyChecking=yes",
|
558
613
|
"-o PasswordAuthentication=no",
|
559
614
|
"-o PubkeyAcceptedKeyTypes=+ssh-rsa",
|
@@ -565,15 +620,35 @@ class Orchestrator:
|
|
565
620
|
log.debug("Generated SSH command: %s", masked_cmd)
|
566
621
|
return ssh_cmd
|
567
622
|
|
623
|
+
def _ssh_env(self) -> dict[str, str]:
|
624
|
+
"""Centralized non-interactive SSH/Git environment."""
|
625
|
+
cmd = self._git_ssh_command or (
|
626
|
+
"ssh -F /dev/null "
|
627
|
+
"-o IdentitiesOnly=yes "
|
628
|
+
"-o IdentityAgent=none "
|
629
|
+
"-o BatchMode=yes "
|
630
|
+
"-o PreferredAuthentications=publickey "
|
631
|
+
"-o StrictHostKeyChecking=yes "
|
632
|
+
"-o PasswordAuthentication=no "
|
633
|
+
"-o PubkeyAcceptedKeyTypes=+ssh-rsa "
|
634
|
+
"-o ConnectTimeout=10"
|
635
|
+
)
|
636
|
+
return {
|
637
|
+
"GIT_SSH_COMMAND": cmd,
|
638
|
+
"SSH_AUTH_SOCK": "",
|
639
|
+
"SSH_AGENT_PID": "",
|
640
|
+
"SSH_ASKPASS": "/usr/bin/false",
|
641
|
+
"DISPLAY": "",
|
642
|
+
"SSH_ASKPASS_REQUIRE": "never",
|
643
|
+
}
|
644
|
+
|
568
645
|
def _cleanup_ssh(self) -> None:
|
569
646
|
"""Clean up temporary SSH files created by this tool.
|
570
647
|
|
571
648
|
Removes the workspace-specific .ssh-g2g directory and all contents.
|
572
649
|
This ensures no temporary files are left behind.
|
573
650
|
"""
|
574
|
-
if not hasattr(self, "_ssh_key_path") or not hasattr(
|
575
|
-
self, "_ssh_known_hosts_path"
|
576
|
-
):
|
651
|
+
if not hasattr(self, "_ssh_key_path") or not hasattr(self, "_ssh_known_hosts_path"):
|
577
652
|
return
|
578
653
|
|
579
654
|
try:
|
@@ -583,9 +658,7 @@ class Orchestrator:
|
|
583
658
|
import shutil
|
584
659
|
|
585
660
|
shutil.rmtree(tool_ssh_dir)
|
586
|
-
log.debug(
|
587
|
-
"Cleaned up temporary SSH directory: %s", tool_ssh_dir
|
588
|
-
)
|
661
|
+
log.debug("Cleaned up temporary SSH directory: %s", tool_ssh_dir)
|
589
662
|
except Exception as exc:
|
590
663
|
log.warning("Failed to clean up temporary SSH files: %s", exc)
|
591
664
|
|
@@ -605,9 +678,7 @@ class Orchestrator:
|
|
605
678
|
cwd=self.workspace,
|
606
679
|
)
|
607
680
|
except GitError:
|
608
|
-
git_config(
|
609
|
-
"gitreview.username", inputs.gerrit_ssh_user_g2g, global_=True
|
610
|
-
)
|
681
|
+
git_config("gitreview.username", inputs.gerrit_ssh_user_g2g, global_=True)
|
611
682
|
try:
|
612
683
|
git_config(
|
613
684
|
"user.name",
|
@@ -625,9 +696,26 @@ class Orchestrator:
|
|
625
696
|
cwd=self.workspace,
|
626
697
|
)
|
627
698
|
except GitError:
|
699
|
+
git_config("user.email", inputs.gerrit_ssh_user_g2g_email, global_=True)
|
700
|
+
# Disable GPG signing to avoid interactive prompts for signing keys
|
701
|
+
try:
|
628
702
|
git_config(
|
629
|
-
"
|
703
|
+
"commit.gpgsign",
|
704
|
+
"false",
|
705
|
+
global_=False,
|
706
|
+
cwd=self.workspace,
|
630
707
|
)
|
708
|
+
except GitError:
|
709
|
+
git_config("commit.gpgsign", "false", global_=True)
|
710
|
+
try:
|
711
|
+
git_config(
|
712
|
+
"tag.gpgsign",
|
713
|
+
"false",
|
714
|
+
global_=False,
|
715
|
+
cwd=self.workspace,
|
716
|
+
)
|
717
|
+
except GitError:
|
718
|
+
git_config("tag.gpgsign", "false", global_=True)
|
631
719
|
|
632
720
|
# Ensure git-review host/port/project are configured
|
633
721
|
# when .gitreview is absent
|
@@ -663,16 +751,10 @@ class Orchestrator:
|
|
663
751
|
)
|
664
752
|
except CommandError:
|
665
753
|
ssh_user = inputs.gerrit_ssh_user_g2g.strip()
|
666
|
-
remote_url =
|
667
|
-
f"ssh://{ssh_user}@{gerrit.host}:{gerrit.port}/{gerrit.project}"
|
668
|
-
)
|
754
|
+
remote_url = f"ssh://{ssh_user}@{gerrit.host}:{gerrit.port}/{gerrit.project}"
|
669
755
|
log.info("Adding 'gerrit' remote: %s", remote_url)
|
670
756
|
# Use our specific SSH configuration for adding remote
|
671
|
-
env = (
|
672
|
-
{"GIT_SSH_COMMAND": self._git_ssh_command}
|
673
|
-
if self._git_ssh_command
|
674
|
-
else None
|
675
|
-
)
|
757
|
+
env = self._ssh_env()
|
676
758
|
run_cmd(
|
677
759
|
["git", "remote", "add", "gerrit", remote_url],
|
678
760
|
check=False,
|
@@ -681,9 +763,7 @@ class Orchestrator:
|
|
681
763
|
)
|
682
764
|
|
683
765
|
# Workaround for submodules commit-msg hook
|
684
|
-
hooks_path = run_cmd(
|
685
|
-
["git", "rev-parse", "--show-toplevel"], cwd=self.workspace
|
686
|
-
).stdout.strip()
|
766
|
+
hooks_path = run_cmd(["git", "rev-parse", "--show-toplevel"], cwd=self.workspace).stdout.strip()
|
687
767
|
try:
|
688
768
|
git_config(
|
689
769
|
"core.hooksPath",
|
@@ -699,11 +779,7 @@ class Orchestrator:
|
|
699
779
|
# Initialize git-review (copies commit-msg hook)
|
700
780
|
try:
|
701
781
|
# Use our specific SSH configuration for git-review setup
|
702
|
-
env = (
|
703
|
-
{"GIT_SSH_COMMAND": self._git_ssh_command}
|
704
|
-
if self._git_ssh_command
|
705
|
-
else None
|
706
|
-
)
|
782
|
+
env = self._ssh_env()
|
707
783
|
run_cmd(["git", "review", "-s", "-v"], cwd=self.workspace, env=env)
|
708
784
|
except CommandError as exc:
|
709
785
|
msg = f"Failed to initialize git-review: {exc}"
|
@@ -721,12 +797,12 @@ class Orchestrator:
|
|
721
797
|
# Determine commit range: commits in HEAD not in base branch
|
722
798
|
base_ref = f"origin/{branch}"
|
723
799
|
# Use our SSH command for git operations that might need SSH
|
724
|
-
|
725
|
-
|
726
|
-
|
727
|
-
|
800
|
+
|
801
|
+
run_cmd(
|
802
|
+
["git", "fetch", "origin", branch],
|
803
|
+
cwd=self.workspace,
|
804
|
+
env=self._ssh_env(),
|
728
805
|
)
|
729
|
-
run_cmd(["git", "fetch", "origin", branch], cwd=self.workspace, env=env)
|
730
806
|
revs = run_cmd(
|
731
807
|
["git", "rev-list", "--reverse", f"{base_ref}..HEAD"],
|
732
808
|
cwd=self.workspace,
|
@@ -736,14 +812,10 @@ class Orchestrator:
|
|
736
812
|
log.info("No commits to submit; returning empty PreparedChange")
|
737
813
|
return PreparedChange(change_ids=[], commit_shas=[])
|
738
814
|
# Create temp branch from base sha; export for downstream
|
739
|
-
base_sha = run_cmd(
|
740
|
-
["git", "rev-parse", base_ref], cwd=self.workspace
|
741
|
-
).stdout.strip()
|
815
|
+
base_sha = run_cmd(["git", "rev-parse", base_ref], cwd=self.workspace).stdout.strip()
|
742
816
|
tmp_branch = f"g2g_tmp_{gh.pr_number or 'pr'!s}_{os.getpid()}"
|
743
817
|
os.environ["G2G_TMP_BRANCH"] = tmp_branch
|
744
|
-
run_cmd(
|
745
|
-
["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace
|
746
|
-
)
|
818
|
+
run_cmd(["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace)
|
747
819
|
change_ids: list[str] = []
|
748
820
|
for csha in commit_list:
|
749
821
|
run_cmd(["git", "checkout", tmp_branch], cwd=self.workspace)
|
@@ -753,13 +825,9 @@ class Orchestrator:
|
|
753
825
|
["git", "show", "-s", "--pretty=format:%an <%ae>", csha],
|
754
826
|
cwd=self.workspace,
|
755
827
|
).stdout.strip()
|
756
|
-
git_commit_amend(
|
757
|
-
author=author, no_edit=True, signoff=True, cwd=self.workspace
|
758
|
-
)
|
828
|
+
git_commit_amend(author=author, no_edit=True, signoff=True, cwd=self.workspace)
|
759
829
|
# Extract newly added Change-Id from last commit trailers
|
760
|
-
trailers = git_last_commit_trailers(
|
761
|
-
keys=["Change-Id"], cwd=self.workspace
|
762
|
-
)
|
830
|
+
trailers = git_last_commit_trailers(keys=["Change-Id"], cwd=self.workspace)
|
763
831
|
for cid in trailers.get("Change-Id", []):
|
764
832
|
if cid:
|
765
833
|
change_ids.append(cid)
|
@@ -793,26 +861,20 @@ class Orchestrator:
|
|
793
861
|
"""Squash PR commits into a single commit and handle Change-Id."""
|
794
862
|
log.info("Preparing squashed commit for PR #%s", gh.pr_number)
|
795
863
|
branch = self._resolve_target_branch()
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
864
|
+
|
865
|
+
run_cmd(
|
866
|
+
["git", "fetch", "origin", branch],
|
867
|
+
cwd=self.workspace,
|
868
|
+
env=self._ssh_env(),
|
800
869
|
)
|
801
|
-
run_cmd(["git", "fetch", "origin", branch], cwd=self.workspace, env=env)
|
802
870
|
base_ref = f"origin/{branch}"
|
803
|
-
base_sha = run_cmd(
|
804
|
-
|
805
|
-
).stdout.strip()
|
806
|
-
head_sha = run_cmd(
|
807
|
-
["git", "rev-parse", "HEAD"], cwd=self.workspace
|
808
|
-
).stdout.strip()
|
871
|
+
base_sha = run_cmd(["git", "rev-parse", base_ref], cwd=self.workspace).stdout.strip()
|
872
|
+
head_sha = run_cmd(["git", "rev-parse", "HEAD"], cwd=self.workspace).stdout.strip()
|
809
873
|
|
810
874
|
# Create temp branch from base and merge-squash PR head
|
811
875
|
tmp_branch = f"g2g_tmp_{gh.pr_number or 'pr'!s}_{os.getpid()}"
|
812
876
|
os.environ["G2G_TMP_BRANCH"] = tmp_branch
|
813
|
-
run_cmd(
|
814
|
-
["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace
|
815
|
-
)
|
877
|
+
run_cmd(["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace)
|
816
878
|
run_cmd(["git", "merge", "--squash", head_sha], cwd=self.workspace)
|
817
879
|
|
818
880
|
def _collect_log_lines() -> list[str]:
|
@@ -840,18 +902,13 @@ class Orchestrator:
|
|
840
902
|
message_lines: list[str] = []
|
841
903
|
in_metadata_section = False
|
842
904
|
for ln in lines:
|
843
|
-
if ln.strip() in ("---", "```") or ln.startswith(
|
844
|
-
"updated-dependencies:"
|
845
|
-
):
|
905
|
+
if ln.strip() in ("---", "```") or ln.startswith("updated-dependencies:"):
|
846
906
|
in_metadata_section = True
|
847
907
|
continue
|
848
908
|
if in_metadata_section:
|
849
909
|
if ln.startswith(("- dependency-", " dependency-")):
|
850
910
|
continue
|
851
|
-
if (
|
852
|
-
not ln.startswith((" ", "-", "dependency-"))
|
853
|
-
and ln.strip()
|
854
|
-
):
|
911
|
+
if not ln.startswith((" ", "-", "dependency-")) and ln.strip():
|
855
912
|
in_metadata_section = False
|
856
913
|
if ln.startswith("Change-Id:"):
|
857
914
|
cid = ln.split(":", 1)[1].strip()
|
@@ -882,17 +939,11 @@ class Orchestrator:
|
|
882
939
|
break_points = [". ", "! ", "? ", " - ", ": "]
|
883
940
|
for bp in break_points:
|
884
941
|
if bp in title_line[:100]:
|
885
|
-
title_line = title_line[
|
886
|
-
: title_line.index(bp) + len(bp.strip())
|
887
|
-
]
|
942
|
+
title_line = title_line[: title_line.index(bp) + len(bp.strip())]
|
888
943
|
break
|
889
944
|
else:
|
890
945
|
words = title_line[:100].split()
|
891
|
-
title_line = (
|
892
|
-
" ".join(words[:-1])
|
893
|
-
if len(words) > 1
|
894
|
-
else title_line[:100].rstrip()
|
895
|
-
)
|
946
|
+
title_line = " ".join(words[:-1]) if len(words) > 1 else title_line[:100].rstrip()
|
896
947
|
return title_line
|
897
948
|
|
898
949
|
def _build_clean_message_lines(message_lines: list[str]) -> list[str]:
|
@@ -902,10 +953,7 @@ class Orchestrator:
|
|
902
953
|
out: list[str] = [title_line]
|
903
954
|
if len(message_lines) > 1:
|
904
955
|
body_start = 1
|
905
|
-
while (
|
906
|
-
body_start < len(message_lines)
|
907
|
-
and not message_lines[body_start].strip()
|
908
|
-
):
|
956
|
+
while body_start < len(message_lines) and not message_lines[body_start].strip():
|
909
957
|
body_start += 1
|
910
958
|
if body_start < len(message_lines):
|
911
959
|
out.append("")
|
@@ -914,9 +962,7 @@ class Orchestrator:
|
|
914
962
|
|
915
963
|
def _maybe_reuse_change_id(pr_str: str) -> str:
|
916
964
|
reuse = ""
|
917
|
-
sync_all_prs = (
|
918
|
-
os.getenv("SYNC_ALL_OPEN_PRS", "false").lower() == "true"
|
919
|
-
)
|
965
|
+
sync_all_prs = os.getenv("SYNC_ALL_OPEN_PRS", "false").lower() == "true"
|
920
966
|
if (
|
921
967
|
not sync_all_prs
|
922
968
|
and gh.event_name == "pull_request_target"
|
@@ -926,9 +972,7 @@ class Orchestrator:
|
|
926
972
|
client = build_client()
|
927
973
|
repo = get_repo_from_env(client)
|
928
974
|
pr_obj = get_pull(repo, int(pr_str))
|
929
|
-
cand = get_recent_change_ids_from_comments(
|
930
|
-
pr_obj, max_comments=50
|
931
|
-
)
|
975
|
+
cand = get_recent_change_ids_from_comments(pr_obj, max_comments=50)
|
932
976
|
if cand:
|
933
977
|
reuse = cand[-1]
|
934
978
|
log.debug(
|
@@ -950,12 +994,8 @@ class Orchestrator:
|
|
950
994
|
signed_off: list[str],
|
951
995
|
reuse_cid: str,
|
952
996
|
) -> str:
|
953
|
-
from .duplicate_detection import DuplicateDetector
|
954
|
-
|
955
997
|
msg = "\n".join(lines_in).strip()
|
956
998
|
msg = _insert_issue_id_into_commit_message(msg, inputs.issue_id)
|
957
|
-
github_hash = DuplicateDetector._generate_github_change_hash(gh)
|
958
|
-
msg += f"\n\nGitHub-Hash: {github_hash}"
|
959
999
|
if signed_off:
|
960
1000
|
msg += "\n\n" + "\n".join(signed_off)
|
961
1001
|
if reuse_cid:
|
@@ -964,9 +1004,7 @@ class Orchestrator:
|
|
964
1004
|
|
965
1005
|
# Build message parts
|
966
1006
|
raw_lines = _collect_log_lines()
|
967
|
-
message_lines, signed_off, _existing_cids = _parse_message_parts(
|
968
|
-
raw_lines
|
969
|
-
)
|
1007
|
+
message_lines, signed_off, _existing_cids = _parse_message_parts(raw_lines)
|
970
1008
|
clean_lines = _build_clean_message_lines(message_lines)
|
971
1009
|
pr_str = str(gh.pr_number or "").strip()
|
972
1010
|
reuse_cid = _maybe_reuse_change_id(pr_str)
|
@@ -1040,29 +1078,51 @@ class Orchestrator:
|
|
1040
1078
|
title = re.sub(r"[*_`]", "", title)
|
1041
1079
|
title = title.strip()
|
1042
1080
|
|
1043
|
-
# Compose message; preserve
|
1044
|
-
|
1045
|
-
|
1046
|
-
|
1047
|
-
|
1048
|
-
|
1049
|
-
]
|
1081
|
+
# Compose message; preserve existing trailers at footer
|
1082
|
+
# (Signed-off-by, Change-Id)
|
1083
|
+
current_body = git_show("HEAD", fmt="%B", cwd=self.workspace)
|
1084
|
+
# Extract existing trailers from current commit body
|
1085
|
+
lines_cur = current_body.splitlines()
|
1086
|
+
signed_lines = [ln for ln in lines_cur if ln.startswith("Signed-off-by:")]
|
1087
|
+
change_id_lines = [ln for ln in lines_cur if ln.startswith("Change-Id:")]
|
1088
|
+
github_hash_lines = [ln for ln in lines_cur if ln.startswith("GitHub-Hash:")]
|
1089
|
+
|
1050
1090
|
msg_parts = [title, "", body] if title or body else [current_body]
|
1051
1091
|
commit_message = "\n".join(msg_parts).strip()
|
1052
1092
|
|
1053
1093
|
# Add Issue-ID if provided
|
1054
|
-
commit_message = _insert_issue_id_into_commit_message(
|
1055
|
-
|
1056
|
-
)
|
1094
|
+
commit_message = _insert_issue_id_into_commit_message(commit_message, inputs.issue_id)
|
1095
|
+
|
1096
|
+
# Ensure GitHub-Hash is part of the body (not trailers)
|
1097
|
+
# to keep a blank line before Signed-off-by/Change-Id trailers.
|
1098
|
+
if github_hash_lines:
|
1099
|
+
gh_hash_line = github_hash_lines[-1]
|
1100
|
+
else:
|
1101
|
+
from .duplicate_detection import DuplicateDetector
|
1102
|
+
|
1103
|
+
gh_val = DuplicateDetector._generate_github_change_hash(gh)
|
1104
|
+
gh_hash_line = f"GitHub-Hash: {gh_val}"
|
1105
|
+
|
1106
|
+
commit_message += "\n\n" + gh_hash_line
|
1107
|
+
|
1108
|
+
# Build trailers: Signed-off-by first, Change-Id last.
|
1109
|
+
trailers_out: list[str] = []
|
1110
|
+
if signed_lines:
|
1111
|
+
trailers_out.extend(signed_lines)
|
1112
|
+
if change_id_lines:
|
1113
|
+
trailers_out.append(change_id_lines[-1])
|
1114
|
+
if trailers_out:
|
1115
|
+
commit_message += "\n\n" + "\n".join(trailers_out)
|
1057
1116
|
|
1058
|
-
if signed:
|
1059
|
-
commit_message += "\n\n" + "\n".join(signed)
|
1060
1117
|
author = run_cmd(
|
1061
|
-
["git", "show", "-s", "--pretty=format:%an <%ae>", "HEAD"]
|
1118
|
+
["git", "show", "-s", "--pretty=format:%an <%ae>", "HEAD"],
|
1119
|
+
cwd=self.workspace,
|
1120
|
+
env=self._ssh_env(),
|
1062
1121
|
).stdout.strip()
|
1063
1122
|
git_commit_amend(
|
1123
|
+
cwd=self.workspace,
|
1064
1124
|
no_edit=False,
|
1065
|
-
signoff=not bool(
|
1125
|
+
signoff=not bool(signed_lines),
|
1066
1126
|
author=author,
|
1067
1127
|
message=commit_message,
|
1068
1128
|
)
|
@@ -1090,10 +1150,7 @@ class Orchestrator:
|
|
1090
1150
|
run_cmd(["git", "checkout", tmp_branch], cwd=self.workspace)
|
1091
1151
|
prefix = os.getenv("G2G_TOPIC_PREFIX", "GH").strip() or "GH"
|
1092
1152
|
pr_num = os.getenv("PR_NUMBER", "").strip()
|
1093
|
-
if pr_num
|
1094
|
-
topic = f"{prefix}-{repo.project_github}-{pr_num}"
|
1095
|
-
else:
|
1096
|
-
topic = f"{prefix}-{repo.project_github}"
|
1153
|
+
topic = f"{prefix}-{repo.project_github}-{pr_num}" if pr_num else f"{prefix}-{repo.project_github}"
|
1097
1154
|
try:
|
1098
1155
|
args = [
|
1099
1156
|
"git",
|
@@ -1103,31 +1160,22 @@ class Orchestrator:
|
|
1103
1160
|
"-t",
|
1104
1161
|
topic,
|
1105
1162
|
]
|
1106
|
-
revs = [
|
1107
|
-
r.strip() for r in (reviewers or "").split(",") if r.strip()
|
1108
|
-
]
|
1163
|
+
revs = [r.strip() for r in (reviewers or "").split(",") if r.strip()]
|
1109
1164
|
for r in revs:
|
1110
1165
|
args.extend(["--reviewer", r])
|
1111
1166
|
# Branch as positional argument (not a flag)
|
1112
1167
|
args.append(branch)
|
1113
1168
|
|
1114
1169
|
# Use our specific SSH configuration
|
1115
|
-
env = (
|
1116
|
-
{"GIT_SSH_COMMAND": self._git_ssh_command}
|
1117
|
-
if self._git_ssh_command
|
1118
|
-
else None
|
1119
|
-
)
|
1170
|
+
env = self._ssh_env()
|
1120
1171
|
log.debug("Executing git review command: %s", " ".join(args))
|
1121
1172
|
run_cmd(args, cwd=self.workspace, env=env)
|
1122
1173
|
log.info("Successfully pushed changes to Gerrit")
|
1123
1174
|
except CommandError as exc:
|
1124
1175
|
# Analyze the specific failure reason from git review output
|
1125
1176
|
error_details = self._analyze_gerrit_push_failure(exc)
|
1126
|
-
log
|
1127
|
-
msg =
|
1128
|
-
f"Failed to push changes to Gerrit with git-review: "
|
1129
|
-
f"{error_details}"
|
1130
|
-
)
|
1177
|
+
_log_exception_conditionally(log, "Gerrit push failed: %s", error_details)
|
1178
|
+
msg = f"Failed to push changes to Gerrit with git-review: {error_details}"
|
1131
1179
|
raise OrchestratorError(msg) from exc
|
1132
1180
|
# Cleanup temporary branch used during preparation
|
1133
1181
|
tmp_branch = (os.getenv("G2G_TMP_BRANCH", "") or "").strip()
|
@@ -1137,11 +1185,13 @@ class Orchestrator:
|
|
1137
1185
|
["git", "checkout", f"origin/{branch}"],
|
1138
1186
|
check=False,
|
1139
1187
|
cwd=self.workspace,
|
1188
|
+
env=env,
|
1140
1189
|
)
|
1141
1190
|
run_cmd(
|
1142
1191
|
["git", "branch", "-D", tmp_branch],
|
1143
1192
|
check=False,
|
1144
1193
|
cwd=self.workspace,
|
1194
|
+
env=env,
|
1145
1195
|
)
|
1146
1196
|
|
1147
1197
|
def _analyze_gerrit_push_failure(self, exc: CommandError) -> str:
|
@@ -1151,14 +1201,60 @@ class Orchestrator:
|
|
1151
1201
|
combined_output = f"{stdout}\n{stderr}"
|
1152
1202
|
combined_lower = combined_output.lower()
|
1153
1203
|
|
1154
|
-
|
1204
|
+
# Check for SSH host key verification failures first
|
1205
|
+
if (
|
1206
|
+
"host key verification failed" in combined_lower
|
1207
|
+
or "no ed25519 host key is known" in combined_lower
|
1208
|
+
or "no rsa host key is known" in combined_lower
|
1209
|
+
or "no ecdsa host key is known" in combined_lower
|
1210
|
+
):
|
1211
|
+
return (
|
1212
|
+
"SSH host key verification failed. The GERRIT_KNOWN_HOSTS "
|
1213
|
+
"value is missing or contains an outdated host key for the "
|
1214
|
+
"Gerrit server. The tool will attempt to auto-discover "
|
1215
|
+
"host keys "
|
1216
|
+
"on the next run, or you can manually run "
|
1217
|
+
"'ssh-keyscan -p 29418 <gerrit-host>' "
|
1218
|
+
"to get the current host keys."
|
1219
|
+
)
|
1220
|
+
elif "authenticity of host" in combined_lower and "can't be established" in combined_lower:
|
1221
|
+
return (
|
1222
|
+
"SSH host key unknown. The GERRIT_KNOWN_HOSTS value does not "
|
1223
|
+
"contain the host key for the Gerrit server. "
|
1224
|
+
"The tool will attempt "
|
1225
|
+
"to auto-discover host keys on the next run, or you can "
|
1226
|
+
"manually run "
|
1227
|
+
"'ssh-keyscan -p 29418 <gerrit-host>' to get the host keys."
|
1228
|
+
)
|
1229
|
+
# Check for specific SSH key issues before general permission denied
|
1230
|
+
elif "key_load_public" in combined_lower and "invalid format" in combined_lower:
|
1231
|
+
return "SSH key format is invalid. Check that the SSH private key is properly formatted."
|
1232
|
+
elif "no matching host key type found" in combined_lower:
|
1233
|
+
return "SSH key type not supported by server. The server may not accept this SSH key algorithm."
|
1234
|
+
elif "authentication failed" in combined_lower:
|
1235
|
+
return "SSH authentication failed - check SSH key, username, and server configuration"
|
1236
|
+
# Check for connection timeout/refused before "could not read" check
|
1237
|
+
elif "connection timed out" in combined_lower or "connection refused" in combined_lower:
|
1238
|
+
return "Connection failed - check network connectivity and Gerrit server availability"
|
1239
|
+
# Check for specific SSH publickey-only authentication failures
|
1240
|
+
elif "permission denied (publickey)" in combined_lower and not any(
|
1241
|
+
auth_method in combined_lower for auth_method in ["gssapi", "password", "keyboard"]
|
1242
|
+
):
|
1243
|
+
return (
|
1244
|
+
"SSH public key authentication failed. The SSH key may be "
|
1245
|
+
"invalid, not authorized for this user, or the wrong key type."
|
1246
|
+
)
|
1247
|
+
# Check for general SSH permission issues
|
1248
|
+
elif "permission denied" in combined_lower:
|
1249
|
+
return "SSH permission denied - check SSH key and user permissions"
|
1250
|
+
elif "could not read from remote repository" in combined_lower:
|
1251
|
+
return "Could not read from remote repository - check SSH authentication and repository access permissions"
|
1252
|
+
# Check for Gerrit-specific issues
|
1253
|
+
elif "missing issue-id" in combined_lower:
|
1155
1254
|
return "Missing Issue-ID in commit message."
|
1156
1255
|
elif "commit not associated to any issue" in combined_lower:
|
1157
1256
|
return "Commit not associated to any issue."
|
1158
|
-
elif
|
1159
|
-
"remote rejected" in combined_lower
|
1160
|
-
and "refs/for/" in combined_lower
|
1161
|
-
):
|
1257
|
+
elif "remote rejected" in combined_lower and "refs/for/" in combined_lower:
|
1162
1258
|
# Extract specific rejection reason from output
|
1163
1259
|
lines = combined_output.split("\n")
|
1164
1260
|
for line in lines:
|
@@ -1169,15 +1265,6 @@ class Orchestrator:
|
|
1169
1265
|
return f"Gerrit rejected the push: {reason}"
|
1170
1266
|
return f"Gerrit rejected the push: {line.strip()}"
|
1171
1267
|
return "Gerrit rejected the push for an unknown reason"
|
1172
|
-
elif "permission denied" in combined_lower:
|
1173
|
-
return "Permission denied - check SSH key and user permissions"
|
1174
|
-
elif "connection" in combined_lower and (
|
1175
|
-
"refused" in combined_lower or "timeout" in combined_lower
|
1176
|
-
):
|
1177
|
-
return (
|
1178
|
-
"Connection failed - check network connectivity and "
|
1179
|
-
"Gerrit server availability"
|
1180
|
-
)
|
1181
1268
|
else:
|
1182
1269
|
return f"Unknown error: {exc}"
|
1183
1270
|
|
@@ -1190,32 +1277,28 @@ class Orchestrator:
|
|
1190
1277
|
) -> SubmissionResult:
|
1191
1278
|
"""Query Gerrit for change URL/number and patchset sha via REST."""
|
1192
1279
|
log.info("Querying Gerrit for submitted change(s) via REST")
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
|
1197
|
-
|
1198
|
-
|
1199
|
-
)
|
1200
|
-
http_user = (
|
1201
|
-
os.getenv("GERRIT_HTTP_USER", "").strip()
|
1202
|
-
or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
|
1203
|
-
)
|
1280
|
+
|
1281
|
+
# Create centralized URL builder
|
1282
|
+
url_builder = create_gerrit_url_builder(gerrit.host)
|
1283
|
+
|
1284
|
+
# Get authentication credentials
|
1285
|
+
http_user = os.getenv("GERRIT_HTTP_USER", "").strip() or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
|
1204
1286
|
http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
|
1287
|
+
|
1288
|
+
# Build Gerrit REST client (prefer HTTP basic auth if provided)
|
1205
1289
|
if GerritRestAPI is None:
|
1206
|
-
raise OrchestratorError(
|
1207
|
-
|
1208
|
-
|
1209
|
-
|
1210
|
-
if
|
1211
|
-
|
1212
|
-
|
1213
|
-
)
|
1214
|
-
|
1215
|
-
url=base_url
|
1216
|
-
|
1217
|
-
|
1218
|
-
rest = GerritRestAPI(url=base_url)
|
1290
|
+
raise OrchestratorError(_MSG_PYGERRIT2_REQUIRED_REST)
|
1291
|
+
|
1292
|
+
def _create_rest_client(base_url: str) -> Any:
|
1293
|
+
"""Helper to create REST client with optional auth."""
|
1294
|
+
if http_user and http_pass:
|
1295
|
+
if HTTPBasicAuth is None:
|
1296
|
+
raise OrchestratorError(_MSG_PYGERRIT2_REQUIRED_AUTH)
|
1297
|
+
return GerritRestAPI(url=base_url, auth=HTTPBasicAuth(http_user, http_pass))
|
1298
|
+
else:
|
1299
|
+
return GerritRestAPI(url=base_url)
|
1300
|
+
|
1301
|
+
# Try API URLs in order of preference (client creation happens in retry loop)
|
1219
1302
|
urls: list[str] = []
|
1220
1303
|
nums: list[str] = []
|
1221
1304
|
shas: list[str] = []
|
@@ -1226,56 +1309,44 @@ class Orchestrator:
|
|
1226
1309
|
# include current revision
|
1227
1310
|
query = f"limit:1 is:open project:{repo.project_gerrit} {cid}"
|
1228
1311
|
path = f"/changes/?q={query}&o=CURRENT_REVISION&n=1"
|
1229
|
-
|
1230
|
-
|
1231
|
-
|
1232
|
-
|
1233
|
-
|
1234
|
-
|
1235
|
-
|
1236
|
-
|
1237
|
-
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
if http_user and http_pass:
|
1244
|
-
if HTTPBasicAuth is None:
|
1245
|
-
log.warning(
|
1246
|
-
"pygerrit2 auth missing; skipping fallback"
|
1247
|
-
)
|
1248
|
-
continue
|
1249
|
-
rest_fallback = GerritRestAPI(
|
1250
|
-
url=fallback_url,
|
1251
|
-
auth=HTTPBasicAuth(http_user, http_pass),
|
1252
|
-
)
|
1253
|
-
else:
|
1254
|
-
rest_fallback = GerritRestAPI(url=fallback_url)
|
1255
|
-
changes = rest_fallback.get(path)
|
1256
|
-
except Exception as exc2:
|
1257
|
-
log.warning(
|
1258
|
-
"Failed to query change via REST for %s "
|
1259
|
-
"(including '/r' fallback): %s",
|
1260
|
-
cid,
|
1261
|
-
exc2,
|
1262
|
-
)
|
1263
|
-
continue
|
1264
|
-
else:
|
1265
|
-
log.warning(
|
1266
|
-
"Failed to query change via REST for %s: %s", cid, exc
|
1267
|
-
)
|
1312
|
+
# Try API URLs in order of preference (handles fallbacks automatically)
|
1313
|
+
api_candidates = url_builder.get_api_url_candidates(path)
|
1314
|
+
changes = None
|
1315
|
+
last_error = None
|
1316
|
+
|
1317
|
+
for api_url in api_candidates:
|
1318
|
+
try:
|
1319
|
+
# Create a new client for each URL attempt
|
1320
|
+
current_rest = _create_rest_client(api_url.rsplit(path, 1)[0])
|
1321
|
+
changes = current_rest.get(path)
|
1322
|
+
break # Success, exit the retry loop
|
1323
|
+
except Exception as exc:
|
1324
|
+
last_error = exc
|
1325
|
+
log.debug("Failed API attempt for %s at %s: %s", cid, api_url, exc)
|
1268
1326
|
continue
|
1327
|
+
|
1328
|
+
if changes is None:
|
1329
|
+
log.warning(
|
1330
|
+
"Failed to query change via REST for %s (tried %d URLs): %s",
|
1331
|
+
cid,
|
1332
|
+
len(api_candidates),
|
1333
|
+
last_error,
|
1334
|
+
)
|
1335
|
+
continue
|
1269
1336
|
if not changes:
|
1270
1337
|
continue
|
1271
1338
|
change = changes[0]
|
1272
|
-
|
1273
|
-
|
1339
|
+
# Type guard to ensure mapping-like before dict access
|
1340
|
+
if isinstance(change, dict):
|
1341
|
+
num = str(change.get("_number", ""))
|
1342
|
+
current_rev = change.get("current_revision", "")
|
1343
|
+
else:
|
1344
|
+
# Unexpected type; skip this result
|
1345
|
+
continue
|
1274
1346
|
# Construct a stable web URL for the change
|
1275
1347
|
if num:
|
1276
|
-
|
1277
|
-
|
1278
|
-
)
|
1348
|
+
change_url = url_builder.change_url(repo.project_gerrit, int(num))
|
1349
|
+
urls.append(change_url)
|
1279
1350
|
nums.append(num)
|
1280
1351
|
if current_rev:
|
1281
1352
|
shas.append(current_rev)
|
@@ -1286,9 +1357,7 @@ class Orchestrator:
|
|
1286
1357
|
os.environ["GERRIT_CHANGE_REQUEST_NUM"] = "\n".join(nums)
|
1287
1358
|
if shas:
|
1288
1359
|
os.environ["GERRIT_COMMIT_SHA"] = "\n".join(shas)
|
1289
|
-
return SubmissionResult(
|
1290
|
-
change_urls=urls, change_numbers=nums, commit_shas=shas
|
1291
|
-
)
|
1360
|
+
return SubmissionResult(change_urls=urls, change_numbers=nums, commit_shas=shas)
|
1292
1361
|
|
1293
1362
|
def _setup_git_workspace(self, inputs: Inputs, gh: GitHubContext) -> None:
|
1294
1363
|
"""Initialize and set up git workspace for PR processing."""
|
@@ -1309,10 +1378,7 @@ class Orchestrator:
|
|
1309
1378
|
|
1310
1379
|
# Fetch PR head
|
1311
1380
|
if gh.pr_number:
|
1312
|
-
pr_ref =
|
1313
|
-
f"refs/pull/{gh.pr_number}/head:"
|
1314
|
-
f"refs/remotes/origin/pr/{gh.pr_number}/head"
|
1315
|
-
)
|
1381
|
+
pr_ref = f"refs/pull/{gh.pr_number}/head:refs/remotes/origin/pr/{gh.pr_number}/head"
|
1316
1382
|
run_cmd(
|
1317
1383
|
[
|
1318
1384
|
"git",
|
@@ -1341,13 +1407,48 @@ class Orchestrator:
|
|
1341
1407
|
# Download commit-msg hook using SSH
|
1342
1408
|
try:
|
1343
1409
|
# Use curl to download the hook (more reliable than scp)
|
1344
|
-
|
1345
|
-
|
1346
|
-
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1410
|
+
# Create centralized URL builder for hook URLs
|
1411
|
+
url_builder = create_gerrit_url_builder(gerrit.host)
|
1412
|
+
candidates = url_builder.get_hook_url_candidates("commit-msg")
|
1413
|
+
|
1414
|
+
last_error = ""
|
1415
|
+
installed = False
|
1416
|
+
for candidate_url in candidates:
|
1417
|
+
try:
|
1418
|
+
curl_cmd = [
|
1419
|
+
"curl",
|
1420
|
+
"-fL",
|
1421
|
+
"-o",
|
1422
|
+
str(hook_path),
|
1423
|
+
candidate_url,
|
1424
|
+
]
|
1425
|
+
run_cmd(curl_cmd, cwd=self.workspace)
|
1426
|
+
# Make hook executable
|
1427
|
+
hook_path.chmod(hook_path.stat().st_mode | stat.S_IEXEC)
|
1428
|
+
log.debug(
|
1429
|
+
"Successfully installed commit-msg hook from %s",
|
1430
|
+
candidate_url,
|
1431
|
+
)
|
1432
|
+
installed = True
|
1433
|
+
break
|
1434
|
+
except Exception as exc2:
|
1435
|
+
last_error = f"{candidate_url}: {exc2}"
|
1436
|
+
log.debug(
|
1437
|
+
"Failed to fetch commit-msg hook from %s: %s",
|
1438
|
+
candidate_url,
|
1439
|
+
exc2,
|
1440
|
+
)
|
1441
|
+
|
1442
|
+
def _raise_install_error() -> None:
|
1443
|
+
raise RuntimeError("Hook install failed") # noqa: TRY301, TRY003
|
1444
|
+
|
1445
|
+
if not installed:
|
1446
|
+
# Log detailed reason separately to satisfy linting rules
|
1447
|
+
log.error(
|
1448
|
+
"All commit-msg hook URLs failed. Last error: %s",
|
1449
|
+
last_error,
|
1450
|
+
)
|
1451
|
+
_raise_install_error()
|
1351
1452
|
|
1352
1453
|
# Make hook executable
|
1353
1454
|
hook_path.chmod(hook_path.stat().st_mode | stat.S_IEXEC)
|
@@ -1358,34 +1459,55 @@ class Orchestrator:
|
|
1358
1459
|
msg = f"Could not install commit-msg hook: {exc}"
|
1359
1460
|
raise OrchestratorError(msg) from exc
|
1360
1461
|
|
1361
|
-
def _ensure_change_id_present(
|
1362
|
-
self, gerrit: GerritInfo, author: str
|
1363
|
-
) -> list[str]:
|
1462
|
+
def _ensure_change_id_present(self, gerrit: GerritInfo, author: str) -> list[str]:
|
1364
1463
|
"""Ensure the last commit has a Change-Id.
|
1365
1464
|
|
1366
1465
|
Installs the commit-msg hook and amends the commit if needed.
|
1367
1466
|
"""
|
1368
|
-
trailers = git_last_commit_trailers(
|
1369
|
-
keys=["Change-Id"], cwd=self.workspace
|
1370
|
-
)
|
1467
|
+
trailers = git_last_commit_trailers(keys=["Change-Id"], cwd=self.workspace)
|
1371
1468
|
if not trailers.get("Change-Id"):
|
1372
|
-
log.debug(
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
|
1469
|
+
log.debug("No Change-Id found; attempting to install commit-msg hook and amend commit")
|
1470
|
+
try:
|
1471
|
+
self._install_commit_msg_hook(gerrit)
|
1472
|
+
git_commit_amend(
|
1473
|
+
no_edit=True,
|
1474
|
+
signoff=True,
|
1475
|
+
author=author,
|
1476
|
+
cwd=self.workspace,
|
1477
|
+
)
|
1478
|
+
except Exception as exc:
|
1479
|
+
log.warning(
|
1480
|
+
"Commit-msg hook installation failed, falling back to direct Change-Id injection: %s",
|
1481
|
+
exc,
|
1482
|
+
)
|
1483
|
+
# Fallback: generate a Change-Id and append to the commit
|
1484
|
+
# message directly
|
1485
|
+
import time
|
1486
|
+
|
1487
|
+
current_msg = run_cmd(
|
1488
|
+
["git", "show", "-s", "--pretty=format:%B", "HEAD"],
|
1489
|
+
cwd=self.workspace,
|
1490
|
+
).stdout
|
1491
|
+
seed = f"{current_msg}\n{time.time()}"
|
1492
|
+
import hashlib as _hashlib # local alias to satisfy linters
|
1493
|
+
|
1494
|
+
change_id = "I" + _hashlib.sha256(seed.encode("utf-8")).hexdigest()[:40]
|
1495
|
+
if "Change-Id:" not in current_msg:
|
1496
|
+
new_msg = current_msg.rstrip() + "\n\n" + f"Change-Id: {change_id}\n"
|
1497
|
+
git_commit_amend(
|
1498
|
+
no_edit=False,
|
1499
|
+
signoff=True,
|
1500
|
+
author=author,
|
1501
|
+
message=new_msg,
|
1502
|
+
cwd=self.workspace,
|
1503
|
+
)
|
1380
1504
|
# Debug: Check commit message after amend
|
1381
1505
|
actual_msg = run_cmd(
|
1382
1506
|
["git", "show", "-s", "--pretty=format:%B", "HEAD"],
|
1383
1507
|
cwd=self.workspace,
|
1384
1508
|
).stdout.strip()
|
1385
1509
|
log.debug("Commit message after amend:\n%s", actual_msg)
|
1386
|
-
trailers = git_last_commit_trailers(
|
1387
|
-
keys=["Change-Id"], cwd=self.workspace
|
1388
|
-
)
|
1510
|
+
trailers = git_last_commit_trailers(keys=["Change-Id"], cwd=self.workspace)
|
1389
1511
|
return [c for c in trailers.get("Change-Id", []) if c]
|
1390
1512
|
|
1391
1513
|
def _add_backref_comment_in_gerrit(
|
@@ -1408,21 +1530,14 @@ class Orchestrator:
|
|
1408
1530
|
"1",
|
1409
1531
|
"yes",
|
1410
1532
|
):
|
1411
|
-
log.info(
|
1412
|
-
"Skipping back-reference comments "
|
1413
|
-
"(G2G_SKIP_GERRIT_COMMENTS=true)"
|
1414
|
-
)
|
1533
|
+
log.info("Skipping back-reference comments (G2G_SKIP_GERRIT_COMMENTS=true)")
|
1415
1534
|
return
|
1416
1535
|
|
1417
1536
|
log.info("Adding back-reference comment in Gerrit")
|
1418
1537
|
user = os.getenv("GERRIT_SSH_USER_G2G", "")
|
1419
1538
|
server = gerrit.host
|
1420
1539
|
pr_url = f"{gh.server_url}/{gh.repository}/pull/{gh.pr_number}"
|
1421
|
-
run_url =
|
1422
|
-
f"{gh.server_url}/{gh.repository}/actions/runs/{gh.run_id}"
|
1423
|
-
if gh.run_id
|
1424
|
-
else "N/A"
|
1425
|
-
)
|
1540
|
+
run_url = f"{gh.server_url}/{gh.repository}/actions/runs/{gh.run_id}" if gh.run_id else "N/A"
|
1426
1541
|
message = f"GHPR: {pr_url} | Action-Run: {run_url}"
|
1427
1542
|
log.info("Adding back-reference comment: %s", message)
|
1428
1543
|
for csha in commit_shas:
|
@@ -1430,39 +1545,84 @@ class Orchestrator:
|
|
1430
1545
|
continue
|
1431
1546
|
try:
|
1432
1547
|
log.debug("Executing SSH command for commit %s", csha)
|
1433
|
-
# Build SSH command
|
1434
|
-
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
1438
|
-
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1444
|
-
|
1548
|
+
# Build SSH command. If isolated SSH key/known_hosts are
|
1549
|
+
# available, use strict options; otherwise fall back to the
|
1550
|
+
# minimal form expected by tests.
|
1551
|
+
if self._ssh_key_path and self._ssh_known_hosts_path:
|
1552
|
+
ssh_cmd = [
|
1553
|
+
"ssh",
|
1554
|
+
"-F",
|
1555
|
+
"/dev/null",
|
1556
|
+
"-i",
|
1557
|
+
str(self._ssh_key_path),
|
1558
|
+
"-o",
|
1559
|
+
f"UserKnownHostsFile={self._ssh_known_hosts_path}",
|
1560
|
+
"-o",
|
1561
|
+
"IdentitiesOnly=yes",
|
1562
|
+
"-o",
|
1563
|
+
"IdentityAgent=none",
|
1564
|
+
"-o",
|
1565
|
+
"BatchMode=yes",
|
1566
|
+
"-o",
|
1567
|
+
"StrictHostKeyChecking=yes",
|
1568
|
+
"-o",
|
1569
|
+
"PasswordAuthentication=no",
|
1570
|
+
"-o",
|
1571
|
+
"PubkeyAcceptedKeyTypes=+ssh-rsa",
|
1572
|
+
"-n",
|
1573
|
+
"-p",
|
1574
|
+
str(gerrit.port),
|
1575
|
+
f"{user}@{server}",
|
1576
|
+
(
|
1577
|
+
"gerrit review -m "
|
1578
|
+
f"{shlex.quote(message)} "
|
1579
|
+
"--branch "
|
1580
|
+
f"{shlex.quote(branch)} "
|
1581
|
+
"--project "
|
1582
|
+
f"{shlex.quote(repo.project_gerrit)} "
|
1583
|
+
f"{shlex.quote(csha)}"
|
1584
|
+
),
|
1585
|
+
]
|
1445
1586
|
else:
|
1446
|
-
|
1447
|
-
|
1448
|
-
|
1449
|
-
|
1450
|
-
|
1587
|
+
# Strict non-interactive SSH without
|
1588
|
+
# isolated key/known_hosts
|
1589
|
+
ssh_cmd = [
|
1590
|
+
"ssh",
|
1591
|
+
"-F",
|
1592
|
+
"/dev/null",
|
1593
|
+
"-o",
|
1594
|
+
"IdentitiesOnly=yes",
|
1595
|
+
"-o",
|
1596
|
+
"IdentityAgent=none",
|
1597
|
+
"-o",
|
1598
|
+
"BatchMode=yes",
|
1599
|
+
"-o",
|
1600
|
+
"StrictHostKeyChecking=yes",
|
1601
|
+
"-o",
|
1602
|
+
"PasswordAuthentication=no",
|
1603
|
+
"-o",
|
1604
|
+
"PubkeyAcceptedKeyTypes=+ssh-rsa",
|
1605
|
+
"-n",
|
1606
|
+
"-p",
|
1607
|
+
str(gerrit.port),
|
1451
1608
|
f"{user}@{server}",
|
1452
|
-
|
1453
|
-
|
1454
|
-
|
1455
|
-
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
1459
|
-
|
1460
|
-
|
1609
|
+
(
|
1610
|
+
"gerrit review -m "
|
1611
|
+
f"{shlex.quote(message)} "
|
1612
|
+
"--branch "
|
1613
|
+
f"{shlex.quote(branch)} "
|
1614
|
+
"--project "
|
1615
|
+
f"{shlex.quote(repo.project_gerrit)} "
|
1616
|
+
f"{shlex.quote(csha)}"
|
1617
|
+
),
|
1461
1618
|
]
|
1462
|
-
)
|
1463
1619
|
|
1464
1620
|
log.debug("Final SSH command: %s", " ".join(ssh_cmd))
|
1465
|
-
run_cmd(
|
1621
|
+
run_cmd(
|
1622
|
+
ssh_cmd,
|
1623
|
+
cwd=self.workspace,
|
1624
|
+
env=self._ssh_env(),
|
1625
|
+
)
|
1466
1626
|
log.info(
|
1467
1627
|
"Successfully added back-reference comment for %s: %s",
|
1468
1628
|
csha,
|
@@ -1470,8 +1630,7 @@ class Orchestrator:
|
|
1470
1630
|
)
|
1471
1631
|
except CommandError as exc:
|
1472
1632
|
log.warning(
|
1473
|
-
"Failed to add back-reference comment for %s "
|
1474
|
-
"(non-fatal): %s",
|
1633
|
+
"Failed to add back-reference comment for %s (non-fatal): %s",
|
1475
1634
|
csha,
|
1476
1635
|
exc,
|
1477
1636
|
)
|
@@ -1487,14 +1646,11 @@ class Orchestrator:
|
|
1487
1646
|
# Continue processing - this is not a fatal error
|
1488
1647
|
except Exception as exc:
|
1489
1648
|
log.warning(
|
1490
|
-
"Failed to add back-reference comment for %s "
|
1491
|
-
"(non-fatal): %s",
|
1649
|
+
"Failed to add back-reference comment for %s (non-fatal): %s",
|
1492
1650
|
csha,
|
1493
1651
|
exc,
|
1494
1652
|
)
|
1495
|
-
log.debug(
|
1496
|
-
"Back-reference comment failure details:", exc_info=True
|
1497
|
-
)
|
1653
|
+
log.debug("Back-reference comment failure details:", exc_info=True)
|
1498
1654
|
# Continue processing - this is not a fatal error
|
1499
1655
|
|
1500
1656
|
def _comment_on_pull_request(
|
@@ -1509,10 +1665,10 @@ class Orchestrator:
|
|
1509
1665
|
return
|
1510
1666
|
urls = result.change_urls or []
|
1511
1667
|
org = os.getenv("ORGANIZATION", gh.repository_owner)
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
)
|
1668
|
+
# Create centralized URL builder for organization link
|
1669
|
+
url_builder = create_gerrit_url_builder(gerrit.host)
|
1670
|
+
org_url = url_builder.web_url()
|
1671
|
+
text = f"The pull-request PR-{gh.pr_number} is submitted to Gerrit [{org}]({org_url})!\n\n"
|
1516
1672
|
if urls:
|
1517
1673
|
text += "To follow up on the change visit:\n\n" + "\n".join(urls)
|
1518
1674
|
try:
|
@@ -1521,6 +1677,13 @@ class Orchestrator:
|
|
1521
1677
|
# At this point, gh.pr_number is non-None due to earlier guard.
|
1522
1678
|
pr_obj = get_pull(repo, int(gh.pr_number))
|
1523
1679
|
create_pr_comment(pr_obj, text)
|
1680
|
+
# Also post a succinct one-line comment
|
1681
|
+
# for each Gerrit change URL
|
1682
|
+
for u in urls:
|
1683
|
+
create_pr_comment(
|
1684
|
+
pr_obj,
|
1685
|
+
f"Change raised in Gerrit by GitHub2Gerrit: {u}",
|
1686
|
+
)
|
1524
1687
|
except Exception as exc:
|
1525
1688
|
log.warning("Failed to add PR comment: %s", exc)
|
1526
1689
|
|
@@ -1582,20 +1745,15 @@ class Orchestrator:
|
|
1582
1745
|
"yes",
|
1583
1746
|
"on",
|
1584
1747
|
):
|
1748
|
+
log.info("Dry-run: network checks disabled (G2G_DRYRUN_DISABLE_NETWORK)")
|
1585
1749
|
log.info(
|
1586
|
-
"Dry-run:
|
1587
|
-
)
|
1588
|
-
log.info(
|
1589
|
-
"Dry-run targets: Gerrit project=%s branch=%s "
|
1590
|
-
"topic_prefix=GH-%s",
|
1750
|
+
"Dry-run targets: Gerrit project=%s branch=%s topic_prefix=GH-%s",
|
1591
1751
|
repo.project_gerrit,
|
1592
1752
|
self._resolve_target_branch(),
|
1593
1753
|
repo.project_github,
|
1594
1754
|
)
|
1595
1755
|
if inputs.reviewers_email:
|
1596
|
-
log.info(
|
1597
|
-
"Reviewers (from inputs/config): %s", inputs.reviewers_email
|
1598
|
-
)
|
1756
|
+
log.info("Reviewers (from inputs/config): %s", inputs.reviewers_email)
|
1599
1757
|
elif os.getenv("REVIEWERS_EMAIL"):
|
1600
1758
|
log.info(
|
1601
1759
|
"Reviewers (from environment): %s",
|
@@ -1606,18 +1764,14 @@ class Orchestrator:
|
|
1606
1764
|
# DNS resolution for Gerrit host
|
1607
1765
|
try:
|
1608
1766
|
socket.getaddrinfo(gerrit.host, None)
|
1609
|
-
log.info(
|
1610
|
-
"DNS resolution for Gerrit host '%s' succeeded", gerrit.host
|
1611
|
-
)
|
1767
|
+
log.info("DNS resolution for Gerrit host '%s' succeeded", gerrit.host)
|
1612
1768
|
except Exception as exc:
|
1613
1769
|
msg = "DNS resolution failed"
|
1614
1770
|
raise OrchestratorError(msg) from exc
|
1615
1771
|
|
1616
1772
|
# SSH (TCP) reachability on Gerrit port
|
1617
1773
|
try:
|
1618
|
-
with socket.create_connection(
|
1619
|
-
(gerrit.host, gerrit.port), timeout=5
|
1620
|
-
):
|
1774
|
+
with socket.create_connection((gerrit.host, gerrit.port), timeout=5):
|
1621
1775
|
pass
|
1622
1776
|
log.info(
|
1623
1777
|
"SSH TCP connectivity to %s:%s verified",
|
@@ -1630,10 +1784,7 @@ class Orchestrator:
|
|
1630
1784
|
|
1631
1785
|
# Gerrit REST reachability and optional auth check
|
1632
1786
|
base_path = os.getenv("GERRIT_HTTP_BASE_PATH", "").strip().strip("/")
|
1633
|
-
http_user = (
|
1634
|
-
os.getenv("GERRIT_HTTP_USER", "").strip()
|
1635
|
-
or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
|
1636
|
-
)
|
1787
|
+
http_user = os.getenv("GERRIT_HTTP_USER", "").strip() or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
|
1637
1788
|
http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
|
1638
1789
|
self._verify_gerrit_rest(gerrit.host, base_path, http_user, http_pass)
|
1639
1790
|
|
@@ -1643,9 +1794,7 @@ class Orchestrator:
|
|
1643
1794
|
repo_obj = get_repo_from_env(client)
|
1644
1795
|
if gh.pr_number is not None:
|
1645
1796
|
pr_obj = get_pull(repo_obj, gh.pr_number)
|
1646
|
-
log.info(
|
1647
|
-
"GitHub PR #%s metadata loaded successfully", gh.pr_number
|
1648
|
-
)
|
1797
|
+
log.info("GitHub PR #%s metadata loaded successfully", gh.pr_number)
|
1649
1798
|
try:
|
1650
1799
|
title, _ = get_pr_title_body(pr_obj)
|
1651
1800
|
log.info("GitHub PR title: %s", title)
|
@@ -1671,13 +1820,9 @@ class Orchestrator:
|
|
1671
1820
|
repo.project_github,
|
1672
1821
|
)
|
1673
1822
|
if inputs.reviewers_email:
|
1674
|
-
log.info(
|
1675
|
-
"Reviewers (from inputs/config): %s", inputs.reviewers_email
|
1676
|
-
)
|
1823
|
+
log.info("Reviewers (from inputs/config): %s", inputs.reviewers_email)
|
1677
1824
|
elif os.getenv("REVIEWERS_EMAIL"):
|
1678
|
-
log.info(
|
1679
|
-
"Reviewers (from environment): %s", os.getenv("REVIEWERS_EMAIL")
|
1680
|
-
)
|
1825
|
+
log.info("Reviewers (from environment): %s", os.getenv("REVIEWERS_EMAIL"))
|
1681
1826
|
|
1682
1827
|
def _verify_gerrit_rest(
|
1683
1828
|
self,
|
@@ -1691,15 +1836,13 @@ class Orchestrator:
|
|
1691
1836
|
def _build_client(url: str) -> Any:
|
1692
1837
|
if http_user and http_pass:
|
1693
1838
|
if GerritRestAPI is None:
|
1694
|
-
raise OrchestratorError(
|
1839
|
+
raise OrchestratorError(_MSG_PYGERRIT2_MISSING)
|
1695
1840
|
if HTTPBasicAuth is None:
|
1696
|
-
raise OrchestratorError(
|
1697
|
-
return GerritRestAPI(
|
1698
|
-
url=url, auth=HTTPBasicAuth(http_user, http_pass)
|
1699
|
-
)
|
1841
|
+
raise OrchestratorError(_MSG_PYGERRIT2_AUTH_MISSING)
|
1842
|
+
return GerritRestAPI(url=url, auth=HTTPBasicAuth(http_user, http_pass))
|
1700
1843
|
else:
|
1701
1844
|
if GerritRestAPI is None:
|
1702
|
-
raise OrchestratorError(
|
1845
|
+
raise OrchestratorError(_MSG_PYGERRIT2_MISSING)
|
1703
1846
|
return GerritRestAPI(url=url)
|
1704
1847
|
|
1705
1848
|
def _probe(url: str) -> None:
|
@@ -1714,29 +1857,30 @@ class Orchestrator:
|
|
1714
1857
|
_ = rest.get("/dashboard/self")
|
1715
1858
|
log.info("Gerrit REST endpoint reachable (unauthenticated)")
|
1716
1859
|
|
1717
|
-
|
1718
|
-
|
1719
|
-
|
1720
|
-
|
1721
|
-
|
1722
|
-
|
1723
|
-
|
1724
|
-
|
1725
|
-
|
1726
|
-
|
1860
|
+
# Create centralized URL builder for REST probing
|
1861
|
+
url_builder = create_gerrit_url_builder(host, base_path)
|
1862
|
+
api_candidates = url_builder.get_api_url_candidates()
|
1863
|
+
|
1864
|
+
# Try API URLs in order of preference
|
1865
|
+
probe_successful = False
|
1866
|
+
last_error = None
|
1867
|
+
|
1868
|
+
for api_url in api_candidates:
|
1869
|
+
try:
|
1870
|
+
_probe(api_url)
|
1871
|
+
probe_successful = True
|
1872
|
+
break
|
1873
|
+
except Exception as exc:
|
1874
|
+
last_error = exc
|
1875
|
+
log.debug("Gerrit REST probe failed for %s: %s", api_url, exc)
|
1876
|
+
continue
|
1877
|
+
|
1878
|
+
if not probe_successful and last_error:
|
1879
|
+
log.warning(
|
1880
|
+
"Gerrit REST probe did not succeed (tried %d URLs): %s",
|
1881
|
+
len(api_candidates),
|
1882
|
+
last_error,
|
1727
1883
|
)
|
1728
|
-
if not base_path and status == 404:
|
1729
|
-
try:
|
1730
|
-
fallback_url = f"https://{host}/r/"
|
1731
|
-
_probe(fallback_url)
|
1732
|
-
except Exception as exc2:
|
1733
|
-
log.warning(
|
1734
|
-
"Gerrit REST probe did not succeed "
|
1735
|
-
"(including '/r' fallback): %s",
|
1736
|
-
exc2,
|
1737
|
-
)
|
1738
|
-
else:
|
1739
|
-
log.warning("Gerrit REST probe did not succeed: %s", exc)
|
1740
1884
|
|
1741
1885
|
# ---------------
|
1742
1886
|
# Helpers
|