feat:新增部署脚本 #7
@@ -0,0 +1,25 @@
|
|||||||
|
.git
|
||||||
|
.gitea
|
||||||
|
.agents
|
||||||
|
.claude
|
||||||
|
.hermes
|
||||||
|
.idea
|
||||||
|
.omc
|
||||||
|
.jbeval
|
||||||
|
.venv
|
||||||
|
**/__pycache__
|
||||||
|
**/*.py[cod]
|
||||||
|
.*cache*
|
||||||
|
.env*
|
||||||
|
*.log
|
||||||
|
.mcp.json
|
||||||
|
data
|
||||||
|
logs
|
||||||
|
docs
|
||||||
|
tests
|
||||||
|
evals
|
||||||
|
examples
|
||||||
|
results
|
||||||
|
AGENTS.md
|
||||||
|
README.md
|
||||||
|
skills-lock.json
|
||||||
@@ -0,0 +1,123 @@
|
|||||||
|
name: my-v0-server
|
||||||
|
run-name: "${{ gitea.event.head_commit.message }}"
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
|
||||||
|
concurrency:
|
||||||
|
group: deploy-my-v0-server
|
||||||
|
cancel-in-progress: true
|
||||||
|
|
||||||
|
env:
|
||||||
|
IMAGE_NAME: my-v0-server
|
||||||
|
HOST_PORT: 33020
|
||||||
|
# Docker daemon 所在宿主机的目录;不要求 runner 容器能直接读取
|
||||||
|
DEPLOY_DIR: /app/my-v0-server
|
||||||
|
DOCKER_BUILDKIT: "0"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
deploy:
|
||||||
|
runs-on:
|
||||||
|
- vps_jp
|
||||||
|
timeout-minutes: 20
|
||||||
|
steps:
|
||||||
|
- name: Prepare environment
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
STEP_START=$(date +%s)
|
||||||
|
|
||||||
|
if ! command -v docker > /dev/null 2>&1; then
|
||||||
|
apk add docker-cli
|
||||||
|
fi
|
||||||
|
if ! command -v git > /dev/null 2>&1; then
|
||||||
|
apk add git
|
||||||
|
fi
|
||||||
|
if ! command -v node > /dev/null 2>&1; then
|
||||||
|
apk add nodejs
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker --version
|
||||||
|
echo "Environment preparation took $(( $(date +%s) - STEP_START ))s"
|
||||||
|
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Build application image
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
STEP_START=$(date +%s)
|
||||||
|
SHORT_SHA=$(echo "${{ gitea.sha }}" | cut -c1-8)
|
||||||
|
docker build \
|
||||||
|
--label "git.sha=${{ gitea.sha }}" \
|
||||||
|
-t "$IMAGE_NAME:latest" \
|
||||||
|
-t "$IMAGE_NAME:$SHORT_SHA" .
|
||||||
|
docker images "$IMAGE_NAME:latest" --format "Image size: {{.Size}}"
|
||||||
|
echo "Application image step took $(( $(date +%s) - STEP_START ))s"
|
||||||
|
|
||||||
|
- name: Validate host environment
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
SHORT_SHA=$(echo "${{ gitea.sha }}" | cut -c1-8)
|
||||||
|
# --mount 从宿主机读取;文件缺失会直接失败,在此之前不会停止旧服务
|
||||||
|
docker run --rm \
|
||||||
|
--mount "type=bind,src=$DEPLOY_DIR/.env,dst=/app/.env,readonly" \
|
||||||
|
"$IMAGE_NAME:$SHORT_SHA" \
|
||||||
|
python -c "from pathlib import Path; p = Path('/app/.env'); assert p.is_file() and p.stat().st_size > 0, 'Missing or empty host .env'; from app.config import settings; print('Host environment file loaded')"
|
||||||
|
|
||||||
|
- name: Deploy container
|
||||||
|
run: |
|
||||||
|
set -eu
|
||||||
|
STEP_START=$(date +%s)
|
||||||
|
SHORT_SHA=$(echo "${{ gitea.sha }}" | cut -c1-8)
|
||||||
|
|
||||||
|
if docker ps -a --filter "name=^${IMAGE_NAME}$" -q | grep -q .; then
|
||||||
|
docker stop --timeout 30 "$IMAGE_NAME"
|
||||||
|
docker rm "$IMAGE_NAME"
|
||||||
|
fi
|
||||||
|
|
||||||
|
docker run -d \
|
||||||
|
--name "$IMAGE_NAME" \
|
||||||
|
--network host \
|
||||||
|
--mount "type=bind,src=$DEPLOY_DIR/.env,dst=/app/.env,readonly" \
|
||||||
|
-v "$DEPLOY_DIR/data:/app/data" \
|
||||||
|
-v "$DEPLOY_DIR/logs:/app/logs" \
|
||||||
|
-e UVICORN_HOST=127.0.0.1 \
|
||||||
|
-e UVICORN_PORT="$HOST_PORT" \
|
||||||
|
-e TZ=Asia/Shanghai \
|
||||||
|
--restart unless-stopped \
|
||||||
|
"$IMAGE_NAME:$SHORT_SHA"
|
||||||
|
|
||||||
|
MAX_WAIT=120
|
||||||
|
WAITED=0
|
||||||
|
HEALTH="unknown"
|
||||||
|
RUNNING_ID=""
|
||||||
|
while [ "$WAITED" -lt "$MAX_WAIT" ]; do
|
||||||
|
RUNNING_ID=$(docker ps --filter "name=^${IMAGE_NAME}$" --filter "status=running" -q)
|
||||||
|
HEALTH=$(docker inspect --format '{{if .State.Health}}{{.State.Health.Status}}{{else}}none{{end}}' "$IMAGE_NAME" 2>/dev/null || echo "unknown")
|
||||||
|
if [ -n "$RUNNING_ID" ] && [ "$HEALTH" = "healthy" ]; then
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
sleep 2
|
||||||
|
WAITED=$((WAITED + 2))
|
||||||
|
done
|
||||||
|
|
||||||
|
if [ -n "$RUNNING_ID" ] && [ "$HEALTH" = "healthy" ]; then
|
||||||
|
docker ps --filter "name=^${IMAGE_NAME}$" --format "Status: {{.Status}}"
|
||||||
|
echo "Startup wait: ${WAITED}s"
|
||||||
|
else
|
||||||
|
echo "Container not healthy after ${MAX_WAIT}s (health=$HEALTH)"
|
||||||
|
docker inspect "$IMAGE_NAME" --format 'ExitCode={{.State.ExitCode}} Error={{.State.Error}} StartedAt={{.State.StartedAt}} FinishedAt={{.State.FinishedAt}}' 2>/dev/null || true
|
||||||
|
docker logs --tail 30 "$IMAGE_NAME" || true
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "Deployment step took $(( $(date +%s) - STEP_START ))s"
|
||||||
|
|
||||||
|
- name: Clean up old images
|
||||||
|
run: docker image prune -f
|
||||||
|
|
||||||
|
- name: Show startup logs
|
||||||
|
if: always()
|
||||||
|
run: docker logs --tail 15 "$IMAGE_NAME" || true
|
||||||
+39
@@ -0,0 +1,39 @@
|
|||||||
|
FROM python:3.11-slim-bookworm AS builder
|
||||||
|
|
||||||
|
COPY --from=ghcr.io/astral-sh/uv:0.12.17 /uv /usr/local/bin/uv
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
ENV UV_PYTHON_DOWNLOADS=never
|
||||||
|
|
||||||
|
COPY pyproject.toml uv.lock ./
|
||||||
|
RUN uv sync --locked --no-dev --no-install-project
|
||||||
|
|
||||||
|
FROM python:3.11-slim-bookworm
|
||||||
|
|
||||||
|
# PyArrow、DuckDB 的 Linux wheels 需要 C++ 运行库
|
||||||
|
RUN apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends libstdc++6 \
|
||||||
|
&& rm -rf /var/lib/apt/lists/*
|
||||||
|
|
||||||
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
|
PATH="/app/.venv/bin:$PATH" \
|
||||||
|
UVICORN_HOST=0.0.0.0 \
|
||||||
|
UVICORN_PORT=8000
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
COPY --from=builder /app/.venv ./.venv
|
||||||
|
COPY app ./app
|
||||||
|
COPY mcp_servers ./mcp_servers
|
||||||
|
COPY mcp_servers.json ./
|
||||||
|
COPY skills ./skills
|
||||||
|
COPY scripts/seed_materials.py ./scripts/seed_materials.py
|
||||||
|
|
||||||
|
EXPOSE 8000
|
||||||
|
|
||||||
|
HEALTHCHECK --interval=10s --timeout=5s --start-period=30s --retries=3 \
|
||||||
|
CMD python -c "import os, urllib.request; urllib.request.urlopen('http://127.0.0.1:' + os.environ['UVICORN_PORT'] + '/api/files/limits', timeout=3).close()" || exit 1
|
||||||
|
|
||||||
|
CMD ["python", "-m", "uvicorn", "app.main:app", "--workers", "1"]
|
||||||
Reference in New Issue
Block a user