From 41119593643dc47df916d94f243ffcf9cdf189da Mon Sep 17 00:00:00 2001 From: wiiiii123 Date: Fri, 10 Jul 2026 05:07:19 +0700 Subject: [PATCH] ci: add pull-request quality checks --- .gitattributes | 1 + .github/workflows/quality.yml | 60 +++++++++++++++++++ biome.json | 35 +---------- electron/ipc/register/assets.ts | 2 +- package.json | 5 +- src/components/launch/SourceSelector.tsx | 2 +- .../launch/contexts/HudInteractionContext.tsx | 4 +- src/components/video-editor/VideoPlayback.tsx | 1 - .../audio/waveform/waveform.worker.ts | 2 +- src/lib/exporter/streamingDecoder.test.ts | 8 +-- 10 files changed, 75 insertions(+), 45 deletions(-) create mode 100644 .gitattributes create mode 100644 .github/workflows/quality.yml diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 00000000..6313b56c --- /dev/null +++ b/.gitattributes @@ -0,0 +1 @@ +* text=auto eol=lf diff --git a/.github/workflows/quality.yml b/.github/workflows/quality.yml new file mode 100644 index 00000000..3b8169b8 --- /dev/null +++ b/.github/workflows/quality.yml @@ -0,0 +1,60 @@ +name: Code Quality + +on: + pull_request: + types: [opened, reopened, synchronize, ready_for_review] + push: + branches: [main] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref }} + cancel-in-progress: true + +jobs: + quality: + name: Repository quality + runs-on: ubuntu-latest + timeout-minutes: 20 + env: + CI: true + + steps: + - name: Checkout code + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + persist-credentials: false + + - name: Setup Node.js + uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0 + with: + node-version: '22' + cache: npm + cache-dependency-path: package-lock.json + + - name: Install dependencies + id: install + run: npm ci --ignore-scripts + + - name: Typecheck + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npx tsc --noEmit + + - name: Lint + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm run lint + + - name: Test + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + run: npm test + + # Main currently has known locale-parity debt covered by PR #710. Keep the + # check visible without blocking unrelated PRs; make it required once that + # existing translation PR (or an equivalent fix) lands. + - name: Check translations (advisory) + if: ${{ !cancelled() && steps.install.outcome == 'success' }} + continue-on-error: true + run: npm run i18n:check diff --git a/biome.json b/biome.json index 30eb475d..c1d2304e 100644 --- a/biome.json +++ b/biome.json @@ -92,42 +92,11 @@ "noWith": "error", "useGetterReturn": "error" } - }, - "includes": ["**", "**/dist", "**/.eslintrc.cjs", "**", "**/dist", "**/.eslintrc.cjs"] + } }, "javascript": { "formatter": { "quoteStyle": "double" } }, - "css": { "parser": { "tailwindDirectives": true } }, + "css": { "parser": { "cssModules": true, "tailwindDirectives": true } }, "overrides": [ - { - "includes": ["*.ts", "*.tsx", "*.mts", "*.cts"], - "linter": { - "rules": { - "complexity": { "noArguments": "error" }, - "correctness": { - "noConstAssign": "off", - "noGlobalObjectCalls": "off", - "noInvalidBuiltinInstantiation": "off", - "noInvalidConstructorSuper": "off", - "noSetterReturn": "off", - "noUndeclaredVariables": "off", - "noUnreachable": "off", - "noUnreachableSuper": "off" - }, - "style": { "useConst": "error" }, - "suspicious": { - "noDuplicateClassMembers": "off", - "noDuplicateObjectKeys": "off", - "noDuplicateParameters": "off", - "noFunctionAssign": "off", - "noImportAssign": "off", - "noRedeclare": "off", - "noUnsafeNegation": "off", - "noVar": "error", - "useGetterReturn": "off" - } - } - } - }, { "includes": ["*.ts", "*.tsx", "*.mts", "*.cts"], "linter": { diff --git a/electron/ipc/register/assets.ts b/electron/ipc/register/assets.ts index a0b132ea..f18bf7b6 100644 --- a/electron/ipc/register/assets.ts +++ b/electron/ipc/register/assets.ts @@ -62,7 +62,7 @@ export function registerAssetHandlers() { await fs.writeFile(thumbPath, jpegData) }) // Keep the queue moving even if one fails - thumbGenerationQueue = generation.catch(() => {}) + thumbGenerationQueue = generation.catch(() => undefined) await generation return { success: true, data: jpegData! } diff --git a/package.json b/package.json index 6357c666..c9266412 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,10 @@ "dev": "vite --config vite.config.ts", "postinstall": "node scripts/postinstall.mjs", "build": "npm run build:platform-native-helpers && tsc && vite build --config vite.config.ts && npm run normalize:electron-main-cjs && npm run smoke:electron-main-cjs && electron-builder", - "lint": "biome check .", - "lint:fix": "biome check --write .", + "lint": "biome lint .", + "lint:fix": "biome lint --write .", "format": "biome format --write .", + "format:check": "biome format .", "preview": "vite preview --config vite.config.ts", "rebuild:native": "node ./node_modules/@electron/rebuild/lib/cli.js --force --only uiohook-napi", "build:native-helpers": "node scripts/build-native-helpers.mjs", diff --git a/src/components/launch/SourceSelector.tsx b/src/components/launch/SourceSelector.tsx index 158b53e1..baa4da59 100644 --- a/src/components/launch/SourceSelector.tsx +++ b/src/components/launch/SourceSelector.tsx @@ -80,7 +80,7 @@ export const SourceSelectorContent = ({ windowSources = [], selectedSource = "Screen", loading = false, - onSourceSelect = () => {}, + onSourceSelect = () => undefined, }: Pick) => { const t = useScopedT("launch"); const renderSourceItem = (source: DesktopSource, index: number) => { diff --git a/src/components/launch/contexts/HudInteractionContext.tsx b/src/components/launch/contexts/HudInteractionContext.tsx index 65a1accb..66b5159e 100644 --- a/src/components/launch/contexts/HudInteractionContext.tsx +++ b/src/components/launch/contexts/HudInteractionContext.tsx @@ -1,8 +1,8 @@ -import { createContext, useContext } from "react"; +import { createContext, type MouseEvent, useContext } from "react"; interface HudInteractionContextType { onMouseEnter: () => void; - onMouseLeave: (event: any) => void; + onMouseLeave: (event: MouseEvent) => void; } export const HudInteractionContext = createContext(null); diff --git a/src/components/video-editor/VideoPlayback.tsx b/src/components/video-editor/VideoPlayback.tsx index 499179f3..8de8b685 100644 --- a/src/components/video-editor/VideoPlayback.tsx +++ b/src/components/video-editor/VideoPlayback.tsx @@ -169,7 +169,6 @@ import { import { clampFocusToStage as clampFocusToStageUtil } from "./videoPlayback/focusUtils"; import { layoutVideoContent as layoutVideoContentUtil, - scalePreviewBorderRadius, } from "./videoPlayback/layoutUtils"; import { updateOverlayIndicator } from "./videoPlayback/overlayUtils"; import { createVideoEventHandlers } from "./videoPlayback/videoEventHandlers"; diff --git a/src/components/video-editor/audio/waveform/waveform.worker.ts b/src/components/video-editor/audio/waveform/waveform.worker.ts index 80403a09..28e2ebd9 100644 --- a/src/components/video-editor/audio/waveform/waveform.worker.ts +++ b/src/components/video-editor/audio/waveform/waveform.worker.ts @@ -6,7 +6,7 @@ type WaveformWorkerRequest = { interface WorkerContext { onmessage: (e: MessageEvent) => void; - postMessage: (message: any, transfer?: Transferable[]) => void; + postMessage: (message: unknown, transfer?: Transferable[]) => void; } const workerScope = self as unknown as WorkerContext; diff --git a/src/lib/exporter/streamingDecoder.test.ts b/src/lib/exporter/streamingDecoder.test.ts index de831671..f49e835c 100644 --- a/src/lib/exporter/streamingDecoder.test.ts +++ b/src/lib/exporter/streamingDecoder.test.ts @@ -67,7 +67,7 @@ describe("StreamingVideoDecoder local media loading", () => { const decoder = new StreamingVideoDecoder(); await decoder.loadMetadata("http://127.0.0.1:43123/video?path=%2Ftmp%2Fcapture.mp4"); - expect((window as any).electronAPI.readLocalFile).not.toHaveBeenCalled(); + expect(window.electronAPI.readLocalFile).not.toHaveBeenCalled(); expect(mockDemuxerLoad).toHaveBeenCalledWith( "http://127.0.0.1:43123/video?path=%2Ftmp%2Fcapture.mp4", ); @@ -77,7 +77,7 @@ describe("StreamingVideoDecoder local media loading", () => { const decoder = new StreamingVideoDecoder(); await decoder.loadMetadata("/tmp/capture.mp4"); - expect((window as any).electronAPI.getLocalMediaUrl).toHaveBeenCalledWith( + expect(window.electronAPI.getLocalMediaUrl).toHaveBeenCalledWith( "/tmp/capture.mp4", ); expect(mockDemuxerLoad).toHaveBeenCalledWith( @@ -90,7 +90,7 @@ describe("StreamingVideoDecoder local media loading", () => { mockDemuxerLoad .mockRejectedValueOnce(new Error("get_media_info failed: Failed after 3 attempts")) .mockResolvedValueOnce(undefined); - (window as any).electronAPI.readLocalFile = vi.fn(async () => ({ + window.electronAPI.readLocalFile = vi.fn(async () => ({ success: true, data: new Uint8Array([1, 2, 3]), })); @@ -103,7 +103,7 @@ describe("StreamingVideoDecoder local media loading", () => { "http://127.0.0.1:4321/video?path=%2Ftmp%2Ffallback.mp4", ); expect(mockDemuxerLoad.mock.calls[1]?.[0]).toBeInstanceOf(File); - expect((window as any).electronAPI.readLocalFile).toHaveBeenCalledWith( + expect(window.electronAPI.readLocalFile).toHaveBeenCalledWith( "/tmp/fallback.mp4", ); });