diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b75f96b96..818495d14 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -20,6 +20,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - uses: actions/setup-python@a26af69be951a213d495a4c3e4e4022e16d87065 # v5 with: python-version: "3.11" @@ -31,6 +33,8 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 + with: + persist-credentials: false - uses: actions/setup-node@49933ea5288caeca8642d1e84afbd3f7d6820020 # v4 with: node-version: "20" @@ -53,6 +57,7 @@ jobs: - uses: actions/checkout@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 with: fetch-depth: 0 + persist-credentials: false # Detect whether this PR only touches documentation files. # If so, skip the expensive pytest run while still reporting a passing check. diff --git a/.github/workflows/docker-publish.yml b/.github/workflows/docker-publish.yml index 528a5ef6c..5e822ab07 100644 --- a/.github/workflows/docker-publish.yml +++ b/.github/workflows/docker-publish.yml @@ -22,7 +22,6 @@ concurrency: permissions: contents: read - packages: write env: REGISTRY: ghcr.io @@ -32,6 +31,9 @@ jobs: build: name: build (${{ matrix.arch }}) runs-on: ${{ matrix.runner }} + permissions: + contents: read + packages: write strategy: fail-fast: false matrix: @@ -44,6 +46,8 @@ jobs: runner: ubuntu-24.04-arm steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Set up Buildx uses: docker/setup-buildx-action@d7f5e7f509e45cec5c76c4d5afdd7de93d0b3df5 # v4.1.0 - name: Log in to GHCR @@ -78,8 +82,13 @@ jobs: name: merge manifest + tag runs-on: ubuntu-latest needs: build + permissions: + contents: read + packages: write steps: - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 + with: + persist-credentials: false - name: Read APP_VERSION + short sha id: ver run: | @@ -116,6 +125,8 @@ jobs: run: | tags=$(jq -cr '.tags | map("-t " + .) | join(" ")' <<< "$DOCKER_METADATA_OUTPUT_JSON") digests=$(printf "${REGISTRY}/${IMAGE_NAME}@sha256:%s " *) + # word-splitting is intended: $tags and $digests each expand to multiple args + # shellcheck disable=SC2086 docker buildx imagetools create $tags $digests env: REGISTRY: ${{ env.REGISTRY }} diff --git a/.github/workflows/issue-description-check.yml b/.github/workflows/issue-description-check.yml index 5dc3fdf82..3d0cf094e 100644 --- a/.github/workflows/issue-description-check.yml +++ b/.github/workflows/issue-description-check.yml @@ -14,10 +14,11 @@ jobs: # Skip bots (Dependabot, release-drafter, etc.) if: ${{ github.event.issue.user.type != 'Bot' }} steps: - - uses: actions/checkout@v4 + - uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3 with: sparse-checkout: .github/scripts + persist-credentials: false - - uses: actions/github-script@v7 + - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: script: return require('./.github/scripts/check-issue-description.js')({github, context, core}) diff --git a/.github/workflows/pr-description-check.yml b/.github/workflows/pr-description-check.yml index 899d74c7a..c8fbe4b0f 100644 --- a/.github/workflows/pr-description-check.yml +++ b/.github/workflows/pr-description-check.yml @@ -1,26 +1,25 @@ name: ci / PR checks on: - pull_request_target: + # pull_request_target runs in the base-repo context (has secrets) so the check + # works on fork PRs. Safe here: the checkout pins to the base branch (no fork + # code runs) and the scripts only read context.payload and call the GitHub API. + pull_request_target: # zizmor: ignore[dangerous-triggers] types: [opened, edited, synchronize, reopened, ready_for_review] -# pull_request_target runs in the base-repo context (has secrets). -# The checkout below pins to the base branch so no fork code is executed. -# The script only reads context.payload and calls the GitHub API. -# Least privilege: contents:read for the base-ref checkout, pull-requests:write -# to add/remove labels and post comments on PRs, and issues:write for the same -# on real issues. NOTE: modifying a *pull request's* labels/comments needs the -# `pull-requests` scope even though the REST path is under `/issues/{n}/...`; -# `issues:write` alone returns 403 on PRs. -permissions: - contents: read - pull-requests: write - issues: write +# Default-deny at the workflow level; each job opts into only the scopes it needs. +# Note: modifying a PR's labels/comments needs pull-requests:write even though the +# REST path is under /issues/{n}/...; issues:write alone returns 403 on PRs. +permissions: {} jobs: check-description: name: Check PR description runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + issues: write # Skip bots: they open PRs programmatically and have their own process. if: github.event.pull_request.user.type != 'Bot' steps: @@ -28,6 +27,7 @@ jobs: with: ref: ${{ github.base_ref }} sparse-checkout: .github/scripts + persist-credentials: false - uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 with: @@ -36,6 +36,7 @@ jobs: check-title: name: Check PR title (Conventional Commits) runs-on: ubuntu-latest + permissions: {} # Skip bots: they open PRs programmatically and have their own process. if: github.event.pull_request.user.type != 'Bot' steps: @@ -59,6 +60,9 @@ jobs: check-mergeable: name: Flag unmergeable PRs runs-on: ubuntu-latest + permissions: + pull-requests: write + issues: write # Skip bots: they open PRs programmatically and have their own process. if: github.event.pull_request.user.type != 'Bot' steps: