github2gerrit 1.0.7__tar.gz → 1.0.9__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 (128) hide show
  1. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/.pre-commit-config.yaml +4 -4
  2. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/PKG-INFO +119 -3
  3. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/README.md +118 -1
  4. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/action.yaml +15 -1
  5. github2gerrit-1.0.9/docs/COMMIT_RULES.md +255 -0
  6. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/pyproject.toml +0 -2
  7. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/cli.py +99 -52
  8. github2gerrit-1.0.9/src/github2gerrit/commit_rules.py +518 -0
  9. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/config.py +44 -3
  10. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/core.py +368 -189
  11. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/duplicate_detection.py +26 -60
  12. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/error_codes.py +15 -11
  13. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/gerrit_pr_closer.py +13 -10
  14. github2gerrit-1.0.9/src/github2gerrit/gitreview.py +592 -0
  15. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/models.py +4 -0
  16. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/orchestrator/reconciliation.py +6 -12
  17. github2gerrit-1.0.9/src/github2gerrit/pr_commands.py +355 -0
  18. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/rich_display.py +12 -13
  19. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/rich_logging.py +2 -7
  20. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/ssh_discovery.py +12 -12
  21. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/conftest.py +4 -2
  22. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_change_id_deduplication.py +1 -0
  23. github2gerrit-1.0.9/tests/test_commit_rules.py +1029 -0
  24. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_config_and_errors.py +1 -0
  25. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_integration_fixture_repo.py +2 -0
  26. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_prepare_commits.py +1 -0
  27. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_ssh_setup.py +5 -0
  28. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_force_flag_cli.py +1 -0
  29. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_gerrit_change_id_footer.py +1 -0
  30. github2gerrit-1.0.9/tests/test_gitreview.py +1011 -0
  31. github2gerrit-1.0.9/tests/test_issue_157_regressions.py +512 -0
  32. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_metadata_trailer_separation_bug.py +1 -0
  33. github2gerrit-1.0.9/tests/test_pr_commands.py +959 -0
  34. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_pr_content_filter_integration.py +1 -0
  35. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_pr_update_detection.py +2 -0
  36. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/uv.lock +24 -35
  37. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/.editorconfig +0 -0
  38. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/.gitignore +0 -0
  39. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/.gitlint +0 -0
  40. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/.markdownlint.yaml +0 -0
  41. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/.readthedocs.yml +0 -0
  42. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/.yamllint +0 -0
  43. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/LICENSE +0 -0
  44. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/LICENSES/Apache-2.0.txt +0 -0
  45. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/REUSE.toml +0 -0
  46. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/docs/COMPOSITE_ACTION_TESTING.md +0 -0
  47. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/docs/PR_UPDATE_IMPLEMENTATION.md +0 -0
  48. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/docs/RELEASE-v0.2.0.md +0 -0
  49. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/docs/github2gerrit_token_permissions_classic.png +0 -0
  50. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/sitecustomize.py +0 -0
  51. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/__init__.py +0 -0
  52. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/commit_normalization.py +0 -0
  53. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/constants.py +0 -0
  54. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/external_api.py +0 -0
  55. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/gerrit_query.py +0 -0
  56. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/gerrit_rest.py +0 -0
  57. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/gerrit_urls.py +0 -0
  58. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/github_api.py +0 -0
  59. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/gitutils.py +0 -0
  60. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/mapping_comment.py +0 -0
  61. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/netrc.py +0 -0
  62. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/orchestrator/__init__.py +0 -0
  63. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/pr_content_filter.py +0 -0
  64. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/reconcile_matcher.py +0 -0
  65. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/similarity.py +0 -0
  66. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/ssh_agent_setup.py +0 -0
  67. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/ssh_common.py +0 -0
  68. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/ssh_config_parser.py +0 -0
  69. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/trailers.py +0 -0
  70. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/src/github2gerrit/utils.py +0 -0
  71. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/fixtures/__init__.py +0 -0
  72. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/fixtures/make_repo.py +0 -0
  73. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/fixtures/ssh_config_samples.py +0 -0
  74. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_action_environment_mapping.py +0 -0
  75. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_action_outputs.py +0 -0
  76. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_action_pr_number_handling.py +0 -0
  77. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_action_step_validation.py +0 -0
  78. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_automation_only.py +0 -0
  79. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_cli.py +0 -0
  80. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_cli_helpers.py +0 -0
  81. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_cli_netrc_options.py +0 -0
  82. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_cli_outputs_file.py +0 -0
  83. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_cli_url_and_dryrun.py +0 -0
  84. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_commit_normalization.py +0 -0
  85. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_composite_action_coverage.py +0 -0
  86. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_config_and_reviewers.py +0 -0
  87. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_config_helpers.py +0 -0
  88. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_close_pr_policy.py +0 -0
  89. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_gerrit_backref_comment.py +0 -0
  90. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_gerrit_push_errors.py +0 -0
  91. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_gerrit_rest_results.py +0 -0
  92. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_shallow_clone.py +0 -0
  93. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_core_ssrf_protection.py +0 -0
  94. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_duplicate_detection.py +0 -0
  95. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_email_case_normalization.py +0 -0
  96. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_error_codes.py +0 -0
  97. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_external_api_framework.py +0 -0
  98. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_gerrit_change_status_checks.py +0 -0
  99. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_gerrit_pr_closer.py +0 -0
  100. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_gerrit_rest_client.py +0 -0
  101. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_gerrit_urls.py +0 -0
  102. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_gerrit_urls_more.py +0 -0
  103. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_ghe_and_gitreview_args.py +0 -0
  104. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_github_api_error_handling.py +0 -0
  105. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_github_api_helpers.py +0 -0
  106. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_github_api_retry_and_helpers.py +0 -0
  107. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_gitutils_helpers.py +0 -0
  108. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_mapping_comment_additional.py +0 -0
  109. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  110. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_metadata_and_reconciliation.py +0 -0
  111. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_misc_small_coverage.py +0 -0
  112. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_netrc.py +0 -0
  113. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_orphan_rest_side_effects.py +0 -0
  114. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_pr_content_filter.py +0 -0
  115. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_reconciliation_extracted_module.py +0 -0
  116. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  117. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_reconciliation_scenarios.py +0 -0
  118. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_ssh_agent.py +0 -0
  119. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_ssh_agent_ownership.py +0 -0
  120. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_ssh_artifact_prevention.py +0 -0
  121. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_ssh_common.py +0 -0
  122. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_ssh_discovery.py +0 -0
  123. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_ssh_discovery_dry_run.py +0 -0
  124. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_trailers_additional.py +0 -0
  125. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_url_parser.py +0 -0
  126. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/test_utils.py +0 -0
  127. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/tests/unit/test_config_integration.py +0 -0
  128. {github2gerrit-1.0.7 → github2gerrit-1.0.9}/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: 0839f92796ae388643a08a21640a029b322be5c2 # frozen: v0.15.2
