build: tighten runtime contracts and dependency reproducibility

This commit is contained in:
Hermes Agent
2026-06-18 15:50:23 +08:00
parent 2da926bb5e
commit d415d8a494
10 changed files with 183 additions and 42 deletions

View File

@@ -37,7 +37,7 @@ jobs:
uses: actions/cache@v4 uses: actions/cache@v4
with: with:
path: ~/.cache/pip path: ~/.cache/pip
key: pip-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements-dev.txt') }} key: pip-${{ runner.os }}-py${{ matrix.python-version }}-${{ hashFiles('requirements-admin.txt', 'requirements-dev.txt', 'constraints.txt') }}
restore-keys: | restore-keys: |
pip-${{ runner.os }}-py${{ matrix.python-version }}- pip-${{ runner.os }}-py${{ matrix.python-version }}-
pip-${{ runner.os }}- pip-${{ runner.os }}-
@@ -47,13 +47,13 @@ jobs:
python -m pip install --upgrade pip python -m pip install --upgrade pip
# clean env 必须同时安装测试依赖 + admin 运行依赖,避免本地环境污染掩盖导入问题 # clean env 必须同时安装测试依赖 + admin 运行依赖,避免本地环境污染掩盖导入问题
if [ -f requirements-dev.txt ]; then if [ -f requirements-dev.txt ]; then
pip install -r requirements-dev.txt pip install -c constraints.txt -r requirements-dev.txt
else else
echo "::warning::requirements-dev.txt not found, installing test deps directly" echo "::warning::requirements-dev.txt not found, installing test deps directly"
pip install pytest pytest-cov pip install -c constraints.txt pytest pytest-cov
fi fi
if [ -f requirements-admin.txt ]; then if [ -f requirements-admin.txt ]; then
pip install -r requirements-admin.txt pip install -c constraints.txt -r requirements-admin.txt
fi fi
- name: Verify Python and pytest versions - name: Verify Python and pytest versions

View File

@@ -13,13 +13,18 @@ ENV PYTHONDONTWRITEBYTECODE=1 \
WORKDIR /app WORKDIR /app
COPY requirements-admin.txt /tmp/requirements-admin.txt COPY constraints.txt requirements-admin.txt /tmp/
RUN python -m pip install --upgrade pip \ RUN python -m pip install --upgrade pip \
&& python -m pip install -r /tmp/requirements-admin.txt && python -m pip install -c /tmp/constraints.txt -r /tmp/requirements-admin.txt
COPY . /app COPY . /app
ARG GAOKAO_ADMIN_BIND=0.0.0.0
ARG GAOKAO_ADMIN_PORT=8000
ENV GAOKAO_ADMIN_BIND=${GAOKAO_ADMIN_BIND} \
GAOKAO_ADMIN_PORT=${GAOKAO_ADMIN_PORT}
EXPOSE 8000 EXPOSE 8000
CMD ["python", "-m", "admin.app", "--host", "0.0.0.0", "--port", "8000", "--log-format", "json"] CMD ["sh", "-c", "python -m admin.app --host ${GAOKAO_ADMIN_BIND} --port ${GAOKAO_ADMIN_PORT} --log-format json"]

View File

