pytest-selective 0.4.0__tar.gz → 0.4.1__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (170) hide show
  1. pytest_selective-0.4.1/.release-please-manifest.json +3 -0
  2. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/CHANGELOG.md +7 -0
  3. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/PKG-INFO +1 -1
  4. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/action.yml +7 -2
  5. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/actions/plan-lanes/action.yml +1 -1
  6. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/actions/test-impact/action.yml +6 -2
  7. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/ci-toolkit.md +3 -2
  8. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/ci.md +14 -0
  9. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/tutorials/adoption-checklist.md +1 -1
  10. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/tutorials/skip-unrelated-ci.md +5 -4
  11. pytest_selective-0.4.1/docs/work-in-progress/tickets/rerun-selection-test-plan.md +7 -0
  12. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/pyproject.toml +1 -1
  13. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/skills/pytest-selective/SKILL.md +2 -1
  14. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/skills/pytest-selective/references/adoption.md +2 -2
  15. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/checkpoint.py +20 -9
  16. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/ci.py +7 -3
  17. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_cli.py +15 -4
  18. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_explanation.py +1 -0
  19. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_models.py +3 -0
  20. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_checkpoint_policy.py +38 -4
  21. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/uv.lock +1 -1
  22. pytest_selective-0.4.0/.release-please-manifest.json +0 -3
  23. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.gitattributes +0 -0
  24. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/ISSUE_TEMPLATE/bug_report.yml +0 -0
  25. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/PULL_REQUEST_TEMPLATE.md +0 -0
  26. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/dependabot.yml +0 -0
  27. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/test-lanes.json +0 -0
  28. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/workflows/ci.yml +0 -0
  29. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/workflows/docs.yml +0 -0
  30. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/workflows/observe-ci.yml +0 -0
  31. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/workflows/pr-title.yml +0 -0
  32. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/workflows/public-actions.yml +0 -0
  33. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/workflows/release-please.yml +0 -0
  34. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/workflows/release.yml +0 -0
  35. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.github/workflows/windows-performance.yml +0 -0
  36. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.gitignore +0 -0
  37. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.pre-commit-config.yaml +0 -0
  38. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/.readthedocs.yaml +0 -0
  39. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/AGENTS.md +0 -0
  40. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/CLAUDE.md +0 -0
  41. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/CONTRIBUTING.md +0 -0
  42. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/LICENSE +0 -0
  43. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/PROVENANCE.md +0 -0
  44. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/README.md +0 -0
  45. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/RELEASING.md +0 -0
  46. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/actions/README.md +0 -0
  47. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/actions/pytest-expansion/action.yml +0 -0
  48. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/agentsync.toml +0 -0
  49. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/README.md +0 -0
  50. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/actions.md +0 -0
  51. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/agents.md +0 -0
  52. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/assets/banner.png +0 -0
  53. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/assets/banner.svg +0 -0
  54. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/assets/brand.json +0 -0
  55. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/assets/icon.png +0 -0
  56. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/assets/logo.svg +0 -0
  57. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/assets/raster-sources.json +0 -0
  58. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/assets/social-preview.png +0 -0
  59. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/assets/social-preview.svg +0 -0
  60. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-due-ci.json +0 -0
  61. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-due-docs-ci.json +0 -0
  62. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-due-docs-widget.json +0 -0
  63. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-due-widget.json +0 -0
  64. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-input-replays.json +0 -0
  65. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-obligation-docs.json +0 -0
  66. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-obligation.json +0 -0
  67. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-routing.md +0 -0
  68. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22-selective-docs.json +0 -0
  69. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-22.md +0 -0
  70. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor/obligation-baseline.json +0 -0
  71. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.11.xml +0 -0
  72. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor/selective-after-3.14.xml +0 -0
  73. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor/selective-after.json +0 -0
  74. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.11.xml +0 -0
  75. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor/selective-control-3.14.xml +0 -0
  76. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor/selective-control.json +0 -0
  77. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor/selective-earlier.json +0 -0
  78. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/2026-10-23-windows-saleor.md +0 -0
  79. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/benchmarks/windows-runtime.md +0 -0
  80. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/comparison-pytest-impacted.md +0 -0
  81. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/configuration.md +0 -0
  82. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/evidence.md +0 -0
  83. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/local-development.md +0 -0
  84. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/pytest-hygiene.md +0 -0
  85. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/selection-health.md +0 -0
  86. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/tutorial.md +0 -0
  87. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/tutorials/sidecar-dependencies.md +0 -0
  88. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/work-in-progress/tickets/actions-directory-test-plan.md +0 -0
  89. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/work-in-progress/tickets/codex-selector-correctness-performance-test-plan.md +0 -0
  90. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/work-in-progress/tickets/codex-windows-git-performance-test-plan.md +0 -0
  91. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/work-in-progress/tickets/impacted-improvements-test-plan.md +0 -0
  92. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/work-in-progress/tickets/move-action-implementations-test-plan.md +0 -0
  93. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/docs/work-in-progress/tickets/public-action-onboarding-test-plan.md +0 -0
  94. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/assets/routing-comparison.svg +0 -0
  95. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/check_site.py +0 -0
  96. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/ecosystem.md +0 -0
  97. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/hooks.py +0 -0
  98. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/index.md +0 -0
  99. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/overrides/main.html +0 -0
  100. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/publishing.md +0 -0
  101. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/requirements.in +0 -0
  102. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/documentation/requirements.txt +0 -0
  103. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/mkdocs.yml +0 -0
  104. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/release-please-config.json +0 -0
  105. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/benchmark_graph.py +0 -0
  106. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/benchmark_parsing.py +0 -0
  107. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/benchmark_rust.py +0 -0
  108. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/check_docs.py +0 -0
  109. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/check_metadata.py +0 -0
  110. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/ci_file_config.py +0 -0
  111. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/compare_impacted.py +0 -0
  112. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/measure_ci_runs.py +0 -0
  113. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/public_actions.py +0 -0
  114. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/render_brand.py +0 -0
  115. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/run_ci_tests.py +0 -0
  116. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/smoke_distribution.py +0 -0
  117. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/scripts/tutorial.py +0 -0
  118. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/skills/pytest-selective/agents/openai.yaml +0 -0
  119. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/__init__.py +0 -0
  120. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/__main__.py +0 -0
  121. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/ast_imports.py +0 -0
  122. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/hygiene.py +0 -0
  123. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_audit.py +0 -0
  124. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_audit_plugin.py +0 -0
  125. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_declarations.py +0 -0
  126. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_first_party.py +0 -0
  127. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_graph.py +0 -0
  128. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_name_scope.py +0 -0
  129. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/impact_selection.py +0 -0
  130. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/input_policy.py +0 -0
  131. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/local.py +0 -0
  132. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/parse_batch.py +0 -0
  133. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/plugin.py +0 -0
  134. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/py.typed +0 -0
  135. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/pytest_expansion.py +0 -0
  136. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/pytest_plugins.py +0 -0
  137. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/pytest_sharding.py +0 -0
  138. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/test_inventory.py +0 -0
  139. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/typecheck_scope.py +0 -0
  140. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/src/pytest_selective/working_tree.py +0 -0
  141. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/__init__.py +0 -0
  142. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_ci_admission.py +0 -0
  143. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_ci_measurements.py +0 -0
  144. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_ci_test_execution.py +0 -0
  145. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_distribution_metadata.py +0 -0
  146. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_hygiene.py +0 -0
  147. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_public_actions.py +0 -0
  148. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_pytest_expansion.py +0 -0
  149. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_repository_ci.py +0 -0
  150. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_repository_hygiene.py +0 -0
  151. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_repository_hygiene.py.gv-dependency.yaml +0 -0
  152. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/test_tutorial.py +0 -0
  153. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/__init__.py +0 -0
  154. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/git_repo.py +0 -0
  155. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_ast_imports.py +0 -0
  156. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_ci.py +0 -0
  157. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_git_repo.py +0 -0
  158. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_global_inputs.py +0 -0
  159. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_impact_audit.py +0 -0
  160. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_impact_explanation.py +0 -0
  161. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_impact_first_party.py +0 -0
  162. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_impact_graph.py +0 -0
  163. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_impact_name_scope.py +0 -0
  164. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_parse_batch.py +0 -0
  165. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_portability.py +0 -0
  166. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_pytest_plugin.py +0 -0
  167. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_pytest_plugins.py +0 -0
  168. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_pytest_sharding.py +0 -0
  169. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_typecheck_scope.py +0 -0
  170. {pytest_selective-0.4.0 → pytest_selective-0.4.1}/tests/testing/test_working_tree.py +0 -0
