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
65 changes: 65 additions & 0 deletions .github/workflows/python-314-compatibility.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
name: Python 3.14 Compatibility (Advisory)

'on':
pull_request:
paths:
- '.github/workflows/python-314-compatibility.yml'
- 'pyproject.toml'
- 'uv.lock'
- 'tests/test_ci_uv_lock_policy.py'
workflow_dispatch:
schedule:
- cron: '17 2 * * 1'

permissions:
contents: read

concurrency:
group: ${{ github.workflow }}-${{ github.event_name == 'pull_request' && format('pr-{0}', github.event.pull_request.number) || format('{0}-{1}', github.event_name, github.run_id) }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}

jobs:
python314-compatibility:
name: Python 3.14 source quality and non-live tests (advisory)
runs-on: ubuntu-latest
timeout-minutes: 120
env:
UV_PYTHON: "3.14"
PLAYWRIGHT_BROWSERS_PATH: ${{ github.workspace }}/.playwright-browsers
AUTO_CODER_REQUIRE_BROWSER: "1"
AUTO_CODER_IMMEDIATE_TEST_FAILURES: "1"
PYTEST_ADDOPTS: "-o faulthandler_timeout=60 -o faulthandler_exit_on_timeout=false"
steps:
- name: Checkout exact source revision
uses: actions/checkout@v7
with:
fetch-depth: 0
ref: ${{ github.event.pull_request.head.sha || github.sha }}
- name: Select supported uv release
uses: astral-sh/setup-uv@v7
with:
version: "0.7.22"
- name: Select advisory Python
uses: actions/setup-python@v7
with:
python-version: "3.14"
- name: Admit unchanged locked dependencies
run: uv sync --locked --dev --extra test
- name: Verify selected interpreter and source identity
run: |
uv run python -c 'import sys; assert sys.version_info[:2] == (3, 14), sys.version'
uv run python -c 'from auto_coder.build_identity import resolve_build_identity; r=resolve_build_identity("."); assert r.state == "clean" and r.source_revision == "${{ github.event.pull_request.head.sha || github.sha }}"'
- name: Check source quality on Python 3.14
run: |
uv run black --check src/ tests/
uv run isort --check-only src/ tests/
uv run flake8 src/ tests/
uv run mypy --config-file pyproject.toml -p auto_coder
- name: Prepare required Python-only management fixture
run: uv run python scripts/prepare_management_test_image.py
- name: Install real Chromium
run: uv run playwright install --with-deps chromium
- name: Verify real headless Chromium
run: uv run python scripts/browser_preflight.py
- name: Run complete non-live test selection
run: uv run python -m pytest -m 'not opencode_live' -vv
34 changes: 34 additions & 0 deletions docs/client-features/python-314-advisory-compatibility.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# Advisory Python 3.14 compatibility evidence

The independent `Python 3.14 Compatibility (Advisory)` GitHub Actions
workflow runs for pull requests changing its workflow or shared dependency
policy inputs, manual dispatch and a weekly check. Ordinary application-only
pull requests do not incur this separate, potentially expensive advisory job. It
uses the same supported `astral-sh/setup-uv@v7` and pinned uv `0.7.22`
as the required Python 3.12 workflows. Its `uv sync --locked --dev --extra
test` installs the committed dependency graph without regenerating the
lockfile. Stale or incompatible dependencies, missing Python 3.14 wheels,
source-quality failures, and test failures remain visible red job outcomes.
A failed or incomplete run is not compatibility evidence.

The workflow sets `UV_PYTHON=3.14` for uv sync and every uv run so the
repository's default interpreter pin cannot silently create a Python 3.12
virtual environment even after `setup-python` selects 3.14. A separate
version assertion refuses an unexpected interpreter. The workflow verifies
the selected interpreter and clean exact Git checkout,
then runs Black, isort, Flake8 and package-wide mypy before the complete
`not opencode_live` pytest selection. Like the required source workflow, it
prepares the Python-only management Docker fixture from the tested source and
verifies real headless Chromium before running the tests. These prerequisites
are required to avoid incorrectly reporting success with eligible tests
deselected or skipped for missing fixtures; they do not run OpenCode live
tasks. The fixture's isolated container Python is not the interpreter under
evaluation: Python 3.14 is selected for the installed application and host
pytest process.

