github2gerrit 1.4.4__tar.gz → 2.0.0__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 (136) hide show
  1. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/.pre-commit-config.yaml +7 -6
  2. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/PKG-INFO +4 -4
  3. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/README.md +2 -2
  4. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/action.yaml +32 -19
  5. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/docs/cli.md +1 -1
  6. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/docs/features.md +69 -0
  7. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/pyproject.toml +5 -0
  8. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/cli.py +103 -37
  9. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/config.py +40 -5
  10. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/core.py +129 -46
  11. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/duplicate_detection.py +13 -2
  12. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/gitreview.py +18 -2
  13. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/models.py +61 -1
  14. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/conftest.py +29 -0
  15. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_action_environment_mapping.py +6 -0
  16. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_action_step_validation.py +131 -17
  17. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_cli_netrc_options.py +25 -10
  18. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_composite_action_coverage.py +24 -16
  19. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_ssh_setup.py +6 -2
  20. github2gerrit-2.0.0/tests/test_fork_pr_hardening.py +580 -0
  21. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_issue_157_regressions.py +19 -15
  22. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/uv.lock +129 -111
  23. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/.aislop/config.yml +0 -0
  24. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/.editorconfig +0 -0
  25. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/.gitignore +0 -0
  26. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/.gitlint +0 -0
  27. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/.markdownlint.yaml +0 -0
  28. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/.yamllint +0 -0
  29. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/LICENSE +0 -0
  30. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/LICENSES/Apache-2.0.txt +0 -0
  31. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/REUSE.toml +0 -0
  32. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/SECURITY.md +0 -0
  33. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/docs/COMMIT_RULES.md +0 -0
  34. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/docs/development.md +0 -0
  35. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/docs/github2gerrit_token_permissions_classic.png +0 -0
  36. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/sitecustomize.py +0 -0
  37. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/__init__.py +0 -0
  38. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/commit_normalization.py +0 -0
  39. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/commit_rules.py +0 -0
  40. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/constants.py +0 -0
  41. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/error_codes.py +0 -0
  42. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/external_api.py +0 -0
  43. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/gerrit_pr_closer.py +0 -0
  44. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/gerrit_query.py +0 -0
  45. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/gerrit_rest.py +0 -0
  46. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/gerrit_ssh.py +0 -0
  47. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/gerrit_urls.py +0 -0
  48. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/github_api.py +0 -0
  49. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/gitutils.py +0 -0
  50. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/mapping_comment.py +0 -0
  51. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/netrc.py +0 -0
  52. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/orchestrator/__init__.py +0 -0
  53. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
  54. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/pr_commands.py +0 -0
  55. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/pr_content_filter.py +0 -0
  56. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/reconcile_matcher.py +0 -0
  57. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/rich_display.py +0 -0
  58. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/rich_logging.py +0 -0
  59. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/similarity.py +0 -0
  60. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/ssh_agent_setup.py +0 -0
  61. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/ssh_common.py +0 -0
  62. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/ssh_config_parser.py +0 -0
  63. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/ssh_discovery.py +0 -0
  64. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/trailers.py +0 -0
  65. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/src/github2gerrit/utils.py +0 -0
  66. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/fixtures/__init__.py +0 -0
  67. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/fixtures/make_repo.py +0 -0
  68. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/fixtures/ssh_config_samples.py +0 -0
  69. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_action_outputs.py +0 -0
  70. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_action_pr_number_handling.py +0 -0
  71. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_automation_only.py +0 -0
  72. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_change_id_deduplication.py +0 -0
  73. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_clean_squash_title.py +0 -0
  74. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_cli.py +0 -0
  75. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_cli_helpers.py +0 -0
  76. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_cli_outputs_file.py +0 -0
  77. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_cli_url_and_dryrun.py +0 -0
  78. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_commit_normalization.py +0 -0
  79. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_commit_rules.py +0 -0
  80. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_config_and_reviewers.py +0 -0
  81. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_config_helpers.py +0 -0
  82. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_close_pr_policy.py +0 -0
  83. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_config_and_errors.py +0 -0
  84. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_gerrit_backref_comment.py +0 -0
  85. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_gerrit_push_errors.py +0 -0
  86. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_gerrit_rest_results.py +0 -0
  87. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_integration_fixture_repo.py +0 -0
  88. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_prepare_commits.py +0 -0
  89. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_shallow_clone.py +0 -0
  90. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_core_ssrf_protection.py +0 -0
  91. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_dependency_supersession.py +0 -0
  92. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_dns_validation_and_no_gerrit.py +0 -0
  93. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_duplicate_detection.py +0 -0
  94. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_email_case_normalization.py +0 -0
  95. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_error_codes.py +0 -0
  96. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_external_api_framework.py +0 -0
  97. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_final_state_reconciliation.py +0 -0
  98. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_force_flag_cli.py +0 -0
  99. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gerrit_change_id_footer.py +0 -0
  100. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gerrit_change_status_checks.py +0 -0
  101. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gerrit_pr_closer.py +0 -0
  102. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gerrit_rest_client.py +0 -0
  103. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gerrit_ssh_abandon.py +0 -0
  104. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gerrit_urls.py +0 -0
  105. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gerrit_urls_more.py +0 -0
  106. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_ghe_and_gitreview_args.py +0 -0
  107. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_github_api_error_handling.py +0 -0
  108. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_github_api_helpers.py +0 -0
  109. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_github_api_retry_and_helpers.py +0 -0
  110. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gitreview.py +0 -0
  111. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_gitutils_helpers.py +0 -0
  112. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_mapping_comment_additional.py +0 -0
  113. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_mapping_comment_digest_and_backref.py +0 -0
  114. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_metadata_and_reconciliation.py +0 -0
  115. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_metadata_trailer_separation_bug.py +0 -0
  116. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_misc_small_coverage.py +0 -0
  117. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_netrc.py +0 -0
  118. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_orphan_rest_side_effects.py +0 -0
  119. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_pr_commands.py +0 -0
  120. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_pr_content_filter.py +0 -0
  121. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_pr_content_filter_integration.py +0 -0
  122. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_pr_update_detection.py +0 -0
  123. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_reconciliation_extracted_module.py +0 -0
  124. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_reconciliation_plan_and_orphans.py +0 -0
  125. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_reconciliation_scenarios.py +0 -0
  126. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_ssh_agent.py +0 -0
  127. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_ssh_agent_ownership.py +0 -0
  128. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_ssh_artifact_prevention.py +0 -0
  129. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_ssh_common.py +0 -0
  130. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_ssh_discovery.py +0 -0
  131. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_ssh_discovery_dry_run.py +0 -0
  132. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_trailers_additional.py +0 -0
  133. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_url_parser.py +0 -0
  134. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/test_utils.py +0 -0
  135. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/unit/test_config_integration.py +0 -0
  136. {github2gerrit-1.4.4 → github2gerrit-2.0.0}/tests/unit/test_ssh_config_parser.py +0 -0
