github2gerrit 2.2.2__tar.gz → 2.4.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.2.2 → github2gerrit-2.4.0}/PKG-INFO +25 -6
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/README.md +24 -5
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/action.yaml +20 -4
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/docs/cli.md +1 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/docs/features.md +55 -7
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/cli.py +223 -50
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/core.py +5 -1
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/models.py +5 -2
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/pr_approval.py +52 -0
- github2gerrit-2.4.0/tests/test_bulk_fan_out.py +721 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_cli_outputs_file.py +1 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_cli_url_and_dryrun.py +1 -0
- github2gerrit-2.4.0/tests/test_fork_transfer_record.py +471 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_precommit_mypy_pins.py +7 -0
- github2gerrit-2.4.0/tests/test_scheduled_sweep.py +410 -0
- github2gerrit-2.4.0/tests/workflow_harness.py +312 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/.aislop/config.yml +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/.editorconfig +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/.gitignore +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/.gitlint +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/.markdownlint.yaml +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/.pre-commit-config.yaml +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/.yamllint +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/LICENSE +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/REUSE.toml +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/SECURITY.md +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/docs/COMMIT_RULES.md +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/docs/development.md +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/pyproject.toml +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/sitecustomize.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/approvers.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/commit_rules.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/config.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/config_ini.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/constants.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/error_codes.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/external_api.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_pr_closer.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_query.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_rest.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_ssh.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/github_api.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/gitreview.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/gitutils.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/netrc.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/pr_commands.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/pr_content_filter.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/pr_directives.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/project_names.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/ssh_agent_setup.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/ssh_config_parser.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/ssh_discovery.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/trust.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/conftest.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/fixtures/ssh_config_samples.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_action_outputs.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_approver_sources.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_automation_only.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_cache_isolation.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_change_id_deduplication.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_clean_squash_title.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_cli.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_cli_netrc_options.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_commit_rules.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_config_helpers.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_close_pr_policy.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_config_and_errors.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_prepare_commits.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_shallow_clone.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_ssh_setup.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_core_ssrf_protection.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_dependency_supersession.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_dns_validation_and_no_gerrit.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_error_codes.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_external_api_framework.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_final_state_reconciliation.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_force_flag_cli.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_fork_approval_gate.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_fork_pr_hardening.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gerrit_change_id_footer.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gerrit_change_status_checks.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gerrit_pr_closer.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gerrit_ssh_abandon.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_github_api_error_handling.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gitreview.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_gitutils_helpers.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_issue_157_regressions.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_metadata_trailer_separation_bug.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_netrc.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_pr_closed_handling.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_pr_command_authorisation.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_pr_commands.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_pr_update_detection.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_project_names.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_ssh_agent_ownership.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_ssh_artifact_prevention.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_ssh_common.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_ssh_discovery_dry_run.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_sync_mypy_pins.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_url_parser.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/test_utils.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/unit/test_config_integration.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/tests/unit/test_ssh_config_parser.py +0 -0
- {github2gerrit-2.2.2 → github2gerrit-2.4.0}/uv.lock +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: github2gerrit
|
|
3
|
-
Version: 2.
|
|
3
|
+
Version: 2.4.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
|
|
@@ -145,6 +145,9 @@ on:
|
|
|
145
145
|
# pull_request_review runs on fork PRs.
|
|
146
146
|
issue_comment:
|
|
147
147
|
types: [created]
|
|
148
|
+
# Optional: transfer approved fork PRs without anyone commenting.
|
|
149
|
+
# schedule:
|
|
150
|
+
# - cron: "17 * * * *"
|
|
148
151
|
push:
|
|
149
152
|
branches: [main, master]
|
|
150
153
|
workflow_dispatch:
|
|
@@ -189,17 +192,26 @@ which closes a human-authored pull request before the approval gate
|
|
|
189
192
|
sees it, so leaving it set would make the fork path unreachable. Drop
|
|
190
193
|
both if the repository only ever receives automation PRs.
|
|
191
194
|
|
|
195
|
+
The optional `schedule` trigger does the comment's job unattended: each
|
|
196
|
+
run transfers fork pull requests whose current commit a maintainer has
|
|
197
|
+
approved, one job per pull request, and leaves every other pull request
|
|
198
|
+
alone. See
|
|
199
|
+
[scheduled sweeps](docs/features.md#which-triggers-can-lift-the-gate).
|
|
200
|
+
|
|
192
201
|
Repositories using the Gerrit-side dispatch integration should also
|
|
193
202
|
declare `GERRIT_CHANGE_URL`, `GERRIT_EVENT_TYPE`, and `GERRIT_BRANCH`
|
|
194
203
|
as dispatch inputs and forward them the same way.
|
|
195
204
|
|
|
196
205
|
Pin `@main` to a release tag or commit SHA for production use.
|
|
197
206
|
|
|
198
|
-
A bulk `workflow_dispatch` (`PR_NUMBER` of `0`)
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
207
|
+
A bulk `workflow_dispatch` (`PR_NUMBER` of `0`) fans out: the reusable
|
|
208
|
+
workflow lists the open pull requests and runs one job per pull request,
|
|
209
|
+
four at a time, each in that pull request's own concurrency group, so a sweep
|
|
210
|
+
queues behind the runs that events start for the same pull request
|
|
211
|
+
instead of racing them. One further job runs the repository-wide cleanup once.
|
|
212
|
+
A sweep covers at most 255 open pull requests, one fewer than GitHub's 256-job
|
|
213
|
+
matrix limit; beyond that it fails, and you dispatch individual `PR_NUMBER`
|
|
214
|
+
values instead. A bulk dispatch sets no workflow outputs.
|
|
203
215
|
|
|
204
216
|
### Option B: composite action
|
|
205
217
|
|
|
@@ -256,6 +268,13 @@ jobs:
|
|
|
256
268
|
AUTOMATION_ONLY: false
|
|
257
269
|
```
|
|
258
270
|
|
|
271
|
+
A composite action cannot start jobs, so here a bulk `workflow_dispatch`
|
|
272
|
+
(`PR_NUMBER` of `0`) still processes every open pull request in one job. That
|
|
273
|
+
job does not serialise against the per-pull-request runs events start, so
|
|
274
|
+
avoid triggering one while pull request activity is in flight, or use the
|
|
275
|
+
reusable workflow, which fans the sweep out. For the same reason a `schedule`
|
|
276
|
+
trigger works only through the reusable workflow.
|
|
277
|
+
|
|
259
278
|
### Option C: command-line tool
|
|
260
279
|
|
|
261
280
|
The underlying Python CLI supports local and ad-hoc use (for example,
|
|
@@ -98,6 +98,9 @@ on:
|
|
|
98
98
|
# pull_request_review runs on fork PRs.
|
|
99
99
|
issue_comment:
|
|
100
100
|
types: [created]
|
|
101
|
+
# Optional: transfer approved fork PRs without anyone commenting.
|
|
102
|
+
# schedule:
|
|
103
|
+
# - cron: "17 * * * *"
|
|
101
104
|
push:
|
|
102
105
|
branches: [main, master]
|
|
103
106
|
workflow_dispatch:
|
|
@@ -142,17 +145,26 @@ which closes a human-authored pull request before the approval gate
|
|
|
142
145
|
sees it, so leaving it set would make the fork path unreachable. Drop
|
|
143
146
|
both if the repository only ever receives automation PRs.
|
|
144
147
|
|
|
148
|
+
The optional `schedule` trigger does the comment's job unattended: each
|
|
149
|
+
run transfers fork pull requests whose current commit a maintainer has
|
|
150
|
+
approved, one job per pull request, and leaves every other pull request
|
|
151
|
+
alone. See
|
|
152
|
+
[scheduled sweeps](docs/features.md#which-triggers-can-lift-the-gate).
|
|
153
|
+
|
|
145
154
|
Repositories using the Gerrit-side dispatch integration should also
|
|
146
155
|
declare `GERRIT_CHANGE_URL`, `GERRIT_EVENT_TYPE`, and `GERRIT_BRANCH`
|
|
147
156
|
as dispatch inputs and forward them the same way.
|
|
148
157
|
|
|
149
158
|
Pin `@main` to a release tag or commit SHA for production use.
|
|
150
159
|
|
|
151
|
-
A bulk `workflow_dispatch` (`PR_NUMBER` of `0`)
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
160
|
+
A bulk `workflow_dispatch` (`PR_NUMBER` of `0`) fans out: the reusable
|
|
161
|
+
workflow lists the open pull requests and runs one job per pull request,
|
|
162
|
+
four at a time, each in that pull request's own concurrency group, so a sweep
|
|
163
|
+
queues behind the runs that events start for the same pull request
|
|
164
|
+
instead of racing them. One further job runs the repository-wide cleanup once.
|
|
165
|
+
A sweep covers at most 255 open pull requests, one fewer than GitHub's 256-job
|
|
166
|
+
matrix limit; beyond that it fails, and you dispatch individual `PR_NUMBER`
|
|
167
|
+
values instead. A bulk dispatch sets no workflow outputs.
|
|
156
168
|
|
|
157
169
|
### Option B: composite action
|
|
158
170
|
|
|
@@ -209,6 +221,13 @@ jobs:
|
|
|
209
221
|
AUTOMATION_ONLY: false
|
|
210
222
|
```
|
|
211
223
|
|
|
224
|
+
A composite action cannot start jobs, so here a bulk `workflow_dispatch`
|
|
225
|
+
(`PR_NUMBER` of `0`) still processes every open pull request in one job. That
|
|
226
|
+
job does not serialise against the per-pull-request runs events start, so
|
|
227
|
+
avoid triggering one while pull request activity is in flight, or use the
|
|
228
|
+
reusable workflow, which fans the sweep out. For the same reason a `schedule`
|
|
229
|
+
trigger works only through the reusable workflow.
|
|
230
|
+
|
|
212
231
|
### Option C: command-line tool
|
|
213
232
|
|
|
214
233
|
The underlying Python CLI supports local and ad-hoc use (for example,
|
|
@@ -293,17 +293,22 @@ runs:
|
|
|
293
293
|
fi
|
|
294
294
|
|
|
295
295
|
- name: "Validate PR_NUMBER usage"
|
|
296
|
+
# A scheduled sweep names its pull request the same way a dispatch
|
|
297
|
+
# does: the reusable workflow fans the schedule out to one job per
|
|
298
|
+
# pull request, each passing that pull request's number.
|
|
296
299
|
# yamllint disable-line rule:line-length
|
|
297
|
-
if: ${{ steps.disabled-check.outputs.disabled != 'true' && github.event_name != 'workflow_dispatch' && inputs.PR_NUMBER != '' && inputs.PR_NUMBER != '0' }}
|
|
300
|
+
if: ${{ steps.disabled-check.outputs.disabled != 'true' && github.event_name != 'workflow_dispatch' && github.event_name != 'schedule' && inputs.PR_NUMBER != '' && inputs.PR_NUMBER != '0' }}
|
|
298
301
|
shell: bash
|
|
299
302
|
run: |
|
|
300
303
|
# Validate PR_NUMBER usage
|
|
301
304
|
set -euo pipefail
|
|
302
|
-
echo "Error: PR_NUMBER only valid during workflow_dispatch events
|
|
305
|
+
echo "Error: PR_NUMBER only valid during workflow_dispatch events," \
|
|
306
|
+
"or on schedule for one job of a scheduled sweep."
|
|
303
307
|
exit 2
|
|
304
308
|
|
|
305
309
|
- name: "Normalize PR_NUMBER"
|
|
306
|
-
|
|
310
|
+
# yamllint disable-line rule:line-length
|
|
311
|
+
if: ${{ steps.disabled-check.outputs.disabled != 'true' && (github.event_name == 'workflow_dispatch' || github.event_name == 'schedule') }}
|
|
307
312
|
id: normalize
|
|
308
313
|
shell: bash
|
|
309
314
|
env:
|
|
@@ -347,7 +352,18 @@ runs:
|
|
|
347
352
|
fi
|
|
348
353
|
|
|
349
354
|
# Honor PR_NUMBER or sync-all from workflow_dispatch normalization
|
|
350
|
-
if [[ "${GITHUB_EVENT_NAME}" == "
|
|
355
|
+
if [[ "${GITHUB_EVENT_NAME}" == "schedule" ]]; then
|
|
356
|
+
# One job of a scheduled sweep, which names its pull request.
|
|
357
|
+
# A schedule sweeping every pull request in one job would race
|
|
358
|
+
# the runs events start for them (#421), so it is refused.
|
|
359
|
+
if [[ -n "${DISPATCH_SYNC_ALL:-}" || -z "${DISPATCH_PR_NUMBER:-}" ]]; then
|
|
360
|
+
echo "Error: a scheduled run needs PR_NUMBER naming one pull request."
|
|
361
|
+
echo "The reusable workflow fans a schedule out to one job per pull"
|
|
362
|
+
echo "request; a composite action cannot start jobs of its own."
|
|
363
|
+
exit 2
|
|
364
|
+
fi
|
|
365
|
+
echo "pr_number=${DISPATCH_PR_NUMBER}" >> "$GITHUB_OUTPUT"
|
|
366
|
+
elif [[ "${GITHUB_EVENT_NAME}" == "workflow_dispatch" ]]; then
|
|
351
367
|
if [[ -n "${DISPATCH_SYNC_ALL:-}" ]]; then
|
|
352
368
|
echo "Processing all open pull requests via workflow_dispatch."
|
|
353
369
|
echo "sync_all=true" >> "$GITHUB_OUTPUT"
|
|
@@ -206,6 +206,7 @@ forwarded setting treats blank as unset and keeps its documented default.
|
|
|
206
206
|
| `CLEANUP_GERRIT` | `true` | Abandon Gerrit changes for closed GitHub PRs |
|
|
207
207
|
| `PR_NUMBER` | unset | Pull request number to process in CI/CD mode (`0` means bulk mode) |
|
|
208
208
|
| `SYNC_ALL_OPEN_PRS` | `false` | Process all open PRs in the repository (bulk mode) |
|
|
209
|
+
| `G2G_SWEEP_LEG` | `false` | Set by the reusable workflow on each job of a fanned-out bulk sweep |
|
|
209
210
|
| `GERRIT_HTTP_USER` | unset | Username for the Gerrit REST API (when required) |
|
|
210
211
|
| `GERRIT_HTTP_PASSWORD` | unset | HTTP password/token for the Gerrit REST API |
|
|
211
212
|
| `GERRIT_HTTP_BASE_PATH` | unset | Gerrit REST base path for non-standard deployments (e.g., `/r`) |
|
|
@@ -505,7 +505,8 @@ decides *whether* to proceed, and the tool always re-reads it from the pull
|
|
|
505
505
|
request's reviews. So no trigger, and nobody who fires one, can grant
|
|
506
506
|
anything.
|
|
507
507
|
|
|
508
|
-
Of the privileged triggers, this mechanism uses `issue_comment
|
|
508
|
+
Of the privileged triggers, this mechanism uses `issue_comment` and, through
|
|
509
|
+
the reusable workflow, `schedule`.
|
|
509
510
|
|
|
510
511
|
**A comment** is the privileged re-check. Subscribe to `issue_comment`:
|
|
511
512
|
|
|
@@ -531,11 +532,38 @@ This trigger needs `AUTOMATION_ONLY: false`. It defaults to `true` and closes
|
|
|
531
532
|
a human-authored pull request before the gate sees it, which would leave
|
|
532
533
|
nothing to unblock.
|
|
533
534
|
|
|
534
|
-
A
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
535
|
+
**A schedule** removes the comment. A maintainer approves in the ordinary way
|
|
536
|
+
and the next sweep transfers the change. Add a `schedule` trigger to a caller
|
|
537
|
+
of the reusable workflow:
|
|
538
|
+
|
|
539
|
+
```yaml
|
|
540
|
+
on:
|
|
541
|
+
schedule:
|
|
542
|
+
- cron: "17 * * * *" # hourly, off the top of the hour
|
|
543
|
+
```
|
|
544
|
+
|
|
545
|
+
A scheduled run lists the open pull requests and starts one job for each that
|
|
546
|
+
the gate could be holding back: a head not known to be in this repository,
|
|
547
|
+
with an approving review of its current commit. Same-repository pull requests
|
|
548
|
+
never enter a sweep. That also covers every automation pull request, which
|
|
549
|
+
the events already handle. The sweep still visits a head whose repository no
|
|
550
|
+
longer exists, since the gate applies to it. The approval check only narrows
|
|
551
|
+
the list; the job's own gate still decides whether that approval counts.
|
|
552
|
+
|
|
553
|
+
Each job runs in its pull request's concurrency group, so it queues behind a
|
|
554
|
+
comment re-check or a push for the same pull request rather than racing it.
|
|
555
|
+
Once a pull request has transferred, the transfer record described below has
|
|
556
|
+
later sweeps pass it over until its head moves. A pull request approved but
|
|
557
|
+
not yet transferred gets a job on every run until it transfers, so pick an
|
|
558
|
+
interval to match: hourly suits most projects.
|
|
559
|
+
|
|
560
|
+
The same caveats apply as for comments: the trigger needs
|
|
561
|
+
`AUTOMATION_ONLY: false`, and a scheduled workflow runs from the default
|
|
562
|
+
branch. A composite action cannot start jobs of its own, so a scheduled run
|
|
563
|
+
calling it directly fails with an error saying so, rather than sweeping every
|
|
564
|
+
pull request in one job and racing the runs events start for them. GitHub
|
|
565
|
+
suspends schedules in a public repository after 60 days without activity; the
|
|
566
|
+
comment re-check keeps working regardless.
|
|
539
567
|
|
|
540
568
|
`pull_request_review` should be **removed** from any workflow that still
|
|
541
569
|
carries it. It can no longer transfer anything: on a fork pull request it runs
|
|
@@ -592,7 +620,27 @@ covers an earlier commit, the comment says so, so a maintainer who did approve
|
|
|
592
620
|
is not told that nobody has.
|
|
593
621
|
|
|
594
622
|
Once approval arrives, the tool edits that comment again to record it, so a
|
|
595
|
-
transferred pull request does not keep displaying a stale block.
|
|
623
|
+
transferred pull request does not keep displaying a stale block. After the
|
|
624
|
+
transfer succeeds it edits the comment once more to say so, recording the
|
|
625
|
+
transferred commit in a hidden marker.
|
|
626
|
+
|
|
627
|
+
A sweep reads that marker and passes over a pull request whose current head it
|
|
628
|
+
names, rather than submitting the same commit again. Sweeps are the bulk
|
|
629
|
+
dispatch (`PR_NUMBER` of `0`) and the scheduled run; through the reusable
|
|
630
|
+
workflow each pull request in either runs as a job of its own, and the marker
|
|
631
|
+
behaves the same there. Anything less means the sweep processes the pull
|
|
632
|
+
request as before: no comment, no marker, a comment it cannot read, or a head
|
|
633
|
+
that has moved since. Only a run that pushes records anything: a dry run does
|
|
634
|
+
not, nor does one that finds the pull request's changes already merged or
|
|
635
|
+
abandoned and acts on GitHub instead. Neither does a pull request approved
|
|
636
|
+
before its first run, which never had a comment to edit. Only sweeps consult
|
|
637
|
+
the marker. A dispatch naming the pull request, a push and a
|
|
638
|
+
`@github2gerrit check` comment transfer whatever it says.
|
|
639
|
+
|
|
640
|
+
The marker proves nothing about who wrote it, and the tool does not assume it
|
|
641
|
+
did. Believing a pasted copy is harmless: skipping transfers nothing, it
|
|
642
|
+
delays only the sweep, and the tool reads the marker only on pull requests
|
|
643
|
+
the gate applies to.
|
|
596
644
|
|
|
597
645
|
#### If the head moves mid-run
|
|
598
646
|
|
|
@@ -84,8 +84,10 @@ from .pr_approval import APPROVAL_MARKER
|
|
|
84
84
|
from .pr_approval import ApprovalStatus
|
|
85
85
|
from .pr_approval import describe_approver_policy
|
|
86
86
|
from .pr_approval import evaluate_fork_approval
|
|
87
|
+
from .pr_approval import recorded_transfer
|
|
87
88
|
from .pr_approval import render_blocked_comment
|
|
88
89
|
from .pr_approval import render_cleared_comment
|
|
90
|
+
from .pr_approval import render_transferred_comment
|
|
89
91
|
from .pr_commands import CMD_CHECK
|
|
90
92
|
from .pr_commands import MENTION_PREFIX
|
|
91
93
|
from .pr_commands import find_open_command
|
|
@@ -120,6 +122,18 @@ def _exit_for_pr_state_error(pr_number: int, pr_state: str) -> None:
|
|
|
120
122
|
exit_for_pr_state_error(pr_number, pr_state)
|
|
121
123
|
|
|
122
124
|
|
|
125
|
+
def _is_sweep_leg() -> bool:
|
|
126
|
+
"""Report whether this run is one leg of a fanned-out bulk sweep.
|
|
127
|
+
|
|
128
|
+
The reusable workflow splits a bulk dispatch into a leg per open
|
|
129
|
+
pull request, plus a leg that runs only the repository-wide cleanup
|
|
130
|
+
(#422). ``G2G_SWEEP_LEG`` marks those legs. A leg naming a pull
|
|
131
|
+
request is still a sweep, not a request for that pull request, so
|
|
132
|
+
it keeps the bulk semantics the single-job sweep had.
|
|
133
|
+
"""
|
|
134
|
+
return env_bool("G2G_SWEEP_LEG", False)
|
|
135
|
+
|
|
136
|
+
|
|
123
137
|
def _stop_for_pr_state(
|
|
124
138
|
gh: GitHubContext, pr_state: str, progress_tracker: Any = None
|
|
125
139
|
) -> NoReturn:
|
|
@@ -133,11 +147,17 @@ def _stop_for_pr_state(
|
|
|
133
147
|
misreport that. The report behind #441 cited exactly this run and
|
|
134
148
|
took it for the close handler failing.
|
|
135
149
|
|
|
150
|
+
A sweep leg is the same case: the pull request was open when the
|
|
151
|
+
sweep listed it and closed before its leg ran.
|
|
152
|
+
|
|
136
153
|
A dispatch or direct invocation naming a closed pull request is
|
|
137
154
|
different: somebody asked for that one specifically, and being told
|
|
138
155
|
it cannot be processed is the useful answer.
|
|
139
156
|
"""
|
|
140
|
-
if gh.event_name in (
|
|
157
|
+
if _is_sweep_leg() or gh.event_name in (
|
|
158
|
+
"pull_request",
|
|
159
|
+
"pull_request_target",
|
|
160
|
+
):
|
|
141
161
|
log.info(
|
|
142
162
|
"Pull request #%s is %s; nothing to do for a '%s' event. Cleanup "
|
|
143
163
|
"for a closed pull request runs on the 'closed' event itself.",
|
|
@@ -266,7 +286,9 @@ def _check_automation_only(
|
|
|
266
286
|
|
|
267
287
|
close_pr(pr_obj, comment=comment)
|
|
268
288
|
|
|
269
|
-
|
|
289
|
+
# A sweep leg counts this as the single bulk job did: a
|
|
290
|
+
# pull request skipped, not the sweep failed.
|
|
291
|
+
sys.exit(0 if _is_sweep_leg() else 1)
|
|
270
292
|
except Exception:
|
|
271
293
|
log.exception("Failed to close non-automation PR")
|
|
272
294
|
raise
|
|
@@ -684,6 +706,25 @@ def _check_fork_approval(
|
|
|
684
706
|
return False, ""
|
|
685
707
|
|
|
686
708
|
|
|
709
|
+
def _marker_comments_newest_first(pr_obj: Any) -> list[Any]:
|
|
710
|
+
"""Return the comments carrying the approval marker, newest first.
|
|
711
|
+
|
|
712
|
+
Empty when the comments cannot be read, which every caller treats
|
|
713
|
+
as "no notice": the edit then falls back to posting one, and a
|
|
714
|
+
sweep to processing the pull request.
|
|
715
|
+
"""
|
|
716
|
+
try:
|
|
717
|
+
comments = list(pr_obj.as_issue().get_comments())
|
|
718
|
+
except Exception as exc:
|
|
719
|
+
log.debug("Could not read comments for approval notice: %s", exc)
|
|
720
|
+
return []
|
|
721
|
+
return [
|
|
722
|
+
comment
|
|
723
|
+
for comment in reversed(comments)
|
|
724
|
+
if APPROVAL_MARKER in (getattr(comment, "body", "") or "")
|
|
725
|
+
]
|
|
726
|
+
|
|
727
|
+
|
|
687
728
|
def _edit_owned_marker_comment(pr_obj: Any, body: str) -> bool:
|
|
688
729
|
"""Replace the tool's own approval notice, if one exists.
|
|
689
730
|
|
|
@@ -695,17 +736,8 @@ def _edit_owned_marker_comment(pr_obj: Any, body: str) -> bool:
|
|
|
695
736
|
Returns:
|
|
696
737
|
``True`` when a notice of ours was updated.
|
|
697
738
|
"""
|
|
698
|
-
try:
|
|
699
|
-
issue = pr_obj.as_issue()
|
|
700
|
-
comments = list(issue.get_comments())
|
|
701
|
-
except Exception as exc:
|
|
702
|
-
log.debug("Could not read comments for approval notice: %s", exc)
|
|
703
|
-
return False
|
|
704
|
-
|
|
705
739
|
# Newest first: if several carry the marker, keep the most recent.
|
|
706
|
-
for comment in
|
|
707
|
-
if APPROVAL_MARKER not in (getattr(comment, "body", "") or ""):
|
|
708
|
-
continue
|
|
740
|
+
for comment in _marker_comments_newest_first(pr_obj):
|
|
709
741
|
try:
|
|
710
742
|
comment.edit(body)
|
|
711
743
|
except Exception as exc:
|
|
@@ -741,6 +773,74 @@ def _clear_fork_approval_notice(
|
|
|
741
773
|
)
|
|
742
774
|
|
|
743
775
|
|
|
776
|
+
def _record_fork_transfer(
|
|
777
|
+
pr_obj: Any | None, approved_sha: str, *, pushed: bool
|
|
778
|
+
) -> None:
|
|
779
|
+
"""Note on the approval notice that the approved head transferred.
|
|
780
|
+
|
|
781
|
+
Gives a later sweep a way to tell that this head has already gone
|
|
782
|
+
to Gerrit (#419). Only a gated pull request carries an approved
|
|
783
|
+
commit, and only a run that pushed transferred one: a dry run, and
|
|
784
|
+
a pull request reconciled against changes already merged or
|
|
785
|
+
abandoned, both succeed without pushing, and recording either
|
|
786
|
+
would have sweeps skip a head that never reached Gerrit. Called
|
|
787
|
+
after the push, never before, for the same reason.
|
|
788
|
+
|
|
789
|
+
Edits the existing notice without creating one, like the
|
|
790
|
+
retraction. Best-effort: the transfer has happened, and a missing
|
|
791
|
+
record only costs a later sweep a redundant visit.
|
|
792
|
+
"""
|
|
793
|
+
if not approved_sha or not pushed or pr_obj is None:
|
|
794
|
+
return
|
|
795
|
+
if env_bool("CI_TESTING", False):
|
|
796
|
+
return
|
|
797
|
+
_edit_owned_marker_comment(
|
|
798
|
+
pr_obj, render_transferred_comment(head_sha=approved_sha)
|
|
799
|
+
)
|
|
800
|
+
|
|
801
|
+
|
|
802
|
+
def _sweep_can_skip(pr_obj: Any | None, gh: GitHubContext) -> bool:
|
|
803
|
+
"""Report whether a sweep may pass over an already-transferred PR.
|
|
804
|
+
|
|
805
|
+
True only when the newest approval notice records the pull
|
|
806
|
+
request's current head as transferred (#419). Anything less — no
|
|
807
|
+
notice, no record, an unreadable comment list, a head that has
|
|
808
|
+
moved since — means "process it", so a record that failed to be
|
|
809
|
+
written degrades to visiting the pull request as before rather
|
|
810
|
+
than stranding it.
|
|
811
|
+
|
|
812
|
+
Sweeps alone consult this. A dispatch naming the pull request, a
|
|
813
|
+
push and a comment are each somebody asking for this pull request,
|
|
814
|
+
and must work whatever a comment says.
|
|
815
|
+
|
|
816
|
+
The record is not proof of authorship, and nothing here assumes the
|
|
817
|
+
tool wrote it: anyone may paste one into a comment. Believing a
|
|
818
|
+
forgery is safe because skipping is the harmless direction. It
|
|
819
|
+
transfers nothing and delays only the sweep, which the explicit
|
|
820
|
+
routes above still override. The record is read only for heads the
|
|
821
|
+
approval gate applies to, so a forgery cannot keep a
|
|
822
|
+
same-repository pull request out of a sweep either.
|
|
823
|
+
"""
|
|
824
|
+
if gh.head_is_trusted or pr_obj is None:
|
|
825
|
+
return False
|
|
826
|
+
head_sha = str(
|
|
827
|
+
getattr(getattr(pr_obj, "head", None), "sha", "") or ""
|
|
828
|
+
).strip()
|
|
829
|
+
if not head_sha:
|
|
830
|
+
return False
|
|
831
|
+
notices = _marker_comments_newest_first(pr_obj)
|
|
832
|
+
body = str(getattr(notices[0], "body", "") or "") if notices else ""
|
|
833
|
+
if recorded_transfer(body) != head_sha.lower():
|
|
834
|
+
return False
|
|
835
|
+
log.info(
|
|
836
|
+
"⏩ Pull request #%s already transferred at %s; nothing for a "
|
|
837
|
+
"sweep to add",
|
|
838
|
+
gh.pr_number,
|
|
839
|
+
head_sha[:7],
|
|
840
|
+
)
|
|
841
|
+
return True
|
|
842
|
+
|
|
843
|
+
|
|
744
844
|
def _post_fork_approval_notice(
|
|
745
845
|
pr_obj: Any,
|
|
746
846
|
status: ApprovalStatus,
|
|
@@ -2115,6 +2215,10 @@ def _process_bulk_pr(
|
|
|
2115
2215
|
log.debug("PR #%d rejected by automation_only check", pr_number)
|
|
2116
2216
|
return "skipped", None, None
|
|
2117
2217
|
|
|
2218
|
+
# A sweep has nothing to add for a head already transferred
|
|
2219
|
+
if _sweep_can_skip(pr, per_ctx):
|
|
2220
|
+
return "skipped", None, None
|
|
2221
|
+
|
|
2118
2222
|
# Fork PRs need a maintainer's approval before anything is fetched
|
|
2119
2223
|
allowed, approved_sha = _check_fork_approval(pr, per_ctx, progress_tracker)
|
|
2120
2224
|
if not allowed:
|
|
@@ -2126,9 +2230,13 @@ def _process_bulk_pr(
|
|
|
2126
2230
|
if skip_result is not None:
|
|
2127
2231
|
return skip_result
|
|
2128
2232
|
|
|
2129
|
-
|
|
2233
|
+
result = _submit_bulk_pr(
|
|
2130
2234
|
data, per_ctx, pr_number, progress_tracker, approved_sha
|
|
2131
2235
|
)
|
|
2236
|
+
status, submission, _exc = result
|
|
2237
|
+
if status == "success" and submission is not None:
|
|
2238
|
+
_record_fork_transfer(pr, approved_sha, pushed=submission.pushed)
|
|
2239
|
+
return result
|
|
2132
2240
|
|
|
2133
2241
|
|
|
2134
2242
|
def _record_bulk_success(
|
|
@@ -3322,6 +3430,18 @@ def _handle_bulk_mode(
|
|
|
3322
3430
|
):
|
|
3323
3431
|
return False
|
|
3324
3432
|
|
|
3433
|
+
if _is_sweep_leg():
|
|
3434
|
+
# The reusable workflow gave every pull request a leg of its
|
|
3435
|
+
# own (#422); this one keeps only the repository-wide part.
|
|
3436
|
+
log.info(
|
|
3437
|
+
"Bulk sweep fanned out per pull request; running the "
|
|
3438
|
+
"repository-wide cleanup only"
|
|
3439
|
+
)
|
|
3440
|
+
_run_gerrit_cleanup_tasks(
|
|
3441
|
+
data, gh, no_gerrit=no_gerrit, gerrit_cleanup_info_log=True
|
|
3442
|
+
)
|
|
3443
|
+
return True
|
|
3444
|
+
|
|
3325
3445
|
bulk_success = _process_bulk(data, gh)
|
|
3326
3446
|
|
|
3327
3447
|
try:
|
|
@@ -3445,9 +3565,35 @@ def _check_single_pr_duplicates(
|
|
|
3445
3565
|
gh.pr_number,
|
|
3446
3566
|
)
|
|
3447
3567
|
except DuplicateChangeError as exc:
|
|
3568
|
+
if _is_sweep_leg():
|
|
3569
|
+
_skip_duplicate_in_sweep(exc, gh, progress_tracker)
|
|
3448
3570
|
_handle_single_pr_duplicate_error(exc, progress_tracker)
|
|
3449
3571
|
|
|
3450
3572
|
|
|
3573
|
+
def _skip_duplicate_in_sweep(
|
|
3574
|
+
exc: DuplicateChangeError,
|
|
3575
|
+
gh: GitHubContext,
|
|
3576
|
+
progress_tracker: G2GProgressTracker | DummyProgressTracker,
|
|
3577
|
+
) -> NoReturn:
|
|
3578
|
+
"""End a sweep leg cleanly on a blocked duplicate.
|
|
3579
|
+
|
|
3580
|
+
With ``ALLOW_DUPLICATES`` off, the single bulk job skipped a
|
|
3581
|
+
duplicate and carried on; a run naming the pull request fails on
|
|
3582
|
+
one, which is the answer somebody asking for it needs. A leg is a
|
|
3583
|
+
sweep, so it keeps the bulk behaviour rather than marking the
|
|
3584
|
+
whole sweep failed.
|
|
3585
|
+
"""
|
|
3586
|
+
progress_tracker.duplicate_skipped()
|
|
3587
|
+
progress_tracker.stop()
|
|
3588
|
+
log.warning(
|
|
3589
|
+
"Skipping PR #%s due to duplicate detection: %s. Use "
|
|
3590
|
+
"--allow-duplicates to override this check.",
|
|
3591
|
+
gh.pr_number,
|
|
3592
|
+
exc,
|
|
3593
|
+
)
|
|
3594
|
+
sys.exit(int(ExitCode.SUCCESS))
|
|
3595
|
+
|
|
3596
|
+
|
|
3451
3597
|
def _print_single_pr_summary(
|
|
3452
3598
|
progress_tracker: G2GProgressTracker | DummyProgressTracker,
|
|
3453
3599
|
pipeline_success: bool,
|
|
@@ -3480,6 +3626,62 @@ def _print_single_pr_summary(
|
|
|
3480
3626
|
safe_console_print(f"🔗 Gerrit change: {url}", style="green")
|
|
3481
3627
|
|
|
3482
3628
|
|
|
3629
|
+
def _gate_single_pr(
|
|
3630
|
+
gh: GitHubContext,
|
|
3631
|
+
data: Inputs,
|
|
3632
|
+
progress_tracker: G2GProgressTracker | DummyProgressTracker,
|
|
3633
|
+
) -> tuple[GitHubContext, Any | None, str]:
|
|
3634
|
+
"""Resolve the pull request and run every check ahead of transfer.
|
|
3635
|
+
|
|
3636
|
+
Exits successfully when there is nothing to transfer: a re-check
|
|
3637
|
+
with no gate to lift, a sweep leg whose head already transferred,
|
|
3638
|
+
or a fork pull request still awaiting approval.
|
|
3639
|
+
|
|
3640
|
+
Returns:
|
|
3641
|
+
The context with any metadata recovered from the pull request,
|
|
3642
|
+
the pull request itself, and the commit the approval gate
|
|
3643
|
+
authorised (empty when no gate applied).
|
|
3644
|
+
"""
|
|
3645
|
+
pr_obj = _extract_and_display_pr_info(gh, data, progress_tracker)
|
|
3646
|
+
|
|
3647
|
+
# Fork PRs need a maintainer's approval before anything is
|
|
3648
|
+
# fetched and before the Gerrit key is materialised. Resolve
|
|
3649
|
+
# the PR here when the display step could not, so a missing
|
|
3650
|
+
# token cannot skip the gate.
|
|
3651
|
+
if pr_obj is None:
|
|
3652
|
+
pr_obj = _resolve_pr_for_gate(gh, data)
|
|
3653
|
+
|
|
3654
|
+
# Second chance at the pull request's own metadata, and a
|
|
3655
|
+
# second look at the short-circuit above. An issue_comment
|
|
3656
|
+
# payload carries none of it, so the check before this
|
|
3657
|
+
# depended on _augment_pr_refs_if_needed's API call; that
|
|
3658
|
+
# helper swallows a failure and returns the context
|
|
3659
|
+
# unresolved, while the fetch just above is a separate call
|
|
3660
|
+
# that may well have succeeded. Without this, one transient
|
|
3661
|
+
# failure would let a commenter resubmit an unchanged
|
|
3662
|
+
# same-repository pull request — exactly what the
|
|
3663
|
+
# short-circuit exists to prevent — and would leave the base
|
|
3664
|
+
# ref empty for the transfer that followed.
|
|
3665
|
+
gh = _recover_pr_metadata(gh, pr_obj)
|
|
3666
|
+
if _recheck_has_nothing_to_unblock(gh):
|
|
3667
|
+
log.info(
|
|
3668
|
+
"Re-check (%s) for PR #%s, whose head is in this "
|
|
3669
|
+
"repository; nothing to unblock, so no transfer is needed",
|
|
3670
|
+
gh.event_name,
|
|
3671
|
+
gh.pr_number,
|
|
3672
|
+
)
|
|
3673
|
+
sys.exit(int(ExitCode.SUCCESS))
|
|
3674
|
+
|
|
3675
|
+
# A sweep leg has nothing to add for a head already transferred
|
|
3676
|
+
if _is_sweep_leg() and _sweep_can_skip(pr_obj, gh):
|
|
3677
|
+
sys.exit(int(ExitCode.SUCCESS))
|
|
3678
|
+
|
|
3679
|
+
allowed, approved_sha = _check_fork_approval(pr_obj, gh, progress_tracker)
|
|
3680
|
+
if not allowed:
|
|
3681
|
+
sys.exit(int(ExitCode.SUCCESS))
|
|
3682
|
+
return gh, pr_obj, approved_sha
|
|
3683
|
+
|
|
3684
|
+
|
|
3483
3685
|
def _handle_single_pr(
|
|
3484
3686
|
data: Inputs,
|
|
3485
3687
|
gh: GitHubContext,
|
|
@@ -3539,42 +3741,9 @@ def _handle_single_pr(
|
|
|
3539
3741
|
|
|
3540
3742
|
# Display PR information with Rich formatting
|
|
3541
3743
|
approved_sha = ""
|
|
3744
|
+
pr_obj: Any | None = None
|
|
3542
3745
|
if gh.pr_number:
|
|
3543
|
-
pr_obj =
|
|
3544
|
-
|
|
3545
|
-
# Fork PRs need a maintainer's approval before anything is
|
|
3546
|
-
# fetched and before the Gerrit key is materialised. Resolve
|
|
3547
|
-
# the PR here when the display step could not, so a missing
|
|
3548
|
-
# token cannot skip the gate.
|
|
3549
|
-
if pr_obj is None:
|
|
3550
|
-
pr_obj = _resolve_pr_for_gate(gh, data)
|
|
3551
|
-
|
|
3552
|
-
# Second chance at the pull request's own metadata, and a
|
|
3553
|
-
# second look at the short-circuit above. An issue_comment
|
|
3554
|
-
# payload carries none of it, so the check before this
|
|
3555
|
-
# depended on _augment_pr_refs_if_needed's API call; that
|
|
3556
|
-
# helper swallows a failure and returns the context
|
|
3557
|
-
# unresolved, while the fetch just above is a separate call
|
|
3558
|
-
# that may well have succeeded. Without this, one transient
|
|
3559
|
-
# failure would let a commenter resubmit an unchanged
|
|
3560
|
-
# same-repository pull request — exactly what the
|
|
3561
|
-
# short-circuit exists to prevent — and would leave the base
|
|
3562
|
-
# ref empty for the transfer that followed.
|
|
3563
|
-
gh = _recover_pr_metadata(gh, pr_obj)
|
|
3564
|
-
if _recheck_has_nothing_to_unblock(gh):
|
|
3565
|
-
log.info(
|
|
3566
|
-
"Re-check (%s) for PR #%s, whose head is in this "
|
|
3567
|
-
"repository; nothing to unblock, so no transfer is needed",
|
|
3568
|
-
gh.event_name,
|
|
3569
|
-
gh.pr_number,
|
|
3570
|
-
)
|
|
3571
|
-
sys.exit(int(ExitCode.SUCCESS))
|
|
3572
|
-
|
|
3573
|
-
allowed, approved_sha = _check_fork_approval(
|
|
3574
|
-
pr_obj, gh, progress_tracker
|
|
3575
|
-
)
|
|
3576
|
-
if not allowed:
|
|
3577
|
-
sys.exit(int(ExitCode.SUCCESS))
|
|
3746
|
+
gh, pr_obj, approved_sha = _gate_single_pr(gh, data, progress_tracker)
|
|
3578
3747
|
|
|
3579
3748
|
# Check for duplicates in single-PR mode (before workspace setup)
|
|
3580
3749
|
if gh.pr_number and not env_bool("SYNC_ALL_OPEN_PRS", False):
|
|
@@ -3591,9 +3760,13 @@ def _handle_single_pr(
|
|
|
3591
3760
|
)
|
|
3592
3761
|
|
|
3593
3762
|
# Run abandoned-PR and Gerrit cleanup if the pipeline was successful
|
|
3594
|
-
# Skip in G2G_NO_GERRIT: no Gerrit server to query
|
|
3763
|
+
# Skip in G2G_NO_GERRIT: no Gerrit server to query. A sweep leg
|
|
3764
|
+
# leaves it to the sweep's cleanup leg, rather than every leg
|
|
3765
|
+
# racing to close and abandon the same things.
|
|
3595
3766
|
if pipeline_success:
|
|
3596
|
-
|
|
3767
|
+
_record_fork_transfer(pr_obj, approved_sha, pushed=result.pushed)
|
|
3768
|
+
if not _is_sweep_leg():
|
|
3769
|
+
_run_gerrit_cleanup_tasks(data, gh, no_gerrit=no_gerrit)
|
|
3597
3770
|
|
|
3598
3771
|
try:
|
|
3599
3772
|
log_api_metrics_summary()
|
|
@@ -381,6 +381,10 @@ class SubmissionResult:
|
|
|
381
381
|
change_numbers: list[str]
|
|
382
382
|
# Associated patch set commit shas in Gerrit (if available).
|
|
383
383
|
commit_shas: list[str]
|
|
384
|
+
# Whether this run pushed to Gerrit. A dry run, and a pull request
|
|
385
|
+
# reconciled against changes already merged or abandoned, succeed
|
|
386
|
+
# without pushing, and must not be taken for a transfer.
|
|
387
|
+
pushed: bool = False
|
|
384
388
|
|
|
385
389
|
|
|
386
390
|
@dataclass(frozen=True)
|
|
@@ -2643,7 +2647,7 @@ class Orchestrator:
|
|
|
2643
2647
|
|
|
2644
2648
|
log.debug("Pipeline complete: %s", result)
|
|
2645
2649
|
self._cleanup_ssh()
|
|
2646
|
-
return result
|
|
2650
|
+
return dataclass_replace(result, pushed=True)
|
|
2647
2651
|
|
|
2648
2652
|
def _determine_operation_mode(
|
|
2649
2653
|
self,
|