github2gerrit 0.1.11__tar.gz → 0.1.13__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.
Files changed (96) hide show
  1. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/PKG-INFO +1 -1
  2. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/action.yaml +9 -3
  3. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/cli.py +1 -1
  4. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/core.py +66 -12
  5. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/duplicate_detection.py +5 -15
  6. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/ssh_discovery.py +131 -9
  7. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_action_step_validation.py +9 -2
  8. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_duplicate_detection.py +7 -14
  9. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/.editorconfig +0 -0
  10. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/.gitignore +0 -0
  11. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/.gitlint +0 -0
  12. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/.markdownlint.yaml +0 -0
  13. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/.pre-commit-config.yaml +0 -0
  14. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/.readthedocs.yml +0 -0
  15. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/.yamllint +0 -0
  16. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/LICENSE +0 -0
  17. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/LICENSES/Apache-2.0.txt +0 -0
  18. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/README.md +0 -0
  19. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/REUSE.toml +0 -0
  20. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/REVISION_PLAN.md +0 -0
  21. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  22. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/docs/github2gerrit_token_permissions_classic.png +0 -0
  23. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/pyproject.toml +0 -0
  24. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/sitecustomize.py +0 -0
  25. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/__init__.py +0 -0
  26. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/commit_normalization.py +0 -0
  27. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/config.py +0 -0
  28. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/external_api.py +0 -0
  29. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/gerrit_query.py +0 -0
  30. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/gerrit_rest.py +0 -0
  31. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/gerrit_urls.py +0 -0
  32. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/github_api.py +0 -0
  33. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/gitutils.py +0 -0
  34. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/mapping_comment.py +0 -0
  35. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/models.py +0 -0
  36. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/orchestrator/__init__.py +0 -0
  37. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  38. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/pr_content_filter.py +0 -0
  39. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/reconcile_matcher.py +0 -0
  40. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/rich_display.py +0 -0
  41. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/rich_logging.py +0 -0
  42. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/similarity.py +0 -0
  43. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/ssh_agent_setup.py +0 -0
  44. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/ssh_common.py +0 -0
  45. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/trailers.py +0 -0
  46. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/src/github2gerrit/utils.py +0 -0
  47. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/conftest.py +0 -0
  48. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/fixtures/__init__.py +0 -0
  49. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/fixtures/make_repo.py +0 -0
  50. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_action_environment_mapping.py +0 -0
  51. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_action_outputs.py +0 -0
  52. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_action_pr_number_handling.py +0 -0
  53. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_change_id_deduplication.py +0 -0
  54. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_cli.py +0 -0
  55. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_cli_helpers.py +0 -0
  56. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_cli_outputs_file.py +0 -0
  57. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_cli_url_and_dryrun.py +0 -0
  58. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_commit_normalization.py +0 -0
  59. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_composite_action_coverage.py +0 -0
  60. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_config_and_reviewers.py +0 -0
  61. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_config_helpers.py +0 -0
  62. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_core_close_pr_policy.py +0 -0
  63. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_core_config_and_errors.py +0 -0
  64. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_core_gerrit_backref_comment.py +0 -0
  65. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_core_gerrit_push_errors.py +0 -0
  66. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_core_gerrit_rest_results.py +0 -0
  67. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_core_integration_fixture_repo.py +0 -0
  68. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_core_prepare_commits.py +0 -0
  69. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_core_ssh_setup.py +0 -0
  70. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_email_case_normalization.py +0 -0
  71. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_external_api_framework.py +0 -0
  72. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_gerrit_change_id_footer.py +0 -0
  73. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_gerrit_rest_client.py +0 -0
  74. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_gerrit_urls.py +0 -0
  75. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_gerrit_urls_more.py +0 -0
  76. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_ghe_and_gitreview_args.py +0 -0
  77. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_github_api_helpers.py +0 -0
  78. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_github_api_retry_and_helpers.py +0 -0
  79. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_gitutils_helpers.py +0 -0
  80. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_mapping_comment_additional.py +0 -0
  81. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  82. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_metadata_and_reconciliation.py +0 -0
  83. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_misc_small_coverage.py +0 -0
  84. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_orphan_rest_side_effects.py +0 -0
  85. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_pr_content_filter.py +0 -0
  86. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_pr_content_filter_integration.py +0 -0
  87. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_reconciliation_extracted_module.py +0 -0
  88. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  89. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_reconciliation_scenarios.py +0 -0
  90. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_ssh_agent.py +0 -0
  91. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_ssh_common.py +0 -0
  92. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_ssh_discovery.py +0 -0
  93. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_trailers_additional.py +0 -0
  94. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_url_parser.py +0 -0
  95. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/tests/test_utils.py +0 -0
  96. {github2gerrit-0.1.11 → github2gerrit-0.1.13}/uv.lock +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 0.1.11
