github2gerrit 1.0.5__tar.gz → 1.0.7__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 (120) hide show
  1. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.pre-commit-config.yaml +3 -3
  2. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/PKG-INFO +2 -1
  3. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/action.yaml +1 -1
  4. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/pyproject.toml +3 -0
  5. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/core.py +123 -6
  6. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/models.py +6 -1
  7. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/pr_content_filter.py +19 -21
  8. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_change_id_deduplication.py +1 -1
  9. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli_outputs_file.py +1 -1
  10. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli_url_and_dryrun.py +2 -2
  11. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_close_pr_policy.py +59 -7
  12. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_prepare_commits.py +1 -1
  13. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_shallow_clone.py +339 -0
  14. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_duplicate_detection.py +10 -2
  15. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_change_id_footer.py +1 -1
  16. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gitutils_helpers.py +1 -1
  17. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/uv.lock +96 -89
  18. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.editorconfig +0 -0
  19. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.gitignore +0 -0
  20. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.gitlint +0 -0
  21. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.markdownlint.yaml +0 -0
  22. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.readthedocs.yml +0 -0
  23. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/.yamllint +0 -0
  24. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/LICENSE +0 -0
  25. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/LICENSES/Apache-2.0.txt +0 -0
  26. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/README.md +0 -0
  27. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/REUSE.toml +0 -0
  28. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  29. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  30. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/docs/RELEASE-v0.2.0.md +0 -0
  31. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/docs/github2gerrit_token_permissions_classic.png +0 -0
  32. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/sitecustomize.py +0 -0
  33. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/__init__.py +0 -0
  34. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/cli.py +0 -0
  35. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/commit_normalization.py +0 -0
  36. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/config.py +0 -0
  37. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/constants.py +0 -0
  38. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/duplicate_detection.py +0 -0
  39. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/error_codes.py +0 -0
  40. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/external_api.py +0 -0
  41. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gerrit_pr_closer.py +0 -0
  42. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gerrit_query.py +0 -0
  43. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gerrit_rest.py +0 -0
  44. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gerrit_urls.py +0 -0
  45. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/github_api.py +0 -0
  46. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/gitutils.py +0 -0
  47. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/mapping_comment.py +0 -0
  48. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/netrc.py +0 -0
  49. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/orchestrator/__init__.py +0 -0
  50. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  51. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/reconcile_matcher.py +0 -0
  52. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/rich_display.py +0 -0
  53. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/rich_logging.py +0 -0
  54. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/similarity.py +0 -0
  55. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/ssh_agent_setup.py +0 -0
  56. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/ssh_common.py +0 -0
  57. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/ssh_config_parser.py +0 -0
  58. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/ssh_discovery.py +0 -0
  59. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/trailers.py +0 -0
  60. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/src/github2gerrit/utils.py +0 -0
  61. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/conftest.py +0 -0
  62. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/fixtures/__init__.py +0 -0
  63. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/fixtures/make_repo.py +0 -0
  64. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/fixtures/ssh_config_samples.py +0 -0
  65. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_action_environment_mapping.py +0 -0
  66. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_action_outputs.py +0 -0
  67. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_action_pr_number_handling.py +0 -0
  68. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_action_step_validation.py +0 -0
  69. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_automation_only.py +0 -0
  70. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli.py +0 -0
  71. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli_helpers.py +0 -0
  72. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_cli_netrc_options.py +0 -0
  73. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_commit_normalization.py +0 -0
  74. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_composite_action_coverage.py +0 -0
  75. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_config_and_reviewers.py +0 -0
  76. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_config_helpers.py +0 -0
  77. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_config_and_errors.py +0 -0
  78. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_gerrit_backref_comment.py +0 -0
  79. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_gerrit_push_errors.py +0 -0
  80. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_gerrit_rest_results.py +0 -0
  81. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_integration_fixture_repo.py +0 -0
  82. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_ssh_setup.py +0 -0
  83. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_core_ssrf_protection.py +0 -0
  84. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_email_case_normalization.py +0 -0
  85. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_error_codes.py +0 -0
  86. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_external_api_framework.py +0 -0
  87. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_force_flag_cli.py +0 -0
  88. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_change_status_checks.py +0 -0
  89. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_pr_closer.py +0 -0
  90. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_rest_client.py +0 -0
  91. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_urls.py +0 -0
  92. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_gerrit_urls_more.py +0 -0
  93. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ghe_and_gitreview_args.py +0 -0
  94. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_github_api_error_handling.py +0 -0
  95. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_github_api_helpers.py +0 -0
  96. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_github_api_retry_and_helpers.py +0 -0
  97. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_mapping_comment_additional.py +0 -0
  98. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  99. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_metadata_and_reconciliation.py +0 -0
  100. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_metadata_trailer_separation_bug.py +0 -0
  101. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_misc_small_coverage.py +0 -0
  102. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_netrc.py +0 -0
  103. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_orphan_rest_side_effects.py +0 -0
  104. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_pr_content_filter.py +0 -0
  105. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_pr_content_filter_integration.py +0 -0
  106. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_pr_update_detection.py +0 -0
  107. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_reconciliation_extracted_module.py +0 -0
  108. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  109. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_reconciliation_scenarios.py +0 -0
  110. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_agent.py +0 -0
  111. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_agent_ownership.py +0 -0
  112. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_artifact_prevention.py +0 -0
  113. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_common.py +0 -0
  114. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_discovery.py +0 -0
  115. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_ssh_discovery_dry_run.py +0 -0
  116. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_trailers_additional.py +0 -0
  117. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_url_parser.py +0 -0
  118. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/test_utils.py +0 -0
  119. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/unit/test_config_integration.py +0 -0
  120. {github2gerrit-1.0.5 → github2gerrit-1.0.7}/tests/unit/test_ssh_config_parser.py +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: 45ef068da5f21267bb2a7ec4a623092959f09ce5 # frozen: v0.14.14
