name: CI/CD Web + Desktop on: push: branches: [ main ] tags: - 'v*.*.*' workflow_dispatch: inputs: force_web_deploy: description: 'Forçar deploy do Web (ignorar filtro)?' required: false default: 'false' force_convex_deploy: description: 'Forçar deploy do Convex (ignorar filtro)?' required: false default: 'false' env: APP_DIR: /srv/apps/sistema VPS_UPDATES_DIR: /var/www/updates jobs: changes: name: Detect changes runs-on: ubuntu-latest outputs: convex: ${{ steps.filter.outputs.convex }} web: ${{ steps.filter.outputs.web }} steps: - name: Checkout uses: actions/checkout@v4 - name: Paths filter id: filter uses: dorny/paths-filter@v3 with: filters: | convex: - 'convex/**' web: - 'src/**' - 'public/**' - 'prisma/**' - 'next.config.ts' - 'package.json' - 'pnpm-lock.yaml' - 'tsconfig.json' - 'middleware.ts' - 'stack.yml' deploy: name: Deploy (VPS Linux) needs: changes # Executa em qualquer push na main (independente do filtro) ou quando disparado manualmente if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' }} runs-on: [ self-hosted, linux, vps ] steps: - name: Checkout uses: actions/checkout@v4 - name: Determine APP_DIR (fallback safe path) id: appdir run: | TS=$(date +%s) FALLBACK_DIR="$HOME/apps/sistema.build.$TS" mkdir -p "$FALLBACK_DIR" echo "Using APP_DIR (fallback)=$FALLBACK_DIR" echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV" - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Permissions diagnostic (server paths) run: | set +e echo "== Basic context ==" whoami || true id || true groups || true umask || true echo "HOME=$HOME" echo "APP_DIR(default)=${APP_DIR:-/srv/apps/sistema}" echo "EFFECTIVE_APP_DIR=$EFFECTIVE_APP_DIR" echo "\n== Permissions check ==" check_path() { P="$1" echo "-- $P" if [ -e "$P" ]; then stat -c '%A %U:%G %n' "$P" 2>/dev/null || ls -ld "$P" || true echo -n "WRITABLE? "; [ -w "$P" ] && echo yes || echo no if command -v namei >/dev/null 2>&1; then namei -l "$P" || true fi TMP="$P/.permtest.$$" (echo test > "$TMP" 2>/dev/null && echo "CREATE_FILE: ok" && rm -f "$TMP") || echo "CREATE_FILE: failed" else echo "(missing)" fi } check_path "/srv/apps/sistema" check_path "/srv/apps/sistema/src/app/machines/handshake" check_path "/srv/apps/sistema/apps/desktop/node_modules" check_path "/srv/apps/sistema/node_modules" check_path "$EFFECTIVE_APP_DIR" check_path "$EFFECTIVE_APP_DIR/node_modules" - name: Sync workspace to APP_DIR (preserving local env) run: | mkdir -p "$EFFECTIVE_APP_DIR" RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete" # Excluir .env apenas quando copiando para o diretório padrão (/srv) para preservar segredos locais EXCLUDE_ENV="--exclude '.env*' --exclude 'apps/desktop/.env*' --exclude 'convex/.env*'" if [ "$EFFECTIVE_APP_DIR" != "${APP_DIR:-/srv/apps/sistema}" ]; then EXCLUDE_ENV="" fi rsync $RSYNC_FLAGS \ --filter='protect .next.old*' \ --exclude '.next.old*' \ --filter='protect node_modules' \ --filter='protect node_modules/**' \ --filter='protect .pnpm-store' \ --filter='protect .pnpm-store/**' \ --filter='protect .env' \ --filter='protect .env*' \ --filter='protect apps/desktop/.env*' \ --filter='protect convex/.env*' \ --exclude '.git' \ --exclude '.next' \ --exclude 'node_modules' \ --exclude 'node_modules/**' \ --exclude '.pnpm-store' \ --exclude '.pnpm-store/**' \ $EXCLUDE_ENV \ ./ "$EFFECTIVE_APP_DIR"/ - name: Copy production .env if present run: | DEFAULT_DIR="${APP_DIR:-/srv/apps/sistema}" if [ "$EFFECTIVE_APP_DIR" != "$DEFAULT_DIR" ] && [ -f "$DEFAULT_DIR/.env" ]; then echo "Copying production .env from $DEFAULT_DIR to $EFFECTIVE_APP_DIR" cp -f "$DEFAULT_DIR/.env" "$EFFECTIVE_APP_DIR/.env" fi - name: Prune workspace for server-only build run: | cd "$EFFECTIVE_APP_DIR" # Keep only root (web) as a package in this effective workspace printf "packages:\n - .\n\nignoredBuiltDependencies:\n - '@prisma/client'\n - '@prisma/engines'\n - '@tailwindcss/oxide'\n - esbuild\n - prisma\n - sharp\n - unrs-resolver\n" > pnpm-workspace.yaml # Remove desktop app to avoid pnpm touching its node_modules on this runner rm -rf apps/desktop || true - name: Clean Next.js cache (.next) to avoid EACCES run: | cd "$EFFECTIVE_APP_DIR" if [ -e .next ]; then echo "Removing existing .next (may be root-owned from previous container)" rm -rf .next || (mv .next ".next.old.$(date +%s)" || true) fi mkdir -p .next chmod -R u+rwX .next || true - name: Install and build (Next.js) run: | cd "$EFFECTIVE_APP_DIR" corepack enable || true pnpm --filter web install --no-frozen-lockfile pnpm prisma:generate pnpm build - name: Swarm deploy (stack.yml) run: | cd "$EFFECTIVE_APP_DIR" # Exporta variáveis do .env para substituição no stack (ex.: MACHINE_PROVISIONING_SECRET) set -o allexport if [ -f .env ]; then . ./.env; fi set +o allexport APP_DIR="$EFFECTIVE_APP_DIR" RELEASE_SHA=${{ github.sha }} docker stack deploy --with-registry-auth -c stack.yml sistema - name: Restart web service with new code run: | docker service update --force sistema_web || true - name: Restart Convex backend service (optional) run: | docker service update --force sistema_convex_backend || true convex_deploy: name: Deploy Convex functions needs: changes # Executa em workflow_dispatch, push na main, ou quando convex/** mudar if: ${{ github.event_name == 'workflow_dispatch' || github.ref == 'refs/heads/main' || needs.changes.outputs.convex == 'true' }} runs-on: [ self-hosted, linux, vps ] env: APP_DIR: /srv/apps/sistema steps: - name: Checkout uses: actions/checkout@v4 - name: Determine APP_DIR (fallback safe path) id: appdir run: | TS=$(date +%s) FALLBACK_DIR="$HOME/apps/sistema.build.$TS" mkdir -p "$FALLBACK_DIR" echo "Using APP_DIR (fallback)=$FALLBACK_DIR" echo "EFFECTIVE_APP_DIR=$FALLBACK_DIR" >> "$GITHUB_ENV" - name: Sync workspace to APP_DIR (preserving local env) run: | mkdir -p "$EFFECTIVE_APP_DIR" RSYNC_FLAGS="-az --inplace --no-times --no-perms --no-owner --no-group --delete" rsync $RSYNC_FLAGS \ --filter='protect .next.old*' \ --exclude '.next.old*' \ --exclude '.env*' \ --exclude 'apps/desktop/.env*' \ --exclude 'convex/.env*' \ --filter='protect node_modules' \ --filter='protect node_modules/**' \ --filter='protect .pnpm-store' \ --filter='protect .pnpm-store/**' \ --exclude '.git' \ --exclude '.next' \ --exclude 'node_modules' \ --exclude 'node_modules/**' \ --exclude '.pnpm-store' \ --exclude '.pnpm-store/**' \ ./ "$EFFECTIVE_APP_DIR"/ - name: Ensure .env is not present for Convex deploy run: | cd "$EFFECTIVE_APP_DIR" if [ -f .env ]; then echo "Renaming .env -> .env.bak (Convex self-hosted deploy)" mv -f .env .env.bak fi - name: Deploy functions to Convex self-hosted if: ${{ env.CONVEX_SELF_HOSTED_URL != '' && env.CONVEX_SELF_HOSTED_ADMIN_KEY != '' }} env: CONVEX_SELF_HOSTED_URL: ${{ secrets.CONVEX_SELF_HOSTED_URL }} CONVEX_SELF_HOSTED_ADMIN_KEY: ${{ secrets.CONVEX_SELF_HOSTED_ADMIN_KEY }} run: | docker run --rm -i \ -v "$EFFECTIVE_APP_DIR":/app \ -w /app \ -e CI=true \ -e CONVEX_SELF_HOSTED_URL \ -e CONVEX_SELF_HOSTED_ADMIN_KEY \ node:20-bullseye bash -lc "set -euo pipefail; unset CONVEX_DEPLOYMENT; corepack enable; corepack prepare pnpm@9 --activate; pnpm install --frozen-lockfile --prod=false; pnpm exec convex deploy" desktop_release: name: Desktop Release (Windows) if: ${{ startsWith(github.ref, 'refs/tags/v') }} runs-on: [ self-hosted, windows, desktop ] defaults: run: working-directory: apps/desktop steps: - name: Checkout uses: actions/checkout@v4 - name: Setup pnpm uses: pnpm/action-setup@v4 with: version: 9 - name: Setup Node.js uses: actions/setup-node@v4 with: node-version: 20 cache: 'pnpm' - name: Install deps (desktop) run: pnpm install --frozen-lockfile - name: Build with Tauri uses: tauri-apps/tauri-action@v0 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} TAURI_PRIVATE_KEY: ${{ secrets.TAURI_PRIVATE_KEY }} TAURI_KEY_PASSWORD: ${{ secrets.TAURI_KEY_PASSWORD }} with: projectPath: apps/desktop - name: Upload latest.json + bundles to VPS uses: appleboy/scp-action@v0.1.7 with: host: ${{ secrets.VPS_HOST }} username: ${{ secrets.VPS_USER }} key: ${{ secrets.VPS_SSH_KEY }} source: | **/bundle/**/latest.json **/bundle/**/* target: ${{ env.VPS_UPDATES_DIR }} overwrite: true