HYPERFLEET-1637: update to Claude Haiku 4.5 and add debug output #94
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
| 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@34e114876b0b11c390a56381ad16ebd13914f8d5 # v4 | ||
| 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@c200f3691d83b41bf9bbd8638997a462592937ed # v2 | ||
| with: | ||
| credentials_json: ${{ secrets.GCP_SA_KEY }} | ||
| - 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 "- 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: Run /open-prs via Claude | ||
| uses: anthropics/claude-code-action@428971d2ecd6e3a7cb0ee0da2a3a8b33fdb3678d # v1 | ||
| id: claude | ||
| with: | ||
| use_vertex: "true" | ||
| prompt: | | ||
| Run the /open-prs skill with --slack flag. | ||
| Follow SKILL.md instructions: run collect-data.sh, 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 Bash,Read,Glob,Grep --model claude-haiku-4-5@20251001" | ||
| env: | ||
| ANTHROPIC_VERTEX_PROJECT_ID: ${{ secrets.ANTHROPIC_VERTEX_PROJECT_ID }} | ||
| CLOUD_ML_REGION: global | ||
| JIRA_API_TOKEN: ${{ secrets.JIRA_API_TOKEN }} | ||
| GH_TOKEN: ${{ secrets.GH_TOKEN_ORG_READ }} | ||
| - 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 | ||
| 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" | ||
| fi | ||
| 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() | ||
| 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 | ||