ci: cut PR runner waste and harden workflow permissions (#1226)

Pipeline audit follow-up: reduce wasted runner time on PRs and tighten CI
security, without reducing what actually gets validated.

Runner-time waste:
- Concurrency with PR-only cancel-in-progress on CI, E2E, and docker-build,
  so a new push cancels the previous commit's still-running checks. Non-PR
  runs use the unique run_id as the group, because GitHub keeps at most one
  pending run per group even with cancel-in-progress: false — a shared ref
  group could silently drop a queued master run.
- paths-ignore for docs-only changes (Markdown, docs/, .plans/, .codex/,
  .claude/) on the Electron build matrix and the E2E suites; E2E also skips
  apps/website/**. The build workflow keeps apps/website/** because its Linux
  job builds the website to verify AppStream assets. Tag pushes are
  unaffected: GitHub does not evaluate paths filters for tags.
- PRs lint affected projects only; master pushes keep the full run-many.
  Lint-global inputs (eslint.config.mjs, tools/eslint/**) now mark all 41
  lint projects affected, including the run-commands targets database and
  packaging, so the max-lines baseline cannot be widened without lint.

Hardening:
- Explicit least-privilege permissions on CI, E2E, and build-and-make; the
  create-release job keeps its job-level contents: write. The repository
  default workflow token was switched to read-only.
- New actionlint job (image pinned by digest, shellcheck at warning+), with
  the shared-anchor false positive suppressed in .github/actionlint.yaml.
  Fixed one real finding: unquoted $GITHUB_OUTPUT.
- .github/dependabot.yml: weekly cadence, minor+patch grouped per ecosystem
  (npm, GitHub Actions, Docker), majors stay individual PRs.

Docs updated: CLAUDE.md, docs/architecture/nx-workspace-boundaries.md, and
docs/architecture/validation-map.md now describe affected-lint on PRs and the
E2E path-filter exceptions.
This commit is contained in:
4gray authored and GitHub committed 2026-07-25 14:37:40 +02:00
1 parent e24da447c1
commit cfa602d5b1
12 files changed
+197 -28

No files matched your search

+31 -1
View File
@@ -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-<n> 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
+47 -1
View File
@@ -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
+4 -18
View File
@@ -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
+8
View File
@@ -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
+26
View File
@@ -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 }}