From 7b696b92c0faee999791daac4ca537ca106027b2 Mon Sep 17 00:00:00 2001 From: Rafael Benevides Date: Mon, 1 Jun 2026 16:13:46 -0300 Subject: [PATCH] HYPERFLEET-1030: add GitHub Actions workflow for daily /open-prs Slack digest --- .github/workflows/README.md | 97 ++++++++++++++++++++ .github/workflows/open-prs-digest.yml | 126 ++++++++++++++++++++++++++ 2 files changed, 223 insertions(+) create mode 100644 .github/workflows/README.md create mode 100644 .github/workflows/open-prs-digest.yml diff --git a/.github/workflows/README.md b/.github/workflows/README.md new file mode 100644 index 0000000..172ea57 --- /dev/null +++ b/.github/workflows/README.md @@ -0,0 +1,97 @@ +# CI Workflows + +## Open PRs Digest + +**File:** `open-prs-digest.yml` + +Runs the `/open-prs --slack` skill every weekday via `claude-code-action` and posts the prioritized PR review queue to Slack. + +### Schedule + +- Weekdays at 9:00 AM UTC (cron) +- Manual trigger via `workflow_dispatch` + +### Architecture + +```text +cron/manual → checkout repo → install jira-cli → GCP SA key auth → claude-code-action → Slack webhook + │ + on failure → error webhook +``` + +The workflow loads the `/open-prs` SKILL.md as a system prompt via `--append-system-prompt-file`. Claude analyzes open PRs across `openshift-hyperfleet`, cross-references with JIRA, scores them using the 8-factor algorithm, and outputs a Slack-formatted digest. + +### Prerequisites + +#### GCP Service Account + +The workflow authenticates to Vertex AI using a service account key. You need: + +1. A GCP project with Vertex AI enabled (the team uses `itpc-gcp-hcm-pe-eng-claude`) +2. A service account with `roles/aiplatform.user` permission +3. A JSON key for the service account + +Create the key and store its contents as the `GCP_SA_KEY` secret: + +```bash +gcloud iam service-accounts keys create key.json \ + --iam-account=openshift-ci-github-action@itpc-gcp-hcm-pe-eng-claude.iam.gserviceaccount.com + +# Copy the JSON contents into the GCP_SA_KEY secret, then delete the file +rm key.json +``` + +#### GitHub Fine-Grained PAT + +The `/open-prs` skill queries PRs across all repos in `openshift-hyperfleet`. The default `GITHUB_TOKEN` is scoped to this repo only, so a fine-grained PAT with org-wide read access is required. + +1. Go to [GitHub → New fine-grained token](https://github.com/settings/personal-access-tokens/new) +2. **Token name**: `hyperfleet-open-prs-digest` +3. **Resource owner**: select `openshift-hyperfleet` +4. **Repository access**: All repositories +5. **Permissions** → Repository permissions: + - Contents: **Read-only** + - Pull requests: **Read-only** + - Metadata: **Read-only** (selected automatically) +6. Click **Generate token** → copy the token +7. Save it as the `GH_TOKEN_ORG_READ` secret in the repo + +#### Slack Incoming Webhooks + +Create two webhooks: + +1. **Team channel** — for the daily digest +2. **Personal/ops channel** — for error notifications + +Create webhooks in the [HyperFleet Slack App](https://api.slack.com/apps/A0B3FC58FPE/incoming-webhooks). + +### Required Secrets + +Configure these in the repo's Settings → Secrets and variables → Actions: + +| Secret | Description | Example | +|--------|-------------|---------| +| `GCP_SA_KEY` | GCP service account key JSON | (see GCP Service Account section above) | +| `ANTHROPIC_VERTEX_PROJECT_ID` | GCP project with Vertex AI | `itpc-gcp-hcm-pe-eng-claude` | +| `GH_TOKEN_ORG_READ` | GitHub PAT with repo read access across `openshift-hyperfleet` | ([generate fine-grained token](https://github.com/settings/personal-access-tokens/new) — owner: `openshift-hyperfleet`, permissions: Contents + Pull requests + Metadata read-only) | +| `JIRA_API_TOKEN` | JIRA Personal Access Token | (generate at [Atlassian API tokens](https://id.atlassian.com/manage-profile/security/api-tokens)) | +| `JIRA_AUTH_LOGIN` | JIRA account email | `user@redhat.com` | +| `SLACK_WEBHOOK_URL` | Webhook for the team channel | `https://hooks.slack.com/services/T.../B.../...` | +| `SLACK_WEBHOOK_URL_ERRORS` | Webhook for error notifications | `https://hooks.slack.com/services/T.../B.../...` | + +### Manual Trigger + +Go to Actions → Open PRs Digest → Run workflow, or: + +```bash +gh workflow run open-prs-digest.yml +``` + +### Troubleshooting + +| Symptom | Cause | Fix | +|---------|-------|-----| +| GCP auth fails | SA key invalid or expired | Generate a new key and update `GCP_SA_KEY` | +| JIRA data missing | Token expired | Generate a new PAT and update `JIRA_API_TOKEN` | +| Slack post returns non-200 | Webhook revoked | Recreate the webhook and update `SLACK_WEBHOOK_URL` | +| Claude times out | Too many PRs / max-turns too low | Increase `--max-turns` in `claude_args` | diff --git a/.github/workflows/open-prs-digest.yml b/.github/workflows/open-prs-digest.yml new file mode 100644 index 0000000..393ed4d --- /dev/null +++ b/.github/workflows/open-prs-digest.yml @@ -0,0 +1,126 @@ +name: Open PRs Digest + +on: + schedule: + - cron: '0 9 * * 1-5' + workflow_dispatch: + +jobs: + open-prs: + runs-on: ubuntu-latest + timeout-minutes: 15 + concurrency: + group: open-prs-digest + cancel-in-progress: true + permissions: + contents: read + 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=1 -f "/tmp/${JIRA_CLI_TAR}" "jira_${JIRA_CLI_VERSION}_linux_x86_64/bin/jira" + rm "/tmp/${JIRA_CLI_TAR}" + 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: Authenticate to GCP + uses: google-github-actions/auth@c200f3691d83b41bf9bbd8638997a462592937ed # v2 + with: + credentials_json: ${{ secrets.GCP_SA_KEY }} + + - name: Run /open-prs via Claude + uses: anthropics/claude-code-action@537ffff2eff706bd7e3e1c3daf2d4b39067a9f85 # v1 + id: claude + with: + use_vertex: "true" + model: "claude-sonnet-4-20250514" + prompt: | + Run the /open-prs skill with --slack flag. Analyze all open PRs across the + openshift-hyperfleet organization, cross-reference with JIRA, score and rank + them, and output a Slack-formatted prioritized list. + Output ONLY the Slack mrkdwn content — no surrounding explanation or code blocks. + claude_args: >- + --append-system-prompt-file ./hyperfleet-work-triage/skills/open-prs/SKILL.md + --allowedTools Bash,Read,Agent + --max-turns 20 + 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: Post to Slack + env: + OUTPUT_FILE: ${{ steps.claude.outputs.execution_file }} + SLACK_WEBHOOK_URL: ${{ secrets.SLACK_WEBHOOK_URL }} + run: | + if [ ! -f "$OUTPUT_FILE" ] || [ ! -s "$OUTPUT_FILE" ]; then + echo "::error::Claude output file is missing or empty" + exit 1 + fi + + 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 + + PAYLOAD="$(jq -n --arg text "$CONTENT" '{"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