feat(local-chat): add local conversation with file attachment support #441
Workflow file for this run
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: Desktop E2E | |
| permissions: | |
| contents: read | |
| id-token: write | |
| on: | |
| pull_request: | |
| paths: | |
| - "apps/controller/**" | |
| - "apps/desktop/**" | |
| - "apps/web/**" | |
| - "e2e/desktop/**" | |
| - ".github/workflows/desktop-e2e.yml" | |
| - "codecov.yml" | |
| push: | |
| branches: | |
| - main | |
| paths: | |
| - "apps/controller/**" | |
| - "apps/desktop/**" | |
| - "apps/web/**" | |
| - "e2e/desktop/**" | |
| - ".github/workflows/desktop-e2e.yml" | |
| - "codecov.yml" | |
| workflow_dispatch: | |
| inputs: | |
| mode: | |
| description: 'E2E mode' | |
| required: false | |
| default: 'model' | |
| type: choice | |
| options: [smoke, login, model, update, resilience, full] | |
| source: | |
| description: 'Artifact source' | |
| required: false | |
| default: 'download' | |
| type: choice | |
| options: | |
| - download # Download published build (nightly/beta/stable) | |
| - build # Build unsigned from current branch | |
| channel: | |
| description: 'Channel (only for download source)' | |
| required: false | |
| default: 'nightly' | |
| type: choice | |
| options: [nightly, beta, stable] | |
| coverage: | |
| description: 'Enable precise coverage collection (build source only)' | |
| required: false | |
| default: 'false' | |
| type: choice | |
| options: ['false', 'true'] | |
| schedule: | |
| # Run nightly at 03:00 UTC (11:00 CST) | |
| - cron: '0 3 * * *' | |
| env: | |
| RELEASE_BASE: https://desktop-releases.nexu.io | |
| concurrency: | |
| group: desktop-e2e-${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} | |
| cancel-in-progress: true | |
| jobs: | |
| validate-inputs: | |
| if: github.event_name == 'workflow_dispatch' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Validate coverage input | |
| if: github.event.inputs.coverage == 'true' && github.event.inputs.source != 'build' | |
| run: | | |
| echo "coverage=true requires source=build for precise path remapping and per-run source map parity." | |
| echo "Use source=build for coverage runs, or set coverage=false when testing downloaded artifacts." | |
| exit 1 | |
| # -------------------------------------------------------------------------- | |
| # Option A: Build unsigned from current branch | |
| # -------------------------------------------------------------------------- | |
| build: | |
| needs: [validate-inputs] | |
| if: always() && (needs.validate-inputs.result == 'success' || needs.validate-inputs.result == 'skipped') && (github.event_name != 'workflow_dispatch' || github.event.inputs.source == 'build') && (github.event_name == 'workflow_dispatch' || github.event_name == 'pull_request' || github.event_name == 'push') | |
| runs-on: [self-hosted, macOS, ARM64] | |
| timeout-minutes: 30 | |
| env: | |
| E2E_MODE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.mode || 'model' }} | |
| E2E_COVERAGE: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.coverage == 'true') || github.event_name == 'pull_request' || github.event_name == 'push' }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup pnpm | |
| uses: pnpm/action-setup@v4 | |
| with: | |
| version: 10.26.0 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 24 | |
| cache: pnpm | |
| cache-dependency-path: pnpm-lock.yaml | |
| - name: Install dependencies | |
| run: pnpm install --frozen-lockfile | |
| - name: Build unsigned desktop (arm64) | |
| run: | | |
| rm -f apps/desktop/release/*.dmg apps/desktop/release/*.zip | |
| pnpm dist:mac:unsigned:arm64 | |
| - name: Copy build artifacts to E2E | |
| run: | | |
| mkdir -p e2e/desktop/artifacts | |
| rm -f e2e/desktop/artifacts/*.dmg e2e/desktop/artifacts/*.zip | |
| cp apps/desktop/release/*.dmg e2e/desktop/artifacts/ | |
| cp apps/desktop/release/*.zip e2e/desktop/artifacts/ | |
| echo "Artifacts:" | |
| ls -lh e2e/desktop/artifacts/ | |
| - name: Collect coverage remap artifacts | |
| if: env.E2E_COVERAGE == 'true' | |
| run: | | |
| mkdir -p e2e/desktop/artifacts/source-maps/dist | |
| mkdir -p e2e/desktop/artifacts/source-maps/dist-electron/preload | |
| mkdir -p e2e/desktop/artifacts/source-maps/web-dist | |
| cp -R apps/desktop/dist/. e2e/desktop/artifacts/source-maps/dist/ | |
| cp -R apps/desktop/dist-electron/preload/. e2e/desktop/artifacts/source-maps/dist-electron/preload/ | |
| cp -R apps/web/dist/. e2e/desktop/artifacts/source-maps/web-dist/ | |
| - name: Write coverage build manifest | |
| if: env.E2E_COVERAGE == 'true' | |
| run: | | |
| node -e "const fs = require('node:fs'); const path = require('node:path'); const manifestPath = path.join('e2e', 'desktop', 'artifacts', 'coverage-build-manifest.json'); const manifest = { gitSha: process.env.GITHUB_SHA, workflowRunId: process.env.GITHUB_RUN_ID, mode: process.env.NEXU_DESKTOP_E2E_MODE, source: 'build', coverageEnabled: true, builtAt: new Date().toISOString(), pathNormalizationVersion: 1, sourceMaps: ['source-maps/dist/**', 'source-maps/dist-electron/preload/**', 'source-maps/web-dist/**'] }; fs.writeFileSync(manifestPath, JSON.stringify(manifest, null, 2) + '\n');" | |
| env: | |
| NEXU_DESKTOP_E2E_MODE: ${{ env.E2E_MODE }} | |
| - name: Upload build artifacts | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: desktop-e2e-build-${{ github.run_id }} | |
| path: e2e/desktop/artifacts/ | |
| retention-days: 3 | |
| # -------------------------------------------------------------------------- | |
| # E2E test | |
| # -------------------------------------------------------------------------- | |
| e2e: | |
| needs: [validate-inputs, build] | |
| if: always() && (needs.validate-inputs.result == 'success' || needs.validate-inputs.result == 'skipped') && (needs.build.result == 'success' || needs.build.result == 'skipped') | |
| runs-on: [self-hosted, macOS, ARM64] | |
| timeout-minutes: 30 | |
| env: | |
| E2E_MODE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.mode || 'model' }} | |
| E2E_SOURCE: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.source || (github.event_name == 'schedule' && 'download' || 'build') }} | |
| E2E_CHANNEL: ${{ github.event_name == 'workflow_dispatch' && github.event.inputs.channel || 'nightly' }} | |
| E2E_COVERAGE: ${{ (github.event_name == 'workflow_dispatch' && github.event.inputs.coverage == 'true') || github.event_name == 'pull_request' || github.event_name == 'push' }} | |
| defaults: | |
| run: | |
| working-directory: e2e/desktop | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: 22 | |
| - name: Install E2E dependencies | |
| run: npm install | |
| # --- Download source --- | |
| - name: Download published artifacts | |
| if: env.E2E_SOURCE != 'build' | |
| env: | |
| CHANNEL: ${{ env.E2E_CHANNEL }} | |
| NEXU_DESKTOP_E2E_DMG_URL: ${{ env.RELEASE_BASE }}/${{ env.E2E_CHANNEL }}/arm64/nexu-latest-${{ env.E2E_CHANNEL }}-mac-arm64.dmg | |
| NEXU_DESKTOP_E2E_ZIP_URL: ${{ env.RELEASE_BASE }}/${{ env.E2E_CHANNEL }}/arm64/nexu-latest-${{ env.E2E_CHANNEL }}-mac-arm64.zip | |
| run: | | |
| echo "Channel: $CHANNEL" | |
| npm run download | |
| # --- Build source --- | |
| - name: Download build artifacts | |
| if: env.E2E_SOURCE == 'build' | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: desktop-e2e-build-${{ github.run_id }} | |
| path: e2e/desktop/artifacts/ | |
| - name: Verify artifacts | |
| run: | | |
| echo "Artifacts:" | |
| ls -lh artifacts/ | |
| test -f artifacts/*.dmg || { echo "ERROR: No DMG found"; exit 1; } | |
| test -f artifacts/*.zip || { echo "ERROR: No ZIP found"; exit 1; } | |
| - name: Run desktop E2E | |
| env: | |
| NEXU_DESKTOP_E2E_SKIP_CODESIGN: ${{ env.E2E_SOURCE == 'build' && 'true' || 'false' }} | |
| NEXU_DESKTOP_E2E_COVERAGE: ${{ env.E2E_COVERAGE == 'true' && '1' || '0' }} | |
| NEXU_DESKTOP_E2E_COVERAGE_RUN_ID: desktop-e2e-${{ github.run_id }}-${{ github.run_attempt }}-${{ env.E2E_MODE }} | |
| NEXU_DESKTOP_E2E_GIT_SHA: ${{ github.sha }} | |
| NEXU_DESKTOP_E2E_SOURCE: ${{ env.E2E_SOURCE }} | |
| NEXU_DESKTOP_E2E_WORKFLOW_RUN_ID: ${{ github.run_id }} | |
| run: bash scripts/run-e2e.sh "${{ env.E2E_MODE }}" | |
| - name: Merge desktop E2E coverage | |
| if: always() && env.E2E_COVERAGE == 'true' | |
| run: npm run coverage:merge | |
| - name: Write desktop E2E coverage summary | |
| if: always() && env.E2E_COVERAGE == 'true' | |
| run: npm run coverage:report | |
| - name: Upload desktop E2E coverage artifacts | |
| if: always() && env.E2E_COVERAGE == 'true' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: desktop-e2e-coverage-${{ env.E2E_MODE }}-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| e2e/desktop/captures/coverage/ | |
| e2e/desktop/artifacts/coverage-build-manifest.json | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Upload desktop E2E coverage to Codecov | |
| if: ${{ !cancelled() && env.E2E_COVERAGE == 'true' && hashFiles('e2e/desktop/captures/coverage/lcov.info') != '' }} | |
| uses: codecov/codecov-action@v6 | |
| with: | |
| use_oidc: true | |
| skip_validation: true | |
| disable_search: true | |
| files: e2e/desktop/captures/coverage/lcov.info | |
| flags: desktop-e2e | |
| name: desktop-e2e-${{ github.run_id }}-${{ github.run_attempt }} | |
| fail_ci_if_error: false | |
| verbose: true | |
| os: macos | |
| - name: Upload E2E diagnostics | |
| if: always() | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: desktop-e2e-${{ env.E2E_SOURCE }}-${{ env.E2E_CHANNEL }}-${{ env.E2E_MODE }}-${{ github.run_id }}-${{ github.run_attempt }} | |
| path: | | |
| e2e/desktop/captures/ | |
| !e2e/desktop/captures/coverage/** | |
| if-no-files-found: warn | |
| retention-days: 14 | |
| - name: Notify Feishu E2E result | |
| if: always() | |
| env: | |
| FEISHU_WEBHOOK: ${{ secrets.NIGHTLY_FEISHU_WEBHOOK }} | |
| E2E_MODE: ${{ env.E2E_MODE }} | |
| E2E_SOURCE: ${{ env.E2E_SOURCE }} | |
| E2E_CHANNEL: ${{ env.E2E_CHANNEL }} | |
| E2E_STATUS: ${{ job.status }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| if [ -z "$FEISHU_WEBHOOK" ]; then | |
| echo "No Feishu webhook, skipping" | |
| exit 0 | |
| fi | |
| run_url="https://github.com/${{ github.repository }}/actions/runs/${{ github.run_id }}" | |
| short_sha="${GITHUB_SHA::7}" | |
| branch="${GITHUB_HEAD_REF:-${GITHUB_REF_NAME}}" | |
| trigger="${GITHUB_TRIGGERING_ACTOR:-unknown}" | |
| if [ "$E2E_STATUS" = "success" ]; then | |
| template="green" | |
| title="✅ Desktop E2E 测试通过" | |
| else | |
| template="red" | |
| title="❌ Desktop E2E 测试失败" | |
| fi | |
| # Determine what triggered this: PR, commit, or scheduled | |
| trigger_info="" | |
| if [ -n "$GITHUB_HEAD_REF" ]; then | |
| pr_number=$(echo "$GITHUB_REF" | grep -oE '[0-9]+' || echo "") | |
| trigger_info="PR [#${pr_number}](https://github.com/${{ github.repository }}/pull/${pr_number}) on \`${branch}\`" | |
| elif [ "${{ github.event_name }}" = "schedule" ]; then | |
| trigger_info="定时触发 (\`${branch}\` @ \`${short_sha}\`)" | |
| else | |
| trigger_info="手动触发 (\`${branch}\` @ \`${short_sha}\`)" | |
| fi | |
| card=$(jq -n \ | |
| --arg title "$title" \ | |
| --arg template "$template" \ | |
| --arg mode "$E2E_MODE" \ | |
| --arg source "$E2E_SOURCE" \ | |
| --arg channel "$E2E_CHANNEL" \ | |
| --arg trigger_info "$trigger_info" \ | |
| --arg trigger "$trigger" \ | |
| --arg run_url "$run_url" \ | |
| '{ | |
| msg_type: "interactive", | |
| card: { | |
| header: { | |
| template: $template, | |
| title: { | |
| tag: "plain_text", | |
| content: $title | |
| } | |
| }, | |
| elements: [ | |
| { | |
| tag: "markdown", | |
| content: ("**触发来源**\n" + $trigger_info + "\n触发人: " + $trigger + "\n\n**测试配置**\n- 模式: `" + $mode + "`\n- 构建来源: `" + $source + "`\n- 频道: `" + $channel + "`") | |
| }, | |
| { | |
| tag: "action", | |
| actions: [ | |
| { | |
| tag: "button", | |
| text: { tag: "plain_text", content: "📋 查看详情" }, | |
| type: "default", | |
| url: $run_url | |
| } | |
| ] | |
| } | |
| ] | |
| } | |
| }') | |
| curl -sf -X POST "$FEISHU_WEBHOOK" \ | |
| -H "Content-Type: application/json" \ | |
| -d "$card" || echo "Feishu notification failed (non-fatal)" |