pytest-selective 0.1.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (103) hide show
  1. pytest_selective-0.1.0/.gitattributes +5 -0
  2. pytest_selective-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +29 -0
  3. pytest_selective-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +15 -0
  4. pytest_selective-0.1.0/.github/actions/plan-lanes/action.yml +60 -0
  5. pytest_selective-0.1.0/.github/actions/pytest-expansion/action.yml +44 -0
  6. pytest_selective-0.1.0/.github/actions/test-impact/action.yml +210 -0
  7. pytest_selective-0.1.0/.github/dependabot.yml +16 -0
  8. pytest_selective-0.1.0/.github/test-lanes.json +38 -0
  9. pytest_selective-0.1.0/.github/workflows/ci.yml +119 -0
  10. pytest_selective-0.1.0/.github/workflows/observe-ci.yml +24 -0
  11. pytest_selective-0.1.0/.github/workflows/release.yml +64 -0
  12. pytest_selective-0.1.0/.github/workflows/windows-performance.yml +71 -0
  13. pytest_selective-0.1.0/.gitignore +13 -0
  14. pytest_selective-0.1.0/.pre-commit-config.yaml +19 -0
  15. pytest_selective-0.1.0/AGENTS.md +23 -0
  16. pytest_selective-0.1.0/CHANGELOG.md +34 -0
  17. pytest_selective-0.1.0/CONTRIBUTING.md +63 -0
  18. pytest_selective-0.1.0/LICENSE +202 -0
  19. pytest_selective-0.1.0/PKG-INFO +263 -0
  20. pytest_selective-0.1.0/PROVENANCE.md +23 -0
  21. pytest_selective-0.1.0/README.md +233 -0
  22. pytest_selective-0.1.0/RELEASING.md +63 -0
  23. pytest_selective-0.1.0/docs/README.md +15 -0
  24. pytest_selective-0.1.0/docs/agents.md +72 -0
  25. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-due-ci.json +118 -0
  26. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-due-docs-ci.json +74 -0
  27. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-due-docs-widget.json +32 -0
  28. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-due-widget.json +83 -0
  29. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-input-replays.json +407 -0
  30. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-obligation-docs.json +293 -0
  31. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-obligation.json +293 -0
  32. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-routing.md +89 -0
  33. pytest_selective-0.1.0/docs/benchmarks/2026-10-22-selective-docs.json +139 -0
  34. pytest_selective-0.1.0/docs/benchmarks/2026-10-22.md +179 -0
  35. pytest_selective-0.1.0/docs/benchmarks/windows-runtime.md +93 -0
  36. pytest_selective-0.1.0/docs/ci-toolkit.md +51 -0
  37. pytest_selective-0.1.0/docs/ci.md +164 -0
  38. pytest_selective-0.1.0/docs/comparison-pytest-impacted.md +126 -0
  39. pytest_selective-0.1.0/docs/configuration.md +129 -0
  40. pytest_selective-0.1.0/docs/evidence.md +75 -0
  41. pytest_selective-0.1.0/docs/local-development.md +51 -0
  42. pytest_selective-0.1.0/docs/pytest-hygiene.md +64 -0
  43. pytest_selective-0.1.0/docs/pytest-impacted.md +69 -0
  44. pytest_selective-0.1.0/docs/tutorial.md +59 -0
  45. pytest_selective-0.1.0/docs/work-in-progress/tickets/codex-windows-git-performance-test-plan.md +21 -0
  46. pytest_selective-0.1.0/docs/work-in-progress/tickets/impacted-improvements-test-plan.md +98 -0
  47. pytest_selective-0.1.0/pyproject.toml +68 -0
  48. pytest_selective-0.1.0/scripts/benchmark_parsing.py +57 -0
  49. pytest_selective-0.1.0/scripts/check_docs.py +25 -0
  50. pytest_selective-0.1.0/scripts/check_metadata.py +40 -0
  51. pytest_selective-0.1.0/scripts/compare_impacted.py +129 -0
  52. pytest_selective-0.1.0/scripts/measure_ci_runs.py +79 -0
  53. pytest_selective-0.1.0/scripts/smoke_distribution.py +58 -0
  54. pytest_selective-0.1.0/scripts/tutorial.py +147 -0
  55. pytest_selective-0.1.0/skills/pytest-selective/SKILL.md +84 -0
  56. pytest_selective-0.1.0/skills/pytest-selective/agents/openai.yaml +4 -0
  57. pytest_selective-0.1.0/skills/pytest-selective/references/adoption.md +93 -0
  58. pytest_selective-0.1.0/src/pytest_selective/__init__.py +0 -0
  59. pytest_selective-0.1.0/src/pytest_selective/__main__.py +3 -0
  60. pytest_selective-0.1.0/src/pytest_selective/ast_imports.py +75 -0
  61. pytest_selective-0.1.0/src/pytest_selective/ci.py +242 -0
  62. pytest_selective-0.1.0/src/pytest_selective/hygiene.py +118 -0
  63. pytest_selective-0.1.0/src/pytest_selective/impact_audit.py +104 -0
  64. pytest_selective-0.1.0/src/pytest_selective/impact_audit_plugin.py +217 -0
  65. pytest_selective-0.1.0/src/pytest_selective/impact_cli.py +432 -0
  66. pytest_selective-0.1.0/src/pytest_selective/impact_declarations.py +123 -0
  67. pytest_selective-0.1.0/src/pytest_selective/impact_explanation.py +481 -0
  68. pytest_selective-0.1.0/src/pytest_selective/impact_first_party.py +362 -0
  69. pytest_selective-0.1.0/src/pytest_selective/impact_graph.py +885 -0
  70. pytest_selective-0.1.0/src/pytest_selective/impact_models.py +491 -0
  71. pytest_selective-0.1.0/src/pytest_selective/impact_name_scope.py +498 -0
  72. pytest_selective-0.1.0/src/pytest_selective/impact_selection.py +242 -0
  73. pytest_selective-0.1.0/src/pytest_selective/input_policy.py +27 -0
  74. pytest_selective-0.1.0/src/pytest_selective/local.py +78 -0
  75. pytest_selective-0.1.0/src/pytest_selective/py.typed +0 -0
  76. pytest_selective-0.1.0/src/pytest_selective/pytest_expansion.py +535 -0
  77. pytest_selective-0.1.0/src/pytest_selective/pytest_sharding.py +269 -0
  78. pytest_selective-0.1.0/src/pytest_selective/test_inventory.py +39 -0
  79. pytest_selective-0.1.0/src/pytest_selective/typecheck_scope.py +111 -0
  80. pytest_selective-0.1.0/src/pytest_selective/working_tree.py +43 -0
  81. pytest_selective-0.1.0/tests/__init__.py +0 -0
  82. pytest_selective-0.1.0/tests/test_ci_measurements.py +70 -0
  83. pytest_selective-0.1.0/tests/test_distribution_metadata.py +17 -0
  84. pytest_selective-0.1.0/tests/test_hygiene.py +203 -0
  85. pytest_selective-0.1.0/tests/test_pytest_expansion.py +628 -0
  86. pytest_selective-0.1.0/tests/test_repository_ci.py +46 -0
  87. pytest_selective-0.1.0/tests/test_repository_hygiene.py +9 -0
  88. pytest_selective-0.1.0/tests/test_tutorial.py +72 -0
  89. pytest_selective-0.1.0/tests/testing/__init__.py +0 -0
  90. pytest_selective-0.1.0/tests/testing/git_repo.py +72 -0
  91. pytest_selective-0.1.0/tests/testing/test_ast_imports.py +97 -0
  92. pytest_selective-0.1.0/tests/testing/test_ci.py +253 -0
  93. pytest_selective-0.1.0/tests/testing/test_global_inputs.py +51 -0
  94. pytest_selective-0.1.0/tests/testing/test_impact_audit.py +362 -0
  95. pytest_selective-0.1.0/tests/testing/test_impact_explanation.py +827 -0
  96. pytest_selective-0.1.0/tests/testing/test_impact_first_party.py +640 -0
  97. pytest_selective-0.1.0/tests/testing/test_impact_graph.py +2394 -0
  98. pytest_selective-0.1.0/tests/testing/test_impact_name_scope.py +532 -0
  99. pytest_selective-0.1.0/tests/testing/test_portability.py +51 -0
  100. pytest_selective-0.1.0/tests/testing/test_pytest_sharding.py +237 -0
  101. pytest_selective-0.1.0/tests/testing/test_typecheck_scope.py +169 -0
  102. pytest_selective-0.1.0/tests/testing/test_working_tree.py +212 -0
  103. pytest_selective-0.1.0/uv.lock +1229 -0
