name: Security

on:
    pull_request:
    push:
        branches:
            - main
    schedule:
        - cron: '0 5 * * 1'
    workflow_dispatch:

permissions:
    contents: read
    security-events: write

concurrency:
    group: ${{ github.workflow }}-${{ github.event.pull_request.number || github.ref || github.run_id }}
    cancel-in-progress: true

jobs:
    changes:
        name: Classify PR changes
        runs-on: ubuntu-latest
        outputs:
            docs_only: ${{ steps.classify.outputs.docs_only }}
        steps:
            - id: classify
              name: Detect docs-only pull requests
              uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9
              with:
                  script: |
                      if (context.eventName !== 'pull_request' && context.eventName !== 'push') {
                        core.setOutput('docs_only', 'false');
                        return;
                      }

                      const docsOnlyPatterns = [
                        /^docs\//,
                        /^[^/]+\.md$/,
                        /^COPYING$/,
                        /^\.github\/ISSUE_TEMPLATE\//,
                        /^\.github\/dependabot\.yml$/,
                        /^\.github\/labeler\.yml$/,
                        /^\.github\/labels\.yml$/,
                        /^\.github\/pull_request_template\.md$/,
                        /^CLAUDE\.md$/,
                        /^CODE_OF_CONDUCT\.md$/,
                        /^CONTRIBUTING\.md$/,
                        /^README\.en\.md$/,
                        /^README\.md$/,
                        /^SECURITY\.md$/,
                        /^SUPPORT\.md$/,
                      ];

                      let files = [];

                      if (context.eventName === 'pull_request') {
                        files = await github.paginate(github.rest.pulls.listFiles, {
                          ...context.repo,
                          pull_number: context.issue.number,
                          per_page: 100,
                        });
                      } else if (context.eventName === 'push') {
                        const before = context.payload.before;
                        const after = context.payload.after;

                        if (!before || !after || /^0+$/.test(before)) {
                          core.setOutput('docs_only', 'false');
                          return;
                        }

                        const comparison = await github.rest.repos.compareCommits({
                          ...context.repo,
                          base: before,
                          head: after,
                        });

                        files = comparison.data.files ?? [];

                        // GitHub truncates compareCommits file lists at 300 entries. Fall back to full CI when unsure.
                        if (files.length >= 300) {
                          core.setOutput('docs_only', 'false');
                          return;
                        }
                      }

                      const docsOnly =
                        files.length > 0 &&
                        files.every((file) => docsOnlyPatterns.some((pattern) => pattern.test(file.filename)));

                      core.setOutput('docs_only', docsOnly ? 'true' : 'false');

            - name: Report classification
              run: echo "docs_only=${{ steps.classify.outputs.docs_only }}"

    codeql:
        name: CodeQL (${{ matrix.language }})
        needs: changes
        strategy:
            fail-fast: false
            matrix:
                include:
                    - language: javascript-typescript
                      os: ubuntu-latest
                    - language: rust
                      os: windows-latest
        runs-on: ${{ matrix.os }}
        steps:
            - name: Skip CodeQL for docs-only PRs
              if: needs.changes.outputs.docs_only == 'true'
              run: echo "CodeQL is not required for docs-only pull requests (${{ matrix.language }})."

            - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6
              if: needs.changes.outputs.docs_only != 'true'

            - name: Initialize CodeQL
              if: needs.changes.outputs.docs_only != 'true'
              uses: github/codeql-action/init@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4
              with:
                  languages: ${{ matrix.language }}

            - name: Set up Node
              if: needs.changes.outputs.docs_only != 'true' && matrix.language == 'rust'
              uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
              with:
                  node-version: 22

            - name: Set up pnpm
              if: needs.changes.outputs.docs_only != 'true' && matrix.language == 'rust'
              uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6

            - name: Install Rust stable
              if: needs.changes.outputs.docs_only != 'true' && matrix.language == 'rust'
              uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

            - name: Install dependencies
              if: needs.changes.outputs.docs_only != 'true' && matrix.language == 'rust'
              run: pnpm install --frozen-lockfile

            - name: Build Rust targets for analysis
              if: needs.changes.outputs.docs_only != 'true' && matrix.language == 'rust'
              run: cargo check --manifest-path apps/desktop/src-tauri/Cargo.toml --all-targets --profile ci-check

            - name: Analyze
              if: needs.changes.outputs.docs_only != 'true'
              uses: github/codeql-action/analyze@7211b7c8077ea37d8641b6271f6a365a22a5fbfa # v4

    audits:
        name: Dependency audits
        if: github.event_name == 'schedule' || github.event_name == 'workflow_dispatch'
        runs-on: ubuntu-latest
        steps:
            - uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6

            - uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6
              with:
                  node-version: 22
                  cache: pnpm

            - uses: pnpm/action-setup@0e279bb959325dab635dd2c09392533439d90093 # v6

            - name: Install Rust stable
              uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable

            - name: Install dependencies
              run: pnpm install --frozen-lockfile

            - name: Audit npm dependencies
              run: pnpm audit --prod --audit-level high

            - name: Install cargo-audit
              run: cargo install cargo-audit --locked

            - name: Audit Rust dependencies
              run: cargo audit --manifest-path apps/desktop/src-tauri/Cargo.toml
