From a7575710fd0ff68da1b81473ea5205d96677e038 Mon Sep 17 00:00:00 2001 From: bgard68 Date: Tue, 25 Aug 2026 06:14:45 +0000 Subject: [PATCH] ci: scope the write permissions to the jobs that use them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit GHA002 flags contents: write and actions: write on dependabot-lockfix.yml, and actions: write on cleanup-runs.yml, with the remediation "drop it, or state which step requires it". Dropping any of the three breaks the workflow that declares it — each is used by exactly one step: dependabot-lockfix contents: write "Push the fix if anything changed" (git push origin HEAD:$BRANCH) dependabot-lockfix actions: write "Approve the pull-request checks on the fixed commit" (POST .../runs/{id}/approve) cleanup-runs actions: write "Delete old runs of the high-frequency workflows" (DELETE .../runs/{id}) So this takes the second half of the remediation, and takes it further than a comment. Both files declared their write scopes at the workflow level, where they apply to every job the file will ever contain. They now sit on the single job that needs them, with the workflow level dropped to contents: read — the shape container-build.yml already uses. A job added to either file tomorrow starts read-only instead of inheriting a push token. Behaviour is unchanged: the one existing job in each file resolves to exactly the permissions it had before. Whether the scanner is satisfied by a job-level grant is its business; least privilege at the workflow scope is worth having either way. Also normalises a stray CRLF at the end of dependabot-lockfix.yml, which .gitattributes (* text=auto eol=lf) already asks for. Co-Authored-By: Claude Opus 5 Claude-Session: https://claude.ai/code/session_019h7znwAftvs36vD4YwKMRf --- .github/workflows/cleanup-runs.yml | 12 ++++++++++-- .github/workflows/dependabot-lockfix.yml | 17 ++++++++++++++--- 2 files changed, 24 insertions(+), 5 deletions(-) diff --git a/.github/workflows/cleanup-runs.yml b/.github/workflows/cleanup-runs.yml index 26d8ec6..574549b 100644 --- a/.github/workflows/cleanup-runs.yml +++ b/.github/workflows/cleanup-runs.yml @@ -21,9 +21,10 @@ on: required: false default: '1' -# Deleting runs requires actions: write. Everything else stays least-privilege. +# Read-only at the workflow level, so any job added to this file starts with nothing. The +# write scope belongs to the single job below and is declared there, next to the step that +# needs it. permissions: - actions: write contents: read concurrency: @@ -34,6 +35,13 @@ jobs: cleanup: runs-on: ubuntu-latest timeout-minutes: 15 + + # actions: write is used by exactly one step below, "Delete old runs of the high-frequency + # workflows" (DELETE /repos/{repo}/actions/runs/{id}). Deleting runs is what this workflow + # is for, so dropping it leaves the job with nothing to do. + permissions: + actions: write + contents: read env: GH_TOKEN: ${{ github.token }} KEEP: ${{ github.event.inputs.keep || '1' }} diff --git a/.github/workflows/dependabot-lockfix.yml b/.github/workflows/dependabot-lockfix.yml index 60a32f3..4f242ef 100644 --- a/.github/workflows/dependabot-lockfix.yml +++ b/.github/workflows/dependabot-lockfix.yml @@ -44,9 +44,11 @@ on: required: true type: string +# Read-only at the workflow level, so any job added to this file starts with nothing. The two +# write scopes belong to the single job below and are declared there, next to the steps that +# need them. permissions: - contents: write # push the regenerated lock files to the Dependabot branch - actions: write # approve the PR checks GitHub creates after the bot push + contents: read concurrency: group: dependabot-lockfix-${{ github.event.workflow_run.head_branch || inputs.branch }} @@ -65,6 +67,15 @@ jobs: runs-on: ubuntu-latest timeout-minutes: 10 + # Both scopes are load-bearing, and each is used by exactly one step below: + # contents: write -> "Push the fix if anything changed" (git push origin HEAD:$BRANCH) + # actions: write -> "Approve the pull-request checks on the fixed commit" + # (POST /repos/{repo}/actions/runs/{id}/approve) + # Dropping either one breaks the repair this workflow exists to perform. + permissions: + contents: write + actions: write + env: BRANCH: ${{ github.event.workflow_run.head_branch || inputs.branch }} @@ -149,4 +160,4 @@ jobs: fi echo "::notice::Approved $approved pull-request workflow run(s) for $head." - +