Forbidden Words Suggestions #421
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Forbidden Words Suggestions | |
| # Runs after the "Forbidden Words" check completes. That check runs on | |
| # `pull_request` (no secrets on fork PRs), so the actual suggestion posting is | |
| # done here in the privileged `workflow_run` context, which has access to the | |
| # Aspire bot credentials even for pull requests from forks. | |
| # | |
| # This job only reads the findings artifact as data and posts review comments. | |
| # It derives the target pull request from the trusted workflow_run payload and | |
| # GitHub API, runs scripts from the default branch, and never executes PR code. | |
| on: | |
| workflow_run: | |
| workflows: ["Forbidden Words"] | |
| types: [completed] | |
| permissions: | |
| contents: read | |
| actions: read | |
| pull-requests: write | |
| concurrency: | |
| # Group by the source pull request (head repo + branch) so rapid pushes don't | |
| # run concurrently and race the dedupe check. head_repository.full_name keeps | |
| # forks with the same branch name in separate groups. | |
| group: ${{ github.workflow }}-${{ github.event.workflow_run.head_repository.full_name }}-${{ github.event.workflow_run.head_branch }} | |
| cancel-in-progress: true | |
| jobs: | |
| suggest: | |
| name: Post inline suggestions | |
| runs-on: ubuntu-latest | |
| # Only act on runs triggered by pull requests. | |
| if: ${{ github.event.workflow_run.event == 'pull_request' }} | |
| steps: | |
| # Check out the trusted scripts from the default branch (NOT the PR head). | |
| - name: Checkout scripts | |
| uses: actions/checkout@de0fac2e4500dabe0009e67214ff5f5447ce83dd # v6.0.2 | |
| with: | |
| sparse-checkout: | | |
| .github/forbidden-words.json | |
| .github/scripts | |
| sparse-checkout-cone-mode: false | |
| persist-credentials: false | |
| - name: Download findings artifact | |
| uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 | |
| continue-on-error: true | |
| with: | |
| name: forbidden-words-findings | |
| path: findings | |
| run-id: ${{ github.event.workflow_run.id }} | |
| github-token: ${{ github.token }} | |
| - name: Inspect artifact | |
| id: check | |
| shell: bash | |
| run: | | |
| if [[ -f findings/findings.json ]]; then | |
| echo "present=true" >> "$GITHUB_OUTPUT" | |
| count=$(jq '.findings | length' findings/findings.json) | |
| echo "Findings: $count" | |
| [[ "$count" -gt 0 ]] && echo "has_findings=true" >> "$GITHUB_OUTPUT" || echo "has_findings=false" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "present=false" >> "$GITHUB_OUTPUT" | |
| echo "has_findings=false" >> "$GITHUB_OUTPUT" | |
| echo "No findings artifact was produced; nothing to suggest." | |
| fi | |
| # Never trust PR coordinates from the artifact. Resolve the source PR from | |
| # the workflow_run head identity and require exactly one matching open PR. | |
| - name: Resolve source pull request | |
| id: source | |
| if: ${{ steps.check.outputs.has_findings == 'true' }} | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| HEAD_REPO: ${{ github.event.workflow_run.head_repository.full_name }} | |
| HEAD_BRANCH: ${{ github.event.workflow_run.head_branch }} | |
| run: | | |
| candidates=$( | |
| gh api --paginate --slurp "/repos/$REPO/pulls?state=open&per_page=100" | | |
| jq -c \ | |
| --arg repo "$REPO" \ | |
| --arg head "$HEAD_SHA" \ | |
| --arg head_repo "$HEAD_REPO" \ | |
| --arg head_branch "$HEAD_BRANCH" \ | |
| '[.[][] | select( | |
| .state == "open" and | |
| .base.repo.full_name == $repo and | |
| .head.sha == $head and | |
| .head.repo.full_name == $head_repo and | |
| .head.ref == $head_branch | |
| )]' | |
| ) | |
| count=$(jq 'length' <<< "$candidates") | |
| if [[ "$count" -ne 1 ]]; then | |
| echo "found=false" >> "$GITHUB_OUTPUT" | |
| echo "::warning::Expected exactly one open pull request for workflow head $HEAD_SHA, found $count; skipping suggestions." | |
| exit 0 | |
| fi | |
| echo "found=true" >> "$GITHUB_OUTPUT" | |
| echo "pr=$(jq -r '.[0].number' <<< "$candidates")" >> "$GITHUB_OUTPUT" | |
| # Skip gracefully when the Aspire bot credentials aren't configured | |
| # (e.g. on a fork that runs this workflow without the secrets). | |
| - name: Check Aspire bot credentials | |
| id: creds | |
| if: ${{ steps.source.outputs.found == 'true' }} | |
| shell: bash | |
| env: | |
| APP_ID: ${{ secrets.ASPIRE_BOT_APP_ID }} | |
| PRIVATE_KEY: ${{ secrets.ASPIRE_BOT_PRIVATE_KEY }} | |
| run: | | |
| if [[ -n "$APP_ID" && -n "$PRIVATE_KEY" ]]; then | |
| echo "ok=true" >> "$GITHUB_OUTPUT" | |
| else | |
| echo "ok=false" >> "$GITHUB_OUTPUT" | |
| echo "::notice::Aspire bot credentials unavailable; skipping inline suggestions." | |
| fi | |
| - name: Create Aspire bot token | |
| id: app_token | |
| if: ${{ steps.creds.outputs.ok == 'true' }} | |
| uses: actions/create-github-app-token@1b10c78c7865c340bc4f6099eb2f838309f1e8c3 # v3.1.1 | |
| with: | |
| client-id: ${{ secrets.ASPIRE_BOT_APP_ID }} | |
| private-key: ${{ secrets.ASPIRE_BOT_PRIVATE_KEY }} | |
| owner: ${{ github.repository_owner }} | |
| repositories: ${{ github.event.repository.name }} | |
| github-api-url: ${{ github.api_url }} | |
| permission-pull-requests: write | |
| permission-contents: read | |
| - name: Post inline suggestions | |
| if: ${{ steps.app_token.outputs.token != '' }} | |
| shell: bash | |
| env: | |
| GH_TOKEN: ${{ steps.app_token.outputs.token }} | |
| REPO: ${{ github.repository }} | |
| PR_NUMBER: ${{ steps.source.outputs.pr }} | |
| HEAD_SHA: ${{ github.event.workflow_run.head_sha }} | |
| FINDINGS_FILE: findings/findings.json | |
| CONFIG_FILE: .github/forbidden-words.json | |
| run: | | |
| .github/scripts/post-forbidden-word-suggestions.sh |