Skip to content

Commit ab8508d

Browse files
committed
fix(ci): consolidate recent nightly CI fixes
Squash the recent Eric-authored main commits above 26dc6fd into one branch-head commit.
1 parent 26dc6fd commit ab8508d

8 files changed

Lines changed: 161 additions & 10 deletions

File tree

.github/workflows/db-migration-validation.yml

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -322,6 +322,15 @@ jobs:
322322
- "7860:7860"
323323
environment: # pragma: allowlist secret
324324
- LANGFLOW_DATABASE_URL=postgresql://langflow:langflow@postgres:5432/langflow # pragma: allowlist secret
325+
# The published Docker image ships secure-by-default: its Dockerfile bakes
326+
# LANGFLOW_AUTO_LOGIN=false and there is no built-in superuser password (#13822).
327+
# This migration test authenticates via GET /api/v1/auto_login, which requires
328+
# AUTO_LOGIN=true, so pin it here (overriding the image ENV) to keep that token
329+
# flow working and ensure boot never lands in the "Username and password must be
330+
# set" branch. The explicit superuser credentials are a fallback for that branch.
331+
- LANGFLOW_AUTO_LOGIN=true
332+
- LANGFLOW_SUPERUSER=langflow # pragma: allowlist secret
333+
- LANGFLOW_SUPERUSER_PASSWORD=langflow # pragma: allowlist secret
325334
depends_on:
326335
postgres:
327336
condition: service_healthy

