github2gerrit 0.1.3__py3-none-any.whl → 0.1.5__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- github2gerrit/cli.py +161 -27
- github2gerrit/config.py +214 -2
- github2gerrit/core.py +274 -38
- github2gerrit/duplicate_detection.py +1 -1
- github2gerrit/github_api.py +11 -3
- github2gerrit/gitutils.py +22 -4
- github2gerrit/ssh_discovery.py +412 -0
- {github2gerrit-0.1.3.dist-info → github2gerrit-0.1.5.dist-info}/METADATA +104 -14
- github2gerrit-0.1.5.dist-info/RECORD +15 -0
- {github2gerrit-0.1.3.dist-info → github2gerrit-0.1.5.dist-info}/WHEEL +2 -1
- {github2gerrit-0.1.3.dist-info → github2gerrit-0.1.5.dist-info}/entry_points.txt +0 -3
- github2gerrit-0.1.5.dist-info/licenses/LICENSE +201 -0
- github2gerrit-0.1.5.dist-info/top_level.txt +1 -0
- github2gerrit-0.1.3.dist-info/RECORD +0 -12
github2gerrit/cli.py
CHANGED
@@ -7,8 +7,12 @@ import json
|
|
7
7
|
import logging
|
8
8
|
import os
|
9
9
|
import tempfile
|
10
|
+
from collections.abc import Callable
|
10
11
|
from pathlib import Path
|
12
|
+
from typing import TYPE_CHECKING
|
11
13
|
from typing import Any
|
14
|
+
from typing import Protocol
|
15
|
+
from typing import TypeVar
|
12
16
|
from typing import cast
|
13
17
|
from urllib.parse import urlparse
|
14
18
|
|
@@ -16,7 +20,9 @@ import click
|
|
16
20
|
import typer
|
17
21
|
|
18
22
|
from . import models
|
23
|
+
from .config import _is_github_actions_context
|
19
24
|
from .config import apply_config_to_env
|
25
|
+
from .config import apply_parameter_derivation
|
20
26
|
from .config import load_org_config
|
21
27
|
from .core import Orchestrator
|
22
28
|
from .core import SubmissionResult
|
@@ -31,6 +37,31 @@ from .models import GitHubContext
|
|
31
37
|
from .models import Inputs
|
32
38
|
|
33
39
|
|
40
|
+
def _is_verbose_mode() -> bool:
|
41
|
+
"""Check if verbose mode is enabled via environment variable."""
|
42
|
+
return os.getenv("G2G_VERBOSE", "").lower() in ("true", "1", "yes")
|
43
|
+
|
44
|
+
|
45
|
+
def _log_exception_conditionally(
|
46
|
+
logger: logging.Logger, message: str, *args: Any
|
47
|
+
) -> None:
|
48
|
+
"""Log exception with traceback only if verbose mode is enabled."""
|
49
|
+
if _is_verbose_mode():
|
50
|
+
logger.exception(message, *args)
|
51
|
+
else:
|
52
|
+
logger.error(message, *args)
|
53
|
+
|
54
|
+
|
55
|
+
class ConfigurationError(Exception):
|
56
|
+
"""Raised when configuration validation fails.
|
57
|
+
|
58
|
+
This custom exception is used instead of typer.BadParameter to provide
|
59
|
+
cleaner error messages to end users without exposing Python tracebacks.
|
60
|
+
When caught, it displays user-friendly messages prefixed with
|
61
|
+
"Configuration validation failed:" rather than raw exception details.
|
62
|
+
"""
|
63
|
+
|
64
|
+
|
34
65
|
def _parse_github_target(url: str) -> tuple[str | None, str | None, int | None]:
|
35
66
|
"""
|
36
67
|
Parse a GitHub repository or pull request URL.
|
@@ -73,14 +104,36 @@ def _parse_github_target(url: str) -> tuple[str | None, str | None, int | None]:
|
|
73
104
|
APP_NAME = "github2gerrit"
|
74
105
|
|
75
106
|
|
76
|
-
|
77
|
-
|
107
|
+
if TYPE_CHECKING:
|
108
|
+
BaseGroup = object
|
109
|
+
else:
|
110
|
+
BaseGroup = click.Group
|
111
|
+
|
112
|
+
|
113
|
+
class _FormatterProto(Protocol):
|
114
|
+
def write_usage(self, prog: str, args: str, prefix: str = ...) -> None: ...
|
115
|
+
|
116
|
+
|
117
|
+
class _ContextProto(Protocol):
|
118
|
+
@property
|
119
|
+
def command_path(self) -> str: ...
|
120
|
+
|
121
|
+
|
122
|
+
class _SingleUsageGroup(BaseGroup):
|
123
|
+
def format_usage(
|
124
|
+
self, ctx: _ContextProto, formatter: _FormatterProto
|
125
|
+
) -> None:
|
78
126
|
# Force a simplified usage line without COMMAND [ARGS]...
|
79
127
|
formatter.write_usage(
|
80
128
|
ctx.command_path, "[OPTIONS] TARGET_URL", prefix="Usage: "
|
81
129
|
)
|
82
130
|
|
83
131
|
|
132
|
+
# Error message constants to comply with TRY003
|
133
|
+
_MSG_MISSING_REQUIRED_INPUT = "Missing required input: {field_name}"
|
134
|
+
_MSG_INVALID_FETCH_DEPTH = "FETCH_DEPTH must be a positive integer"
|
135
|
+
_MSG_ISSUE_ID_MULTILINE = "Issue ID must be single line"
|
136
|
+
|
84
137
|
app: typer.Typer = typer.Typer(
|
85
138
|
add_completion=False,
|
86
139
|
no_args_is_help=False,
|
@@ -98,7 +151,17 @@ def _resolve_org(default_org: str | None) -> str:
|
|
98
151
|
return ""
|
99
152
|
|
100
153
|
|
101
|
-
|
154
|
+
if TYPE_CHECKING:
|
155
|
+
F = TypeVar("F", bound=Callable[..., object])
|
156
|
+
|
157
|
+
def typed_app_command(
|
158
|
+
*args: object, **kwargs: object
|
159
|
+
) -> Callable[[F], F]: ...
|
160
|
+
else:
|
161
|
+
typed_app_command = app.command
|
162
|
+
|
163
|
+
|
164
|
+
@typed_app_command()
|
102
165
|
def main(
|
103
166
|
ctx: typer.Context,
|
104
167
|
target_url: str | None = typer.Argument(
|
@@ -390,7 +453,7 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> None:
|
|
390
453
|
per_ctx, allow_duplicates=data.allow_duplicates
|
391
454
|
)
|
392
455
|
except DuplicateChangeError as exc:
|
393
|
-
log
|
456
|
+
_log_exception_conditionally(log, "Skipping PR #%d", pr_number)
|
394
457
|
typer.echo(f"Skipping PR #{pr_number}: {exc}")
|
395
458
|
continue
|
396
459
|
|
@@ -402,7 +465,7 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> None:
|
|
402
465
|
if result_multi.change_urls:
|
403
466
|
all_urls.extend(result_multi.change_urls)
|
404
467
|
for url in result_multi.change_urls:
|
405
|
-
|
468
|
+
log.info("Gerrit change URL: %s", url)
|
406
469
|
log.info(
|
407
470
|
"PR #%d created Gerrit change: %s",
|
408
471
|
pr_number,
|
@@ -416,7 +479,9 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> None:
|
|
416
479
|
result_multi.change_numbers,
|
417
480
|
)
|
418
481
|
except Exception as exc:
|
419
|
-
|
482
|
+
_log_exception_conditionally(
|
483
|
+
log, "Failed to process PR #%d", pr_number
|
484
|
+
)
|
420
485
|
typer.echo(f"Failed to process PR #{pr_number}: {exc}")
|
421
486
|
log.info("Continuing to next PR despite failure")
|
422
487
|
continue
|
@@ -452,8 +517,10 @@ def _process_single(data: Inputs, gh: GitHubContext) -> None:
|
|
452
517
|
log.debug("Local checkout preparation failed: %s", exc)
|
453
518
|
|
454
519
|
orch = Orchestrator(workspace=workspace)
|
520
|
+
pipeline_success = False
|
455
521
|
try:
|
456
522
|
result = orch.execute(inputs=data, gh=gh)
|
523
|
+
pipeline_success = True
|
457
524
|
except Exception as exc:
|
458
525
|
log.debug("Execution failed; continuing to write outputs: %s", exc)
|
459
526
|
|
@@ -466,7 +533,7 @@ def _process_single(data: Inputs, gh: GitHubContext) -> None:
|
|
466
533
|
)
|
467
534
|
# Output Gerrit change URL(s) to console
|
468
535
|
for url in result.change_urls:
|
469
|
-
|
536
|
+
log.info("Gerrit change URL: %s", url)
|
470
537
|
if result.change_numbers:
|
471
538
|
os.environ["GERRIT_CHANGE_REQUEST_NUM"] = "\n".join(
|
472
539
|
result.change_numbers
|
@@ -485,7 +552,10 @@ def _process_single(data: Inputs, gh: GitHubContext) -> None:
|
|
485
552
|
}
|
486
553
|
)
|
487
554
|
|
488
|
-
|
555
|
+
if pipeline_success:
|
556
|
+
log.info("Submission pipeline completed SUCCESSFULLY ✅")
|
557
|
+
else:
|
558
|
+
log.error("Submission pipeline FAILED ❌")
|
489
559
|
return
|
490
560
|
|
491
561
|
|
@@ -562,6 +632,10 @@ def _load_effective_inputs() -> Inputs:
|
|
562
632
|
or os.getenv("GITHUB_REPOSITORY_OWNER")
|
563
633
|
)
|
564
634
|
cfg = load_org_config(org_for_cfg)
|
635
|
+
|
636
|
+
# Apply dynamic parameter derivation for missing Gerrit parameters
|
637
|
+
cfg = apply_parameter_derivation(cfg, org_for_cfg, save_to_config=True)
|
638
|
+
|
565
639
|
apply_config_to_env(cfg)
|
566
640
|
|
567
641
|
# Refresh inputs after applying configuration to environment
|
@@ -661,9 +735,9 @@ def _process() -> None:
|
|
661
735
|
# Validate inputs
|
662
736
|
try:
|
663
737
|
_validate_inputs(data)
|
664
|
-
except
|
665
|
-
log
|
666
|
-
typer.echo(
|
738
|
+
except ConfigurationError as exc:
|
739
|
+
_log_exception_conditionally(log, "Configuration validation failed")
|
740
|
+
typer.echo(f"Configuration validation failed: {exc}", err=True)
|
667
741
|
raise typer.Exit(code=2) from exc
|
668
742
|
|
669
743
|
gh = _read_github_context()
|
@@ -707,7 +781,7 @@ def _process() -> None:
|
|
707
781
|
try:
|
708
782
|
check_for_duplicates(gh, allow_duplicates=data.allow_duplicates)
|
709
783
|
except DuplicateChangeError as exc:
|
710
|
-
log
|
784
|
+
_log_exception_conditionally(log, "Duplicate change detected")
|
711
785
|
typer.echo(f"Error: {exc}", err=True)
|
712
786
|
typer.echo(
|
713
787
|
"Use --allow-duplicates to override this check.", err=True
|
@@ -803,27 +877,87 @@ def _validate_inputs(data: Inputs) -> None:
|
|
803
877
|
"USE_PR_AS_COMMIT and SUBMIT_SINGLE_COMMITS cannot be enabled at "
|
804
878
|
"the same time"
|
805
879
|
)
|
806
|
-
raise
|
807
|
-
|
808
|
-
#
|
809
|
-
|
810
|
-
|
811
|
-
|
812
|
-
|
813
|
-
|
814
|
-
|
880
|
+
raise ConfigurationError(msg)
|
881
|
+
|
882
|
+
# Context-aware validation: different requirements for GH Actions vs CLI
|
883
|
+
is_github_actions = _is_github_actions_context()
|
884
|
+
|
885
|
+
# SSH private key is always required
|
886
|
+
required_fields = ["gerrit_ssh_privkey_g2g"]
|
887
|
+
|
888
|
+
# Gerrit parameters can be derived in GH Actions if organization available
|
889
|
+
# In local CLI context, we're more strict about explicit configuration
|
890
|
+
if is_github_actions:
|
891
|
+
# In GitHub Actions: allow derivation if organization is available
|
892
|
+
if not data.organization:
|
893
|
+
# No organization means no derivation possible
|
894
|
+
required_fields.extend(
|
895
|
+
[
|
896
|
+
"gerrit_ssh_user_g2g",
|
897
|
+
"gerrit_ssh_user_g2g_email",
|
898
|
+
]
|
899
|
+
)
|
900
|
+
else:
|
901
|
+
# In local CLI: require explicit values or organization + derivation
|
902
|
+
# This prevents unexpected behavior when running locally
|
903
|
+
missing_gerrit_params = [
|
904
|
+
field
|
905
|
+
for field in ["gerrit_ssh_user_g2g", "gerrit_ssh_user_g2g_email"]
|
906
|
+
if not getattr(data, field)
|
907
|
+
]
|
908
|
+
if missing_gerrit_params:
|
909
|
+
if data.organization:
|
910
|
+
log.info(
|
911
|
+
"Local CLI usage: Gerrit parameters can be derived from "
|
912
|
+
"organization '%s'. Missing: %s. Consider setting "
|
913
|
+
"G2G_ENABLE_DERIVATION=true to enable derivation.",
|
914
|
+
data.organization,
|
915
|
+
", ".join(missing_gerrit_params),
|
916
|
+
)
|
917
|
+
# Allow derivation in local mode only if explicitly enabled
|
918
|
+
if not _env_bool("G2G_ENABLE_DERIVATION", False):
|
919
|
+
required_fields.extend(missing_gerrit_params)
|
920
|
+
else:
|
921
|
+
required_fields.extend(missing_gerrit_params)
|
922
|
+
|
923
|
+
for field_name in required_fields:
|
815
924
|
if not getattr(data, field_name):
|
816
925
|
log.error("Missing required input: %s", field_name)
|
817
|
-
|
926
|
+
if field_name in [
|
927
|
+
"gerrit_ssh_user_g2g",
|
928
|
+
"gerrit_ssh_user_g2g_email",
|
929
|
+
]:
|
930
|
+
if data.organization:
|
931
|
+
if is_github_actions:
|
932
|
+
log.error(
|
933
|
+
"These fields can be derived automatically from "
|
934
|
+
"organization '%s'",
|
935
|
+
data.organization,
|
936
|
+
)
|
937
|
+
else:
|
938
|
+
log.error(
|
939
|
+
"These fields can be derived from organization "
|
940
|
+
"'%s'",
|
941
|
+
data.organization,
|
942
|
+
)
|
943
|
+
log.error("Set G2G_ENABLE_DERIVATION=true to enable")
|
944
|
+
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
|
+
)
|
818
952
|
|
819
953
|
# Validate fetch depth is a positive integer
|
820
954
|
if data.fetch_depth <= 0:
|
821
955
|
log.error("Invalid FETCH_DEPTH: %s", data.fetch_depth)
|
822
|
-
raise
|
956
|
+
raise ConfigurationError(_MSG_INVALID_FETCH_DEPTH)
|
823
957
|
|
824
958
|
# Validate Issue ID is a single line string if provided
|
825
959
|
if data.issue_id and ("\n" in data.issue_id or "\r" in data.issue_id):
|
826
|
-
raise
|
960
|
+
raise ConfigurationError(_MSG_ISSUE_ID_MULTILINE)
|
827
961
|
|
828
962
|
|
829
963
|
def _log_effective_config(data: Inputs, gh: GitHubContext) -> None:
|
@@ -833,10 +967,10 @@ def _log_effective_config(data: Inputs, gh: GitHubContext) -> None:
|
|
833
967
|
log.info(" SUBMIT_SINGLE_COMMITS: %s", data.submit_single_commits)
|
834
968
|
log.info(" USE_PR_AS_COMMIT: %s", data.use_pr_as_commit)
|
835
969
|
log.info(" FETCH_DEPTH: %s", data.fetch_depth)
|
836
|
-
|
837
|
-
"
|
838
|
-
"<provided>" if data.gerrit_known_hosts else "<missing>",
|
970
|
+
known_hosts_status = (
|
971
|
+
"<provided>" if data.gerrit_known_hosts else "<will auto-discover>"
|
839
972
|
)
|
973
|
+
log.info(" GERRIT_KNOWN_HOSTS: %s", known_hosts_status)
|
840
974
|
log.info(" GERRIT_SSH_PRIVKEY_G2G: %s", safe_privkey)
|
841
975
|
log.info(" GERRIT_SSH_USER_G2G: %s", data.gerrit_ssh_user_g2g)
|
842
976
|
log.info(" GERRIT_SSH_USER_G2G_EMAIL: %s", data.gerrit_ssh_user_g2g_email)
|
github2gerrit/config.py
CHANGED
@@ -58,8 +58,8 @@ log = logging.getLogger("github2gerrit.config")
|
|
58
58
|
|
59
59
|
DEFAULT_CONFIG_PATH = "~/.config/github2gerrit/configuration.txt"
|
60
60
|
|
61
|
-
# Recognized keys. Unknown keys
|
62
|
-
#
|
61
|
+
# Recognized keys. Unknown keys will be reported as warnings to help
|
62
|
+
# users catch typos and missing functionality.
|
63
63
|
KNOWN_KEYS: set[str] = {
|
64
64
|
# Action inputs
|
65
65
|
"SUBMIT_SINGLE_COMMITS",
|
@@ -74,6 +74,15 @@ KNOWN_KEYS: set[str] = {
|
|
74
74
|
"PR_NUMBER",
|
75
75
|
"SYNC_ALL_OPEN_PRS",
|
76
76
|
"PRESERVE_GITHUB_PRS",
|
77
|
+
"ALLOW_GHE_URLS",
|
78
|
+
"DRY_RUN",
|
79
|
+
"ALLOW_DUPLICATES",
|
80
|
+
"ISSUE_ID",
|
81
|
+
"G2G_VERBOSE",
|
82
|
+
"G2G_SKIP_GERRIT_COMMENTS",
|
83
|
+
"G2G_ENABLE_DERIVATION",
|
84
|
+
"G2G_AUTO_SAVE_CONFIG",
|
85
|
+
"GITHUB_TOKEN",
|
77
86
|
# Optional inputs (reusable workflow compatibility)
|
78
87
|
"GERRIT_SERVER",
|
79
88
|
"GERRIT_SERVER_PORT",
|
@@ -264,6 +273,17 @@ def load_org_config(
|
|
264
273
|
)
|
265
274
|
|
266
275
|
normalized = _normalize_keys(result)
|
276
|
+
|
277
|
+
# Report unknown configuration keys to help users catch typos
|
278
|
+
unknown_keys = set(normalized.keys()) - KNOWN_KEYS
|
279
|
+
if unknown_keys:
|
280
|
+
log.warning(
|
281
|
+
"Unknown configuration keys found in [%s]: %s. "
|
282
|
+
"These will be ignored. Check for typos or missing functionality.",
|
283
|
+
effective_org or "default",
|
284
|
+
", ".join(sorted(unknown_keys)),
|
285
|
+
)
|
286
|
+
|
267
287
|
return normalized
|
268
288
|
|
269
289
|
|
@@ -295,6 +315,198 @@ def filter_known(
|
|
295
315
|
return {k: v for k, v in cfg.items() if k in KNOWN_KEYS}
|
296
316
|
|
297
317
|
|
318
|
+
def _is_github_actions_context() -> bool:
|
319
|
+
"""Detect if running in GitHub Actions environment."""
|
320
|
+
return (
|
321
|
+
os.getenv("GITHUB_ACTIONS") == "true"
|
322
|
+
or os.getenv("GITHUB_EVENT_NAME", "").strip() != ""
|
323
|
+
)
|
324
|
+
|
325
|
+
|
326
|
+
def _is_local_cli_context() -> bool:
|
327
|
+
"""Detect if running as local CLI tool."""
|
328
|
+
return not _is_github_actions_context()
|
329
|
+
|
330
|
+
|
331
|
+
def derive_gerrit_parameters(organization: str | None) -> dict[str, str]:
|
332
|
+
"""Derive Gerrit parameters from GitHub organization name.
|
333
|
+
|
334
|
+
Args:
|
335
|
+
organization: GitHub organization name
|
336
|
+
|
337
|
+
Returns:
|
338
|
+
Dict with derived parameter values:
|
339
|
+
- GERRIT_SSH_USER_G2G: [org].gh2gerrit
|
340
|
+
- GERRIT_SSH_USER_G2G_EMAIL: releng+[org]-gh2gerrit@linuxfoundation.org
|
341
|
+
- GERRIT_SERVER: gerrit.[org].org
|
342
|
+
"""
|
343
|
+
if not organization:
|
344
|
+
return {}
|
345
|
+
|
346
|
+
org = organization.strip().lower()
|
347
|
+
return {
|
348
|
+
"GERRIT_SSH_USER_G2G": f"{org}.gh2gerrit",
|
349
|
+
"GERRIT_SSH_USER_G2G_EMAIL": (
|
350
|
+
f"releng+{org}-gh2gerrit@linuxfoundation.org"
|
351
|
+
),
|
352
|
+
"GERRIT_SERVER": f"gerrit.{org}.org",
|
353
|
+
}
|
354
|
+
|
355
|
+
|
356
|
+
def apply_parameter_derivation(
|
357
|
+
cfg: dict[str, str],
|
358
|
+
organization: str | None = None,
|
359
|
+
save_to_config: bool = True,
|
360
|
+
) -> dict[str, str]:
|
361
|
+
"""Apply dynamic parameter derivation for missing Gerrit parameters.
|
362
|
+
|
363
|
+
This function derives standard Gerrit parameters when they are not
|
364
|
+
explicitly configured. The derivation is based on the GitHub organization:
|
365
|
+
|
366
|
+
- gerrit_ssh_user_g2g: [org].gh2gerrit
|
367
|
+
- gerrit_ssh_user_g2g_email: releng+[org]-gh2gerrit@linuxfoundation.org
|
368
|
+
- gerrit_server: gerrit.[org].org
|
369
|
+
|
370
|
+
Derivation behavior depends on execution context:
|
371
|
+
- GitHub Actions: Automatic derivation when organization is available
|
372
|
+
- Local CLI: Requires G2G_ENABLE_DERIVATION=true for automatic derivation
|
373
|
+
|
374
|
+
Args:
|
375
|
+
cfg: Configuration dictionary to augment
|
376
|
+
organization: GitHub organization name for derivation
|
377
|
+
save_to_config: Whether to save derived parameters to config file
|
378
|
+
|
379
|
+
Returns:
|
380
|
+
Configuration dictionary with derived values for missing parameters
|
381
|
+
"""
|
382
|
+
if not organization:
|
383
|
+
return cfg
|
384
|
+
|
385
|
+
# Check execution context to determine derivation strategy
|
386
|
+
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")
|
390
|
+
|
391
|
+
if not enable_derivation:
|
392
|
+
log.debug(
|
393
|
+
"Parameter derivation disabled for local CLI usage. "
|
394
|
+
"Set G2G_ENABLE_DERIVATION=true to enable automatic derivation."
|
395
|
+
)
|
396
|
+
return cfg
|
397
|
+
|
398
|
+
# Only derive parameters that are missing or empty
|
399
|
+
derived = derive_gerrit_parameters(organization)
|
400
|
+
result = dict(cfg)
|
401
|
+
newly_derived = {}
|
402
|
+
|
403
|
+
for key, value in derived.items():
|
404
|
+
if key not in result or not result[key].strip():
|
405
|
+
log.debug(
|
406
|
+
"Deriving %s from organization '%s': %s (context: %s)",
|
407
|
+
key,
|
408
|
+
organization,
|
409
|
+
value,
|
410
|
+
"GitHub Actions" if is_github_actions else "Local CLI",
|
411
|
+
)
|
412
|
+
result[key] = value
|
413
|
+
newly_derived[key] = value
|
414
|
+
|
415
|
+
# Save newly derived parameters to configuration file for future use
|
416
|
+
# Default to true for local CLI, false for GitHub Actions
|
417
|
+
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")
|
421
|
+
if save_to_config and newly_derived and auto_save_enabled:
|
422
|
+
# Save to config in local CLI mode to create persistent configuration
|
423
|
+
try:
|
424
|
+
save_derived_parameters_to_config(organization, newly_derived)
|
425
|
+
log.info(
|
426
|
+
"Automatically saved derived parameters to configuration "
|
427
|
+
"file for organization '%s'. "
|
428
|
+
"This creates a persistent configuration that you can "
|
429
|
+
"customize if needed.",
|
430
|
+
organization,
|
431
|
+
)
|
432
|
+
except Exception as exc:
|
433
|
+
log.warning("Failed to save derived parameters to config: %s", exc)
|
434
|
+
|
435
|
+
return result
|
436
|
+
|
437
|
+
|
438
|
+
def save_derived_parameters_to_config(
|
439
|
+
organization: str,
|
440
|
+
derived_params: dict[str, str],
|
441
|
+
config_path: str | None = None,
|
442
|
+
) -> None:
|
443
|
+
"""Save derived parameters to the organization's configuration file.
|
444
|
+
|
445
|
+
This function updates the configuration file to include any derived
|
446
|
+
parameters that are not already present in the organization section.
|
447
|
+
This creates a persistent configuration that users can modify if needed.
|
448
|
+
|
449
|
+
Args:
|
450
|
+
organization: GitHub organization name for config section
|
451
|
+
derived_params: Dictionary of derived parameter key-value pairs
|
452
|
+
config_path: Path to config file (optional, uses default if not
|
453
|
+
provided)
|
454
|
+
|
455
|
+
Raises:
|
456
|
+
Exception: If saving fails
|
457
|
+
"""
|
458
|
+
if not organization or not derived_params:
|
459
|
+
return
|
460
|
+
|
461
|
+
if config_path is None:
|
462
|
+
config_path = (
|
463
|
+
os.getenv("G2G_CONFIG_PATH", "").strip() or DEFAULT_CONFIG_PATH
|
464
|
+
)
|
465
|
+
|
466
|
+
config_file = Path(config_path).expanduser()
|
467
|
+
|
468
|
+
try:
|
469
|
+
# Ensure the directory exists
|
470
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
471
|
+
|
472
|
+
# Parse existing content using configparser
|
473
|
+
cp = _load_ini(config_file)
|
474
|
+
|
475
|
+
# Find or create the organization section
|
476
|
+
org_section = _select_section(cp, organization)
|
477
|
+
if org_section is None:
|
478
|
+
# Section doesn't exist, we'll need to add it
|
479
|
+
cp.add_section(organization)
|
480
|
+
org_section = organization
|
481
|
+
|
482
|
+
# Add derived parameters that don't already exist
|
483
|
+
params_added = []
|
484
|
+
for key, value in derived_params.items():
|
485
|
+
if not cp.has_option(org_section, key):
|
486
|
+
cp.set(org_section, key, f'"{value}"')
|
487
|
+
params_added.append(key)
|
488
|
+
|
489
|
+
# Only write if we added parameters
|
490
|
+
if params_added:
|
491
|
+
# Write the updated configuration
|
492
|
+
with config_file.open("w", encoding="utf-8") as f:
|
493
|
+
cp.write(f)
|
494
|
+
|
495
|
+
log.debug(
|
496
|
+
"Saved derived parameters to configuration file %s [%s]: %s",
|
497
|
+
config_file,
|
498
|
+
organization,
|
499
|
+
", ".join(params_added),
|
500
|
+
)
|
501
|
+
|
502
|
+
except Exception as exc:
|
503
|
+
log.warning(
|
504
|
+
"Failed to save derived parameters to configuration file %s: %s",
|
505
|
+
config_file,
|
506
|
+
exc,
|
507
|
+
)
|
508
|
+
|
509
|
+
|
298
510
|
def overlay_missing(
|
299
511
|
primary: dict[str, str],
|
300
512
|
fallback: dict[str, str],
|