Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions .github/labels.yml
Original file line number Diff line number Diff line change
Expand Up @@ -72,3 +72,6 @@
- color: fbca04
description: "Unit tests, linting, CI, etc."
name: testing
- color: fbca04
description: "Run every CI lane on this PR (applies from the next push or reopen)"
name: "test:full"
40 changes: 40 additions & 0 deletions .github/test-lanes.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
{
"graph": {
"module_roots": ["src", "."],
"test_roots": ["tests", "ci/tests"],
"global_inputs": ["pyproject.toml", "tox.ini"],
"pytest_policy": {
"pyproject.toml": "8c6a5fd2043787c01e67a463fc6bb2494764f55c2d2c5d358ebe9f46dad0829c",
"tox.ini": "751ee0be550f8a9de9dea8d6f74b0178cc17c099cc18086106bb9b397016076c"
}
},
"global_inputs": ["pyproject.toml", "tox.ini", ".github/test-lanes.json"],
"lanes": {
"tests": {
"roots": ["tests"],
"inputs": ["src/**", "scripts/**", ".github/workflows/test.yml#test"]
},
"benchmark": {
"inputs": [
"tests/test_benchmarks.py",
"src/**",
"scripts/generate-translation-binaries.sh",
".github/workflows/benchmark.yml#benchmarks"
]
},
"package": {
"inputs": [
"src/**",
"README.md",
"LICENCE",
".gitignore",
"scripts/generate-translation-binaries.sh",
".github/workflows/release.yml#build-package"
]
},
"routing": {
"roots": ["ci/tests"],
"inputs": [".github/workflows/test.yml#routing"]
}
}
}
33 changes: 32 additions & 1 deletion .github/workflows/benchmark.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,11 @@
name: Benchmark

on: [push, pull_request, workflow_dispatch]
on:
push:
pull_request:
workflow_dispatch:
schedule:
- cron: "17 4 * * 1"

permissions: {}

Expand All @@ -9,7 +14,27 @@ env:
PIP_DISABLE_PIP_VERSION_CHECK: 1

