From 24c1094cd5fb655cd9ef6db59a0ffba7e344f11c Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Tue, 23 Jun 2026 10:13:18 -0400 Subject: [PATCH 1/7] CNTRLPLANE-3674: Add generic jira-agent step registry for cross-team reuse Extract the HyperShift-specific jira-agent prow job into a generic, parameterized step registry that any team can reuse by creating a thin wrapper workflow with their own env vars and credentials. New generic steps at ci-operator/step-registry/jira-agent/: - setup: Verifies Claude Code CLI with Vertex AI auth - process: 4-phase pipeline (solve, review, fix, PR) with 15+ configurable env vars replacing all hardcoded HyperShift values - report: HTML report with token usage and cost breakdown The existing hypershift/jira-agent workflow is converted to a thin wrapper that references the generic steps and sets HyperShift-specific configuration. Co-Authored-By: Claude Opus 4.6 --- .../hypershift-jira-agent-workflow.yaml | 41 +- ci-operator/step-registry/jira-agent/OWNERS | 12 + .../step-registry/jira-agent/README.md | 76 ++ .../jira-agent-workflow.metadata.json | 8 + .../jira-agent/jira-agent-workflow.yaml | 23 + .../step-registry/jira-agent/process/OWNERS | 12 + .../process/jira-agent-process-commands.sh | 846 ++++++++++++++++++ .../jira-agent-process-ref.metadata.json | 8 + .../process/jira-agent-process-ref.yaml | 120 +++ .../step-registry/jira-agent/report/OWNERS | 12 + .../report/jira-agent-report-commands.sh | 362 ++++++++ .../jira-agent-report-ref.metadata.json | 8 + .../report/jira-agent-report-ref.yaml | 17 + .../step-registry/jira-agent/setup/OWNERS | 12 + .../setup/jira-agent-setup-commands.sh | 10 + .../setup/jira-agent-setup-ref.metadata.json | 8 + .../setup/jira-agent-setup-ref.yaml | 34 + 17 files changed, 1595 insertions(+), 14 deletions(-) create mode 100644 ci-operator/step-registry/jira-agent/OWNERS create mode 100644 ci-operator/step-registry/jira-agent/README.md create mode 100644 ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json create mode 100644 ci-operator/step-registry/jira-agent/jira-agent-workflow.yaml create mode 100644 ci-operator/step-registry/jira-agent/process/OWNERS create mode 100644 ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh create mode 100644 ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json create mode 100644 ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.yaml create mode 100644 ci-operator/step-registry/jira-agent/report/OWNERS create mode 100644 ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh create mode 100644 ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json create mode 100644 ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.yaml create mode 100644 ci-operator/step-registry/jira-agent/setup/OWNERS create mode 100644 ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh create mode 100644 ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json create mode 100644 ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.yaml diff --git a/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml b/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml index 42857ce8af563..aba0f9ce91d0c 100644 --- a/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml +++ b/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml @@ -2,22 +2,35 @@ workflow: as: hypershift-jira-agent steps: pre: - - ref: hypershift-jira-agent-setup + - ref: jira-agent-setup test: - - ref: hypershift-jira-agent-process + - ref: jira-agent-process post: - - ref: hypershift-jira-agent-report + - ref: jira-agent-report + env: + JIRA_AGENT_FORK_REPO: "hypershift-community/hypershift" + JIRA_AGENT_UPSTREAM_REPO: "openshift/hypershift" + JIRA_AGENT_JQL: 'project in (OCPBUGS, CNTRLPLANE) AND resolution = Unresolved AND status in (New, "To Do") AND labels = issue-for-agent AND labels != agent-processed' + JIRA_AGENT_TARGET_STATUS: '{"OCPBUGS":"ASSIGNED","CNTRLPLANE":"Code Review"}' + JIRA_AGENT_ASSIGNEE: "hypershift-automation" + JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY: "o-h-installation-id" + JIRA_AGENT_FORK_INSTALLATION_ID_KEY: "installation-id" + JIRA_AGENT_EXTRA_PLUGIN_COMMANDS: | + claude plugin install utils@ai-helpers + claude plugin install golang@ai-helpers + claude plugin marketplace add enxebre/ai-scripts + claude plugin install git@enxebre + JIRA_AGENT_TOOL_SETUP_SCRIPT: "GOFLAGS='' go install golang.org/x/tools/gopls@v0.21.0 && python3.9 -m ensurepip --user 2>/dev/null || true && python3.9 -m pip install --user pre-commit 2>&1 | tail -1" + JIRA_AGENT_REVIEW_LANGUAGE: "go" + JIRA_AGENT_REVIEW_PROFILE: "hypershift" + JIRA_AGENT_SLACK_EMOJI: ":hypershift-bot:" documentation: |- - HyperShift Jira Agent workflow for automated issue processing. + HyperShift-specific wrapper for the generic Jira Agent workflow. - This workflow: - 1. Setup: Verifies Claude Code CLI is available - 2. Process: For each Jira issue, runs a four-phase pipeline: - a. Phase 1 - Solve: Runs /jira-solve to implement, commit, and push changes - b. Phase 2 - Review: Runs /code-review:pre-commit-review to review code quality (read-only) - c. Phase 3 - Fix: Addresses review findings by editing code and pushing fixes - d. Phase 4 - PR: Creates a draft PR after review is complete - 3. Report: Generates HTML report with per-phase token usage, cost estimates, and posts link on PRs + This workflow delegates to the generic jira-agent steps with HyperShift-specific + configuration (fork repo, JQL query, status transitions, plugins, etc.). - The workflow uses /jira-solve and /code-review:pre-commit-review in non-interactive mode. - Issues are queried from Jira with: project in (OCPBUGS, CNTRLPLANE) AND status in (New, "To Do") AND labels = issue-for-agent + Credentials: Uses hypershift-team-claude-prow (configured in generic step refs). + When another team onboards, they will need to either: + 1. Create their own ref YAMLs pointing to the generic commands with their credential + 2. Request the generic credential name be updated to a shared secret diff --git a/ci-operator/step-registry/jira-agent/OWNERS b/ci-operator/step-registry/jira-agent/OWNERS new file mode 100644 index 0000000000000..ff943340794d2 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/OWNERS @@ -0,0 +1,12 @@ +approvers: + - bryan-cox + - csrwng + - celebdor + - enxebre + - sjenning +reviewers: + - bryan-cox + - csrwng + - celebdor + - enxebre + - sjenning diff --git a/ci-operator/step-registry/jira-agent/README.md b/ci-operator/step-registry/jira-agent/README.md new file mode 100644 index 0000000000000..248c4a1b0a89c --- /dev/null +++ b/ci-operator/step-registry/jira-agent/README.md @@ -0,0 +1,76 @@ +# jira-agent Step Registry + +Generic, reusable Jira Agent workflow for automated issue processing using Claude Code. + +## Overview + +This step registry provides a parameterized workflow that: +1. **Setup** — Verifies Claude Code CLI is available with Vertex AI authentication +2. **Process** — Runs a four-phase pipeline for each Jira issue: + - Phase 1: Solve (implement changes, commit, push) + - Phase 2: Review (pre-commit code review, read-only) + - Phase 3: Fix (address review findings, commit, push) + - Phase 4: PR Creation (create draft PR via `gh`) +3. **Report** — Generates an HTML report with token usage and cost breakdown + +## Quick Start + +Create a wrapper workflow in your team's directory that references the generic steps and sets your team-specific env vars: + +```yaml +workflow: + as: my-team-jira-agent + steps: + pre: + - ref: jira-agent-setup + test: + - ref: jira-agent-process + post: + - ref: jira-agent-report + env: + JIRA_AGENT_FORK_REPO: "my-org/my-repo" + JIRA_AGENT_UPSTREAM_REPO: "openshift/my-repo" + JIRA_AGENT_JQL: 'project = MYPROJ AND resolution = Unresolved AND labels = issue-for-agent' +``` + +Override the credential secret in each ref's `credentials` block to point to your team's Vault secret. + +## Required Environment Variables + +| Variable | Description | +|----------|-------------| +| `JIRA_AGENT_FORK_REPO` | Fork repo in `org/repo` format (e.g., `my-org/my-repo`) | +| `JIRA_AGENT_UPSTREAM_REPO` | Upstream repo in `org/repo` format (e.g., `openshift/my-repo`) | +| `JIRA_AGENT_JQL` | JQL query to find issues to process | + +## Optional Environment Variables + +| Variable | Default | Description | +|----------|---------|-------------| +| `JIRA_AGENT_TARGET_STATUS` | `""` | JSON map of project prefix → target status | +| `JIRA_AGENT_ASSIGNEE` | `""` | Display name for assignee lookup | +| `JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY` | `o-h-installation-id` | Vault key for upstream GH App installation ID | +| `JIRA_AGENT_FORK_INSTALLATION_ID_KEY` | `installation-id` | Vault key for fork GH App installation ID | +| `JIRA_AGENT_EXTRA_PLUGIN_COMMANDS` | `""` | Newline-separated Claude plugin install commands | +| `JIRA_AGENT_TOOL_SETUP_SCRIPT` | `""` | Inline shell for project-specific tool installs | +| `JIRA_AGENT_REVIEW_LANGUAGE` | `go` | Language for code-review plugin | +| `JIRA_AGENT_REVIEW_PROFILE` | `""` | Profile for code-review plugin | +| `JIRA_AGENT_SLACK_EMOJI` | `:robot:` | Slack emoji for notifications | +| `JIRA_AGENT_MAX_ISSUES` | `1` | Max issues per run | +| `JIRA_AGENT_ISSUE_KEY` | `""` | Override to process a specific issue | +| `CLAUDE_MODEL` | `claude-opus-4-6` | Claude model to use | +| `JIRA_BASE_URL` | `https://redhat.atlassian.net` | Jira instance base URL | + +## Credentials + +Each team needs a Vault secret containing: +- `claude-prow` — GCP service account JSON for Vertex AI +- `jira-token` — Jira API token (Basic auth) +- `jira-user` — Jira username +- `github-app-id` — GitHub App ID +- `github-app-private-key` — GitHub App private key (PEM) +- `installation-id` — Fork GitHub App installation ID +- Upstream installation ID (key name configurable via `JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY`) +- `slack-webhook` — Slack incoming webhook URL + +See the onboarding guide in `openshift-eng/ai-helpers` for full setup instructions. diff --git a/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json b/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json new file mode 100644 index 0000000000000..00ebde1f41384 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json @@ -0,0 +1,8 @@ +{ + "kind": "workflow", + "api_version": "v1", + "metadata": { + "name": "jira-agent", + "description": "Generic Jira Agent workflow for automated issue processing using Claude Code." + } +} diff --git a/ci-operator/step-registry/jira-agent/jira-agent-workflow.yaml b/ci-operator/step-registry/jira-agent/jira-agent-workflow.yaml new file mode 100644 index 0000000000000..2f58105a30105 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/jira-agent-workflow.yaml @@ -0,0 +1,23 @@ +workflow: + as: jira-agent + steps: + pre: + - ref: jira-agent-setup + test: + - ref: jira-agent-process + post: + - ref: jira-agent-report + documentation: |- + Generic Jira Agent workflow for automated issue processing. + + This workflow runs a four-phase pipeline for each matching Jira issue: + 1. Setup: Verifies Claude Code CLI is available + 2. Process: Solve, review, fix, and create PR for each issue + 3. Report: Generates HTML report with token usage and cost breakdown + + Required env vars (set in your team's wrapper workflow): + - JIRA_AGENT_FORK_REPO: Fork repo (org/repo) for pushing branches + - JIRA_AGENT_UPSTREAM_REPO: Upstream repo (org/repo) for creating PRs + - JIRA_AGENT_JQL: JQL query for finding issues to process + + See the onboarding guide in openshift-eng/ai-helpers for full configuration reference. diff --git a/ci-operator/step-registry/jira-agent/process/OWNERS b/ci-operator/step-registry/jira-agent/process/OWNERS new file mode 100644 index 0000000000000..ff943340794d2 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/process/OWNERS @@ -0,0 +1,12 @@ +approvers: + - bryan-cox + - csrwng + - celebdor + - enxebre + - sjenning +reviewers: + - bryan-cox + - csrwng + - celebdor + - enxebre + - sjenning diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh new file mode 100644 index 0000000000000..efbb67e9a8ac1 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh @@ -0,0 +1,846 @@ +#!/bin/bash +set -euo pipefail + +echo "=== Jira Agent Process ===" + +# Validate required env vars +for required_var in JIRA_AGENT_FORK_REPO JIRA_AGENT_UPSTREAM_REPO JIRA_AGENT_JQL; do + if [ -z "${!required_var:-}" ]; then + echo "ERROR: Required env var $required_var is not set" + exit 1 + fi +done + +# Apply Gangway API overrides (MULTISTAGE_PARAM_OVERRIDE_* prefix) +if [[ -n "${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY:-}" ]]; then + echo "Applying Gangway override: JIRA_AGENT_ISSUE_KEY=${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" + export JIRA_AGENT_ISSUE_KEY="${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" +fi + +# Derive org names from repo slugs +FORK_ORG="${JIRA_AGENT_FORK_REPO%%/*}" +UPSTREAM_ORG="${JIRA_AGENT_UPSTREAM_REPO%%/*}" +UPSTREAM_REPO_NAME="${JIRA_AGENT_UPSTREAM_REPO##*/}" + +# Configurable defaults +UPSTREAM_INSTALL_ID_KEY="${JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY:-o-h-installation-id}" +FORK_INSTALL_ID_KEY="${JIRA_AGENT_FORK_INSTALLATION_ID_KEY:-installation-id}" +REVIEW_LANGUAGE="${JIRA_AGENT_REVIEW_LANGUAGE:-go}" +REVIEW_PROFILE="${JIRA_AGENT_REVIEW_PROFILE:-}" +SLACK_EMOJI="${JIRA_AGENT_SLACK_EMOJI:-:robot:}" +JIRA_BASE_URL="${JIRA_BASE_URL:-https://redhat.atlassian.net}" + +# State file for sharing results with report step +STATE_FILE="${SHARED_DIR}/processed-issues.txt" + +# Clone ai-helpers repository (contains /jira-solve command) +echo "Cloning ai-helpers repository..." +git clone https://github.com/openshift-eng/ai-helpers /tmp/ai-helpers + +# Clone the project fork (we push here and create PRs to upstream) +echo "Cloning ${JIRA_AGENT_FORK_REPO}..." +git clone "https://github.com/${JIRA_AGENT_FORK_REPO}" /tmp/project-repo + +# Copy jira-solve command from ai-helpers to project repo +echo "Setting up Claude commands..." +mkdir -p /tmp/project-repo/.claude/commands +cp /tmp/ai-helpers/plugins/jira/commands/solve.md /tmp/project-repo/.claude/commands/jira-solve.md + +# Check if code-review plugin is available for Phase 2 +REVIEW_PLUGIN_DIR="/tmp/ai-helpers/plugins/code-review" +if [ ! -d "${REVIEW_PLUGIN_DIR}/.claude-plugin" ]; then + echo "ERROR: code-review plugin not found at ${REVIEW_PLUGIN_DIR}/.claude-plugin" + exit 1 +fi +echo "Code-review plugin found" + +# Install tool dependencies (project-specific) +if [ -n "${JIRA_AGENT_TOOL_SETUP_SCRIPT:-}" ]; then + echo "Running project-specific tool setup..." + eval "$JIRA_AGENT_TOOL_SETUP_SCRIPT" +fi +export PATH="${GOPATH:-$HOME/go}/bin:$HOME/.local/bin:$PATH" + +# Install plugins +echo "Installing Claude Code plugins..." +claude plugin marketplace add openshift-eng/ai-helpers + +# Run any extra plugin setup commands +if [ -n "${JIRA_AGENT_EXTRA_PLUGIN_COMMANDS:-}" ]; then + echo "Running extra plugin commands..." + echo "$JIRA_AGENT_EXTRA_PLUGIN_COMMANDS" | while IFS= read -r cmd; do + [ -n "$cmd" ] && eval "$cmd" + done +fi + +cd /tmp/project-repo + +# Configure git +git config user.name "OpenShift CI Bot" +git config user.email "ci-bot@redhat.com" + +# Sync fork with upstream before doing any work +echo "Syncing fork with upstream ${JIRA_AGENT_UPSTREAM_REPO}..." +git remote add upstream "https://github.com/${JIRA_AGENT_UPSTREAM_REPO}.git" +git fetch upstream main +git checkout main +git rebase upstream/main +echo "Fork synced with upstream successfully" + +# Generate GitHub App installation token +echo "Generating GitHub App token..." + +GITHUB_APP_CREDS_DIR="/var/run/claude-code-service-account" +APP_ID_FILE="${GITHUB_APP_CREDS_DIR}/app-id" +INSTALLATION_ID_FILE="${GITHUB_APP_CREDS_DIR}/${FORK_INSTALL_ID_KEY}" +PRIVATE_KEY_FILE="${GITHUB_APP_CREDS_DIR}/private-key" + +# Check if all required credentials exist +INSTALLATION_ID_UPSTREAM_FILE="${GITHUB_APP_CREDS_DIR}/${UPSTREAM_INSTALL_ID_KEY}" + +if [ ! -f "$APP_ID_FILE" ] || [ ! -f "$INSTALLATION_ID_FILE" ] || [ ! -f "$PRIVATE_KEY_FILE" ] || [ ! -f "$INSTALLATION_ID_UPSTREAM_FILE" ]; then + echo "GitHub App credentials not yet available in ${GITHUB_APP_CREDS_DIR}" + echo "Available files:" + ls -la "${GITHUB_APP_CREDS_DIR}/" || echo "Directory does not exist" + echo "" + echo "Waiting for Vault secretsync to complete. The following keys are required:" + echo " - app-id" + echo " - ${FORK_INSTALL_ID_KEY} (for ${FORK_ORG} fork)" + echo " - ${UPSTREAM_INSTALL_ID_KEY} (for ${JIRA_AGENT_UPSTREAM_REPO} upstream)" + echo " - private-key" + echo "" + echo "Exiting gracefully. Re-run once secrets are synced." + exit 0 +fi + +APP_ID=$(cat "$APP_ID_FILE") +INSTALLATION_ID_FORK=$(cat "$INSTALLATION_ID_FILE") +INSTALLATION_ID_UPSTREAM=$(cat "$INSTALLATION_ID_UPSTREAM_FILE") + +# Function to generate GitHub App token for a given installation ID +generate_github_token() { + local INSTALL_ID=$1 + local NOW + NOW=$(date +%s) + local IAT=$((NOW - 60)) + local EXP=$((NOW + 600)) + + local HEADER + HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') + local PAYLOAD + PAYLOAD=$(echo -n "{\"iat\":${IAT},\"exp\":${EXP},\"iss\":\"${APP_ID}\"}" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') + local SIGNATURE + SIGNATURE=$(echo -n "${HEADER}.${PAYLOAD}" | openssl dgst -sha256 -sign "$PRIVATE_KEY_FILE" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') + local JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}" + + curl -s -X POST \ + -H "Authorization: Bearer ${JWT}" \ + -H "Accept: application/vnd.github+json" \ + "https://api.github.com/app/installations/${INSTALL_ID}/access_tokens" \ + | jq -r '.token' +} + +# Generate token for fork - for pushing branches +echo "Generating GitHub App token for fork..." +GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") +if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then + echo "ERROR: Failed to generate GitHub App token for fork" + exit 1 +fi +echo "Fork token generated successfully" + +# Generate token for upstream - for creating PRs +echo "Generating GitHub App token for upstream..." +GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") +if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then + echo "ERROR: Failed to generate GitHub App token for upstream" + exit 1 +fi +echo "Upstream token generated successfully" + +# Configure git to use the fork token for push operations via credential helper +git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" + +# Export upstream token as GITHUB_TOKEN for gh CLI (used for PR creation) +export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" +echo "GitHub App tokens configured successfully" + +# Configuration: maximum issues to process per run (default: 1) +MAX_ISSUES=${JIRA_AGENT_MAX_ISSUES:-1} +echo "Configuration: MAX_ISSUES=$MAX_ISSUES" + +# Shared prompt instruction for subagent behavior +SUBAGENT_PROMPT="SUBAGENTS: Launch ALL subagents in parallel (single message with multiple Task tool calls) for maximum speed. Each subagent should be given subagent_type: \"general-purpose\". Do NOT set the model parameter — let subagents inherit the parent model, as these analysis tasks require a capable model." + +# Load Jira API credentials for Atlassian Cloud (Basic Auth: email:api-token) +JIRA_TOKEN_FILE="/var/run/claude-code-service-account/jira-pat" +JIRA_EMAIL_FILE="/var/run/claude-code-service-account/jira-email" +if [ -f "$JIRA_TOKEN_FILE" ] && [ -f "$JIRA_EMAIL_FILE" ]; then + JIRA_TOKEN=$(cat "$JIRA_TOKEN_FILE") + JIRA_EMAIL=$(cat "$JIRA_EMAIL_FILE") + JIRA_AUTH=$(echo -n "${JIRA_EMAIL}:${JIRA_TOKEN}" | base64 | tr -d '\n') + echo "Jira API credentials loaded (email + token)" +else + echo "Warning: Jira credentials not found (need both jira-pat and jira-email)" + echo "Labels will not be added to processed issues" + JIRA_TOKEN="" + JIRA_AUTH="" +fi + +# Load Slack webhook URL for notifications (tracing disabled to protect credential) +SLACK_WEBHOOK_FILE="/var/run/claude-code-service-account/slack-webhook-url" +[[ $- == *x* ]] && _SLACK_WAS_TRACING=true || _SLACK_WAS_TRACING=false +set +x +if [ -f "$SLACK_WEBHOOK_FILE" ]; then + SLACK_WEBHOOK_URL=$(cat "$SLACK_WEBHOOK_FILE") + echo "Slack webhook URL loaded" +else + echo "Warning: Slack webhook URL not found at $SLACK_WEBHOOK_FILE" + echo "Slack notifications will be skipped" + SLACK_WEBHOOK_URL="" +fi +$_SLACK_WAS_TRACING && set -x + +# Load GitHub-to-Slack user ID mapping +GITHUB_SLACK_MAP_FILE="/var/run/claude-code-service-account/gh-to-slack-ids" +if [ -f "$GITHUB_SLACK_MAP_FILE" ]; then + if GITHUB_SLACK_MAP=$(jq -c . < "$GITHUB_SLACK_MAP_FILE" 2>/dev/null); then + echo "GitHub-to-Slack mapping loaded" + else + echo "Warning: GitHub-to-Slack mapping is invalid JSON" + echo "Reviewer pings will use GitHub usernames instead of Slack mentions" + GITHUB_SLACK_MAP="{}" + fi +else + echo "Warning: GitHub-to-Slack mapping not found at $GITHUB_SLACK_MAP_FILE" + echo "Reviewer pings will use GitHub usernames instead of Slack mentions" + GITHUB_SLACK_MAP="{}" +fi + +# Extract Slack fallback user ID from mapping (pinged when no reviewers are assigned) +SLACK_FALLBACK_USER_ID=$(jq -r '.["backup-user"] // empty' <<<"$GITHUB_SLACK_MAP") +if [ -n "$SLACK_FALLBACK_USER_ID" ]; then + echo "Slack fallback user ID loaded from mapping" +else + echo "Warning: No 'backup-user' key in GitHub-to-Slack mapping" +fi + +# Function to transition a Jira issue to a target status +transition_issue() { + local ISSUE_KEY=$1 + local TARGET_STATUS=$2 + + # Get available transitions + TRANSITIONS=$(curl -s \ + "${JIRA_BASE_URL}/rest/api/3/issue/$ISSUE_KEY/transitions" \ + -H "Authorization: Basic $JIRA_AUTH" \ + -H "Content-Type: application/json") + + # Find transition ID for target status (match by name) + TRANSITION_ID=$(echo "$TRANSITIONS" | jq -r --arg status "$TARGET_STATUS" \ + '.transitions[] | select(.name == $status) | .id' | head -1) + + if [ -n "$TRANSITION_ID" ] && [ "$TRANSITION_ID" != "null" ]; then + curl -s -X POST \ + "${JIRA_BASE_URL}/rest/api/3/issue/$ISSUE_KEY/transitions" \ + -H "Authorization: Basic $JIRA_AUTH" \ + -H "Content-Type: application/json" \ + -d "{\"transition\":{\"id\":\"$TRANSITION_ID\"}}" + return 0 + else + echo " Warning: Transition to '$TARGET_STATUS' not available" + return 1 + fi +} + +# Function to set assignee on a Jira issue (Cloud uses accountId) +set_assignee() { + local ISSUE_KEY=$1 + local ACCOUNT_ID=$2 + + curl -s -w "\n%{http_code}" -X PUT \ + "${JIRA_BASE_URL}/rest/api/3/issue/$ISSUE_KEY/assignee" \ + -H "Authorization: Basic $JIRA_AUTH" \ + -H "Content-Type: application/json" \ + -d "{\"accountId\":\"$ACCOUNT_ID\"}" +} + +# Function to send Slack notification after PR creation +send_slack_notification() { + local PR_URL=$1 + local PR_NUM=$2 + + if [ -z "$SLACK_WEBHOOK_URL" ]; then + echo " Skipping Slack notification (no webhook URL configured)" + return 0 + fi + + echo " Polling for PR reviewers (up to 2 minutes)..." + local REVIEWERS="" + local PR_TITLE="" + local ATTEMPT=0 + local MAX_ATTEMPTS=5 + + while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do + local PR_DATA + PR_DATA=$(gh pr view "$PR_NUM" --repo "${JIRA_AGENT_UPSTREAM_REPO}" --json reviewRequests,title 2>/dev/null || echo "{}") + PR_TITLE=$(echo "$PR_DATA" | jq -r '.title // empty' 2>/dev/null) + REVIEWERS=$(echo "$PR_DATA" | jq -r '.reviewRequests[]?.login // empty' 2>/dev/null) + if [ -n "$REVIEWERS" ]; then + echo " Reviewers found: $REVIEWERS" + break + fi + ATTEMPT=$((ATTEMPT + 1)) + if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then + echo " No reviewers yet, retrying in 30s (attempt $ATTEMPT/$MAX_ATTEMPTS)..." + sleep 30 + fi + done + + # Fallback PR title if not fetched + if [ -z "$PR_TITLE" ]; then + PR_TITLE="PR #${PR_NUM}" + fi + + # Build reviewer mention string + local REVIEWER_MENTIONS="" + if [ -n "$REVIEWERS" ]; then + while IFS= read -r gh_user; do + local slack_id + slack_id=$(echo "$GITHUB_SLACK_MAP" | jq -r --arg user "$gh_user" '.[$user] // empty' 2>/dev/null) + if [ -n "$slack_id" ]; then + REVIEWER_MENTIONS="${REVIEWER_MENTIONS} <@${slack_id}>" + else + REVIEWER_MENTIONS="${REVIEWER_MENTIONS} ${gh_user}" + fi + done <<< "$REVIEWERS" + else + echo " No reviewers assigned after 2 minutes, using fallback" + if [ -n "$SLACK_FALLBACK_USER_ID" ]; then + REVIEWER_MENTIONS="<@${SLACK_FALLBACK_USER_ID}>" + else + REVIEWER_MENTIONS="(none assigned)" + fi + fi + REVIEWER_MENTIONS=$(echo "$REVIEWER_MENTIONS" | sed 's/^ //') + + # Send Slack message (tracing disabled to protect webhook URL) + local SLACK_PAYLOAD + SLACK_PAYLOAD=$(jq -n --arg title "$PR_TITLE" --arg url "$PR_URL" --arg reviewers "$REVIEWER_MENTIONS" --arg emoji "$SLACK_EMOJI" \ + '{text: "\($emoji) *Jira Agent PR ready for review*\n:review: <\($url)|\($title)>\n:eyes: Reviewers: \($reviewers)"}') + + [[ $- == *x* ]] && local _was_tracing=true || local _was_tracing=false + set +x + set +e + local SLACK_RESPONSE + SLACK_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ + --connect-timeout 10 \ + --max-time 20 \ + -H 'Content-type: application/json' \ + --data "$SLACK_PAYLOAD" \ + "$SLACK_WEBHOOK_URL") + local CURL_EXIT_CODE=$? + set -e + $_was_tracing && set -x + + if [ $CURL_EXIT_CODE -ne 0 ]; then + echo " Warning: Failed to send Slack notification (curl exit $CURL_EXIT_CODE)" + return 0 + fi + + local SLACK_HTTP_CODE + SLACK_HTTP_CODE=$(echo "$SLACK_RESPONSE" | tail -1) + + if [ "$SLACK_HTTP_CODE" = "200" ]; then + echo " Slack notification sent successfully" + else + echo " Warning: Failed to send Slack notification (HTTP $SLACK_HTTP_CODE)" + fi +} + +# Query Jira for issues (excluding already processed ones via label) +echo "Querying Jira for issues..." +if [ -n "${JIRA_AGENT_ISSUE_KEY:-}" ]; then + echo "Using override: JIRA_AGENT_ISSUE_KEY=$JIRA_AGENT_ISSUE_KEY" + JQL="key = ${JIRA_AGENT_ISSUE_KEY}" +else + JQL="$JIRA_AGENT_JQL" +fi +SEARCH_PAYLOAD=$(jq -n --arg jql "$JQL" --argjson max "$MAX_ISSUES" \ + '{jql: $jql, fields: ["key", "summary"], maxResults: $max}') +SEARCH_RESPONSE=$(curl -s -w "\n%{http_code}" "${JIRA_BASE_URL}/rest/api/3/search/jql" \ + -X POST \ + -H "Authorization: Basic $JIRA_AUTH" \ + -H "Content-Type: application/json" \ + -d "$SEARCH_PAYLOAD") +SEARCH_HTTP_CODE=$(echo "$SEARCH_RESPONSE" | tail -1) +SEARCH_BODY=$(echo "$SEARCH_RESPONSE" | sed '$d') + +if [ "$SEARCH_HTTP_CODE" != "200" ]; then + echo "ERROR: Jira search failed (HTTP $SEARCH_HTTP_CODE)" + echo "Response: $SEARCH_BODY" + exit 1 +fi + +TOTAL_RESULTS=$(echo "$SEARCH_BODY" | jq -r '.total // 0') +echo "Jira search returned $TOTAL_RESULTS result(s)" +ISSUES=$(echo "$SEARCH_BODY" | jq -r '.issues[]? | "\(.key) \(.fields.summary)"') + +if [ -z "$ISSUES" ]; then + echo "No issues found matching criteria" + exit 0 +fi + +echo "Found issues:" +echo "$ISSUES" | awk '{print " - " $1}' + +# Counters for summary +PROCESSED_COUNT=0 +FAILED_COUNT=0 +TOTAL_PROCESSED_OR_FAILED=0 + +# Process each issue +while IFS= read -r line; do + # Stop if we've reached the max issues limit (counting both successful and failed) + if [ $TOTAL_PROCESSED_OR_FAILED -ge "$MAX_ISSUES" ]; then + echo "Reached maximum issues limit ($MAX_ISSUES). Stopping." + break + fi + # Reset to main branch for clean state between issues + git checkout main 2>/dev/null || true + git reset --hard upstream/main 2>/dev/null || true + + ISSUE_KEY=$(echo "$line" | awk '{print $1}') + ISSUE_SUMMARY=$(echo "$line" | cut -d' ' -f2-) + + echo "" + echo "==========================================" + echo "Processing: $ISSUE_KEY" + echo "Summary: $ISSUE_SUMMARY" + echo "==========================================" + + # Run jira-solve command non-interactively using --system-prompt + TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) + + echo "Running: jira-solve $ISSUE_KEY origin --ci" + + PHASE1_START=$(date +%s) + + # Load the skill content as system prompt + SKILL_CONTENT=$(cat /tmp/project-repo/.claude/commands/jira-solve.md) + + # Additional context for fork-based workflow + FORK_CONTEXT="IMPORTANT: You are working in a fork (${JIRA_AGENT_FORK_REPO}). Git push is pre-configured to work with the fork. After creating commits on your feature branch, push the branch to origin. Do NOT create a Pull Request - the PR will be created in a subsequent automated step after code review. SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. ${SUBAGENT_PROMPT}" + + set +e # Don't exit on error for individual issues + echo "Starting Claude processing with streaming output..." + claude -p "$ISSUE_KEY origin --ci. $FORK_CONTEXT" \ + --system-prompt "$SKILL_CONTENT" \ + --allowedTools "Bash Read Write Edit Grep Glob WebFetch" \ + --max-turns 300 \ + --effort max \ + --model "$CLAUDE_MODEL" \ + --verbose \ + --output-format stream-json \ + 2> "/tmp/claude-${ISSUE_KEY}-output.log" \ + | tee "/tmp/claude-${ISSUE_KEY}-output.json" + EXIT_CODE=$? + set -e + jq -j 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "/tmp/claude-${ISSUE_KEY}-output.json" > "${SHARED_DIR}/claude-${ISSUE_KEY}-output-text.txt" 2>/dev/null || true + jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "/tmp/claude-${ISSUE_KEY}-output.json" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-${ISSUE_KEY}-output-tools.txt" 2>/dev/null || true + jq -r 'select(.type == "user") | .tool_use_result | select(type == "string") | select(startswith("Error:")) | gsub("\n"; "⏎")' "/tmp/claude-${ISSUE_KEY}-output.json" 2>/dev/null | sort | uniq -c | sort -rn | sed 's/⏎/\n/g' > "${SHARED_DIR}/claude-${ISSUE_KEY}-output-errors.txt" 2>/dev/null || true + # Extract token usage for Phase 1 + grep '"type":"result"' "/tmp/claude-${ISSUE_KEY}-output.json" \ + | head -1 \ + | jq '{ + total_cost_usd: (.total_cost_usd // 0), + duration_ms: (.duration_ms // 0), + num_turns: (.num_turns // 0), + input_tokens: (.usage.input_tokens // 0), + output_tokens: (.usage.output_tokens // 0), + cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), + cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), + model_usage: (.modelUsage // {}), + model: ((.modelUsage // {} | keys | first) // "unknown") + }' > "${SHARED_DIR}/claude-${ISSUE_KEY}-solve-tokens.json" 2>/dev/null \ + || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-${ISSUE_KEY}-solve-tokens.json" + echo "Phase 1 tokens: $(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-solve-tokens.json")" + + PHASE1_END=$(date +%s) + PHASE1_DURATION=$((PHASE1_END - PHASE1_START)) + echo "Phase 1 duration: ${PHASE1_DURATION}s" + echo "$PHASE1_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-solve-duration.txt" + + if [ $EXIT_CODE -eq 0 ]; then + echo "Phase 1 (jira-solve) completed for $ISSUE_KEY" + + # Check if code changes were made (branch changed from main) + BRANCH_NAME=$(git branch --show-current) + HAS_CODE_CHANGES=false + PR_URL="" + + if [ "$BRANCH_NAME" != "main" ] && [ "$BRANCH_NAME" != "master" ] && [ -n "$BRANCH_NAME" ]; then + DIFF_FILES=$(git diff main...HEAD --name-only 2>/dev/null || echo "") + if [ -n "$DIFF_FILES" ]; then + HAS_CODE_CHANGES=true + echo "Code changes detected on branch '$BRANCH_NAME':" + echo "$DIFF_FILES" | sed 's/^/ /' | head -20 + fi + fi + + if [ "$HAS_CODE_CHANGES" = true ]; then + # === Phase 2: Pre-commit quality review === + echo "" + echo "==========================================" + echo "Phase 2: Pre-commit quality review for $ISSUE_KEY" + echo "==========================================" + + PHASE2_START=$(date +%s) + + REVIEW_PROMPT="/code-review:pre-commit-review --language ${REVIEW_LANGUAGE}" + if [ -n "$REVIEW_PROFILE" ]; then + REVIEW_PROMPT="${REVIEW_PROMPT} --profile ${REVIEW_PROFILE}" + fi + + set +e + claude -p "$REVIEW_PROMPT" \ + --plugin-dir "${REVIEW_PLUGIN_DIR}" \ + --append-system-prompt "SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. ${SUBAGENT_PROMPT}" \ + --allowedTools "Bash Read Grep Glob Task" \ + --max-turns 225 \ + --effort max \ + --model "$CLAUDE_MODEL" \ + --verbose \ + --output-format stream-json \ + 2> "/tmp/claude-${ISSUE_KEY}-review.log" \ + | tee "/tmp/claude-${ISSUE_KEY}-review.json" + REVIEW_EXIT_CODE=$? + set -e + + jq -j 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "/tmp/claude-${ISSUE_KEY}-review.json" > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt" 2>/dev/null || true + jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "/tmp/claude-${ISSUE_KEY}-review.json" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tools.txt" 2>/dev/null || true + jq -r 'select(.type == "user") | .tool_use_result | select(type == "string") | select(startswith("Error:")) | gsub("\n"; "⏎")' "/tmp/claude-${ISSUE_KEY}-review.json" 2>/dev/null | sort | uniq -c | sort -rn | sed 's/⏎/\n/g' > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-errors.txt" 2>/dev/null || true + # Extract token usage for Phase 2 + grep '"type":"result"' "/tmp/claude-${ISSUE_KEY}-review.json" \ + | head -1 \ + | jq '{ + total_cost_usd: (.total_cost_usd // 0), + duration_ms: (.duration_ms // 0), + num_turns: (.num_turns // 0), + input_tokens: (.usage.input_tokens // 0), + output_tokens: (.usage.output_tokens // 0), + cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), + cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), + model_usage: (.modelUsage // {}), + model: ((.modelUsage // {} | keys | first) // "unknown") + }' > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tokens.json" 2>/dev/null \ + || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tokens.json" + echo "Phase 2 tokens: $(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tokens.json")" + + PHASE2_END=$(date +%s) + PHASE2_DURATION=$((PHASE2_END - PHASE2_START)) + echo "Phase 2 duration: ${PHASE2_DURATION}s" + echo "$PHASE2_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-duration.txt" + + if [ $REVIEW_EXIT_CODE -eq 0 ]; then + echo "Phase 2 (pre-commit review) completed for $ISSUE_KEY" + else + echo "Phase 2 (pre-commit review) failed for $ISSUE_KEY (exit code: $REVIEW_EXIT_CODE)" + echo "Continuing with PR creation despite review failure..." + fi + + # === Phase 3: Address review findings === + echo "" + echo "==========================================" + echo "Phase 3: Addressing review findings for $ISSUE_KEY" + echo "==========================================" + + # Read the review text to feed as context + REVIEW_FINDINGS="" + if [ -f "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt" ] && \ + [ -s "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt" ]; then + REVIEW_FINDINGS=$(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt") + fi + + # Refresh tokens before Phase 3 since it pushes code. + echo "Refreshing GitHub App tokens before Phase 3..." + GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") + if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then + echo "ERROR: Failed to refresh GitHub App token for fork" + else + git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" + echo "Fork token refreshed" + fi + + PHASE3_START=$(date +%s) + + if [ -n "$REVIEW_FINDINGS" ]; then + FIX_PROMPT="A code review was performed on the changes in the current branch. Below are the review findings. Address all actions and improvements by editing the code. After making all fixes, commit the changes (amend existing commits or create new commits as appropriate) and push the branch to origin. + +REVIEW FINDINGS: +${REVIEW_FINDINGS} + +IMPORTANT: +- Fix every issue identified in the review — all actions and improvements. +- Run 'make test' and 'make verify' after fixes to verify nothing is broken. +- If 'make verify' generates new files, commit those too and run 'make verify' again to confirm it passes. +- Commit all fixes and push to origin. +- SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. +- ${SUBAGENT_PROMPT}" + + set +e + claude -p "$FIX_PROMPT" \ + --allowedTools "Bash Read Write Edit Grep Glob" \ + --max-turns 225 \ + --effort max \ + --model "$CLAUDE_MODEL" \ + --verbose \ + --output-format stream-json \ + 2> "/tmp/claude-${ISSUE_KEY}-fix.log" \ + | tee "/tmp/claude-${ISSUE_KEY}-fix.json" + FIX_EXIT_CODE=$? + set -e + + # Extract fix phase output for report + jq -j 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "/tmp/claude-${ISSUE_KEY}-fix.json" > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-text.txt" 2>/dev/null || true + jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "/tmp/claude-${ISSUE_KEY}-fix.json" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tools.txt" 2>/dev/null || true + jq -r 'select(.type == "user") | .tool_use_result | select(type == "string") | select(startswith("Error:")) | gsub("\n"; "⏎")' "/tmp/claude-${ISSUE_KEY}-fix.json" 2>/dev/null | sort | uniq -c | sort -rn | sed 's/⏎/\n/g' > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-errors.txt" 2>/dev/null || true + # Extract token usage for Phase 3 + grep '"type":"result"' "/tmp/claude-${ISSUE_KEY}-fix.json" \ + | head -1 \ + | jq '{ + total_cost_usd: (.total_cost_usd // 0), + duration_ms: (.duration_ms // 0), + num_turns: (.num_turns // 0), + input_tokens: (.usage.input_tokens // 0), + output_tokens: (.usage.output_tokens // 0), + cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), + cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), + model_usage: (.modelUsage // {}), + model: ((.modelUsage // {} | keys | first) // "unknown") + }' > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tokens.json" 2>/dev/null \ + || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tokens.json" + echo "Phase 3 tokens: $(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tokens.json")" + + if [ $FIX_EXIT_CODE -eq 0 ]; then + echo "Phase 3 (address review) completed for $ISSUE_KEY" + else + echo "Phase 3 (address review) failed (exit code: $FIX_EXIT_CODE)" + echo "Continuing with PR creation..." + fi + else + echo "No review findings to address, skipping Phase 3" + fi + + PHASE3_END=$(date +%s) + PHASE3_DURATION=$((PHASE3_END - PHASE3_START)) + echo "Phase 3 duration: ${PHASE3_DURATION}s" + echo "$PHASE3_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-duration.txt" + + # Regenerate GitHub App tokens before Phase 4. + echo "Refreshing GitHub App tokens before Phase 4..." + GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") + if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then + echo "ERROR: Failed to refresh GitHub App token for fork" + else + git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" + echo "Fork token refreshed" + fi + + GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") + if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then + echo "ERROR: Failed to refresh GitHub App token for upstream" + else + export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" + echo "Upstream token refreshed" + fi + + # === Phase 4: Create Pull Request === + echo "" + echo "==========================================" + echo "Phase 4: Creating Pull Request for $ISSUE_KEY" + echo "==========================================" + + PHASE4_START=$(date +%s) + + PR_PROMPT="Create a pull request for the changes on branch '${BRANCH_NAME}'. Details: +- Jira issue: ${ISSUE_KEY} +- Jira summary: ${ISSUE_SUMMARY} +- Jira URL: ${JIRA_BASE_URL}/browse/${ISSUE_KEY} +- Read the PR template at .github/PULL_REQUEST_TEMPLATE.md and use it to structure the PR body. +- Use 'git log main..HEAD' to understand what changed and write a meaningful description. +- PR title must start with '${ISSUE_KEY}: '. +- The PR body MUST end with the following two lines: + Always review AI generated responses prior to use. + Generated with [Claude Code](https://claude.com/claude-code) via \`/jira:solve ${ISSUE_KEY}\` +- Create the PR by running: gh pr create --repo ${JIRA_AGENT_UPSTREAM_REPO} --head ${FORK_ORG}:${BRANCH_NAME} --no-maintainer-edit --title '' --body '<body>' +- SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. +- ${SUBAGENT_PROMPT}" + + set +e + claude -p "$PR_PROMPT" \ + --allowedTools "Bash Read Grep Glob" \ + --max-turns 90 \ + --effort max \ + --model "$CLAUDE_MODEL" \ + --verbose \ + --output-format stream-json \ + 2> "/tmp/claude-${ISSUE_KEY}-pr.log" \ + | tee "/tmp/claude-${ISSUE_KEY}-pr.json" + PR_EXIT_CODE=$? + set -e + + jq -j 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "/tmp/claude-${ISSUE_KEY}-pr.json" > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-text.txt" 2>/dev/null || true + jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "/tmp/claude-${ISSUE_KEY}-pr.json" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tools.txt" 2>/dev/null || true + jq -r 'select(.type == "user") | .tool_use_result | select(type == "string") | select(startswith("Error:")) | gsub("\n"; "⏎")' "/tmp/claude-${ISSUE_KEY}-pr.json" 2>/dev/null | sort | uniq -c | sort -rn | sed 's/⏎/\n/g' > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-errors.txt" 2>/dev/null || true + # Extract token usage for Phase 4 + grep '"type":"result"' "/tmp/claude-${ISSUE_KEY}-pr.json" \ + | head -1 \ + | jq '{ + total_cost_usd: (.total_cost_usd // 0), + duration_ms: (.duration_ms // 0), + num_turns: (.num_turns // 0), + input_tokens: (.usage.input_tokens // 0), + output_tokens: (.usage.output_tokens // 0), + cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), + cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), + model_usage: (.modelUsage // {}), + model: ((.modelUsage // {} | keys | first) // "unknown") + }' > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tokens.json" 2>/dev/null \ + || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tokens.json" + echo "Phase 4 tokens: $(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tokens.json")" + + PHASE4_END=$(date +%s) + PHASE4_DURATION=$((PHASE4_END - PHASE4_START)) + echo "Phase 4 duration: ${PHASE4_DURATION}s" + echo "$PHASE4_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-duration.txt" + + if [ $PR_EXIT_CODE -eq 0 ]; then + PR_URL=$(grep -o "https://github.com/${JIRA_AGENT_UPSTREAM_REPO}/pull/[0-9]*" "/tmp/claude-${ISSUE_KEY}-pr.json" | head -1 || echo "") + if [ -n "$PR_URL" ]; then + echo "PR created: $PR_URL" + else + echo "Phase 4 completed but no PR URL found in output" + fi + else + echo "Phase 4 (PR creation) failed for $ISSUE_KEY (exit code: $PR_EXIT_CODE)" + PR_URL="" + fi + + # Append report link to PR description + if [ -n "$PR_URL" ]; then + PR_NUM=$(echo "$PR_URL" | grep -o '[0-9]*$' || true) + if [ -n "$PR_NUM" ]; then + REPORT_URL="" + if [ -n "${BUILD_ID:-}" ] && [ -n "${JOB_NAME:-}" ]; then + if [ "${JOB_TYPE:-}" = "periodic" ]; then + REPORT_URL="https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/${JOB_NAME}/${BUILD_ID}/artifacts/periodic-jira-agent/jira-agent-report/artifacts/jira-agent-report.html" + else + REPORT_URL="https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/openshift_release/${PULL_NUMBER:-0}/${JOB_NAME}/${BUILD_ID}/artifacts/periodic-jira-agent/jira-agent-report/artifacts/jira-agent-report.html" + fi + fi + + if [ -n "$REPORT_URL" ]; then + echo "Appending report link to PR #${PR_NUM} description..." + CURRENT_BODY=$(gh pr view "$PR_NUM" --repo "${JIRA_AGENT_UPSTREAM_REPO}" --json body -q .body 2>/dev/null || echo "") + REPORT_SECTION="--- + +> **Note:** This PR was auto-generated by the jira-agent periodic CI job in response to [${ISSUE_KEY}](${JIRA_BASE_URL}/browse/${ISSUE_KEY}). See the [full report](${REPORT_URL}) for token usage, cost breakdown, and detailed phase output." + UPDATED_BODY="${CURRENT_BODY} + +${REPORT_SECTION}" + gh pr edit "$PR_NUM" --repo "${JIRA_AGENT_UPSTREAM_REPO}" --body "$UPDATED_BODY" 2>/dev/null || echo "Warning: Failed to update PR #${PR_NUM} description" + fi + fi + fi + + # Send Slack notification to team channel + if [ -n "$PR_URL" ] && [ -n "$PR_NUM" ]; then + send_slack_notification "$PR_URL" "$PR_NUM" + fi + else + echo "No code changes detected for $ISSUE_KEY, skipping review and PR creation" + fi + + # Add 'agent-processed' label to mark issue as handled + if [ -n "$JIRA_AUTH" ]; then + echo "Adding 'agent-processed' label to $ISSUE_KEY..." + LABEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \ + "${JIRA_BASE_URL}/rest/api/3/issue/$ISSUE_KEY" \ + -H "Authorization: Basic $JIRA_AUTH" \ + -H "Content-Type: application/json" \ + -d '{"update":{"labels":[{"add":"agent-processed"}]}}') + HTTP_CODE=$(echo "$LABEL_RESPONSE" | tail -1) + if [ "$HTTP_CODE" = "204" ] || [ "$HTTP_CODE" = "200" ]; then + echo " Label added successfully" + else + echo " Warning: Failed to add label (HTTP $HTTP_CODE)" + fi + + # Transition issue to appropriate status based on configured mapping + if [ -n "${JIRA_AGENT_TARGET_STATUS:-}" ]; then + PROJECT_PREFIX=$(echo "$ISSUE_KEY" | cut -d'-' -f1) + TARGET_STATUS=$(echo "$JIRA_AGENT_TARGET_STATUS" | jq -r --arg prefix "$PROJECT_PREFIX" '.[$prefix] // empty') + if [ -n "$TARGET_STATUS" ]; then + echo "Transitioning $ISSUE_KEY to '$TARGET_STATUS'..." + if transition_issue "$ISSUE_KEY" "$TARGET_STATUS"; then + echo " Transition successful" + else + echo " Transition failed or not available" + fi + fi + fi + + # Set assignee if configured + if [ -n "${JIRA_AGENT_ASSIGNEE:-}" ]; then + echo "Looking up accountId for '${JIRA_AGENT_ASSIGNEE}'..." + ASSIGNEE_ACCOUNT_ID=$(curl -s -G \ + "${JIRA_BASE_URL}/rest/api/3/user/search" \ + -H "Authorization: Basic $JIRA_AUTH" \ + --data-urlencode "query=${JIRA_AGENT_ASSIGNEE}" \ + | jq -r '[.[] | select(.displayName | test("'"${JIRA_AGENT_ASSIGNEE}"'"; "i"))] | .[0].accountId // empty') + if [ -n "$ASSIGNEE_ACCOUNT_ID" ]; then + echo "Setting assignee to account ID '${ASSIGNEE_ACCOUNT_ID}'..." + ASSIGNEE_RESPONSE=$(set_assignee "$ISSUE_KEY" "$ASSIGNEE_ACCOUNT_ID") + else + echo " Warning: Could not find accountId for '${JIRA_AGENT_ASSIGNEE}', skipping assignee" + ASSIGNEE_RESPONSE="skipped +200" + fi + HTTP_CODE=$(echo "$ASSIGNEE_RESPONSE" | tail -1) + if [ "$HTTP_CODE" = "204" ] || [ "$HTTP_CODE" = "200" ]; then + echo " Assignee set successfully" + else + echo " Warning: Failed to set assignee (HTTP $HTTP_CODE)" + fi + fi + fi + + PROCESSED_COUNT=$((PROCESSED_COUNT + 1)) + echo "$ISSUE_KEY $TIMESTAMP $PR_URL SUCCESS" >> "$STATE_FILE" + else + # Log failure but don't mark as processed (will be retried next run) + echo "Failed to process $ISSUE_KEY" + echo "Error output (last 20 lines):" + tail -20 "/tmp/claude-${ISSUE_KEY}-output.log" + FAILED_COUNT=$((FAILED_COUNT + 1)) + echo "$ISSUE_KEY $TIMESTAMP - FAILED" >> "$STATE_FILE" + fi + + # Increment total counter + TOTAL_PROCESSED_OR_FAILED=$((TOTAL_PROCESSED_OR_FAILED + 1)) + + # Rate limiting between issues (60 seconds) + # Skip sleep if we've reached the limit + if [ $TOTAL_PROCESSED_OR_FAILED -lt "$MAX_ISSUES" ]; then + echo "Waiting 60 seconds before next issue..." + sleep 60 + fi + +done <<< "$ISSUES" + +echo "" +echo "=== Processing Summary ===" +echo "Processed: $PROCESSED_COUNT" +echo "Failed: $FAILED_COUNT" +echo "==========================" diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json new file mode 100644 index 0000000000000..a00eb6697ec06 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json @@ -0,0 +1,8 @@ +{ + "kind": "reference", + "api_version": "v1", + "metadata": { + "name": "jira-agent-process", + "description": "Generic process step for the Jira agent periodic job. Runs four-phase pipeline (solve, review, fix, PR) for Jira issues." + } +} diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.yaml b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.yaml new file mode 100644 index 0000000000000..f8286dc731a8a --- /dev/null +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.yaml @@ -0,0 +1,120 @@ +ref: + as: jira-agent-process + from: claude-ai-helpers + commands: jira-agent-process-commands.sh + timeout: 14400s + env: + - name: CLAUDE_CODE_USE_VERTEX + default: "1" + documentation: |- + Enable Vertex AI for Claude Code. + - name: CLOUD_ML_REGION + default: "global" + documentation: |- + Google Cloud region for Vertex AI. + - name: ANTHROPIC_VERTEX_PROJECT_ID + default: "itpc-gcp-hybrid-pe-eng-claude" + documentation: |- + Google Cloud project ID for Vertex AI authentication. + - name: GOOGLE_APPLICATION_CREDENTIALS + default: "/var/run/claude-code-service-account/claude-prow" + documentation: |- + Path to the Google Cloud service account JSON key file for Vertex AI authentication. + - name: JIRA_AGENT_FORK_REPO + default: "" + documentation: |- + Required. Fork repository in org/repo format (e.g., "my-team/my-repo"). + The agent clones this repo and pushes branches to it. + - name: JIRA_AGENT_UPSTREAM_REPO + default: "" + documentation: |- + Required. Upstream repository in org/repo format (e.g., "openshift/my-repo"). + The agent creates pull requests against this repo. + - name: JIRA_AGENT_JQL + default: "" + documentation: |- + Required. JQL query for finding Jira issues to process. + Example: 'project = MYPROJ AND resolution = Unresolved AND status in (New, "To Do") AND labels = issue-for-agent AND labels != agent-processed' + - name: JIRA_AGENT_TARGET_STATUS + default: "" + documentation: |- + Optional JSON map of Jira project prefix to target status after processing. + Example: '{"OCPBUGS":"ASSIGNED","CNTRLPLANE":"Code Review"}' + Leave empty to skip status transitions. + - name: JIRA_AGENT_ASSIGNEE + default: "" + documentation: |- + Optional display name to search for when setting assignee on processed issues. + Example: "my-automation-user" + Leave empty to skip assignee updates. + - name: JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY + default: "o-h-installation-id" + documentation: |- + Key name in the Vault secret for the upstream GitHub App installation ID. + - name: JIRA_AGENT_FORK_INSTALLATION_ID_KEY + default: "installation-id" + documentation: |- + Key name in the Vault secret for the fork GitHub App installation ID. + - name: JIRA_AGENT_EXTRA_PLUGIN_COMMANDS + default: "" + documentation: |- + Optional newline-separated Claude plugin install commands to run after + the base ai-helpers marketplace is added. Example: + claude plugin install utils@ai-helpers + claude plugin install golang@ai-helpers + - name: JIRA_AGENT_TOOL_SETUP_SCRIPT + default: "" + documentation: |- + Optional inline shell commands to install project-specific tools. + Example: "GOFLAGS='' go install golang.org/x/tools/gopls@v0.21.0" + - name: JIRA_AGENT_REVIEW_LANGUAGE + default: "go" + documentation: |- + Language for the code-review plugin (Phase 2). + - name: JIRA_AGENT_REVIEW_PROFILE + default: "" + documentation: |- + Optional profile name for the code-review plugin (Phase 2). + - name: JIRA_AGENT_SLACK_EMOJI + default: ":robot:" + documentation: |- + Slack emoji used in notification messages. + - name: JIRA_AGENT_ISSUE_KEY + default: "" + documentation: |- + Optional override to process a specific Jira issue instead of querying. + When set (e.g., "MYPROJ-123"), skips the JQL query and processes only this issue. + - name: MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY + default: "" + documentation: |- + Gangway API override for JIRA_AGENT_ISSUE_KEY. + - name: JIRA_AGENT_MAX_ISSUES + default: "1" + documentation: |- + Maximum number of Jira issues to process per run. + - name: CLAUDE_MODEL + default: "claude-opus-4-6" + documentation: |- + Claude model to use for processing Jira issues. + - name: JIRA_BASE_URL + default: "https://redhat.atlassian.net" + documentation: |- + Base URL for the Jira instance. + resources: + requests: + cpu: 500m + memory: 1Gi + credentials: + - namespace: test-credentials + name: hypershift-team-claude-prow + mount_path: /var/run/claude-code-service-account + documentation: |- + Generic process step for the Jira agent periodic job. + This step runs a four-phase pipeline for each issue: + Phase 1 - Solve: Runs /jira-solve to implement changes, commit, and push the branch + Phase 2 - Review: Runs /code-review:pre-commit-review for code quality (read-only) + Phase 3 - Fix: Addresses review findings by editing code, committing, and pushing fixes + Phase 4 - PR Creation: Creates a draft PR via gh CLI after review is complete + + Required env vars: JIRA_AGENT_FORK_REPO, JIRA_AGENT_UPSTREAM_REPO, JIRA_AGENT_JQL + Teams should override the credential secret name in their wrapper workflow. diff --git a/ci-operator/step-registry/jira-agent/report/OWNERS b/ci-operator/step-registry/jira-agent/report/OWNERS new file mode 100644 index 0000000000000..ff943340794d2 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/report/OWNERS @@ -0,0 +1,12 @@ +approvers: + - bryan-cox + - csrwng + - celebdor + - enxebre + - sjenning +reviewers: + - bryan-cox + - csrwng + - celebdor + - enxebre + - sjenning diff --git a/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh b/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh new file mode 100644 index 0000000000000..9b9923a6c1b29 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh @@ -0,0 +1,362 @@ +#!/bin/bash +set -euo pipefail + +echo "=== Jira Agent Report Generation ===" + +STATE_FILE="${SHARED_DIR}/processed-issues.txt" +REPORT_FILE="${ARTIFACT_DIR}/jira-agent-report.html" + +if [ ! -f "$STATE_FILE" ]; then + echo "No processed issues state file found. Nothing to report." + exit 0 +fi + +# Count issues by status +TOTAL=$(wc -l < "$STATE_FILE" | tr -d ' ') +SUCCESS_COUNT=$(grep -c 'SUCCESS$' "$STATE_FILE" 2>/dev/null || true) +FAILED_COUNT=$(grep -c 'FAILED$' "$STATE_FILE" 2>/dev/null || true) +: "${SUCCESS_COUNT:=0}" +: "${FAILED_COUNT:=0}" +RUN_TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") + +echo "Generating report for $TOTAL issues ($SUCCESS_COUNT succeeded, $FAILED_COUNT failed)" + +# Read a pre-extracted text file, or return a placeholder +read_extracted() { + local file=$1 + if [ -f "$file" ] && [ -s "$file" ]; then + cat "$file" + else + echo "(no output captured)" + fi +} + +# Read a JSON token file and extract a field, defaulting to 0 +read_token_field() { + local file=$1 + local field=$2 + if [ -f "$file" ] && [ -s "$file" ]; then + jq -r ".${field} // 0" "$file" 2>/dev/null || echo "0" + else + echo "0" + fi +} + +# Format token count with comma separators (GNU sed compatible) +format_number() { + local num=$1 + printf "%s" "$num" | sed -e ':a' -e 's/\([0-9]\)\([0-9]\{3\}\)\(\b\)/\1,\2\3/' -e 'ta' +} + +# Format a cost value as "$X.XXXX" +format_cost() { + local cost_usd=${1:-0} + printf '$%.4f' "$cost_usd" +} + +# Sum two floating-point cost values +sum_costs() { + local a=${1:-0} + local b=${2:-0} + awk "BEGIN {printf \"%.6f\", $a + $b}" 2>/dev/null || echo "0" +} + +# HTML-escape a string +html_escape() { + sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g' +} + +# Read a duration file and return the value in seconds, or 0 if missing +read_duration() { + local file=$1 + if [ -f "$file" ] && [ -s "$file" ]; then + cat "$file" | tr -d '[:space:]' + else + echo "0" + fi +} + +# Format seconds into a human-readable string (e.g. "40m 36s") +format_duration() { + local secs=$1 + if [ "$secs" -eq 0 ]; then + echo "-" + return + fi + local hours=$((secs / 3600)) + local mins=$(( (secs % 3600) / 60 )) + local s=$((secs % 60)) + if [ "$hours" -gt 0 ]; then + printf "%dh %dm %ds" "$hours" "$mins" "$s" + elif [ "$mins" -gt 0 ]; then + printf "%dm %ds" "$mins" "$s" + else + printf "%ds" "$s" + fi +} + +JIRA_BASE_URL="${JIRA_BASE_URL:-https://redhat.atlassian.net}" + +# Build issue rows for summary table and detail sections +SUMMARY_ROWS="" +DETAIL_SECTIONS="" +GRAND_TOTAL_INPUT=0 +GRAND_TOTAL_OUTPUT=0 +GRAND_TOTAL_CACHE_READ=0 +GRAND_TOTAL_CACHE_CREATE=0 +GRAND_TOTAL_COST_USD="0" + +while IFS= read -r line; do + ISSUE_KEY=$(echo "$line" | awk '{print $1}') + ISSUE_TIMESTAMP=$(echo "$line" | awk '{print $2}') + PR_URL=$(echo "$line" | awk '{print $3}') + STATUS=$(echo "$line" | awk '{print $4}') + + # Debug: verify token files exist and jq is available + echo "Processing issue $ISSUE_KEY (status=$STATUS)" + echo " Token files check:" + for phase in solve review fix pr; do + tf="${SHARED_DIR}/claude-${ISSUE_KEY}-${phase}-tokens.json" + if [ -f "$tf" ]; then + echo " ${phase}: $(cat "$tf" | tr -d '\n' | cut -c1-120)" + else + echo " ${phase}: FILE NOT FOUND" + fi + done + + if [ "$STATUS" = "SUCCESS" ]; then + STATUS_CLASS="success" + STATUS_LABEL="Success" + else + STATUS_CLASS="failed" + STATUS_LABEL="Failed" + fi + + # PR link or dash + if [ -n "$PR_URL" ] && [ "$PR_URL" != "-" ]; then + PR_LINK="<a href=\"${PR_URL}\">${PR_URL}</a>" + else + PR_LINK="-" + fi + + # Read pre-extracted phase outputs + SOLVE_TEXT=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-output-text.txt" | html_escape) + REVIEW_TEXT=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt" | html_escape) + FIX_TEXT=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-text.txt" | html_escape) + PR_TEXT=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-text.txt" | html_escape) + + SOLVE_TOOLS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-output-tools.txt" | html_escape) + REVIEW_TOOLS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tools.txt" | html_escape) + FIX_TOOLS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tools.txt" | html_escape) + PR_TOOLS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tools.txt" | html_escape) + + SOLVE_ERRORS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-output-errors.txt" | html_escape) + REVIEW_ERRORS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-review-errors.txt" | html_escape) + FIX_ERRORS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-errors.txt" | html_escape) + PR_ERRORS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-errors.txt" | html_escape) + + # Read token usage per phase + ISSUE_TOTAL_INPUT=0 + ISSUE_TOTAL_OUTPUT=0 + ISSUE_TOTAL_CACHE_READ=0 + ISSUE_TOTAL_CACHE_CREATE=0 + ISSUE_TOTAL_COST_USD="0" + TOKEN_ROWS="" + MODEL="unknown" + + ISSUE_TOTAL_DURATION=0 + + for phase_info in "solve:Phase 1: Solve" "review:Phase 2: Review" "fix:Phase 3: Fix" "pr:Phase 4: PR"; do + PHASE_KEY="${phase_info%%:*}" + PHASE_LABEL="${phase_info#*:}" + TOKEN_FILE="${SHARED_DIR}/claude-${ISSUE_KEY}-${PHASE_KEY}-tokens.json" + DURATION_FILE="${SHARED_DIR}/claude-${ISSUE_KEY}-${PHASE_KEY}-duration.txt" + + P_INPUT=$(read_token_field "$TOKEN_FILE" "input_tokens") + P_OUTPUT=$(read_token_field "$TOKEN_FILE" "output_tokens") + P_CACHE_READ=$(read_token_field "$TOKEN_FILE" "cache_read_input_tokens") + P_CACHE_CREATE=$(read_token_field "$TOKEN_FILE" "cache_creation_input_tokens") + P_MODEL=$(read_token_field "$TOKEN_FILE" "model") + P_DURATION=$(read_duration "$DURATION_FILE") + P_COST_RAW=$(read_token_field "$TOKEN_FILE" "total_cost_usd") + if [ "$P_MODEL" != "0" ] && [ "$P_MODEL" != "unknown" ]; then + MODEL="$P_MODEL" + fi + + P_COST=$(format_cost "$P_COST_RAW") + + ISSUE_TOTAL_INPUT=$((ISSUE_TOTAL_INPUT + P_INPUT)) + ISSUE_TOTAL_OUTPUT=$((ISSUE_TOTAL_OUTPUT + P_OUTPUT)) + ISSUE_TOTAL_CACHE_READ=$((ISSUE_TOTAL_CACHE_READ + P_CACHE_READ)) + ISSUE_TOTAL_CACHE_CREATE=$((ISSUE_TOTAL_CACHE_CREATE + P_CACHE_CREATE)) + ISSUE_TOTAL_COST_USD=$(sum_costs "$ISSUE_TOTAL_COST_USD" "$P_COST_RAW") + ISSUE_TOTAL_DURATION=$((ISSUE_TOTAL_DURATION + P_DURATION)) + + if [ "$P_INPUT" -gt 0 ] || [ "$P_OUTPUT" -gt 0 ]; then + TOKEN_ROWS="${TOKEN_ROWS}<tr><td>${PHASE_LABEL}</td><td>$(format_duration "$P_DURATION")</td><td>$(format_number "$P_INPUT")</td><td>$(format_number "$P_OUTPUT")</td><td>$(format_number "$P_CACHE_READ")</td><td>$(format_number "$P_CACHE_CREATE")</td><td>${P_COST}</td></tr>" + fi + done + + ISSUE_COST=$(format_cost "$ISSUE_TOTAL_COST_USD") + + # Build per-model breakdown rows from aggregated model_usage across phases + MODEL_BREAKDOWN_ROWS="" + MODEL_FILES="" + for phase_key in solve review fix pr; do + tf="${SHARED_DIR}/claude-${ISSUE_KEY}-${phase_key}-tokens.json" + if [ -f "$tf" ]; then + MODEL_FILES="$MODEL_FILES $tf" + fi + done + if [ -n "$MODEL_FILES" ]; then + MODEL_BREAKDOWN=$(jq -s ' + [.[].model_usage // {} | to_entries[]] + | group_by(.key) + | map({ + model: .[0].key, + input: (map(.value.inputTokens // .value.input_tokens // 0) | add), + output: (map(.value.outputTokens // .value.output_tokens // 0) | add), + cache_read: (map(.value.cacheReadInputTokens // .value.cache_read_input_tokens // 0) | add), + cache_create: (map(.value.cacheCreationInputTokens // .value.cache_creation_input_tokens // 0) | add) + }) + | sort_by(.model) + | .[] + | "\(.model)|\(.input)|\(.output)|\(.cache_read)|\(.cache_create)" + ' $MODEL_FILES 2>/dev/null || echo "") + if [ -n "$MODEL_BREAKDOWN" ]; then + MODEL_BREAKDOWN_ROWS="<tr><td colspan=\"7\" style=\"background:#f0f0f0; font-size:0.85em; color:#666; padding:0.3em 1em;\"><em>Per-model breakdown</em></td></tr>" + while IFS='|' read -r M_NAME M_INPUT M_OUTPUT M_CACHE_READ M_CACHE_CREATE; do + if [ -n "$M_NAME" ]; then + M_SHORT=$(echo "$M_NAME" | sed 's/-[0-9]*$//') + MODEL_BREAKDOWN_ROWS="${MODEL_BREAKDOWN_ROWS}<tr style=\"font-size:0.85em; color:#666;\"><td>  ${M_SHORT}</td><td>-</td><td>$(format_number "$M_INPUT")</td><td>$(format_number "$M_OUTPUT")</td><td>$(format_number "$M_CACHE_READ")</td><td>$(format_number "$M_CACHE_CREATE")</td><td>-</td></tr>" + fi + done <<< "$MODEL_BREAKDOWN" + fi + fi + + # Accumulate grand totals + GRAND_TOTAL_INPUT=$((GRAND_TOTAL_INPUT + ISSUE_TOTAL_INPUT)) + GRAND_TOTAL_OUTPUT=$((GRAND_TOTAL_OUTPUT + ISSUE_TOTAL_OUTPUT)) + GRAND_TOTAL_CACHE_READ=$((GRAND_TOTAL_CACHE_READ + ISSUE_TOTAL_CACHE_READ)) + GRAND_TOTAL_CACHE_CREATE=$((GRAND_TOTAL_CACHE_CREATE + ISSUE_TOTAL_CACHE_CREATE)) + GRAND_TOTAL_COST_USD=$(sum_costs "$GRAND_TOTAL_COST_USD" "$ISSUE_TOTAL_COST_USD") + + # Token usage table for this issue + TOKEN_TABLE="" + if [ -n "$TOKEN_ROWS" ]; then + TOKEN_TABLE=" + <h3>Token Usage & Cost</h3> + <table class=\"token-table\"> + <thead><tr><th>Phase</th><th>Duration</th><th>Input Tokens</th><th>Output Tokens</th><th>Cache Read</th><th>Cache Create</th><th>Cost</th></tr></thead> + <tbody> + ${TOKEN_ROWS} + <tr class=\"total-row\"><td><strong>Total</strong></td><td><strong>$(format_duration "$ISSUE_TOTAL_DURATION")</strong></td><td><strong>$(format_number "$ISSUE_TOTAL_INPUT")</strong></td><td><strong>$(format_number "$ISSUE_TOTAL_OUTPUT")</strong></td><td><strong>$(format_number "$ISSUE_TOTAL_CACHE_READ")</strong></td><td><strong>$(format_number "$ISSUE_TOTAL_CACHE_CREATE")</strong></td><td><strong>${ISSUE_COST}</strong></td></tr> + ${MODEL_BREAKDOWN_ROWS} + </tbody> + </table> + <p class=\"model-info\">Model: ${MODEL}</p>" + fi + + # Summary table row + SUMMARY_ROWS="${SUMMARY_ROWS}<tr><td><a href=\"${JIRA_BASE_URL}/browse/${ISSUE_KEY}\">${ISSUE_KEY}</a></td><td>${ISSUE_TIMESTAMP}</td><td><span class=\"status ${STATUS_CLASS}\">${STATUS_LABEL}</span></td><td>${PR_LINK}</td><td>${ISSUE_COST}</td></tr>" + + DETAIL_SECTIONS="${DETAIL_SECTIONS} +<div class=\"issue-card\"> + <h2><a href=\"${JIRA_BASE_URL}/browse/${ISSUE_KEY}\">${ISSUE_KEY}</a> <span class=\"status ${STATUS_CLASS}\">${STATUS_LABEL}</span></h2> + ${TOKEN_TABLE} + + <h3>Phase 1: Solve</h3> + <div class=\"phase-output\"><pre>${SOLVE_TEXT}</pre></div> + <details><summary>Tool calls</summary><pre>${SOLVE_TOOLS}</pre></details> + <details><summary>Tool errors</summary><pre class=\"error-pre\">${SOLVE_ERRORS}</pre></details> + + <h3>Phase 2: Pre-commit Review</h3> + <div class=\"phase-output\"><pre>${REVIEW_TEXT}</pre></div> + <details><summary>Tool calls</summary><pre>${REVIEW_TOOLS}</pre></details> + <details><summary>Tool errors</summary><pre class=\"error-pre\">${REVIEW_ERRORS}</pre></details> + + <h3>Phase 3: Review Fixes</h3> + <div class=\"phase-output\"><pre>${FIX_TEXT}</pre></div> + <details><summary>Tool calls</summary><pre>${FIX_TOOLS}</pre></details> + <details><summary>Tool errors</summary><pre class=\"error-pre\">${FIX_ERRORS}</pre></details> + + <h3>Phase 4: PR Creation</h3> + <div class=\"phase-output\"><pre>${PR_TEXT}</pre></div> + <details><summary>Tool calls</summary><pre>${PR_TOOLS}</pre></details> + <details><summary>Tool errors</summary><pre class=\"error-pre\">${PR_ERRORS}</pre></details> +</div>" + +done < "$STATE_FILE" + +# Format grand total cost +GRAND_TOTAL_COST=$(format_cost "$GRAND_TOTAL_COST_USD") + +# Write the HTML report +cat > "$REPORT_FILE" <<EOF +<!DOCTYPE html> +<html lang="en"> +<head> +<meta charset="UTF-8"> +<title>Jira Agent Report + + + +