@@ -3,6 +3,7 @@
3
3
  # SPDX-FileCopyrightText: 2025 The Linux Foundation
4
4
 
5
5
  ci:
6
+ autofix_prs: false
6
7
  # pre-commit.ci sandbox prevents network calls; aislop runs engines
7
8
  # that can call the network at scan time, so it cannot run there
8
9
  # either. gha-workflow-linter validates action calls over the
@@ -63,7 +64,7 @@ repos:
63
64
  types: [yaml]
64
65
 
65
66
  - repo: https://github.com/astral-sh/ruff-pre-commit
66
- rev: 2700fd5671c633760d912769c041bfcde2b9a01b # frozen: v0.15.22
67
+ rev: 65dbdb59d2f2d9c3bdc343c566821ad3319ddaa3 # frozen: v0.16.3
67
68
  hooks:
68
69
  - id: ruff
69
70
  files: ^(src|scripts|tests)/.+\.py$
@@ -72,7 +73,7 @@ repos:
72
73
  files: ^(src|scripts|tests)/.+\.py$
73
74
 
74
75
  - repo: https://github.com/pre-commit/mirrors-mypy
75
- rev: 41e691678310dfd3833f7ab4e180ddb014310356 # frozen: v2.3.0
76
+ rev: 7ff8d35ae36a7d2b968f2f90b4c723e292e594ee # frozen: v2.3.1
76
77
  hooks:
77
78
  - id: mypy
78
79
  files: ^src/.+\.py$
@@ -119,13 +120,13 @@ repos:
119
120
 
120
121
  # Requires a mirror, primary repo lacks .pre-commit-hooks.yaml
121
122
  - repo: https://github.com/DetachHead/basedpyright-prek-mirror
122
- rev: d55eabd2531b4f4a4c49c30fba627ce3a636ab79 # frozen: 1.39.9
123
+ rev: 3c94d276aa3005fa1f5a977946c9dc106135c083 # frozen: 1.39.10
123
124
  hooks:
124
125
  - id: basedpyright
125
126
  files: ^src/.+\.py$
126
127
 
127
128
  - repo: https://github.com/lfreleng-actions/gha-workflow-linter
128
- rev: cec679e88cd818179079f52925ba80b9e6cb705c # frozen: v1.2.3
129
+ rev: 7f11ad46be1cd2b3d778428d40fcb09309bb3be5 # frozen: v1.4.1
129
130
  hooks:
130
131
  - id: gha-workflow-linter
131
132
 
@@ -141,12 +142,12 @@ repos:
141
142
  name: aislop
142
143
  entry: aislop ci --human
143
144
  language: node
