-
Notifications
You must be signed in to change notification settings - Fork 9.7k
167 lines (153 loc) · 7.2 KB
/
Copy pathcross-bundle-test.yml
File metadata and controls
167 lines (153 loc) · 7.2 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
name: Cross-Bundle Test
# Tests every extracted bundle (the lfx-bundles metapackage + each graduated
# lfx-<provider>) against the lfx contract surface it depends on. lfx is the
# PRIMARY axis because the BUNDLE_API contract lives in lfx: a bundle that
# imports cleanly, validates, and passes its tests against a given lfx is
# compatible with that lfx minor.
#
# Cost-control shape (from the bundle-separation epic):
# - contract-smoke (install + import + discover + validate): every bundle,
# on the oldest and latest supported Python.
# - bundle tests (pytest the bundle's own tests/): same matrix.
# - scheduled run: weekly, the exhaustive grid.
#
# The lfx-minor axis is currently a single entry -- the IN-REPO lfx -- because
# the 1.10 line is not yet published to PyPI. When lfx minors publish, add an
# explicit lfx-version matrix dimension here (oldest + latest get the full
# tests; every supported minor gets contract-smoke), and wire langflow RCs as
# the secondary axis via workflow_call from the release pipeline.
on:
workflow_call:
workflow_dispatch:
pull_request:
paths:
- "src/bundles/**"
- "src/lfx/**"
- ".github/workflows/cross-bundle-test.yml"
schedule:
- cron: "0 6 * * 1" # Monday 06:00 UTC -- exhaustive grid
# Stacked bundle PRs would otherwise queue N-bundles x N-pythons jobs per push.
concurrency:
group: cross-bundle-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: ${{ github.event_name == 'pull_request' }}
jobs:
discover:
name: Discover bundles
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
bundles: ${{ steps.find.outputs.bundles }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- id: find
name: Enumerate src/bundles/*/pyproject.toml
run: |
shopt -s nullglob
dirs=()
for p in src/bundles/*/pyproject.toml; do
dirs+=("$(dirname "$p")")
done
if [ ${#dirs[@]} -eq 0 ]; then
echo "No bundles found under src/bundles/*/" >&2
echo "bundles=[]" >> "$GITHUB_OUTPUT"
exit 0
fi
json=$(printf '%s\n' "${dirs[@]}" | sort | jq -R . | jq -cs .)
echo "bundles=$json" >> "$GITHUB_OUTPUT"
echo "Discovered bundles: $json"
bundle:
name: ${{ matrix.bundle }} (py${{ matrix.python-version }})
needs: discover
if: needs.discover.outputs.bundles != '[]'
runs-on: ubuntu-latest
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
bundle: ${{ fromJson(needs.discover.outputs.bundles) }}
# PR runs: oldest + latest supported Python (cost-control smoke).
# Scheduled runs: every supported Python (the exhaustive grid).
# The lfx-minor axis collapses to the in-repo lfx until the 1.10 line
# publishes (see header).
python-version: ${{ github.event_name == 'schedule' && fromJson('["3.10", "3.11", "3.12", "3.13", "3.14"]') || fromJson('["3.10", "3.13"]') }}
steps:
- uses: actions/checkout@v6
with:
persist-credentials: false
- uses: astral-sh/setup-uv@v6
with:
enable-cache: true
cache-dependency-glob: "uv.lock"
python-version: ${{ matrix.python-version }}
- name: Install in-repo lfx + the bundle into a clean venv
run: |
uv venv
# The in-repo lfx satisfies the bundle's `lfx>=X,<Y` pin and is the
# contract surface under test; pytest is for the bundle's own tests.
# tomli backports stdlib tomllib for the py3.10 matrix leg.
uv pip install ./src/lfx "./${{ matrix.bundle }}" pytest pytest-asyncio tomli
- name: Contract smoke -- import + discovery
run: |
.venv/bin/python - "${{ matrix.bundle }}" <<'PY'
import importlib
import sys
from pathlib import Path
try:
import tomllib # stdlib from 3.11
except ModuleNotFoundError:
import tomli as tomllib # py3.10 backport, installed above
bundle_dir = Path(sys.argv[1])
meta = tomllib.loads((bundle_dir / "pyproject.toml").read_text())
name = meta["project"]["name"]
eps = meta["project"].get("entry-points", {})
# Manifest bundles declare langflow.extensions; the manifest-less
# metapackage declares lfx.bundles. Either way the declared package
# must import.
modules = list(eps.get("langflow.extensions", {}).values()) or list(eps.get("lfx.bundles", {}).values())
assert modules, f"{name}: no langflow.extensions or lfx.bundles entry-point declared"
for value in modules:
importlib.import_module(value.split(":", 1)[0])
print(f" imported {value}")
# The manifest-less metapackage must be discoverable by the loader.
# This venv installs the bundle WITHOUT its per-provider extras, so
# providers whose modules import their SDK at top level degrade with
# `module-import-failed` -- that is the expected graceful-degradation
# contract, not a failure. Structural errors (bundle-empty,
# path-escape, invalid names, ...) still fail the smoke. A provider
# whose every module failed import also reports no components, which
# is fine here; the bundle's own tests cover behavior with SDKs.
if "lfx.bundles" in eps:
from lfx.extension import load_lfx_bundles_extensions
EXPECTED_WITHOUT_EXTRAS = {"module-import-failed"}
results = load_lfx_bundles_extensions()
bad = [e.code for r in results for e in r.errors if e.code not in EXPECTED_WITHOUT_EXTRAS]
assert not bad, f"{name}: lfx.bundles structural discovery errors: {bad}"
providers = sum(1 for r in results if r.bundle)
degraded = sum(1 for r in results for e in r.errors if e.code in EXPECTED_WITHOUT_EXTRAS)
print(f" lfx.bundles discovery OK ({providers} providers; {degraded} module(s) degraded sans extras)")
print(f"{name}: contract smoke OK")
PY
- name: Validate manifest (manifest-shipping bundles only)
run: |
# Manifest bundles ship extension.json; the manifest-less metapackage
# has none and is exempt from `lfx extension validate`.
manifest=$(ls "${{ matrix.bundle }}"/src/*/extension.json 2>/dev/null | head -1 || true)
if [ -n "$manifest" ]; then
.venv/bin/lfx extension validate "$(dirname "$manifest")"
else
echo "manifest-less bundle; skipping extension validate"
fi
- name: Run the bundle's own tests
run: |
if [ -d "${{ matrix.bundle }}/tests" ]; then
# Provider regression tests for the manifest-less metapackage live
# with the provider code and require only that provider's SDKs.
if [ "${{ matrix.bundle }}" = "src/bundles/lfx-bundles" ]; then
uv pip install "./${{ matrix.bundle }}[google]"
fi
.venv/bin/python -m pytest "${{ matrix.bundle }}/tests" -q
else
echo "no tests/ directory in ${{ matrix.bundle }}; skipping"
fi