62
+ rev: 0839f92796ae388643a08a21640a029b322be5c2 # frozen: v0.15.2
63
63
  hooks:
64
64
  - id: ruff
65
65
  files: ^(src|scripts|tests)/.+\.py$
@@ -110,7 +110,7 @@ repos:
110
110
  # Replaces: https://github.com/rhysd/actionlint
111
111
  # Permits actionlint to run both locally and with precommit.ci/GitHub
112
112
  - repo: https://github.com/Mateusz-Grzelinski/actionlint-py
113
- rev: 85c37735ea69e5baf0681530e57e63deee0ce733 # frozen: v1.7.10.24
113
+ rev: 694e2c0dfb4253d51f3c6c54b8f9fec0a16764dc # frozen: v1.7.11.24
114
114
  hooks:
115
115
  - id: actionlint
116
116
 
@@ -121,7 +121,7 @@ repos:
121
121
  - id: codespell
122
122
 
123
123
  - repo: https://github.com/python-jsonschema/check-jsonschema
124
- rev: ccf21790019848af3eb4464be2a9d5efed6358f3 # frozen: 0.36.1
124
+ rev: ec368acd16deee9c560c105ab6d27db4ee19a5ec # frozen: 0.36.2
125
125
  hooks:
126
126
  - id: check-github-actions
127
127
  - id: check-github-workflows
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 1.0.5
3
+ Version: 1.0.7
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
@@ -22,6 +22,7 @@ Classifier: Topic :: Software Development :: Build Tools
22
22
  Classifier: Topic :: Software Development :: Version Control
23
23
  Classifier: Typing :: Typed
24
24
  Requires-Python: >=3.11
25
+ Requires-Dist: cryptography>=46.0.5
25
26
  Requires-Dist: git-review>=2.5.0
26
27
  Requires-Dist: pygerrit2>=2.0.15
