github2gerrit 1.1.0__tar.gz → 1.2.1__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 (131) hide show
  1. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/.pre-commit-config.yaml +17 -12
  2. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/PKG-INFO +1 -1
  3. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/action.yaml +40 -3
  4. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/pyproject.toml +1 -13
  5. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/cli.py +33 -12
  6. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/commit_normalization.py +3 -2
  7. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/core.py +243 -26
  8. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/gerrit_pr_closer.py +231 -28
  9. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/gerrit_query.py +67 -2
  10. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/netrc.py +4 -6
  11. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/pr_content_filter.py +1 -1
  12. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/similarity.py +21 -6
  13. github2gerrit-1.2.1/tests/test_clean_squash_title.py +298 -0
  14. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_cli.py +49 -0
  15. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_commit_normalization.py +1 -1
  16. github2gerrit-1.2.1/tests/test_dependency_supersession.py +776 -0
  17. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/uv.lock +204 -183
  18. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/.editorconfig +0 -0
  19. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/.gitignore +0 -0
  20. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/.gitlint +0 -0
  21. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/.markdownlint.yaml +0 -0
  22. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/.readthedocs.yml +0 -0
  23. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/.yamllint +0 -0
  24. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/LICENSE +0 -0
  25. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/LICENSES/Apache-2.0.txt +0 -0
  26. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/README.md +0 -0
  27. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/REUSE.toml +0 -0
  28. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/docs/COMMIT_RULES.md +0 -0
  29. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  30. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  31. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/docs/RELEASE-v0.2.0.md +0 -0
  32. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/docs/github2gerrit_token_permissions_classic.png +0 -0
  33. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/sitecustomize.py +0 -0
  34. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/__init__.py +0 -0
  35. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/commit_rules.py +0 -0
  36. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/config.py +0 -0
  37. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/constants.py +0 -0
  38. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/duplicate_detection.py +0 -0
  39. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/error_codes.py +0 -0
  40. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/external_api.py +0 -0
  41. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/gerrit_rest.py +0 -0
  42. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/gerrit_urls.py +0 -0
  43. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/github_api.py +0 -0
  44. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/gitreview.py +0 -0
  45. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/gitutils.py +0 -0
  46. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/mapping_comment.py +0 -0
  47. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/models.py +0 -0
  48. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/orchestrator/__init__.py +0 -0
  49. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  50. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/pr_commands.py +0 -0
  51. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/reconcile_matcher.py +0 -0
  52. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/rich_display.py +0 -0
  53. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/rich_logging.py +0 -0
  54. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/ssh_agent_setup.py +0 -0
  55. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/ssh_common.py +0 -0
  56. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/ssh_config_parser.py +0 -0
  57. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/ssh_discovery.py +0 -0
  58. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/trailers.py +0 -0
  59. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/src/github2gerrit/utils.py +0 -0
  60. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/conftest.py +0 -0
  61. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/fixtures/__init__.py +0 -0
  62. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/fixtures/make_repo.py +0 -0
  63. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/fixtures/ssh_config_samples.py +0 -0
  64. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_action_environment_mapping.py +0 -0
  65. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_action_outputs.py +0 -0
  66. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_action_pr_number_handling.py +0 -0
  67. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_action_step_validation.py +0 -0
  68. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_automation_only.py +0 -0
  69. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_change_id_deduplication.py +0 -0
  70. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_cli_helpers.py +0 -0
  71. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_cli_netrc_options.py +0 -0
  72. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_cli_outputs_file.py +0 -0
  73. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_cli_url_and_dryrun.py +0 -0
  74. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_commit_rules.py +0 -0
  75. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_composite_action_coverage.py +0 -0
  76. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_config_and_reviewers.py +0 -0
  77. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_config_helpers.py +0 -0
  78. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_close_pr_policy.py +0 -0
  79. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_config_and_errors.py +0 -0
  80. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_gerrit_backref_comment.py +0 -0
  81. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_gerrit_push_errors.py +0 -0
  82. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_gerrit_rest_results.py +0 -0
  83. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_integration_fixture_repo.py +0 -0
  84. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_prepare_commits.py +0 -0
  85. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_shallow_clone.py +0 -0
  86. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_ssh_setup.py +0 -0
  87. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_core_ssrf_protection.py +0 -0
  88. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_dns_validation_and_no_gerrit.py +0 -0
  89. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_duplicate_detection.py +0 -0
  90. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_email_case_normalization.py +0 -0
  91. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_error_codes.py +0 -0
  92. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_external_api_framework.py +0 -0
  93. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_force_flag_cli.py +0 -0
  94. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_gerrit_change_id_footer.py +0 -0
  95. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_gerrit_change_status_checks.py +0 -0
  96. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_gerrit_pr_closer.py +0 -0
  97. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_gerrit_rest_client.py +0 -0
  98. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_gerrit_urls.py +0 -0
  99. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_gerrit_urls_more.py +0 -0
  100. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_ghe_and_gitreview_args.py +0 -0
  101. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_github_api_error_handling.py +0 -0
  102. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_github_api_helpers.py +0 -0
  103. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_github_api_retry_and_helpers.py +0 -0
  104. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_gitreview.py +0 -0
  105. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_gitutils_helpers.py +0 -0
  106. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_issue_157_regressions.py +0 -0
  107. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_mapping_comment_additional.py +0 -0
  108. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  109. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_metadata_and_reconciliation.py +0 -0
  110. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_metadata_trailer_separation_bug.py +0 -0
  111. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_misc_small_coverage.py +0 -0
  112. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_netrc.py +0 -0
  113. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_orphan_rest_side_effects.py +0 -0
  114. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_pr_commands.py +0 -0
  115. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_pr_content_filter.py +0 -0
  116. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_pr_content_filter_integration.py +0 -0
  117. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_pr_update_detection.py +0 -0
  118. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_reconciliation_extracted_module.py +0 -0
  119. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  120. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_reconciliation_scenarios.py +0 -0
  121. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_ssh_agent.py +0 -0
  122. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_ssh_agent_ownership.py +0 -0
  123. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_ssh_artifact_prevention.py +0 -0
  124. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_ssh_common.py +0 -0
  125. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_ssh_discovery.py +0 -0
  126. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_ssh_discovery_dry_run.py +0 -0
  127. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_trailers_additional.py +0 -0
  128. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_url_parser.py +0 -0
  129. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/test_utils.py +0 -0
  130. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/unit/test_config_integration.py +0 -0
  131. {github2gerrit-1.1.0 → github2gerrit-1.2.1}/tests/unit/test_ssh_config_parser.py +0 -0