.secrets.baseline

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -2837,15 +2837,15 @@
28372837
"filename": "src/backend/tests/conftest.py",
28382838
"hashed_secret": "8bb6118f8fd6935ad0876a3be34a717d32708ffd",
28392839
"is_verified": false,
2840-
"line_number": 515,
2840+
"line_number": 531,
28412841
"is_secret": false
28422842
},
28432843
{
28442844
"type": "Secret Keyword",
28452845
"filename": "src/backend/tests/conftest.py",
28462846
"hashed_secret": "61fbb5a12cd7b1f1fe1624120089efc0cd299e43",
28472847
"is_verified": false,
2848-
"line_number": 725,
2848+
"line_number": 741,
28492849
"is_secret": false
28502850
}
28512851
],
@@ -9287,5 +9287,5 @@
92879287
}
92889288
]
92899289
},
9290-
"generated_at": "2026-06-24T03:30:55Z"
9290+
"generated_at": "2026-07-01T16:38:59Z"
92919291
}
Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
"""Regression tests for ``update_lf_base_dependency``.
2+
3+
The nightly bump pins base's ``lfx`` dependency to the exact ``==X.Y.0.devN`` so the dev
4+
release resolves down the tree. Base also carries ``lfx[extra]`` references (the relocated
5+
cassio/toolguard features) that are pulled by ``langflow-base[complete]``. If those keep a
6+
``~=X.Y.0`` floor while the bare ``lfx`` dep is pinned to the dev version, the floor
7+
(``>=X.Y.0``) excludes ``X.Y.0.devN`` (PEP 440 dev releases sort *below* the final) and the
8+
resolve becomes unsatisfiable. These tests lock in that all ``lfx`` forms -- bare and with
9+
extras -- get the same exact dev pin.
10+
"""
11+
12+
import re
13+
import sys
14+
from pathlib import Path
15+
16+
import pytest
17+
18+
sys.path.insert(0, str(Path(__file__).resolve().parent))
19+
20+
import update_lf_base_dependency as mod
21+
22+
23+
@pytest.fixture
24+
def pyproject(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
25+
"""A throwaway pyproject whose lfx refs mirror src/backend/base/pyproject.toml."""
26+
content = (
27+
"[project]\n"
28+
"dependencies = [\n"
29+
' "lfx~=1.11.0",\n'
30+
"]\n"
31+
"\n"
32+
"[project.optional-dependencies]\n"
33+
'cassandra = ["lfx[cassandra]~=1.11.0"]\n'
34+
"toolguard = [\"lfx[toolguard]~=1.11.0; python_version < '3.14'\"]\n"
35+
'beautifulsoup = ["lfx~=1.11.0"]\n'
36+
)
37+
path = tmp_path / "pyproject.toml"
38+
path.write_text(content, encoding="utf-8")
39+
# The script resolves paths relative to BASE_DIR; point it at tmp_path.
40+
monkeypatch.setattr(mod, "BASE_DIR", tmp_path)
41+
return path
42+
43+
44+
def test_pins_bare_and_extras_lfx_to_exact_dev(pyproject: Path) -> None:
45+
mod.update_lfx_dep_in_base(pyproject.name, "1.11.0.dev26")
46+
result = pyproject.read_text(encoding="utf-8")
47+
48+
# Every lfx reference -- bare and with extras -- is pinned to the exact dev version.
49+
assert '"lfx==1.11.0.dev26"' in result
50+
assert '"lfx[cassandra]==1.11.0.dev26"' in result
51+
assert "\"lfx[toolguard]==1.11.0.dev26; python_version < '3.14'\"" in result
52+
53+
# No `~=` floor survives -- a surviving floor is exactly what makes the nightly
54+
# resolve unsatisfiable.
55+
assert "~=" not in result
56+
# Extras are preserved, never dropped.
57+
assert "[cassandra]" in result
58+
assert "[toolguard]" in result
59+
60+
61+
def test_idempotent_on_already_pinned(pyproject: Path) -> None:
62+
mod.update_lfx_dep_in_base(pyproject.name, "1.11.0.dev26")
63+
once = pyproject.read_text(encoding="utf-8")
64+
mod.update_lfx_dep_in_base(pyproject.name, "1.11.0.dev26")
65+
twice = pyproject.read_text(encoding="utf-8")
66+
assert once == twice
67+
68+
69+
def test_raises_when_no_lfx_dependency(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
70+
path = tmp_path / "pyproject.toml"
71+
path.write_text('[project]\ndependencies = ["langflow-base~=1.11.0"]\n', encoding="utf-8")
72+
monkeypatch.setattr(mod, "BASE_DIR", tmp_path)
73+
with pytest.raises(ValueError, match="LFX dependency not found"):
74+
mod.update_lfx_dep_in_base(path.name, "1.11.0.dev26")
75+
76+
77+
def test_pattern_skips_unrelated_packages(pyproject: Path) -> None:
78+
"""Sibling packages whose names merely start with ``lfx`` must not be repinned."""
79+
extra = ' "lfx-bundles~=1.11.0",\n "lfxthing~=1.11.0",\n'
80+
pyproject.write_text(pyproject.read_text(encoding="utf-8") + extra, encoding="utf-8")
81+
mod.update_lfx_dep_in_base(pyproject.name, "1.11.0.dev26")
82+
result = pyproject.read_text(encoding="utf-8")
83+
# The dedicated `lfx` distribution and its extras are repinned...
84+
assert '"lfx==1.11.0.dev26"' in result
85+
# ...but `lfx-bundles` / `lfxthing` keep their own floors untouched.
86+
assert re.search(r'"lfx-bundles~=1\.11\.0"', result)
87+
assert re.search(r'"lfxthing~=1\.11\.0"', result)

scripts/ci/update_lf_base_dependency.py

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -45,19 +45,24 @@ def update_lfx_dep_in_base(pyproject_path: str, lfx_version: str) -> None:
4545
content = filepath.read_text(encoding="utf-8")
4646

4747
# Updated pattern to handle PEP 440 version suffixes, both ~= and == version specifiers,
48-
# and both lfx and lfx-nightly names
49-
pattern = re.compile(r'("lfx(?:-nightly)?(?:~=|==)[\d.]+(?:\.(?:post|dev|a|b|rc)\d+)*")')
48+
# both lfx and lfx-nightly names, extras (e.g. lfx[cassandra], lfx[toolguard]), and
49+
# trailing markers (e.g. `; python_version < '3.14'`).
50+
# The extras group (1) MUST be preserved: base's `[complete]` extra pulls these
51+
# `lfx[extra]` references, and if they keep a `~=X.Y.0` floor while base's bare `lfx`
52+
# dep is pinned to `==X.Y.0.devN`, the floor (>=X.Y.0) excludes the dev release and
53+
# the nightly resolve becomes unsatisfiable.
54+
version_pattern = r"[0-9]+(?:\.[0-9]+)*(?:\.(?:post|dev|a|b|rc)\d+)*"
55+
pattern = re.compile(rf'"lfx(?:-nightly)?((?:\[[^\]]+\])?)(?:~=|==){version_pattern}([^"]*)"')
5056
# Pin base's lfx dep to the exact canonical dev version (single `lfx` distribution, no
5157
# `lfx-nightly`), so there is no `lfx` vs `lfx-nightly` install collision with the bundles.
52-
replacement = f'"lfx=={lfx_version}"'
5358

5459
# Check if the pattern is found
5560
if not pattern.search(content):
5661
msg = f'LFX dependency not found in "{filepath}"'
5762
raise ValueError(msg)
5863

59-
# Replace the matched pattern with the new one
60-
content = pattern.sub(replacement, content)
64+
# Replace each match, preserving its own extras and environment marker.
65+
content = pattern.sub(lambda m: f'"lfx{m.group(1)}=={lfx_version}{m.group(2)}"', content)
6166
filepath.write_text(content, encoding="utf-8")
6267

6368

src/backend/base/langflow/main.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -452,7 +452,10 @@ async def delayed_init_mcp_servers():
452452

453453
# Start the delayed initialization as a background task
454454
# Allows the server to start first to avoid race conditions with MCP Server startup
455-
mcp_init_task = asyncio.create_task(delayed_init_mcp_servers())
455+
if get_settings_service().settings.skip_mcp_auto_init:
456+
await logger.adebug("Skipping MCP server auto-initialization (skip_mcp_auto_init=True)")
457+
else:
458+
mcp_init_task = asyncio.create_task(delayed_init_mcp_servers())
456459

457460
async def refresh_models_dev_periodically() -> None:
458461
"""Hydrate the models.dev catalog at startup and refresh daily.

src/backend/tests/conftest.py

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,13 @@
2626
from langflow.services.database.models.transactions.model import TransactionTable
2727
from langflow.services.database.models.user.model import User, UserCreate, UserRead
2828
from langflow.services.database.models.vertex_builds.crud import delete_vertex_builds_by_flow_id_unchecked
29-
from langflow.services.deps import get_auth_service, get_db_service, session_scope
29+
from langflow.services.deps import (
30+
get_auth_service,
31+
get_db_service,
32+
get_settings_service,
33+
is_settings_service_initialized,
34+
session_scope,
35+
)
3036
from lfx.components.input_output import ChatInput
3137
from lfx.graph import Graph
3238
from lfx.log.logger import logger
@@ -65,6 +71,36 @@ def disable_models_dev_refresh():
6571
os.environ.pop("LANGFLOW_MODELS_DEV_REFRESH", None)
6672

6773

74+
@pytest.fixture(scope="session", autouse=True)
75+
def disable_mcp_auto_init():
76+
"""Keep the MCP server auto-initialization out of tests.
77+
78+
Every app boot otherwise schedules a lifespan task (``delayed_init_mcp_servers``)
79+
that, ~10s in, reconciles each project's MCP server config. For apikey/none projects
80+
that reconciliation spawns ``uvx mcp-proxy`` and makes an outbound connect with no
81+
bounded timeout, so on a slow/CI runner it hangs until the OS connect timeout (~127s),
82+
inflating every app-fixture test by ~130s and pushing the heaviest test split past the
83+
CI step timeout. Skipping it keeps the boot local and deterministic.
84+
"""
85+
previous_env_value = os.environ.get("LANGFLOW_SKIP_MCP_AUTO_INIT")
86+
previous_setting = (
87+
get_settings_service().settings.skip_mcp_auto_init
88+
if is_settings_service_initialized()
89+
else (previous_env_value or "").lower() in {"1", "true", "yes", "on"}
90+
)
91+
92+
os.environ["LANGFLOW_SKIP_MCP_AUTO_INIT"] = "true"
93+
if is_settings_service_initialized():
94+
get_settings_service().set("skip_mcp_auto_init", value=True)
95+
yield
96+
if previous_env_value is None:
97+
os.environ.pop("LANGFLOW_SKIP_MCP_AUTO_INIT", None)
98+
else:
99+
os.environ["LANGFLOW_SKIP_MCP_AUTO_INIT"] = previous_env_value
100+
if is_settings_service_initialized():
101+
get_settings_service().set("skip_mcp_auto_init", previous_setting)
102+
103+
68104
# TODO: Revert this to True once bb.functions[func].can_block_in("http/client.py", "_safe_read") is fixed
69105
@pytest.fixture(autouse=False)
70106
def blockbuster(request):

src/lfx/src/lfx/services/settings/groups/mcp.py

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,15 @@ def validate_mcp_tool_execution_timeout(cls, v: float) -> float:
5858
add_projects_to_mcp_servers: bool = True
5959
"""If set to True, newly created projects will be added to the user's MCP servers config automatically."""
6060

61+
skip_mcp_auto_init: bool = False
62+
"""If set to True, Langflow skips the background MCP server auto-initialization on startup.
63+
64+
The startup task reconciles every project's MCP server config, which for apikey/none
65+
projects can spawn ``uvx mcp-proxy`` and open an outbound connection. On an offline or
66+
firewalled host (or CI) that connect has no bounded timeout and blocks until the OS
67+
connect timeout (~127s). Enable this in tests or air-gapped deployments to keep startup
68+
local and deterministic."""
69+
6170
# MCP Composer
6271
mcp_composer_enabled: bool = True
6372
"""If set to False, Langflow will not start the MCP Composer service."""

src/lfx/tests/unit/services/settings/test_settings_composition.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -83,6 +83,7 @@
8383
"mcp_server_enabled",
8484
"mcp_server_enable_progress_notifications",
8585
"add_projects_to_mcp_servers",
86+
"skip_mcp_auto_init",
8687
"mcp_composer_enabled",
8788
"mcp_composer_version",
8889
# TelemetrySettings
@@ -357,6 +358,7 @@ def test_yaml_round_trip():
357358
("LANGFLOW_PROMETHEUS_ENABLED", "true", "prometheus_enabled", True),
358359
("LANGFLOW_PROMETHEUS_PORT", "9999", "prometheus_port", 9999),
359360
("LANGFLOW_MCP_SERVER_ENABLED", "false", "mcp_server_enabled", False),
361+
("LANGFLOW_SKIP_MCP_AUTO_INIT", "true", "skip_mcp_auto_init", True),
360362
("LANGFLOW_DO_NOT_TRACK", "true", "do_not_track", True),
361363
("LANGFLOW_DEV", "true", "dev", True),
362364
("LANGFLOW_BACKEND_ONLY", "true", "backend_only", True),

0 commit comments

Comments
 (0)