27
28
  Requires-Dist: pygithub>=2.8.1
@@ -164,7 +164,7 @@ runs:
164
164
 
165
165
  - name: "Setup uv"
166
166
  # yamllint disable-line rule:line-length
167
- uses: astral-sh/setup-uv@803947b9bd8e9f986429fa0c5a41c367cd732b41 # v7.2.1
167
+ uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
168
168
  with:
169
169
  enable-cache: false
170
170
 
@@ -50,6 +50,9 @@ dependencies = [
50
50
 
51
51
  # Security: Fix CVE-2026-21441 (decompression-bomb vulnerability)
52
52
  "urllib3>=2.6.3",
53
+
54
+ # Security: Fix CVE-2026-26007 (SECT curve subgroup attack)
55
+ "cryptography>=46.0.5",
53
56
  ]
54
57
 
55
58
  [project.urls]
@@ -2739,6 +2739,113 @@ class Orchestrator:
2739
2739
  run_cmd(cmd, cwd=self.workspace)
2740
2740
  log.info("Checkout succeeded after full unshallow")
2741
2741
 
2742
+ def _merge_squash_with_unshallow_fallback(self, head_sha: str) -> None:
2743
+ """Perform git merge --squash with graduated deepening fallback.
2744
+
2745
+ If the initial merge fails because the shallow clone lacks a common
2746
+ ancestor between the base and head (causing "refusing to merge
2747
+ unrelated histories"), this method will:
2748
+ 1. First try to deepen the repository (fetch 100 more commits)
2749
+ 2. If that fails, fully unshallow the repository
2750
+ 3. Retry the merge after each attempt
2751
+
2752
+ This mirrors the graduated approach used by
2753
+ ``_checkout_with_unshallow_fallback`` but for merge operations,
2754
+ which are equally susceptible to shallow clone limitations.
2755
+
2756
+ Args:
2757
+ head_sha: The SHA to merge (PR head commit)
2758
+
2759
+ Raises:
2760
+ CommandError: If merge fails even after unshallowing, or if
2761
+ the failure is not related to shallow clone history
2762
+ """
2763
+ merge_cmd = ["git", "merge", "--squash", head_sha]
2764
+
2765
+ merge_exc: CommandError | None = None
2766
+ try:
2767
+ run_cmd(merge_cmd, cwd=self.workspace)
2768
+ except CommandError as exc:
2769
+ merge_exc = exc
2770
+
2771
+ if merge_exc is None:
2772
+ return # Success on first attempt
2773
+
2774
+ # Check if the failure is due to unrelated histories in a shallow clone
2775
+ error_str = str(merge_exc).lower()
2776
+ stderr_str = (merge_exc.stderr or "").lower()
2777
+ combined = f"{error_str} {stderr_str}"
2778
+
2779
+ is_unrelated_histories = (
2780
+ "refusing to merge unrelated histories" in combined
2781
+ or "unrelated histories" in combined
2782
+ or "no common ancestor" in combined
2783
+ )
2784
+
2785
+ if not is_unrelated_histories or not self._is_shallow_clone():
2786
+ # Not a shallow clone issue — let the caller handle the error
2787
+ raise merge_exc
2788
+
2789
+ log.warning(
2790
+ "Merge --squash failed due to unrelated histories in shallow "
2791
+ "clone. Attempting graduated deepening to recover..."
2792
+ )
2793
+
2794
+ # Step 1: Try deepening first (cheaper than full unshallow)
2795
+ if self._deepen_repository(depth=100):
2796
+ log.debug("Retrying merge --squash after deepening...")
2797
+ # Reset the failed merge state before retrying
2798
+ run_cmd(
2799
+ ["git", "merge", "--abort"],
2800
+ cwd=self.workspace,
2801
+ check=False,
2802
+ )
2803
+ try:
2804
+ run_cmd(merge_cmd, cwd=self.workspace)
2805
+ except CommandError as deepen_exc:
2806
+ log.debug(
2807
+ "Merge --squash still failed after deepening: %s",
2808
+ deepen_exc,
2809
+ )
2810
+ # Re-check whether this is still a shallow-history problem;
2811
+ # if the error has changed (e.g. real merge conflict), an
2812
+ # expensive full unshallow cannot help — propagate immediately.
2813
+ deepen_combined = (
2814
+ f"{deepen_exc} {deepen_exc.stderr or ''}".lower()
2815
+ )
2816
+ deepen_is_unrelated = (
2817
+ "refusing to merge unrelated histories" in deepen_combined
2818
+ or "unrelated histories" in deepen_combined
2819
+ or "no common ancestor" in deepen_combined
2820
+ )
2821
+ if not deepen_is_unrelated:
2822
+ raise
2823
+ else:
2824
+ log.info("Merge --squash succeeded after deepening repository")
2825
+ return
2826
+
2827
+ # Step 2: Full unshallow as last resort
2828
+ log.info(
2829
+ "Deepening insufficient, performing full unshallow for merge..."
2830
+ )
2831
+ # Reset the failed merge state before retrying
2832
+ run_cmd(
2833
+ ["git", "merge", "--abort"],
2834
+ cwd=self.workspace,
2835
+ check=False,
2836
+ )
2837
+ if not self._unshallow_repository():
2838
+ log.error(
2839
+ "Failed to unshallow repository. Cannot merge SHA: %s",
2840
+ head_sha,
2841
+ )
2842
+ raise merge_exc
2843
+
2844
+ # Retry the merge after full unshallow
2845
+ log.debug("Retrying merge --squash after full unshallow...")
2846
+ run_cmd(merge_cmd, cwd=self.workspace)
2847
+ log.info("Merge --squash succeeded after full unshallow")
2848
+
2742
2849
  def _cleanup_ssh(self) -> None:
