github2gerrit 2.0.0__tar.gz → 2.1.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.
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/.pre-commit-config.yaml +3 -3
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/PKG-INFO +7 -1
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/README.md +6 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/docs/cli.md +1 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/docs/features.md +132 -3
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/cli.py +263 -9
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/config.py +1 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/core.py +135 -21
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/github_api.py +109 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/models.py +16 -4
- github2gerrit-2.1.0/src/github2gerrit/pr_approval.py +300 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/pr_commands.py +79 -15
- github2gerrit-2.1.0/src/github2gerrit/pr_directives.py +115 -0
- github2gerrit-2.1.0/src/github2gerrit/trust.py +122 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_cli_outputs_file.py +12 -3
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_cli_url_and_dryrun.py +1 -1
- github2gerrit-2.1.0/tests/test_fork_approval_gate.py +679 -0
- github2gerrit-2.1.0/tests/test_pr_command_authorisation.py +357 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_pr_commands.py +13 -6
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/.aislop/config.yml +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/.editorconfig +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/.gitignore +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/.gitlint +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/.markdownlint.yaml +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/.yamllint +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/LICENSE +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/REUSE.toml +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/SECURITY.md +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/action.yaml +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/docs/COMMIT_RULES.md +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/docs/development.md +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/pyproject.toml +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/sitecustomize.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/commit_rules.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/constants.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/error_codes.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/external_api.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/gerrit_pr_closer.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/gerrit_query.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/gerrit_rest.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/gerrit_ssh.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/gitreview.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/gitutils.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/netrc.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/pr_content_filter.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/ssh_agent_setup.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/ssh_config_parser.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/ssh_discovery.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/conftest.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/fixtures/ssh_config_samples.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_action_outputs.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_automation_only.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_change_id_deduplication.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_clean_squash_title.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_cli.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_cli_netrc_options.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_commit_rules.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_config_helpers.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_close_pr_policy.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_config_and_errors.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_prepare_commits.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_shallow_clone.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_ssh_setup.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_core_ssrf_protection.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_dependency_supersession.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_dns_validation_and_no_gerrit.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_error_codes.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_external_api_framework.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_final_state_reconciliation.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_force_flag_cli.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_fork_pr_hardening.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gerrit_change_id_footer.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gerrit_change_status_checks.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gerrit_pr_closer.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gerrit_ssh_abandon.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_github_api_error_handling.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gitreview.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_gitutils_helpers.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_issue_157_regressions.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_metadata_trailer_separation_bug.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_netrc.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_pr_update_detection.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_ssh_agent_ownership.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_ssh_artifact_prevention.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_ssh_common.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_ssh_discovery_dry_run.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_url_parser.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/test_utils.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/unit/test_config_integration.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/tests/unit/test_ssh_config_parser.py +0 -0
- {github2gerrit-2.0.0 → github2gerrit-2.1.0}/uv.lock +0 -0
|
@@ -64,7 +64,7 @@ repos:
|
|
|
64
64
|
types: [yaml]
|
|
65
65
|
|
|
66
66
|
- repo: https://github.com/astral-sh/ruff-pre-commit
|
|
67
|
-
rev:
|
|
67
|
+
rev: aab412d509121cb5f7533134b7e67f9fab59c682 # frozen: v0.16.4
|
|
68
68
|
hooks:
|
|
69
69
|
- id: ruff
|
|
70
70
|
files: ^(src|scripts|tests)/.+\.py$
|
|
@@ -89,7 +89,7 @@ repos:
|
|
|
89
89
|
args: ["--no-adverb"]
|
|
90
90
|
|
|
91
91
|
- repo: https://github.com/shellcheck-py/shellcheck-py
|
|
92
|
-
rev:
|
|
92
|
+
rev: a6564f5d57444e123433a5c9af733e3975c77229 # frozen: v0.11.0.1-1
|
|
93
93
|
hooks:
|
|
94
94
|
- id: shellcheck
|
|
95
95
|
|
|
@@ -126,7 +126,7 @@ repos:
|
|
|
126
126
|
files: ^src/.+\.py$
|
|
127
127
|
|
|
128
128
|
- repo: https://github.com/lfreleng-actions/gha-workflow-linter
|
|
129
|
-
rev:
|
|
129
|
+
rev: 42aa4db931ab4c9da0930ae5ca2259ab08da79cb # frozen: v1.5.2
|
|
130
130
|
hooks:
|
|
131
131
|
- id: gha-workflow-linter
|
|
132
132
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: github2gerrit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.1.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
|
|
@@ -139,6 +139,9 @@ name: github2gerrit
|
|
|
139
139
|
on:
|
|
140
140
|
pull_request_target:
|
|
141
141
|
types: [opened, reopened, edited, synchronize, closed]
|
|
142
|
+
# Re-runs when a maintainer approves a fork pull request
|
|
143
|
+
pull_request_review:
|
|
144
|
+
types: [submitted, dismissed]
|
|
142
145
|
push:
|
|
143
146
|
branches: [main, master]
|
|
144
147
|
workflow_dispatch:
|
|
@@ -186,6 +189,9 @@ name: github2gerrit
|
|
|
186
189
|
on:
|
|
187
190
|
pull_request_target:
|
|
188
191
|
types: [opened, reopened, edited, synchronize, closed]
|
|
192
|
+
# Re-runs when a maintainer approves a fork pull request
|
|
193
|
+
pull_request_review:
|
|
194
|
+
types: [submitted, dismissed]
|
|
189
195
|
workflow_dispatch:
|
|
190
196
|
|
|
191
197
|
permissions:
|
|
@@ -93,6 +93,9 @@ name: github2gerrit
|
|
|
93
93
|
on:
|
|
94
94
|
pull_request_target:
|
|
95
95
|
types: [opened, reopened, edited, synchronize, closed]
|
|
96
|
+
# Re-runs when a maintainer approves a fork pull request
|
|
97
|
+
pull_request_review:
|
|
98
|
+
types: [submitted, dismissed]
|
|
96
99
|
push:
|
|
97
100
|
branches: [main, master]
|
|
98
101
|
workflow_dispatch:
|
|
@@ -140,6 +143,9 @@ name: github2gerrit
|
|
|
140
143
|
on:
|
|
141
144
|
pull_request_target:
|
|
142
145
|
types: [opened, reopened, edited, synchronize, closed]
|
|
146
|
+
# Re-runs when a maintainer approves a fork pull request
|
|
147
|
+
pull_request_review:
|
|
148
|
+
types: [submitted, dismissed]
|
|
143
149
|
workflow_dispatch:
|
|
144
150
|
|
|
145
151
|
permissions:
|
|
@@ -165,6 +165,7 @@ Some settings have no CLI flag and use environment variables:
|
|
|
165
165
|
| `G2G_LOG_LEVEL` | `WARNING` | Logging level; set `DEBUG` for verbose output |
|
|
166
166
|
| `G2G_TOPIC_PREFIX` | `GH` | Prefix used when generating Gerrit topics |
|
|
167
167
|
| `G2G_SKIP_GERRIT_COMMENTS` | `false` | Skip posting back-reference comments on Gerrit changes |
|
|
168
|
+
| `G2G_TRUSTED_ASSOCIATIONS` | `OWNER,MEMBER,COLLABORATOR` | Author associations trusted to issue `@github2gerrit` comment directives |
|
|
168
169
|
| `G2G_ENABLE_DERIVATION` | `true` | Enable automatic derivation of Gerrit parameters from organization defaults |
|
|
169
170
|
| `G2G_AUTO_SAVE_CONFIG` | context-dependent | Save derived parameters back to the configuration file |
|
|
170
171
|
| `G2G_RESPECT_USER_SSH` | `false` (`true` in direct URL mode) | Use the local user's SSH configuration and keys instead of provided key material |
|
|
@@ -102,6 +102,57 @@ acts on it during the next workflow run.
|
|
|
102
102
|
- When the same command appears in more than one comment, only the latest
|
|
103
103
|
occurrence takes effect.
|
|
104
104
|
- The tool logs unrecognized directives at debug level and ignores them.
|
|
105
|
+
- Only comments from trusted authors count. See below.
|
|
106
|
+
|
|
107
|
+
### Who May Issue Commands
|
|
108
|
+
|
|
109
|
+
These repositories are public, so the tool checks who wrote a comment before
|
|
110
|
+
acting on it. Without that check, any GitHub user able to leave a comment
|
|
111
|
+
could direct the tool.
|
|
112
|
+
|
|
113
|
+
A comment counts only when GitHub reports its author's `author_association`
|
|
114
|
+
as one of:
|
|
115
|
+
|
|
116
|
+
| Association | Meaning |
|
|
117
|
+
| -------------- | ------------------------------------------ |
|
|
118
|
+
| `OWNER` | Owns the repository |
|
|
119
|
+
| `MEMBER` | Member of the owning organization |
|
|
120
|
+
| `COLLABORATOR` | Invited collaborator on the repository |
|
|
121
|
+
|
|
122
|
+
`CONTRIBUTOR` is **not** trusted. It means only that the author has had a
|
|
123
|
+
pull request merged at some point, which any outside contributor can achieve
|
|
124
|
+
and which carries no authority.
|
|
125
|
+
|
|
126
|
+
The tool ignores directives from anyone else and logs a warning naming the
|
|
127
|
+
author and their association, so a refused command is visible rather than
|
|
128
|
+
silently dropped.
|
|
129
|
+
|
|
130
|
+
#### For contributors adding commands
|
|
131
|
+
|
|
132
|
+
Adding a `CommandDefinition` to the registry gives the new command the same
|
|
133
|
+
authorisation as the existing one, so long as consumers reach it through
|
|
134
|
+
`github2gerrit.pr_directives`, which fetches comments, discards untrusted
|
|
135
|
+
authors and parses in a single step.
|
|
136
|
+
|
|
137
|
+
The parsing module performs no authorisation of its own. Calling
|
|
138
|
+
`parse_commands`, `find_command` or `has_command` with comments taken straight
|
|
139
|
+
from the GitHub API skips the check and recreates the original defect.
|
|
140
|
+
|
|
141
|
+
Override the trusted set with `G2G_TRUSTED_ASSOCIATIONS`, a comma-separated
|
|
142
|
+
list. Setting it to `OWNER` alone is the strictest useful value:
|
|
143
|
+
|
|
144
|
+
```yaml
|
|
145
|
+
env:
|
|
146
|
+
G2G_TRUSTED_ASSOCIATIONS: "OWNER,MEMBER"
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
An empty or blank value keeps the default rather than trusting nobody or
|
|
150
|
+
everyone.
|
|
151
|
+
|
|
152
|
+
Note that `MEMBER` means organization member, which is not the same as write
|
|
153
|
+
access to a given repository. On a Gerrit mirror that remains the only
|
|
154
|
+
available signal, because the GitHub collaborator list holds infrastructure
|
|
155
|
+
accounts rather than the project's reviewers.
|
|
105
156
|
|
|
106
157
|
### Available Commands
|
|
107
158
|
|
|
@@ -349,9 +400,87 @@ Same-repository pull requests keep their previous behaviour and still read
|
|
|
349
400
|
|
|
350
401
|
### Approval gating
|
|
351
402
|
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
403
|
+
A fork pull request does not transfer to Gerrit until a maintainer approves it.
|
|
404
|
+
The check runs before the tool fetches anything from the pull request and before
|
|
405
|
+
it unlocks the Gerrit SSH key, so a pull request awaiting review reaches
|
|
406
|
+
neither.
|
|
407
|
+
|
|
408
|
+
Same-repository pull requests skip the check entirely. Pushing a branch to the
|
|
409
|
+
base repository already requires write access, and automation such as
|
|
410
|
+
Dependabot, pre-commit.ci and Copilot works that way, so the gate never affects
|
|
411
|
+
them.
|
|
412
|
+
|
|
413
|
+
#### What counts as approval
|
|
414
|
+
|
|
415
|
+
An approving review, where every one of the following holds:
|
|
416
|
+
|
|
417
|
+
- the reviewer's `author_association` falls in the trusted set, the same one
|
|
418
|
+
used for [comment commands](#who-may-issue-commands);
|
|
419
|
+
- the review targets the pull request's **current head commit**;
|
|
420
|
+
- no trusted reviewer has since requested changes; and
|
|
421
|
+
- the reviewer is not the pull request author.
|
|
422
|
+
|
|
423
|
+
A pull request whose provenance the tool cannot establish takes the same path as
|
|
424
|
+
a fork. Approval is a question about authority, and an absent signal is not an
|
|
425
|
+
answer.
|
|
426
|
+
|
|
427
|
+
The gate applies to unattended runs. Running the CLI directly against a pull
|
|
428
|
+
request URL does not require an approval: the operator chose that pull request
|
|
429
|
+
and is using their own credentials, so they are already the authority the gate
|
|
430
|
+
looks for. It exists for the automated path, where the tool acts on a shared
|
|
431
|
+
identity with nobody watching.
|
|
432
|
+
|
|
433
|
+
Binding to the head commit matters. GitHub keeps approvals across pushes unless
|
|
434
|
+
branch protection dismisses them, so an approval that was not checked against a
|
|
435
|
+
commit would let a contributor gain approval for one revision and then push a
|
|
436
|
+
different one. The cost is that maintainers re-approve after each push, which is
|
|
437
|
+
the correct trade.
|
|
438
|
+
|
|
439
|
+
The author exclusion is belt and braces. GitHub already rejects approving your
|
|
440
|
+
own pull request, and that guarantee is a large part of why the tool uses
|
|
441
|
+
reviews rather than comment directives — on a Gerrit mirror the pull request
|
|
442
|
+
author often holds the same organization membership as the reviewers.
|
|
443
|
+
|
|
444
|
+
#### Enabling re-runs on approval
|
|
445
|
+
|
|
446
|
+
`pull_request_target` carries no event for a submitted review, so approving a
|
|
447
|
+
pull request does nothing unless the calling workflow listens for it:
|
|
448
|
+
|
|
449
|
+
```yaml
|
|
450
|
+
on:
|
|
451
|
+
pull_request_target:
|
|
452
|
+
types: [opened, reopened, edited, synchronize, closed]
|
|
453
|
+
pull_request_review:
|
|
454
|
+
types: [submitted, dismissed]
|
|
455
|
+
```
|
|
456
|
+
|
|
457
|
+
Without that trigger the tool only reconsiders a pull request on its next
|
|
458
|
+
push, or when a maintainer dispatches the workflow by hand.
|
|
459
|
+
|
|
460
|
+
#### When the gate blocks
|
|
461
|
+
|
|
462
|
+
The run finishes successfully rather than failing. A pull request waiting for a
|
|
463
|
+
human is not broken, and a red check would suggest otherwise.
|
|
464
|
+
|
|
465
|
+
The tool posts one comment explaining what is missing, and edits that same
|
|
466
|
+
comment on later runs rather than adding another. Where an approval exists but
|
|
467
|
+
covers an earlier commit, the comment says so, so a maintainer who did approve
|
|
468
|
+
is not told that nobody has.
|
|
469
|
+
|
|
470
|
+
Once approval arrives, the tool edits that comment again to record it, so a
|
|
471
|
+
transferred pull request does not keep displaying a stale block.
|
|
472
|
+
|
|
473
|
+
#### If the head moves mid-run
|
|
474
|
+
|
|
475
|
+
The gate reads the head commit from the API, while the workspace fetch reads
|
|
476
|
+
`refs/pull/<N>/head`, which the contributor can move meanwhile. The tool
|
|
477
|
+
compares the commit it fetched against the commit the maintainer approved and
|
|
478
|
+
refuses a mismatch, so a push timed against a running workflow cannot slip an
|
|
479
|
+
unreviewed commit into Gerrit.
|
|
480
|
+
|
|
481
|
+
That refusal fails the run, unlike the ordinary blocked case, because it falls
|
|
482
|
+
outside the normal course of events. The push that caused it triggers a fresh
|
|
483
|
+
run, which asks for approval of the new commit in the usual way.
|
|
355
484
|
|
|
356
485
|
## Duplicate Detection
|
|
357
486
|
|
|
@@ -69,6 +69,7 @@ from .gerrit_pr_closer import extract_pr_url_from_gerrit_change
|
|
|
69
69
|
from .gerrit_pr_closer import parse_pr_url
|
|
70
70
|
from .gerrit_pr_closer import process_recent_commits_for_pr_closure
|
|
71
71
|
from .github_api import build_client
|
|
72
|
+
from .github_api import create_pr_comment
|
|
72
73
|
from .github_api import get_pr_title_body
|
|
73
74
|
from .github_api import get_pull
|
|
74
75
|
from .github_api import get_repo_from_env
|
|
@@ -80,6 +81,11 @@ from .models import GitHubContext
|
|
|
80
81
|
from .models import Inputs
|
|
81
82
|
from .netrc import NetrcParseError
|
|
82
83
|
from .netrc import get_credentials_for_host
|
|
84
|
+
from .pr_approval import APPROVAL_MARKER
|
|
85
|
+
from .pr_approval import ApprovalStatus
|
|
86
|
+
from .pr_approval import evaluate_fork_approval
|
|
87
|
+
from .pr_approval import render_blocked_comment
|
|
88
|
+
from .pr_approval import render_cleared_comment
|
|
83
89
|
from .rich_display import RICH_AVAILABLE
|
|
84
90
|
from .rich_display import DummyProgressTracker
|
|
85
91
|
from .rich_display import G2GProgressTracker
|
|
@@ -87,6 +93,7 @@ from .rich_display import display_pr_info
|
|
|
87
93
|
from .rich_display import safe_console_print
|
|
88
94
|
from .rich_display import safe_typer_echo
|
|
89
95
|
from .rich_logging import setup_rich_aware_logging
|
|
96
|
+
from .trust import describe_trust_policy
|
|
90
97
|
from .utils import append_github_output
|
|
91
98
|
from .utils import env_bool
|
|
92
99
|
from .utils import env_str
|
|
@@ -228,14 +235,222 @@ def _check_automation_only(
|
|
|
228
235
|
)
|
|
229
236
|
|
|
230
237
|
|
|
238
|
+
def _resolve_pr_for_gate(gh: GitHubContext, data: Inputs) -> Any | None:
|
|
239
|
+
"""Fetch the pull request for the approval gate.
|
|
240
|
+
|
|
241
|
+
Used when the display step returned nothing — most notably the
|
|
242
|
+
no-token branch, which returns early. Without this the gate would
|
|
243
|
+
fail open exactly when the environment is least well configured.
|
|
244
|
+
"""
|
|
245
|
+
if not gh.pr_number:
|
|
246
|
+
return None
|
|
247
|
+
try:
|
|
248
|
+
token = getattr(data, "github_token", "") or os.getenv(
|
|
249
|
+
"GITHUB_TOKEN", ""
|
|
250
|
+
)
|
|
251
|
+
client = build_client(token)
|
|
252
|
+
repo = get_repo_from_env(client)
|
|
253
|
+
return get_pull(repo, int(gh.pr_number))
|
|
254
|
+
except Exception as exc:
|
|
255
|
+
log.debug("Could not resolve PR for the approval gate: %s", exc)
|
|
256
|
+
return None
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def _check_fork_approval(
|
|
260
|
+
pr_obj: Any | None,
|
|
261
|
+
gh: GitHubContext,
|
|
262
|
+
progress_tracker: Any = None,
|
|
263
|
+
) -> tuple[bool, str]:
|
|
264
|
+
"""Report whether a pull request may transfer to Gerrit.
|
|
265
|
+
|
|
266
|
+
Same-repository pull requests are unaffected: their head branch
|
|
267
|
+
already implies write access. Everything else — forks, and pull
|
|
268
|
+
requests whose provenance could not be established — requires an
|
|
269
|
+
approving review from a trusted maintainer, bound to the current
|
|
270
|
+
head commit.
|
|
271
|
+
|
|
272
|
+
Only a head positively known to live in the base repository skips
|
|
273
|
+
the gate. Unknown provenance does not: `is_fork_pr` answers a
|
|
274
|
+
factual question and reports ``False`` when it cannot tell, whereas
|
|
275
|
+
``head_is_trusted`` answers the authorisation question and reports
|
|
276
|
+
``False`` in the same case. Using the latter keeps the gate closed
|
|
277
|
+
when metadata is missing, which is exactly when it matters most.
|
|
278
|
+
|
|
279
|
+
The gate applies to unattended runs only. A direct CLI invocation
|
|
280
|
+
is a person acting deliberately with their own credentials, and
|
|
281
|
+
needs no second signature.
|
|
282
|
+
|
|
283
|
+
Runs before any fork content is fetched and before the Gerrit key
|
|
284
|
+
is materialised, so a blocked pull request never reaches either.
|
|
285
|
+
|
|
286
|
+
Returns:
|
|
287
|
+
``(allowed, approved_sha)``. ``approved_sha`` is the commit the
|
|
288
|
+
gate authorised, empty when no gate applied. It is returned
|
|
289
|
+
rather than stored globally because bulk runs process several
|
|
290
|
+
pull requests concurrently, where shared state would let one
|
|
291
|
+
worker clear or overwrite another's constraint.
|
|
292
|
+
|
|
293
|
+
Blocking is not an error: the pull request is simply waiting
|
|
294
|
+
for a human, and a failed check would misrepresent that.
|
|
295
|
+
"""
|
|
296
|
+
if gh.head_is_trusted:
|
|
297
|
+
return True, ""
|
|
298
|
+
|
|
299
|
+
if not _is_github_actions_context():
|
|
300
|
+
# Direct CLI invocation. The operator chose this pull request
|
|
301
|
+
# and is using their own credentials, so they are the authority
|
|
302
|
+
# the gate would otherwise be looking for. The gate exists for
|
|
303
|
+
# the unattended path, where the tool acts on a shared identity
|
|
304
|
+
# with nobody watching.
|
|
305
|
+
log.debug(
|
|
306
|
+
"Not running under GitHub Actions; approval gate not applied "
|
|
307
|
+
"to PR #%s",
|
|
308
|
+
gh.pr_number,
|
|
309
|
+
)
|
|
310
|
+
return True, ""
|
|
311
|
+
|
|
312
|
+
if pr_obj is None:
|
|
313
|
+
# Fail closed. Reaching here means the pull request could not
|
|
314
|
+
# be resolved, so approval cannot be established either way.
|
|
315
|
+
log.warning(
|
|
316
|
+
"🛑 Pull request #%s not transferred to Gerrit: the tool "
|
|
317
|
+
"could not resolve the pull request to check for approval",
|
|
318
|
+
gh.pr_number,
|
|
319
|
+
)
|
|
320
|
+
return False, ""
|
|
321
|
+
|
|
322
|
+
head_sha = str(
|
|
323
|
+
getattr(getattr(pr_obj, "head", None), "sha", "") or ""
|
|
324
|
+
).strip()
|
|
325
|
+
author = str(
|
|
326
|
+
getattr(getattr(pr_obj, "user", None), "login", "") or ""
|
|
327
|
+
).strip()
|
|
328
|
+
|
|
329
|
+
status = evaluate_fork_approval(
|
|
330
|
+
pr_obj, head_sha=head_sha, author_login=author
|
|
331
|
+
)
|
|
332
|
+
|
|
333
|
+
if status.approved:
|
|
334
|
+
log.info(
|
|
335
|
+
"✅ Pull request #%s authorised: %s",
|
|
336
|
+
gh.pr_number,
|
|
337
|
+
status.reason,
|
|
338
|
+
)
|
|
339
|
+
# The workspace fetch reads refs/pull/<N>/head, which the
|
|
340
|
+
# contributor can move after this check. Carry what was
|
|
341
|
+
# approved so the fetch can refuse anything else.
|
|
342
|
+
_clear_fork_approval_notice(pr_obj, status, head_sha)
|
|
343
|
+
return True, head_sha
|
|
344
|
+
|
|
345
|
+
log.warning(
|
|
346
|
+
"🛑 Pull request #%s not transferred to Gerrit: %s. "
|
|
347
|
+
"Reviews count from: %s",
|
|
348
|
+
gh.pr_number,
|
|
349
|
+
status.reason,
|
|
350
|
+
describe_trust_policy(),
|
|
351
|
+
)
|
|
352
|
+
safe_console_print(
|
|
353
|
+
f"🛑 Awaiting maintainer approval: {status.reason}",
|
|
354
|
+
style="yellow",
|
|
355
|
+
progress_tracker=progress_tracker,
|
|
356
|
+
)
|
|
357
|
+
|
|
358
|
+
_post_fork_approval_notice(pr_obj, status, head_sha)
|
|
359
|
+
return False, ""
|
|
360
|
+
|
|
361
|
+
|
|
362
|
+
def _edit_owned_marker_comment(pr_obj: Any, body: str) -> bool:
|
|
363
|
+
"""Replace the tool's own approval notice, if one exists.
|
|
364
|
+
|
|
365
|
+
The marker is not proof of authorship — anyone may paste it into a
|
|
366
|
+
comment. Ownership is established by *attempting the edit*: the API
|
|
367
|
+
refuses to edit another user's comment, so a failure means the
|
|
368
|
+
comment was not ours and the search continues.
|
|
369
|
+
|
|
370
|
+
Returns:
|
|
371
|
+
``True`` when a notice of ours was updated.
|
|
372
|
+
"""
|
|
373
|
+
try:
|
|
374
|
+
issue = pr_obj.as_issue()
|
|
375
|
+
comments = list(issue.get_comments())
|
|
376
|
+
except Exception as exc:
|
|
377
|
+
log.debug("Could not read comments for approval notice: %s", exc)
|
|
378
|
+
return False
|
|
379
|
+
|
|
380
|
+
# Newest first: if several carry the marker, keep the most recent.
|
|
381
|
+
for comment in reversed(comments):
|
|
382
|
+
if APPROVAL_MARKER not in (getattr(comment, "body", "") or ""):
|
|
383
|
+
continue
|
|
384
|
+
try:
|
|
385
|
+
comment.edit(body)
|
|
386
|
+
except Exception as exc:
|
|
387
|
+
log.debug(
|
|
388
|
+
"Could not edit comment carrying the approval marker; "
|
|
389
|
+
"it is probably not ours: %s",
|
|
390
|
+
exc,
|
|
391
|
+
)
|
|
392
|
+
continue
|
|
393
|
+
else:
|
|
394
|
+
return True
|
|
395
|
+
return False
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
def _clear_fork_approval_notice(
|
|
399
|
+
pr_obj: Any,
|
|
400
|
+
status: ApprovalStatus,
|
|
401
|
+
head_sha: str,
|
|
402
|
+
) -> None:
|
|
403
|
+
"""Retract an earlier block notice once approval arrives.
|
|
404
|
+
|
|
405
|
+
Only edits an existing notice; it does not create one. A pull
|
|
406
|
+
request that was never blocked has nothing to retract, and adding a
|
|
407
|
+
comment to say so would be noise.
|
|
408
|
+
|
|
409
|
+
Best-effort: failing to tidy a comment must not stop a transfer
|
|
410
|
+
that has been authorised.
|
|
411
|
+
"""
|
|
412
|
+
if env_bool("CI_TESTING", False):
|
|
413
|
+
return
|
|
414
|
+
_edit_owned_marker_comment(
|
|
415
|
+
pr_obj, render_cleared_comment(status, head_sha=head_sha)
|
|
416
|
+
)
|
|
417
|
+
|
|
418
|
+
|
|
419
|
+
def _post_fork_approval_notice(
|
|
420
|
+
pr_obj: Any,
|
|
421
|
+
status: ApprovalStatus,
|
|
422
|
+
head_sha: str,
|
|
423
|
+
) -> None:
|
|
424
|
+
"""Explain the block on the pull request, editing any prior notice.
|
|
425
|
+
|
|
426
|
+
Best-effort throughout: a comment API failure must never turn a
|
|
427
|
+
block into a crash, nor a block into a pass.
|
|
428
|
+
"""
|
|
429
|
+
if env_bool("CI_TESTING", False):
|
|
430
|
+
return
|
|
431
|
+
|
|
432
|
+
body = render_blocked_comment(status, head_sha=head_sha)
|
|
433
|
+
if _edit_owned_marker_comment(pr_obj, body):
|
|
434
|
+
return
|
|
435
|
+
|
|
436
|
+
try:
|
|
437
|
+
create_pr_comment(pr_obj, body)
|
|
438
|
+
except Exception as exc:
|
|
439
|
+
log.debug("Could not post fork approval notice: %s", exc)
|
|
440
|
+
|
|
441
|
+
|
|
231
442
|
def _extract_and_display_pr_info(
|
|
232
443
|
gh: GitHubContext,
|
|
233
444
|
data: Inputs,
|
|
234
445
|
progress_tracker: Any = None,
|
|
235
|
-
) -> None:
|
|
236
|
-
"""Extract PR information and display it with Rich formatting.
|
|
446
|
+
) -> Any | None:
|
|
447
|
+
"""Extract PR information and display it with Rich formatting.
|
|
448
|
+
|
|
449
|
+
Returns the pull request object when one was fetched, so callers
|
|
450
|
+
can reuse it instead of making a further API call.
|
|
451
|
+
"""
|
|
237
452
|
if not gh.pr_number:
|
|
238
|
-
return
|
|
453
|
+
return None
|
|
239
454
|
|
|
240
455
|
try:
|
|
241
456
|
# Get GitHub token from inputs if available, fallback to environment
|
|
@@ -250,7 +465,7 @@ def _extract_and_display_pr_info(
|
|
|
250
465
|
style="yellow",
|
|
251
466
|
progress_tracker=progress_tracker,
|
|
252
467
|
)
|
|
253
|
-
return
|
|
468
|
+
return None
|
|
254
469
|
|
|
255
470
|
client = build_client(token)
|
|
256
471
|
repo = get_repo_from_env(client)
|
|
@@ -298,6 +513,9 @@ def _extract_and_display_pr_info(
|
|
|
298
513
|
_exit_for_pr_not_found(gh.pr_number, gh.repository)
|
|
299
514
|
else:
|
|
300
515
|
_exit_for_pr_fetch_error(exc)
|
|
516
|
+
else:
|
|
517
|
+
return pr_obj
|
|
518
|
+
return None
|
|
301
519
|
|
|
302
520
|
|
|
303
521
|
class ConfigurationError(Exception):
|
|
@@ -1522,12 +1740,13 @@ def _submit_bulk_pr(
|
|
|
1522
1740
|
per_ctx: models.GitHubContext,
|
|
1523
1741
|
pr_number: int,
|
|
1524
1742
|
progress_tracker: G2GProgressTracker | DummyProgressTracker,
|
|
1743
|
+
approved_sha: str = "",
|
|
1525
1744
|
) -> _BulkPrResult:
|
|
1526
1745
|
"""Run the orchestrator for a single PR in multi-PR mode."""
|
|
1527
1746
|
try:
|
|
1528
1747
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
1529
1748
|
workspace = Path(temp_dir)
|
|
1530
|
-
orch = Orchestrator(workspace=workspace)
|
|
1749
|
+
orch = Orchestrator(workspace=workspace, approved_sha=approved_sha)
|
|
1531
1750
|
result_multi = orch.execute(inputs=data, gh=per_ctx)
|
|
1532
1751
|
_record_change_tracker_result(progress_tracker, result_multi)
|
|
1533
1752
|
return "success", result_multi, None
|
|
@@ -1576,13 +1795,20 @@ def _process_bulk_pr(
|
|
|
1576
1795
|
log.debug("PR #%d rejected by automation_only check", pr_number)
|
|
1577
1796
|
return "skipped", None, None
|
|
1578
1797
|
|
|
1798
|
+
# Fork PRs need a maintainer's approval before anything is fetched
|
|
1799
|
+
allowed, approved_sha = _check_fork_approval(pr, per_ctx, progress_tracker)
|
|
1800
|
+
if not allowed:
|
|
1801
|
+
return "skipped", None, None
|
|
1802
|
+
|
|
1579
1803
|
skip_result = _check_bulk_pr_duplicates(
|
|
1580
1804
|
data, per_ctx, pr_number, progress_tracker
|
|
1581
1805
|
)
|
|
1582
1806
|
if skip_result is not None:
|
|
1583
1807
|
return skip_result
|
|
1584
1808
|
|
|
1585
|
-
return _submit_bulk_pr(
|
|
1809
|
+
return _submit_bulk_pr(
|
|
1810
|
+
data, per_ctx, pr_number, progress_tracker, approved_sha
|
|
1811
|
+
)
|
|
1586
1812
|
|
|
1587
1813
|
|
|
1588
1814
|
def _record_bulk_success(
|
|
@@ -2079,10 +2305,11 @@ def _process_single(
|
|
|
2079
2305
|
data: Inputs,
|
|
2080
2306
|
gh: GitHubContext,
|
|
2081
2307
|
progress_tracker: G2GProgressTracker | DummyProgressTracker | None = None,
|
|
2308
|
+
approved_sha: str = "",
|
|
2082
2309
|
) -> tuple[bool, SubmissionResult]:
|
|
2083
2310
|
with tempfile.TemporaryDirectory() as temp_dir:
|
|
2084
2311
|
workspace = Path(temp_dir)
|
|
2085
|
-
orch = Orchestrator(workspace=workspace)
|
|
2312
|
+
orch = Orchestrator(workspace=workspace, approved_sha=approved_sha)
|
|
2086
2313
|
_prepare_single_checkout(orch, workspace, data, gh, progress_tracker)
|
|
2087
2314
|
_log_single_pre_submit(data, progress_tracker)
|
|
2088
2315
|
pipeline_success, result = _run_single_submission(
|
|
@@ -2932,9 +3159,34 @@ def _handle_single_pr(
|
|
|
2932
3159
|
# Augment PR refs via API when in URL mode and token present
|
|
2933
3160
|
gh = _augment_pr_refs_if_needed(gh)
|
|
2934
3161
|
|
|
3162
|
+
# A review only exists to unblock a gated pull request. On a
|
|
3163
|
+
# same-repository head there is nothing to unblock, so stop rather
|
|
3164
|
+
# than resubmitting an unchanged commit every time somebody
|
|
3165
|
+
# comments on, approves or requests changes to a pull request.
|
|
3166
|
+
if gh.event_name == "pull_request_review" and gh.head_is_trusted:
|
|
3167
|
+
log.info(
|
|
3168
|
+
"Review on PR #%s, whose head is in this repository; "
|
|
3169
|
+
"nothing to unblock, so no transfer is needed",
|
|
3170
|
+
gh.pr_number,
|
|
3171
|
+
)
|
|
3172
|
+
sys.exit(int(ExitCode.SUCCESS))
|
|
3173
|
+
|
|
2935
3174
|
# Display PR information with Rich formatting
|
|
3175
|
+
approved_sha = ""
|
|
2936
3176
|
if gh.pr_number:
|
|
2937
|
-
_extract_and_display_pr_info(gh, data, progress_tracker)
|
|
3177
|
+
pr_obj = _extract_and_display_pr_info(gh, data, progress_tracker)
|
|
3178
|
+
|
|
3179
|
+
# Fork PRs need a maintainer's approval before anything is
|
|
3180
|
+
# fetched and before the Gerrit key is materialised. Resolve
|
|
3181
|
+
# the PR here when the display step could not, so a missing
|
|
3182
|
+
# token cannot skip the gate.
|
|
3183
|
+
if pr_obj is None:
|
|
3184
|
+
pr_obj = _resolve_pr_for_gate(gh, data)
|
|
3185
|
+
allowed, approved_sha = _check_fork_approval(
|
|
3186
|
+
pr_obj, gh, progress_tracker
|
|
3187
|
+
)
|
|
3188
|
+
if not allowed:
|
|
3189
|
+
sys.exit(int(ExitCode.SUCCESS))
|
|
2938
3190
|
|
|
2939
3191
|
# Check for duplicates in single-PR mode (before workspace setup)
|
|
2940
3192
|
if gh.pr_number and not env_bool("SYNC_ALL_OPEN_PRS", False):
|
|
@@ -2946,7 +3198,9 @@ def _handle_single_pr(
|
|
|
2946
3198
|
log.debug("Processing PR #%s from %s", gh.pr_number, gh.repository)
|
|
2947
3199
|
log.debug("Target Gerrit server: %s", data.gerrit_server)
|
|
2948
3200
|
log.debug("Target Gerrit project: %s", data.gerrit_project)
|
|
2949
|
-
pipeline_success, result = _process_single(
|
|
3201
|
+
pipeline_success, result = _process_single(
|
|
3202
|
+
data, gh, progress_tracker, approved_sha
|
|
3203
|
+
)
|
|
2950
3204
|
|
|
2951
3205
|
# Run abandoned-PR and Gerrit cleanup if the pipeline was successful
|
|
2952
3206
|
# Skip in G2G_NO_GERRIT: no Gerrit server to query
|