3
+ Version: 0.1.13
4
4
  Summary: Submit a GitHub pull request to a Gerrit repository.
5
5
  Project-URL: Homepage, https://github.com/lfreleng-actions/github2gerrit
6
6
  Project-URL: Repository, https://github.com/lfreleng-actions/github2gerrit
@@ -132,7 +132,7 @@ runs:
132
132
  # yamllint disable-line rule:line-length
133
133
  uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5.6.0
134
134
  with:
135
- python-version-file: 'pyproject.toml'
135
+ python-version-file: '${{ github.action_path }}/pyproject.toml'
136
136
 
137
137
  - name: Setup uv
138
138
  # yamllint disable-line rule:line-length
@@ -186,8 +186,14 @@ runs:
186
186
  run: |
187
187
  set -euo pipefail
188
188
  uv --version
189
- # Install the package into the system environment using the lockfile
190
- uv pip install --system .
189
+ # Install from PyPI when called from external repos, local source for self-testing
190
+ if [[ "${{ github.repository }}" == "modeseven-lfreleng-actions/github2gerrit-action" ]]; then
191
+ echo "Installing from local source (self-testing)"
192
+ uv pip install --system ${{ github.action_path }}
193
+ else
194
+ echo "Installing from PyPI (external repository)"
195
+ uv pip install --system github2gerrit
196
+ fi
191
197
 
192
198
  - name: Validate PR_NUMBER usage (non-dispatch)
193
199
  # yamllint disable-line rule:line-length