2743
2850
  """Clean up temporary SSH files created by this tool.
2744
2851
 
@@ -3103,6 +3210,15 @@ class Orchestrator:
3103
3210
 
3104
3211
  except Exception as debug_exc:
3105
3212
  log.warning("Failed to analyze merge situation: %s", debug_exc)
3213
+ # Proactively deepen if merge-base fails in a shallow clone,
3214
+ # as this strongly indicates the shallow history is insufficient
3215
+ # for the upcoming merge --squash operation.
3216
+ if self._is_shallow_clone():
3217
+ log.info(
3218
+ "merge-base failed in shallow clone — proactively "
3219
+ "deepening repository to improve merge success chances"
3220
+ )
3221
+ self._deepen_repository(depth=100)
3106
3222
 
3107
3223
  self._checkout_with_unshallow_fallback(
3108
3224
  branch_name=tmp_branch,
@@ -3134,7 +3250,7 @@ class Orchestrator:
3134
3250
 
3135
3251
  log.debug("About to run: git merge --squash %s", head_sha)
3136
3252
  try:
3137
- run_cmd(["git", "merge", "--squash", head_sha], cwd=self.workspace)
3253
+ self._merge_squash_with_unshallow_fallback(head_sha)
3138
3254
  except CommandError as merge_exc:
3139
3255
  # Enhanced error handling for git merge failures
3140
3256
  error_details = self._analyze_merge_failure(
@@ -3308,7 +3424,7 @@ class Orchestrator:
3308
3424
  )
3309
3425
  if (
3310
3426
  not sync_all_prs
3311
- and gh.event_name == "pull_request_target"
3427
+ and gh.event_name in ("pull_request", "pull_request_target")
3312
3428
  and gh.event_action in ("reopened", "synchronize")
3313
3429
  ):
3314
3430
  try:
@@ -5402,8 +5518,9 @@ class Orchestrator:
5402
5518
  self,
5403
5519
  gh: GitHubContext,
5404
5520
  ) -> None:
5405
- """Close the PR if policy requires (pull_request_target events).
5521
+ """Close the PR if policy requires.
5406
5522
 
5523
+ Supports both ``pull_request`` and ``pull_request_target`` triggers.
5407
5524
  When PRESERVE_GITHUB_PRS is true, skip closing PRs (useful for testing).
5408
5525
  """
