feat(query-insights): streaming UX for Stage 3 AI recommendations #2264
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: CI | |
| # This workflow handles the following scenarios: | |
| # | |
| # 1. Push to `main` or `release/**`: | |
| # - Runs all jobs: code checks, tests, packaging, and caches build sizes | |
| # for PR comparisons | |
| # | |
| # 2. Pull Requests to `main`, `release/**`, or `feature/**`: | |
| # - Runs all jobs: code checks, tests, integration tests, and packaging | |
| # - Posts PR comments with code quality report and build size comparison | |
| # | |
| # 3. Manual run via `workflow_dispatch`: | |
| # - Optional `enforce_full_run` input forces integration tests and packaging on any branch | |
| on: | |
| workflow_dispatch: | |
| inputs: | |
| enforce_full_run: | |
| description: 'Run integration tests and packaging jobs on manual dispatch' | |
| required: false | |
| type: boolean | |
| default: false | |
| push: | |
| branches: | |
| - main | |
| - release/** | |
| pull_request: | |
| branches: | |
| - main | |
| - release/** | |
| - feature/** | |
| concurrency: | |
| group: ${{ github.head_ref || github.run_id }} | |
| cancel-in-progress: true | |
| jobs: | |
| code-quality-and-tests: | |
| name: Code Quality & Tests | |
| runs-on: ubuntu-latest | |
| permissions: | |
| pull-requests: write | |
| defaults: | |
| run: | |
| working-directory: '.' | |
| steps: | |
| - name: ✅ Checkout Repository | |
| uses: actions/checkout@v6 | |
| - name: 🛠 Setup Node.js Environment (with cache) | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| cache-dependency-path: '**/package-lock.json' | |
| - name: 📦 Install Dependencies (npm ci) | |
| run: npm ci --prefer-offline --no-audit --no-fund --progress=false --verbose | |
| - name: 🔨 Build Workspace Packages | |
| run: npm run build --workspaces --if-present | |
| - name: 🌐 Check Localization Files | |
| id: l10n | |
| continue-on-error: true | |
| run: npm run l10n:check | |
| - name: 🧹 Run ESLint | |
| id: lint | |
| continue-on-error: true | |
| run: npm run lint | |
| - name: 🎨 Check Code Formatting (Prettier) | |
| id: prettier | |
| continue-on-error: true | |
| run: npm run prettier | |
| - name: 🧪 Run Unit Tests (Jest) | |
| run: npm run jesttest | |
| - name: 💬 Post PR Code Quality Report | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const results = { | |
| l10n: '${{ steps.l10n.outcome }}', | |
| lint: '${{ steps.lint.outcome }}', | |
| prettier: '${{ steps.prettier.outcome }}', | |
| }; | |
| const labels = { | |
| l10n: 'Localization (`l10n`)', | |
| lint: 'ESLint', | |
| prettier: 'Prettier formatting', | |
| }; | |
| const hints = { | |
| l10n: 'Run `npm run l10n` and commit the updated files.', | |
| lint: 'Run `npm run lint` and fix the reported issues.', | |
| prettier: 'Run `npm run prettier-fix` to auto-format, then commit.', | |
| }; | |
| const marker = '<!-- code-quality-report -->'; | |
| const failed = Object.keys(results).filter(k => results[k] === 'failure'); | |
| const rows = Object.keys(results).map(k => { | |
| const ok = results[k] === 'success'; | |
| const status = ok ? '✅ Passed' : '❌ Failed'; | |
| const hint = ok ? '' : hints[k]; | |
| return `| ${labels[k]} | ${status} | ${hint} |`; | |
| }).join('\n'); | |
| const title = failed.length === 0 | |
| ? '## ✅ Code Quality Checks' | |
| : '## ⚠️ Code Quality Issues'; | |
| const body = [ | |
| marker, | |
| title, | |
| '', | |
| '| Check | Status | How to fix |', | |
| '|-------|--------|------------|', | |
| rows, | |
| '', | |
| '_This comment is updated automatically on each push._', | |
| ].join('\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.body && c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| }); | |
| } | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); | |
| - name: ❌ Fail if code quality checks failed | |
| if: steps.l10n.outcome == 'failure' || steps.lint.outcome == 'failure' || steps.prettier.outcome == 'failure' | |
| run: | | |
| echo "One or more code quality checks failed (l10n=${{ steps.l10n.outcome }}, lint=${{ steps.lint.outcome }}, prettier=${{ steps.prettier.outcome }})" | |
| exit 1 | |
| integration-tests: | |
| name: Integration Tests | |
| runs-on: ubuntu-latest | |
| needs: [code-quality-and-tests] | |
| if: | | |
| (github.event_name == 'workflow_dispatch' && inputs.enforce_full_run) || | |
| github.ref == 'refs/heads/main' || | |
| startsWith(github.ref, 'refs/heads/release/') || | |
| (startsWith(github.ref, 'refs/pull/') && ( | |
| github.base_ref == 'main' || | |
| startsWith(github.base_ref, 'release/') | |
| )) | |
| defaults: | |
| run: | |
| working-directory: '.' | |
| steps: | |
| - name: ✅ Checkout Repository | |
| uses: actions/checkout@v6 | |
| - name: 🛠 Setup Node.js Environment (with cache) | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| cache-dependency-path: '**/package-lock.json' | |
| - name: 📦 Install Dependencies (npm ci) | |
| run: npm ci --prefer-offline --no-audit --no-fund --progress=false | |
| - name: 🔄 Run Integration Tests (Headless UI) | |
| if: false # TODO: Disabled — unreliable, needs revisiting | |
| run: xvfb-run -a npm test | |
| # Run on push to `main`/`release/**` (for cache seeding) and for PRs to main/release/** | |
| build-and-package: | |
| name: Build & Package | |
| runs-on: ubuntu-latest | |
| needs: [code-quality-and-tests] | |
| permissions: | |
| pull-requests: write | |
| if: | | |
| (github.event_name == 'workflow_dispatch' && inputs.enforce_full_run) || | |
| github.ref == 'refs/heads/main' || | |
| startsWith(github.ref, 'refs/heads/release/') || | |
| (startsWith(github.ref, 'refs/pull/') && ( | |
| github.base_ref == 'main' || | |
| startsWith(github.base_ref, 'release/') | |
| )) | |
| defaults: | |
| run: | |
| working-directory: '.' | |
| steps: | |
| - name: ✅ Checkout Repository | |
| uses: actions/checkout@v6 | |
| - name: 🛠 Setup Node.js Environment (with cache) | |
| uses: actions/setup-node@v5 | |
| with: | |
| node-version-file: .nvmrc | |
| cache: npm | |
| cache-dependency-path: '**/package-lock.json' | |
| - name: 📦 Install Dependencies (npm ci) | |
| run: npm ci --prefer-offline --no-audit --no-fund --progress=false | |
| - name: 🏗 Build Project | |
| run: npm run build | |
| - name: 📦 Package Distributables (vsix/tgz) | |
| run: npm run package | |
| - name: 📐 Collect build sizes | |
| id: sizes | |
| run: | | |
| VSIX_FILE=$(find . -maxdepth 1 -name '*.vsix' | head -1) | |
| VSIX_SIZE=$(stat --format=%s "$VSIX_FILE") | |
| WEBVIEW_SIZE=$(stat --format=%s dist/views.js 2>/dev/null || echo 0) | |
| echo "vsix_file=$(basename "$VSIX_FILE")" >> $GITHUB_OUTPUT | |
| echo "vsix_size=$VSIX_SIZE" >> $GITHUB_OUTPUT | |
| echo "webview_size=$WEBVIEW_SIZE" >> $GITHUB_OUTPUT | |
| echo "{\"vsixSize\": $VSIX_SIZE, \"webviewSize\": $WEBVIEW_SIZE}" > build-sizes.json | |
| echo "VSIX: $VSIX_FILE ($VSIX_SIZE bytes), Webview: $WEBVIEW_SIZE bytes" | |
| - name: 📤 Upload Artifacts | |
| uses: actions/upload-artifact@v6 | |
| with: | |
| name: Artifacts-${{ github.run_id }} | |
| path: | | |
| **/*.vsix | |
| **/*.tgz | |
| !**/node_modules | |
| - name: 💾 Cache build sizes (push only) | |
| if: github.event_name == 'push' | |
| uses: actions/cache/save@v5 | |
| with: | |
| path: build-sizes.json | |
| key: build-sizes-${{ github.ref_name }}-${{ github.sha }} | |
| - name: 📥 Restore base branch sizes (PR only) | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| id: base-cache | |
| uses: actions/cache/restore@v5 | |
| with: | |
| path: build-sizes.json | |
| key: build-sizes-${{ github.event.pull_request.base.ref }}-${{ github.event.pull_request.base.sha }} | |
| restore-keys: | | |
| build-sizes-${{ github.event.pull_request.base.ref }}- | |
| - name: 📐 Prepare base sizes for comparison | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository && steps.base-cache.outputs.cache-matched-key | |
| run: | | |
| # Cache restore overwrites build-sizes.json with base branch data. | |
| # Rename it so the PR comment step can read base vs PR separately. | |
| mv build-sizes.json base-sizes.json | |
| - name: 💬 Post PR Build Size Report | |
| if: github.event_name == 'pull_request' && github.event.pull_request.head.repo.full_name == github.repository | |
| uses: actions/github-script@v8 | |
| with: | |
| github-token: ${{ secrets.GITHUB_TOKEN }} | |
| script: | | |
| const fs = require('fs'); | |
| const prVsix = parseInt('${{ steps.sizes.outputs.vsix_size }}', 10); | |
| const prWebview = parseInt('${{ steps.sizes.outputs.webview_size }}', 10); | |
| const vsixFile = '${{ steps.sizes.outputs.vsix_file }}'; | |
| const fmt = (bytes) => { | |
| if (bytes >= 1024 * 1024) return (bytes / 1024 / 1024).toFixed(2) + ' MB'; | |
| return Math.round(bytes / 1024).toLocaleString() + ' KB'; | |
| }; | |
| const delta = (pr, base) => { | |
| const diff = pr - base; | |
| const pct = base > 0 ? ((diff / base) * 100).toFixed(1) : '∞'; | |
| const sign = diff > 0 ? '+' : ''; | |
| const icon = diff > 0 ? '⬆️' : diff < 0 ? '⬇️' : '✅'; | |
| return `${icon} ${sign}${fmt(diff)} (${sign}${pct}%)`; | |
| }; | |
| let baseSizes = null; | |
| try { | |
| baseSizes = JSON.parse(fs.readFileSync('base-sizes.json', 'utf8')); | |
| } catch (_) { /* no cache */ } | |
| const runUrl = `${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}`; | |
| const marker = '<!-- build-size-report -->'; | |
| let body; | |
| if (baseSizes) { | |
| body = [ | |
| marker, | |
| '## 📦 Build Size Report', | |
| '', | |
| '| Metric | Base (`${{ github.event.pull_request.base.ref }}`) | PR | Delta |', | |
| '|--------|------|-----|-------|', | |
| `| VSIX (\`${vsixFile}\`) | ${fmt(baseSizes.vsixSize)} | ${fmt(prVsix)} | ${delta(prVsix, baseSizes.vsixSize)} |`, | |
| `| Webview bundle (\`views.js\`) | ${fmt(baseSizes.webviewSize)} | ${fmt(prWebview)} | ${delta(prWebview, baseSizes.webviewSize)} |`, | |
| '', | |
| `_[Download artifact](${runUrl}) · updated automatically on each push._`, | |
| ].join('\n'); | |
| } else { | |
| body = [ | |
| marker, | |
| '## 📦 Build Size Report', | |
| '', | |
| '| Metric | Size |', | |
| '|--------|------|', | |
| `| VSIX (\`${vsixFile}\`) | ${fmt(prVsix)} |`, | |
| `| Webview bundle (\`views.js\`) | ${fmt(prWebview)} |`, | |
| '', | |
| `> ⚠️ No baseline cached for \`${{ github.event.pull_request.base.ref }}\` yet — delta will appear after the next push to that branch.`, | |
| '', | |
| `_[Download artifact](${runUrl}) · updated automatically on each push._`, | |
| ].join('\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.body && c.body.includes(marker)); | |
| if (existing) { | |
| await github.rest.issues.deleteComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| comment_id: existing.id, | |
| }); | |
| } | |
| await github.rest.issues.createComment({ | |
| owner: context.repo.owner, | |
| repo: context.repo.repo, | |
| issue_number: context.issue.number, | |
| body, | |
| }); |