Skip to content
Merged
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
37 changes: 36 additions & 1 deletion .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,10 +23,17 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
# 3.15 is a prerelease with no prebuilt native-extension wheels
# (cryptography, pydantic-core, ...), so tox-uv compiles them from
# source and the job runs ~3x slower (~137s vs ~45s) — the long pole on
# every PR (issue #736). 3.14 already covers newest-stable behavior, so
# 3.15 is exercised by the post-merge `test-py315` job below instead.
# To fold it back once cp315 wheels ship: add "3.15" here and delete the
# `test-py315` job.
# 3.15t (free-threaded) is omitted until cryptography ships free-threaded
# 3.15 wheels; building from source yields an abi3 wheel that the
# free-threaded ABI can't load. Re-add once upstream wheels exist.
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t", "3.15"]
python-version: ["3.10", "3.11", "3.12", "3.13", "3.14", "3.14t"]

steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
Expand Down Expand Up @@ -217,3 +224,31 @@ jobs:
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
continue-on-error: true

# Post-merge early-warning for Python 3.15 (prerelease). Kept off the per-PR
# matrix because its source-built deps make it ~3x slower (issue #736); running
# it on pushes to main still surfaces 3.15-specific breakage on every merge.
# Remove this job (and re-add "3.15" to the test matrix) once cp315 wheels ship.
test-py315:
if: github.event_name == 'push'
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v4
with:
persist-credentials: false

- name: Install uv
uses: astral-sh/setup-uv@d31148d669074a8d0a63714ba94f3201e7020bc3 # v4
with:
version: "latest"
enable-cache: true
cache-dependency-glob: "uv.lock"

- name: Set up Python 3.15
uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v5
with:
python-version: "3.15"
allow-prereleases: true

- name: Run tests with tox-uv
run: uvx --with tox-uv tox -e py315
40 changes: 40 additions & 0 deletions specs/002-ci-py315-speedup/checklists/requirements.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Specification Quality Checklist: Speed up the Python 3.15 CI test job

**Purpose**: Validate specification completeness and quality before proceeding to planning
**Created**: 2026-07-08
**Feature**: [spec.md](../spec.md)

## Content Quality

- [x] No implementation details (languages, frameworks, APIs)
- [x] Focused on user value and business needs
- [x] Written for non-technical stakeholders
- [x] All mandatory sections completed

## Requirement Completeness

- [x] No [NEEDS CLARIFICATION] markers remain
- [x] Requirements are testable and unambiguous
- [x] Success criteria are measurable
- [x] Success criteria are technology-agnostic (no implementation details)
- [x] All acceptance scenarios are defined
- [x] Edge cases are identified
- [x] Scope is clearly bounded
- [x] Dependencies and assumptions identified

## Feature Readiness

- [x] All functional requirements have clear acceptance criteria
- [x] User scenarios cover primary flows
- [x] Feature meets measurable outcomes defined in Success Criteria
- [x] No implementation details leak into specification

## Notes

- The spec deliberately leaves the *choice of mechanism* (reduced cadence vs.
caching the built env) to the plan phase; both are documented as options in
Assumptions. This is intentional and does not constitute an unresolved
clarification, since either satisfies the functional requirements.
- "Technology-agnostic" is interpreted for a CI/infrastructure feature: CI
jobs, matrices, and required checks are the domain nouns here, not leaked
implementation detail.
102 changes: 102 additions & 0 deletions specs/002-ci-py315-speedup/plan.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
# Implementation Plan: Speed up the Python 3.15 CI test job

**Branch**: `ci-py315-speedup-736` | **Date**: 2026-07-08 | **Spec**: [spec.md](./spec.md)

**Input**: Feature specification from `specs/002-ci-py315-speedup/spec.md`

## Summary

Python 3.15 is a prerelease with no prebuilt native-extension wheels, so its
per-PR `test` job compiles dependencies from source and takes ~137s — ~3x the
stable versions and the long pole on every PR. Remove `3.15` from the per-PR
`test` matrix and instead run a dedicated Python 3.15 job on pushes to `main`
(post squash-merge), preserving early-warning coverage off the PR critical path.
Single-file change to `.github/workflows/ci.yml`; no branch-protection edit is
needed because the required `test` check is the summary aggregation job, not an
individual matrix leg.

## Technical Context

**Language/Version**: N/A (CI configuration change; affects GitHub Actions YAML)

**Primary Dependencies**: GitHub Actions (`actions/setup-python`,
`astral-sh/setup-uv`), `tox` / `tox-uv`, `uv`

**Storage**: N/A

**Testing**: The change is validated by CI behavior itself — PR runs (no 3.15
leg) and push-to-`main` runs (3.15 job present). Existing `pytest`/`tox` suite
is unchanged.

**Target Platform**: `ubuntu-latest` GitHub-hosted runners

