diff --git a/.github/actionlint.yaml b/.github/actionlint.yaml new file mode 100644 index 000000000..7cd37535a --- /dev/null +++ b/.github/actionlint.yaml @@ -0,0 +1,10 @@ +paths: + # build-cross-platform and build-linux share their steps via a YAML anchor + # (steps: *electron-build-steps). actionlint type-checks the anchored steps + # against each job's own matrix, so matrix.linux_profile — defined only in + # build-linux — is reported as unknown when the same steps are checked + # against the build-cross-platform matrix. The steps guard every use with + # `matrix.os == 'linux'` or a `|| ''` fallback, so this is a false positive. + .github/workflows/build-and-make.yaml: + ignore: + - 'property "linux_profile" is not defined in object type' diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 000000000..fdccfe138 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,39 @@ +# Every Dependabot PR triggers the full pipeline (~15 jobs), so version +# updates are batched: weekly cadence, minor+patch bumps grouped into one PR +# per ecosystem, majors as individual PRs so CI gates them one by one. +# Security updates are separate and are not limited by this schedule. +version: 2 +updates: + - package-ecosystem: npm + directory: / + schedule: + interval: weekly + day: monday + time: '06:00' + open-pull-requests-limit: 5 + groups: + npm-minor-patch: + update-types: + - minor + - patch + + - package-ecosystem: github-actions + directory: / + schedule: + interval: weekly + day: monday + time: '06:00' + groups: + actions-minor-patch: + patterns: + - '*' + update-types: + - minor + - patch + + - package-ecosystem: docker + directory: /docker + schedule: + interval: weekly + day: monday + time: '06:00' diff --git a/.github/workflows/build-and-make.yaml b/.github/workflows/build-and-make.yaml index 7ddc41e6e..bb162af7f 100644 --- a/.github/workflows/build-and-make.yaml +++ b/.github/workflows/build-and-make.yaml @@ -1,16 +1,46 @@ name: Build and Make Electron App +# Docs-only changes never affect the packaged app, so they skip the build +# matrix. apps/website/** is intentionally NOT ignored: the Linux build job +# builds the website to verify AppStream screenshot assets. Tag pushes are +# unaffected — GitHub does not evaluate paths filters for tags, so v* release +# builds always run. +# +# Known accepted edge case: if a PR built a test-pr- draft and later +# reverts its code changes so the remaining diff is docs-only, new pushes +# skip this workflow and the draft keeps assets from the older commit. The +# draft's title/body name the exact commit they were built from, and +# cleanup-pr-draft.yml deletes the draft when the PR closes, so the stale +# window is visible and bounded; refreshing drafts on skipped runs is not +# worth a separate workflow. on: push: branches: - master tags: - 'v*.*.*' + paths-ignore: + - '**/*.md' + - 'docs/**' + - '.plans/**' + - '.codex/**' + - '.claude/**' pull_request: branches: - master + paths-ignore: + - '**/*.md' + - 'docs/**' + - '.plans/**' + - '.codex/**' + - '.claude/**' workflow_dispatch: +# Build jobs only read the repo; the create-release job raises itself to +# contents: write at the job level to manage the rolling draft release. +permissions: + contents: read + jobs: linux-embedded-mpv-runtime: name: Build pinned Linux Embedded MPV runtime @@ -1389,7 +1419,7 @@ jobs: - name: Get version from package.json id: package-version - run: echo "version=$(node -p "require('./package.json').version")" >> $GITHUB_OUTPUT + run: echo "version=$(node -p "require('./package.json').version")" >> "$GITHUB_OUTPUT" # Test drafts (PR/master) get a self-describing title plus a context # header linking the PR, real head commit, and workflow run. A stable diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index bf4da6b24..9e5ef8c4f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -9,7 +9,39 @@ on: - master workflow_dispatch: +# Superseded PR pushes cancel their still-running checks. Non-PR runs get a +# unique group (run_id) because GitHub keeps at most one pending run per +# group even with cancel-in-progress: false — a shared ref group would let a +# rapid master push silently replace a queued sibling and leave a merged +# commit without a lint/test record. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + jobs: + actionlint: + name: Workflow lint + runs-on: ubuntu-latest + timeout-minutes: 10 + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + # Image pinned by digest (tag 1.7.12). False positives are + # suppressed in .github/actionlint.yaml; shellcheck runs at + # warning+ severity so style/info notes in long release scripts + # don't fail CI while real quoting/logic bugs still do. + - name: Run actionlint + uses: docker://rhysd/actionlint:1.7.12@sha256:b1934ee5f1c509618f2508e6eb47ee0d3520686341fec936f3b79331f9315667 + with: + args: -color + env: + SHELLCHECK_OPTS: --severity=warning + lint: name: Lint runs-on: ubuntu-latest @@ -18,6 +50,9 @@ jobs: steps: - name: Checkout code uses: actions/checkout@v4 + with: + # nx affected needs the merge-base with the PR target branch. + fetch-depth: 0 - name: Install pnpm uses: pnpm/action-setup@v4 @@ -31,7 +66,18 @@ jobs: - name: Install dependencies run: pnpm install --frozen-lockfile - - name: Lint all projects + # PRs lint only affected projects for faster feedback; root config + # or lockfile changes make every project affected, so the + # module-boundary and max-lines rules cannot be dodged this way. + - name: Lint affected projects (PR) + if: github.event_name == 'pull_request' + run: pnpm nx affected --target=lint --base=origin/${{ github.base_ref }} --head=HEAD --parallel=3 --output-style=static + env: + CI: true + NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false + + - name: Lint all projects (master) + if: github.event_name != 'pull_request' run: pnpm nx run-many --target=lint --all --parallel=3 --output-style=static env: CI: true diff --git a/.github/workflows/codeql-analysis.yml b/.github/workflows/codeql-analysis.yml index 104345e48..498b03e7c 100644 --- a/.github/workflows/codeql-analysis.yml +++ b/.github/workflows/codeql-analysis.yml @@ -40,31 +40,17 @@ jobs: - name: Checkout repository uses: actions/checkout@v4 - # Initializes the CodeQL tools for scanning. + # Initializes the CodeQL tools for scanning. PR runs analyze the merge + # commit checked out above (the modern default); JavaScript is + # interpreted, so no build step is needed before analysis. - name: Initialize CodeQL uses: github/codeql-action/init@v3 with: languages: ${{ matrix.language }} # If you wish to specify custom queries, you can do so here or in a config file. - # By default, queries listed here will override any specified in a config file. + # By default, queries listed here will override any specified in a config file. # Prefix the list here with "+" to use these queries and those in the config file. # queries: ./path/to/local/query, your-org/your-repo/queries@main - # Autobuild attempts to build any compiled languages (C/C++, C#, or Java). - # If this step fails, then you should remove it and run the build manually (see below) - - name: Autobuild - uses: github/codeql-action/autobuild@v3 - - # ℹ️ Command-line programs to run using the OS shell. - # 📚 https://git.io/JvXDl - - # ✏️ If the Autobuild fails above, remove it and uncomment the following three lines - # and modify them (or add more) to build your code if your project - # uses a compiled language - - #- run: | - # make bootstrap - # make release - - name: Perform CodeQL Analysis uses: github/codeql-action/analyze@v3 diff --git a/.github/workflows/docker.yml b/.github/workflows/docker.yml index 45ebf9fd3..c68aaf8a7 100644 --- a/.github/workflows/docker.yml +++ b/.github/workflows/docker.yml @@ -26,6 +26,14 @@ on: permissions: contents: read +# Superseded PR pushes cancel their still-running image build. Master/tag/ +# manual runs get a unique group (run_id): GitHub keeps at most one pending +# run per group even with cancel-in-progress: false, so a shared ref group +# could silently drop a queued publish between two rapid master pushes. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + jobs: build: name: Build Docker image diff --git a/.github/workflows/e2e-tests.yaml b/.github/workflows/e2e-tests.yaml index bacc48746..62ed7c390 100644 --- a/.github/workflows/e2e-tests.yaml +++ b/.github/workflows/e2e-tests.yaml @@ -4,11 +4,37 @@ on: push: branches: - master + paths-ignore: + - '**/*.md' + - 'docs/**' + - '.plans/**' + - '.codex/**' + - '.claude/**' + - 'apps/website/**' pull_request: branches: - master + paths-ignore: + - '**/*.md' + - 'docs/**' + - '.plans/**' + - '.codex/**' + - '.claude/**' + - 'apps/website/**' workflow_dispatch: +# Superseded PR pushes cancel their still-running E2E matrix (the most +# expensive per-PR runner time). Non-PR runs get a unique group (run_id): +# GitHub keeps at most one pending run per group even with +# cancel-in-progress: false, so a shared ref group could silently drop a +# queued master run between two rapid pushes. +concurrency: + group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.run_id }} + cancel-in-progress: ${{ github.event_name == 'pull_request' }} + +permissions: + contents: read + jobs: electron-e2e-tests: name: Electron E2E on ${{ matrix.os }} diff --git a/CLAUDE.md b/CLAUDE.md index 2935ced70..580c7e82e 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -193,7 +193,7 @@ Before finishing behavior changes or bug fixes, follow `Regression Prevention An ### Linting ```bash -# Lint all projects (what CI enforces on every PR) +# Lint all projects (CI runs this on master; PRs lint affected projects) pnpm run lint # Lint a single project @@ -201,7 +201,8 @@ nx lint web nx lint electron-backend ``` -CI runs lint for every project (`.github/workflows/ci.yml`). This enforces the +CI lints affected projects on PRs (`nx affected`) and every project on master +pushes (`.github/workflows/ci.yml`). This enforces the Nx module-boundary tags, the legacy bare-alias ban, and a `max-lines` ESLint rule (hard maximum 400 lines per TypeScript file). Pre-existing oversized files are baselined in `tools/eslint/max-lines-baseline.mjs`; regenerate the baseline diff --git a/docs/architecture/nx-workspace-boundaries.md b/docs/architecture/nx-workspace-boundaries.md index 44a43772c..17c085040 100644 --- a/docs/architecture/nx-workspace-boundaries.md +++ b/docs/architecture/nx-workspace-boundaries.md @@ -92,6 +92,9 @@ pulling the lazy-loaded workspace shell feature bundle into the initial chunk. ## CI Enforcement The `Lint` job in `.github/workflows/ci.yml` runs -`pnpm nx run-many --target=lint --all` on every PR, so +`pnpm nx affected --target=lint` on PRs and +`pnpm nx run-many --target=lint --all` on master pushes, so `@nx/enforce-module-boundaries` violations, legacy bare-alias imports, and -`max-lines` violations fail CI. Run `pnpm run lint` locally before pushing. +`max-lines` violations fail CI. Root config or lockfile changes mark every +project affected, so the boundary rules cannot be dodged on PRs. Run +`pnpm run lint` locally before pushing. diff --git a/docs/architecture/validation-map.md b/docs/architecture/validation-map.md index 387bfcea7..139fea90a 100644 --- a/docs/architecture/validation-map.md +++ b/docs/architecture/validation-map.md @@ -30,8 +30,9 @@ pnpm run lint # nx run-many --target=lint --all pnpm nx lint # single project ``` -The CI workflow (`.github/workflows/ci.yml`) runs lint for every project on -each PR. This enforces `@nx/enforce-module-boundaries` (scope/domain/type tag +The CI workflow (`.github/workflows/ci.yml`) lints affected projects on PRs +(`nx affected`) and every project on master pushes. +This enforces `@nx/enforce-module-boundaries` (scope/domain/type tag constraints), the legacy bare-alias ban, and the `max-lines` file-size rule (hard maximum 400 lines per TypeScript file). Files that predate the `max-lines` rule are baselined in `tools/eslint/max-lines-baseline.mjs`; after @@ -48,7 +49,11 @@ project is missing from the policy, a listed project no longer exists, or a Tier A entry has no test target. CI runs Tier A with coverage (uploaded to Codecov) and each Tier B/C project's `validationCommand` (falling back to `nx test`) without coverage; projects with an `e2e` target are skipped there -because the E2E workflow already runs them on every PR. +because the E2E workflow already runs them on every PR that touches app code. +Docs-only changes (Markdown, `docs/`, `.plans/`, `.codex/`, `.claude/`) and +`apps/website/**` changes skip the E2E workflow via `paths-ignore` — for those +PRs no E2E validation runs in CI, which is intentional: they cannot affect app +behavior. | Tier | Rule | Validation | | --- | --- | --- | diff --git a/nx.json b/nx.json index 26d293fe9..808b047e9 100644 --- a/nx.json +++ b/nx.json @@ -28,7 +28,19 @@ "{workspaceRoot}/.eslintrc.json", "{workspaceRoot}/.eslintignore", "{workspaceRoot}/eslint.config.mjs", - "{workspaceRoot}/tools/eslint-rules/**/*" + "{workspaceRoot}/tools/eslint-rules/**/*", + "{workspaceRoot}/tools/eslint/**/*" + ] + }, + "lint": { + "cache": true, + "inputs": [ + "default", + "{workspaceRoot}/.eslintrc.json", + "{workspaceRoot}/.eslintignore", + "{workspaceRoot}/eslint.config.mjs", + "{workspaceRoot}/tools/eslint-rules/**/*", + "{workspaceRoot}/tools/eslint/**/*" ] }, "@nx/jest:jest": { diff --git a/tools/packaging/project.json b/tools/packaging/project.json index 2c3097c63..709602181 100644 --- a/tools/packaging/project.json +++ b/tools/packaging/project.json @@ -59,6 +59,9 @@ "lint": { "inputs": [ "default", + "{workspaceRoot}/eslint.config.mjs", + "{workspaceRoot}/tools/eslint-rules/**/*", + "{workspaceRoot}/tools/eslint/**/*", "{workspaceRoot}/.github/workflows/build-and-make.yaml", "{workspaceRoot}/.github/workflows/publish-snap.yaml", "{workspaceRoot}/tools/packaging/prepare-linux-runtime-source-snapshot.cjs",