@@ -55,31 +55,32 @@ chmod +x ~/.local/bin/gaokao-checker
## 🐍 Python依赖安装 ## 🐍 Python依赖安装
### 安装必要包 ### 创建并激活项目虚拟环境
```bash ```bash
# 基础依赖 cd /home/long/project/gaokao-volunteer-system
pip3 install --user --break-system-packages \ python3 -m venv .venv
weasyprint \ source .venv/bin/activate
jinja2 \ python -m pip install --upgrade pip
markdown ```
# 可选依赖(用于开发和测试) ### 按锁定约束安装依赖
pip3 install --user --break-system-packages \
pytest \ ```bash
black \ python -m pip install -c constraints.txt \
flake8 -r requirements-admin.txt \
-r requirements-dev.txt
``` ```
### 验证安装 ### 验证安装
```bash ```bash
# 检查Python版本 # 检查Python版本
python3 --version python --version
# 检查必要包 # 检查必要包
python3 -c "import weasyprint; print('weasyprint OK')" python -c "import weasyprint; print('weasyprint OK')"
python3 -c "import jinja2; print('jinja2 OK')" python -c "import jinja2; print('jinja2 OK')"
# 检查脚本 # 检查脚本
which gaokao-checker which gaokao-checker
@@ -134,7 +135,7 @@ sudo apt-get install wkhtmltopdf
cd /home/long/project/gaokao-volunteer-system cd /home/long/project/gaokao-volunteer-system
# 运行自动化测试 # 运行自动化测试
python3 tests/test_all.py python -m pytest -q
# 预期输出:所有测试通过 # 预期输出:所有测试通过
``` ```
@@ -143,13 +144,13 @@ python3 tests/test_all.py
```bash ```bash
# 测试规范检查器 # 测试规范检查器
python3 ~/.local/bin/gaokao-checker tests/cases/hunan-578.md python ~/.local/bin/gaokao-checker tests/cases/hunan-578.md
# 测试可视化报告 # 测试可视化报告
python3 ~/.local/bin/gaokao-visual-report-v2.py python ~/.local/bin/gaokao-visual-report-v2.py
# 测试问卷 # 测试问卷
python3 ~/.local/bin/gaokao-quick-3min.py python ~/.local/bin/gaokao-quick-3min.py
``` ```
--- ---
@@ -171,20 +172,19 @@ source ~/.bashrc
which gaokao-checker which gaokao-checker
``` ```
### Q2: `pip3 install` 失败 ### Q2: 依赖安装失败
**原因**: 权限问题或系统包限制 **原因**: 未激活项目 `.venv`、系统包限制,或未使用锁定约束
**解决**: **解决**:
```bash ```bash
# 使用 --user 参数 python3 -m venv .venv
pip3 install --user weasyprint jinja2 markdown source .venv/bin/activate
python -m pip install --upgrade pip
# 或使用虚拟环境 python -m pip install -c constraints.txt \
python3 -m venv venv -r requirements-admin.txt \
source venv/bin/activate -r requirements-dev.txt
pip3 install -r requirements.txt
``` ```
### Q3: Skills 在 Hermes 中不显示 ### Q3: Skills 在 Hermes 中不显示
@@ -281,7 +281,7 @@ pip3 uninstall weasyprint jinja2 markdown
- **文档**: [TUTORIAL.md](docs/TUTORIAL.md) - **文档**: [TUTORIAL.md](docs/TUTORIAL.md)
- **开发**: [DEVELOPMENT.md](docs/DEVELOPMENT.md) - **开发**: [DEVELOPMENT.md](docs/DEVELOPMENT.md)
- **测试**: `python3 tests/test_all.py` - **测试**: `python -m pytest -q`
- **FAQ**: [FAQ.md](FAQ.md) - **FAQ**: [FAQ.md](FAQ.md)
--- ---

View File

@@ -124,12 +124,17 @@ python3 ~/.local/bin/gaokao-quick-3min.py
管理后台代码位于 `admin/`。当前已落地服务启动、JWT 登录/鉴权、Swagger/OpenAPI、T6.2 仪表盘、T6.3 用户管理,以及 T6.4 订单管理(手工录单 / 状态流转 / CSV 导出 / 退款)。当前更准确的项目标签是“运营后台 + 人工服务增强链路”,不是完整用户端 Web 自助产品。 管理后台代码位于 `admin/`。当前已落地服务启动、JWT 登录/鉴权、Swagger/OpenAPI、T6.2 仪表盘、T6.3 用户管理,以及 T6.4 订单管理(手工录单 / 状态流转 / CSV 导出 / 退款)。当前更准确的项目标签是“运营后台 + 人工服务增强链路”,不是完整用户端 Web 自助产品。
```bash ```bash
# 安装管理后台依赖(与测试依赖分离) # 先创建并激活项目虚拟环境
pip install -r requirements-admin.txt -r requirements-dev.txt python3 -m venv .venv
source .venv/bin/activate
python -m pip install --upgrade pip
# 安装管理后台依赖(统一受 constraints.txt 锁定)
pip install -c constraints.txt -r requirements-admin.txt -r requirements-dev.txt
# 启动服务 # 启动服务
export GAOKAO_JWT_SECRET="$(python3 -c 'import secrets; print(secrets.token_hex(32))')" export GAOKAO_JWT_SECRET="$(python -c 'import secrets; print(secrets.token_hex(32))')"
python3 -m admin.app --port 8000 python -m admin.app --port 8000
# 验证 Swagger / OpenAPI # 验证 Swagger / OpenAPI
curl http://127.0.0.1:8000/health curl http://127.0.0.1:8000/health
@@ -144,7 +149,8 @@ xdg-open http://127.0.0.1:8000/docs
仓库已提供 `scripts/dev-verify.sh`,用于统一执行: 仓库已提供 `scripts/dev-verify.sh`,用于统一执行:
- 创建/复用 `.venv` - 创建/复用 `.venv`
- 安装 `requirements-admin.txt` + `requirements-dev.txt` - 检查 `.venv/bin/python` `PYTHON_BIN` 是否漂移
-`constraints.txt` 安装 `requirements-admin.txt` + `requirements-dev.txt`
- 运行 `pytest` + coverage gate - 运行 `pytest` + coverage gate
- 运行 `ruff` / `mypy` - 运行 `ruff` / `mypy`

22
constraints.txt Normal file
View File

@@ -0,0 +1,22 @@
# Single source of truth for reproducible local/CI/container installs.
# Update with care; scripts/dev-verify.sh, CI, and Dockerfile all consume this file.
fastapi==0.133.1
uvicorn==0.41.0
PyJWT==2.10.1
pydantic==2.13.4
cryptography==45.0.7
Jinja2==3.1.6
weasyprint==66.0
cairocffi==1.7.1
python-multipart==0.0.32
PyYAML==6.0.3
pytest==9.1.0
pytest-benchmark==5.2.3
pytest-cov==7.1.0
pytest-timeout==2.4.0
pytest-xdist==3.8.0
httpx==0.28.1
locust==2.44.3
ruff==0.15.17
mypy==2.1.0
types-PyYAML==6.0.12.20260518

View File

@@ -31,7 +31,7 @@ services:
GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH: ${GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH:-} GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH: ${GAOKAO_PAYMENT_ALIPAY_PUBLIC_KEY_PATH:-}
GAOKAO_OPS_ALERT_LOG: ${GAOKAO_OPS_ALERT_LOG:-/var/lib/gaokao/ops-alerts.jsonl} GAOKAO_OPS_ALERT_LOG: ${GAOKAO_OPS_ALERT_LOG:-/var/lib/gaokao/ops-alerts.jsonl}
ports: ports:
- "${GAOKAO_ADMIN_BIND:-127.0.0.1}:${GAOKAO_ADMIN_PORT:-8000}:8000" - "${GAOKAO_ADMIN_BIND:-127.0.0.1}:${GAOKAO_ADMIN_PORT:-8000}:${GAOKAO_ADMIN_PORT:-8000}"
volumes: volumes:
- gaokao_admin_data:/var/lib/gaokao - gaokao_admin_data:/var/lib/gaokao
healthcheck: healthcheck:

View File

@@ -22,11 +22,31 @@ log() {
printf '[dev-verify] %s\n' "$1" printf '[dev-verify] %s\n' "$1"
} }
python_version_of() {
"$1" --version 2>&1 | tr -d '\r'
}
ensure_python_bin_matches_venv() {
if [[ ! -x "${VENV_DIR}/bin/python" ]]; then
return
fi
local venv_version
local target_version
venv_version="$(python_version_of "${VENV_DIR}/bin/python")"
target_version="$(python_version_of "${PYTHON_BIN}")"
if [[ "${venv_version}" != "${target_version}" ]]; then
log "python bin drift detected: venv=${venv_version} target=${target_version}"
log "remove ${VENV_DIR} and rerun, or point PYTHON_BIN at a matching interpreter"
return 1
fi
}
ensure_venv() { ensure_venv() {
if [[ ! -d "${VENV_DIR}" ]]; then if [[ ! -d "${VENV_DIR}" ]]; then
log "creating venv at ${VENV_DIR}" log "creating venv at ${VENV_DIR}"
"${PYTHON_BIN}" -m venv "${VENV_DIR}" "${PYTHON_BIN}" -m venv "${VENV_DIR}"
fi fi
ensure_python_bin_matches_venv
# shellcheck disable=SC1091 # shellcheck disable=SC1091
source "${VENV_DIR}/bin/activate" source "${VENV_DIR}/bin/activate"
if ! python -m pip --version >/dev/null 2>&1; then if ! python -m pip --version >/dev/null 2>&1; then
@@ -47,7 +67,7 @@ install_requirements() {
return return
fi fi
log "installing requirements" log "installing requirements"
pip install -r "${ROOT_DIR}/requirements-admin.txt" -r "${ROOT_DIR}/requirements-dev.txt" pip install -c "${ROOT_DIR}/constraints.txt" -r "${ROOT_DIR}/requirements-admin.txt" -r "${ROOT_DIR}/requirements-dev.txt"
} }
run_checks() { run_checks() {

View File

@@ -172,3 +172,45 @@ printf '%s\n' "${{PRE_EXISTING_IGNORES[@]}}"
assert ignores == [ assert ignores == [
"tests/test_t5_performance.py::test_admin_locust_10_concurrency_success_rate_above_95" "tests/test_t5_performance.py::test_admin_locust_10_concurrency_success_rate_above_95"
] ]
def test_dev_verify_detects_python_bin_drift(tmp_path: Path):
venv_dir = tmp_path / ".venv"
subprocess.run(
["python3", "-m", "venv", str(venv_dir)],
check=True,
text=True,
capture_output=True,
)
fake_python = tmp_path / "python-fake"
fake_python.write_text(
"""#!/usr/bin/env bash
set -euo pipefail
if [[ \"${1:-}\" == \"--version\" ]]; then
echo \"Python 9.9.9\"
exit 0
fi
exec /usr/bin/python3 \"$@\"
""",
encoding="utf-8",
)
fake_python.chmod(0o755)
probe = f"""
set -euo pipefail
export GAOKAO_SOURCE_ONLY=1
source {SCRIPT}
VENV_DIR={venv_dir}
PYTHON_BIN={fake_python}
ensure_venv
"""
proc = subprocess.run(
[BASH, "-lc", probe],
cwd=REPO_ROOT,
text=True,
capture_output=True,
check=False,
)
combined = proc.stdout + proc.stderr
assert proc.returncode != 0
assert "python bin drift" in combined

View File

@@ -36,3 +36,11 @@ def test_docker_compose_passes_prod_critical_gaokao_env_vars() -> None:
missing = sorted(required_keys - set(service_env)) missing = sorted(required_keys - set(service_env))
assert not missing, f"docker-compose.yml missing gaokao env pass-through: {missing}" assert not missing, f"docker-compose.yml missing gaokao env pass-through: {missing}"
def test_docker_compose_ports_follow_admin_bind_and_port() -> None:
compose = yaml.safe_load(
(PROJECT_ROOT / "docker-compose.yml").read_text(encoding="utf-8")
)
ports = compose["services"]["gaokao-admin"]["ports"]
assert ports == ["${GAOKAO_ADMIN_BIND:-127.0.0.1}:${GAOKAO_ADMIN_PORT:-8000}:${GAOKAO_ADMIN_PORT:-8000}"]

View File

@@ -0,0 +1,38 @@
from __future__ import annotations
from pathlib import Path
PROJECT_ROOT = Path(__file__).resolve().parents[1]
CI_WORKFLOW = PROJECT_ROOT / ".github" / "workflows" / "ci.yml"
DOCKERFILE = PROJECT_ROOT / "Dockerfile"
CONSTRAINTS = PROJECT_ROOT / "constraints.txt"
def test_ci_cache_key_tracks_all_runtime_requirement_inputs() -> None:
text = CI_WORKFLOW.read_text(encoding="utf-8")
assert "requirements-admin.txt" in text
assert "requirements-dev.txt" in text
assert "constraints.txt" in text
assert "hashFiles(" in text
def test_dockerfile_installs_with_constraints_and_runtime_env_contract() -> None:
text = DOCKERFILE.read_text(encoding="utf-8")
assert "constraints.txt" in text
assert "requirements-admin.txt" in text
assert 'GAOKAO_ADMIN_BIND' in text
assert 'GAOKAO_ADMIN_PORT' in text
def test_constraints_file_locks_runtime_and_dev_packages() -> None:
text = CONSTRAINTS.read_text(encoding="utf-8")
for needle in (
"fastapi==",
"uvicorn==",
"weasyprint==",
"pytest==",
"ruff==",
"mypy==",
):
assert needle in text