From 38576fd5289d1538077ae771aa122a5d0b20c79f Mon Sep 17 00:00:00 2001 From: Millian Lamiaux Date: Sat, 23 May 2026 12:09:28 +0200 Subject: [PATCH] ci: replace dead Expo CI with linux-only monorepo pipeline Remove: root Expo typecheck/lint/test/build-check (project deleted Apr 19) Remove: swift-build-test + app-store.yml (no macOS runner on Gitea) Add: path-filtered conditional jobs with dorny/paths-filter@v3 Add: youtube-worker deploy to deploy-functions job Fix: deploy-functions always() to handle skipped upstream jobs Fix: SPM cache key includes Package.resolved (before removal) Fix: remove continue-on-error from vitest/playwright steps --- .github/workflows/app-store.yml | 82 ----------- .github/workflows/ci.yml | 232 ++++++++++++-------------------- 2 files changed, 83 insertions(+), 231 deletions(-) delete mode 100644 .github/workflows/app-store.yml diff --git a/.github/workflows/app-store.yml b/.github/workflows/app-store.yml deleted file mode 100644 index 89b3a8f..0000000 --- a/.github/workflows/app-store.yml +++ /dev/null @@ -1,82 +0,0 @@ -name: App Store Submission - -on: - push: - tags: - - 'v*' - workflow_dispatch: - -jobs: - upload-to-app-store: - name: Archive & Upload to App Store - runs-on: macos-15 - timeout-minutes: 60 - defaults: - run: - working-directory: tabatago-swift - - steps: - - name: Checkout repository - uses: actions/checkout@v4 - - - name: Select Xcode - run: sudo xcode-select -switch /Applications/Xcode.app - - - name: Cache SPM dependencies - uses: actions/cache@v4 - with: - path: ~/Library/Developer/Xcode/DerivedData/**/SourcePackages - key: spm-macos-${{ hashFiles('tabatago-swift/project.yml') }} - restore-keys: | - spm-macos- - - - name: Write App Store Connect API key - env: - API_KEY_P8: ${{ secrets.APP_STORE_CONNECT_API_KEY_P8 }} - run: | - printf '%s' "$API_KEY_P8" > "$RUNNER_TEMP/AuthKey.p8" - - - name: Archive - run: | - xcodebuild archive \ - -project TabataGo.xcodeproj \ - -scheme TabataGo \ - -configuration Release \ - -archivePath ./build/TabataGo.xcarchive \ - -allowProvisioningUpdates \ - -authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8" \ - -authenticationKeyID "${{ secrets.APP_STORE_CONNECT_KEY_ID }}" \ - -authenticationKeyIssuerID "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" \ - || { echo "❌ Archive failed — check signing and API key permissions"; exit 1; } - - - name: Verify build settings - run: | - echo "Checking version and build number..." - xcodebuild -showBuildSettings \ - -project TabataGo.xcodeproj \ - -scheme TabataGo \ - -configuration Release \ - | grep -E "MARKETING_VERSION|CURRENT_PROJECT_VERSION" - - - name: Export IPA - run: | - xcodebuild -exportArchive \ - -archivePath ./build/TabataGo.xcarchive \ - -exportPath ./build/export \ - -exportOptionsPlist ExportOptions.plist \ - -allowProvisioningUpdates \ - -authenticationKeyPath "$RUNNER_TEMP/AuthKey.p8" \ - -authenticationKeyID "${{ secrets.APP_STORE_CONNECT_KEY_ID }}" \ - -authenticationKeyIssuerID "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" \ - || { echo "❌ Export failed — check ExportOptions.plist and provisioning"; exit 1; } - - # NOTE: The first upload automatically creates the app record in - # App Store Connect if one does not already exist. - - name: Upload to App Store - run: | - xcrun altool --upload-app \ - --type ios \ - --file ./build/export/TabataGo.ipa \ - --apiKey "${{ secrets.APP_STORE_CONNECT_KEY_ID }}" \ - --apiIssuer "${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}" \ - || { echo "❌ Upload failed — check API key permissions and app record"; exit 1; } diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50698b8..1948f33 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,150 +2,42 @@ name: CI on: push: - branches: [main, master] + branches: [main] pull_request: - branches: [main, master] + branches: [main] jobs: - typecheck: - name: TypeScript + # ── Path filter — determines which downstream jobs run ── + changes: + name: Detect Changes runs-on: ubuntu-latest + outputs: + admin-web: ${{ steps.filter.outputs.admin-web }} + youtube-worker: ${{ steps.filter.outputs.youtube-worker }} + supabase-functions: ${{ steps.filter.outputs.supabase-functions }} steps: - uses: actions/checkout@v4 - - name: Setup Node.js - uses: actions/setup-node@v4 + - uses: dorny/paths-filter@v3 + id: filter with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Type check - run: npx tsc --noEmit - - lint: - name: ESLint - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Lint - run: npm run lint - - test: - name: Tests - runs-on: ubuntu-latest - steps: - - uses: actions/checkout@v4 - - - name: Setup Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - cache: 'npm' - - - name: Install dependencies - run: npm ci - - - name: Run unit tests with coverage - run: npm run test:coverage - - - name: Run component render tests - run: npm run test:render - - - name: Upload coverage report - uses: actions/upload-artifact@v4 - if: always() - with: - name: coverage-report - path: coverage/ - retention-days: 7 - - - name: Coverage summary - if: always() - run: | - echo "## Test Coverage Summary" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - if [ -f coverage/coverage-summary.json ]; then - echo '```' >> $GITHUB_STEP_SUMMARY - node -e " - const c = require('./coverage/coverage-summary.json').total; - const fmt = (v) => v.pct + '%'; - console.log('Statements: ' + fmt(c.statements)); - console.log('Branches: ' + fmt(c.branches)); - console.log('Functions: ' + fmt(c.functions)); - console.log('Lines: ' + fmt(c.lines)); - " >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - elif [ -f coverage/coverage-final.json ]; then - echo "Coverage report generated. Download the artifact for details." >> $GITHUB_STEP_SUMMARY - else - echo "Coverage report not found." >> $GITHUB_STEP_SUMMARY - fi - - - name: Comment coverage on PR - if: github.event_name == 'pull_request' && always() - uses: actions/github-script@v7 - with: - script: | - const fs = require('fs'); - let body = '## Test Coverage Report\n\n'; - - try { - const summary = JSON.parse(fs.readFileSync('coverage/coverage-summary.json', 'utf8')); - const total = summary.total; - const fmt = (v) => `${v.pct}%`; - const icon = (v) => v.pct >= 80 ? '✅' : v.pct >= 60 ? '⚠️' : '❌'; - - body += '| Metric | Coverage | Status |\n'; - body += '|--------|----------|--------|\n'; - body += `| Statements | ${fmt(total.statements)} | ${icon(total.statements)} |\n`; - body += `| Branches | ${fmt(total.branches)} | ${icon(total.branches)} |\n`; - body += `| Functions | ${fmt(total.functions)} | ${icon(total.functions)} |\n`; - body += `| Lines | ${fmt(total.lines)} | ${icon(total.lines)} |\n`; - } catch (e) { - body += '_Coverage summary not available._\n'; - } - - const { data: comments } = await github.rest.issues.listComments({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - }); - - const existing = comments.find(c => - c.user.type === 'Bot' && c.body.includes('## Test Coverage Report') - ); - - if (existing) { - await github.rest.issues.updateComment({ - owner: context.repo.owner, - repo: context.repo.repo, - comment_id: existing.id, - body, - }); - } else { - await github.rest.issues.createComment({ - owner: context.repo.owner, - repo: context.repo.repo, - issue_number: context.issue.number, - body, - }); - } + filters: | + admin-web: + - 'admin-web/**' + - '.github/workflows/ci.yml' + youtube-worker: + - 'youtube-worker/**' + - '.github/workflows/ci.yml' + supabase-functions: + - 'supabase/functions/**' + - 'youtube-worker/**' + - '.github/workflows/ci.yml' + # ── Admin Web: Next.js ── admin-web-test: - name: Admin Web Tests + name: Admin Web CI + needs: changes + if: needs.changes.outputs.admin-web == 'true' runs-on: ubuntu-latest defaults: run: @@ -168,19 +60,22 @@ jobs: - name: Run unit tests run: npx vitest run - continue-on-error: true - name: Install Playwright browsers run: npx playwright install --with-deps chromium - name: Run E2E tests run: npx playwright test - continue-on-error: true - build-check: - name: Build Check + # ── YouTube Worker: Node.js microservice ── + youtube-worker-check: + name: YouTube Worker + needs: changes + if: needs.changes.outputs.youtube-worker == 'true' runs-on: ubuntu-latest - needs: [typecheck, lint, test] + defaults: + run: + working-directory: youtube-worker steps: - uses: actions/checkout@v4 @@ -189,39 +84,78 @@ jobs: with: node-version: '20' cache: 'npm' + cache-dependency-path: youtube-worker/package-lock.json - name: Install dependencies run: npm ci - - name: Export web build - run: npx expo export --platform web - continue-on-error: true + - name: Validate syntax + run: node --check server.js + # ── Deploy: Supabase edge functions + YouTube worker ── deploy-functions: - name: Deploy Edge Functions + name: Deploy + needs: [changes, admin-web-test, youtube-worker-check] + if: | + always() && + github.ref == 'refs/heads/main' && + github.event_name == 'push' && + !contains(needs.*.result, 'failure') && + (needs.changes.outputs.supabase-functions == 'true' || + needs.changes.outputs.youtube-worker == 'true') runs-on: ubuntu-latest - needs: [typecheck, lint, test] - if: github.ref == 'refs/heads/main' && github.event_name == 'push' steps: - uses: actions/checkout@v4 - - name: Deploy to self-hosted Supabase + - name: Setup SSH env: DEPLOY_HOST: ${{ secrets.SUPABASE_DEPLOY_HOST }} - DEPLOY_USER: ${{ secrets.SUPABASE_DEPLOY_USER }} - DEPLOY_PATH: ${{ secrets.SUPABASE_DEPLOY_PATH }} run: | mkdir -p ~/.ssh echo "${{ secrets.SUPABASE_SSH_KEY }}" > ~/.ssh/deploy_key chmod 600 ~/.ssh/deploy_key ssh-keyscan -H $DEPLOY_HOST >> ~/.ssh/known_hosts 2>/dev/null + - name: Deploy Supabase Edge Functions + env: + DEPLOY_HOST: ${{ secrets.SUPABASE_DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.SUPABASE_DEPLOY_USER }} + DEPLOY_PATH: ${{ secrets.SUPABASE_DEPLOY_PATH }} + run: | rsync -avz --delete \ --exclude='node_modules' \ --exclude='.DS_Store' \ -e "ssh -i ~/.ssh/deploy_key" \ supabase/functions/ \ "$DEPLOY_USER@$DEPLOY_HOST:$DEPLOY_PATH/" - ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" \ "docker restart supabase-edge-functions" + + - name: Deploy YouTube Worker + env: + DEPLOY_HOST: ${{ secrets.SUPABASE_DEPLOY_HOST }} + DEPLOY_USER: ${{ secrets.SUPABASE_DEPLOY_USER }} + WORKER_PATH: /opt/supabase/youtube-worker + run: | + rsync -avz --delete \ + --exclude='node_modules' \ + --exclude='.DS_Store' \ + -e "ssh -i ~/.ssh/deploy_key" \ + youtube-worker/ \ + "$DEPLOY_USER@$DEPLOY_HOST:$WORKER_PATH/" + ssh -i ~/.ssh/deploy_key "$DEPLOY_USER@$DEPLOY_HOST" "\ + cd $WORKER_PATH && \ + docker build -t youtube-worker:latest . && \ + docker stop youtube-worker 2>/dev/null || true && \ + docker rm youtube-worker 2>/dev/null || true && \ + docker run -d \ + --name youtube-worker \ + --restart unless-stopped \ + --network supabase_supabase-network \ + -e SUPABASE_URL=\$(docker exec supabase-edge-functions printenv SUPABASE_URL) \ + -e SUPABASE_SERVICE_ROLE_KEY=\$(docker exec supabase-edge-functions printenv SUPABASE_SERVICE_ROLE_KEY) \ + -e SUPABASE_PUBLIC_URL=https://supabase.1000co.fr \ + -e GEMINI_API_KEY=\$(cat /opt/supabase/.env.gemini 2>/dev/null || echo '') \ + -e STORAGE_BUCKET=workout-audio \ + -e PORT=3001 \ + youtube-worker:latest"