github2gerrit 0.1.5__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 +86 -117
- github2gerrit/config.py +32 -24
- github2gerrit/core.py +425 -417
- github2gerrit/duplicate_detection.py +375 -193
- github2gerrit/gerrit_urls.py +256 -0
- github2gerrit/github_api.py +6 -17
- github2gerrit/gitutils.py +30 -13
- github2gerrit/models.py +1 -0
- github2gerrit/similarity.py +458 -0
- github2gerrit/ssh_discovery.py +20 -67
- {github2gerrit-0.1.5.dist-info → github2gerrit-0.1.6.dist-info}/METADATA +22 -25
- github2gerrit-0.1.6.dist-info/RECORD +17 -0
- github2gerrit-0.1.5.dist-info/RECORD +0 -15
- {github2gerrit-0.1.5.dist-info → github2gerrit-0.1.6.dist-info}/WHEEL +0 -0
- {github2gerrit-0.1.5.dist-info → github2gerrit-0.1.6.dist-info}/entry_points.txt +0 -0
- {github2gerrit-0.1.5.dist-info → github2gerrit-0.1.6.dist-info}/licenses/LICENSE +0 -0
- {github2gerrit-0.1.5.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,6 +39,7 @@ from dataclasses import dataclass
|
|
38
39
|
from pathlib import Path
|
39
40
|
from typing import Any
|
40
41
|
|
42
|
+
from .gerrit_urls import create_gerrit_url_builder
|
41
43
|
from .github_api import build_client
|
42
44
|
from .github_api import close_pr
|
43
45
|
from .github_api import create_pr_comment
|
@@ -80,9 +82,7 @@ def _is_verbose_mode() -> bool:
|
|
80
82
|
return os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes")
|
81
83
|
|
82
84
|
|
83
|
-
def _log_exception_conditionally(
|
84
|
-
logger: logging.Logger, message: str, *args: Any
|
85
|
-
) -> None:
|
85
|
+
def _log_exception_conditionally(logger: logging.Logger, message: str, *args: Any) -> None:
|
86
86
|
"""Log exception with traceback only if verbose mode is enabled."""
|
87
87
|
if _is_verbose_mode():
|
88
88
|
logger.exception(message, *args)
|
@@ -125,10 +125,7 @@ def _insert_issue_id_into_commit_message(message: str, issue_id: str) -> str:
|
|
125
125
|
raise ValueError(_MSG_ISSUE_ID_MULTILINE)
|
126
126
|
|
127
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}"
|
128
|
+
issue_line = cleaned_issue_id if cleaned_issue_id.startswith("Issue-ID:") else f"Issue-ID: {cleaned_issue_id}"
|
132
129
|
|
133
130
|
lines = message.splitlines()
|
134
131
|
if not lines:
|
@@ -263,14 +260,34 @@ class Orchestrator:
|
|
263
260
|
|
264
261
|
if inputs.dry_run:
|
265
262
|
# Perform preflight validations and exit without making changes
|
266
|
-
self._dry_run_preflight(
|
267
|
-
gerrit=gerrit, inputs=inputs, gh=gh, repo=repo_names
|
268
|
-
)
|
263
|
+
self._dry_run_preflight(gerrit=gerrit, inputs=inputs, gh=gh, repo=repo_names)
|
269
264
|
log.info("Dry run complete; skipping write operations to Gerrit")
|
270
|
-
return SubmissionResult(
|
271
|
-
change_urls=[], change_numbers=[], commit_shas=[]
|
272
|
-
)
|
265
|
+
return SubmissionResult(change_urls=[], change_numbers=[], commit_shas=[])
|
273
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,
|
279
|
+
)
|
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)
|
274
291
|
|
275
292
|
if inputs.submit_single_commits:
|
276
293
|
prep = self._prepare_single_commits(inputs, gh, gerrit)
|
@@ -354,13 +371,8 @@ class Orchestrator:
|
|
354
371
|
repo_obj: Any = get_repo_from_env(client)
|
355
372
|
# Prefer a specific ref when available; otherwise default branch
|
356
373
|
ref = os.getenv("GITHUB_HEAD_REF") or os.getenv("GITHUB_SHA")
|
357
|
-
if ref
|
358
|
-
|
359
|
-
else:
|
360
|
-
content = repo_obj.get_contents(".gitreview")
|
361
|
-
text_remote = (
|
362
|
-
getattr(content, "decoded_content", b"") or b""
|
363
|
-
).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")
|
364
376
|
info_remote = self._parse_gitreview_text(text_remote)
|
365
377
|
if info_remote:
|
366
378
|
log.debug("Parsed remote .gitreview: %s", info_remote)
|
@@ -370,14 +382,7 @@ class Orchestrator:
|
|
370
382
|
log.debug("Remote .gitreview not available: %s", exc)
|
371
383
|
# Attempt raw.githubusercontent.com as a fallback
|
372
384
|
try:
|
373
|
-
repo_full = (
|
374
|
-
(
|
375
|
-
gh.repository
|
376
|
-
if gh
|
377
|
-
else os.getenv("GITHUB_REPOSITORY", "")
|
378
|
-
)
|
379
|
-
or ""
|
380
|
-
).strip()
|
385
|
+
repo_full = ((gh.repository if gh else os.getenv("GITHUB_REPOSITORY", "")) or "").strip()
|
381
386
|
branches: list[str] = []
|
382
387
|
# Prefer PR head/base refs via GitHub API when running
|
383
388
|
# from a direct URL when a token is available
|
@@ -391,18 +396,8 @@ class Orchestrator:
|
|
391
396
|
client = build_client()
|
392
397
|
repo_obj = get_repo_from_env(client)
|
393
398
|
pr_obj = get_pull(repo_obj, int(gh.pr_number))
|
394
|
-
api_head = str(
|
395
|
-
|
396
|
-
getattr(pr_obj, "head", object()), "ref", ""
|
397
|
-
)
|
398
|
-
or ""
|
399
|
-
)
|
400
|
-
api_base = str(
|
401
|
-
getattr(
|
402
|
-
getattr(pr_obj, "base", object()), "ref", ""
|
403
|
-
)
|
404
|
-
or ""
|
405
|
-
)
|
399
|
+
api_head = str(getattr(getattr(pr_obj, "head", object()), "ref", "") or "")
|
400
|
+
api_base = str(getattr(getattr(pr_obj, "base", object()), "ref", "") or "")
|
406
401
|
if api_head:
|
407
402
|
branches.append(api_head)
|
408
403
|
if api_base:
|
@@ -422,15 +417,9 @@ class Orchestrator:
|
|
422
417
|
if not br or br in tried:
|
423
418
|
continue
|
424
419
|
tried.add(br)
|
425
|
-
url =
|
426
|
-
f"https://raw.githubusercontent.com/"
|
427
|
-
f"{repo_full}/refs/heads/{br}/.gitreview"
|
428
|
-
)
|
420
|
+
url = f"https://raw.githubusercontent.com/{repo_full}/refs/heads/{br}/.gitreview"
|
429
421
|
parsed = urllib.parse.urlparse(url)
|
430
|
-
if
|
431
|
-
parsed.scheme != "https"
|
432
|
-
or parsed.netloc != "raw.githubusercontent.com"
|
433
|
-
):
|
422
|
+
if parsed.scheme != "https" or parsed.netloc != "raw.githubusercontent.com":
|
434
423
|
continue
|
435
424
|
log.info("Fetching .gitreview via raw URL: %s", url)
|
436
425
|
with urllib.request.urlopen(url, timeout=5) as resp: # noqa: S310
|
@@ -547,13 +536,8 @@ class Orchestrator:
|
|
547
536
|
|
548
537
|
# Auto-discover host keys if not provided
|
549
538
|
effective_known_hosts = inputs.gerrit_known_hosts
|
550
|
-
if
|
551
|
-
not
|
552
|
-
and auto_discover_gerrit_host_keys is not None
|
553
|
-
):
|
554
|
-
log.info(
|
555
|
-
"GERRIT_KNOWN_HOSTS not provided, attempting auto-discovery..."
|
556
|
-
)
|
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...")
|
557
541
|
try:
|
558
542
|
discovered_keys = auto_discover_gerrit_host_keys(
|
559
543
|
gerrit_hostname=gerrit.host,
|
@@ -569,18 +553,12 @@ class Orchestrator:
|
|
569
553
|
gerrit.port,
|
570
554
|
)
|
571
555
|
else:
|
572
|
-
log.warning(
|
573
|
-
"Auto-discovery failed, SSH host key verification may "
|
574
|
-
"fail"
|
575
|
-
)
|
556
|
+
log.warning("Auto-discovery failed, SSH host key verification may fail")
|
576
557
|
except Exception as exc:
|
577
558
|
log.warning("SSH host key auto-discovery failed: %s", exc)
|
578
559
|
|
579
560
|
if not effective_known_hosts:
|
580
|
-
log.debug(
|
581
|
-
"No SSH host keys available (manual or auto-discovered), "
|
582
|
-
"skipping SSH setup"
|
583
|
-
)
|
561
|
+
log.debug("No SSH host keys available (manual or auto-discovered), skipping SSH setup")
|
584
562
|
return
|
585
563
|
|
586
564
|
log.info("Setting up temporary SSH configuration for Gerrit")
|
@@ -624,9 +602,13 @@ class Orchestrator:
|
|
624
602
|
|
625
603
|
# Build SSH command with strict options to prevent key scanning
|
626
604
|
ssh_options = [
|
605
|
+
"-F /dev/null",
|
627
606
|
f"-i {self._ssh_key_path}",
|
628
607
|
f"-o UserKnownHostsFile={self._ssh_known_hosts_path}",
|
629
608
|
"-o IdentitiesOnly=yes", # Critical: prevents SSH agent scanning
|
609
|
+
"-o IdentityAgent=none",
|
610
|
+
"-o BatchMode=yes",
|
611
|
+
"-o PreferredAuthentications=publickey",
|
630
612
|
"-o StrictHostKeyChecking=yes",
|
631
613
|
"-o PasswordAuthentication=no",
|
632
614
|
"-o PubkeyAcceptedKeyTypes=+ssh-rsa",
|
@@ -638,15 +620,35 @@ class Orchestrator:
|
|
638
620
|
log.debug("Generated SSH command: %s", masked_cmd)
|
639
621
|
return ssh_cmd
|
640
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
|
+
|
641
645
|
def _cleanup_ssh(self) -> None:
|
642
646
|
"""Clean up temporary SSH files created by this tool.
|
643
647
|
|
644
648
|
Removes the workspace-specific .ssh-g2g directory and all contents.
|
645
649
|
This ensures no temporary files are left behind.
|
646
650
|
"""
|
647
|
-
if not hasattr(self, "_ssh_key_path") or not hasattr(
|
648
|
-
self, "_ssh_known_hosts_path"
|
649
|
-
):
|
651
|
+
if not hasattr(self, "_ssh_key_path") or not hasattr(self, "_ssh_known_hosts_path"):
|
650
652
|
return
|
651
653
|
|
652
654
|
try:
|
@@ -656,9 +658,7 @@ class Orchestrator:
|
|
656
658
|
import shutil
|
657
659
|
|
658
660
|
shutil.rmtree(tool_ssh_dir)
|
659
|
-
log.debug(
|
660
|
-
"Cleaned up temporary SSH directory: %s", tool_ssh_dir
|
661
|
-
)
|
661
|
+
log.debug("Cleaned up temporary SSH directory: %s", tool_ssh_dir)
|
662
662
|
except Exception as exc:
|
663
663
|
log.warning("Failed to clean up temporary SSH files: %s", exc)
|
664
664
|
|
@@ -678,9 +678,7 @@ class Orchestrator:
|
|
678
678
|
cwd=self.workspace,
|
679
679
|
)
|
680
680
|
except GitError:
|
681
|
-
git_config(
|
682
|
-
"gitreview.username", inputs.gerrit_ssh_user_g2g, global_=True
|
683
|
-
)
|
681
|
+
git_config("gitreview.username", inputs.gerrit_ssh_user_g2g, global_=True)
|
684
682
|
try:
|
685
683
|
git_config(
|
686
684
|
"user.name",
|
@@ -698,9 +696,26 @@ class Orchestrator:
|
|
698
696
|
cwd=self.workspace,
|
699
697
|
)
|
700
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:
|
702
|
+
git_config(
|
703
|
+
"commit.gpgsign",
|
704
|
+
"false",
|
705
|
+
global_=False,
|
706
|
+
cwd=self.workspace,
|
707
|
+
)
|
708
|
+
except GitError:
|
709
|
+
git_config("commit.gpgsign", "false", global_=True)
|
710
|
+
try:
|
701
711
|
git_config(
|
702
|
-
"
|
712
|
+
"tag.gpgsign",
|
713
|
+
"false",
|
714
|
+
global_=False,
|
715
|
+
cwd=self.workspace,
|
703
716
|
)
|
717
|
+
except GitError:
|
718
|
+
git_config("tag.gpgsign", "false", global_=True)
|
704
719
|
|
705
720
|
# Ensure git-review host/port/project are configured
|
706
721
|
# when .gitreview is absent
|
@@ -736,16 +751,10 @@ class Orchestrator:
|
|
736
751
|
)
|
737
752
|
except CommandError:
|
738
753
|
ssh_user = inputs.gerrit_ssh_user_g2g.strip()
|
739
|
-
remote_url =
|
740
|
-
f"ssh://{ssh_user}@{gerrit.host}:{gerrit.port}/{gerrit.project}"
|
741
|
-
)
|
754
|
+
remote_url = f"ssh://{ssh_user}@{gerrit.host}:{gerrit.port}/{gerrit.project}"
|
742
755
|
log.info("Adding 'gerrit' remote: %s", remote_url)
|
743
756
|
# Use our specific SSH configuration for adding remote
|
744
|
-
env = (
|
745
|
-
{"GIT_SSH_COMMAND": self._git_ssh_command}
|
746
|
-
if self._git_ssh_command
|
747
|
-
else None
|
748
|
-
)
|
757
|
+
env = self._ssh_env()
|
749
758
|
run_cmd(
|
750
759
|
["git", "remote", "add", "gerrit", remote_url],
|
751
760
|
check=False,
|
@@ -754,9 +763,7 @@ class Orchestrator:
|
|
754
763
|
)
|
755
764
|
|
756
765
|
# Workaround for submodules commit-msg hook
|
757
|
-
hooks_path = run_cmd(
|
758
|
-
["git", "rev-parse", "--show-toplevel"], cwd=self.workspace
|
759
|
-
).stdout.strip()
|
766
|
+
hooks_path = run_cmd(["git", "rev-parse", "--show-toplevel"], cwd=self.workspace).stdout.strip()
|
760
767
|
try:
|
761
768
|
git_config(
|
762
769
|
"core.hooksPath",
|
@@ -772,11 +779,7 @@ class Orchestrator:
|
|
772
779
|
# Initialize git-review (copies commit-msg hook)
|
773
780
|
try:
|
774
781
|
# Use our specific SSH configuration for git-review setup
|
775
|
-
env = (
|
776
|
-
{"GIT_SSH_COMMAND": self._git_ssh_command}
|
777
|
-
if self._git_ssh_command
|
778
|
-
else None
|
779
|
-
)
|
782
|
+
env = self._ssh_env()
|
780
783
|
run_cmd(["git", "review", "-s", "-v"], cwd=self.workspace, env=env)
|
781
784
|
except CommandError as exc:
|
782
785
|
msg = f"Failed to initialize git-review: {exc}"
|
@@ -794,12 +797,12 @@ class Orchestrator:
|
|
794
797
|
# Determine commit range: commits in HEAD not in base branch
|
795
798
|
base_ref = f"origin/{branch}"
|
796
799
|
# Use our SSH command for git operations that might need SSH
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
800
|
+
|
801
|
+
run_cmd(
|
802
|
+
["git", "fetch", "origin", branch],
|
803
|
+
cwd=self.workspace,
|
804
|
+
env=self._ssh_env(),
|
801
805
|
)
|
802
|
-
run_cmd(["git", "fetch", "origin", branch], cwd=self.workspace, env=env)
|
803
806
|
revs = run_cmd(
|
804
807
|
["git", "rev-list", "--reverse", f"{base_ref}..HEAD"],
|
805
808
|
cwd=self.workspace,
|
@@ -809,14 +812,10 @@ class Orchestrator:
|
|
809
812
|
log.info("No commits to submit; returning empty PreparedChange")
|
810
813
|
return PreparedChange(change_ids=[], commit_shas=[])
|
811
814
|
# Create temp branch from base sha; export for downstream
|
812
|
-
base_sha = run_cmd(
|
813
|
-
["git", "rev-parse", base_ref], cwd=self.workspace
|
814
|
-
).stdout.strip()
|
815
|
+
base_sha = run_cmd(["git", "rev-parse", base_ref], cwd=self.workspace).stdout.strip()
|
815
816
|
tmp_branch = f"g2g_tmp_{gh.pr_number or 'pr'!s}_{os.getpid()}"
|
816
817
|
os.environ["G2G_TMP_BRANCH"] = tmp_branch
|
817
|
-
run_cmd(
|
818
|
-
["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace
|
819
|
-
)
|
818
|
+
run_cmd(["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace)
|
820
819
|
change_ids: list[str] = []
|
821
820
|
for csha in commit_list:
|
822
821
|
run_cmd(["git", "checkout", tmp_branch], cwd=self.workspace)
|
@@ -826,13 +825,9 @@ class Orchestrator:
|
|
826
825
|
["git", "show", "-s", "--pretty=format:%an <%ae>", csha],
|
827
826
|
cwd=self.workspace,
|
828
827
|
).stdout.strip()
|
829
|
-
git_commit_amend(
|
830
|
-
author=author, no_edit=True, signoff=True, cwd=self.workspace
|
831
|
-
)
|
828
|
+
git_commit_amend(author=author, no_edit=True, signoff=True, cwd=self.workspace)
|
832
829
|
# Extract newly added Change-Id from last commit trailers
|
833
|
-
trailers = git_last_commit_trailers(
|
834
|
-
keys=["Change-Id"], cwd=self.workspace
|
835
|
-
)
|
830
|
+
trailers = git_last_commit_trailers(keys=["Change-Id"], cwd=self.workspace)
|
836
831
|
for cid in trailers.get("Change-Id", []):
|
837
832
|
if cid:
|
838
833
|
change_ids.append(cid)
|
@@ -866,26 +861,20 @@ class Orchestrator:
|
|
866
861
|
"""Squash PR commits into a single commit and handle Change-Id."""
|
867
862
|
log.info("Preparing squashed commit for PR #%s", gh.pr_number)
|
868
863
|
branch = self._resolve_target_branch()
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
864
|
+
|
865
|
+
run_cmd(
|
866
|
+
["git", "fetch", "origin", branch],
|
867
|
+
cwd=self.workspace,
|
868
|
+
env=self._ssh_env(),
|
873
869
|
)
|
874
|
-
run_cmd(["git", "fetch", "origin", branch], cwd=self.workspace, env=env)
|
875
870
|
base_ref = f"origin/{branch}"
|
876
|
-
base_sha = run_cmd(
|
877
|
-
|
878
|
-
).stdout.strip()
|
879
|
-
head_sha = run_cmd(
|
880
|
-
["git", "rev-parse", "HEAD"], cwd=self.workspace
|
881
|
-
).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()
|
882
873
|
|
883
874
|
# Create temp branch from base and merge-squash PR head
|
884
875
|
tmp_branch = f"g2g_tmp_{gh.pr_number or 'pr'!s}_{os.getpid()}"
|
885
876
|
os.environ["G2G_TMP_BRANCH"] = tmp_branch
|
886
|
-
run_cmd(
|
887
|
-
["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace
|
888
|
-
)
|
877
|
+
run_cmd(["git", "checkout", "-b", tmp_branch, base_sha], cwd=self.workspace)
|
889
878
|
run_cmd(["git", "merge", "--squash", head_sha], cwd=self.workspace)
|
890
879
|
|
891
880
|
def _collect_log_lines() -> list[str]:
|
@@ -913,18 +902,13 @@ class Orchestrator:
|
|
913
902
|
message_lines: list[str] = []
|
914
903
|
in_metadata_section = False
|
915
904
|
for ln in lines:
|
916
|
-
if ln.strip() in ("---", "```") or ln.startswith(
|
917
|
-
"updated-dependencies:"
|
918
|
-
):
|
905
|
+
if ln.strip() in ("---", "```") or ln.startswith("updated-dependencies:"):
|
919
906
|
in_metadata_section = True
|
920
907
|
continue
|
921
908
|
if in_metadata_section:
|
922
909
|
if ln.startswith(("- dependency-", " dependency-")):
|
923
910
|
continue
|
924
|
-
if (
|
925
|
-
not ln.startswith((" ", "-", "dependency-"))
|
926
|
-
and ln.strip()
|
927
|
-
):
|
911
|
+
if not ln.startswith((" ", "-", "dependency-")) and ln.strip():
|
928
912
|
in_metadata_section = False
|
929
913
|
if ln.startswith("Change-Id:"):
|
930
914
|
cid = ln.split(":", 1)[1].strip()
|
@@ -955,17 +939,11 @@ class Orchestrator:
|
|
955
939
|
break_points = [". ", "! ", "? ", " - ", ": "]
|
956
940
|
for bp in break_points:
|
957
941
|
if bp in title_line[:100]:
|
958
|
-
title_line = title_line[
|
959
|
-
: title_line.index(bp) + len(bp.strip())
|
960
|
-
]
|
942
|
+
title_line = title_line[: title_line.index(bp) + len(bp.strip())]
|
961
943
|
break
|
962
944
|
else:
|
963
945
|
words = title_line[:100].split()
|
964
|
-
title_line = (
|
965
|
-
" ".join(words[:-1])
|
966
|
-
if len(words) > 1
|
967
|
-
else title_line[:100].rstrip()
|
968
|
-
)
|
946
|
+
title_line = " ".join(words[:-1]) if len(words) > 1 else title_line[:100].rstrip()
|
969
947
|
return title_line
|
970
948
|
|
971
949
|
def _build_clean_message_lines(message_lines: list[str]) -> list[str]:
|
@@ -975,10 +953,7 @@ class Orchestrator:
|
|
975
953
|
out: list[str] = [title_line]
|
976
954
|
if len(message_lines) > 1:
|
977
955
|
body_start = 1
|
978
|
-
while (
|
979
|
-
body_start < len(message_lines)
|
980
|
-
and not message_lines[body_start].strip()
|
981
|
-
):
|
956
|
+
while body_start < len(message_lines) and not message_lines[body_start].strip():
|
982
957
|
body_start += 1
|
983
958
|
if body_start < len(message_lines):
|
984
959
|
out.append("")
|
@@ -987,9 +962,7 @@ class Orchestrator:
|
|
987
962
|
|
988
963
|
def _maybe_reuse_change_id(pr_str: str) -> str:
|
989
964
|
reuse = ""
|
990
|
-
sync_all_prs = (
|
991
|
-
os.getenv("SYNC_ALL_OPEN_PRS", "false").lower() == "true"
|
992
|
-
)
|
965
|
+
sync_all_prs = os.getenv("SYNC_ALL_OPEN_PRS", "false").lower() == "true"
|
993
966
|
if (
|
994
967
|
not sync_all_prs
|
995
968
|
and gh.event_name == "pull_request_target"
|
@@ -999,9 +972,7 @@ class Orchestrator:
|
|
999
972
|
client = build_client()
|
1000
973
|
repo = get_repo_from_env(client)
|
1001
974
|
pr_obj = get_pull(repo, int(pr_str))
|
1002
|
-
cand = get_recent_change_ids_from_comments(
|
1003
|
-
pr_obj, max_comments=50
|
1004
|
-
)
|
975
|
+
cand = get_recent_change_ids_from_comments(pr_obj, max_comments=50)
|
1005
976
|
if cand:
|
1006
977
|
reuse = cand[-1]
|
1007
978
|
log.debug(
|
@@ -1023,12 +994,8 @@ class Orchestrator:
|
|
1023
994
|
signed_off: list[str],
|
1024
995
|
reuse_cid: str,
|
1025
996
|
) -> str:
|
1026
|
-
from .duplicate_detection import DuplicateDetector
|
1027
|
-
|
1028
997
|
msg = "\n".join(lines_in).strip()
|
1029
998
|
msg = _insert_issue_id_into_commit_message(msg, inputs.issue_id)
|
1030
|
-
github_hash = DuplicateDetector._generate_github_change_hash(gh)
|
1031
|
-
msg += f"\n\nGitHub-Hash: {github_hash}"
|
1032
999
|
if signed_off:
|
1033
1000
|
msg += "\n\n" + "\n".join(signed_off)
|
1034
1001
|
if reuse_cid:
|
@@ -1037,9 +1004,7 @@ class Orchestrator:
|
|
1037
1004
|
|
1038
1005
|
# Build message parts
|
1039
1006
|
raw_lines = _collect_log_lines()
|
1040
|
-
message_lines, signed_off, _existing_cids = _parse_message_parts(
|
1041
|
-
raw_lines
|
1042
|
-
)
|
1007
|
+
message_lines, signed_off, _existing_cids = _parse_message_parts(raw_lines)
|
1043
1008
|
clean_lines = _build_clean_message_lines(message_lines)
|
1044
1009
|
pr_str = str(gh.pr_number or "").strip()
|
1045
1010
|
reuse_cid = _maybe_reuse_change_id(pr_str)
|
@@ -1113,29 +1078,51 @@ class Orchestrator:
|
|
1113
1078
|
title = re.sub(r"[*_`]", "", title)
|
1114
1079
|
title = title.strip()
|
1115
1080
|
|
1116
|
-
# Compose message; preserve
|
1117
|
-
|
1118
|
-
|
1119
|
-
|
1120
|
-
|
1121
|
-
|
1122
|
-
]
|
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
|
+
|
1123
1090
|
msg_parts = [title, "", body] if title or body else [current_body]
|
1124
1091
|
commit_message = "\n".join(msg_parts).strip()
|
1125
1092
|
|
1126
1093
|
# Add Issue-ID if provided
|
1127
|
-
commit_message = _insert_issue_id_into_commit_message(
|
1128
|
-
|
1129
|
-
)
|
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)
|
1130
1116
|
|
1131
|
-
if signed:
|
1132
|
-
commit_message += "\n\n" + "\n".join(signed)
|
1133
1117
|
author = run_cmd(
|
1134
|
-
["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(),
|
1135
1121
|
).stdout.strip()
|
1136
1122
|
git_commit_amend(
|
1123
|
+
cwd=self.workspace,
|
1137
1124
|
no_edit=False,
|
1138
|
-
signoff=not bool(
|
1125
|
+
signoff=not bool(signed_lines),
|
1139
1126
|
author=author,
|
1140
1127
|
message=commit_message,
|
1141
1128
|
)
|
@@ -1163,10 +1150,7 @@ class Orchestrator:
|
|
1163
1150
|
run_cmd(["git", "checkout", tmp_branch], cwd=self.workspace)
|
1164
1151
|
prefix = os.getenv("G2G_TOPIC_PREFIX", "GH").strip() or "GH"
|
1165
1152
|
pr_num = os.getenv("PR_NUMBER", "").strip()
|
1166
|
-
if pr_num
|
1167
|
-
topic = f"{prefix}-{repo.project_github}-{pr_num}"
|
1168
|
-
else:
|
1169
|
-
topic = f"{prefix}-{repo.project_github}"
|
1153
|
+
topic = f"{prefix}-{repo.project_github}-{pr_num}" if pr_num else f"{prefix}-{repo.project_github}"
|
1170
1154
|
try:
|
1171
1155
|
args = [
|
1172
1156
|
"git",
|
@@ -1176,33 +1160,22 @@ class Orchestrator:
|
|
1176
1160
|
"-t",
|
1177
1161
|
topic,
|
1178
1162
|
]
|
1179
|
-
revs = [
|
1180
|
-
r.strip() for r in (reviewers or "").split(",") if r.strip()
|
1181
|
-
]
|
1163
|
+
revs = [r.strip() for r in (reviewers or "").split(",") if r.strip()]
|
1182
1164
|
for r in revs:
|
1183
1165
|
args.extend(["--reviewer", r])
|
1184
1166
|
# Branch as positional argument (not a flag)
|
1185
1167
|
args.append(branch)
|
1186
1168
|
|
1187
1169
|
# Use our specific SSH configuration
|
1188
|
-
env = (
|
1189
|
-
{"GIT_SSH_COMMAND": self._git_ssh_command}
|
1190
|
-
if self._git_ssh_command
|
1191
|
-
else None
|
1192
|
-
)
|
1170
|
+
env = self._ssh_env()
|
1193
1171
|
log.debug("Executing git review command: %s", " ".join(args))
|
1194
1172
|
run_cmd(args, cwd=self.workspace, env=env)
|
1195
1173
|
log.info("Successfully pushed changes to Gerrit")
|
1196
1174
|
except CommandError as exc:
|
1197
1175
|
# Analyze the specific failure reason from git review output
|
1198
1176
|
error_details = self._analyze_gerrit_push_failure(exc)
|
1199
|
-
_log_exception_conditionally(
|
1200
|
-
|
1201
|
-
)
|
1202
|
-
msg = (
|
1203
|
-
f"Failed to push changes to Gerrit with git-review: "
|
1204
|
-
f"{error_details}"
|
1205
|
-
)
|
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}"
|
1206
1179
|
raise OrchestratorError(msg) from exc
|
1207
1180
|
# Cleanup temporary branch used during preparation
|
1208
1181
|
tmp_branch = (os.getenv("G2G_TMP_BRANCH", "") or "").strip()
|
@@ -1212,11 +1185,13 @@ class Orchestrator:
|
|
1212
1185
|
["git", "checkout", f"origin/{branch}"],
|
1213
1186
|
check=False,
|
1214
1187
|
cwd=self.workspace,
|
1188
|
+
env=env,
|
1215
1189
|
)
|
1216
1190
|
run_cmd(
|
1217
1191
|
["git", "branch", "-D", tmp_branch],
|
1218
1192
|
check=False,
|
1219
1193
|
cwd=self.workspace,
|
1194
|
+
env=env,
|
1220
1195
|
)
|
1221
1196
|
|
1222
1197
|
def _analyze_gerrit_push_failure(self, exc: CommandError) -> str:
|
@@ -1242,10 +1217,7 @@ class Orchestrator:
|
|
1242
1217
|
"'ssh-keyscan -p 29418 <gerrit-host>' "
|
1243
1218
|
"to get the current host keys."
|
1244
1219
|
)
|
1245
|
-
elif
|
1246
|
-
"authenticity of host" in combined_lower
|
1247
|
-
and "can't be established" in combined_lower
|
1248
|
-
):
|
1220
|
+
elif "authenticity of host" in combined_lower and "can't be established" in combined_lower:
|
1249
1221
|
return (
|
1250
1222
|
"SSH host key unknown. The GERRIT_KNOWN_HOSTS value does not "
|
1251
1223
|
"contain the host key for the Gerrit server. "
|
@@ -1255,37 +1227,18 @@ class Orchestrator:
|
|
1255
1227
|
"'ssh-keyscan -p 29418 <gerrit-host>' to get the host keys."
|
1256
1228
|
)
|
1257
1229
|
# Check for specific SSH key issues before general permission denied
|
1258
|
-
elif
|
1259
|
-
"
|
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
|
-
)
|
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."
|
1266
1232
|
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
|
-
)
|
1233
|
+
return "SSH key type not supported by server. The server may not accept this SSH key algorithm."
|
1271
1234
|
elif "authentication failed" in combined_lower:
|
1272
|
-
return
|
1273
|
-
"SSH authentication failed - check SSH key, username, and "
|
1274
|
-
"server configuration"
|
1275
|
-
)
|
1235
|
+
return "SSH authentication failed - check SSH key, username, and server configuration"
|
1276
1236
|
# Check for connection timeout/refused before "could not read" check
|
1277
|
-
elif
|
1278
|
-
"
|
1279
|
-
or "connection refused" in combined_lower
|
1280
|
-
):
|
1281
|
-
return (
|
1282
|
-
"Connection failed - check network connectivity and "
|
1283
|
-
"Gerrit server availability"
|
1284
|
-
)
|
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"
|
1285
1239
|
# Check for specific SSH publickey-only authentication failures
|
1286
1240
|
elif "permission denied (publickey)" in combined_lower and not any(
|
1287
|
-
auth_method in combined_lower
|
1288
|
-
for auth_method in ["gssapi", "password", "keyboard"]
|
1241
|
+
auth_method in combined_lower for auth_method in ["gssapi", "password", "keyboard"]
|
1289
1242
|
):
|
1290
1243
|
return (
|
1291
1244
|
"SSH public key authentication failed. The SSH key may be "
|
@@ -1295,19 +1248,13 @@ class Orchestrator:
|
|
1295
1248
|
elif "permission denied" in combined_lower:
|
1296
1249
|
return "SSH permission denied - check SSH key and user permissions"
|
1297
1250
|
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
|
-
)
|
1251
|
+
return "Could not read from remote repository - check SSH authentication and repository access permissions"
|
1302
1252
|
# Check for Gerrit-specific issues
|
1303
1253
|
elif "missing issue-id" in combined_lower:
|
1304
1254
|
return "Missing Issue-ID in commit message."
|
1305
1255
|
elif "commit not associated to any issue" in combined_lower:
|
1306
1256
|
return "Commit not associated to any issue."
|
1307
|
-
elif
|
1308
|
-
"remote rejected" in combined_lower
|
1309
|
-
and "refs/for/" in combined_lower
|
1310
|
-
):
|
1257
|
+
elif "remote rejected" in combined_lower and "refs/for/" in combined_lower:
|
1311
1258
|
# Extract specific rejection reason from output
|
1312
1259
|
lines = combined_output.split("\n")
|
1313
1260
|
for line in lines:
|
@@ -1330,28 +1277,28 @@ class Orchestrator:
|
|
1330
1277
|
) -> SubmissionResult:
|
1331
1278
|
"""Query Gerrit for change URL/number and patchset sha via REST."""
|
1332
1279
|
log.info("Querying Gerrit for submitted change(s) via REST")
|
1333
|
-
|
1334
|
-
|
1335
|
-
|
1336
|
-
|
1337
|
-
|
1338
|
-
|
1339
|
-
)
|
1340
|
-
http_user = (
|
1341
|
-
os.getenv("GERRIT_HTTP_USER", "").strip()
|
1342
|
-
or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
|
1343
|
-
)
|
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()
|
1344
1286
|
http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
|
1287
|
+
|
1288
|
+
# Build Gerrit REST client (prefer HTTP basic auth if provided)
|
1345
1289
|
if GerritRestAPI is None:
|
1346
1290
|
raise OrchestratorError(_MSG_PYGERRIT2_REQUIRED_REST)
|
1347
|
-
|
1348
|
-
|
1349
|
-
|
1350
|
-
|
1351
|
-
|
1352
|
-
|
1353
|
-
|
1354
|
-
|
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)
|
1355
1302
|
urls: list[str] = []
|
1356
1303
|
nums: list[str] = []
|
1357
1304
|
shas: list[str] = []
|
@@ -1362,56 +1309,44 @@ class Orchestrator:
|
|
1362
1309
|
# include current revision
|
1363
1310
|
query = f"limit:1 is:open project:{repo.project_gerrit} {cid}"
|
1364
1311
|
path = f"/changes/?q={query}&o=CURRENT_REVISION&n=1"
|
1365
|
-
|
1366
|
-
|
1367
|
-
|
1368
|
-
|
1369
|
-
|
1370
|
-
|
1371
|
-
|
1372
|
-
|
1373
|
-
|
1374
|
-
|
1375
|
-
|
1376
|
-
|
1377
|
-
|
1378
|
-
|
1379
|
-
if http_user and http_pass:
|
1380
|
-
if HTTPBasicAuth is None:
|
1381
|
-
log.warning(
|
1382
|
-
"pygerrit2 auth missing; skipping fallback"
|
1383
|
-
)
|
1384
|
-
continue
|
1385
|
-
rest_fallback = GerritRestAPI(
|
1386
|
-
url=fallback_url,
|
1387
|
-
auth=HTTPBasicAuth(http_user, http_pass),
|
1388
|
-
)
|
1389
|
-
else:
|
1390
|
-
rest_fallback = GerritRestAPI(url=fallback_url)
|
1391
|
-
changes = rest_fallback.get(path)
|
1392
|
-
except Exception as exc2:
|
1393
|
-
log.warning(
|
1394
|
-
"Failed to query change via REST for %s "
|
1395
|
-
"(including '/r' fallback): %s",
|
1396
|
-
cid,
|
1397
|
-
exc2,
|
1398
|
-
)
|
1399
|
-
continue
|
1400
|
-
else:
|
1401
|
-
log.warning(
|
1402
|
-
"Failed to query change via REST for %s: %s", cid, exc
|
1403
|
-
)
|
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)
|
1404
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
|
1405
1336
|
if not changes:
|
1406
1337
|
continue
|
1407
1338
|
change = changes[0]
|
1408
|
-
|
1409
|
-
|
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
|
1410
1346
|
# Construct a stable web URL for the change
|
1411
1347
|
if num:
|
1412
|
-
|
1413
|
-
|
1414
|
-
)
|
1348
|
+
change_url = url_builder.change_url(repo.project_gerrit, int(num))
|
1349
|
+
urls.append(change_url)
|
1415
1350
|
nums.append(num)
|
1416
1351
|
if current_rev:
|
1417
1352
|
shas.append(current_rev)
|
@@ -1422,9 +1357,7 @@ class Orchestrator:
|
|
1422
1357
|
os.environ["GERRIT_CHANGE_REQUEST_NUM"] = "\n".join(nums)
|
1423
1358
|
if shas:
|
1424
1359
|
os.environ["GERRIT_COMMIT_SHA"] = "\n".join(shas)
|
1425
|
-
return SubmissionResult(
|
1426
|
-
change_urls=urls, change_numbers=nums, commit_shas=shas
|
1427
|
-
)
|
1360
|
+
return SubmissionResult(change_urls=urls, change_numbers=nums, commit_shas=shas)
|
1428
1361
|
|
1429
1362
|
def _setup_git_workspace(self, inputs: Inputs, gh: GitHubContext) -> None:
|
1430
1363
|
"""Initialize and set up git workspace for PR processing."""
|
@@ -1445,10 +1378,7 @@ class Orchestrator:
|
|
1445
1378
|
|
1446
1379
|
# Fetch PR head
|
1447
1380
|
if gh.pr_number:
|
1448
|
-
pr_ref =
|
1449
|
-
f"refs/pull/{gh.pr_number}/head:"
|
1450
|
-
f"refs/remotes/origin/pr/{gh.pr_number}/head"
|
1451
|
-
)
|
1381
|
+
pr_ref = f"refs/pull/{gh.pr_number}/head:refs/remotes/origin/pr/{gh.pr_number}/head"
|
1452
1382
|
run_cmd(
|
1453
1383
|
[
|
1454
1384
|
"git",
|
@@ -1477,13 +1407,48 @@ class Orchestrator:
|
|
1477
1407
|
# Download commit-msg hook using SSH
|
1478
1408
|
try:
|
1479
1409
|
# Use curl to download the hook (more reliable than scp)
|
1480
|
-
|
1481
|
-
|
1482
|
-
|
1483
|
-
|
1484
|
-
|
1485
|
-
|
1486
|
-
|
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()
|
1487
1452
|
|
1488
1453
|
# Make hook executable
|
1489
1454
|
hook_path.chmod(hook_path.stat().st_mode | stat.S_IEXEC)
|
@@ -1494,34 +1459,55 @@ class Orchestrator:
|
|
1494
1459
|
msg = f"Could not install commit-msg hook: {exc}"
|
1495
1460
|
raise OrchestratorError(msg) from exc
|
1496
1461
|
|
1497
|
-
def _ensure_change_id_present(
|
1498
|
-
self, gerrit: GerritInfo, author: str
|
1499
|
-
) -> list[str]:
|
1462
|
+
def _ensure_change_id_present(self, gerrit: GerritInfo, author: str) -> list[str]:
|
1500
1463
|
"""Ensure the last commit has a Change-Id.
|
1501
1464
|
|
1502
1465
|
Installs the commit-msg hook and amends the commit if needed.
|
1503
1466
|
"""
|
1504
|
-
trailers = git_last_commit_trailers(
|
1505
|
-
keys=["Change-Id"], cwd=self.workspace
|
1506
|
-
)
|
1467
|
+
trailers = git_last_commit_trailers(keys=["Change-Id"], cwd=self.workspace)
|
1507
1468
|
if not trailers.get("Change-Id"):
|
1508
|
-
log.debug(
|
1509
|
-
|
1510
|
-
|
1511
|
-
|
1512
|
-
|
1513
|
-
|
1514
|
-
|
1515
|
-
|
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
|
+
)
|
1516
1504
|
# Debug: Check commit message after amend
|
1517
1505
|
actual_msg = run_cmd(
|
1518
1506
|
["git", "show", "-s", "--pretty=format:%B", "HEAD"],
|
1519
1507
|
cwd=self.workspace,
|
1520
1508
|
).stdout.strip()
|
1521
1509
|
log.debug("Commit message after amend:\n%s", actual_msg)
|
1522
|
-
trailers = git_last_commit_trailers(
|
1523
|
-
keys=["Change-Id"], cwd=self.workspace
|
1524
|
-
)
|
1510
|
+
trailers = git_last_commit_trailers(keys=["Change-Id"], cwd=self.workspace)
|
1525
1511
|
return [c for c in trailers.get("Change-Id", []) if c]
|
1526
1512
|
|
1527
1513
|
def _add_backref_comment_in_gerrit(
|
@@ -1544,21 +1530,14 @@ class Orchestrator:
|
|
1544
1530
|
"1",
|
1545
1531
|
"yes",
|
1546
1532
|
):
|
1547
|
-
log.info(
|
1548
|
-
"Skipping back-reference comments "
|
1549
|
-
"(G2G_SKIP_GERRIT_COMMENTS=true)"
|
1550
|
-
)
|
1533
|
+
log.info("Skipping back-reference comments (G2G_SKIP_GERRIT_COMMENTS=true)")
|
1551
1534
|
return
|
1552
1535
|
|
1553
1536
|
log.info("Adding back-reference comment in Gerrit")
|
1554
1537
|
user = os.getenv("GERRIT_SSH_USER_G2G", "")
|
1555
1538
|
server = gerrit.host
|
1556
1539
|
pr_url = f"{gh.server_url}/{gh.repository}/pull/{gh.pr_number}"
|
1557
|
-
run_url =
|
1558
|
-
f"{gh.server_url}/{gh.repository}/actions/runs/{gh.run_id}"
|
1559
|
-
if gh.run_id
|
1560
|
-
else "N/A"
|
1561
|
-
)
|
1540
|
+
run_url = f"{gh.server_url}/{gh.repository}/actions/runs/{gh.run_id}" if gh.run_id else "N/A"
|
1562
1541
|
message = f"GHPR: {pr_url} | Action-Run: {run_url}"
|
1563
1542
|
log.info("Adding back-reference comment: %s", message)
|
1564
1543
|
for csha in commit_shas:
|
@@ -1566,39 +1545,84 @@ class Orchestrator:
|
|
1566
1545
|
continue
|
1567
1546
|
try:
|
1568
1547
|
log.debug("Executing SSH command for commit %s", csha)
|
1569
|
-
# Build SSH command
|
1570
|
-
|
1571
|
-
|
1572
|
-
|
1573
|
-
|
1574
|
-
|
1575
|
-
|
1576
|
-
|
1577
|
-
|
1578
|
-
|
1579
|
-
|
1580
|
-
|
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
|
+
]
|
1581
1586
|
else:
|
1582
|
-
|
1583
|
-
|
1584
|
-
|
1585
|
-
|
1586
|
-
|
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),
|
1587
1608
|
f"{user}@{server}",
|
1588
|
-
|
1589
|
-
|
1590
|
-
|
1591
|
-
|
1592
|
-
|
1593
|
-
|
1594
|
-
|
1595
|
-
|
1596
|
-
|
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
|
+
),
|
1597
1618
|
]
|
1598
|
-
)
|
1599
1619
|
|
1600
1620
|
log.debug("Final SSH command: %s", " ".join(ssh_cmd))
|
1601
|
-
run_cmd(
|
1621
|
+
run_cmd(
|
1622
|
+
ssh_cmd,
|
1623
|
+
cwd=self.workspace,
|
1624
|
+
env=self._ssh_env(),
|
1625
|
+
)
|
1602
1626
|
log.info(
|
1603
1627
|
"Successfully added back-reference comment for %s: %s",
|
1604
1628
|
csha,
|
@@ -1606,8 +1630,7 @@ class Orchestrator:
|
|
1606
1630
|
)
|
1607
1631
|
except CommandError as exc:
|
1608
1632
|
log.warning(
|
1609
|
-
"Failed to add back-reference comment for %s "
|
1610
|
-
"(non-fatal): %s",
|
1633
|
+
"Failed to add back-reference comment for %s (non-fatal): %s",
|
1611
1634
|
csha,
|
1612
1635
|
exc,
|
1613
1636
|
)
|
@@ -1623,14 +1646,11 @@ class Orchestrator:
|
|
1623
1646
|
# Continue processing - this is not a fatal error
|
1624
1647
|
except Exception as exc:
|
1625
1648
|
log.warning(
|
1626
|
-
"Failed to add back-reference comment for %s "
|
1627
|
-
"(non-fatal): %s",
|
1649
|
+
"Failed to add back-reference comment for %s (non-fatal): %s",
|
1628
1650
|
csha,
|
1629
1651
|
exc,
|
1630
1652
|
)
|
1631
|
-
log.debug(
|
1632
|
-
"Back-reference comment failure details:", exc_info=True
|
1633
|
-
)
|
1653
|
+
log.debug("Back-reference comment failure details:", exc_info=True)
|
1634
1654
|
# Continue processing - this is not a fatal error
|
1635
1655
|
|
1636
1656
|
def _comment_on_pull_request(
|
@@ -1645,10 +1665,10 @@ class Orchestrator:
|
|
1645
1665
|
return
|
1646
1666
|
urls = result.change_urls or []
|
1647
1667
|
org = os.getenv("ORGANIZATION", gh.repository_owner)
|
1648
|
-
|
1649
|
-
|
1650
|
-
|
1651
|
-
)
|
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"
|
1652
1672
|
if urls:
|
1653
1673
|
text += "To follow up on the change visit:\n\n" + "\n".join(urls)
|
1654
1674
|
try:
|
@@ -1657,6 +1677,13 @@ class Orchestrator:
|
|
1657
1677
|
# At this point, gh.pr_number is non-None due to earlier guard.
|
1658
1678
|
pr_obj = get_pull(repo, int(gh.pr_number))
|
1659
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
|
+
)
|
1660
1687
|
except Exception as exc:
|
1661
1688
|
log.warning("Failed to add PR comment: %s", exc)
|
1662
1689
|
|
@@ -1718,20 +1745,15 @@ class Orchestrator:
|
|
1718
1745
|
"yes",
|
1719
1746
|
"on",
|
1720
1747
|
):
|
1748
|
+
log.info("Dry-run: network checks disabled (G2G_DRYRUN_DISABLE_NETWORK)")
|
1721
1749
|
log.info(
|
1722
|
-
"Dry-run:
|
1723
|
-
)
|
1724
|
-
log.info(
|
1725
|
-
"Dry-run targets: Gerrit project=%s branch=%s "
|
1726
|
-
"topic_prefix=GH-%s",
|
1750
|
+
"Dry-run targets: Gerrit project=%s branch=%s topic_prefix=GH-%s",
|
1727
1751
|
repo.project_gerrit,
|
1728
1752
|
self._resolve_target_branch(),
|
1729
1753
|
repo.project_github,
|
1730
1754
|
)
|
1731
1755
|
if inputs.reviewers_email:
|
1732
|
-
log.info(
|
1733
|
-
"Reviewers (from inputs/config): %s", inputs.reviewers_email
|
1734
|
-
)
|
1756
|
+
log.info("Reviewers (from inputs/config): %s", inputs.reviewers_email)
|
1735
1757
|
elif os.getenv("REVIEWERS_EMAIL"):
|
1736
1758
|
log.info(
|
1737
1759
|
"Reviewers (from environment): %s",
|
@@ -1742,18 +1764,14 @@ class Orchestrator:
|
|
1742
1764
|
# DNS resolution for Gerrit host
|
1743
1765
|
try:
|
1744
1766
|
socket.getaddrinfo(gerrit.host, None)
|
1745
|
-
log.info(
|
1746
|
-
"DNS resolution for Gerrit host '%s' succeeded", gerrit.host
|
1747
|
-
)
|
1767
|
+
log.info("DNS resolution for Gerrit host '%s' succeeded", gerrit.host)
|
1748
1768
|
except Exception as exc:
|
1749
1769
|
msg = "DNS resolution failed"
|
1750
1770
|
raise OrchestratorError(msg) from exc
|
1751
1771
|
|
1752
1772
|
# SSH (TCP) reachability on Gerrit port
|
1753
1773
|
try:
|
1754
|
-
with socket.create_connection(
|
1755
|
-
(gerrit.host, gerrit.port), timeout=5
|
1756
|
-
):
|
1774
|
+
with socket.create_connection((gerrit.host, gerrit.port), timeout=5):
|
1757
1775
|
pass
|
1758
1776
|
log.info(
|
1759
1777
|
"SSH TCP connectivity to %s:%s verified",
|
@@ -1766,10 +1784,7 @@ class Orchestrator:
|
|
1766
1784
|
|
1767
1785
|
# Gerrit REST reachability and optional auth check
|
1768
1786
|
base_path = os.getenv("GERRIT_HTTP_BASE_PATH", "").strip().strip("/")
|
1769
|
-
http_user = (
|
1770
|
-
os.getenv("GERRIT_HTTP_USER", "").strip()
|
1771
|
-
or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
|
1772
|
-
)
|
1787
|
+
http_user = os.getenv("GERRIT_HTTP_USER", "").strip() or os.getenv("GERRIT_SSH_USER_G2G", "").strip()
|
1773
1788
|
http_pass = os.getenv("GERRIT_HTTP_PASSWORD", "").strip()
|
1774
1789
|
self._verify_gerrit_rest(gerrit.host, base_path, http_user, http_pass)
|
1775
1790
|
|
@@ -1779,9 +1794,7 @@ class Orchestrator:
|
|
1779
1794
|
repo_obj = get_repo_from_env(client)
|
1780
1795
|
if gh.pr_number is not None:
|
1781
1796
|
pr_obj = get_pull(repo_obj, gh.pr_number)
|
1782
|
-
log.info(
|
1783
|
-
"GitHub PR #%s metadata loaded successfully", gh.pr_number
|
1784
|
-
)
|
1797
|
+
log.info("GitHub PR #%s metadata loaded successfully", gh.pr_number)
|
1785
1798
|
try:
|
1786
1799
|
title, _ = get_pr_title_body(pr_obj)
|
1787
1800
|
log.info("GitHub PR title: %s", title)
|
@@ -1807,13 +1820,9 @@ class Orchestrator:
|
|
1807
1820
|
repo.project_github,
|
1808
1821
|
)
|
1809
1822
|
if inputs.reviewers_email:
|
1810
|
-
log.info(
|
1811
|
-
"Reviewers (from inputs/config): %s", inputs.reviewers_email
|
1812
|
-
)
|
1823
|
+
log.info("Reviewers (from inputs/config): %s", inputs.reviewers_email)
|
1813
1824
|
elif os.getenv("REVIEWERS_EMAIL"):
|
1814
|
-
log.info(
|
1815
|
-
"Reviewers (from environment): %s", os.getenv("REVIEWERS_EMAIL")
|
1816
|
-
)
|
1825
|
+
log.info("Reviewers (from environment): %s", os.getenv("REVIEWERS_EMAIL"))
|
1817
1826
|
|
1818
1827
|
def _verify_gerrit_rest(
|
1819
1828
|
self,
|
@@ -1830,9 +1839,7 @@ class Orchestrator:
|
|
1830
1839
|
raise OrchestratorError(_MSG_PYGERRIT2_MISSING)
|
1831
1840
|
if HTTPBasicAuth is None:
|
1832
1841
|
raise OrchestratorError(_MSG_PYGERRIT2_AUTH_MISSING)
|
1833
|
-
return GerritRestAPI(
|
1834
|
-
url=url, auth=HTTPBasicAuth(http_user, http_pass)
|
1835
|
-
)
|
1842
|
+
return GerritRestAPI(url=url, auth=HTTPBasicAuth(http_user, http_pass))
|
1836
1843
|
else:
|
1837
1844
|
if GerritRestAPI is None:
|
1838
1845
|
raise OrchestratorError(_MSG_PYGERRIT2_MISSING)
|
@@ -1850,29 +1857,30 @@ class Orchestrator:
|
|
1850
1857
|
_ = rest.get("/dashboard/self")
|
1851
1858
|
log.info("Gerrit REST endpoint reachable (unauthenticated)")
|
1852
1859
|
|
1853
|
-
|
1854
|
-
|
1855
|
-
|
1856
|
-
|
1857
|
-
|
1858
|
-
|
1859
|
-
|
1860
|
-
|
1861
|
-
|
1862
|
-
|
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,
|
1863
1883
|
)
|
1864
|
-
if not base_path and status == 404:
|
1865
|
-
try:
|
1866
|
-
fallback_url = f"https://{host}/r/"
|
1867
|
-
_probe(fallback_url)
|
1868
|
-
except Exception as exc2:
|
1869
|
-
log.warning(
|
1870
|
-
"Gerrit REST probe did not succeed "
|
1871
|
-
"(including '/r' fallback): %s",
|
1872
|
-
exc2,
|
1873
|
-
)
|
1874
|
-
else:
|
1875
|
-
log.warning("Gerrit REST probe did not succeed: %s", exc)
|
1876
1884
|
|
1877
1885
|
# ---------------
|
1878
1886
|
# Helpers
|