diff --git a/.github/workflows/audit.yml b/.github/workflows/audit.yml index 6eed5ae..fefa464 100644 --- a/.github/workflows/audit.yml +++ b/.github/workflows/audit.yml @@ -62,7 +62,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.12.10" + version: "0.13.1" enable-cache: true - name: Install Python diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c461c51..79c5a2a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -31,7 +31,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 @@ -41,7 +41,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.12.10" + version: "0.13.1" enable-cache: true - name: Install Python ${{ matrix.python-version }} @@ -74,7 +74,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.12.10" + version: "0.13.1" enable-cache: true - name: Install Python @@ -134,7 +134,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.12.10" + version: "0.13.1" enable-cache: true - name: Install Python diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index 320c3c3..45c625b 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -34,7 +34,7 @@ jobs: strategy: fail-fast: false matrix: - python-version: ["3.11", "3.12", "3.13", "3.14"] + python-version: ["3.11", "3.12", "3.13", "3.14", "3.15"] steps: - uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 with: @@ -42,7 +42,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.12.10" + version: "0.13.1" enable-cache: true - run: uv python install ${{ matrix.python-version }} - run: uv sync --locked --extra dev @@ -67,7 +67,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.12.10" + version: "0.13.1" enable-cache: true - run: uv python install 3.13 - run: uv sync --locked --extra dev @@ -89,7 +89,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.12.10" + version: "0.13.1" enable-cache: true - run: uv python install 3.13 - run: uv sync --locked --extra dev @@ -115,7 +115,7 @@ jobs: - name: Install uv uses: astral-sh/setup-uv@1c37ad07a6a961277cf70c0d37d6f313000f5884 # v10.3.0 with: - version: "0.12.10" + version: "0.13.1" enable-cache: true - run: uv python install 3.13 diff --git a/AGENTS.md b/AGENTS.md index 6229af6..6612e09 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -10,7 +10,7 @@ Command sources: `.github/workflows/ci.yml` (the gates), `.github/workflows/rele - **Purpose:** SSH MCP server that lets AI assistants execute commands on remote servers. - **Owner:** TODO: team/maintainer - **Type:** app — published as a PyPI wheel (`uvx blc-ssh-mcp`) *and* a container image (`ghcr.io/blackaxgit/ssh-mcp`). **The PyPI distribution is `blc-ssh-mcp`, not `ssh-mcp`** — that name belongs to an unrelated project this repo has never owned; see the comment above `name` in `pyproject.toml`. The import package is still `ssh_mcp` and the image is still `.../ssh-mcp`. -- **Stacks:** Python 3.11-3.14, `uv` + hatchling, asyncssh, MCP SDK, Starlette/uvicorn (HTTP transport only) +- **Stacks:** Python 3.11-3.15, `uv` + hatchling, asyncssh, MCP SDK, Starlette/uvicorn (HTTP transport only) - **Deploy targets:** N/A — this is a tool operators run themselves, not a hosted service - **Version:** `src/ssh_mcp/__init__.py` is the single source (hatchling reads it). Currently `0.9.0`. `0.7.0` shipped 2026-09-06 (the MCP SDK v2 migration) and is the first release published under a reviewer-gated `pypi` environment; `0.6.0`, `0.6.1` and `0.6.2` all shipped 2026-07-26. `CHANGELOG.md` `[Unreleased]` is where post-0.9.0 work accrues. @@ -122,7 +122,7 @@ The full state matrix is documented on `ExecResult` in `models.py`. Do not "norm | Workflow | Trigger | Jobs | |---|---|---| -| `ci.yml` | push/PR to `main`, tags `v*` | `test` (3.11-3.14), `lint`, `audit`, `docker` (`needs: [test, lint, audit]`) | +| `ci.yml` | push/PR to `main`, tags `v*` | `test` (3.11-3.15), `lint`, `audit`, `docker` (`needs: [test, lint, audit]`) | | `audit.yml` | daily cron + manual | `pip-audit` against `main`, so a new advisory surfaces on its own schedule instead of reddening an unrelated PR | | `release.yml` | tag `v*` | re-runs test/lint/audit → `build` (asserts tag == packaged version, emits CycloneDX SBOM) → `publish-pypi` → `github-release` | @@ -130,7 +130,7 @@ The full state matrix is documented on `ExecResult` in `models.py`. Do not "norm **The image is multi-arch (`linux/amd64,linux/arm64`) as of 0.8.1, and that makes the Trivy gate arity load-bearing.** A multi-arch push produces an *index*, and Trivy resolves an index to the runner's own platform, so **one** scan step gates amd64 and publishes arm64 uninspected — with a green gate. There is therefore one scan step per platform, selected via `TRIVY_PLATFORM` (trivy-action v0.36.0 exposes no `platform` input but passes environment through). For the same reason the digest-verification step **derives** its expected manifest set from the scanned reference instead of comparing against `steps.build.outputs.digest`, which is now an index digest whose children are what `imagetools create` copies into the tag — hardcoding it would fail every publish run, the same shape as the 2026-07-26 incident one level down. `test_published_image_is_multi_arch_and_every_platform_is_scanned` asserts platforms-built equals platforms-scanned. -**The PR path builds amd64 only, so arm64 build breakage surfaces on `main`, not on the PR.** `load: true` cannot load a multi-platform result, so the PR build stays single-platform for fast feedback and Trivy has a local image to scan. The consequence is asymmetric and deliberate: a Dockerfile change that breaks the arm64 leg passes review and turns `main` red. That is loud and publishes nothing — the digest push happens in the same failed step — but it does mean an arch-sensitive change should be built locally first (`docker buildx build --platform linux/arm64 .`), which is how 0.8.1's arm64 leg was verified before merge. Both pinned bases were checked to be multi-arch indexes at the same time (`python:3.14-slim-trixie` and `ghcr.io/astral-sh/uv:0.12.10`); pinning a *single-platform* digest there is the failure this would otherwise find late. +**The PR path builds amd64 only, so arm64 build breakage surfaces on `main`, not on the PR.** `load: true` cannot load a multi-platform result, so the PR build stays single-platform for fast feedback and Trivy has a local image to scan. The consequence is asymmetric and deliberate: a Dockerfile change that breaks the arm64 leg passes review and turns `main` red. That is loud and publishes nothing — the digest push happens in the same failed step — but it does mean an arch-sensitive change should be built locally first (`docker buildx build --platform linux/arm64 .`), which is how 0.8.1's arm64 leg was verified before merge. Both pinned bases were checked to be multi-arch indexes at the same time (`python:3.14-slim-trixie` and `ghcr.io/astral-sh/uv:0.13.1`); pinning a *single-platform* digest there is the failure this would otherwise find late. All third-party actions are pinned to a full commit SHA with a `# vX` comment, and `astral-sh/setup-uv` additionally pins a `version:` so `uv` itself cannot float. Keep both — an unpinned tool is how this repo previously had CI go red on commits that had passed days earlier. Never replace a SHA with a tag reference. None of this is prose-only convention: `tests/test_ci_lint_determinism.py` asserts it, so a workflow edit that drops a pin fails the suite. diff --git a/CHANGELOG.md b/CHANGELOG.md index 1296f9d..42955ed 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +### Added + +**Python 3.15 is supported and tested.** CI and the release gate now run the test suite on 3.15 alongside 3.11-3.14, and the wheel carries the 3.15 classifier. No code change was needed. The pinned `uv` moves from 0.12.10 to 0.13.1 in every workflow and the Dockerfile, because 0.12.10 only knows CPython 3.15.0rc2 — a 3.15 leg on it would have tested a release candidate while reporting 3.15. 0.13.1 rather than 0.13.0 because 0.13.0 regressed on extracting source distributions that contain hard links, and the 3.15 leg builds PyYAML from its sdist (6.0.3 ships no cp315 wheel). The container image stays on Python 3.14. + ## [0.9.0] - 2026-09-30 > **Why 0.9.0 and not 0.8.2:** the `groups` fix under Fixed makes two configurations that started on 0.8.1 refuse to start, a string `groups` and an inline-table one, which is the test 0.8.0 recorded for taking the minor. The fix for both is a list: `groups = ["web"]`. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index d2956d3..4e5d3be 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -2,7 +2,7 @@ ## Development Setup -Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/getting-started/installation/). CI tests against 3.11, 3.12, 3.13 and 3.14. +Requires Python 3.11+ and [uv](https://docs.astral.sh/uv/getting-started/installation/). CI tests against 3.11, 3.12, 3.13, 3.14 and 3.15. ```bash git clone https://github.com/blackaxgit/ssh-mcp.git diff --git a/Dockerfile b/Dockerfile index f60e968..18a73f8 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,15 +1,15 @@ # Multi-stage build for Python MCP server with uv package manager # Base: python:3.14-slim-trixie (Debian 13, 2026 standard) # 3.14 is a deliberate image target; the package itself supports >=3.11 and CI -# runs the 3.11-3.14 matrix (pyproject.toml, .github/workflows/ci.yml). +# runs the 3.11-3.15 matrix (pyproject.toml, .github/workflows/ci.yml). # Stage 1: Builder - compile dependencies with uv # python:3.14-slim-trixie FROM python:3.14-slim-trixie@sha256:a2b82f3c48559aa0a8446d9af49826b6e2b2016f4cd2afabfe6013ec53729170 AS builder # Copy uv from official distribution image (not using as base to keep image small) -# ghcr.io/astral-sh/uv:0.12.10 -COPY --from=ghcr.io/astral-sh/uv:0.12.10@sha256:2bb3ebca0a796a155094a27773d290c4b074572e6107f171d88d086682fd2500 /uv /usr/local/bin/uv +# ghcr.io/astral-sh/uv:0.13.1 +COPY --from=ghcr.io/astral-sh/uv:0.13.1@sha256:4a1f3666816dafadd20ceb71eac9a7d56d7ef30279f7d64048519ab901ada336 /uv /usr/local/bin/uv # UV_COMPILE_BYTECODE=1 precompiles .pyc for faster cold start. # UV_LINK_MODE=copy copies out of the uv cache instead of hardlinking — the diff --git a/pyproject.toml b/pyproject.toml index f9106cf..a6652ba 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -36,6 +36,7 @@ classifiers = [ "Programming Language :: Python :: 3.12", "Programming Language :: Python :: 3.13", "Programming Language :: Python :: 3.14", + "Programming Language :: Python :: 3.15", "Topic :: System :: Systems Administration", "Topic :: Software Development :: Libraries :: Python Modules", ]