Jira Agent Report

+

Generated: ${RUN_TIMESTAMP}

+ +
+
${TOTAL}
Total
+
${SUCCESS_COUNT}
Succeeded
+
${FAILED_COUNT}
Failed
+
$(format_number "$GRAND_TOTAL_INPUT")
Input Tokens
+
$(format_number "$GRAND_TOTAL_OUTPUT")
Output Tokens
+
${GRAND_TOTAL_COST}
Cost
+
+ +

Summary

+ + + +${SUMMARY_ROWS} + +
IssueTimestampStatusPull RequestCost
+ +

Details

+${DETAIL_SECTIONS} + + + +EOF + +echo "Report written to ${REPORT_FILE}" + +echo "=== Report generation complete ===" diff --git a/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json b/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json new file mode 100644 index 0000000000000..6811805fc5fc4 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json @@ -0,0 +1,8 @@ +{ + "kind": "reference", + "api_version": "v1", + "metadata": { + "name": "jira-agent-report", + "description": "Generates an HTML report from jira-agent processing output with token usage and cost breakdown." + } +} diff --git a/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.yaml b/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.yaml new file mode 100644 index 0000000000000..e8accc95ee06a --- /dev/null +++ b/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.yaml @@ -0,0 +1,17 @@ +ref: + as: jira-agent-report + from: claude-ai-helpers + commands: jira-agent-report-commands.sh + env: + - name: JIRA_BASE_URL + default: "https://redhat.atlassian.net" + documentation: |- + Base URL for the Jira instance. Used for linking to issues in the report. + resources: + requests: + cpu: 100m + memory: 256Mi + documentation: |- + Generates an HTML report from the jira-agent processing output. + Parses stream-json output from all phases (solve, review, fix, PR) + and produces a readable report in ${ARTIFACT_DIR}. diff --git a/ci-operator/step-registry/jira-agent/setup/OWNERS b/ci-operator/step-registry/jira-agent/setup/OWNERS new file mode 100644 index 0000000000000..ff943340794d2 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/setup/OWNERS @@ -0,0 +1,12 @@ +approvers: + - bryan-cox + - csrwng + - celebdor + - enxebre + - sjenning +reviewers: + - bryan-cox + - csrwng + - celebdor + - enxebre + - sjenning diff --git a/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh new file mode 100644 index 0000000000000..96208b06b3a4f --- /dev/null +++ b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh @@ -0,0 +1,10 @@ +#!/bin/bash +set -euo pipefail + +echo "=== Jira Agent Setup ===" + +# Verify Claude Code is available (Vertex AI authentication is handled via GOOGLE_APPLICATION_CREDENTIALS env var) +echo "Verifying Claude Code CLI..." +claude --version || { echo "ERROR: Claude Code CLI not found"; exit 1; } + +echo "Setup complete" diff --git a/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json new file mode 100644 index 0000000000000..eeb2b1995faf3 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json @@ -0,0 +1,8 @@ +{ + "kind": "reference", + "api_version": "v1", + "metadata": { + "name": "jira-agent-setup", + "description": "Generic setup step for the Jira agent periodic job. Verifies Claude Code CLI is available." + } +} diff --git a/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.yaml b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.yaml new file mode 100644 index 0000000000000..60c5d35fe39de --- /dev/null +++ b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.yaml @@ -0,0 +1,34 @@ +ref: + as: jira-agent-setup + from: claude-ai-helpers + commands: jira-agent-setup-commands.sh + env: + - name: CLAUDE_CODE_USE_VERTEX + default: "1" + documentation: |- + Enable Vertex AI for Claude Code. + - name: CLOUD_ML_REGION + default: "global" + documentation: |- + Google Cloud region for Vertex AI. + - name: ANTHROPIC_VERTEX_PROJECT_ID + default: "itpc-gcp-hybrid-pe-eng-claude" + documentation: |- + Google Cloud project ID for Vertex AI authentication. + - name: GOOGLE_APPLICATION_CREDENTIALS + default: "/var/run/claude-code-service-account/claude-prow" + documentation: |- + Path to the Google Cloud service account JSON key file for Vertex AI authentication. + resources: + requests: + cpu: 100m + memory: 200Mi + credentials: + - namespace: test-credentials + name: hypershift-team-claude-prow + mount_path: /var/run/claude-code-service-account + documentation: |- + Generic setup step for the Jira agent periodic job. + Verifies Claude Code CLI is available. + Uses Vertex AI for Claude authentication via GCP service account. + Teams should override the credential secret name in their wrapper workflow. From 92b06d869063d1689e77a94dbd735a5eb8e095b8 Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Tue, 23 Jun 2026 10:26:18 -0400 Subject: [PATCH 2/7] fix: Move env inside steps block, regenerate metadata, remove unused vars - Move env block inside workflow steps (workflows don't support top-level env) - Regenerate metadata JSON files with make registry-metadata - Remove unused UPSTREAM_ORG and UPSTREAM_REPO_NAME variables (shellcheck SC2034) Co-Authored-By: Claude Opus 4.6 --- .../hypershift-jira-agent-workflow.yaml | 34 +++++++++---------- .../jira-agent-workflow.metadata.json | 25 ++++++++++---- .../process/jira-agent-process-commands.sh | 4 +-- .../jira-agent-process-ref.metadata.json | 25 ++++++++++---- .../jira-agent-report-ref.metadata.json | 25 ++++++++++---- .../setup/jira-agent-setup-ref.metadata.json | 25 ++++++++++---- 6 files changed, 90 insertions(+), 48 deletions(-) diff --git a/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml b/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml index aba0f9ce91d0c..cad7046952060 100644 --- a/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml +++ b/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml @@ -7,23 +7,23 @@ workflow: - ref: jira-agent-process post: - ref: jira-agent-report - env: - JIRA_AGENT_FORK_REPO: "hypershift-community/hypershift" - JIRA_AGENT_UPSTREAM_REPO: "openshift/hypershift" - JIRA_AGENT_JQL: 'project in (OCPBUGS, CNTRLPLANE) AND resolution = Unresolved AND status in (New, "To Do") AND labels = issue-for-agent AND labels != agent-processed' - JIRA_AGENT_TARGET_STATUS: '{"OCPBUGS":"ASSIGNED","CNTRLPLANE":"Code Review"}' - JIRA_AGENT_ASSIGNEE: "hypershift-automation" - JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY: "o-h-installation-id" - JIRA_AGENT_FORK_INSTALLATION_ID_KEY: "installation-id" - JIRA_AGENT_EXTRA_PLUGIN_COMMANDS: | - claude plugin install utils@ai-helpers - claude plugin install golang@ai-helpers - claude plugin marketplace add enxebre/ai-scripts - claude plugin install git@enxebre - JIRA_AGENT_TOOL_SETUP_SCRIPT: "GOFLAGS='' go install golang.org/x/tools/gopls@v0.21.0 && python3.9 -m ensurepip --user 2>/dev/null || true && python3.9 -m pip install --user pre-commit 2>&1 | tail -1" - JIRA_AGENT_REVIEW_LANGUAGE: "go" - JIRA_AGENT_REVIEW_PROFILE: "hypershift" - JIRA_AGENT_SLACK_EMOJI: ":hypershift-bot:" + env: + JIRA_AGENT_FORK_REPO: "hypershift-community/hypershift" + JIRA_AGENT_UPSTREAM_REPO: "openshift/hypershift" + JIRA_AGENT_JQL: 'project in (OCPBUGS, CNTRLPLANE) AND resolution = Unresolved AND status in (New, "To Do") AND labels = issue-for-agent AND labels != agent-processed' + JIRA_AGENT_TARGET_STATUS: '{"OCPBUGS":"ASSIGNED","CNTRLPLANE":"Code Review"}' + JIRA_AGENT_ASSIGNEE: "hypershift-automation" + JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY: "o-h-installation-id" + JIRA_AGENT_FORK_INSTALLATION_ID_KEY: "installation-id" + JIRA_AGENT_EXTRA_PLUGIN_COMMANDS: | + claude plugin install utils@ai-helpers + claude plugin install golang@ai-helpers + claude plugin marketplace add enxebre/ai-scripts + claude plugin install git@enxebre + JIRA_AGENT_TOOL_SETUP_SCRIPT: "GOFLAGS='' go install golang.org/x/tools/gopls@v0.21.0 && python3.9 -m ensurepip --user 2>/dev/null || true && python3.9 -m pip install --user pre-commit 2>&1 | tail -1" + JIRA_AGENT_REVIEW_LANGUAGE: "go" + JIRA_AGENT_REVIEW_PROFILE: "hypershift" + JIRA_AGENT_SLACK_EMOJI: ":hypershift-bot:" documentation: |- HyperShift-specific wrapper for the generic Jira Agent workflow. diff --git a/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json b/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json index 00ebde1f41384..9c077f1c492e8 100644 --- a/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json +++ b/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json @@ -1,8 +1,19 @@ { - "kind": "workflow", - "api_version": "v1", - "metadata": { - "name": "jira-agent", - "description": "Generic Jira Agent workflow for automated issue processing using Claude Code." - } -} + "path": "jira-agent/jira-agent-workflow.yaml", + "owners": { + "approvers": [ + "bryan-cox", + "csrwng", + "celebdor", + "enxebre", + "sjenning" + ], + "reviewers": [ + "bryan-cox", + "csrwng", + "celebdor", + "enxebre", + "sjenning" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh index efbb67e9a8ac1..7259693c27e80 100644 --- a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh @@ -17,10 +17,8 @@ if [[ -n "${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY:-}" ]]; then export JIRA_AGENT_ISSUE_KEY="${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" fi -# Derive org names from repo slugs +# Derive org name from fork repo slug FORK_ORG="${JIRA_AGENT_FORK_REPO%%/*}" -UPSTREAM_ORG="${JIRA_AGENT_UPSTREAM_REPO%%/*}" -UPSTREAM_REPO_NAME="${JIRA_AGENT_UPSTREAM_REPO##*/}" # Configurable defaults UPSTREAM_INSTALL_ID_KEY="${JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY:-o-h-installation-id}" diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json index a00eb6697ec06..1b0e52b803db0 100644 --- a/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json @@ -1,8 +1,19 @@ { - "kind": "reference", - "api_version": "v1", - "metadata": { - "name": "jira-agent-process", - "description": "Generic process step for the Jira agent periodic job. Runs four-phase pipeline (solve, review, fix, PR) for Jira issues." - } -} + "path": "jira-agent/process/jira-agent-process-ref.yaml", + "owners": { + "approvers": [ + "bryan-cox", + "csrwng", + "celebdor", + "enxebre", + "sjenning" + ], + "reviewers": [ + "bryan-cox", + "csrwng", + "celebdor", + "enxebre", + "sjenning" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json b/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json index 6811805fc5fc4..e2b373761e895 100644 --- a/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json +++ b/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json @@ -1,8 +1,19 @@ { - "kind": "reference", - "api_version": "v1", - "metadata": { - "name": "jira-agent-report", - "description": "Generates an HTML report from jira-agent processing output with token usage and cost breakdown." - } -} + "path": "jira-agent/report/jira-agent-report-ref.yaml", + "owners": { + "approvers": [ + "bryan-cox", + "csrwng", + "celebdor", + "enxebre", + "sjenning" + ], + "reviewers": [ + "bryan-cox", + "csrwng", + "celebdor", + "enxebre", + "sjenning" + ] + } +} \ No newline at end of file diff --git a/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json index eeb2b1995faf3..7615067e833c7 100644 --- a/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json +++ b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json @@ -1,8 +1,19 @@ { - "kind": "reference", - "api_version": "v1", - "metadata": { - "name": "jira-agent-setup", - "description": "Generic setup step for the Jira agent periodic job. Verifies Claude Code CLI is available." - } -} + "path": "jira-agent/setup/jira-agent-setup-ref.yaml", + "owners": { + "approvers": [ + "bryan-cox", + "csrwng", + "celebdor", + "enxebre", + "sjenning" + ], + "reviewers": [ + "bryan-cox", + "csrwng", + "celebdor", + "enxebre", + "sjenning" + ] + } +} \ No newline at end of file From ed842121ff8e9c9726db6990a213dbc44554207e Mon Sep 17 00:00:00 2001 From: Bryan Cox Date: Tue, 23 Jun 2026 10:43:29 -0400 Subject: [PATCH 3/7] fix: Address code review findings for jira-agent step registry - Fix credential key names in README (jira-pat, jira-email, app-id, private-key, slack-webhook-url, gh-to-slack-ids) - Clarify that credential secret name requires forking ref YAMLs - Add git config commands to security warnings in all 4 Claude prompts - Add set +x tracing guards around all credential operations (GitHub token gen, Jira auth, token refreshes) - Change exit 0 to exit 1 on missing credentials with state file - Delete orphaned hypershift/jira-agent/setup|process|report directories - Fix unquoted $MODEL_FILES in report script (use bash array) - Improve token refresh error messages to note stale token fallback - Add trust boundary comment for eval'd env vars Co-Authored-By: Claude Opus 4.6 --- .../hypershift/jira-agent/README.md | 286 ------ .../hypershift/jira-agent/process/OWNERS | 12 - .../hypershift-jira-agent-process-commands.sh | 822 ------------------ ...shift-jira-agent-process-ref.metadata.json | 19 - .../hypershift-jira-agent-process-ref.yaml | 68 -- .../hypershift/jira-agent/report/OWNERS | 12 - .../hypershift-jira-agent-report-commands.sh | 360 -------- ...rshift-jira-agent-report-ref.metadata.json | 19 - .../hypershift-jira-agent-report-ref.yaml | 12 - .../hypershift/jira-agent/setup/OWNERS | 12 - .../hypershift-jira-agent-setup-commands.sh | 10 - ...ershift-jira-agent-setup-ref.metadata.json | 19 - .../hypershift-jira-agent-setup-ref.yaml | 37 - .../step-registry/jira-agent/README.md | 30 +- .../process/jira-agent-process-commands.sh | 37 +- .../report/jira-agent-report-commands.sh | 8 +- 16 files changed, 49 insertions(+), 1714 deletions(-) delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/README.md delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/process/OWNERS delete mode 100755 ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-commands.sh delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.metadata.json delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.yaml delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/report/OWNERS delete mode 100755 ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-commands.sh delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.metadata.json delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.yaml delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/setup/OWNERS delete mode 100755 ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-commands.sh delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.metadata.json delete mode 100644 ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml diff --git a/ci-operator/step-registry/hypershift/jira-agent/README.md b/ci-operator/step-registry/hypershift/jira-agent/README.md deleted file mode 100644 index 0630d7c03d8f3..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/README.md +++ /dev/null @@ -1,286 +0,0 @@ -# HyperShift Jira Agent Workflow - -Automated periodic job that processes Jira issues labeled with `issue-for-agent` and creates pull requests using Claude Code. - -## Overview - -This workflow implements a fully automated system for processing HyperShift Jira issues: - -1. **Query**: Searches Jira for unresolved issues in OCPBUGS and CNTRLPLANE projects with label `issue-for-agent` (excluding those with `agent-processed`) -2. **Process**: For each issue, runs the `/jira-solve` command from the HyperShift repository non-interactively -3. **Track**: Adds `agent-processed` label to successfully processed issues to prevent reprocessing - -## Data Flow Diagram - -```mermaid -flowchart TD - %% Trigger - Start([Cron Trigger
Daily 9:00 AM UTC]):::trigger --> PrePhase - - %% PRE-PHASE: Setup - subgraph PrePhase[PRE-PHASE: Setup] - direction TB - Verify[Verify Claude Code CLI
claude --version]:::setup - end - - %% TEST-PHASE: Process - PrePhase --> TestPhase - - subgraph TestPhase[TEST-PHASE: Process Issues] - direction TB - - CloneRepos[Clone Repositories
ai-helpers + hypershift-community/hypershift]:::setup - CopyCommand[Copy jira-solve command
to .claude/commands/]:::setup - GitConfig[Configure Git
user: OpenShift CI Bot]:::setup - GenTokens[Generate GitHub App Tokens
JWT auth for fork + upstream]:::setup - - QueryJira[Query Jira API
JQL: status in New, To Do
AND labels = issue-for-agent
AND labels != agent-processed]:::process - - CheckIssues{Issues
Found?}:::decision - CheckMax{Processed <
MAX_ISSUES
Default: 1}:::decision - CheckSuccess{Processing
Successful?}:::decision - - ProcessIssue[Run Claude Code CLI
--system-prompt jira-solve.md
--max-turns 100]:::ai - - AddLabel[Add label
agent-processed
to Jira issue]:::success - LogFailure[Log failure
Will retry next run]:::failure - NoIssues[Exit: No issues to process]:::skip - - RateLimit[Wait 60 seconds
Rate limiting]:::process - Summary[Print Summary
Processed/Failed counts]:::process - - CloneRepos --> CopyCommand --> GitConfig --> GenTokens --> QueryJira - QueryJira --> CheckIssues - CheckIssues -->|No| NoIssues - CheckIssues -->|Yes| CheckMax - CheckMax -->|No| Summary - CheckMax -->|Yes| ProcessIssue - ProcessIssue --> CheckSuccess - CheckSuccess -->|Yes| AddLabel - CheckSuccess -->|No| LogFailure - AddLabel --> RateLimit - LogFailure --> RateLimit - RateLimit --> CheckMax - end - - %% Secrets - Secret1[(Secret:
hypershift-team-claude-prow
app-id, private-key,
installation-ids)]:::secret -.->|GitHub App auth| GenTokens - Secret1 -.->|Vertex AI auth| ProcessIssue - - %% External Systems - JiraAPI[(Jira API
redhat.atlassian.net)]:::external -.->|Return issues| QueryJira - JiraAPI -.->|Add label| AddLabel - ClaudeAPI[(Claude API
via Vertex AI)]:::external -.->|Generate solution| ProcessIssue - GitHubAPI[(GitHub API)]:::external -.->|Push to fork| ProcessIssue - GitHubAPI -.->|Create PR to upstream| ProcessIssue - - TestPhase --> End([Workflow Complete]):::trigger - NoIssues --> End - Summary --> End - - %% Style Definitions - classDef trigger fill:#e1f5ff,stroke:#01579b,stroke-width:3px,color:#000 - classDef setup fill:#f3e5f5,stroke:#4a148c,stroke-width:2px,color:#000 - classDef process fill:#e8f5e9,stroke:#1b5e20,stroke-width:2px,color:#000 - classDef decision fill:#fff3e0,stroke:#e65100,stroke-width:2px,color:#000 - classDef ai fill:#fce4ec,stroke:#880e4f,stroke-width:3px,color:#000 - classDef success fill:#c8e6c9,stroke:#2e7d32,stroke-width:2px,color:#000 - classDef failure fill:#ffcdd2,stroke:#c62828,stroke-width:2px,color:#000 - classDef skip fill:#f5f5f5,stroke:#757575,stroke-width:1px,color:#000 - classDef external fill:#fff9c4,stroke:#f57f17,stroke-width:2px,color:#000 - classDef secret fill:#ffebee,stroke:#b71c1c,stroke-width:2px,color:#000 -``` - -## Components - -### Workflow -- **File**: `hypershift-jira-agent-workflow.yaml` -- **Description**: Defines the two-phase workflow (pre/test) - -### Steps - -#### 1. Setup (`hypershift-jira-agent-setup`) -- Verifies Claude Code CLI is available - -#### 2. Process (`hypershift-jira-agent-process`) -- Clones ai-helpers and hypershift-community/hypershift repositories -- Copies jira-solve command to `.claude/commands/` -- Configures git and generates GitHub App tokens (JWT auth) -- Queries Jira API for labeled issues (excluding those with `agent-processed`) -- Runs jira-solve for each issue using Claude Code CLI with `--system-prompt` -- Pushes branches to fork, creates PRs to upstream openshift/hypershift -- Implements rate limiting (60s between issues) -- Adds `agent-processed` label to successfully processed issues - -## Configuration - -### Secrets Required - -The workflow requires a single secret in the `test-credentials` namespace: - -**`hypershift-team-claude-prow`** -- Mount path: `/var/run/claude-code-service-account` -- Required keys: - - `claude-prow`: GCP service account JSON key for Vertex AI authentication - - `app-id`: GitHub App ID - - `private-key`: GitHub App private key for JWT signing - - `installation-id`: GitHub App installation ID for hypershift-community fork - - `o-h-installation-id`: GitHub App installation ID for openshift/hypershift upstream - -The workflow uses GitHub App authentication (JWT-based) rather than personal access tokens. This provides better security and allows fine-grained permissions. - -**Optional:** -- `hypershift-jira-token`: Jira API token for adding `agent-processed` labels -- `slack-webhook-url`: Slack incoming webhook URL for posting PR notifications to team-ocp-hypershift -- `gh-to-slack-ids`: JSON mapping of GitHub usernames to Slack member IDs, plus a `backup-user` key for fallback (e.g., `{"gh-username": "UXXXXXXXXXX", "backup-user": "UXXXXXXXXXX"}`) - -These should be configured in Vault with secretsync metadata and synced automatically. - -### Periodic Job - -Configured in `ci-operator/config/openshift/hypershift/openshift-hypershift-main.yaml`: - -```yaml -- as: periodic-jira-agent - cron: 0 9 * * * # Daily at 9:00 AM UTC - steps: - env: - JIRA_AGENT_MAX_ISSUES: "1" # Start with 1 for testing, increase later - workflow: hypershift-jira-agent -``` - -### Environment Variables - -- **`JIRA_AGENT_MAX_ISSUES`** (default: `1`) - - Maximum number of issues to process per run - - Set to `1` initially for safe testing - - Can be increased to `5`, `10`, or higher once validated - - Counts both successful and failed processing attempts - -### State Management - -State is tracked using Jira labels: -- **Label**: `agent-processed` -- When an issue is successfully processed, the `agent-processed` label is added -- The JQL query excludes issues with this label, preventing reprocessing -- Failed issues are NOT labeled, allowing automatic retry on subsequent runs - -To reprocess an issue: -1. Remove the `agent-processed` label from the Jira issue -2. The issue will be picked up on the next run - -## How It Works - -### Non-Interactive Execution - -The workflow uses Claude Code CLI's non-interactive mode with a system prompt: - -```bash -claude -p "$ISSUE_KEY origin --ci" \ - --system-prompt "$SKILL_CONTENT" \ - --allowedTools "Bash Read Write Edit Grep Glob WebFetch" \ - --max-turns 100 \ - --verbose \ - --output-format stream-json -``` - -The jira-solve command is loaded from `ai-helpers/plugins/jira/commands/solve.md` and passed as a system prompt. This allows Claude to analyze the Jira issue and create a PR automatically. - -### Jira Query - -Issues are queried using JQL: -``` -project in (OCPBUGS, CNTRLPLANE) AND resolution = Unresolved AND status in (New, "To Do") AND labels = issue-for-agent AND labels != agent-processed -``` - -Maximum issues queried and processed is controlled by `JIRA_AGENT_MAX_ISSUES` (default: 1). - -### Rate Limiting - -- 60 seconds between processing each issue -- Maximum 100 agentic turns per issue -- Maximum issues per run: configurable via `JIRA_AGENT_MAX_ISSUES` -- Runs once daily at 9:00 AM UTC - -## Container Image - -Uses the `claude-ai-helpers` image from OpenShift CI containing: -- Claude Code CLI -- GitHub CLI (gh) -- jq, git, curl -- Required dependencies - -## Local Testing - -Use the test script: - -```bash -export ANTHROPIC_API_KEY=your-key -export GITHUB_TOKEN=your-token -./tools/hypershift-jira-agent/test-locally.sh -``` - -## Monitoring - -### Success Indicators -- Issues processed successfully with PRs created -- `agent-processed` label added to processed issues -- No authentication errors - -### Failure Indicators -- Failed to authenticate with Claude API -- Failed to create PRs (GitHub auth issues) -- Individual issue processing failures - -### Logs -Check Prow job logs for: -- Jira query results -- Processing output for each issue -- PR URLs created -- Error messages - -## Maintenance - -### Adding/Removing Issues -Add or remove the `issue-for-agent` label in Jira to control which issues are processed. - -### Reprocessing an Issue -To reprocess an issue, remove the `agent-processed` label from the Jira issue: -1. Open the issue in Jira -2. Remove the `agent-processed` label -3. The issue will be picked up on the next scheduled run - -### Adjusting Frequency -Modify the `cron` schedule in the CI config file. Currently runs daily at 9:00 AM UTC. - -### Adjusting Issue Limit -Modify the `JIRA_AGENT_MAX_ISSUES` environment variable in the CI config file: -```yaml -env: - JIRA_AGENT_MAX_ISSUES: "5" # Increase from 1 to 5 -``` -Then run `make update` to regenerate job configs. - -## Troubleshooting - -### Issue: No issues being processed -- Check Jira query returns results -- Verify `issue-for-agent` label exists on issues -- Verify `agent-processed` label is NOT on issues (or remove it to reprocess) - -### Issue: Authentication failures -- Verify secrets are mounted correctly -- Check API keys are valid and not expired -- Ensure GitHub token has required permissions - -### Issue: PR creation fails -- Check GitHub token permissions -- Verify HyperShift repository access -- Review `/jira-solve` command output in logs - -## Future Enhancements - -- Metrics push to Prometheus -- Automatic retries for transient failures -- Priority-based processing -- Issue assignment tracking diff --git a/ci-operator/step-registry/hypershift/jira-agent/process/OWNERS b/ci-operator/step-registry/hypershift/jira-agent/process/OWNERS deleted file mode 100644 index e39269bf55090..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/process/OWNERS +++ /dev/null @@ -1,12 +0,0 @@ -approvers: -- bryan-cox -- csrwng -- celebdor -- enxebre -- sjenning -reviewers: -- bryan-cox -- csrwng -- celebdor -- enxebre -- sjenning diff --git a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-commands.sh b/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-commands.sh deleted file mode 100755 index fee7b43f3bec0..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-commands.sh +++ /dev/null @@ -1,822 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "=== HyperShift Jira Agent Process ===" - -# Apply Gangway API overrides (MULTISTAGE_PARAM_OVERRIDE_* prefix) -if [[ -n "${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY:-}" ]]; then - echo "Applying Gangway override: JIRA_AGENT_ISSUE_KEY=${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" - export JIRA_AGENT_ISSUE_KEY="${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" -fi - -# State file for sharing results with report step -STATE_FILE="${SHARED_DIR}/processed-issues.txt" - -# Clone ai-helpers repository (contains /jira-solve command) -echo "Cloning ai-helpers repository..." -git clone https://github.com/openshift-eng/ai-helpers /tmp/ai-helpers - -# Clone HyperShift fork (we push here and create PRs to upstream) -echo "Cloning HyperShift repository..." -git clone https://github.com/hypershift-community/hypershift /tmp/hypershift - -# Copy jira-solve command from ai-helpers to hypershift -echo "Setting up Claude commands..." -mkdir -p /tmp/hypershift/.claude/commands -cp /tmp/ai-helpers/plugins/jira/commands/solve.md /tmp/hypershift/.claude/commands/jira-solve.md - -# Check if code-review plugin is available for Phase 2 -REVIEW_PLUGIN_DIR="/tmp/ai-helpers/plugins/code-review" -if [ ! -d "${REVIEW_PLUGIN_DIR}/.claude-plugin" ]; then - echo "ERROR: code-review plugin not found at ${REVIEW_PLUGIN_DIR}/.claude-plugin" - exit 1 -fi -echo "Code-review plugin found" - -# Install tool dependencies -echo "Installing tool dependencies..." -GOFLAGS="" go install golang.org/x/tools/gopls@v0.21.0 -python3.9 -m ensurepip --user 2>/dev/null || true -python3.9 -m pip install --user pre-commit 2>&1 | tail -1 -export PATH="${GOPATH:-$HOME/go}/bin:$HOME/.local/bin:$PATH" - -# Install plugins -echo "Installing Claude Code plugins..." -claude plugin marketplace add openshift-eng/ai-helpers -claude plugin install utils@ai-helpers -claude plugin install golang@ai-helpers -claude plugin marketplace add enxebre/ai-scripts -claude plugin install git@enxebre - -cd /tmp/hypershift - -# Configure git -git config user.name "OpenShift CI Bot" -git config user.email "ci-bot@redhat.com" - -# Sync fork with upstream before doing any work -echo "Syncing fork with upstream openshift/hypershift..." -git remote add upstream https://github.com/openshift/hypershift.git -git fetch upstream main -git checkout main -git rebase upstream/main -echo "Fork synced with upstream successfully" - -# Generate GitHub App installation token -echo "Generating GitHub App token..." - -GITHUB_APP_CREDS_DIR="/var/run/claude-code-service-account" -APP_ID_FILE="${GITHUB_APP_CREDS_DIR}/app-id" -INSTALLATION_ID_FILE="${GITHUB_APP_CREDS_DIR}/installation-id" -PRIVATE_KEY_FILE="${GITHUB_APP_CREDS_DIR}/private-key" - -# Check if all required credentials exist -INSTALLATION_ID_UPSTREAM_FILE="${GITHUB_APP_CREDS_DIR}/o-h-installation-id" - -if [ ! -f "$APP_ID_FILE" ] || [ ! -f "$INSTALLATION_ID_FILE" ] || [ ! -f "$PRIVATE_KEY_FILE" ] || [ ! -f "$INSTALLATION_ID_UPSTREAM_FILE" ]; then - echo "GitHub App credentials not yet available in ${GITHUB_APP_CREDS_DIR}" - echo "Available files:" - ls -la "${GITHUB_APP_CREDS_DIR}/" || echo "Directory does not exist" - echo "" - echo "Waiting for Vault secretsync to complete. The following keys are required:" - echo " - app-id" - echo " - installation-id (for hypershift-community fork)" - echo " - o-h-installation-id (for openshift/hypershift upstream)" - echo " - private-key" - echo "" - echo "Exiting gracefully. Re-run once secrets are synced." - exit 0 -fi - -APP_ID=$(cat "$APP_ID_FILE") -INSTALLATION_ID_FORK=$(cat "$INSTALLATION_ID_FILE") -INSTALLATION_ID_UPSTREAM=$(cat "$INSTALLATION_ID_UPSTREAM_FILE") - -# Function to generate GitHub App token for a given installation ID -generate_github_token() { - local INSTALL_ID=$1 - local NOW - NOW=$(date +%s) - local IAT=$((NOW - 60)) - local EXP=$((NOW + 600)) - - local HEADER - HEADER=$(echo -n '{"alg":"RS256","typ":"JWT"}' | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') - local PAYLOAD - PAYLOAD=$(echo -n "{\"iat\":${IAT},\"exp\":${EXP},\"iss\":\"${APP_ID}\"}" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') - local SIGNATURE - SIGNATURE=$(echo -n "${HEADER}.${PAYLOAD}" | openssl dgst -sha256 -sign "$PRIVATE_KEY_FILE" | base64 | tr -d '=' | tr '/+' '_-' | tr -d '\n') - local JWT="${HEADER}.${PAYLOAD}.${SIGNATURE}" - - curl -s -X POST \ - -H "Authorization: Bearer ${JWT}" \ - -H "Accept: application/vnd.github+json" \ - "https://api.github.com/app/installations/${INSTALL_ID}/access_tokens" \ - | jq -r '.token' -} - -# Generate token for fork (hypershift-community/hypershift) - for pushing branches -echo "Generating GitHub App token for fork..." -GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") -if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then - echo "ERROR: Failed to generate GitHub App token for fork" - exit 1 -fi -echo "Fork token generated successfully" - -# Generate token for upstream (openshift/hypershift) - for creating PRs -echo "Generating GitHub App token for upstream..." -GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") -if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then - echo "ERROR: Failed to generate GitHub App token for upstream" - exit 1 -fi -echo "Upstream token generated successfully" - -# Configure git to use the fork token for push operations via credential helper -# Using credential helper instead of URL rewriting prevents token leaking in git remote output -git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" - -# Export upstream token as GITHUB_TOKEN for gh CLI (used for PR creation) -export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" -echo "GitHub App tokens configured successfully" - -# Configuration: maximum issues to process per run (default: 1) -MAX_ISSUES=${JIRA_AGENT_MAX_ISSUES:-1} -echo "Configuration: MAX_ISSUES=$MAX_ISSUES" - -# Shared prompt instruction for subagent behavior -SUBAGENT_PROMPT="SUBAGENTS: Launch ALL subagents in parallel (single message with multiple Task tool calls) for maximum speed. Each subagent should be given subagent_type: \"general-purpose\". Do NOT set the model parameter — let subagents inherit the parent model, as these analysis tasks require a capable model." - -# Load Jira API credentials for Atlassian Cloud (Basic Auth: email:api-token) -JIRA_TOKEN_FILE="/var/run/claude-code-service-account/jira-pat" -JIRA_EMAIL_FILE="/var/run/claude-code-service-account/jira-email" -if [ -f "$JIRA_TOKEN_FILE" ] && [ -f "$JIRA_EMAIL_FILE" ]; then - JIRA_TOKEN=$(cat "$JIRA_TOKEN_FILE") - JIRA_EMAIL=$(cat "$JIRA_EMAIL_FILE") - JIRA_AUTH=$(echo -n "${JIRA_EMAIL}:${JIRA_TOKEN}" | base64 | tr -d '\n') - echo "Jira API credentials loaded (email + token)" -else - echo "Warning: Jira credentials not found (need both jira-pat and jira-email)" - echo "Labels will not be added to processed issues" - JIRA_TOKEN="" - JIRA_AUTH="" -fi - -# Load Slack webhook URL for notifications (tracing disabled to protect credential) -SLACK_WEBHOOK_FILE="/var/run/claude-code-service-account/slack-webhook-url" -[[ $- == *x* ]] && _SLACK_WAS_TRACING=true || _SLACK_WAS_TRACING=false -set +x -if [ -f "$SLACK_WEBHOOK_FILE" ]; then - SLACK_WEBHOOK_URL=$(cat "$SLACK_WEBHOOK_FILE") - echo "Slack webhook URL loaded" -else - echo "Warning: Slack webhook URL not found at $SLACK_WEBHOOK_FILE" - echo "Slack notifications will be skipped" - SLACK_WEBHOOK_URL="" -fi -$_SLACK_WAS_TRACING && set -x - -# Load GitHub-to-Slack user ID mapping -GITHUB_SLACK_MAP_FILE="/var/run/claude-code-service-account/gh-to-slack-ids" -if [ -f "$GITHUB_SLACK_MAP_FILE" ]; then - if GITHUB_SLACK_MAP=$(jq -c . < "$GITHUB_SLACK_MAP_FILE" 2>/dev/null); then - echo "GitHub-to-Slack mapping loaded" - else - echo "Warning: GitHub-to-Slack mapping is invalid JSON" - echo "Reviewer pings will use GitHub usernames instead of Slack mentions" - GITHUB_SLACK_MAP="{}" - fi -else - echo "Warning: GitHub-to-Slack mapping not found at $GITHUB_SLACK_MAP_FILE" - echo "Reviewer pings will use GitHub usernames instead of Slack mentions" - GITHUB_SLACK_MAP="{}" -fi - -# Extract Slack fallback user ID from mapping (pinged when no reviewers are assigned) -SLACK_FALLBACK_USER_ID=$(jq -r '.["backup-user"] // empty' <<<"$GITHUB_SLACK_MAP") -if [ -n "$SLACK_FALLBACK_USER_ID" ]; then - echo "Slack fallback user ID loaded from mapping" -else - echo "Warning: No 'backup-user' key in GitHub-to-Slack mapping" -fi - -# Function to transition a Jira issue to a target status -transition_issue() { - local ISSUE_KEY=$1 - local TARGET_STATUS=$2 - - # Get available transitions - TRANSITIONS=$(curl -s \ - "https://redhat.atlassian.net/rest/api/3/issue/$ISSUE_KEY/transitions" \ - -H "Authorization: Basic $JIRA_AUTH" \ - -H "Content-Type: application/json") - - # Find transition ID for target status (match by name) - TRANSITION_ID=$(echo "$TRANSITIONS" | jq -r --arg status "$TARGET_STATUS" \ - '.transitions[] | select(.name == $status) | .id' | head -1) - - if [ -n "$TRANSITION_ID" ] && [ "$TRANSITION_ID" != "null" ]; then - curl -s -X POST \ - "https://redhat.atlassian.net/rest/api/3/issue/$ISSUE_KEY/transitions" \ - -H "Authorization: Basic $JIRA_AUTH" \ - -H "Content-Type: application/json" \ - -d "{\"transition\":{\"id\":\"$TRANSITION_ID\"}}" - return 0 - else - echo " Warning: Transition to '$TARGET_STATUS' not available" - return 1 - fi -} - -# Function to set assignee on a Jira issue (Cloud uses accountId) -set_assignee() { - local ISSUE_KEY=$1 - local ACCOUNT_ID=$2 - - curl -s -w "\n%{http_code}" -X PUT \ - "https://redhat.atlassian.net/rest/api/3/issue/$ISSUE_KEY/assignee" \ - -H "Authorization: Basic $JIRA_AUTH" \ - -H "Content-Type: application/json" \ - -d "{\"accountId\":\"$ACCOUNT_ID\"}" -} - -# Function to send Slack notification after PR creation -send_slack_notification() { - local PR_URL=$1 - local PR_NUM=$2 - - if [ -z "$SLACK_WEBHOOK_URL" ]; then - echo " Skipping Slack notification (no webhook URL configured)" - return 0 - fi - - echo " Polling for PR reviewers (up to 2 minutes)..." - local REVIEWERS="" - local PR_TITLE="" - local ATTEMPT=0 - local MAX_ATTEMPTS=5 - - while [ $ATTEMPT -lt $MAX_ATTEMPTS ]; do - local PR_DATA - PR_DATA=$(gh pr view "$PR_NUM" --repo openshift/hypershift --json reviewRequests,title 2>/dev/null || echo "{}") - PR_TITLE=$(echo "$PR_DATA" | jq -r '.title // empty' 2>/dev/null) - REVIEWERS=$(echo "$PR_DATA" | jq -r '.reviewRequests[]?.login // empty' 2>/dev/null) - if [ -n "$REVIEWERS" ]; then - echo " Reviewers found: $REVIEWERS" - break - fi - ATTEMPT=$((ATTEMPT + 1)) - if [ $ATTEMPT -lt $MAX_ATTEMPTS ]; then - echo " No reviewers yet, retrying in 30s (attempt $ATTEMPT/$MAX_ATTEMPTS)..." - sleep 30 - fi - done - - # Fallback PR title if not fetched - if [ -z "$PR_TITLE" ]; then - PR_TITLE="PR #${PR_NUM}" - fi - - # Build reviewer mention string - local REVIEWER_MENTIONS="" - if [ -n "$REVIEWERS" ]; then - while IFS= read -r gh_user; do - local slack_id - slack_id=$(echo "$GITHUB_SLACK_MAP" | jq -r --arg user "$gh_user" '.[$user] // empty' 2>/dev/null) - if [ -n "$slack_id" ]; then - REVIEWER_MENTIONS="${REVIEWER_MENTIONS} <@${slack_id}>" - else - REVIEWER_MENTIONS="${REVIEWER_MENTIONS} ${gh_user}" - fi - done <<< "$REVIEWERS" - else - echo " No reviewers assigned after 2 minutes, using fallback" - if [ -n "$SLACK_FALLBACK_USER_ID" ]; then - REVIEWER_MENTIONS="<@${SLACK_FALLBACK_USER_ID}>" - else - REVIEWER_MENTIONS="(none assigned)" - fi - fi - REVIEWER_MENTIONS=$(echo "$REVIEWER_MENTIONS" | sed 's/^ //') - - # Send Slack message (tracing disabled to protect webhook URL) - local SLACK_PAYLOAD - SLACK_PAYLOAD=$(jq -n --arg title "$PR_TITLE" --arg url "$PR_URL" --arg reviewers "$REVIEWER_MENTIONS" \ - '{text: ":hypershift-bot: *Jira Agent PR ready for review*\n:review: <\($url)|\($title)>\n:eyes: Reviewers: \($reviewers)"}') - - [[ $- == *x* ]] && local _was_tracing=true || local _was_tracing=false - set +x - set +e - local SLACK_RESPONSE - SLACK_RESPONSE=$(curl -s -w "\n%{http_code}" -X POST \ - --connect-timeout 10 \ - --max-time 20 \ - -H 'Content-type: application/json' \ - --data "$SLACK_PAYLOAD" \ - "$SLACK_WEBHOOK_URL") - local CURL_EXIT_CODE=$? - set -e - $_was_tracing && set -x - - if [ $CURL_EXIT_CODE -ne 0 ]; then - echo " Warning: Failed to send Slack notification (curl exit $CURL_EXIT_CODE)" - return 0 - fi - - local SLACK_HTTP_CODE - SLACK_HTTP_CODE=$(echo "$SLACK_RESPONSE" | tail -1) - - if [ "$SLACK_HTTP_CODE" = "200" ]; then - echo " Slack notification sent successfully" - else - echo " Warning: Failed to send Slack notification (HTTP $SLACK_HTTP_CODE)" - fi -} - -# Query Jira for issues (excluding already processed ones via label) -echo "Querying Jira for issues..." -if [ -n "${JIRA_AGENT_ISSUE_KEY:-}" ]; then - echo "Using override: JIRA_AGENT_ISSUE_KEY=$JIRA_AGENT_ISSUE_KEY" - JQL="key = ${JIRA_AGENT_ISSUE_KEY}" -else - JQL='project in (OCPBUGS, CNTRLPLANE) AND resolution = Unresolved AND status in (New, "To Do") AND labels = issue-for-agent AND labels != agent-processed' -fi -SEARCH_PAYLOAD=$(jq -n --arg jql "$JQL" --argjson max "$MAX_ISSUES" \ - '{jql: $jql, fields: ["key", "summary"], maxResults: $max}') -SEARCH_RESPONSE=$(curl -s -w "\n%{http_code}" "https://redhat.atlassian.net/rest/api/3/search/jql" \ - -X POST \ - -H "Authorization: Basic $JIRA_AUTH" \ - -H "Content-Type: application/json" \ - -d "$SEARCH_PAYLOAD") -SEARCH_HTTP_CODE=$(echo "$SEARCH_RESPONSE" | tail -1) -SEARCH_BODY=$(echo "$SEARCH_RESPONSE" | sed '$d') - -if [ "$SEARCH_HTTP_CODE" != "200" ]; then - echo "ERROR: Jira search failed (HTTP $SEARCH_HTTP_CODE)" - echo "Response: $SEARCH_BODY" - exit 1 -fi - -TOTAL_RESULTS=$(echo "$SEARCH_BODY" | jq -r '.total // 0') -echo "Jira search returned $TOTAL_RESULTS result(s)" -ISSUES=$(echo "$SEARCH_BODY" | jq -r '.issues[]? | "\(.key) \(.fields.summary)"') - -if [ -z "$ISSUES" ]; then - echo "No issues found matching criteria" - exit 0 -fi - -echo "Found issues:" -echo "$ISSUES" | awk '{print " - " $1}' - -# Counters for summary -PROCESSED_COUNT=0 -FAILED_COUNT=0 -TOTAL_PROCESSED_OR_FAILED=0 - -# Process each issue -while IFS= read -r line; do - # Stop if we've reached the max issues limit (counting both successful and failed) - if [ $TOTAL_PROCESSED_OR_FAILED -ge "$MAX_ISSUES" ]; then - echo "Reached maximum issues limit ($MAX_ISSUES). Stopping." - break - fi - # Reset to main branch for clean state between issues - git checkout main 2>/dev/null || true - git reset --hard upstream/main 2>/dev/null || true - - ISSUE_KEY=$(echo "$line" | awk '{print $1}') - ISSUE_SUMMARY=$(echo "$line" | cut -d' ' -f2-) - - echo "" - echo "==========================================" - echo "Processing: $ISSUE_KEY" - echo "Summary: $ISSUE_SUMMARY" - echo "==========================================" - - # Run jira-solve command non-interactively using --system-prompt - # (Claude's -p mode doesn't support slash commands directly) - TIMESTAMP=$(date -u +%Y-%m-%dT%H:%M:%SZ) - - echo "Running: jira-solve $ISSUE_KEY origin --ci" - - PHASE1_START=$(date +%s) - - # Load the skill content as system prompt - SKILL_CONTENT=$(cat /tmp/hypershift/.claude/commands/jira-solve.md) - - # Additional context for fork-based workflow - # Git push uses fork token (configured via credential helper), gh CLI uses upstream token (GITHUB_TOKEN env var) - FORK_CONTEXT="IMPORTANT: You are working in a fork (hypershift-community/hypershift). Git push is pre-configured to work with the fork. After creating commits on your feature branch, push the branch to origin. Do NOT create a Pull Request - the PR will be created in a subsequent automated step after code review. SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. ${SUBAGENT_PROMPT}" - - set +e # Don't exit on error for individual issues - echo "Starting Claude processing with streaming output..." - claude -p "$ISSUE_KEY origin --ci. $FORK_CONTEXT" \ - --system-prompt "$SKILL_CONTENT" \ - --allowedTools "Bash Read Write Edit Grep Glob WebFetch" \ - --max-turns 300 \ - --effort max \ - --model "$CLAUDE_MODEL" \ - --verbose \ - --output-format stream-json \ - 2> "/tmp/claude-${ISSUE_KEY}-output.log" \ - | tee "/tmp/claude-${ISSUE_KEY}-output.json" - EXIT_CODE=$? - set -e - jq -j 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "/tmp/claude-${ISSUE_KEY}-output.json" > "${SHARED_DIR}/claude-${ISSUE_KEY}-output-text.txt" 2>/dev/null || true - jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "/tmp/claude-${ISSUE_KEY}-output.json" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-${ISSUE_KEY}-output-tools.txt" 2>/dev/null || true - jq -r 'select(.type == "user") | .tool_use_result | select(type == "string") | select(startswith("Error:")) | gsub("\n"; "⏎")' "/tmp/claude-${ISSUE_KEY}-output.json" 2>/dev/null | sort | uniq -c | sort -rn | sed 's/⏎/\n/g' > "${SHARED_DIR}/claude-${ISSUE_KEY}-output-errors.txt" 2>/dev/null || true - # Extract token usage for Phase 1 - grep '"type":"result"' "/tmp/claude-${ISSUE_KEY}-output.json" \ - | head -1 \ - | jq '{ - total_cost_usd: (.total_cost_usd // 0), - duration_ms: (.duration_ms // 0), - num_turns: (.num_turns // 0), - input_tokens: (.usage.input_tokens // 0), - output_tokens: (.usage.output_tokens // 0), - cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), - cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), - model_usage: (.modelUsage // {}), - model: ((.modelUsage // {} | keys | first) // "unknown") - }' > "${SHARED_DIR}/claude-${ISSUE_KEY}-solve-tokens.json" 2>/dev/null \ - || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-${ISSUE_KEY}-solve-tokens.json" - echo "Phase 1 tokens: $(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-solve-tokens.json")" - - PHASE1_END=$(date +%s) - PHASE1_DURATION=$((PHASE1_END - PHASE1_START)) - echo "Phase 1 duration: ${PHASE1_DURATION}s" - echo "$PHASE1_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-solve-duration.txt" - - if [ $EXIT_CODE -eq 0 ]; then - echo "✅ Phase 1 (jira-solve) completed for $ISSUE_KEY" - - # Check if code changes were made (branch changed from main) - BRANCH_NAME=$(git branch --show-current) - HAS_CODE_CHANGES=false - PR_URL="" - - if [ "$BRANCH_NAME" != "main" ] && [ "$BRANCH_NAME" != "master" ] && [ -n "$BRANCH_NAME" ]; then - DIFF_FILES=$(git diff main...HEAD --name-only 2>/dev/null || echo "") - if [ -n "$DIFF_FILES" ]; then - HAS_CODE_CHANGES=true - echo "Code changes detected on branch '$BRANCH_NAME':" - echo "$DIFF_FILES" | sed 's/^/ /' | head -20 - fi - fi - - if [ "$HAS_CODE_CHANGES" = true ]; then - # === Phase 2: Pre-commit quality review === - echo "" - echo "==========================================" - echo "Phase 2: Pre-commit quality review for $ISSUE_KEY" - echo "==========================================" - - PHASE2_START=$(date +%s) - - REVIEW_PROMPT="/code-review:pre-commit-review --language go --profile hypershift" - - set +e - claude -p "$REVIEW_PROMPT" \ - --plugin-dir "${REVIEW_PLUGIN_DIR}" \ - --append-system-prompt "SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. ${SUBAGENT_PROMPT}" \ - --allowedTools "Bash Read Grep Glob Task" \ - --max-turns 225 \ - --effort max \ - --model "$CLAUDE_MODEL" \ - --verbose \ - --output-format stream-json \ - 2> "/tmp/claude-${ISSUE_KEY}-review.log" \ - | tee "/tmp/claude-${ISSUE_KEY}-review.json" - REVIEW_EXIT_CODE=$? - set -e - - jq -j 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "/tmp/claude-${ISSUE_KEY}-review.json" > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt" 2>/dev/null || true - jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "/tmp/claude-${ISSUE_KEY}-review.json" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tools.txt" 2>/dev/null || true - jq -r 'select(.type == "user") | .tool_use_result | select(type == "string") | select(startswith("Error:")) | gsub("\n"; "⏎")' "/tmp/claude-${ISSUE_KEY}-review.json" 2>/dev/null | sort | uniq -c | sort -rn | sed 's/⏎/\n/g' > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-errors.txt" 2>/dev/null || true - # Extract token usage for Phase 2 - grep '"type":"result"' "/tmp/claude-${ISSUE_KEY}-review.json" \ - | head -1 \ - | jq '{ - total_cost_usd: (.total_cost_usd // 0), - duration_ms: (.duration_ms // 0), - num_turns: (.num_turns // 0), - input_tokens: (.usage.input_tokens // 0), - output_tokens: (.usage.output_tokens // 0), - cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), - cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), - model_usage: (.modelUsage // {}), - model: ((.modelUsage // {} | keys | first) // "unknown") - }' > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tokens.json" 2>/dev/null \ - || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tokens.json" - echo "Phase 2 tokens: $(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tokens.json")" - - PHASE2_END=$(date +%s) - PHASE2_DURATION=$((PHASE2_END - PHASE2_START)) - echo "Phase 2 duration: ${PHASE2_DURATION}s" - echo "$PHASE2_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-review-duration.txt" - - if [ $REVIEW_EXIT_CODE -eq 0 ]; then - echo "✅ Phase 2 (pre-commit review) completed for $ISSUE_KEY" - else - echo "⚠️ Phase 2 (pre-commit review) failed for $ISSUE_KEY (exit code: $REVIEW_EXIT_CODE)" - echo "Continuing with PR creation despite review failure..." - fi - - # === Phase 3: Address review findings === - echo "" - echo "==========================================" - echo "Phase 3: Addressing review findings for $ISSUE_KEY" - echo "==========================================" - - # Read the review text to feed as context - REVIEW_FINDINGS="" - if [ -f "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt" ] && \ - [ -s "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt" ]; then - REVIEW_FINDINGS=$(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt") - fi - - # Refresh tokens before Phase 3 since it pushes code. - # Phases 1-2 can exceed the 1-hour GitHub App token lifetime. - echo "Refreshing GitHub App tokens before Phase 3..." - GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") - if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for fork" - else - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" - echo "Fork token refreshed" - fi - - PHASE3_START=$(date +%s) - - if [ -n "$REVIEW_FINDINGS" ]; then - FIX_PROMPT="A code review was performed on the changes in the current branch. Below are the review findings. Address all actions and improvements by editing the code. After making all fixes, commit the changes (amend existing commits or create new commits as appropriate) and push the branch to origin. - -REVIEW FINDINGS: -${REVIEW_FINDINGS} - -IMPORTANT: -- Fix every issue identified in the review — all actions and improvements. -- Run 'make test' and 'make verify' after fixes to verify nothing is broken. -- If 'make verify' generates new files, commit those too and run 'make verify' again to confirm it passes. -- Commit all fixes and push to origin. -- SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. -- ${SUBAGENT_PROMPT}" - - set +e - claude -p "$FIX_PROMPT" \ - --allowedTools "Bash Read Write Edit Grep Glob" \ - --max-turns 225 \ - --effort max \ - --model "$CLAUDE_MODEL" \ - --verbose \ - --output-format stream-json \ - 2> "/tmp/claude-${ISSUE_KEY}-fix.log" \ - | tee "/tmp/claude-${ISSUE_KEY}-fix.json" - FIX_EXIT_CODE=$? - set -e - - # Extract fix phase output for report - jq -j 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "/tmp/claude-${ISSUE_KEY}-fix.json" > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-text.txt" 2>/dev/null || true - jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "/tmp/claude-${ISSUE_KEY}-fix.json" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tools.txt" 2>/dev/null || true - jq -r 'select(.type == "user") | .tool_use_result | select(type == "string") | select(startswith("Error:")) | gsub("\n"; "⏎")' "/tmp/claude-${ISSUE_KEY}-fix.json" 2>/dev/null | sort | uniq -c | sort -rn | sed 's/⏎/\n/g' > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-errors.txt" 2>/dev/null || true - # Extract token usage for Phase 3 - grep '"type":"result"' "/tmp/claude-${ISSUE_KEY}-fix.json" \ - | head -1 \ - | jq '{ - total_cost_usd: (.total_cost_usd // 0), - duration_ms: (.duration_ms // 0), - num_turns: (.num_turns // 0), - input_tokens: (.usage.input_tokens // 0), - output_tokens: (.usage.output_tokens // 0), - cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), - cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), - model_usage: (.modelUsage // {}), - model: ((.modelUsage // {} | keys | first) // "unknown") - }' > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tokens.json" 2>/dev/null \ - || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tokens.json" - echo "Phase 3 tokens: $(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tokens.json")" - - if [ $FIX_EXIT_CODE -eq 0 ]; then - echo "✅ Phase 3 (address review) completed for $ISSUE_KEY" - else - echo "⚠️ Phase 3 (address review) failed (exit code: $FIX_EXIT_CODE)" - echo "Continuing with PR creation..." - fi - else - echo "No review findings to address, skipping Phase 3" - fi - - PHASE3_END=$(date +%s) - PHASE3_DURATION=$((PHASE3_END - PHASE3_START)) - echo "Phase 3 duration: ${PHASE3_DURATION}s" - echo "$PHASE3_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-duration.txt" - - # Regenerate GitHub App tokens before Phase 4. - # Phase 3 may also have taken significant time, so refresh again - # to ensure PR creation uses a valid token. - echo "Refreshing GitHub App tokens before Phase 4..." - GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") - if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for fork" - else - git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" - echo "Fork token refreshed" - fi - - GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") - if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for upstream" - else - export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" - echo "Upstream token refreshed" - fi - - # === Phase 4: Create Pull Request === - echo "" - echo "==========================================" - echo "Phase 4: Creating Pull Request for $ISSUE_KEY" - echo "==========================================" - - PHASE4_START=$(date +%s) - - PR_PROMPT="Create a pull request for the changes on branch '${BRANCH_NAME}'. Details: -- Jira issue: ${ISSUE_KEY} -- Jira summary: ${ISSUE_SUMMARY} -- Jira URL: https://redhat.atlassian.net/browse/${ISSUE_KEY} -- Read the PR template at .github/PULL_REQUEST_TEMPLATE.md and use it to structure the PR body. -- Use 'git log main..HEAD' to understand what changed and write a meaningful description. -- PR title must start with '${ISSUE_KEY}: '. -- The PR body MUST end with the following two lines: - Always review AI generated responses prior to use. - Generated with [Claude Code](https://claude.com/claude-code) via \`/jira:solve ${ISSUE_KEY}\` -- Create the PR by running: gh pr create --repo openshift/hypershift --head hypershift-community:${BRANCH_NAME} --no-maintainer-edit --title '' --body '<body>' -- SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. -- ${SUBAGENT_PROMPT}" - - set +e - claude -p "$PR_PROMPT" \ - --allowedTools "Bash Read Grep Glob" \ - --max-turns 90 \ - --effort max \ - --model "$CLAUDE_MODEL" \ - --verbose \ - --output-format stream-json \ - 2> "/tmp/claude-${ISSUE_KEY}-pr.log" \ - | tee "/tmp/claude-${ISSUE_KEY}-pr.json" - PR_EXIT_CODE=$? - set -e - - jq -j 'select(.type == "assistant") | .message.content[]? | select(.type == "text") | .text // empty' "/tmp/claude-${ISSUE_KEY}-pr.json" > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-text.txt" 2>/dev/null || true - jq -r 'select(.type == "assistant") | .message.content[]? | select(.type == "tool_use") | "\(.name): \(.input | keys | join(", "))"' "/tmp/claude-${ISSUE_KEY}-pr.json" 2>/dev/null | sort | uniq -c | sort -rn > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tools.txt" 2>/dev/null || true - jq -r 'select(.type == "user") | .tool_use_result | select(type == "string") | select(startswith("Error:")) | gsub("\n"; "⏎")' "/tmp/claude-${ISSUE_KEY}-pr.json" 2>/dev/null | sort | uniq -c | sort -rn | sed 's/⏎/\n/g' > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-errors.txt" 2>/dev/null || true - # Extract token usage for Phase 4 - grep '"type":"result"' "/tmp/claude-${ISSUE_KEY}-pr.json" \ - | head -1 \ - | jq '{ - total_cost_usd: (.total_cost_usd // 0), - duration_ms: (.duration_ms // 0), - num_turns: (.num_turns // 0), - input_tokens: (.usage.input_tokens // 0), - output_tokens: (.usage.output_tokens // 0), - cache_read_input_tokens: (.usage.cache_read_input_tokens // 0), - cache_creation_input_tokens: (.usage.cache_creation_input_tokens // 0), - model_usage: (.modelUsage // {}), - model: ((.modelUsage // {} | keys | first) // "unknown") - }' > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tokens.json" 2>/dev/null \ - || echo '{"total_cost_usd":0,"duration_ms":0,"num_turns":0,"input_tokens":0,"output_tokens":0,"cache_read_input_tokens":0,"cache_creation_input_tokens":0,"model_usage":{},"model":"unknown"}' > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tokens.json" - echo "Phase 4 tokens: $(cat "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tokens.json")" - - PHASE4_END=$(date +%s) - PHASE4_DURATION=$((PHASE4_END - PHASE4_START)) - echo "Phase 4 duration: ${PHASE4_DURATION}s" - echo "$PHASE4_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-duration.txt" - - if [ $PR_EXIT_CODE -eq 0 ]; then - PR_URL=$(grep -o 'https://github.com/openshift/hypershift/pull/[0-9]*' "/tmp/claude-${ISSUE_KEY}-pr.json" | head -1 || echo "") - if [ -n "$PR_URL" ]; then - echo "✅ PR created: $PR_URL" - else - echo "⚠️ Phase 4 completed but no PR URL found in output" - fi - else - echo "❌ Phase 4 (PR creation) failed for $ISSUE_KEY (exit code: $PR_EXIT_CODE)" - PR_URL="" - fi - - # Append report link to PR description - if [ -n "$PR_URL" ]; then - PR_NUM=$(echo "$PR_URL" | grep -o '[0-9]*$' || true) - if [ -n "$PR_NUM" ]; then - REPORT_URL="" - if [ -n "${BUILD_ID:-}" ] && [ -n "${JOB_NAME:-}" ]; then - if [ "${JOB_TYPE:-}" = "periodic" ]; then - REPORT_URL="https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/logs/${JOB_NAME}/${BUILD_ID}/artifacts/periodic-jira-agent/hypershift-jira-agent-report/artifacts/jira-agent-report.html" - else - REPORT_URL="https://gcsweb-ci.apps.ci.l2s4.p1.openshiftapps.com/gcs/test-platform-results/pr-logs/pull/openshift_release/${PULL_NUMBER:-0}/${JOB_NAME}/${BUILD_ID}/artifacts/periodic-jira-agent/hypershift-jira-agent-report/artifacts/jira-agent-report.html" - fi - fi - - if [ -n "$REPORT_URL" ]; then - echo "Appending report link to PR #${PR_NUM} description..." - CURRENT_BODY=$(gh pr view "$PR_NUM" --repo openshift/hypershift --json body -q .body 2>/dev/null || echo "") - REPORT_SECTION="--- - -> **Note:** This PR was auto-generated by the [jira-agent](https://github.com/openshift/release/tree/main/ci-operator/step-registry/hypershift/jira-agent) periodic CI job in response to [${ISSUE_KEY}](https://redhat.atlassian.net/browse/${ISSUE_KEY}). See the [full report](${REPORT_URL}) for token usage, cost breakdown, and detailed phase output." - UPDATED_BODY="${CURRENT_BODY} - -${REPORT_SECTION}" - gh pr edit "$PR_NUM" --repo openshift/hypershift --body "$UPDATED_BODY" 2>/dev/null || echo "Warning: Failed to update PR #${PR_NUM} description" - fi - fi - fi - - # Send Slack notification to team channel - if [ -n "$PR_URL" ] && [ -n "$PR_NUM" ]; then - send_slack_notification "$PR_URL" "$PR_NUM" - fi - else - echo "No code changes detected for $ISSUE_KEY, skipping review and PR creation" - fi - - # Add 'agent-processed' label to mark issue as handled - if [ -n "$JIRA_AUTH" ]; then - echo "Adding 'agent-processed' label to $ISSUE_KEY..." - LABEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \ - "https://redhat.atlassian.net/rest/api/3/issue/$ISSUE_KEY" \ - -H "Authorization: Basic $JIRA_AUTH" \ - -H "Content-Type: application/json" \ - -d '{"update":{"labels":[{"add":"agent-processed"}]}}') - HTTP_CODE=$(echo "$LABEL_RESPONSE" | tail -1) - if [ "$HTTP_CODE" = "204" ] || [ "$HTTP_CODE" = "200" ]; then - echo " Label added successfully" - else - echo " Warning: Failed to add label (HTTP $HTTP_CODE)" - fi - - # Transition issue to appropriate status based on project - if [[ "$ISSUE_KEY" == OCPBUGS-* ]]; then - TARGET_STATUS="ASSIGNED" - else - TARGET_STATUS="Code Review" - fi - - echo "Transitioning $ISSUE_KEY to '$TARGET_STATUS'..." - if transition_issue "$ISSUE_KEY" "$TARGET_STATUS"; then - echo " Transition successful" - else - echo " Transition failed or not available" - fi - - # Set assignee to hypershift-team automation (Cloud requires accountId, look it up by display name) - echo "Looking up accountId for 'hypershift-team automation'..." - ASSIGNEE_ACCOUNT_ID=$(curl -s -G \ - "https://redhat.atlassian.net/rest/api/3/user/search" \ - -H "Authorization: Basic $JIRA_AUTH" \ - --data-urlencode "query=hypershift-automation" \ - | jq -r '[.[] | select(.displayName == "hypershift-team automation")] | .[0].accountId // empty') - if [ -n "$ASSIGNEE_ACCOUNT_ID" ]; then - echo "Setting assignee to account ID '${ASSIGNEE_ACCOUNT_ID}'..." - ASSIGNEE_RESPONSE=$(set_assignee "$ISSUE_KEY" "$ASSIGNEE_ACCOUNT_ID") - else - echo " Warning: Could not find accountId for 'hypershift-team automation', skipping assignee" - ASSIGNEE_RESPONSE="skipped -200" - fi - HTTP_CODE=$(echo "$ASSIGNEE_RESPONSE" | tail -1) - if [ "$HTTP_CODE" = "204" ] || [ "$HTTP_CODE" = "200" ]; then - echo " Assignee set successfully" - else - echo " Warning: Failed to set assignee (HTTP $HTTP_CODE)" - fi - fi - - PROCESSED_COUNT=$((PROCESSED_COUNT + 1)) - echo "$ISSUE_KEY $TIMESTAMP $PR_URL SUCCESS" >> "$STATE_FILE" - else - # Log failure but don't mark as processed (will be retried next run) - echo "❌ Failed to process $ISSUE_KEY" - echo "Error output (last 20 lines):" - tail -20 "/tmp/claude-${ISSUE_KEY}-output.log" - FAILED_COUNT=$((FAILED_COUNT + 1)) - echo "$ISSUE_KEY $TIMESTAMP - FAILED" >> "$STATE_FILE" - fi - - # Increment total counter - TOTAL_PROCESSED_OR_FAILED=$((TOTAL_PROCESSED_OR_FAILED + 1)) - - # Rate limiting between issues (60 seconds) - # Skip sleep if we've reached the limit - if [ $TOTAL_PROCESSED_OR_FAILED -lt "$MAX_ISSUES" ]; then - echo "Waiting 60 seconds before next issue..." - sleep 60 - fi - -done <<< "$ISSUES" - -echo "" -echo "=== Processing Summary ===" -echo "Processed: $PROCESSED_COUNT" -echo "Failed: $FAILED_COUNT" -echo "==========================" diff --git a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.metadata.json b/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.metadata.json deleted file mode 100644 index 22d12984f05fb..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.metadata.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "path": "hypershift/jira-agent/process/hypershift-jira-agent-process-ref.yaml", - "owners": { - "approvers": [ - "bryan-cox", - "csrwng", - "celebdor", - "enxebre", - "sjenning" - ], - "reviewers": [ - "bryan-cox", - "csrwng", - "celebdor", - "enxebre", - "sjenning" - ] - } -} \ No newline at end of file diff --git a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.yaml b/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.yaml deleted file mode 100644 index 5cfad84f6418b..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.yaml +++ /dev/null @@ -1,68 +0,0 @@ - -ref: - as: hypershift-jira-agent-process - from: claude-ai-helpers - commands: hypershift-jira-agent-process-commands.sh - timeout: 14400s - env: - - name: CLAUDE_CODE_USE_VERTEX - default: "1" - documentation: |- - Enable Vertex AI for Claude Code. - - name: CLOUD_ML_REGION - default: "global" - documentation: |- - Google Cloud region for Vertex AI. - - name: ANTHROPIC_VERTEX_PROJECT_ID - default: "itpc-gcp-hybrid-pe-eng-claude" - documentation: |- - Google Cloud project ID for Vertex AI authentication. - - name: GOOGLE_APPLICATION_CREDENTIALS - default: "/var/run/claude-code-service-account/claude-prow" - documentation: |- - Path to the Google Cloud service account JSON key file for Vertex AI authentication. - - name: JIRA_AGENT_ISSUE_KEY - default: "" - documentation: |- - Optional override to process a specific Jira issue instead of querying. - When set (e.g., "CNTRLPLANE-2784"), skips the JQL query and processes - only this issue. Leave empty for normal JQL-based discovery. - - name: MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY - default: "" - documentation: |- - Gangway API override for JIRA_AGENT_ISSUE_KEY. When triggering - this job via the Gangway API, pass it as: - "pod_spec_options": { - "envs": { - "MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY": "CNTRLPLANE-2784" - } - } - - name: JIRA_AGENT_MAX_ISSUES - default: "1" - documentation: |- - Maximum number of Jira issues to process per run. Defaults to 1 for conservative processing. - - name: CLAUDE_MODEL - default: "claude-opus-4-6" - documentation: |- - Claude model to use for processing Jira issues. - resources: - requests: - cpu: 500m - memory: 1Gi - credentials: - - namespace: test-credentials - name: hypershift-team-claude-prow - mount_path: /var/run/claude-code-service-account - documentation: |- - Process step for the HyperShift Jira agent periodic job. - This step runs a four-phase pipeline for each issue: - Phase 1 - Solve: Runs /jira-solve to implement changes, commit, and push the branch (no PR created) - Phase 2 - Review: Runs /code-review:pre-commit-review to review code quality (read-only) - Phase 3 - Fix: Addresses review findings by editing code, committing, and pushing fixes - Phase 4 - PR Creation: Creates a draft PR via gh CLI after review is complete - Token usage (input/output) is extracted per phase and saved for reporting. - Post-processing: Adds 'agent-processed' label, transitions issue status, sets assignee - - Queries Jira for issues with label 'issue-for-agent' (excluding 'agent-processed') - - Failed issues are retried on subsequent runs - - If the review skill is unavailable or fails, the PR is still created - - Uses Vertex AI for Claude authentication via GCP service account diff --git a/ci-operator/step-registry/hypershift/jira-agent/report/OWNERS b/ci-operator/step-registry/hypershift/jira-agent/report/OWNERS deleted file mode 100644 index e39269bf55090..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/report/OWNERS +++ /dev/null @@ -1,12 +0,0 @@ -approvers: -- bryan-cox -- csrwng -- celebdor -- enxebre -- sjenning -reviewers: -- bryan-cox -- csrwng -- celebdor -- enxebre -- sjenning diff --git a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-commands.sh b/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-commands.sh deleted file mode 100755 index ed1bebcd86cea..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-commands.sh +++ /dev/null @@ -1,360 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "=== Jira Agent Report Generation ===" - -STATE_FILE="${SHARED_DIR}/processed-issues.txt" -REPORT_FILE="${ARTIFACT_DIR}/jira-agent-report.html" - -if [ ! -f "$STATE_FILE" ]; then - echo "No processed issues state file found. Nothing to report." - exit 0 -fi - -# Count issues by status -TOTAL=$(wc -l < "$STATE_FILE" | tr -d ' ') -SUCCESS_COUNT=$(grep -c 'SUCCESS$' "$STATE_FILE" 2>/dev/null || true) -FAILED_COUNT=$(grep -c 'FAILED$' "$STATE_FILE" 2>/dev/null || true) -: "${SUCCESS_COUNT:=0}" -: "${FAILED_COUNT:=0}" -RUN_TIMESTAMP=$(date -u +"%Y-%m-%d %H:%M:%S UTC") - -echo "Generating report for $TOTAL issues ($SUCCESS_COUNT succeeded, $FAILED_COUNT failed)" - -# Read a pre-extracted text file, or return a placeholder -read_extracted() { - local file=$1 - if [ -f "$file" ] && [ -s "$file" ]; then - cat "$file" - else - echo "(no output captured)" - fi -} - -# Read a JSON token file and extract a field, defaulting to 0 -read_token_field() { - local file=$1 - local field=$2 - if [ -f "$file" ] && [ -s "$file" ]; then - jq -r ".${field} // 0" "$file" 2>/dev/null || echo "0" - else - echo "0" - fi -} - -# Format token count with comma separators (GNU sed compatible) -format_number() { - local num=$1 - printf "%s" "$num" | sed -e ':a' -e 's/\([0-9]\)\([0-9]\{3\}\)\(\b\)/\1,\2\3/' -e 'ta' -} - -# Format a cost value as "$X.XXXX" -format_cost() { - local cost_usd=${1:-0} - printf '$%.4f' "$cost_usd" -} - -# Sum two floating-point cost values -sum_costs() { - local a=${1:-0} - local b=${2:-0} - awk "BEGIN {printf \"%.6f\", $a + $b}" 2>/dev/null || echo "0" -} - -# HTML-escape a string -html_escape() { - sed 's/&/\&/g; s/</\</g; s/>/\>/g; s/"/\"/g' -} - -# Read a duration file and return the value in seconds, or 0 if missing -read_duration() { - local file=$1 - if [ -f "$file" ] && [ -s "$file" ]; then - cat "$file" | tr -d '[:space:]' - else - echo "0" - fi -} - -# Format seconds into a human-readable string (e.g. "40m 36s") -format_duration() { - local secs=$1 - if [ "$secs" -eq 0 ]; then - echo "-" - return - fi - local hours=$((secs / 3600)) - local mins=$(( (secs % 3600) / 60 )) - local s=$((secs % 60)) - if [ "$hours" -gt 0 ]; then - printf "%dh %dm %ds" "$hours" "$mins" "$s" - elif [ "$mins" -gt 0 ]; then - printf "%dm %ds" "$mins" "$s" - else - printf "%ds" "$s" - fi -} - -# Build issue rows for summary table and detail sections -SUMMARY_ROWS="" -DETAIL_SECTIONS="" -GRAND_TOTAL_INPUT=0 -GRAND_TOTAL_OUTPUT=0 -GRAND_TOTAL_CACHE_READ=0 -GRAND_TOTAL_CACHE_CREATE=0 -GRAND_TOTAL_COST_USD="0" - -while IFS= read -r line; do - ISSUE_KEY=$(echo "$line" | awk '{print $1}') - ISSUE_TIMESTAMP=$(echo "$line" | awk '{print $2}') - PR_URL=$(echo "$line" | awk '{print $3}') - STATUS=$(echo "$line" | awk '{print $4}') - - # Debug: verify token files exist and jq is available - echo "Processing issue $ISSUE_KEY (status=$STATUS)" - echo " Token files check:" - for phase in solve review fix pr; do - tf="${SHARED_DIR}/claude-${ISSUE_KEY}-${phase}-tokens.json" - if [ -f "$tf" ]; then - echo " ${phase}: $(cat "$tf" | tr -d '\n' | cut -c1-120)" - else - echo " ${phase}: FILE NOT FOUND" - fi - done - - if [ "$STATUS" = "SUCCESS" ]; then - STATUS_CLASS="success" - STATUS_LABEL="Success" - else - STATUS_CLASS="failed" - STATUS_LABEL="Failed" - fi - - # PR link or dash - if [ -n "$PR_URL" ] && [ "$PR_URL" != "-" ]; then - PR_LINK="<a href=\"${PR_URL}\">${PR_URL}</a>" - else - PR_LINK="-" - fi - - # Read pre-extracted phase outputs - SOLVE_TEXT=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-output-text.txt" | html_escape) - REVIEW_TEXT=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-review-text.txt" | html_escape) - FIX_TEXT=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-text.txt" | html_escape) - PR_TEXT=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-text.txt" | html_escape) - - SOLVE_TOOLS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-output-tools.txt" | html_escape) - REVIEW_TOOLS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-review-tools.txt" | html_escape) - FIX_TOOLS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-tools.txt" | html_escape) - PR_TOOLS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-tools.txt" | html_escape) - - SOLVE_ERRORS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-output-errors.txt" | html_escape) - REVIEW_ERRORS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-review-errors.txt" | html_escape) - FIX_ERRORS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-fix-errors.txt" | html_escape) - PR_ERRORS=$(read_extracted "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-errors.txt" | html_escape) - - # Read token usage per phase - ISSUE_TOTAL_INPUT=0 - ISSUE_TOTAL_OUTPUT=0 - ISSUE_TOTAL_CACHE_READ=0 - ISSUE_TOTAL_CACHE_CREATE=0 - ISSUE_TOTAL_COST_USD="0" - TOKEN_ROWS="" - MODEL="unknown" - - ISSUE_TOTAL_DURATION=0 - - for phase_info in "solve:Phase 1: Solve" "review:Phase 2: Review" "fix:Phase 3: Fix" "pr:Phase 4: PR"; do - PHASE_KEY="${phase_info%%:*}" - PHASE_LABEL="${phase_info#*:}" - TOKEN_FILE="${SHARED_DIR}/claude-${ISSUE_KEY}-${PHASE_KEY}-tokens.json" - DURATION_FILE="${SHARED_DIR}/claude-${ISSUE_KEY}-${PHASE_KEY}-duration.txt" - - P_INPUT=$(read_token_field "$TOKEN_FILE" "input_tokens") - P_OUTPUT=$(read_token_field "$TOKEN_FILE" "output_tokens") - P_CACHE_READ=$(read_token_field "$TOKEN_FILE" "cache_read_input_tokens") - P_CACHE_CREATE=$(read_token_field "$TOKEN_FILE" "cache_creation_input_tokens") - P_MODEL=$(read_token_field "$TOKEN_FILE" "model") - P_DURATION=$(read_duration "$DURATION_FILE") - P_COST_RAW=$(read_token_field "$TOKEN_FILE" "total_cost_usd") - if [ "$P_MODEL" != "0" ] && [ "$P_MODEL" != "unknown" ]; then - MODEL="$P_MODEL" - fi - - P_COST=$(format_cost "$P_COST_RAW") - - ISSUE_TOTAL_INPUT=$((ISSUE_TOTAL_INPUT + P_INPUT)) - ISSUE_TOTAL_OUTPUT=$((ISSUE_TOTAL_OUTPUT + P_OUTPUT)) - ISSUE_TOTAL_CACHE_READ=$((ISSUE_TOTAL_CACHE_READ + P_CACHE_READ)) - ISSUE_TOTAL_CACHE_CREATE=$((ISSUE_TOTAL_CACHE_CREATE + P_CACHE_CREATE)) - ISSUE_TOTAL_COST_USD=$(sum_costs "$ISSUE_TOTAL_COST_USD" "$P_COST_RAW") - ISSUE_TOTAL_DURATION=$((ISSUE_TOTAL_DURATION + P_DURATION)) - - if [ "$P_INPUT" -gt 0 ] || [ "$P_OUTPUT" -gt 0 ]; then - TOKEN_ROWS="${TOKEN_ROWS}<tr><td>${PHASE_LABEL}</td><td>$(format_duration "$P_DURATION")</td><td>$(format_number "$P_INPUT")</td><td>$(format_number "$P_OUTPUT")</td><td>$(format_number "$P_CACHE_READ")</td><td>$(format_number "$P_CACHE_CREATE")</td><td>${P_COST}</td></tr>" - fi - done - - ISSUE_COST=$(format_cost "$ISSUE_TOTAL_COST_USD") - - # Build per-model breakdown rows from aggregated model_usage across phases - MODEL_BREAKDOWN_ROWS="" - MODEL_FILES="" - for phase_key in solve review fix pr; do - tf="${SHARED_DIR}/claude-${ISSUE_KEY}-${phase_key}-tokens.json" - if [ -f "$tf" ]; then - MODEL_FILES="$MODEL_FILES $tf" - fi - done - if [ -n "$MODEL_FILES" ]; then - MODEL_BREAKDOWN=$(jq -s ' - [.[].model_usage // {} | to_entries[]] - | group_by(.key) - | map({ - model: .[0].key, - input: (map(.value.inputTokens // .value.input_tokens // 0) | add), - output: (map(.value.outputTokens // .value.output_tokens // 0) | add), - cache_read: (map(.value.cacheReadInputTokens // .value.cache_read_input_tokens // 0) | add), - cache_create: (map(.value.cacheCreationInputTokens // .value.cache_creation_input_tokens // 0) | add) - }) - | sort_by(.model) - | .[] - | "\(.model)|\(.input)|\(.output)|\(.cache_read)|\(.cache_create)" - ' $MODEL_FILES 2>/dev/null || echo "") - if [ -n "$MODEL_BREAKDOWN" ]; then - MODEL_BREAKDOWN_ROWS="<tr><td colspan=\"7\" style=\"background:#f0f0f0; font-size:0.85em; color:#666; padding:0.3em 1em;\"><em>Per-model breakdown</em></td></tr>" - while IFS='|' read -r M_NAME M_INPUT M_OUTPUT M_CACHE_READ M_CACHE_CREATE; do - if [ -n "$M_NAME" ]; then - M_SHORT=$(echo "$M_NAME" | sed 's/-[0-9]*$//') - MODEL_BREAKDOWN_ROWS="${MODEL_BREAKDOWN_ROWS}<tr style=\"font-size:0.85em; color:#666;\"><td>  ${M_SHORT}</td><td>-</td><td>$(format_number "$M_INPUT")</td><td>$(format_number "$M_OUTPUT")</td><td>$(format_number "$M_CACHE_READ")</td><td>$(format_number "$M_CACHE_CREATE")</td><td>-</td></tr>" - fi - done <<< "$MODEL_BREAKDOWN" - fi - fi - - # Accumulate grand totals - GRAND_TOTAL_INPUT=$((GRAND_TOTAL_INPUT + ISSUE_TOTAL_INPUT)) - GRAND_TOTAL_OUTPUT=$((GRAND_TOTAL_OUTPUT + ISSUE_TOTAL_OUTPUT)) - GRAND_TOTAL_CACHE_READ=$((GRAND_TOTAL_CACHE_READ + ISSUE_TOTAL_CACHE_READ)) - GRAND_TOTAL_CACHE_CREATE=$((GRAND_TOTAL_CACHE_CREATE + ISSUE_TOTAL_CACHE_CREATE)) - GRAND_TOTAL_COST_USD=$(sum_costs "$GRAND_TOTAL_COST_USD" "$ISSUE_TOTAL_COST_USD") - - # Token usage table for this issue - TOKEN_TABLE="" - if [ -n "$TOKEN_ROWS" ]; then - TOKEN_TABLE=" - <h3>Token Usage & Cost</h3> - <table class=\"token-table\"> - <thead><tr><th>Phase</th><th>Duration</th><th>Input Tokens</th><th>Output Tokens</th><th>Cache Read</th><th>Cache Create</th><th>Cost</th></tr></thead> - <tbody> - ${TOKEN_ROWS} - <tr class=\"total-row\"><td><strong>Total</strong></td><td><strong>$(format_duration "$ISSUE_TOTAL_DURATION")</strong></td><td><strong>$(format_number "$ISSUE_TOTAL_INPUT")</strong></td><td><strong>$(format_number "$ISSUE_TOTAL_OUTPUT")</strong></td><td><strong>$(format_number "$ISSUE_TOTAL_CACHE_READ")</strong></td><td><strong>$(format_number "$ISSUE_TOTAL_CACHE_CREATE")</strong></td><td><strong>${ISSUE_COST}</strong></td></tr> - ${MODEL_BREAKDOWN_ROWS} - </tbody> - </table> - <p class=\"model-info\">Model: ${MODEL}</p>" - fi - - # Summary table row - SUMMARY_ROWS="${SUMMARY_ROWS}<tr><td><a href=\"https://redhat.atlassian.net/browse/${ISSUE_KEY}\">${ISSUE_KEY}</a></td><td>${ISSUE_TIMESTAMP}</td><td><span class=\"status ${STATUS_CLASS}\">${STATUS_LABEL}</span></td><td>${PR_LINK}</td><td>${ISSUE_COST}</td></tr>" - - DETAIL_SECTIONS="${DETAIL_SECTIONS} -<div class=\"issue-card\"> - <h2><a href=\"https://redhat.atlassian.net/browse/${ISSUE_KEY}\">${ISSUE_KEY}</a> <span class=\"status ${STATUS_CLASS}\">${STATUS_LABEL}</span></h2> - ${TOKEN_TABLE} - - <h3>Phase 1: Solve</h3> - <div class=\"phase-output\"><pre>${SOLVE_TEXT}</pre></div> - <details><summary>Tool calls</summary><pre>${SOLVE_TOOLS}</pre></details> - <details><summary>Tool errors</summary><pre class=\"error-pre\">${SOLVE_ERRORS}</pre></details> - - <h3>Phase 2: Pre-commit Review</h3> - <div class=\"phase-output\"><pre>${REVIEW_TEXT}</pre></div> - <details><summary>Tool calls</summary><pre>${REVIEW_TOOLS}</pre></details> - <details><summary>Tool errors</summary><pre class=\"error-pre\">${REVIEW_ERRORS}</pre></details> - - <h3>Phase 3: Review Fixes</h3> - <div class=\"phase-output\"><pre>${FIX_TEXT}</pre></div> - <details><summary>Tool calls</summary><pre>${FIX_TOOLS}</pre></details> - <details><summary>Tool errors</summary><pre class=\"error-pre\">${FIX_ERRORS}</pre></details> - - <h3>Phase 4: PR Creation</h3> - <div class=\"phase-output\"><pre>${PR_TEXT}</pre></div> - <details><summary>Tool calls</summary><pre>${PR_TOOLS}</pre></details> - <details><summary>Tool errors</summary><pre class=\"error-pre\">${PR_ERRORS}</pre></details> -</div>" - -done < "$STATE_FILE" - -# Format grand total cost -GRAND_TOTAL_COST=$(format_cost "$GRAND_TOTAL_COST_USD") - -# Write the HTML report -cat > "$REPORT_FILE" <<EOF -<!DOCTYPE html> -<html lang="en"> -<head> -<meta charset="UTF-8"> -<title>Jira Agent Report - - - -