5409
5526
  # Respect PRESERVE_GITHUB_PRS to avoid closing PRs during tests
@@ -5414,9 +5531,9 @@ class Orchestrator:
5414
5531
  gh.pr_number,
5415
5532
  )
5416
5533
  return
5417
- # The current shell action closes PR on pull_request_target events.
5418
- if gh.event_name != "pull_request_target":
5419
- log.debug("Event is not pull_request_target; not closing PR")
5534
+ # Close PRs on pull_request or pull_request_target events.
5535
+ if gh.event_name not in ("pull_request", "pull_request_target"):
5536
+ log.debug("Event is not a pull_request event; not closing PR")
5420
5537
  return
5421
5538
  log.info("Closing PR #%s", gh.pr_number)
5422
5539
  try:
@@ -117,10 +117,15 @@ class GitHubContext:
117
117
  def get_operation_mode(self) -> PROperationMode:
118
118
  """Determine the operation mode based on event type and action.
119
119
 
120
+ Supports both ``pull_request`` and ``pull_request_target`` triggers.
121
+ Using ``pull_request`` is preferred for security (avoids granting
122
+ secrets to untrusted fork code), while ``pull_request_target`` is
123
+ accepted for backward compatibility.
124
+
120
125
  Returns:
121
126
  PROperationMode enum indicating the type of operation
122
127
  """
123
- if self.event_name != "pull_request_target":
128
+ if self.event_name not in ("pull_request", "pull_request_target"):
124
129
  return PROperationMode.UNKNOWN
125
130
 
126
131
  action = self.event_action.lower() if self.event_action else ""
@@ -44,27 +44,6 @@ _MULTIPLE_NEWLINES_PATTERN = re.compile(r"\n{3,}")
44
44
  _EMOJI_PATTERN = re.compile(r":[a-z_]+:") # GitHub emoji codes like :sparkles:
45
45
 
46
46
 
47
- @dataclass
48
- class FilterConfig:
49
- """Configuration for PR content filtering."""
50
-
51
- # Global options
52
- enabled: bool = True
53
- remove_emoji_codes: bool = True
54
- deduplicate_title_in_body: bool = True
55
-
56
- # Author-specific filtering
57
- author_rules: dict[str, str] = field(default_factory=dict)
58
-
59
- # Rule-specific configurations
60
- dependabot_config: DependabotConfig = field(
61
- default_factory=lambda: DependabotConfig()
62
- )
63
- precommit_config: PrecommitConfig = field(
64
- default_factory=lambda: PrecommitConfig()
65
- )
66
-
67
-
68
47
  @dataclass
69
48
  class DependabotConfig:
70
49
  """Configuration for Dependabot PR filtering."""
@@ -85,6 +64,25 @@ class PrecommitConfig:
85
64
  # Future: add pre-commit.ci specific options
86
65
 
87
66
 
67
+ @dataclass
68
+ class FilterConfig:
69
+ """Configuration for PR content filtering."""
70
+
71
+ # Global options
72
+ enabled: bool = True
73
+ remove_emoji_codes: bool = True
74
+ deduplicate_title_in_body: bool = True
75
+
76
+ # Author-specific filtering
77
+ author_rules: dict[str, str] = field(default_factory=dict)
78
+
79
+ # Rule-specific configurations
80
+ dependabot_config: DependabotConfig = field(
81
+ default_factory=DependabotConfig
82
+ )
83
+ precommit_config: PrecommitConfig = field(default_factory=PrecommitConfig)
84
+
85
+
88
86
  class FilterRule(ABC):
89
87
  """Abstract base class for PR content filtering rules."""
