fix preflight

This commit is contained in:
ykcheungab
2026-09-12 16:00:40 +08:00
parent 9537cf91a1
commit 57bef85e32
6 changed files with 151 additions and 20 deletions
+12 -4
View File
@@ -53,10 +53,18 @@ python build/run.py release preflight --all-wheel-pythons --check-pypi
python build/run.py release audit SDK\0.5.2\orisys-0.5.2-cp312-cp312-win_amd64.whl
```
`preflight` runs: repo state → version consistency → duplicate guard → pytest →
bundle precheck → per-wheel static content/metadata assertions → isolated runtime
verification (bundled assets + real frames, no camera). Reports land in
`build/release/reports/<version>/`.
`preflight` runs: repo state → version consistency → duplicate guard (fails only
when the version is already released: a local git tag, or PyPI with
`--check-pypi`) → pytest → bundle precheck → per-wheel static content/metadata
assertions → isolated runtime verification (bundled assets + real frames, no
camera). A populated `SDK/<version>/` is the expected input, so build first:
```bash
python build/run.py wheel-win
python build/run.py release preflight --all-wheel-pythons --check-pypi
```
Reports land in `build/release/reports/<version>/`.
### GUI
+5 -1
View File
@@ -34,7 +34,11 @@ def _cmd_preflight(argv: list[str]) -> int:
parser.add_argument("--skip-tests", action="store_true", help="skip pytest (recorded in the report)")
parser.add_argument("--longrun", action="store_true", help="include the slow long-run stability test")
parser.add_argument("--allow-dirty", action="store_true", help="allow a dirty working tree")
parser.add_argument("--force", action="store_true", help="allow re-staging an existing SDK/<version>")
parser.add_argument(
"--force",
action="store_true",
help="accept a version that a git tag already marks as released",
)
parser.add_argument("--check-pypi", action="store_true", help="read PyPI and fail if the version exists")
parser.add_argument(
"--all-wheel-pythons",
+27 -7
View File
@@ -209,23 +209,43 @@ class Preflight:
return Step("version-consistency", not problems, detail, data=data)
def _duplicate_guard(self) -> Step:
"""Fail only when this version has already been *released*.
A populated ``SDK/<version>/`` is the normal input to preflight (it is
what gets verified), so it is reported, not blocked. What must not happen
silently is re-releasing a version that is already out: that is detected
offline through git tags and, opt-in, against PyPI.
"""
problems: list[str] = []
lines: list[str] = []
existing = version_mod.sdk_wheels(self.repo_root, self.version)
if existing:
if self.options.force:
lines.append(
f"SDK/{self.version}/ already holds {len(existing)} wheel(s); "
"overwriting because --force was passed"
f"SDK/{self.version}/ holds {len(existing)} staged wheel(s) to verify: "
f"{[w.name for w in existing]}"
)
else:
lines.append(
f"SDK/{self.version}/ is empty; nothing is staged yet "
"(build with `python build/run.py wheel-win` / `manylinux`)"
)
tags = version_mod.git_tags(self.repo_root)
tag_hits = version_mod.released_tags_for(self.version, tags)
if tag_hits:
message = f"git tag(s) {tag_hits} already mark {self.version} as released"
if self.options.force:
lines.append(message + " (accepted via --force)")
else:
problems.append(
f"SDK/{self.version}/ already holds {len(existing)} wheel(s): "
f"{[w.name for w in existing]}. Pass --force to rebuild over them."
message + "; pass --force to re-verify or intentionally re-release"
)
else:
lines.append(f"SDK/{self.version}/ is empty (no wheel has been staged yet)")
lines.append(
f"no local git tag marks {self.version} as released "
f"({len(tags)} tag(s) checked in this repo and {version_mod.RELEASE_REPO_DIRNAME})"
)
if self.options.check_pypi:
versions, error = version_mod.pypi_released_versions()
@@ -240,7 +260,7 @@ class Preflight:
)
else:
lines.append(
f"PyPI has no {self.version}; published versions: " + (", ".join(versions) or "none")
f"PyPI has no {self.version}; published: " + (", ".join(versions) or "none")
)
else:
lines.append("PyPI existence check skipped (pass --check-pypi to enable)")
+40
View File
@@ -5,12 +5,16 @@ from __future__ import annotations
import json
import os
import re
import subprocess
import urllib.error
import urllib.request
from pathlib import Path
PYPI_JSON_URL = "https://pypi.org/pypi/orisys/json"
# Sibling repository that carries the public GitHub releases and their tags.
RELEASE_REPO_DIRNAME = "orisys-sdk-release"
# orisys-<version>-<pytag>-<abitag>-<platformtag>.whl
_WHEEL_NAME_RE = re.compile(r"^orisys-(?P<version>[^-]+)-[^-]+-[^-]+-.+\.whl$")
@@ -103,6 +107,42 @@ def _version_sort_key(name: str):
return ([int(part) for part in parts] or [0], name)
def git_tags(repo_root: Path) -> list[str]:
"""Local git tags from this repo and the sibling release repo.
Tags are the only offline evidence that a version was already released
(``v0.5.1`` in ``orisys-sdk-release`` marks a published GitHub release), so
the duplicate guard works without network access.
"""
roots = [Path(repo_root)]
sibling = Path(repo_root).parent / RELEASE_REPO_DIRNAME
if sibling.is_dir():
roots.append(sibling)
tags: set[str] = set()
for root in roots:
try:
completed = subprocess.run(
["git", "-C", str(root), "tag", "--list"],
capture_output=True,
text=True,
check=False,
)
except OSError:
continue
if completed.returncode != 0:
continue
tags.update(
line.strip() for line in (completed.stdout or "").splitlines() if line.strip()
)
return sorted(tags)
def released_tags_for(version: str, tags: list[str]) -> list[str]:
"""Tag spellings that mark ``version`` as already released."""
return sorted({version, f"v{version}"} & set(tags))
def conda_envs_root() -> Path | None:
"""Locate the conda envs directory the same way build/wheel/win.bat does."""
override = os.getenv("CONDA_ENVS_ROOT")
+17 -4
View File
@@ -53,14 +53,24 @@ python build/run.py release preflight --version 0.5.2
| 步骤 | 检查内容 |
| --- | --- |
| `repo-state` | 是 git 仓库、工作树干净、记录 HEAD(`--allow-dirty` 可豁免) |
| `repo-state` | 是 git 仓库、工作树干净、记录 HEAD(`--allow-dirty` 可豁免,用于提交前预检) |
| `version-consistency` | `src/orisys/__init__.py::__version__` == pyproject 的 dynamic attr == `SDK/<version>/` 中 wheel 文件名里的版本 |
| `duplicate-guard` | `SDK/<version>/` 已有产物则拒绝(`--force` 豁免);`--check-pypi` 时只读查询 PyPI,版本已存在则拒绝 |
| `duplicate-guard` | 该版本是否**已经发布过**:本地 git tag(`v<version>` / `<version>`,含 sibling `orisys-sdk-release` 仓库)命中即拒绝;`--check-pypi` 时再查 PyPI。`SDK/<version>/` 里已有 wheel 只作信息展示——那是 preflight 的正常输入,不是错误 |
| `tests` | `pytest -m "not slow and not hardware"`(`--longrun` 时包含 slow) |
| `bundle-precheck` | 每个 `[[tool.orisys.bundle.items]]` 至少解析出 1 个文件;finger 必需资产存在 |
| `wheel-inspection` | 逐个 wheel 做静态断言:文件名/版本/METADATA 字段/RECORD 哈希/压缩包路径/体积上限/无 pyc 与缓存/无 `orisys/**/*.py` 源码泄漏/与源码树逐文件字节一致 |
| `wheel-runtime` | 在一次性 venv 中安装该 wheel,断言内置 configs/sensors 资产可加载,并对真实视频跑 40 帧公开流水线(无需相机、无 GUI) |
正常顺序是**先构建、再 preflight**,不需要额外开关:
```powershell
python build/run.py wheel-win
python build/run.py manylinux
python build/run.py release preflight --all-wheel-pythons --check-pypi
```
提交前想先看结果(此时工作树通常还是脏的):加 `--allow-dirty --keep-going`。
产物报告:
- `build/release/reports/<version>/preflight.json` — 机器可读,含每个 wheel 的 sha256,供后续发布步骤复用
@@ -115,7 +125,7 @@ python build/run.py verify --gui-dist --product ory_finger
1. 用 `release preflight` 拿到 PASS(并保留 `preflight.json`)。
2. 把 `SDK/<version>/` 同步到 `orisys-sdk-release` 仓库的 `release/<version>/`。
3. GitHub:打 tag **`v<version>`** 并创建 Release,上传 6 个 wheel 作为附件。
- 注意历史 tag 里有一个 typo:`0.50`(与 `v0.5.2` 指向同一 commit)。
- 注意历史 tag 里有一个 typo:`0.50`(与 `v0.5.1` 指向同一 commit)。
4. PyPI:先 TestPyPI 再正式仓库。
```powershell
@@ -133,7 +143,10 @@ python -m twine upload SDK/0.5.2/*.whl
| 0.4.3 | — | 有 | 有 |
| 0.4.4 | 有 | **无** | **无** |
| 0.5.0 | 有 | **无** | **无** |
| 0.5.2 | 有 | 有 | 有(tag `v0.5.2` + typo tag `0.50`) |
| 0.5.1 | 有 | 有 | 有(tag `v0.5.1` + typo tag `0.50`) |
| 0.5.2 | 有 | **待发布** | **待发布**(`release preflight --check-pypi` 确认 PyPI 尚无 0.5.2) |
> 本表按 2026 年 0.5.2 构建时的实际状态填写。`0.5.1` 这一行曾被一次全局替换误改成 `0.5.2` 并丢失,请勿再用「全局把 0.5.1 替换成新版本号」的方式改本文件——历史事实不应该被改。
---
+49 -3
View File
@@ -4,6 +4,7 @@ from __future__ import annotations
import io
import json
import subprocess
import sys
import urllib.error
from pathlib import Path
@@ -193,25 +194,47 @@ def test_version_consistency_passes_on_match(tmp_path):
assert step.ok, step.detail
def test_duplicate_guard_blocks_existing_sdk_dir(tmp_path):
def test_duplicate_guard_allows_staged_sdk_dir(tmp_path, monkeypatch):
"""A populated SDK/<version>/ is the normal preflight input, not a hazard."""
repo = _make_repo(tmp_path, version="0.5.2")
_stage_wheel(repo, "0.5.2")
monkeypatch.setattr(version_mod, "git_tags", lambda root: [])
step = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo)._duplicate_guard()
assert step.ok, step.detail
assert "staged wheel(s) to verify" in step.detail
def test_duplicate_guard_blocks_released_tag(tmp_path, monkeypatch):
repo = _make_repo(tmp_path, version="0.5.2")
_stage_wheel(repo, "0.5.2")
monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v0.5.2", "v0.5.1"])
step = Preflight(PreflightOptions(version="0.5.2"), repo_root=repo)._duplicate_guard()
assert not step.ok
assert "already mark 0.5.2 as released" in step.detail
assert "--force" in step.detail
def test_duplicate_guard_allows_force(tmp_path):
def test_duplicate_guard_force_accepts_released_tag(tmp_path, monkeypatch):
repo = _make_repo(tmp_path, version="0.5.2")
_stage_wheel(repo, "0.5.2")
monkeypatch.setattr(version_mod, "git_tags", lambda root: ["0.5.2"])
step = Preflight(
PreflightOptions(version="0.5.2", force=True), repo_root=repo
)._duplicate_guard()
assert step.ok, step.detail
assert "accepted via --force" in step.detail
def test_duplicate_guard_reports_unreleased_version(tmp_path, monkeypatch):
repo = _make_repo(tmp_path, version="0.5.3")
monkeypatch.setattr(version_mod, "git_tags", lambda root: ["v0.5.2"])
step = Preflight(PreflightOptions(version="0.5.3"), repo_root=repo)._duplicate_guard()
assert step.ok, step.detail
assert "no local git tag marks 0.5.3" in step.detail
def test_duplicate_guard_flags_published_version(tmp_path, monkeypatch):
repo = _make_repo(tmp_path, version="0.5.2")
monkeypatch.setattr(version_mod, "git_tags", lambda root: [])
monkeypatch.setattr(version_mod, "pypi_released_versions", lambda *a, **k: (["0.5.2"], None))
step = Preflight(
PreflightOptions(version="0.5.2", check_pypi=True), repo_root=repo
@@ -220,6 +243,29 @@ def test_duplicate_guard_flags_published_version(tmp_path, monkeypatch):
assert "already published on PyPI" in step.detail
def test_released_tags_for_matches_version_spellings():
tags = ["v0.5.1", "0.5.1", "v0.4.3", "0.50"]
assert version_mod.released_tags_for("0.5.1", tags) == ["0.5.1", "v0.5.1"]
assert version_mod.released_tags_for("0.5.2", tags) == []
def test_git_tags_reads_repo_tags(tmp_path):
repo = tmp_path / "repo"
repo.mkdir()
for command in (
["git", "init", "-q"],
["git", "config", "user.email", "test@example.invalid"],
["git", "config", "user.name", "test"],
):
subprocess.run(command, cwd=repo, check=True, capture_output=True)
(repo / "f.txt").write_text("x", encoding="utf-8")
subprocess.run(["git", "add", "f.txt"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "commit", "-qm", "init"], cwd=repo, check=True, capture_output=True)
subprocess.run(["git", "tag", "v0.5.2"], cwd=repo, check=True, capture_output=True)
assert "v0.5.2" in version_mod.git_tags(repo)
def test_bundle_precheck_fails_when_pattern_resolves_nothing(tmp_path):
repo = _make_repo(tmp_path, version="0.5.2")
(repo / "pyproject.toml").write_text(