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.
- pytest_selective-0.1.0/.gitattributes +5 -0
- pytest_selective-0.1.0/.github/ISSUE_TEMPLATE/bug_report.yml +29 -0
- pytest_selective-0.1.0/.github/PULL_REQUEST_TEMPLATE.md +15 -0
- pytest_selective-0.1.0/.github/actions/plan-lanes/action.yml +60 -0
- pytest_selective-0.1.0/.github/actions/pytest-expansion/action.yml +44 -0
- pytest_selective-0.1.0/.github/actions/test-impact/action.yml +210 -0
- pytest_selective-0.1.0/.github/dependabot.yml +16 -0
- pytest_selective-0.1.0/.github/test-lanes.json +38 -0
- pytest_selective-0.1.0/.github/workflows/ci.yml +119 -0
- pytest_selective-0.1.0/.github/workflows/observe-ci.yml +24 -0
- pytest_selective-0.1.0/.github/workflows/release.yml +64 -0
- pytest_selective-0.1.0/.github/workflows/windows-performance.yml +71 -0
- pytest_selective-0.1.0/.gitignore +13 -0
- pytest_selective-0.1.0/.pre-commit-config.yaml +19 -0
- pytest_selective-0.1.0/AGENTS.md +23 -0
- pytest_selective-0.1.0/CHANGELOG.md +34 -0
- pytest_selective-0.1.0/CONTRIBUTING.md +63 -0
- pytest_selective-0.1.0/LICENSE +202 -0
- pytest_selective-0.1.0/PKG-INFO +263 -0
- pytest_selective-0.1.0/PROVENANCE.md +23 -0
- pytest_selective-0.1.0/README.md +233 -0
- pytest_selective-0.1.0/RELEASING.md +63 -0
- pytest_selective-0.1.0/docs/README.md +15 -0
- pytest_selective-0.1.0/docs/agents.md +72 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-due-ci.json +118 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-due-docs-ci.json +74 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-due-docs-widget.json +32 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-due-widget.json +83 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-input-replays.json +407 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-obligation-docs.json +293 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-obligation.json +293 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-routing.md +89 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22-selective-docs.json +139 -0
- pytest_selective-0.1.0/docs/benchmarks/2026-10-22.md +179 -0
- pytest_selective-0.1.0/docs/benchmarks/windows-runtime.md +93 -0
- pytest_selective-0.1.0/docs/ci-toolkit.md +51 -0
- pytest_selective-0.1.0/docs/ci.md +164 -0
- pytest_selective-0.1.0/docs/comparison-pytest-impacted.md +126 -0
- pytest_selective-0.1.0/docs/configuration.md +129 -0
- pytest_selective-0.1.0/docs/evidence.md +75 -0
- pytest_selective-0.1.0/docs/local-development.md +51 -0
- pytest_selective-0.1.0/docs/pytest-hygiene.md +64 -0
- pytest_selective-0.1.0/docs/pytest-impacted.md +69 -0
- pytest_selective-0.1.0/docs/tutorial.md +59 -0
- pytest_selective-0.1.0/docs/work-in-progress/tickets/codex-windows-git-performance-test-plan.md +21 -0
- pytest_selective-0.1.0/docs/work-in-progress/tickets/impacted-improvements-test-plan.md +98 -0
- pytest_selective-0.1.0/pyproject.toml +68 -0
- pytest_selective-0.1.0/scripts/benchmark_parsing.py +57 -0
- pytest_selective-0.1.0/scripts/check_docs.py +25 -0
- pytest_selective-0.1.0/scripts/check_metadata.py +40 -0
- pytest_selective-0.1.0/scripts/compare_impacted.py +129 -0
- pytest_selective-0.1.0/scripts/measure_ci_runs.py +79 -0
- pytest_selective-0.1.0/scripts/smoke_distribution.py +58 -0
- pytest_selective-0.1.0/scripts/tutorial.py +147 -0
- pytest_selective-0.1.0/skills/pytest-selective/SKILL.md +84 -0
- pytest_selective-0.1.0/skills/pytest-selective/agents/openai.yaml +4 -0
- pytest_selective-0.1.0/skills/pytest-selective/references/adoption.md +93 -0
- pytest_selective-0.1.0/src/pytest_selective/__init__.py +0 -0
- pytest_selective-0.1.0/src/pytest_selective/__main__.py +3 -0
- pytest_selective-0.1.0/src/pytest_selective/ast_imports.py +75 -0
- pytest_selective-0.1.0/src/pytest_selective/ci.py +242 -0
- pytest_selective-0.1.0/src/pytest_selective/hygiene.py +118 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_audit.py +104 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_audit_plugin.py +217 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_cli.py +432 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_declarations.py +123 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_explanation.py +481 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_first_party.py +362 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_graph.py +885 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_models.py +491 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_name_scope.py +498 -0
- pytest_selective-0.1.0/src/pytest_selective/impact_selection.py +242 -0
- pytest_selective-0.1.0/src/pytest_selective/input_policy.py +27 -0
- pytest_selective-0.1.0/src/pytest_selective/local.py +78 -0
- pytest_selective-0.1.0/src/pytest_selective/py.typed +0 -0
- pytest_selective-0.1.0/src/pytest_selective/pytest_expansion.py +535 -0
- pytest_selective-0.1.0/src/pytest_selective/pytest_sharding.py +269 -0
- pytest_selective-0.1.0/src/pytest_selective/test_inventory.py +39 -0
- pytest_selective-0.1.0/src/pytest_selective/typecheck_scope.py +111 -0
- pytest_selective-0.1.0/src/pytest_selective/working_tree.py +43 -0
- pytest_selective-0.1.0/tests/__init__.py +0 -0
- pytest_selective-0.1.0/tests/test_ci_measurements.py +70 -0
- pytest_selective-0.1.0/tests/test_distribution_metadata.py +17 -0
- pytest_selective-0.1.0/tests/test_hygiene.py +203 -0
- pytest_selective-0.1.0/tests/test_pytest_expansion.py +628 -0
- pytest_selective-0.1.0/tests/test_repository_ci.py +46 -0
- pytest_selective-0.1.0/tests/test_repository_hygiene.py +9 -0
- pytest_selective-0.1.0/tests/test_tutorial.py +72 -0
- pytest_selective-0.1.0/tests/testing/__init__.py +0 -0
- pytest_selective-0.1.0/tests/testing/git_repo.py +72 -0
- pytest_selective-0.1.0/tests/testing/test_ast_imports.py +97 -0
- pytest_selective-0.1.0/tests/testing/test_ci.py +253 -0
- pytest_selective-0.1.0/tests/testing/test_global_inputs.py +51 -0
- pytest_selective-0.1.0/tests/testing/test_impact_audit.py +362 -0
- pytest_selective-0.1.0/tests/testing/test_impact_explanation.py +827 -0
- pytest_selective-0.1.0/tests/testing/test_impact_first_party.py +640 -0
- pytest_selective-0.1.0/tests/testing/test_impact_graph.py +2394 -0
- pytest_selective-0.1.0/tests/testing/test_impact_name_scope.py +532 -0
- pytest_selective-0.1.0/tests/testing/test_portability.py +51 -0
- pytest_selective-0.1.0/tests/testing/test_pytest_sharding.py +237 -0
- pytest_selective-0.1.0/tests/testing/test_typecheck_scope.py +169 -0
- pytest_selective-0.1.0/tests/testing/test_working_tree.py +212 -0
- pytest_selective-0.1.0/uv.lock +1229 -0
|
@@ -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
|