Skip to content

ci: run CI on uv 0.11.33 #1

ci: run CI on uv 0.11.33

ci: run CI on uv 0.11.33 #1

name: Dependency canary
# Weekly: re-resolve the runtime dependencies of `mcp[cli,rich]` to the newest
# versions our (floors-only) specifiers allow, ignoring uv.lock, run the test
# suite against them, and keep ONE tracking issue in sync with the result —
# opened (and assigned) when newest-allowed breaks, refreshed weekly while it
# stays broken, closed automatically once it passes again.
#
# Why this exists: users who `pip install mcp` get the newest release of every
# dependency the day it ships, while PR CI only ever sees uv.lock (`locked`) and
# the floors (`lowest-direct`). This is deliberately NOT part of PR CI, so an
# upstream release can never turn an unrelated PR red; the price is up to a
# week of latency, which the incident history says is fine.
#
# What it does not do, on purpose:
# - open a PR adding a ceiling. A cap only helps once released, resolvers
# route around retroactive caps by picking an older uncapped mcp, and the
# bot cannot tell which package (or interaction) is at fault. The issue's
# "What to do" section is the runbook; a human decides.
# - float test tooling (pytest, ruff, pyright, coverage, ...). Those stay at
# uv.lock so a pytest major cannot masquerade as an SDK break; Dependabot
# owns moving them.
# - test pre-releases on the schedule. `workflow_dispatch` with
# `prerelease: true` does that on demand and never files an issue.
# - bisect. The issue lists what changed since the last green run (usually
# one to three packages) and the one-line command to pin a suspect back.
#
# Known blind spots: Python 3.11-3.13 and macOS are not run; runtime deps that a
# *dev* dependency caps (e.g. logfire pins opentelemetry-sdk, which pins
# opentelemetry-api) cannot reach their newest release here — the issue's "Not
# tested at their newest release" section lists them each run. Dependency
# groups outside `default-groups` (translate, codegen) are stripped before
# resolving so their caps (anthropic: pydantic<3) do not apply.
#
# Notifications: assignees get the issue traffic. GitHub additionally e-mails
# scheduled-run failures only to whoever last edited the `cron:` line below.
on:
schedule:
- cron: "23 5 * * 1" # Mondays 05:23 UTC
workflow_dispatch:
inputs:
prerelease:
description: "Also consider pre-releases (investigative run; never files an issue)"
type: boolean
default: false
file-issue:
description: "Create/update/close the tracking issue exactly as a scheduled run would"
type: boolean
default: false
# TEMPORARY while this workflow is under review: exercise it on the PR branch.
# Report-only (push runs never touch issues). Remove before merging.
push:
branches: ["ci/dependency-canary"]
permissions: {}
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
COLUMNS: 150
UV_VERSION: "0.11.33"
# Releases younger than this are invisible to the run: skips half-uploaded
# releases (the ruff 0.14.12 incident that got the per-PR "highest" leg
# removed in #1869) and same-day yanks. A weekly job loses nothing by it.
CANARY_LAG: "24 hours"
CANARY_LABEL: dependency-canary
CANARY_ASSIGNEES: "maxisbey,Kludex"
jobs:
resolve:
# Don't run the schedule on forks.
if: github.repository == 'modelcontextprotocol/python-sdk'
runs-on: ubuntu-latest
timeout-minutes: 15
permissions:
contents: read
actions: read # `gh run list`: find the last green scheduled run to diff against
outputs:
cutoff: ${{ steps.cutoffs.outputs.cutoff }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
# setup-uv's manifest fetch is a single request with a hard 5s timeout
# (astral-sh/setup-uv#869); retry once. Drop when upstream adds a retry.
- name: Install uv
id: setup-uv
continue-on-error: true
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false
version: ${{ env.UV_VERSION }}
- name: Install uv (retry)
if: steps.setup-uv.outcome == 'failure'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false
version: ${{ env.UV_VERSION }}
- name: Compute cutoffs
id: cutoffs
env:
GH_TOKEN: ${{ github.token }}
run: |
mkdir -p canary-resolve
cutoff=$(date -u -d "-$CANARY_LAG" +%Y-%m-%dT%H:%M:%SZ)
echo "cutoff=$cutoff" >>"$GITHUB_OUTPUT"
echo "$cutoff" >canary-resolve/cutoff.txt
uv self version >canary-resolve/uv-version.txt
# Baseline = what the last green *scheduled* run saw (its start time minus the same lag).
last_green=$(gh run list --repo "$GITHUB_REPOSITORY" --workflow dependency-canary.yml \
--branch main --event schedule --status success --limit 1 --json startedAt --jq '.[0].startedAt // empty')
if [ -n "$last_green" ]; then
date -u -d "$last_green -$CANARY_LAG" +%Y-%m-%dT%H:%M:%SZ >canary-resolve/baseline.txt
else
: >canary-resolve/baseline.txt
fi
echo "cutoff=$cutoff baseline=$(cat canary-resolve/baseline.txt)"
- name: Work out what to float
run: |
# The runtime closure of mcp[cli,rich] as currently locked: exactly the set
# `pip install "mcp[cli,rich]"` pulls in. New transitive deps that a newer
# release introduces have no lock entry and so resolve to newest anyway.
uv export --frozen --no-default-groups --all-extras --no-emit-workspace \
--no-hashes --no-header --no-annotate | sed -E 's/[=; @].*//' | sort -u >canary-resolve/closure.txt
echo "Floating $(wc -l <canary-resolve/closure.txt) packages:"; tr '\n' ' ' <canary-resolve/closure.txt; echo
# Dependency groups that `uv sync` does not install still constrain the
# resolution (uv.lock is universal). Strip the non-default ones so e.g. the
# translate group's `anthropic` cannot hold pydantic below a new major.
python3 - <<'EOF' >canary-resolve/strip.sh
import re, tomllib
project = tomllib.load(open("pyproject.toml", "rb"))
keep = set(project.get("tool", {}).get("uv", {}).get("default-groups", []))
for group, deps in project.get("dependency-groups", {}).items():
names = [re.match(r"[A-Za-z0-9._-]+", d).group(0) for d in deps if isinstance(d, str)]
if group not in keep and names:
print("uv remove --frozen --group", group, *names)
EOF
cat canary-resolve/strip.sh
bash -e canary-resolve/strip.sh
- name: Resolve newest allowed versions
env:
PRERELEASE: ${{ inputs.prerelease && 'allow' || '' }}
run: |
set -o pipefail
args=(--exclude-newer "$(cat canary-resolve/cutoff.txt)")
if [ -n "$PRERELEASE" ]; then args+=(--prerelease "$PRERELEASE"); fi
while read -r pkg; do args+=(-P "$pkg"); done <canary-resolve/closure.txt
cp uv.lock canary-resolve/committed.lock
# Reconstruct what the last green run resolved (same command, its cutoff) so the
# report can list only what moved since. Best effort: a failure here just drops that table.
baseline=$(cat canary-resolve/baseline.txt)
if [ -n "$baseline" ]; then
echo "::group::Baseline resolution as of the last green run ($baseline)"
base_args=("${args[@]}")
base_args[1]=$baseline
if uv lock "${base_args[@]}" 2>&1 | tee canary-resolve/baseline.log; then
cp uv.lock canary-resolve/baseline.lock
else
echo "::warning::could not re-resolve the last-green baseline; the report will only diff against uv.lock"
: >canary-resolve/baseline.txt
fi
cp canary-resolve/committed.lock uv.lock
echo "::endgroup::"
fi
uv lock "${args[@]}" 2>&1 | tee canary-resolve/lock.log
cp uv.lock canary-resolve/uv.lock
- name: Summarise what moved
run: |
python3 scripts/ci/canary_lock_diff.py canary-resolve/committed.lock uv.lock \
--old-label "uv.lock" --new-label "this run" --suspects canary-resolve/suspects-vs-lock.txt >canary-resolve/vs-lock.md
if [ -f canary-resolve/baseline.lock ]; then
python3 scripts/ci/canary_lock_diff.py canary-resolve/baseline.lock uv.lock \
--old-label "last green" --new-label "this run" --suspects canary-resolve/suspects-since-green.txt >canary-resolve/since-green.md
fi
# Direct runtime deps that could not reach their newest release (capped by something else in the resolution).
uv tree --frozen --outdated --depth 1 --package mcp >canary-resolve/tree.txt 2>/dev/null || true
{
echo "| Package | Resolved | Latest |"
echo "| --- | --- | --- |"
sed -nE 's/^[^A-Za-z0-9]*([A-Za-z0-9._-]+)(\[[^]]*\])? v([^ ]+)( \(extra: [^)]*\))? \(latest: v([^)]+)\)$/| \1 | \3 | \5 |/p' canary-resolve/tree.txt
} >canary-resolve/held-back.md
if [ "$(wc -l <canary-resolve/held-back.md)" -le 2 ]; then : >canary-resolve/held-back.md; fi
{
echo "## Resolution (cutoff $(cat canary-resolve/cutoff.txt))"
echo
if [ -s canary-resolve/since-green.md ]; then echo "### Since last green ($(cat canary-resolve/baseline.txt))"; cat canary-resolve/since-green.md; echo; fi
echo "### vs uv.lock"; cat canary-resolve/vs-lock.md; echo
if [ -s canary-resolve/held-back.md ]; then echo "### Held back below latest"; cat canary-resolve/held-back.md; fi
} >>"$GITHUB_STEP_SUMMARY"
- name: Upload resolution
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: canary-resolve
path: canary-resolve/
retention-days: 90
if-no-files-found: error
test:
name: test (${{ matrix.cell }})
needs: resolve
runs-on: ${{ matrix.os }}
timeout-minutes: 20
permissions:
contents: read
strategy:
fail-fast: false
matrix:
include:
# Oldest and newest supported Python bracket the marker forks in the
# lock (deps drop 3.10 first; 3.14 gets wheels last). Windows/newest is
# where a fresh release most often lacks a wheel, and pywin32 lives there.
- { cell: ubuntu-3.10, os: ubuntu-latest, python: "3.10" }
- {
cell: ubuntu-3.14,
os: ubuntu-latest,
python: "3.14",
smoke: "1",
pyright: "1",
}
- { cell: windows-3.14, os: windows-latest, python: "3.14" }
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
- name: Install uv
id: setup-uv
continue-on-error: true
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false
version: ${{ env.UV_VERSION }}
- name: Install uv (retry)
if: steps.setup-uv.outcome == 'failure'
uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0
with:
enable-cache: false
version: ${{ env.UV_VERSION }}
- name: Fetch the resolved lock
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
name: canary-resolve
path: canary-resolve
- name: Install and test
shell: bash
env:
CANARY_CELL: ${{ matrix.cell }}
CANARY_PYTHON: ${{ matrix.python }}
CANARY_PYRIGHT: ${{ matrix.pyright }}
# Same switches as PR CI: real stdio/uvicorn subprocess smoke tests on one
# cell, and PEP 597 EncodingWarnings surfaced (as an env var so xdist workers inherit it).
MCP_EXAMPLES_SMOKE: ${{ matrix.smoke }}
PYTHONWARNDEFAULTENCODING: "1"
run: |
cp canary-resolve/uv.lock uv.lock
# The lock was resolved with these groups stripped; keep pyproject consistent so --frozen holds.
bash -e canary-resolve/strip.sh
bash scripts/ci/canary_cell.sh
- name: Upload cell result
if: always()
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: canary-cell-${{ matrix.cell }}
path: |
canary-out/status
canary-out/cell.md
retention-days: 30
if-no-files-found: error
report:
needs: [resolve, test]
# always(): a red resolve/test job is exactly when this must run.
if: always() && github.repository == 'modelcontextprotocol/python-sdk' && needs.resolve.result != 'cancelled' && needs.resolve.result != 'skipped'
runs-on: ubuntu-latest
timeout-minutes: 10
permissions:
contents: read # checkout, for scripts/ci/canary_report.sh
issues: write # open / refresh / close the tracking issue
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
persist-credentials: false
sparse-checkout: scripts/ci
- name: Collect results
uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1
with:
pattern: canary-*
path: artifacts
- name: Report
env:
GH_TOKEN: ${{ github.token }}
CANARY_ARTIFACTS: artifacts
CANARY_RESOLVE_RESULT: ${{ needs.resolve.result }}
CANARY_RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
# Only the schedule (or an explicit dispatch asking for it) touches issues, and never a pre-release run.
CANARY_FILE_ISSUES: ${{ !inputs.prerelease && (github.event_name == 'schedule' || inputs.file-issue) && 'true' || 'false' }}
run: bash scripts/ci/canary_report.sh