Skip to content
Closed
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
131 changes: 131 additions & 0 deletions .github/workflows/backcompat_matrix.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,131 @@
name: Mixed-version back-compat matrix

# Runs the intent-topic back-compat suite across four skill/core version
# combinations. Each combination needs TWO python environments alive at once —
# one for the skill's packages, one for the core's — so it cannot live in
# `build_tests.yml`, which installs a single environment and runs pytest in it.
#
# The four cells and the pins behind them are documented in
# `test/backcompat/test_mixed_version_matrix.py` and
# `test/backcompat/build_venvs.sh`.
#
# Alongside the boundary-pin cells there are four channel cells
# (stable-skill/dev-core, dev-skill/stable-core, testing-skill/dev-core,
# dev-skill/testing-core) built from the OVOS distro's own constraint
# files, fetched fresh at build time. Those track the fleet rather than a
# boundary: a distro pin bump past a behavior boundary flips one of these
# red at that exact moment.
#
# The `old-skill/new-core` cell is expected to xfail until ovos-bus-client#271
# releases. It is `strict=True`, so the day the fix ships the job goes RED with
# an XPASS. That is the intended signal, not a regression: drop the marker and
# the cell becomes a permanent guard.
#
# This job does NOT use the shared gh-automations reusable workflow: it builds
# its own venv pairs rather than one environment.

on:
pull_request:
branches: [dev]
paths:
- 'ovos_core/**'
- 'test/backcompat/**'
- '.github/workflows/backcompat_matrix.yml'
- 'requirements/**'
push:
branches: [dev]
workflow_dispatch:

jobs:
matrix:
runs-on: ubuntu-latest
timeout-minutes: 45
strategy:
fail-fast: false
matrix:
include:
# combo id skill venv core venv
- combo: old-skill/old-core
skill: venv_skill_old
core: venv_core_old
- combo: old-skill/new-core
skill: venv_skill_old
core: venv_core_new
- combo: new-skill/old-core
skill: venv_skill_new
core: venv_core_old
- combo: new-skill/new-core
skill: venv_skill_new
core: venv_core_new
# channel cells: one side pinned by a live OVOS distro constraints
# file (fetched fresh in build_venvs.sh), the other at dev. These
# verify the fleet, not a boundary — see test_mixed_version_matrix.py.
- combo: stable-skill/dev-core
skill: venv_skill_stable
core: venv_core_new
- combo: dev-skill/stable-core
skill: venv_skill_new
core: venv_core_stable
- combo: testing-skill/dev-core
skill: venv_skill_testing
core: venv_core_new
- combo: dev-skill/testing-core
skill: venv_skill_new
core: venv_core_testing
name: ${{ matrix.combo }}
steps:
- uses: actions/checkout@v4

- name: Set up Python 3.11
uses: actions/setup-python@v5
with:
python-version: "3.11"

- name: Install system deps (padatious needs swig + libfann)
run: sudo apt-get update && sudo apt-get install -y swig libfann-dev

- name: Install uv
uses: astral-sh/setup-uv@v5

- name: Build the venv pair
env:
BACKCOMPAT_PYTHON: "3.11"
run: |
chmod +x test/backcompat/build_venvs.sh
test/backcompat/build_venvs.sh "$RUNNER_TEMP/venvs" "$GITHUB_WORKSPACE"

- name: Record the resolved versions
run: |
for v in venv_skill_old venv_skill_new venv_core_old venv_core_new; do
echo "== $v"
"$RUNNER_TEMP/venvs/$v/bin/python" -m pip freeze 2>/dev/null \
|| "$RUNNER_TEMP/venvs/$v/bin/python" -c \
"from importlib.metadata import distributions; [print(f'{d.metadata[\"Name\"]}=={d.version}') for d in sorted(distributions(), key=lambda x: x.metadata['Name'] or '')]"
done | tee venv-freeze.txt