62
+ rev: b969e2851312ca2b24bbec879ba4954341d1bd12 # frozen: v0.15.5
63
63
  hooks:
64
64
  - id: ruff
65
65
  files: ^(src|scripts|tests)/.+\.py$
@@ -96,7 +96,7 @@ repos:
96
96
  - id: shellcheck
97
97
 
98
98
  - repo: https://github.com/igorshubovych/markdownlint-cli
99
- rev: 76b3d32d3f4b965e1d6425253c59407420ae2c43 # frozen: v0.47.0
99
+ rev: e72a3ca1632f0b11a07d171449fe447a7ff6795e # frozen: v0.48.0
100
100
  hooks:
101
101
  - id: markdownlint
102
102
  args: ["--fix", "--config", ".markdownlint.yaml"]
@@ -116,12 +116,12 @@ repos:
116
116
 
117
117
  # Check for misspellings in documentation files
118
118
  - repo: https://github.com/codespell-project/codespell
119
- rev: 63c8f8312b7559622c0d82815639671ae42132ac # frozen: v2.4.1
119
+ rev: 2ccb47ff45ad361a21071a7eedda4c37e6ae8c5a # frozen: v2.4.2
120
120
  hooks:
121
121
  - id: codespell
122
122
 
123
123
  - repo: https://github.com/python-jsonschema/check-jsonschema
124
- rev: ec368acd16deee9c560c105ab6d27db4ee19a5ec # frozen: 0.36.2
124
+ rev: 8db279a37c552206d2df62269ff6f9d31125815a # frozen: 0.37.0
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.7
3
+ Version: 1.0.9
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
@@ -39,7 +39,6 @@ Requires-Dist: pytest-mock>=3.15.1; extra == 'dev'
39
39
  Requires-Dist: pytest>=9.0.2; extra == 'dev'
