docs: replace api build automation (#12214)

* remove-workflows-file-and-script

* tag-hidden-endpoints

* update-scripts-and-specs

* fix: nightly now properly gets 1.9.0 branch (#12215)

before it was attempting to pull release-notes as letters are alphanumerically after numbers when we sort -V then grab tail
now we only look at branch names that follow the pattern '^release-[0-9]+\.[0-9]+\.[0-9]+$'

* docs: add search icon (#12216)

add-back-svg

---------

Co-authored-by: Adam-Aghili <149833988+Adam-Aghili@users.noreply.github.com>
Co-authored-by: Debojit Kaushik <Kaushik.debojit@gmail.com>
This commit is contained in:
Mendon Kissling
2026-03-18 16:08:44 -04:00
committed by GitHub
parent b36444f5d9
commit e8bbae8eeb
10 changed files with 7272 additions and 8618 deletions

View File

@ -1,124 +0,0 @@
name: Update OpenAPI Spec
on:
schedule:
- cron: "0 20 * * 1" # Monday 4pm EST
workflow_dispatch: # Allow manual trigger
permissions:
contents: write
pull-requests: write
jobs:
check-openapi-updates:
runs-on: ubuntu-latest
steps:
- name: Checkout repository
uses: actions/checkout@v6
- name: Install jq
run: sudo apt-get install -y jq
- name: Check if there is already an open pull request
id: check_pull_request
run: |
if [[ -n $(gh pr list --state open --repo ${{ github.repository }} | grep "docs: OpenAPI spec") ]]; then
echo "There is already an open PR with updates to the OpenAPI spec. Merge or close that PR first. Skipping."
echo "pr_exists=true" >> $GITHUB_OUTPUT
else
echo "pr_exists=false" >> $GITHUB_OUTPUT
fi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Run Langflow container and get OpenAPI spec
if: steps.check_pull_request.outputs.pr_exists != 'true'
run: |
# Start the container in the background
docker run -d --name langflow -p 7860:7860 langflowai/langflow:latest
# Wait for the service to be ready (adjust timeout as needed)
echo "Waiting for Langflow to start..."
timeout=60
elapsed=0
while ! curl -s http://localhost:7860/health > /dev/null; do
sleep 2
elapsed=$((elapsed+2))
if [ "$elapsed" -ge "$timeout" ]; then
echo "Timed out waiting for Langflow to start"
exit 1
fi
echo "Still waiting... ($elapsed seconds)"
done
# Get the OpenAPI spec and save to file
echo "Fetching OpenAPI spec..."
curl -s http://localhost:7860/openapi.json > docs/openapi/new_openapi.json
# Verify file was created
ls -la docs/openapi/new_openapi.json
# stop the container when done
docker stop langflow
- name: Compare OpenAPI files
if: steps.check_pull_request.outputs.pr_exists != 'true'
id: compare
run: |
# Extract versions
NEW_VERSION=$(jq -r '.info.version' docs/openapi/new_openapi.json)
CURRENT_VERSION=$(jq -r '.info.version' docs/openapi/openapi.json)
echo "Current version: $CURRENT_VERSION"
echo "New version: $NEW_VERSION"
# Compare file content (normalize by sorting keys)
jq --sort-keys . docs/openapi/new_openapi.json > docs/openapi/sorted_new.json
jq --sort-keys . docs/openapi/openapi.json > docs/openapi/sorted_current.json
if ! cmp -s docs/openapi/sorted_new.json docs/openapi/sorted_current.json; then
echo "OpenAPI spec content has changed."
# Clean the new spec for ReDoc formatting
echo "Cleaning OpenAPI spec formatting..."
python3 docs/scripts/clean_openapi_formatting.py docs/openapi/new_openapi.json
# Compare versions (assuming semantic versioning)
if [ "$(printf '%s\n' "$CURRENT_VERSION" "$NEW_VERSION" | sort -V | tail -n1)" == "$NEW_VERSION" ] && [ "$CURRENT_VERSION" != "$NEW_VERSION" ]; then
echo "New version detected. Creating PR."
echo "NEEDS_UPDATE=true" >> $GITHUB_ENV
echo "NEW_VERSION=$NEW_VERSION" >> $GITHUB_ENV
echo "UPDATE_REASON=version upgraded from $CURRENT_VERSION to $NEW_VERSION" >> $GITHUB_ENV
else
echo "File content changed but version remains the same. Creating PR."
echo "NEEDS_UPDATE=true" >> $GITHUB_ENV
echo "NEW_VERSION=$CURRENT_VERSION" >> $GITHUB_ENV
echo "UPDATE_REASON=content updated without version change" >> $GITHUB_ENV
fi
cat docs/openapi/new_openapi.json | jq > docs/openapi/openapi.json
else
echo "No changes detected in OpenAPI spec content."
echo "NEEDS_UPDATE=false" >> $GITHUB_ENV
fi
# Clean up
rm docs/openapi/new_openapi.json docs/openapi/sorted_new.json docs/openapi/sorted_current.json
- name: Create Pull Request
if: env.NEEDS_UPDATE == 'true' && steps.check_pull_request.outputs.pr_exists != 'true'
uses: peter-evans/create-pull-request@v8
with:
token: ${{ secrets.GITHUB_TOKEN }}
commit-message: "docs: OpenAPI spec ${{ env.UPDATE_REASON }}"
title: "docs: OpenAPI spec ${{ env.UPDATE_REASON }}"
body: |
This PR updates the OpenAPI spec.
Update reason: ${{ env.UPDATE_REASON }}
Version: ${{ env.NEW_VERSION }}
branch: update-openapi-spec
branch-suffix: timestamp
delete-branch: true
reviewers: mendonk

View File

@ -1,6 +1,10 @@
#!/usr/bin/env python3
"""Pull OpenAPI spec files from the langflow-ai/sdk repository.
This script is only for syncing external SDK specs when needed.
It is not used by the main Langflow OpenAPI generation workflow,
which now relies on `docs/openapi/generate_openapi.py`.
Usage:
python3 fetch_openapi_spec.py # Download all files
python3 fetch_openapi_spec.py --file <filename> # Download specific file

114
docs/openapi/generate_openapi.py Executable file
View File

@ -0,0 +1,114 @@
#!/usr/bin/env python3
"""Generate the Langflow OpenAPI specification.
This script imports the Langflow FastAPI application and writes its OpenAPI
schema to a JSON file in this directory.
Usage (from repository root):
uv run python docs/openapi/generate_openapi.py
uv run python docs/openapi/generate_openapi.py --output openapi-1.5.0.json
"""
from __future__ import annotations
import argparse
import json
from pathlib import Path
from typing import Any
from langflow.main import create_app
def _clean_descriptions(spec: dict[str, Any]) -> None:
"""Convert newlines in operation descriptions to <br> for better ReDoc rendering."""
paths = spec.get("paths") or {}
for path_item in paths.values():
if not isinstance(path_item, dict):
continue
for operation in path_item.values():
if not isinstance(operation, dict):
continue
description = operation.get("description")
if isinstance(description, str) and description:
operation["description"] = description.replace("\n", "<br>")
def _collect_and_rewrite_defs(node: Any, collected: dict[str, Any]) -> None:
"""Hoist JSON Schema `$defs` into `components.schemas` and rewrite refs.
Some tooling (like Redoc's sampler) cannot handle JSON Pointer segments
that contain `$defs`. To keep the schema tool-friendly, we:
- Collect any `$defs` blocks we find anywhere in the tree.
- Remove those local `$defs` blocks.
- Rewrite `"$ref": "#/$defs/Name"` to `"#/components/schemas/Name"`.
"""
if isinstance(node, dict):
# Hoist local $defs
if "$defs" in node and isinstance(node["$defs"], dict):
for name, schema in node["$defs"].items():
# Only add if not already present; avoid clobbering explicit components
collected.setdefault(name, schema)
node.pop("$defs", None)
# Rewrite local refs
ref = node.get("$ref")
if isinstance(ref, str) and ref.startswith("#/$defs/"):
name = ref.split("/")[-1]
node["$ref"] = f"#/components/schemas/{name}"
# Recurse into values
for value in node.values():
_collect_and_rewrite_defs(value, collected)
elif isinstance(node, list):
for item in node:
_collect_and_rewrite_defs(item, collected)
def _normalize_defs(spec: dict[str, Any]) -> None:
"""Normalize `$defs` usage for better compatibility with tooling."""
collected: dict[str, Any] = {}
_collect_and_rewrite_defs(spec, collected)
if not collected:
return
components = spec.setdefault("components", {})
schemas = components.setdefault("schemas", {})
for name, schema in collected.items():
schemas.setdefault(name, schema)
def generate_openapi(output_path: Path) -> None:
"""Generate the OpenAPI spec and write it to ``output_path``."""
app = create_app()
spec = app.openapi()
_clean_descriptions(spec)
_normalize_defs(spec)
output_path.parent.mkdir(parents=True, exist_ok=True)
with output_path.open("w", encoding="utf-8") as f:
json.dump(spec, f, indent=2, sort_keys=True, ensure_ascii=False)
def parse_args() -> argparse.Namespace:
parser = argparse.ArgumentParser(description="Generate Langflow OpenAPI specification.")
parser.add_argument(
"--output",
"-o",
type=Path,
default=Path(__file__).parent / "openapi.json",
help="Output file path (default: docs/openapi/openapi.json)",
)
return parser.parse_args()
def main() -> None:
args = parse_args()
generate_openapi(args.output)
if __name__ == "__main__":
main()

File diff suppressed because it is too large Load Diff

View File

@ -1,73 +0,0 @@
#!/usr/bin/env python3
"""Clean OpenAPI specification formatting for better ReDoc display.
This script converts newlines in descriptions to HTML breaks for proper
rendering in ReDoc documentation.
"""
import json
import sys
from pathlib import Path
MIN_ARGS = 2
def clean_openapi_formatting(input_file: str, output_file: str | None = None) -> None:
"""Clean OpenAPI spec formatting by converting newlines to HTML breaks.
Args:
input_file: Path to input OpenAPI JSON file
output_file: Path to output file (defaults to overwriting input)
"""
if output_file is None:
output_file = input_file
try:
# Load the OpenAPI spec
input_path = Path(input_file)
with input_path.open(encoding="utf-8") as f:
spec = json.load(f)
# Fix description formatting by converting newlines to HTML breaks
if "paths" in spec:
for path_item in spec["paths"].values():
for operation in path_item.values():
if isinstance(operation, dict) and "description" in operation:
description = operation["description"]
if description:
# Convert newlines to HTML breaks for better ReDoc rendering
operation["description"] = description.replace("\n", "<br>")
# Save the cleaned spec
output_path = Path(output_file)
with output_path.open("w", encoding="utf-8") as f:
json.dump(spec, f, indent=2, ensure_ascii=False)
# Success message (using sys.stdout for consistency)
sys.stdout.write(f"OpenAPI spec cleaned successfully: {output_file}\n")
except FileNotFoundError:
sys.stderr.write(f"Error: File not found: {input_file}\n")
sys.exit(1)
except json.JSONDecodeError as e:
sys.stderr.write(f"Error: Invalid JSON in {input_file}: {e}\n")
sys.exit(1)
except OSError as e:
sys.stderr.write(f"Error: {e}\n")
sys.exit(1)
def main():
"""Main entry point."""
if len(sys.argv) < MIN_ARGS:
sys.stderr.write("Usage: python clean_openapi_formatting.py <input_file> [output_file]\n")
sys.exit(1)
input_file = sys.argv[1]
output_file = sys.argv[2] if len(sys.argv) > MIN_ARGS else None
clean_openapi_formatting(input_file, output_file)
if __name__ == "__main__":
main()

View File

@ -36,7 +36,7 @@ from langflow.agentic.services.provider_service import (
)
from langflow.api.utils.core import CurrentActiveUser, DbSession
router = APIRouter(prefix="/agentic", tags=["Agentic"])
router = APIRouter(prefix="/agentic", tags=["Agentic"], include_in_schema=False)
@dataclass(frozen=True)

View File

@ -33,7 +33,7 @@ from langflow.api.v1.schemas.deployments import (
FlowVersionIdsQuery,
)
router = APIRouter(prefix="/deployments", tags=["Deployments"])
router = APIRouter(prefix="/deployments", tags=["Deployments"], include_in_schema=False)
DeploymentProviderAccountIdQuery = Annotated[

View File

@ -33,7 +33,7 @@ from langflow.services.database.models.flow_version.model import (
)
from langflow.services.deps import get_settings_service
router = APIRouter(prefix="/flows/{flow_id}/versions", tags=["Flow Versions"])
router = APIRouter(prefix="/flows/{flow_id}/versions", tags=["Flow Versions"], include_in_schema=False)
def strip_version_data(data: dict | None) -> dict | None:

View File

@ -20,7 +20,7 @@ from langflow.api.v1.mcp_utils import (
handle_read_resource,
)
router = APIRouter(prefix="/mcp", tags=["mcp"])
router = APIRouter(prefix="/mcp", tags=["mcp"], include_in_schema=False)
server = Server("langflow-mcp-server")

View File

@ -313,6 +313,7 @@ async def list_project_tools(
"/{project_id}/sse",
response_class=HTMLResponse,
dependencies=[Depends(raise_error_if_astra_cloud_env)],
include_in_schema=False,
)
async def im_alive(project_id: str): # noqa: ARG001
return Response()
@ -322,6 +323,7 @@ async def im_alive(project_id: str): # noqa: ARG001
"/{project_id}/sse",
response_class=HTMLResponse,
dependencies=[Depends(raise_error_if_astra_cloud_env)],
include_in_schema=False,
)
async def handle_project_sse(
project_id: UUID,
@ -399,8 +401,16 @@ async def _handle_project_sse_messages(
current_request_variables_ctx.reset(req_vars_token)
@router.post("/{project_id}", dependencies=[Depends(raise_error_if_astra_cloud_env)])
@router.post("/{project_id}/", dependencies=[Depends(raise_error_if_astra_cloud_env)])
@router.post(
"/{project_id}",
dependencies=[Depends(raise_error_if_astra_cloud_env)],
include_in_schema=False,
)
@router.post(
"/{project_id}/",
dependencies=[Depends(raise_error_if_astra_cloud_env)],
include_in_schema=False,
)
async def handle_project_messages(
project_id: UUID,
request: Request,
@ -415,7 +425,7 @@ async def handle_project_messages(
########################################################
@router.head("/{project_id}/streamable")
@router.head("/{project_id}/streamable", include_in_schema=False)
async def streamable_health(project_id: UUID): # noqa: ARG001
return Response()
@ -454,6 +464,7 @@ async def _dispatch_project_streamable_http(
streamable_http_route_config = {
"methods": ["GET", "POST", "DELETE"],
"response_class": ResponseNoOp,
"include_in_schema": False,
}