From a473e0cfd7852e88ef1c105844a39e720cd9411c Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 5 Sep 2026 13:48:26 -0400 Subject: [PATCH 1/3] ci: resolve CodeQL code-scanning alerts in workflows Clears all 27 open code-scanning alerts. Both classes are in GitHub Actions workflows; no Fortran or toolchain source is touched. missing-workflow-permissions (26 alerts, medium) No workflow declared a `permissions:` block, so every job inherited the repository-default GITHUB_TOKEN scope. None of the flagged jobs write to the repo through GITHUB_TOKEN -- the three that do publish use their own secrets (DOC_PUSH_URL, TAP_REPO_TOKEN, CODECOV_TOKEN) -- so a workflow-level `contents: read` default is sufficient everywhere. untrusted-checkout/high (1 alert, high) claude-code-review.yml runs privileged (pull_request_target and issue_comment: it holds CLAUDE_CODE_OAUTH_TOKEN plus pull-requests and issues write) and fetched fork commits into that workspace via `git fetch origin pull//head`. That step was dead code: nothing read FETCH_HEAD, and the pr_head_ref output was written but never consumed. The review already sources its diff and per-file context through gh API calls that treat fork content as data rather than as checked-out code, so the step and the unused output are removed instead of sandboxed. No behavior change. Claude-Session: https://claude.ai/code/session_01XPqfEaUBG7ZaZVzeMWnKHd --- .github/workflows/bench.yml | 4 ++++ .github/workflows/claude-code-review.yml | 17 ++++------------- .github/workflows/cleanliness.yml | 4 ++++ .github/workflows/convergence.yml | 4 ++++ .github/workflows/coverage-health.yml | 4 ++++ .github/workflows/coverage.yml | 4 ++++ .github/workflows/docs.yml | 4 ++++ .github/workflows/formatting.yml | 4 ++++ .github/workflows/fp-stability.yml | 4 ++++ .github/workflows/homebrew-release.yml | 4 ++++ .github/workflows/homebrew.yml | 4 ++++ .github/workflows/lint-toolchain.yml | 4 ++++ .github/workflows/pmd.yml | 4 ++++ .github/workflows/spelling.yml | 4 ++++ .github/workflows/test-toolchain-compat.yml | 4 ++++ .github/workflows/test.yml | 4 ++++ 16 files changed, 64 insertions(+), 13 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index f35054e59b..cdd00638ac 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -10,6 +10,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }}${{ github.event_name == 'pull_request_review' && format('-review-{0}', github.run_id) || '' }} cancel-in-progress: true +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: file-changes: name: Detect File Changes diff --git a/.github/workflows/claude-code-review.yml b/.github/workflows/claude-code-review.yml index 0bcc06dfbc..d5fc7a32d3 100644 --- a/.github/workflows/claude-code-review.yml +++ b/.github/workflows/claude-code-review.yml @@ -58,26 +58,17 @@ jobs: exit 1 fi - PR_HEAD_REF="$(gh pr view "$PR_NUMBER" --repo "${{ github.repository }}" --json headRefName --jq .headRefName)" - echo "pr_number=$PR_NUMBER" >> "$GITHUB_OUTPUT" - echo "pr_head_ref=$PR_HEAD_REF" >> "$GITHUB_OUTPUT" + # Base repo only. This job is privileged (pull_request_target / issue_comment: + # it holds secrets and a write-capable token), so the PR head is never checked + # out or fetched here. The diff and per-file context are pulled through the gh + # API in later steps and treated purely as data. - name: Checkout base repo uses: actions/checkout@v5 with: fetch-depth: 0 - - name: Fetch PR head - shell: bash - env: - PR_NUMBER: ${{ steps.mode.outputs.pr_number }} - run: | - set -euo pipefail - # Fetch the PR merge ref — works for both same-repo and fork PRs - # (fork branches don't exist on origin, but pull//head always does) - git fetch origin "pull/${PR_NUMBER}/head" - - name: Resolve review state id: state shell: bash diff --git a/.github/workflows/cleanliness.yml b/.github/workflows/cleanliness.yml index 8e7a1511fc..b5977397b1 100644 --- a/.github/workflows/cleanliness.yml +++ b/.github/workflows/cleanliness.yml @@ -10,6 +10,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: file-changes: name: Detect File Changes diff --git a/.github/workflows/convergence.yml b/.github/workflows/convergence.yml index 0acad5b59c..c876875bb7 100644 --- a/.github/workflows/convergence.yml +++ b/.github/workflows/convergence.yml @@ -10,6 +10,10 @@ on: env: OMPI_MCA_rmaps_base_oversubscribe: 1 +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: file-changes: name: Detect File Changes diff --git a/.github/workflows/coverage-health.yml b/.github/workflows/coverage-health.yml index e50d300272..16f501d36b 100644 --- a/.github/workflows/coverage-health.yml +++ b/.github/workflows/coverage-health.yml @@ -4,6 +4,10 @@ on: schedule: - cron: '0 7 * * *' # daily; loud if the refresh stopped working workflow_dispatch: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: health: if: github.repository == 'MFlowCode/MFC' diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index f331b5808b..0ef16c6855 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -11,6 +11,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: file-changes: name: Detect File Changes diff --git a/.github/workflows/docs.yml b/.github/workflows/docs.yml index 212db44b9a..341269c31e 100644 --- a/.github/workflows/docs.yml +++ b/.github/workflows/docs.yml @@ -8,6 +8,10 @@ on: branches: [master] pull_request: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: build: name: Build & Verify diff --git a/.github/workflows/formatting.yml b/.github/workflows/formatting.yml index 0689b891f3..580948f813 100644 --- a/.github/workflows/formatting.yml +++ b/.github/workflows/formatting.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: docs: name: Formatting diff --git a/.github/workflows/fp-stability.yml b/.github/workflows/fp-stability.yml index 1bf8313d90..963a87884b 100644 --- a/.github/workflows/fp-stability.yml +++ b/.github/workflows/fp-stability.yml @@ -35,6 +35,10 @@ on: types: [opened, synchronize, reopened, ready_for_review] workflow_dispatch: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: file-changes: name: Detect File Changes diff --git a/.github/workflows/homebrew-release.yml b/.github/workflows/homebrew-release.yml index a9f99e204d..fe616e96c4 100644 --- a/.github/workflows/homebrew-release.yml +++ b/.github/workflows/homebrew-release.yml @@ -21,6 +21,10 @@ on: type: boolean default: false +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: update-homebrew-tap: name: Update homebrew-mfc tap diff --git a/.github/workflows/homebrew.yml b/.github/workflows/homebrew.yml index 7511486be5..df17d218b0 100644 --- a/.github/workflows/homebrew.yml +++ b/.github/workflows/homebrew.yml @@ -11,6 +11,10 @@ on: - '.github/workflows/homebrew.yml' workflow_dispatch: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: # Fast smoke tests that run before expensive operations smoke-test: diff --git a/.github/workflows/lint-toolchain.yml b/.github/workflows/lint-toolchain.yml index 2d7431c245..13f6fa825c 100644 --- a/.github/workflows/lint-toolchain.yml +++ b/.github/workflows/lint-toolchain.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: lint-toolchain: name: Lint Toolchain diff --git a/.github/workflows/pmd.yml b/.github/workflows/pmd.yml index d440270176..b873028230 100644 --- a/.github/workflows/pmd.yml +++ b/.github/workflows/pmd.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: pmd: name: PMD diff --git a/.github/workflows/spelling.yml b/.github/workflows/spelling.yml index ca2e5d8015..b2e0352169 100644 --- a/.github/workflows/spelling.yml +++ b/.github/workflows/spelling.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: run: name: Spell Check diff --git a/.github/workflows/test-toolchain-compat.yml b/.github/workflows/test-toolchain-compat.yml index e024091769..2a60519531 100644 --- a/.github/workflows/test-toolchain-compat.yml +++ b/.github/workflows/test-toolchain-compat.yml @@ -6,6 +6,10 @@ on: pull_request: workflow_dispatch: +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: test-toolchain: name: "Python ${{ matrix.python-version }}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 8c57bb5595..ba88126144 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -12,6 +12,10 @@ concurrency: group: ${{ github.workflow }}-${{ github.event_name == 'push' && github.sha || github.ref }} cancel-in-progress: ${{ github.event_name != 'push' }} +# Least-privilege default: no job in this workflow writes to the repo. +permissions: + contents: read + jobs: lint-gate: name: Lint Gate From 5f4be217a2d9587b02129b3e107c4da415d265d4 Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 5 Sep 2026 13:55:35 -0400 Subject: [PATCH 2/3] ci: grant pull-requests: read where paths-filter needs it Declaring a `permissions:` block sets every unlisted scope to none, which dropped `pull-requests` for the six workflows whose file-changes job runs dorny/paths-filter. That action defaults to `token: ${{ github.token }}` and reads the PR's changed-file list via pulls.listFiles on pull_request events, so it needs pull-requests: read. test.yml also passes `list-files: shell`, which depends on that list directly. Applied to exactly the six paths-filter users: bench, cleanliness, convergence, coverage, fp-stability, test. The other nine workflows reference no GitHub API beyond contents and stay at contents: read. Claude-Session: https://claude.ai/code/session_01XPqfEaUBG7ZaZVzeMWnKHd --- .github/workflows/bench.yml | 3 +++ .github/workflows/cleanliness.yml | 3 +++ .github/workflows/convergence.yml | 3 +++ .github/workflows/coverage.yml | 3 +++ .github/workflows/fp-stability.yml | 3 +++ .github/workflows/test.yml | 3 +++ 6 files changed, 18 insertions(+) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index cdd00638ac..19368961e4 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -11,8 +11,11 @@ concurrency: cancel-in-progress: true # Least-privilege default: no job in this workflow writes to the repo. +# pull-requests: read is required by dorny/paths-filter, which reads the PR's +# changed-file list through the API on pull_request events. permissions: contents: read + pull-requests: read jobs: file-changes: diff --git a/.github/workflows/cleanliness.yml b/.github/workflows/cleanliness.yml index b5977397b1..b563ff40a4 100644 --- a/.github/workflows/cleanliness.yml +++ b/.github/workflows/cleanliness.yml @@ -11,8 +11,11 @@ concurrency: cancel-in-progress: true # Least-privilege default: no job in this workflow writes to the repo. +# pull-requests: read is required by dorny/paths-filter, which reads the PR's +# changed-file list through the API on pull_request events. permissions: contents: read + pull-requests: read jobs: file-changes: diff --git a/.github/workflows/convergence.yml b/.github/workflows/convergence.yml index c876875bb7..1b5656d90b 100644 --- a/.github/workflows/convergence.yml +++ b/.github/workflows/convergence.yml @@ -11,8 +11,11 @@ env: OMPI_MCA_rmaps_base_oversubscribe: 1 # Least-privilege default: no job in this workflow writes to the repo. +# pull-requests: read is required by dorny/paths-filter, which reads the PR's +# changed-file list through the API on pull_request events. permissions: contents: read + pull-requests: read jobs: file-changes: diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 0ef16c6855..c19d16cf87 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,8 +12,11 @@ concurrency: cancel-in-progress: true # Least-privilege default: no job in this workflow writes to the repo. +# pull-requests: read is required by dorny/paths-filter, which reads the PR's +# changed-file list through the API on pull_request events. permissions: contents: read + pull-requests: read jobs: file-changes: diff --git a/.github/workflows/fp-stability.yml b/.github/workflows/fp-stability.yml index 963a87884b..a075eada1a 100644 --- a/.github/workflows/fp-stability.yml +++ b/.github/workflows/fp-stability.yml @@ -36,8 +36,11 @@ on: workflow_dispatch: # Least-privilege default: no job in this workflow writes to the repo. +# pull-requests: read is required by dorny/paths-filter, which reads the PR's +# changed-file list through the API on pull_request events. permissions: contents: read + pull-requests: read jobs: file-changes: diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index ba88126144..b814f38ac8 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,8 +13,11 @@ concurrency: cancel-in-progress: ${{ github.event_name != 'push' }} # Least-privilege default: no job in this workflow writes to the repo. +# pull-requests: read is required by dorny/paths-filter, which reads the PR's +# changed-file list through the API on pull_request events. permissions: contents: read + pull-requests: read jobs: lint-gate: From f62c5cd27bcd5ad0d3cabc3605a236bd24f9e50f Mon Sep 17 00:00:00 2001 From: Spencer Bryngelson Date: Sat, 5 Sep 2026 14:32:43 -0400 Subject: [PATCH 3/3] ci: scope pull-requests: read to the file-changes job Granting pull-requests: read at the workflow level handed it to every job in these six files, including the multi-hour NVHPC container lanes and the self-hosted Frontier and Phoenix runs, none of which touch the PR API. Only file-changes needs it: it is the sole job running dorny/paths-filter, it finishes in about ten seconds, and it holds no other privilege. Move the grant there and return the workflow default to contents: read. Job-level permissions replace the workflow default outright rather than merging with it, so contents: read is restated alongside pull-requests: read in the job block; a bare pull-requests: read there would have revoked contents and broken the job's own checkout. Claude-Session: https://claude.ai/code/session_01XPqfEaUBG7ZaZVzeMWnKHd --- .github/workflows/bench.yml | 10 +++++++--- .github/workflows/cleanliness.yml | 10 +++++++--- .github/workflows/convergence.yml | 10 +++++++--- .github/workflows/coverage.yml | 10 +++++++--- .github/workflows/fp-stability.yml | 10 +++++++--- .github/workflows/test.yml | 10 +++++++--- 6 files changed, 42 insertions(+), 18 deletions(-) diff --git a/.github/workflows/bench.yml b/.github/workflows/bench.yml index 19368961e4..097b55dcc6 100644 --- a/.github/workflows/bench.yml +++ b/.github/workflows/bench.yml @@ -11,11 +11,8 @@ concurrency: cancel-in-progress: true # Least-privilege default: no job in this workflow writes to the repo. -# pull-requests: read is required by dorny/paths-filter, which reads the PR's -# changed-file list through the API on pull_request events. permissions: contents: read - pull-requests: read jobs: file-changes: @@ -23,6 +20,13 @@ jobs: if: > github.event_name != 'pull_request_review' || github.event.review.user.type != 'Bot' + # Job-level permissions replace the workflow default outright rather than + # merging with it, so contents must be restated here. paths-filter reads the + # PR's changed-file list via pulls.listFiles; this is the only job that needs + # it, so it is granted here instead of workflow-wide. + permissions: + contents: read + pull-requests: read runs-on: 'ubuntu-latest' outputs: checkall: ${{ steps.changes.outputs.checkall }} diff --git a/.github/workflows/cleanliness.yml b/.github/workflows/cleanliness.yml index b563ff40a4..dd1003042a 100644 --- a/.github/workflows/cleanliness.yml +++ b/.github/workflows/cleanliness.yml @@ -11,15 +11,19 @@ concurrency: cancel-in-progress: true # Least-privilege default: no job in this workflow writes to the repo. -# pull-requests: read is required by dorny/paths-filter, which reads the PR's -# changed-file list through the API on pull_request events. permissions: contents: read - pull-requests: read jobs: file-changes: name: Detect File Changes + # Job-level permissions replace the workflow default outright rather than + # merging with it, so contents must be restated here. paths-filter reads the + # PR's changed-file list via pulls.listFiles; this is the only job that needs + # it, so it is granted here instead of workflow-wide. + permissions: + contents: read + pull-requests: read runs-on: 'ubuntu-latest' outputs: checkall: ${{ steps.changes.outputs.checkall }} diff --git a/.github/workflows/convergence.yml b/.github/workflows/convergence.yml index 1b5656d90b..e4dd8d3731 100644 --- a/.github/workflows/convergence.yml +++ b/.github/workflows/convergence.yml @@ -11,15 +11,19 @@ env: OMPI_MCA_rmaps_base_oversubscribe: 1 # Least-privilege default: no job in this workflow writes to the repo. -# pull-requests: read is required by dorny/paths-filter, which reads the PR's -# changed-file list through the API on pull_request events. permissions: contents: read - pull-requests: read jobs: file-changes: name: Detect File Changes + # Job-level permissions replace the workflow default outright rather than + # merging with it, so contents must be restated here. paths-filter reads the + # PR's changed-file list via pulls.listFiles; this is the only job that needs + # it, so it is granted here instead of workflow-wide. + permissions: + contents: read + pull-requests: read runs-on: ubuntu-latest outputs: checkall: ${{ steps.changes.outputs.checkall }} diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index c19d16cf87..357deef541 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -12,15 +12,19 @@ concurrency: cancel-in-progress: true # Least-privilege default: no job in this workflow writes to the repo. -# pull-requests: read is required by dorny/paths-filter, which reads the PR's -# changed-file list through the API on pull_request events. permissions: contents: read - pull-requests: read jobs: file-changes: name: Detect File Changes + # Job-level permissions replace the workflow default outright rather than + # merging with it, so contents must be restated here. paths-filter reads the + # PR's changed-file list via pulls.listFiles; this is the only job that needs + # it, so it is granted here instead of workflow-wide. + permissions: + contents: read + pull-requests: read runs-on: 'ubuntu-latest' outputs: checkall: ${{ steps.changes.outputs.checkall }} diff --git a/.github/workflows/fp-stability.yml b/.github/workflows/fp-stability.yml index a075eada1a..68a6ac4eac 100644 --- a/.github/workflows/fp-stability.yml +++ b/.github/workflows/fp-stability.yml @@ -36,15 +36,19 @@ on: workflow_dispatch: # Least-privilege default: no job in this workflow writes to the repo. -# pull-requests: read is required by dorny/paths-filter, which reads the PR's -# changed-file list through the API on pull_request events. permissions: contents: read - pull-requests: read jobs: file-changes: name: Detect File Changes + # Job-level permissions replace the workflow default outright rather than + # merging with it, so contents must be restated here. paths-filter reads the + # PR's changed-file list via pulls.listFiles; this is the only job that needs + # it, so it is granted here instead of workflow-wide. + permissions: + contents: read + pull-requests: read runs-on: ubuntu-latest outputs: checkall: ${{ steps.changes.outputs.checkall }} diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index b814f38ac8..1119bed64b 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -13,11 +13,8 @@ concurrency: cancel-in-progress: ${{ github.event_name != 'push' }} # Least-privilege default: no job in this workflow writes to the repo. -# pull-requests: read is required by dorny/paths-filter, which reads the PR's -# changed-file list through the API on pull_request events. permissions: contents: read - pull-requests: read jobs: lint-gate: @@ -57,6 +54,13 @@ jobs: file-changes: name: Detect File Changes + # Job-level permissions replace the workflow default outright rather than + # merging with it, so contents must be restated here. paths-filter reads the + # PR's changed-file list via pulls.listFiles; this is the only job that needs + # it, so it is granted here instead of workflow-wide. + permissions: + contents: read + pull-requests: read runs-on: 'ubuntu-latest' outputs: checkall: ${{ steps.changes.outputs.checkall }}