40
40
  Requires-Dist: responses>=0.25.8; extra == 'dev'
41
41
  Requires-Dist: ruff>=0.6.3; extra == 'dev'
42
- Requires-Dist: types-click>=7.1.8; extra == 'dev'
43
42
  Requires-Dist: types-requests>=2.31.0; extra == 'dev'
44
43
  Requires-Dist: types-urllib3>=1.26.25.14; extra == 'dev'
45
44
  Description-Content-Type: text/markdown
@@ -147,6 +146,117 @@ If UPDATE fails to find existing change:
147
146
  To create a new change, trigger the 'opened' workflow action.
148
147
  ```
149
148
 
149
+ ## PR Comment Commands
150
+
151
+ GitHub2Gerrit supports an extensible set of directives issued through
152
+ pull request comments. Add a comment containing `@github2gerrit`
153
+ followed by a command phrase and the tool will act on it during
154
+ the next workflow run.
155
+
156
+ ### Command Format
157
+
158
+ <!-- markdownlint-disable MD013 -->
159
+
160
+ ```text
161
+ @github2gerrit <command>
162
+ ```
163
+
164
+ <!-- markdownlint-enable MD013 -->
165
+
166
+ - Commands are **case-insensitive** — `@github2gerrit Create Missing Change`
167
+ works the same as `@github2gerrit create missing change`.
168
+ - Only the **latest** occurrence of each command takes effect when the same
169
+ command appears in more than one comment.
170
+ - The tool logs unrecognised directives at debug level and ignores them.
171
+
172
+ ### Available Commands
173
+
174
+ <!-- markdownlint-disable MD013 MD060 -->
175
+
176
+ | Command | Aliases | Description |
177
+ | --- | --- | --- |
178
+ | `create missing change` | `create-missing`, `create missing` | Create a Gerrit change when an UPDATE operation cannot find an existing one |
179
+
180
+ <!-- markdownlint-enable MD013 MD060 -->
181
+
182
+ ### Create Missing Change
183
+
184
+ When a PR `synchronize` event fires, GitHub2Gerrit treats it as an
185
+ **UPDATE** operation and expects a Gerrit change to exist. If the
186
+ original `opened` event failed (for example due to a bug or transient
187
+ error), no Gerrit change exists and every following update fails with:
188
+
189
+ ```text
190
+ ❌ UPDATE FAILED: Cannot update non-existent Gerrit change
191
+ ```
192
+
193
+ The **create missing change** command resolves this without manual
194
+ intervention in Gerrit. Two mechanisms trigger it:
195
+
196
+ #### 1. PR Comment Directive
197
+
198
+ Add a comment on the stuck pull request:
199
+
200
+ ```text
201
+ @github2gerrit create missing change
202
+ ```
203
+
204
+ Then re-trigger the workflow (push a trivial change or re-run the
205
+ workflow manually). GitHub2Gerrit detects the directive, switches
206
+ from UPDATE to CREATE mode, and pushes a new Gerrit change.
207
+
208
+ #### 2. CLI Flag
209
+
210
+ Outside GitHub Actions you can pass the flag directly:
211
+
212
+ ```shell
213
+ github2gerrit \
214
+ --create-missing \
215
+ https://github.com/MyOrg/my-repo/pull/42
216
+ ```
217
+
218
+ Or set the environment variable:
219
+
220
+ ```shell
221
+ export CREATE_MISSING=true
222
+ github2gerrit https://github.com/MyOrg/my-repo/pull/42
223
+ ```
224
+
225
+ #### What Happens During Fallback
226
+
227
+ 1. The tool attempts the normal UPDATE flow and finds no existing
228
+ Gerrit change.
229
+ 2. It checks for `--create-missing` **or** scans PR comments for the
230
+ `@github2gerrit create missing change` directive.
231
+ 3. If authorised, the operation mode switches from UPDATE to CREATE.
232
+ 4. The tool posts a notice on the PR:
233
+
234
+ ```text
235
+ 🔄 GitHub2Gerrit: No existing Gerrit change found for this PR.
236
+ Creating a new Gerrit change (fallback from UPDATE operation).
237
+ ```
238
+
239
+ 5. The pipeline continues as a normal CREATE — preparing commits,
240
+ pushing to Gerrit, posting the change URL back on the PR.
241
+
242
+ #### GitHub Actions Workflow Example
243
+
244
+ <!-- markdownlint-disable MD013 -->
245
+
246
+ ```yaml
247
+ - name: Submit PR to Gerrit
248
+ uses: lfreleng-actions/github2gerrit-action@main
249
+ with:
250
+ GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}
251
+ CREATE_MISSING: "true" # always allow fallback
252
+ ```
253
+
254
+ <!-- markdownlint-enable MD013 -->
255
+
256
+ > **Tip:** Setting `CREATE_MISSING` to `true` in your workflow means
257
+ > stuck PRs self-heal on the next `synchronize` event without requiring
258
+ > a comment directive.
259
+
150
260
  ## Close Merged PRs Feature
151
261
 
152
262
  GitHub2Gerrit now includes **automatic PR closure** when Gerrit merges changes
@@ -1526,7 +1636,9 @@ requiring manual configuration per PR or user.
1526
1636
  - `src/github2gerrit/gitutils.py` (subprocess and git helpers)
1527
1637
  - Linting and type checking
1528
1638
  - Ruff and MyPy use settings in `pyproject.toml`.
1529
- - Run from pre‑commit hooks and CI.
1639
+ - Run from [prek](https://github.com/j178/prek) hooks and CI.
1640
+ - prek is a faster, Rust-based drop-in replacement for pre-commit
1641
+ that reads the existing `.pre-commit-config.yaml` unchanged.
1530
1642
  - Tests
1531
1643
  - Pytest with coverage targets around 80%.
1532
1644
  - Add unit and integration tests for each feature.
@@ -1536,6 +1648,10 @@ requiring manual configuration per PR or user.
1536
1648
  - Install `uv` and run:
1537
1649
  - `uv pip install --system .`
1538
1650
  - `uv run github2gerrit --help`
1651
+ - Install prek hooks:
1652
+ - `uv tool install prek && prek install -f`
1653
+ - Run all checks (including tests) manually:
1654
+ - `prek run --all-files`
1539
1655
  - Run tests:
1540
1656
  - `uv run pytest -q`
1541
1657
  - Lint and type check:
@@ -101,6 +101,117 @@ If UPDATE fails to find existing change:
101
101
  To create a new change, trigger the 'opened' workflow action.
102
102
  ```
