github2gerrit 0.1.14__tar.gz → 0.1.15__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.14 → github2gerrit-0.1.15}/PKG-INFO +1 -1
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/action.yaml +5 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/cli.py +12 -1
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/core.py +354 -27
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/ssh_agent_setup.py +40 -6
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_core_ssh_setup.py +37 -28
- github2gerrit-0.1.15/tests/test_metadata_trailer_separation_bug.py +323 -0
- github2gerrit-0.1.15/tests/test_ssh_artifact_prevention.py +357 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/.editorconfig +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/.gitignore +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/.gitlint +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/.markdownlint.yaml +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/.pre-commit-config.yaml +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/.readthedocs.yml +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/.yamllint +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/LICENSE +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/README.md +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/REUSE.toml +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/REVISION_PLAN.md +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/pyproject.toml +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/sitecustomize.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/config.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/external_api.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/gerrit_query.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/gerrit_rest.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/github_api.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/gitutils.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/models.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/pr_content_filter.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/ssh_discovery.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/conftest.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_action_outputs.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_change_id_deduplication.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_cli.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_cli_outputs_file.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_cli_url_and_dryrun.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_config_helpers.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_core_close_pr_policy.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_core_config_and_errors.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_core_prepare_commits.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_external_api_framework.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_gerrit_change_id_footer.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_gitutils_helpers.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_ssh_common.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_url_parser.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/tests/test_utils.py +0 -0
- {github2gerrit-0.1.14 → github2gerrit-0.1.15}/uv.lock +0 -0
@@ -1,6 +1,6 @@
|
|
1
1
|
Metadata-Version: 2.4
|
2
2
|
Name: github2gerrit
|
3
|
-
Version: 0.1.
|
3
|
+
Version: 0.1.15
|
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
|
@@ -81,6 +81,10 @@ inputs:
|
|
81
81
|
description: "Normalize commit messages to conventional commit format"
|
82
82
|
required: false
|
83
83
|
default: "false"
|
84
|
+
VERBOSE:
|
85
|
+
description: "Enable verbose output (sets log level to DEBUG)"
|
86
|
+
required: false
|
87
|
+
default: "false"
|
84
88
|
GERRIT_SERVER:
|
85
89
|
description: "Gerrit server hostname; derived if not supplied explicitly"
|
86
90
|
required: false
|
@@ -280,6 +284,7 @@ runs:
|
|
280
284
|
DUPLICATE_TYPES: ${{ inputs.DUPLICATE_TYPES }}
|
281
285
|
NORMALISE_COMMIT: ${{ inputs.NORMALISE_COMMIT }}
|
282
286
|
G2G_USE_SSH_AGENT: ${{ inputs.G2G_USE_SSH_AGENT }}
|
287
|
+
G2G_VERBOSE: ${{ inputs.VERBOSE }}
|
283
288
|
|
284
289
|
# Optional Gerrit overrides (when .gitreview is missing)
|
285
290
|
GERRIT_SERVER: ${{ inputs.GERRIT_SERVER }}
|
@@ -51,6 +51,7 @@ from .ssh_common import build_non_interactive_ssh_env
|
|
51
51
|
from .utils import append_github_output
|
52
52
|
from .utils import env_bool
|
53
53
|
from .utils import env_str
|
54
|
+
from .utils import is_verbose_mode
|
54
55
|
from .utils import log_exception_conditionally
|
55
56
|
from .utils import parse_bool_env
|
56
57
|
|
@@ -989,9 +990,19 @@ def _process_single(
|
|
989
990
|
else:
|
990
991
|
progress_tracker.change_updated()
|
991
992
|
except Exception as exc:
|
993
|
+
error_msg = str(exc)
|
992
994
|
log.debug("Execution failed; continuing to write outputs: %s", exc)
|
995
|
+
|
996
|
+
# Always show the actual error to the user, not just in debug mode
|
993
997
|
if progress_tracker:
|
994
|
-
progress_tracker.add_error("Execution failed")
|
998
|
+
progress_tracker.add_error(f"Execution failed: {error_msg}")
|
999
|
+
else:
|
1000
|
+
# If no progress tracker, show error directly
|
1001
|
+
safe_console_print(f"❌ Error: {error_msg}", style="red")
|
1002
|
+
|
1003
|
+
# In verbose mode, also log the full exception with traceback
|
1004
|
+
if is_verbose_mode():
|
1005
|
+
log.exception("Full exception details:")
|
995
1006
|
|
996
1007
|
result = SubmissionResult(
|
997
1008
|
change_urls=[], change_numbers=[], commit_shas=[]
|
@@ -69,7 +69,7 @@ from .reconcile_matcher import LocalCommit
|
|
69
69
|
from .reconcile_matcher import create_local_commit
|
70
70
|
from .ssh_common import merge_known_hosts_content
|
71
71
|
from .utils import env_bool
|
72
|
-
from .utils import
|
72
|
+
from .utils import is_verbose_mode
|
73
73
|
|
74
74
|
|
75
75
|
try:
|
@@ -708,6 +708,8 @@ class Orchestrator:
|
|
708
708
|
self._ssh_known_hosts_path: Path | None = None
|
709
709
|
self._ssh_agent_manager: SSHAgentManager | None = None
|
710
710
|
self._use_ssh_agent: bool = False
|
711
|
+
# Secure temporary directory for SSH files (outside workspace)
|
712
|
+
self._ssh_temp_dir: Path | None = None
|
711
713
|
# Store inputs for access by helper methods
|
712
714
|
self._inputs: Inputs | None = None
|
713
715
|
|
@@ -865,6 +867,9 @@ class Orchestrator:
|
|
865
867
|
|
866
868
|
self._comment_on_pull_request(gh, gerrit, result)
|
867
869
|
|
870
|
+
# Validate that no unexpected files were committed
|
871
|
+
self._validate_committed_files(gh, result)
|
872
|
+
|
868
873
|
self._close_pull_request_if_required(gh)
|
869
874
|
|
870
875
|
log.debug("Pipeline complete: %s", result)
|
@@ -1111,10 +1116,15 @@ class Orchestrator:
|
|
1111
1116
|
|
1112
1117
|
Does not modify user files.
|
1113
1118
|
"""
|
1119
|
+
log.debug(
|
1120
|
+
"Starting SSH setup for Gerrit %s:%d", gerrit.host, gerrit.port
|
1121
|
+
)
|
1114
1122
|
if not inputs.gerrit_ssh_privkey_g2g:
|
1115
1123
|
log.debug("SSH private key not provided, skipping SSH setup")
|
1116
1124
|
return
|
1117
1125
|
|
1126
|
+
log.debug("SSH private key provided, proceeding with SSH configuration")
|
1127
|
+
|
1118
1128
|
# Check for ssh-keyscan availability early if auto-discovery needed
|
1119
1129
|
if (
|
1120
1130
|
auto_discover_gerrit_host_keys is not None
|
@@ -1259,16 +1269,25 @@ class Orchestrator:
|
|
1259
1269
|
|
1260
1270
|
# Check if SSH agent authentication is preferred
|
1261
1271
|
use_ssh_agent = env_bool("G2G_USE_SSH_AGENT", default=True)
|
1272
|
+
log.debug(
|
1273
|
+
"SSH agent preference: use_ssh_agent=%s, "
|
1274
|
+
"setup_ssh_agent_auth available=%s",
|
1275
|
+
use_ssh_agent,
|
1276
|
+
setup_ssh_agent_auth is not None,
|
1277
|
+
)
|
1262
1278
|
|
1263
1279
|
if use_ssh_agent and setup_ssh_agent_auth is not None:
|
1264
1280
|
# Try SSH agent first as it's more secure and avoids file
|
1265
1281
|
# permission issues
|
1282
|
+
log.debug("Attempting SSH agent-based authentication")
|
1266
1283
|
if self._try_ssh_agent_setup(inputs, effective_known_hosts):
|
1284
|
+
log.debug("SSH agent setup successful")
|
1267
1285
|
return
|
1268
1286
|
|
1269
1287
|
# Fall back to file-based SSH if agent setup fails
|
1270
1288
|
log.info("Falling back to file-based SSH authentication")
|
1271
1289
|
|
1290
|
+
log.debug("Using file-based SSH authentication")
|
1272
1291
|
self._setup_file_based_ssh(inputs, effective_known_hosts)
|
1273
1292
|
|
1274
1293
|
def _try_ssh_agent_setup(
|
@@ -1288,8 +1307,36 @@ class Orchestrator:
|
|
1288
1307
|
|
1289
1308
|
try:
|
1290
1309
|
log.debug("Setting up SSH agent-based authentication (more secure)")
|
1310
|
+
log.debug("Workspace: %s", self.workspace)
|
1311
|
+
log.debug(
|
1312
|
+
"Private key length: %d characters",
|
1313
|
+
len(inputs.gerrit_ssh_privkey_g2g),
|
1314
|
+
)
|
1315
|
+
log.debug(
|
1316
|
+
"Known hosts length: %d characters", len(effective_known_hosts)
|
1317
|
+
)
|
1318
|
+
|
1319
|
+
# Create secure separate temp directory for SSH agent if needed
|
1320
|
+
import secrets
|
1321
|
+
import tempfile
|
1322
|
+
|
1323
|
+
if not self._ssh_temp_dir:
|
1324
|
+
# Use secure random suffix to prevent predictable paths
|
1325
|
+
secure_suffix = secrets.token_hex(8)
|
1326
|
+
self._ssh_temp_dir = Path(
|
1327
|
+
tempfile.mkdtemp(
|
1328
|
+
prefix=f"g2g_ssh_{secure_suffix}_",
|
1329
|
+
dir=tempfile.gettempdir(),
|
1330
|
+
)
|
1331
|
+
)
|
1332
|
+
# Ensure directory has restrictive permissions
|
1333
|
+
self._ssh_temp_dir.chmod(0o700)
|
1334
|
+
log.debug(
|
1335
|
+
"Created secure SSH temp directory: %s", self._ssh_temp_dir
|
1336
|
+
)
|
1337
|
+
|
1291
1338
|
self._ssh_agent_manager = setup_ssh_agent_auth(
|
1292
|
-
workspace=self.
|
1339
|
+
workspace=self._ssh_temp_dir,
|
1293
1340
|
private_key_content=inputs.gerrit_ssh_privkey_g2g,
|
1294
1341
|
known_hosts_content=effective_known_hosts,
|
1295
1342
|
)
|
@@ -1297,6 +1344,7 @@ class Orchestrator:
|
|
1297
1344
|
log.debug("SSH agent authentication configured successfully")
|
1298
1345
|
|
1299
1346
|
except Exception as exc:
|
1347
|
+
log.debug("SSH agent setup failed: %s", exc)
|
1300
1348
|
log.warning(
|
1301
1349
|
"SSH agent setup failed, falling back to file-based SSH: %s",
|
1302
1350
|
exc,
|
@@ -1317,28 +1365,60 @@ class Orchestrator:
|
|
1317
1365
|
inputs: Validated input configuration
|
1318
1366
|
effective_known_hosts: Known hosts content
|
1319
1367
|
"""
|
1320
|
-
log.
|
1321
|
-
log.debug(
|
1368
|
+
log.debug("Using file-based SSH configuration for Gerrit")
|
1369
|
+
log.debug(
|
1370
|
+
"Using secure temporary SSH files outside workspace to prevent "
|
1371
|
+
"artifacts"
|
1372
|
+
)
|
1373
|
+
log.debug(
|
1374
|
+
"Private key length: %d characters",
|
1375
|
+
len(inputs.gerrit_ssh_privkey_g2g),
|
1376
|
+
)
|
1377
|
+
log.debug(
|
1378
|
+
"Known hosts length: %d characters", len(effective_known_hosts)
|
1379
|
+
)
|
1322
1380
|
|
1323
|
-
# Create tool-specific SSH directory
|
1324
|
-
#
|
1325
|
-
|
1381
|
+
# Create secure tool-specific SSH directory outside workspace to prevent
|
1382
|
+
# SSH artifacts from being accidentally committed
|
1383
|
+
import secrets
|
1384
|
+
import tempfile
|
1385
|
+
|
1386
|
+
if not self._ssh_temp_dir:
|
1387
|
+
# Use secure random suffix to prevent predictable paths
|
1388
|
+
secure_suffix = secrets.token_hex(8)
|
1389
|
+
self._ssh_temp_dir = Path(
|
1390
|
+
tempfile.mkdtemp(
|
1391
|
+
prefix=f"g2g_ssh_{secure_suffix}_",
|
1392
|
+
dir=tempfile.gettempdir(),
|
1393
|
+
)
|
1394
|
+
)
|
1395
|
+
# Ensure directory has restrictive permissions
|
1396
|
+
self._ssh_temp_dir.chmod(0o700)
|
1397
|
+
log.debug(
|
1398
|
+
"Created secure SSH temp directory: %s", self._ssh_temp_dir
|
1399
|
+
)
|
1400
|
+
tool_ssh_dir = self._ssh_temp_dir
|
1326
1401
|
tool_ssh_dir.mkdir(mode=0o700, exist_ok=True)
|
1327
1402
|
|
1328
1403
|
# Write SSH private key to tool-specific location with secure
|
1329
1404
|
# permissions
|
1330
1405
|
key_path = tool_ssh_dir / "gerrit_key"
|
1406
|
+
log.debug("SSH key file path: %s", key_path)
|
1331
1407
|
|
1332
1408
|
# Use a more robust approach for creating the file with secure
|
1333
1409
|
# permissions
|
1334
1410
|
key_content = inputs.gerrit_ssh_privkey_g2g.strip() + "\n"
|
1335
1411
|
|
1336
1412
|
# Multiple strategies to create secure key file
|
1413
|
+
log.debug("Attempting to create secure key file")
|
1337
1414
|
success = self._create_secure_key_file(key_path, key_content)
|
1415
|
+
log.debug("Secure key file creation success: %s", success)
|
1338
1416
|
|
1339
1417
|
if not success:
|
1340
1418
|
# If all permission strategies fail, create in memory directory
|
1419
|
+
log.debug("Falling back to memory-based key file creation")
|
1341
1420
|
success = self._create_key_in_memory_fs(key_path, key_content)
|
1421
|
+
log.debug("Memory-based key file creation success: %s", success)
|
1342
1422
|
|
1343
1423
|
if not success:
|
1344
1424
|
msg = (
|
@@ -1347,8 +1427,11 @@ class Orchestrator:
|
|
1347
1427
|
"Consider using G2G_USE_SSH_AGENT=true (default) for SSH "
|
1348
1428
|
"agent authentication."
|
1349
1429
|
)
|
1430
|
+
log.error("SSH key file creation failed: %s", msg)
|
1350
1431
|
raise RuntimeError(msg)
|
1351
1432
|
|
1433
|
+
log.debug("SSH key file created successfully: %s", key_path)
|
1434
|
+
|
1352
1435
|
# Write known hosts to tool-specific location
|
1353
1436
|
known_hosts_path = tool_ssh_dir / "known_hosts"
|
1354
1437
|
with open(known_hosts_path, "w", encoding="utf-8") as f:
|
@@ -1360,6 +1443,7 @@ class Orchestrator:
|
|
1360
1443
|
# Store paths for later use in git commands
|
1361
1444
|
self._ssh_key_path = key_path
|
1362
1445
|
self._ssh_known_hosts_path = known_hosts_path
|
1446
|
+
log.debug("File-based SSH setup completed successfully")
|
1363
1447
|
|
1364
1448
|
def _create_secure_key_file(self, key_path: Path, key_content: str) -> bool:
|
1365
1449
|
"""Try multiple strategies to create a secure SSH key file.
|
@@ -1564,29 +1648,46 @@ class Orchestrator:
|
|
1564
1648
|
"""Centralized non-interactive SSH/Git environment."""
|
1565
1649
|
from .ssh_common import build_non_interactive_ssh_env
|
1566
1650
|
|
1651
|
+
log.debug("Building SSH environment for git operations")
|
1567
1652
|
env = build_non_interactive_ssh_env()
|
1568
1653
|
|
1569
1654
|
# Set GIT_SSH_COMMAND based on available configuration
|
1570
1655
|
cmd = self._build_git_ssh_command
|
1571
1656
|
if cmd:
|
1657
|
+
log.debug("Using custom GIT_SSH_COMMAND: %s", cmd)
|
1572
1658
|
env["GIT_SSH_COMMAND"] = cmd
|
1573
1659
|
else:
|
1574
1660
|
# Fallback to basic non-interactive SSH command
|
1575
1661
|
from .ssh_common import build_git_ssh_command
|
1576
1662
|
|
1577
|
-
|
1663
|
+
fallback_cmd = build_git_ssh_command()
|
1664
|
+
log.debug("Using fallback GIT_SSH_COMMAND: %s", fallback_cmd)
|
1665
|
+
env["GIT_SSH_COMMAND"] = fallback_cmd
|
1578
1666
|
|
1579
1667
|
# Override SSH agent settings if using SSH agent
|
1580
1668
|
if self._use_ssh_agent and self._ssh_agent_manager:
|
1581
|
-
|
1669
|
+
log.debug("Applying SSH agent environment variables")
|
1670
|
+
agent_env = self._ssh_agent_manager.get_ssh_env()
|
1671
|
+
log.debug(
|
1672
|
+
"SSH agent environment: %s",
|
1673
|
+
{k: v for k, v in agent_env.items() if "SSH" in k},
|
1674
|
+
)
|
1675
|
+
env.update(agent_env)
|
1676
|
+
else:
|
1677
|
+
log.debug(
|
1678
|
+
"Not using SSH agent (use_ssh_agent=%s, manager=%s)",
|
1679
|
+
self._use_ssh_agent,
|
1680
|
+
self._ssh_agent_manager is not None,
|
1681
|
+
)
|
1582
1682
|
|
1683
|
+
log.debug("Final SSH environment contains %d variables", len(env))
|
1583
1684
|
return env
|
1584
1685
|
|
1585
1686
|
def _cleanup_ssh(self) -> None:
|
1586
1687
|
"""Clean up temporary SSH files created by this tool.
|
1587
1688
|
|
1588
|
-
|
1589
|
-
This ensures no temporary files are left behind.
|
1689
|
+
Securely removes the separate SSH temporary directory and all contents.
|
1690
|
+
This ensures no temporary files or credentials are left behind.
|
1590
1691
|
"""
|
1591
1692
|
log.debug("Cleaning up temporary SSH configuration files")
|
1592
1693
|
|
@@ -1597,15 +1698,47 @@ class Orchestrator:
|
|
1597
1698
|
self._ssh_agent_manager = None
|
1598
1699
|
self._use_ssh_agent = False
|
1599
1700
|
|
1600
|
-
#
|
1601
|
-
|
1602
|
-
|
1701
|
+
# Securely remove separate SSH temporary directory and all contents
|
1702
|
+
if self._ssh_temp_dir and self._ssh_temp_dir.exists():
|
1703
|
+
import os
|
1603
1704
|
import shutil
|
1604
1705
|
|
1605
|
-
|
1706
|
+
# First, overwrite any key files to prevent recovery
|
1707
|
+
try:
|
1708
|
+
for root, _dirs, files in os.walk(self._ssh_temp_dir):
|
1709
|
+
for file in files:
|
1710
|
+
file_path = Path(root) / file
|
1711
|
+
if file_path.exists() and file_path.is_file():
|
1712
|
+
# Overwrite file with random data
|
1713
|
+
try:
|
1714
|
+
size = file_path.stat().st_size
|
1715
|
+
if size > 0:
|
1716
|
+
import secrets
|
1717
|
+
|
1718
|
+
with open(file_path, "wb") as f:
|
1719
|
+
f.write(secrets.token_bytes(size))
|
1720
|
+
# Sync to ensure write completes
|
1721
|
+
os.fsync(f.fileno())
|
1722
|
+
except Exception as overwrite_exc:
|
1723
|
+
log.debug(
|
1724
|
+
"Failed to overwrite %s: %s",
|
1725
|
+
file_path,
|
1726
|
+
overwrite_exc,
|
1727
|
+
)
|
1728
|
+
except Exception as walk_exc:
|
1729
|
+
log.debug(
|
1730
|
+
"Failed to walk SSH temp directory for secure "
|
1731
|
+
"cleanup: %s",
|
1732
|
+
walk_exc,
|
1733
|
+
)
|
1734
|
+
|
1735
|
+
# Remove the directory tree
|
1736
|
+
shutil.rmtree(self._ssh_temp_dir)
|
1606
1737
|
log.debug(
|
1607
|
-
"
|
1738
|
+
"Securely cleaned up temporary SSH directory: %s",
|
1739
|
+
self._ssh_temp_dir,
|
1608
1740
|
)
|
1741
|
+
self._ssh_temp_dir = None
|
1609
1742
|
except Exception as exc:
|
1610
1743
|
log.warning("Failed to clean up temporary SSH files: %s", exc)
|
1611
1744
|
|
@@ -1821,8 +1954,8 @@ class Orchestrator:
|
|
1821
1954
|
cur_msg = _clean_ellipses_from_message(cur_msg)
|
1822
1955
|
needed = [m for m in meta if m not in cur_msg]
|
1823
1956
|
if needed:
|
1824
|
-
new_msg = (
|
1825
|
-
cur_msg
|
1957
|
+
new_msg = Orchestrator._append_missing_trailers(
|
1958
|
+
cur_msg, needed
|
1826
1959
|
)
|
1827
1960
|
git_commit_amend(
|
1828
1961
|
message=new_msg,
|
@@ -2113,7 +2246,9 @@ class Orchestrator:
|
|
2113
2246
|
if meta:
|
2114
2247
|
needed = [m for m in meta if m not in commit_msg]
|
2115
2248
|
if needed:
|
2116
|
-
commit_msg =
|
2249
|
+
commit_msg = Orchestrator._append_missing_trailers(
|
2250
|
+
commit_msg, needed
|
2251
|
+
)
|
2117
2252
|
except Exception as meta_exc:
|
2118
2253
|
log.debug(
|
2119
2254
|
"Skipping metadata trailer injection (squash path): %s",
|
@@ -2314,7 +2449,9 @@ class Orchestrator:
|
|
2314
2449
|
).stdout
|
2315
2450
|
needed = [m for m in meta if m not in cur_msg]
|
2316
2451
|
if needed:
|
2317
|
-
new_msg =
|
2452
|
+
new_msg = Orchestrator._append_missing_trailers(
|
2453
|
+
cur_msg, needed
|
2454
|
+
)
|
2318
2455
|
git_commit_amend(
|
2319
2456
|
cwd=self.workspace,
|
2320
2457
|
no_edit=False,
|
@@ -2327,6 +2464,32 @@ class Orchestrator:
|
|
2327
2464
|
"Skipping post-apply metadata trailer ensure: %s", meta_exc
|
2328
2465
|
)
|
2329
2466
|
|
2467
|
+
@staticmethod
|
2468
|
+
def _append_missing_trailers(
|
2469
|
+
message: str, trailers: list[str], *, ensure_final_newline: bool = True
|
2470
|
+
) -> str:
|
2471
|
+
"""Append missing trailers to a commit message with proper formatting.
|
2472
|
+
|
2473
|
+
Args:
|
2474
|
+
message: The base commit message
|
2475
|
+
trailers: List of trailer lines to potentially append
|
2476
|
+
ensure_final_newline: Whether to ensure the message ends with a
|
2477
|
+
newline
|
2478
|
+
|
2479
|
+
Returns:
|
2480
|
+
The message with missing trailers appended, properly formatted
|
2481
|
+
"""
|
2482
|
+
needed = [trailer for trailer in trailers if trailer not in message]
|
2483
|
+
if not needed:
|
2484
|
+
return message
|
2485
|
+
|
2486
|
+
result = message.rstrip() + "\n\n" + "\n".join(needed)
|
2487
|
+
|
2488
|
+
if ensure_final_newline:
|
2489
|
+
result = result.rstrip("\n") + "\n"
|
2490
|
+
|
2491
|
+
return result
|
2492
|
+
|
2330
2493
|
def _push_to_gerrit(
|
2331
2494
|
self,
|
2332
2495
|
*,
|
@@ -2369,6 +2532,7 @@ class Orchestrator:
|
|
2369
2532
|
"-t",
|
2370
2533
|
topic,
|
2371
2534
|
]
|
2535
|
+
log.debug("Building git review command with topic: %s", topic)
|
2372
2536
|
collected_change_ids: list[str] = []
|
2373
2537
|
if prepared:
|
2374
2538
|
collected_change_ids.extend(prepared.all_change_ids())
|
@@ -2396,7 +2560,15 @@ class Orchestrator:
|
|
2396
2560
|
gerrit, repo, branch, reviewers, topic, env
|
2397
2561
|
)
|
2398
2562
|
return
|
2399
|
-
|
2563
|
+
|
2564
|
+
log.debug("Final git review command: %s", " ".join(args))
|
2565
|
+
log.debug(
|
2566
|
+
"Git review environment variables: %s",
|
2567
|
+
{k: v for k, v in env.items() if "SSH" in k or "GIT" in k},
|
2568
|
+
)
|
2569
|
+
log.debug("Working directory: %s", self.workspace)
|
2570
|
+
|
2571
|
+
# Execute the git review command
|
2400
2572
|
run_cmd(args, cwd=self.workspace, env=env)
|
2401
2573
|
log.debug("Successfully pushed changes to Gerrit")
|
2402
2574
|
except CommandError as exc:
|
@@ -2498,9 +2670,33 @@ class Orchestrator:
|
|
2498
2670
|
|
2499
2671
|
# Analyze the specific failure reason from git review output
|
2500
2672
|
error_details = self._analyze_gerrit_push_failure(exc)
|
2501
|
-
|
2502
|
-
|
2503
|
-
)
|
2673
|
+
|
2674
|
+
# Always log the error details, even if not in verbose mode
|
2675
|
+
log.exception("Gerrit push failed: %s", error_details)
|
2676
|
+
|
2677
|
+
# In debug mode, also show the raw command output
|
2678
|
+
if is_verbose_mode():
|
2679
|
+
log.debug("Git review command: %s", " ".join(exc.cmd or []))
|
2680
|
+
log.debug("Return code: %s", exc.returncode)
|
2681
|
+
if exc.stdout:
|
2682
|
+
log.debug("Command stdout:\n%s", exc.stdout)
|
2683
|
+
if exc.stderr:
|
2684
|
+
log.debug("Command stderr:\n%s", exc.stderr)
|
2685
|
+
|
2686
|
+
# Include raw output in error message if analysis didn't provide
|
2687
|
+
# useful info
|
2688
|
+
has_raw_output = exc.stdout or exc.stderr
|
2689
|
+
if error_details.startswith("Unknown error") and has_raw_output:
|
2690
|
+
raw_output = ""
|
2691
|
+
if exc.stdout:
|
2692
|
+
raw_output += f"stdout: {exc.stdout.strip()}\n"
|
2693
|
+
if exc.stderr:
|
2694
|
+
raw_output += f"stderr: {exc.stderr.strip()}"
|
2695
|
+
if raw_output:
|
2696
|
+
error_details = (
|
2697
|
+
f"{error_details}\nRaw output:\n{raw_output}"
|
2698
|
+
)
|
2699
|
+
|
2504
2700
|
msg = (
|
2505
2701
|
f"Failed to push changes to Gerrit with git-review: "
|
2506
2702
|
f"{error_details}"
|
@@ -2758,9 +2954,10 @@ class Orchestrator:
|
|
2758
2954
|
env=env,
|
2759
2955
|
check=False,
|
2760
2956
|
)
|
2761
|
-
# 2) Clean untracked files/dirs (
|
2957
|
+
# 2) Clean untracked files/dirs (SSH files are now outside
|
2958
|
+
# workspace)
|
2762
2959
|
run_cmd(
|
2763
|
-
["git", "clean", "-fdx"
|
2960
|
+
["git", "clean", "-fdx"],
|
2764
2961
|
cwd=self.workspace,
|
2765
2962
|
env=env,
|
2766
2963
|
check=False,
|
@@ -2968,7 +3165,15 @@ class Orchestrator:
|
|
2968
3165
|
return f"Gerrit rejected the push: {line.strip()}"
|
2969
3166
|
return "Gerrit rejected the push for an unknown reason"
|
2970
3167
|
else:
|
2971
|
-
|
3168
|
+
# For unknown errors, include more context
|
3169
|
+
context_parts = []
|
3170
|
+
if exc.returncode is not None:
|
3171
|
+
context_parts.append(f"exit code {exc.returncode}")
|
3172
|
+
if exc.cmd:
|
3173
|
+
context_parts.append(f"command: {' '.join(exc.cmd)}")
|
3174
|
+
|
3175
|
+
context = f" ({', '.join(context_parts)})" if context_parts else ""
|
3176
|
+
return f"Unknown error{context}: {exc}"
|
2972
3177
|
|
2973
3178
|
def _query_gerrit_for_results(
|
2974
3179
|
self,
|
@@ -3933,6 +4138,128 @@ class Orchestrator:
|
|
3933
4138
|
out.append(c)
|
3934
4139
|
return out
|
3935
4140
|
|
4141
|
+
def _validate_committed_files(
|
4142
|
+
self, gh: GitHubContext, result: SubmissionResult
|
4143
|
+
) -> None:
|
4144
|
+
"""Validate that only expected files from the GitHub PR were
|
4145
|
+
committed to Gerrit.
|
4146
|
+
|
4147
|
+
This is a safety check to ensure no tool artifacts (like SSH keys) were
|
4148
|
+
accidentally included in the Gerrit change.
|
4149
|
+
"""
|
4150
|
+
if not gh.pr_number or not result.commit_shas:
|
4151
|
+
log.debug("Skipping file validation - no PR number or commit SHAs")
|
4152
|
+
return
|
4153
|
+
|
4154
|
+
try:
|
4155
|
+
# Get files changed in the GitHub PR
|
4156
|
+
from .github_api import build_client
|
4157
|
+
from .github_api import get_pull
|
4158
|
+
from .github_api import get_repo_from_env
|
4159
|
+
|
4160
|
+
client = build_client()
|
4161
|
+
repo = get_repo_from_env(client)
|
4162
|
+
pr_obj = get_pull(repo, int(gh.pr_number))
|
4163
|
+
|
4164
|
+
# Get list of files changed in the PR
|
4165
|
+
github_files = set()
|
4166
|
+
for file in pr_obj.get_files(): # type: ignore[attr-defined]
|
4167
|
+
github_files.add(file.filename)
|
4168
|
+
|
4169
|
+
log.debug(
|
4170
|
+
"GitHub PR files (%d): %s",
|
4171
|
+
len(github_files),
|
4172
|
+
sorted(github_files),
|
4173
|
+
)
|
4174
|
+
|
4175
|
+
# Check files in each commit SHA that was pushed to Gerrit
|
4176
|
+
for commit_sha in result.commit_shas:
|
4177
|
+
try:
|
4178
|
+
# Get files changed in the Gerrit commit
|
4179
|
+
from .gitutils import run_cmd
|
4180
|
+
|
4181
|
+
files_output = run_cmd(
|
4182
|
+
[
|
4183
|
+
"git",
|
4184
|
+
"show",
|
4185
|
+
"--name-only",
|
4186
|
+
"--pretty=format:",
|
4187
|
+
commit_sha,
|
4188
|
+
],
|
4189
|
+
cwd=self.workspace,
|
4190
|
+
).stdout.strip()
|
4191
|
+
|
4192
|
+
if not files_output:
|
4193
|
+
continue
|
4194
|
+
|
4195
|
+
gerrit_files = {
|
4196
|
+
f.strip() for f in files_output.split("\n") if f.strip()
|
4197
|
+
}
|
4198
|
+
log.debug(
|
4199
|
+
"Gerrit commit %s files (%d): %s",
|
4200
|
+
commit_sha[:8],
|
4201
|
+
len(gerrit_files),
|
4202
|
+
sorted(gerrit_files),
|
4203
|
+
)
|
4204
|
+
|
4205
|
+
# Check for unexpected files
|
4206
|
+
unexpected_files = gerrit_files - github_files
|
4207
|
+
if unexpected_files:
|
4208
|
+
# Filter out known safe files that might legitimately
|
4209
|
+
# differ
|
4210
|
+
suspicious_files = []
|
4211
|
+
for f in unexpected_files:
|
4212
|
+
# Skip files that are legitimately different
|
4213
|
+
if f in [".gitreview", ".gitignore"]:
|
4214
|
+
continue
|
4215
|
+
# Flag SSH artifacts and other suspicious files
|
4216
|
+
if (
|
4217
|
+
".ssh" in f
|
4218
|
+
or "known_hosts" in f
|
4219
|
+
or f.startswith("gerrit_key")
|
4220
|
+
):
|
4221
|
+
suspicious_files.append(f)
|
4222
|
+
else:
|
4223
|
+
# Other unexpected files - log but don't error
|
4224
|
+
log.warning(
|
4225
|
+
"Unexpected file in Gerrit commit: %s", f
|
4226
|
+
)
|
4227
|
+
|
4228
|
+
if suspicious_files:
|
4229
|
+
log.error(
|
4230
|
+
"❌ CRITICAL: SSH artifacts detected in Gerrit "
|
4231
|
+
"commit %s: %s",
|
4232
|
+
commit_sha[:8],
|
4233
|
+
suspicious_files,
|
4234
|
+
)
|
4235
|
+
log.error(
|
4236
|
+
"This indicates a serious bug where tool "
|
4237
|
+
"artifacts were committed. The Gerrit change "
|
4238
|
+
"may need manual cleanup."
|
4239
|
+
)
|
4240
|
+
# Don't fail the pipeline, but log prominently for
|
4241
|
+
# monitoring
|
4242
|
+
|
4243
|
+
# Also check if we're missing expected files
|
4244
|
+
missing_files = github_files - gerrit_files
|
4245
|
+
if missing_files:
|
4246
|
+
log.warning(
|
4247
|
+
"Files in GitHub PR but not in Gerrit commit "
|
4248
|
+
"%s: %s",
|
4249
|
+
commit_sha[:8],
|
4250
|
+
sorted(missing_files),
|
4251
|
+
)
|
4252
|
+
|
4253
|
+
except Exception as commit_exc:
|
4254
|
+
log.debug(
|
4255
|
+
"Failed to validate files for commit %s: %s",
|
4256
|
+
commit_sha[:8],
|
4257
|
+
commit_exc,
|
4258
|
+
)
|
4259
|
+
|
4260
|
+
except Exception as exc:
|
4261
|
+
log.debug("File validation failed (non-critical): %s", exc)
|
4262
|
+
|
3936
4263
|
|
3937
4264
|
# ---------------------
|
3938
4265
|
# Utility functions
|