name: Deploy to Server on: push: branches: [ main ] workflow_dispatch: jobs: deploy: runs-on: - ubuntu-latest - self-hosted steps: - name: Locate node/npm and fix PATH run: | set +e echo "--- initial PATH ---" echo "$PATH" echo echo "--- find node/npm binaries (maxdepth 6, 15s timeout) ---" timeout 15 find / \( -path /proc -o -path /sys \) -prune -o -type f \( -name node -o -name npm \) -print 2>/dev/null | head -n 30 || true echo echo "--- common locations ---" for d in /usr/local/bin /usr/bin /opt/node/bin /root/.nvm/versions/node/*/bin /home/*/.nvm/versions/node/*/bin /snap/bin /opt/homebrew/bin; do [ -x "$d/node" ] && echo "FOUND node in $d: $($d/node -v 2>/dev/null)" [ -x "$d/npm" ] && echo "FOUND npm in $d: $($d/npm -v 2>/dev/null)" done echo echo "--- choose first available node/npm dir and write to $GITHUB_ENV ---" NODE_DIR="" for d in /usr/local/bin /usr/bin /opt/node/bin /opt/homebrew/bin /snap/bin; do [ -x "$d/node" ] && [ -x "$d/npm" ] && { NODE_DIR="$d"; break; } done if [ -z "$NODE_DIR" ]; then d=$(dirname "$(find / -path /proc -prune -o -type f -name node -print 2>/dev/null | head -n 1)" 2>/dev/null) [ -n "$d" ] && [ -x "$d/node" ] && [ -x "$d/npm" ] && NODE_DIR="$d" fi if [ -z "$NODE_DIR" ]; then for candidate in /root/.nvm/versions/node/*/bin /home/*/.nvm/versions/node/*/bin; do [ -d "$candidate" ] && [ -x "$candidate/node" ] && [ -x "$candidate/npm" ] && { NODE_DIR="$candidate"; break; } done fi if [ -z "$NODE_DIR" ]; then echo "FAIL: could not locate node/npm on this runner. Install node 20 on the runner host first." exit 1 fi echo "SELECTED NODE_DIR=$NODE_DIR" echo "PATH=$NODE_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" >> "$GITHUB_ENV" echo echo "--- verify ---" export "PATH=$NODE_DIR:/usr/local/sbin:/usr/local/bin:/usr/sbin:/usr/bin:/sbin:/bin:$PATH" which node && node -v which npm && npm -v which git && git --version | head -n 1 which curl && which ssh && which scp && (docker --version || docker-compose --version || true) - name: Prepare workspace working-directory: /tmp run: | rm -rf volunteer-deploy mkdir -p /tmp/volunteer-deploy - name: Checkout (git) working-directory: /tmp/volunteer-deploy run: | if [ -d src ]; then rm -rf src; fi mkdir src && cd src git clone --depth 1 -b main https://gitea.fse-media.group/Henry_Du/VolunteerManage-for-byw.git . - name: Configure npm mirror working-directory: /tmp/volunteer-deploy/src run: | echo "registry=https://registry.npmmirror.com" > ~/.npmrc npm config set registry https://registry.npmmirror.com - name: Build client (native) working-directory: /tmp/volunteer-deploy/src/client run: | npm install --no-audit --no-fund npm run build - name: Build server (native) working-directory: /tmp/volunteer-deploy/src/server run: | npm install --no-audit --no-fund npm run build - name: Install server production deps working-directory: /tmp/volunteer-deploy/src/server run: | rm -rf node_modules npm install --omit=dev --no-audit --no-fund - name: Prepare deploy package working-directory: /tmp/volunteer-deploy run: | rm -rf deploy-pkg && mkdir -p deploy-pkg/app cp -r src/client/dist deploy-pkg/app/client/dist cp -r src/server/dist deploy-pkg/app/server/dist cp -r src/server/node_modules deploy-pkg/app/server/node_modules mkdir -p deploy-pkg/app/server/src/db cp src/server/src/db/schema.sql deploy-pkg/app/server/src/db/schema.sql cat > deploy-pkg/docker-compose.yml <<'EOF' services: volunteer: image: node:20-alpine container_name: volunteer-manage restart: unless-stopped working_dir: /app/server command: ["node", "dist/index.js"] ports: - "3000:3000" environment: - NODE_ENV=production - PORT=3000 - JWT_SECRET=${JWT_SECRET:-volunteer-manage-secret-key-2026} volumes: - /opt/volunteer/data:/app/server/data - /opt/volunteer/app:/app:ro EOF - name: Prepare SSH key env: SSH_PRIVATE_KEY: ${{ secrets.SSH_PRIVATE_KEY }} run: | mkdir -p ~/.ssh && chmod 700 ~/.ssh rm -f ~/.ssh/deploy_key ~/.ssh/known_hosts printf '%s\n' "$SSH_PRIVATE_KEY" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key printf 'Host *\n StrictHostKeyChecking no\n UserKnownHostsFile /dev/null\n IdentityFile ~/.ssh/deploy_key\n' > ~/.ssh/config chmod 600 ~/.ssh/config ssh -V - name: Deploy via SSH env: SSH_HOST: ${{ secrets.SSH_HOST }} SSH_PORT: ${{ secrets.SSH_PORT || 22 }} SSH_USER: ${{ secrets.SSH_USER }} working-directory: /tmp/volunteer-deploy run: | PORT="${SSH_PORT:-22}" echo "Deploy target: $SSH_USER@$SSH_HOST:$PORT" ssh -p $PORT $SSH_USER@$SSH_HOST "mkdir -p /opt/volunteer/app /opt/volunteer/data" scp -P $PORT -r deploy-pkg/app/* $SSH_USER@$SSH_HOST:/opt/volunteer/app/ scp -P $PORT deploy-pkg/docker-compose.yml $SSH_USER@$SSH_HOST:/opt/volunteer/docker-compose.yml ssh -p $PORT $SSH_USER@$SSH_HOST "cd /opt/volunteer && (docker compose down || docker-compose down || true) && (docker compose up -d --force-recreate || docker-compose up -d --force-recreate) && sleep 5 && (docker compose ps || docker-compose ps)" - name: Health check env: SSH_HOST: ${{ secrets.SSH_HOST }} SSH_PORT: ${{ secrets.SSH_PORT || 22 }} SSH_USER: ${{ secrets.SSH_USER }} run: | PORT="${SSH_PORT:-22}" ssh -p $PORT $SSH_USER@$SSH_HOST "for i in 1 2 3 4 5 6 7 8 9 10 11 12; do if curl -fsS http://localhost:3000/u > /dev/null; then echo 'Service OK'; exit 0; fi; echo Wait...; sleep 3; done; echo '--- Service FAIL, diagnose ---'; (docker compose ps || docker-compose ps); echo '--- container logs last 30 ---'; (docker compose logs --tail 30 || docker-compose logs --tail 30); exit 1"