@@ -3,7 +3,7 @@
3
3
  # SPDX-FileCopyrightText: 2025 The Linux Foundation
4
4
 
5
5
  ci:
6
- skip: [pytest]
6
+ skip: [pytest, gha-workflow-linter]
7
7
  autofix_commit_msg: |
8
8
  Chore: pre-commit autofixes
9
9
 
@@ -59,7 +59,7 @@ repos:
59
59
  types: [yaml]
60
60
 
61
61
  - repo: https://github.com/astral-sh/ruff-pre-commit
62
- rev: b969e2851312ca2b24bbec879ba4954341d1bd12 # frozen: v0.15.5
62
+ rev: b831c3dc5d27d9da294ae4e915773b99aa24a7c5 # frozen: v0.15.10
63
63
  hooks:
64
64
  - id: ruff
65
65
  files: ^(src|scripts|tests)/.+\.py$
@@ -68,7 +68,7 @@ repos:
68
68
  files: ^(src|scripts|tests)/.+\.py$
69
69
 
70
70
  - repo: https://github.com/pre-commit/mirrors-mypy
71
- rev: a66e98df7b4aeeb3724184b332785976d062b92e # frozen: v1.19.1
71
+ rev: 0f369d245750787ce34997d464ed9605391a5283 # frozen: v1.20.1
72
72
  hooks:
73
73
  - id: mypy
74
74
  files: ^src/.+\.py$
@@ -76,13 +76,6 @@ repos:
76
76
  - types-PyYAML
77
77
  - types-requests
78
78
 
79
- # Doesn't seem to run reliably inside pre-commit.ci
80
- # - repo: https://github.com/RobertCraigie/pyright-python
81
- # rev: d393df1703a808473b84bd14a2702f4793014031 # frozen: v1.1.404
82
- # hooks:
83
- # - id: pyright
84
- # files: ^(src|scripts|tests)/.+\.py$
85
-
86
79
  - repo: https://github.com/btford/write-good
