diff --git a/.github/actionlint.yml b/.github/actionlint.yml new file mode 100644 index 0000000..813317a --- /dev/null +++ b/.github/actionlint.yml @@ -0,0 +1,13 @@ +# Configuration for actionlint, run by the `actionlint` job in +# `.github/workflows/reusable-code-quality.yml`. +paths: + .github/workflows/**/*.{yml,yaml}: + ignore: + # `copilot-requests` is a real permission scope - it is what lets a + # workflow authenticate the Copilot CLI with the built-in `GITHUB_TOKEN` + # instead of a personal access token - but actionlint's hard-coded scope + # list has not caught up with it yet, so it reports every use as unknown. + # Drop this entry once actionlint ships the scope, and it will go back to + # catching genuine typos in permission names. + # See https://docs.github.com/en/copilot/how-tos/copilot-cli/use-copilot-cli-in-actions + - 'unknown permission scope "copilot-requests"' diff --git a/.github/workflows/issue-triage.yml b/.github/workflows/issue-triage.yml index bf816ae..0795944 100644 --- a/.github/workflows/issue-triage.yml +++ b/.github/workflows/issue-triage.yml @@ -17,7 +17,10 @@ permissions: issues: write pull-requests: write contents: read - models: read + # A caller can only cap a reusable workflow's permissions, so the scope the + # Copilot CLI needs has to be granted here too. `models: read` was for the + # GitHub Models provider, which `actions/ai-inference` v3 removed. + copilot-requests: write # A caller can only cap a reusable workflow's permissions, never raise them, # so `actions: write` has to be granted here for the dispatch job downstream # to work at all. The reusable workflow narrows it to that single job, so the diff --git a/.github/workflows/reusable-issue-triage.yml b/.github/workflows/reusable-issue-triage.yml index 02f051d..538b23d 100644 --- a/.github/workflows/reusable-issue-triage.yml +++ b/.github/workflows/reusable-issue-triage.yml @@ -9,17 +9,30 @@ name: Issue and PR Triage required: false type: string +# Every job narrows this further, and no job holds both `copilot-requests: +# write` and a write scope. `actions/ai-inference` v3 dropped the GitHub Models +# provider and now runs the Copilot CLI, installed from npm at run time, with a +# token in its environment - so inference and label writes are split into +# separate jobs and the token that can write never reaches the CLI. permissions: - issues: write - pull-requests: write contents: read - models: read jobs: triage-new-item: name: Triage New Issue or PR if: github.event_name == 'issues' || github.event_name == 'pull_request_target' runs-on: ubuntu-latest + # Read-only, plus the scope that pays for the inference. This is the job + # that runs the Copilot CLI, so it is the job that must not be able to + # write anything. + permissions: + contents: read + issues: read + pull-requests: read + copilot-requests: write + outputs: + response: ${{ steps.ai-triage.outputs.response }} + labels: ${{ steps.get-labels.outputs.result }} steps: - name: Get available labels id: get-labels @@ -34,10 +47,38 @@ jobs: const labelNames = labels.data.map(label => label.name); return labelNames.join(', '); + # The Copilot CLI is not preinstalled on GitHub-hosted runners, and + # `actions/ai-inference` v3 only shells out to it - it never installs it. + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: lts/* + + # Deliberately unpinned, unlike everything else here: authenticating with + # `GITHUB_TOKEN` instead of a PAT only works on recent CLI releases, so a + # pin would be a slow trap rather than a safeguard. The exposure that + # would otherwise argue for pinning is handled by this job's permissions + # instead - the CLI runs with a read-only token and cannot write even if + # a release is compromised. + - name: Install Copilot CLI + run: npm install -g @github/copilot + - name: Analyze with AI id: ai-triage - uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2 + uses: actions/ai-inference@2c43c91ae16266ca159d311430343c67a5ffa222 # v3 env: + # No PAT secret needed: the CLI authenticates as the workflow, and + # `copilot-requests: write` meters the request to the organization. + # + # This job runs on `pull_request_target`, so the prompt below carries + # pull request text written by anyone who can open one. Two separate + # things contain that. The action passes no `--allow-tool` flags, so + # Copilot gets no shell, filesystem or network access - that stops a + # prompt injection. And this job's token is read-only, so even a + # malicious CLI release, which no prompt restriction would stop, + # cannot write anything with it. Adding `copilot-allow-tools` here + # would give up the first of those. + GITHUB_TOKEN: ${{ github.token }} AVAILABLE_LABELS: ${{ steps.get-labels.outputs.result }} ITEM_TITLE: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.title || github.event.issue.title }} ITEM_BODY: ${{ github.event_name == 'pull_request_target' && github.event.pull_request.body || github.event.issue.body }} @@ -47,9 +88,9 @@ jobs: ## Role You are an issue and pull request triage assistant. Analyze the current GitHub - ${{ env.ITEM_TYPE }} and identify the most appropriate existing labels. Use the - available tools to gather information; do not ask for information - to be provided. + ${{ env.ITEM_TYPE }} and identify the most appropriate existing labels. Work + only from the input data below; you have no tools available and no + further information can be provided. ## Guidelines @@ -91,12 +132,24 @@ jobs: label1, label2, label3 ``` + apply-new-item-labels: + name: Apply Labels to New Issue or PR + needs: triage-new-item + if: needs.triage-new-item.outputs.response != '' + runs-on: ubuntu-latest + # Holds the write scopes `triage-new-item` gave up. Nothing here installs or + # runs the Copilot CLI, so this token is only ever exposed to the allowlist + # check below, which is what keeps a model response from becoming a write. + permissions: + contents: read + issues: write + pull-requests: write + steps: - name: Apply labels - if: steps.ai-triage.outputs.response != '' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - AI_RESPONSE: ${{ steps.ai-triage.outputs.response }} - AVAILABLE_LABELS: ${{ steps.get-labels.outputs.result }} + AI_RESPONSE: ${{ needs.triage-new-item.outputs.response }} + AVAILABLE_LABELS: ${{ needs.triage-new-item.outputs.labels }} with: script: | const response = process.env.AI_RESPONSE; @@ -237,6 +290,16 @@ jobs: github.event_name == 'workflow_dispatch' && inputs.issue_number != '' runs-on: ubuntu-latest + # Same split as `triage-new-item`: this job runs the Copilot CLI, so it + # holds no write scope. `apply-single-item-labels` does the writing. + permissions: + contents: read + issues: read + pull-requests: read + copilot-requests: write + outputs: + response: ${{ steps.ai-triage.outputs.response }} + labels: ${{ steps.get-labels.outputs.result }} steps: - name: Get available labels id: get-labels @@ -271,10 +334,29 @@ jobs: type: itemType }; + # The Copilot CLI is not preinstalled on GitHub-hosted runners, and + # `actions/ai-inference` v3 only shells out to it - it never installs it. + - name: Set up Node.js + uses: actions/setup-node@820762786026740c76f36085b0efc47a31fe5020 # v7.0.0 + with: + node-version: lts/* + + # Deliberately unpinned, unlike everything else here: authenticating with + # `GITHUB_TOKEN` instead of a PAT only works on recent CLI releases, so a + # pin would be a slow trap rather than a safeguard. The exposure that + # would otherwise argue for pinning is handled by this job's permissions + # instead - the CLI runs with a read-only token and cannot write even if + # a release is compromised. + - name: Install Copilot CLI + run: npm install -g @github/copilot + - name: Analyze with AI id: ai-triage - uses: actions/ai-inference@a7805884c80886efc241e94a5351df715968a0ad # v2 + uses: actions/ai-inference@2c43c91ae16266ca159d311430343c67a5ffa222 # v3 env: + # No PAT secret needed: the CLI authenticates as the workflow, and + # `copilot-requests: write` meters the request to the organization. + GITHUB_TOKEN: ${{ github.token }} AVAILABLE_LABELS: ${{ steps.get-labels.outputs.result }} ITEM_TITLE: ${{ fromJSON(steps.get-item.outputs.result).title }} ITEM_BODY: ${{ fromJSON(steps.get-item.outputs.result).body }} @@ -284,9 +366,9 @@ jobs: ## Role You are an issue and pull request triage assistant. Analyze the current GitHub - ${{ env.ITEM_TYPE }} and identify the most appropriate existing labels. Use the - available tools to gather information; do not ask for information - to be provided. + ${{ env.ITEM_TYPE }} and identify the most appropriate existing labels. Work + only from the input data below; you have no tools available and no + further information can be provided. ## Guidelines @@ -328,13 +410,23 @@ jobs: label1, label2, label3 ``` + apply-single-item-labels: + name: Apply Labels to Single Issue or PR + needs: triage-single-item + if: needs.triage-single-item.outputs.response != '' + runs-on: ubuntu-latest + # Holds the write scopes `triage-single-item` gave up, and runs no CLI. + permissions: + contents: read + issues: write + pull-requests: write + steps: - name: Apply labels - if: steps.ai-triage.outputs.response != '' uses: actions/github-script@3a2844b7e9c422d3c10d287c895573f7108da1b3 # v9.0.0 env: - AI_RESPONSE: ${{ steps.ai-triage.outputs.response }} + AI_RESPONSE: ${{ needs.triage-single-item.outputs.response }} ITEM_NUMBER: ${{ inputs.issue_number }} - AVAILABLE_LABELS: ${{ steps.get-labels.outputs.result }} + AVAILABLE_LABELS: ${{ needs.triage-single-item.outputs.labels }} with: script: | const response = process.env.AI_RESPONSE; diff --git a/.github/workflows/sync-workflows.yml b/.github/workflows/sync-workflows.yml index 3382f05..307b49f 100644 --- a/.github/workflows/sync-workflows.yml +++ b/.github/workflows/sync-workflows.yml @@ -10,6 +10,7 @@ on: - '.actrc' - '.editorconfig' - 'AGENTS.md' + - '.github/actionlint.yml' - '.github/dependabot.yml' - '.github/workflows/check-branch-alias.yml' - '.github/workflows/copilot-setup-steps.yml' @@ -54,6 +55,7 @@ jobs: FILE_PATTERNS: | ^\.actrc ^\.editorconfig + ^\.github/actionlint\.yml ^\.github/workflows/copilot-setup-steps\.yml ^\.github/workflows/regenerate-readme\.yml ^\.github/workflows/welcome-new-contributors\.yml