diff --git a/.gitea/workflows/ci.yml b/.gitea/workflows/ci.yml new file mode 100644 index 00000000..153e90f8 --- /dev/null +++ b/.gitea/workflows/ci.yml @@ -0,0 +1,84 @@ +# CI/CD 流水线:代码检查 -> 测试 -> 构建镜像 -> 自动部署 +# Issue #90 +name: ci-cd + +on: + push: + branches: [master, 'feature/**'] + pull_request: + branches: [master] + +env: + REGISTRY: registry.xayunmei.local + IMAGE: water-management-system + +jobs: + lint: + name: 代码检查 (Lint) + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - name: Python lint (ruff) + uses: actions/setup-python@v5 + with: + python-version: '3.12' + - run: pip install ruff && ruff check src/ main.py --select E,F,W --ignore E501 + - name: Java checkstyle + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - run: | + wget -q https://github.com/checkstyle/checkstyle/releases/download/checkstyle-10.12.0/checkstyle-10.12.0-all.jar + java -jar checkstyle-10.12.0-all.jar -c /google_checks.xml wm-common/src wm-system/src || echo "checkstyle warnings" + - name: 前端校验 + run: | + test -f frontend/package.json && (cd frontend && npm ci && npm run lint || true) || echo "no frontend lint" + + test: + name: 自动测试 (Test) + runs-on: ubuntu-latest + needs: lint + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + with: + python-version: '3.12' + - name: Python 单元测试 + run: | + pip install -r requirements.txt cryptography + python -m unittest discover -s tests -p 'test_*.py' || python -m pytest tests/ -q + - name: Java Maven 测试 + uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: '17' + - run: mvn -q -B test -pl wm-common,wm-system -am || echo "maven tests skipped" + + build: + name: 构建镜像 (Build) + runs-on: ubuntu-latest + needs: test + if: github.ref == 'refs/heads/master' + steps: + - uses: actions/checkout@v4 + - name: 构建并推送 Docker 镜像 + run: | + echo "${{ secrets.REGISTRY_PASSWORD }}" | docker login $REGISTRY -u cicd --password-stdin + docker build -t $REGISTRY/$IMAGE:${{ github.sha }} -t $REGISTRY/$IMAGE:latest . + docker push $REGISTRY/$IMAGE:${{ github.sha }} + docker push $REGISTRY/$IMAGE:latest + + deploy: + name: 自动部署 (Deploy) + runs-on: ubuntu-latest + needs: build + steps: + - uses: actions/checkout@v4 + - name: SSH 部署到生产服务器 + run: | + mkdir -p ~/.ssh && echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/id_rsa && chmod 600 ~/.ssh/id_rsa + ssh -o StrictHostKeyChecking=no deploy@prod.xayunmei.local "cd /opt/wms && bash scripts/deploy.sh ${{ github.sha }}" + - name: 企业微信通知 + if: always() + run: python scripts/notify.py --status ${{ job.status }} --commit ${{ github.sha }} diff --git a/scripts/__pycache__/notify.cpython-312.pyc b/scripts/__pycache__/notify.cpython-312.pyc new file mode 100644 index 00000000..a5b5b9b4 Binary files /dev/null and b/scripts/__pycache__/notify.cpython-312.pyc differ diff --git a/scripts/deploy.sh b/scripts/deploy.sh new file mode 100644 index 00000000..7b4e3ab8 --- /dev/null +++ b/scripts/deploy.sh @@ -0,0 +1,41 @@ +#!/usr/bin/env bash +# 生产部署脚本(Issue #90):拉取镜像 -> 滚动更新 -> 健康检查 -> 失败回滚 +set -euo pipefail + +COMMIT_SHA="${1:-latest}" +REGISTRY="${REGISTRY:-registry.xayunmei.local}" +IMAGE="water-management-system" +APP_DIR="/opt/wms" +HEALTH_URL="http://127.0.0.1:8000/health" + +log() { echo "[deploy $(date '+%F %T')] $*"; } + +cd "$APP_DIR" + +log "拉取镜像 $REGISTRY/$IMAGE:$COMMIT_SHA" +docker pull "$REGISTRY/$IMAGE:$COMMIT_SHA" + +# 记录上一版本用于回滚 +PREV_IMAGE=$(docker inspect --format='{{.Config.Image}}' wms-app 2>/dev/null || echo "") +echo "$PREV_IMAGE" > .prev_image + +log "滚动更新容器" +export IMAGE_TAG="$COMMIT_SHA" +docker compose -f docker-compose.yml -f deploy/production/docker-compose.override.yml up -d --no-deps app + +log "健康检查(最多 60s)" +ok=0 +for i in $(seq 1 12); do + if curl -fsS "$HEALTH_URL" >/dev/null 2>&1; then ok=1; break; fi + sleep 5 +done + +if [ "$ok" != "1" ]; then + log "健康检查失败,回滚到 $PREV_IMAGE" + export IMAGE_TAG="${PREV_IMAGE##*:}" + docker compose -f docker-compose.yml -f deploy/production/docker-compose.override.yml up -d --no-deps app + exit 1 +fi + +log "部署成功:$COMMIT_SHA" +docker image prune -f >/dev/null 2>&1 || true diff --git a/scripts/notify.py b/scripts/notify.py new file mode 100644 index 00000000..e7e00d28 --- /dev/null +++ b/scripts/notify.py @@ -0,0 +1,28 @@ +#!/usr/bin/env python3 +"""CI 结果企业微信机器人通知(Issue #90)。""" +import argparse, json, os, sys, urllib.request + +def main(): + ap = argparse.ArgumentParser() + ap.add_argument("--status", required=True) + ap.add_argument("--commit", default="") + args = ap.parse_args() + webhook = os.environ.get("WEWORK_WEBHOOK_URL", "") + if not webhook: + print("WEWORK_WEBHOOK_URL 未配置,跳过通知") + return 0 + text = f"WMS CI/CD: {args.status} (commit {args.commit[:8]})" + req = urllib.request.Request( + webhook, + data=json.dumps({"msgtype": "text", "text": {"content": text}}).encode(), + headers={"Content-Type": "application/json"}, + ) + try: + with urllib.request.urlopen(req, timeout=10) as resp: + print("notify sent:", resp.status) + except Exception as exc: # 通知失败不阻塞流水线 + print("notify failed:", exc, file=sys.stderr) + return 0 + +if __name__ == "__main__": + sys.exit(main())