144
- additional_dependencies: ["aislop@0.13.1"]
145
+ additional_dependencies: ["aislop@0.14.1"]
145
146
  pass_filenames: false
146
147
  always_run: true
147
148
 
148
149
  - repo: https://github.com/python-jsonschema/check-jsonschema
149
- rev: 1a4bb160cab6417b3045e1b37b6b72449243e658 # frozen: 0.37.4
150
+ rev: c4654a7e007c9619a5eb63c39a456572a0fa1851 # frozen: 0.38.0
150
151
  hooks:
151
152
  - id: check-github-actions
152
153
  - id: check-github-workflows
@@ -1,6 +1,6 @@
1
- Metadata-Version: 2.4
1
+ Metadata-Version: 2.5
2
2
  Name: github2gerrit
3
- Version: 1.4.4
3
+ Version: 2.0.0
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
@@ -235,7 +235,7 @@ uvx github2gerrit https://github.com/onap/portal-ng-bff/pull/33
235
235
  | `GERRIT_HTTP_PASSWORD` | No | `""` | Gerrit HTTP password/token for REST queries |
236
236
  | `ORGANIZATION` | No | repository owner | GitHub organization/owner used for credential derivation |
237
237
  | `PR_NUMBER` | No | `"0"` | PR number to process; `0` processes all open PRs (dispatch) |
238
- | `FETCH_DEPTH` | No | `"10"` | Fetch depth for checkout |
238
+ | `FETCH_DEPTH` | No | `"10"` | Git history depth: PR fetch, push checkout, reconciliation |
239
239
  | `SUBMIT_SINGLE_COMMITS` | No | `"false"` | Submit one commit at a time to Gerrit |
240
240
  | `USE_PR_AS_COMMIT` | No | `"false"` | Use PR title and body as the commit message |
241
241
  | `PRESERVE_GITHUB_PRS` | No | `"true"` | Do not close GitHub PRs after pushing to Gerrit |
@@ -312,7 +312,7 @@ Input defaults match the composite action defaults.
312
312
  | `G2G_USE_SSH_AGENT` | boolean | `true` | Use SSH agent instead of file-based keys |
313
313
  | `ORGANIZATION` | string | repository owner | GitHub organization/owner |
314
314
  | `PR_NUMBER` | string | `"0"` | PR to process on dispatch; `0` processes all |
315
- | `FETCH_DEPTH` | string | `"10"` | Fetch depth for checkout |
315
+ | `FETCH_DEPTH` | string | `"10"` | Git depth: PR, push, reconciliation |
316
316
  | `SUBMIT_SINGLE_COMMITS` | boolean | `false` | Submit one commit at a time |
317
317
  | `USE_PR_AS_COMMIT` | boolean | `false` | Use PR title and body as the commit message |
318
318
  | `PRESERVE_GITHUB_PRS` | boolean | `true` | Do not close GitHub PRs after pushing |
@@ -189,7 +189,7 @@ uvx github2gerrit https://github.com/onap/portal-ng-bff/pull/33
189
189
  | `GERRIT_HTTP_PASSWORD` | No | `""` | Gerrit HTTP password/token for REST queries |
190
190
  | `ORGANIZATION` | No | repository owner | GitHub organization/owner used for credential derivation |
191
191
  | `PR_NUMBER` | No | `"0"` | PR number to process; `0` processes all open PRs (dispatch) |
192
- | `FETCH_DEPTH` | No | `"10"` | Fetch depth for checkout |
192
+ | `FETCH_DEPTH` | No | `"10"` | Git history depth: PR fetch, push checkout, reconciliation |
193
193
  | `SUBMIT_SINGLE_COMMITS` | No | `"false"` | Submit one commit at a time to Gerrit |
194
194
  | `USE_PR_AS_COMMIT` | No | `"false"` | Use PR title and body as the commit message |
195
195
  | `PRESERVE_GITHUB_PRS` | No | `"true"` | Do not close GitHub PRs after pushing to Gerrit |
@@ -266,7 +266,7 @@ Input defaults match the composite action defaults.
266
266
  | `G2G_USE_SSH_AGENT` | boolean | `true` | Use SSH agent instead of file-based keys |
267
267
  | `ORGANIZATION` | string | repository owner | GitHub organization/owner |
268
268
  | `PR_NUMBER` | string | `"0"` | PR to process on dispatch; `0` processes all |
269
- | `FETCH_DEPTH` | string | `"10"` | Fetch depth for checkout |
269
+ | `FETCH_DEPTH` | string | `"10"` | Git depth: PR, push, reconciliation |
270
270
  | `SUBMIT_SINGLE_COMMITS` | boolean | `false` | Submit one commit at a time |
271
271
  | `USE_PR_AS_COMMIT` | boolean | `false` | Use PR title and body as the commit message |
