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/cli.py
CHANGED
@@ -42,9 +42,7 @@ def _is_verbose_mode() -> bool:
|
|
42
42
|
return os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes")
|
43
43
|
|
44
44
|
|
45
|
-
def _log_exception_conditionally(
|
46
|
-
logger: logging.Logger, message: str, *args: Any
|
47
|
-
) -> None:
|
45
|
+
def _log_exception_conditionally(logger: logging.Logger, message: str, *args: Any) -> None:
|
48
46
|
"""Log exception with traceback only if verbose mode is enabled."""
|
49
47
|
if _is_verbose_mode():
|
50
48
|
logger.exception(message, *args)
|
@@ -120,13 +118,9 @@ class _ContextProto(Protocol):
|
|
120
118
|
|
121
119
|
|
122
120
|
class _SingleUsageGroup(BaseGroup):
|
123
|
-
def format_usage(
|
124
|
-
self, ctx: _ContextProto, formatter: _FormatterProto
|
125
|
-
) -> None:
|
121
|
+
def format_usage(self, ctx: _ContextProto, formatter: _FormatterProto) -> None:
|
126
122
|
# Force a simplified usage line without COMMAND [ARGS]...
|
127
|
-
formatter.write_usage(
|
128
|
-
ctx.command_path, "[OPTIONS] TARGET_URL", prefix="Usage: "
|
129
|
-
)
|
123
|
+
formatter.write_usage(ctx.command_path, "[OPTIONS] TARGET_URL", prefix="Usage: ")
|
130
124
|
|
131
125
|
|
132
126
|
# Error message constants to comply with TRY003
|
@@ -137,7 +131,7 @@ _MSG_ISSUE_ID_MULTILINE = "Issue ID must be single line"
|
|
137
131
|
app: typer.Typer = typer.Typer(
|
138
132
|
add_completion=False,
|
139
133
|
no_args_is_help=False,
|
140
|
-
cls=_SingleUsageGroup,
|
134
|
+
cls=cast(Any, _SingleUsageGroup),
|
141
135
|
)
|
142
136
|
|
143
137
|
|
@@ -154,9 +148,7 @@ def _resolve_org(default_org: str | None) -> str:
|
|
154
148
|
if TYPE_CHECKING:
|
155
149
|
F = TypeVar("F", bound=Callable[..., object])
|
156
150
|
|
157
|
-
def typed_app_command(
|
158
|
-
*args: object, **kwargs: object
|
159
|
-
) -> Callable[[F], F]: ...
|
151
|
+
def typed_app_command(*args: object, **kwargs: object) -> Callable[[F], F]: ...
|
160
152
|
else:
|
161
153
|
typed_app_command = app.command
|
162
154
|
|
@@ -269,6 +261,14 @@ def main(
|
|
269
261
|
envvar="ALLOW_DUPLICATES",
|
270
262
|
help="Allow submitting duplicate changes without error.",
|
271
263
|
),
|
264
|
+
duplicates: str = typer.Option(
|
265
|
+
"open",
|
266
|
+
"--duplicates",
|
267
|
+
envvar="DUPLICATES",
|
268
|
+
help=(
|
269
|
+
'Gerrit statuses for duplicate detection (comma-separated). E.g. "open,merged,abandoned". Default: "open".'
|
270
|
+
),
|
271
|
+
),
|
272
272
|
verbose: bool = typer.Option(
|
273
273
|
False,
|
274
274
|
"--verbose",
|
@@ -328,6 +328,8 @@ def main(
|
|
328
328
|
os.environ["ISSUE_ID"] = issue_id
|
329
329
|
if allow_duplicates:
|
330
330
|
os.environ["ALLOW_DUPLICATES"] = "true"
|
331
|
+
if duplicates:
|
332
|
+
os.environ["DUPLICATES"] = duplicates
|
331
333
|
# URL mode handling
|
332
334
|
if target_url:
|
333
335
|
org, repo, pr = _parse_github_target(target_url)
|
@@ -355,10 +357,7 @@ def main(
|
|
355
357
|
def _setup_logging() -> logging.Logger:
|
356
358
|
level_name = os.getenv("G2G_LOG_LEVEL", "INFO").upper()
|
357
359
|
level = getattr(logging, level_name, logging.INFO)
|
358
|
-
fmt = (
|
359
|
-
"%(asctime)s %(levelname)-8s %(name)s "
|
360
|
-
"%(filename)s:%(lineno)d | %(message)s"
|
361
|
-
)
|
360
|
+
fmt = "%(asctime)s %(levelname)-8s %(name)s %(filename)s:%(lineno)d | %(message)s"
|
362
361
|
logging.basicConfig(level=level, format=fmt)
|
363
362
|
return logging.getLogger(APP_NAME)
|
364
363
|
|
@@ -397,9 +396,7 @@ def _build_inputs_from_env() -> Inputs:
|
|
397
396
|
gerrit_ssh_privkey_g2g=_env_str("GERRIT_SSH_PRIVKEY_G2G"),
|
398
397
|
gerrit_ssh_user_g2g=_env_str("GERRIT_SSH_USER_G2G"),
|
399
398
|
gerrit_ssh_user_g2g_email=_env_str("GERRIT_SSH_USER_G2G_EMAIL"),
|
400
|
-
organization=_env_str(
|
401
|
-
"ORGANIZATION", _env_str("GITHUB_REPOSITORY_OWNER")
|
402
|
-
),
|
399
|
+
organization=_env_str("ORGANIZATION", _env_str("GITHUB_REPOSITORY_OWNER")),
|
403
400
|
reviewers_email=_env_str("REVIEWERS_EMAIL", ""),
|
404
401
|
preserve_github_prs=_env_bool("PRESERVE_GITHUB_PRS", False),
|
405
402
|
dry_run=_env_bool("DRY_RUN", False),
|
@@ -408,6 +405,7 @@ def _build_inputs_from_env() -> Inputs:
|
|
408
405
|
gerrit_project=_env_str("GERRIT_PROJECT"),
|
409
406
|
issue_id=_env_str("ISSUE_ID"),
|
410
407
|
allow_duplicates=_env_bool("ALLOW_DUPLICATES", False),
|
408
|
+
duplicates_filter=_env_str("DUPLICATES", "open"),
|
411
409
|
)
|
412
410
|
|
413
411
|
|
@@ -441,20 +439,23 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> None:
|
|
441
439
|
|
442
440
|
log.info("Starting processing of PR #%d", pr_number)
|
443
441
|
log.debug(
|
444
|
-
"Processing PR #%d in multi-PR mode with event_name=%s, "
|
445
|
-
"event_action=%s",
|
442
|
+
"Processing PR #%d in multi-PR mode with event_name=%s, event_action=%s",
|
446
443
|
pr_number,
|
447
444
|
gh.event_name,
|
448
445
|
gh.event_action,
|
449
446
|
)
|
450
447
|
|
451
448
|
try:
|
452
|
-
|
453
|
-
|
454
|
-
)
|
449
|
+
if data.duplicates_filter:
|
450
|
+
os.environ["DUPLICATES"] = data.duplicates_filter
|
451
|
+
check_for_duplicates(per_ctx, allow_duplicates=data.allow_duplicates)
|
455
452
|
except DuplicateChangeError as exc:
|
456
453
|
_log_exception_conditionally(log, "Skipping PR #%d", pr_number)
|
457
|
-
|
454
|
+
log.warning(
|
455
|
+
"Skipping PR #%d due to duplicate detection: %s. Use --allow-duplicates to override this check.",
|
456
|
+
pr_number,
|
457
|
+
exc,
|
458
|
+
)
|
458
459
|
continue
|
459
460
|
|
460
461
|
try:
|
@@ -479,9 +480,7 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> None:
|
|
479
480
|
result_multi.change_numbers,
|
480
481
|
)
|
481
482
|
except Exception as exc:
|
482
|
-
_log_exception_conditionally(
|
483
|
-
log, "Failed to process PR #%d", pr_number
|
484
|
-
)
|
483
|
+
_log_exception_conditionally(log, "Failed to process PR #%d", pr_number)
|
485
484
|
typer.echo(f"Failed to process PR #{pr_number}: {exc}")
|
486
485
|
log.info("Continuing to next PR despite failure")
|
487
486
|
continue
|
@@ -493,12 +492,8 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> None:
|
|
493
492
|
|
494
493
|
_append_github_output(
|
495
494
|
{
|
496
|
-
"gerrit_change_request_url": os.getenv(
|
497
|
-
|
498
|
-
),
|
499
|
-
"gerrit_change_request_num": os.getenv(
|
500
|
-
"GERRIT_CHANGE_REQUEST_NUM", ""
|
501
|
-
),
|
495
|
+
"gerrit_change_request_url": os.getenv("GERRIT_CHANGE_REQUEST_URL", ""),
|
496
|
+
"gerrit_change_request_num": os.getenv("GERRIT_CHANGE_REQUEST_NUM", ""),
|
502
497
|
}
|
503
498
|
)
|
504
499
|
|
@@ -524,30 +519,20 @@ def _process_single(data: Inputs, gh: GitHubContext) -> None:
|
|
524
519
|
except Exception as exc:
|
525
520
|
log.debug("Execution failed; continuing to write outputs: %s", exc)
|
526
521
|
|
527
|
-
result = SubmissionResult(
|
528
|
-
change_urls=[], change_numbers=[], commit_shas=[]
|
529
|
-
)
|
522
|
+
result = SubmissionResult(change_urls=[], change_numbers=[], commit_shas=[])
|
530
523
|
if result.change_urls:
|
531
|
-
os.environ["GERRIT_CHANGE_REQUEST_URL"] = "\n".join(
|
532
|
-
result.change_urls
|
533
|
-
)
|
524
|
+
os.environ["GERRIT_CHANGE_REQUEST_URL"] = "\n".join(result.change_urls)
|
534
525
|
# Output Gerrit change URL(s) to console
|
535
526
|
for url in result.change_urls:
|
536
527
|
log.info("Gerrit change URL: %s", url)
|
537
528
|
if result.change_numbers:
|
538
|
-
os.environ["GERRIT_CHANGE_REQUEST_NUM"] = "\n".join(
|
539
|
-
result.change_numbers
|
540
|
-
)
|
529
|
+
os.environ["GERRIT_CHANGE_REQUEST_NUM"] = "\n".join(result.change_numbers)
|
541
530
|
|
542
531
|
# Also write outputs to GITHUB_OUTPUT if available
|
543
532
|
_append_github_output(
|
544
533
|
{
|
545
|
-
"gerrit_change_request_url": os.getenv(
|
546
|
-
|
547
|
-
),
|
548
|
-
"gerrit_change_request_num": os.getenv(
|
549
|
-
"GERRIT_CHANGE_REQUEST_NUM", ""
|
550
|
-
),
|
534
|
+
"gerrit_change_request_url": os.getenv("GERRIT_CHANGE_REQUEST_URL", ""),
|
535
|
+
"gerrit_change_request_num": os.getenv("GERRIT_CHANGE_REQUEST_NUM", ""),
|
551
536
|
"gerrit_commit_sha": os.getenv("GERRIT_COMMIT_SHA", ""),
|
552
537
|
}
|
553
538
|
)
|
@@ -559,13 +544,9 @@ def _process_single(data: Inputs, gh: GitHubContext) -> None:
|
|
559
544
|
return
|
560
545
|
|
561
546
|
|
562
|
-
def _prepare_local_checkout(
|
563
|
-
workspace: Path, gh: GitHubContext, data: Inputs
|
564
|
-
) -> None:
|
547
|
+
def _prepare_local_checkout(workspace: Path, gh: GitHubContext, data: Inputs) -> None:
|
565
548
|
repo_full = gh.repository.strip() if gh.repository else ""
|
566
|
-
server_url = gh.server_url or os.getenv(
|
567
|
-
"GITHUB_SERVER_URL", "https://github.com"
|
568
|
-
)
|
549
|
+
server_url = gh.server_url or os.getenv("GITHUB_SERVER_URL", "https://github.com")
|
569
550
|
server_url = (server_url or "https://github.com").rstrip("/")
|
570
551
|
base_ref = gh.base_ref or ""
|
571
552
|
pr_num_str: str = str(gh.pr_number) if gh.pr_number else "0"
|
@@ -577,6 +558,26 @@ def _prepare_local_checkout(
|
|
577
558
|
run_cmd(["git", "init"], cwd=workspace)
|
578
559
|
run_cmd(["git", "remote", "add", "origin", repo_url], cwd=workspace)
|
579
560
|
|
561
|
+
# Non-interactive SSH/Git environment for any network operations
|
562
|
+
env = {
|
563
|
+
"GIT_SSH_COMMAND": (
|
564
|
+
"ssh -F /dev/null "
|
565
|
+
"-o IdentitiesOnly=yes "
|
566
|
+
"-o IdentityAgent=none "
|
567
|
+
"-o BatchMode=yes "
|
568
|
+
"-o PreferredAuthentications=publickey "
|
569
|
+
"-o StrictHostKeyChecking=yes "
|
570
|
+
"-o PasswordAuthentication=no "
|
571
|
+
"-o PubkeyAcceptedKeyTypes=+ssh-rsa "
|
572
|
+
"-o ConnectTimeout=10"
|
573
|
+
),
|
574
|
+
"SSH_AUTH_SOCK": "",
|
575
|
+
"SSH_AGENT_PID": "",
|
576
|
+
"SSH_ASKPASS": "/usr/bin/false",
|
577
|
+
"DISPLAY": "",
|
578
|
+
"SSH_ASKPASS_REQUIRE": "never",
|
579
|
+
}
|
580
|
+
|
580
581
|
# Fetch base branch and PR head
|
581
582
|
if base_ref:
|
582
583
|
try:
|
@@ -590,15 +591,13 @@ def _prepare_local_checkout(
|
|
590
591
|
branch_ref,
|
591
592
|
],
|
592
593
|
cwd=workspace,
|
594
|
+
env=env,
|
593
595
|
)
|
594
596
|
except Exception as exc:
|
595
597
|
log.debug("Base branch fetch failed for %s: %s", base_ref, exc)
|
596
598
|
|
597
599
|
if pr_num_str:
|
598
|
-
pr_ref =
|
599
|
-
f"refs/pull/{pr_num_str}/head:"
|
600
|
-
f"refs/remotes/origin/pr/{pr_num_str}/head"
|
601
|
-
)
|
600
|
+
pr_ref = f"refs/pull/{pr_num_str}/head:refs/remotes/origin/pr/{pr_num_str}/head"
|
602
601
|
run_cmd(
|
603
602
|
[
|
604
603
|
"git",
|
@@ -608,6 +607,7 @@ def _prepare_local_checkout(
|
|
608
607
|
pr_ref,
|
609
608
|
],
|
610
609
|
cwd=workspace,
|
610
|
+
env=env,
|
611
611
|
)
|
612
612
|
run_cmd(
|
613
613
|
[
|
@@ -618,6 +618,7 @@ def _prepare_local_checkout(
|
|
618
618
|
f"refs/remotes/origin/pr/{pr_num_str}/head",
|
619
619
|
],
|
620
620
|
cwd=workspace,
|
621
|
+
env=env,
|
621
622
|
)
|
622
623
|
|
623
624
|
|
@@ -626,11 +627,7 @@ def _load_effective_inputs() -> Inputs:
|
|
626
627
|
data = _build_inputs_from_env()
|
627
628
|
|
628
629
|
# Load per-org configuration and apply to environment before validation
|
629
|
-
org_for_cfg = (
|
630
|
-
data.organization
|
631
|
-
or os.getenv("ORGANIZATION")
|
632
|
-
or os.getenv("GITHUB_REPOSITORY_OWNER")
|
633
|
-
)
|
630
|
+
org_for_cfg = data.organization or os.getenv("ORGANIZATION") or os.getenv("GITHUB_REPOSITORY_OWNER")
|
634
631
|
cfg = load_org_config(org_for_cfg)
|
635
632
|
|
636
633
|
# Apply dynamic parameter derivation for missing Gerrit parameters
|
@@ -642,9 +639,7 @@ def _load_effective_inputs() -> Inputs:
|
|
642
639
|
data = _build_inputs_from_env()
|
643
640
|
|
644
641
|
# Derive reviewers from local git config if running locally and unset
|
645
|
-
if not os.getenv("REVIEWERS_EMAIL") and (
|
646
|
-
os.getenv("G2G_TARGET_URL") or not os.getenv("GITHUB_EVENT_NAME")
|
647
|
-
):
|
642
|
+
if not os.getenv("REVIEWERS_EMAIL") and (os.getenv("G2G_TARGET_URL") or not os.getenv("GITHUB_EVENT_NAME")):
|
648
643
|
try:
|
649
644
|
from .gitutils import enumerate_reviewer_emails
|
650
645
|
|
@@ -668,6 +663,7 @@ def _load_effective_inputs() -> Inputs:
|
|
668
663
|
gerrit_project=data.gerrit_project,
|
669
664
|
issue_id=data.issue_id,
|
670
665
|
allow_duplicates=data.allow_duplicates,
|
666
|
+
duplicates_filter=data.duplicates_filter,
|
671
667
|
)
|
672
668
|
log.info("Derived reviewers: %s", data.reviewers_email)
|
673
669
|
except Exception as exc:
|
@@ -696,24 +692,14 @@ def _append_github_output(outputs: dict[str, str]) -> None:
|
|
696
692
|
|
697
693
|
|
698
694
|
def _augment_pr_refs_if_needed(gh: GitHubContext) -> GitHubContext:
|
699
|
-
if (
|
700
|
-
os.getenv("G2G_TARGET_URL")
|
701
|
-
and gh.pr_number
|
702
|
-
and (not gh.head_ref or not gh.base_ref)
|
703
|
-
):
|
695
|
+
if os.getenv("G2G_TARGET_URL") and gh.pr_number and (not gh.head_ref or not gh.base_ref):
|
704
696
|
try:
|
705
697
|
client = build_client()
|
706
698
|
repo = get_repo_from_env(client)
|
707
699
|
pr_obj = get_pull(repo, int(gh.pr_number))
|
708
|
-
base_ref = str(
|
709
|
-
|
710
|
-
)
|
711
|
-
head_ref = str(
|
712
|
-
getattr(getattr(pr_obj, "head", object()), "ref", "") or ""
|
713
|
-
)
|
714
|
-
head_sha = str(
|
715
|
-
getattr(getattr(pr_obj, "head", object()), "sha", "") or ""
|
716
|
-
)
|
700
|
+
base_ref = str(getattr(getattr(pr_obj, "base", object()), "ref", "") or "")
|
701
|
+
head_ref = str(getattr(getattr(pr_obj, "head", object()), "ref", "") or "")
|
702
|
+
head_sha = str(getattr(getattr(pr_obj, "head", object()), "sha", "") or "")
|
717
703
|
if base_ref:
|
718
704
|
os.environ["GITHUB_BASE_REF"] = base_ref
|
719
705
|
log.info("Resolved base_ref via GitHub API: %s", base_ref)
|
@@ -751,21 +737,17 @@ def _process() -> None:
|
|
751
737
|
|
752
738
|
# Bulk mode for URL/workflow_dispatch
|
753
739
|
sync_all = _env_bool("SYNC_ALL_OPEN_PRS", False)
|
754
|
-
if sync_all and (
|
755
|
-
gh.event_name == "workflow_dispatch" or os.getenv("G2G_TARGET_URL")
|
756
|
-
):
|
740
|
+
if sync_all and (gh.event_name == "workflow_dispatch" or os.getenv("G2G_TARGET_URL")):
|
757
741
|
_process_bulk(data, gh)
|
758
742
|
return
|
759
743
|
|
760
744
|
if not gh.pr_number:
|
761
745
|
log.error(
|
762
|
-
"PR_NUMBER is empty. This tool requires a valid pull request "
|
763
|
-
"context. Current event: %s",
|
746
|
+
"PR_NUMBER is empty. This tool requires a valid pull request context. Current event: %s",
|
764
747
|
gh.event_name,
|
765
748
|
)
|
766
749
|
typer.echo(
|
767
|
-
"PR_NUMBER is empty. This tool requires a valid pull request "
|
768
|
-
f"context. Current event: {gh.event_name}",
|
750
|
+
f"PR_NUMBER is empty. This tool requires a valid pull request context. Current event: {gh.event_name}",
|
769
751
|
err=True,
|
770
752
|
)
|
771
753
|
raise typer.Exit(code=2)
|
@@ -779,13 +761,16 @@ def _process() -> None:
|
|
779
761
|
# Check for duplicates in single-PR mode (before workspace setup)
|
780
762
|
if gh.pr_number and not _env_bool("SYNC_ALL_OPEN_PRS", False):
|
781
763
|
try:
|
764
|
+
if data.duplicates_filter:
|
765
|
+
os.environ["DUPLICATES"] = data.duplicates_filter
|
782
766
|
check_for_duplicates(gh, allow_duplicates=data.allow_duplicates)
|
783
767
|
except DuplicateChangeError as exc:
|
784
|
-
_log_exception_conditionally(
|
785
|
-
|
786
|
-
|
787
|
-
|
768
|
+
_log_exception_conditionally(
|
769
|
+
log,
|
770
|
+
"Duplicate detection blocked submission for PR #%d",
|
771
|
+
gh.pr_number,
|
788
772
|
)
|
773
|
+
log.info("Use --allow-duplicates to override this check.")
|
789
774
|
raise typer.Exit(code=3) from exc
|
790
775
|
|
791
776
|
_process_single(data, gh)
|
@@ -804,9 +789,7 @@ def _load_event(path: Path | None) -> dict[str, Any]:
|
|
804
789
|
if not path or not path.exists():
|
805
790
|
return {}
|
806
791
|
try:
|
807
|
-
return cast(
|
808
|
-
dict[str, Any], json.loads(path.read_text(encoding="utf-8"))
|
809
|
-
)
|
792
|
+
return cast(dict[str, Any], json.loads(path.read_text(encoding="utf-8")))
|
810
793
|
except Exception as exc:
|
811
794
|
log.warning("Failed to parse GITHUB_EVENT_PATH: %s", exc)
|
812
795
|
return {}
|
@@ -873,10 +856,7 @@ def _read_github_context() -> GitHubContext:
|
|
873
856
|
|
874
857
|
def _validate_inputs(data: Inputs) -> None:
|
875
858
|
if data.use_pr_as_commit and data.submit_single_commits:
|
876
|
-
msg =
|
877
|
-
"USE_PR_AS_COMMIT and SUBMIT_SINGLE_COMMITS cannot be enabled at "
|
878
|
-
"the same time"
|
879
|
-
)
|
859
|
+
msg = "USE_PR_AS_COMMIT and SUBMIT_SINGLE_COMMITS cannot be enabled at the same time"
|
880
860
|
raise ConfigurationError(msg)
|
881
861
|
|
882
862
|
# Context-aware validation: different requirements for GH Actions vs CLI
|
@@ -890,7 +870,6 @@ def _validate_inputs(data: Inputs) -> None:
|
|
890
870
|
if is_github_actions:
|
891
871
|
# In GitHub Actions: allow derivation if organization is available
|
892
872
|
if not data.organization:
|
893
|
-
# No organization means no derivation possible
|
894
873
|
required_fields.extend(
|
895
874
|
[
|
896
875
|
"gerrit_ssh_user_g2g",
|
@@ -901,9 +880,7 @@ def _validate_inputs(data: Inputs) -> None:
|
|
901
880
|
# In local CLI: require explicit values or organization + derivation
|
902
881
|
# This prevents unexpected behavior when running locally
|
903
882
|
missing_gerrit_params = [
|
904
|
-
field
|
905
|
-
for field in ["gerrit_ssh_user_g2g", "gerrit_ssh_user_g2g_email"]
|
906
|
-
if not getattr(data, field)
|
883
|
+
field for field in ["gerrit_ssh_user_g2g", "gerrit_ssh_user_g2g_email"] if not getattr(data, field)
|
907
884
|
]
|
908
885
|
if missing_gerrit_params:
|
909
886
|
if data.organization:
|
@@ -931,24 +908,18 @@ def _validate_inputs(data: Inputs) -> None:
|
|
931
908
|
if is_github_actions:
|
932
909
|
log.error(
|
933
910
|
"These fields can be derived automatically from "
|
934
|
-
"organization '%s'",
|
911
|
+
"organization '%s' if G2G_ENABLE_DERIVATION=true",
|
935
912
|
data.organization,
|
936
913
|
)
|
937
914
|
else:
|
938
915
|
log.error(
|
939
|
-
"These fields can be derived from organization "
|
940
|
-
"'%s'",
|
916
|
+
"These fields can be derived from organization '%s'",
|
941
917
|
data.organization,
|
942
918
|
)
|
943
919
|
log.error("Set G2G_ENABLE_DERIVATION=true to enable")
|
944
920
|
else:
|
945
|
-
log.error(
|
946
|
-
|
947
|
-
"ORGANIZATION for derivation"
|
948
|
-
)
|
949
|
-
raise ConfigurationError(
|
950
|
-
_MSG_MISSING_REQUIRED_INPUT.format(field_name=field_name)
|
951
|
-
)
|
921
|
+
log.error("These fields require either explicit values or an ORGANIZATION for derivation")
|
922
|
+
raise ConfigurationError(_MSG_MISSING_REQUIRED_INPUT.format(field_name=field_name))
|
952
923
|
|
953
924
|
# Validate fetch depth is a positive integer
|
954
925
|
if data.fetch_depth <= 0:
|
@@ -967,9 +938,7 @@ def _log_effective_config(data: Inputs, gh: GitHubContext) -> None:
|
|
967
938
|
log.info(" SUBMIT_SINGLE_COMMITS: %s", data.submit_single_commits)
|
968
939
|
log.info(" USE_PR_AS_COMMIT: %s", data.use_pr_as_commit)
|
969
940
|
log.info(" FETCH_DEPTH: %s", data.fetch_depth)
|
970
|
-
known_hosts_status =
|
971
|
-
"<provided>" if data.gerrit_known_hosts else "<will auto-discover>"
|
972
|
-
)
|
941
|
+
known_hosts_status = "<provided>" if data.gerrit_known_hosts else "<will auto-discover>"
|
973
942
|
log.info(" GERRIT_KNOWN_HOSTS: %s", known_hosts_status)
|
974
943
|
log.info(" GERRIT_SSH_PRIVKEY_G2G: %s", safe_privkey)
|
975
944
|
log.info(" GERRIT_SSH_USER_G2G: %s", data.gerrit_ssh_user_g2g)
|
github2gerrit/config.py
CHANGED
@@ -130,11 +130,7 @@ def _coerce_value(raw: str) -> str:
|
|
130
130
|
# Normalize escaped newline sequences into real newlines so that values
|
131
131
|
# like SSH keys or known_hosts entries can be specified inline using
|
132
132
|
# '\n' or '\r\n' in configuration files.
|
133
|
-
normalized_newlines = (
|
134
|
-
unquoted.replace("\\r\\n", "\n")
|
135
|
-
.replace("\\n", "\n")
|
136
|
-
.replace("\r\n", "\n")
|
137
|
-
)
|
133
|
+
normalized_newlines = unquoted.replace("\\r\\n", "\n").replace("\\n", "\n").replace("\r\n", "\n")
|
138
134
|
b = _normalize_bool_like(normalized_newlines)
|
139
135
|
return b if b is not None else normalized_newlines
|
140
136
|
|
@@ -317,10 +313,7 @@ def filter_known(
|
|
317
313
|
|
318
314
|
def _is_github_actions_context() -> bool:
|
319
315
|
"""Detect if running in GitHub Actions environment."""
|
320
|
-
return (
|
321
|
-
os.getenv("GITHUB_ACTIONS") == "true"
|
322
|
-
or os.getenv("GITHUB_EVENT_NAME", "").strip() != ""
|
323
|
-
)
|
316
|
+
return os.getenv("GITHUB_ACTIONS") == "true" or os.getenv("GITHUB_EVENT_NAME", "").strip() != ""
|
324
317
|
|
325
318
|
|
326
319
|
def _is_local_cli_context() -> bool:
|
@@ -346,9 +339,7 @@ def derive_gerrit_parameters(organization: str | None) -> dict[str, str]:
|
|
346
339
|
org = organization.strip().lower()
|
347
340
|
return {
|
348
341
|
"GERRIT_SSH_USER_G2G": f"{org}.gh2gerrit",
|
349
|
-
"GERRIT_SSH_USER_G2G_EMAIL": (
|
350
|
-
f"releng+{org}-gh2gerrit@linuxfoundation.org"
|
351
|
-
),
|
342
|
+
"GERRIT_SSH_USER_G2G_EMAIL": (f"releng+{org}-gh2gerrit@linuxfoundation.org"),
|
352
343
|
"GERRIT_SERVER": f"gerrit.{org}.org",
|
353
344
|
}
|
354
345
|
|
@@ -369,7 +360,8 @@ def apply_parameter_derivation(
|
|
369
360
|
|
370
361
|
Derivation behavior depends on execution context:
|
371
362
|
- GitHub Actions: Automatic derivation when organization is available
|
372
|
-
- Local CLI: Requires G2G_ENABLE_DERIVATION=true for automatic
|
363
|
+
- Local CLI: Requires G2G_ENABLE_DERIVATION=true for automatic
|
364
|
+
derivation
|
373
365
|
|
374
366
|
Args:
|
375
367
|
cfg: Configuration dictionary to augment
|
@@ -384,9 +376,12 @@ def apply_parameter_derivation(
|
|
384
376
|
|
385
377
|
# Check execution context to determine derivation strategy
|
386
378
|
is_github_actions = _is_github_actions_context()
|
387
|
-
enable_derivation = is_github_actions or os.getenv(
|
388
|
-
"
|
389
|
-
|
379
|
+
enable_derivation = is_github_actions or os.getenv("G2G_ENABLE_DERIVATION", "").strip().lower() in (
|
380
|
+
"1",
|
381
|
+
"true",
|
382
|
+
"yes",
|
383
|
+
"on",
|
384
|
+
)
|
390
385
|
|
391
386
|
if not enable_derivation:
|
392
387
|
log.debug(
|
@@ -412,12 +407,22 @@ def apply_parameter_derivation(
|
|
412
407
|
result[key] = value
|
413
408
|
newly_derived[key] = value
|
414
409
|
|
410
|
+
if newly_derived:
|
411
|
+
log.info(
|
412
|
+
"Derived parameters applied for organization '%s' (%s): %s",
|
413
|
+
organization,
|
414
|
+
"GitHub Actions" if is_github_actions else "Local CLI",
|
415
|
+
", ".join(f"{k}={v}" for k, v in newly_derived.items()),
|
416
|
+
)
|
415
417
|
# Save newly derived parameters to configuration file for future use
|
416
418
|
# Default to true for local CLI, false for GitHub Actions
|
417
419
|
default_auto_save = "false" if _is_github_actions_context() else "true"
|
418
|
-
auto_save_enabled = os.getenv(
|
419
|
-
"
|
420
|
-
|
420
|
+
auto_save_enabled = os.getenv("G2G_AUTO_SAVE_CONFIG", default_auto_save).strip().lower() in (
|
421
|
+
"1",
|
422
|
+
"true",
|
423
|
+
"yes",
|
424
|
+
"on",
|
425
|
+
)
|
421
426
|
if save_to_config and newly_derived and auto_save_enabled:
|
422
427
|
# Save to config in local CLI mode to create persistent configuration
|
423
428
|
try:
|
@@ -459,15 +464,18 @@ def save_derived_parameters_to_config(
|
|
459
464
|
return
|
460
465
|
|
461
466
|
if config_path is None:
|
462
|
-
config_path = (
|
463
|
-
os.getenv("G2G_CONFIG_PATH", "").strip() or DEFAULT_CONFIG_PATH
|
464
|
-
)
|
467
|
+
config_path = os.getenv("G2G_CONFIG_PATH", "").strip() or DEFAULT_CONFIG_PATH
|
465
468
|
|
466
469
|
config_file = Path(config_path).expanduser()
|
467
470
|
|
468
471
|
try:
|
469
|
-
#
|
470
|
-
config_file.
|
472
|
+
# Only update when a configuration file already exists
|
473
|
+
if not config_file.exists():
|
474
|
+
log.debug(
|
475
|
+
"Configuration file does not exist; skipping auto-save of derived parameters: %s",
|
476
|
+
config_file,
|
477
|
+
)
|
478
|
+
return
|
471
479
|
|
472
480
|
# Parse existing content using configparser
|
473
481
|
cp = _load_ini(config_file)
|