Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
97 changes: 97 additions & 0 deletions .github/workflows/README.md
Original file line number Diff line number Diff line change
@@ -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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggestion: Some sections (schedule, architecture paragraph) restate the YAML and could drift. Consider trimming to just: what it does, secrets table, setup steps, manual trigger, and troubleshooting.


- 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` |
126 changes: 126 additions & 0 deletions .github/workflows/open-prs-digest.yml
Original file line number Diff line number Diff line change
@@ -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"
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Consider pinning model explicitly so the workflow stays predictable if the action's default model changes:

with:
  use_vertex: "true"
  model: "claude-sonnet-4-20250514"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Indeed. Furthermore we don't need more than Sonnet here. Thank you. I'll update

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 }}
Comment thread
coderabbitai[bot] marked this conversation as resolved.
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")"
Comment thread
coderabbitai[bot] marked this conversation as resolved.

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