Jira Agent Report

-

Generated: ${RUN_TIMESTAMP}

- -
-
${TOTAL}
Total
-
${SUCCESS_COUNT}
Succeeded
-
${FAILED_COUNT}
Failed
-
$(format_number "$GRAND_TOTAL_INPUT")
Input Tokens
-
$(format_number "$GRAND_TOTAL_OUTPUT")
Output Tokens
-
${GRAND_TOTAL_COST}
Cost
-
- -

Summary

- - - -${SUMMARY_ROWS} - -
IssueTimestampStatusPull RequestCost
- -

Details

-${DETAIL_SECTIONS} - - - -EOF - -echo "Report written to ${REPORT_FILE}" - -echo "=== Report generation complete ===" diff --git a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.metadata.json b/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.metadata.json deleted file mode 100644 index 7b678563c9443..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.metadata.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "path": "hypershift/jira-agent/report/hypershift-jira-agent-report-ref.yaml", - "owners": { - "approvers": [ - "bryan-cox", - "csrwng", - "celebdor", - "enxebre", - "sjenning" - ], - "reviewers": [ - "bryan-cox", - "csrwng", - "celebdor", - "enxebre", - "sjenning" - ] - } -} \ No newline at end of file diff --git a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.yaml b/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.yaml deleted file mode 100644 index 0bf59445bd783..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.yaml +++ /dev/null @@ -1,12 +0,0 @@ -ref: - as: hypershift-jira-agent-report - from: claude-ai-helpers - commands: hypershift-jira-agent-report-commands.sh - resources: - requests: - cpu: 100m - memory: 256Mi - documentation: |- - Generates an HTML report from the jira-agent processing output. - Parses stream-json output from all three phases (solve, review, PR) - and produces a readable report in ${ARTIFACT_DIR}. diff --git a/ci-operator/step-registry/hypershift/jira-agent/setup/OWNERS b/ci-operator/step-registry/hypershift/jira-agent/setup/OWNERS deleted file mode 100644 index e39269bf55090..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/setup/OWNERS +++ /dev/null @@ -1,12 +0,0 @@ -approvers: -- bryan-cox -- csrwng -- celebdor -- enxebre -- sjenning -reviewers: -- bryan-cox -- csrwng -- celebdor -- enxebre -- sjenning diff --git a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-commands.sh b/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-commands.sh deleted file mode 100755 index 94cc7c5d99139..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-commands.sh +++ /dev/null @@ -1,10 +0,0 @@ -#!/bin/bash -set -euo pipefail - -echo "=== HyperShift Jira Agent Setup ===" - -# Verify Claude Code is available (Vertex AI authentication is handled via GOOGLE_APPLICATION_CREDENTIALS env var) -echo "Verifying Claude Code CLI..." -claude --version || { echo "ERROR: Claude Code CLI not found"; exit 1; } - -echo "Setup complete" diff --git a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.metadata.json b/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.metadata.json deleted file mode 100644 index 59e74a6fdf8fb..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.metadata.json +++ /dev/null @@ -1,19 +0,0 @@ -{ - "path": "hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml", - "owners": { - "approvers": [ - "bryan-cox", - "csrwng", - "celebdor", - "enxebre", - "sjenning" - ], - "reviewers": [ - "bryan-cox", - "csrwng", - "celebdor", - "enxebre", - "sjenning" - ] - } -} \ No newline at end of file diff --git a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml b/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml deleted file mode 100644 index 7aa6338f82932..0000000000000 --- a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml +++ /dev/null @@ -1,37 +0,0 @@ -ref: - as: hypershift-jira-agent-setup - from: claude-ai-helpers - commands: hypershift-jira-agent-setup-commands.sh - env: - - name: CLAUDE_CODE_USE_VERTEX - default: "1" - documentation: |- - Enable Vertex AI for Claude Code. - - name: CLOUD_ML_REGION - default: "global" - documentation: |- - Google Cloud region for Vertex AI. - - name: ANTHROPIC_VERTEX_PROJECT_ID - default: "itpc-gcp-hybrid-pe-eng-claude" - documentation: |- - Google Cloud project ID for Vertex AI authentication. - - name: GOOGLE_APPLICATION_CREDENTIALS - default: "/var/run/claude-code-service-account/claude-prow" - documentation: |- - Path to the Google Cloud service account JSON key file for Vertex AI authentication. - resources: - requests: - cpu: 100m - memory: 200Mi - credentials: - - namespace: test-credentials - name: hypershift-team-claude-prow - mount_path: /var/run/claude-code-service-account - documentation: |- - Setup step for the HyperShift Jira agent periodic job. - This step: - - Clones the HyperShift repository - - Configures git credentials for creating commits - - Sets up GitHub CLI authentication - - Verifies Claude Code CLI is available - - Uses Vertex AI for Claude authentication via GCP service account diff --git a/ci-operator/step-registry/jira-agent/README.md b/ci-operator/step-registry/jira-agent/README.md index 248c4a1b0a89c..7e8b90b1a21ea 100644 --- a/ci-operator/step-registry/jira-agent/README.md +++ b/ci-operator/step-registry/jira-agent/README.md @@ -27,13 +27,16 @@ workflow: - ref: jira-agent-process post: - ref: jira-agent-report - env: - JIRA_AGENT_FORK_REPO: "my-org/my-repo" - JIRA_AGENT_UPSTREAM_REPO: "openshift/my-repo" - JIRA_AGENT_JQL: 'project = MYPROJ AND resolution = Unresolved AND labels = issue-for-agent' + env: + JIRA_AGENT_FORK_REPO: "my-org/my-repo" + JIRA_AGENT_UPSTREAM_REPO: "openshift/my-repo" + JIRA_AGENT_JQL: 'project = MYPROJ AND resolution = Unresolved AND labels = issue-for-agent' ``` -Override the credential secret in each ref's `credentials` block to point to your team's Vault secret. +**Credentials:** The generic ref YAMLs use the `hypershift-team-claude-prow` Vault secret. +Teams using a different secret must create their own ref YAMLs (setup + process) that +reference the same command scripts but with their credential name. Workflow-level `env` +cannot override a ref's `credentials` block. ## Required Environment Variables @@ -65,12 +68,13 @@ Override the credential secret in each ref's `credentials` block to point to you Each team needs a Vault secret containing: - `claude-prow` — GCP service account JSON for Vertex AI -- `jira-token` — Jira API token (Basic auth) -- `jira-user` — Jira username -- `github-app-id` — GitHub App ID -- `github-app-private-key` — GitHub App private key (PEM) -- `installation-id` — Fork GitHub App installation ID -- Upstream installation ID (key name configurable via `JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY`) -- `slack-webhook` — Slack incoming webhook URL +- `jira-pat` — Jira API token (Basic auth) +- `jira-email` — Jira account email +- `app-id` — GitHub App ID +- `private-key` — GitHub App private key (PEM) +- `installation-id` — Fork GitHub App installation ID (key name configurable via `JIRA_AGENT_FORK_INSTALLATION_ID_KEY`) +- Upstream installation ID (key name configurable via `JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY`, default: `o-h-installation-id`) +- `slack-webhook-url` — Slack incoming webhook URL +- `gh-to-slack-ids` — JSON mapping of GitHub usernames to Slack user IDs (optional) -See the onboarding guide in `openshift-eng/ai-helpers` for full setup instructions. +See the onboarding guide in `openshift/hypershift` docs for full setup instructions. diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh index 7259693c27e80..f0206a002e71a 100644 --- a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh @@ -53,6 +53,7 @@ fi echo "Code-review plugin found" # Install tool dependencies (project-specific) +# Trust boundary: these env vars come from the workflow YAML authored by team members if [ -n "${JIRA_AGENT_TOOL_SETUP_SCRIPT:-}" ]; then echo "Running project-specific tool setup..." eval "$JIRA_AGENT_TOOL_SETUP_SCRIPT" @@ -107,10 +108,15 @@ if [ ! -f "$APP_ID_FILE" ] || [ ! -f "$INSTALLATION_ID_FILE" ] || [ ! -f "$PRIVA echo " - ${UPSTREAM_INSTALL_ID_KEY} (for ${JIRA_AGENT_UPSTREAM_REPO} upstream)" echo " - private-key" echo "" - echo "Exiting gracefully. Re-run once secrets are synced." - exit 0 + echo "ERROR: Required credentials are missing. Re-run once secrets are synced." + echo "no_credentials" > "${SHARED_DIR}/processed-issues.txt" + exit 1 fi +# Disable tracing for credential handling +[[ $- == *x* ]] && _TOKEN_WAS_TRACING=true || _TOKEN_WAS_TRACING=false +set +x + APP_ID=$(cat "$APP_ID_FILE") INSTALLATION_ID_FORK=$(cat "$INSTALLATION_ID_FILE") INSTALLATION_ID_UPSTREAM=$(cat "$INSTALLATION_ID_UPSTREAM_FILE") @@ -143,6 +149,7 @@ echo "Generating GitHub App token for fork..." GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then echo "ERROR: Failed to generate GitHub App token for fork" + $_TOKEN_WAS_TRACING && set -x exit 1 fi echo "Fork token generated successfully" @@ -152,6 +159,7 @@ echo "Generating GitHub App token for upstream..." GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then echo "ERROR: Failed to generate GitHub App token for upstream" + $_TOKEN_WAS_TRACING && set -x exit 1 fi echo "Upstream token generated successfully" @@ -163,6 +171,8 @@ git config --global credential.helper "!f() { echo username=x-access-token; echo export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" echo "GitHub App tokens configured successfully" +$_TOKEN_WAS_TRACING && set -x + # Configuration: maximum issues to process per run (default: 1) MAX_ISSUES=${JIRA_AGENT_MAX_ISSUES:-1} echo "Configuration: MAX_ISSUES=$MAX_ISSUES" @@ -173,6 +183,8 @@ SUBAGENT_PROMPT="SUBAGENTS: Launch ALL subagents in parallel (single message wit # Load Jira API credentials for Atlassian Cloud (Basic Auth: email:api-token) JIRA_TOKEN_FILE="/var/run/claude-code-service-account/jira-pat" JIRA_EMAIL_FILE="/var/run/claude-code-service-account/jira-email" +[[ $- == *x* ]] && _JIRA_WAS_TRACING=true || _JIRA_WAS_TRACING=false +set +x if [ -f "$JIRA_TOKEN_FILE" ] && [ -f "$JIRA_EMAIL_FILE" ]; then JIRA_TOKEN=$(cat "$JIRA_TOKEN_FILE") JIRA_EMAIL=$(cat "$JIRA_EMAIL_FILE") @@ -184,6 +196,7 @@ else JIRA_TOKEN="" JIRA_AUTH="" fi +$_JIRA_WAS_TRACING && set -x # Load Slack webhook URL for notifications (tracing disabled to protect credential) SLACK_WEBHOOK_FILE="/var/run/claude-code-service-account/slack-webhook-url" @@ -428,7 +441,7 @@ while IFS= read -r line; do SKILL_CONTENT=$(cat /tmp/project-repo/.claude/commands/jira-solve.md) # Additional context for fork-based workflow - FORK_CONTEXT="IMPORTANT: You are working in a fork (${JIRA_AGENT_FORK_REPO}). Git push is pre-configured to work with the fork. After creating commits on your feature branch, push the branch to origin. Do NOT create a Pull Request - the PR will be created in a subsequent automated step after code review. SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. ${SUBAGENT_PROMPT}" + FORK_CONTEXT="IMPORTANT: You are working in a fork (${JIRA_AGENT_FORK_REPO}). Git push is pre-configured to work with the fork. After creating commits on your feature branch, push the branch to origin. Do NOT create a Pull Request - the PR will be created in a subsequent automated step after code review. SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v', 'git remote get-url origin', 'git config --list', 'git config --global credential.helper', or 'cat ~/.gitconfig'. ${SUBAGENT_PROMPT}" set +e # Don't exit on error for individual issues echo "Starting Claude processing with streaming output..." @@ -503,7 +516,7 @@ while IFS= read -r line; do set +e claude -p "$REVIEW_PROMPT" \ --plugin-dir "${REVIEW_PLUGIN_DIR}" \ - --append-system-prompt "SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. ${SUBAGENT_PROMPT}" \ + --append-system-prompt "SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v', 'git remote get-url origin', 'git config --list', 'git config --global credential.helper', or 'cat ~/.gitconfig'. ${SUBAGENT_PROMPT}" \ --allowedTools "Bash Read Grep Glob Task" \ --max-turns 225 \ --effort max \ @@ -562,13 +575,16 @@ while IFS= read -r line; do # Refresh tokens before Phase 3 since it pushes code. echo "Refreshing GitHub App tokens before Phase 3..." + [[ $- == *x* ]] && _REFRESH3_TRACING=true || _REFRESH3_TRACING=false + set +x GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for fork" + echo "ERROR: Failed to refresh GitHub App token for fork — continuing with previous token" else git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" echo "Fork token refreshed" fi + $_REFRESH3_TRACING && set -x PHASE3_START=$(date +%s) @@ -583,7 +599,7 @@ IMPORTANT: - Run 'make test' and 'make verify' after fixes to verify nothing is broken. - If 'make verify' generates new files, commit those too and run 'make verify' again to confirm it passes. - Commit all fixes and push to origin. -- SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. +- SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v', 'git remote get-url origin', 'git config --list', 'git config --global credential.helper', or 'cat ~/.gitconfig'. - ${SUBAGENT_PROMPT}" set +e @@ -637,9 +653,11 @@ IMPORTANT: # Regenerate GitHub App tokens before Phase 4. echo "Refreshing GitHub App tokens before Phase 4..." + [[ $- == *x* ]] && _REFRESH4_TRACING=true || _REFRESH4_TRACING=false + set +x GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for fork" + echo "ERROR: Failed to refresh GitHub App token for fork — continuing with previous token" else git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" echo "Fork token refreshed" @@ -647,11 +665,12 @@ IMPORTANT: GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for upstream" + echo "ERROR: Failed to refresh GitHub App token for upstream — continuing with previous token" else export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" echo "Upstream token refreshed" fi + $_REFRESH4_TRACING && set -x # === Phase 4: Create Pull Request === echo "" @@ -672,7 +691,7 @@ IMPORTANT: Always review AI generated responses prior to use. Generated with [Claude Code](https://claude.com/claude-code) via \`/jira:solve ${ISSUE_KEY}\` - Create the PR by running: gh pr create --repo ${JIRA_AGENT_UPSTREAM_REPO} --head ${FORK_ORG}:${BRANCH_NAME} --no-maintainer-edit --title '' --body '<body>' -- SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v' or 'git remote get-url origin'. +- SECURITY: Do NOT run commands that reveal git credentials like 'git remote -v', 'git remote get-url origin', 'git config --list', 'git config --global credential.helper', or 'cat ~/.gitconfig'. - ${SUBAGENT_PROMPT}" set +e diff --git a/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh b/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh index 9b9923a6c1b29..509a8bc75d084 100644 --- a/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh +++ b/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh @@ -201,14 +201,14 @@ while IFS= read -r line; do # Build per-model breakdown rows from aggregated model_usage across phases MODEL_BREAKDOWN_ROWS="" - MODEL_FILES="" + MODEL_FILES_ARR=() for phase_key in solve review fix pr; do tf="${SHARED_DIR}/claude-${ISSUE_KEY}-${phase_key}-tokens.json" if [ -f "$tf" ]; then - MODEL_FILES="$MODEL_FILES $tf" + MODEL_FILES_ARR+=("$tf") fi done - if [ -n "$MODEL_FILES" ]; then + if [ ${#MODEL_FILES_ARR[@]} -gt 0 ]; then MODEL_BREAKDOWN=$(jq -s ' [.[].model_usage // {} | to_entries[]] | group_by(.key) @@ -222,7 +222,7 @@ while IFS= read -r line; do | sort_by(.model) | .[] | "\(.model)|\(.input)|\(.output)|\(.cache_read)|\(.cache_create)" - ' $MODEL_FILES 2>/dev/null || echo "") + ' "${MODEL_FILES_ARR[@]}" 2>/dev/null || echo "") if [ -n "$MODEL_BREAKDOWN" ]; then MODEL_BREAKDOWN_ROWS="<tr><td colspan=\"7\" style=\"background:#f0f0f0; font-size:0.85em; color:#666; padding:0.3em 1em;\"><em>Per-model breakdown</em></td></tr>" while IFS='|' read -r M_NAME M_INPUT M_OUTPUT M_CACHE_READ M_CACHE_CREATE; do From f0cc17b317c21357a02682926a94578a2840457b Mon Sep 17 00:00:00 2001 From: Bryan Cox <brcox@redhat.com> Date: Tue, 23 Jun 2026 11:41:18 -0400 Subject: [PATCH 4/7] fix: address CodeRabbit review findings in jira-agent step registry - Move Gangway override before validation so JIRA_AGENT_JQL is optional when JIRA_AGENT_ISSUE_KEY is provided (fixes single-issue mode) - Check Jira transition POST HTTP status before returning success - Don't mark issues as agent-processed when PR creation fails - Use temp variable pattern for token refresh to preserve previous token on failure instead of overwriting - Fix awk code injection in sum_costs by using -v variable assignment - Add Vertex AI credentials preflight check in setup step Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../process/jira-agent-process-commands.sh | 69 ++++++++++++------- .../report/jira-agent-report-commands.sh | 2 +- .../setup/jira-agent-setup-commands.sh | 6 ++ 3 files changed, 51 insertions(+), 26 deletions(-) diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh index f0206a002e71a..eef3d691cafeb 100644 --- a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh @@ -3,18 +3,22 @@ set -euo pipefail echo "=== Jira Agent Process ===" +# Apply Gangway API overrides (MULTISTAGE_PARAM_OVERRIDE_* prefix) +if [[ -n "${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY:-}" ]]; then + echo "Applying Gangway override: JIRA_AGENT_ISSUE_KEY=${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" + export JIRA_AGENT_ISSUE_KEY="${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" +fi + # Validate required env vars -for required_var in JIRA_AGENT_FORK_REPO JIRA_AGENT_UPSTREAM_REPO JIRA_AGENT_JQL; do +for required_var in JIRA_AGENT_FORK_REPO JIRA_AGENT_UPSTREAM_REPO; do if [ -z "${!required_var:-}" ]; then echo "ERROR: Required env var $required_var is not set" exit 1 fi done - -# Apply Gangway API overrides (MULTISTAGE_PARAM_OVERRIDE_* prefix) -if [[ -n "${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY:-}" ]]; then - echo "Applying Gangway override: JIRA_AGENT_ISSUE_KEY=${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" - export JIRA_AGENT_ISSUE_KEY="${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY}" +if [ -z "${JIRA_AGENT_ISSUE_KEY:-}" ] && [ -z "${JIRA_AGENT_JQL:-}" ]; then + echo "ERROR: JIRA_AGENT_JQL must be set when JIRA_AGENT_ISSUE_KEY is not provided" + exit 1 fi # Derive org name from fork repo slug @@ -252,12 +256,16 @@ transition_issue() { '.transitions[] | select(.name == $status) | .id' | head -1) if [ -n "$TRANSITION_ID" ] && [ "$TRANSITION_ID" != "null" ]; then - curl -s -X POST \ + TRANSITION_HTTP_CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \ "${JIRA_BASE_URL}/rest/api/3/issue/$ISSUE_KEY/transitions" \ -H "Authorization: Basic $JIRA_AUTH" \ -H "Content-Type: application/json" \ - -d "{\"transition\":{\"id\":\"$TRANSITION_ID\"}}" - return 0 + -d "{\"transition\":{\"id\":\"$TRANSITION_ID\"}}") + if [ "$TRANSITION_HTTP_CODE" = "204" ] || [ "$TRANSITION_HTTP_CODE" = "200" ]; then + return 0 + fi + echo " Warning: Jira transition API returned HTTP $TRANSITION_HTTP_CODE" + return 1 else echo " Warning: Transition to '$TARGET_STATUS' not available" return 1 @@ -577,12 +585,13 @@ while IFS= read -r line; do echo "Refreshing GitHub App tokens before Phase 3..." [[ $- == *x* ]] && _REFRESH3_TRACING=true || _REFRESH3_TRACING=false set +x - GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") - if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for fork — continuing with previous token" - else + if _NEW_TOKEN=$(generate_github_token "$INSTALLATION_ID_FORK") \ + && [ -n "$_NEW_TOKEN" ] && [ "$_NEW_TOKEN" != "null" ]; then + GITHUB_TOKEN_FORK="$_NEW_TOKEN" git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" echo "Fork token refreshed" + else + echo "ERROR: Failed to refresh GitHub App token for fork — continuing with previous token" fi $_REFRESH3_TRACING && set -x @@ -655,20 +664,22 @@ IMPORTANT: echo "Refreshing GitHub App tokens before Phase 4..." [[ $- == *x* ]] && _REFRESH4_TRACING=true || _REFRESH4_TRACING=false set +x - GITHUB_TOKEN_FORK=$(generate_github_token "$INSTALLATION_ID_FORK") - if [ -z "$GITHUB_TOKEN_FORK" ] || [ "$GITHUB_TOKEN_FORK" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for fork — continuing with previous token" - else + if _NEW_TOKEN=$(generate_github_token "$INSTALLATION_ID_FORK") \ + && [ -n "$_NEW_TOKEN" ] && [ "$_NEW_TOKEN" != "null" ]; then + GITHUB_TOKEN_FORK="$_NEW_TOKEN" git config --global credential.helper "!f() { echo username=x-access-token; echo password=${GITHUB_TOKEN_FORK}; }; f" echo "Fork token refreshed" + else + echo "ERROR: Failed to refresh GitHub App token for fork — continuing with previous token" fi - GITHUB_TOKEN_UPSTREAM=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") - if [ -z "$GITHUB_TOKEN_UPSTREAM" ] || [ "$GITHUB_TOKEN_UPSTREAM" = "null" ]; then - echo "ERROR: Failed to refresh GitHub App token for upstream — continuing with previous token" - else + if _NEW_TOKEN=$(generate_github_token "$INSTALLATION_ID_UPSTREAM") \ + && [ -n "$_NEW_TOKEN" ] && [ "$_NEW_TOKEN" != "null" ]; then + GITHUB_TOKEN_UPSTREAM="$_NEW_TOKEN" export GITHUB_TOKEN="$GITHUB_TOKEN_UPSTREAM" echo "Upstream token refreshed" + else + echo "ERROR: Failed to refresh GitHub App token for upstream — continuing with previous token" fi $_REFRESH4_TRACING && set -x @@ -732,16 +743,19 @@ IMPORTANT: echo "Phase 4 duration: ${PHASE4_DURATION}s" echo "$PHASE4_DURATION" > "${SHARED_DIR}/claude-${ISSUE_KEY}-pr-duration.txt" + ISSUE_SUCCESS=true if [ $PR_EXIT_CODE -eq 0 ]; then PR_URL=$(grep -o "https://github.com/${JIRA_AGENT_UPSTREAM_REPO}/pull/[0-9]*" "/tmp/claude-${ISSUE_KEY}-pr.json" | head -1 || echo "") if [ -n "$PR_URL" ]; then echo "PR created: $PR_URL" else echo "Phase 4 completed but no PR URL found in output" + ISSUE_SUCCESS=false fi else echo "Phase 4 (PR creation) failed for $ISSUE_KEY (exit code: $PR_EXIT_CODE)" PR_URL="" + ISSUE_SUCCESS=false fi # Append report link to PR description @@ -779,8 +793,8 @@ ${REPORT_SECTION}" echo "No code changes detected for $ISSUE_KEY, skipping review and PR creation" fi - # Add 'agent-processed' label to mark issue as handled - if [ -n "$JIRA_AUTH" ]; then + # Add 'agent-processed' label only when end-to-end processing succeeded + if [ "${ISSUE_SUCCESS:-true}" = true ] && [ -n "$JIRA_AUTH" ]; then echo "Adding 'agent-processed' label to $ISSUE_KEY..." LABEL_RESPONSE=$(curl -s -w "\n%{http_code}" -X PUT \ "${JIRA_BASE_URL}/rest/api/3/issue/$ISSUE_KEY" \ @@ -833,8 +847,13 @@ ${REPORT_SECTION}" fi fi - PROCESSED_COUNT=$((PROCESSED_COUNT + 1)) - echo "$ISSUE_KEY $TIMESTAMP $PR_URL SUCCESS" >> "$STATE_FILE" + if [ "${ISSUE_SUCCESS:-true}" = true ]; then + PROCESSED_COUNT=$((PROCESSED_COUNT + 1)) + echo "$ISSUE_KEY $TIMESTAMP $PR_URL SUCCESS" >> "$STATE_FILE" + else + FAILED_COUNT=$((FAILED_COUNT + 1)) + echo "$ISSUE_KEY $TIMESTAMP - FAILED" >> "$STATE_FILE" + fi else # Log failure but don't mark as processed (will be retried next run) echo "Failed to process $ISSUE_KEY" diff --git a/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh b/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh index 509a8bc75d084..8c090fc24716b 100644 --- a/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh +++ b/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh @@ -58,7 +58,7 @@ format_cost() { sum_costs() { local a=${1:-0} local b=${2:-0} - awk "BEGIN {printf \"%.6f\", $a + $b}" 2>/dev/null || echo "0" + awk -v a="$a" -v b="$b" 'BEGIN {printf "%.6f", (a + b)}' 2>/dev/null || echo "0" } # HTML-escape a string diff --git a/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh index 96208b06b3a4f..145cd3035294f 100644 --- a/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh +++ b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh @@ -7,4 +7,10 @@ echo "=== Jira Agent Setup ===" echo "Verifying Claude Code CLI..." claude --version || { echo "ERROR: Claude Code CLI not found"; exit 1; } +echo "Verifying Vertex AI credentials..." +if [ -z "${GOOGLE_APPLICATION_CREDENTIALS:-}" ] || [ ! -r "${GOOGLE_APPLICATION_CREDENTIALS}" ]; then + echo "ERROR: GOOGLE_APPLICATION_CREDENTIALS is not set or not readable" + exit 1 +fi + echo "Setup complete" From 535e53868793a8e587b92d4a6a36ce9096787d29 Mon Sep 17 00:00:00 2001 From: Bryan Cox <brcox@redhat.com> Date: Tue, 23 Jun 2026 11:44:54 -0400 Subject: [PATCH 5/7] fix: add GitHub SSH host keys before plugin installation The CI pod doesn't have GitHub's ED25519 host key in known_hosts, causing claude plugin install to fail with strict host key checking. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../jira-agent/process/jira-agent-process-commands.sh | 3 +++ 1 file changed, 3 insertions(+) diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh index eef3d691cafeb..caf143b14066e 100644 --- a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh @@ -64,6 +64,9 @@ if [ -n "${JIRA_AGENT_TOOL_SETUP_SCRIPT:-}" ]; then fi export PATH="${GOPATH:-$HOME/go}/bin:$HOME/.local/bin:$PATH" +# Ensure GitHub SSH host keys are trusted (plugin installer uses SSH) +mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null + # Install plugins echo "Installing Claude Code plugins..." claude plugin marketplace add openshift-eng/ai-helpers From 2c2c6f31b26ba510a7e7b7cbe25ea0ff9cee6040 Mon Sep 17 00:00:00 2001 From: Bryan Cox <brcox@redhat.com> Date: Tue, 23 Jun 2026 12:07:36 -0400 Subject: [PATCH 6/7] fix: force HTTPS for git clones in plugin installer claude plugin install clones via SSH as a different user, so ssh-keyscan for the script user doesn't help. Use git insteadOf to rewrite git@github.com: URLs to HTTPS globally. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../jira-agent/process/jira-agent-process-commands.sh | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh index caf143b14066e..c5ccaaae99f5c 100644 --- a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh @@ -65,7 +65,13 @@ fi export PATH="${GOPATH:-$HOME/go}/bin:$HOME/.local/bin:$PATH" # Ensure GitHub SSH host keys are trusted (plugin installer uses SSH) -mkdir -p ~/.ssh && ssh-keyscan github.com >> ~/.ssh/known_hosts 2>/dev/null +# Write to both current user and /home/claude since claude CLI runs as its own user +for ssh_dir in ~/.ssh /home/claude/.ssh; do + mkdir -p "$ssh_dir" 2>/dev/null || true + ssh-keyscan github.com >> "$ssh_dir/known_hosts" 2>/dev/null || true +done +# Fallback: disable strict host key checking for github.com +git config --global url."https://github.com/".insteadOf "git@github.com:" # Install plugins echo "Installing Claude Code plugins..." From 94d6b49be88fa1765bedb3efb727a4418a8e647f Mon Sep 17 00:00:00 2001 From: Bryan Cox <brcox@redhat.com> Date: Tue, 23 Jun 2026 12:50:58 -0400 Subject: [PATCH 7/7] fix: prevent set -euo pipefail from killing script on empty YAML lines The YAML | block scalar for JIRA_AGENT_EXTRA_PLUGIN_COMMANDS adds a trailing newline. The piped while loop ran in a subshell, and when [ -n "" ] && eval "$cmd" evaluated to exit 1 on the empty trailing line, pipefail propagated the non-zero status and set -e killed the script immediately after plugin installation with no output. Fix: - Use heredoc (<<<) instead of pipe to avoid subshell - Use if/fi instead of && so empty lines don't produce exit 1 - Remove redundant ssh-keyscan (insteadOf config alone suffices) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com> --- .../process/jira-agent-process-commands.sh | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh index c5ccaaae99f5c..2def314a3a2dc 100644 --- a/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh @@ -64,13 +64,7 @@ if [ -n "${JIRA_AGENT_TOOL_SETUP_SCRIPT:-}" ]; then fi export PATH="${GOPATH:-$HOME/go}/bin:$HOME/.local/bin:$PATH" -# Ensure GitHub SSH host keys are trusted (plugin installer uses SSH) -# Write to both current user and /home/claude since claude CLI runs as its own user -for ssh_dir in ~/.ssh /home/claude/.ssh; do - mkdir -p "$ssh_dir" 2>/dev/null || true - ssh-keyscan github.com >> "$ssh_dir/known_hosts" 2>/dev/null || true -done -# Fallback: disable strict host key checking for github.com +# Force HTTPS for plugin installs (claude CLI defaults to SSH which lacks host keys in CI) git config --global url."https://github.com/".insteadOf "git@github.com:" # Install plugins @@ -80,9 +74,11 @@ claude plugin marketplace add openshift-eng/ai-helpers # Run any extra plugin setup commands if [ -n "${JIRA_AGENT_EXTRA_PLUGIN_COMMANDS:-}" ]; then echo "Running extra plugin commands..." - echo "$JIRA_AGENT_EXTRA_PLUGIN_COMMANDS" | while IFS= read -r cmd; do - [ -n "$cmd" ] && eval "$cmd" - done + while IFS= read -r cmd; do + if [ -n "$cmd" ]; then + eval "$cmd" + fi + done <<< "$JIRA_AGENT_EXTRA_PLUGIN_COMMANDS" fi cd /tmp/project-repo