github2gerrit 1.2.2__tar.gz → 1.2.3__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-1.2.2 → github2gerrit-1.2.3}/.pre-commit-config.yaml +1 -1
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/PKG-INFO +3 -3
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/README.md +2 -2
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/gerrit_pr_closer.py +58 -2
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/gerrit_rest.py +13 -0
- github2gerrit-1.2.3/src/github2gerrit/gerrit_ssh.py +284 -0
- github2gerrit-1.2.3/tests/test_gerrit_ssh_abandon.py +258 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/.editorconfig +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/.gitignore +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/.gitlint +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/.markdownlint.yaml +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/.readthedocs.yml +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/.yamllint +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/LICENSE +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/REUSE.toml +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/action.yaml +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/docs/COMMIT_RULES.md +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/docs/RELEASE-v0.2.0.md +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/pyproject.toml +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/sitecustomize.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/cli.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/commit_rules.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/config.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/constants.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/core.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/error_codes.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/external_api.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/gerrit_query.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/github_api.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/gitreview.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/gitutils.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/models.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/netrc.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/pr_commands.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/pr_content_filter.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/ssh_agent_setup.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/ssh_config_parser.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/ssh_discovery.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/conftest.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/fixtures/ssh_config_samples.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_action_outputs.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_automation_only.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_change_id_deduplication.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_clean_squash_title.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_cli.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_cli_netrc_options.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_cli_outputs_file.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_cli_url_and_dryrun.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_commit_rules.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_config_helpers.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_close_pr_policy.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_config_and_errors.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_prepare_commits.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_shallow_clone.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_ssh_setup.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_core_ssrf_protection.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_dependency_supersession.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_dns_validation_and_no_gerrit.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_error_codes.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_external_api_framework.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_force_flag_cli.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_gerrit_change_id_footer.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_gerrit_change_status_checks.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_gerrit_pr_closer.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_github_api_error_handling.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_gitreview.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_gitutils_helpers.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_issue_157_regressions.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_metadata_trailer_separation_bug.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_netrc.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_pr_commands.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_pr_update_detection.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_ssh_agent_ownership.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_ssh_artifact_prevention.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_ssh_common.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_ssh_discovery_dry_run.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_url_parser.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_utils.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/unit/test_config_integration.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/unit/test_ssh_config_parser.py +0 -0
- {github2gerrit-1.2.2 → github2gerrit-1.2.3}/uv.lock +0 -0
|
@@ -59,7 +59,7 @@ repos:
|
|
|
59
59
|
types: [yaml]
|
|
60
60
|
|
|
61
61
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
62
|
-
rev:
|
|
62
|
+
rev: 0671d8ab202c4ac093b78433ae5baf74f3fc7246 # frozen: v0.15.15
|
|
63
63
|
hooks:
|
|
64
64
|
- id: ruff
|
|
65
65
|
files: ^(src|scripts|tests)/.+\.py$
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: github2gerrit
|
|
3
|
-
Version: 1.2.
|
|
3
|
+
Version: 1.2.3
|
|
4
4
|
Summary: Submit a GitHub pull request to a Gerrit repository.
|
|
5
5
|
Project-URL: Homepage, https://github.com/lfreleng-actions/github2gerrit-action
|
|
6
6
|
Project-URL: Repository, https://github.com/lfreleng-actions/github2gerrit-action
|
|
@@ -963,7 +963,7 @@ name: github2gerrit
|
|
|
963
963
|
|
|
964
964
|
on:
|
|
965
965
|
pull_request_target:
|
|
966
|
-
types: [opened, reopened, edited, synchronize]
|
|
966
|
+
types: [opened, reopened, edited, synchronize, closed]
|
|
967
967
|
workflow_dispatch:
|
|
968
968
|
|
|
969
969
|
permissions:
|
|
@@ -1192,7 +1192,7 @@ name: github2gerrit (advanced)
|
|
|
1192
1192
|
|
|
1193
1193
|
on:
|
|
1194
1194
|
pull_request_target:
|
|
1195
|
-
types: [opened, reopened, edited, synchronize]
|
|
1195
|
+
types: [opened, reopened, edited, synchronize, closed]
|
|
1196
1196
|
workflow_dispatch:
|
|
1197
1197
|
|
|
1198
1198
|
permissions:
|
|
@@ -917,7 +917,7 @@ name: github2gerrit
|
|
|
917
917
|
|
|
918
918
|
on:
|
|
919
919
|
pull_request_target:
|
|
920
|
-
types: [opened, reopened, edited, synchronize]
|
|
920
|
+
types: [opened, reopened, edited, synchronize, closed]
|
|
921
921
|
workflow_dispatch:
|
|
922
922
|
|
|
923
923
|
permissions:
|
|
@@ -1146,7 +1146,7 @@ name: github2gerrit (advanced)
|
|
|
1146
1146
|
|
|
1147
1147
|
on:
|
|
1148
1148
|
pull_request_target:
|
|
1149
|
-
types: [opened, reopened, edited, synchronize]
|
|
1149
|
+
types: [opened, reopened, edited, synchronize, closed]
|
|
1150
1150
|
workflow_dispatch:
|
|
1151
1151
|
|
|
1152
1152
|
permissions:
|
|
@@ -11,6 +11,7 @@ merged and close the corresponding GitHub pull request that originated it.
|
|
|
11
11
|
from __future__ import annotations
|
|
12
12
|
|
|
13
13
|
import logging
|
|
14
|
+
import os
|
|
14
15
|
import re
|
|
15
16
|
from typing import Any
|
|
16
17
|
from typing import Literal
|
|
@@ -1679,11 +1680,57 @@ def _build_gerrit_abandon_message(pr_obj: Any, pr_url: str) -> str:
|
|
|
1679
1680
|
)
|
|
1680
1681
|
|
|
1681
1682
|
|
|
1683
|
+
def _abandon_change_via_ssh_if_possible(
|
|
1684
|
+
client: Any, change_number: str, message: str
|
|
1685
|
+
) -> bool:
|
|
1686
|
+
"""Attempt to abandon a change over SSH using ambient credentials.
|
|
1687
|
+
|
|
1688
|
+
Reads the Gerrit SSH connection details from the environment
|
|
1689
|
+
(populated by the action) and the host from the REST *client*.
|
|
1690
|
+
|
|
1691
|
+
Returns ``True`` only if the change was abandoned via SSH; ``False``
|
|
1692
|
+
when SSH is not configured or the SSH abandon did not succeed (in
|
|
1693
|
+
which case the caller should fall back to REST).
|
|
1694
|
+
"""
|
|
1695
|
+
ssh_privkey = os.getenv("GERRIT_SSH_PRIVKEY_G2G", "").strip()
|
|
1696
|
+
ssh_user = os.getenv("GERRIT_SSH_USER_G2G", "").strip()
|
|
1697
|
+
if not ssh_privkey or not ssh_user:
|
|
1698
|
+
return False
|
|
1699
|
+
|
|
1700
|
+
host = os.getenv("GERRIT_SERVER", "").strip()
|
|
1701
|
+
if not host:
|
|
1702
|
+
host = getattr(client, "host", "") or ""
|
|
1703
|
+
if not host:
|
|
1704
|
+
return False
|
|
1705
|
+
|
|
1706
|
+
try:
|
|
1707
|
+
port = int(os.getenv("GERRIT_SERVER_PORT", "29418") or "29418")
|
|
1708
|
+
except ValueError:
|
|
1709
|
+
port = 29418
|
|
1710
|
+
|
|
1711
|
+
from .gerrit_ssh import abandon_change_via_ssh
|
|
1712
|
+
|
|
1713
|
+
return abandon_change_via_ssh(
|
|
1714
|
+
host=host,
|
|
1715
|
+
change_number=str(change_number),
|
|
1716
|
+
message=message,
|
|
1717
|
+
user=ssh_user,
|
|
1718
|
+
ssh_privkey=ssh_privkey,
|
|
1719
|
+
known_hosts=os.getenv("GERRIT_KNOWN_HOSTS", ""),
|
|
1720
|
+
port=port,
|
|
1721
|
+
)
|
|
1722
|
+
|
|
1723
|
+
|
|
1682
1724
|
def _abandon_gerrit_change(
|
|
1683
1725
|
client: Any, change_number: str, message: str
|
|
1684
1726
|
) -> None:
|
|
1685
1727
|
"""
|
|
1686
|
-
Abandon a Gerrit change
|
|
1728
|
+
Abandon a Gerrit change.
|
|
1729
|
+
|
|
1730
|
+
Prefers SSH (``gerrit review --abandon``) because mutating REST calls
|
|
1731
|
+
are rejected with HTTP 403 on Gerrit servers that do not allow
|
|
1732
|
+
unauthenticated REST writes and where only SSH credentials are
|
|
1733
|
+
configured. Falls back to the REST API when SSH is unavailable.
|
|
1687
1734
|
|
|
1688
1735
|
Args:
|
|
1689
1736
|
client: Gerrit REST client
|
|
@@ -1691,9 +1738,18 @@ def _abandon_gerrit_change(
|
|
|
1691
1738
|
message: Abandon message
|
|
1692
1739
|
|
|
1693
1740
|
Raises:
|
|
1694
|
-
Exception: If abandon operation fails
|
|
1741
|
+
Exception: If the abandon operation fails
|
|
1695
1742
|
"""
|
|
1743
|
+
if _abandon_change_via_ssh_if_possible(client, change_number, message):
|
|
1744
|
+
log.debug(
|
|
1745
|
+
"Successfully abandoned Gerrit change %s via SSH", change_number
|
|
1746
|
+
)
|
|
1747
|
+
return
|
|
1748
|
+
|
|
1696
1749
|
try:
|
|
1750
|
+
log.debug(
|
|
1751
|
+
"Falling back to REST abandon for Gerrit change %s", change_number
|
|
1752
|
+
)
|
|
1697
1753
|
abandon_path = f"/changes/{change_number}/abandon"
|
|
1698
1754
|
abandon_data = {"message": message}
|
|
1699
1755
|
client.post(abandon_path, data=abandon_data)
|
|
@@ -163,6 +163,19 @@ class GerritRestClient:
|
|
|
163
163
|
"""Return True if client has authentication credentials."""
|
|
164
164
|
return self._auth is not None
|
|
165
165
|
|
|
166
|
+
@property
|
|
167
|
+
def host(self) -> str:
|
|
168
|
+
"""Return the Gerrit hostname derived from the base URL.
|
|
169
|
+
|
|
170
|
+
Returns an empty string when the host cannot be determined.
|
|
171
|
+
"""
|
|
172
|
+
from urllib.parse import urlparse
|
|
173
|
+
|
|
174
|
+
try:
|
|
175
|
+
return urlparse(self._base_url).hostname or ""
|
|
176
|
+
except Exception:
|
|
177
|
+
return ""
|
|
178
|
+
|
|
166
179
|
def get(self, path: str) -> Any:
|
|
167
180
|
"""HTTP GET, returning parsed JSON."""
|
|
168
181
|
return self._request_json_with_retry("GET", path)
|
|
@@ -0,0 +1,284 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
SSH-based Gerrit operations.
|
|
6
|
+
|
|
7
|
+
Some Gerrit deployments (notably those fronted by a CDN/WAF, or that
|
|
8
|
+
disallow anonymous REST writes) reject mutating REST calls such as
|
|
9
|
+
``POST /changes/{id}/abandon`` with HTTP 403 unless dedicated HTTP API
|
|
10
|
+
credentials are supplied. The github2gerrit action authenticates to
|
|
11
|
+
Gerrit over SSH (the same channel used to push changes), so this module
|
|
12
|
+
performs the abandon over SSH instead, reusing the already-configured
|
|
13
|
+
SSH key and known_hosts.
|
|
14
|
+
|
|
15
|
+
The single public entry point :func:`abandon_change_via_ssh` is designed
|
|
16
|
+
to be used as the preferred path with a REST fallback: it never raises
|
|
17
|
+
and returns ``True`` only when the change was actually abandoned.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
21
|
+
|
|
22
|
+
import json
|
|
23
|
+
import logging
|
|
24
|
+
import os
|
|
25
|
+
import secrets
|
|
26
|
+
import shlex
|
|
27
|
+
import tempfile
|
|
28
|
+
from pathlib import Path
|
|
29
|
+
|
|
30
|
+
from .gitutils import CommandError
|
|
31
|
+
from .gitutils import run_cmd
|
|
32
|
+
from .ssh_common import augment_known_hosts_with_bracketed_entries
|
|
33
|
+
from .ssh_common import build_non_interactive_ssh_env
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
log = logging.getLogger(__name__)
|
|
37
|
+
|
|
38
|
+
DEFAULT_GERRIT_SSH_PORT = 29418
|
|
39
|
+
_SSH_TIMEOUT_SECONDS = 30.0
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
def _write_secure_file(path: Path, content: str, mode: int) -> None:
|
|
43
|
+
"""Write *content* to *path* with restrictive *mode* permissions."""
|
|
44
|
+
# Create with secure permissions from the start (avoid a window where
|
|
45
|
+
# the file is world-readable).
|
|
46
|
+
fd = os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, mode)
|
|
47
|
+
try:
|
|
48
|
+
handle = os.fdopen(fd, "w", encoding="utf-8")
|
|
49
|
+
except BaseException:
|
|
50
|
+
# os.fdopen did not take ownership of fd; close it to avoid a leak.
|
|
51
|
+
os.close(fd)
|
|
52
|
+
raise
|
|
53
|
+
with handle:
|
|
54
|
+
handle.write(content)
|
|
55
|
+
# Re-assert mode in case umask altered it.
|
|
56
|
+
path.chmod(mode)
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _build_ssh_base_argv(
|
|
60
|
+
*,
|
|
61
|
+
key_path: Path,
|
|
62
|
+
known_hosts_path: Path | None,
|
|
63
|
+
port: int,
|
|
64
|
+
user: str,
|
|
65
|
+
host: str,
|
|
66
|
+
) -> list[str]:
|
|
67
|
+
"""Build the common ``ssh`` argv prefix for non-interactive auth."""
|
|
68
|
+
argv: list[str] = [
|
|
69
|
+
"ssh",
|
|
70
|
+
"-F",
|
|
71
|
+
"/dev/null",
|
|
72
|
+
"-i",
|
|
73
|
+
str(key_path),
|
|
74
|
+
"-o",
|
|
75
|
+
"IdentitiesOnly=yes",
|
|
76
|
+
"-o",
|
|
77
|
+
"IdentityAgent=none",
|
|
78
|
+
"-o",
|
|
79
|
+
"BatchMode=yes",
|
|
80
|
+
"-o",
|
|
81
|
+
"PreferredAuthentications=publickey",
|
|
82
|
+
"-o",
|
|
83
|
+
"PasswordAuthentication=no",
|
|
84
|
+
"-o",
|
|
85
|
+
"PubkeyAcceptedKeyTypes=+ssh-rsa",
|
|
86
|
+
"-o",
|
|
87
|
+
"ConnectTimeout=10",
|
|
88
|
+
]
|
|
89
|
+
if known_hosts_path is not None:
|
|
90
|
+
argv += [
|
|
91
|
+
"-o",
|
|
92
|
+
f"UserKnownHostsFile={known_hosts_path}",
|
|
93
|
+
"-o",
|
|
94
|
+
"StrictHostKeyChecking=yes",
|
|
95
|
+
]
|
|
96
|
+
else:
|
|
97
|
+
# No known_hosts supplied: we cannot verify the host key, but we
|
|
98
|
+
# still want a non-interactive connection. accept-new records the
|
|
99
|
+
# key on first use; pair it with an explicit (throwaway)
|
|
100
|
+
# UserKnownHostsFile so OpenSSH does not mutate the runner's
|
|
101
|
+
# default ~/.ssh/known_hosts (which may be missing/unwritable).
|
|
102
|
+
log.warning(
|
|
103
|
+
"No GERRIT_KNOWN_HOSTS available for SSH abandon; using "
|
|
104
|
+
"StrictHostKeyChecking=accept-new with a throwaway known_hosts"
|
|
105
|
+
)
|
|
106
|
+
argv += [
|
|
107
|
+
"-o",
|
|
108
|
+
"UserKnownHostsFile=/dev/null",
|
|
109
|
+
"-o",
|
|
110
|
+
"StrictHostKeyChecking=accept-new",
|
|
111
|
+
]
|
|
112
|
+
argv += ["-n", "-p", str(port), f"{user}@{host}"]
|
|
113
|
+
return argv
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
def _resolve_current_patchset(
|
|
117
|
+
base_argv: list[str],
|
|
118
|
+
change_number: str,
|
|
119
|
+
env: dict[str, str],
|
|
120
|
+
) -> str | None:
|
|
121
|
+
"""Return the current patch-set number for *change_number*, or None."""
|
|
122
|
+
remote_cmd = (
|
|
123
|
+
"gerrit query --format=JSON --current-patch-set "
|
|
124
|
+
f"change:{shlex.quote(change_number)}"
|
|
125
|
+
)
|
|
126
|
+
try:
|
|
127
|
+
result = run_cmd(
|
|
128
|
+
[*base_argv, remote_cmd],
|
|
129
|
+
timeout=_SSH_TIMEOUT_SECONDS,
|
|
130
|
+
env=env,
|
|
131
|
+
)
|
|
132
|
+
except CommandError as exc:
|
|
133
|
+
log.debug(
|
|
134
|
+
"Gerrit SSH query for change %s failed: %s",
|
|
135
|
+
change_number,
|
|
136
|
+
exc,
|
|
137
|
+
)
|
|
138
|
+
return None
|
|
139
|
+
|
|
140
|
+
for raw_line in result.stdout.splitlines():
|
|
141
|
+
line = raw_line.strip()
|
|
142
|
+
if not line:
|
|
143
|
+
continue
|
|
144
|
+
try:
|
|
145
|
+
record = json.loads(line)
|
|
146
|
+
except (ValueError, TypeError):
|
|
147
|
+
continue
|
|
148
|
+
patch_set = record.get("currentPatchSet")
|
|
149
|
+
if isinstance(patch_set, dict) and patch_set.get("number") is not None:
|
|
150
|
+
return str(patch_set["number"])
|
|
151
|
+
log.debug(
|
|
152
|
+
"No currentPatchSet found in Gerrit query output for change %s",
|
|
153
|
+
change_number,
|
|
154
|
+
)
|
|
155
|
+
return None
|
|
156
|
+
|
|
157
|
+
|
|
158
|
+
def abandon_change_via_ssh(
|
|
159
|
+
*,
|
|
160
|
+
host: str,
|
|
161
|
+
change_number: str,
|
|
162
|
+
message: str,
|
|
163
|
+
user: str,
|
|
164
|
+
ssh_privkey: str,
|
|
165
|
+
known_hosts: str | None = None,
|
|
166
|
+
port: int = DEFAULT_GERRIT_SSH_PORT,
|
|
167
|
+
) -> bool:
|
|
168
|
+
"""Abandon a Gerrit change over SSH using ``gerrit review --abandon``.
|
|
169
|
+
|
|
170
|
+
This is the preferred abandon path because it uses the same SSH
|
|
171
|
+
credentials that the action already relies on to push changes, and
|
|
172
|
+
therefore works on Gerrit servers that reject unauthenticated REST
|
|
173
|
+
writes.
|
|
174
|
+
|
|
175
|
+
Args:
|
|
176
|
+
host: Gerrit SSH hostname (no scheme).
|
|
177
|
+
change_number: Numeric Gerrit change number.
|
|
178
|
+
message: Abandon message (may be multi-line).
|
|
179
|
+
user: Gerrit SSH username.
|
|
180
|
+
ssh_privkey: SSH private key content.
|
|
181
|
+
known_hosts: Optional known_hosts content for host verification.
|
|
182
|
+
port: Gerrit SSH port (default 29418).
|
|
183
|
+
|
|
184
|
+
Returns:
|
|
185
|
+
``True`` if the change was abandoned successfully; ``False`` when
|
|
186
|
+
prerequisites are missing or the SSH operation failed (so the
|
|
187
|
+
caller may fall back to another mechanism). This function never
|
|
188
|
+
raises.
|
|
189
|
+
"""
|
|
190
|
+
if not (host and user and ssh_privkey and str(change_number).strip()):
|
|
191
|
+
log.debug(
|
|
192
|
+
"SSH abandon prerequisites missing (host=%s, user=%s, "
|
|
193
|
+
"key=%s, change=%s); skipping SSH abandon",
|
|
194
|
+
bool(host),
|
|
195
|
+
bool(user),
|
|
196
|
+
bool(ssh_privkey),
|
|
197
|
+
change_number,
|
|
198
|
+
)
|
|
199
|
+
return False
|
|
200
|
+
|
|
201
|
+
change_number = str(change_number).strip()
|
|
202
|
+
try:
|
|
203
|
+
tmp_dir = Path(
|
|
204
|
+
tempfile.mkdtemp(prefix=f"g2g_abandon_{secrets.token_hex(8)}_")
|
|
205
|
+
)
|
|
206
|
+
except OSError as exc:
|
|
207
|
+
# Honor the "never raises" contract so callers can fall back to REST.
|
|
208
|
+
log.debug("Could not create temp dir for SSH abandon: %s", exc)
|
|
209
|
+
return False
|
|
210
|
+
try:
|
|
211
|
+
tmp_dir.chmod(0o700)
|
|
212
|
+
key_path = tmp_dir / "gerrit_key"
|
|
213
|
+
_write_secure_file(key_path, ssh_privkey.strip() + "\n", 0o600)
|
|
214
|
+
|
|
215
|
+
known_hosts_path: Path | None = None
|
|
216
|
+
if known_hosts and known_hosts.strip():
|
|
217
|
+
# OpenSSH looks up host keys under a bracketed "[host]:port"
|
|
218
|
+
# entry when connecting on a non-default port (Gerrit uses
|
|
219
|
+
# 29418). Augment the provided content with bracketed variants
|
|
220
|
+
# so StrictHostKeyChecking can verify the key and we don't fall
|
|
221
|
+
# back to REST unnecessarily.
|
|
222
|
+
augmented = augment_known_hosts_with_bracketed_entries(
|
|
223
|
+
known_hosts.strip(), host, port
|
|
224
|
+
)
|
|
225
|
+
known_hosts_path = tmp_dir / "known_hosts"
|
|
226
|
+
_write_secure_file(known_hosts_path, augmented, 0o644)
|
|
227
|
+
|
|
228
|
+
base_argv = _build_ssh_base_argv(
|
|
229
|
+
key_path=key_path,
|
|
230
|
+
known_hosts_path=known_hosts_path,
|
|
231
|
+
port=port,
|
|
232
|
+
user=user,
|
|
233
|
+
host=host,
|
|
234
|
+
)
|
|
235
|
+
|
|
236
|
+
# Disable any ambient SSH agent so only the provided key is used.
|
|
237
|
+
ssh_env = build_non_interactive_ssh_env()
|
|
238
|
+
|
|
239
|
+
patch_set = _resolve_current_patchset(base_argv, change_number, ssh_env)
|
|
240
|
+
if patch_set is None:
|
|
241
|
+
log.debug(
|
|
242
|
+
"Could not resolve current patch-set for change %s via SSH",
|
|
243
|
+
change_number,
|
|
244
|
+
)
|
|
245
|
+
return False
|
|
246
|
+
|
|
247
|
+
target = f"{change_number},{patch_set}"
|
|
248
|
+
remote_cmd = (
|
|
249
|
+
"gerrit review --abandon "
|
|
250
|
+
f"-m {shlex.quote(message)} {shlex.quote(target)}"
|
|
251
|
+
)
|
|
252
|
+
try:
|
|
253
|
+
run_cmd(
|
|
254
|
+
[*base_argv, remote_cmd],
|
|
255
|
+
timeout=_SSH_TIMEOUT_SECONDS,
|
|
256
|
+
env=ssh_env,
|
|
257
|
+
)
|
|
258
|
+
except CommandError as exc:
|
|
259
|
+
log.warning(
|
|
260
|
+
"SSH abandon failed for change %s: %s",
|
|
261
|
+
change_number,
|
|
262
|
+
exc,
|
|
263
|
+
)
|
|
264
|
+
return False
|
|
265
|
+
else:
|
|
266
|
+
log.debug(
|
|
267
|
+
"Successfully abandoned Gerrit change %s via SSH",
|
|
268
|
+
change_number,
|
|
269
|
+
)
|
|
270
|
+
return True
|
|
271
|
+
except Exception:
|
|
272
|
+
log.warning(
|
|
273
|
+
"Unexpected error during SSH abandon for change %s",
|
|
274
|
+
change_number,
|
|
275
|
+
exc_info=True,
|
|
276
|
+
)
|
|
277
|
+
return False
|
|
278
|
+
finally:
|
|
279
|
+
try:
|
|
280
|
+
import shutil
|
|
281
|
+
|
|
282
|
+
shutil.rmtree(tmp_dir, ignore_errors=True)
|
|
283
|
+
except Exception:
|
|
284
|
+
log.debug("Failed to clean up SSH temp dir %s", tmp_dir)
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
# SPDX-License-Identifier: Apache-2.0
|
|
2
|
+
# SPDX-FileCopyrightText: 2025 The Linux Foundation
|
|
3
|
+
|
|
4
|
+
"""
|
|
5
|
+
Tests for SSH-based Gerrit abandon (gerrit_ssh module) and the SSH-first
|
|
6
|
+
routing in gerrit_pr_closer._abandon_gerrit_change.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from __future__ import annotations
|
|
10
|
+
|
|
11
|
+
import contextlib
|
|
12
|
+
from pathlib import Path
|
|
13
|
+
from unittest.mock import MagicMock
|
|
14
|
+
from unittest.mock import patch
|
|
15
|
+
|
|
16
|
+
from github2gerrit import gerrit_ssh
|
|
17
|
+
from github2gerrit.gitutils import CommandError
|
|
18
|
+
from github2gerrit.gitutils import CommandResult
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _ok(stdout: str = "") -> CommandResult:
|
|
22
|
+
return CommandResult(returncode=0, stdout=stdout, stderr="")
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
_QUERY_JSON = (
|
|
26
|
+
'{"project":"netconf","number":123344,'
|
|
27
|
+
'"currentPatchSet":{"number":2}}\n'
|
|
28
|
+
'{"type":"stats","rowCount":1}\n'
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class TestAbandonChangeViaSsh:
|
|
33
|
+
"""Tests for gerrit_ssh.abandon_change_via_ssh."""
|
|
34
|
+
|
|
35
|
+
def test_returns_false_when_prerequisites_missing(self):
|
|
36
|
+
assert (
|
|
37
|
+
gerrit_ssh.abandon_change_via_ssh(
|
|
38
|
+
host="",
|
|
39
|
+
change_number="1",
|
|
40
|
+
message="m",
|
|
41
|
+
user="u",
|
|
42
|
+
ssh_privkey="key",
|
|
43
|
+
)
|
|
44
|
+
is False
|
|
45
|
+
)
|
|
46
|
+
assert (
|
|
47
|
+
gerrit_ssh.abandon_change_via_ssh(
|
|
48
|
+
host="h",
|
|
49
|
+
change_number="1",
|
|
50
|
+
message="m",
|
|
51
|
+
user="",
|
|
52
|
+
ssh_privkey="key",
|
|
53
|
+
)
|
|
54
|
+
is False
|
|
55
|
+
)
|
|
56
|
+
assert (
|
|
57
|
+
gerrit_ssh.abandon_change_via_ssh(
|
|
58
|
+
host="h",
|
|
59
|
+
change_number="1",
|
|
60
|
+
message="m",
|
|
61
|
+
user="u",
|
|
62
|
+
ssh_privkey="",
|
|
63
|
+
)
|
|
64
|
+
is False
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
def test_mkdtemp_failure_returns_false(self):
|
|
68
|
+
# Honor the "never raises" contract: a temp-dir failure must result
|
|
69
|
+
# in a clean False so the caller can fall back to REST.
|
|
70
|
+
with patch.object(
|
|
71
|
+
gerrit_ssh.tempfile,
|
|
72
|
+
"mkdtemp",
|
|
73
|
+
side_effect=OSError("disk full"),
|
|
74
|
+
):
|
|
75
|
+
result = gerrit_ssh.abandon_change_via_ssh(
|
|
76
|
+
host="git.example.org",
|
|
77
|
+
change_number="123344",
|
|
78
|
+
message="m",
|
|
79
|
+
user="u",
|
|
80
|
+
ssh_privkey="key",
|
|
81
|
+
)
|
|
82
|
+
assert result is False
|
|
83
|
+
|
|
84
|
+
def test_successful_abandon_queries_then_reviews(self):
|
|
85
|
+
calls: list[list[str]] = []
|
|
86
|
+
known_hosts_seen: list[str] = []
|
|
87
|
+
|
|
88
|
+
def fake_run_cmd(cmd, **kwargs):
|
|
89
|
+
calls.append(list(cmd))
|
|
90
|
+
# Capture the known_hosts file content referenced in the argv.
|
|
91
|
+
for tok in cmd:
|
|
92
|
+
if isinstance(tok, str) and tok.startswith(
|
|
93
|
+
"UserKnownHostsFile="
|
|
94
|
+
):
|
|
95
|
+
p = tok.split("=", 1)[1]
|
|
96
|
+
with contextlib.suppress(OSError):
|
|
97
|
+
known_hosts_seen.append(
|
|
98
|
+
Path(p).read_text(encoding="utf-8")
|
|
99
|
+
)
|
|
100
|
+
remote = cmd[-1]
|
|
101
|
+
if remote.startswith("gerrit query"):
|
|
102
|
+
return _ok(_QUERY_JSON)
|
|
103
|
+
return _ok("")
|
|
104
|
+
|
|
105
|
+
with patch.object(gerrit_ssh, "run_cmd", side_effect=fake_run_cmd):
|
|
106
|
+
result = gerrit_ssh.abandon_change_via_ssh(
|
|
107
|
+
host="git.example.org",
|
|
108
|
+
change_number="123344",
|
|
109
|
+
message="PR closed",
|
|
110
|
+
user="gh2gerrit",
|
|
111
|
+
ssh_privkey="PRIVKEY",
|
|
112
|
+
known_hosts="git.example.org ssh-ed25519 AAAA",
|
|
113
|
+
port=29418,
|
|
114
|
+
)
|
|
115
|
+
|
|
116
|
+
assert result is True
|
|
117
|
+
# Two SSH invocations: query then review.
|
|
118
|
+
assert len(calls) == 2
|
|
119
|
+
query_remote = calls[0][-1]
|
|
120
|
+
review_remote = calls[1][-1]
|
|
121
|
+
assert query_remote.startswith("gerrit query")
|
|
122
|
+
assert "change:123344" in query_remote
|
|
123
|
+
assert review_remote.startswith("gerrit review --abandon")
|
|
124
|
+
assert " -m " in review_remote
|
|
125
|
+
# The resolved patch-set must be used as <change>,<patchset>.
|
|
126
|
+
assert "123344,2" in review_remote
|
|
127
|
+
# Connection target is user@host on the given port.
|
|
128
|
+
assert "gh2gerrit@git.example.org" in calls[1]
|
|
129
|
+
assert "29418" in calls[1]
|
|
130
|
+
# known_hosts must be augmented with a bracketed [host]:port entry
|
|
131
|
+
# so verification works on the non-default Gerrit SSH port.
|
|
132
|
+
assert known_hosts_seen
|
|
133
|
+
assert "[git.example.org]:29418" in known_hosts_seen[0]
|
|
134
|
+
|
|
135
|
+
def test_no_known_hosts_uses_throwaway_file(self):
|
|
136
|
+
calls: list[list[str]] = []
|
|
137
|
+
|
|
138
|
+
def fake_run_cmd(cmd, **kwargs):
|
|
139
|
+
calls.append(list(cmd))
|
|
140
|
+
if cmd[-1].startswith("gerrit query"):
|
|
141
|
+
return _ok(_QUERY_JSON)
|
|
142
|
+
return _ok("")
|
|
143
|
+
|
|
144
|
+
with patch.object(gerrit_ssh, "run_cmd", side_effect=fake_run_cmd):
|
|
145
|
+
result = gerrit_ssh.abandon_change_via_ssh(
|
|
146
|
+
host="git.example.org",
|
|
147
|
+
change_number="123344",
|
|
148
|
+
message="m",
|
|
149
|
+
user="u",
|
|
150
|
+
ssh_privkey="k",
|
|
151
|
+
known_hosts=None,
|
|
152
|
+
)
|
|
153
|
+
|
|
154
|
+
assert result is True
|
|
155
|
+
# Without known_hosts we must not mutate the default known_hosts.
|
|
156
|
+
flat = " ".join(calls[0])
|
|
157
|
+
assert "UserKnownHostsFile=/dev/null" in flat
|
|
158
|
+
assert "StrictHostKeyChecking=accept-new" in flat
|
|
159
|
+
|
|
160
|
+
def test_returns_false_when_patchset_unresolved(self):
|
|
161
|
+
def fake_run_cmd(cmd, **kwargs):
|
|
162
|
+
remote = cmd[-1]
|
|
163
|
+
if remote.startswith("gerrit query"):
|
|
164
|
+
return _ok('{"type":"stats","rowCount":0}\n')
|
|
165
|
+
raise AssertionError("review should not run without a patch-set")
|
|
166
|
+
|
|
167
|
+
with patch.object(gerrit_ssh, "run_cmd", side_effect=fake_run_cmd):
|
|
168
|
+
result = gerrit_ssh.abandon_change_via_ssh(
|
|
169
|
+
host="git.example.org",
|
|
170
|
+
change_number="999",
|
|
171
|
+
message="m",
|
|
172
|
+
user="u",
|
|
173
|
+
ssh_privkey="k",
|
|
174
|
+
)
|
|
175
|
+
assert result is False
|
|
176
|
+
|
|
177
|
+
def test_returns_false_when_review_fails(self):
|
|
178
|
+
def fake_run_cmd(cmd, **kwargs):
|
|
179
|
+
remote = cmd[-1]
|
|
180
|
+
if remote.startswith("gerrit query"):
|
|
181
|
+
return _ok(_QUERY_JSON)
|
|
182
|
+
raise CommandError("abandon failed", returncode=1)
|
|
183
|
+
|
|
184
|
+
with patch.object(gerrit_ssh, "run_cmd", side_effect=fake_run_cmd):
|
|
185
|
+
result = gerrit_ssh.abandon_change_via_ssh(
|
|
186
|
+
host="git.example.org",
|
|
187
|
+
change_number="123344",
|
|
188
|
+
message="m",
|
|
189
|
+
user="u",
|
|
190
|
+
ssh_privkey="k",
|
|
191
|
+
known_hosts="kh",
|
|
192
|
+
)
|
|
193
|
+
assert result is False
|
|
194
|
+
|
|
195
|
+
def test_returns_false_when_query_fails(self):
|
|
196
|
+
def fake_run_cmd(cmd, **kwargs):
|
|
197
|
+
raise CommandError("query failed", returncode=255)
|
|
198
|
+
|
|
199
|
+
with patch.object(gerrit_ssh, "run_cmd", side_effect=fake_run_cmd):
|
|
200
|
+
result = gerrit_ssh.abandon_change_via_ssh(
|
|
201
|
+
host="git.example.org",
|
|
202
|
+
change_number="123344",
|
|
203
|
+
message="m",
|
|
204
|
+
user="u",
|
|
205
|
+
ssh_privkey="k",
|
|
206
|
+
)
|
|
207
|
+
assert result is False
|
|
208
|
+
|
|
209
|
+
|
|
210
|
+
class TestAbandonRouting:
|
|
211
|
+
"""Tests for SSH-first routing in _abandon_gerrit_change."""
|
|
212
|
+
|
|
213
|
+
def test_prefers_ssh_and_skips_rest(self, monkeypatch):
|
|
214
|
+
from github2gerrit import gerrit_pr_closer
|
|
215
|
+
|
|
216
|
+
monkeypatch.setenv("GERRIT_SSH_PRIVKEY_G2G", "PRIVKEY")
|
|
217
|
+
monkeypatch.setenv("GERRIT_SSH_USER_G2G", "gh2gerrit")
|
|
218
|
+
monkeypatch.setenv("GERRIT_SERVER", "git.example.org")
|
|
219
|
+
monkeypatch.setenv("GERRIT_KNOWN_HOSTS", "kh")
|
|
220
|
+
monkeypatch.setenv("GERRIT_SERVER_PORT", "29418")
|
|
221
|
+
|
|
222
|
+
client = MagicMock()
|
|
223
|
+
with patch(
|
|
224
|
+
"github2gerrit.gerrit_ssh.abandon_change_via_ssh",
|
|
225
|
+
return_value=True,
|
|
226
|
+
) as mock_ssh:
|
|
227
|
+
gerrit_pr_closer._abandon_gerrit_change(client, "123344", "msg")
|
|
228
|
+
|
|
229
|
+
mock_ssh.assert_called_once()
|
|
230
|
+
client.post.assert_not_called()
|
|
231
|
+
|
|
232
|
+
def test_falls_back_to_rest_when_ssh_unavailable(self, monkeypatch):
|
|
233
|
+
from github2gerrit import gerrit_pr_closer
|
|
234
|
+
|
|
235
|
+
monkeypatch.delenv("GERRIT_SSH_PRIVKEY_G2G", raising=False)
|
|
236
|
+
monkeypatch.delenv("GERRIT_SSH_USER_G2G", raising=False)
|
|
237
|
+
|
|
238
|
+
client = MagicMock()
|
|
239
|
+
client.host = "git.example.org"
|
|
240
|
+
gerrit_pr_closer._abandon_gerrit_change(client, "123344", "msg")
|
|
241
|
+
client.post.assert_called_once()
|
|
242
|
+
args, _kwargs = client.post.call_args
|
|
243
|
+
assert "/changes/123344/abandon" in args[0]
|
|
244
|
+
|
|
245
|
+
def test_falls_back_to_rest_when_ssh_fails(self, monkeypatch):
|
|
246
|
+
from github2gerrit import gerrit_pr_closer
|
|
247
|
+
|
|
248
|
+
monkeypatch.setenv("GERRIT_SSH_PRIVKEY_G2G", "PRIVKEY")
|
|
249
|
+
monkeypatch.setenv("GERRIT_SSH_USER_G2G", "gh2gerrit")
|
|
250
|
+
monkeypatch.setenv("GERRIT_SERVER", "git.example.org")
|
|
251
|
+
|
|
252
|
+
client = MagicMock()
|
|
253
|
+
with patch(
|
|
254
|
+
"github2gerrit.gerrit_ssh.abandon_change_via_ssh",
|
|
255
|
+
return_value=False,
|
|
256
|
+
):
|
|
257
|
+
gerrit_pr_closer._abandon_gerrit_change(client, "123344", "msg")
|
|
258
|
+
client.post.assert_called_once()
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github2gerrit-1.2.2 → github2gerrit-1.2.3}/docs/github2gerrit_token_permissions_classic.png
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github2gerrit-1.2.2 → github2gerrit-1.2.3}/src/github2gerrit/orchestrator/reconciliation.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{github2gerrit-1.2.2 → github2gerrit-1.2.3}/tests/test_mapping_comment_digest_and_backref.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|