feat(deploy): 添加服务器端直接构建部署脚本 #4
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
| # ============================================================ | ||
| # GitHub Actions - 网站部署 (Website) | ||
| # ============================================================ | ||
| name: Deploy Website | ||
| on: | ||
| push: | ||
| branches: [main, develop] | ||
| paths: | ||
| - 'website/**' | ||
| - '.github/workflows/deploy-website.yml' | ||
| workflow_dispatch: | ||
| env: | ||
| NODE_VERSION: '22' | ||
| DEPLOY_DIR: /var/www/qzt/website | ||
| PM2_APP_NAME: qzt-website | ||
| jobs: | ||
| deploy: | ||
| runs-on: ubuntu-latest | ||
| steps: | ||
| - name: Checkout code | ||
| uses: actions/checkout@v4 | ||
| - name: Setup Node.js | ||
| uses: actions/setup-node@v4 | ||
| with: | ||
| node-version: ${{ env.NODE_VERSION }} | ||
| - name: Install pnpm | ||
| uses: pnpm/action-setup@v4 | ||
| with: | ||
| version: 9 | ||
| - name: Get pnpm store directory | ||
| id: pnpm-cache | ||
| shell: bash | ||
| run: | | ||
| echo "STORE_PATH=$(pnpm store path)" >> $GITHUB_OUTPUT | ||
| - name: Setup pnpm cache | ||
| uses: actions/cache@v4 | ||
| with: | ||
| path: ${{ steps.pnpm-cache.outputs.STORE_PATH }} | ||
| key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} | ||
| restore-keys: | | ||
| ${{ runner.os }}-pnpm-store- | ||
| - name: Install dependencies | ||
| run: | | ||
| cd website | ||
| pnpm install --frozen-lockfile | ||
| - name: Build Website | ||
| run: | | ||
| cd website | ||
| pnpm run build | ||
| env: | ||
| NEXT_PUBLIC_API_BASE_URL: https://admin.devlovecode.com/api | ||
| - name: Create deployment package | ||
| run: | | ||
| # 检查 standalone 目录是否存在 | ||
| if [ -d "website/.next/standalone" ]; then | ||
| tar -czf website-dist.tar.gz -C website/.next/standalone . | ||
| else | ||
| echo "警告: standalone 目录不存在,使用默认构建产物" | ||
| tar -czf website-dist.tar.gz -C website/.next . | ||
| fi | ||
| tar -czf website-public.tar.gz -C website/public . | ||
| - name: Setup SSH | ||
| run: | | ||
| mkdir -p ~/.ssh | ||
| echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_rsa | ||
| chmod 600 ~/.ssh/id_rsa | ||
| ssh-keyscan -H ${{ secrets.SERVER_HOST }} >> ~/.ssh/known_hosts | ||
| - name: Deploy to server | ||
| run: | | ||
| # 上传部署包 | ||
| scp -o StrictHostKeyChecking=no website-dist.tar.gz \ | ||
| root@${{ secrets.SERVER_HOST }}:/tmp/ | ||
| scp -o StrictHostKeyChecking=no website-public.tar.gz \ | ||
| root@${{ secrets.SERVER_HOST }}:/tmp/ | ||
| ssh -o StrictHostKeyChecking=no root@${{ secrets.SERVER_HOST }} << 'ENDSSH' | ||
| set -e | ||
| DEPLOY_DIR="${{ env.DEPLOY_DIR }}" | ||
| BACKUP_DIR="/var/www/qzt/backups" | ||
| # 创建目录 | ||
| mkdir -p $DEPLOY_DIR $BACKUP_DIR | ||
| # 备份 | ||
| if [ -d "$DEPLOY_DIR/.next" ]; then | ||
| tar -czf $BACKUP_DIR/website-backup-$(date +%Y%m%d_%H%M%S).tar.gz -C $DEPLOY_DIR . 2>/dev/null || true | ||
| echo "备份已创建" | ||
| fi | ||
| # 解压新版本 | ||
| rm -rf $DEPLOY_DIR/.next | ||
| rm -rf $DEPLOY_DIR/public | ||
| mkdir -p $DEPLOY_DIR | ||
| tar -xzf /tmp/website-dist.tar.gz -C $DEPLOY_DIR/ | ||
| tar -xzf /tmp/website-public.tar.gz -C $DEPLOY_DIR/ | ||
| rm /tmp/website-dist.tar.gz | ||
| rm /tmp/website-public.tar.gz | ||
| # 创建 server.js(如果不存在) | ||
| if [ ! -f "$DEPLOY_DIR/server.js" ]; then | ||
| cat > $DEPLOY_DIR/server.js << 'EOF' | ||
| const { createServer } = require('http') | ||
| const { parse } = require('url') | ||
| const next = require('./next/dist/bin/next') | ||
| const dev = false | ||
| const hostname = '0.0.0.0' | ||
| const port = 5180 | ||
| const app = next({ dev, hostname, port }) | ||
| const handle = app.getRequestHandler() | ||
| app.prepare().then(() => { | ||
| createServer(async (req, res) => { | ||
| try { | ||
| const parsedUrl = parse(req.url, true) | ||
| await handle(req, res, parsedUrl) | ||
| } catch (err) { | ||
| console.error('Error occurred handling', req.url, err) | ||
| res.statusCode = 500 | ||
| res.end('internal server error') | ||
| } | ||
| }) | ||
| .once('error', (err) => { | ||
| console.error(err) | ||
| process.exit(1) | ||
| }) | ||
| .listen(port, () => { | ||
| console.log(`> Ready on http://${hostname}:${port}`) | ||
| }) | ||
| }) | ||
| EOF | ||
| fi | ||
| # 重启 PM2 服务 | ||
| pm2 restart ${{ env.PM2_APP_NAME }} || \ | ||
| pm2 start $DEPLOY_DIR/server.js --name ${{ env.PM2_APP_NAME }} | ||
| # 保存 PM2 配置 | ||
| pm2 save | ||
| echo "网站部署成功!" | ||
| ENDSSH | ||
| - name: Health check | ||
| run: | | ||
| sleep 10 | ||
| curl -f https://devlovecode.com/health || exit 1 | ||
| echo "健康检查通过!" | ||