mirror of
https://github.com/langflow-ai/langflow.git
synced 2026-07-26 00:28:17 +08:00
fix(ci): lockstep langflow-nightly and langflow-base-nightly versions (forward-port #13413) (#13416)
fix(ci): lockstep langflow-nightly and langflow-base-nightly versions Forward-port of #13413 (merged to release-1.10.0) to main. pypi_nightly_tag.py now derives a single shared dev number from max(dev across BOTH langflow-nightly and langflow-base-nightly PyPI histories) + 1 (restricted to the root base_version) and emits it twice in `both` mode. nightly_build.yml reads the release and base tags from one invocation (single PyPI snapshot) and fails closed on mismatch, so the latest langflow-nightly always pins a langflow-base-nightly[complete]==X.Y.Z.devN that was built and published in the same run (no more uninstallable nightly). Safe to land now: the nightly create-nightly-tag job checks out the latest release-* branch for the script while taking the run-block from the triggering ref (main for scheduled runs). release-1.10.0 already carries the new script (via #13413), so main's `both` run-block matches it, and future release branches cut from main inherit the lockstep script. Adds scripts/ci/test_pypi_nightly_tag.py + .github/workflows/ci-scripts-test.yml and the update_pyproject_combined.py lockstep-invariant note.
This commit is contained in:
27
.github/workflows/ci-scripts-test.yml
vendored
Normal file
27
.github/workflows/ci-scripts-test.yml
vendored
Normal file
@ -0,0 +1,27 @@
|
||||
name: CI Scripts Tests
|
||||
|
||||
on:
|
||||
pull_request:
|
||||
paths:
|
||||
- "scripts/ci/**"
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
test:
|
||||
runs-on: ubuntu-latest
|
||||
name: Test CI release scripts
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: "3.13"
|
||||
|
||||
- name: Install dependencies
|
||||
run: pip install pytest requests packaging
|
||||
|
||||
- name: Run CI script tests
|
||||
run: python -m pytest scripts/ci/ -v
|
||||
42
.github/workflows/nightly_build.yml
vendored
42
.github/workflows/nightly_build.yml
vendored
@ -82,7 +82,7 @@ jobs:
|
||||
# Required to create tag
|
||||
contents: write
|
||||
outputs:
|
||||
release_tag: ${{ steps.generate_release_tag.outputs.release_tag }}
|
||||
release_tag: ${{ steps.generate_shared_tag.outputs.release_tag }}
|
||||
base_tag: ${{ steps.set_base_tag.outputs.base_tag }}
|
||||
lfx_tag: ${{ steps.generate_lfx_tag.outputs.lfx_tag }}
|
||||
sdk_tag: ${{ steps.generate_sdk_tag.outputs.sdk_tag }}
|
||||
@ -107,30 +107,32 @@ jobs:
|
||||
run: |
|
||||
echo "PWD: $(pwd)"
|
||||
find . -name "pypi_nightly_tag.py"
|
||||
- name: Generate main nightly tag
|
||||
id: generate_release_tag
|
||||
- name: Generate shared nightly tag (main == base, lockstep)
|
||||
id: generate_shared_tag
|
||||
run: |
|
||||
# langflow-nightly pins langflow-base-nightly[complete]==<exact dev>, so both tags
|
||||
# MUST be identical. Compute them in ONE invocation (single PyPI snapshot) so they
|
||||
# cannot drift across separate calls.
|
||||
# NOTE: This outputs the tag with the `v` prefix.
|
||||
RELEASE_TAG="$(uv run ./scripts/ci/pypi_nightly_tag.py main)"
|
||||
TAGS_OUTPUT="$(uv run ./scripts/ci/pypi_nightly_tag.py both)"
|
||||
RELEASE_TAG="$(printf '%s\n' "$TAGS_OUTPUT" | sed -n '1p')"
|
||||
BASE_TAG="$(printf '%s\n' "$TAGS_OUTPUT" | sed -n '2p')"
|
||||
if [ -z "$RELEASE_TAG" ] || [ "$RELEASE_TAG" != "$BASE_TAG" ]; then
|
||||
echo "Shared tag mismatch: release='$RELEASE_TAG' base='$BASE_TAG'. Exiting the workflow."
|
||||
exit 1
|
||||
fi
|
||||
echo "release_tag=$RELEASE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "release_tag=$RELEASE_TAG"
|
||||
echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "release_tag=$RELEASE_TAG base_tag=$BASE_TAG"
|
||||
|
||||
- name: Delete existing tag if it exists
|
||||
id: check_release_tag
|
||||
run: |
|
||||
git fetch --tags
|
||||
git tag -d ${{ steps.generate_release_tag.outputs.release_tag }} || true
|
||||
git push --delete origin ${{ steps.generate_release_tag.outputs.release_tag }} || true
|
||||
git tag -d ${{ steps.generate_shared_tag.outputs.release_tag }} || true
|
||||
git push --delete origin ${{ steps.generate_shared_tag.outputs.release_tag }} || true
|
||||
echo "release_tag_exists=false" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Generate base nightly tag
|
||||
id: generate_base_tag
|
||||
run: |
|
||||
# NOTE: This outputs the tag with the `v` prefix.
|
||||
BASE_TAG="$(uv run ./scripts/ci/pypi_nightly_tag.py base)"
|
||||
echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "base_tag=$BASE_TAG"
|
||||
|
||||
- name: Generate LFX nightly tag
|
||||
id: generate_lfx_tag
|
||||
run: |
|
||||
@ -155,8 +157,8 @@ jobs:
|
||||
git config --global user.email "bot-nightly-builds@langflow.org"
|
||||
git config --global user.name "Langflow Bot"
|
||||
|
||||
RELEASE_TAG="${{ steps.generate_release_tag.outputs.release_tag }}"
|
||||
BASE_TAG="${{ steps.generate_base_tag.outputs.base_tag }}"
|
||||
RELEASE_TAG="${{ steps.generate_shared_tag.outputs.release_tag }}"
|
||||
BASE_TAG="${{ steps.generate_shared_tag.outputs.base_tag }}"
|
||||
LFX_TAG="${{ steps.generate_lfx_tag.outputs.lfx_tag }}"
|
||||
SDK_TAG="${{ steps.generate_sdk_tag.outputs.sdk_tag }}"
|
||||
echo "Updating SDK project version to $SDK_TAG"
|
||||
@ -195,7 +197,7 @@ jobs:
|
||||
- name: Checkout main nightly tag
|
||||
uses: actions/checkout@v6
|
||||
with:
|
||||
ref: ${{ steps.generate_release_tag.outputs.release_tag }}
|
||||
ref: ${{ steps.generate_shared_tag.outputs.release_tag }}
|
||||
persist-credentials: true
|
||||
|
||||
- name: Retrieve Base Tag
|
||||
@ -214,8 +216,8 @@ jobs:
|
||||
BASE_TAG="${{ steps.retrieve_base_tag.outputs.base_tag }}"
|
||||
echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "base_tag=$BASE_TAG"
|
||||
elif [ "${{ steps.commit_tag.conclusion }}" != "skipped" ] && [ "${{ steps.generate_base_tag.outputs.base_tag }}" ]; then
|
||||
BASE_TAG="${{ steps.generate_base_tag.outputs.base_tag }}"
|
||||
elif [ "${{ steps.commit_tag.conclusion }}" != "skipped" ] && [ "${{ steps.generate_shared_tag.outputs.base_tag }}" ]; then
|
||||
BASE_TAG="${{ steps.generate_shared_tag.outputs.base_tag }}"
|
||||
echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
|
||||
echo "base_tag=$BASE_TAG"
|
||||
else
|
||||
|
||||
@ -187,7 +187,7 @@
|
||||
"filename": ".github/workflows/nightly_build.yml",
|
||||
"hashed_secret": "3e26d6750975d678acb8fa35a0f69237881576b0",
|
||||
"is_verified": false,
|
||||
"line_number": 303,
|
||||
"line_number": 305,
|
||||
"is_secret": false
|
||||
}
|
||||
],
|
||||
@ -8272,5 +8272,5 @@
|
||||
}
|
||||
]
|
||||
},
|
||||
"generated_at": "2026-05-26T14:34:58Z"
|
||||
"generated_at": "2026-05-29T18:18:04Z"
|
||||
}
|
||||
|
||||
@ -271,6 +271,13 @@ external = ["RUF027"]
|
||||
"TRY003", # Long exception messages
|
||||
"EM101", # String literals in exceptions
|
||||
]
|
||||
"scripts/ci/test_*.py" = [
|
||||
"D1", # Missing docstrings
|
||||
"PLR2004", # Magic value comparisons
|
||||
"S101", # Use of assert (required for pytest)
|
||||
"TRY003", # Long exception messages
|
||||
"EM101", # String literals in exceptions
|
||||
]
|
||||
"src/backend/base/langflow/alembic/versions/*" = ["INP001", "D415", "PGH003"]
|
||||
"src/backend/base/langflow/custom/__init__.py" = [
|
||||
"I001", # Import order affects initialization - must import custom module first
|
||||
|
||||
@ -1,100 +1,122 @@
|
||||
#!/usr/bin/env python
|
||||
"""Idea from https://github.com/streamlit/streamlit/blob/4841cf91f1c820a392441092390c4c04907f9944/scripts/pypi_nightly_create_tag.py."""
|
||||
"""Idea from https://github.com/streamlit/streamlit/blob/4841cf91f1c820a392441092390c4c04907f9944/scripts/pypi_nightly_create_tag.py.
|
||||
|
||||
`langflow-nightly` pins an EXACT dependency on `langflow-base-nightly[complete]==X.Y.Z.devN`.
|
||||
For the latest published `langflow-nightly` to be installable, the base version it pins must
|
||||
exist on PyPI. The two packages are therefore versioned in lockstep: they share a single dev
|
||||
number so that, in a single nightly run (publish order base -> main, gated), main's `devN` pin
|
||||
always references the base `devN` built and published in the same run.
|
||||
|
||||
The shared dev number is `max(dev across BOTH packages' PyPI histories) + 1`, restricted to
|
||||
releases whose base_version matches the root pyproject. Both "main" and "base" build types
|
||||
return the identical tag; the "both" mode emits it twice so the workflow can read the release
|
||||
and base tags from a single invocation (one PyPI snapshot) and avoid any cross-call drift.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
|
||||
import packaging.version
|
||||
import requests
|
||||
from packaging.version import Version
|
||||
|
||||
PYPI_LANGFLOW_URL = "https://pypi.org/pypi/langflow/json"
|
||||
PYPI_LANGFLOW_NIGHTLY_URL = "https://pypi.org/pypi/langflow-nightly/json"
|
||||
|
||||
PYPI_LANGFLOW_BASE_URL = "https://pypi.org/pypi/langflow-base/json"
|
||||
PYPI_LANGFLOW_BASE_NIGHTLY_URL = "https://pypi.org/pypi/langflow-base-nightly/json"
|
||||
|
||||
# main and base MUST share one dev number, so the shared number is derived from both packages.
|
||||
PYPI_NIGHTLY_URLS = (PYPI_LANGFLOW_NIGHTLY_URL, PYPI_LANGFLOW_BASE_NIGHTLY_URL)
|
||||
|
||||
ARGUMENT_NUMBER = 2
|
||||
VALID_BUILD_TYPES = ("main", "base", "both")
|
||||
|
||||
|
||||
def get_latest_published_version(build_type: str, *, is_nightly: bool) -> Version:
|
||||
url = ""
|
||||
if build_type == "base":
|
||||
url = PYPI_LANGFLOW_BASE_NIGHTLY_URL if is_nightly else PYPI_LANGFLOW_BASE_URL
|
||||
elif build_type == "main":
|
||||
url = PYPI_LANGFLOW_NIGHTLY_URL if is_nightly else PYPI_LANGFLOW_URL
|
||||
else:
|
||||
msg = f"Invalid build type: {build_type}"
|
||||
raise ValueError(msg)
|
||||
|
||||
res = requests.get(url, timeout=10)
|
||||
res.raise_for_status()
|
||||
try:
|
||||
version_str = res.json()["info"]["version"]
|
||||
except Exception as e:
|
||||
msg = "Got unexpected response from PyPI"
|
||||
raise RuntimeError(msg) from e
|
||||
return Version(version_str)
|
||||
|
||||
|
||||
def create_tag(build_type: str):
|
||||
from pathlib import Path
|
||||
def _root_base_version() -> str:
|
||||
"""Return the base_version (e.g. "1.10.0") from the root pyproject.toml.
|
||||
|
||||
Both langflow-nightly and langflow-base-nightly are versioned from the ROOT pyproject on
|
||||
purpose. Do not switch base to read src/backend/base/pyproject.toml, or the two dev counters
|
||||
will fork again and the exact `==` pin can reference a version that was never published.
|
||||
"""
|
||||
import tomllib
|
||||
|
||||
# Read version from pyproject.toml
|
||||
main_tag_pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
|
||||
pyproject_data = tomllib.loads(main_tag_pyproject_path.read_text())
|
||||
pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml"
|
||||
pyproject_data = tomllib.loads(pyproject_path.read_text())
|
||||
return Version(pyproject_data["project"]["version"]).base_version
|
||||
|
||||
current_version_str = pyproject_data["project"]["version"]
|
||||
current_version = Version(current_version_str)
|
||||
|
||||
def _all_dev_numbers(url: str, base_version: str) -> list[int]:
|
||||
"""Dev numbers of every release of url whose base_version matches base_version.
|
||||
|
||||
A 404 means the package genuinely has no releases yet (e.g. the first-ever nightly): it
|
||||
contributes nothing and returns an empty list. Every OTHER failure -- a network error, a
|
||||
non-404 HTTP status (5xx / 403 / ...), or a malformed 200 response -- is fatal and raises,
|
||||
so the nightly job aborts BEFORE mutating tags. Failing closed prevents a transient lookup
|
||||
failure on the higher-versioned package from lowering max(dev) + 1 and regenerating an
|
||||
already-published version. Non-dev/final releases and releases from another base_version
|
||||
never contribute.
|
||||
"""
|
||||
res = requests.get(url, timeout=10)
|
||||
if res.status_code == requests.codes.not_found:
|
||||
return []
|
||||
res.raise_for_status()
|
||||
try:
|
||||
current_nightly_version = get_latest_published_version(build_type, is_nightly=True)
|
||||
except (requests.RequestException, KeyError, ValueError):
|
||||
# If nightly doesn't exist yet
|
||||
current_nightly_version = None
|
||||
releases = res.json()["releases"]
|
||||
except (ValueError, KeyError) as e:
|
||||
msg = f"Unexpected response from {url!r}: missing 'releases' mapping"
|
||||
raise RuntimeError(msg) from e
|
||||
|
||||
build_number = "0"
|
||||
latest_base_version = current_version.base_version
|
||||
nightly_base_version = current_nightly_version.base_version if current_nightly_version else None
|
||||
dev_numbers: list[int] = []
|
||||
for version_str in releases:
|
||||
try:
|
||||
version = Version(version_str)
|
||||
except packaging.version.InvalidVersion:
|
||||
continue
|
||||
if version.base_version == base_version and version.dev is not None:
|
||||
dev_numbers.append(version.dev)
|
||||
return dev_numbers
|
||||
|
||||
if latest_base_version == nightly_base_version:
|
||||
# If the latest version is the same as the nightly version, increment the build number
|
||||
dev_number = (current_nightly_version.dev or 0) if current_nightly_version else 0
|
||||
build_number = str(dev_number + 1)
|
||||
|
||||
new_nightly_version = latest_base_version + ".dev" + build_number
|
||||
def _shared_nightly_version() -> str:
|
||||
"""Compute the single dev number shared by langflow-nightly and langflow-base-nightly."""
|
||||
base_version = _root_base_version()
|
||||
|
||||
# Prepend "v" to the version, if DNE.
|
||||
# This is an update to the nightly version format.
|
||||
if not new_nightly_version.startswith("v"):
|
||||
new_nightly_version = "v" + new_nightly_version
|
||||
dev_numbers = [dev for url in PYPI_NIGHTLY_URLS for dev in _all_dev_numbers(url, base_version)]
|
||||
|
||||
# X.Y.Z.dev.YYYYMMDD
|
||||
# This takes the base version of the current version and appends the
|
||||
# current date. If the last release was on the same day, we exit, as
|
||||
# pypi does not allow for overwriting the same version.
|
||||
# First-ever nightly for this base_version -> dev0. Otherwise max+1, so the result is
|
||||
# strictly ahead of BOTH packages' newest same-series dev release.
|
||||
next_dev = max(dev_numbers) + 1 if dev_numbers else 0
|
||||
|
||||
# We could use a different versioning scheme, such as just incrementing
|
||||
# an integer.
|
||||
# version_with_date = (
|
||||
# ".".join([str(x) for x in current_version.release])
|
||||
# + ".dev"
|
||||
# + "0"
|
||||
# + datetime.now(pytz.timezone("UTC")).strftime("%Y%m%d")
|
||||
# )
|
||||
new_nightly_version = f"v{base_version}.dev{next_dev}"
|
||||
|
||||
# Verify if version is PEP440 compliant.
|
||||
# Verify the version is PEP 440 compliant.
|
||||
packaging.version.Version(new_nightly_version)
|
||||
|
||||
return new_nightly_version
|
||||
|
||||
|
||||
def create_tag(build_type: str) -> str:
|
||||
"""Return the shared nightly tag (with a leading ``v``).
|
||||
|
||||
``build_type`` is accepted for backward compatibility and validated, but "main" and "base"
|
||||
always return the identical version by design (lockstep versioning).
|
||||
"""
|
||||
if build_type not in VALID_BUILD_TYPES:
|
||||
msg = f"Invalid build type: {build_type}"
|
||||
raise ValueError(msg)
|
||||
return _shared_nightly_version()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
if len(sys.argv) != ARGUMENT_NUMBER:
|
||||
msg = "Specify base or main"
|
||||
msg = "Specify base, main, or both"
|
||||
raise ValueError(msg)
|
||||
|
||||
build_type = sys.argv[1]
|
||||
tag = create_tag(build_type)
|
||||
print(tag)
|
||||
requested_build_type = sys.argv[1]
|
||||
tag = create_tag(requested_build_type)
|
||||
if requested_build_type == "both":
|
||||
# Emit twice so the workflow can capture release_tag and base_tag from a SINGLE
|
||||
# invocation -> one PyPI snapshot -> guaranteed-identical tags.
|
||||
print(tag)
|
||||
print(tag)
|
||||
else:
|
||||
print(tag)
|
||||
|
||||
192
scripts/ci/test_pypi_nightly_tag.py
Normal file
192
scripts/ci/test_pypi_nightly_tag.py
Normal file
@ -0,0 +1,192 @@
|
||||
"""Unit tests for pypi_nightly_tag.py (lockstep nightly versioning).
|
||||
|
||||
Run directly (matches test_constraint_preservation.py style) or via pytest:
|
||||
|
||||
cd scripts/ci && uv run python -m pytest test_pypi_nightly_tag.py -q
|
||||
cd scripts/ci && uv run python test_pypi_nightly_tag.py
|
||||
|
||||
All PyPI HTTP traffic is mocked; no network access. `_root_base_version` is patched so the
|
||||
tests do not depend on the repo's live version.
|
||||
"""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from unittest import mock
|
||||
|
||||
import pytest
|
||||
import requests
|
||||
|
||||
# Ensure the sibling module is importable regardless of how pytest is invoked.
|
||||
sys.path.insert(0, str(Path(__file__).parent))
|
||||
import pypi_nightly_tag as nt
|
||||
|
||||
MAIN_URL = nt.PYPI_LANGFLOW_NIGHTLY_URL
|
||||
BASE_URL = nt.PYPI_LANGFLOW_BASE_NIGHTLY_URL
|
||||
|
||||
|
||||
class _FakeResponse:
|
||||
"""Minimal stand-in for a requests.Response from PyPI's /pypi/<pkg>/json endpoint."""
|
||||
|
||||
def __init__(self, *, releases=None, status_code=200, malformed=False):
|
||||
self._releases = list(releases) if releases else []
|
||||
self.status_code = status_code
|
||||
self._malformed = malformed
|
||||
|
||||
def raise_for_status(self):
|
||||
if self.status_code >= 400 and self.status_code != requests.codes.not_found:
|
||||
msg = f"status {self.status_code}"
|
||||
raise requests.HTTPError(msg)
|
||||
|
||||
def json(self):
|
||||
if self._malformed:
|
||||
return {"info": {"version": "1.10.0.dev1"}} # 200 OK but missing "releases"
|
||||
# Real PyPI shape: {"releases": {"<version>": [<files>], ...}}.
|
||||
return {"releases": {version: [] for version in self._releases}}
|
||||
|
||||
|
||||
def _build_response(spec):
|
||||
"""Turn a per-package spec into a _FakeResponse.
|
||||
|
||||
spec: iterable of version strings, an int HTTP status (e.g. 404), the string "malformed",
|
||||
or None (absent package -> empty release list).
|
||||
"""
|
||||
if spec is None:
|
||||
return _FakeResponse(releases=[])
|
||||
if isinstance(spec, int):
|
||||
return _FakeResponse(status_code=spec)
|
||||
if spec == "malformed":
|
||||
return _FakeResponse(malformed=True)
|
||||
return _FakeResponse(releases=spec)
|
||||
|
||||
|
||||
def _run(main=None, base=None, *, base_version="1.10.0", build_type="both"):
|
||||
"""Compute a tag with mocked PyPI responses for the main and base nightly packages."""
|
||||
responses = {MAIN_URL: _build_response(main), BASE_URL: _build_response(base)}
|
||||
|
||||
def fake_get(url, timeout=10): # noqa: ARG001
|
||||
return responses[url]
|
||||
|
||||
with (
|
||||
mock.patch.object(nt.requests, "get", side_effect=fake_get),
|
||||
mock.patch.object(nt, "_root_base_version", return_value=base_version),
|
||||
):
|
||||
return nt.create_tag(build_type)
|
||||
|
||||
|
||||
# --- core invariant: main and base always resolve to the identical shared version -------------
|
||||
|
||||
|
||||
def test_main_and_base_return_identical_version_given_offset_histories():
|
||||
# Live-state analog: langflow-nightly latest dev54, langflow-base-nightly latest dev48.
|
||||
main_hist = [f"1.10.0.dev{n}" for n in range(55)]
|
||||
base_hist = [f"1.10.0.dev{n}" for n in range(49)]
|
||||
main_tag = _run(main_hist, base_hist, build_type="main")
|
||||
base_tag = _run(main_hist, base_hist, build_type="base")
|
||||
both_tag = _run(main_hist, base_hist, build_type="both")
|
||||
assert main_tag == base_tag == both_tag == "v1.10.0.dev55" # max(54, 48) + 1
|
||||
|
||||
|
||||
def test_aligned_histories_increment_by_one():
|
||||
hist = [f"1.10.0.dev{n}" for n in range(43)] # both latest dev42
|
||||
assert _run(hist, hist) == "v1.10.0.dev43"
|
||||
|
||||
|
||||
# --- edge cases -------------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_first_ever_nightly_both_404():
|
||||
assert _run(404, 404) == "v1.10.0.dev0"
|
||||
|
||||
|
||||
def test_first_ever_nightly_both_absent():
|
||||
assert _run(None, None) == "v1.10.0.dev0"
|
||||
|
||||
|
||||
def test_base_version_bump_resets_counter():
|
||||
# Root pyproject bumped to 1.10.1; old 1.10.0 devs must not leak into the new series.
|
||||
old_series = [f"1.10.0.dev{n}" for n in range(55)]
|
||||
assert _run(old_series, old_series, base_version="1.10.1") == "v1.10.1.dev0"
|
||||
|
||||
|
||||
def test_base_version_bump_with_some_new_series_releases():
|
||||
main_hist = ["1.10.0.dev54", "1.10.1.dev0", "1.10.1.dev1"]
|
||||
base_hist = ["1.10.0.dev48", "1.10.1.dev0"]
|
||||
assert _run(main_hist, base_hist, base_version="1.10.1") == "v1.10.1.dev2"
|
||||
|
||||
|
||||
def test_one_present_one_404():
|
||||
assert _run(["1.10.0.dev54"], 404) == "v1.10.0.dev55"
|
||||
assert _run(404, ["1.10.0.dev70"]) == "v1.10.0.dev71"
|
||||
|
||||
|
||||
def test_final_release_does_not_advance_counter():
|
||||
# A non-dev/final release must not bump the nightly dev counter.
|
||||
assert _run(["1.10.0", "1.10.0.dev3"], ["1.10.0.dev2"]) == "v1.10.0.dev4"
|
||||
|
||||
|
||||
def test_only_final_releases_yield_dev0():
|
||||
assert _run(["1.10.0"], ["1.10.0"]) == "v1.10.0.dev0"
|
||||
|
||||
|
||||
def test_malformed_response_raises():
|
||||
# A 200 response missing "releases" is fatal (fail closed), not silently ignored.
|
||||
with pytest.raises(RuntimeError):
|
||||
_run("malformed", ["1.10.0.dev10"])
|
||||
|
||||
|
||||
def test_server_error_raises():
|
||||
# A non-404 HTTP status on the higher-versioned package must abort, not emit a stale tag.
|
||||
with pytest.raises(requests.HTTPError):
|
||||
_run(500, ["1.10.0.dev48"])
|
||||
|
||||
|
||||
def test_higher_package_lookup_failure_aborts():
|
||||
# P1 regression: langflow-nightly (dev54) lookup fails transiently while
|
||||
# langflow-base-nightly reports dev48. Must NOT emit v1.10.0.dev49 -- must raise so the job
|
||||
# stops before deleting/recreating tags or republishing an already-existing version.
|
||||
with pytest.raises(requests.HTTPError):
|
||||
_run(503, [f"1.10.0.dev{n}" for n in range(49)])
|
||||
|
||||
|
||||
def test_network_error_raises():
|
||||
# A transient connection failure must abort rather than lower the computed dev number.
|
||||
def boom(url, timeout=10): # noqa: ARG001
|
||||
raise requests.ConnectionError("transient")
|
||||
|
||||
with (
|
||||
mock.patch.object(nt.requests, "get", side_effect=boom),
|
||||
mock.patch.object(nt, "_root_base_version", return_value="1.10.0"),
|
||||
pytest.raises(requests.ConnectionError),
|
||||
):
|
||||
nt.create_tag("both")
|
||||
|
||||
|
||||
def test_unparseable_version_string_is_skipped():
|
||||
assert _run(["not-a-version", "1.10.0.dev9"], None) == "v1.10.0.dev10"
|
||||
|
||||
|
||||
def test_invalid_build_type_raises():
|
||||
with pytest.raises(ValueError, match="Invalid build type"):
|
||||
_run(["1.10.0.dev1"], ["1.10.0.dev1"], build_type="frontend")
|
||||
|
||||
|
||||
def _main():
|
||||
"""Run all tests without pytest (mirrors test_constraint_preservation.py)."""
|
||||
failures = 0
|
||||
for name, fn in sorted(globals().items()):
|
||||
if name.startswith("test_") and callable(fn):
|
||||
try:
|
||||
fn()
|
||||
except Exception as exc: # noqa: BLE001
|
||||
failures += 1
|
||||
print(f"FAIL {name}: {exc}")
|
||||
else:
|
||||
print(f"ok {name}")
|
||||
if failures:
|
||||
msg = f"{failures} test(s) failed."
|
||||
raise SystemExit(msg)
|
||||
print("All pypi_nightly_tag tests passed.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
_main()
|
||||
@ -36,6 +36,12 @@ def main():
|
||||
base_tag = sys.argv[3]
|
||||
lfx_tag = sys.argv[4]
|
||||
|
||||
# Lockstep invariant: langflow-base-nightly's published version (set here from `base_tag`)
|
||||
# and langflow-nightly's exact `==` pin on it (set below from the same `base_tag`) MUST come
|
||||
# from the same value, so the latest langflow-nightly always pins a base version published in
|
||||
# the same run. `pypi_nightly_tag.py` makes the main and base tags identical; keep both writes
|
||||
# sourced from `base_tag` or the pin can reference a version that was never published.
|
||||
|
||||
# First handle base package updates
|
||||
update_pyproject_name("src/backend/base/pyproject.toml", "langflow-base-nightly")
|
||||
update_name_uv_dep("pyproject.toml", "langflow-base-nightly")
|
||||
|
||||
Reference in New Issue
Block a user