ok-to-test-review.yml 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105
  1. # If someone with write access submits a PR review (Approve or Comment) whose
  2. # body contains "/ok-to-test", emit a repository_dispatch pinned to the exact
  3. # commit that was reviewed (github.event.review.commit_id). A review records the
  4. # head the reviewer actually saw, so the tested SHA cannot be moved by a later
  5. # push, and no PR-head lookup or sha= argument is needed.
  6. name: Ok To Test (review)
  7. on:
  8. pull_request_review:
  9. types: [submitted]
  10. permissions:
  11. contents: read
  12. jobs:
  13. ok-to-test-review:
  14. # Only Approve or Comment reviews (never Request changes), and only when the
  15. # review body mentions the command. contains() is a coarse pre-filter; the
  16. # step below enforces that /ok-to-test is the command on the first line.
  17. if: >-
  18. (github.event.review.state == 'approved' ||
  19. github.event.review.state == 'commented') &&
  20. contains(github.event.review.body, '/ok-to-test')
  21. runs-on: ubuntu-latest
  22. steps:
  23. - uses: step-security/harden-runner@e14015d583714f6e62063499dc959a02595150a1 # v2.21.1
  24. with:
  25. egress-policy: audit
  26. # Mirror how slash-command-dispatch parses the comment path: the command
  27. # must be on the FIRST line and start with the slash command. This rejects a
  28. # /ok-to-test mentioned in prose elsewhere in a longer review, and the
  29. # /ok-to-test-managed prefix. Do not echo the body back: it is attacker
  30. # controlled and could inject `::workflow commands::` into the log.
  31. - name: Match the command in the review body
  32. id: cmd
  33. env:
  34. REVIEW_BODY: ${{ github.event.review.body }}
  35. run: |
  36. first=$(printf '%s' "$REVIEW_BODY" | head -n1 | tr -d '\r' \
  37. | sed -E 's/^[[:space:]]+//; s/[[:space:]]+$//')
  38. if printf '%s' "$first" | grep -qE '^/ok-to-test([[:space:]]|$)'; then
  39. echo "match=true" >> "$GITHUB_OUTPUT"
  40. else
  41. echo "match=false" >> "$GITHUB_OUTPUT"
  42. echo "first line is not an /ok-to-test command; no e2e triggered"
  43. fi
  44. # App installation token, reused for the permission check and the dispatch.
  45. - name: Generate token
  46. id: generate_token
  47. if: steps.cmd.outputs.match == 'true'
  48. uses: actions/create-github-app-token@bcd2ba49218906704ab6c1aa796996da409d3eb1 # v3.2.0
  49. env:
  50. APP_ID: ${{ secrets.APP_ID }}
  51. with:
  52. app-id: ${{ env.APP_ID }}
  53. private-key: ${{ secrets.PRIVATE_KEY }}
  54. owner: ${{ github.repository_owner }}
  55. repositories: ${{ github.event.repository.name }}
  56. # contents:write to POST the repository_dispatch; metadata:read (implicit
  57. # for any app token, stated for clarity) to read the reviewer's
  58. # collaborator permission level.
  59. permission-contents: write
  60. permission-metadata: read
  61. # pull_request_review does not restrict to maintainers, and
  62. # author_association is not authoritative for repo write access, so verify it
  63. # explicitly. This is the equivalent of slash-command-dispatch's
  64. # `permission: maintain` on the comment path.
  65. - name: Verify the reviewer has write access
  66. if: steps.cmd.outputs.match == 'true'
  67. env:
  68. GH_TOKEN: ${{ steps.generate_token.outputs.token }}
  69. REPO: ${{ github.repository }}
  70. REVIEWER: ${{ github.event.review.user.login }}
  71. run: |
  72. perm=$(gh api "repos/${REPO}/collaborators/${REVIEWER}/permission" \
  73. --jq '.permission')
  74. echo "reviewer ${REVIEWER} permission: ${perm}"
  75. # .permission is rolled up to admin/write/read/none (Maintain surfaces
  76. # as write, Triage as read); accept write and above.
  77. case "${perm}" in
  78. admin|write) : ;;
  79. *) echo "::error::${REVIEWER} lacks write access (${perm})"; exit 1 ;;
  80. esac
  81. # Emit the client_payload fields e2e.yml consumes on the comment path:
  82. # slash_command.args.named.sha (the target SHA) and pull_request.number
  83. # (used by report-fork to comment the result). jq quotes both safely.
  84. - name: Dispatch ok-to-test-command
  85. if: steps.cmd.outputs.match == 'true'
  86. env:
  87. GH_TOKEN: ${{ steps.generate_token.outputs.token }}
  88. REPO: ${{ github.repository }}
  89. SHA: ${{ github.event.review.commit_id }}
  90. PR_NUMBER: ${{ github.event.pull_request.number }}
  91. run: |
  92. jq -cn --arg sha "$SHA" --argjson num "$PR_NUMBER" '{
  93. event_type: "ok-to-test-command",
  94. client_payload: {
  95. slash_command: { args: { named: { sha: $sha } } },
  96. pull_request: { number: $num }
  97. }
  98. }' | gh api --method POST "repos/${REPO}/dispatches" --input -