github2gerrit 2.3.0__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.3.0 → github2gerrit-2.4.0}/PKG-INFO +12 -2
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/README.md +11 -1
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/action.yaml +20 -4
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/docs/features.md +44 -17
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/models.py +5 -2
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_bulk_fan_out.py +15 -258
- {github2gerrit-2.3.0 → 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.3.0 → github2gerrit-2.4.0}/.aislop/config.yml +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/.editorconfig +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/.gitignore +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/.gitlint +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/.markdownlint.yaml +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/.pre-commit-config.yaml +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/.yamllint +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/LICENSE +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/LICENSES/Apache-2.0.txt +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/REUSE.toml +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/SECURITY.md +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/docs/COMMIT_RULES.md +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/docs/cli.md +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/docs/development.md +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/docs/github2gerrit_token_permissions_classic.png +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/pyproject.toml +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/sitecustomize.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/__init__.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/approvers.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/cli.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/commit_normalization.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/commit_rules.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/config.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/config_ini.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/constants.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/core.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/duplicate_detection.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/error_codes.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/external_api.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_pr_closer.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_query.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_rest.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_ssh.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/gerrit_urls.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/github_api.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/gitreview.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/gitutils.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/mapping_comment.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/netrc.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/orchestrator/__init__.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/orchestrator/reconciliation.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/pr_approval.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/pr_commands.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/pr_content_filter.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/pr_directives.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/project_names.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/reconcile_matcher.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/rich_display.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/rich_logging.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/similarity.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/ssh_agent_setup.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/ssh_common.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/ssh_config_parser.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/ssh_discovery.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/trailers.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/trust.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/src/github2gerrit/utils.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/conftest.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/fixtures/__init__.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/fixtures/make_repo.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/fixtures/ssh_config_samples.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_action_environment_mapping.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_action_outputs.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_action_pr_number_handling.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_action_step_validation.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_approver_sources.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_automation_only.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_cache_isolation.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_change_id_deduplication.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_clean_squash_title.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_cli.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_cli_helpers.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_cli_netrc_options.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_cli_outputs_file.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_cli_url_and_dryrun.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_commit_normalization.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_commit_rules.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_composite_action_coverage.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_config_and_reviewers.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_config_helpers.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_close_pr_policy.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_config_and_errors.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_gerrit_backref_comment.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_gerrit_push_errors.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_gerrit_rest_results.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_integration_fixture_repo.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_prepare_commits.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_shallow_clone.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_ssh_setup.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_core_ssrf_protection.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_dependency_supersession.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_dns_validation_and_no_gerrit.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_duplicate_detection.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_email_case_normalization.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_error_codes.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_external_api_framework.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_final_state_reconciliation.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_force_flag_cli.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_fork_approval_gate.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_fork_pr_hardening.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_fork_transfer_record.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gerrit_change_id_footer.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gerrit_change_status_checks.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gerrit_pr_closer.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gerrit_rest_client.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gerrit_ssh_abandon.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gerrit_urls.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gerrit_urls_more.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_ghe_and_gitreview_args.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_github_api_error_handling.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_github_api_helpers.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_github_api_retry_and_helpers.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gitreview.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_gitutils_helpers.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_issue_157_regressions.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_mapping_comment_additional.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_mapping_comment_digest_and_backref.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_metadata_and_reconciliation.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_metadata_trailer_separation_bug.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_misc_small_coverage.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_netrc.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_orphan_rest_side_effects.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_pr_closed_handling.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_pr_command_authorisation.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_pr_commands.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_pr_content_filter.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_pr_content_filter_integration.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_pr_update_detection.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_project_names.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_reconciliation_extracted_module.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_reconciliation_plan_and_orphans.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_reconciliation_scenarios.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_ssh_agent.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_ssh_agent_ownership.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_ssh_artifact_prevention.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_ssh_common.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_ssh_discovery.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_ssh_discovery_dry_run.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_sync_mypy_pins.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_trailers_additional.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_url_parser.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/test_utils.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/unit/test_config_integration.py +0 -0
- {github2gerrit-2.3.0 → github2gerrit-2.4.0}/tests/unit/test_ssh_config_parser.py +0 -0
- {github2gerrit-2.3.0 → 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,6 +192,12 @@ 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.
|
|
@@ -263,7 +272,8 @@ A composite action cannot start jobs, so here a bulk `workflow_dispatch`
|
|
|
263
272
|
(`PR_NUMBER` of `0`) still processes every open pull request in one job. That
|
|
264
273
|
job does not serialise against the per-pull-request runs events start, so
|
|
265
274
|
avoid triggering one while pull request activity is in flight, or use the
|
|
266
|
-
reusable workflow, which fans the sweep out.
|
|
275
|
+
reusable workflow, which fans the sweep out. For the same reason a `schedule`
|
|
276
|
+
trigger works only through the reusable workflow.
|
|
267
277
|
|
|
268
278
|
### Option C: command-line tool
|
|
269
279
|
|
|
@@ -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,6 +145,12 @@ 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.
|
|
@@ -216,7 +225,8 @@ A composite action cannot start jobs, so here a bulk `workflow_dispatch`
|
|
|
216
225
|
(`PR_NUMBER` of `0`) still processes every open pull request in one job. That
|
|
217
226
|
job does not serialise against the per-pull-request runs events start, so
|
|
218
227
|
avoid triggering one while pull request activity is in flight, or use the
|
|
219
|
-
reusable workflow, which fans the sweep out.
|
|
228
|
+
reusable workflow, which fans the sweep out. For the same reason a `schedule`
|
|
229
|
+
trigger works only through the reusable workflow.
|
|
220
230
|
|
|
221
231
|
### Option C: command-line tool
|
|
222
232
|
|
|
@@ -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"
|
|
@@ -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,13 +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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
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.
|
|
541
567
|
|
|
542
568
|
`pull_request_review` should be **removed** from any workflow that still
|
|
543
569
|
carries it. It can no longer transfer anything: on a fork pull request it runs
|
|
@@ -598,15 +624,16 @@ transferred pull request does not keep displaying a stale block. After the
|
|
|
598
624
|
transfer succeeds it edits the comment once more to say so, recording the
|
|
599
625
|
transferred commit in a hidden marker.
|
|
600
626
|
|
|
601
|
-
A
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
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
|
|
610
637
|
the marker. A dispatch naming the pull request, a push and a
|
|
611
638
|
`@github2gerrit check` comment transfer whatever it says.
|
|
612
639
|
|
|
@@ -27,18 +27,21 @@ RECHECK_EVENTS: frozenset[str] = frozenset(
|
|
|
27
27
|
{
|
|
28
28
|
"issue_comment",
|
|
29
29
|
"pull_request_review",
|
|
30
|
+
"schedule",
|
|
30
31
|
}
|
|
31
32
|
)
|
|
32
33
|
"""Events that ask the tool to look at a pull request again.
|
|
33
34
|
|
|
34
|
-
|
|
35
|
+
None changes the pull request's code, so each maps to UPDATE: an
|
|
35
36
|
existing Gerrit change should gain a patchset rather than a sibling.
|
|
36
37
|
|
|
37
38
|
They exist because the fork approval gate needs a **privileged** run to
|
|
38
39
|
notice that a maintainer has approved. GitHub withholds secrets from
|
|
39
40
|
``pull_request_review`` on a fork pull request, so the approval itself
|
|
40
41
|
cannot be the run that acts on it (see
|
|
41
|
-
:func:`github2gerrit.cli._skip_unprivileged_fork_run`).
|
|
42
|
+
:func:`github2gerrit.cli._skip_unprivileged_fork_run`). A comment
|
|
43
|
+
asks for the re-check; a schedule makes it without anyone asking, one
|
|
44
|
+
pull request per job of the reusable workflow's sweep (#421).
|
|
42
45
|
|
|
43
46
|
The important property is that these are *triggers only*. They decide
|
|
44
47
|
when to re-evaluate, never whether to proceed: authorisation is always
|
|
@@ -18,12 +18,9 @@ from __future__ import annotations
|
|
|
18
18
|
|
|
19
19
|
import copy
|
|
20
20
|
import json
|
|
21
|
-
import math
|
|
22
|
-
import os
|
|
23
|
-
import re
|
|
24
21
|
import shutil
|
|
25
22
|
import subprocess
|
|
26
|
-
from collections.abc import
|
|
23
|
+
from collections.abc import Sequence
|
|
27
24
|
from pathlib import Path
|
|
28
25
|
from typing import Any
|
|
29
26
|
from unittest.mock import MagicMock
|
|
@@ -31,7 +28,15 @@ from unittest.mock import patch
|
|
|
31
28
|
|
|
32
29
|
import pytest
|
|
33
30
|
import typer
|
|
34
|
-
import
|
|
31
|
+
from workflow_harness import REPO
|
|
32
|
+
from workflow_harness import Page
|
|
33
|
+
from workflow_harness import evaluate as _evaluate
|
|
34
|
+
from workflow_harness import load_jobs
|
|
35
|
+
from workflow_harness import matrix_of
|
|
36
|
+
from workflow_harness import render as _render
|
|
37
|
+
from workflow_harness import request_of
|
|
38
|
+
from workflow_harness import run_enumeration
|
|
39
|
+
from workflow_harness import truthy as _truthy
|
|
35
40
|
|
|
36
41
|
from github2gerrit.cli import _check_automation_only
|
|
37
42
|
from github2gerrit.cli import _check_single_pr_duplicates
|
|
@@ -45,13 +50,6 @@ from github2gerrit.models import PROperationMode
|
|
|
45
50
|
from github2gerrit.pr_approval import render_transferred_comment
|
|
46
51
|
|
|
47
52
|
|
|
48
|
-
WORKFLOW = (
|
|
49
|
-
Path(__file__).parent.parent
|
|
50
|
-
/ ".github"
|
|
51
|
-
/ "workflows"
|
|
52
|
-
/ "github2gerrit.yaml"
|
|
53
|
-
)
|
|
54
|
-
REPO = "opendaylight/mdsal"
|
|
55
53
|
HEAD_SHA = "0b2abdcf7bb2fb5ed6620f214968ae2b3c5e70e6"
|
|
56
54
|
ACTION_STEP = "Run github2gerrit composite action"
|
|
57
55
|
LEG_ONLY = {
|
|
@@ -62,172 +60,6 @@ LEG_ONLY = {
|
|
|
62
60
|
"""The action-step keys that make a sweep leg, and all that may differ."""
|
|
63
61
|
|
|
64
62
|
|
|
65
|
-
# ---------------------------------------------------------------------
|
|
66
|
-
# A small evaluator for the subset of GitHub expressions the workflow's
|
|
67
|
-
# conditions and concurrency groups use. Comparing the expressions as
|
|
68
|
-
# strings could not tell whether two groups actually render the same.
|
|
69
|
-
# ---------------------------------------------------------------------
|
|
70
|
-
|
|
71
|
-
_TOKEN = re.compile(
|
|
72
|
-
r"\s*(?:(?P<op>\|\||&&|==|!=|!|\(|\)|,)"
|
|
73
|
-
r"|'(?P<str>(?:[^']|'')*)'"
|
|
74
|
-
r"|(?P<num>\d+)"
|
|
75
|
-
r"|(?P<name>[A-Za-z_][\w-]*(?:\.[\w-]+)*))"
|
|
76
|
-
)
|
|
77
|
-
_TEMPLATE = re.compile(r"\$\{\{(.*?)\}\}", re.DOTALL)
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
def _truthy(value: Any) -> bool:
|
|
81
|
-
if isinstance(value, float) and math.isnan(value):
|
|
82
|
-
return False
|
|
83
|
-
return value not in (None, False, 0, "")
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
def _as_number(value: Any) -> float:
|
|
87
|
-
if value is None:
|
|
88
|
-
return 0.0
|
|
89
|
-
if isinstance(value, bool):
|
|
90
|
-
return float(value)
|
|
91
|
-
if isinstance(value, (int, float)):
|
|
92
|
-
return float(value)
|
|
93
|
-
try:
|
|
94
|
-
return float(str(value).strip() or 0)
|
|
95
|
-
except ValueError:
|
|
96
|
-
return math.nan
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
def _equal(left: Any, right: Any) -> bool:
|
|
100
|
-
# GitHub compares strings case-insensitively and coerces mixed types
|
|
101
|
-
# to numbers.
|
|
102
|
-
if isinstance(left, str) and isinstance(right, str):
|
|
103
|
-
return left.lower() == right.lower()
|
|
104
|
-
if type(left) is type(right):
|
|
105
|
-
return bool(left == right)
|
|
106
|
-
return _as_number(left) == _as_number(right)
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
def _contains(haystack: Any, needle: Any) -> bool:
|
|
110
|
-
if isinstance(haystack, list):
|
|
111
|
-
return any(_equal(item, needle) for item in haystack)
|
|
112
|
-
return str(needle or "").lower() in str(haystack or "").lower()
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
def _format(template: str, *args: Any) -> str:
|
|
116
|
-
return re.sub(
|
|
117
|
-
r"\{(\d+)\}", lambda m: _render_value(args[int(m.group(1))]), template
|
|
118
|
-
)
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
_FUNCTIONS: dict[str, Callable[..., Any]] = {
|
|
122
|
-
"contains": _contains,
|
|
123
|
-
"format": _format,
|
|
124
|
-
"fromJSON": json.loads,
|
|
125
|
-
}
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
class _Expression:
|
|
129
|
-
"""Recursive descent over ``||``, ``&&``, ``==``/``!=``, ``!``."""
|
|
130
|
-
|
|
131
|
-
def __init__(self, text: str, context: dict[str, Any]) -> None:
|
|
132
|
-
self.tokens = [m for m in _TOKEN.finditer(text) if m.group().strip()]
|
|
133
|
-
assert "".join(m.group() for m in self.tokens).strip() == text.strip()
|
|
134
|
-
self.pos = 0
|
|
135
|
-
self.context = context
|
|
136
|
-
|
|
137
|
-
def evaluate(self) -> Any:
|
|
138
|
-
value = self._or()
|
|
139
|
-
assert self.pos == len(self.tokens), "trailing tokens"
|
|
140
|
-
return value
|
|
141
|
-
|
|
142
|
-
def _peek(self, op: str) -> bool:
|
|
143
|
-
return (
|
|
144
|
-
self.pos < len(self.tokens)
|
|
145
|
-
and self.tokens[self.pos].group("op") == op
|
|
146
|
-
)
|
|
147
|
-
|
|
148
|
-
def _take(self, op: str) -> None:
|
|
149
|
-
assert self._peek(op), f"expected {op!r}"
|
|
150
|
-
self.pos += 1
|
|
151
|
-
|
|
152
|
-
def _or(self) -> Any:
|
|
153
|
-
value = self._and()
|
|
154
|
-
while self._peek("||"):
|
|
155
|
-
self._take("||")
|
|
156
|
-
right = self._and()
|
|
157
|
-
value = value if _truthy(value) else right
|
|
158
|
-
return value
|
|
159
|
-
|
|
160
|
-
def _and(self) -> Any:
|
|
161
|
-
value = self._compare()
|
|
162
|
-
while self._peek("&&"):
|
|
163
|
-
self._take("&&")
|
|
164
|
-
right = self._compare()
|
|
165
|
-
value = right if _truthy(value) else value
|
|
166
|
-
return value
|
|
167
|
-
|
|
168
|
-
def _compare(self) -> Any:
|
|
169
|
-
value = self._unary()
|
|
170
|
-
while self._peek("==") or self._peek("!="):
|
|
171
|
-
negate = self._peek("!=")
|
|
172
|
-
self.pos += 1
|
|
173
|
-
right = self._unary()
|
|
174
|
-
value = _equal(value, right) != negate
|
|
175
|
-
return value
|
|
176
|
-
|
|
177
|
-
def _unary(self) -> Any:
|
|
178
|
-
if self._peek("!"):
|
|
179
|
-
self._take("!")
|
|
180
|
-
return not _truthy(self._unary())
|
|
181
|
-
return self._primary()
|
|
182
|
-
|
|
183
|
-
def _primary(self) -> Any:
|
|
184
|
-
if self._peek("("):
|
|
185
|
-
self._take("(")
|
|
186
|
-
value = self._or()
|
|
187
|
-
self._take(")")
|
|
188
|
-
return value
|
|
189
|
-
token = self.tokens[self.pos]
|
|
190
|
-
self.pos += 1
|
|
191
|
-
if token.group("str") is not None:
|
|
192
|
-
return token.group("str").replace("''", "'")
|
|
193
|
-
if token.group("num") is not None:
|
|
194
|
-
return int(token.group("num"))
|
|
195
|
-
name = token.group("name")
|
|
196
|
-
if self._peek("("):
|
|
197
|
-
return _FUNCTIONS[name](*self._arguments())
|
|
198
|
-
literals = {"true": True, "false": False, "null": None}
|
|
199
|
-
return literals.get(name, self.context.get(name))
|
|
200
|
-
|
|
201
|
-
def _arguments(self) -> list[Any]:
|
|
202
|
-
self._take("(")
|
|
203
|
-
args = [self._or()]
|
|
204
|
-
while self._peek(","):
|
|
205
|
-
self._take(",")
|
|
206
|
-
args.append(self._or())
|
|
207
|
-
self._take(")")
|
|
208
|
-
return args
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
def _evaluate(expression: str, context: dict[str, Any]) -> Any:
|
|
212
|
-
match = _TEMPLATE.fullmatch(expression.strip())
|
|
213
|
-
inner = match.group(1) if match else expression
|
|
214
|
-
return _Expression(inner, {"github.repository": REPO, **context}).evaluate()
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
def _render_value(value: Any) -> str:
|
|
218
|
-
if value is None:
|
|
219
|
-
return ""
|
|
220
|
-
if isinstance(value, bool):
|
|
221
|
-
return "true" if value else "false"
|
|
222
|
-
return str(value)
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
def _render(template: str, context: dict[str, Any]) -> str:
|
|
226
|
-
return _TEMPLATE.sub(
|
|
227
|
-
lambda m: _render_value(_evaluate(m.group(1), context)), template
|
|
228
|
-
)
|
|
229
|
-
|
|
230
|
-
|
|
231
63
|
class TestTheEvaluatorItself:
|
|
232
64
|
"""Enough confidence in the evaluator for the tests built on it."""
|
|
233
65
|
|
|
@@ -257,7 +89,7 @@ class TestTheEvaluatorItself:
|
|
|
257
89
|
|
|
258
90
|
@pytest.fixture(scope="module")
|
|
259
91
|
def jobs() -> dict[str, Any]:
|
|
260
|
-
return
|
|
92
|
+
return load_jobs()
|
|
261
93
|
|
|
262
94
|
|
|
263
95
|
def _dispatch(pr_number: str, change_url: str = "") -> dict[str, Any]:
|
|
@@ -465,97 +297,22 @@ class TestTheLegIsTheSingleJob:
|
|
|
465
297
|
# The enumeration script, executed out of the workflow
|
|
466
298
|
# ---------------------------------------------------------------------
|
|
467
299
|
|
|
468
|
-
_CURL_STUB = r"""#!/usr/bin/env bash
|
|
469
|
-
# Stands in for the GraphQL endpoint. Cursors here are page numbers;
|
|
470
|
-
# the real ones are opaque, which the script must not rely on either way.
|
|
471
|
-
python3 -c 'import json, sys; print(json.dumps(sys.argv[1:]))' "$@" \
|
|
472
|
-
>> "${CURL_LOG}"
|
|
473
|
-
args=("$@")
|
|
474
|
-
data=''
|
|
475
|
-
for ((i = 0; i < ${#args[@]}; i++)); do
|
|
476
|
-
if [[ "${args[i]}" == "--data" ]]; then data="${args[i + 1]}"; fi
|
|
477
|
-
done
|
|
478
|
-
if [[ -n "${CURL_FAIL:-}" ]]; then
|
|
479
|
-
echo '{"message": "Bad credentials"}'
|
|
480
|
-
exit 22
|
|
481
|
-
fi
|
|
482
|
-
if [[ -n "${GRAPHQL_ERRORS:-}" ]]; then
|
|
483
|
-
echo '{"data": null, "errors": [{"message": "Resource not accessible"}]}'
|
|
484
|
-
exit 0
|
|
485
|
-
fi
|
|
486
|
-
page=$(( $(jq -r '.variables.after // "0"' <<< "${data}") + 1 ))
|
|
487
|
-
pages=$(find "${CURL_PAGES}" -name '*.json' | wc -l)
|
|
488
|
-
if (( page < pages )); then next=true; else next=false; fi
|
|
489
|
-
jq -nc --slurpfile nodes "${CURL_PAGES}/${page}.json" \
|
|
490
|
-
--argjson next "${next}" --arg cursor "${page}" \
|
|
491
|
-
'{data: {repository: {pullRequests: {nodes: $nodes[0],
|
|
492
|
-
pageInfo: {hasNextPage: $next, endCursor: $cursor}}}}}'
|
|
493
|
-
"""
|
|
494
|
-
|
|
495
300
|
|
|
496
301
|
@pytest.mark.skipif(shutil.which("jq") is None, reason="needs jq")
|
|
497
302
|
class TestTheEnumeration:
|
|
498
303
|
"""The step that builds the sweep's matrix."""
|
|
499
304
|
|
|
500
|
-
def _script(self, jobs: dict[str, Any]) -> str:
|
|
501
|
-
return str(jobs["enumerate"]["steps"][0]["run"])
|
|
502
|
-
|
|
503
305
|
def _run(
|
|
504
306
|
self,
|
|
505
307
|
jobs: dict[str, Any],
|
|
506
308
|
tmp_path: Path,
|
|
507
|
-
pages:
|
|
309
|
+
pages: Sequence[Page],
|
|
508
310
|
**env: str,
|
|
509
311
|
) -> tuple[subprocess.CompletedProcess[str], str, list[list[str]]]:
|
|
510
|
-
|
|
511
|
-
stub_dir.mkdir()
|
|
512
|
-
curl = stub_dir / "curl"
|
|
513
|
-
curl.write_text(_CURL_STUB)
|
|
514
|
-
curl.chmod(0o755)
|
|
515
|
-
page_dir = tmp_path / "pages"
|
|
516
|
-
page_dir.mkdir()
|
|
517
|
-
for index, numbers in enumerate(pages, start=1):
|
|
518
|
-
(page_dir / f"{index}.json").write_text(
|
|
519
|
-
json.dumps([{"number": n} for n in numbers])
|
|
520
|
-
)
|
|
521
|
-
output = tmp_path / "github_output"
|
|
522
|
-
output.touch()
|
|
523
|
-
log = tmp_path / "curl.log"
|
|
524
|
-
log.touch()
|
|
525
|
-
result = subprocess.run(
|
|
526
|
-
["bash", "-c", self._script(jobs)],
|
|
527
|
-
capture_output=True,
|
|
528
|
-
text=True,
|
|
529
|
-
check=False,
|
|
530
|
-
# A cursor that never advances must fail the test, not hang it
|
|
531
|
-
timeout=60,
|
|
532
|
-
env={
|
|
533
|
-
**os.environ,
|
|
534
|
-
"PATH": f"{stub_dir}{os.pathsep}{os.environ['PATH']}",
|
|
535
|
-
"GITHUB_TOKEN": "t0ken",
|
|
536
|
-
"GITHUB_GRAPHQL_URL": "https://api.github.example/graphql",
|
|
537
|
-
"GITHUB_REPOSITORY": REPO,
|
|
538
|
-
"GITHUB_OUTPUT": str(output),
|
|
539
|
-
"G2G_DISABLED": "",
|
|
540
|
-
"CURL_PAGES": str(page_dir),
|
|
541
|
-
"CURL_LOG": str(log),
|
|
542
|
-
**env,
|
|
543
|
-
},
|
|
544
|
-
)
|
|
545
|
-
calls = [json.loads(line) for line in log.read_text().splitlines()]
|
|
546
|
-
return result, output.read_text(), calls
|
|
547
|
-
|
|
548
|
-
@staticmethod
|
|
549
|
-
def _matrix(output: str) -> str:
|
|
550
|
-
return next(
|
|
551
|
-
line.split("=", 1)[1]
|
|
552
|
-
for line in output.splitlines()
|
|
553
|
-
if line.startswith("matrix=")
|
|
554
|
-
)
|
|
312
|
+
return run_enumeration(jobs, tmp_path, pages, **env)
|
|
555
313
|
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
return dict(json.loads(call[call.index("--data") + 1]))
|
|
314
|
+
_matrix = staticmethod(matrix_of)
|
|
315
|
+
_request = staticmethod(request_of)
|
|
559
316
|
|
|
560
317
|
def test_a_leg_per_pull_request_and_one_cleanup(
|
|
561
318
|
self, jobs: dict[str, Any], tmp_path: Path
|
|
@@ -195,6 +195,13 @@ def _third_party_imports() -> set[str]:
|
|
|
195
195
|
import sys
|
|
196
196
|
|
|
197
197
|
first_party = {"github2gerrit", "tests", "fixtures", "conftest"}
|
|
198
|
+
# Shared helpers beside the tests import as top-level modules
|
|
199
|
+
# (pytest puts tests/ on sys.path), so each is first-party too.
|
|
200
|
+
first_party |= {
|
|
201
|
+
Path(relative).stem
|
|
202
|
+
for relative in _tracked_python_files()
|
|
203
|
+
if Path(relative).parent == Path("tests")
|
|
204
|
+
}
|
|
198
205
|
names: set[str] = set()
|
|
199
206
|
for relative in _tracked_python_files():
|
|
200
207
|
if not _scan_selects(relative):
|