Sidecar architecture: edge functions (auth + DB) → youtube-worker container (youtubei.js for playlist metadata, yt-dlp for audio downloads). - Edge functions: youtube-playlist, youtube-process, youtube-status (CRUD) - youtube-worker sidecar: Node.js + yt-dlp, downloads M4A to Supabase Storage - Admin music page: import playlists, process queue, inline genre editing - 12 music genres with per-track assignment and import-time defaults - DB migrations: download_jobs, download_items tables with genre column - Deploy script and CI workflow for edge functions + sidecar
228 lines
6.6 KiB
YAML
228 lines
6.6 KiB
YAML
name: CI
|
|
|
|
on:
|
|
push:
|
|
branches: [main, master]
|
|
pull_request:
|
|
branches: [main, master]
|
|
|
|
jobs:
|
|
typecheck:
|
|
name: TypeScript
|
|
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: 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,
|
|
});
|
|
}
|
|
|
|
admin-web-test:
|
|
name: Admin Web Tests
|
|
runs-on: ubuntu-latest
|
|
defaults:
|
|
run:
|
|
working-directory: admin-web
|
|
steps:
|
|
- uses: actions/checkout@v4
|
|
|
|
- name: Setup Node.js
|
|
uses: actions/setup-node@v4
|
|
with:
|
|
node-version: '20'
|
|
cache: 'npm'
|
|
cache-dependency-path: admin-web/package-lock.json
|
|
|
|
- name: Install dependencies
|
|
run: npm ci
|
|
|
|
- name: Type check
|
|
run: npx tsc --noEmit
|
|
|
|
- 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
|
|
runs-on: ubuntu-latest
|
|
needs: [typecheck, lint, test]
|
|
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: Export web build
|
|
run: npx expo export --platform web
|
|
continue-on-error: true
|
|
|
|
deploy-functions:
|
|
name: Deploy Edge Functions
|
|
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
|
|
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
|
|
|
|
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"
|