Compare commits
2
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
69a7d53aff | ||
|
|
e4975b7be3 |
+5
-2
@@ -11,9 +11,12 @@ dist/
|
|||||||
package/
|
package/
|
||||||
temp/
|
temp/
|
||||||
data/
|
data/
|
||||||
# db_inp/
|
db_inp/
|
||||||
inp/
|
inp/
|
||||||
# .env
|
.env
|
||||||
|
.env.*
|
||||||
|
logs/
|
||||||
|
coverage/
|
||||||
*.pyc
|
*.pyc
|
||||||
*.dump
|
*.dump
|
||||||
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
app/algorithms/health/model/my_survival_forest_model_quxi.joblib
|
||||||
|
|||||||
+13
-255
@@ -1,263 +1,21 @@
|
|||||||
name: Server CI/CD
|
name: Server CI/CD v2
|
||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
tags:
|
tags:
|
||||||
- "v*"
|
- "v*"
|
||||||
- "latest"
|
|
||||||
workflow_dispatch: {}
|
workflow_dispatch: {}
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
docker-image:
|
build-test-publish-and-deploy:
|
||||||
runs-on: ubuntu-22.04
|
uses: OrgTJWater/ci-templates/.gitea/workflows/container-cd.yml@main
|
||||||
if: startsWith(github.ref, 'refs/tags/')
|
with:
|
||||||
permissions:
|
image_name: gitea.waternetwork.cn/orgtjwater/tjwater-backend
|
||||||
contents: read
|
dockerfile: Dockerfile
|
||||||
defaults:
|
build_context: .
|
||||||
run:
|
deploy_service: backend
|
||||||
shell: bash
|
deploy_host: 192.168.1.114
|
||||||
|
secrets:
|
||||||
steps:
|
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
||||||
- name: Checkout repository
|
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
||||||
uses: https://gitea.waternetwork.cn/actions/checkout@v4
|
DEV_DEPLOY_SSH_KEY: ${{ secrets.DEV_DEPLOY_SSH_KEY }}
|
||||||
with:
|
|
||||||
fetch-depth: 1
|
|
||||||
|
|
||||||
- name: Normalize image metadata
|
|
||||||
env:
|
|
||||||
RAW_REGISTRY_HOST: ${{ vars.REGISTRY_HOST }}
|
|
||||||
RAW_REPOSITORY: ${{ github.repository }}
|
|
||||||
RAW_REF_NAME: ${{ github.ref_name }}
|
|
||||||
run: |
|
|
||||||
RAW_REGISTRY_HOST="$(printf '%s' "${RAW_REGISTRY_HOST}" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')"
|
|
||||||
|
|
||||||
if [ -z "${RAW_REGISTRY_HOST}" ]; then
|
|
||||||
echo "Missing required repository variable: REGISTRY_HOST"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
REGISTRY_HOST="${RAW_REGISTRY_HOST#http://}"
|
|
||||||
REGISTRY_HOST="${REGISTRY_HOST#https://}"
|
|
||||||
REGISTRY_HOST="${REGISTRY_HOST%/}"
|
|
||||||
|
|
||||||
if [ -z "${REGISTRY_HOST}" ]; then
|
|
||||||
echo "Repository variable REGISTRY_HOST resolves to an empty host"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
REPOSITORY_PATH="${RAW_REPOSITORY#/}"
|
|
||||||
IMAGE_OWNER="${REPOSITORY_PATH%%/*}"
|
|
||||||
IMAGE_REPOSITORY_PATH="$(printf '%s' "${IMAGE_OWNER}/tjwater-backend" | tr '[:upper:]' '[:lower:]')"
|
|
||||||
IMAGE_NAME="${REGISTRY_HOST}/${IMAGE_REPOSITORY_PATH}"
|
|
||||||
IMAGE_TAG="${RAW_REF_NAME}"
|
|
||||||
{
|
|
||||||
echo "REGISTRY_HOST=${REGISTRY_HOST}"
|
|
||||||
echo "REPOSITORY_PATH=${REPOSITORY_PATH}"
|
|
||||||
echo "IMAGE_REPOSITORY_PATH=${IMAGE_REPOSITORY_PATH}"
|
|
||||||
echo "IMAGE_NAME=${IMAGE_NAME}"
|
|
||||||
echo "IMAGE_TAG=${IMAGE_TAG}"
|
|
||||||
echo "IMAGE_REF=${IMAGE_NAME}:${IMAGE_TAG}"
|
|
||||||
} >> "$GITHUB_ENV"
|
|
||||||
|
|
||||||
- name: Login to Gitea Container Registry
|
|
||||||
env:
|
|
||||||
REGISTRY_USERNAME: ${{ secrets.REGISTRY_USERNAME }}
|
|
||||||
REGISTRY_PASSWORD: ${{ secrets.REGISTRY_PASSWORD }}
|
|
||||||
GITEA_SERVER_URL: ${{ github.server_url }}
|
|
||||||
run: |
|
|
||||||
if [ -z "${REGISTRY_HOST:-}" ]; then
|
|
||||||
echo "Missing resolved environment value: REGISTRY_HOST"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${REGISTRY_USERNAME}" ]; then
|
|
||||||
echo "Missing required repository secret: REGISTRY_USERNAME"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "${REGISTRY_PASSWORD}" ]; then
|
|
||||||
echo "Missing required repository secret: REGISTRY_PASSWORD"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Registry username: ${REGISTRY_USERNAME}"
|
|
||||||
echo "Image target: ${IMAGE_REF}"
|
|
||||||
|
|
||||||
API_SERVER_URL="${GITEA_SERVER_URL%/}"
|
|
||||||
api_user="$(curl -fsS \
|
|
||||||
-H "Authorization: token ${REGISTRY_PASSWORD}" \
|
|
||||||
"${API_SERVER_URL}/api/v1/user" \
|
|
||||||
| sed -n 's/.*"login"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' \
|
|
||||||
| head -n 1 || true)"
|
|
||||||
|
|
||||||
if [ -n "${api_user}" ]; then
|
|
||||||
echo "Registry token resolves to Gitea user: ${api_user}"
|
|
||||||
else
|
|
||||||
echo "Could not resolve Gitea user from REGISTRY_PASSWORD token; docker login may still use a password or a token without API access."
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Logging into registry host: ${REGISTRY_HOST}"
|
|
||||||
echo "${REGISTRY_PASSWORD}" | docker login "$REGISTRY_HOST" \
|
|
||||||
--username "${REGISTRY_USERNAME}" \
|
|
||||||
--password-stdin
|
|
||||||
|
|
||||||
- name: Materialize runtime env file
|
|
||||||
env:
|
|
||||||
TJWATER_SERVER_ENV: ${{ secrets.TJWATER_SERVER_ENV }}
|
|
||||||
run: |
|
|
||||||
if [ -z "${TJWATER_SERVER_ENV}" ]; then
|
|
||||||
echo "Missing required repository secret: TJWATER_SERVER_ENV"
|
|
||||||
echo "Store the backend .env file content as a multiline Gitea repository secret named TJWATER_SERVER_ENV."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
printf '%s\n' "${TJWATER_SERVER_ENV}" > .env
|
|
||||||
chmod 600 .env
|
|
||||||
|
|
||||||
required_env_keys=(
|
|
||||||
ENVIRONMENT
|
|
||||||
NETWORK_NAME
|
|
||||||
DB_NAME
|
|
||||||
DB_HOST
|
|
||||||
DB_PORT
|
|
||||||
DB_USER
|
|
||||||
DB_PASSWORD
|
|
||||||
TIMESCALEDB_DB_NAME
|
|
||||||
TIMESCALEDB_DB_HOST
|
|
||||||
TIMESCALEDB_DB_PORT
|
|
||||||
TIMESCALEDB_DB_USER
|
|
||||||
TIMESCALEDB_DB_PASSWORD
|
|
||||||
METADATA_DB_NAME
|
|
||||||
METADATA_DB_HOST
|
|
||||||
METADATA_DB_PORT
|
|
||||||
METADATA_DB_USER
|
|
||||||
METADATA_DB_PASSWORD
|
|
||||||
DATABASE_ENCRYPTION_KEY
|
|
||||||
)
|
|
||||||
|
|
||||||
missing_keys=()
|
|
||||||
for key in "${required_env_keys[@]}"; do
|
|
||||||
if ! grep -Eq "^[[:space:]]*(export[[:space:]]+)?${key}[[:space:]]*=" .env; then
|
|
||||||
missing_keys+=("$key")
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
|
|
||||||
if [ "${#missing_keys[@]}" -gt 0 ]; then
|
|
||||||
echo "TJWATER_SERVER_ENV is missing required keys: ${missing_keys[*]}"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Validate workspace
|
|
||||||
run: |
|
|
||||||
if [ ! -f ./Dockerfile ]; then
|
|
||||||
echo "Dockerfile not found in workspace. Repository checkout may have failed or produced an unexpected workspace."
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build and Push Image
|
|
||||||
run: |
|
|
||||||
if [ -z "${IMAGE_NAME:-}" ] || [ -z "${IMAGE_TAG:-}" ]; then
|
|
||||||
echo "Missing resolved image metadata: IMAGE_NAME or IMAGE_TAG"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
push_with_retry() {
|
|
||||||
image_ref="$1"
|
|
||||||
attempt=1
|
|
||||||
max_attempts=3
|
|
||||||
|
|
||||||
while [ "$attempt" -le "$max_attempts" ]; do
|
|
||||||
if docker push "$image_ref"; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ "$attempt" -eq "$max_attempts" ]; then
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Push failed for $image_ref (attempt $attempt/$max_attempts); retrying in 10s..."
|
|
||||||
attempt=$((attempt + 1))
|
|
||||||
sleep 10
|
|
||||||
done
|
|
||||||
}
|
|
||||||
|
|
||||||
if [ "${IMAGE_TAG}" = "latest" ]; then
|
|
||||||
docker build \
|
|
||||||
-f ./Dockerfile \
|
|
||||||
-t "${IMAGE_NAME}:latest" \
|
|
||||||
.
|
|
||||||
push_with_retry "${IMAGE_NAME}:latest"
|
|
||||||
else
|
|
||||||
docker build \
|
|
||||||
-f ./Dockerfile \
|
|
||||||
-t "${IMAGE_NAME}:${IMAGE_TAG}" \
|
|
||||||
-t "${IMAGE_NAME}:latest" \
|
|
||||||
.
|
|
||||||
push_with_retry "${IMAGE_NAME}:${IMAGE_TAG}"
|
|
||||||
push_with_retry "${IMAGE_NAME}:latest"
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Notify Deploy Server
|
|
||||||
run: |
|
|
||||||
post_deploy_webhook() {
|
|
||||||
label="$1"
|
|
||||||
payload="$2"
|
|
||||||
webhook_url="${{ vars.DEPLOY_WEBHOOK_URL }}"
|
|
||||||
token="${{ secrets.DEPLOY_WEBHOOK_TOKEN }}"
|
|
||||||
|
|
||||||
webhook_url=$(echo "$webhook_url" | xargs)
|
|
||||||
|
|
||||||
if [ -z "$webhook_url" ]; then
|
|
||||||
echo "Missing required repository variable: DEPLOY_WEBHOOK_URL"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
if [ -z "$token" ]; then
|
|
||||||
echo "Missing required repository secret: DEPLOY_WEBHOOK_TOKEN"
|
|
||||||
return 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$label] Calling webhook: $webhook_url"
|
|
||||||
|
|
||||||
http_code=$(curl -sS -D /tmp/deploy_headers.txt -o /tmp/deploy_response.txt -w "%{http_code}" -X POST "$webhook_url" \
|
|
||||||
-H "Content-Type: application/json" \
|
|
||||||
-H "Authorization: Bearer $token" \
|
|
||||||
-d "$payload")
|
|
||||||
|
|
||||||
echo "[$label] webhook HTTP status: ${http_code}"
|
|
||||||
if [ "$http_code" -ge 200 ] && [ "$http_code" -lt 300 ]; then
|
|
||||||
return 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "[$label] response headers:"
|
|
||||||
cat /tmp/deploy_headers.txt
|
|
||||||
echo "[$label] response body:"
|
|
||||||
cat /tmp/deploy_response.txt
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
|
|
||||||
PRIMARY_PAYLOAD="{\"image\":\"${IMAGE_REF}\",\"tag\":\"${IMAGE_TAG}\",\"repo\":\"${REPOSITORY_PATH}\"}"
|
|
||||||
FALLBACK_PAYLOAD="{\"image\":\"${IMAGE_REF}\",\"tag\":\"${IMAGE_TAG}\",\"repo\":\"${IMAGE_REPOSITORY_PATH}\"}"
|
|
||||||
|
|
||||||
echo "Deploy webhook target: ${{ vars.DEPLOY_WEBHOOK_URL }}"
|
|
||||||
echo "Deploy payload(primary): image=${IMAGE_REF}, tag=${IMAGE_TAG}, repo=${REPOSITORY_PATH}"
|
|
||||||
if post_deploy_webhook "primary" "$PRIMARY_PAYLOAD"; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Primary webhook request failed, retrying with lowercase repo path..."
|
|
||||||
echo "Deploy payload(fallback): image=${IMAGE_REF}, tag=${IMAGE_TAG}, repo=${IMAGE_REPOSITORY_PATH}"
|
|
||||||
if post_deploy_webhook "fallback" "$FALLBACK_PAYLOAD"; then
|
|
||||||
exit 0
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Deploy webhook failed after primary and fallback attempts."
|
|
||||||
exit 1
|
|
||||||
|
|
||||||
deploy-fallback-log:
|
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
needs: docker-image
|
|
||||||
if: failure()
|
|
||||||
steps:
|
|
||||||
- name: Deployment not triggered
|
|
||||||
run: echo "Image build/push failed, deployment webhook was not called."
|
|
||||||
|
|||||||
+2
-4
@@ -14,13 +14,11 @@ COPY requirements.txt .
|
|||||||
RUN pip install --no-cache-dir uv
|
RUN pip install --no-cache-dir uv
|
||||||
RUN uv pip install --system --no-cache-dir -r requirements.txt
|
RUN uv pip install --system --no-cache-dir -r requirements.txt
|
||||||
|
|
||||||
# 将代码放入子目录 'app',将数据放入子目录 'db_inp'
|
# 本地数据目录和环境变量在运行时通过 Compose 挂载或注入,
|
||||||
# 这样临时文件默认会生成在 /app 下,而代码在 /app/app 下,实现了分离
|
# 不应进入镜像构建上下文。
|
||||||
COPY app ./app
|
COPY app ./app
|
||||||
RUN python -c "from pathlib import Path; from zipfile import ZipFile; model_dir = Path('app/algorithms/health/model'); zip_path = model_dir / 'my_survival_forest_model_quxi.zip'; joblib_name = 'my_survival_forest_model_quxi.joblib'; joblib_path = model_dir / joblib_name; assert zip_path.exists(), f'Model archive not found: {zip_path}'; archive = ZipFile(zip_path); archive.extract(joblib_name, model_dir); archive.close(); assert joblib_path.exists(), f'Model file not extracted: {joblib_path}'" && \
|
RUN python -c "from pathlib import Path; from zipfile import ZipFile; model_dir = Path('app/algorithms/health/model'); zip_path = model_dir / 'my_survival_forest_model_quxi.zip'; joblib_name = 'my_survival_forest_model_quxi.joblib'; joblib_path = model_dir / joblib_name; assert zip_path.exists(), f'Model archive not found: {zip_path}'; archive = ZipFile(zip_path); archive.extract(joblib_name, model_dir); archive.close(); assert joblib_path.exists(), f'Model file not extracted: {joblib_path}'" && \
|
||||||
rm -f app/algorithms/health/model/my_survival_forest_model_quxi.zip
|
rm -f app/algorithms/health/model/my_survival_forest_model_quxi.zip
|
||||||
# COPY db_inp ./db_inp
|
|
||||||
COPY .env .
|
|
||||||
RUN mkdir -p db_inp temp data inp
|
RUN mkdir -p db_inp temp data inp
|
||||||
|
|
||||||
# 设置 PYTHONPATH 以便 uvicorn 找到 app 模块
|
# 设置 PYTHONPATH 以便 uvicorn 找到 app 模块
|
||||||
|
|||||||
Reference in New Issue
Block a user