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.
@@ -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
1
+ Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 0.1.3
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
- Description-Content-Type: text/markdown
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. **SSH Permission Denied**: Ensure `GERRIT_SSH_PRIVKEY_G2G` and
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
- 2. **Branch Not Found**: Check that the target branch exists in both GitHub and Gerrit
233
- 3. **Change-Id Issues**: Enable debug logging to see Change-Id generation and validation
234
- 4. **Gerrit API Errors**: Verify Gerrit server connectivity and project permissions
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 PDM backend. Use `uv` to install and run.
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,,
@@ -1,4 +1,5 @@
1
1
  Wheel-Version: 1.0
2
- Generator: pdm-backend (2.4.5)
2
+ Generator: setuptools (80.9.0)
3
3
  Root-Is-Purelib: true
4
4
  Tag: py3-none-any
5
+
@@ -1,5 +1,2 @@
1
1
  [console_scripts]
2
2
  github2gerrit = github2gerrit.cli:app
3
-
4
- [gui_scripts]
5
-