**Project Type**: Single-project Python CLI (change is confined to CI config)

**Performance Goals**: Per-PR test gate's slowest job under ~60s (down from
~137s); ~1.5 min removed from the PR critical path.

**Constraints**: Must keep stable-version coverage (3.10–3.14, 3.14t) on every
PR; must keep the `test` required check reporting; must keep 3.15 exercised on
every merge to `main`; must be reversible in one obvious edit.

**Scale/Scope**: One file (`.github/workflows/ci.yml`); ~1 line removed from the
matrix + ~1 new job (~20 lines) + comment updates.

## Constitution Check

*GATE: Must pass before Phase 0 research. Re-check after Phase 1 design.*

- **I. Code Quality**: No application code changes. `zizmor` reviews GitHub
Actions — the workflow security job (`uvx zizmor .github/workflows/`) must
stay clean for the new job. Pinned action SHAs reused from existing steps.
✅ PASS.
- **II. Testing Standards (NON-NEGOTIABLE)**: No test code changes; the full
`pytest`/`tox` suite still runs — on 3.10–3.14/3.14t per PR and on 3.15 per
merge to `main`. Total test coverage of the code is unchanged; only the
*cadence* of the 3.15 leg moves. This is a CI-timing change, not a coverage
reduction (spec SC-002). ✅ PASS.
- **III. User Experience Consistency**: No CLI surface change. ✅ N/A.
- **IV. Performance Requirements**: This *improves* CI responsiveness (the
spirit of the principle) without regressing any runtime latency. Performance
impact stated here and to be restated in the PR. ✅ PASS.
- **Workflow**: Change lives on feature branch `ci-py315-speedup-736`, tied to
issue #736; `docs/` reviewed for CI references (see Phase 1). ✅ PASS.

No violations. Complexity Tracking not required.

## Project Structure

### Documentation (this feature)

```text
specs/002-ci-py315-speedup/
├── plan.md # This file
├── spec.md # Feature spec (+ Clarifications)
├── research.md # Phase 0 output
├── quickstart.md # Phase 1 output (validation guide)
├── checklists/
│ └── requirements.md # Spec quality checklist
└── tasks.md # Phase 2 output (/speckit-tasks)
```

Not generated (justified): `data-model.md` — no data entities; this is a CI
config change. `contracts/` — no external/API/CLI interface changes.

### Source Code (repository root)

```text
.github/workflows/ci.yml # ONLY file changed
```

**Structure Decision**: Single-file CI change. Two edits to `ci.yml`:
1. Remove `"3.15"` from the `test` job's `strategy.matrix.python-version`,
updating the adjacent comment to explain 3.15 now runs post-merge and how to
fold it back when cp315 wheels ship.
2. Add a push-only job (`if: github.event_name == 'push'`) named e.g.
`test-py315` that sets up uv + Python 3.15 (`allow-prereleases: true`) and
runs `uvx --with tox-uv tox -e py315`, mirroring the existing
`coverage-baseline` job's step structure.

## Complexity Tracking

No constitution violations — section intentionally empty.
55 changes: 55 additions & 0 deletions specs/002-ci-py315-speedup/quickstart.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# Quickstart / Validation Guide: Python 3.15 CI cadence change

**Feature**: 002-ci-py315-speedup

This change is entirely in `.github/workflows/ci.yml`. Validate it by inspecting
the workflow locally and observing CI behavior on a PR and on `main`.

## Prerequisites

- `uv` installed (repo already uses it).
- On the feature branch `ci-py315-speedup-736`.

## Local validation (before pushing)

1. **Workflow security lint** (constitution: zizmor must stay clean):
```sh
uvx zizmor .github/workflows/
```
Expected: no new findings.

2. **YAML sanity** — confirm the two edits landed:
```sh
# 3.15 removed from the per-PR test matrix:
grep -n 'python-version: \["3.10"' .github/workflows/ci.yml # line should NOT contain "3.15"
# a push-only 3.15 job exists running tox -e py315:
grep -n 'tox -e py315' .github/workflows/ci.yml
```
Expected: matrix line ends at `"3.14t"`; a `tox -e py315` line exists in a
`github.event_name == 'push'` job.

3. **Tox env exists** (the job invokes `tox -e py315`):
```sh
uvx --with tox-uv tox -l | grep -x py315 || echo "py315 env resolves from generative config"
```
Note: envs are generated from the version list in `tox.ini`/`pyproject.toml`;
`tox -e py315` works as long as 3.15 is a declared env (it already is, since
the per-PR job invoked `py315` before this change).

## CI validation (after pushing)

