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 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
- check_for_duplicates(
453
- per_ctx, allow_duplicates=data.allow_duplicates
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
- typer.echo(f"Skipping PR #{pr_number}: {exc}")
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
- "GERRIT_CHANGE_REQUEST_URL", ""
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
- "GERRIT_CHANGE_REQUEST_URL", ""
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
- getattr(getattr(pr_obj, "base", object()), "ref", "") or ""
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(log, "Duplicate change detected")
785
- typer.echo(f"Error: {exc}", err=True)
786
- typer.echo(
787
- "Use --allow-duplicates to override this check.", err=True
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
- "These fields require either explicit values or an "
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 derivation
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
- "G2G_ENABLE_DERIVATION", ""
389
- ).strip().lower() in ("1", "true", "yes", "on")
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
- "G2G_AUTO_SAVE_CONFIG", default_auto_save
420
- ).strip().lower() in ("1", "true", "yes", "on")
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
- # Ensure the directory exists
470
- config_file.parent.mkdir(parents=True, exist_ok=True)
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)