fix and update #26
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| # ============================================================ | |
| # THVote Result 前端独立部署流水线 | |
| # ============================================================ | |
| name: Build & Deploy Result Frontend | |
| on: | |
| push: | |
| branches: | |
| - zfq_dev_fe | |
| paths: | |
| - '.github/workflows/navigator-image.yml' | |
| - '.github/workflows/result-image.yml' | |
| - '.github/workflows/vote-image.yml' | |
| - 'docker/**' | |
| - 'packages/result/**' | |
| - 'packages/shared/**' | |
| - 'packages/result-codegen/**' | |
| workflow_dispatch: | |
| inputs: | |
| environment: | |
| description: '目标环境' | |
| required: false | |
| default: 'test' | |
| type: choice | |
| options: | |
| - test | |
| - prod | |
| env: | |
| REGISTRY: ghcr.io | |
| OWNER: patchyvideo | |
| IMAGE_NAME: thvote-result | |
| jobs: | |
| env-setup: | |
| name: 确定环境配置 | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: "${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && 'prod' || 'test' }}" | |
| outputs: | |
| env: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && 'prod' || 'test' }} | |
| backend_dir: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && (vars.PROD_SERVER_DIR || '/opt/thvote/prod') || (vars.TEST_SERVER_DIR || '/opt/thvote/test') }} | |
| frontend_dir: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && (vars.PROD_FRONTEND_SERVER_DIR || '/opt/thvote-fe/prod') || (vars.TEST_FRONTEND_SERVER_DIR || '/opt/thvote-fe/test') }} | |
| ssh_ready: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && 'true' || 'true' }} | |
| steps: | |
| - name: 输出当前 GitHub Environment | |
| run: | | |
| echo "GitHub Environment: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && 'prod' || 'test' }}" | |
| - name: 检测环境 & 输出所有变量 | |
| id: env-detect | |
| shell: bash | |
| run: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.environment }}" == "prod" ]]; then | |
| ENV="prod" | |
| GRAPHQL_URL="http://${{ secrets.PROD_SERVER_HOST }}:8000/graphql" | |
| SERVER_HOST="${{ secrets.PROD_SERVER_HOST }}" | |
| SERVER_USER="${{ secrets.PROD_SERVER_USER }}" | |
| BACKEND_DIR="${{ vars.PROD_SERVER_DIR || '/opt/thvote/prod' }}" | |
| FRONTEND_DIR="${{ vars.PROD_FRONTEND_SERVER_DIR || '/opt/thvote-fe/prod' }}" | |
| SSH_KEY="${{ secrets.PROD_SERVER_SSH_KEY }}" | |
| else | |
| ENV="test" | |
| GRAPHQL_URL="http://152.53.165.220:18000/graphql" | |
| SERVER_HOST="${{ secrets.TEST_SERVER_HOST }}" | |
| SERVER_USER="${{ secrets.TEST_SERVER_USER }}" | |
| BACKEND_DIR="${{ vars.TEST_SERVER_DIR || '/opt/thvote/test' }}" | |
| FRONTEND_DIR="${{ vars.TEST_FRONTEND_SERVER_DIR || '/opt/thvote-fe/test' }}" | |
| SSH_KEY="${{ secrets.TEST_SERVER_SSH_KEY }}" | |
| fi | |
| echo "env=$ENV" >> $GITHUB_OUTPUT | |
| echo "graphql_url=$GRAPHQL_URL" >> $GITHUB_OUTPUT | |
| echo "backend_dir=$BACKEND_DIR" >> $GITHUB_OUTPUT | |
| echo "frontend_dir=$FRONTEND_DIR" >> $GITHUB_OUTPUT | |
| echo "server_host=$SERVER_HOST" >> $GITHUB_OUTPUT | |
| echo "server_user=$SERVER_USER" >> $GITHUB_OUTPUT | |
| if [[ -n "$SERVER_HOST" && -n "$SERVER_USER" && -n "$SSH_KEY" ]]; then | |
| echo "ssh_ready=true" >> $GITHUB_OUTPUT | |
| else | |
| echo "ssh_ready=false" >> $GITHUB_OUTPUT | |
| fi | |
| backend-ready: | |
| name: 确保后端服务就绪 | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: "${{ needs.env-setup.outputs.env }}" | |
| needs: env-setup | |
| outputs: | |
| graphql_url: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && format('http://{0}:8000/graphql', secrets.PROD_SERVER_HOST) || 'http://152.53.165.220:18000/graphql' }} | |
| steps: | |
| - name: 输出 SSH 检查结果 | |
| run: | | |
| if [[ "${{ needs.env-setup.outputs.ssh_ready }}" == "true" ]]; then | |
| echo "SSH 配置完整,执行远端后端健康检查" | |
| else | |
| echo "SSH 配置缺失,跳过远端后端检查,直接使用 GRAPHQL_SCHEMA_URL=http://152.53.165.220:18000/graphql" | |
| fi | |
| - name: 确保后端服务运行 | |
| if: needs.env-setup.outputs.ssh_ready == 'true' | |
| uses: appleboy/ssh-action@v1.2.5 | |
| with: | |
| host: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && secrets.PROD_SERVER_HOST || secrets.TEST_SERVER_HOST }} | |
| username: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && secrets.PROD_SERVER_USER || secrets.TEST_SERVER_USER }} | |
| key: ${{ needs.env-setup.outputs.env == 'prod' && secrets.PROD_SERVER_SSH_KEY || secrets.TEST_SERVER_SSH_KEY }} | |
| script: | | |
| set -e | |
| BACKEND_DIR="${{ needs.env-setup.outputs.backend_dir }}" | |
| cd "$BACKEND_DIR" | |
| echo ${{ secrets.GITHUB_TOKEN }} | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin || true | |
| docker-compose pull backend || true | |
| docker-compose up -d backend | |
| - name: 等待后端健康检查 | |
| if: needs.env-setup.outputs.ssh_ready == 'true' | |
| uses: appleboy/ssh-action@v1.2.5 | |
| with: | |
| host: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && secrets.PROD_SERVER_HOST || secrets.TEST_SERVER_HOST }} | |
| username: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && secrets.PROD_SERVER_USER || secrets.TEST_SERVER_USER }} | |
| key: ${{ needs.env-setup.outputs.env == 'prod' && secrets.PROD_SERVER_SSH_KEY || secrets.TEST_SERVER_SSH_KEY }} | |
| script: | | |
| set -e | |
| BACKEND_DIR="${{ needs.env-setup.outputs.backend_dir }}" | |
| cd "$BACKEND_DIR" | |
| for i in $(seq 1 60); do | |
| if docker-compose exec -T backend curl -sf http://localhost:8000/health > /dev/null 2>&1; then | |
| echo "后端就绪" | |
| exit 0 | |
| fi | |
| echo "等待后端... ($i/60)" | |
| sleep 3 | |
| done | |
| docker-compose logs backend | tail -20 | |
| exit 1 | |
| - name: 验证 GraphQL schema 端点 | |
| if: needs.env-setup.outputs.ssh_ready == 'true' | |
| uses: appleboy/ssh-action@v1.2.5 | |
| with: | |
| host: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && secrets.PROD_SERVER_HOST || secrets.TEST_SERVER_HOST }} | |
| username: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && secrets.PROD_SERVER_USER || secrets.TEST_SERVER_USER }} | |
| key: ${{ needs.env-setup.outputs.env == 'prod' && secrets.PROD_SERVER_SSH_KEY || secrets.TEST_SERVER_SSH_KEY }} | |
| script: | | |
| set -e | |
| BACKEND_DIR="${{ needs.env-setup.outputs.backend_dir }}" | |
| cd "$BACKEND_DIR" | |
| SCHEMA_OUTPUT=$(docker-compose exec -T backend curl -sf http://localhost:8000/graphql/schema 2>&1 | head -c 200) | |
| if [ -n "$SCHEMA_OUTPUT" ]; then | |
| echo "GraphQL schema 端点正常" | |
| else | |
| SCHEMA_CHECK=$(docker-compose exec -T backend curl -sf -X POST http://localhost:8000/graphql \ | |
| -H "Content-Type: application/json" \ | |
| -d '{"query":"{ __schema { queryType { name } } }"}' 2>&1) | |
| if ! echo "$SCHEMA_CHECK" | grep -q "queryType"; then | |
| echo "警告:无法获取 GraphQL schema" | |
| fi | |
| fi | |
| build: | |
| name: 构建 Result 镜像 | |
| runs-on: ubuntu-latest | |
| environment: | |
| name: "${{ needs.env-setup.outputs.env }}" | |
| needs: [env-setup, backend-ready] | |
| permissions: | |
| contents: read | |
| packages: write | |
| steps: | |
| - name: 检出代码 | |
| uses: actions/checkout@v4 | |
| - name: 设置 pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 7.33.7 | |
| - name: 设置 Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| cache: 'pnpm' | |
| - name: 安装依赖 | |
| run: pnpm install --frozen-lockfile | |
| - name: Codegen(从后端获取 schema) | |
| env: | |
| GRAPHQL_SCHEMA_URL: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && format('http://{0}:8000/graphql', secrets.PROD_SERVER_HOST) || 'http://152.53.165.220:18000/graphql' }} | |
| run: | | |
| echo "=== Codegen: schema from $GRAPHQL_SCHEMA_URL ===" | |
| curl -sf "$GRAPHQL_SCHEMA_URL" -o /dev/null | |
| pnpm --filter @touhou-vote/result run codegen | |
| - name: 构建 Result 应用 | |
| run: pnpm --filter @touhou-vote/result run build -- --mode test | |
| - name: 登录容器仓库 | |
| uses: docker/login-action@v3 | |
| with: | |
| registry: ${{ env.REGISTRY }} | |
| username: ${{ env.OWNER }} | |
| password: ${{ secrets.GITHUB_TOKEN }} | |
| - name: 提取镜像版本 | |
| id: meta | |
| run: | | |
| if [[ "${{ github.event_name }}" == "workflow_dispatch" && "${{ github.event.inputs.environment }}" == "prod" ]]; then | |
| VERSION="prod-$(date +%Y%m%d)-$(git rev-parse --short HEAD)" | |
| elif [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then | |
| VERSION="test-$(date +%Y%m%d-%H%M%S)" | |
| else | |
| VERSION="test-$(date +%Y%m%d)-$(git rev-parse --short HEAD)" | |
| fi | |
| echo "version=$VERSION" >> $GITHUB_OUTPUT | |
| - name: 构建并推送镜像 | |
| run: | | |
| cd packages/result/dist | |
| VERSION_TAG="${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.IMAGE_NAME }}:${{ steps.meta.outputs.version }}" | |
| TEST_TAG="${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.IMAGE_NAME }}:test" | |
| PROD_TAG="${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.IMAGE_NAME }}:prod" | |
| docker build -f - -t $VERSION_TAG -t $TEST_TAG -t $PROD_TAG . << 'DOCKEREOF' | |
| FROM nginx:alpine | |
| RUN echo 'server { \ | |
| listen 8084; \ | |
| location / { \ | |
| root /usr/share/nginx/html; \ | |
| try_files $uri $uri/ /index.html; \ | |
| } \ | |
| location /res-be/ { \ | |
| proxy_pass http://thvote-backend:8000/; \ | |
| proxy_http_version 1.1; \ | |
| proxy_set_header Host $host; \ | |
| proxy_set_header X-Real-IP $remote_addr; \ | |
| proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; \ | |
| proxy_set_header X-Forwarded-Proto $scheme; \ | |
| } \ | |
| }' > /etc/nginx/conf.d/default.conf | |
| EXPOSE 8084 | |
| CMD ["nginx", "-g", "daemon off;"] | |
| COPY . /usr/share/nginx/html | |
| DOCKEREOF | |
| docker push $VERSION_TAG | |
| docker push $TEST_TAG | |
| docker push $PROD_TAG | |
| - name: 部署到目标服务器 | |
| if: needs.env-setup.outputs.ssh_ready == 'true' | |
| uses: appleboy/ssh-action@v1.2.5 | |
| with: | |
| host: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && secrets.PROD_SERVER_HOST || secrets.TEST_SERVER_HOST }} | |
| username: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && secrets.PROD_SERVER_USER || secrets.TEST_SERVER_USER }} | |
| key: ${{ needs.env-setup.outputs.env == 'prod' && secrets.PROD_SERVER_SSH_KEY || secrets.TEST_SERVER_SSH_KEY }} | |
| script: | | |
| set -e | |
| FRONTEND_DIR="${{ needs.env-setup.outputs.frontend_dir }}" | |
| mkdir -p "$FRONTEND_DIR" | |
| cd "$FRONTEND_DIR" | |
| echo ${{ secrets.GITHUB_TOKEN }} | docker login ${{ env.REGISTRY }} -u ${{ github.actor }} --password-stdin || true | |
| cat > docker-compose.yml << 'COMPOSEEOF' | |
| version: "3.8" | |
| x-frontend-common: &frontend_common | |
| restart: unless-stopped | |
| networks: | |
| - thvote-net | |
| pull_policy: always | |
| services: | |
| vote: | |
| <<: *frontend_common | |
| image: ${VOTE_IMAGE:-ghcr.io/patchyvideo/thvote-vote:test} | |
| container_name: thvote-vote | |
| ports: | |
| - "${VOTE_PORT:-8082}:8082" | |
| navigator: | |
| <<: *frontend_common | |
| image: ${NAVIGATOR_IMAGE:-ghcr.io/patchyvideo/thvote-navigator:test} | |
| container_name: thvote-navigator | |
| ports: | |
| - "${NAVIGATOR_PORT:-8083}:8083" | |
| result: | |
| <<: *frontend_common | |
| image: ${RESULT_IMAGE:-ghcr.io/patchyvideo/thvote-result:test} | |
| container_name: thvote-result | |
| ports: | |
| - "${RESULT_PORT:-8084}:8084" | |
| networks: | |
| thvote-net: | |
| name: thvote-net | |
| external: true | |
| COMPOSEEOF | |
| ENV_FILE=".env.${{ needs.env-setup.outputs.env }}" | |
| if [ ! -f "$ENV_FILE" ]; then | |
| cat > "$ENV_FILE" << 'ENVEOF' | |
| VOTE_IMAGE=ghcr.io/patchyvideo/thvote-vote:${{ needs.env-setup.outputs.env }} | |
| NAVIGATOR_IMAGE=ghcr.io/patchyvideo/thvote-navigator:${{ needs.env-setup.outputs.env }} | |
| RESULT_IMAGE=ghcr.io/patchyvideo/thvote-result:${{ needs.env-setup.outputs.env }} | |
| VOTE_PORT=8082 | |
| NAVIGATOR_PORT=8083 | |
| RESULT_PORT=8084 | |
| ENVEOF | |
| fi | |
| cp "$ENV_FILE" .env | |
| sed -i.bak "s|^RESULT_IMAGE=.*|RESULT_IMAGE=${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.IMAGE_NAME }}:${{ needs.env-setup.outputs.env }}|" .env | |
| rm -f .env.bak | |
| docker pull "${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.IMAGE_NAME }}:${{ needs.env-setup.outputs.env }}" || true | |
| docker-compose up -d --no-deps result | |
| docker image prune -f | |
| - name: 总结 | |
| run: | | |
| echo "## Result 镜像构建完成" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "| 项目 | 值 |" >> $GITHUB_STEP_SUMMARY | |
| echo "|------|----|" >> $GITHUB_STEP_SUMMARY | |
| echo "| 镜像 | ${{ env.REGISTRY }}/${{ env.OWNER }}/${{ env.IMAGE_NAME }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Tag | ${{ steps.meta.outputs.version }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| 环境 | ${{ needs.env-setup.outputs.env }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| Schema 来源 | ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.environment == 'prod' && format('http://{0}:8000/graphql', secrets.PROD_SERVER_HOST) || 'http://152.53.165.220:18000/graphql' }} |" >> $GITHUB_STEP_SUMMARY | |
| echo "| SSH 部署 | ${{ needs.env-setup.outputs.ssh_ready == 'true' && 'enabled' || 'skipped' }} |" >> $GITHUB_STEP_SUMMARY |