From 65ed502e5455c9e3929e573820a07d4dfc7baeb2 Mon Sep 17 00:00:00 2001 From: IronMurphy Date: Fri, 25 Sep 2026 20:42:27 +0800 Subject: [PATCH] fix(release): refresh backend/uv.lock when bumping the version (#5859) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * fix(release): refresh backend/uv.lock when bumping the version `scripts/bump_version.sh` rewrote backend/pyproject.toml, frontend/package.json and the Helm chart's `version`/`appVersion`, but `backend/uv.lock` also records the root package version (uv keeps its PEP 440 form, so `2.1.0-rc0` is stored as `2.1.0rc0`). The documented release step left the lock pinning the previous version, and that is not cosmetic drift: `uv lock --check` (lint CI) fails on a stale lock and `uv sync --locked` (e2e / plugin workflows) refuses the tree, so the release commit could not pass CI. RELEASING.md only mentioned the manual `cd backend && uv lock` step for release candidates. The script now refreshes the lock via `uv lock` and refuses to start without `uv` — it exits before editing anything rather than leaving a half-bumped tree whose lock CI rejects. `uv lock` stays the authority for the normalized prerelease form; the lock diff for a bump is a single line. RELEASING.md and AGENTS.md now describe the lock as part of the bump. Tests: backend/tests/test_bump_version_lockfile_sync.py (red on main, green here) pins both halves — the lock moves with the sources, and a missing uv is an error instead of a silent no-op. * docs(changelog): record the release bump lockfile fix (#5859) --------- Co-authored-by: Willem Jiang --- AGENTS.md | 8 +- CHANGELOG.md | 53 ++++++ CHANGELOG_zh.md | 41 ++++ RELEASING.md | 28 ++- .../tests/test_bump_version_lockfile_sync.py | 175 ++++++++++++++++++ scripts/bump_version.sh | 21 +++ 6 files changed, 313 insertions(+), 13 deletions(-) create mode 100644 backend/tests/test_bump_version_lockfile_sync.py diff --git a/AGENTS.md b/AGENTS.md index b13cff909d..bdca8853ba 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -231,9 +231,11 @@ These apply repo-wide; module guides own the module-specific detail. relying on the platform locale. - **Version sources must stay in lockstep** — a release version must match identically in `backend/pyproject.toml`, `frontend/package.json`, and `deploy/helm/deer-flow/Chart.yaml` - (`version` + `appVersion`). Pushing a `v*` git tag triggers CI that runs - `scripts/verify_versions.sh` and **blocks all publishing** if any source drifts. Before - bumping a version, run `scripts/bump_version.sh ` (aligns all four at once) and + (`version` + `appVersion`), and `backend/uv.lock` must record the same version for the root + package (uv stores its PEP 440 form, e.g. `2.1.0rc0`). Pushing a `v*` git tag triggers CI + that runs `scripts/verify_versions.sh` and **blocks all publishing** if any source drifts. + Before bumping a version, run `scripts/bump_version.sh ` (aligns the four fields and + refreshes `backend/uv.lock` at once — it needs `uv` on `PATH`) and `scripts/verify_versions.sh ` to catch drift early. See [RELEASING.md](RELEASING.md). - **Don't edit `CLAUDE.md`** — it only contains `@AGENTS.md`. All agent guidance changes belong here in `AGENTS.md`; `CLAUDE.md` is a thin import shim. diff --git a/CHANGELOG.md b/CHANGELOG.md index ac447f9557..89934e1439 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2404,6 +2404,57 @@ This release closes that milestone with **772 merged pull requests**. ### Fixed +- **release:** Bumping the version no longer leaves `backend/uv.lock` behind. + `scripts/bump_version.sh` rewrote `backend/pyproject.toml`, `frontend/package.json` + and the Helm chart, but the lockfile records the root package's own version too + (uv keeps its PEP 440 form, so `2.1.0-rc0` is stored as `2.1.0rc0`). The + documented release step therefore produced a commit whose lock CI rejects: + `uv lock --check` fails on the stale lock and `uv sync --locked` refuses the + tree, and with pre-commit installed it broke a step earlier on the + `uv-lock-check` hook. The script now refreshes the lock with `uv lock` and exits + before editing anything when `uv` is missing, instead of leaving a half-bumped + working tree behind. Only the root package's version line moves. ([#5859]) +- **config:** A `config.yaml` edit that lands while the previous edit is still + being loaded is no longer lost until the next edit. `get_app_config()`'s + loader parsed the file and then hashed it again to record the cache + signature, so a write between those two reads left the cache holding the + older content under the newer content's signature — a state the signature + comparison can never detect. The loader now reads the file once and signs + the bytes it parsed; a write that races the load just triggers one more + reload on the next call. ([#5848]) +- **config:** `request_admission.requests_per_minute` and `max_queue_size` now + accept `$VAR` environment references like every other field. Both are strict + integers so a bool or float is still rejected, but `$VAR` substitution always + produces a string, so `requests_per_minute: $RPM` failed the whole config load + with "Input should be a valid integer" even when `RPM=60`. A decimal literal + delivered as a string is now converted before the strict check; any other + string is still rejected. ([#5838]) +- **scheduler:** Pausing a scheduled task no longer loses the pause when a + dispatch is in flight on SQLite. `release_dispatch_lease` guards on the lease + owner — which pausing clears — but read the row without taking SQLite's + writer, so a stale read passed the guard and wrote the task back to + `enabled` with `next_run_at` untouched, leaving the scheduler firing a task + the API had reported as paused. The read now takes the writer first, as every + other mutating path in that repository does. PostgreSQL was unaffected. + ([#5777]) +- **mcp:** Lazy MCP initialization no longer runs tool discovery twice when + discovery itself raises a `RuntimeError` such as `McpTaskConfigurationError`. + The `asyncio.run` fallback in `get_cached_mcp_tools()` was meant only for + `get_event_loop()` failing, but it also caught discovery errors and + re-spawned every stdio server (and re-fetched OAuth tokens) before giving + up; inside a running loop it also logged a misleading "asyncio.run() cannot + be called from a running event loop" traceback instead of the real cause. +- **uploads:** Deleting an uploaded document no longer deletes the converted + Markdown beside it. Conversion names a companion after the document's stem + and falls back to a `_N` suffix when that name is taken, so the `.md` next to + a document can belong to another document sharing the stem, or to the user: + uploading `a.docx` and `a.pdf` produced `a.md` and `a_1.md`, and deleting + `a.pdf` destroyed `a.docx`'s companion. Companions now survive their + document, stay listed, and can be deleted on their own. ([#5673]) +- **subagents:** Recognize zero-byte regular deliverables in remote sandbox + acceptance checks. Readable empty files now satisfy `exists` and + `file_written` and deterministically fail `non-empty`, instead of remaining + UNVERIFIED. ([#5559]) - **frontend:** Keep the `…` (kebab) menu on project chat rows inside the sidebar. In the sidebar's grouped Projects mode the indented nested menus kept `SidebarMenu`'s `w-full` while carrying an extra `ml-4`, so they were @@ -6026,3 +6077,5 @@ with **180 merged pull requests** since the first 2.0 milestone tag. [#5844]: https://github.com/bytedance/deer-flow/pull/5844 [#5845]: https://github.com/bytedance/deer-flow/pull/5845 [#5848]: https://github.com/bytedance/deer-flow/pull/5848 +[#5859]: https://github.com/bytedance/deer-flow/pull/5859 + diff --git a/CHANGELOG_zh.md b/CHANGELOG_zh.md index d4b8d1f79e..96eab4188d 100644 --- a/CHANGELOG_zh.md +++ b/CHANGELOG_zh.md @@ -2120,6 +2120,46 @@ ### 修复 +- **发布:** 版本升级不再把 `backend/uv.lock` 落下。`scripts/bump_version.sh` 会改写 + `backend/pyproject.toml`、`frontend/package.json` 与 Helm chart,但 lockfile 同样记录了 + 根包自身的版本(uv 保留其 PEP 440 形式,因此 `2.1.0-rc0` 存为 `2.1.0rc0`),于是文档给出的 + 发布步骤产出的提交会被 lock 相关 CI 拦下:`uv lock --check` 判其过期,`uv sync --locked` + 也会拒绝该工作区;装了 pre-commit 时还会更早在 `uv-lock-check` 钩子上失败。现在该脚本会用 + `uv lock` 刷新 lockfile,并在缺少 `uv` 时于修改任何文件之前退出,而不是留下一个只改一半的 + 工作区。实际改动仅涉及根包的那一行版本号。([#5859]) +- **配置:** 在上一次编辑仍在加载时落盘的 `config.yaml` 编辑,不再要等到下一次编辑才生效。 + `get_app_config()` 的加载器先解析文件,再重新读取一遍来计算缓存签名,因此夹在两次读取 + 之间的写入会让缓存以较新内容的签名保存较旧的内容,而签名比较永远无法发现这种状态。 + 现在加载器只读取文件一次,并对解析的那份字节计算签名;与加载竞争的写入只会在下一次 + 调用时多触发一次重载。([#5848]) +- **配置:** `request_admission.requests_per_minute` 与 `max_queue_size` 现在与其他字段一样 + 接受 `$VAR` 环境变量引用。这两个字段是严格整数,布尔值与浮点数仍会被拒绝;但 `$VAR` + 替换得到的永远是字符串,因此即使 `RPM=60`,`requests_per_minute: $RPM` 也会让整个配置 + 加载失败并报 "Input should be a valid integer"。现在以字符串形式到达的十进制整数字面量会在 + 严格校验之前被转换;其他字符串仍会被拒绝。([#5838]) +- **调度器:** 在 SQLite 上,调度分发进行中暂停计划任务时不再丢失暂停状态。 + `release_dispatch_lease` 依据租约持有者做校验(暂停会清除该字段),但读取任务行时没有先获取 + SQLite 的写锁,因此过期的读取会通过校验,并把任务状态写回 `enabled` 且不改动 `next_run_at`, + 导致接口已回复“已暂停”的任务仍被继续触发。现在该读取会像该仓储中其他写入路径一样先获取写锁。 + PostgreSQL 不受影响。([#5777]) +- **mcp:** MCP 延迟初始化在工具发现本身抛出 `RuntimeError`(例如 + `McpTaskConfigurationError`)时,不再把发现流程跑两遍。`get_cached_mcp_tools()` 里的 + `asyncio.run` 兜底只为 `get_event_loop()` 失败而设,却同时捕获了发现阶段的错误,于是在 + 放弃之前会重新拉起每一个 stdio 服务器(并重新获取 OAuth 令牌);在运行中的事件循环里, + 它记录的还是误导性的 "asyncio.run() cannot be called from a running event loop" + 堆栈,而不是真正的原因。 +- **上传:** 删除已上传的文档时,不再连带删除其旁边转换生成的 Markdown。转换以文档主干名 + 命名配套文件,名称被占用时回退为 `_N` 后缀,因此文档旁的 `.md` 可能属于主干名相同的另一个 + 文档,或属于用户自己:上传 `a.docx` 与 `a.pdf` 会生成 `a.md` 与 `a_1.md`,删除 `a.pdf` + 却会销毁 `a.docx` 的配套文件。现在配套文件会保留、继续出现在列表中,可单独删除。([#5673]) +- **nginx:** 把 600 秒读取超时扩展到其余两个会等待 Gateway 的 location,它们在线程路由的修复 + 之后仍沿用 nginx 默认的 60 秒。`/api/` 兜底 location 之后:无状态的 `POST /api/runs/wait` + 阻塞在同一套运行完成等待上,并在客户端断开时取消该运行,因此等待超过 60 秒的 API 调用方会 + 同时收到 504 **并且**运行被取消;输入框的 `POST /api/input-polish` 则等待一次性模型调用。 + `/api/skills` 之后:安装 `.skill` 压缩包会对其中每个文件各做一次 LLM 安全扫描,自定义技能的 + 编辑与回滚各再做一次,它们都没有自己的超时;此前只有同级的 `/api/skills/install/upload` + 拿到了更长的超时,因此同样的安装经由 `POST /api/skills/install` 会在 60 秒失败。 + Docker、本地开发与 Helm 配置均已应用。([#5524]) - **前端:** 项目会话行上的 `…`(kebab)菜单不再溢出侧边栏。在侧边栏的按项目分组模式下, 缩进的嵌套菜单继承了 `SidebarMenu` 的 `w-full` 又额外带着 `ml-4`,实际宽度是"100% + 16px", 其绝对定位的 `right-1` 操作按钮因此落到侧边栏边缘之外——活跃项目分组下的行被裁剪,Archived @@ -5135,3 +5175,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子 [#5844]: https://github.com/bytedance/deer-flow/pull/5844 [#5845]: https://github.com/bytedance/deer-flow/pull/5845 [#5848]: https://github.com/bytedance/deer-flow/pull/5848 +[#5859]: https://github.com/bytedance/deer-flow/pull/5859 diff --git a/RELEASING.md b/RELEASING.md index 0f3e17dbeb..5e7ec3dbae 100644 --- a/RELEASING.md +++ b/RELEASING.md @@ -32,8 +32,11 @@ distinguishes it from a release. ## Helper scripts -- `scripts/bump_version.sh ` — set all four fields at once, then - self-verify. Tolerates a leading `v` (e.g. `v2.1.0`). +- `scripts/bump_version.sh ` — set all four fields at once, refresh + `backend/uv.lock`, then self-verify. Tolerates a leading `v` (e.g. `v2.1.0`). + Needs `uv` on `PATH` (`backend/uv.lock` pins the root package version too, and + lint CI runs `uv lock --check`); the script fails before editing anything when + `uv` is missing. ```bash scripts/bump_version.sh 2.1.0 ``` @@ -47,7 +50,8 @@ distinguishes it from a release. ## Release procedure -1. **Bump the version** across all sources: +1. **Bump the version** across all sources (this refreshes + `backend/uv.lock` too): ```bash scripts/bump_version.sh 2.1.0 ``` @@ -74,13 +78,13 @@ distinguishes it from a release. Release-candidate tags must include the same prerelease suffix in all four version fields. For example, before tagging `v2.1.0-rc0`, run -`bash scripts/bump_version.sh 2.1.0-rc0`, refresh `backend/uv.lock` with -`cd backend && uv lock`, and run `bash scripts/verify_versions.sh 2.1.0-rc0` -from the repository root. Commit the version and lockfile changes before -creating the tag. Python lockfiles normalize this version to `2.1.0rc0`; -the source version fields checked by the release gate retain `2.1.0-rc0`. -Re-running a failed workflow on an unchanged tag does not pick up a later -version-fix commit. +`bash scripts/bump_version.sh 2.1.0-rc0` and run +`bash scripts/verify_versions.sh 2.1.0-rc0` from the repository root. Commit +the version and lockfile changes before creating the tag. Python lockfiles +normalize this version to `2.1.0rc0` — `bump_version.sh` leaves the normalizing +to `uv lock` — while the source version fields checked by the release gate +retain `2.1.0-rc0`. Re-running a failed workflow on an unchanged tag does not +pick up a later version-fix commit. ## What CI publishes on a `v*` tag @@ -148,6 +152,10 @@ first job. It runs `scripts/verify_versions.sh` against the tag (minus the `v`). If any of the four version sources doesn't match the tag, the verify job fails and **all** publish jobs are skipped — no images, no chart. +The gate covers those four fields. `backend/uv.lock` is refreshed to the same +version by `scripts/bump_version.sh` and checked by `uv lock --check` in lint CI, +which fails on a stale lock. + When it fails, the job annotation names the offending file and suggests the fix: diff --git a/backend/tests/test_bump_version_lockfile_sync.py b/backend/tests/test_bump_version_lockfile_sync.py new file mode 100644 index 0000000000..10500fc9a2 --- /dev/null +++ b/backend/tests/test_bump_version_lockfile_sync.py @@ -0,0 +1,175 @@ +"""Regression test: ``scripts/bump_version.sh`` must refresh ``backend/uv.lock``. + +``RELEASING.md`` tells releasers that step 1 of a release is +``scripts/bump_version.sh `` — "set all four fields at once, then +self-verify" — and ``verify_versions.sh`` agrees that the tree is consistent +afterwards. But ``backend/uv.lock`` records the root package's own version too +(uv normalizes ``2.1.0-rc0`` to ``2.1.0rc0``), and the script only rewrote +``pyproject.toml`` / ``package.json`` / ``Chart.yaml``. The documented release +step therefore left the lock pinned to the *previous* version. + +Measured on ``3a862780`` (``main``, version ``2.1.0-rc0``) before this fix:: + + $ scripts/bump_version.sh 2.2.0 # documented step, exits 0, self-verifies + $ sed -n '/name = "deer-flow"/,+1p' backend/uv.lock + name = "deer-flow" + version = "2.1.0rc0" # still the old version + $ cd backend && uv lock --check # .github/workflows/lint-check.yml:59 + error: The lockfile at `uv.lock` needs to be updated, but `--check` was provided. + +The release commit that the script claims is verified turns PR CI red, and +``uv sync --locked`` (``e2e-tests.yml``, ``jev-plugin-package.yml``) refuses the +tree. ``RELEASING.md`` only mentions the manual ``cd backend && uv lock`` step in +the pre-release section, so a normal release hit this every time. + +The script now refreshes the lock itself and refuses to run without ``uv`` — +better an early error than a half-bumped tree whose lock CI rejects. +""" + +from __future__ import annotations + +import os +import shutil +import stat +import subprocess +from pathlib import Path + +import pytest +from support.shell import find_script_bash + +REPO_ROOT = Path(__file__).resolve().parents[2] +SCRIPT_BASH = find_script_bash() + +pytestmark = pytest.mark.skipif( + SCRIPT_BASH is None or os.name == "nt", + reason="the fake `uv` on PATH is an extensionless shebang script; Windows does not execute those", +) + +# Minimal versions of the three files bump_version.sh rewrites, plus the lock +# entry uv writes for the root ("virtual") package. Only the fields the script +# touches are present, so a stray rewrite shows up as a diff. +PYPROJECT = '[project]\nname = "deer-flow"\nversion = "2.1.0-rc0"\n' +PACKAGE_JSON = '{\n "name": "deer-flow-frontend",\n "version": "2.1.0-rc0",\n "private": true\n}\n' +CHART = 'apiVersion: v2\nname: deer-flow\nversion: 2.1.0-rc0\nappVersion: "2.1.0-rc0"\n' +UV_LOCK = 'version = 1\n\n[[package]]\nname = "deer-flow"\nversion = "2.1.0rc0"\nsource = { virtual = "." }\n' + +# Emulates the uv behavior the release flow depends on: `uv lock` records the +# pyproject version in the root lock entry, in uv's PEP 440 form (for these +# inputs that only means dropping the `-`). The invocation is logged so the test +# can tell whether the script called uv at all. +FAKE_UV = """#!/usr/bin/env python3 +import re +import sys +from pathlib import Path + +log = Path(__file__).with_name("uv-calls.log") +with log.open("a", encoding="utf-8") as handle: + handle.write("uv " + " ".join(sys.argv[1:]) + "\\n") + +args = sys.argv[1:] +if args != ["lock"]: + sys.exit(f"fake uv: unsupported invocation {args}") + +root = Path.cwd() +lock_path = root / "uv.lock" +lock_text = lock_path.read_text(encoding="utf-8") +pyproject = (root / "pyproject.toml").read_text(encoding="utf-8") +declared = re.search(r'(?m)^version\\s*=\\s*"([^"]+)"', pyproject).group(1) +normalized = declared.replace("-", "") +recorded = re.search(r'(?m)^name = "deer-flow"\\nversion = "([^"]+)"', lock_text).group(1) + +lock_path.write_text(lock_text.replace(f'version = "{recorded}"', f'version = "{normalized}"', 1), encoding="utf-8") +""" + + +def _write_sandbox(root: Path) -> Path: + """Materialize a repo-shaped tree the scripts can run against.""" + (root / "scripts").mkdir(parents=True) + (root / "backend").mkdir() + (root / "frontend").mkdir() + (root / "deploy" / "helm" / "deer-flow").mkdir(parents=True) + + for name in ("bump_version.sh", "verify_versions.sh"): + shutil.copy2(REPO_ROOT / "scripts" / name, root / "scripts" / name) + + (root / "backend" / "pyproject.toml").write_text(PYPROJECT, encoding="utf-8") + (root / "backend" / "uv.lock").write_text(UV_LOCK, encoding="utf-8") + (root / "frontend" / "package.json").write_text(PACKAGE_JSON, encoding="utf-8") + (root / "deploy" / "helm" / "deer-flow" / "Chart.yaml").write_text(CHART, encoding="utf-8") + return root + + +def _install_fake_uv(root: Path) -> Path: + """Put a fake ``uv`` and the log it appends to on PATH.""" + bin_dir = root / "fake-bin" + bin_dir.mkdir() + uv = bin_dir / "uv" + uv.write_text(FAKE_UV, encoding="utf-8") + uv.chmod(uv.stat().st_mode | stat.S_IXUSR | stat.S_IXGRP | stat.S_IXOTH) + (bin_dir / "uv-calls.log").write_text("", encoding="utf-8") + return bin_dir + + +def _run_bump(root: Path, version: str, path_prefix: Path | None = None, path: str | None = None) -> subprocess.CompletedProcess[str]: + env = dict(os.environ) + if path is not None: + env["PATH"] = path + elif path_prefix is not None: + env["PATH"] = f"{path_prefix}{os.pathsep}{env.get('PATH', '')}" + return subprocess.run( + [SCRIPT_BASH, str(root / "scripts" / "bump_version.sh"), version], + cwd=root, + env=env, + capture_output=True, + text=True, + check=False, + ) + + +def _lock_version(root: Path) -> str: + lock = (root / "backend" / "uv.lock").read_text(encoding="utf-8") + return lock.split('version = "', 2)[1].split('"', 1)[0] + + +def test_bump_version_refreshes_the_lockfile(tmp_path: Path) -> None: + """After the documented release step the lock records the new version.""" + root = _write_sandbox(tmp_path / "repo") + bin_dir = _install_fake_uv(root) + + result = _run_bump(root, "2.2.0", path_prefix=bin_dir) + + assert result.returncode == 0, result.stderr + assert _lock_version(root) == "2.2.0", "backend/uv.lock still pins the previous version" + assert (root / "backend" / "pyproject.toml").read_text(encoding="utf-8") == PYPROJECT.replace("2.1.0-rc0", "2.2.0") + invocations = (bin_dir / "uv-calls.log").read_text(encoding="utf-8").splitlines() + assert invocations and all(line.startswith("uv lock") for line in invocations), f"bump_version.sh never invoked uv lock: {invocations}" + + +def test_bump_version_normalizes_a_prerelease_into_the_lock(tmp_path: Path) -> None: + """A prerelease keeps its `-rc` form in the sources and PEP 440 in the lock.""" + root = _write_sandbox(tmp_path / "repo") + bin_dir = _install_fake_uv(root) + + result = _run_bump(root, "2.1.0-rc1", path_prefix=bin_dir) + + assert result.returncode == 0, result.stderr + assert (root / "backend" / "pyproject.toml").read_text(encoding="utf-8") == PYPROJECT.replace("2.1.0-rc0", "2.1.0-rc1") + assert _lock_version(root) == "2.1.0rc1", "uv normalizes the prerelease in the lock, and the script must let it" + + +def test_bump_version_refuses_to_run_without_uv(tmp_path: Path) -> None: + """Without uv the script fails before touching anything, instead of half-bumping.""" + root = _write_sandbox(tmp_path / "repo") + # Keep every PATH entry that does not provide a `uv`, so the script still + # finds awk/grep/sed/python3 and can only fail on the missing uv itself. + without_uv = [entry for entry in os.environ.get("PATH", "").split(os.pathsep) if entry and not (Path(entry) / "uv").exists()] + path = os.pathsep.join(without_uv) + assert shutil.which("uv", path=path) is None, "test setup: uv is still reachable on PATH" + assert shutil.which("awk", path=path) is not None, "test setup: awk must stay reachable" + + result = _run_bump(root, "2.2.0", path=path) + + assert result.returncode != 0, "a missing uv must be an error: the lock would stay stale" + assert "uv" in (result.stderr + result.stdout) + assert (root / "backend" / "pyproject.toml").read_text(encoding="utf-8") == PYPROJECT, "the script edited files before checking for uv" + assert _lock_version(root) == "2.1.0rc0" diff --git a/scripts/bump_version.sh b/scripts/bump_version.sh index d82fd29318..3f342d07ac 100755 --- a/scripts/bump_version.sh +++ b/scripts/bump_version.sh @@ -6,9 +6,13 @@ # # Updates: # backend/pyproject.toml (version = "...") +# backend/uv.lock (root package version, via `uv lock`) # frontend/package.json ("version": "...") # deploy/helm/deer-flow/Chart.yaml (version: + appVersion:) # +# Requires `uv` on PATH: backend/uv.lock pins the root package version too, and +# lint CI runs `uv lock --check`, so the lock has to move with the sources. +# # This does NOT edit CHANGELOG.md or create/push a git tag — keep those manual. # After running, commit and tag v to trigger the release workflows # (container.yaml + chart.yaml), which gate on scripts/verify_versions.sh. @@ -39,6 +43,17 @@ for f in "$PYPROJECT" "$PACKAGE" "$CHART"; do fi done +# backend/uv.lock records the root package's version as well (uv normalizes a +# prerelease, so `2.1.0-rc0` is stored as `2.1.0rc0`). Leaving it behind is not +# a cosmetic drift: `uv lock --check` in lint CI fails on the stale lock, and +# `uv sync --locked` refuses the tree — so the release step would produce a +# commit whose own CI is red. Refresh it here, and fail before editing anything +# when uv is missing rather than leaving a half-bumped tree behind. +if ! command -v uv >/dev/null 2>&1; then + echo "error: 'uv' is required to refresh backend/uv.lock in lockstep; install it from https://docs.astral.sh/uv/ and re-run" >&2 + exit 1 +fi + python3 - "$PYPROJECT" "$PACKAGE" "$CHART" "$VERSION" <<'PY' import re import sys @@ -79,8 +94,14 @@ with open(chart, "w") as f: f.write(new) PY +if ! (cd "$ROOT/backend" && uv lock); then + echo "error: 'uv lock' failed; backend/uv.lock still pins the previous version. Fix the resolution error and re-run, or revert the version edits above." >&2 + exit 1 +fi + echo "Bumped version to $VERSION in:" echo " backend/pyproject.toml" +echo " backend/uv.lock" echo " frontend/package.json" echo " deploy/helm/deer-flow/Chart.yaml (version + appVersion)" echo