forked from langflow-ai/langflow
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathcheck_bundle_api_changelog.py
More file actions
executable file
·205 lines (173 loc) · 7.32 KB
/
Copy pathcheck_bundle_api_changelog.py
File metadata and controls
executable file
·205 lines (173 loc) · 7.32 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
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
#!/usr/bin/env python3
"""CI guard: in-scope BUNDLE_API surface changes require a changelog entry.
The Bundle API contract is enumerated in ``BUNDLE_API.md`` at the repo root.
Files that contain in-scope public surface (the manifest schema, the loader's
public entry points, the typed error envelope, etc.) must not change without
a corresponding ``## Changelog`` entry being added to ``BUNDLE_API.md`` in
the same PR.
This script compares the working tree against a baseline branch and:
1. Reads the changed files in this branch.
2. Filters them to the in-scope set (see ``IN_SCOPE_PATHS`` below).
3. If any in-scope file changed, asserts ``BUNDLE_API.md`` also changed
AND that the diff includes at least one new line under a
``## Changelog`` heading.
Usage::
python scripts/migrate/check_bundle_api_changelog.py
python scripts/migrate/check_bundle_api_changelog.py --base origin/main
Exit codes:
0 -- no in-scope changes, or in-scope changes paired with a changelog entry
1 -- in-scope changes without a changelog entry (details to stderr)
2 -- usage / git error
"""
from __future__ import annotations
import argparse
import subprocess
import sys
from pathlib import Path
REPO_ROOT = Path(__file__).resolve().parents[2]
BUNDLE_API_RELPATH = "BUNDLE_API.md"
# Files / directories whose changes affect the BUNDLE_API contract.
# Glob-style relative to repo root; checked via str.startswith on relative path
# OR exact match. Keep this list deliberately tight: it only covers the
# surface enumerated in BUNDLE_API.md.
IN_SCOPE_PATHS: tuple[str, ...] = (
# Manifest schema + JSON-Schema export
"src/lfx/src/lfx/extension/manifest.py",
"src/lfx/src/lfx/extension/schema.py",
# Typed error envelope + ERROR_CODES set
"src/lfx/src/lfx/extension/errors.py",
# Loader subpackage public surface
"src/lfx/src/lfx/extension/loader/__init__.py",
"src/lfx/src/lfx/extension/loader/_orchestrator.py",
"src/lfx/src/lfx/extension/loader/_plugins.py",
"src/lfx/src/lfx/extension/loader/_types.py",
# Discovery + registry
"src/lfx/src/lfx/extension/discovery.py",
"src/lfx/src/lfx/extension/registry.py",
# Reload pipeline + bundle registry
"src/lfx/src/lfx/extension/reload.py",
"src/lfx/src/lfx/extension/bundle_registry.py",
# HTTP surface (reload endpoint)
"src/backend/base/langflow/api/v1/extensions.py",
# Migration table schema
"src/lfx/src/lfx/extension/migration/schema.py",
# The package facade (re-exports define the surface)
"src/lfx/src/lfx/extension/__init__.py",
"src/lfx/src/lfx/extension/validate.py",
)
def _git(*args: str) -> str:
"""Run a git command and return stdout (or '' if it failed cleanly)."""
try:
result = subprocess.run( # noqa: S603 - args is a literal list, no shell
["git", *args], # noqa: S607
cwd=REPO_ROOT,
check=True,
capture_output=True,
text=True,
)
except subprocess.CalledProcessError as exc:
# Surface stderr to the caller; treat absent base ref as "no diff".
if exc.stderr and "unknown revision" in exc.stderr.lower():
return ""
msg = f"git {' '.join(args)} failed: {exc.stderr.strip() or exc.stdout.strip()}"
raise RuntimeError(msg) from exc
return result.stdout
def changed_files(base: str) -> list[str]:
"""Return paths (relative to repo root) changed between ``base`` and HEAD."""
out = _git("diff", "--name-only", base)
return [line.strip() for line in out.splitlines() if line.strip()]
def is_in_scope(path: str) -> bool:
return path in IN_SCOPE_PATHS or any(path.startswith(p + "/") for p in IN_SCOPE_PATHS)
def _changelog_lines_in_current_file() -> set[str]:
"""Return the set of stripped lines that live under ``## Changelog`` in the current file.
Scans the working-tree ``BUNDLE_API.md`` (the post-edit version) so we can
cross-reference the diff's ``+`` lines against actual section membership.
Section ends at the next heading at the same level (``## ``) or shallower
(``# ``), or at end-of-file.
"""
path = REPO_ROOT / BUNDLE_API_RELPATH
if not path.exists():
return set()
in_changelog = False
found: set[str] = set()
for raw in path.read_text(encoding="utf-8").splitlines():
stripped = raw.strip()
if stripped.startswith("## "):
in_changelog = stripped[3:].strip().lower().startswith("changelog")
continue
if stripped.startswith("# "):
# Top-level heading; section ends.
in_changelog = False
continue
if in_changelog and stripped:
found.add(stripped)
return found
def changelog_diff_added_lines(base: str) -> list[str]:
"""Return added lines (``+`` lines) that fall under ``## Changelog`` in the new file.
We can't rely on the diff hunk's local context to identify section
membership (default 3 lines of context often hides the section heading).
Instead we collect every added line from the diff and cross-reference
against lines that actually live under ``## Changelog`` in the post-edit
file.
"""
diff = _git("diff", base, "--", BUNDLE_API_RELPATH)
if not diff:
return []
diff_added: list[str] = []
for line in diff.splitlines():
if line.startswith("+") and not line.startswith("+++"):
stripped = line[1:].strip()
if stripped:
diff_added.append(stripped)
if not diff_added:
return []
changelog_lines = _changelog_lines_in_current_file()
return [a for a in diff_added if a in changelog_lines]
def main(argv: list[str] | None = None) -> int:
parser = argparse.ArgumentParser(description=__doc__.split("\n", 1)[0])
parser.add_argument(
"--base",
default="origin/main",
help="Git ref to diff against (default: origin/main).",
)
args = parser.parse_args(argv)
try:
changed = changed_files(args.base)
except RuntimeError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
in_scope_changes = [p for p in changed if is_in_scope(p)]
if not in_scope_changes:
print("ok: no in-scope BUNDLE_API surface files changed.")
return 0
bundle_api_changed = BUNDLE_API_RELPATH in changed
if not bundle_api_changed:
print(
f"error: in-scope BUNDLE_API surface changed but {BUNDLE_API_RELPATH} "
f"was not updated. Add a ## Changelog entry describing the change.",
file=sys.stderr,
)
for p in in_scope_changes:
print(f" - {p}", file=sys.stderr)
return 1
try:
added = changelog_diff_added_lines(args.base)
except RuntimeError as exc:
print(f"error: {exc}", file=sys.stderr)
return 2
if not added:
print(
f"error: in-scope BUNDLE_API surface changed and {BUNDLE_API_RELPATH} "
f"was modified, but no new line was added under a ## Changelog heading.",
file=sys.stderr,
)
for p in in_scope_changes:
print(f" - {p}", file=sys.stderr)
return 1
print(
f"ok: {len(in_scope_changes)} in-scope file(s) changed; "
f"{BUNDLE_API_RELPATH} has {len(added)} new changelog line(s)."
)
return 0
if __name__ == "__main__":
raise SystemExit(main())