Hotfix Review Monitor #18
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
| # Hotfix Review Monitor | |
| # | |
| # Runs daily and checks for merged PRs with the 'hotfix' label that have not | |
| # received a post-merge review approval within one business day. Posts a summary to | |
| # Slack if any are found. This is a SOC2 compensating control for the | |
| # emergency hotfix fast path. | |
| # | |
| # Security note: No untrusted input (PR titles, bodies, etc.) is interpolated | |
| # into shell commands. All PR metadata is read via gh API + jq, not via | |
| # github.event context expressions. | |
| # | |
| # Required secrets: | |
| # SLACK_WEBHOOK_PR_REVIEW_BOT - Incoming webhook URL for the #pr-review-ops channel | |
| name: Hotfix Review Monitor | |
| on: | |
| schedule: | |
| - cron: "30 13 * * 1-5" # 1:30 PM UTC weekdays | |
| workflow_dispatch: {} | |
| permissions: | |
| contents: read | |
| pull-requests: read | |
| jobs: | |
| check-hotfix-reviews: | |
| if: github.repository_owner == 'zed-industries' | |
| runs-on: ubuntu-latest | |
| timeout-minutes: 5 | |
| env: | |
| REPO: ${{ github.repository }} | |
| steps: | |
| - name: Find unreviewed hotfixes | |
| id: check | |
| env: | |
| GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| # 80h lookback covers the Friday-to-Monday gap (72h) with buffer. | |
| # Overlap on weekdays is harmless — reviewed PRs are filtered out below. | |
| SINCE=$(date -u -v-80H +%Y-%m-%dT%H:%M:%SZ 2>/dev/null \ | |
| || date -u -d '80 hours ago' +%Y-%m-%dT%H:%M:%SZ) | |
| SINCE_DATE=$(echo "$SINCE" | cut -dT -f1) | |
| # Use the Search API to find hotfix PRs merged in the lookback window. | |
| # The Pulls API with state=closed paginates through all closed PRs in | |
| # the repo, which times out on large repos. The Search API supports | |
| # merged:>DATE natively so GitHub does the filtering server-side. | |
| gh api --paginate \ | |
| "search/issues?q=repo:${REPO}+is:pr+is:merged+label:hotfix+merged:>${SINCE_DATE}&per_page=100" \ | |
| --jq '[.items[] | {number, title, merged_at: .pull_request.merged_at}]' \ | |
| > /tmp/hotfix_prs.json | |
| # Check each hotfix PR for a post-merge approving review | |
| jq -r '.[].number' /tmp/hotfix_prs.json | while read -r PR_NUMBER; do | |
| APPROVALS=$(gh api \ | |
| "repos/${REPO}/pulls/${PR_NUMBER}/reviews" \ | |
| --jq "[.[] | select(.state == \"APPROVED\")] | length") | |
| if [ "$APPROVALS" -eq 0 ]; then | |
| jq ".[] | select(.number == ${PR_NUMBER})" /tmp/hotfix_prs.json | |
| fi | |
| done | jq -s '.' > /tmp/unreviewed.json | |
| COUNT=$(jq 'length' /tmp/unreviewed.json) | |
| echo "count=$COUNT" >> "$GITHUB_OUTPUT" | |
| - name: Notify Slack | |
| if: steps.check.outputs.count != '0' | |
| env: | |
| SLACK_WEBHOOK_PR_REVIEW_BOT: ${{ secrets.SLACK_WEBHOOK_PR_REVIEW_BOT }} | |
| COUNT: ${{ steps.check.outputs.count }} | |
| run: | | |
| # Build Block Kit payload from JSON — no shell interpolation of PR titles. | |
| # Why jq? PR titles are attacker-controllable input. By reading them | |
| # through jq -r from the JSON file and passing the result to jq --arg, | |
| # the content stays safely JSON-encoded in the final payload. Block Kit | |
| # doesn't change this — the same jq pipeline feeds into the blocks | |
| # structure instead of plain text. | |
| PRS=$(jq -r '.[] | "• <https://github.com/'"${REPO}"'/pull/\(.number)|#\(.number)> — \(.title) (merged \(.merged_at | split("T")[0]))"' /tmp/unreviewed.json) | |
| jq -n \ | |
| --arg count "$COUNT" \ | |
| --arg prs "$PRS" \ | |
| '{ | |
| text: ($count + " hotfix PR(s) still need post-merge review"), | |
| blocks: [ | |
| { | |
| type: "section", | |
| text: { | |
| type: "mrkdwn", | |
| text: (":rotating_light: *" + $count + " Hotfix PR(s) Need Post-Merge Review*") | |
| } | |
| }, | |
| { | |
| type: "section", | |
| text: { type: "mrkdwn", text: $prs } | |
| }, | |
| { type: "divider" }, | |
| { | |
| type: "context", | |
| elements: [{ | |
| type: "mrkdwn", | |
| text: "Hotfix PRs require review within one business day of merge." | |
| }] | |
| } | |
| ] | |
| }' | \ | |
| curl -s -X POST "$SLACK_WEBHOOK_PR_REVIEW_BOT" \ | |
| -H 'Content-Type: application/json' \ | |
| -d @- | |
| defaults: | |
| run: | |
| shell: bash -euxo pipefail {0} |