@@ -0,0 +1,3 @@
1
+ {
2
+ ".": "0.4.1"
3
+ }
@@ -1,5 +1,12 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.4.1](https://github.com/gigaverse-app/pytest-selective/compare/v0.4.0...v0.4.1) (2026-10-24)
4
+
5
+
6
+ ### Bug fixes
7
+
8
+ * **selection:** preserve rerun scope and record planner reasons ([29d1df8](https://github.com/gigaverse-app/pytest-selective/commit/29d1df806816203bad1e7a4a53f38b7e75c1d268))
9
+
3
10
  ## [0.4.0](https://github.com/gigaverse-app/pytest-selective/compare/v0.3.0...v0.4.0) (2026-10-24)
4
11
 
5
12
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: pytest-selective
3
- Version: 0.4.0
3
+ Version: 0.4.1
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/
@@ -18,8 +18,8 @@ inputs:
18
18
  description: Space-separated complete test roots used if planning cannot prove a narrower scope.
19
19
  required: true
20
20
  full:
21
- description: auto selects on PRs/pushes and runs full on reruns, manual/scheduled/release events or test:full. Override
22
- with true or false.
21
+ description: auto preserves selection on PR/push reruns and runs full for manual/scheduled/release checkpoints or test:full.
22
+ Override with true or false.
23
23
  default: auto
24
24
  extra-files:
25
25
  description: Optional newline file containing prior failure canaries or additional changed paths.
@@ -59,6 +59,10 @@ inputs:
59
59
  with `head`, so they stay visible after `base` moves to a validated tree. Without it, or without the history, they compare
60
60
  against `base`.
61
61
  default: ${{ github.event.pull_request.base.sha }}
62
+ explanation-format:
63
+ description: 1 preserves the legacy explanation schema; 2 includes the actual planner reasons. Use 2 with a compatible
64
+ reporter.
65
+ default: '1'
62
66
  outputs:
63
67
  base:
64
68
  description: The base the selection was made from; `alternate-base` when it selected fewer tests.
@@ -108,3 +112,4 @@ runs:
108
112
  dependency-token: ${{ inputs.dependency-token }}
109
113
  explanation-artifact: ${{ inputs.explanation-artifact }}
110
114
  graph-base: ${{ inputs.graph-base }}
115
+ explanation-format: ${{ inputs.explanation-format }}
@@ -8,7 +8,7 @@ inputs:
8
8
  description: Base commit containing the complete change set.
9
9
  default: ${{ github.event.pull_request.base.sha || github.event.before }}
10
10
  full:
11
- description: 'auto selects on PRs/pushes and runs full on reruns, manual/scheduled/release events or test:full. Override with true or false.'
11
+ 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
12
  default: auto
13
13
  artifact-name:
14
14
  description: >
@@ -17,7 +17,7 @@ inputs:
17
17
  description: Space-separated complete test roots used if planning cannot prove a narrower scope.
18
18
  required: true
19
19
  full:
20
- description: 'auto selects on PRs/pushes and runs full on reruns, manual/scheduled/release events or test:full. Override with true or false.'
20
+ description: 'auto preserves selection on PR/push reruns and runs full for manual/scheduled/release checkpoints or test:full. Override with true or false.'
21
21
  default: auto
22
22
  extra-files:
23
23
  description: Optional newline file containing prior failure canaries or additional changed paths.
@@ -60,6 +60,9 @@ inputs:
60
60
  its merge base with `head`, so they stay visible after `base` moves to a validated tree.
61
61
  Without it, or without the history, they compare against `base`.
62
62
  default: ${{ github.event.pull_request.base.sha }}
63
+ explanation-format:
64
+ description: '1 preserves the legacy explanation schema; 2 includes the actual planner reasons. Use 2 with a compatible reporter.'
65
+ default: '1'
63
66
  outputs:
64
67
  base:
65
68
  value: ${{ steps.select.outputs.base }}
@@ -123,6 +126,7 @@ runs:
123
126
  # Environment only: the CLI never takes it on argv, and git gets it per process, not in config.
124
127
  IMPACT_DEPENDENCY_TOKEN: ${{ inputs.dependency-token }}
125
128
  IMPACT_GRAPH_BASE: ${{ inputs.graph-base }}
129
+ IMPACT_EXPLANATION_FORMAT: ${{ inputs.explanation-format }}
126
130
  SHARED_ACTION_PATH: ${{ github.action_path }}
127
131
  run: |
128
132
  export PYTHONPATH="$SHARED_ACTION_PATH/../../src${PYTHONPATH:+:$PYTHONPATH}"
@@ -139,7 +143,7 @@ runs:
139
143
  args=(affected --config "$IMPACT_CONFIG" --base "$IMPACT_BASE" --head "$IMPACT_HEAD"
140
144
  --max-shards "$IMPACT_SHARDS" --minimum-files "$IMPACT_MINIMUM" --github-output "$GITHUB_OUTPUT" --output test-impact-report.json
141
145
  --durations "$IMPACT_DURATIONS" --shard-seconds "$IMPACT_SHARD_SECONDS"
142
- --explanation-output test-impact-explanation.json)
146
+ --explanation-output test-impact-explanation.json --explanation-format "${IMPACT_EXPLANATION_FORMAT:-1}")
143
147
  # A reused workspace must not upload a previous plan's explanation.
144
148
  rm -f test-impact-explanation.json
145
149
  args+=(--full "$IMPACT_FULL")
@@ -60,8 +60,9 @@ benchmark pairs can be declared with exact run/attempt/head references through
60
60
  Never turn the fraction of files or lanes skipped into a measured time saving.
61
61
 
62
62
  Ordinary label edits no longer launch the CI validation workflows. Add `test:full`
63
- before the next code update to force full validation, or use an explicit rerun
64
- or manual dispatch for immediate full execution. Ordinary PRs and main pushes
63
+ before the next code update to force full validation, or use a manual dispatch
64
+ for immediate full execution. A PR/push rerun preserves selection and uses the
65
+ original event payload; adding a label afterward may not change that payload. Ordinary PRs and main pushes
65
66
  use dependency selection, including for contract and installed-package tests.
66
67
  Unreferenced documentation runs zero tests; explicit full checkpoints and
67
68
  failed-planning fallbacks retain complete coverage and required gates.
@@ -4,6 +4,20 @@ Plan on committed revisions with complete Git history and a known passing base.
4
4
  Keep a scheduled full-suite run. Do not drop unresolved failures merely by moving
5
5
  the base forward after a failed run.
6
6
 
7
+ PR and push reruns preserve the selection policy for the same base, head and
8
+ inputs. Rerunning does not request the full suite. Use `full: 'true'`, a
9
+ `test:full` label present in the event, or a scheduled/manual/release checkpoint
10
+ for full validation. `full: 'false'` overrides event defaults, while missing
11
+ history and operational uncertainty still retain conservative execution.
12
+
13
+ The planner records its actual reasons once and carries them into its report.
14
+ The CLI explanation format 2 includes `selection_reasons`; `--explanation-format 1`
15
+ omits that field for older strict readers. The public `actions/test-impact@v1`
16
+ action defaults to format 1 for compatibility. A compatible reporter can request
17
+ `explanation-format: '2'`; CI-esta's wrapper does this automatically. Older
18
+ artifacts cannot identify a trigger they never recorded, so readers must say
19
+ that the reason is unavailable rather than infer it from the attempt number.
20
+
7
21
  ## GitHub Actions
8
22
 
9
23
  This job uses the repository's composite action and runs the plan on the checked-out
@@ -12,7 +12,7 @@ services and application installation altogether.
12
12
 
13
13
  Use this checklist in your adoption PR. Mark each stage **verified**, **deferred**
14
14
  or **not applicable**, with a reason. Pair it with the
15
- [CI-esta adoption checklist](https://github.com/gigaverse-app/ci-esta/blob/6a694957568c81929a2b3021064ad9674516fb7b/docs/tutorials/adoption-checklist.md)
15
+ [CI-esta adoption checklist](https://github.com/gigaverse-app/ci-esta/blob/main/docs/tutorials/adoption-checklist.md)
16
16
  for reporting, setup, sharding, history, evidence reuse and security integration.
17
17
  Selection remains opt-in; installing the package alone does not narrow pytest.
18
18
 
@@ -113,7 +113,8 @@ Exercise actual Git commits, not a function that classifies extensions:
113
113
  | Missing history or operational uncertainty | Conservative full execution |
114
114
  | Invalid configuration | Diagnostic and failed required gate |
115
115
  | Ordinary PR or main push containing only unreferenced docs | Zero tests |
116
- | Manual/release checkpoint, explicit rerun, or `test:full` | Full execution |
116
+ | PR/push rerun | Same selection policy as the first attempt |
117
+ | Manual/release checkpoint or explicit `test:full` request | Full execution |
117
118
 
118
119
  Select these routing tests through declared dependencies on configuration,
119
120
  execution workflows, helpers and the inputs they inspect. Include wheel smoke,
@@ -127,9 +128,9 @@ you can enumerate every job, action, and reusable workflow that affects a lane.
127
128
  ## 4. Measure one equivalent pair
128
129
 
129
130
  Open a README-only probe PR. Save the source SHA, tested merge, run ID and attempt
130
- of its successful selected run. Add `test:full`, then explicitly rerun the workflow at that revision. A label
131
- change alone may not trigger CI. The example also forces full execution on
132
- reruns. Do not change source or workflow settings between the paired runs. Compare successful completed runs;
131
+ of its successful selected run. Use a manual full-checkpoint dispatch at that revision.
132
+ A PR/push rerun alone preserves selection; its event payload may also retain the original labels.
133
+ Do not change source or workflow settings between the paired runs. Compare successful completed runs;
133
134
  keep cancelled experiments and observer overhead separate.
134
135
 
135
136
  Use [CI-esta's critical-path tutorial](https://ci-esta.readthedocs.io/en/latest/docs/tutorials/measure-critical-path/)
@@ -0,0 +1,7 @@
1
+ # Rerun selection and explanation reasons
2
+
3
+ - [x] Real Git file and lane CLI controls: PR/push attempts preserve empty and nonempty selection, including paths with spaces.
4
+ - [x] Explicit full input, label and checkpoint event controls retain full work; uncertain event metadata remains conservative.
5
+ - [x] File explanations carry the resolved planner reasons; format 1 omits the additive field for strict legacy readers.
6
+ - [x] Real action-shell tests preserve operational fallback and explanation replacement.
7
+ - [ ] Hosted platform checks and released-action consumer reruns preserve checks and selective scope.
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "pytest-selective"
7
- version = "0.4.0"
7
+ version = "0.4.1"
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"
@@ -100,7 +100,8 @@ before/after table with evidence. Follow the user's existing PR/merge scope.
100
100
  and overlapping runs separately; receipt reuse is a distinct optional decision.
101
101
  Keep the [measured impact](https://github.com/gigaverse-app/ci-esta/blob/main/docs/measured-impact.md)
102
102
  visible in the report, with linked full/selected controls and a collapsed timeline.
103
- File-count reduction alone cannot establish time savings. Preserve full reruns
103
+ File-count reduction alone cannot establish time savings. PR/push reruns preserve selection;
104
+ request a full checkpoint explicitly. Preserve explicit full requests
104
105
  when removing broad label triggers. Bun projects use their native runner and
105
106
  CI-esta's Bun evidence adapter; pytest-selective stays optional for CI-esta.
106
107
  The [92.3% README runner-time reduction](https://github.com/gigaverse-app/pytest-selective/blob/main/docs/benchmarks/2026-10-22-routing.md)
@@ -83,8 +83,8 @@ python -m pytest_selective.ci --repo . --config .github/test-lanes.json \
83
83
 
84
84
  For Actions, use `plan-lanes` with full history and expose its `lanes` job output.
85
85
  An admitted job runs when its lane is true **or planning failed**. Prefer the
86
- shared `full: auto` and `base` defaults: reruns, manual/release runs, schedules
87
- and `test:full` become complete checkpoints; ordinary main pushes use the changed
86
+ shared `full: auto` and `base` defaults: PR/push reruns preserve selection;
87
+ manual/release runs, schedules and `test:full` become complete checkpoints. Ordinary main pushes use the changed
88
88
  base/head trees so docs-only merges also execute zero tests. Preserve deliberate
89
89
  consumer overrides with explicit `full: 'true'` or `full: 'false'`. Use
90
90
  `test_roots_json` when forwarding a file plan; do not split paths on spaces or
@@ -19,26 +19,37 @@ class _PullRequestEvent(BaseModel):
19
19
  pull_request: _PullRequest
20
20
 
21
21
 
22
- def full_checkpoint(value: str) -> bool:
22
+ def full_checkpoint_reason(value: str) -> str | None:
23
23
  """Resolve auto once at planning; uncertain event metadata requires full work.
24
24
 
25
25
  Explicit booleans bypass event policy, but never bypass the planners' missing
26
26
  history/configuration fallbacks. CLI callers remain selective unless they
27
27
  request ``--full`` or ``--full auto``.
28
28
  """
29
- if value in {"true", "false"}:
30
- return value == "true"
29
+ if value == "true":
30
+ return "full suite explicitly requested (--full true)"
31
+ if value == "false":
32
+ return None
31
33
  if value != "auto":
32
34
  raise argparse.ArgumentTypeError("full must be auto, true, or false")
33
35
  try:
34
- if int(os.environ.get("GITHUB_RUN_ATTEMPT", "0")) != 1:
35
- return True
36
36
  event = os.environ.get("GITHUB_EVENT_NAME", "")
37
37
  if event == "push":
38
- return False
38
+ return None
39
+ if event in {"workflow_dispatch", "workflow_call", "schedule", "release"}:
40
+ return f"full suite required by {event} checkpoint"
39
41
  if event != "pull_request":
40
- return True
42
+ return "full suite fallback: unsupported or missing event metadata"
41
43
  payload = _PullRequestEvent.model_validate_json(Path(os.environ["GITHUB_EVENT_PATH"]).read_text())
42
- return any(label.name == "test:full" for label in payload.pull_request.labels)
44
+ return (
45
+ "full suite requested by test:full label"
46
+ if any(label.name == "test:full" for label in payload.pull_request.labels)
47
+ else None
48
+ )
43
49
  except (KeyError, OSError, ValueError, ValidationError):
44
- return True
50
+ return "full suite fallback: pull request event metadata unavailable or invalid"
51
+
52
+
53
+ def full_checkpoint(value: str) -> bool:
54
+ """Compatibility boolean API; the planners retain the reason as well."""
55
+ return full_checkpoint_reason(value) is not None
@@ -11,7 +11,7 @@ from typing import Any
11
11
  import yaml
12
12
  from pydantic import Field, field_validator, model_validator
13
13
 
14
- from pytest_selective.checkpoint import full_checkpoint
14
+ from pytest_selective.checkpoint import full_checkpoint_reason
15
15
  from pytest_selective.impact_graph import DependencyDeclarationError, GraphCache, git, matches, within
16
16
  from pytest_selective.impact_models import ImpactConfig, ImpactModel
17
17
  from pytest_selective.impact_selection import affected
@@ -232,12 +232,16 @@ def main(argv: list[str] | None = None) -> int:
232
232
  parser.add_argument("--repo", type=Path, default=Path("."))
233
233
  parser.add_argument("--base", required=True)
234
234
  parser.add_argument("--head", default="HEAD")
235
- parser.add_argument("--full", nargs="?", const=True, default=False, type=full_checkpoint, metavar="auto|true|false")
235
+ parser.add_argument(
236
+ "--full", nargs="?", const="true", default="false", type=full_checkpoint_reason, metavar="auto|true|false"
237
+ )
236
238
  parser.add_argument("--github-output", type=Path)
237
239
  args = parser.parse_args(argv)
238
240
  try:
239
241
  config = LaneConfig.model_validate_json(args.config.read_text(encoding="utf-8"))
240
- result = plan(args.repo, config, args.base, args.head, full=args.full)
242
+ result = plan(args.repo, config, args.base, args.head, full=args.full is not None)
243
+ if args.full is not None:
244
+ result.reason = args.full
241
245
  except (ValueError, OSError, DependencyDeclarationError) as error:
242
246
  print(f"Invalid lane configuration/dependency: {error}", file=sys.stderr)
243
247
  return 2
@@ -14,7 +14,7 @@ import time
14
14
  from collections.abc import Callable
15
15
  from pathlib import Path
16
16
 
17
- from pytest_selective.checkpoint import full_checkpoint
17
+ from pytest_selective.checkpoint import full_checkpoint_reason
18
18
  from pytest_selective.impact_audit import audit, load_records, summary
19
19
  from pytest_selective.impact_explanation import dependent_tests, explain
20
20
  from pytest_selective.impact_graph import (
@@ -161,7 +161,8 @@ def _write_explanation(
161
161
  print(f"::warning::Test selection explanation unavailable: {error}", file=sys.stderr)
162
162
  return
163
163
  args.explanation_output.parent.mkdir(parents=True, exist_ok=True)
164
- args.explanation_output.write_text(explanation.model_dump_json(indent=2) + "\n", encoding="utf-8")
164
+ excluded = {"selection_reasons"} if args.explanation_format == "1" else set()
165
+ args.explanation_output.write_text(explanation.model_dump_json(indent=2, exclude=excluded) + "\n", encoding="utf-8")
165
166
 
166
167
 
167
168
  def _typecheck_scope(args: argparse.Namespace) -> int:
@@ -291,11 +292,19 @@ def main(argv: list[str] | None = None) -> int:
291
292
  parser.add_argument("--output", type=Path)
292
293
  # affected: why the selection is this large, for the PR sticky (never changes the selection).
293
294
  parser.add_argument("--explanation-output", type=Path)
295
+ parser.add_argument(
296
+ "--explanation-format",
297
+ choices=["1", "2"],
298
+ default="2",
299
+ help="format 1 supports legacy readers; format 2 includes planner reasons",
300
+ )
294
301
  # affected: the PR's base, which floor and widening deltas compare against (its merge base).
295
302
  parser.add_argument("--graph-base", default="")
296
303
  parser.add_argument("--extra-files", type=Path)
297
304
  parser.add_argument("--github-output", type=Path)
298
- parser.add_argument("--full", nargs="?", const=True, default=False, type=full_checkpoint, metavar="auto|true|false")
305
+ parser.add_argument(
306
+ "--full", nargs="?", const="true", default="false", type=full_checkpoint_reason, metavar="auto|true|false"
307
+ )
299
308
  parser.add_argument("--minimum-files", type=int, default=0)
300
309
  parser.add_argument("--max-shards", type=int, default=8)
301
310
  parser.add_argument("--durations", type=Path, default=Path(".test_durations"))
@@ -316,6 +325,8 @@ def main(argv: list[str] | None = None) -> int:
316
325
  # floor-budget: the explanation `affected --explanation-output` wrote for this plan.
317
326
  parser.add_argument("--explanation", type=Path, default=Path("test-impact-explanation.json"))
318
327
  args = parser.parse_args(argv)
328
+ full_reason = args.full
329
+ args.full = full_reason is not None
319
330
  args.base = args.base or ("HEAD" if args.working_tree else "HEAD^")
320
331
  if args.working_tree and args.command not in {"affected", "warm", "dependencies", "explain", "conftest-impact"}:
321
332
  parser.error("--working-tree is supported for affected, warm, dependencies, explain and conftest-impact")
@@ -355,7 +366,7 @@ def main(argv: list[str] | None = None) -> int:
355
366
  snapshot = cache.snapshot(args.repo, args.head, config)
356
367
  report.selected = snapshot.tests
357
368
  report.mode = "full"
358
- report.reasons.append("explicit full-suite event/label")
369
+ report.reasons.append(full_reason)
359
370
  if report.selected and len(report.selected) < args.minimum_files:
360
371
  snapshot = cache.snapshot(args.repo, args.head, config)
361
372
  extras = [test for test in snapshot.tests if test not in report.selected]
@@ -448,6 +448,7 @@ def explain(
448
448
  selected=len(report.selected),
449
449
  total=report.total_files,
450
450
  forced_full=forced_full,
451
+ selection_reasons=report.reasons,
451
452
  graph_base=graph_before.tree,
452
453
  floor_seeds=floor_seeds,
453
454
  routes=_routes(union_edges(before, after), report.seeds, report.selected, floor_seeds, reach_after, after),
@@ -458,6 +458,9 @@ class SelectionExplanation(ImpactModel):
458
458
  #: The complete suite ran because it was requested (a schedule, a dispatch, coverage, a label),
459
459
  #: not because the change reaches every test. Such a run times something other than a PR run.
460
460
  forced_full: bool = False
461
+ #: Planner decisions, including the exact full override, fallback or padding reason.
462
+ #: Older artifacts omit this field; readers must not guess the missing trigger.
463
+ selection_reasons: list[str] = Field(default_factory=list)
461
464
  #: The tree floor and widening deltas compare against: the PR's merge base, else `base`.
462
465
  graph_base: str
463
466
  #: Traversal seeds that are startup-floor files: each alone selects nearly every test.
@@ -15,14 +15,16 @@ from tests.testing.git_repo import commit, init, write
15
15
  [
16
16
  ("pull_request", "1", [], "auto", False),
17
17
  ("push", "1", [], "auto", False),
18
- ("pull_request", "2", [], "auto", True),
18
+ ("pull_request", "2", [], "auto", False),
19
+ ("push", "3", [], "auto", False),
20
+ ("pull_request", "2", ["test:full"], "auto", True),
19
21
  ("pull_request", "1", ["test:full"], "auto", True),
20
22
  ("workflow_dispatch", "1", [], "auto", True),
21
23
  ("workflow_call", "1", [], "auto", True),
22
24
  ("schedule", "1", [], "auto", True),
23
25
  ("release", "1", [], "auto", True),
24
26
  ("unknown", "1", [], "auto", True),
25
- ("pull_request", "invalid", [], "auto", True),
27
+ ("pull_request", "invalid", [], "auto", False),
26
28
  ("pull_request", "1", None, "auto", True),
27
29
  ("schedule", "2", [], "false", False),
28
30
  ("push", "1", [], "true", True),
@@ -60,7 +62,27 @@ def test_checkpoint_policy_drives_real_selection(tmp_path, monkeypatch, planner,
60
62
  str(output),
61
63
  ]
62
64
  if planner == "files":
63
- assert files_main(["affected", *args, "--cache", str(tmp_path / "cache.sqlite3")]) == 0
65
+ explanation = tmp_path / "explanation.json"
66
+ assert (
67
+ files_main(
68
+ [
69
+ "affected",
70
+ *args,
71
+ "--cache",
72
+ str(tmp_path / "cache.sqlite3"),
73
+ "--explanation-output",
74
+ str(explanation),
75
+ ]
76
+ )
77
+ == 0
78
+ )
79
+ reasons = json.loads(explanation.read_text())["selection_reasons"]
80
+ if policy == "true":
81
+ assert "full suite explicitly requested (--full true)" in reasons
82
+ elif policy == "auto" and labels == ["test:full"]:
83
+ assert "full suite requested by test:full label" in reasons
84
+ elif policy == "auto" and event in {"schedule", "workflow_dispatch", "workflow_call", "release"}:
85
+ assert f"full suite required by {event} checkpoint" in reasons
64
86
  assert ("mode=full" if full else "mode=none") in output.read_text()
65
87
  roots = next(
66
88
  line.removeprefix("test_roots_json=")
@@ -73,7 +95,11 @@ def test_checkpoint_policy_drives_real_selection(tmp_path, monkeypatch, planner,
73
95
  assert f"tests={str(full).lower()}" in output.read_text()
74
96
 
75
97
 
76
- def test_json_output_preserves_selected_paths_with_spaces(tmp_path):
98
+ @pytest.mark.parametrize("attempt", ["1", "2", "7"])
99
+ @pytest.mark.parametrize("explanation_format", ["1", "2"])
100
+ def test_json_output_preserves_selected_paths_with_spaces(tmp_path, monkeypatch, attempt, explanation_format):
101
+ monkeypatch.setenv("GITHUB_EVENT_NAME", "push")
102
+ monkeypatch.setenv("GITHUB_RUN_ATTEMPT", attempt)
77
103
  init(tmp_path)
78
104
  test = "tests/test_with spaces.py"
79
105
  write(tmp_path, test, "def test_example(): assert True")
@@ -83,6 +109,7 @@ def test_json_output_preserves_selected_paths_with_spaces(tmp_path):
83
109
  commit(tmp_path)
84
110
  write(tmp_path, "config.json", json.dumps({"module_roots": ["."], "test_roots": ["tests"]}))
85
111
  output = tmp_path / "output"
112
+ explanation = tmp_path / "explanation.json"
86
113
  assert (
87
114
  files_main(
88
115
  [
@@ -93,6 +120,12 @@ def test_json_output_preserves_selected_paths_with_spaces(tmp_path):
93
120
  str(tmp_path / "config.json"),
94
121
  "--base",
95
122
  base,
123
+ "--full",
124
+ "auto",
125
+ "--explanation-output",
126
+ str(explanation),
127
+ "--explanation-format",
128
+ explanation_format,
96
129
  "--github-output",
97
130
  str(output),
98
131
  "--cache",
@@ -104,3 +137,4 @@ def test_json_output_preserves_selected_paths_with_spaces(tmp_path):
104
137
  values = dict(line.split("=", 1) for line in output.read_text().splitlines())
105
138
  assert values["mode"] == "scoped"
106
139
  assert json.loads(values["test_roots_json"]) == [test]
140
+ assert ("selection_reasons" in json.loads(explanation.read_text())) == (explanation_format == "2")
@@ -907,7 +907,7 @@ wheels = [
907
907
 
908
908
  [[package]]
909
909
  name = "pytest-selective"
910
- version = "0.4.0"
910
+ version = "0.4.1"
911
911
  source = { editable = "." }
912
912
  dependencies = [
913
913
  { name = "pydantic" },
@@ -1,3 +0,0 @@
1
- {
2
- ".": "0.4.0"
3
- }