mirror of
https://github.com/bytedance/deer-flow.git
synced 2026-10-26 13:16:24 +00:00
fix(release): reject a stale backend/uv.lock in the version gate (#6588)
* fix(release): reject a stale backend/uv.lock in the version gate verify_versions.sh compared only Chart.yaml, pyproject.toml and package.json, so bumping those three by hand passed the v* gate while backend/uv.lock still recorded the previous root package version. The backend image installs with uv sync --locked, so the chart and the other images published while the backend image failed to build. Run uv lock --check in backend/ as part of the gate, letting uv own the PEP 440 normalization, and fail when uv is missing. The reusable verify-versions workflow installs the uv version the backend image pins. * docs(changelog): reference #6588 * fix(release): explain an unparsed lock version and stop blaming the lock for every uv failure Print a fallback when the lock's root entry is not found, so the job log does not show an empty field, and word the uv lock --check annotation so a network error or uv crash is not reported as a stale lock.
This commit is contained in:
parent
6c4c74a504
commit
db9b0b4bf0
5
.github/workflows/chart.yaml
vendored
5
.github/workflows/chart.yaml
vendored
@ -76,8 +76,9 @@ jobs:
|
||||
|
||||
verify-versions:
|
||||
# Gate the release: every version source must match the v* tag. A forgotten
|
||||
# bump in Chart.yaml, pyproject.toml, or package.json fails here and skips
|
||||
# the publish. See scripts/verify_versions.sh.
|
||||
# bump in Chart.yaml, pyproject.toml, or package.json, or a stale
|
||||
# backend/uv.lock, fails here and skips the publish. See
|
||||
# scripts/verify_versions.sh.
|
||||
if: startsWith(github.ref, 'refs/tags/v')
|
||||
uses: ./.github/workflows/verify-versions.yml
|
||||
|
||||
|
||||
5
.github/workflows/container.yaml
vendored
5
.github/workflows/container.yaml
vendored
@ -8,8 +8,9 @@ on:
|
||||
jobs:
|
||||
verify-versions:
|
||||
# Gate the release: every version source must match the v* tag. A forgotten
|
||||
# bump in Chart.yaml, pyproject.toml, or package.json fails here and skips
|
||||
# all image builds. See scripts/verify_versions.sh.
|
||||
# bump in Chart.yaml, pyproject.toml, or package.json, or a stale
|
||||
# backend/uv.lock, fails here and skips all image builds. See
|
||||
# scripts/verify_versions.sh.
|
||||
uses: ./.github/workflows/verify-versions.yml
|
||||
|
||||
backend-container:
|
||||
|
||||
11
.github/workflows/verify-versions.yml
vendored
11
.github/workflows/verify-versions.yml
vendored
@ -6,8 +6,9 @@ name: Verify Versions
|
||||
# images + chart), not just the chart.
|
||||
#
|
||||
# Sources verified: deploy/helm/deer-flow/Chart.yaml (version + appVersion),
|
||||
# backend/pyproject.toml, frontend/package.json. Logic lives in
|
||||
# scripts/verify_versions.sh so it can also be run locally.
|
||||
# backend/pyproject.toml, frontend/package.json, and backend/uv.lock (via
|
||||
# `uv lock --check`, which the backend image's `uv sync --locked` depends on).
|
||||
# Logic lives in scripts/verify_versions.sh so it can also be run locally.
|
||||
|
||||
on:
|
||||
workflow_call:
|
||||
@ -21,6 +22,12 @@ jobs:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 #v6.0.3
|
||||
|
||||
- name: Install uv
|
||||
uses: astral-sh/setup-uv@v7
|
||||
with:
|
||||
# Must match backend/Dockerfile's UV_IMAGE tag; pinned by backend/tests/test_ci_uv_version_pin.py
|
||||
version: "0.11.1"
|
||||
|
||||
- name: Verify all version sources match tag
|
||||
run: |
|
||||
TAG_VERSION="${GITHUB_REF_NAME#v}"
|
||||
|
||||
12
CHANGELOG.md
12
CHANGELOG.md
@ -4126,6 +4126,17 @@ This release closes that milestone with **439 merged pull requests**.
|
||||
value that yields no valid ID denies every guild and logs an error. Unset,
|
||||
`null`, `[]`, or a blank string still allows all guilds; `allowed_channels`
|
||||
gains the same scalar handling. ([#6338])
|
||||
- **release:** The `v*` release gate now rejects a stale `backend/uv.lock`.
|
||||
`scripts/verify_versions.sh` compared only `Chart.yaml`, `pyproject.toml` and
|
||||
`package.json`, so bumping those three by hand passed the gate even though the
|
||||
lock still recorded the previous root package version. The backend image
|
||||
installs with `uv sync --locked`, so on a tag the chart and the frontend and
|
||||
provisioner images published while the backend image failed to build, and the
|
||||
immutable chart version meant the fix needed a new version number. The script
|
||||
now also runs `uv lock --check` in `backend/` (uv owns the PEP 440
|
||||
normalization, so `2.1.0-rc0` still matches `2.1.0rc0`) and fails when `uv` is
|
||||
missing; `verify-versions.yml` installs the uv version the backend image pins.
|
||||
([#6588])
|
||||
|
||||
### Security
|
||||
|
||||
@ -9452,3 +9463,4 @@ with **180 merged pull requests** since the first 2.0 milestone tag.
|
||||
[#6558]: https://github.com/bytedance/deer-flow/pull/6558
|
||||
[#6580]: https://github.com/bytedance/deer-flow/pull/6580
|
||||
[#6582]: https://github.com/bytedance/deer-flow/pull/6582
|
||||
[#6588]: https://github.com/bytedance/deer-flow/pull/6588
|
||||
|
||||
@ -3170,6 +3170,14 @@
|
||||
条目在警告中丢弃,而配置了却得不到任何有效 ID 的值会拒绝所有公会并记录
|
||||
错误;未设置、`null`、`[]` 或空白字符串仍允许所有公会。
|
||||
`allowed_channels` 获得同样的标量处理。([#6338])
|
||||
- **发布:** `v*` 发布门禁现在会拦下过期的 `backend/uv.lock`。此前
|
||||
`scripts/verify_versions.sh` 只比较 `Chart.yaml`、`pyproject.toml` 和
|
||||
`package.json`,手动改这三处就能通过门禁,而 lock 里记录的根包版本仍是旧版本。
|
||||
backend 镜像用 `uv sync --locked` 安装依赖,因此打标签后 chart 以及 frontend、
|
||||
provisioner 镜像都已发布,backend 镜像却构建失败;chart 版本不可覆盖,修复只能
|
||||
换一个新版本号。现在该脚本还会在 `backend/` 中运行 `uv lock --check`(PEP 440
|
||||
规范化交给 uv,`2.1.0-rc0` 仍与 `2.1.0rc0` 匹配),缺少 `uv` 时直接失败;
|
||||
`verify-versions.yml` 会安装与 backend 镜像相同的固定 uv 版本。([#6588])
|
||||
|
||||
### 安全
|
||||
|
||||
@ -7613,3 +7621,4 @@ DeerFlow 2.0 是围绕"超级智能体"框架的彻底重写,核心包含子
|
||||
[#6556]: https://github.com/bytedance/deer-flow/pull/6556
|
||||
[#6580]: https://github.com/bytedance/deer-flow/pull/6580
|
||||
[#6582]: https://github.com/bytedance/deer-flow/pull/6582
|
||||
[#6588]: https://github.com/bytedance/deer-flow/pull/6588
|
||||
|
||||
16
RELEASING.md
16
RELEASING.md
@ -42,7 +42,8 @@ distinguishes it from a release.
|
||||
```
|
||||
- `scripts/verify_versions.sh [version]` — check that all sources agree. With
|
||||
no argument it requires mutual equality; with an argument it requires every
|
||||
source to equal it. Exits non-zero on mismatch. Run it locally before tagging
|
||||
source to equal it. It also runs `uv lock --check` in `backend/`, so it needs
|
||||
`uv` on `PATH` too. Exits non-zero on mismatch. Run it locally before tagging
|
||||
to catch drift early:
|
||||
```bash
|
||||
scripts/verify_versions.sh 2.1.0
|
||||
@ -149,12 +150,15 @@ Both features stay opt-in: the provisioner ignores them until
|
||||
|
||||
Both publishing workflows call `.github/workflows/verify-versions.yml` as their
|
||||
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.
|
||||
`v`). If any of the four version sources doesn't match the tag, or
|
||||
`backend/uv.lock` is stale, 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.
|
||||
The lock records the root package version in uv's PEP 440 form, so the gate
|
||||
checks it with `uv lock --check` (the job installs the same pinned uv as the
|
||||
backend image) rather than comparing strings. Without that check, a lock left on
|
||||
the previous version would pass the gate and then fail the backend image's
|
||||
`uv sync --locked`, after the chart and the other images had already published.
|
||||
|
||||
When it fails, the job annotation names the offending file and suggests the
|
||||
fix:
|
||||
|
||||
@ -55,8 +55,9 @@ UV_LOCK = 'version = 1\n\n[[package]]\nname = "deer-flow"\nversion = "2.1.0rc0"\
|
||||
|
||||
# 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.
|
||||
# inputs that only means dropping the `-`), and `uv lock --check` (run by the
|
||||
# verify_versions.sh self-check) fails while the entry lags. 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
|
||||
@ -67,7 +68,7 @@ with log.open("a", encoding="utf-8") as handle:
|
||||
handle.write("uv " + " ".join(sys.argv[1:]) + "\\n")
|
||||
|
||||
args = sys.argv[1:]
|
||||
if args != ["lock"]:
|
||||
if args not in (["lock"], ["lock", "--check"]):
|
||||
sys.exit(f"fake uv: unsupported invocation {args}")
|
||||
|
||||
root = Path.cwd()
|
||||
@ -78,6 +79,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)
|
||||
|
||||
if args == ["lock", "--check"]:
|
||||
sys.exit(0 if recorded == normalized else "The lockfile at `uv.lock` needs to be updated, but `--check` was provided.")
|
||||
lock_path.write_text(lock_text.replace(f'version = "{recorded}"', f'version = "{normalized}"', 1), encoding="utf-8")
|
||||
"""
|
||||
|
||||
|
||||
198
backend/tests/test_verify_versions_lockfile.py
Normal file
198
backend/tests/test_verify_versions_lockfile.py
Normal file
@ -0,0 +1,198 @@
|
||||
"""Regression test: the release gate must reject a stale ``backend/uv.lock``.
|
||||
|
||||
``scripts/verify_versions.sh`` gates every ``v*`` publish (``chart.yaml`` and
|
||||
``container.yaml`` through ``verify-versions.yml``), but it compared only
|
||||
``Chart.yaml``, ``pyproject.toml`` and ``package.json``. ``backend/uv.lock``
|
||||
records the root package version too, and ``backend/Dockerfile`` installs with
|
||||
``uv sync --locked``. Bumping the three text sources by hand therefore passed
|
||||
the gate::
|
||||
|
||||
$ scripts/verify_versions.sh 2.2.0
|
||||
OK — all version sources agree on 2.2.0.
|
||||
$ cd backend && uv sync --locked --extra redis --extra postgres
|
||||
The lockfile at `uv.lock` needs to be updated, but `--locked` was provided.
|
||||
|
||||
On a tag, the chart and the frontend and provisioner images published while the
|
||||
backend image build failed, and the immutable chart version meant the fix
|
||||
needed a new version number. ``uv lock --check`` in lint CI does not run on
|
||||
tags.
|
||||
|
||||
The gate now asks uv itself (``uv lock --check``), which owns the PEP 440
|
||||
normalization (``2.1.0-rc0`` is stored as ``2.1.0rc0``), and refuses to pass
|
||||
without uv.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import os
|
||||
import shutil
|
||||
import stat
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
import yaml
|
||||
from support.shell import find_script_bash
|
||||
|
||||
REPO_ROOT = Path(__file__).resolve().parents[2]
|
||||
SCRIPT_BASH = find_script_bash()
|
||||
VERIFY_WORKFLOW = REPO_ROOT / ".github" / "workflows" / "verify-versions.yml"
|
||||
|
||||
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",
|
||||
)
|
||||
|
||||
# Fake `uv lock --check`: the lock is current when its root entry records the
|
||||
# pyproject version in uv's PEP 440 form (for these inputs, without the `-`).
|
||||
# The invocation and its working directory are logged so a test can tell
|
||||
# whether the gate asked uv at all, and from backend/. FAKE_UV_ERROR makes it
|
||||
# fail for a reason other than a stale lock (a network error, a crash).
|
||||
FAKE_UV = """#!/usr/bin/env python3
|
||||
import os
|
||||
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(os.getcwd() + " uv " + " ".join(sys.argv[1:]) + "\\n")
|
||||
|
||||
if sys.argv[1:] != ["lock", "--check"]:
|
||||
sys.exit(f"fake uv: unsupported invocation {sys.argv[1:]}")
|
||||
if os.environ.get("FAKE_UV_ERROR"):
|
||||
sys.exit(os.environ["FAKE_UV_ERROR"])
|
||||
|
||||
root = Path.cwd()
|
||||
declared = re.search(r'(?m)^version\\s*=\\s*"([^"]+)"', (root / "pyproject.toml").read_text(encoding="utf-8")).group(1)
|
||||
recorded = re.search(r'(?m)^name = "deer-flow"\\nversion = "([^"]+)"', (root / "uv.lock").read_text(encoding="utf-8"))
|
||||
if recorded is None or recorded.group(1) != declared.replace("-", ""):
|
||||
sys.exit("The lockfile at `uv.lock` needs to be updated, but `--check` was provided. To update the lockfile, run `uv lock`.")
|
||||
print("Resolved 1 package")
|
||||
"""
|
||||
|
||||
|
||||
def _write_sandbox(root: Path, *, sources: str, lock: str, lock_name: str = "deer-flow") -> Path:
|
||||
"""A repo-shaped tree with every version source at ``sources`` and the lock at ``lock``."""
|
||||
(root / "scripts").mkdir(parents=True)
|
||||
(root / "backend").mkdir()
|
||||
(root / "frontend").mkdir()
|
||||
(root / "deploy" / "helm" / "deer-flow").mkdir(parents=True)
|
||||
shutil.copy2(REPO_ROOT / "scripts" / "verify_versions.sh", root / "scripts" / "verify_versions.sh")
|
||||
|
||||
(root / "backend" / "pyproject.toml").write_text(f'[project]\nname = "deer-flow"\nversion = "{sources}"\n', encoding="utf-8")
|
||||
(root / "backend" / "uv.lock").write_text(
|
||||
f'version = 1\n\n[[package]]\nname = "{lock_name}"\nversion = "{lock}"\nsource = {{ virtual = "." }}\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
(root / "frontend" / "package.json").write_text(f'{{\n "name": "deer-flow-frontend",\n "version": "{sources}"\n}}\n', encoding="utf-8")
|
||||
(root / "deploy" / "helm" / "deer-flow" / "Chart.yaml").write_text(
|
||||
f'apiVersion: v2\nname: deer-flow\nversion: {sources}\nappVersion: "{sources}"\n',
|
||||
encoding="utf-8",
|
||||
)
|
||||
return root
|
||||
|
||||
|
||||
def _install_fake_uv(root: Path) -> 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_verify(root: Path, *args: str, path: str, env: dict[str, str] | None = None) -> subprocess.CompletedProcess[str]:
|
||||
return subprocess.run(
|
||||
[SCRIPT_BASH, str(root / "scripts" / "verify_versions.sh"), *args],
|
||||
cwd=root,
|
||||
env={**os.environ, **(env or {}), "PATH": path},
|
||||
capture_output=True,
|
||||
text=True,
|
||||
check=False,
|
||||
)
|
||||
|
||||
|
||||
def _with_fake_uv(bin_dir: Path) -> str:
|
||||
return f"{bin_dir}{os.pathsep}{os.environ.get('PATH', '')}"
|
||||
|
||||
|
||||
def _uv_calls(bin_dir: Path) -> list[str]:
|
||||
return (bin_dir / "uv-calls.log").read_text(encoding="utf-8").splitlines()
|
||||
|
||||
|
||||
@pytest.mark.parametrize(("sources", "lock"), [("2.2.0", "2.2.0"), ("2.1.0-rc1", "2.1.0rc1")], ids=["release", "prerelease"])
|
||||
def test_gate_passes_when_the_lock_matches(tmp_path: Path, sources: str, lock: str):
|
||||
root = _write_sandbox(tmp_path / "repo", sources=sources, lock=lock)
|
||||
bin_dir = _install_fake_uv(root)
|
||||
|
||||
result = _run_verify(root, sources, path=_with_fake_uv(bin_dir))
|
||||
|
||||
assert result.returncode == 0, result.stderr
|
||||
assert f"backend/uv.lock: {lock}" in result.stdout
|
||||
assert _uv_calls(bin_dir) == [f"{(root / 'backend').resolve()} uv lock --check"]
|
||||
|
||||
|
||||
@pytest.mark.parametrize("args", [("2.2.0",), ()], ids=["against-tag", "mutual"])
|
||||
def test_gate_rejects_a_lock_left_on_the_previous_version(tmp_path: Path, args: tuple[str, ...]):
|
||||
"""The three text sources were bumped by hand; the lock was not."""
|
||||
root = _write_sandbox(tmp_path / "repo", sources="2.2.0", lock="2.2.0.dev0")
|
||||
bin_dir = _install_fake_uv(root)
|
||||
|
||||
result = _run_verify(root, *args, path=_with_fake_uv(bin_dir))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "::error::'uv lock --check' failed on backend/uv.lock (output above); if the lock is stale, run scripts/bump_version.sh" in result.stderr
|
||||
assert "needs to be updated" in result.stderr, "uv's own explanation should reach the job log"
|
||||
assert "Tip: run scripts/bump_version.sh" in result.stderr
|
||||
assert "OK —" not in result.stdout
|
||||
|
||||
|
||||
def test_gate_does_not_blame_the_lock_for_an_unrelated_uv_failure(tmp_path: Path):
|
||||
"""A uv failure that is not a stale lock is still fatal, but the annotation must not claim staleness."""
|
||||
root = _write_sandbox(tmp_path / "repo", sources="2.2.0", lock="2.2.0")
|
||||
bin_dir = _install_fake_uv(root)
|
||||
|
||||
result = _run_verify(root, "2.2.0", path=_with_fake_uv(bin_dir), env={"FAKE_UV_ERROR": "error: Failed to fetch: `https://pypi.org/simple/langgraph/`"})
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "Failed to fetch" in result.stderr, "uv's own explanation should reach the job log"
|
||||
assert "::error::'uv lock --check' failed on backend/uv.lock (output above); if the lock is stale, run scripts/bump_version.sh" in result.stderr
|
||||
assert "out of date" not in result.stderr
|
||||
|
||||
|
||||
def test_gate_explains_a_lock_without_the_root_entry(tmp_path: Path):
|
||||
"""The printed lock version is for the job log; an unparsable lock must say so instead of printing nothing."""
|
||||
root = _write_sandbox(tmp_path / "repo", sources="2.2.0", lock="2.2.0", lock_name="deer-flow-renamed")
|
||||
bin_dir = _install_fake_uv(root)
|
||||
|
||||
result = _run_verify(root, "2.2.0", path=_with_fake_uv(bin_dir))
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "backend/uv.lock: (root entry not found; 'uv lock --check' is authoritative)" in result.stdout
|
||||
assert "::error::'uv lock --check' failed on backend/uv.lock (output above); if the lock is stale, run scripts/bump_version.sh" in result.stderr
|
||||
|
||||
|
||||
def test_gate_refuses_to_pass_without_uv(tmp_path: Path):
|
||||
"""Skipping the lock check when uv is absent would reopen the gap silently."""
|
||||
root = _write_sandbox(tmp_path / "repo", sources="2.2.0", lock="2.2.0")
|
||||
# Keep every PATH entry that does not provide a `uv`, so the script still
|
||||
# finds awk/grep and can only fail on the missing uv itself.
|
||||
without_uv = os.pathsep.join(entry for entry in os.environ.get("PATH", "").split(os.pathsep) if entry and not (Path(entry) / "uv").exists())
|
||||
assert shutil.which("uv", path=without_uv) is None, "test setup: uv is still reachable on PATH"
|
||||
assert shutil.which("awk", path=without_uv) is not None, "test setup: awk must stay reachable"
|
||||
|
||||
result = _run_verify(root, "2.2.0", path=without_uv)
|
||||
|
||||
assert result.returncode == 1
|
||||
assert "::error::uv is required to check backend/uv.lock" in result.stderr
|
||||
|
||||
|
||||
def test_release_gate_workflow_installs_uv_before_verifying():
|
||||
steps = yaml.safe_load(VERIFY_WORKFLOW.read_text(encoding="utf-8"))["jobs"]["verify-versions"]["steps"]
|
||||
setup_uv = [index for index, step in enumerate(steps) if str(step.get("uses", "")).startswith("astral-sh/setup-uv@")]
|
||||
verify = [index for index, step in enumerate(steps) if "scripts/verify_versions.sh" in str(step.get("run", ""))]
|
||||
|
||||
assert setup_uv and verify, f"verify-versions.yml must install uv and run verify_versions.sh: {steps}"
|
||||
assert setup_uv[0] < verify[0], "uv must be installed before the gate runs its lock check"
|
||||
@ -158,9 +158,9 @@ These span both layers and require reading multiple files to understand:
|
||||
- **Long-running MCP** — a durable `McpTaskService` (leased rows, DB as source of truth)
|
||||
keeps remote task IDs/polling out of the agent loop.
|
||||
- **Version sources** — a release version must match in `backend/pyproject.toml`,
|
||||
`frontend/package.json`, and `deploy/helm/deer-flow/Chart.yaml` (`version` + `appVersion`);
|
||||
pushing a `v*` tag triggers CI that runs `scripts/verify_versions.sh` and blocks all
|
||||
publishing on drift. See [`RELEASING.md`](../RELEASING.md).
|
||||
`frontend/package.json`, and `deploy/helm/deer-flow/Chart.yaml` (`version` + `appVersion`),
|
||||
and `backend/uv.lock` must not be stale; pushing a `v*` tag triggers CI that runs
|
||||
`scripts/verify_versions.sh` (including `uv lock --check`) and blocks all publishing on drift. See [`RELEASING.md`](../RELEASING.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@ -5,6 +5,7 @@
|
||||
# deploy/helm/deer-flow/Chart.yaml — version + appVersion
|
||||
# backend/pyproject.toml — version
|
||||
# frontend/package.json — version
|
||||
# backend/uv.lock — root package version, via `uv lock --check`
|
||||
#
|
||||
# Usage:
|
||||
# scripts/verify_versions.sh # all sources must be mutually equal
|
||||
@ -13,7 +14,7 @@
|
||||
# Exit status is 0 when consistent, 1 otherwise. The release workflows
|
||||
# (.github/workflows/chart.yaml and container.yaml) call this on v* tags — via
|
||||
# the reusable .github/workflows/verify-versions.yml — to gate publishing when
|
||||
# a version source was forgotten.
|
||||
# a version source was forgotten. Requires `uv` on PATH for the lock check.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
@ -22,8 +23,9 @@ ROOT="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
|
||||
CHART="$ROOT/deploy/helm/deer-flow/Chart.yaml"
|
||||
PYPROJECT="$ROOT/backend/pyproject.toml"
|
||||
PACKAGE="$ROOT/frontend/package.json"
|
||||
UV_LOCK="$ROOT/backend/uv.lock"
|
||||
|
||||
for f in "$CHART" "$PYPROJECT" "$PACKAGE"; do
|
||||
for f in "$CHART" "$PYPROJECT" "$PACKAGE" "$UV_LOCK"; do
|
||||
if [ ! -f "$f" ]; then
|
||||
echo "::error::missing version file: $f" >&2
|
||||
exit 1
|
||||
@ -34,11 +36,20 @@ CHART_VERSION=$(awk '/^version:/ {print $2; exit}' "$CHART")
|
||||
APP_VERSION=$(awk '/^appVersion:/ {gsub(/"/, ""); print $2; exit}' "$CHART")
|
||||
PY_VERSION=$(awk -F'"' '/^version[[:space:]]*=/ {print $2; exit}' "$PYPROJECT")
|
||||
JS_VERSION=$(grep -m1 '"version"' "$PACKAGE" | awk -F'"' '{print $4}')
|
||||
PY_NAME=$(awk -F'"' '/^name[[:space:]]*=/ {print $2; exit}' "$PYPROJECT")
|
||||
LOCK_VERSION=$(awk -v name="$PY_NAME" '
|
||||
/^\[\[package\]\]/ { root = 0 }
|
||||
$0 == "name = \"" name "\"" { root = 1 }
|
||||
root && /^version = / { gsub(/"/, "", $3); print $3; exit }
|
||||
' "$UV_LOCK")
|
||||
# Display only: `uv lock --check` below is what passes or fails the lock.
|
||||
[ -n "$LOCK_VERSION" ] || LOCK_VERSION="(root entry not found; 'uv lock --check' is authoritative)"
|
||||
|
||||
printf 'Chart.yaml version: %s\n' "$CHART_VERSION"
|
||||
printf 'Chart.yaml appVersion: %s\n' "$APP_VERSION"
|
||||
printf 'backend/pyproject.toml: %s\n' "$PY_VERSION"
|
||||
printf 'frontend/package.json: %s\n' "$JS_VERSION"
|
||||
printf 'backend/uv.lock: %s\n' "$LOCK_VERSION"
|
||||
|
||||
# mismatch <name> <actual> <expected>: prints a GitHub Actions annotation and
|
||||
# returns 1 when they differ, 0 when equal.
|
||||
@ -66,6 +77,20 @@ else
|
||||
mismatch "frontend/package.json" "$JS_VERSION" "$CHART_VERSION" || status=1
|
||||
fi
|
||||
|
||||
# backend/uv.lock records the root package version in uv's PEP 440 form
|
||||
# (2.1.0-rc0 is stored as 2.1.0rc0), so let uv judge whether it still matches
|
||||
# pyproject.toml instead of re-implementing the normalization. A stale lock
|
||||
# passes the checks above, then fails the backend image's `uv sync --locked`
|
||||
# after the chart and the other images are already published.
|
||||
if ! command -v uv >/dev/null 2>&1; then
|
||||
echo "::error::uv is required to check backend/uv.lock; install it from https://docs.astral.sh/uv/." >&2
|
||||
status=1
|
||||
elif ! lock_output=$(cd "$ROOT/backend" && uv lock --check 2>&1); then
|
||||
printf '%s\n' "$lock_output" >&2
|
||||
echo "::error::'uv lock --check' failed on backend/uv.lock (output above); if the lock is stale, run scripts/bump_version.sh." >&2
|
||||
status=1
|
||||
fi
|
||||
|
||||
if [ "$status" -ne 0 ]; then
|
||||
if [ -n "$EXPECTED" ]; then
|
||||
echo "Tip: run scripts/bump_version.sh $EXPECTED to align all sources." >&2
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user