What looks like a single event of applying a few labels is actually multiple events, and the raciness is racy as we've recently experienced. One solution would've been adding a “first responders notified” label for enforcing consistency but issues already have enough labels (and we could take it off by mistake), so a bot reaction will instead serve as a marker for any later runs. So if we're applying multiple labels or changing our mind about, say, an area label after the notification has already been sent, this should work fine now, without duplicate notifications. Release Notes: - N/A
199 lines
7.7 KiB
YAML
199 lines
7.7 KiB
YAML
name: Slack Notify First Responders
|
|
|
|
on:
|
|
issues:
|
|
types: [labeled]
|
|
|
|
permissions:
|
|
contents: read
|
|
|
|
env:
|
|
PRIORITY_LABELS: '["priority:P0", "priority:P1"]'
|
|
REPRODUCIBLE_LABEL: 'state:reproducible'
|
|
FREQUENCY_LABELS: '["frequency:always", "frequency:common"]'
|
|
MARKER_REACTION: 'eyes'
|
|
|
|
jobs:
|
|
notify-slack:
|
|
if: github.repository_owner == 'zed-industries' && github.event.issue.state == 'open'
|
|
runs-on: namespace-profile-2x4-ubuntu-2404
|
|
permissions:
|
|
contents: read
|
|
# For the issue reaction used to dedupe notifications.
|
|
issues: write
|
|
# Serialize per issue so the reaction claim below can't race.
|
|
concurrency:
|
|
group: slack-notify-first-responders-${{ github.event.issue.number }}
|
|
cancel-in-progress: false
|
|
|
|
steps:
|
|
- name: Check label combination and claim the notification
|
|
id: check-label
|
|
env:
|
|
GH_TOKEN: ${{ github.token }}
|
|
GH_REPO: ${{ github.repository }}
|
|
LABEL_NAME: ${{ github.event.label.name }}
|
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
run: |
|
|
set -euo pipefail
|
|
|
|
api() {
|
|
curl -sS \
|
|
-H "Authorization: Bearer $GH_TOKEN" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
"$@"
|
|
}
|
|
|
|
# Ignore labels outside the trifecta so unrelated later edits don't re-fire.
|
|
TRIGGER_LABELS=$(jq -cn \
|
|
--argjson priority "$PRIORITY_LABELS" \
|
|
--arg repro "$REPRODUCIBLE_LABEL" \
|
|
--argjson freq "$FREQUENCY_LABELS" \
|
|
'$priority + [$repro] + $freq')
|
|
|
|
if ! echo "$TRIGGER_LABELS" | jq -e --arg l "$LABEL_NAME" 'index($l) != null' > /dev/null; then
|
|
echo "Added label '$LABEL_NAME' is not in the trigger set, skipping"
|
|
echo "should_notify=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
# The webhook's issue.labels snapshot is racy under bulk apply; read live.
|
|
ISSUE_LABELS_JSON=$(api -f "https://api.github.com/repos/$GH_REPO/issues/$ISSUE_NUMBER/labels?per_page=100" | jq '[.[].name]')
|
|
|
|
MATCHED_PRIORITY=$(echo "$ISSUE_LABELS_JSON" | jq -r --argjson priority "$PRIORITY_LABELS" 'map(select(. as $x | $priority | index($x) != null)) | first // ""')
|
|
HAS_REPRO=$(echo "$ISSUE_LABELS_JSON" | jq --arg l "$REPRODUCIBLE_LABEL" 'index($l) != null')
|
|
HAS_FREQ=$(echo "$ISSUE_LABELS_JSON" | jq --argjson freq "$FREQUENCY_LABELS" 'any(.[]; . as $x | $freq | index($x) != null)')
|
|
|
|
if [ -z "$MATCHED_PRIORITY" ] || [ "$HAS_REPRO" != "true" ] || [ "$HAS_FREQ" != "true" ]; then
|
|
echo "Combination not yet satisfied (priority=$MATCHED_PRIORITY, reproducible=$HAS_REPRO, frequency=$HAS_FREQ), skipping"
|
|
echo "should_notify=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
# A bulk label apply emits one `labeled` event per label, so several
|
|
# runs reach this point. Creating the reaction is our atomic claim:
|
|
# one run gets 201 and notifies, the rest get 200. It's scoped to our
|
|
# own reaction, so a human's reaction can't suppress it.
|
|
REACTION_PAYLOAD=$(jq -cn --arg content "$MARKER_REACTION" '{content: $content}')
|
|
REACTION_RESPONSE=$(api -w "\n%{http_code}" -X POST \
|
|
"https://api.github.com/repos/$GH_REPO/issues/$ISSUE_NUMBER/reactions" \
|
|
-d "$REACTION_PAYLOAD")
|
|
REACTION_BODY=$(echo "$REACTION_RESPONSE" | sed '$d')
|
|
REACTION_STATUS=$(echo "$REACTION_RESPONSE" | tail -n1)
|
|
|
|
if [ "$REACTION_STATUS" = "200" ]; then
|
|
echo "First responders already notified for this issue (reaction present), skipping"
|
|
echo "should_notify=false" >> "$GITHUB_OUTPUT"
|
|
exit 0
|
|
fi
|
|
|
|
if [ "$REACTION_STATUS" != "201" ]; then
|
|
echo "::error::Unexpected status $REACTION_STATUS creating reaction: $REACTION_BODY"
|
|
exit 1
|
|
fi
|
|
|
|
REACTION_ID=$(echo "$REACTION_BODY" | jq -r '.id')
|
|
LABELS=$(echo "$ISSUE_LABELS_JSON" | jq -r 'join(", ")')
|
|
|
|
echo "Confirmed high-frequency $MATCHED_PRIORITY, notifying"
|
|
{
|
|
echo "notify_reason=confirmed $MATCHED_PRIORITY"
|
|
echo "issue_labels=$LABELS"
|
|
echo "reaction_id=$REACTION_ID"
|
|
echo "should_notify=true"
|
|
} >> "$GITHUB_OUTPUT"
|
|
|
|
- name: Build Slack message payload
|
|
if: steps.check-label.outputs.should_notify == 'true'
|
|
env:
|
|
ISSUE_TITLE: ${{ github.event.issue.title }}
|
|
ISSUE_URL: ${{ github.event.issue.html_url }}
|
|
LABELED_BY: ${{ github.event.sender.login }}
|
|
NOTIFY_REASON: ${{ steps.check-label.outputs.notify_reason }}
|
|
LABELS: ${{ steps.check-label.outputs.issue_labels }}
|
|
run: |
|
|
jq -n \
|
|
--arg notify_reason "$NOTIFY_REASON" \
|
|
--arg issue_title "$ISSUE_TITLE" \
|
|
--arg issue_url "$ISSUE_URL" \
|
|
--arg labeled_by "$LABELED_BY" \
|
|
--arg labels "$LABELS" \
|
|
'{
|
|
"blocks": [
|
|
{
|
|
"type": "section",
|
|
"text": {
|
|
"type": "mrkdwn",
|
|
"text": "<!subteam^S096CPUUGLF> New *\($notify_reason)* issue"
|
|
}
|
|
},
|
|
{
|
|
"type": "section",
|
|
"fields": [
|
|
{
|
|
"type": "mrkdwn",
|
|
"text": "*Issue:*\n<\($issue_url)|\($issue_title)>"
|
|
},
|
|
{
|
|
"type": "mrkdwn",
|
|
"text": "*Labeled by:*\n\($labeled_by)"
|
|
},
|
|
{
|
|
"type": "mrkdwn",
|
|
"text": "*Labels:*\n\($labels)"
|
|
}
|
|
]
|
|
}
|
|
]
|
|
}' > payload.json
|
|
|
|
echo "Payload built successfully:"
|
|
cat payload.json
|
|
|
|
- name: Send Slack notification
|
|
if: steps.check-label.outputs.should_notify == 'true'
|
|
env:
|
|
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_FIRST_RESPONDERS }}
|
|
GH_TOKEN: ${{ github.token }}
|
|
GH_REPO: ${{ github.repository }}
|
|
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
REACTION_ID: ${{ steps.check-label.outputs.reaction_id }}
|
|
run: |
|
|
set -uo pipefail
|
|
|
|
release_claim() {
|
|
if [ -n "${REACTION_ID:-}" ]; then
|
|
echo "Releasing reaction claim so the notification can be retried"
|
|
curl -sS -X DELETE \
|
|
-H "Authorization: Bearer $GH_TOKEN" \
|
|
-H "Accept: application/vnd.github+json" \
|
|
-H "X-GitHub-Api-Version: 2022-11-28" \
|
|
"https://api.github.com/repos/$GH_REPO/issues/$ISSUE_NUMBER/reactions/$REACTION_ID" || true
|
|
fi
|
|
}
|
|
|
|
if [ -z "$SLACK_WEBHOOK_URL" ]; then
|
|
echo "::error::SLACK_WEBHOOK_FIRST_RESPONDERS secret is not set"
|
|
release_claim
|
|
exit 1
|
|
fi
|
|
|
|
HTTP_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST "$SLACK_WEBHOOK_URL" \
|
|
-H "Content-Type: application/json" \
|
|
-d @payload.json)
|
|
|
|
HTTP_BODY=$(echo "$HTTP_RESPONSE" | sed '$d')
|
|
HTTP_STATUS=$(echo "$HTTP_RESPONSE" | tail -n 1)
|
|
|
|
echo "Slack API response status: $HTTP_STATUS"
|
|
echo "Slack API response body: $HTTP_BODY"
|
|
|
|
if [ "$HTTP_STATUS" -ne 200 ]; then
|
|
echo "::error::Slack notification failed with status $HTTP_STATUS: $HTTP_BODY"
|
|
release_claim
|
|
exit 1
|
|
fi
|
|
|
|
echo "Slack notification sent successfully"
|