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: | set -e echo "--- verify build artifacts exist ---" ls -la src/client/dist >/dev/null 2>&1 || (echo "ERROR: src/client/dist missing, client build failed"; ls -la src/client; exit 1) ls -la src/server/dist >/dev/null 2>&1 || (echo "ERROR: src/server/dist missing, server build failed"; ls -la src/server; exit 1) ls -la src/server/node_modules >/dev/null 2>&1 || (echo "ERROR: src/server/node_modules missing"; exit 1) echo echo "--- prepare deploy-pkg directory tree ---" rm -rf deploy-pkg mkdir -p deploy-pkg/app/client/dist \ deploy-pkg/app/server/dist \ deploy-pkg/app/server/node_modules \ deploy-pkg/app/server/src/db echo echo "--- copy files ---" 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/ cp src/server/src/db/schema.sql deploy-pkg/app/server/src/db/schema.sql echo echo "--- result tree ---" find deploy-pkg -maxdepth 5 -type d | sort echo echo "--- generate docker-compose.yml ---" 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:z - /opt/volunteer/app:/app:z EOF echo echo "compose file:" cat deploy-pkg/docker-compose.yml - 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"