87
80
  rev: ab66ce10136dfad5146e69e70f82a3efac8842c1 # frozen: v1.0.8
88
81
  hooks:
@@ -110,7 +103,7 @@ repos:
110
103
  # Replaces: https://github.com/rhysd/actionlint
111
104
  # Permits actionlint to run both locally and with precommit.ci/GitHub
112
105
  - repo: https://github.com/Mateusz-Grzelinski/actionlint-py
113
- rev: 694e2c0dfb4253d51f3c6c54b8f9fec0a16764dc # frozen: v1.7.11.24
106
+ rev: c04ed26e40637cab1aa9879c693832a9c120fb20 # frozen: v1.7.12.24
114
107
  hooks:
115
108
  - id: actionlint
116
109
 
@@ -120,8 +113,20 @@ repos:
120
113
  hooks:
121
114
  - id: codespell
122
115
 
116
+ # Requires a mirror, primary repo lacks .pre-commit-hooks.yaml
117
+ - repo: https://github.com/DetachHead/basedpyright-prek-mirror
118
+ rev: 7664ed7e31234c8369d85ee9a13a1ca3361c0aa1 # frozen: 1.39.0
119
+ hooks:
120
+ - id: basedpyright
121
+ files: ^src/.+\.py$
122
+
123
+ - repo: https://github.com/lfreleng-actions/gha-workflow-linter
124
+ rev: a7caf8f3a1a05688d1cee46615ff94def617e5a3 # frozen: v1.0.2
125
+ hooks:
126
+ - id: gha-workflow-linter
127
+
123
128
  - repo: https://github.com/python-jsonschema/check-jsonschema
124
- rev: 8db279a37c552206d2df62269ff6f9d31125815a # frozen: 0.37.0
129
+ rev: ed81924a8b1cecdaa570b072528fa80c9c4d6ccd # frozen: 0.37.1
125
130
  hooks:
126
131
  - id: check-github-actions
127
132
  - id: check-github-workflows
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: github2gerrit
3
- Version: 1.1.0
3
+ Version: 1.2.1
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
@@ -157,6 +157,10 @@ inputs:
157
157
  description: "Create a Gerrit change when an UPDATE operation cannot find an existing one"
158
158
  required: false
159
159
  default: "false"
160
+ G2G_DISABLED:
161
+ description: "Set to 'true' to disable the action entirely (exits successfully with a message)"
162
+ required: false
163
+ default: ""
160
164
 
161
165
  outputs:
162
166
  gerrit_change_request_url:
@@ -172,7 +176,34 @@ outputs:
172
176
  runs:
173
177
  using: "composite"
174
178
  steps:
179
+ - name: "Check if GitHub2Gerrit is disabled"
180
+ id: disabled-check
181
+ shell: bash
182
+ run: |
183
+ # Check if GitHub2Gerrit is disabled
184
+ set -euo pipefail
185
+ DISABLED_ENV="${G2G_DISABLED:-}"
186
+ DISABLED_INPUT="${{ inputs.G2G_DISABLED }}"
187
+ # Normalize: accept the same truthy set as env_bool()
188
+ # (1/true/yes/on, case-insensitive, trimmed)
189
+ _normalize() {
190
+ printf '%s' "$1" | tr '[:upper:]' '[:lower:]' | xargs
191
+ }
192
+ _is_truthy() {
193
+ case "$(_normalize "$1")" in
194
+ 1|true|yes|on) return 0 ;;
195
+ *) return 1 ;;
196
+ esac
197
+ }
198
+ if _is_truthy "${DISABLED_ENV}" || _is_truthy "${DISABLED_INPUT}"; then
199
+ echo "🛑 GitHub2Gerrit is disabled by check of G2G_DISABLED variable or input"
200
+ echo "disabled=true" >> "$GITHUB_OUTPUT"
201
+ else
202
+ echo "disabled=false" >> "$GITHUB_OUTPUT"
203
+ fi
204
+
175
205
  - name: "Checkout repository"
206
+ if: steps.disabled-check.outputs.disabled != 'true'
176
207
  # yamllint disable-line rule:line-length
