fix(frontend): keep the attachments that uploaded when one in the bat… #546
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: Quality | |
| # Split into two jobs so a pull request gets fast feedback: | |
| # | |
| # fast Lint, tests, contracts, and the frontend. Runs on every PR and on | |
| # main. The full local tier covers the same fast gates; the quick tier | |
| # omits browser and bundle checks for iteration (see CONTRIBUTING.md). | |
| # | |
| # heavy Windows packaging -- the launcher bundle, the one-folder build, and | |
| # a smoke test of the packaged executable. Runs after the fast and | |
| # supported-Python jobs on PRs, pushes, and manual dispatch. | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| workflow_dispatch: | |
| permissions: | |
| contents: read | |
| jobs: | |
| fast: | |
| runs-on: windows-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.11" | |
| cache: pip | |
| cache-dependency-path: requirements-dev.lock.txt | |
| - name: Set up Node | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| # requirements*.lock.txt pin exact versions and hashes for every | |
| # dependency (direct and transitive) resolved for this job's Python | |
| # 3.11 target. A stale lock -- committed after editing pyproject.toml | |
| # without regenerating it -- would silently let this job install | |
| # something different from what it claims to verify, so check first. | |
| # | |
| # `uv pip compile` reads its own prior output file as a starting point | |
| # and only re-resolves what pyproject.toml's constraints actually force | |
| # -- regenerating in place (the same path already committed) and then | |
| # diffing against git reuses every already-locked version for an | |
| # unrelated transitive dependency, rather than re-resolving it to | |
| # whatever happens to be newest on the index right now. Compiling to a | |
| # fresh path instead would make this check flaky: an unconstrained | |
| # transitive dependency (e.g. numpy) can pick up a new upstream release | |
| # between two independent resolves with no repository change at all. | |
| - name: Verify dependency lockfiles are up to date | |
| shell: pwsh | |
| run: | | |
| python -m pip install uv | |
| uv pip compile pyproject.toml --python-version 3.11 --generate-hashes -o requirements.lock.txt | |
| uv pip compile pyproject.toml --extra dev --python-version 3.11 --generate-hashes -o requirements-dev.lock.txt | |
| # `git diff --name-only` rather than `git diff --exit-code`: a | |
| # non-zero native exit is a terminating error under this shell's | |
| # $ErrorActionPreference, so the throw below never ran and the | |
| # regeneration instructions never reached the log. Without | |
| # --exit-code, diff exits 0 either way and reports drift as output. | |
| # Content comparison also matters: uv writes LF, so `git status` | |
| # would flag a line-ending-only difference that is not real drift. | |
| $drift = git --no-pager diff --name-only -- requirements.lock.txt requirements-dev.lock.txt | |
| if ($drift) { | |
| git --no-pager diff -- requirements.lock.txt requirements-dev.lock.txt | |
| throw "requirements*.lock.txt does not match pyproject.toml. Regenerate with 'uv pip compile pyproject.toml --python-version 3.11 --generate-hashes -o requirements.lock.txt' (add --extra dev for the dev lock) and commit the result." | |
| } | |
| - name: Install dependencies | |
| run: python -m pip install -r requirements-dev.lock.txt | |
| - name: Lint Python | |
| run: python -m ruff check backend tests tools main.py app_factory.py | |
| - name: Type-check Python | |
| run: python -m mypy | |
| - name: Run headless tests | |
| run: python -m pytest | |
| - name: Run the artifact boundary review | |
| run: python tools/artifact_boundary_review.py --json --strict | |
| - name: Compile application modules | |
| run: python -m compileall -q main.py app_factory.py backend | |
| - name: Verify generated API contracts | |
| run: | | |
| python tools/generate_contracts.py --check | |
| - name: Install frontend dependencies | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Install Playwright Chromium | |
| working-directory: frontend | |
| run: npx playwright install chromium | |
| - name: Check frontend types | |
| working-directory: frontend | |
| run: npm run typecheck | |
| - name: Lint frontend | |
| working-directory: frontend | |
| run: npm run lint | |
| - name: Run frontend unit tests | |
| working-directory: frontend | |
| run: npm test -- --run | |
| - name: Run frontend browser tests | |
| working-directory: frontend | |
| timeout-minutes: 10 | |
| run: npm run e2e -- --workers=1 | |
| - name: Build frontend bundle | |
| working-directory: frontend | |
| run: npm run build | |
| python-compatibility: | |
| name: Python ${{ matrix.python-version }} compatibility | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| python-version: ["3.10", "3.11", "3.12", "3.13", "3.14"] | |
| runs-on: windows-latest | |
| timeout-minutes: 30 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: ${{ matrix.python-version }} | |
| cache: pip | |
| cache-dependency-path: requirements-dev.txt | |
| # Deliberately loose, unlocked pins here: this job's purpose is to | |
| # catch a dependency that only breaks on one specific Python version, | |
| # which a lock resolved for a single interpreter (see the `fast` and | |
| # `heavy` jobs) cannot exercise. Do not switch this to | |
| # requirements-dev.lock.txt. | |
| - name: Install dependencies | |
| run: python -m pip install -r requirements-dev.txt | |
| - name: Lint Python | |
| run: python -m ruff check backend tests tools main.py app_factory.py | |
| - name: Run Python tests | |
| run: python -m pytest -q | |
| - name: Compile application modules | |
| run: python -m compileall -q main.py app_factory.py backend | |
| heavy: | |
| # Packaging is slow, but a green PR must prove that launcher and runtime | |
| # changes still produce a build that starts. | |
| needs: [fast, python-compatibility] | |
| runs-on: windows-latest | |
| timeout-minutes: 90 | |
| steps: | |
| - name: Check out repository | |
| uses: actions/checkout@v7 | |
| - name: Set up Python | |
| uses: actions/setup-python@v7 | |
| with: | |
| python-version: "3.11" | |
| cache: pip | |
| cache-dependency-path: requirements.lock.txt | |
| - name: Set up Node | |
| uses: actions/setup-node@v7 | |
| with: | |
| node-version: "22" | |
| cache: npm | |
| cache-dependency-path: frontend/package-lock.json | |
| - name: Install dependencies | |
| run: python -m pip install -r requirements.lock.txt | |
| - name: Install frontend dependencies | |
| working-directory: frontend | |
| run: npm ci | |
| - name: Verify launcher-managed bundle | |
| shell: pwsh | |
| run: python main.py --build-frontend --data-dir "$env:RUNNER_TEMP\cortex-quality-data" | |
| - name: Install package builder | |
| run: python -m pip install "pyinstaller==6.14.2" | |
| - name: Prepare signed WebView2 bootstrapper | |
| shell: pwsh | |
| run: ./packaging/prepare_webview2.ps1 | |
| - name: Build one-folder Windows package | |
| run: python -m PyInstaller --noconfirm --clean packaging/Cortex.spec | |
| - name: Verify packaged WebView2 bootstrapper | |
| shell: pwsh | |
| run: | | |
| $bootstrapper = Get-ChildItem dist/Cortex -Recurse -Filter MicrosoftEdgeWebview2Setup.exe | Select-Object -First 1 | |
| if ($null -eq $bootstrapper) { throw "Packaged WebView2 bootstrapper is missing." } | |
| $signature = Get-AuthenticodeSignature -LiteralPath $bootstrapper.FullName | |
| if ($signature.Status -ne "Valid") { throw "Packaged WebView2 signature is invalid." } | |
| - name: Smoke-test packaged headless launcher | |
| shell: pwsh | |
| timeout-minutes: 5 | |
| run: | | |
| $data = Join-Path $env:RUNNER_TEMP "cortex-package-smoke" | |
| $executable = Join-Path (Get-Location) "dist\Cortex\Cortex.exe" | |
| $process = $null | |
| $ready = $false | |
| try { | |
| $process = Start-Process -FilePath $executable -ArgumentList @("--headless", "--data-dir", $data) -PassThru | |
| $deadline = (Get-Date).AddSeconds(30) | |
| while ((Get-Date) -lt $deadline) { | |
| $process.Refresh() | |
| if ($process.HasExited) { | |
| throw "Packaged Cortex exited before its health endpoint became ready (code $($process.ExitCode))." | |
| } | |
| $recordPath = Join-Path $data "cortex.instance.json" | |
| if (Test-Path -LiteralPath $recordPath -PathType Leaf) { | |
| try { | |
| $record = Get-Content -LiteralPath $recordPath -Raw | ConvertFrom-Json | |
| $response = Invoke-WebRequest -UseBasicParsing -TimeoutSec 2 -Uri "http://127.0.0.1:$($record.port)/api/v1/health/ready" | |
| if ($response.StatusCode -eq 200) { | |
| $ready = $true | |
| break | |
| } | |
| } catch { | |
| # The backend can be between record creation and bind. | |
| } | |
| } | |
| Start-Sleep -Seconds 1 | |
| } | |
| if (-not $ready) { | |
| throw "Packaged Cortex did not expose its health endpoint within 30 seconds." | |
| } | |
| } finally { | |
| if ($null -ne $process) { | |
| $process.Refresh() | |
| if (-not $process.HasExited) { | |
| Stop-Process -Id $process.Id -Force | |
| } | |
| } | |
| } |