272
272
  | `PRESERVE_GITHUB_PRS` | boolean | `true` | Do not close GitHub PRs after pushing |
@@ -19,18 +19,13 @@ inputs:
19
19
  required: false
20
20
  default: "false"
21
21
  FETCH_DEPTH:
22
- description: "Fetch depth for checkout"
23
- required: false
24
- default: "10"
25
- SKIP_CHECKOUT:
26
22
  description: >-
27
- Skip the internal repository checkout. Set when the caller has
28
- already checked out the target repository at the desired ref
29
- (e.g. the bundled reusable workflow, which also checks out this
30
- action into the workspace and must not have it deleted by a
31
- nested checkout)
23
+ Depth of git history fetched. Applied to the tool's own fetch of
24
+ refs/pull/<N>/head, to the push-event checkout, and as the git log
25
+ window used to find merged commits during PR reconciliation, so
26
+ raising it widens how far back a merged change is matched
32
27
  required: false
33
- default: "false"
28
+ default: "10"
34
29
  PR_NUMBER:
35
30
  description: "Pull request number to process, use 0 to process all open"
36
31
  required: false
@@ -213,19 +208,32 @@ runs:
213
208
  echo "disabled=false" >> "$GITHUB_OUTPUT"
214
209
  fi
215
210
 
216
- - name: "Checkout repository"
217
- # yamllint disable-line rule:line-length
218
- if: steps.disabled-check.outputs.disabled != 'true' && inputs.SKIP_CHECKOUT != 'true'
211
+ - name: "Checkout repository (push events)"
212
+ # Push runs reconcile merged Gerrit changes back to their GitHub
213
+ # PRs, which reads commit trailers with a local `git show` in the
214
+ # working directory. Pull request runs need no checkout: the tool
215
+ # fetches refs/pull/<N>/head into its own temporary directory, and
216
+ # checking a fork head out here would both fail under
217
+ # pull_request_target and expose a fork-controlled .gitreview.
218
+ if: >-
219
+ steps.disabled-check.outputs.disabled != 'true'
220
+ && github.event_name == 'push'
219
221
  # yamllint disable-line rule:line-length
220
222
  uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
221
223
  with:
222
224
  fetch-depth: ${{ inputs.FETCH_DEPTH }}
223
- # Ensure we are on the PR's head SHA when triggered by PR events
224
- ref: ${{ github.event.pull_request.head.sha || github.sha }}
225
- # The action authenticates to Gerrit over SSH and to GitHub via an
226
- # explicit GITHUB_TOKEN env var, so the checkout token does not need
227
- # to be persisted into the local git config.
225
+ # The action authenticates to Gerrit over SSH and to GitHub via
226
+ # an explicit GITHUB_TOKEN env var, so the checkout token does
227
+ # not need to be persisted into the local git config.
228
228
  persist-credentials: false
229
+ # Callers may place files in the workspace that this action
230
+ # depends on. The bundled reusable workflow checks the action
231
+ # itself out to .g2g-action and then runs it from there, so a
232
+ # cleaning checkout would delete the action mid-run. That
233
+ # workflow also seeds the workspace root with the target
234
+ # repository first, because actions/checkout wipes a directory
235
+ # that is not already a git repository whatever `clean` says.
236
+ clean: false
229
237
 
230
238
  - name: "Setup Python"
231
239
  if: steps.disabled-check.outputs.disabled != 'true'
@@ -237,7 +245,7 @@ runs:
237
245
  - name: "Setup uv"
238
246
  if: steps.disabled-check.outputs.disabled != 'true'
239
247
  # yamllint disable-line rule:line-length
240
- uses: astral-sh/setup-uv@11f9893b081a58869d3b5fccaea48c9e9e46f990 # v8.3.2
248
+ uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
241
249
  with:
242
250
  enable-cache: false
243
251
 
@@ -396,6 +404,11 @@ runs:
396
404
  GITHUB_SHA: ${{ github.sha }}
397
405
  GITHUB_BASE_REF: ${{ github.base_ref }}
398
406
  GITHUB_HEAD_REF: ${{ github.head_ref }}
407
+ # Full owner/repo of the PR head. Differs from GITHUB_REPOSITORY
408
+ # for fork pull requests, whose tree must not influence Gerrit
409
+ # target resolution.
410
+ # yamllint disable-line rule:line-length
411
+ PR_HEAD_REPO: ${{ github.event.pull_request.head.repo.full_name }}
399
412
  GITHUB_ACTOR: ${{ github.actor }}
400
413
  SYNC_ALL_OPEN_PRS: ${{ steps.extract.outputs.sync_all }}
401
414
  PR_NUMBER: ${{ steps.extract.outputs.pr_number }}
@@ -84,7 +84,7 @@ set by GitHub Actions) and undergo proper boolean parsing.
84
84
  | `--preserve-github-prs/--no-preserve-github-prs` | `PRESERVE_GITHUB_PRS` | `true` | Do not close GitHub PRs after pushing to Gerrit |