103
103
 
104
+ ## PR Comment Commands
105
+
106
+ GitHub2Gerrit supports an extensible set of directives issued through
107
+ pull request comments. Add a comment containing `@github2gerrit`
108
+ followed by a command phrase and the tool will act on it during
109
+ the next workflow run.
110
+
111
+ ### Command Format
112
+
113
+ <!-- markdownlint-disable MD013 -->
114
+
115
+ ```text
116
+ @github2gerrit <command>
117
+ ```
118
+
119
+ <!-- markdownlint-enable MD013 -->
120
+
121
+ - Commands are **case-insensitive** — `@github2gerrit Create Missing Change`
122
+ works the same as `@github2gerrit create missing change`.
123
+ - Only the **latest** occurrence of each command takes effect when the same
124
+ command appears in more than one comment.
125
+ - The tool logs unrecognised directives at debug level and ignores them.
126
+
127
+ ### Available Commands
128
+
129
+ <!-- markdownlint-disable MD013 MD060 -->
130
+
131
+ | Command | Aliases | Description |
132
+ | --- | --- | --- |
133
+ | `create missing change` | `create-missing`, `create missing` | Create a Gerrit change when an UPDATE operation cannot find an existing one |
134
+
135
+ <!-- markdownlint-enable MD013 MD060 -->
136
+
137
+ ### Create Missing Change
138
+
139
+ When a PR `synchronize` event fires, GitHub2Gerrit treats it as an
140
+ **UPDATE** operation and expects a Gerrit change to exist. If the
141
+ original `opened` event failed (for example due to a bug or transient
142
+ error), no Gerrit change exists and every following update fails with:
143
+
144
+ ```text
145
+ ❌ UPDATE FAILED: Cannot update non-existent Gerrit change
146
+ ```
147
+
148
+ The **create missing change** command resolves this without manual
149
+ intervention in Gerrit. Two mechanisms trigger it:
150
+
151
+ #### 1. PR Comment Directive
152
+
153
+ Add a comment on the stuck pull request:
154
+
155
+ ```text
156
+ @github2gerrit create missing change
157
+ ```
158
+
159
+ Then re-trigger the workflow (push a trivial change or re-run the
160
+ workflow manually). GitHub2Gerrit detects the directive, switches
161
+ from UPDATE to CREATE mode, and pushes a new Gerrit change.
162
+
163
+ #### 2. CLI Flag
164
+
165
+ Outside GitHub Actions you can pass the flag directly:
166
+
167
+ ```shell
168
+ github2gerrit \
169
+ --create-missing \
170
+ https://github.com/MyOrg/my-repo/pull/42
171
+ ```
172
+
173
+ Or set the environment variable:
174
+
175
+ ```shell
176
+ export CREATE_MISSING=true
177
+ github2gerrit https://github.com/MyOrg/my-repo/pull/42
178
+ ```
179
+
180
+ #### What Happens During Fallback
181
+
182
+ 1. The tool attempts the normal UPDATE flow and finds no existing
183
+ Gerrit change.
184
+ 2. It checks for `--create-missing` **or** scans PR comments for the
185
+ `@github2gerrit create missing change` directive.
186
+ 3. If authorised, the operation mode switches from UPDATE to CREATE.
187
+ 4. The tool posts a notice on the PR:
188
+
189
+ ```text
190
+ 🔄 GitHub2Gerrit: No existing Gerrit change found for this PR.
191
+ Creating a new Gerrit change (fallback from UPDATE operation).
192
+ ```
193
+
194
+ 5. The pipeline continues as a normal CREATE — preparing commits,
195
+ pushing to Gerrit, posting the change URL back on the PR.
196
+
197
+ #### GitHub Actions Workflow Example
198
+
199
+ <!-- markdownlint-disable MD013 -->
200
+
201
+ ```yaml
202
+ - name: Submit PR to Gerrit
203
+ uses: lfreleng-actions/github2gerrit-action@main
204
+ with:
205
+ GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}
206
+ CREATE_MISSING: "true" # always allow fallback
207
+ ```
208
+
209
+ <!-- markdownlint-enable MD013 -->
210
+
211
+ > **Tip:** Setting `CREATE_MISSING` to `true` in your workflow means
212
+ > stuck PRs self-heal on the next `synchronize` event without requiring
213
+ > a comment directive.
214
+
104
215
  ## Close Merged PRs Feature
105
216
 
106
217
  GitHub2Gerrit now includes **automatic PR closure** when Gerrit merges changes
@@ -1480,7 +1591,9 @@ requiring manual configuration per PR or user.
1480
1591
  - `src/github2gerrit/gitutils.py` (subprocess and git helpers)
1481
1592
  - Linting and type checking
1482
1593
  - Ruff and MyPy use settings in `pyproject.toml`.
1483
- - Run from pre‑commit hooks and CI.
1594
+ - Run from [prek](https://github.com/j178/prek) hooks and CI.
1595
+ - prek is a faster, Rust-based drop-in replacement for pre-commit
1596
+ that reads the existing `.pre-commit-config.yaml` unchanged.
1484
1597
  - Tests
1485
1598
  - Pytest with coverage targets around 80%.
1486
1599
  - Add unit and integration tests for each feature.
@@ -1490,6 +1603,10 @@ requiring manual configuration per PR or user.
1490
1603
  - Install `uv` and run:
1491
1604
  - `uv pip install --system .`
1492
1605
  - `uv run github2gerrit --help`
1606
+ - Install prek hooks:
1607
+ - `uv tool install prek && prek install -f`
1608
+ - Run all checks (including tests) manually:
1609
+ - `prek run --all-files`
1493
1610
  - Run tests:
1494
1611
  - `uv run pytest -q`
1495
1612
  - Lint and type check:
@@ -129,6 +129,14 @@ inputs:
129
129
  (format: [{"key": "username", "value": "ISSUE-ID"}])
130
130
  required: false
131
131
  default: "[]"
132
+ COMMIT_RULES_JSON:
133
+ description: >-
134
+ JSON object defining commit message rules with per-project and
135
+ per-actor overrides. Supports arbitrary label-value pairs placed
136
+ in the commit body or trailer block.
137
+ (see: docs/COMMIT_RULES.md)
138
+ required: false
139
+ default: ""
132
140
  AUTOMATION_ONLY:
133
141
  description: "Only accept pull requests from known automation tools"
134
142
  required: false
@@ -141,6 +149,10 @@ inputs:
141
149
  description: "Abandon Gerrit changes when their GitHub PRs are closed"
142
150
  required: false
143
151
  default: "true"
152
+ CREATE_MISSING:
153
+ description: "Create a Gerrit change when an UPDATE operation cannot find an existing one"
154
+ required: false
155
+ default: "false"
144
156
 
145
157
  outputs:
146
158
  gerrit_change_request_url:
@@ -164,7 +176,7 @@ runs:
164
176
 
165
177
  - name: "Setup uv"
166
178
  # yamllint disable-line rule:line-length
167
- uses: astral-sh/setup-uv@eac588ad8def6316056a12d4907a9d4d84ff7a3b # v7.3.0
179
+ uses: astral-sh/setup-uv@e06108dd0aef18192324c70427afc47652e63a82 # v7.5.0
168
180
  with:
169
181
  enable-cache: false
170
182
 
@@ -290,6 +302,7 @@ runs:
290
302
  ALLOW_DUPLICATES: ${{ inputs.ALLOW_DUPLICATES }}
291
303
  ISSUE_ID: ${{ inputs.ISSUE_ID }}
292
304
  ISSUE_ID_LOOKUP_JSON: ${{ inputs.ISSUE_ID_LOOKUP_JSON }}
305
+ COMMIT_RULES_JSON: ${{ inputs.COMMIT_RULES_JSON }}
293
306
  CI_TESTING: ${{ inputs.CI_TESTING }}
294
307
  CLOSE_MERGED_PRS: ${{ inputs.CLOSE_MERGED_PRS }}
295
308
  FORCE: ${{ inputs.FORCE }}
@@ -300,6 +313,7 @@ runs:
300
313
  AUTOMATION_ONLY: ${{ inputs.AUTOMATION_ONLY }}
301
314
  CLEANUP_ABANDONED: ${{ inputs.CLEANUP_ABANDONED }}
302
315
  CLEANUP_GERRIT: ${{ inputs.CLEANUP_GERRIT }}
316
+ CREATE_MISSING: ${{ inputs.CREATE_MISSING }}
303
317
 
304
318
  # Optional Gerrit overrides (when .gitreview is missing)
305
319
  GERRIT_SERVER: ${{ inputs.GERRIT_SERVER }}
@@ -0,0 +1,255 @@
1
+ <!-- SPDX-License-Identifier: Apache-2.0 -->
2
+ <!-- SPDX-FileCopyrightText: 2025 The Linux Foundation -->
3
+
4
+ # Commit Rules (`COMMIT_RULES_JSON`)
5
+
6
+ The **commit rules** feature provides a flexible, JSON-driven mechanism for
7
+ injecting arbitrary lines into commit messages submitted to Gerrit. It
8
+ generalises the existing `ISSUE_ID` / `ISSUE_ID_LOOKUP_JSON` support to handle
9
+ per-project requirements such as FD.io VPP's mandatory `Type:` field.
10
+
11
+ ## Quick Start
12
+
13
+ Set the `COMMIT_RULES_JSON` GitHub Actions variable (organisation or
14
+ repository level) to a JSON object describing the rules:
15
+
16
+ ```yaml
17
+ # .github/workflows/g2g.yml
18
+ jobs:
19
+ submit:
20
+ steps:
21
+ - uses: lfreleng-actions/github2gerrit-action@main
22
+ with:
23
+ COMMIT_RULES_JSON: ${{ vars.COMMIT_RULES_JSON }}
24
+ # ... other inputs ...
25
+ ```
26
+
27
+ ## JSON Schema
28
+
29
+ The top-level object has three optional sections:
30
+
31
+ | Section | Type | Description |
32
+ |------------|-------------------------------|------------------------------------------------|
33
+ | `defaults` | `array` of rule objects | Baseline rules applied to every commit. |
34
+ | `projects` | `object` (project → rules[]) | Per-Gerrit-project overrides. |
35
+ | `actors` | `object` (actor → rules[]) | Per-GitHub-actor overrides (e.g. bots). |
36
+
37
+ ### Rule Object
38
+
39
+ Each rule object describes a single line to insert into the commit message:
40
+
41
+ | Field | Type | Required | Default | Description |
42
+ |-------------|----------|----------|----------------|----------------------------------------------------------------------------|
43
+ | `key` | `string` | Yes | — | The label name (e.g. `Type`, `Issue-ID`, `Ticket`). |
44
+ | `value` | `string` | Yes | — | The value to insert. |
45
+ | `location` | `string` | No | `"trailer"` | Where to place the line — `"trailer"` or `"body"`. |
46
+ | `separator` | `string` | No | `"blank_line"` | Separation style when `location` is `"body"` — `"blank_line"` or `"none"`. |
47
+
48
+ ### Locations
49
+
50
+ - **`trailer`** — places the line in the Git trailer block at the end of
51
+ the commit message, alongside `Change-Id`, `Signed-off-by`, etc.
52
+ - **`body`** — places the line in the commit body, before the trailer
53
+ block. Fields like VPP's `Type:` need this location because Gerrit
54
+ server-side hooks expect them in the body rather than the trailer section.
55
+
56
+ ### Separators (body location only)
57
+
58
+ - **`blank_line`** (default) — inserts a blank line before the new
59
+ content, matching the conventional VPP commit style.
60
+ - **`none`** — appends the line directly after the existing body text
61
+ without extra blank lines.
62
+
63
+ ## Resolution Precedence
64
+
65
+ The engine resolves rules in this order when building the commit message
66
+ (last writer wins for a given `key`):
67
+
68
+ 1. **`defaults`** — baseline rules for all projects and actors.
69
+ 2. **`projects[<gerrit_project>]`** — overrides defaults for the matching
70
+ Gerrit project (from `.gitreview` or `GERRIT_PROJECT`).
71
+ 3. **`actors[<github_actor>]`** — overrides everything for the matching
72
+ GitHub actor (from `GITHUB_ACTOR`).
73
+
74
+ The existing `ISSUE_ID` input always takes priority over any `Issue-ID`
75
+ rule from commit rules. Both mechanisms can coexist safely.
76
+
77
+ ## Examples
78
+
79
+ ### FD.io (VPP + CSIT on the same Gerrit server)
80
+
81
+ VPP requires a `Type:` field in the commit body; CSIT does not.
82
+ Both projects need `Issue-ID` in the trailer block.
83
+
84
+ ```json
85
+ {
86
+ "defaults": [
87
+ {
88
+ "key": "Issue-ID",
89
+ "value": "CIMAN-33",
90
+ "location": "trailer"
91
+ }
92
+ ],
93
+ "projects": {
94
+ "vpp": [
95
+ {
96
+ "key": "Type",
97
+ "value": "ci",
98
+ "location": "body",
99
+ "separator": "blank_line"
100
+ },
101
+ {
102
+ "key": "Issue-ID",
103
+ "value": "CIMAN-33",
104
+ "location": "trailer"
105
+ }
106
+ ],
107
+ "hicn": [
108
+ {
109
+ "key": "Type",
110
+ "value": "ci",
111
+ "location": "body",
112
+ "separator": "blank_line"
113
+ }
114
+ ]
115
+ },
116
+ "actors": {
117
+ "dependabot[bot]": [
118
+ {
119
+ "key": "Type",
120
+ "value": "ci",
121
+ "location": "body"
122
+ },
123
+ {
124
+ "key": "Issue-ID",
125
+ "value": "CIMAN-33",
126
+ "location": "trailer"
127
+ }
128
+ ],
129
+ "renovate[bot]": [
130
+ {
131
+ "key": "Issue-ID",
132
+ "value": "CIMAN-44",
133
+ "location": "trailer"
134
+ }
135
+ ]
136
+ }
137
+ }
138
+ ```
139
+
140
+ **Result for VPP + dependabot:**
141
+
142
+ ```text
143
+ gha: update actions/checkout from v3 to v4
144
+
145
+ Type: ci
146
+
147
+ Issue-ID: CIMAN-33
148
+ Change-Id: I1234567890abcdef...
149
+ Signed-off-by: dependabot[bot] <support@github.com>
150
+ ```
151
+
152
+ **Result for CSIT + human user:**
153
+
154
+ ```text
155
+ fix: correct test assertion
156
+
157
+ Issue-ID: CIMAN-33
158
+ Change-Id: I1234567890abcdef...
159
+ Signed-off-by: Jane Doe <jane@example.com>
160
+ ```
161
+
162
+ ### ONAP (Issue-ID only)
163
+
164
+ ONAP projects only need `Issue-ID` in the trailer:
165
+
166
+ ```json
167
+ {
168
+ "actors": {
169
+ "dependabot[bot]": [
170
+ {
171
+ "key": "Issue-ID",
172
+ "value": "CIMAN-33"
173
+ }
174
+ ]
175
+ }
176
+ }
177
+ ```
178
+
179
+ ### Extra body fields
180
+
181
+ Some projects need more than one body field:
182
+
183
+ ```json
184
+ {
185
+ "projects": {
186
+ "vpp": [
187
+ {
188
+ "key": "Type",
189
+ "value": "ci",
190
+ "location": "body",
191
+ "separator": "blank_line"
192
+ },
193
+ {
194
+ "key": "Ticket",
195
+ "value": "VPP-2088",
196
+ "location": "body",
197
+ "separator": "none"
198
+ }
199
+ ]
200
+ }
201
+ }
202
+ ```
203
+
204
+ **Result:**
205
+
206
+ ```text
207
+ gha: update dependency versions
208
+
209
+ Type: ci
210
+ Ticket: VPP-2088
211
+
212
+ Change-Id: I1234567890abcdef...
213
+ Signed-off-by: bot <bot@example.com>
214
+ ```
215
+
216
+ ## CLI Usage
217
+
218
+ You can also pass the commit rules JSON via the command line:
219
+
220
+ ```bash
221
+ github2gerrit --commit-rules '{"defaults": [...]}' \
222
+ https://github.com/org/repo/pull/123
223
+ ```
224
+
225
+ Or via the environment variable:
226
+
227
+ ```bash
228
+ export COMMIT_RULES_JSON='{"defaults": [...]}'
229
+ github2gerrit https://github.com/org/repo/pull/123
230
+ ```
231
+
232
+ ## Interaction with ISSUE_ID / ISSUE_ID_LOOKUP_JSON
233
+
234
+ The existing `ISSUE_ID` and `ISSUE_ID_LOOKUP_JSON` inputs continue to
235
+ work unchanged. When both mechanisms specify an `Issue-ID`:
236
+
237
+ 1. An explicit `ISSUE_ID` input (or a value resolved from
238
+ `ISSUE_ID_LOOKUP_JSON`) **always wins**.
239
+ 2. If `ISSUE_ID` is empty, the engine applies the `Issue-ID` rule from
240
+ `COMMIT_RULES_JSON` instead.
241
+
242
+ This means you can safely enable `COMMIT_RULES_JSON` for an organisation
243
+ without breaking workflows that already set `ISSUE_ID` directly.
244
+
245
+ ## Validation and Error Handling
246
+
247
+ - Invalid JSON produces a warning but does **not** fail the workflow
248
+ (matching the existing `ISSUE_ID_LOOKUP_JSON` convention).
249
+ - Individual rule entries with missing or invalid `key`/`value` fields
250
+ produce a warning and the engine skips them; valid entries in the same
251
+ document still apply.
252
+ - Unknown `location` values default to `"trailer"` with a warning.
253
+ - Unknown `separator` values default to `"blank_line"` with a warning.
254
+ - Duplicate lines are automatically detected and skipped (both in body
255
+ and trailer locations).
@@ -102,7 +102,6 @@ dev = [
102
102
  # Type checking helpers
103
103
  "pytest-mock>=3.15.1",
104
104
  "types-requests>=2.31.0",
105
- "types-click>=7.1.8",
106
105
  "types-urllib3>=1.26.25.14",
107
106
  ]
108
107
 
@@ -230,7 +229,6 @@ directory = "coverage_html_report"
230
229
  minversion = "8.0"
231
230
  addopts = "-ra -q --cov=github2gerrit --cov-report=term-missing --cov-report=html"
232
231
  testpaths = ["tests"]
233
- asyncio_default_fixture_loop_scope = "function"
234
232
  markers = [
235
233
  "integration: marks tests as integration tests (deselect with '-m \"not integration\"')",
236
234
  ]