- name: Upload the resolved versions
if: always()
uses: actions/upload-artifact@v4
with:
name: venv-freeze-${{ strategy.job-index }}
path: venv-freeze.txt

# Channel cells only produce these, but the glob is harmless for the
# boundary-pin cells. Kept so a red channel cell can be traced back to
# exactly what the distro pinned that day.
- name: Upload the fetched constraints files
if: always()
uses: actions/upload-artifact@v4
with:
name: constraints-${{ strategy.job-index }}
path: ${{ runner.temp }}/venvs/*.constraints.txt
if-no-files-found: ignore

- name: Run the combo
env:
BACKCOMPAT_COMBO: ${{ matrix.combo }}
run: |
export BACKCOMPAT_SKILL_PYTHON="$RUNNER_TEMP/venvs/${{ matrix.skill }}/bin/python"
"$RUNNER_TEMP/venvs/${{ matrix.core }}/bin/pytest" \
test/backcompat/ -v --tb=short -rxX \
--timeout=300 --timeout-method=thread
Empty file added test/backcompat/__init__.py
Empty file.
123 changes: 123 additions & 0 deletions test/backcompat/build_venvs.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
#!/usr/bin/env bash
# Build the four venvs the mixed-version back-compat matrix runs against.
#
# Two venvs are alive per combo: one holds the skill's packages, one holds the
# core's. They talk over a real websocket, which is the only way one test run
# can observe two different package sets.
#
# Usage: test/backcompat/build_venvs.sh <target-dir> [path-to-core-checkout]
#
# Pins, and why each one is where it is:
#
# skill_old ovos-workshop==9.3.1a2
# The newest release that binds the handler to the suffixed
# `<skill_id>:<file>.intent` topic ONLY. 9.3.2a1 added the
# canonical binding alongside it (ovos-workshop#497), which hides
# the breakage, so the pin must stay below that line.
# ovos-bus-client is deliberately NOT pinned: a real frozen
# container resolves the workshop floor and gets a current client,
# and the repair in ovos-bus-client#271 depends on exactly that.
#
# skill_new ovos-workshop @ dev
# Binds both spellings today. ovos-workshop#500 makes it
# canonical-only, which is what will turn the new-skill/old-core
# cell red.
#
# core_old ovos-core==2.5.5a2 + ovos-padatious==2.0.0a1
# The newest padatious release BEFORE registration-time
# canonicalization (`_dealias_intent_name`, added in 2.0.1a1), so
# this side dispatches whatever the skill registered. ovos-core is
# pinned to its contemporary release; core itself forwards
# `match.match_type` verbatim, so the pipeline plugin is the part
# that decides the spelling.
#
# core_new this checkout + ovos-padatious>=2.0.1a2
# Folds at registration, so it dispatches the canonical topic.
#
# venv_skill_stable / venv_skill_testing
# venv_core_stable / venv_core_testing
# Not boundary pins — fleet pins. Built by installing straight
# off the OVOS distro's own constraint files, fetched at build
# time (not vendored), so this gate TRACKS the fleet: the day the
# distro bumps a pin past a behavior boundary, the affected cell
# goes red at that exact moment, which is the point. Today both
# channels resolve an ovos-workshop and ovos-padatious floor well
# below the 9.3.2a1 / 2.0.1a1 boundaries above, so the *-skill
# side is suffixed-only and the *-core side never canonicalizes
# on either channel — see test_mixed_version_matrix.py for what
# that implies per cell. The fetched constraints file is saved
# into $TARGET for upload as a CI artifact, so a future red cell
# can be traced back to exactly what was pinned that day.
set -euo pipefail

TARGET="${1:?usage: build_venvs.sh <target-dir> [core-checkout]}"
CORE_CHECKOUT="${2:-$(cd "$(dirname "${BASH_SOURCE[0]}")/../.." && pwd)}"

STABLE_CONSTRAINTS_URL="${BACKCOMPAT_STABLE_CONSTRAINTS_URL:-https://raw.githubusercontent.com/OpenVoiceOS/OpenVoiceOS/main/constraints-stable.txt}"
TESTING_CONSTRAINTS_URL="${BACKCOMPAT_TESTING_CONSTRAINTS_URL:-https://raw.githubusercontent.com/OpenVoiceOS/OpenVoiceOS/main/constraints-testing.txt}"

PY="${BACKCOMPAT_PYTHON:-python3.11}"
mkdir -p "$TARGET"

have_uv() { command -v uv >/dev/null 2>&1; }

mkvenv() {
local name="$1"; shift
local dir="$TARGET/$name"
echo "==> building $name"
if have_uv; then
uv venv --python "$PY" "$dir" >/dev/null
VIRTUAL_ENV="$dir" uv pip install --quiet --prerelease=allow "$@"
else
"$PY" -m venv "$dir"
"$dir/bin/pip" install --quiet --upgrade pip
"$dir/bin/pip" install --quiet --pre "$@"
fi
echo " $("$dir/bin/python" -c 'import sys; print(sys.version.split()[0])')"
}

# Like mkvenv, but constrained by a distro constraints file fetched fresh at
# build time. $1=name $2=constraints-url $3..=packages to install
mkvenv_channel() {
local name="$1" url="$2"; shift 2
local dir="$TARGET/$name"
local cfile="$TARGET/${name#venv_}.constraints.txt"
echo "==> fetching constraints for $name from $url"
curl -fsSL "$url" -o "$cfile"
echo "==> building $name (constrained)"
if have_uv; then
uv venv --python "$PY" "$dir" >/dev/null
VIRTUAL_ENV="$dir" uv pip install --quiet --prerelease=allow -c "$cfile" "$@"
else
"$PY" -m venv "$dir"
"$dir/bin/pip" install --quiet --upgrade pip
"$dir/bin/pip" install --quiet --pre -c "$cfile" "$@"
fi
echo " $("$dir/bin/python" -c 'import sys; print(sys.version.split()[0])')"
}

mkvenv venv_skill_old "ovos-workshop==9.3.1a2" "setuptools<81"
mkvenv venv_skill_new "ovos-workshop @ git+https://github.com/OpenVoiceOS/ovos-workshop@dev" "setuptools<81"
mkvenv venv_core_old "ovos-core==2.5.5a2" "ovos-padatious==2.0.0a1" ovos-messagebus pytest pytest-timeout "setuptools<81"
mkvenv venv_core_new "$CORE_CHECKOUT" "ovos-padatious>=2.0.1a2" ovos-messagebus pytest pytest-timeout "setuptools<81"

mkvenv_channel venv_skill_stable "$STABLE_CONSTRAINTS_URL" ovos-workshop "setuptools<81"
mkvenv_channel venv_skill_testing "$TESTING_CONSTRAINTS_URL" ovos-workshop "setuptools<81"
mkvenv_channel venv_core_stable "$STABLE_CONSTRAINTS_URL" ovos-core ovos-padatious ovos-messagebus pytest pytest-timeout "setuptools<81"
mkvenv_channel venv_core_testing "$TESTING_CONSTRAINTS_URL" ovos-core ovos-padatious ovos-messagebus pytest pytest-timeout "setuptools<81"

echo
echo "resolved versions:"
for v in venv_skill_old venv_skill_new venv_core_old venv_core_new \
venv_skill_stable venv_skill_testing venv_core_stable venv_core_testing; do
echo " $v:"
"$TARGET/$v/bin/python" - <<'EOF' || true
from importlib.metadata import version, PackageNotFoundError
for p in ("ovos-workshop", "ovos-bus-client", "ovos-core", "ovos-padatious",
"ovos-spec-tools"):
try:
print(f" {p}=={version(p)}")
except PackageNotFoundError:
pass
EOF
done
Loading
Loading