diff --git a/.github/actions/setup/action.yaml b/.github/actions/setup/action.yaml index fec2ea8f..bac64be7 100644 --- a/.github/actions/setup/action.yaml +++ b/.github/actions/setup/action.yaml @@ -1,10 +1,21 @@ name: 'Setup Action' description: 'Checks out the repo, sets up node, and installs dependencies' +inputs: + persist-credentials: + description: >- + Whether the checkout should keep the GITHUB_TOKEN credential in + the local git config. Only callers that push commits after this + action runs (e.g. the upstream-release-docs bootstrap step) + need this set to 'true'. + required: false + default: 'false' runs: using: 'composite' steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: ${{ inputs.persist-credentials }} - name: Set up Node.js uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 diff --git a/.github/workflows/_static-checks.yaml b/.github/workflows/_static-checks.yaml index d2959ad8..97aaa924 100644 --- a/.github/workflows/_static-checks.yaml +++ b/.github/workflows/_static-checks.yaml @@ -13,6 +13,8 @@ jobs: steps: - name: Checkout repository uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false - name: Setup uses: ./.github/actions/setup diff --git a/.github/workflows/autogen-docs-notify.yml b/.github/workflows/autogen-docs-notify.yml index c8eafd78..a751744c 100644 --- a/.github/workflows/autogen-docs-notify.yml +++ b/.github/workflows/autogen-docs-notify.yml @@ -55,7 +55,12 @@ name: Autogen Docs Slack Notify # directly as an env var below (DOCS_SLACK_CHANNEL_ID), not as a secret. # --------------------------------------------------------------------- -on: +# workflow_run is used deliberately here, not as an oversight. See +# the SECURITY MODEL comment above: it always runs the trusted +# main-branch definition, never checks out or executes PR-supplied +# code, and the Claude step has no Slack token or network access +# beyond `gh`. +on: # zizmor: ignore[dangerous-triggers] workflow_run: workflows: ['Upstream Release Docs'] types: [completed] @@ -96,11 +101,14 @@ jobs: # Sets skip=true and exits cleanly when the PR is out of scope. - name: Resolve PR and check eligibility id: pr + env: + DISPATCH_PR_NUMBER: ${{ inputs.pr_number }} + WORKFLOW_RUN_PR_NUMBER: ${{ github.event.workflow_run.pull_requests[0].number }} run: | if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then - PR_NUMBER="${{ inputs.pr_number }}" + PR_NUMBER="$DISPATCH_PR_NUMBER" else - PR_NUMBER="${{ github.event.workflow_run.pull_requests[0].number }}" + PR_NUMBER="$WORKFLOW_RUN_PR_NUMBER" if [ -z "$PR_NUMBER" ]; then echo "No PR associated with this workflow run; skipping." echo "skip=true" >> "$GITHUB_OUTPUT" @@ -136,6 +144,8 @@ jobs: - name: Checkout base branch if: steps.pr.outputs.skip != 'true' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false # STEP 1 — Claude composes content only. Tools limited to gh + Write. # No Slack token in this step's env; no curl tool. So even a prompt # injection from PR content cannot exfiltrate the Slack token (absent) diff --git a/.github/workflows/claude.yml b/.github/workflows/claude.yml index 4699e338..20cdbbdd 100644 --- a/.github/workflows/claude.yml +++ b/.github/workflows/claude.yml @@ -47,6 +47,7 @@ jobs: with: fetch-depth: 1 sparse-checkout: .github + persist-credentials: false # Checkout + Node + deps so Claude can run build/lint/format # scripts when asked. diff --git a/.github/workflows/upstream-release-docs.yml b/.github/workflows/upstream-release-docs.yml index 669ba84e..95c30dc3 100644 --- a/.github/workflows/upstream-release-docs.yml +++ b/.github/workflows/upstream-release-docs.yml @@ -151,16 +151,23 @@ jobs: # `), bump the YAML, create the PR, and emit its number + # branch so the rest of the workflow proceeds as if Renovate had # opened it. + # persist-credentials must stay true here: the "Bootstrap + # branch and PR" step below pushes a new branch with this + # checkout's credential. No artifact upload runs in this job, + # and only trusted, pinned actions run afterward. - name: Checkout dispatching branch for bootstrap if: steps.pr.outputs.mode == 'bootstrap' uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: + with: # zizmor: ignore[artipacked] ref: ${{ github.ref_name }} fetch-depth: 0 + persist-credentials: true - name: Setup (bootstrap) if: steps.pr.outputs.mode == 'bootstrap' uses: ./.github/actions/setup + with: + persist-credentials: true - name: Set up Git (bootstrap) if: steps.pr.outputs.mode == 'bootstrap' @@ -285,11 +292,17 @@ jobs: echo "PR #$PR_NUMBER is already a draft; nothing to do." fi + # persist-credentials must stay true here: the "Commit + push + # refreshed reference assets" and "Commit and push" steps below + # both push to this branch with this checkout's credential. No + # artifact upload runs in this job, and only trusted, pinned + # actions run afterward. - name: Checkout PR branch uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 - with: + with: # zizmor: ignore[artipacked] ref: ${{ steps.eff.outputs.head_ref }} fetch-depth: 0 + persist-credentials: true # NOTE: we inline the node/deps setup rather than calling the # ./.github/actions/setup composite because that composite starts diff --git a/.github/workflows/zizmor.yaml b/.github/workflows/zizmor.yaml new file mode 100644 index 00000000..76a486dc --- /dev/null +++ b/.github/workflows/zizmor.yaml @@ -0,0 +1,27 @@ +name: 'GitHub Actions Security (zizmor)' + +on: + push: + branches: [main] + pull_request: + +permissions: {} + +jobs: + zizmor: + name: zizmor + runs-on: ubuntu-latest + permissions: + contents: read + steps: + - name: Checkout + uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1 + with: + persist-credentials: false + + - name: Run zizmor + uses: zizmorcore/zizmor-action@3dc1ecc9bcb9e94e9b2c709687979e1298497054 # v0.6.2 + with: + inputs: ./.github/workflows/ + advanced-security: 'false' + annotations: 'true'