From ef0aaf7c6164105156797c195f1179fed1ed1ae4 Mon Sep 17 00:00:00 2001 From: YEONGUK LEE Date: Thu, 29 Oct 2026 22:16:47 +0900 Subject: [PATCH 1/2] ci: validate Python 3.14 across supported runtime profiles --- .github/workflows/ci.yml | 46 +++++--- tests/README.md | 18 ++- tests/docs/test_mcp_dependency_profiles.py | 2 +- tests/docs/test_python_matrix.py | 124 +++++++++++++++++++++ 4 files changed, 175 insertions(+), 15 deletions(-) create mode 100644 tests/docs/test_python_matrix.py diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f006d9b..34cafa4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,7 +51,7 @@ jobs: matrix: # 3.9 is the floor the README promises; macOS is half the fleet. os: [ubuntu-latest, macos-latest] - python: ['3.9', '3.13'] + python: ['3.9', '3.13', '3.14'] include: - os: windows-latest python: '3.13' @@ -92,13 +92,23 @@ jobs: - run: python -m unittest discover -s tests/docs -t . -v package: - name: wheel and sdist - runs-on: ubuntu-latest + name: wheel and sdist (${{ matrix.os }}, python ${{ matrix.python }}) + runs-on: ${{ matrix.os }} + strategy: + fail-fast: false + matrix: + include: + - os: ubuntu-latest + python: '3.13' + - os: ubuntu-latest + python: '3.14' + - os: macos-latest + python: '3.14' steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 with: - python-version: '3.13' + python-version: ${{ matrix.python }} - run: python -m pip install --requirement .github/requirements/build.txt - run: python tools/generate_session_peer.py --check - run: python -m build @@ -107,14 +117,23 @@ jobs: SESSION_PEER_DIST_DIR: dist run: python -m unittest discover -s tests/distribution -t . -v - name: Install wheel and sdist independently + shell: bash run: | - python -m venv /tmp/session-peer-wheel - /tmp/session-peer-wheel/bin/pip install dist/*.whl - /tmp/session-peer-wheel/bin/session-peer --version - /tmp/session-peer-wheel/bin/session-peer update --json - python -m venv /tmp/session-peer-sdist - /tmp/session-peer-sdist/bin/pip install dist/*.tar.gz - /tmp/session-peer-sdist/bin/session-peer --version + expected=$(python -c 'import session_peer; print(session_peer.__version__)') + for kind in wheel sdist; do + venv="$RUNNER_TEMP/session-peer-package-$kind" + python -m venv "$venv" + if test "$kind" = wheel; then + "$venv/bin/python" -m pip install dist/*.whl + else + "$venv/bin/python" -m pip install dist/*.tar.gz + fi + test "$("$venv/bin/python" -I -c 'import pathlib, session_peer, sysconfig; pathlib.Path(session_peer.__file__).resolve().relative_to(pathlib.Path(sysconfig.get_path("purelib")).resolve()); print(session_peer.__version__)')" = "$expected" + test "$("$venv/bin/session-peer" --version)" = "session-peer $expected" + "$venv/bin/session-peer" --help + "$venv/bin/python" -m pip check + done + "$RUNNER_TEMP/session-peer-package-wheel/bin/session-peer" update --json shell: name: shellcheck install.sh @@ -169,7 +188,7 @@ jobs: fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - python: ['3.10', '3.13'] + python: ['3.10', '3.13', '3.14'] profile: [minimum, latest] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -251,9 +270,10 @@ jobs: name: optional relay (${{ matrix.os }}, python ${{ matrix.python }}) runs-on: ${{ matrix.os }} strategy: + fail-fast: false matrix: os: [ubuntu-latest, macos-latest] - python: ['3.11', '3.13'] + python: ['3.11', '3.13', '3.14'] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - uses: actions/setup-python@5fda3b95a4ea91299a34e894583c3862153e4b97 # v7.0.0 diff --git a/tests/README.md b/tests/README.md index 1150119..a3a9efe 100644 --- a/tests/README.md +++ b/tests/README.md @@ -26,13 +26,29 @@ control integration suite additionally requires `npm ci` in `control/` and `SESSION_PEER_CONTROL_INTEGRATION=1`; without that explicit opt-in, the live Node/Python fixture is skipped. -The MCP CI matrix tests Python 3.10/3.13 on macOS/Linux with both the exact SDK +The CI matrix includes standard CPython 3.9/3.13/3.14 core fixture cells on +macOS/Linux and retains Windows CPython 3.13. The 3.14 core cells run the +generated-standalone, core, adapter, Codex and distribution-source checks. +Separate macOS/Linux +3.14 package cells build wheel/sdist, verify their full contents and install +each independently in fresh environments, checking the installed import origin, +exact CLI version, help and dependency consistency. The existing Linux 3.13 +package cell is retained. + +The MCP CI matrix tests Python 3.10/3.13/3.14 on macOS/Linux with both the exact SDK floor (`.github/requirements/mcp-minimum.txt`) and latest compatible 1.x. Transitive dependencies resolve normally, not at their individual minima. Both runtime graphs (including pip/setuptools bootstrap packages) are audited separately from the pinned build/audit requirements and retained as CI artifacts. Existing latest-resolution and release audits remain required. +Relay fixtures also run on macOS/Linux with Python 3.11/3.13/3.14. All core, +package, MCP and Relay cells are required by the existing release gate. These +are fixture/package checks, not new live native-agent, SSH, Windows 3.14 or +free-threaded Python acceptance. Python floors remain core 3.9, MCP 3.10 and +Relay 3.11. CI outcomes must be recorded at the exact reviewed source before +expanding supported-version metadata. + After building exactly one wheel and one sdist, validate the full distribution contract with: diff --git a/tests/docs/test_mcp_dependency_profiles.py b/tests/docs/test_mcp_dependency_profiles.py index 730256b..50ef25c 100644 --- a/tests/docs/test_mcp_dependency_profiles.py +++ b/tests/docs/test_mcp_dependency_profiles.py @@ -23,7 +23,7 @@ def test_floor_matrix_and_release_gate_preserve_supported_contract(self): self.assertIn('mcp>=1.30,<2; python_version >= \'3.10\'', manifest) constraint = (ROOT / '.github/requirements/mcp-minimum.txt').read_text() self.assertEqual(['mcp==1.30.0'], [x for x in constraint.splitlines() if x and not x.startswith('#')]) - for text in ("os: [ubuntu-latest, macos-latest]", "python: ['3.10', '3.13']", + for text in ("os: [ubuntu-latest, macos-latest]", "python: ['3.10', '3.13', '3.14']", 'profile: [minimum, latest]', 'fail-fast: false'): self.assertIn(text, MCP) install = run_script(MCP, 'Install MCP runtime profile') diff --git a/tests/docs/test_python_matrix.py b/tests/docs/test_python_matrix.py new file mode 100644 index 0000000..e170871 --- /dev/null +++ b/tests/docs/test_python_matrix.py @@ -0,0 +1,124 @@ +"""Keep interpreter/profile coverage and installed-artifact probes release-gated.""" + +from itertools import product +import os +from pathlib import Path +import re +import subprocess +import sys +import tempfile +import unittest +import venv + +from tests.docs.test_dependency_audit_workflow import run_script + + +ROOT = Path(__file__).resolve().parents[2] +CI = (ROOT / '.github/workflows/ci.yml').read_text(encoding='utf-8') + + +def job(name): + start = re.search(r'^ ' + re.escape(name) + r':\n', CI, re.M) + if start is None: + raise AssertionError('Missing CI job: ' + name) + remainder = CI[start.end():] + next_job = re.search(r'^ [a-z][a-z0-9-]*:\n', remainder, re.M) + return remainder[:next_job.start()] if next_job else remainder + + +def cells(name): + # This is the small matrix subset used by this workflow, not a YAML parser. + # Accept only simple literal scalar data, never evaluate YAML expressions. + text = job(name).split(' steps:\n', 1)[0] + axes = [] + for key, values in re.findall(r'^ ([a-z]+): (\[[^\n]+\])$', text, re.M): + scalars = [] + for value in values[1:-1].split(','): + value = value.strip() + if not re.fullmatch(r"(?:'[A-Za-z0-9_.-]+'|\"[A-Za-z0-9_.-]+\"|[A-Za-z0-9_.-]+)", value): + raise AssertionError('Unsupported matrix literal') + scalars.append(value.strip("'\"")) + axes.append((key, scalars)) + result = {tuple(zip((key for key, _ in axes), values)) + for values in product(*(values for _, values in axes))} if axes else set() + includes = re.findall(r'^ - os: ([^\n]+)\n python: \'([^\']+)\'$', text, re.M) + result.update((('os', os_name), ('python', python)) for os_name, python in includes) + return [dict(cell) for cell in result] + + +class PythonMatrix(unittest.TestCase): + def test_core_floors_new_posix_interpreter_and_windows_are_retained(self): + actual = {(x['os'], x['python']) for x in cells('test')} + expected = set(product(('ubuntu-latest', 'macos-latest'), ('3.9', '3.13', '3.14'))) + expected.add(('windows-latest', '3.13')) + self.assertEqual(expected, actual) + for suite in ('core', 'adapters', 'codex', 'distribution'): + self.assertIn('unittest discover -s tests/' + suite, job('test')) + self.assertIn('tools/generate_session_peer.py --check', job('test')) + + def test_optional_profiles_keep_their_minimums_and_all_release_gates(self): + mcp = {(x['os'], x['python'], x['profile']) for x in cells('mcp')} + self.assertEqual(set(product(('ubuntu-latest', 'macos-latest'), + ('3.10', '3.13', '3.14'), ('minimum', 'latest'))), mcp) + relay = {(x['os'], x['python']) for x in cells('relay')} + self.assertEqual(set(product(('ubuntu-latest', 'macos-latest'), ('3.11', '3.13', '3.14'))), relay) + self.assertIn('--constraint .github/requirements/mcp-minimum.txt', job('mcp')) + self.assertIn('Audit resolved MCP runtime graph', job('mcp')) + self.assertIn('unittest discover -s tests/mcp_integration', job('mcp')) + self.assertIn('unittest discover -s tests/relay', job('relay')) + gate = job('release-gate') + for name in ('test', 'package', 'mcp', 'relay'): + self.assertRegex(gate, r'needs: \[[^\]]*\b' + name + r'\b') + self.assertIn('needs.' + name + '.result', gate) + for name in ('test', 'package', 'mcp', 'relay'): + self.assertIn('fail-fast: false', job(name)) + self.assertNotIn('continue-on-error', job(name)) + + def test_package_cells_build_and_install_both_artifacts_on_314(self): + self.assertEqual({('ubuntu-latest', '3.13'), ('ubuntu-latest', '3.14'), ('macos-latest', '3.14')}, + {(x['os'], x['python']) for x in cells('package')}) + package = job('package') + self.assertIn('python-version: ${{ matrix.python }}', package) + self.assertIn('tools/generate_session_peer.py --check', package) + self.assertIn('python -m build', package) + self.assertIn('SESSION_PEER_DIST_DIR: dist', package) + script = run_script(package, 'Install wheel and sdist independently') + self.assertIn('for kind in wheel sdist', script) + self.assertIn('"$RUNNER_TEMP/session-peer-package-$kind"', script) + for marker in ('pip install dist/*.whl', 'pip install dist/*.tar.gz', '--help', 'pip check'): + self.assertIn(marker, script) + + @unittest.skipIf(os.name == 'nt', 'package cells use POSIX shell') + def test_actual_package_version_probe_rejects_checkout_shadowing(self): + script = run_script(job('package'), 'Install wheel and sdist independently') + probe = next(line for line in script.splitlines() if '-I -c ' in line) + expected = re.search(r'^__version__ = "([^"]+)"$', + (ROOT / 'session_peer.py').read_text(), re.M).group(1) + with tempfile.TemporaryDirectory(prefix='codex-python-package-') as directory: + env_dir = Path(directory) / 'venv' + venv.EnvBuilder(with_pip=False, symlinks=True).create(env_dir) + python = str(env_dir / 'bin/python') + purelib = Path(subprocess.check_output( + [python, '-I', '-c', 'import sysconfig; print(sysconfig.get_path("purelib"))'], + text=True).strip()) + installed = purelib / 'session_peer.py' + for index, mode in enumerate(('wrong-version', 'installed-version', 'outside-purelib')): + with self.subTest(mode=mode): + version = expected if mode != 'wrong-version' else '0.0.0' + installed.write_text('__version__ = ' + repr(version) + '\n' + + ('__file__ = ' + repr(str(ROOT / 'session_peer.py')) + '\n' + if mode == 'outside-purelib' else '')) + # Equal-sized fixture versions written within one second + # must not reuse the preceding subprocess's timestamp pyc. + stamp = 1690000000 + index * 2 + os.utime(installed, (stamp, stamp)) + result = subprocess.run( + ['bash', '--noprofile', '--norc', '-e', '-o', 'pipefail', '-c', probe], + cwd=ROOT, env=dict(os.environ, venv=str(env_dir), expected=expected, + PYTHONPATH=str(ROOT)), capture_output=True, text=True) + self.assertEqual(mode == 'installed-version', result.returncode == 0, + 'Package import/version boundary mismatch') + + +if __name__ == '__main__': + unittest.main() From 507fc2bdb9cfa96a89b0ed33300faca0bf188d00 Mon Sep 17 00:00:00 2001 From: YEONGUK LEE Date: Thu, 29 Oct 2026 22:30:55 +0900 Subject: [PATCH 2/2] chore(metadata): declare Python 3.14 after matrix validation --- pyproject.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/pyproject.toml b/pyproject.toml index 1d4340f..af6ed89 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -26,6 +26,7 @@ classifiers = [ "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", + "Programming Language :: Python :: 3.14", "Topic :: Software Development", "Topic :: System :: Networking", "Topic :: Utilities",