Files
modelcontextprotocol--pytho…/scripts/ci/canary_cell.sh
Max Isbey 00f5e50a81 ci: make the canary report reason over the planned matrix
Review feedback, taken as one structural change rather than patches:
the report classified whatever artifacts happened to arrive, so a cell
that timed out or lost its runner simply vanished (two passing cells
read as green and would have closed a live incident), a cancelled run
could still write to the issue, and any resolve-job failure was
reported as "cannot be resolved". Now the matrix is defined once and
published by the resolve job, the report checks every planned cell
against `needs.test.result`, a cell writes a provisional status before
doing anything, `uv lock` records whether it was the thing that failed,
and incomplete runs are their own class that comments on an open
incident instead of rewriting or closing it. Artifacts carry their own
top-level directory and are merged on download, so the layout no longer
depends on how many of them exist.

Smaller corrections from the same review: per-package cutoffs instead
of a global --exclude-newer (a freshly bumped exact pin elsewhere could
otherwise fail the resolution); the since-last-green diff uses the lock
the last green run actually uploaded rather than re-resolving today's
tree at an old cutoff; the issue's reproduce block is the literal
commands the job ran (group strip included) under `uvx uv@<version>`;
per-cell output is size-bounded at the source and the full report goes
to the step summary before any truncation; the flake/deprecation re-runs
key on whether pytest recorded failures rather than on its exit code;
titles say "nothing changed since last green" when that is the case;
bash runs with pipefail everywhere; the group-strip generator follows
include-group and default-groups = "all"; wording says tooling is
preferred at uv.lock, not frozen. The temporary branch trigger is back
for one more end-to-end run.

No-Verification-Needed: CI-only change (workflow + scripts/ci); scripts exercised locally, workflow verified by a branch run
2026-08-16 13:09:01 +00:00

130 lines
5.3 KiB
Bash
Executable File

