|
1 | 1 | #!/usr/bin/env python |
2 | | -"""Idea from https://github.com/streamlit/streamlit/blob/4841cf91f1c820a392441092390c4c04907f9944/scripts/pypi_nightly_create_tag.py.""" |
| 2 | +"""Idea from https://github.com/streamlit/streamlit/blob/4841cf91f1c820a392441092390c4c04907f9944/scripts/pypi_nightly_create_tag.py. |
| 3 | +
|
| 4 | +`langflow-nightly` pins an EXACT dependency on `langflow-base-nightly[complete]==X.Y.Z.devN`. |
| 5 | +For the latest published `langflow-nightly` to be installable, the base version it pins must |
| 6 | +exist on PyPI. The two packages are therefore versioned in lockstep: they share a single dev |
| 7 | +number so that, in a single nightly run (publish order base -> main, gated), main's `devN` pin |
| 8 | +always references the base `devN` built and published in the same run. |
| 9 | +
|
| 10 | +The shared dev number is `max(dev across BOTH packages' PyPI histories) + 1`, restricted to |
| 11 | +releases whose base_version matches the root pyproject. Both "main" and "base" build types |
| 12 | +return the identical tag; the "both" mode emits it twice so the workflow can read the release |
| 13 | +and base tags from a single invocation (one PyPI snapshot) and avoid any cross-call drift. |
| 14 | +""" |
3 | 15 |
|
4 | 16 | import sys |
| 17 | +from pathlib import Path |
5 | 18 |
|
6 | 19 | import packaging.version |
7 | 20 | import requests |
8 | 21 | from packaging.version import Version |
9 | 22 |
|
10 | | -PYPI_LANGFLOW_URL = "https://pypi.org/pypi/langflow/json" |
11 | 23 | PYPI_LANGFLOW_NIGHTLY_URL = "https://pypi.org/pypi/langflow-nightly/json" |
12 | | - |
13 | | -PYPI_LANGFLOW_BASE_URL = "https://pypi.org/pypi/langflow-base/json" |
14 | 24 | PYPI_LANGFLOW_BASE_NIGHTLY_URL = "https://pypi.org/pypi/langflow-base-nightly/json" |
15 | 25 |
|
| 26 | +# main and base MUST share one dev number, so the shared number is derived from both packages. |
| 27 | +PYPI_NIGHTLY_URLS = (PYPI_LANGFLOW_NIGHTLY_URL, PYPI_LANGFLOW_BASE_NIGHTLY_URL) |
| 28 | + |
16 | 29 | ARGUMENT_NUMBER = 2 |
| 30 | +VALID_BUILD_TYPES = ("main", "base", "both") |
17 | 31 |
|
18 | 32 |
|
19 | | -def get_latest_published_version(build_type: str, *, is_nightly: bool) -> Version: |
20 | | - url = "" |
21 | | - if build_type == "base": |
22 | | - url = PYPI_LANGFLOW_BASE_NIGHTLY_URL if is_nightly else PYPI_LANGFLOW_BASE_URL |
23 | | - elif build_type == "main": |
24 | | - url = PYPI_LANGFLOW_NIGHTLY_URL if is_nightly else PYPI_LANGFLOW_URL |
25 | | - else: |
26 | | - msg = f"Invalid build type: {build_type}" |
27 | | - raise ValueError(msg) |
| 33 | +def _root_base_version() -> str: |
| 34 | + """Return the base_version (e.g. "1.10.0") from the root pyproject.toml. |
| 35 | +
|
| 36 | + Both langflow-nightly and langflow-base-nightly are versioned from the ROOT pyproject on |
| 37 | + purpose. Do not switch base to read src/backend/base/pyproject.toml, or the two dev counters |
| 38 | + will fork again and the exact `==` pin can reference a version that was never published. |
| 39 | + """ |
| 40 | + import tomllib |
| 41 | + |
| 42 | + pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml" |
| 43 | + pyproject_data = tomllib.loads(pyproject_path.read_text()) |
| 44 | + return Version(pyproject_data["project"]["version"]).base_version |
| 45 | + |
28 | 46 |
|
| 47 | +def _all_dev_numbers(url: str, base_version: str) -> list[int]: |
| 48 | + """Dev numbers of every release of url whose base_version matches base_version. |
| 49 | +
|
| 50 | + A 404 means the package genuinely has no releases yet (e.g. the first-ever nightly): it |
| 51 | + contributes nothing and returns an empty list. Every OTHER failure -- a network error, a |
| 52 | + non-404 HTTP status (5xx / 403 / ...), or a malformed 200 response -- is fatal and raises, |
| 53 | + so the nightly job aborts BEFORE mutating tags. Failing closed prevents a transient lookup |
| 54 | + failure on the higher-versioned package from lowering max(dev) + 1 and regenerating an |
| 55 | + already-published version. Non-dev/final releases and releases from another base_version |
| 56 | + never contribute. |
| 57 | + """ |
29 | 58 | res = requests.get(url, timeout=10) |
| 59 | + if res.status_code == requests.codes.not_found: |
| 60 | + return [] |
30 | 61 | res.raise_for_status() |
31 | 62 | try: |
32 | | - version_str = res.json()["info"]["version"] |
33 | | - except Exception as e: |
34 | | - msg = "Got unexpected response from PyPI" |
| 63 | + releases = res.json()["releases"] |
| 64 | + except (ValueError, KeyError) as e: |
| 65 | + msg = f"Unexpected response from {url!r}: missing 'releases' mapping" |
35 | 66 | raise RuntimeError(msg) from e |
36 | | - return Version(version_str) |
37 | 67 |
|
| 68 | + dev_numbers: list[int] = [] |
| 69 | + for version_str in releases: |
| 70 | + try: |
| 71 | + version = Version(version_str) |
| 72 | + except packaging.version.InvalidVersion: |
| 73 | + continue |
| 74 | + if version.base_version == base_version and version.dev is not None: |
| 75 | + dev_numbers.append(version.dev) |
| 76 | + return dev_numbers |
38 | 77 |
|
39 | | -def create_tag(build_type: str): |
40 | | - from pathlib import Path |
41 | 78 |
|
42 | | - import tomllib |
| 79 | +def _shared_nightly_version() -> str: |
| 80 | + """Compute the single dev number shared by langflow-nightly and langflow-base-nightly.""" |
| 81 | + base_version = _root_base_version() |
43 | 82 |
|
44 | | - # Read version from pyproject.toml |
45 | | - main_tag_pyproject_path = Path(__file__).parent.parent.parent / "pyproject.toml" |
46 | | - pyproject_data = tomllib.loads(main_tag_pyproject_path.read_text()) |
| 83 | + dev_numbers = [dev for url in PYPI_NIGHTLY_URLS for dev in _all_dev_numbers(url, base_version)] |
47 | 84 |
|
48 | | - current_version_str = pyproject_data["project"]["version"] |
49 | | - current_version = Version(current_version_str) |
| 85 | + # First-ever nightly for this base_version -> dev0. Otherwise max+1, so the result is |
| 86 | + # strictly ahead of BOTH packages' newest same-series dev release. |
| 87 | + next_dev = max(dev_numbers) + 1 if dev_numbers else 0 |
50 | 88 |
|
51 | | - try: |
52 | | - current_nightly_version = get_latest_published_version(build_type, is_nightly=True) |
53 | | - except (requests.RequestException, KeyError, ValueError): |
54 | | - # If nightly doesn't exist yet |
55 | | - current_nightly_version = None |
56 | | - |
57 | | - build_number = "0" |
58 | | - latest_base_version = current_version.base_version |
59 | | - nightly_base_version = current_nightly_version.base_version if current_nightly_version else None |
60 | | - |
61 | | - if latest_base_version == nightly_base_version: |
62 | | - # If the latest version is the same as the nightly version, increment the build number |
63 | | - dev_number = (current_nightly_version.dev or 0) if current_nightly_version else 0 |
64 | | - build_number = str(dev_number + 1) |
65 | | - |
66 | | - new_nightly_version = latest_base_version + ".dev" + build_number |
67 | | - |
68 | | - # Prepend "v" to the version, if DNE. |
69 | | - # This is an update to the nightly version format. |
70 | | - if not new_nightly_version.startswith("v"): |
71 | | - new_nightly_version = "v" + new_nightly_version |
72 | | - |
73 | | - # X.Y.Z.dev.YYYYMMDD |
74 | | - # This takes the base version of the current version and appends the |
75 | | - # current date. If the last release was on the same day, we exit, as |
76 | | - # pypi does not allow for overwriting the same version. |
77 | | - |
78 | | - # We could use a different versioning scheme, such as just incrementing |
79 | | - # an integer. |
80 | | - # version_with_date = ( |
81 | | - # ".".join([str(x) for x in current_version.release]) |
82 | | - # + ".dev" |
83 | | - # + "0" |
84 | | - # + datetime.now(pytz.timezone("UTC")).strftime("%Y%m%d") |
85 | | - # ) |
86 | | - |
87 | | - # Verify if version is PEP440 compliant. |
| 89 | + new_nightly_version = f"v{base_version}.dev{next_dev}" |
| 90 | + |
| 91 | + # Verify the version is PEP 440 compliant. |
88 | 92 | packaging.version.Version(new_nightly_version) |
89 | 93 |
|
90 | 94 | return new_nightly_version |
91 | 95 |
|
92 | 96 |
|
| 97 | +def create_tag(build_type: str) -> str: |
| 98 | + """Return the shared nightly tag (with a leading ``v``). |
| 99 | +
|
| 100 | + ``build_type`` is accepted for backward compatibility and validated, but "main" and "base" |
| 101 | + always return the identical version by design (lockstep versioning). |
| 102 | + """ |
| 103 | + if build_type not in VALID_BUILD_TYPES: |
| 104 | + msg = f"Invalid build type: {build_type}" |
| 105 | + raise ValueError(msg) |
| 106 | + return _shared_nightly_version() |
| 107 | + |
| 108 | + |
93 | 109 | if __name__ == "__main__": |
94 | 110 | if len(sys.argv) != ARGUMENT_NUMBER: |
95 | | - msg = "Specify base or main" |
| 111 | + msg = "Specify base, main, or both" |
96 | 112 | raise ValueError(msg) |
97 | 113 |
|
98 | | - build_type = sys.argv[1] |
99 | | - tag = create_tag(build_type) |
100 | | - print(tag) |
| 114 | + requested_build_type = sys.argv[1] |
| 115 | + tag = create_tag(requested_build_type) |
| 116 | + if requested_build_type == "both": |
| 117 | + # Emit twice so the workflow can capture release_tag and base_tag from a SINGLE |
| 118 | + # invocation -> one PyPI snapshot -> guaranteed-identical tags. |
| 119 | + print(tag) |
| 120 | + print(tag) |
| 121 | + else: |
| 122 | + print(tag) |
0 commit comments