@@ -525,7 +525,7 @@ def main(
525
525
  if _is_github_actions_context():
526
526
  try:
527
527
  app_version = get_version("github2gerrit")
528
- log.info("github2gerrit version %s", app_version)
528
+ log.debug("github2gerrit version %s", app_version)
529
529
  except Exception:
530
530
  log.warning("Version information not available")
531
531
 
@@ -1115,6 +1115,41 @@ class Orchestrator:
1115
1115
  log.debug("SSH private key not provided, skipping SSH setup")
1116
1116
  return
1117
1117
 
1118
+ # Check for ssh-keyscan availability early if auto-discovery needed
1119
+ if (
1120
+ auto_discover_gerrit_host_keys is not None
1121
+ and not inputs.gerrit_known_hosts
1122
+ ):
1123
+ import shutil
1124
+
1125
+ keyscan_path = shutil.which("ssh-keyscan")
1126
+ if not keyscan_path:
1127
+ log.error(
1128
+ "❌ ssh-keyscan not found in PATH but is required for SSH "
1129
+ "host key auto-discovery"
1130
+ )
1131
+ log.error(
1132
+ "Available tools in PATH: %s",
1133
+ ", ".join(
1134
+ [
1135
+ tool
1136
+ for tool in [
1137
+ "ssh",
1138
+ "ssh-keygen",
1139
+ "ssh-add",
1140
+ "ssh-agent",
1141
+ "ssh-keyscan",
1142
+ ]
1143
+ if shutil.which(tool)
1144
+ ]
1145
+ ),
1146
+ )
1147
+ log.error("To fix this issue:")
1148
+ log.error("1. Install openssh-client package, OR")
1149
+ log.error("2. Provide GERRIT_KNOWN_HOSTS manually")
1150
+ else:
1151
+ log.debug("✅ ssh-keyscan found at: %s", keyscan_path)
1152
+
1118
1153
  # Auto-discover or augment host keys (merge missing
1119
1154
  # types/[host]:port entries)
1120
1155
  effective_known_hosts = inputs.gerrit_known_hosts
@@ -1122,9 +1157,18 @@ class Orchestrator:
1122
1157
  try:
1123
1158
  if not effective_known_hosts:
1124
1159
  log.info(
1125
- "GERRIT_KNOWN_HOSTS not provided, attempting "
1126
- "auto-discovery..."
1160
+ "🔍 GERRIT_KNOWN_HOSTS not provided, attempting "
1161
+ "auto-discovery for %s:%d...",
1162
+ gerrit.host,
1163
+ gerrit.port,
1127
1164
  )
1165
+ log.debug(
1166
+ "Auto-discovery params: host=%s, port=%d, org=%s",
1167
+ gerrit.host,
1168
+ gerrit.port,
1169
+ inputs.organization,
1170
+ )
1171
+
1128
1172
  discovered_keys = auto_discover_gerrit_host_keys(
1129
1173
  gerrit_hostname=gerrit.host,
1130
1174
  gerrit_port=gerrit.port,
@@ -1134,15 +1178,18 @@ class Orchestrator:
1134
1178
  if discovered_keys:
1135
1179
  effective_known_hosts = discovered_keys
1136
1180
  log.info(
1137
- "Successfully auto-discovered SSH host keys for "
1181
+ "Successfully auto-discovered SSH host keys for "
1138
1182
  "%s:%d",
1139
1183
  gerrit.host,
1140
1184
  gerrit.port,
1141
1185
  )
1142
1186
  else:
1143
- log.warning(
1144
- "Auto-discovery failed, SSH host key verification "
1145
- "may fail"
1187
+ log.error(
1188
+ "Auto-discovery failed for %s:%d - SSH host key "
1189
+ "verification will likely fail. Check network "
1190
+ "connectivity and ssh-keyscan availability.",
1191
+ gerrit.host,
1192
+ gerrit.port,
1146
1193
  )
1147
1194
  else:
1148
1195
  # Provided known_hosts exists; ensure it contains
@@ -1191,15 +1238,22 @@ class Orchestrator:
1191
1238
  "Auto-discovery returned no keys; known_hosts "
1192
1239
  "not augmented"
1193
1240
  )
1194
- except Exception as exc:
1195
- log.warning(
1196
- "SSH host key auto-discovery/augmentation failed: %s", exc
1241
+ except Exception:
1242
+ log.exception(
1243
+ "SSH host key auto-discovery/augmentation failed "
1244
+ "for %s:%d",
1245
+ gerrit.host,
1246
+ gerrit.port,
1197
1247
  )
1198
1248
 
1199
1249
  if not effective_known_hosts:
1200
- log.debug(
1201
- "No SSH host keys available (manual or auto-discovered), "
1202
- "skipping SSH setup"
1250
+ log.warning(
1251
+ "⚠️ No SSH host keys available (manual or auto-discovered) "
1252
+ "for %s:%d. SSH connections may fail due to host key "
1253
+ "verification. Consider setting GERRIT_KNOWN_HOSTS or ensure "
1254
+ "ssh-keyscan is available.",
1255
+ gerrit.host,
1256
+ gerrit.port,
1203
1257
  )
1204
1258
  return
1205
1259
 
@@ -20,7 +20,6 @@ from collections.abc import Iterable
20
20
  from datetime import UTC
21
21
  from datetime import datetime
22
22
  from datetime import timedelta
23
- from pathlib import Path
24
23
  from typing import Any
25
24
 
26
25
  from .gerrit_urls import create_gerrit_url_builder
@@ -205,20 +204,11 @@ class DuplicateDetector:
205
204
  if gerrit_host and gerrit_project:
206
205
  return (gerrit_host, gerrit_project)
207
206
 
208
- # Try to read .gitreview file locally first
209
- gitreview_path = Path(".gitreview")
210
- if gitreview_path.exists():
211
- try:
212
- text = gitreview_path.read_text(encoding="utf-8")
213
- host = self._match_first_group(r"(?m)^host=(.+)$", text)
214
- proj = self._match_first_group(r"(?m)^project=(.+)$", text)
215
- if host and proj:
216
- project = proj.removesuffix(".git")
217
- return (host.strip(), project.strip())
218
- if host and not proj:
219
- return (host.strip(), "")
220
- except Exception as exc:
221
- log.debug("Failed to read local .gitreview: %s", exc)
207
+ # Skip local .gitreview check in composite action context
208
+ # The duplicate detection runs before workspace setup, so there's no
209
+ # reliable local .gitreview file to check. Instead, rely on environment
210
+ # variables or remote fetching.
211
+ log.debug("Skipping local .gitreview check (composite action context)")
222
212
 
223
213
  # Try to fetch .gitreview remotely (simplified version of core logic)
224
214
  try:
@@ -13,6 +13,7 @@ from __future__ import annotations
13
13
 
14
14
  import logging
15
15
  import os
16
+ import shutil
16
17
  import socket
17
18
  from pathlib import Path
18
19
 
@@ -55,6 +56,18 @@ _MSG_UNEXPECTED_ERROR = (
55
56
  _MSG_SAVE_FAILED = (
56
57
  "Failed to save host keys to configuration file {config_file}: {error}"
57
58
  )
59
+ _MSG_KEYSCAN_NOT_FOUND = (
60
+ "ssh-keyscan command not found in PATH. This tool is required for SSH "
61
+ "host key auto-discovery."
62
+ )
63
+ _MSG_KEYSCAN_CHECK_FAILED = (
64
+ "Failed to check for ssh-keyscan availability: {error}"
65
+ )
66
+
67
+
68
+ def _raise_keyscan_not_found() -> None:
69
+ """Helper function to raise keyscan not found error."""
70
+ raise SSHDiscoveryError(_MSG_KEYSCAN_NOT_FOUND)
58
71
 
59
72
 
60
73
  def is_host_reachable(hostname: str, port: int, timeout: int = 5) -> bool:
@@ -84,13 +97,36 @@ def fetch_ssh_host_keys(
84
97
  Raises:
85
98
  SSHDiscoveryError: If the host keys cannot be fetched
86
99
  """
87
- log.debug("Fetching SSH host keys for %s:%d", hostname, port)
100
+ log.info("🔍 Starting SSH host key discovery for %s:%d", hostname, port)
101
+
102
+ # Check if ssh-keyscan is available
103
+ try:
104
+ keyscan_path = shutil.which("ssh-keyscan")
105
+ if not keyscan_path:
106
+ log.error("❌ ssh-keyscan not found in PATH")
107
+ _raise_keyscan_not_found()
108
+ log.debug("✅ Found ssh-keyscan at: %s", keyscan_path)
109
+ except Exception as exc:
110
+ log.exception("❌ Failed to check for ssh-keyscan")
111
+ raise SSHDiscoveryError(
112
+ _MSG_KEYSCAN_CHECK_FAILED.format(error=exc)
113
+ ) from exc
114
+
115
+ log.debug(
116
+ "🔍 Fetching SSH host keys for %s:%d (timeout: %ds)",
117
+ hostname,
118
+ port,
119
+ timeout,
120
+ )
88
121
 
89
122
  # First check if the host is reachable
123
+ log.debug("🔍 Testing connectivity to %s:%d...", hostname, port)
90
124
  if not is_host_reachable(hostname, port, timeout=5):
125
+ log.error("❌ Host %s:%d is not reachable", hostname, port)
91
126
  raise SSHDiscoveryError(
92
127
  _MSG_HOST_UNREACHABLE.format(hostname=hostname, port=port)
93
128
  )
129
+ log.debug("✅ Host %s:%d is reachable", hostname, port)
94
130
 
95
131
  try:
96
132
  # Use ssh-keyscan to fetch all available key types
@@ -105,9 +141,26 @@ def fetch_ssh_host_keys(
105
141
  hostname,
106
142
  ]
107
143
 
144
+ log.debug("🔍 Running command: %s", " ".join(cmd))
108
145
  result = run_cmd(cmd, timeout=timeout + 5)
109
146
 
147
+ log.debug(
148
+ "ssh-keyscan stdout length: %d chars",
149
+ len(result.stdout) if result.stdout else 0,
150
+ )
151
+ log.debug(
152
+ "ssh-keyscan stderr: %s",
153
+ result.stderr if result.stderr else "(empty)",
154
+ )
155
+
110
156
  if not result.stdout or not result.stdout.strip():
157
+ log.error(
158
+ "❌ ssh-keyscan returned no output for %s:%d", hostname, port
159
+ )
160
+ log.error("Command: %s", " ".join(cmd))
161
+ log.error(
162
+ "Stderr: %s", result.stderr if result.stderr else "(none)"
163
+ )
111
164
  raise SSHDiscoveryError( # noqa: TRY301
112
165
  _MSG_NO_KEYS_FOUND.format(hostname=hostname, port=port)
113
166
  )
@@ -127,20 +180,34 @@ def fetch_ssh_host_keys(
127
180
  valid_lines.append(stripped_line)
128
181
 
129
182
  if not valid_lines:
183
+ log.error(
184
+ "❌ No valid SSH host keys found in output for %s:%d",
185
+ hostname,
186
+ port,
187
+ )
188
+ log.error("Raw output was: %s", result.stdout)
130
189
  raise SSHDiscoveryError( # noqa: TRY301
131
190
  _MSG_NO_VALID_KEYS.format(hostname=hostname, port=port)
132
191
  )
133
192
 
134
193
  discovered_keys = "\n".join(valid_lines)
135
194
  log.info(
136
- "Successfully discovered %d SSH host key(s) for %s:%d",
195
+ "Successfully discovered %d SSH host key(s) for %s:%d",
137
196
  len(valid_lines),
138
197
  hostname,
139
198
  port,
140
199
  )
141
- log.debug("Discovered keys:\n%s", discovered_keys)
200
+ log.info("📋 Discovered keys:\n%s", discovered_keys)
142
201
 
143
202
  except CommandError as exc:
203
+ log.exception("❌ ssh-keyscan command failed for %s:%d", hostname, port)
204
+ log.debug("Return code: %d", exc.returncode)
205
+ stdout_msg = exc.stdout if exc.stdout else "(empty)"
206
+ stderr_msg = exc.stderr if exc.stderr else "(empty)"
207
+ log.debug("Stdout: %s", stdout_msg)
208
+ log.debug("Stderr: %s", stderr_msg)
209
+ log.debug("Command: %s", " ".join(cmd))
210
+
144
211
  if exc.returncode == 1:
145
212
  # ssh-keyscan returns 1 when it can't connect
146
213
  error_msg = exc.stderr or exc.stdout or "Connection failed"
@@ -381,11 +448,60 @@ def auto_discover_gerrit_host_keys(
381
448
  )
382
449
  save_to_config = False
383
450
 
451
+ # Log system diagnostics for debugging
452
+
384
453
  log.info(
385
- "Attempting to auto-discover SSH host keys for %s:%d",
454
+ "🔍 Attempting to auto-discover SSH host keys for %s:%d "
455
+ "(org: %s, save: %s)",
386
456
  gerrit_hostname,
387
457
  gerrit_port,
458
+ organization or "none",
459
+ save_to_config,
460
+ )
461
+
462
+ # System diagnostics
463
+ log.debug("🔧 System diagnostics:")
464
+ log.debug(" - OS: %s", os.name)
465
+ log.debug(
466
+ " - Platform: %s",
467
+ os.uname() if hasattr(os, "uname") else "unknown",
468
+ )
469
+ path_env = os.getenv("PATH", "not set")
470
+ path_display = (
471
+ path_env[:200] + "..." if len(path_env) > 200 else path_env
388
472
  )
473
+ log.debug(" - PATH: %s", path_display)
474
+ ssh_tools = [
475
+ f"{tool}={shutil.which(tool) or 'missing'}"
476
+ for tool in [
477
+ "ssh",
478
+ "ssh-keygen",
479
+ "ssh-add",
480
+ "ssh-agent",
481
+ "ssh-keyscan",
482
+ ]
483
+ ]
484
+ log.debug(" - Available SSH tools: %s", ", ".join(ssh_tools))
485
+ dns_status = "checking..." if gerrit_hostname else "no hostname"
486
+ log.debug(" - Network test (DNS resolution): %s", dns_status)
487
+
488
+ # Test DNS resolution
489
+ if gerrit_hostname:
490
+ try:
491
+ import socket
492
+
493
+ addr_info = socket.getaddrinfo(gerrit_hostname, gerrit_port)
494
+ log.debug(
495
+ " - DNS resolution: ✅ %s resolves to %d addresses",
496
+ gerrit_hostname,
497
+ len(addr_info),
498
+ )
499
+ except Exception as dns_exc:
500
+ log.debug(
501
+ " - DNS resolution: ❌ %s failed: %s",
502
+ gerrit_hostname,
503
+ dns_exc,
504
+ )
389
505
 
390
506
  # Discover the host keys
391
507
  host_keys = fetch_ssh_host_keys(gerrit_hostname, gerrit_port)
@@ -404,12 +520,18 @@ def auto_discover_gerrit_host_keys(
404
520
  "Set ORGANIZATION environment variable to enable auto-saving."
405
521
  )
406
522
 
407
- except SSHDiscoveryError as exc:
408
- log.warning("SSH host key auto-discovery failed: %s", exc)
523
+ except SSHDiscoveryError:
524
+ log.exception(
525
+ "❌ SSH host key auto-discovery failed for %s:%d",
526
+ gerrit_hostname,
527
+ gerrit_port,
528
+ )
409
529
  return None
410
- except Exception as exc:
411
- log.warning(
412
- "Unexpected error during SSH host key auto-discovery: %s", exc
530
+ except Exception:
531
+ log.exception(
532
+ "Unexpected error during SSH host key auto-discovery for %s:%d",
533
+ gerrit_hostname,
534
+ gerrit_port,
413
535
  )
414
536
  return None
415
537
  else:
@@ -164,7 +164,10 @@ class TestActionStepValidation:
164
164
 
165
165
  with_config = python_step.get("with", {})
166
166
  assert "python-version-file" in with_config
167
- assert with_config["python-version-file"] == "pyproject.toml"
167
+ assert (
168
+ with_config["python-version-file"]
169
+ == "${{ github.action_path }}/pyproject.toml"
170
+ )
168
171
  # No pip caching since we use uv for dependency management
169
172
  assert "cache" not in with_config
170
173
 
@@ -216,7 +219,11 @@ class TestActionStepValidation:
216
219
 
217
220
  script = install_step["run"]
218
221
  assert "uv --version" in script
219
- assert "uv pip install --system ." in script
222
+ # Should contain both PyPI and local installation logic
223
+ assert "github.repository" in script
224
+ assert "modeseven-lfreleng-actions/github2gerrit-action" in script
225
+ assert "uv pip install --system ${{ github.action_path }}" in script
226
+ assert "uv pip install --system github2gerrit" in script
220
227
 
221
228
  def test_cli_execution_step(self, action_config):
222
229
  """Test CLI execution step configuration."""
@@ -342,25 +342,18 @@ class TestGerritDuplicateDetection:
342
342
  result = detector._resolve_gerrit_info_from_env_or_gitreview(gh)
343
343
  assert result is None
344
344
 
345
- @patch("pathlib.Path.exists")
346
- @patch("pathlib.Path.read_text")
347
- def test_resolve_gerrit_info_from_local_gitreview(
348
- self, mock_read_text: Any, mock_exists: Any
349
- ) -> None:
350
- """Test resolving Gerrit info from local .gitreview file."""
345
+ def test_resolve_gerrit_info_skips_local_gitreview(self) -> None:
346
+ """Test that local .gitreview reading is skipped in composite action context."""
351
347
  detector = DuplicateDetector(Mock())
352
348
  gh = self._create_mock_github_context()
353
349
 
354
- mock_exists.return_value = True
355
- mock_read_text.return_value = """[gerrit]
356
- host=gerrit.example.org
357
- port=29418
358
- project=test/project.git
359
- """
360
-
350
+ # Even if a local .gitreview exists, it should be skipped
351
+ # and fall back to remote fetching or return None
361
352
  with patch.dict("os.environ", {}, clear=True):
362
353
  result = detector._resolve_gerrit_info_from_env_or_gitreview(gh)
363
- assert result == ("gerrit.example.org", "test/project")
354
+ assert (
355
+ result is None
356
+ ) # Should skip local file and return None for remote fallback
364
357
 
365
358
  @patch("urllib.request.urlopen")
366
359
  @patch("pathlib.Path.exists")
File without changes
File without changes
File without changes
File without changes
File without changes