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
2 changes: 1 addition & 1 deletion .github/workflows/audit.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 4 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -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 }}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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
Expand Down
10 changes: 5 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -34,15 +34,15 @@ 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:
persist-credentials: false
- 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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand Down
6 changes: 3 additions & 3 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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.

Expand Down Expand Up @@ -122,15 +122,15 @@ 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` |

**Every push to `main` publishes a container image.** The `docker` job's `PUBLISH` is `github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))`, so a merged PR pushes `latest` + `type=sha` tags to GHCR with **no environment gate and no human approval** — a tag is not the only path to a publish. PRs build and scan only.

**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.

Expand Down
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]`.
Expand Down
2 changes: 1 addition & 1 deletion CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
6 changes: 3 additions & 3 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -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
Expand Down
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
]
Expand Down
Loading