forked from datahub-project/datahub
-
Notifications
You must be signed in to change notification settings - Fork 0
157 lines (137 loc) · 6.55 KB
/
linear-assignment-notify.yml
File metadata and controls
157 lines (137 loc) · 6.55 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
name: Notify PR Author of Linear Assignment
# Polls every 15 minutes. Uses a single paginated GraphQL query to fetch all
# open PRs with their comments (~8 API calls total regardless of PR count),
# then only calls Linear for PRs that have a ticket but no notification yet.
#
# Designed to fail silently — errors are logged but never propagate.
# continue-on-error: true ensures this job never blocks other workflows.
on:
schedule:
- cron: "*/15 * * * *"
workflow_dispatch:
jobs:
notify:
runs-on: ubuntu-latest
continue-on-error: true
timeout-minutes: 10
steps:
- name: Notify PR authors when Linear ticket is assigned
env:
GH_TOKEN: ${{ github.token }}
LINEAR_API_KEY: ${{ secrets.INGESTION_LINEAR_KEY }}
GH_REPO: ${{ github.repository }}
TEAM_MEMBERS: ${{ vars.DATAHUB_TEAM_MEMBERS }}
run: |
# Never let any failure exit the script — log and move on
set +e
OWNER=$(echo "$GH_REPO" | cut -d'/' -f1)
REPO=$(echo "$GH_REPO" | cut -d'/' -f2)
if [ -z "$OWNER" ] || [ -z "$REPO" ]; then
echo "⚠️ Could not parse GH_REPO=$GH_REPO, exiting"
exit 0
fi
CURSOR=""
PAGE=0
MAX_PAGES=20 # safety cap (~1000 PRs)
while true; do
PAGE=$((PAGE + 1))
if [ "$PAGE" -gt "$MAX_PAGES" ]; then
echo "⚠️ Reached page cap ($MAX_PAGES), stopping pagination"
break
fi
# shellcheck disable=SC2016
ARGS=(-f query='
query($owner: String!, $repo: String!, $cursor: String) {
repository(owner: $owner, name: $repo) {
pullRequests(first: 50, states: [OPEN], after: $cursor) {
pageInfo { hasNextPage endCursor }
nodes {
number
comments(first: 100) {
nodes { body }
}
}
}
}
}
' -f owner="$OWNER" -f repo="$REPO")
[ -n "$CURSOR" ] && ARGS+=(-f cursor="$CURSOR")
RESULT=$(gh api graphql "${ARGS[@]}" 2>&1)
if [ -z "$RESULT" ]; then
echo "⚠️ Empty response from GitHub GraphQL on page $PAGE, stopping"
break
fi
# Validate it's parseable JSON before doing anything with it
if ! echo "$RESULT" | jq empty 2>/dev/null; then
echo "⚠️ Invalid JSON from GitHub GraphQL on page $PAGE, stopping"
break
fi
# Client-side: find PRs with a Linear ticket but no notification yet
CANDIDATES=$(echo "$RESULT" | jq -c '
.data.repository.pullRequests.nodes[] |
. as $pr |
($pr.comments.nodes | map(.body) | join("\n")) as $comments |
select(
($comments | test("<!-- linear-assigned -->") | not) and
($comments | test("Linear: [A-Z]+-[0-9]+"))
) |
{
number: $pr.number,
ticket: ($comments | capture("Linear: (?<id>[A-Z]+-[0-9]+)") | .id)
}
' 2>/dev/null)
echo "$CANDIDATES" | while read -r CANDIDATE; do
[ -z "$CANDIDATE" ] && continue
PR_NUMBER=$(echo "$CANDIDATE" | jq -r '.number' 2>/dev/null)
TICKET_ID=$(echo "$CANDIDATE" | jq -r '.ticket' 2>/dev/null)
# Sanity-check extracted values before acting on them
if [ -z "$PR_NUMBER" ] || [ -z "$TICKET_ID" ]; then
echo "⚠️ Skipping malformed candidate: $CANDIDATE"
continue
fi
if ! echo "$PR_NUMBER" | grep -qE '^[0-9]+$'; then
echo "⚠️ PR_NUMBER '$PR_NUMBER' is not numeric, skipping"
continue
fi
if ! echo "$TICKET_ID" | grep -qE '^[A-Z]+-[0-9]+$'; then
echo "⚠️ TICKET_ID '$TICKET_ID' has unexpected format, skipping"
continue
fi
RESPONSE=$(curl -s --max-time 10 -X POST https://api.linear.app/graphql \
-H "Authorization: $LINEAR_API_KEY" \
-H "Content-Type: application/json" \
-d "{\"query\": \"query { issue(id: \\\"$TICKET_ID\\\") { state { name } assignee { displayName } } }\"}" 2>/dev/null)
if [ -z "$RESPONSE" ]; then
echo "⚠️ Empty response from Linear for $TICKET_ID, skipping"
continue
fi
STATE=$(echo "$RESPONSE" | jq -r '.data.issue.state.name' 2>/dev/null)
ASSIGNEE=$(echo "$RESPONSE" | jq -r '.data.issue.assignee.displayName' 2>/dev/null)
if [ "$STATE" != "Triage" ] && [ -n "$ASSIGNEE" ] && [ "$ASSIGNEE" != "null" ]; then
echo "Notifying PR #$PR_NUMBER — $TICKET_ID assigned to $ASSIGNEE"
GH_USER=$(echo "$TEAM_MEMBERS" | jq -r --arg linear "$ASSIGNEE" '.[] | select(.linear == $linear) | .github' 2>/dev/null | head -n1)
if [ -n "$GH_USER" ] && [ "$GH_USER" != "null" ]; then
ASSIGNEE_TAG="@${GH_USER} (${ASSIGNEE})"
gh pr edit "$PR_NUMBER" --add-reviewer "$GH_USER" 2>/dev/null \
&& echo "✅ Assigned $GH_USER as reviewer on PR #$PR_NUMBER" \
|| echo "⚠️ Failed to assign reviewer on PR #$PR_NUMBER"
else
ASSIGNEE_TAG="**${ASSIGNEE}**"
fi
BODY=$(printf '<!-- linear-assigned -->\nYour PR has been assigned to %s for review (%s).' "$ASSIGNEE_TAG" "$TICKET_ID")
gh pr comment "$PR_NUMBER" --body "$BODY" 2>/dev/null \
&& echo "✅ Commented on PR #$PR_NUMBER" \
|| echo "⚠️ Failed to comment on PR #$PR_NUMBER, will retry next run"
fi
done
HAS_NEXT=$(echo "$RESULT" | jq -r '.data.repository.pullRequests.pageInfo.hasNextPage' 2>/dev/null)
[ "$HAS_NEXT" != "true" ] && break
CURSOR=$(echo "$RESULT" | jq -r '.data.repository.pullRequests.pageInfo.endCursor' 2>/dev/null)
if [ -z "$CURSOR" ] || [ "$CURSOR" = "null" ]; then
echo "⚠️ Missing endCursor despite hasNextPage=true, stopping"
break
fi
done
echo "✅ Done (processed $PAGE page(s))"
# Always exit 0 — this workflow is non-critical
exit 0