90
88
 
@@ -275,7 +275,7 @@ Change-Id: {reused_change_id}
275
275
  monkeypatch.setattr("github2gerrit.core.run_cmd", mock_run_cmd)
276
276
 
277
277
  # Mock GitHub API for Change-ID reuse
278
- monkeypatch.setattr("github2gerrit.core.build_client", lambda: object())
278
+ monkeypatch.setattr("github2gerrit.core.build_client", object)
279
279
  monkeypatch.setattr(
280
280
  "github2gerrit.core.get_repo_from_env", lambda _: object()
281
281
  )
@@ -232,7 +232,7 @@ def test_multi_pr_url_mode_writes_aggregated_outputs(
232
232
  def __init__(self, number: int) -> None:
233
233
  self.number = number
234
234
 
235
- monkeypatch.setattr(cli_mod, "build_client", lambda: object())
235
+ monkeypatch.setattr(cli_mod, "build_client", object)
236
236
  monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
237
237
  monkeypatch.setattr(
238
238
  cli_mod,
@@ -168,7 +168,7 @@ def test_repo_url_dry_run_invokes_for_each_open_pr(
168
168
  monkeypatch.setattr(cli_mod, "Orchestrator", _DummyOrchestrator)
169
169
 
170
170
  # Patch PyGithub wrapper functions used by CLI bulk path
171
- monkeypatch.setattr(cli_mod, "build_client", lambda: object())
171
+ monkeypatch.setattr(cli_mod, "build_client", object)
172
172
  monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
173
173
  monkeypatch.setattr(
174
174
  cli_mod, "iter_open_pulls", lambda _repo: iter(dummy_prs)
@@ -212,7 +212,7 @@ def test_url_mode_sets_environment_for_config_resolution(
212
212
  monkeypatch.setattr(cli_mod, "Orchestrator", _DummyOrchestrator)
213
213
 
214
214
  # Minimal patches for bulk flow
215
- monkeypatch.setattr(cli_mod, "build_client", lambda: object())
215
+ monkeypatch.setattr(cli_mod, "build_client", object)
216
216
  monkeypatch.setattr(cli_mod, "get_repo_from_env", lambda _client: object())
217
217
  monkeypatch.setattr(
218
218
  cli_mod,
@@ -58,16 +58,16 @@ def test_close_pr_skipped_when_preserve_github_prs_true(
58
58
  orch._close_pull_request_if_required(gh)
59
59
 
60
60
 
61
- def test_close_pr_not_invoked_for_non_target_event(
61
+ def test_close_pr_not_invoked_for_non_pr_event(
62
62
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
63
63
  ) -> None:
64
64
  # Ensure preservation is disabled so only event controls behavior
65
65
  monkeypatch.delenv("PRESERVE_GITHUB_PRS", raising=False)
66
66
 
67
- # Non-target event should not attempt to close
67
+ # Non-PR events (e.g. workflow_dispatch, push) should not attempt to close
68
68
  def _fail(*args: Any, **kwargs: Any) -> Any:
69
69
  raise AssertionError(
70
- "GitHub API should not be called for non-target events"
70
+ "GitHub API should not be called for non-PR events"
71
71
  )
72
72
 
73
73
  monkeypatch.setattr("github2gerrit.core.build_client", _fail)
@@ -76,15 +76,20 @@ def test_close_pr_not_invoked_for_non_target_event(
76
76
  monkeypatch.setattr("github2gerrit.core.close_pr", _fail)
77
77
 
78
78
  orch = Orchestrator(workspace=tmp_path)
79
- gh = _gh_ctx(event_name="pull_request", pr_number=42)
79
+ gh = _gh_ctx(event_name="workflow_dispatch", pr_number=42)
80
80
 
81
- # Act: should no-op
81
+ # Act: should no-op for non-PR events
82
82
  orch._close_pull_request_if_required(gh)
83
83
 
84
84
 
85
- def test_close_pr_invoked_for_pull_request_target_event(
85
+ def test_close_pr_invoked_for_pull_request_event(
86
86
  tmp_path: Path, monkeypatch: pytest.MonkeyPatch
87
87
  ) -> None:
88
+ """Verify that pull_request events (not just pull_request_target) close PRs.
89
+
90
+ The code now supports both ``pull_request`` and ``pull_request_target``
91
+ triggers. Using ``pull_request`` is preferred for security.
92
+ """
88
93
  # Ensure preservation is disabled so closing is allowed
89
94
  monkeypatch.delenv("PRESERVE_GITHUB_PRS", raising=False)
90
95
 
@@ -102,9 +107,56 @@ def test_close_pr_invoked_for_pull_request_target_event(
102
107
  self.closed_state: str | None = None
103
108
 
104
109
  # Patch the GitHub helper functions used by the close path
110
+ monkeypatch.setattr("github2gerrit.core.build_client", DummyClient)
105
111
  monkeypatch.setattr(
106
- "github2gerrit.core.build_client", lambda: DummyClient()
112
+ "github2gerrit.core.get_repo_from_env", lambda _c: DummyRepo()
107
113
  )
114
+
115
+ def _get_pull(_repo: DummyRepo, number: int) -> DummyPR:
116
+ calls["get_pull_number"] = number
117
+ return DummyPR(number)
118
+
119
+ def _close_pr(pr: DummyPR, *, comment: str | None = None) -> None:
120
+ calls["closed_pr_number"] = pr.number
121
+ calls["comment"] = comment
122
+ pr.closed_state = "closed"
123
+
124
+ monkeypatch.setattr("github2gerrit.core.get_pull", _get_pull)
125
+ monkeypatch.setattr("github2gerrit.core.close_pr", _close_pr)
126
+
127
+ orch = Orchestrator(workspace=tmp_path)
128
+ gh = _gh_ctx(event_name="pull_request", pr_number=42)
129
+
130
+ # Act: pull_request events should now close the PR
131
+ orch._close_pull_request_if_required(gh)
132
+
133
+ # Assert
134
+ assert calls.get("get_pull_number") == 42
135
+ assert calls.get("closed_pr_number") == 42
136
+ assert calls.get("comment") == "Auto-closing pull request"
137
+
138
+
139
+ def test_close_pr_invoked_for_pull_request_target_event(
140
+ tmp_path: Path, monkeypatch: pytest.MonkeyPatch
141
+ ) -> None:
142
+ # Ensure preservation is disabled so closing is allowed
143
+ monkeypatch.delenv("PRESERVE_GITHUB_PRS", raising=False)
144
+
145
+ calls: dict[str, Any] = {}
146
+
147
+ class DummyClient:
148
+ pass
149
+
150
+ class DummyRepo:
151
+ pass
152
+
153
+ class DummyPR:
154
+ def __init__(self, number: int) -> None:
155
+ self.number = number
156
+ self.closed_state: str | None = None
157
+
158
+ # Patch the GitHub helper functions used by the close path
159
+ monkeypatch.setattr("github2gerrit.core.build_client", DummyClient)
108
160
  monkeypatch.setattr(
109
161
  "github2gerrit.core.get_repo_from_env", lambda _c: DummyRepo()
110
162
  )
@@ -237,7 +237,7 @@ def test_prepare_squashed_commit_reuses_change_id_from_comments(
237
237
  monkeypatch.setattr("github2gerrit.core.run_cmd", fake_run_cmd)
238
238
 
239
239
  # GitHub API helpers used to fetch PR comments
240
- monkeypatch.setattr("github2gerrit.core.build_client", lambda: object())
240
+ monkeypatch.setattr("github2gerrit.core.build_client", object)
241
241
  monkeypatch.setattr(
242
242
  "github2gerrit.core.get_repo_from_env", lambda _c: object()
243
243
  )