177
208
  uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2
178
209
  with:
@@ -181,18 +212,21 @@ runs:
181
212
  ref: ${{ github.event.pull_request.head.sha || github.sha }}
182
213
 
183
214
  - name: "Setup Python"
215
+ if: steps.disabled-check.outputs.disabled != 'true'
184
216
  # yamllint disable-line rule:line-length
185
217
  uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
186
218
  with:
187
219
  python-version-file: '${{ github.action_path }}/pyproject.toml'
188
220
 
189
221
  - name: "Setup uv"
222
+ if: steps.disabled-check.outputs.disabled != 'true'
190
223
  # yamllint disable-line rule:line-length
191
- uses: astral-sh/setup-uv@37802adc94f370d6bfd71619e3f0bf239e1f3b78 # v7.6.0
224
+ uses: astral-sh/setup-uv@cec208311dfd045dd5311c1add060b2062131d57 # v8.0.0
192
225
  with:
193
226
  enable-cache: false
194
227
 
195
228
  - name: "Setup github2gerrit"
229
+ if: steps.disabled-check.outputs.disabled != 'true'
196
230
  shell: bash
197
231
  env:
198
232
  # Provide version for hatch-vcs (via setuptools-scm)
@@ -214,7 +248,7 @@ runs:
214
248
 
215
249
  - name: "Validate PR_NUMBER usage"
216
250
  # yamllint disable-line rule:line-length
217
- if: ${{ github.event_name != 'workflow_dispatch' && inputs.PR_NUMBER != '' && inputs.PR_NUMBER != '0' }}
251
+ if: ${{ steps.disabled-check.outputs.disabled != 'true' && github.event_name != 'workflow_dispatch' && inputs.PR_NUMBER != '' && inputs.PR_NUMBER != '0' }}
218
252
  shell: bash
219
253
  run: |
220
254
  # Validate PR_NUMBER usage
@@ -223,7 +257,7 @@ runs:
223
257
  exit 2
224
258
 
225
259
  - name: "Normalize PR_NUMBER"
226
- if: ${{ github.event_name == 'workflow_dispatch' }}
260
+ if: ${{ steps.disabled-check.outputs.disabled != 'true' && github.event_name == 'workflow_dispatch' }}
227
261
  shell: bash
228
262
  run: |
229
263
  # Normalize PR_NUMBER
@@ -243,6 +277,7 @@ runs:
243
277
  fi
244
278
 
245
279
  - name: "Extract PR number, validate context"
280
+ if: steps.disabled-check.outputs.disabled != 'true'
246
281
  shell: bash
247
282
  run: |
248
283
  # Extract PR number, validate context
@@ -284,6 +319,7 @@ runs:
284
319
  fi
285
320
 
286
321
  - name: "Run github2gerrit Python CLI"
322
+ if: steps.disabled-check.outputs.disabled != 'true'
287
323
  id: run-cli
288
324
  shell: bash
289
325
  env:
@@ -357,6 +393,7 @@ runs:
357
393
  fi
358
394
 
359
395
  - name: "Capture outputs"
396
+ if: steps.disabled-check.outputs.disabled != 'true'
360
397
  id: capture-outputs
361
398
  shell: bash
362
399
  # yamllint disable rule:line-length
