github2gerrit 0.1.4__tar.gz → 0.1.5__tar.gz
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-0.1.4/src/github2gerrit.egg-info → github2gerrit-0.1.5}/PKG-INFO +3 -1
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/action.yaml +18 -5
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/pyproject.toml +20 -2
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/sitecustomize.py +7 -2
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit/cli.py +139 -20
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit/config.py +194 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit/core.py +174 -38
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit/github_api.py +9 -3
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit/gitutils.py +22 -3
- github2gerrit-0.1.5/src/github2gerrit/ssh_discovery.py +412 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5/src/github2gerrit.egg-info}/PKG-INFO +3 -1
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit.egg-info/SOURCES.txt +3 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit.egg-info/requires.txt +2 -0
- github2gerrit-0.1.5/tests/test_cli.py +270 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_cli_helpers.py +14 -1
- github2gerrit-0.1.5/tests/test_config_helpers.py +683 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_core_config_and_errors.py +12 -1
- github2gerrit-0.1.5/tests/test_core_gerrit_push_errors.py +471 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_core_ssh_setup.py +215 -9
- github2gerrit-0.1.5/tests/test_ssh_discovery.py +362 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_url_parser.py +14 -2
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/uv.lock +273 -15
- github2gerrit-0.1.4/tests/test_cli.py +0 -137
- github2gerrit-0.1.4/tests/test_config_helpers.py +0 -271
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.editorconfig +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/actionlint.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/dependabot.yml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/release-drafter.yml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/workflows/build-test-release.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/workflows/build-test.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/workflows/dependencies.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/workflows/github2gerrit.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/workflows/release-drafter.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/workflows/semantic-pull-request.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.github/workflows/sha-pinned-actions.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.gitignore +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.gitlint +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.pre-commit-config.yaml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.readthedocs.yml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/.yamllint +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/LICENSE +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/README.md +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/REUSE.toml +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/setup.cfg +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit/models.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit.egg-info/dependency_links.txt +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit.egg-info/entry_points.txt +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/src/github2gerrit.egg-info/top_level.txt +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/conftest.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_cli_outputs_file.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_cli_url_and_dryrun.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_core_close_pr_policy.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_core_prepare_commits.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-0.1.4 → github2gerrit-0.1.5}/tests/test_gitutils_helpers.py +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: github2gerrit
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.5
|
4
4
|
Summary: Submit a GitHub pull request to a Gerrit repository.
|
5
5
|
Author-email: Matthew Watkins <mwatkins@linuxfoundation.org>
|
6
6
|
License-Expression: Apache-2.0
|
@@ -34,6 +34,8 @@ Requires-Dist: ruff>=0.6.3; extra == "dev"
|
|
34
34
|
Requires-Dist: black>=24.8.0; extra == "dev"
|
35
35
|
Requires-Dist: mypy>=1.11.2; extra == "dev"
|
36
36
|
Requires-Dist: pytest-mock>=3.14.0; extra == "dev"
|
37
|
+
Requires-Dist: types-requests>=2.31.0; extra == "dev"
|
38
|
+
Requires-Dist: types-click>=7.1.8; extra == "dev"
|
37
39
|
Dynamic: license-file
|
38
40
|
|
39
41
|
<!--
|
@@ -31,11 +31,20 @@ inputs:
|
|
31
31
|
description: "SSH private key content used to authenticate to Gerrit"
|
32
32
|
required: true
|
33
33
|
GERRIT_SSH_USER_G2G:
|
34
|
-
description:
|
35
|
-
|
34
|
+
description: >
|
35
|
+
Gerrit SSH username (e.g. automation bot account). In GitHub Actions,
|
36
|
+
automatically derived as [ORGANIZATION].gh2gerrit when not provided.
|
37
|
+
For local CLI usage, set G2G_ENABLE_DERIVATION=true to enable derivation.
|
38
|
+
required: false
|
39
|
+
default: ""
|
36
40
|
GERRIT_SSH_USER_G2G_EMAIL:
|
37
|
-
description:
|
38
|
-
|
41
|
+
description: >
|
42
|
+
Email address associated to the Gerrit SSH user. In GitHub Actions,
|
43
|
+
automatically derived as
|
44
|
+
releng+[ORGANIZATION]-gh2gerrit@linuxfoundation.org when not provided.
|
45
|
+
For local CLI usage, set G2G_ENABLE_DERIVATION=true to enable derivation.
|
46
|
+
required: false
|
47
|
+
default: ""
|
39
48
|
|
40
49
|
# Behavior and metadata
|
41
50
|
ORGANIZATION:
|
@@ -69,7 +78,11 @@ inputs:
|
|
69
78
|
|
70
79
|
# Optional Gerrit overrides (used when .gitreview is missing)
|
71
80
|
GERRIT_SERVER:
|
72
|
-
description:
|
81
|
+
description: >
|
82
|
+
Gerrit server hostname (optional; use .gitreview if present). In GitHub
|
83
|
+
Actions, automatically derived as gerrit.[ORGANIZATION].org when not
|
84
|
+
provided. For local CLI usage, set G2G_ENABLE_DERIVATION=true to enable
|
85
|
+
derivation.
|
73
86
|
required: false
|
74
87
|
default: ""
|
75
88
|
GERRIT_SERVER_PORT:
|
@@ -71,6 +71,8 @@ dev = [
|
|
71
71
|
|
72
72
|
# Type checking helpers
|
73
73
|
"pytest-mock>=3.14.0",
|
74
|
+
"types-requests>=2.31.0",
|
75
|
+
"types-click>=7.1.8",
|
74
76
|
]
|
75
77
|
|
76
78
|
[tool.ruff]
|
@@ -91,6 +93,7 @@ ignore = [
|
|
91
93
|
|
92
94
|
[tool.ruff.lint.per-file-ignores]
|
93
95
|
"tests/*.py" = ["S101", "S105", "PLW0603", "E501", "TRY003"]
|
96
|
+
"sitecustomize.py" = ["S110", "SIM103", "SIM105", "PLW2901", "E501"]
|
94
97
|
|
95
98
|
[tool.ruff.lint.isort]
|
96
99
|
known-first-party = ["github2gerrit"]
|
@@ -127,6 +130,23 @@ exclude = [
|
|
127
130
|
module = ["pygerrit2", "pygerrit2.*"]
|
128
131
|
ignore_missing_imports = true
|
129
132
|
|
133
|
+
[[tool.mypy.overrides]]
|
134
|
+
module = ["click", "click.*"]
|
135
|
+
ignore_missing_imports = true
|
136
|
+
|
137
|
+
[[tool.mypy.overrides]]
|
138
|
+
module = ["typer", "typer.*"]
|
139
|
+
ignore_missing_imports = true
|
140
|
+
|
141
|
+
[[tool.mypy.overrides]]
|
142
|
+
module = ["pytest", "pytest.*"]
|
143
|
+
ignore_missing_imports = true
|
144
|
+
|
145
|
+
[[tool.mypy.overrides]]
|
146
|
+
module = ["tests.*"]
|
147
|
+
disallow_untyped_defs = false
|
148
|
+
disallow_untyped_calls = false
|
149
|
+
|
130
150
|
[tool.coverage.run]
|
131
151
|
branch = true
|
132
152
|
source = ["src/github2gerrit"]
|
@@ -153,5 +173,3 @@ testpaths = ["tests"]
|
|
153
173
|
# No extra settings are required here; this stanza reserves the
|
154
174
|
# namespace for future use if needed.
|
155
175
|
managed = true
|
156
|
-
|
157
|
-
|
@@ -81,7 +81,9 @@ def _iter_cov_candidates(base: Path) -> Iterable[Path]:
|
|
81
81
|
_dbg(f"Failed to iterate {base}: {exc}")
|
82
82
|
|
83
83
|
|
84
|
-
def _clean_stale_coverage_files(
|
84
|
+
def _clean_stale_coverage_files(
|
85
|
+
bases: Iterable[Path], protect: Path | None
|
86
|
+
) -> None:
|
85
87
|
protected = str(protect.resolve()) if protect else None
|
86
88
|
for base in bases:
|
87
89
|
try:
|
@@ -109,7 +111,10 @@ def _ensure_unique_coverage_file() -> Path:
|
|
109
111
|
except Exception:
|
110
112
|
# Fall through to create a sane default
|
111
113
|
pass
|
112
|
-
unique =
|
114
|
+
unique = (
|
115
|
+
Path(tempfile.gettempdir())
|
116
|
+
/ f".coverage.pytest.{os.getpid()}.{uuid.uuid4().hex}"
|
117
|
+
)
|
113
118
|
os.environ["COVERAGE_FILE"] = str(unique)
|
114
119
|
return unique
|
115
120
|
|
@@ -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,21 @@ 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
|
+
|
34
55
|
class ConfigurationError(Exception):
|
35
56
|
"""Raised when configuration validation fails.
|
36
57
|
|
@@ -83,14 +104,36 @@ def _parse_github_target(url: str) -> tuple[str | None, str | None, int | None]:
|
|
83
104
|
APP_NAME = "github2gerrit"
|
84
105
|
|
85
106
|
|
86
|
-
|
87
|
-
|
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:
|
88
126
|
# Force a simplified usage line without COMMAND [ARGS]...
|
89
127
|
formatter.write_usage(
|
90
128
|
ctx.command_path, "[OPTIONS] TARGET_URL", prefix="Usage: "
|
91
129
|
)
|
92
130
|
|
93
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
|
+
|
94
137
|
app: typer.Typer = typer.Typer(
|
95
138
|
add_completion=False,
|
96
139
|
no_args_is_help=False,
|
@@ -108,7 +151,17 @@ def _resolve_org(default_org: str | None) -> str:
|
|
108
151
|
return ""
|
109
152
|
|
110
153
|
|
111
|
-
|
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()
|
112
165
|
def main(
|
113
166
|
ctx: typer.Context,
|
114
167
|
target_url: str | None = typer.Argument(
|
@@ -400,7 +453,7 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> None:
|
|
400
453
|
per_ctx, allow_duplicates=data.allow_duplicates
|
401
454
|
)
|
402
455
|
except DuplicateChangeError as exc:
|
403
|
-
log
|
456
|
+
_log_exception_conditionally(log, "Skipping PR #%d", pr_number)
|
404
457
|
typer.echo(f"Skipping PR #{pr_number}: {exc}")
|
405
458
|
continue
|
406
459
|
|
@@ -426,7 +479,9 @@ def _process_bulk(data: Inputs, gh: GitHubContext) -> None:
|
|
426
479
|
result_multi.change_numbers,
|
427
480
|
)
|
428
481
|
except Exception as exc:
|
429
|
-
|
482
|
+
_log_exception_conditionally(
|
483
|
+
log, "Failed to process PR #%d", pr_number
|
484
|
+
)
|
430
485
|
typer.echo(f"Failed to process PR #{pr_number}: {exc}")
|
431
486
|
log.info("Continuing to next PR despite failure")
|
432
487
|
continue
|
@@ -577,6 +632,10 @@ def _load_effective_inputs() -> Inputs:
|
|
577
632
|
or os.getenv("GITHUB_REPOSITORY_OWNER")
|
578
633
|
)
|
579
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
|
+
|
580
639
|
apply_config_to_env(cfg)
|
581
640
|
|
582
641
|
# Refresh inputs after applying configuration to environment
|
@@ -677,7 +736,7 @@ def _process() -> None:
|
|
677
736
|
try:
|
678
737
|
_validate_inputs(data)
|
679
738
|
except ConfigurationError as exc:
|
680
|
-
log
|
739
|
+
_log_exception_conditionally(log, "Configuration validation failed")
|
681
740
|
typer.echo(f"Configuration validation failed: {exc}", err=True)
|
682
741
|
raise typer.Exit(code=2) from exc
|
683
742
|
|
@@ -722,7 +781,7 @@ def _process() -> None:
|
|
722
781
|
try:
|
723
782
|
check_for_duplicates(gh, allow_duplicates=data.allow_duplicates)
|
724
783
|
except DuplicateChangeError as exc:
|
725
|
-
log
|
784
|
+
_log_exception_conditionally(log, "Duplicate change detected")
|
726
785
|
typer.echo(f"Error: {exc}", err=True)
|
727
786
|
typer.echo(
|
728
787
|
"Use --allow-duplicates to override this check.", err=True
|
@@ -820,25 +879,85 @@ def _validate_inputs(data: Inputs) -> None:
|
|
820
879
|
)
|
821
880
|
raise ConfigurationError(msg)
|
822
881
|
|
823
|
-
#
|
824
|
-
|
825
|
-
|
826
|
-
|
827
|
-
|
828
|
-
|
829
|
-
|
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:
|
830
924
|
if not getattr(data, field_name):
|
831
925
|
log.error("Missing required input: %s", field_name)
|
832
|
-
|
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
|
+
)
|
833
952
|
|
834
953
|
# Validate fetch depth is a positive integer
|
835
954
|
if data.fetch_depth <= 0:
|
836
955
|
log.error("Invalid FETCH_DEPTH: %s", data.fetch_depth)
|
837
|
-
raise ConfigurationError(
|
956
|
+
raise ConfigurationError(_MSG_INVALID_FETCH_DEPTH)
|
838
957
|
|
839
958
|
# Validate Issue ID is a single line string if provided
|
840
959
|
if data.issue_id and ("\n" in data.issue_id or "\r" in data.issue_id):
|
841
|
-
raise ConfigurationError(
|
960
|
+
raise ConfigurationError(_MSG_ISSUE_ID_MULTILINE)
|
842
961
|
|
843
962
|
|
844
963
|
def _log_effective_config(data: Inputs, gh: GitHubContext) -> None:
|
@@ -848,10 +967,10 @@ def _log_effective_config(data: Inputs, gh: GitHubContext) -> None:
|
|
848
967
|
log.info(" SUBMIT_SINGLE_COMMITS: %s", data.submit_single_commits)
|
849
968
|
log.info(" USE_PR_AS_COMMIT: %s", data.use_pr_as_commit)
|
850
969
|
log.info(" FETCH_DEPTH: %s", data.fetch_depth)
|
851
|
-
|
852
|
-
"
|
853
|
-
"<provided>" if data.gerrit_known_hosts else "<missing>",
|
970
|
+
known_hosts_status = (
|
971
|
+
"<provided>" if data.gerrit_known_hosts else "<will auto-discover>"
|
854
972
|
)
|
973
|
+
log.info(" GERRIT_KNOWN_HOSTS: %s", known_hosts_status)
|
855
974
|
log.info(" GERRIT_SSH_PRIVKEY_G2G: %s", safe_privkey)
|
856
975
|
log.info(" GERRIT_SSH_USER_G2G: %s", data.gerrit_ssh_user_g2g)
|
857
976
|
log.info(" GERRIT_SSH_USER_G2G_EMAIL: %s", data.gerrit_ssh_user_g2g_email)
|
@@ -80,6 +80,8 @@ KNOWN_KEYS: set[str] = {
|
|
80
80
|
"ISSUE_ID",
|
81
81
|
"G2G_VERBOSE",
|
82
82
|
"G2G_SKIP_GERRIT_COMMENTS",
|
83
|
+
"G2G_ENABLE_DERIVATION",
|
84
|
+
"G2G_AUTO_SAVE_CONFIG",
|
83
85
|
"GITHUB_TOKEN",
|
84
86
|
# Optional inputs (reusable workflow compatibility)
|
85
87
|
"GERRIT_SERVER",
|
@@ -313,6 +315,198 @@ def filter_known(
|
|
313
315
|
return {k: v for k, v in cfg.items() if k in KNOWN_KEYS}
|
314
316
|
|
315
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
|
+
|
316
510
|
def overlay_missing(
|
317
511
|
primary: dict[str, str],
|
318
512
|
fallback: dict[str, str],
|