347 lines
15 KiB
YAML
347 lines
15 KiB
YAML
name: '🔄 刷新推理端点'
|
||
|
||
# ════════════════════════════════════════════════════════════
|
||
# AutoDL 推理机端口刷新 · refresh-autodl-endpoint
|
||
# 编号: ZY-WF-REFRESH-AUTODL-ENDPOINT
|
||
# 守护: 铸渊 · ICE-GL-ZY001
|
||
# 版权: 国作登字-2026-A-00037559
|
||
#
|
||
# 触发场景:
|
||
# AutoDL 实例每次开机 host:port 都漂移. 冰朔/Awen 在 AutoDL 开
|
||
# 机后跑 setup-inference.sh, 然后来这里填新的 host:port + 误触
|
||
# 锁口令「刷新推理端点」, 工作流自动:
|
||
# 1. check-secrets 校验
|
||
# 2. curl /v1/health 探活 (不通就不写, 避免污染 2C2G)
|
||
# 3. SSH 到 ZY-SVR-CN01 写 inference-endpoint.json
|
||
# 4. pm2 reload portal (热加载)
|
||
# 5. 中文回执
|
||
#
|
||
# 误触锁: confirm_phrase = 「刷新推理端点」
|
||
# 因果链: cc-003 (动态适配) + cc-004 (中文一次性)
|
||
# ════════════════════════════════════════════════════════════
|
||
|
||
on:
|
||
workflow_dispatch:
|
||
inputs:
|
||
autodl_host:
|
||
description: 'AutoDL 推理服务公网 host (例: connect.westa.seetacloud.com 或 8.131.xx.xx)'
|
||
required: true
|
||
type: string
|
||
autodl_port:
|
||
description: 'AutoDL 推理服务公网端口 (推理服务 HTTPS 暴露端口, 不是 SSH 端口)'
|
||
required: true
|
||
type: string
|
||
use_https:
|
||
description: '协议: https (推荐) / http'
|
||
required: true
|
||
default: 'https'
|
||
type: choice
|
||
options:
|
||
- 'https'
|
||
- 'http'
|
||
confirm_phrase:
|
||
description: '误触锁 · 必须输入「刷新推理端点」才会真写, 其他值降级 dry-run'
|
||
required: false
|
||
default: ''
|
||
type: string
|
||
dry_run:
|
||
description: '只打印计划, 不真写域名机 (true/false)'
|
||
required: false
|
||
default: 'false'
|
||
type: choice
|
||
options:
|
||
- 'false'
|
||
- 'true'
|
||
|
||
permissions:
|
||
contents: read
|
||
|
||
concurrency:
|
||
group: refresh-autodl-endpoint
|
||
cancel-in-progress: false
|
||
|
||
jobs:
|
||
preflight:
|
||
name: 启动前预检 · 密钥 + 误触锁 + 探活
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 5
|
||
outputs:
|
||
effective_dry_run: ${{ steps.misclick.outputs.effective_dry_run }}
|
||
health_ok: ${{ steps.health.outputs.health_ok }}
|
||
gpu_name: ${{ steps.health.outputs.gpu_name }}
|
||
gpu_mem_gb: ${{ steps.health.outputs.gpu_mem_gb }}
|
||
active_model: ${{ steps.health.outputs.active_model }}
|
||
tier: ${{ steps.health.outputs.tier }}
|
||
quant: ${{ steps.health.outputs.quant }}
|
||
endpoint_url: ${{ steps.endpoint.outputs.endpoint_url }}
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- uses: actions/setup-node@v4
|
||
with:
|
||
node-version: '20'
|
||
|
||
- name: 启动前最小密钥校验 (cn-domain-deploy)
|
||
env:
|
||
ZY_CN_SERVER_HOST: ${{ secrets.ZY_CN_SERVER_HOST }}
|
||
ZY_CN_SERVER_USER: ${{ secrets.ZY_CN_SERVER_USER }}
|
||
ZY_CN_SERVER_KEY: ${{ secrets.ZY_CN_SERVER_KEY }}
|
||
ZY_CN_SERVER_PATH: ${{ secrets.ZY_CN_SERVER_PATH }}
|
||
ZY_CN_SSH_PORT: ${{ secrets.ZY_CN_SSH_PORT }}
|
||
run: |
|
||
# 这里只校验 ZY_CN_SERVER_*, AutoDL 的 host:port 是 input 不是 secret
|
||
node scripts/preflight/check-secrets.js \
|
||
--workflow cn-domain-deploy \
|
||
--stage update \
|
||
--target main
|
||
|
||
- name: 误触锁 · 必须输入「刷新推理端点」
|
||
id: misclick
|
||
env:
|
||
PHRASE: ${{ inputs.confirm_phrase }}
|
||
DRY: ${{ inputs.dry_run }}
|
||
run: |
|
||
set -e
|
||
EFFECTIVE_DRY="$DRY"
|
||
REQUIRED="刷新推理端点"
|
||
if [ "$PHRASE" != "$REQUIRED" ]; then
|
||
echo "═══════════════════════════════════════════════"
|
||
echo " ⚠️ 误触保护"
|
||
echo " 要真写 2C2G 域名机, 必须在 confirm_phrase"
|
||
echo " 字段精确输入: 「$REQUIRED」"
|
||
echo " 当前值=「$PHRASE」 → 自动切到 dry-run."
|
||
echo "═══════════════════════════════════════════════"
|
||
EFFECTIVE_DRY="true"
|
||
else
|
||
echo "✅ 已显式确认 (phrase=「$PHRASE」), 即将真写"
|
||
fi
|
||
echo "effective_dry_run=$EFFECTIVE_DRY" >> "$GITHUB_OUTPUT"
|
||
|
||
- name: 组装端点 URL
|
||
id: endpoint
|
||
env:
|
||
HOST: ${{ inputs.autodl_host }}
|
||
PORT: ${{ inputs.autodl_port }}
|
||
PROTO: ${{ inputs.use_https }}
|
||
run: |
|
||
set -euo pipefail
|
||
# 简易格式校验 — 防止 Awen 误粘 ssh 命令进来
|
||
if echo "$HOST" | grep -qE '[[:space:]]'; then
|
||
echo "❌ host 含空白字符, 请只填裸 host (不要带 ssh -p ...)" >&2; exit 1
|
||
fi
|
||
if ! echo "$PORT" | grep -qE '^[0-9]+$'; then
|
||
echo "❌ port 必须是纯数字, 当前=「$PORT」" >&2; exit 1
|
||
fi
|
||
if [ "$PORT" -lt 1 ] || [ "$PORT" -gt 65535 ]; then
|
||
echo "❌ port 越界 (1..65535), 当前=$PORT" >&2; exit 1
|
||
fi
|
||
URL="${PROTO}://${HOST}:${PORT}"
|
||
echo "endpoint_url=$URL" >> "$GITHUB_OUTPUT"
|
||
echo "✅ 目标端点: $URL"
|
||
|
||
- name: 探活 /v1/health (不通就不往下走)
|
||
id: health
|
||
env:
|
||
URL: ${{ steps.endpoint.outputs.endpoint_url }}
|
||
run: |
|
||
set -uo pipefail
|
||
echo "[health] curl $URL/v1/health (重试 3 次, 每次 10s 超时)"
|
||
OK="false"
|
||
BODY=""
|
||
for i in 1 2 3; do
|
||
BODY="$(curl -fsS -m 10 "$URL/v1/health" 2>/dev/null || true)"
|
||
if [ -n "$BODY" ] && echo "$BODY" | grep -q '"status"'; then
|
||
OK="true"
|
||
break
|
||
fi
|
||
echo " 第 $i 次失败, 2s 后重试..."
|
||
sleep 2
|
||
done
|
||
if [ "$OK" != "true" ]; then
|
||
echo "═══════════════════════════════════════════════════════════"
|
||
echo " ❌ 推理服务不可达: $URL/v1/health"
|
||
echo "═══════════════════════════════════════════════════════════"
|
||
echo " 常见原因:"
|
||
echo " 1. AutoDL 实例没开机 / 自定义服务端口没暴露"
|
||
echo " 2. host:port 抄错了 (注意是推理服务端口, 不是 SSH 端口)"
|
||
echo " 3. setup-inference.sh 还没跑完 (大模型加载需要 60-120s)"
|
||
echo " 4. 协议错: 实际只有 http 但填了 https (或反过来)"
|
||
echo ""
|
||
echo " 下一步: 先在 AutoDL 上 curl 本机 http://127.0.0.1:8000/v1/health"
|
||
echo " 确认服务在跑, 再来重试本工作流."
|
||
echo "═══════════════════════════════════════════════════════════"
|
||
echo "health_ok=false" >> "$GITHUB_OUTPUT"
|
||
exit 1
|
||
fi
|
||
echo "$BODY" > /tmp/health.json
|
||
GPU_NAME="$(echo "$BODY" | jq -r '.gpu.name // "unknown"')"
|
||
GPU_MEM_GB="$(echo "$BODY" | jq -r '.gpu.memory_gb // "0"')"
|
||
ACTIVE_MODEL="$(echo "$BODY" | jq -r '.model.name // "unknown"')"
|
||
TIER="$(echo "$BODY" | jq -r '.tier.size_tier // "unknown"')"
|
||
QUANT="$(echo "$BODY" | jq -r '.tier.quantization // "unknown"')"
|
||
{
|
||
echo "health_ok=true"
|
||
echo "gpu_name=$GPU_NAME"
|
||
echo "gpu_mem_gb=$GPU_MEM_GB"
|
||
echo "active_model=$ACTIVE_MODEL"
|
||
echo "tier=$TIER"
|
||
echo "quant=$QUANT"
|
||
} >> "$GITHUB_OUTPUT"
|
||
echo "✅ 推理服务在跑 · GPU=$GPU_NAME · 当前模型=$ACTIVE_MODEL · 档位=$TIER/$QUANT"
|
||
|
||
write-endpoint:
|
||
name: 写 inference-endpoint.json + 重载 portal
|
||
needs: preflight
|
||
if: needs.preflight.outputs.health_ok == 'true'
|
||
runs-on: ubuntu-latest
|
||
timeout-minutes: 10
|
||
steps:
|
||
- uses: actions/checkout@v4
|
||
|
||
- name: 装载 SSH 凭据 (写到 2C2G 域名机)
|
||
id: target
|
||
env:
|
||
HOST: ${{ secrets.ZY_CN_SERVER_HOST }}
|
||
USER: ${{ secrets.ZY_CN_SERVER_USER }}
|
||
KEY: ${{ secrets.ZY_CN_SERVER_KEY }}
|
||
PORT: ${{ secrets.ZY_CN_SSH_PORT }}
|
||
DEPLOY_PATH: ${{ secrets.ZY_CN_SERVER_PATH }}
|
||
run: |
|
||
set -euo pipefail
|
||
if [ -z "$HOST" ] || [ -z "$KEY" ] || [ -z "$USER" ]; then
|
||
echo "❌ ZY_CN_SERVER_{HOST,USER,KEY} 任一为空" >&2; exit 1
|
||
fi
|
||
PORT="${PORT:-22}"
|
||
DEPLOY_PATH="${DEPLOY_PATH:-/data/guanghulab}"
|
||
echo "user=$USER" >> "$GITHUB_OUTPUT"
|
||
echo "port=$PORT" >> "$GITHUB_OUTPUT"
|
||
echo "deploy_path=$DEPLOY_PATH" >> "$GITHUB_OUTPUT"
|
||
umask 077
|
||
mkdir -p ~/.ssh
|
||
printf '%s\n' "$KEY" > ~/.ssh/refresh_key
|
||
chmod 600 ~/.ssh/refresh_key
|
||
ssh-keyscan -p "$PORT" -H "$HOST" >> ~/.ssh/known_hosts 2>/dev/null || true
|
||
printf '%s' "$HOST" > ~/.ssh/refresh_host
|
||
chmod 600 ~/.ssh/refresh_host
|
||
|
||
- name: 组装 inference-endpoint.json
|
||
env:
|
||
URL: ${{ needs.preflight.outputs.endpoint_url }}
|
||
GPU_NAME: ${{ needs.preflight.outputs.gpu_name }}
|
||
GPU_MEM_GB: ${{ needs.preflight.outputs.gpu_mem_gb }}
|
||
ACTIVE_MODEL: ${{ needs.preflight.outputs.active_model }}
|
||
TIER: ${{ needs.preflight.outputs.tier }}
|
||
QUANT: ${{ needs.preflight.outputs.quant }}
|
||
RUN_ID: ${{ github.run_id }}
|
||
run: |
|
||
set -euo pipefail
|
||
REFRESHED_AT="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
|
||
# jq 安全组装, 避免 shell 注入
|
||
jq -n \
|
||
--arg url "$URL" \
|
||
--arg gpu_name "$GPU_NAME" \
|
||
--arg gpu_mem_gb "$GPU_MEM_GB" \
|
||
--arg active_model "$ACTIVE_MODEL" \
|
||
--arg tier "$TIER" \
|
||
--arg quant "$QUANT" \
|
||
--arg refreshed_at "$REFRESHED_AT" \
|
||
--arg run_id "$RUN_ID" \
|
||
'{
|
||
_sovereign: "TCS-0002∞ · 国作登字-2026-A-00037559",
|
||
_守护: "铸渊 · ICE-GL-ZY001",
|
||
endpoint_url: $url,
|
||
refreshed_at: $refreshed_at,
|
||
refreshed_by_run_id: $run_id,
|
||
gpu: { name: $gpu_name, memory_gb: $gpu_mem_gb },
|
||
tier: { size_tier: $tier, quantization: $quant },
|
||
active_model: $active_model
|
||
}' > inference-endpoint.json
|
||
echo "组装完成:"
|
||
cat inference-endpoint.json
|
||
|
||
- name: Dry-run · 只打印计划
|
||
if: needs.preflight.outputs.effective_dry_run == 'true'
|
||
env:
|
||
USER: ${{ steps.target.outputs.user }}
|
||
PORT: ${{ steps.target.outputs.port }}
|
||
DEPLOY_PATH: ${{ steps.target.outputs.deploy_path }}
|
||
run: |
|
||
echo "═══════════════ DRY RUN ═══════════════"
|
||
echo "would scp inference-endpoint.json → $USER@<host>:$DEPLOY_PATH/portal/data/"
|
||
echo "would ssh $USER@<host> 'sudo -u $USER mkdir -p $DEPLOY_PATH/portal/data && pm2 reload portal || true'"
|
||
echo "(real host masked)"
|
||
|
||
- name: 真跑 · scp 写入 + pm2 reload
|
||
if: needs.preflight.outputs.effective_dry_run != 'true'
|
||
env:
|
||
USER: ${{ steps.target.outputs.user }}
|
||
PORT: ${{ steps.target.outputs.port }}
|
||
DEPLOY_PATH: ${{ steps.target.outputs.deploy_path }}
|
||
run: |
|
||
set -euo pipefail
|
||
HOST="$(cat ~/.ssh/refresh_host)"
|
||
|
||
# 1. 确保目录存在 (PR-4 portal 会用这个路径; PR-4 之前也先建好)
|
||
ssh -i ~/.ssh/refresh_key -p "$PORT" \
|
||
-o StrictHostKeyChecking=accept-new \
|
||
"$USER@$HOST" \
|
||
"sudo mkdir -p $DEPLOY_PATH/portal/data && sudo chown -R $USER:$USER $DEPLOY_PATH/portal"
|
||
|
||
# 2. scp 上去
|
||
scp -i ~/.ssh/refresh_key -P "$PORT" \
|
||
-o StrictHostKeyChecking=accept-new \
|
||
inference-endpoint.json \
|
||
"$USER@$HOST:$DEPLOY_PATH/portal/data/inference-endpoint.json"
|
||
|
||
# 3. pm2 reload portal (PR-4 之前 portal 不存在, 命令失败也算成功)
|
||
ssh -i ~/.ssh/refresh_key -p "$PORT" \
|
||
-o StrictHostKeyChecking=accept-new \
|
||
"$USER@$HOST" \
|
||
"if pm2 describe portal >/dev/null 2>&1; then \
|
||
pm2 reload portal && echo '✅ pm2 reload portal 成功'; \
|
||
else \
|
||
echo 'ℹ️ portal 还没起 (PR-4 之前正常), 已写入 endpoint.json 等 portal 上线后自动读'; \
|
||
fi"
|
||
|
||
- name: 中文回执到 GitHub Actions Summary
|
||
if: always()
|
||
env:
|
||
URL: ${{ needs.preflight.outputs.endpoint_url }}
|
||
GPU_NAME: ${{ needs.preflight.outputs.gpu_name }}
|
||
GPU_MEM_GB: ${{ needs.preflight.outputs.gpu_mem_gb }}
|
||
ACTIVE_MODEL: ${{ needs.preflight.outputs.active_model }}
|
||
TIER: ${{ needs.preflight.outputs.tier }}
|
||
QUANT: ${{ needs.preflight.outputs.quant }}
|
||
DRY: ${{ needs.preflight.outputs.effective_dry_run }}
|
||
run: |
|
||
{
|
||
echo "# 🔄 推理端点刷新回执"
|
||
echo ""
|
||
echo "> 给冰朔 / 霜砚 / Awen 看的中文回执"
|
||
echo ""
|
||
if [ "$DRY" = "true" ]; then
|
||
echo "## ⚠️ DRY RUN (没真写)"
|
||
echo ""
|
||
echo "误触锁口令没填对, 工作流只打印计划, 没真写 2C2G 域名机."
|
||
echo ""
|
||
else
|
||
echo "## ✅ 已切到新端点"
|
||
echo ""
|
||
fi
|
||
echo "| 项 | 值 |"
|
||
echo "|---|---|"
|
||
echo "| 推理端点 | \`$URL\` |"
|
||
echo "| GPU | $GPU_NAME ($GPU_MEM_GB GB) |"
|
||
echo "| 当前模型 | $ACTIVE_MODEL |"
|
||
echo "| 档位 | $TIER / $QUANT |"
|
||
echo "| 刷新时间 | $(date -u +%Y-%m-%dT%H:%M:%SZ) |"
|
||
echo ""
|
||
echo "下一步: 冰朔打开 \`https://guanghulab.com/portal\` 试一下, 看模型有没有响应."
|
||
echo "如果还断, 看 \`/v1/health\`: \`$URL/v1/health\`"
|
||
} >> "$GITHUB_STEP_SUMMARY"
|
||
|
||
- name: 清理 SSH 私钥
|
||
if: always()
|
||
run: |
|
||
rm -f ~/.ssh/refresh_key ~/.ssh/refresh_host
|