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
@@ -0,0 +1,412 @@
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
3
|
+
|
4
|
+
"""
|
5
|
+
SSH host key auto-discovery for github2gerrit.
|
6
|
+
|
7
|
+
This module provides functionality to automatically discover and fetch SSH
|
8
|
+
host keys for Gerrit servers, eliminating the need for manual
|
9
|
+
GERRIT_KNOWN_HOSTS configuration.
|
10
|
+
"""
|
11
|
+
|
12
|
+
from __future__ import annotations
|
13
|
+
|
14
|
+
import logging
|
15
|
+
import os
|
16
|
+
import socket
|
17
|
+
from pathlib import Path
|
18
|
+
|
19
|
+
from .gitutils import CommandError
|
20
|
+
from .gitutils import run_cmd
|
21
|
+
|
22
|
+
|
23
|
+
log = logging.getLogger(__name__)
|
24
|
+
|
25
|
+
|
26
|
+
class SSHDiscoveryError(Exception):
|
27
|
+
"""Raised when SSH host key discovery fails."""
|
28
|
+
|
29
|
+
|
30
|
+
# Error message constants to comply with TRY003
|
31
|
+
_MSG_HOST_UNREACHABLE = (
|
32
|
+
"Host {hostname}:{port} is not reachable. "
|
33
|
+
"Check network connectivity and server availability."
|
34
|
+
)
|
35
|
+
_MSG_NO_KEYS_FOUND = (
|
36
|
+
"No SSH host keys found for {hostname}:{port}. "
|
37
|
+
"The server may not be running SSH or may be blocking connections."
|
38
|
+
)
|
39
|
+
_MSG_NO_VALID_KEYS = (
|
40
|
+
"No valid SSH host keys found for {hostname}:{port}. "
|
41
|
+
"The ssh-keyscan output was empty or malformed."
|
42
|
+
)
|
43
|
+
_MSG_CONNECTION_FAILED = (
|
44
|
+
"Failed to connect to {hostname}:{port} for SSH key discovery. "
|
45
|
+
"Error: {error}"
|
46
|
+
)
|
47
|
+
_MSG_KEYSCAN_FAILED = (
|
48
|
+
"ssh-keyscan failed with return code {returncode}: {error}"
|
49
|
+
)
|
50
|
+
_MSG_UNEXPECTED_ERROR = (
|
51
|
+
"Unexpected error during SSH key discovery for {hostname}:{port}: {error}"
|
52
|
+
)
|
53
|
+
_MSG_SAVE_FAILED = (
|
54
|
+
"Failed to save host keys to configuration file {config_file}: {error}"
|
55
|
+
)
|
56
|
+
|
57
|
+
|
58
|
+
def is_host_reachable(hostname: str, port: int, timeout: int = 5) -> bool:
|
59
|
+
"""Check if a host and port are reachable via TCP."""
|
60
|
+
try:
|
61
|
+
with socket.create_connection((hostname, port), timeout=timeout):
|
62
|
+
return True
|
63
|
+
except OSError:
|
64
|
+
return False
|
65
|
+
|
66
|
+
|
67
|
+
def fetch_ssh_host_keys(
|
68
|
+
hostname: str, port: int = 22, timeout: int = 10
|
69
|
+
) -> str:
|
70
|
+
"""
|
71
|
+
Fetch SSH host keys for a given hostname and port using ssh-keyscan.
|
72
|
+
|
73
|
+
Args:
|
74
|
+
hostname: The hostname to scan
|
75
|
+
port: The SSH port (default: 22)
|
76
|
+
timeout: Connection timeout in seconds (default: 10)
|
77
|
+
|
78
|
+
Returns:
|
79
|
+
A string containing the host keys in known_hosts format
|
80
|
+
|
81
|
+
Raises:
|
82
|
+
SSHDiscoveryError: If the host keys cannot be fetched
|
83
|
+
"""
|
84
|
+
log.debug("Fetching SSH host keys for %s:%d", hostname, port)
|
85
|
+
|
86
|
+
# First check if the host is reachable
|
87
|
+
if not is_host_reachable(hostname, port, timeout=5):
|
88
|
+
raise SSHDiscoveryError(
|
89
|
+
_MSG_HOST_UNREACHABLE.format(hostname=hostname, port=port)
|
90
|
+
)
|
91
|
+
|
92
|
+
try:
|
93
|
+
# Use ssh-keyscan to fetch all available key types
|
94
|
+
cmd = [
|
95
|
+
"ssh-keyscan",
|
96
|
+
"-p",
|
97
|
+
str(port),
|
98
|
+
"-T",
|
99
|
+
str(timeout),
|
100
|
+
"-t",
|
101
|
+
"rsa,ecdsa,ed25519",
|
102
|
+
hostname,
|
103
|
+
]
|
104
|
+
|
105
|
+
result = run_cmd(cmd, timeout=timeout + 5)
|
106
|
+
|
107
|
+
if not result.stdout or not result.stdout.strip():
|
108
|
+
raise SSHDiscoveryError( # noqa: TRY301
|
109
|
+
_MSG_NO_KEYS_FOUND.format(hostname=hostname, port=port)
|
110
|
+
)
|
111
|
+
|
112
|
+
# Validate that we got proper known_hosts format
|
113
|
+
lines = result.stdout.strip().split("\n")
|
114
|
+
valid_lines = []
|
115
|
+
|
116
|
+
for line in lines:
|
117
|
+
stripped_line = line.strip()
|
118
|
+
if not stripped_line or stripped_line.startswith("#"):
|
119
|
+
continue
|
120
|
+
|
121
|
+
# Basic validation: should have hostname, key type, and key
|
122
|
+
parts = stripped_line.split()
|
123
|
+
if len(parts) >= 3:
|
124
|
+
valid_lines.append(stripped_line)
|
125
|
+
|
126
|
+
if not valid_lines:
|
127
|
+
raise SSHDiscoveryError( # noqa: TRY301
|
128
|
+
_MSG_NO_VALID_KEYS.format(hostname=hostname, port=port)
|
129
|
+
)
|
130
|
+
|
131
|
+
discovered_keys = "\n".join(valid_lines)
|
132
|
+
log.info(
|
133
|
+
"Successfully discovered %d SSH host key(s) for %s:%d",
|
134
|
+
len(valid_lines),
|
135
|
+
hostname,
|
136
|
+
port,
|
137
|
+
)
|
138
|
+
log.debug("Discovered keys:\n%s", discovered_keys)
|
139
|
+
|
140
|
+
except CommandError as exc:
|
141
|
+
if exc.returncode == 1:
|
142
|
+
# ssh-keyscan returns 1 when it can't connect
|
143
|
+
error_msg = exc.stderr or exc.stdout or "Connection failed"
|
144
|
+
raise SSHDiscoveryError(
|
145
|
+
_MSG_CONNECTION_FAILED.format(
|
146
|
+
hostname=hostname, port=port, error=error_msg
|
147
|
+
)
|
148
|
+
) from exc
|
149
|
+
else:
|
150
|
+
error_msg = exc.stderr or exc.stdout or "Unknown error"
|
151
|
+
raise SSHDiscoveryError(
|
152
|
+
_MSG_KEYSCAN_FAILED.format(
|
153
|
+
returncode=exc.returncode, error=error_msg
|
154
|
+
)
|
155
|
+
) from exc
|
156
|
+
except Exception as exc:
|
157
|
+
raise SSHDiscoveryError(
|
158
|
+
_MSG_UNEXPECTED_ERROR.format(
|
159
|
+
hostname=hostname, port=port, error=exc
|
160
|
+
)
|
161
|
+
) from exc
|
162
|
+
else:
|
163
|
+
return discovered_keys
|
164
|
+
|
165
|
+
|
166
|
+
def extract_gerrit_info_from_gitreview(content: str) -> tuple[str, int] | None:
|
167
|
+
"""
|
168
|
+
Extract Gerrit hostname and port from .gitreview file content.
|
169
|
+
|
170
|
+
Args:
|
171
|
+
content: The content of a .gitreview file
|
172
|
+
|
173
|
+
Returns:
|
174
|
+
A tuple of (hostname, port) or None if not found
|
175
|
+
"""
|
176
|
+
hostname = None
|
177
|
+
port = 29418 # Default Gerrit SSH port
|
178
|
+
|
179
|
+
for line in content.split("\n"):
|
180
|
+
stripped_line = line.strip()
|
181
|
+
if "=" not in stripped_line:
|
182
|
+
continue
|
183
|
+
|
184
|
+
key, value = stripped_line.split("=", 1)
|
185
|
+
key = key.strip().lower()
|
186
|
+
value = value.strip()
|
187
|
+
|
188
|
+
if key == "host":
|
189
|
+
hostname = value
|
190
|
+
elif key == "port":
|
191
|
+
try:
|
192
|
+
port = int(value)
|
193
|
+
except ValueError:
|
194
|
+
log.warning("Invalid port in .gitreview: %s", value)
|
195
|
+
|
196
|
+
return (hostname, port) if hostname else None
|
197
|
+
|
198
|
+
|
199
|
+
def discover_and_save_host_keys(
|
200
|
+
hostname: str, port: int, organization: str, config_path: str | None = None
|
201
|
+
) -> str:
|
202
|
+
"""
|
203
|
+
Discover SSH host keys and save them to the organization's configuration.
|
204
|
+
|
205
|
+
Args:
|
206
|
+
hostname: Gerrit hostname
|
207
|
+
port: Gerrit SSH port
|
208
|
+
organization: GitHub organization name for config section
|
209
|
+
config_path: Path to config file (optional, uses default if not
|
210
|
+
provided)
|
211
|
+
|
212
|
+
Returns:
|
213
|
+
The discovered host keys string
|
214
|
+
|
215
|
+
Raises:
|
216
|
+
SSHDiscoveryError: If discovery or saving fails
|
217
|
+
"""
|
218
|
+
# Discover the host keys
|
219
|
+
host_keys = fetch_ssh_host_keys(hostname, port)
|
220
|
+
|
221
|
+
# Save to configuration file
|
222
|
+
save_host_keys_to_config(host_keys, organization, config_path)
|
223
|
+
|
224
|
+
return host_keys
|
225
|
+
|
226
|
+
|
227
|
+
def save_host_keys_to_config(
|
228
|
+
host_keys: str, organization: str, config_path: str | None = None
|
229
|
+
) -> None:
|
230
|
+
"""
|
231
|
+
Save SSH host keys to the organization's configuration file.
|
232
|
+
|
233
|
+
Args:
|
234
|
+
host_keys: The host keys in known_hosts format
|
235
|
+
organization: GitHub organization name for config section
|
236
|
+
config_path: Path to config file (optional, uses default if not
|
237
|
+
provided)
|
238
|
+
|
239
|
+
Raises:
|
240
|
+
SSHDiscoveryError: If saving fails
|
241
|
+
"""
|
242
|
+
from .config import DEFAULT_CONFIG_PATH
|
243
|
+
|
244
|
+
if config_path is None:
|
245
|
+
config_path = (
|
246
|
+
os.getenv("G2G_CONFIG_PATH", "").strip() or DEFAULT_CONFIG_PATH
|
247
|
+
)
|
248
|
+
|
249
|
+
config_file = Path(config_path).expanduser()
|
250
|
+
|
251
|
+
try:
|
252
|
+
# Ensure the directory exists
|
253
|
+
config_file.parent.mkdir(parents=True, exist_ok=True)
|
254
|
+
|
255
|
+
# Read existing configuration
|
256
|
+
existing_content = ""
|
257
|
+
if config_file.exists():
|
258
|
+
existing_content = config_file.read_text(encoding="utf-8")
|
259
|
+
|
260
|
+
# Parse existing content to find the organization section
|
261
|
+
lines = existing_content.split("\n")
|
262
|
+
new_lines = []
|
263
|
+
in_org_section = False
|
264
|
+
org_section_found = False
|
265
|
+
gerrit_known_hosts_updated = False
|
266
|
+
|
267
|
+
for line in lines:
|
268
|
+
stripped = line.strip()
|
269
|
+
|
270
|
+
# Check for section headers
|
271
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
272
|
+
section_name = stripped[1:-1].strip().lower()
|
273
|
+
in_org_section = section_name == organization.lower()
|
274
|
+
if in_org_section:
|
275
|
+
org_section_found = True
|
276
|
+
|
277
|
+
# If we're in the org section and find GERRIT_KNOWN_HOSTS, replace
|
278
|
+
elif in_org_section and "=" in line:
|
279
|
+
key = line.split("=", 1)[0].strip().upper()
|
280
|
+
if key == "GERRIT_KNOWN_HOSTS":
|
281
|
+
# Replace with new host keys (properly escaped for INI)
|
282
|
+
escaped_keys = host_keys.replace("\n", "\\n")
|
283
|
+
new_lines.append(f'GERRIT_KNOWN_HOSTS = "{escaped_keys}"')
|
284
|
+
gerrit_known_hosts_updated = True
|
285
|
+
continue
|
286
|
+
|
287
|
+
new_lines.append(line)
|
288
|
+
|
289
|
+
# If organization section wasn't found, add it
|
290
|
+
if not org_section_found:
|
291
|
+
if new_lines and new_lines[-1].strip():
|
292
|
+
new_lines.append("") # Add blank line before new section
|
293
|
+
new_lines.append(f"[{organization}]")
|
294
|
+
escaped_keys = host_keys.replace("\n", "\\n")
|
295
|
+
new_lines.append(f'GERRIT_KNOWN_HOSTS = "{escaped_keys}"')
|
296
|
+
gerrit_known_hosts_updated = True
|
297
|
+
|
298
|
+
# If section existed but didn't have GERRIT_KNOWN_HOSTS, add it
|
299
|
+
elif not gerrit_known_hosts_updated:
|
300
|
+
# Find the end of the organization section and add the key there
|
301
|
+
section_end = len(new_lines)
|
302
|
+
for i, line in enumerate(new_lines):
|
303
|
+
stripped = line.strip()
|
304
|
+
if stripped.startswith("[") and stripped.endswith("]"):
|
305
|
+
section_name = stripped[1:-1].strip().lower()
|
306
|
+
if section_name == organization.lower():
|
307
|
+
# Find the end of this section
|
308
|
+
for j in range(i + 1, len(new_lines)):
|
309
|
+
if new_lines[j].strip().startswith("["):
|
310
|
+
section_end = j
|
311
|
+
break
|
312
|
+
break
|
313
|
+
|
314
|
+
# Insert the GERRIT_KNOWN_HOSTS entry
|
315
|
+
escaped_keys = host_keys.replace("\n", "\\n")
|
316
|
+
new_lines.insert(
|
317
|
+
section_end, f'GERRIT_KNOWN_HOSTS = "{escaped_keys}"'
|
318
|
+
)
|
319
|
+
|
320
|
+
# Write the updated configuration
|
321
|
+
config_file.write_text("\n".join(new_lines), encoding="utf-8")
|
322
|
+
|
323
|
+
log.info(
|
324
|
+
"Successfully saved SSH host keys to configuration file: %s [%s]",
|
325
|
+
config_file,
|
326
|
+
organization,
|
327
|
+
)
|
328
|
+
|
329
|
+
except Exception as exc:
|
330
|
+
raise SSHDiscoveryError(
|
331
|
+
_MSG_SAVE_FAILED.format(config_file=config_file, error=exc)
|
332
|
+
) from exc
|
333
|
+
|
334
|
+
|
335
|
+
def auto_discover_gerrit_host_keys(
|
336
|
+
gerrit_hostname: str | None = None,
|
337
|
+
gerrit_port: int | None = None,
|
338
|
+
organization: str | None = None,
|
339
|
+
save_to_config: bool = True,
|
340
|
+
) -> str | None:
|
341
|
+
"""
|
342
|
+
Automatically discover Gerrit SSH host keys and optionally save to config.
|
343
|
+
|
344
|
+
This is the main entry point for auto-discovery functionality.
|
345
|
+
|
346
|
+
Args:
|
347
|
+
gerrit_hostname: Gerrit hostname (if not provided, tries to detect
|
348
|
+
from context)
|
349
|
+
gerrit_port: Gerrit SSH port (defaults to 29418)
|
350
|
+
organization: GitHub organization (if not provided, tries to detect
|
351
|
+
from env)
|
352
|
+
save_to_config: Whether to save discovered keys to config file
|
353
|
+
|
354
|
+
Returns:
|
355
|
+
The discovered host keys string, or None if discovery failed
|
356
|
+
"""
|
357
|
+
try:
|
358
|
+
# Set defaults
|
359
|
+
if gerrit_port is None:
|
360
|
+
gerrit_port = 29418
|
361
|
+
|
362
|
+
if organization is None:
|
363
|
+
organization = (
|
364
|
+
os.getenv("ORGANIZATION")
|
365
|
+
or os.getenv("GITHUB_REPOSITORY_OWNER")
|
366
|
+
or ""
|
367
|
+
).strip()
|
368
|
+
|
369
|
+
if not gerrit_hostname:
|
370
|
+
log.debug("No Gerrit hostname provided for auto-discovery")
|
371
|
+
return None
|
372
|
+
|
373
|
+
if not organization:
|
374
|
+
log.warning(
|
375
|
+
"No organization specified for SSH host key auto-discovery. "
|
376
|
+
"Cannot save to configuration file."
|
377
|
+
)
|
378
|
+
save_to_config = False
|
379
|
+
|
380
|
+
log.info(
|
381
|
+
"Attempting to auto-discover SSH host keys for %s:%d",
|
382
|
+
gerrit_hostname,
|
383
|
+
gerrit_port,
|
384
|
+
)
|
385
|
+
|
386
|
+
# Discover the host keys
|
387
|
+
host_keys = fetch_ssh_host_keys(gerrit_hostname, gerrit_port)
|
388
|
+
|
389
|
+
# Save to configuration if requested and possible
|
390
|
+
if save_to_config and organization:
|
391
|
+
save_host_keys_to_config(host_keys, organization)
|
392
|
+
log.info(
|
393
|
+
"SSH host keys automatically discovered and saved to config "
|
394
|
+
"for organization '%s'. Future runs will use the cached keys.",
|
395
|
+
organization,
|
396
|
+
)
|
397
|
+
else:
|
398
|
+
log.info(
|
399
|
+
"SSH host keys discovered but not saved to configuration. "
|
400
|
+
"Set ORGANIZATION environment variable to enable auto-saving."
|
401
|
+
)
|
402
|
+
|
403
|
+
except SSHDiscoveryError as exc:
|
404
|
+
log.warning("SSH host key auto-discovery failed: %s", exc)
|
405
|
+
return None
|
406
|
+
except Exception as exc:
|
407
|
+
log.warning(
|
408
|
+
"Unexpected error during SSH host key auto-discovery: %s", exc
|
409
|
+
)
|
410
|
+
return None
|
411
|
+
else:
|
412
|
+
return host_keys
|
@@ -1,14 +1,16 @@
|
|
1
|
-
Metadata-Version: 2.
|
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
|
+
Author-email: Matthew Watkins <mwatkins@linuxfoundation.org>
|
6
|
+
License-Expression: Apache-2.0
|
7
|
+
Project-URL: Homepage, https://github.com//lfreleng-actions/github2gerrit
|
8
|
+
Project-URL: Repository, https://github.com//lfreleng-actions/github2gerrit
|
9
|
+
Project-URL: Issues, https://github.com//lfreleng-actions/github2gerrit/issues
|
5
10
|
Keywords: github,gerrit,ci,actions,typer,cli
|
6
|
-
Author-Email: Matthew Watkins <mwatkins@linuxfoundation.org>
|
7
|
-
License: Apache-2.0
|
8
11
|
Classifier: Development Status :: 4 - Beta
|
9
12
|
Classifier: Environment :: Console
|
10
13
|
Classifier: Intended Audience :: Developers
|
11
|
-
Classifier: License :: OSI Approved :: Apache Software License
|
12
14
|
Classifier: Programming Language :: Python :: 3
|
13
15
|
Classifier: Programming Language :: Python :: 3 :: Only
|
14
16
|
Classifier: Programming Language :: Python :: 3.11
|
@@ -17,15 +19,24 @@ Classifier: Programming Language :: Python :: 3.13
|
|
17
19
|
Classifier: Topic :: Software Development :: Build Tools
|
18
20
|
Classifier: Topic :: Software Development :: Version Control
|
19
21
|
Classifier: Typing :: Typed
|
20
|
-
Project-URL: Homepage, https://github.com//lfreleng-actions/github2gerrit
|
21
|
-
Project-URL: Repository, https://github.com//lfreleng-actions/github2gerrit
|
22
|
-
Project-URL: Issues, https://github.com//lfreleng-actions/github2gerrit/issues
|
23
22
|
Requires-Python: <3.14,>=3.11
|
23
|
+
Description-Content-Type: text/markdown
|
24
|
+
License-File: LICENSE
|
24
25
|
Requires-Dist: typer>=0.12.5
|
25
26
|
Requires-Dist: PyGithub>=2.3.0
|
26
27
|
Requires-Dist: pygerrit2>=2.0.0
|
27
28
|
Requires-Dist: git-review>=2.3.1
|
28
|
-
|
29
|
+
Provides-Extra: dev
|
30
|
+
Requires-Dist: pytest>=8.3.2; extra == "dev"
|
31
|
+
Requires-Dist: pytest-cov>=5.0.0; extra == "dev"
|
32
|
+
Requires-Dist: coverage[toml]>=7.6.1; extra == "dev"
|
33
|
+
Requires-Dist: ruff>=0.6.3; extra == "dev"
|
34
|
+
Requires-Dist: black>=24.8.0; extra == "dev"
|
35
|
+
Requires-Dist: mypy>=1.11.2; extra == "dev"
|
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"
|
39
|
+
Dynamic: license-file
|
29
40
|
|
30
41
|
<!--
|
31
42
|
SPDX-License-Identifier: Apache-2.0
|
@@ -227,11 +238,18 @@ Debug output includes:
|
|
227
238
|
|
228
239
|
Common issues and solutions:
|
229
240
|
|
230
|
-
1. **
|
241
|
+
1. **Configuration Validation Errors**: The tool provides clear error messages when
|
242
|
+
required configuration is missing or invalid. Look for messages starting with
|
243
|
+
"Configuration validation failed:" that specify missing inputs like
|
244
|
+
`GERRIT_KNOWN_HOSTS`, `GERRIT_SSH_PRIVKEY_G2G`, etc.
|
245
|
+
2. **SSH Permission Denied**: Ensure `GERRIT_SSH_PRIVKEY_G2G` and
|
231
246
|
`GERRIT_KNOWN_HOSTS` are properly set
|
232
|
-
|
233
|
-
|
234
|
-
|
247
|
+
3. **Branch Not Found**: Check that the target branch exists in both GitHub and Gerrit
|
248
|
+
4. **Change-Id Issues**: Enable debug logging to see Change-Id generation and validation
|
249
|
+
5. **Gerrit API Errors**: Verify Gerrit server connectivity and project permissions
|
250
|
+
|
251
|
+
> **Note**: The tool displays configuration errors cleanly without Python tracebacks.
|
252
|
+
> If you see a traceback in the output, please report it as a bug.
|
235
253
|
|
236
254
|
### Environment Variables
|
237
255
|
|
@@ -383,6 +401,78 @@ the environment as:
|
|
383
401
|
- GERRIT_CHANGE_REQUEST_URL
|
384
402
|
- GERRIT_CHANGE_REQUEST_NUM
|
385
403
|
|
404
|
+
## Known Keys
|
405
|
+
|
406
|
+
The table below lists all the configuration directives supported by the tool,
|
407
|
+
along with the corresponding environment variable (also GitHub action input)
|
408
|
+
and the corresponding CLI flags.
|
409
|
+
|
410
|
+
<!-- markdownlint-disable MD013 -->
|
411
|
+
|
412
|
+
| Environment Variable / GitHub Input | Configuration Directive | CLI Flag | Description |
|
413
|
+
|-------------------------------------|-------------------------|----------|-------------|
|
414
|
+
| `SUBMIT_SINGLE_COMMITS` | `submit_single_commits` | `--submit-single-commits` | Submit one commit at a time to the Gerrit repository |
|
415
|
+
| `USE_PR_AS_COMMIT` | `use_pr_as_commit` | `--use-pr-as-commit` | Use PR title and body as the commit message |
|
416
|
+
| `FETCH_DEPTH` | `fetch_depth` | `--fetch-depth` | Fetch-depth for the clone (default: 10) |
|
417
|
+
| `GERRIT_KNOWN_HOSTS` | `gerrit_known_hosts` | `--gerrit-known-hosts` | Known hosts entries for Gerrit SSH |
|
418
|
+
| `GERRIT_SSH_PRIVKEY_G2G` | `gerrit_ssh_privkey_g2g` | `--gerrit-ssh-privkey-g2g` | SSH private key for Gerrit (string content) |
|
419
|
+
| `GERRIT_SSH_USER_G2G` | `gerrit_ssh_user_g2g` | `--gerrit-ssh-user-g2g` | Gerrit SSH user |
|
420
|
+
| `GERRIT_SSH_USER_G2G_EMAIL` | `gerrit_ssh_user_g2g_email` | `--gerrit-ssh-user-g2g-email` | Email address for the Gerrit SSH user |
|
421
|
+
| `ORGANIZATION` | `organization` | `--organization` | Organization (defaults to GITHUB_REPOSITORY_OWNER when unset) |
|
422
|
+
| `REVIEWERS_EMAIL` | `reviewers_email` | `--reviewers-email` | Comma-separated list of reviewer emails |
|
423
|
+
| `ALLOW_GHE_URLS` | `allow_ghe_urls` | `--allow-ghe-urls` | Allow non-github.com GitHub Enterprise URLs in direct URL mode |
|
424
|
+
| `PRESERVE_GITHUB_PRS` | `preserve_github_prs` | `--preserve-github-prs` | Do not close GitHub PRs after pushing to Gerrit |
|
425
|
+
| `DRY_RUN` | `dry_run` | `--dry-run` | Check settings and PR metadata; do not write to Gerrit |
|
426
|
+
| `GERRIT_SERVER` | `gerrit_server` | `--gerrit-server` | Gerrit server hostname (optional; .gitreview preferred) |
|
427
|
+
| `GERRIT_SERVER_PORT` | `gerrit_server_port` | `--gerrit-server-port` | Gerrit SSH port (default: 29418) |
|
428
|
+
| `GERRIT_PROJECT` | `gerrit_project` | `--gerrit-project` | Gerrit project (optional; .gitreview preferred) |
|
429
|
+
| `ISSUE_ID` | `issue_id` | `--issue-id` | Issue ID to include in commit message (e.g., Issue-ID: ABC-123) |
|
430
|
+
| `ALLOW_DUPLICATES` | `allow_duplicates` | `--allow-duplicates` | Allow submitting duplicate changes without error |
|
431
|
+
| `G2G_VERBOSE` | `g2g_verbose` | `--verbose` / `-v` | Enable verbose debug logging |
|
432
|
+
| `G2G_SKIP_GERRIT_COMMENTS` | `g2g_skip_gerrit_comments` | N/A | Skip adding back-reference comments to Gerrit changes |
|
433
|
+
| `GITHUB_TOKEN` | `github_token` | N/A | GitHub API token for accessing repository and PR data |
|
434
|
+
| `PR_NUMBER` | `pr_number` | N/A | Pull request number (set automatically in CI) |
|
435
|
+
| `SYNC_ALL_OPEN_PRS` | `sync_all_open_prs` | N/A | Process all open pull requests (internal use) |
|
436
|
+
| `GERRIT_HTTP_BASE_PATH` | `gerrit_http_base_path` | N/A | HTTP base path for Gerrit API (e.g., "/r") |
|
437
|
+
| `GERRIT_HTTP_USER` | `gerrit_http_user` | N/A | Gerrit HTTP username for REST API authentication |
|
438
|
+
| `GERRIT_HTTP_PASSWORD` | `gerrit_http_password` | N/A | Gerrit HTTP password/token for REST API authentication |
|
439
|
+
|
440
|
+
<!-- markdownlint-enable MD013 -->
|
441
|
+
|
442
|
+
### Configuration Precedence
|
443
|
+
|
444
|
+
The tool follows this precedence order for configuration values:
|
445
|
+
|
446
|
+
1. **CLI flags** (highest priority)
|
447
|
+
2. **Environment variables**
|
448
|
+
3. **Configuration file values**
|
449
|
+
4. **Tool defaults** (lowest priority)
|
450
|
+
|
451
|
+
### Configuration File Format
|
452
|
+
|
453
|
+
Configuration files use INI format with organization-specific sections:
|
454
|
+
|
455
|
+
```ini
|
456
|
+
[default]
|
457
|
+
GERRIT_SERVER = "gerrit.example.org"
|
458
|
+
PRESERVE_GITHUB_PRS = "true"
|
459
|
+
|
460
|
+
[onap]
|
461
|
+
ISSUE_ID = "CIMAN-33"
|
462
|
+
REVIEWERS_EMAIL = "user@example.org"
|
463
|
+
|
464
|
+
[opendaylight]
|
465
|
+
GERRIT_HTTP_USER = "bot-user"
|
466
|
+
GERRIT_HTTP_PASSWORD = "${ENV:ODL_GERRIT_TOKEN}"
|
467
|
+
```
|
468
|
+
|
469
|
+
The tool loads configuration from `~/.config/github2gerrit/configuration.txt`
|
470
|
+
by default, or from the path specified in the `G2G_CONFIG_PATH` environment
|
471
|
+
variable.
|
472
|
+
|
473
|
+
**Note**: Unknown configuration keys will generate warnings to help catch typos
|
474
|
+
and missing functionality.
|
475
|
+
|
386
476
|
## Behavior details
|
387
477
|
|
388
478
|
- Branch resolution
|
@@ -428,7 +518,7 @@ This repository follows the guidelines in `CLAUDE.md`.
|
|
428
518
|
- Language and CLI
|
429
519
|
- Python 3.11. The CLI uses Typer.
|
430
520
|
- Packaging
|
431
|
-
- `pyproject.toml` with
|
521
|
+
- `pyproject.toml` with setuptools backend. Use `uv` to install and run.
|
432
522
|
- Structure
|
433
523
|
- `src/github2gerrit/cli.py` (CLI entrypoint)
|
434
524
|
- `src/github2gerrit/core.py` (orchestration)
|
@@ -0,0 +1,15 @@
|
|
1
|
+
github2gerrit/__init__.py,sha256=N1Vj1HJ28LKCJLAynQdm5jFGQQAz9YSMzZhEfvbBgow,886
|
2
|
+
github2gerrit/cli.py,sha256=30hvpwZCs-xeWfP6TJmcluebmPVU88AUUNprd0Szt-8,34043
|
3
|
+
github2gerrit/config.py,sha256=4RmAyRFs1CxeGlAjbCaVW63EqEnBt5Vag0jTTMzfKyU,16948
|
4
|
+
github2gerrit/core.py,sha256=HKgSmh792sbdTV_vuNLos-eaYgj3W0F0H72N7KBV6IA,75175
|
5
|
+
github2gerrit/duplicate_detection.py,sha256=J6a8t3ih-ebr6FEhWsaKnXYPQCzwcnFEWhdstmtjnMo,19475
|
6
|
+
github2gerrit/github_api.py,sha256=G_VRvIzpugDeNRyw1y-KGQQ_wvDRl-L6UCqP8BRh-gU,10697
|
7
|
+
github2gerrit/gitutils.py,sha256=8Q94BCLC924zIG2kcCSzxkajTpUamQ3Ul07OqzEv9ic,18664
|
8
|
+
github2gerrit/models.py,sha256=DAm0pEWvAexOInnxTVrvTnKWhLMd86TfSqT78UohOCo,1791
|
9
|
+
github2gerrit/ssh_discovery.py,sha256=xildpri60eQZtnXJuRxcEEb-q71h6D8QUiQvp2P9LlU,13300
|
10
|
+
github2gerrit-0.1.5.dist-info/licenses/LICENSE,sha256=xx0jnfkXJvxRnG63LTGOxlggYnIysveWIZ6H3PNdCrQ,11357
|
11
|
+
github2gerrit-0.1.5.dist-info/METADATA,sha256=5rLt8uNLd0FLcNGqNncQbM7cYm3Ns_cDECz385lqwpk,21545
|
12
|
+
github2gerrit-0.1.5.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
13
|
+
github2gerrit-0.1.5.dist-info/entry_points.txt,sha256=MxN2_liIKo3-xJwtAulAeS5GcOS6JS96nvwOQIkP3W8,56
|
14
|
+
github2gerrit-0.1.5.dist-info/top_level.txt,sha256=bWTYXjvuu4sSU90KLT1JlnjD7xV_iXZ-vKoulpjLTy8,14
|
15
|
+
github2gerrit-0.1.5.dist-info/RECORD,,
|