@@ -0,0 +1,5 @@
1
+ * text=auto
2
+ *.py text eol=lf
3
+ *.sh text eol=lf
4
+ *.yml text eol=lf
5
+ *.yaml text eol=lf
@@ -0,0 +1,29 @@
1
+ name: Bug report
2
+ description: Report a missed test, unexpected selection, or installation problem.
3
+ body:
4
+ - type: textarea
5
+ id: behavior
6
+ attributes:
7
+ label: Expected and observed behavior
8
+ description: Include selected paths and the test you expected to run.
9
+ validations:
10
+ required: true
11
+ - type: textarea
12
+ id: reproduction
13
+ attributes:
14
+ label: Minimal reproduction
15
+ description: Provide a small repository or commits, config, and the exact command. Remove secrets and private code.
16
+ validations:
17
+ required: true
18
+ - type: textarea
19
+ id: environment
20
+ attributes:
21
+ label: Environment
22
+ description: Package commit/version, Python, pytest, Git, operating system, and relevant plugins.
23
+ validations:
24
+ required: true
25
+ - type: textarea
26
+ id: evidence
27
+ attributes:
28
+ label: Selection report and full-suite result
29
+ description: Include sanitized JSON, explanation, or logs. State whether the complete suite catches the failure.
@@ -0,0 +1,15 @@
1
+ ## Summary
2
+
3
+ Explain the problem and the resulting behavior.
4
+
5
+ ## Validation
6
+
7
+ List commands actually run, their results, and evidence links when applicable.
8
+
9
+ ## Selection safety
10
+
11
+ Describe effects on dependency completeness, fallbacks, and existing users.
12
+
13
+ - [ ] Tests cover the behavior at the affected boundary.
14
+ - [ ] Documentation and changelog describe public changes.
15
+ - [ ] Claims distinguish measurements from estimates and limitations.
@@ -0,0 +1,60 @@
1
+ name: Select complete test lanes
2
+ description: Skip unaffected CI lanes while preserving complete conformance runs and full-suite fallbacks.
3
+ inputs:
4
+ config:
5
+ description: Repository-owned lane and dependency graph configuration.
6
+ required: true
7
+ base:
8
+ description: Base commit containing the complete change set.
9
+ required: true
10
+ full:
11
+ description: Run every lane for main, schedules, or an explicit full-test request.
12
+ default: 'false'
13
+ artifact-name:
14
+ description: >
15
+ Upload the lane plan under this name, where CI-esta's report renders it as
16
+ the sticky's lane section. Empty skips the upload.
17
+ default: test-lane-selection
18
+ outputs:
19
+ lanes:
20
+ description: JSON object mapping lane names to booleans.
21
+ value: ${{ steps.plan.outputs.lanes }}
22
+ runs:
23
+ using: composite
24
+ steps:
25
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
26
+ with:
27
+ enable-cache: false
28
+ - uses: actions/cache@v5
29
+ continue-on-error: true
30
+ with:
31
+ path: .test-impact
32
+ key: pytest-selective-lanes-${{ runner.os }}-${{ github.sha }}-${{ github.run_attempt }}
33
+ restore-keys: pytest-selective-lanes-${{ runner.os }}-
34
+ - id: plan
35
+ shell: bash
36
+ env:
37
+ SELECTIVE_ACTION: ${{ github.action_path }}
38
+ SELECTIVE_CONFIG: ${{ inputs.config }}
39
+ SELECTIVE_BASE: ${{ inputs.base }}
40
+ SELECTIVE_FULL: ${{ inputs.full }}
41
+ run: |
42
+ export PYTHONPATH="$SELECTIVE_ACTION/../../../src${PYTHONPATH:+:$PYTHONPATH}"
43
+ args=(--config "$SELECTIVE_CONFIG" --base "$SELECTIVE_BASE" --github-output "$GITHUB_OUTPUT")
44
+ if [ "$SELECTIVE_FULL" = true ]; then args+=(--full); fi
45
+ uv run --no-project --python 3.14 --with pydantic==2.13.4 --with pyyaml==6.0.3 \
46
+ python -m pytest_selective.ci "${args[@]}" | tee test-lanes.json
47
+ {
48
+ echo '### Test lane selection'
49
+ echo '```json'
50
+ cat test-lanes.json
51
+ echo '```'
52
+ } >> "$GITHUB_STEP_SUMMARY"
53
+ - if: ${{ inputs.artifact-name != '' }}
54
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
55
+ with:
56
+ name: ${{ inputs.artifact-name }}
57
+ path: test-lanes.json
58
+ if-no-files-found: error
59
+ # A rerun of the job replaces its plan instead of failing on the existing name.
60
+ overwrite: true
@@ -0,0 +1,44 @@
1
+ name: Explain global pytest expansion
2
+ description: Reject unexplained conftest expansion before dependency setup or test allocation; uses the caller's checkout and Python.
3
+ inputs:
4
+ python-executable:
5
+ description: Python interpreter at least as new as the inspected source syntax.
6
+ default: python3.14
7
+ base:
8
+ description: Full PR base or push-before ref; never an incremental test-selection baseline.
9
+ required: true
10
+ head:
11
+ description: Checked-out commit to inspect.
12
+ default: HEAD
13
+ module-roots:
14
+ description: Space-separated Python source roots, in import order.
15
+ default: '.'
16
+ project-import-prefixes:
17
+ description: Space-separated application/test module prefixes requiring reasons for new imports.
18
+ required: true
19
+ follow-import-prefixes:
20
+ description: Space-separated helper/plugin module prefixes to check transitively when loaded.
21
+ default: ''
22
+ runs:
23
+ using: composite
24
+ steps:
25
+ - name: Check global pytest expansion
26
+ shell: bash
27
+ env:
28
+ EXPANSION_PYTHON: ${{ inputs.python-executable }}
29
+ EXPANSION_BASE: ${{ inputs.base }}
30
+ EXPANSION_HEAD: ${{ inputs.head }}
31
+ EXPANSION_MODULE_ROOTS: ${{ inputs.module-roots }}
32
+ EXPANSION_PROJECT_PREFIXES: ${{ inputs.project-import-prefixes }}
33
+ EXPANSION_FOLLOW_PREFIXES: ${{ inputs.follow-import-prefixes }}
34
+ SHARED_ACTION_PATH: ${{ github.action_path }}
35
+ run: |
36
+ # No install, checkout, or fail-open fallback: missing history is an error.
37
+ args=(--repo "$GITHUB_WORKSPACE" --base "$EXPANSION_BASE" --head "$EXPANSION_HEAD")
38
+ read -r -a roots <<< "$EXPANSION_MODULE_ROOTS"
39
+ read -r -a projects <<< "$EXPANSION_PROJECT_PREFIXES"
40
+ read -r -a helpers <<< "$EXPANSION_FOLLOW_PREFIXES"
41
+ for root in "${roots[@]}"; do args+=(--module-root "$root"); done
42
+ for prefix in "${projects[@]}"; do args+=(--project-import-prefix "$prefix"); done
43
+ for prefix in "${helpers[@]}"; do args+=(--follow-import-prefix "$prefix"); done
44
+ "$EXPANSION_PYTHON" "$SHARED_ACTION_PATH/../../../src/pytest_selective/pytest_expansion.py" "${args[@]}"
@@ -0,0 +1,210 @@
1
+ name: Cached test impact selection
2
+ description: Select affected Python test files from both Git trees, falling back to all configured roots.
3
+ inputs:
4
+ warm-only:
5
+ description: Populate the checked-out merged-tree cache without selecting or running tests.
6
+ default: 'false'
7
+ config:
8
+ description: Consumer-owned dependency graph configuration.
9
+ required: true
10
+ base:
11
+ description: Complete change-set base (a validated ancestor, PR base, or push before SHA).
12
+ required: true
13
+ head:
14
+ description: Exact checked-out commit.
15
+ default: HEAD
16
+ full-roots:
17
+ description: Space-separated complete test roots used if planning cannot prove a narrower scope.
18
+ required: true
19
+ full:
20
+ description: Force the complete suite for schedules, explicit labels, or coverage checkpoints.
21
+ default: 'false'
22
+ extra-files:
23
+ description: Optional newline file containing prior failure canaries or additional changed paths.
24
+ default: ''
25
+ alternate-base:
26
+ description: >-
27
+ A second proven base, normally the PR base when `base` is a previously validated tree.
28
+ Both are selected and the smaller selection wins: after main is merged into a validated
29
+ PR, the delta since that tree carries main's own changes and can reach every test.
30
+ default: ''
31
+ alternate-extra-files:
32
+ description: Newline file of extra paths for `alternate-base` (its unresolved failures and canaries).
33
+ default: ''
34
+ minimum-files:
35
+ description: Minimum nonempty selection for consumers with fixed shard counts.
36
+ default: '0'
37
+ max-shards:
38
+ description: Maximum parallel pytest processes.
39
+ default: '8'
40
+ durations:
41
+ description: pytest-split duration manifest used to size the shard pool; absent or unreadable falls back to file-count sizing.
42
+ default: '.test_durations'
43
+ shard-seconds:
44
+ description: Target wall time per shard. Shard count is the selected work divided by this, bounded by max-shards.
45
+ default: '120'
46
+ dependency-token:
47
+ description: >-
48
+ Read access to the repositories of first-party packages declared in `first_party_packages`.
49
+ With it, a lock change that only moves such a pin selects the tests reaching the modules that
50
+ moved instead of every test. Without it, or when the fetch fails, lock changes stay global.
51
+ default: ''
52
+ explanation-artifact:
53
+ description: >-
54
+ Artifact name for why the selection is this large, which the ci-observability action renders
55
+ into the PR sticky. Writing it never changes the selection. Empty skips the upload.
56
+ default: test-impact-explanation
57
+ graph-base:
58
+ description: >-
59
+ The PR's base commit. The explanation measures startup-floor and blast-radius changes from
60
+ its merge base with `head`, so they stay visible after `base` moves to a validated tree.
61
+ Without it, or without the history, they compare against `base`.
62
+ default: ${{ github.event.pull_request.base.sha }}
63
+ outputs:
64
+ base:
65
+ value: ${{ steps.select.outputs.base }}
66
+ description: The base the selection was made from; `alternate-base` when it selected fewer tests.
67
+ mode:
68
+ value: ${{ steps.select.outputs.mode }}
69
+ description: full, scoped, or none.
70
+ test_roots:
71
+ value: ${{ steps.select.outputs.test_roots }}
72
+ description: Exact selected files, or complete roots on fallback.
73
+ shard_count:
74
+ value: ${{ steps.select.outputs.shard_count }}
75
+ description: Number of shards.
76
+ groups:
77
+ value: ${{ steps.select.outputs.groups }}
78
+ description: JSON shard indexes.
79
+ reason:
80
+ value: ${{ steps.select.outputs.reason }}
81
+ description: Selection summary.
82
+ dependencies-unchanged:
83
+ value: ${{ steps.select.outputs.dependencies_unchanged }}
84
+ description: >-
85
+ 'true' only when every declared first-party lock and manifest is unchanged, or differs only
86
+ by pins whose installed files are byte-identical. Lanes that treat a lock change as "every
87
+ runtime changed" may skip on it. 'false' on any doubt, including every fallback.
88
+ runs:
89
+ using: composite
90
+ steps:
91
+ - uses: astral-sh/setup-uv@20cfd1bf945f4377ade1205e4dbc17946fc9a30d # v10.0.1
92
+ with:
93
+ enable-cache: false
94
+ - name: Restore parsed imports and resolved graphs
95
+ id: graph-cache
96
+ continue-on-error: true
97
+ uses: actions/cache/restore@caa296126883cff596d87d8935842f9db880ef25 # v5
98
+ with:
99
+ path: .test-impact
100
+ key: test-impact-v1-${{ runner.os }}-${{ github.sha }}-${{ github.run_attempt }}
101
+ restore-keys: |
102
+ test-impact-v1-${{ runner.os }}-
103
+ - name: Select affected files
104
+ id: select
105
+ shell: bash
106
+ env:
107
+ IMPACT_WARM_ONLY: ${{ inputs.warm-only }}
108
+ IMPACT_CONFIG: ${{ inputs.config }}
109
+ IMPACT_BASE: ${{ inputs.base }}
110
+ IMPACT_HEAD: ${{ inputs.head }}
111
+ IMPACT_FULL: ${{ inputs.full }}
112
+ IMPACT_EXTRA: ${{ inputs.extra-files }}
113
+ IMPACT_ALTERNATE_BASE: ${{ inputs.alternate-base }}
114
+ IMPACT_ALTERNATE_EXTRA: ${{ inputs.alternate-extra-files }}
115
+ IMPACT_ROOTS: ${{ inputs.full-roots }}
116
+ IMPACT_MINIMUM: ${{ inputs.minimum-files }}
117
+ IMPACT_SHARDS: ${{ inputs.max-shards }}
118
+ IMPACT_DURATIONS: ${{ inputs.durations }}
119
+ IMPACT_SHARD_SECONDS: ${{ inputs.shard-seconds }}
120
+ # Environment only: the CLI never takes it on argv, and git gets it per process, not in config.
121
+ IMPACT_DEPENDENCY_TOKEN: ${{ inputs.dependency-token }}
122
+ IMPACT_GRAPH_BASE: ${{ inputs.graph-base }}
123
+ SHARED_ACTION_PATH: ${{ github.action_path }}
124
+ run: |
125
+ export PYTHONPATH="$SHARED_ACTION_PATH/../../../src${PYTHONPATH:+:$PYTHONPATH}"
126
+ # Both modes must use the same isolated parser runtime, including YAML sidecars.
127
+ impact=(uv run --no-project --python 3.14 --with pydantic==2.13.4 --with pyyaml==6.0.3 python -m pytest_selective.impact_cli)
128
+ if [ "${IMPACT_WARM_ONLY:-false}" = true ]; then
129
+ if "${impact[@]}" warm --config "$IMPACT_CONFIG" --head "$IMPACT_HEAD"; then
130
+ echo "Merged-tree test impact cache is ready" >> "$GITHUB_STEP_SUMMARY"
131
+ else
132
+ echo "::warning::Test impact cache warming failed; future runs will rebuild it"
133
+ fi
134
+ exit 0
135
+ fi
136
+ args=(affected --config "$IMPACT_CONFIG" --base "$IMPACT_BASE" --head "$IMPACT_HEAD"
137
+ --max-shards "$IMPACT_SHARDS" --minimum-files "$IMPACT_MINIMUM" --github-output "$GITHUB_OUTPUT" --output test-impact-report.json
138
+ --durations "$IMPACT_DURATIONS" --shard-seconds "$IMPACT_SHARD_SECONDS"
139
+ --explanation-output test-impact-explanation.json)
140
+ # A reused workspace must not upload a previous plan's explanation.
141
+ rm -f test-impact-explanation.json
142
+ if [ "$IMPACT_FULL" = true ]; then args+=(--full); fi
143
+ if [ -n "$IMPACT_EXTRA" ]; then args+=(--extra-files "$IMPACT_EXTRA"); fi
144
+ if [ -n "${IMPACT_GRAPH_BASE:-}" ]; then args+=(--graph-base "$IMPACT_GRAPH_BASE"); fi
145
+ # Optional: an unfetchable alternate only forgoes a smaller selection.
146
+ if [ -n "${IMPACT_ALTERNATE_BASE:-}" ] && [ "$IMPACT_FULL" != true ] &&
147
+ git fetch --no-tags --depth=1 origin "$IMPACT_ALTERNATE_BASE"; then
148
+ args+=(--alternate-base "$IMPACT_ALTERNATE_BASE")
149
+ if [ -n "${IMPACT_ALTERNATE_EXTRA:-}" ]; then args+=(--alternate-extra-files "$IMPACT_ALTERNATE_EXTRA"); fi
150
+ fi
151
+ if [ -n "$IMPACT_BASE" ] && git fetch --no-tags --depth=1 origin "$IMPACT_BASE" &&
152
+ "${impact[@]}" "${args[@]}"; then
153
+ python3 - <<'PY'
154
+ import json, os
155
+ from pathlib import Path
156
+ report = json.loads(Path("test-impact-report.json").read_text())
157
+ summary = f"Test impact: {len(report['selected'])}/{report['total_files']} files; {report['mode']}; {report['elapsed_seconds']:.2f}s planning"
158
+ print(summary)
159
+ with open(os.environ["GITHUB_STEP_SUMMARY"], "a") as output:
160
+ output.write(summary + "\n")
161
+ PY
162
+ else
163
+ status=$?
164
+ if [ "$status" -eq 2 ]; then
165
+ echo "::error::Invalid test dependency or unparseable Python; fix the reported reader and path"
166
+ exit 2
167
+ fi
168
+ echo "::warning::Test impact proof unavailable; running all configured test roots"
169
+ {
170
+ echo "base=$IMPACT_BASE"
171
+ echo "mode=full"
172
+ echo "test_roots=$IMPACT_ROOTS"
173
+ echo "shard_count=$IMPACT_SHARDS"
174
+ echo "groups=$(python3 -c 'import json,os; print(json.dumps(list(range(1,int(os.environ["IMPACT_SHARDS"])+1))))')"
175
+ echo "reason=Test impact proof unavailable; complete suite fallback"
176
+ echo "dependencies_unchanged=false"
177
+ } >> "$GITHUB_OUTPUT"
178
+ # Replaces an earlier attempt's explanation, which describes a plan this attempt did not run.
179
+ echo '{"unavailable": "Test impact proof unavailable; complete suite fallback"}' > test-impact-explanation.json
180
+ fi
181
+ # A successful plan uploads its explanation; a fallback uploads the marker that replaces one.
182
+ - name: Upload selection explanation
183
+ if: ${{ inputs.warm-only != 'true' && inputs.explanation-artifact != '' && hashFiles('test-impact-explanation.json') != '' }}
184
+ continue-on-error: true
185
+ uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7
186
+ with:
187
+ name: ${{ inputs.explanation-artifact }}
188
+ path: test-impact-explanation.json
189
+ overwrite: true
190
+ retention-days: 7
191
+ - name: Save graph cache
192
+ if: ${{ steps.graph-cache.outputs.cache-hit != 'true' && hashFiles('.test-impact/cache.sqlite3') != '' }}
193
+ continue-on-error: true
194
+ uses: actions/cache/save@caa296126883cff596d87d8935842f9db880ef25 # v5
195
+ with:
196
+ path: .test-impact
197
+ key: test-impact-v1-${{ runner.os }}-${{ github.sha }}-${{ github.run_attempt }}
198
+ # Last, so a failure here costs neither the explanation upload nor the cache: it fails the plan
199
+ # when this PR grows the startup floor past the config's `floor_budget` (no budget, no check).
200
+ - name: Check the startup-floor budget
201
+ if: ${{ inputs.warm-only != 'true' && hashFiles('test-impact-explanation.json') != '' }}
202
+ shell: bash
203
+ env:
204
+ IMPACT_CONFIG: ${{ inputs.config }}
205
+ SHARED_ACTION_PATH: ${{ github.action_path }}
206
+ run: |
207
+ export PYTHONPATH="$SHARED_ACTION_PATH/../../../src${PYTHONPATH:+:$PYTHONPATH}"
208
+ uv run --no-project --python 3.14 --with pydantic==2.13.4 --with pyyaml==6.0.3 \
209
+ python -m pytest_selective.impact_cli floor-budget \
210
+ --config "$IMPACT_CONFIG" --explanation test-impact-explanation.json
@@ -0,0 +1,16 @@
1
+ version: 2
2
+ updates:
3
+ - package-ecosystem: github-actions
4
+ directory: /
5
+ schedule:
6
+ interval: weekly
7
+ groups:
8
+ actions:
9
+ patterns: ["*"]
10
+ - package-ecosystem: uv
11
+ directory: /
12
+ schedule:
13
+ interval: weekly
14
+ groups:
15
+ development:
16
+ dependency-type: development
@@ -0,0 +1,38 @@
1
+ {
2
+ "graph": {
3
+ "module_roots": [
4
+ "src",
5
+ "."
6
+ ],
7
+ "test_roots": [
8
+ "tests"
9
+ ],
10
+ "pytest_policy": {
11
+ "pyproject.toml": "7a78d68735c0e3b091962244620d3a17557951123c645970638f15d3a737d987"
12
+ }
13
+ },
14
+ "lanes": {
15
+ "tests": {
16
+ "roots": [
17
+ "tests"
18
+ ],
19
+ "inputs": [
20
+ "src/**",
21
+ "scripts/**",
22
+ "tests/**",
23
+ ".pre-commit-config.yaml",
24
+ ".python-version"
25
+ ]
26
+ }
27
+ },
28
+ "global_inputs": [
29
+ "**/pyproject.toml",
30
+ "pyproject.toml",
31
+ "**/uv.lock",
32
+ "uv.lock",
33
+ ".github/test-lanes.json",
34
+ ".github/workflows/ci.yml",
35
+ ".github/workflows/release.yml",
36
+ ".github/actions/**"
37
+ ]
38
+ }
@@ -0,0 +1,119 @@
1
+ name: CI
2
+ on:
3
+ push:
4
+ branches: [main]
5
+ pull_request:
6
+ types: [opened, synchronize, reopened, labeled, unlabeled]
7
+ workflow_dispatch:
8
+ workflow_call:
9
+ permissions:
10
+ contents: read
11
+ concurrency:
12
+ # Only newer revisions of the same PR supersede work; keep every main/manual run.
13
+ group: ci-${{ github.workflow }}-${{ github.event_name == 'pull_request' && github.run_attempt == 1 && github.event.pull_request.number || github.run_id }}
14
+ cancel-in-progress: ${{ github.event_name == 'pull_request' && github.run_attempt == 1 }}
15
+ jobs:
16
+ select-tests:
17
+ runs-on: ubuntu-latest
18
+ outputs:
19
+ lanes: ${{ steps.plan.outputs.lanes }}
20
+ steps:
21
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
22
+ with:
23
+ fetch-depth: 0
24
+ - id: plan
25
+ uses: ./.github/actions/plan-lanes
26
+ with:
27
+ config: .github/test-lanes.json
28
+ base: ${{ github.event.pull_request.base.sha || github.event.before }}
29
+ full: ${{ github.event_name != 'pull_request' || contains(github.event.pull_request.labels.*.name, 'test:full') }}
30
+ quality:
31
+ runs-on: ubuntu-latest
32
+ steps:
33
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
34
+ - uses: gigaverse-app/ci-esta/.github/actions/setup-cached-uv@966f25c4fac4c64bf4ff9df0404321f91f882293
35
+ with:
36
+ python-version: '3.14'
37
+ - run: uv run --locked pre-commit run --all-files
38
+ - name: Check aggregate gate against the execution lanes
39
+ run: >-
40
+ uv run --no-project --python 3.14
41
+ --with "ci-esta @ https://github.com/gigaverse-app/ci-esta/archive/966f25c4fac4c64bf4ff9df0404321f91f882293.tar.gz"
42
+ ci-esta check-workflow --workflow .github/workflows/ci.yml --lanes .github/test-lanes.json
43
+ - name: Validate routing even when the platform matrix is skipped
44
+ run: uv run --locked pytest -q tests/test_repository_ci.py
45
+ - name: Validate GitHub Actions
46
+ run: docker run --rm -v "$PWD:/repo" -w /repo rhysd/actionlint:1.7.7 -color
47
+ tests:
48
+ needs: select-tests
49
+ if: >-
50
+ ${{ always() && !cancelled() && (needs.select-tests.result != 'success' ||
51
+ fromJSON(needs.select-tests.outputs.lanes).tests) }}
52
+ strategy:
53
+ fail-fast: false
54
+ matrix:
55
+ python: ['3.11', '3.14']
56
+ os: [ubuntu-latest, windows-latest, macos-latest]
57
+ runs-on: ${{ matrix.os }}
58
+ steps:
59
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
60
+ - uses: gigaverse-app/ci-esta/.github/actions/setup-cached-uv@966f25c4fac4c64bf4ff9df0404321f91f882293
61
+ with:
62
+ python-version: ${{ matrix.python }}
63
+ - name: Use native Git for Windows
64
+ uses: gigaverse-app/ci-esta/.github/actions/native-windows-git@966f25c4fac4c64bf4ff9df0404321f91f882293
65
+ - name: Record coverage runtime
66
+ run: uv run --locked python -m coverage --version
67
+ - run: uv run --locked pytest -q -n 4 --durations=20 --junitxml=test-timings.xml --cov --cov-report=term-missing --cov-report=xml
68
+ - uses: gigaverse-app/ci-esta/.github/actions/learn-test-timings@966f25c4fac4c64bf4ff9df0404321f91f882293
69
+ with:
70
+ junit-pattern: test-timings.xml
71
+ artifact-name: test-timings-${{ matrix.python }}-${{ matrix.os }}
72
+ - run: uv run --locked python scripts/tutorial.py --output tutorial-report.json
73
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
74
+ with:
75
+ name: evidence-${{ matrix.os }}-${{ matrix.python }}
76
+ path: |
77
+ coverage.xml
78
+ tutorial-report.json
79
+ pytest-minimum:
80
+ needs: select-tests
81
+ if: >-
82
+ ${{ always() && !cancelled() && (needs.select-tests.result != 'success' ||
83
+ fromJSON(needs.select-tests.outputs.lanes).tests) }}
84
+ runs-on: ubuntu-latest
85
+ steps:
86
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
87
+ - uses: gigaverse-app/ci-esta/.github/actions/setup-cached-uv@966f25c4fac4c64bf4ff9df0404321f91f882293
88
+ with:
89
+ python-version: '3.11'
90
+ - run: uv pip install 'pytest==8.0.0'
91
+ - run: uv run --no-sync pytest -q -n 4 --durations=20
92
+ distributions:
93
+ runs-on: ubuntu-latest
94
+ steps:
95
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
96
+ - uses: gigaverse-app/ci-esta/.github/actions/setup-cached-uv@966f25c4fac4c64bf4ff9df0404321f91f882293
97
+ with:
98
+ python-version: '3.14'
99
+ - run: uv build
100
+ - run: uv run --locked twine check --strict dist/*
101
+ - run: uv run --locked python scripts/check_metadata.py
102
+ - run: uv run --locked python scripts/smoke_distribution.py
103
+ - uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
104
+ with:
105
+ name: distributions
106
+ path: dist/*
107
+ if-no-files-found: error
108
+
109
+ validation-gate:
110
+ name: Validation gate
111
+ needs: [select-tests, quality, tests, pytest-minimum, distributions]
112
+ if: ${{ !cancelled() }}
113
+ runs-on: ubuntu-latest
114
+ steps:
115
+ - uses: gigaverse-app/ci-esta/.github/actions/validation-gate@966f25c4fac4c64bf4ff9df0404321f91f882293
116
+ with:
117
+ needs: ${{ toJSON(needs) }}
118
+ lanes: ${{ needs.select-tests.outputs.lanes || '{}' }}
119
+ lane-jobs: '{"tests":["tests"],"pytest-minimum":["tests"]}'
@@ -0,0 +1,24 @@
1
+ name: CI observability
2
+
3
+ on:
4
+ workflow_run:
5
+ workflows: [CI]
6
+ types: [completed]
7
+
8
+ permissions:
9
+ contents: read
10
+ actions: read
11
+ checks: read
12
+ pull-requests: write # Posts the CI-esta sticky.
13
+
14
+ jobs:
15
+ report:
16
+ uses: gigaverse-app/ci-esta/.github/workflows/report.yml@966f25c4fac4c64bf4ff9df0404321f91f882293
17
+ with:
18
+ run-id: ${{ github.event.workflow_run.id }}
19
+ run-attempt: ${{ github.event.workflow_run.run_attempt }}
20
+ head-sha: ${{ github.event.workflow_run.head_sha }}
21
+ pull-request-number: ${{ format('{0}', github.event.workflow_run.pull_requests[0].number || '') }}
22
+ post-sticky: true
23
+ required-gates-json: '[{"kind":"actions_job","job_name":"Validation gate"}]'
24
+ analyze-critical-path: true
@@ -0,0 +1,64 @@
1
+ name: Publish
2
+ on:
3
+ release:
4
+ types: [published]
5
+ workflow_dispatch:
6
+ permissions:
7
+ contents: read
8
+
9
+ jobs:
10
+ validate-tag:
11
+ runs-on: ubuntu-latest
12
+ steps:
13
+ - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
14
+ - uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2
15
+ with:
16
+ python-version: '3.14'
17
+ - name: Require a version-matching tag
18
+ env:
19
+ RELEASE_REF: ${{ github.ref }}
20
+ run: |
21
+ uv run --no-project python -c 'import os, pathlib, tomllib; version = tomllib.loads(pathlib.Path("pyproject.toml").read_text())["project"]["version"]; assert os.environ["RELEASE_REF"] == f"refs/tags/v{version}", "Run publishing from the version-matching v tag"'
22
+ checks:
23
+ needs: validate-tag
24
+ uses: ./.github/workflows/ci.yml
25
+ publish:
26
+ needs: checks
27
+ runs-on: ubuntu-latest
28
+ environment:
29
+ name: pypi
30
+ url: https://pypi.org/project/pytest-selective/
31
+ permissions:
32
+ id-token: write
33
+ steps:
34
+ - uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
35
+ with:
36
+ name: distributions
37
+ path: dist
38
+ - name: Publish tested distributions with attestations
39
+ uses: pypa/gh-action-pypi-publish@7245906fb61fa33a13554f9d5fb856fe61758086 # release/v1
40
+
41
+ action-version:
42
+ needs: publish
43
+ runs-on: ubuntu-latest
44
+ permissions:
45
+ contents: write
46
+ steps:
47
+ - name: Advance the compatible Actions tag after publication
48
+ env:
49
+ GH_TOKEN: ${{ github.token }}
50
+ GH_REPO: ${{ github.repository }}
51
+ RELEASE_TAG: ${{ github.ref_name }}
52
+ RELEASE_SHA: ${{ github.sha }}
53
+ run: |
54
+ if [[ ! "$RELEASE_TAG" =~ ^v([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then
55
+ exit 0
56
+ fi
57
+ alias="v${BASH_REMATCH[1]}.${BASH_REMATCH[2]}"
58
+ if gh api "repos/$GH_REPO/git/ref/tags/$alias" --silent; then
59
+ gh api --method PATCH "repos/$GH_REPO/git/refs/tags/$alias" \
60
+ -f sha="$RELEASE_SHA" -F force=true --silent
61
+ else
62
+ gh api --method POST "repos/$GH_REPO/git/refs" \
63
+ -f ref="refs/tags/$alias" -f sha="$RELEASE_SHA" --silent
64
+ fi