85
85
  | `--close-merged-prs/--no-close-merged-prs` | `CLOSE_MERGED_PRS` | `true` | Close GitHub PRs when corresponding Gerrit changes merge |
86
86
  | `--force` | `FORCE` | `false` | Force PR closure regardless of Gerrit change status (abandoned, etc.) |
87
- | `--fetch-depth INTEGER` | `FETCH_DEPTH` | `10` | Fetch depth for checkout |
87
+ | `--fetch-depth INTEGER` | `FETCH_DEPTH` | `10` | Git history depth: PR fetch, push checkout, reconciliation window |
88
88
  | `--reviewers-email TEXT` | `REVIEWERS_EMAIL` | `""` | Email address(es) of reviewers (comma-separated) |
89
89
  | `--organization TEXT` | `ORGANIZATION` | unset | Organization (defaults to `GITHUB_REPOSITORY_OWNER` when unset) |
90
90
  | `--github-actor TEXT` | `GITHUB_ACTOR` | `""` | GitHub actor (username) who triggered the workflow |
@@ -16,6 +16,7 @@ action in detail. For a quick start and the full inputs table, see the
16
16
  - [Closing Merged PRs](#closing-merged-prs)
17
17
  - [Automatic Cleanup](#automatic-cleanup)
18
18
  - [Restricting PRs to Automation Tools](#restricting-prs-to-automation-tools)
19
+ - [Fork Pull Requests](#fork-pull-requests)
19
20
  - [Duplicate Detection](#duplicate-detection)
20
21
  - [Commit Message Normalization](#commit-message-normalization)
21
22
  - [Change-ID Reconciliation](#change-id-reconciliation)
@@ -284,6 +285,74 @@ Set `AUTOMATION_ONLY: "false"` to accept PRs from all users:
284
285
  GERRIT_SSH_PRIVKEY_G2G: ${{ secrets.GERRIT_SSH_PRIVKEY_G2G }}
285
286
  ```
286
287
 
288
+ ## Fork Pull Requests
289
+
290
+ A pull request raised from a fork carries a tree that someone without write
291
+ access to the base repository wrote. The tool treats that tree as untrusted
292
+ input.
293
+
294
+ ### No runner checkout for pull requests
295
+
296
+ The action does not check the target repository out into the runner workspace
297
+ for pull request events. It fetches `refs/pull/<N>/head` into a private
298
+ temporary directory instead. That ref lives on the base repository and resolves
299
+ fork pull request heads, so the tool never contacts the fork directly.
300
+
301
+ This sidesteps the `pull_request_target` checkout restriction that
302
+ `actions/checkout` enforces, and keeps fork content out of the working
303
+ directory, where the tool might otherwise read it as configuration.
304
+
305
+ Push events still check the repository out. Those runs reconcile merged Gerrit
306
+ changes back to their GitHub pull requests, which reads commit trailers from
307
+ the working directory. A push carries no fork content, so the checkout is safe
308
+ there.
309
+
310
+ ### Gerrit target resolution
311
+
312
+ For fork pull requests the tool ignores any `.gitreview` in the pull request
313
+ tree and resolves against the **base repository** instead. A fork could
314
+ otherwise redirect the push to a different Gerrit project simply by editing
315
+ `.gitreview`.
316
+
317
+ The tool applies the same treatment when it cannot establish where the pull
318
+ request head lives. Specific-PR `workflow_dispatch` runs and direct URL
319
+ invocations carry no pull request payload, and GitHub reports a null head
320
+ repository for a deleted fork. The tool resolves provenance through the API
321
+ where it can, and treats whatever remains unresolved as untrusted, so a missing
322
+ signal never earns a fork unwarranted trust.
323
+
324
+ Resolution order for a fork pull request:
325
+
326
+ 1. `.gitreview` from the base repository at the pull request's base branch,
327
+ via the GitHub API or `raw.githubusercontent.com`
328
+ 2. Explicit `GERRIT_SERVER` / `GERRIT_SERVER_PORT` / `GERRIT_PROJECT` inputs
329
+ 3. Per-organization configuration file
330
+ 4. Derivation from the organization name (`gerrit.[org].org`)
331
+
332
+ The lookup stops at the base branch. It does not fall back to the repository
333
+ default branch, and it does not read `.gitreview` through the API without an
334
+ authoritative base ref, because either would answer for a branch the pull
335
+ request does not target — and `.gitreview` outranks the explicit inputs.
336
+
337
+ The base repository's copy carries the same authority and lies outside an
338
+ attacker's reach, so this costs no accuracy. Skipping straight to derivation
339
+ would instead guess the Gerrit project name, and that guess goes wrong for
340
+ repositories whose name contains a hyphen that does not mark a path separator.
341
+
342
+ For the same reason, the tool drops branch names that originate in the fork
343
+ (`GITHUB_HEAD_REF` and the pull request head ref) from base-repository
344
+ lookups. Duplicate detection, which resolves `.gitreview` independently and
345
+ earlier, applies the same rule.
346
+
347
+ Same-repository pull requests keep their previous behaviour and still read
348
+ `.gitreview` from the pull request tree.
349
+
350
+ ### Approval gating
351
+
352
+ The tool does not yet require a maintainer approval before it transfers a fork
353
+ pull request to Gerrit. Until it does, use `AUTOMATION_ONLY` (see above) to
354
+ control which pull requests the tool accepts.
355
+
287
356
  ## Duplicate Detection
288
357
 
289
358
  Duplicate detection uses a scoring-based approach. The detector compares the
@@ -139,6 +139,11 @@ ignore = [
139
139
  "sitecustomize.py" = ["S110", "SIM103", "SIM105", "PLW2901"]
140
140
  "src/github2gerrit/duplicate_detection.py" = ["S310"]
141
141
  "src/github2gerrit/gerrit_rest.py" = ["S310", "TRY301"]
142
+ # S310 audits urlopen for non-HTTP schemes such as file:. This module
143
+ # builds the URL from a literal https://raw.githubusercontent.com/ prefix
144
+ # and passes it through _validate_raw_url, which requires scheme https and
145
+ # host raw.githubusercontent.com before the fetch runs.
146
+ "src/github2gerrit/gitreview.py" = ["S310"]
142
147
 
143
148
  [tool.ruff.lint.isort]
144
149
  known-first-party = ["github2gerrit"]
@@ -961,7 +961,11 @@ def main(
961
961
  10,
962
962
  "--fetch-depth",
963
963
  envvar="FETCH_DEPTH",
964
- help="Fetch depth for checkout.",
964
+ help=(
965
+ "Git history depth. Also the git log window for merged-PR "
966
+ "reconciliation, so lowering it narrows how far back a "
967
+ "merged change is matched."
968
+ ),
965
969
  ),
966
970
  gerrit_known_hosts: str = typer.Option(
967
971
  "",
@@ -1429,6 +1433,25 @@ def _finish_bulk_no_prs(
1429
1433
  return True # Success (no failures because no work)
1430
1434
 
1431
1435
 
1436
+ def _head_repo_for_pr(pr: Any) -> str:
1437
+ """Return the ``owner/repo`` of a PyGithub pull request's head.
1438
+
1439
+ Used in bulk mode, where each pull request carries its own
1440
+ provenance rather than inheriting it from the triggering event.
1441
+ Returns an empty string when unavailable.
1442
+ """
1443
+ head = getattr(pr, "head", None)
1444
+ repo = getattr(head, "repo", None)
1445
+ full_name = getattr(repo, "full_name", "")
1446
+ return str(full_name or "").strip()
1447
+
1448
+
1449
+ def _ref_for_pr(pr: Any, side: str) -> str:
1450
+ """Return ``pr.<side>.ref`` as a string, or empty when unavailable."""
1451
+ end = getattr(pr, side, None)
1452
+ return str(getattr(end, "ref", "") or "").strip()
1453
+
1454
+
1432
1455
  def _build_bulk_pr_tasks(
1433
1456
  gh: GitHubContext, prs_list: list[Any]
1434
1457
  ) -> list[tuple[Any, models.GitHubContext]]:
@@ -1439,6 +1462,10 @@ def _build_bulk_pr_tasks(
1439
1462
  if pr_number <= 0:
1440
1463
  continue
1441
1464
 
1465
+ # Bulk runs are triggered by workflow_dispatch or a repository
1466
+ # URL, so the outer context carries no PR refs. Take each pull
1467
+ # request's own refs so .gitreview resolves against the branch
1468
+ # the PR actually targets.
1442
1469
  per_ctx = models.GitHubContext(
1443
1470
  event_name=gh.event_name,
1444
1471
  event_action=gh.event_action,
@@ -1448,9 +1475,10 @@ def _build_bulk_pr_tasks(
1448
1475
  server_url=gh.server_url,
1449
1476
  run_id=gh.run_id,
1450
1477
  sha=gh.sha,
1451
- base_ref=gh.base_ref,
1452
- head_ref=gh.head_ref,
1478
+ base_ref=_ref_for_pr(pr, "base") or gh.base_ref,
1479
+ head_ref=_ref_for_pr(pr, "head") or gh.head_ref,
1453
1480
  pr_number=pr_number,
1481
+ head_repo=_head_repo_for_pr(pr),
1454
1482
  )
1455
1483
  pr_tasks.append((pr, per_ctx))
1456
1484
  return pr_tasks
@@ -2154,40 +2182,51 @@ def _load_effective_inputs() -> Inputs:
2154
2182
 
2155
2183
 
2156
2184
  def _augment_pr_refs_if_needed(gh: GitHubContext) -> GitHubContext:
2157
- # When a target URL was provided via CLI, G2G_TARGET_URL contains
2158
- # the actual URL string. We use a truthy check (non-empty string is truthy)
2159
- # to detect when running in direct URL mode vs GitHub Actions CI mode.
2160
- if (
2161
- os.getenv("G2G_TARGET_URL")
2162
- and gh.pr_number
2163
- and (not gh.head_ref or not gh.base_ref)
2164
- ):
2165
- try:
2166
- client = build_client()
2167
- repo = get_repo_from_env(client)
2168
- pr_obj = get_pull(repo, int(gh.pr_number))
2169
- base_ref = str(
2170
- getattr(getattr(pr_obj, "base", object()), "ref", "") or ""
2171
- )
2172
- head_ref = str(
2173
- getattr(getattr(pr_obj, "head", object()), "ref", "") or ""
2174
- )
2175
- head_sha = str(
2176
- getattr(getattr(pr_obj, "head", object()), "sha", "") or ""
2177
- )
2178
- if base_ref:
2179
- os.environ["GITHUB_BASE_REF"] = base_ref
2180
- log.debug("Resolved base_ref via GitHub API: %s", base_ref)
2181
- if head_ref:
2182
- os.environ["GITHUB_HEAD_REF"] = head_ref
2183
- log.debug("Resolved head_ref via GitHub API: %s", head_ref)
2184
- if head_sha:
2185
- os.environ["GITHUB_SHA"] = head_sha
2186
- log.debug("Resolved head sha via GitHub API: %s", head_sha)
2187
- return _read_github_context()
2188
- except Exception as exc:
2189
- log.debug("Could not resolve PR refs via GitHub API: %s", exc)
2190
- return gh
2185
+ """Fill in PR refs and provenance that the event context lacks.
2186
+
2187
+ Direct URL invocations and specific-PR ``workflow_dispatch`` runs
2188
+ carry no pull request payload, so ``base_ref``, ``head_ref`` and
2189
+ ``head_repo`` all arrive empty. Provenance in particular must not
2190
+ stay unknown: ``_read_gitreview`` treats an unresolved head
2191
+ repository as untrusted, so leaving it blank needlessly forfeits the
2192
+ PR tree's own ``.gitreview`` on same-repository pull requests.
2193
+ """
2194
+ needs_refs = not gh.head_ref or not gh.base_ref
2195
+ needs_provenance = not gh.head_repo
2196
+ if not gh.pr_number or not (needs_refs or needs_provenance):
2197
+ return gh
2198
+
2199
+ # Deliberately not gated on event name. Pull request events populate
2200
+ # both refs and provenance from the payload and so return above;
2201
+ # anything still missing them here (workflow_dispatch, issue_comment,
2202
+ # direct URL) needs the API regardless of how it was triggered.
2203
+ try:
2204
+ client = build_client()
2205
+ repo = get_repo_from_env(client)
2206
+ pr_obj = get_pull(repo, int(gh.pr_number))
2207
+ base_ref = _ref_for_pr(pr_obj, "base")
2208
+ head_ref = _ref_for_pr(pr_obj, "head")
2209
+ head_repo = _head_repo_for_pr(pr_obj)
2210
+ head_sha = str(
2211
+ getattr(getattr(pr_obj, "head", object()), "sha", "") or ""
2212
+ )
2213
+ if base_ref:
2214
+ os.environ["GITHUB_BASE_REF"] = base_ref
2215
+ log.debug("Resolved base_ref via GitHub API: %s", base_ref)
2216
+ if head_ref:
2217
+ os.environ["GITHUB_HEAD_REF"] = head_ref
2218
+ log.debug("Resolved head_ref via GitHub API: %s", head_ref)
2219
+ if head_repo:
2220
+ os.environ["PR_HEAD_REPO"] = head_repo
2221
+ log.debug("Resolved head repository via GitHub API: %s", head_repo)
2222
+ if head_sha:
2223
+ os.environ["GITHUB_SHA"] = head_sha
2224
+ log.debug("Resolved head sha via GitHub API: %s", head_sha)
2225
+ except Exception as exc:
2226
+ log.debug("Could not resolve PR refs via GitHub API: %s", exc)
2227
+ return gh
2228
+ else:
2229
+ return _read_github_context()
2191
2230
 
2192
2231
 
2193
2232
  def _process_close_gerrit_change(
@@ -3019,6 +3058,31 @@ def _extract_pr_number(evt: dict[str, Any]) -> int | None:
3019
3058
  return None
3020
3059
 
3021
3060
 
3061
+ def _read_head_repo(evt: dict[str, Any]) -> str:
3062
+ """Return the ``owner/repo`` of the pull request head repository.
3063
+
3064
+ Prefers the ``PR_HEAD_REPO`` environment variable supplied by the
3065
+ composite action, falling back to the raw event payload so the CLI
3066
+ behaves identically when invoked outside that wrapper.
3067
+
3068
+ Returns an empty string when the provenance cannot be established.
3069
+ """
3070
+ env_value = os.getenv("PR_HEAD_REPO", "").strip()
3071
+ if env_value:
3072
+ return env_value
3073
+
3074
+ pr = evt.get("pull_request")
3075
+ if isinstance(pr, dict):
3076
+ head = pr.get("head")
3077
+ if isinstance(head, dict):
3078
+ repo = head.get("repo")
3079
+ if isinstance(repo, dict):
3080
+ full_name = repo.get("full_name")
3081
+ if isinstance(full_name, str):
3082
+ return full_name.strip()
3083
+ return ""
3084
+
3085
+
3022
3086
  def _read_github_context() -> GitHubContext:
3023
3087
  event_name = os.getenv("GITHUB_EVENT_NAME", "")
3024
3088
  event_action = ""
@@ -3037,6 +3101,7 @@ def _read_github_context() -> GitHubContext:
3037
3101
 
3038
3102
  base_ref = os.getenv("GITHUB_BASE_REF", "")
3039
3103
  head_ref = os.getenv("GITHUB_HEAD_REF", "")
3104
+ head_repo = _read_head_repo(evt)
3040
3105
 
3041
3106
  pr_number = _extract_pr_number(evt)
3042
3107
  if pr_number is None:
@@ -3056,6 +3121,7 @@ def _read_github_context() -> GitHubContext:
3056
3121
  base_ref=base_ref,
3057
3122
  head_ref=head_ref,
3058
3123
  pr_number=pr_number,
3124
+ head_repo=head_repo,
3059
3125
  )
3060
3126
  return ctx
3061
3127
 
@@ -520,9 +520,26 @@ def _read_gitreview_host(repository: str | None = None) -> str | None:
520
520
  Delegates to the shared :mod:`github2gerrit.gitreview` module which
521
521
  consolidates all ``.gitreview`` parsing and fetching logic.
522
522
 
523
- Checks the local workspace first (available after actions/checkout),
524
- then falls back to fetching via raw.githubusercontent.com when a
525
- GITHUB_REPOSITORY is set.
523
+ This runs during parameter derivation, before the pull request
524
+ context is read, so provenance comes from ``PR_HEAD_REPO`` as the
525
+ composite action exports it.
526
+
527
+ ``GITHUB_HEAD_REF`` is consulted only when the head is known to live
528
+ in the base repository. A fork chooses its own branch name, and one
529
+ matching a real base-repository branch would otherwise select that
530
+ branch's host. Keeping the head ref for a *trusted* head matters
531
+ too: :meth:`Orchestrator._read_gitreview` reads the head tree in
532
+ that case, and dropping it here would let derivation record one host
533
+ while the pipeline pushes to another.
534
+
535
+ Provenance that cannot be established counts as untrusted, so the
536
+ absent-signal case falls back to the base ref alone.
537
+
538
+ The local working-directory file is still read. This layer cannot
539
+ tell a pull request the *tool* is processing from one the *workflow*
540
+ merely runs inside, so any attempt to skip it here also breaks
541
+ legitimate local reads. See issue #386: the fix belongs with the
542
+ wider question of tracking where a configuration value came from.
526
543
 
527
544
  Args:
528
545
  repository: GitHub repository in owner/repo format (optional).
@@ -532,8 +549,26 @@ def _read_gitreview_host(repository: str | None = None) -> str | None:
532
549
  The host string from .gitreview, or None if unavailable.
533
550
  """
534
551
  from .gitreview import read_gitreview_host
535
-
536
- return read_gitreview_host(repository)
552
+ from .models import head_repo_is_trusted
553
+
554
+ repo_full = (repository or os.getenv("GITHUB_REPOSITORY") or "").strip()
555
+ head_repo = os.getenv("PR_HEAD_REPO", "").strip()
556
+ trusted_head = head_repo_is_trusted(repo_full, head_repo)
557
+
558
+ branches: list[str] = []
559
+ if trusted_head:
560
+ head_ref = os.getenv("GITHUB_HEAD_REF", "").strip()
561
+ if head_ref:
562
+ branches.append(head_ref)
563
+ base_ref = os.getenv("GITHUB_BASE_REF", "").strip()
564
+ if base_ref:
565
+ branches.append(base_ref)
566
+
567
+ return read_gitreview_host(
568
+ repository,
569
+ branches=tuple(branches),
570
+ include_env_refs=False,
571
+ )
537
572
 
538
573
 
539
574
  def derive_gerrit_parameters(