jobs:
select-tests:
runs-on: ubuntu-latest
outputs:
lanes: ${{ steps.plan.outputs.lanes }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- id: plan
uses: gigaverse-app/pytest-selective/actions/plan-lanes@286d0aeb75991d35f2ffafc7dda0517b7250b5c3 # v1.4.0
with:
config: .github/test-lanes.json

benchmarks:
needs: select-tests
# Preserve full manual/scheduled checkpoints; ordinary changes use the plan.
# A failed planner never skips the benchmark lane.
if: >-
${{ always() && !cancelled() && (needs.select-tests.result != 'success' ||
fromJSON(needs.select-tests.outputs.lanes).benchmark) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
Expand All @@ -33,7 +58,13 @@ jobs:
- name: Install dependencies
run: pip install -e ".[tests]"

# Forks cannot upload to the upstream CodSpeed integration.
- name: Run benchmark tests on forks
if: github.event.repository.fork == true
run: pytest tests/test_benchmarks.py --benchmark-disable

- name: Run benchmarks
if: github.event.repository.fork == false
uses: CodSpeedHQ/action@78102a468f96d8e0ccb92d0ec49425753fc42062 # v5.6.0
with:
mode: simulation
Expand Down
36 changes: 36 additions & 0 deletions .github/workflows/observe-ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
name: CI report
on: # zizmor: ignore[dangerous-triggers] -- read-only reporter; never executes source PR code
workflow_run:
workflows: [Test]
types: [completed]
workflow_dispatch:
inputs:
run-id:
description: Completed test workflow run to report
required: true
type: string
head-sha:
description: Exact head SHA of the source run
required: true
type: string
run-attempt:
description: Source run attempt
type: string
default: "1"
permissions:
actions: read
checks: read
contents: read
pull-requests: read
jobs:
report:
# The trusted reporter reads source workflow data without executing PR code.
uses: gigaverse-app/ci-esta/.github/workflows/report.yml@b69484796b301a90c73bd7c7f26f8e542c8bb8a3 # v1.14.0
with:
run-id: ${{ github.event.workflow_run.id || fromJSON(inputs.run-id) }}
run-attempt: >-
${{ github.event.workflow_run.run_attempt || fromJSON(inputs.run-attempt) }}
head-sha: ${{ github.event.workflow_run.head_sha || inputs.head-sha }}
required-gates-json: '[{"kind":"actions_job","job_name":"Test successful"}]'
analyze-critical-path: true
post-sticky: false
32 changes: 30 additions & 2 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,9 @@ env:
FORCE_COLOR: 1

jobs:
# Always build & lint package.
# Build & lint the package on every push, tag, release and dispatch.
# On pull requests, skip the build when the package lane's inputs are unchanged.
# The plan runs inside this job, so the publishing jobs keep their single dependency.
build-package:
name: Build & verify package
runs-on: ubuntu-latest
Expand All @@ -28,15 +30,41 @@ jobs:
fetch-depth: 0
persist-credentials: false

- id: plan
if: github.event_name == 'pull_request'
# A failed planner builds the package; only a successful plan can skip it.
continue-on-error: true
uses: gigaverse-app/pytest-selective/actions/plan-lanes@286d0aeb75991d35f2ffafc7dda0517b7250b5c3 # v1.4.0
with:
config: .github/test-lanes.json

- id: admit
name: Package lane admission
env:
EVENT_NAME: ${{ github.event_name }}
PLAN_OUTCOME: ${{ steps.plan.outcome }}
LANES: ${{ steps.plan.outputs.lanes }}
run: |
package_lane="$(python3 -c 'import json, os; print(json.loads(os.environ["LANES"] or "{}").get("package"))')"
if [ "$EVENT_NAME" = pull_request ] && [ "$PLAN_OUTCOME" = success ] && [ "$package_lane" = False ]; then
echo "build=false" >> "$GITHUB_OUTPUT"
echo "Package lane unaffected; skipping the build."
else
echo "build=true" >> "$GITHUB_OUTPUT"
fi

- name: Install gettext
if: steps.admit.outputs.build == 'true'
run: |
sudo apt install gettext

- name: Generate translation binaries
if: steps.admit.outputs.build == 'true'
run: |
scripts/generate-translation-binaries.sh

- uses: hynek/build-and-inspect-python-package@67230f7b8410ae0052b39c1efc89d89fff43ff10 # v3.0.2
- if: steps.admit.outputs.build == 'true'
uses: hynek/build-and-inspect-python-package@67230f7b8410ae0052b39c1efc89d89fff43ff10 # v3.0.2

# Upload to Test PyPI on every commit on main.
release-test-pypi:
Expand Down
70 changes: 67 additions & 3 deletions .github/workflows/test.yml
Original file line number Diff line number Diff line change
@@ -1,6 +1,12 @@
name: Test

on: [push, pull_request, workflow_dispatch]
on:
push:
pull_request:
workflow_dispatch:
schedule:
# Weekly full checkpoint: scheduled runs execute every lane.
- cron: "17 4 * * 1"

permissions: {}

Expand All @@ -9,7 +15,26 @@ env:
PIP_DISABLE_PIP_VERSION_CHECK: 1

jobs:
select-tests:
runs-on: ubuntu-latest
outputs:
lanes: ${{ steps.plan.outputs.lanes }}
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
fetch-depth: 0
persist-credentials: false
- id: plan
uses: gigaverse-app/pytest-selective/actions/plan-lanes@286d0aeb75991d35f2ffafc7dda0517b7250b5c3 # v1.4.0
with:
config: .github/test-lanes.json

test:
needs: select-tests
# A failed planner runs the matrix; only a successful plan can skip it.
if: >-
${{ always() && !cancelled() && (needs.select-tests.result != 'success' ||
fromJSON(needs.select-tests.outputs.lanes).tests) }}
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
Expand Down Expand Up @@ -69,13 +94,52 @@ jobs:
flags: ${{ matrix.os }}
name: ${{ matrix.os }} Python ${{ matrix.python-version }}

routing:
needs: select-tests
if: >-
${{ always() && !cancelled() && (needs.select-tests.result != 'success' ||
fromJSON(needs.select-tests.outputs.lanes).routing) }}
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@07a047e608db84b3d8c06cd9a7c7dcc9c355c18e # v10.3.2

- name: Lane routing regressions
run: |
uv run --no-project --python 3.14 --with "pytest>=9" --with "pytest-selective==1.4.0" \
python -m pytest ci/tests

success:
if: always()
needs: test
needs: [select-tests, test, routing]
runs-on: ubuntu-latest
name: Test successful
steps:
- name: Success
env:
PLAN_RESULT: ${{ needs.select-tests.result }}
LANES: ${{ needs.select-tests.outputs.lanes }}
TEST_RESULT: ${{ needs.test.result }}
run: test "$TEST_RESULT" = "success"
ROUTING_RESULT: ${{ needs.routing.result }}
# Accept a skipped lane only when a successful plan explicitly returned false for it.
run: |
python3 - <<'PY'
import json, os, sys

if os.environ["PLAN_RESULT"] != "success":
sys.exit(f"Lane planner result: {os.environ['PLAN_RESULT']}")
lanes = json.loads(os.environ["LANES"])
# Every required lane must have an explicit boolean decision.
if not isinstance(lanes, dict) or any(type(lanes.get(lane)) is not bool for lane in ("tests", "routing")):
sys.exit("Invalid lane plan")
failed = False
for lane, result in (("tests", os.environ["TEST_RESULT"]), ("routing", os.environ["ROUTING_RESULT"])):
accepted = result == "success" or (result == "skipped" and lanes.get(lane) is False)
print(f"{lane}: lane={lanes.get(lane)} result={result} accepted={accepted}")
failed |= not accepted
sys.exit(1 if failed else 0)
PY
3 changes: 3 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,6 @@ dmypy.json

# hatch-vcs
src/*/_version.py

# pytest-selective graph cache
.test-impact/
Loading
Loading