Skip to content

Commit 7db38d1

Browse files
committed
fix: unblock nightly bundle install resolution
1 parent 1a60220 commit 7db38d1

3 files changed

Lines changed: 220 additions & 16 deletions

File tree

.github/workflows/release_bundles.yml

Lines changed: 109 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ name: Release Bundles
33
# Manually-triggered workflow for publishing stable `lfx-<name>` bundle wheels
44
# from src/bundles/* to PyPI. Bundles change infrequently, so we release them
55
# on demand rather than on a schedule. Re-running the workflow without a
6-
# version bump is a no-op (PyPI rejects the duplicate upload and we tolerate
7-
# the conflict).
6+
# version bump is a no-op; the publish job preflights PyPI and skips bundle
7+
# versions that are already present.
88

99
on:
1010
workflow_dispatch:
@@ -182,6 +182,10 @@ jobs:
182182
needs.test-install.result == 'success'
183183
runs-on: ubuntu-latest
184184
steps:
185+
- name: Checkout code
186+
uses: actions/checkout@v6
187+
with:
188+
ref: ${{ inputs.release_tag || github.ref }}
185189
- name: Download bundles artifact
186190
uses: actions/download-artifact@v7
187191
with:
@@ -196,26 +200,122 @@ jobs:
196200
env:
197201
UV_PUBLISH_TOKEN: ${{ secrets.PYPI_API_TOKEN }}
198202
run: |
203+
set -euo pipefail
199204
shopt -s nullglob
200205
wheels=(bundles-dist/*.whl)
201206
if [ ${#wheels[@]} -eq 0 ]; then
202207
echo "No bundle wheels to publish."
203208
exit 0
204209
fi
210+
211+
publish_plan="$(mktemp)"
212+
uv run --with packaging --no-project python - "${wheels[@]}" > "$publish_plan" <<'PY'
213+
import email
214+
import json
215+
import sys
216+
import tomllib
217+
import urllib.error
218+
import urllib.request
219+
import zipfile
220+
from pathlib import Path
221+
222+
from packaging.requirements import Requirement
223+
from packaging.utils import canonicalize_name
224+
from packaging.version import InvalidVersion, Version
225+
226+
root_order = {}
227+
root_dependencies = tomllib.loads(Path("pyproject.toml").read_text())["project"].get("dependencies", [])
228+
for index, dependency in enumerate(root_dependencies):
229+
try:
230+
requirement = Requirement(dependency)
231+
except Exception:
232+
continue
233+
root_order.setdefault(canonicalize_name(requirement.name), index)
234+
235+
def read_wheel_metadata(path: Path) -> tuple[str, Version]:
236+
with zipfile.ZipFile(path) as wheel:
237+
metadata_path = next(name for name in wheel.namelist() if name.endswith(".dist-info/METADATA"))
238+
metadata = email.message_from_bytes(wheel.read(metadata_path))
239+
return canonicalize_name(metadata["Name"]), Version(metadata["Version"])
240+
241+
plan = []
242+
for wheel_arg in sys.argv[1:]:
243+
wheel_path = Path(wheel_arg)
244+
name, version = read_wheel_metadata(wheel_path)
245+
url = f"https://pypi.org/pypi/{name}/json"
246+
try:
247+
with urllib.request.urlopen(url, timeout=30) as response:
248+
project = json.load(response)
249+
except urllib.error.HTTPError as exc:
250+
if exc.code != 404:
251+
raise
252+
print(f"Will publish new PyPI project {name} {version}: {wheel_path}", file=sys.stderr)
253+
plan.append((0, root_order.get(name, 9999), name, str(wheel_path)))
254+
continue
255+
256+
published_versions = set()
257+
for version_text in project.get("releases", {}):
258+
try:
259+
published_versions.add(Version(version_text))
260+
except InvalidVersion:
261+
continue
262+
263+
if version in published_versions:
264+
print(f"Skipping {name} {version}: already published.", file=sys.stderr)
265+
continue
266+
267+
print(f"Will publish new version {name} {version}: {wheel_path}", file=sys.stderr)
268+
plan.append((1, root_order.get(name, 9999), name, str(wheel_path)))
269+
270+
for _, _, _, wheel_path in sorted(plan):
271+
print(wheel_path)
272+
PY
273+
274+
if [ ! -s "$publish_plan" ]; then
275+
echo "No bundle wheels need publishing."
276+
exit 0
277+
fi
278+
279+
mapfile -t wheels < "$publish_plan"
205280
failed=0
281+
pypi_publish_delay_seconds=60
282+
pypi_publish_max_attempts=5
283+
wheel_count=${#wheels[@]}
284+
wheel_number=0
206285
for wheel in "${wheels[@]}"; do
286+
wheel_number=$((wheel_number + 1))
207287
echo "Publishing $wheel"
208-
# Capture stderr so we can tolerate "already exists" without failing
209-
# the whole run — re-running this workflow without bumping a bundle
210-
# version should be a no-op for that bundle.
211-
if ! err=$(uv publish "$wheel" 2>&1); then
288+
attempt=1
289+
published=0
290+
while true; do
291+
# Capture stderr so we can tolerate "already exists" without failing
292+
# the whole run — re-running this workflow without bumping a bundle
293+
# version should be a no-op for that bundle.
294+
if err=$(uv publish "$wheel" 2>&1); then
295+
echo "$err"
296+
published=1
297+
break
298+
fi
212299
echo "$err"
213300
if echo "$err" | grep -qiE 'already exists|file already exists|HTTP 400|duplicate'; then
214301
echo "Skipping $wheel: already published."
215-
else
216-
echo "Publish failed for $wheel"
217-
failed=1
302+
break
303+
fi
304+
if echo "$err" | grep -qiE 'HTTP 429|429 Too Many Requests|Too many new projects created'; then
305+
if [ "$attempt" -lt "$pypi_publish_max_attempts" ]; then
306+
echo "PyPI rate limited $wheel; sleeping ${pypi_publish_delay_seconds}s before retry $((attempt + 1))/${pypi_publish_max_attempts}."
307+
sleep "$pypi_publish_delay_seconds"
308+
attempt=$((attempt + 1))
309+
continue
310+
fi
218311
fi
312+
echo "Publish failed for $wheel"
313+
failed=1
314+
break
315+
done
316+
if [ "$published" = "1" ] && [ "$wheel_number" -lt "$wheel_count" ]; then
317+
echo "Sleeping ${pypi_publish_delay_seconds}s before the next PyPI upload."
318+
sleep "$pypi_publish_delay_seconds"
219319
fi
220320
done
221321
if [ "$failed" = "1" ]; then

.github/workflows/release_nightly.yml

Lines changed: 109 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -350,9 +350,32 @@ jobs:
350350
mkdir -p main-dist
351351
cp "${MAIN_WHEELS[0]}" main-dist/
352352
353-
# Bundles are NOT rebuilt/republished for nightly. The stable `lfx-*` bundles on
354-
# PyPI work as-is against the canonical `lfx` pre-release (see src/bundles/NIGHTLY.md). The
355-
# cross-platform test self-builds the bundles from src/bundles for install coverage.
353+
- name: Build bundle wheels for cross-platform test
354+
id: build-bundles
355+
run: |
356+
shopt -s nullglob
357+
bundles=(src/bundles/*/pyproject.toml)
358+
if [ ${#bundles[@]} -eq 0 ]; then
359+
echo "No bundles found under src/bundles/*/"
360+
echo "bundles-found=0" >> "$GITHUB_OUTPUT"
361+
exit 0
362+
fi
363+
mkdir -p bundles-dist
364+
BUNDLES_DIST="$PWD/bundles-dist"
365+
for bundle_pyproject in "${bundles[@]}"; do
366+
bundle_dir=$(dirname "$bundle_pyproject")
367+
echo "Building wheel for $bundle_dir"
368+
(cd "$bundle_dir" && uv build --wheel --out-dir "$BUNDLES_DIST")
369+
done
370+
echo "bundles-found=1" >> "$GITHUB_OUTPUT"
371+
ls -la bundles-dist/
372+
373+
- name: Upload Bundles Artifact
374+
if: steps.build-bundles.outputs.bundles-found == '1'
375+
uses: actions/upload-artifact@v6
376+
with:
377+
name: dist-nightly-bundles
378+
path: bundles-dist
356379

357380
# PyPI publishing moved to after cross-platform testing
358381

@@ -371,6 +394,8 @@ jobs:
371394
main-artifact-name: "dist-nightly-main"
372395
lfx-artifact-name: "dist-nightly-lfx"
373396
sdk-artifact-name: "dist-nightly-sdk"
397+
bundles-artifact-name: "dist-nightly-bundles"
398+
pre_release: true
374399

375400
publish-nightly-lfx:
376401
name: Publish LFX Nightly to PyPI
@@ -457,11 +482,90 @@ jobs:
457482
run: |
458483
make publish base=true
459484
460-
publish-nightly-main:
461-
name: Publish Langflow Main Nightly to PyPI
485+
check-nightly-main-pypi-dependencies:
486+
name: Check Main Nightly PyPI Dependencies
462487
needs: [build-nightly-main, test-cross-platform, publish-nightly-base]
463488
if: ${{ always() && needs.build-nightly-main.result == 'success' && needs.test-cross-platform.result == 'success' && needs.publish-nightly-base.result == 'success' }}
464489
runs-on: ubuntu-latest
490+
steps:
491+
- name: Checkout code
492+
uses: actions/checkout@v6
493+
with:
494+
ref: ${{ inputs.nightly_tag_release }}
495+
persist-credentials: true
496+
- name: Setup Environment
497+
uses: astral-sh/setup-uv@v6
498+
with:
499+
enable-cache: false
500+
python-version: "3.13"
501+
- name: Check direct bundle dependencies on PyPI
502+
run: |
503+
uv run --with packaging --no-project python - <<'PY'
504+
import json
505+
import sys
506+
import tomllib
507+
import urllib.error
508+
import urllib.request
509+
from pathlib import Path
510+
511+
from packaging.requirements import Requirement
512+
from packaging.utils import canonicalize_name
513+
from packaging.version import InvalidVersion, Version
514+
515+
data = tomllib.loads(Path("pyproject.toml").read_text())
516+
requirements = []
517+
for dependency in data["project"].get("dependencies", []):
518+
requirement = Requirement(dependency)
519+
name = canonicalize_name(requirement.name)
520+
if name.startswith("lfx-"):
521+
requirements.append(requirement)
522+
523+
if not requirements:
524+
print("No direct lfx-* dependencies found.")
525+
sys.exit(0)
526+
527+
missing = []
528+
unsatisfied = []
529+
for requirement in requirements:
530+
name = canonicalize_name(requirement.name)
531+
url = f"https://pypi.org/pypi/{name}/json"
532+
try:
533+
with urllib.request.urlopen(url, timeout=30) as response:
534+
project = json.load(response)
535+
except urllib.error.HTTPError as exc:
536+
if exc.code == 404:
537+
missing.append(name)
538+
continue
539+
raise
540+
541+
matching_versions = []
542+
for version_text in project.get("releases", {}):
543+
try:
544+
version = Version(version_text)
545+
except InvalidVersion:
546+
continue
547+
if version in requirement.specifier:
548+
matching_versions.append(version)
549+
550+
if not matching_versions:
551+
unsatisfied.append(f"{name}{requirement.specifier}")
552+
else:
553+
latest = max(matching_versions)
554+
print(f"{name}{requirement.specifier}: available, latest matching {latest}")
555+
556+
if missing or unsatisfied:
557+
if missing:
558+
print("Missing PyPI projects: " + ", ".join(missing), file=sys.stderr)
559+
if unsatisfied:
560+
print("No published version satisfies: " + ", ".join(unsatisfied), file=sys.stderr)
561+
sys.exit(1)
562+
PY
563+
564+
publish-nightly-main:
565+
name: Publish Langflow Main Nightly to PyPI
566+
needs: [build-nightly-main, test-cross-platform, publish-nightly-base, check-nightly-main-pypi-dependencies]
567+
if: ${{ always() && needs.build-nightly-main.result == 'success' && needs.test-cross-platform.result == 'success' && needs.publish-nightly-base.result == 'success' && needs.check-nightly-main-pypi-dependencies.result == 'success' }}
568+
runs-on: ubuntu-latest
465569
steps:
466570
- name: Checkout code
467571
uses: actions/checkout@v6

.secrets.baseline

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -153,7 +153,7 @@
153153
"filename": ".github/workflows/release_nightly.yml",
154154
"hashed_secret": "3e26d6750975d678acb8fa35a0f69237881576b0",
155155
"is_verified": false,
156-
"line_number": 497,
156+
"line_number": 601,
157157
"is_secret": false
158158
}
159159
],
@@ -9287,5 +9287,5 @@
92879287
}
92889288
]
92899289
},
9290-
"generated_at": "2026-06-23T23:00:36Z"
9290+
"generated_at": "2026-06-24T03:30:55Z"
92919291
}

0 commit comments

Comments
 (0)