name: CI on: push: branches: - master pull_request: branches: - 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@v7 # 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 release-note-gate: name: Release note gate if: github.event_name == 'pull_request' runs-on: ubuntu-latest timeout-minutes: 10 permissions: contents: read pull-requests: read steps: - name: Checkout code uses: actions/checkout@v7 # The gate scripts are dependency-free Node, so this job skips # pnpm install entirely and stays cheap. - name: Validate release note format run: node tools/release/build-release-notes.mjs --validate # Labels are fetched live rather than read from the (stale) event # payload, so applying `no-release-note` and re-running the check # works without a new push. Policy lives in a unit-tested script, # not in workflow bash. - name: Require a release note for user-visible changes env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} PR_NUMBER: ${{ github.event.pull_request.number }} run: | set -euo pipefail gh api "repos/${GITHUB_REPOSITORY}/pulls/${PR_NUMBER}/files?per_page=100" \ --paginate --jq '[.[] | {filename, status}]' | jq -s 'add // []' > /tmp/pr-files.json gh api "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/labels?per_page=100" \ --paginate --jq '[.[].name]' | jq -s 'add // []' > /tmp/pr-labels.json jq -n \ --slurpfile files /tmp/pr-files.json \ --slurpfile labels /tmp/pr-labels.json \ '{files: $files[0], labels: $labels[0]}' | node tools/release/check-release-note-gate.mjs lint: name: Lint runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Checkout code uses: actions/checkout@v7 with: # nx affected needs the merge-base with the PR target branch. fetch-depth: 0 - name: Install pnpm uses: pnpm/action-setup@v6.0.10 - name: Setup Node.js uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile # 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 NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false unit-and-typecheck: name: Unit Tests and Typechecks runs-on: ubuntu-latest timeout-minutes: 45 steps: - name: Checkout code uses: actions/checkout@v7 - name: Install pnpm uses: pnpm/action-setup@v6.0.10 - name: Setup Node.js uses: actions/setup-node@v7 with: node-version-file: '.nvmrc' cache: 'pnpm' - name: Install dependencies run: pnpm install --frozen-lockfile - name: Validate Nx dependency version policy run: pnpm run deps:nx:validate - name: Validate Vite dev-server transform filter patch run: pnpm run deps:vite:test - name: Validate electron-builder keychain password patch run: pnpm run deps:electron-builder:test - name: Validate stylesheet Nx inputs run: pnpm run styles:inputs:validate - name: Typecheck web and Electron entry points run: pnpm run typecheck:ci - name: Check i18n drift run: pnpm run i18n:check - name: Run Tier A unit coverage suite run: pnpm run coverage:ci env: CI: true NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false - name: Run Tier B/C validation commands run: node tools/coverage/check-coverage-policy.mjs --run-non-tier-a env: CI: true NX_TASKS_RUNNER_DYNAMIC_OUTPUT: false - name: Upload unit coverage artifact if: always() uses: actions/upload-artifact@v7 with: name: unit-coverage path: | coverage/merged/ retention-days: 14 - name: Upload unit coverage to Codecov if: always() uses: codecov/codecov-action@v7 with: files: ./coverage/merged/lcov.info,./coverage/merged/cobertura-coverage.xml flags: unit name: iptvnator-unit fail_ci_if_error: false handle_no_reports_found: true disable_search: true token: ${{ secrets.CODECOV_TOKEN }}