pytest-selective 1.2.0__tar.gz → 1.3.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.
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/ci.yml +0 -2
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/pr-title.yml +2 -1
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/public-actions.yml +0 -2
- pytest_selective-1.3.0/.github/workflows/weekly-checkpoint.yml +21 -0
- pytest_selective-1.3.0/.release-please-manifest.json +3 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/CHANGELOG.md +14 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/CONTRIBUTING.md +8 -3
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/PKG-INFO +1 -1
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/action.yml +6 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/plan-lanes/action.yml +8 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/test-impact/action.yml +8 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/ci.md +8 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/configuration.md +8 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/pyproject.toml +1 -1
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/skills/pytest-selective/SKILL.md +71 -18
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/ci.py +24 -1
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/git_operations.py +23 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_audit_declarations.py +13 -2
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_cli.py +29 -4
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_graph.py +37 -12
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/local.py +3 -5
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_repository_ci.py +12 -1
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_audit.py +7 -1
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_audit_declarations.py +10 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_graph.py +83 -0
- pytest_selective-1.3.0/tests/testing/test_pull_request_merge_base.py +83 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_working_tree.py +10 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/uv.lock +1 -1
- pytest_selective-1.2.0/.release-please-manifest.json +0 -3
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.dry-lint/baseline.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.dry-lint/progress.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.dry-lint.toml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.gitattributes +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/dependabot.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/test-lanes.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/validation-inputs.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/docs.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/dry-lint-audit.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/dry-lint-judge.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/dry-lint.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/observe-ci.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/release-please.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/release.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.github/workflows/windows-performance.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.gitignore +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.pre-commit-config.yaml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/.readthedocs.yaml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/AGENTS.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/CLAUDE.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/LICENSE +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/PROVENANCE.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/README.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/RELEASING.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/README.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/pytest-expansion/action.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/actions/validation-gate/action.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/agentsync.toml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/README.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/conftest.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/quint-connect.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/quint.ini +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/runtime_deps.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/selection.qnt +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/selection_driver.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/contracts/selection/test_selection_contract.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/README.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/actions.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/agents.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/banner.png +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/banner.svg +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/brand.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/icon.png +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/logo.svg +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/raster-sources.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/social-preview.png +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/assets/social-preview.svg +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-due-ci.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-due-docs-ci.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-due-docs-widget.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-due-widget.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-input-replays.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-obligation-docs.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-obligation.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-routing.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22-selective-docs.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-22.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/obligation-baseline.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.11.xml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.14.xml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-after.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.11.xml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.14.xml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-control.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor/selective-earlier.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/2026-10-23-windows-saleor.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/benchmarks/windows-runtime.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/ci-toolkit.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/comparison-pytest-impacted.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/evidence.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/local-development.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/pytest-hygiene.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/selection-health.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/shared-plan-execution.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/tutorial.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/tutorials/adoption-checklist.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/tutorials/sidecar-dependencies.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/tutorials/skip-unrelated-ci.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/actions-directory-test-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/codex-selector-correctness-performance-test-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/codex-windows-git-performance-test-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/graph-based-inputs-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/impacted-improvements-test-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/move-action-implementations-test-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/public-action-onboarding-test-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/ratchet-duplicates-zero-test-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/docs/work-in-progress/tickets/rerun-selection-test-plan.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/assets/routing-comparison.svg +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/check_site.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/ecosystem.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/hooks.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/index.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/llms.txt +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/overrides/main.html +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/publishing.md +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/requirements.in +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/documentation/requirements.txt +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/mkdocs.yml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/release-please-config.json +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/benchmark_graph.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/benchmark_parsing.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/benchmark_rust.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/check_docs.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/check_metadata.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/ci_file_config.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/compare_impacted.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/measure_ci_runs.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/public_actions.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/render_brand.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/run_ci_tests.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/smoke_distribution.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/scripts/tutorial.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/skills/pytest-selective/agents/openai.yaml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/__init__.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/__main__.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/ast_imports.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/checkpoint.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/execution.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/gate.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/hygiene.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_audit.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_audit_plugin.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_declarations.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_explanation.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_first_party.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_models.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_name_scope.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_selection.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/input_policy.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/parse_batch.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/plugin.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/py.typed +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/pytest_expansion.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/pytest_plugins.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/pytest_sharding.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/test_inventory.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/typecheck_scope.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/working_tree.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/__init__.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_checkpoint_policy.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_ci_admission.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_ci_measurements.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_ci_test_execution.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_distribution_metadata.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_hygiene.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_public_actions.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_pytest_expansion.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_release_workflow.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_repository_hygiene.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_repository_hygiene.py.gv-dependency.yaml +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/test_tutorial.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/__init__.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/git_repo.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_ast_imports.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_ci.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_gate.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_git_repo.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_global_inputs.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_explanation.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_first_party.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_impact_name_scope.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_parse_batch.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_portability.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_pytest_plugin.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_pytest_plugins.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_pytest_sharding.py +0 -0
- {pytest_selective-1.2.0 → pytest_selective-1.3.0}/tests/testing/test_typecheck_scope.py +0 -0
|
@@ -2,7 +2,8 @@ name: PR title
|
|
|
2
2
|
|
|
3
3
|
# Squash merges use this checked title as the Conventional Commit header.
|
|
4
4
|
on:
|
|
5
|
-
|
|
5
|
+
# Metadata reads need no privileged target event or repository secrets.
|
|
6
|
+
pull_request:
|
|
6
7
|
types: [opened, edited, synchronize, reopened, ready_for_review]
|
|
7
8
|
|
|
8
9
|
permissions:
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
name: Weekly full checkpoint
|
|
2
|
+
'on':
|
|
3
|
+
schedule:
|
|
4
|
+
- cron: 13 3 * * 1
|
|
5
|
+
workflow_dispatch: null
|
|
6
|
+
permissions:
|
|
7
|
+
contents: read
|
|
8
|
+
actions: write
|
|
9
|
+
concurrency:
|
|
10
|
+
group: weekly-full-checkpoint
|
|
11
|
+
cancel-in-progress: false
|
|
12
|
+
jobs:
|
|
13
|
+
dispatch:
|
|
14
|
+
runs-on: ubuntu-latest
|
|
15
|
+
timeout-minutes: 5
|
|
16
|
+
steps:
|
|
17
|
+
- uses: gigaverse-app/ci-esta/actions/weekly-checkpoint@v1
|
|
18
|
+
with:
|
|
19
|
+
workflows: '["ci.yml", "public-actions.yml"]'
|
|
20
|
+
ref: ''
|
|
21
|
+
workflow-inputs: '{}'
|
|
@@ -1,5 +1,19 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## [1.3.0](https://github.com/gigaverse-app/pytest-selective/compare/v1.2.0...v1.3.0) (2026-11-09)
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
### Features
|
|
7
|
+
|
|
8
|
+
* **audit:** show how many tests each written declaration selects ([#124](https://github.com/gigaverse-app/pytest-selective/issues/124)) ([b660301](https://github.com/gigaverse-app/pytest-selective/commit/b660301be82aefb04fdc79202dc83067c28d7eb1))
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
### Bug fixes
|
|
12
|
+
|
|
13
|
+
* agent setup round 3, from a third adoption trial ([#127](https://github.com/gigaverse-app/pytest-selective/issues/127)) ([a82bea9](https://github.com/gigaverse-app/pytest-selective/commit/a82bea9f607abcb3ed68009571202ad8b7726c7e))
|
|
14
|
+
* **selection:** keep every module a pytest11 entry-point name maps to ([#122](https://github.com/gigaverse-app/pytest-selective/issues/122)) ([acb6649](https://github.com/gigaverse-app/pytest-selective/commit/acb66492c6440621387e0506c1d0071a5a48979c))
|
|
15
|
+
* **selection:** plan a pull request against its merge commit's actual base ([#119](https://github.com/gigaverse-app/pytest-selective/issues/119)) ([276cec8](https://github.com/gigaverse-app/pytest-selective/commit/276cec84f65c889a1820fd5a001a3a4aca8b7e0d))
|
|
16
|
+
|
|
3
17
|
## [1.2.0](https://github.com/gigaverse-app/pytest-selective/compare/v1.1.2...v1.2.0) (2026-11-09)
|
|
4
18
|
|
|
5
19
|
|
|
@@ -199,14 +199,19 @@ history or failed planning must execute the full affected lanes and fail the
|
|
|
199
199
|
required planning gate. Ordinary PRs and pushes use selection; manual/release/scheduled/full overrides
|
|
200
200
|
remain complete checkpoints.
|
|
201
201
|
|
|
202
|
-
CI and public-action smoke
|
|
202
|
+
The **Weekly full checkpoint** workflow dispatches CI and public-action smoke
|
|
203
|
+
every Monday using [CI-esta's shared action](https://github.com/gigaverse-app/ci-esta/blob/main/docs/weekly-checkpoints.md).
|
|
204
|
+
Its small wrapper is generated by `ci-esta checkpoint init`; the source guide
|
|
205
|
+
documents schedule, workflow, ref and input overrides. Ordinary runs keep
|
|
203
206
|
the shared affected-file plan across all six OS/Python combinations and minimum
|
|
204
207
|
pytest. `tests/test_tutorial.py` owns the end-to-end tutorial execution and follows
|
|
205
208
|
that plan; there is no second tutorial invocation in each matrix job. JUnit retains
|
|
206
209
|
its test result and timing, and full plans also upload whole-suite coverage.
|
|
207
210
|
|
|
208
|
-
To run
|
|
209
|
-
and choose **Run workflow
|
|
211
|
+
To run both complete checkpoints after a CI change, select **Weekly full checkpoint**
|
|
212
|
+
in GitHub Actions and choose **Run workflow**, then inspect the dispatched CI and
|
|
213
|
+
public-action results. A green dispatcher means both requests were accepted.
|
|
214
|
+
You can also dispatch either target directly. Retrying an ordinary PR run preserves its selection;
|
|
210
215
|
use the manual checkpoint when you need full-suite coverage evidence.
|
|
211
216
|
|
|
212
217
|
## Duplication
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.5
|
|
2
2
|
Name: pytest-selective
|
|
3
|
-
Version: 1.
|
|
3
|
+
Version: 1.3.0
|
|
4
4
|
Summary: Explainable Python test impact analysis: select affected pytest files from Git changes
|
|
5
5
|
Project-URL: Homepage, https://github.com/gigaverse-app/pytest-selective
|
|
6
6
|
Project-URL: Documentation, https://pytest-selective.readthedocs.io/en/latest/
|
|
@@ -14,6 +14,11 @@ inputs:
|
|
|
14
14
|
head:
|
|
15
15
|
description: Exact checked-out commit.
|
|
16
16
|
default: HEAD
|
|
17
|
+
pr-head:
|
|
18
|
+
description: The pull request's head. When the checkout is GitHub's merge of it (refs/pull/N/merge), changes are measured
|
|
19
|
+
from that merge's base-branch parent, so commits that landed on the base branch after `base` are not counted as the
|
|
20
|
+
PR's. Empty (any other event) keeps `base`.
|
|
21
|
+
default: ${{ github.event.pull_request.head.sha }}
|
|
17
22
|
full-roots:
|
|
18
23
|
description: Space-separated complete test roots used if planning cannot prove a narrower scope.
|
|
19
24
|
required: true
|
|
@@ -100,6 +105,7 @@ runs:
|
|
|
100
105
|
config: ${{ inputs.config }}
|
|
101
106
|
base: ${{ inputs.base }}
|
|
102
107
|
head: ${{ inputs.head }}
|
|
108
|
+
pr-head: ${{ inputs.pr-head }}
|
|
103
109
|
full-roots: ${{ inputs.full-roots }}
|
|
104
110
|
full: ${{ inputs.full }}
|
|
105
111
|
extra-files: ${{ inputs.extra-files }}
|
|
@@ -7,6 +7,12 @@ inputs:
|
|
|
7
7
|
base:
|
|
8
8
|
description: Base commit containing the complete change set.
|
|
9
9
|
default: ${{ github.event.pull_request.base.sha || github.event.before }}
|
|
10
|
+
pr-head:
|
|
11
|
+
description: >-
|
|
12
|
+
The pull request's head. When the checkout is GitHub's merge of it (refs/pull/N/merge), changes are measured from
|
|
13
|
+
that merge's base-branch parent, so commits that landed on the base branch after `base` are not counted as the PR's.
|
|
14
|
+
Empty (any other event) keeps `base`.
|
|
15
|
+
default: ${{ github.event.pull_request.head.sha }}
|
|
10
16
|
full:
|
|
11
17
|
description: 'auto preserves selection on PR/push reruns and runs full for manual/scheduled/release checkpoints or test:full. Override with true or false.'
|
|
12
18
|
default: auto
|
|
@@ -40,10 +46,12 @@ runs:
|
|
|
40
46
|
SELECTIVE_CONFIG: ${{ inputs.config }}
|
|
41
47
|
SELECTIVE_BASE: ${{ inputs.base }}
|
|
42
48
|
SELECTIVE_FULL: ${{ inputs.full }}
|
|
49
|
+
SELECTIVE_PR_HEAD: ${{ inputs.pr-head }}
|
|
43
50
|
run: |
|
|
44
51
|
export PYTHONPATH="$SELECTIVE_ACTION/../../src${PYTHONPATH:+:$PYTHONPATH}"
|
|
45
52
|
args=(--config "$SELECTIVE_CONFIG" --base "$SELECTIVE_BASE" --github-output "$GITHUB_OUTPUT")
|
|
46
53
|
args+=(--full "$SELECTIVE_FULL")
|
|
54
|
+
if [ -n "$SELECTIVE_PR_HEAD" ]; then args+=(--pr-head "$SELECTIVE_PR_HEAD"); fi
|
|
47
55
|
uv run --no-project --python 3.14 --with pydantic==2.13.4 --with pyyaml==6.0.3 \
|
|
48
56
|
python -m pytest_selective.ci "${args[@]}" | tee test-lanes.json
|
|
49
57
|
{
|
|
@@ -13,6 +13,12 @@ inputs:
|
|
|
13
13
|
head:
|
|
14
14
|
description: Exact checked-out commit.
|
|
15
15
|
default: HEAD
|
|
16
|
+
pr-head:
|
|
17
|
+
description: >-
|
|
18
|
+
The pull request's head. When the checkout is GitHub's merge of it (refs/pull/N/merge), changes are measured from
|
|
19
|
+
that merge's base-branch parent, so commits that landed on the base branch after `base` are not counted as the PR's.
|
|
20
|
+
Empty (any other event) keeps `base`.
|
|
21
|
+
default: ${{ github.event.pull_request.head.sha }}
|
|
16
22
|
full-roots:
|
|
17
23
|
description: Space-separated complete test roots used if planning cannot prove a narrower scope.
|
|
18
24
|
required: true
|
|
@@ -114,6 +120,7 @@ runs:
|
|
|
114
120
|
IMPACT_CONFIG: ${{ inputs.config }}
|
|
115
121
|
IMPACT_BASE: ${{ inputs.base }}
|
|
116
122
|
IMPACT_HEAD: ${{ inputs.head }}
|
|
123
|
+
IMPACT_PR_HEAD: ${{ inputs.pr-head }}
|
|
117
124
|
IMPACT_FULL: ${{ inputs.full }}
|
|
118
125
|
IMPACT_EXTRA: ${{ inputs.extra-files }}
|
|
119
126
|
IMPACT_ALTERNATE_BASE: ${{ inputs.alternate-base }}
|
|
@@ -147,6 +154,7 @@ runs:
|
|
|
147
154
|
# A reused workspace must not upload a previous plan's explanation.
|
|
148
155
|
rm -f test-impact-explanation.json
|
|
149
156
|
args+=(--full "$IMPACT_FULL")
|
|
157
|
+
if [ -n "${IMPACT_PR_HEAD:-}" ]; then args+=(--pr-head "$IMPACT_PR_HEAD"); fi
|
|
150
158
|
if [ -n "$IMPACT_EXTRA" ]; then args+=(--extra-files "$IMPACT_EXTRA"); fi
|
|
151
159
|
if [ -n "${IMPACT_GRAPH_BASE:-}" ]; then args+=(--graph-base "$IMPACT_GRAPH_BASE"); fi
|
|
152
160
|
# Optional: an unfetchable alternate only forgoes a smaller selection.
|
|
@@ -64,6 +64,14 @@ controls the test interpreter. `full-roots` must contain the complete roots, not
|
|
|
64
64
|
the previous selection. `base` defaults to the PR base or the push's previous
|
|
65
65
|
commit. Override it only when you have validated a different comparison base.
|
|
66
66
|
|
|
67
|
+
On a pull request, GitHub checks out a merge of the PR head into the base
|
|
68
|
+
branch's current tip, which can be newer than the PR's recorded base. Both
|
|
69
|
+
actions pass the PR head (`pr-head`) so the planner measures changes from that
|
|
70
|
+
merge's base-branch parent. Commits that landed on the base branch after the PR
|
|
71
|
+
was opened, such as a release's version bump, are then not counted as the PR's
|
|
72
|
+
own. A shallow checkout or any other event keeps `base`. Check out with
|
|
73
|
+
`fetch-depth: 0` to get the narrower plan.
|
|
74
|
+
|
|
67
75
|
Both file and lane actions default to `full: auto`:
|
|
68
76
|
|
|
69
77
|
| Trigger | Default |
|
|
@@ -22,6 +22,11 @@ select. The default filenames are `test_*.py`, `*_test.py`, `*_tests.py`, and
|
|
|
22
22
|
`tests.py`; configure
|
|
23
23
|
`test_patterns` for a different collection policy.
|
|
24
24
|
|
|
25
|
+
Every `--config` (`affected`, `explain`, `fingerprints`, `audit`, `run` and the rest) also accepts a
|
|
26
|
+
[lane config](ci.md) such as `.github/test-lanes.json`: it uses the lane config's `graph`, with its
|
|
27
|
+
top-level `global_inputs` when declared. Pass the lane config itself rather than a copy of its graph,
|
|
28
|
+
so a later `graph.pytest_policy` refresh is always the one in use.
|
|
29
|
+
|
|
25
30
|
## Review and record collection policy
|
|
26
31
|
|
|
27
32
|
If your repository has `pyproject.toml`, `pytest.ini`, a `tox.ini` with a `[pytest]`
|
|
@@ -61,6 +66,9 @@ fixture example, directory manifests, dependency comments, and dynamic Python in
|
|
|
61
66
|
|
|
62
67
|
A read that names a tracked file by literal path, such as `open("data/prices.json")` or
|
|
63
68
|
`(Path(__file__).parent / "prices.json").read_text()`, is followed automatically as a content read.
|
|
69
|
+
A string that only names a tracked file, such as a routing test's `("bench.py", ...)` row, adds no
|
|
70
|
+
edge: a dotted string is followed as a possible module name (a patch target, a plugin) only when it
|
|
71
|
+
names no tracked file, from the repository root or beside its reader.
|
|
64
72
|
For a path that is computed, or built from several parts, add a reader-owned comment:
|
|
65
73
|
|
|
66
74
|
```python
|
|
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "pytest-selective"
|
|
7
|
-
version = "1.
|
|
7
|
+
version = "1.3.0"
|
|
8
8
|
description = "Explainable Python test impact analysis: select affected pytest files from Git changes"
|
|
9
9
|
readme = "README.md"
|
|
10
10
|
requires-python = ">=3.11"
|
|
@@ -56,7 +56,10 @@ A fresh fork has no runs yet. Read the typical jobs and durations from UPSTREAM
|
|
|
56
56
|
|
|
57
57
|
Record:
|
|
58
58
|
|
|
59
|
-
- **Conventions**: commit and PR title style, changelog rules, AI policy, pre-commit hooks.
|
|
59
|
+
- **Conventions**: commit and PR title style, changelog rules, AI policy, pre-commit hooks. When the pull request
|
|
60
|
+
template names a CONTRIBUTING, CODE_OF_CONDUCT or security file the repository lacks, read the owner's organization
|
|
61
|
+
default from its `.github` repository (in a fork, the upstream owner's):
|
|
62
|
+
`gh api "repos/OWNER/.github/contents/CONTRIBUTING.md" --jq .content | base64 -d`.
|
|
60
63
|
- **Packaging tool**: uv project, Poetry, PDM, Hatch with tox or nox, or plain pip requirements/extras. It decides how
|
|
61
64
|
you run the planner locally and how routing tests install (step 6). CI planning itself installs nothing in the
|
|
62
65
|
project: the actions bring their own Python 3.14 planner.
|
|
@@ -66,7 +69,11 @@ Record:
|
|
|
66
69
|
its matrix size, services, coverage floors and typical duration (`gh run view RUN --repo "$REPO" --json jobs`).
|
|
67
70
|
- **Publish paths**: jobs that publish or deploy (PyPI, Test PyPI, Pages, containers, releases) and every job they
|
|
68
71
|
`needs:`, directly or transitively. Step 4.4 keeps these paths unchanged.
|
|
69
|
-
- **
|
|
72
|
+
- **Test workflows**: the file name of each workflow that runs tests (`test.yml`, `tests.yaml`, `ci.yml`, ...).
|
|
73
|
+
Below, `test.yml` stands for `<the repo's test workflow>`: write the real names in the config, the routing test and
|
|
74
|
+
the report.
|
|
75
|
+
- **Triggers**: whether `push` runs for every branch, and any `branches:`, `paths:` or `paths-ignore:` filters (4.3
|
|
76
|
+
removes path filters from gated workflows).
|
|
70
77
|
- **Existing gate**: an aggregate job (`all-green`, `alls-green`, `check`) and the required check names.
|
|
71
78
|
|
|
72
79
|
If CI already finishes in a couple of minutes, or nearly every change touches source code, say so in your report:
|
|
@@ -84,7 +91,9 @@ VERSION=${TAG#v}
|
|
|
84
91
|
```
|
|
85
92
|
|
|
86
93
|
Local commands use `pytest-selective==$VERSION`; the routing-test job (step 6) installs `pytest-selective>=$VERSION,<2`,
|
|
87
|
-
so it follows the same major version as `@v1`.
|
|
94
|
+
so it follows the same major version as `@v1`. uv caches the package index, so a release published minutes ago can
|
|
95
|
+
be missing (`No solution found`, `pytest-selective==X.Y.Z` unsatisfiable): the local commands below pass
|
|
96
|
+
`--refresh-package pytest-selective`. With pip, add `--no-cache-dir`.
|
|
88
97
|
|
|
89
98
|
Only when the repository's tooling rejects tags (zizmor's `unpinned-uses`, a `pinact` hook, or an all-SHA policy its
|
|
90
99
|
CI enforces) pin the release instead: `@$SHA # $TAG` on every action, with
|
|
@@ -122,7 +131,7 @@ line or a nox `session.install`). CI does not need this.
|
|
|
122
131
|
"global_inputs": ["pyproject.toml", "tox.ini", "requirements/tests.txt"]
|
|
123
132
|
},
|
|
124
133
|
"lanes": {
|
|
125
|
-
"tests": {"roots": ["tests"]},
|
|
134
|
+
"tests": {"roots": ["tests"], "inputs": ["src/toy/*.c", "src/toy/*.pyi"]},
|
|
126
135
|
"bench": {"roots": ["tests/benchmarks"]},
|
|
127
136
|
"package": {"inputs": ["src/**", "pyproject.toml", "README.md", "LICENSE"]},
|
|
128
137
|
"routing": {"roots": ["ci_tests"]}
|
|
@@ -143,6 +152,11 @@ Adapt every path; this is a shape, not an inventory. The rules:
|
|
|
143
152
|
`dir/**/*.py` matches `dir/x.py` and `dir/a/x.py`. Name execution files; never a directory that also holds prose
|
|
144
153
|
(`examples/**` runs the lane for an example's README; use `examples/**/*.py`). A lane without roots runs only on its
|
|
145
154
|
inputs, on global inputs, or when a change reaches every test.
|
|
155
|
+
- **Non-Python sources are not imports.** A C extension (`_speedups.c`), Cython (`.pyx`, `.pxd`), type stubs
|
|
156
|
+
(`.pyi`), templates or data files the tests read: the graph does not see them unless a test opens them by literal
|
|
157
|
+
path. Add each as an input of the lanes that build or test it, as exact globs
|
|
158
|
+
(`"tests": {"roots": ["tests"], "inputs": ["src/pkg/*.c", "src/pkg/*.pyi"]}`) or the package directory (`src/**`)
|
|
159
|
+
when every file there matters, and give each a routing row (step 6).
|
|
146
160
|
- **`graph.global_inputs`** are the environment files whose change can affect every test, and the only place to narrow
|
|
147
161
|
them. Omitted, the built-in list applies: `pyproject.toml`, `setup.cfg`, `setup.py`, `tox.ini`, `pytest.ini`,
|
|
148
162
|
`pytest.toml`, `uv.lock`, `poetry.lock`, `pdm.lock`, `pixi.lock`, `Pipfile`, `Pipfile.lock`, `*requirements*.txt`,
|
|
@@ -166,30 +180,39 @@ Add `.test-impact/` to `.gitignore`: the planner keeps its graph cache there.
|
|
|
166
180
|
|
|
167
181
|
**Collection policy.** If pytest configuration exists (`pyproject.toml`, `pytest.ini`, `pytest.toml`, a `tox.ini`
|
|
168
182
|
with `[pytest]`, a `setup.cfg` with `[tool:pytest]`), the planner runs every lane until you record its fingerprints
|
|
169
|
-
(`full-suite fallback: pytest configuration changed; review collection policy and refresh its
|
|
183
|
+
(`full-suite fallback: pytest configuration changed (pyproject.toml); review collection policy and refresh its
|
|
184
|
+
fingerprints: ...`). Review
|
|
170
185
|
the pytest settings and collection hooks, commit the config, then:
|
|
171
186
|
|
|
172
187
|
```bash
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
uv run --no-project --with "pytest-selective==$VERSION" \
|
|
176
|
-
pytest-selective fingerprints --config .test-impact/graph.json --head HEAD
|
|
188
|
+
uv run --refresh-package pytest-selective --no-project --with "pytest-selective==$VERSION" \
|
|
189
|
+
pytest-selective fingerprints --config .github/test-lanes.json --head HEAD
|
|
177
190
|
```
|
|
178
191
|
|
|
179
|
-
Copy the printed object into `graph.pytest_policy` and commit.
|
|
192
|
+
Copy the printed object into `graph.pytest_policy` and commit. Every `pytest-selective` command (`fingerprints`,
|
|
193
|
+
`explain`, `affected`, `run`) takes the lane config itself as `--config`, so the policy you record is the one in use.
|
|
194
|
+
Releases up to 1.2.0 need a derived file instead: `pytest-selective file-config --lanes .github/test-lanes.json
|
|
195
|
+
--output .test-impact/graph.json` (it needs top-level `global_inputs`), passed as `--config .test-impact/graph.json`.
|
|
196
|
+
Re-run it after every lane-config change, recording `pytest_policy` included, or `explain` keeps failing with
|
|
197
|
+
`pytest configuration changed`. Releases up to 1.1.1 also print a `tox.ini` or
|
|
180
198
|
`setup.cfg` that has no pytest section (pytest ignores it); keeping that entry is harmless. Without uv, run the same
|
|
181
199
|
commands in a virtual environment after `python -m pip install "pytest-selective==$VERSION"`. When pytest settings
|
|
182
200
|
change later, the planner falls back to every lane until someone reviews and refreshes the fingerprints.
|
|
183
201
|
|
|
184
202
|
### Prove the config locally
|
|
185
203
|
|
|
186
|
-
On a throwaway branch
|
|
204
|
+
On a throwaway branch made from the adoption branch, after committing the lane config, commit one probe edit at a
|
|
205
|
+
time and plan it against its parent:
|
|
187
206
|
|
|
188
207
|
```bash
|
|
189
|
-
uv run --no-project --with "pytest-selective==$VERSION" \
|
|
190
|
-
python -m pytest_selective.ci --config .github/test-lanes.json --base
|
|
208
|
+
uv run --refresh-package pytest-selective --no-project --with "pytest-selective==$VERSION" \
|
|
209
|
+
python -m pytest_selective.ci --config .github/test-lanes.json --base HEAD^ --head HEAD
|
|
191
210
|
```
|
|
192
211
|
|
|
212
|
+
Do not plan against `origin/DEFAULT` before the merge: DEFAULT lacks `.github/test-lanes.json`, a global input, so
|
|
213
|
+
every probe would report `global input: .github/test-lanes.json` and select every lane. The parent commit already has
|
|
214
|
+
the config, so the plan sees only the probe edit.
|
|
215
|
+
|
|
193
216
|
Expected: JSON with `lanes` (one boolean per lane), `reason` and `lane_reasons`. A README edit gives every lane
|
|
194
217
|
`false` (or only `package`, when the build embeds the README) with `no affected tests or changed inputs`; a source
|
|
195
218
|
edit selects the lanes of the tests that import it (`affected test: tests/test_x.py`); a lockfile or workflow edit
|
|
@@ -241,6 +264,10 @@ The action also uploads its plan as a `test-lane-selection` artifact, which
|
|
|
241
264
|
fromJSON(needs.select-tests.outputs.lanes).tests) }}
|
|
242
265
|
```
|
|
243
266
|
|
|
267
|
+
A skipped matrix job is listed under its unevaluated name (for example `${{ matrix.name || matrix.python }}`),
|
|
268
|
+
because the matrix never expands. Mention it in the report, or give the job a name that also reads well unexpanded,
|
|
269
|
+
such as `name: tests (${{ matrix.python }})`.
|
|
270
|
+
|
|
244
271
|
`always()` lets the condition run when the planner failed, and the failed-planner branch then runs the job: a broken
|
|
245
272
|
plan must never become a green skip. The condition applies before the matrix expands, so the whole matrix skips
|
|
246
273
|
together. A job with other needs must check them itself, because `always()` also ignores their failure:
|
|
@@ -259,6 +286,10 @@ together. A job with other needs must check them itself, because `always()` also
|
|
|
259
286
|
control at the probe head (step 8), so keep them; cancel the source probe's push runs as soon as they start
|
|
260
287
|
(`gh run cancel RUN --repo "$REPO"`) and judge that probe by its `pull_request` runs. The cost ends once the
|
|
261
288
|
adoption merges.
|
|
289
|
+
- **`paths:` and `paths-ignore:` filters.** Remove them from every workflow that gets a gate; the lanes replace
|
|
290
|
+
them. A required gate in a workflow that a filter skipped never reports, so a docs-only pull request would wait on
|
|
291
|
+
it forever. This is a behavior change: a change the filter skipped (say `docs/**`) now starts the planner, the
|
|
292
|
+
unconditional jobs and the gate. Say so in the report, with its cost (step 10, net effect).
|
|
262
293
|
- **`branches:` filters.** A probe pull request opened against your adoption branch does not start workflows filtered
|
|
263
294
|
to `pull_request: branches: [main]`. Prove those workflows with routing tests and local plans, and say in the report
|
|
264
295
|
that their hosted probe waits for the merge.
|
|
@@ -365,6 +396,7 @@ ROOT = Path(__file__).resolve().parents[1]
|
|
|
365
396
|
CONFIG = LaneConfig.model_validate_json((ROOT / ".github/test-lanes.json").read_text(encoding="utf-8"))
|
|
366
397
|
EVERY = dict.fromkeys(CONFIG.lanes, True)
|
|
367
398
|
NONE = dict.fromkeys(CONFIG.lanes, False)
|
|
399
|
+
# <the repo's test workflow>: tests.yaml, ci.yml, ...
|
|
368
400
|
WORKFLOW = ROOT / ".github/workflows/test.yml"
|
|
369
401
|
PLANNER = "select-tests"
|
|
370
402
|
# The gate job's id. Keep an existing aggregate job's id, for example "success".
|
|
@@ -406,6 +438,8 @@ def repo(tmp_path: Path) -> Path:
|
|
|
406
438
|
("requirements-mypy.txt", NONE),
|
|
407
439
|
("tests/test_other.py", {**NONE, "tests": True}),
|
|
408
440
|
("src/toy/other.py", {**NONE, "tests": True, "package": True}),
|
|
441
|
+
# Non-Python sources the tests exercise (a C extension, stubs), declared as lane inputs.
|
|
442
|
+
("src/toy/_speedups.c", {**NONE, "tests": True, "package": True}),
|
|
409
443
|
("tests/benchmarks/test_bench.py", {**NONE, "tests": True, "bench": True}),
|
|
410
444
|
("tox.ini", EVERY),
|
|
411
445
|
(".github/test-lanes.json", EVERY),
|
|
@@ -446,12 +480,12 @@ def test_the_gate_guards_every_job_and_lane_jobs_run_when_planning_fails() -> No
|
|
|
446
480
|
assert all(f"fromJSON(needs.{PLANNER}.outputs.lanes).{lane}" in condition for lane in lanes), job
|
|
447
481
|
```
|
|
448
482
|
|
|
449
|
-
Add a row for every directory you narrowed, every requirements file, each
|
|
450
|
-
paths. A row appends a comment; a `<workflow>#<job>` input ignores comments, so edit the job itself in that row. With
|
|
483
|
+
Add a row for every directory you narrowed, every requirements file, each non-Python source input, each workflow and
|
|
484
|
+
each lane, using real tracked paths. A row only names its path; it does not make this test depend on that file. A row appends a comment; a `<workflow>#<job>` input ignores comments, so edit the job itself in that row. With
|
|
451
485
|
several workflows, repeat the gate test per workflow with its own `GATE`. Make the file pass the repository's own
|
|
452
486
|
linters, formatter and type checker before committing: for example, ruff's `required-imports` may demand
|
|
453
487
|
`from __future__ import annotations` (the template has it), and black or `ruff format` with a shorter line length
|
|
454
|
-
reflows it. Run it locally with `uv run --no-project --with "pytest-selective==$VERSION" pytest ci_tests -q` and
|
|
488
|
+
reflows it. Run it locally with `uv run --refresh-package pytest-selective --no-project --with "pytest-selective==$VERSION" pytest ci_tests -q` and
|
|
455
489
|
commit it.
|
|
456
490
|
|
|
457
491
|
## 7. Prove it on hosted CI
|
|
@@ -487,8 +521,12 @@ Compare a selected run with a full control **at the same head**:
|
|
|
487
521
|
gh workflow run WORKFLOW --repo "$REPO" --ref PROBE_BRANCH # once per workflow
|
|
488
522
|
```
|
|
489
523
|
|
|
490
|
-
|
|
491
|
-
|
|
524
|
+
Dispatch only the workflows this adoption gates. A workflow without `workflow_dispatch:` returns HTTP 422
|
|
525
|
+
(`Workflow does not have 'workflow_dispatch' trigger`); do not add the trigger to workflows you otherwise leave
|
|
526
|
+
alone. Leave such workflows out of both sides and say the comparison covers only the gated workflows. GitHub
|
|
527
|
+
dispatches only workflows whose file exists on DEFAULT, and may read the trigger from DEFAULT's copy rather than the
|
|
528
|
+
branch's: if a workflow gains `workflow_dispatch:` only in your adoption and the dispatch is refused, measure after
|
|
529
|
+
the merge. A rerun is
|
|
492
530
|
not a control: it keeps the selection. Do not wait on a `test:full` label; creating one is the human's step 9.
|
|
493
531
|
|
|
494
532
|
Both sides then share a head SHA: a `pull_request` run records the branch head as its `head_sha`, although it checks
|
|
@@ -541,6 +579,15 @@ pytest-selective vX.Y.Z: actions at `@v1` (or `@<sha> # vX.Y.Z` where tags are r
|
|
|
541
579
|
| Elapsed seconds | | |
|
|
542
580
|
|
|
543
581
|
Runs: [full](https://github.com/OWNER/NAME/actions/runs/RUN_ID), [selected](https://github.com/OWNER/NAME/actions/runs/RUN_ID). One pair, not an average or a bill.
|
|
582
|
+
Workflows compared: ... (and any left out, such as one without `workflow_dispatch:`).
|
|
583
|
+
|
|
584
|
+
### Net effect
|
|
585
|
+
- Docs-only pull requests: before ... runner-seconds (0 where a removed `paths-ignore` skipped them), after ....
|
|
586
|
+
- Code changes: lane jobs now wait for the planner, about N s.
|
|
587
|
+
- Changes that now skip tests: ... (prose and tooling the old filters did not cover).
|
|
588
|
+
|
|
589
|
+
### Trigger changes (behavior change)
|
|
590
|
+
- Removed `paths-ignore: [...]` from `<the repo's test workflow>`; added `workflow_dispatch:` ... (or "none").
|
|
544
591
|
|
|
545
592
|
### Evidence
|
|
546
593
|
- N routing tests: docs, requirements, source, tests, benchmarks, workflows, missing history, full checkpoint.
|
|
@@ -551,9 +598,15 @@ Runs: [full](https://github.com/OWNER/NAME/actions/runs/RUN_ID), [selected](http
|
|
|
551
598
|
- Label `test:full`; trigger change; scheduled full run.
|
|
552
599
|
|
|
553
600
|
### Not done or not verified
|
|
601
|
+
- Skipped matrix jobs show their unevaluated name (`matrix.name || matrix.python`), if kept.
|
|
554
602
|
- ...
|
|
555
603
|
```
|
|
556
604
|
|
|
605
|
+
The net effect and trigger changes are required. Write them honestly: when CI was already cheap, or a `paths-ignore`
|
|
606
|
+
already skipped docs, say the adoption saves little, what it costs (planner latency on code changes; docs-only runs
|
|
607
|
+
going from 0 to about N runner-seconds), and what it buys (a gate that can be required, and skips for changes the old
|
|
608
|
+
filters missed).
|
|
609
|
+
|
|
557
610
|
For depth: [configuration reference](https://pytest-selective.readthedocs.io/en/latest/docs/configuration/),
|
|
558
611
|
[CI guide](https://pytest-selective.readthedocs.io/en/latest/docs/ci/),
|
|
559
612
|
[documentation-only audit](https://pytest-selective.readthedocs.io/en/latest/docs/tutorials/skip-unrelated-ci/),
|
|
@@ -12,6 +12,7 @@ import yaml
|
|
|
12
12
|
from pydantic import Field, field_validator, model_validator
|
|
13
13
|
|
|
14
14
|
from pytest_selective.checkpoint import full_checkpoint_reason
|
|
15
|
+
from pytest_selective.git_operations import pull_request_merge_base
|
|
15
16
|
from pytest_selective.impact_graph import DependencyDeclarationError, GraphCache, git, matches, within
|
|
16
17
|
from pytest_selective.impact_models import ImpactConfig, ImpactModel
|
|
17
18
|
from pytest_selective.impact_selection import affected
|
|
@@ -63,6 +64,20 @@ class LaneConfig(ImpactModel):
|
|
|
63
64
|
update={"global_inputs": sorted(set(self.graph.global_inputs) | set(self.global_inputs))}
|
|
64
65
|
)
|
|
65
66
|
|
|
67
|
+
@classmethod
|
|
68
|
+
def file_config_at(cls, path: Path) -> ImpactConfig:
|
|
69
|
+
"""The file-selection config at `path`, which may also be a lane config such as `.github/test-lanes.json`.
|
|
70
|
+
|
|
71
|
+
A lane config gives its graph, with its explicit global inputs when it declares them. Reading
|
|
72
|
+
it directly means no derived copy can miss a later `graph.pytest_policy` refresh.
|
|
73
|
+
"""
|
|
74
|
+
text = path.read_text(encoding="utf-8")
|
|
75
|
+
data = json.loads(text)
|
|
76
|
+
if not isinstance(data, dict) or "lanes" not in data:
|
|
77
|
+
return ImpactConfig.model_validate_json(text)
|
|
78
|
+
lanes = cls.model_validate(data)
|
|
79
|
+
return lanes.graph if lanes.global_inputs is None else lanes.file_config()
|
|
80
|
+
|
|
66
81
|
def is_global_input(self, path: str) -> bool:
|
|
67
82
|
if self.global_inputs is not None:
|
|
68
83
|
return matches(path, self.global_inputs)
|
|
@@ -254,6 +269,8 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
254
269
|
parser.add_argument("--repo", type=Path, default=Path("."))
|
|
255
270
|
parser.add_argument("--base", required=True)
|
|
256
271
|
parser.add_argument("--head", default="HEAD")
|
|
272
|
+
# A pull request's head: when `--head` is GitHub's merge of it, plan against the merge's actual base.
|
|
273
|
+
parser.add_argument("--pr-head", default="")
|
|
257
274
|
parser.add_argument(
|
|
258
275
|
"--full", nargs="?", const="true", default="false", type=full_checkpoint_reason, metavar="auto|true|false"
|
|
259
276
|
)
|
|
@@ -261,7 +278,13 @@ def main(argv: list[str] | None = None) -> int:
|
|
|
261
278
|
args = parser.parse_args(argv)
|
|
262
279
|
try:
|
|
263
280
|
config = LaneConfig.model_validate_json(args.config.read_text(encoding="utf-8"))
|
|
264
|
-
|
|
281
|
+
base = pull_request_merge_base(args.repo, args.base, args.head, args.pr_head)
|
|
282
|
+
if base != args.base:
|
|
283
|
+
print(
|
|
284
|
+
f"Planning against the tested merge's base {base}; the event's base {args.base} is behind it.",
|
|
285
|
+
file=sys.stderr,
|
|
286
|
+
)
|
|
287
|
+
result = plan(args.repo, config, base, args.head, full=args.full is not None)
|
|
265
288
|
if args.full is not None:
|
|
266
289
|
result.reason = args.full
|
|
267
290
|
except (ValueError, OSError, DependencyDeclarationError) as error:
|
|
@@ -47,3 +47,26 @@ def git(repo: Path, *args: str, data: bytes | None = None, index: Path | None =
|
|
|
47
47
|
def git_text(repo: Path, *args: str, index: Path | None = None) -> str:
|
|
48
48
|
"""UTF-8 text with universal newlines, independent of the Windows locale."""
|
|
49
49
|
return git(repo, *args, index=index).decode("utf-8").replace("\r\n", "\n").replace("\r", "\n")
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
def pull_request_merge_base(repo: Path, base: str, head: str, pr_head: str) -> str:
|
|
53
|
+
"""
|
|
54
|
+
The base a pull request's merge commit was built on, or `base` when that is not provable.
|
|
55
|
+
|
|
56
|
+
GitHub checks out `refs/pull/N/merge`: the PR head merged into the base branch's current tip.
|
|
57
|
+
The event's `pull_request.base.sha` can be older, so commits that landed on the base branch
|
|
58
|
+
since would read as the PR's own changes: a release's version bump then selects every test.
|
|
59
|
+
When `head` is that merge (two parents, the second the PR head) and `base` is an ancestor of
|
|
60
|
+
the first parent, the first parent is the tested base. A push, a shallow history or any other
|
|
61
|
+
shape keeps `base`, which is never narrower than the truth.
|
|
62
|
+
"""
|
|
63
|
+
if not pr_head:
|
|
64
|
+
return base
|
|
65
|
+
try:
|
|
66
|
+
parents = git_text(repo, "rev-list", "--parents", "-n", "1", head).split()[1:]
|
|
67
|
+
if len(parents) != 2 or parents[1] != git_text(repo, "rev-parse", f"{pr_head}^{{commit}}").strip():
|
|
68
|
+
return base
|
|
69
|
+
git(repo, "merge-base", "--is-ancestor", base, parents[0])
|
|
70
|
+
except subprocess.CalledProcessError:
|
|
71
|
+
return base
|
|
72
|
+
return parents[0]
|
{pytest_selective-1.2.0 → pytest_selective-1.3.0}/src/pytest_selective/impact_audit_declarations.py
RENAMED
|
@@ -8,6 +8,10 @@ input becomes an ``import`` rule, because a test that loads or runs a script out
|
|
|
8
8
|
``content`` rule. Reads under one directory collapse into ``<directory>/**`` only when the test read
|
|
9
9
|
every tracked file under it, so the glob claims no input the run did not see. Every pattern is
|
|
10
10
|
checked against the analysed tree before anything is written.
|
|
11
|
+
|
|
12
|
+
A declaration selects every test that depends on its reader, not only the reader. Each sidecar
|
|
13
|
+
records how many tests that is, so a reader that every test reaches (a test module another test
|
|
14
|
+
imports, or one that registers session plugins) is visible before the declaration is committed.
|
|
11
15
|
"""
|
|
12
16
|
|
|
13
17
|
import json
|
|
@@ -21,6 +25,7 @@ from pytest_selective.impact_declarations import (
|
|
|
21
25
|
match_inputs,
|
|
22
26
|
parse_manifest,
|
|
23
27
|
)
|
|
28
|
+
from pytest_selective.impact_explanation import dependent_tests
|
|
24
29
|
from pytest_selective.impact_models import (
|
|
25
30
|
AuditRecord,
|
|
26
31
|
AuditReport,
|
|
@@ -40,6 +45,8 @@ class SidecarDeclarations(ImpactModel):
|
|
|
40
45
|
rules: list[ManifestDependencyRule]
|
|
41
46
|
#: Pattern (as written, ``//``-rooted) to the undeclared reads it declares.
|
|
42
47
|
covers: dict[str, list[str]]
|
|
48
|
+
#: Test files a change to any declared input selects: the reader and every test depending on it.
|
|
49
|
+
selects: int
|
|
43
50
|
|
|
44
51
|
@property
|
|
45
52
|
def sidecar(self) -> str:
|
|
@@ -48,6 +55,8 @@ class SidecarDeclarations(ImpactModel):
|
|
|
48
55
|
|
|
49
56
|
class DeclarationPlan(ImpactModel):
|
|
50
57
|
sidecars: list[SidecarDeclarations]
|
|
58
|
+
#: Test files in the analysed tree, the scale ``SidecarDeclarations.selects`` is read against.
|
|
59
|
+
total_tests: int
|
|
51
60
|
#: Reads no valid pattern can name (the path itself is not a valid declaration), with the reason.
|
|
52
61
|
skipped: list[str]
|
|
53
62
|
|
|
@@ -82,6 +91,7 @@ def plan_declarations(report: AuditReport, snapshot: Snapshot, records: list[Aud
|
|
|
82
91
|
"""The sidecar rules that would declare every undeclared read in ``report``."""
|
|
83
92
|
files = set(snapshot.files)
|
|
84
93
|
reads = tracked_reads(snapshot, records)
|
|
94
|
+
reach = dependent_tests(snapshot)
|
|
85
95
|
undeclared: dict[str, set[str]] = {}
|
|
86
96
|
for read in report.undeclared:
|
|
87
97
|
undeclared.setdefault(read.test, set()).add(read.input)
|
|
@@ -106,8 +116,9 @@ def plan_declarations(report: AuditReport, snapshot: Snapshot, records: list[Aud
|
|
|
106
116
|
if (patterns := inputs[kind])
|
|
107
117
|
]
|
|
108
118
|
if rules:
|
|
109
|
-
|
|
110
|
-
|
|
119
|
+
selects = max(reach.get(test, 0).bit_count(), 1)
|
|
120
|
+
sidecars.append(SidecarDeclarations(test=test, rules=rules, covers=covers, selects=selects))
|
|
121
|
+
return DeclarationPlan(sidecars=sidecars, skipped=skipped, total_tests=len(snapshot.tests))
|
|
111
122
|
|
|
112
123
|
|
|
113
124
|
def render_rules(rules: list[ManifestDependencyRule]) -> str:
|