This independent workflow is **advisory**, not a publication, merge or
deployment authority. It publishes no artifact, grants no package write
permission and does not alter any required Python 3.12 workflow, Docker
base image, production worker, release selection or branch-protection rule.
Green advisory evidence alone is not approval to migrate the production
baseline. See `immutable-ci-uv-setup.md` for the shared immutable-lock gate.
57 changes: 55 additions & 2 deletions tests/test_ci_uv_lock_policy.py
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,59 @@ def test_future_workflow_policy_rejects_each_unlocked_or_unpinned_install(tmp_pa
_assert_workflow_policy([valid_path, workflow_path])


def test_python314_advisory_workflow_is_a_real_non_live_compatibility_check() -> None:
workflow_path = WORKFLOWS / "python-314-compatibility.yml"
workflow = yaml.safe_load(workflow_path.read_text(encoding="utf-8"))
assert set(workflow["on"]) == {"pull_request", "workflow_dispatch", "schedule"}
assert set(workflow["on"]["pull_request"]["paths"]) == {
".github/workflows/python-314-compatibility.yml",
"pyproject.toml",
"uv.lock",
"tests/test_ci_uv_lock_policy.py",
}
assert workflow["permissions"] == {"contents": "read"}
assert list(workflow["jobs"]) == ["python314-compatibility"]
job = workflow["jobs"]["python314-compatibility"]
assert job["runs-on"] == "ubuntu-latest"
assert job["env"]["UV_PYTHON"] == "3.14"
assert job["env"]["AUTO_CODER_REQUIRE_BROWSER"] == "1"
steps = job["steps"]
assert not any(step.get("continue-on-error", False) for step in steps)
assert next(step for step in steps if step.get("uses") == "actions/setup-python@v7")["with"]["python-version"] == "3.14"
checkout = next(step for step in steps if step.get("uses") == "actions/checkout@v7")
assert checkout["with"]["fetch-depth"] == 0
assert checkout["with"]["ref"] == "${{ github.event.pull_request.head.sha || github.sha }}"
commands = [step.get("run", "") for step in steps]
assert "uv sync --locked --dev --extra test" in commands
assert any("sys.version_info[:2] == (3, 14)" in command for command in commands)
quality = next(command for command in commands if "uv run black --check" in command)
assert all(gate in quality for gate in ("isort --check-only", "flake8 src/ tests/", "mypy --config-file pyproject.toml -p auto_coder"))
fixture = commands.index("uv run python scripts/prepare_management_test_image.py")
browser = commands.index("uv run python scripts/browser_preflight.py")
tests = commands.index("uv run python -m pytest -m 'not opencode_live' -vv")
assert fixture < browser < tests
assert any("uv run playwright install --with-deps chromium" in command for command in commands)
assert _assert_workflow_policy([workflow_path]) == {"python-314-compatibility.yml:python314-compatibility"}


@pytest.mark.parametrize(
("original", "replacement"),
[
('version: "0.7.22"', 'version: "0.7.21"'),
("astral-sh/setup-uv@v7", "astral-sh/setup-uv@v6"),
("uv sync --locked --dev --extra test", "uv sync --dev --extra test"),
("uv sync --locked --dev --extra test", "pip install ."),
],
)
def test_python314_workflow_cannot_bypass_shared_uv_policy(tmp_path: Path, original: str, replacement: str) -> None:
workflow = (WORKFLOWS / "python-314-compatibility.yml").read_text(encoding="utf-8")
assert workflow.count(original) == 1
corrupted = tmp_path / "python-314-compatibility.yml"
corrupted.write_text(workflow.replace(original, replacement, 1), encoding="utf-8")
with pytest.raises(AssertionError):
_assert_workflow_policy([corrupted])


def _write_lock_fixture(project: Path) -> None:
dependency = project / "dependency"
(dependency / "local_dependency").mkdir(parents=True)
Expand Down Expand Up @@ -236,7 +289,7 @@ def _assert_setup_fails_without_lock_change(project: Path, command: str) -> None

@pytest.mark.parametrize(
"workflow_name",
["test-source-for-runtime.yml", "publish-beta.yml"],
["test-source-for-runtime.yml", "publish-beta.yml", "python-314-compatibility.yml"],
)
def test_production_setup_rejects_stale_lock_before_tests_or_publication(tmp_path: Path, workflow_name: str, _use_real_commands: object) -> None:
command = _sync_commands(WORKFLOWS / workflow_name)[0]
Expand All @@ -255,7 +308,7 @@ def test_production_setup_rejects_stale_lock_before_tests_or_publication(tmp_pat

@pytest.mark.parametrize(
"workflow_name",
["test-source-for-runtime.yml", "publish-beta.yml"],
["test-source-for-runtime.yml", "publish-beta.yml", "python-314-compatibility.yml"],
)
def test_production_setup_rejects_unavailable_locked_package_before_tests_or_publication(tmp_path: Path, workflow_name: str, _use_real_commands: object) -> None:
command = _sync_commands(WORKFLOWS / workflow_name)[0]
Expand Down
Loading