#!/usr/bin/env bash
# One dependency-canary test cell: install the resolved lock, run the suite,
# and classify the outcome so the report can tell a hard break from a new
# deprecation warning or a flake. Driven by .github/workflows/dependency-canary.yml.
#
# Inputs (env): CANARY_CELL (label, e.g. "ubuntu-3.14"), CANARY_PYTHON (e.g. "3.14"),
# CANARY_PYRIGHT=1 to append an informational `pyright src/mcp` result.
# Outputs under out/canary-cell-$CANARY_CELL/:
# status — pass | flaky | warnings-only | error | install-failed
# ("incomplete" is written first, so a killed cell still says something)
# cell.md — Markdown section for the issue body / job summary (bounded to ~20 kB)
# *.log — raw logs, kept in the artifact for debugging
# Exit status: 0 for pass/flaky, 1 otherwise (so the job shows red).
set -uo pipefail
cell="${CANARY_CELL:?}"
python="${CANARY_PYTHON:?}"
out="out/canary-cell-$cell"
mkdir -p "$out"
md="$out/cell.md"
echo incomplete >"$out/status"
printf '#### %s\n\nDid not finish (see the workflow run).\n' "$cell" >"$md"
export COLUMNS=200 # keeps pytest from truncating the -r summary lines quoted in the report
pytest_cmd=(uv run --frozen --no-sync pytest -p no:pretty -q --no-header -rfE --color=no -o log_cli=false)
# Demote only the deprecation family: if failures vanish under these, the newest
# versions still work and merely announce a future removal.
demote=(-W default::DeprecationWarning -W default::PendingDeprecationWarning -W default::FutureWarning)
summary_of() { # the short-summary FAILED/ERROR lines of a pytest log, capped; falls back to the log tail
local lines
lines=$(grep -E '^(FAILED|ERROR) ' "$1" | awk '!seen[$0]++' | cut -c1-240) # xdist repeats collection errors per worker
if [ -z "$lines" ]; then
tail -n 15 "$1" | cut -c1-240
return
fi
head -n 40 <<<"$lines"
local n
n=$(wc -l <<<"$lines")
if [ "$n" -gt 40 ]; then echo "... and $((n - 40)) more"; fi
}
details_of() { # collapsed, size-bounded tail of a log: file, summary text, max lines
printf '<details><summary>%s</summary>\n\n```text\n' "$2"
tail -n "${3:-60}" "$1" | cut -c1-240 | head -c 12000 | sed 's/```/` ` `/g'
printf '\n```\n\n</details>\n'
}
finish() {
echo "$1" >"$out/status"
echo "canary cell $cell: $1"
if [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then cat "$md" >>"$GITHUB_STEP_SUMMARY"; fi
case "$1" in pass | flaky) exit 0 ;; *) exit 1 ;; esac
}
printf '#### %s\n\n' "$cell" >"$md"
if ! uv sync --frozen --all-extras --python "$python" >"$out/sync.log" 2>&1; then
{
echo "**Install failed** — \`uv sync --frozen --all-extras --python $python\` could not install the resolved set on this platform."
echo
details_of "$out/sync.log" "uv sync output" 40
} >>"$md"
finish install-failed
fi
installed=$(uv run --frozen --no-sync python -V 2>/dev/null || echo "Python $python")
# Phase 1: the suite as CI runs it (from a clean last-failed record, which phase 2 keys on).
rm -f .pytest_cache/v/cache/lastfailed
if "${pytest_cmd[@]}" -n auto >"$out/run1.log" 2>&1; then
echo "All tests pass ($installed)." >>"$md"
status=pass
else
# Phase 2: again, to drop flakes. Narrow to the recorded failures (serially) when
# pytest got far enough to record any; a collection/usage error leaves none.
if grep -qs '::' .pytest_cache/v/cache/lastfailed; then
rerun=("${pytest_cmd[@]}" --lf --last-failed-no-failures none -p no:xdist)
rerun_desc="a serial re-run of just the failing tests"
else
rerun=("${pytest_cmd[@]}" -n auto)
rerun_desc="a full re-run"
fi
if "${rerun[@]}" >"$out/run2.log" 2>&1; then
{
echo "Passed on $rerun_desc ($installed); the first attempt failed as below (treated as flaky, not reported):"
echo
echo '```text'
summary_of "$out/run1.log"
echo '```'
} >>"$md"
status=flaky
else
# Phase 3: same again with the deprecation family demoted, to tell "broken" from "deprecated".
if "${rerun[@]}" "${demote[@]}" >"$out/run3.log" 2>&1; then
{
echo "**Deprecation warnings only** ($installed) — the failures below persist on $rerun_desc but disappear once"
echo "\`DeprecationWarning\`/\`PendingDeprecationWarning\`/\`FutureWarning\` are not errors, so the newest versions still work and are announcing a removal we need to get ahead of."
} >>"$md"
status=warnings-only
else
echo "**Hard failures** ($installed) — the failures below persist on $rerun_desc and with deprecation warnings demoted:" >>"$md"
status=error
fi
{
echo
echo '```text'
summary_of "$out/run2.log"
echo '```'
echo
details_of "$out/run2.log" "pytest output (tail)" 80
} >>"$md"
fi
fi
if [ "${CANARY_PYRIGHT:-}" = "1" ]; then
# Typing-only drift (e.g. a dependency tightening a signature) never fails the cell; it is context for the reader.
if PYRIGHT_PYTHON_IGNORE_WARNINGS=1 uv run --frozen --no-sync pyright src/mcp >"$out/pyright.log" 2>&1; then
printf '\npyright on `src/mcp` against these versions: clean (informational).\n' >>"$md"
else
count=$(grep -Eo '^[0-9]+ errors?' "$out/pyright.log" | tail -n1)
{
echo
details_of "$out/pyright.log" "pyright on <code>src/mcp</code> against these versions: ${count:-did not complete} (informational, never filed on its own)" 30
} >>"$md"
fi
fi
finish "$status"