- **On the PR** (SC-001, SC-002): the `test` matrix shows legs for
3.10, 3.11, 3.12, 3.13, 3.14, 3.14t — **no** `test (3.15)` leg. The slowest
test leg finishes in ~40–60s. The required `test` check reports success.
- **After squash-merge to `main`** (SC-003): a `test-py315` job runs on the push
and executes the suite on Python 3.15. A green run confirms 3.15 coverage is
retained off the PR path. (To prove failure visibility, a deliberate
3.15-only break would show this job red on `main`.)
- **Required check (SC-004)**: a normal green PR is mergeable; no check is stuck
"expected" waiting on a job that no longer runs.

## Reversal (when cp315 wheels ship upstream)

1. Add `"3.15"` back to the `test` job matrix `python-version` list.
2. Delete the `test-py315` push-only job.
3. Update the matrix comment. Re-run `uvx zizmor .github/workflows/`.
80 changes: 80 additions & 0 deletions specs/002-ci-py315-speedup/research.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
# Phase 0 Research: Speed up the Python 3.15 CI test job

**Feature**: 002-ci-py315-speedup | **Date**: 2026-07-08

## R1. Root cause of the ~137s 3.15 job

**Decision**: Treat the slowness as dependency source-builds under the 3.15
prerelease, not test execution.

**Rationale**: The test suite runs in <2s locally; the ~90s gap between 3.15
(~137s) and stable versions (~40–55s) is install/build time. Python 3.15 is a
prerelease, so native-extension dependencies (e.g. `cryptography`,
`pydantic-core`) have no prebuilt cp315 wheels on PyPI yet and are compiled from
source under `tox-uv`. Stable versions download prebuilt wheels. The workflow
already carries a note that `cryptography` lacks free-threaded 3.15 wheels,
corroborating the wheel-gap theory.

**Alternatives considered**: Slow test execution (ruled out — <2s locally);
`setup-uv` cache miss as sole cause (contributing, but source builds dominate;
even a warm cache re-pays compilation on resolver differences for a prerelease).

## R2. Mechanism to remove 3.15 from the PR critical path

**Decision**: Drop `3.15` from the per-PR `test` matrix and run a dedicated
Python 3.15 job on pushes to `main` (post squash-merge). (Confirmed in spec
Clarifications, 2026-07-08.)

**Rationale**: Smallest, most reversible change that fully removes the long
pole. 3.14 (newest stable) already exercises the newest stable Python behavior
on every PR, so per-PR 3.15 gives no extra correctness confidence. Running 3.15
on every merge to `main` preserves early-warning coverage: any 3.15-specific
break surfaces as a failed run on `main`.

**Alternatives considered**:
- *Cache the built native-extension env per-PR*: keeps per-PR 3.15 but is
cache-key fragile, still cold-builds on cache miss / upstream churn, and adds
config complexity. Rejected per clarification.
- *Add a scheduled/cron 3.15 run*: more coverage, but post-merge-on-`main`
already runs on every merge; a cron adds config for marginal benefit.
Rejected per clarification.
- *Drop 3.15 entirely*: loses the early-warning signal. Rejected.

## R3. Branch-protection / required-check safety

**Decision**: No branch-protection change is required.

**Rationale**: Verified via `gh api .../branches/main/protection` that the
required status checks are `test`, `lint`, `type-check`, `security`. The `test`
context is the **summary** job `tests-complete` (`name: test`), which aggregates
`needs: [test, lint, type-check, security, documentation]` — it does **not**
name individual matrix legs like `test (3.15)`. Removing `3.15` from the matrix
therefore leaves the `test` required check intact and still reporting on PRs.
This satisfies FR-005 with no GitHub settings edit. (The summary-gate pattern
was designed precisely so matrix changes don't break required checks.)

## R4. Where the post-merge 3.15 job lives

**Decision**: Add a new job gated on `if: github.event_name == 'push'` that runs
`uvx --with tox-uv tox -e py315`, mirroring the existing `coverage-baseline`
push-only job's setup steps.

**Rationale**: `coverage-baseline` already establishes the "runs only on push to
`main`/`develop`, sets up uv + Python, runs tox" pattern. A sibling job reuses
that pattern verbatim with `python-version: "3.15"` + `allow-prereleases: true`
and `tox -e py315`. Keeping it a separate job (not folding 3.15 into
`coverage-baseline`'s matrix) avoids double Codecov uploads and keeps the
concern — "3.15 early warning" — clearly named and independently reversible.

**Alternatives considered**: Add `python-version` matrix to `coverage-baseline`
(would upload coverage twice and conflate coverage-baseline with 3.15 smoke
testing — rejected for clarity).

## R5. Reversibility when cp315 wheels ship

**Decision**: Record, in the workflow comments, that 3.15 lives in a post-merge
job and how to fold it back: add `"3.15"` to the `test` matrix and delete the
post-merge job.

**Rationale**: FR-006/FR-007 — a future maintainer should be able to undo this
in one obvious edit once prebuilt wheels make 3.15 as fast as stable versions.
Loading
Loading