@@ -235,19 +235,7 @@ markers = [
235
235
 
236
236
  [tool.pyright]
237
237
  pythonVersion = "3.11"
238
- include = ["src", "tests"]
239
- exclude = [
240
- "build/",
241
- "dist/",
242
- "docs/",
243
- "scripts/",
244
- "tests/fixtures/",
245
- ".venv/",
246
- ".mypy_cache/",
247
- ".pytest_cache/",
248
- ".ruff_cache/",
249
- "coverage_html_report/",
250
- ]
238
+ include = ["src"]
251
239
  typeCheckingMode = "strict"
252
240
  reportMissingImports = "none"
253
241
  reportMissingTypeStubs = "none"
@@ -845,6 +845,14 @@ def main(
845
845
  typer.echo("Version information not available")
846
846
  sys.exit(int(ExitCode.SUCCESS))
847
847
 
848
+ # Check if GitHub2Gerrit is disabled via environment variable
849
+ if env_bool("G2G_DISABLED"):
850
+ typer.echo(
851
+ "\U0001f6d1 GitHub2Gerrit is disabled by check of "
852
+ "G2G_DISABLED variable"
853
+ )
854
+ sys.exit(int(ExitCode.SUCCESS))
855
+
848
856
  # Override boolean parameters with properly parsed environment variables.
849
857
  # This ensures that string "false" from GitHub Actions is handled
850
858
  # correctly (Typer/Click treats any non-empty string as truthy).
@@ -2218,7 +2226,7 @@ def _process() -> None:
2218
2226
  ):
2219
2227
  try:
2220
2228
  log.debug(
2221
- "🔍 Checking for Gerrit change to abandon for PR #%s",
2229
+ "Checking for Gerrit change to abandon for PR #%s",
2222
2230
  gh.pr_number,
2223
2231
  )
2224
2232
  change_number = abandon_gerrit_change_for_closed_pr(
@@ -2230,16 +2238,29 @@ def _process() -> None:
2230
2238
  progress_tracker=None,
2231
2239
  )
2232
2240
  if change_number:
2233
- gerrit_change_url = (
2234
- f"https://{data.gerrit_server}/c/"
2235
- f"{data.gerrit_project}/+/{change_number}"
2236
- )
2237
- log.debug(
2238
- "✅ Successfully abandoned Gerrit change %s "
2239
- "for pull request #%s",
2240
- gerrit_change_url,
2241
- gh.pr_number,
2242
- )
2241
+ try:
2242
+ from .gerrit_urls import create_gerrit_url_builder
2243
+
2244
+ _url_builder = create_gerrit_url_builder(
2245
+ data.gerrit_server
2246
+ )
2247
+ gerrit_change_url = _url_builder.change_url(
2248
+ data.gerrit_project,
2249
+ int(change_number),
2250
+ )
2251
+ log.debug(
2252
+ "Successfully abandoned Gerrit "
2253
+ "change %s for pull request #%s",
2254
+ gerrit_change_url,
2255
+ gh.pr_number,
2256
+ )
2257
+ except Exception:
2258
+ log.debug(
2259
+ "Successfully abandoned Gerrit "
2260
+ "change %s for pull request #%s",
2261
+ change_number,
2262
+ gh.pr_number,
2263
+ )
2243
2264
  # Console output already done by
2244
2265
  # abandon_gerrit_change_for_closed_pr
2245
2266
  else:
@@ -2287,7 +2308,7 @@ def _process() -> None:
2287
2308
  log.warning("Gerrit cleanup failed: %s", exc)
2288
2309
 
2289
2310
  log.debug(
2290
- "Cleanup operations completed for closed PR #%s",
2311
+ "Cleanup operations completed for closed PR #%s",
2291
2312
  gh.pr_number or "unknown",
2292
2313
  )
2293
2314
  return
@@ -401,8 +401,9 @@ class CommitNormalizer:
401
401
  # Remove trailing ellipsis
402
402
  title = re.sub(r"\s*[.]{3,}.*$", "", title)
403
403
 
404
- # Remove markdown formatting
405
- title = re.sub(r"[*_`]", "", title)
404
+ # Remove markdown bold/code formatting but preserve underscores
405
+ # (which appear in package names and filesystem paths).
406
+ title = re.sub(r"[*`]", "", title)
406
407
 
407
408
  # For dependabot titles, extract the essential information
408
409
  for pattern in DEPENDABOT_PATTERNS:
@@ -154,6 +154,93 @@ def _clean_ellipses_from_message(message: str) -> str:
154
154
  return "\n".join(cleaned_lines)
155
155
 
156
156
 
157
+ def _clean_squash_title_line(title_line: str | None) -> str:
158
+ """Clean and truncate a squashed commit title line.
159
+
160
+ Handles markdown removal, separator splitting, and length
161
+ truncation while preserving conventional commit prefixes
162
+ and underscores in package/path names.
163
+
164
+ Args:
165
+ title_line: Raw title line from git log output.
166
+
167
+ Returns:
168
+ Cleaned title line, safe for use as a commit subject.
169
+ """
170
+ from .similarity import CC_PREFIX_RE
171
+
172
+ if not title_line:
173
+ return ""
174
+
175
+ # Remove markdown links
176
+ title_line = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", title_line)
177
+ # Remove trailing ellipsis/truncation
178
+ title_line = re.sub(r"\s*[.]{3,}.*$", "", title_line)
179
+ # Split on common separators to avoid leaking body content
180
+ for separator in [". Bumps ", " Bumps ", ". - ", " - "]:
181
+ if separator in title_line:
182
+ title_line = title_line.split(separator)[0].strip()
183
+ break
184
+ # Remove markdown bold/code formatting but preserve underscores
185
+ # (which appear in package names and filesystem paths).
186
+ title_line = re.sub(r"[*`]", "", title_line).strip()
187
+
188
+ if len(title_line) > 100:
189
+ # Detect conventional commit prefix length so that the
190
+ # ": " break-point does not split on the prefix separator
191
+ # (e.g. "Build(deps): " should not be treated as a sentence
192
+ # break).
193
+ cc_match = CC_PREFIX_RE.match(title_line)
194
+ cc_prefix_len = cc_match.end() if cc_match else 0
195
+
196
+ break_points = [". ", "! ", "? ", " - ", ": "]
197
+ max_bp_len = max(len(bp) for bp in break_points)
198
+ truncated = False
199
+ for bp in break_points:
200
+ # For the ": " break-point, start searching after the
201
+ # conventional commit prefix to avoid splitting there.
202
+ search_start = cc_prefix_len if bp == ": " else 0
203
+ # Extend the slice by (max_bp_len - 1) so that a
204
+ # break-point starting just before position 100 is
205
+ # still detected even if it spans across the boundary.
206
+ candidate_end = min(len(title_line), 100 + max_bp_len - 1)
207
+ candidate = title_line[search_start:candidate_end]
208
+ bp_offset = candidate.find(bp)
209
+ if bp_offset != -1:
210
+ bp_idx = search_start + bp_offset
211
+ # Only use this break-point if it starts within
212
+ # the 100-char limit.
213
+ if bp_idx >= 100:
214
+ continue
215
+ # Punctuation break-points (". ", ": ") — include
216
+ # the punctuation mark. Separator break-points
217
+ # (" - ") — truncate before the separator.
218
+ if bp[0].isspace():
219
+ title_line = title_line[:bp_idx].rstrip()
220
+ else:
221
+ title_line = title_line[
222
+ : bp_idx + len(bp.rstrip())
223
+ ].rstrip()
224
+ truncated = True
225
+ break
226
+
227
+ if not truncated and cc_prefix_len == 0:
228
+ # Non-CC title with no break-point found: fall back
229
+ # to word-boundary truncation at 100 characters.
230
+ words = title_line[:100].split()
231
+ title_line = (
232
+ " ".join(words[:-1])
233
+ if len(words) > 1
234
+ else title_line[:100].rstrip()
235
+ )
236
+ # For CC titles with no break-point: pass through the
237
+ # full title. The length is inherent to the structured
238
+ # subject (e.g. long dependency paths), not body-content
239
+ # leakage.
240
+
241
+ return title_line
242
+
243
+
157
244
  # ---------------------
158
245
  # Utility functions
159
246
  # ---------------------
@@ -678,6 +765,9 @@ class Orchestrator:
678
765
  2. GitHub-Hash trailer matching
679
766
  3. GitHub-PR trailer URL matching
680
767
  4. Mapping comment parsing from PR comments
768
+ 5. Dependency package match — find an open change that
769
+ bumps the same dependency (for Dependabot / Renovate
770
+ supersession).
681
771
 
682
772
  Args:
683
773
  gh: GitHub context containing PR information
@@ -793,6 +883,10 @@ class Orchestrator:
793
883
  except Exception as exc:
794
884
  log.debug("GitHub-Hash trailer query failed: %s", exc)
795
885
 
886
+ # Cache the PR title for reuse across strategies 4 and 5
887
+ # so we don't duplicate GitHub API requests.
888
+ cached_pr_title: str = ""
889
+
796
890
  # Strategy 4: Parse mapping comments from PR
797
891
  try:
798
892
  from .mapping_comment import parse_mapping_comments
@@ -800,6 +894,7 @@ class Orchestrator:
800
894
  client_gh = build_client()
801
895
  repo = get_repo_from_env(client_gh)
802
896
  pr_obj = get_pull(repo, int(gh.pr_number))
897
+ cached_pr_title = getattr(pr_obj, "title", "") or ""
803
898
 
804
899
  issue = pr_obj.as_issue()
805
900
  comments = list(issue.get_comments())
@@ -830,6 +925,110 @@ class Orchestrator:
830
925
  except Exception as exc:
831
926
  log.debug("Mapping comment parsing failed: %s", exc)
832
927
 
928
+ # Strategy 5: Dependency package match (supersession)
929
+ # When a new Dependabot/Renovate PR bumps the same dependency
930
+ # as an existing open Gerrit change, reuse that Change-Id so
931
+ # the push creates a new patchset instead of a duplicate change.
932
+ try:
933
+ from .gerrit_query import GerritChange
934
+ from .gerrit_query import query_open_changes_by_project
935
+ from .gerrit_rest import build_client_for_host
936
+ from .similarity import extract_dependency_package_from_subject
937
+ from .trailers import GITHUB_PR_TRAILER
938
+ from .trailers import parse_trailers
939
+
940
+ # Reuse PR title cached by Strategy 4 to avoid a
941
+ # duplicate GitHub API request.
942
+ pr_title = cached_pr_title
943
+ if not pr_title:
944
+ log.debug(
945
+ "Strategy 5: PR title cache miss, fetching from GitHub API",
946
+ )
947
+ try:
948
+ gh_client = build_client()
949
+ gh_repo = get_repo_from_env(gh_client)
950
+ pr_obj = get_pull(gh_repo, int(gh.pr_number))
951
+ pr_title = getattr(pr_obj, "title", "") or ""
952
+ except Exception:
953
+ pr_title = ""
954
+
955
+ current_pkg = extract_dependency_package_from_subject(pr_title)
956
+ if current_pkg:
957
+ log.debug(
958
+ "Strategy 5: searching for open changes that "
959
+ "bump dependency '%s'",
960
+ current_pkg,
961
+ )
962
+ dep_client = build_client_for_host(gerrit.host)
963
+ open_changes = query_open_changes_by_project(
964
+ dep_client,
965
+ gerrit.project,
966
+ branch=gh.base_ref,
967
+ max_results=200,
968
+ )
969
+
970
+ # Collect all matching changes, then select the
971
+ # oldest one (lowest change number) to avoid
972
+ # "downgrading" a newer change by uploading an
973
+ # older patchset to it.
974
+ candidates: list[tuple[int, GerritChange]] = []
975
+ for change in open_changes:
976
+ candidate_pkg = extract_dependency_package_from_subject(
977
+ change.subject
978
+ )
979
+ if candidate_pkg and candidate_pkg == current_pkg:
980
+ # Verify this is a GitHub2Gerrit change
981
+ commit_msg = change.commit_message or ""
982
+ trailers = parse_trailers(commit_msg)
983
+ if GITHUB_PR_TRAILER not in trailers:
984
+ log.debug(
985
+ "Strategy 5: skipping change %s "
986
+ "(no GitHub2Gerrit metadata)",
987
+ change.number,
988
+ )
989
+ continue
990
+ try:
991
+ change_num = int(change.number)
992
+ except (TypeError, ValueError):
993
+ log.debug(
994
+ "Strategy 5: skipping change with "
995
+ "invalid number %r for subject %r",
996
+ change.number,
997
+ change.subject,
998
+ )
999
+ continue
1000
+ candidates.append((change_num, change))
1001
+
1002
+ if candidates:
1003
+ # Prefer the oldest open change so the newest
1004
+ # PR always updates the original change and
1005
+ # the post-push sweep abandons the rest.
1006
+ candidates.sort(key=lambda t: t[0])
1007
+ _, oldest = candidates[0]
1008
+ change_ids = [oldest.change_id]
1009
+ log.info(
1010
+ "Found superseding target by dependency "
1011
+ "package '%s': change %s (%s) "
1012
+ "(oldest of %d candidate(s))",
1013
+ current_pkg,
1014
+ oldest.number,
1015
+ oldest.subject,
1016
+ len(candidates),
1017
+ )
1018
+ return change_ids
1019
+
1020
+ log.debug(
1021
+ "No open changes found for dependency '%s'",
1022
+ current_pkg,
1023
+ )
1024
+ else:
1025
+ log.debug(
1026
+ "Strategy 5 skipped: could not extract dependency "
1027
+ "package from PR title"
1028
+ )
1029
+ except Exception as exc:
1030
+ log.debug("Dependency package strategy failed: %s", exc)
1031
+
833
1032
  log.warning(
834
1033
  "⚠️ No existing Gerrit changes found for PR #%s",
835
1034
  gh.pr_number,
@@ -1936,6 +2135,49 @@ class Orchestrator:
1936
2135
  # Validate that no unexpected files were committed
1937
2136
  self._validate_committed_files(gh, result)
1938
2137
 
2138
+ # Post-push supersession sweep (Option A fallback).
2139
+ # After a successful push, check whether other open Gerrit
2140
+ # changes in the same project bump the same dependency
2141
+ # package. If Strategy 5 already reused the old Change-Id
2142
+ # (update-in-place), no duplicates should exist. If that
2143
+ # path was skipped (e.g. non-dependency PR, or the query
2144
+ # failed), this sweep catches and abandons stale changes.
2145
+ if not inputs.dry_run and gerrit and prep.change_ids:
2146
+ try:
2147
+ from .gerrit_pr_closer import (
2148
+ abandon_superseded_dependency_changes,
2149
+ )
2150
+
2151
+ # Derive the subject from the pushed commit,
2152
+ # regardless of whether change URL lookup
2153
+ # succeeded.
2154
+ push_subject = ""
2155
+ try:
2156
+ push_subject = run_cmd(
2157
+ [
2158
+ "git",
2159
+ "show",
2160
+ "-s",
2161
+ "--pretty=format:%s",
2162
+ "HEAD",
2163
+ ],
2164
+ cwd=self.workspace,
2165
+ ).stdout.strip()
2166
+ except Exception:
2167
+ push_subject = ""
2168
+
2169
+ if push_subject:
2170
+ abandon_superseded_dependency_changes(
2171
+ gerrit_server=gerrit.host,
2172
+ gerrit_project=gerrit.project,
2173
+ current_subject=push_subject,
2174
+ exclude_change_ids=prep.change_ids,
2175
+ dry_run=False,
2176
+ target_branch=self._resolve_target_branch(),
2177
+ )
2178
+ except Exception as exc:
2179
+ log.debug("Post-push supersession sweep skipped: %s", exc)
2180
+
1939
2181
  self._close_pull_request_if_required(gh)
1940
2182
 
1941
2183
  log.debug("Pipeline complete: %s", result)
@@ -3564,32 +3806,7 @@ class Orchestrator:
3564
3806
  return message_lines, signed_off, change_ids
3565
3807
 
3566
3808
  def _clean_title_line(title_line: str) -> str:
3567
- # Remove markdown links
3568
- title_line = re.sub(r"\[([^\]]+)\]\([^)]+\)", r"\1", title_line)
3569
- # Remove trailing ellipsis/truncation
3570
- title_line = re.sub(r"\s*[.]{3,}.*$", "", title_line)
3571
- # Split on common separators to avoid leaking body content
3572
- for separator in [". Bumps ", " Bumps ", ". - ", " - "]:
3573
- if separator in title_line:
3574
- title_line = title_line.split(separator)[0].strip()
3575
- break
3576
- # Remove simple markdown/formatting artifacts
3577
- title_line = re.sub(r"[*_`]", "", title_line).strip()
3578
- if len(title_line) > 100:
3579
- break_points = [". ", "! ", "? ", " - ", ": "]
3580
- for bp in break_points:
3581
- if bp in title_line[:100]:
3582
- title_line = title_line[
3583
- : title_line.index(bp) + len(bp.strip())
3584
- ]
3585
- break
3586
- else:
3587
- words = title_line[:100].split()
3588
- title_line = (
3589
- " ".join(words[:-1])
3590
- if len(words) > 1
3591
- else title_line[:100].rstrip()
3592
- )
3809
+ title_line = _clean_squash_title_line(title_line)
3593
3810
 
3594
3811
  # Apply conventional commit normalization if enabled
3595
3812
  if inputs.normalise_commit and gh.pr_number: