build: tighten runtime contracts and dependency reproducibility
This commit is contained in:
8
.github/workflows/ci.yml
vendored
8
.github/workflows/ci.yml
vendored
@@ -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
|
||||||
|
|||||||
11
Dockerfile
11
Dockerfile
@@ -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"]
|
||||||
|
|||||||
56
INSTALL.md
56
INSTALL.md
@@ -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)
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|||||||
16
README.md
16
README.md
@@ -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
22
constraints.txt
Normal 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
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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}"]
|
||||||
|
|||||||
38
tests/test_runtime_contracts.py
Normal file
38
tests/test_runtime_contracts.py
Normal 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
|
||||||
Reference in New Issue
Block a user