-
Notifications
You must be signed in to change notification settings - Fork 17
242 lines (218 loc) · 10.2 KB
/
Copy pathopen-prs-digest.yml
File metadata and controls
242 lines (218 loc) · 10.2 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
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
name: Open PRs Digest
on:
schedule:
- cron: "0 9 * * 1-5"
workflow_dispatch: {}
jobs:
open-prs:
if: github.repository == 'openshift-hyperfleet/hyperfleet-claude-plugins'
runs-on: ubuntu-latest
timeout-minutes: 15
concurrency:
group: open-prs-digest
cancel-in-progress: true
permissions:
contents: read
id-token: write
steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
persist-credentials: false
- name: Install and configure jira-cli
run: |
set -euo pipefail
JIRA_CLI_VERSION="1.5.2"
JIRA_CLI_SHA="a0b3dbfd1eb64e217a2a74a3f79179328b5debf61d6be8a3ed0d0014bd51512a"
JIRA_CLI_TAR="jira_${JIRA_CLI_VERSION}_linux_x86_64.tar.gz"
curl -sSfL "https://github.com/ankitpokhrel/jira-cli/releases/download/v${JIRA_CLI_VERSION}/${JIRA_CLI_TAR}" \
-o "/tmp/${JIRA_CLI_TAR}"
echo "${JIRA_CLI_SHA} /tmp/${JIRA_CLI_TAR}" | sha256sum -c -
tar xz -C /usr/local/bin --strip-components=2 -f "/tmp/${JIRA_CLI_TAR}" "jira_${JIRA_CLI_VERSION}_linux_x86_64/bin/jira"
chmod +x /usr/local/bin/jira
rm "/tmp/${JIRA_CLI_TAR}"
echo "/usr/local/bin" >> "$GITHUB_PATH"
mkdir -p ~/.config/.jira
cat > ~/.config/.jira/.config.yml << EOF
auth_type: basic
server: https://redhat.atlassian.net
login: ${JIRA_AUTH_LOGIN}
project:
key: HYPERFLEET
board:
id: 1013
type: scrum
installation: Cloud
EOF
env:
JIRA_AUTH_LOGIN: ${{ secrets.JIRA_AUTH_LOGIN }}
- name: Verify jira-cli setup
run: |
set -euo pipefail
echo "=== jira binary ==="
command -v jira && jira version
echo "=== config location ==="
ls -la ~/.config/.jira/.config.yml
echo "=== config content (redacted) ==="
sed 's/login:.*/login: [REDACTED]/' ~/.config/.jira/.config.yml
echo "=== test JIRA query ==="
jira issue view HYPERFLEET-1030 --raw 2>&1 | head -5 || echo "JIRA QUERY FAILED with exit code $?"
env:
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
- name: Authenticate to GCP
uses: google-github-actions/auth@71fee32af4ce9b6f7d57c7b86c2cc8147ceadc51 # v2.1.4
with:
credentials_json: ${{ secrets.GCP_SA_KEY }}
create_credentials_file: false
export_environment_variables: false
- name: Resolve dynamic context in SKILL.md
run: |
set -euo pipefail
SKILL="./hyperfleet-work-triage/skills/open-prs/SKILL.md"
RESOLVED="${SKILL}.resolved"
CURRENT_DATE="$(date -u '+%Y-%m-%d %H:%M UTC')"
SKILL_DIR="./hyperfleet-work-triage/skills/open-prs"
awk -v dt="$CURRENT_DATE" -v skilldir="$SKILL_DIR" '
/^## Dynamic context/ {
print
print ""
print "- gh CLI: available"
print "- gh auth: authenticated"
print "- jira CLI: available"
print "- jq: available"
print "- Current date: " dt
skip=1; next
}
skip && /^## / { skip=0 }
skip { next }
{ gsub(/\$\{CLAUDE_SKILL_DIR\}/, skilldir); print }
' "$SKILL" > "$RESOLVED"
- name: Collect PR and JIRA data
id: collect-data
env:
JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }}
GH_TOKEN: ${{ secrets.GH_TOKEN_ORG_READ }}
run: |
set -euo pipefail
./hyperfleet-work-triage/skills/open-prs/scripts/collect-data.sh > /tmp/pr-data.json
- name: Run /open-prs via Claude
uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1
id: claude
with:
use_vertex: "true"
prompt: |
Process the collected PR data from /tmp/pr-data.json.
Follow SKILL.md instructions: pipe through score.jq,
classify Factor 4 for each PR, then pipe through format-output.jq with --arg mode slack.
Output ONLY the final Slack mrkdwn content — no surrounding explanation or code blocks.
claude_args: "--append-system-prompt-file ./hyperfleet-work-triage/skills/open-prs/SKILL.md.resolved --allowedTools Read,Glob,Grep --model claude-haiku-4-5@20251001"
env:
ANTHROPIC_VERTEX_PROJECT_ID: ${{ secrets.ANTHROPIC_VERTEX_PROJECT_ID }}
CLOUD_ML_REGION: global
# Explicitly exclude GCP credential environment variables to prevent credential leakage
GOOGLE_APPLICATION_CREDENTIALS: ""
CLOUDSDK_AUTH_CREDENTIAL_FILE_OVERRIDE: ""
GOOGLE_GHA_CREDS_PATH: ""
- name: Check Claude execution result
run: |
set -euo pipefail
OUTPUT_FILE="${{ steps.claude.outputs.execution_file }}"
echo "=== Claude Execution Output ==="
IS_ERROR="$(jq -r 'map(select(.type == "result")) | last | .is_error // false' "$OUTPUT_FILE" 2>/dev/null || echo 'unknown')"
MODEL_USED="$(jq -r 'map(select(.type == "system")) | last | .model // "unknown"' "$OUTPUT_FILE" 2>/dev/null || echo 'unknown')"
ERROR_MSG="$(jq -r 'map(select(.type == "assistant")) | last | .message.content[0].text // empty' "$OUTPUT_FILE" 2>/dev/null || echo '')"
echo "Is Error: $IS_ERROR"
echo "Model Used: $MODEL_USED"
if [ ! -z "$ERROR_MSG" ]; then
echo "Error Message: $ERROR_MSG"
fi
if [ "$IS_ERROR" = "true" ]; then
echo "::error::Claude execution failed. Check error message above"
exit 1
fi
- name: Post to Slack
id: post-to-slack
if: ${{ !cancelled() }}
env:
OUTPUT_FILE: ${{ steps.claude.outputs.execution_file }}
SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }}
SLACK_WEBHOOK_URL_ERRORS: ${{ secrets.SLACK_WEBHOOK_URL_ERRORS }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
if [ ! -f "$OUTPUT_FILE" ] || [ ! -s "$OUTPUT_FILE" ]; then
echo "::error::Claude output file is missing or empty"
exit 1
fi
IS_ERROR="$(jq -r 'map(select(.type == "result")) | last | .is_error // false' "$OUTPUT_FILE" 2>/dev/null)"
CONTENT="$(jq -r 'map(select(.type == "result")) | last | .result // empty' "$OUTPUT_FILE" 2>/dev/null)"
if [ -z "$CONTENT" ]; then
CONTENT="$(jq -r '[.[] | select(.type == "assistant")] | last | .message.content | map(select(.type == "text")) | last | .text // empty' "$OUTPUT_FILE" 2>/dev/null)"
fi
if [ -z "$CONTENT" ]; then
echo "::error::Could not extract result from Claude output"
exit 1
fi
# Detect error results: is_error flag, or missing expected Slack format markers
if [ "$IS_ERROR" = "true" ] || ! printf '%s' "$CONTENT" | grep -q 'Open PRs'; then
echo "::warning::Claude produced an error or unexpected output — routing to errors channel"
MESSAGE=$(printf '🔴 *Open PRs Digest — unexpected output*\n_%s_\n\n```\n%.500s\n```\n\nWorkflow run: %s' \
"$(date -u '+%Y-%m-%d %H:%M UTC')" "$CONTENT" "$RUN_URL")
HTTP_CODE="$(curl --connect-timeout 10 --max-time 30 --retry 2 --retry-delay 2 --retry-connrefused -sS -o /dev/null -w "%{http_code}" \
-X POST "$SLACK_WEBHOOK_URL_ERRORS" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg text "$MESSAGE" '{"text": $text}')")"
if [ "$HTTP_CODE" -ne 200 ]; then
echo "::error::Error Slack webhook returned HTTP $HTTP_CODE"
exit 1
fi
echo "error_handled=true" >> "$GITHUB_OUTPUT"
exit 1
fi
# Strip any LLM reasoning text before the actual Slack mrkdwn content.
# The formatted output always starts with an emoji header matching format-output.jq:
# 🚨 (Tier 1), 🟡 (Tier 2), 🟢 (Tier 3 / no PRs), 🔴 (all Tier 4), 🔵 (Tier 4 items).
CONTENT="$(printf '%s' "$CONTENT" | sed -n '/^[🟡🚨🟢🔵🔴]/,$p')"
if [ -z "$CONTENT" ]; then
echo "::error::Could not find Slack mrkdwn markers in Claude output"
exit 1
fi
PAYLOAD="$(printf '%s' "$CONTENT" | sed '/^---$/d' | jq -Rs '
split("\n") as $lines |
reduce $lines[] as $line (
[{text: ""}];
if (.[-1].text | length) + ($line | length) + 1 > 2900 then
. + [{text: $line}]
else
.[-1].text += (if .[-1].text == "" then "" else "\n" end) + $line |
.
end
) |
{blocks: [.[] | select(.text != "") | {type: "section", text: {type: "mrkdwn", text: .text}}]}
')"
HTTP_CODE="$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$SLACK_WEBHOOK_URL" \
-H 'Content-Type: application/json' \
-d "$PAYLOAD")"
if [ "$HTTP_CODE" -ne 200 ]; then
echo "::error::Slack webhook returned HTTP $HTTP_CODE"
exit 1
fi
echo "Posted to Slack successfully"
- name: Notify Slack on failure
if: failure() && steps.post-to-slack.outputs.error_handled != 'true'
env:
SLACK_WEBHOOK_URL_ERRORS: ${{ secrets.SLACK_WEBHOOK_URL_ERRORS }}
RUN_URL: ${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}
run: |
set -euo pipefail
MESSAGE=$(printf '🔴 *Open PRs Digest failed*\n_%s_\n\nWorkflow run: %s' \
"$(date -u '+%Y-%m-%d %H:%M UTC')" "$RUN_URL")
HTTP_CODE="$(curl -s -o /dev/null -w "%{http_code}" \
-X POST "$SLACK_WEBHOOK_URL_ERRORS" \
-H 'Content-Type: application/json' \
-d "$(jq -n --arg text "$MESSAGE" '{"text": $text}')")"
if [ "$HTTP_CODE" -ne 200 ]; then
echo "::error::Failure Slack webhook returned HTTP $HTTP_CODE"
exit 1
fi