@@ -0,0 +1,16 @@
|
||||
.git
|
||||
.gitea
|
||||
.idea
|
||||
.venv
|
||||
.uv-cache
|
||||
.pytest_cache
|
||||
.ruff_cache
|
||||
__pycache__
|
||||
*.py[cod]
|
||||
.env
|
||||
artifacts
|
||||
logs
|
||||
tests
|
||||
docs
|
||||
scripts
|
||||
AGENTS.md
|
||||
@@ -0,0 +1,127 @@
|
||||
name: center
|
||||
run-name: '${{ gitea.event.head_commit.message }}'
|
||||
|
||||
on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
|
||||
concurrency:
|
||||
group: deploy-center
|
||||
cancel-in-progress: true
|
||||
|
||||
env:
|
||||
IMAGE_NAME: center
|
||||
HOST_PORT: 22788
|
||||
CONTAINER_PORT: 8100
|
||||
DOCKER_BUILDKIT: '0'
|
||||
|
||||
jobs:
|
||||
deploy:
|
||||
runs-on:
|
||||
- self-hosted
|
||||
timeout-minutes: 20
|
||||
steps:
|
||||
- name: Prepare environment
|
||||
run: |
|
||||
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: Write runtime env file
|
||||
env:
|
||||
CENTER_ENV: ${{ secrets.CENTER_ENV }}
|
||||
run: |
|
||||
if [ -z "$CENTER_ENV" ]; then
|
||||
echo "Secret CENTER_ENV is empty, put the full .env content into it"
|
||||
exit 1
|
||||
fi
|
||||
umask 077
|
||||
printf '%s\n' "$CENTER_ENV" > "$RUNNER_TEMP/center.env"
|
||||
|
||||
- name: Build application image
|
||||
run: |
|
||||
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: Run database migrations
|
||||
run: |
|
||||
STEP_START=$(date +%s)
|
||||
docker run --rm \
|
||||
--env-file "$RUNNER_TEMP/center.env" \
|
||||
-e TZ=Asia/Shanghai \
|
||||
$IMAGE_NAME:latest \
|
||||
alembic upgrade head
|
||||
echo "Migration step took $(( $(date +%s) - STEP_START ))s"
|
||||
|
||||
- name: Deploy container
|
||||
run: |
|
||||
STEP_START=$(date +%s)
|
||||
|
||||
if docker ps -a --filter "name=^${IMAGE_NAME}$" -q | grep -q .; then
|
||||
docker stop --timeout 15 $IMAGE_NAME || true
|
||||
docker rm $IMAGE_NAME || true
|
||||
fi
|
||||
|
||||
docker run -d \
|
||||
--name $IMAGE_NAME \
|
||||
-p 127.0.0.1:$HOST_PORT:$CONTAINER_PORT \
|
||||
--env-file "$RUNNER_TEMP/center.env" \
|
||||
-e TZ=Asia/Shanghai \
|
||||
--restart unless-stopped \
|
||||
$IMAGE_NAME:latest
|
||||
|
||||
MAX_WAIT=90
|
||||
WAITED=0
|
||||
HEALTH="unknown"
|
||||
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 [ "$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
|
||||
if: always()
|
||||
run: |
|
||||
rm -f "$RUNNER_TEMP/center.env"
|
||||
docker image prune -f
|
||||
|
||||
- name: Show startup logs
|
||||
if: always()
|
||||
run: docker logs --tail 15 $IMAGE_NAME
|
||||
+2
-1
@@ -11,4 +11,5 @@ htmlcov/
|
||||
|
||||
tests/
|
||||
docs/
|
||||
AGENTS.md
|
||||
AGENTS.md
|
||||
logs/
|
||||
|
||||
+36
@@ -0,0 +1,36 @@
|
||||
FROM python:3.11-slim AS builder
|
||||
|
||||
ENV UV_COMPILE_BYTECODE=1 \
|
||||
UV_LINK_MODE=copy \
|
||||
UV_PROJECT_ENVIRONMENT=/app/.venv
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN pip install --no-cache-dir uv
|
||||
|
||||
COPY pyproject.toml uv.lock ./
|
||||
RUN uv sync --frozen --no-dev --no-install-project
|
||||
|
||||
FROM python:3.11-slim
|
||||
|
||||
ENV PATH="/app/.venv/bin:$PATH" \
|
||||
PYTHONUNBUFFERED=1 \
|
||||
PYTHONDONTWRITEBYTECODE=1
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
RUN useradd --system --no-create-home --uid 10001 center
|
||||
|
||||
COPY --from=builder /app/.venv /app/.venv
|
||||
COPY alembic.ini ./
|
||||
COPY migrations ./migrations
|
||||
COPY app ./app
|
||||
|
||||
USER center
|
||||
|
||||
EXPOSE 8100
|
||||
|
||||
HEALTHCHECK --interval=10s --timeout=5s --start-period=15s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://127.0.0.1:8100/health', timeout=3)" || exit 1
|
||||
|
||||
CMD ["uvicorn", "app.main:app", "--host", "0.0.0.0", "--port", "8100", "--proxy-headers", "--no-access-log"]
|
||||
+12
-1
@@ -1,11 +1,22 @@
|
||||
# 管理共享 Redis 客户端
|
||||
|
||||
from redis.asyncio import Redis
|
||||
from redis.asyncio.retry import Retry
|
||||
from redis.backoff import ExponentialBackoff
|
||||
from redis.exceptions import ConnectionError, TimeoutError
|
||||
|
||||
from app.core.config import get_settings
|
||||
|
||||
settings = get_settings()
|
||||
redis_client: Redis = Redis.from_url(settings.redis_url, decode_responses=True)
|
||||
# 远端会关闭空闲连接:先用 PING 检查再复用,坏连接自动重连重试
|
||||
redis_client: Redis = Redis.from_url(
|
||||
settings.redis_url,
|
||||
decode_responses=True,
|
||||
health_check_interval=30,
|
||||
socket_keepalive=True,
|
||||
retry=Retry(ExponentialBackoff(), 2),
|
||||
retry_on_error=[ConnectionError, TimeoutError],
|
||||
)
|
||||
|
||||
|
||||
async def get_redis() -> Redis:
|
||||
|
||||
Reference in New Issue
Block a user