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/hypershift-jira-agent-workflow.yaml b/ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml index 42857ce8af563..4b2c39d44e1b6 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,30 @@ 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_TOOL_SETUP_SCRIPT: "claude plugin marketplace add enxebre/ai-scripts && claude plugin install git@enxebre && 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/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-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-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/jira-agent/ONBOARDING.md b/ci-operator/step-registry/jira-agent/ONBOARDING.md new file mode 100644 index 0000000000000..dbe78d20ad691 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/ONBOARDING.md @@ -0,0 +1,251 @@ +# Jira Agent Onboarding Guide + +This guide walks you through setting up the **jira-agent** periodic Prow job for your OpenShift team. The jira-agent automatically picks up Jira issues, solves them using Claude Code, runs code review, addresses findings, creates PRs, and sends Slack notifications. + +## How It Works + +The jira-agent runs as a periodic Prow job with three steps: + +1. **Setup** — Verifies Claude Code CLI and Vertex AI credentials +2. **Process** — For each Jira issue matching your JQL query: + - Phase 1: Runs `/jira:solve` to analyze and fix the issue + - Phase 2: Runs pre-commit code review + - Phase 3: Addresses review findings + - Phase 4: Creates a PR to your upstream repo + - Labels the Jira issue, transitions status, sets assignee, sends Slack notification +3. **Report** — Generates an HTML report with token usage, cost breakdown, and phase output + +Your team creates a **thin workflow YAML** that sets team-specific env vars and references the generic step registry components. No bash scripting required. + +## Prerequisites + +Before starting, you need: + +- [ ] **A GitHub App** installed on both your fork org and upstream repo (for push and PR creation) +- [ ] **A fork organization** on GitHub (e.g., `my-team-community/my-repo`) where the agent pushes branches +- [ ] **Vault secret** synced to OpenShift CI with your credentials (see [Credentials Setup](#credentials-setup)) +- [ ] **Vertex AI access** via a Google Cloud service account (for Claude Code) +- [ ] **Jira labels** on issues you want the agent to process (e.g., `issue-for-agent`) +- [ ] **(Optional)** Slack incoming webhook for PR notifications + +## Step 1: Create Your Workflow YAML + +Create a workflow file in `openshift/release` at: +``` +ci-operator/step-registry//jira-agent/-jira-agent-workflow.yaml +``` + +Here's a template — replace the values with your team's configuration: + +```yaml +workflow: + as: -jira-agent + steps: + pre: + - ref: jira-agent-setup + test: + - ref: jira-agent-process + post: + - ref: jira-agent-report + env: + # Required: your fork and upstream repos + JIRA_AGENT_FORK_REPO: "/" + JIRA_AGENT_UPSTREAM_REPO: "openshift/" + + # Required: JQL query to find issues for the agent + JIRA_AGENT_JQL: 'project = OCPBUGS AND resolution = Unresolved AND status in (New, "To Do") AND labels = issue-for-agent AND labels != agent-processed' + + # Optional: transition issues to a status after processing + JIRA_AGENT_TARGET_STATUS: '{"OCPBUGS":"ASSIGNED"}' + + # Optional: set assignee on processed issues + JIRA_AGENT_ASSIGNEE: "my-team-automation" + + # Optional: credential key names in your Vault secret + JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY: "upstream-installation-id" + JIRA_AGENT_FORK_INSTALLATION_ID_KEY: "fork-installation-id" + + # Optional: project-specific tool/plugin setup (runs before processing) + JIRA_AGENT_TOOL_SETUP_SCRIPT: "GOFLAGS='' go install golang.org/x/tools/gopls@latest" + + # Optional: code review configuration + JIRA_AGENT_REVIEW_LANGUAGE: "go" + JIRA_AGENT_REVIEW_PROFILE: "" + + # Optional: Slack notification emoji + JIRA_AGENT_SLACK_EMOJI: ":robot:" + + documentation: |- + -specific wrapper for the generic Jira Agent workflow. + Credentials: Uses (configured in generic step refs). +``` + +## Step 2: Create the Periodic Job Config + +Create a CI config file in `openshift/release` at: +``` +ci-operator/config/openshift//openshift--main__periodics.yaml +``` + +Example: + +```yaml +base_images: + cli: + name: "4.18" + namespace: ocp + tag: cli +build_root: + image_stream_tag: + name: release + namespace: openshift + tag: golang-1.23 +tests: +- as: periodic-jira-agent + cluster_claim: + architecture: amd64 + cloud: aws + owner: hypershift + product: ocp + timeout: 2h0m0s + version: "4.18" + cron: 0 */4 * * 1-5 + steps: + workflow: -jira-agent +zz_generated_metadata: + branch: main + org: openshift + repo: + variant: periodics +``` + +After creating these files, run: +```bash +make update +make validate-step-registry +``` + +## Credentials Setup + +The jira-agent reads credentials from `/var/run/claude-code-service-account/`. Your Vault secret must contain these keys: + +| Key | Description | +|-----|-------------| +| `app-id` | GitHub App ID | +| `private-key` | GitHub App private key (PEM format) | +| `` | Installation ID for your fork org (default key: `installation-id`) | +| `` | Installation ID for upstream repo (default key: `o-h-installation-id`) | +| `jira-email` | Jira account email for API access | +| `jira-pat` | Jira API token (personal access token) | +| `slack-webhook-url` | **(Optional)** Slack incoming webhook URL | +| `gh-to-slack-ids` | **(Optional)** JSON mapping of GitHub usernames to Slack user IDs | + +### GitHub App Setup + +1. Create a GitHub App at https://github.com/settings/apps +2. Grant permissions: `Contents: Read & write`, `Pull requests: Read & write`, `Metadata: Read-only` +3. Install the app on your fork organization and your upstream repository +4. Note the installation IDs for each (visible in the app settings URL after installation) +5. Download the private key + +### Vault Secret + +Store your credentials in Vault under a collection accessible by OpenShift CI. The generic step registry refs declare the secret mount; your workflow overrides the secret name. + +See [OpenShift CI Secret Management](https://docs.ci.openshift.org/docs/how-tos/adding-a-new-secret-to-ci/) for details on syncing secrets to CI. + +### GitHub-to-Slack Mapping + +The `gh-to-slack-ids` file is a JSON object mapping GitHub usernames to Slack member IDs. Include a `backup-user` key for fallback when no reviewers are assigned: + +```json +{ + "github-user-1": "U01ABCDEF", + "github-user-2": "U02GHIJKL", + "backup-user": "U03MNOPQR" +} +``` + +Find Slack member IDs by viewing a user's profile in Slack and clicking "Copy member ID". + +## Environment Variable Reference + +| Variable | Required | Default | Description | +|----------|----------|---------|-------------| +| `JIRA_AGENT_FORK_REPO` | Yes | — | Fork repo slug (e.g., `my-org/my-repo`) | +| `JIRA_AGENT_UPSTREAM_REPO` | Yes | — | Upstream repo slug (e.g., `openshift/my-repo`) | +| `JIRA_AGENT_JQL` | Yes* | — | JQL query for finding issues (*not required if `JIRA_AGENT_ISSUE_KEY` is set) | +| `JIRA_AGENT_ISSUE_KEY` | No | — | Process a specific issue instead of running JQL | +| `JIRA_AGENT_TARGET_STATUS` | No | `""` | JSON map of project prefix to target status | +| `JIRA_AGENT_ASSIGNEE` | No | `""` | Display name to search when setting assignee | +| `JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY` | No | `o-h-installation-id` | Key name in secret for upstream GitHub App installation ID | +| `JIRA_AGENT_FORK_INSTALLATION_ID_KEY` | No | `installation-id` | Key name in secret for fork GitHub App installation ID | +| `JIRA_AGENT_TOOL_SETUP_SCRIPT` | No | `""` | Shell commands to install project-specific tools or plugins | +| `JIRA_AGENT_REVIEW_LANGUAGE` | No | `go` | Language for the code-review plugin | +| `JIRA_AGENT_REVIEW_PROFILE` | No | `""` | Profile for the code-review plugin | +| `JIRA_AGENT_SLACK_EMOJI` | No | `:robot:` | Slack message emoji prefix | +| `JIRA_AGENT_MAX_ISSUES` | No | `1` | Maximum issues to process per run | +| `CLAUDE_MODEL` | No | `claude-opus-4-6` | Claude model to use | +| `JIRA_BASE_URL` | No | `https://redhat.atlassian.net` | Jira instance base URL | + +## Jira Setup + +### Labels + +The agent uses labels to track which issues have been processed: + +- **`issue-for-agent`** — Add this label to issues you want the agent to pick up +- **`agent-processed`** — The agent adds this label after processing (prevents re-processing) + +Your JQL query should include `labels = issue-for-agent AND labels != agent-processed` to implement this pattern. + +### Security Level + +Make sure your Jira issues are accessible to the service account. If issues have restricted security levels, the agent's API token must have access to that level. Issues with security levels the agent can't see will silently be excluded from JQL results. + +## Troubleshooting + +### "No issues found" + +- Check that your JQL query returns results in the Jira UI +- Verify the Jira API token has access to the project and security level +- Ensure issues have the `issue-for-agent` label (or whatever your JQL filters for) + +### "Required credentials are missing" + +- Verify your Vault secret is synced to the CI namespace +- Check that the key names in your secret match `JIRA_AGENT_FORK_INSTALLATION_ID_KEY` and `JIRA_AGENT_UPSTREAM_INSTALLATION_ID_KEY` +- Required keys: `app-id`, `private-key`, fork installation ID, upstream installation ID + +### "Failed to generate GitHub App token" + +- Verify the GitHub App is installed on the target org/repo +- Check that the installation ID is correct (not the app ID) +- Ensure the private key matches the app + +### Plugin installation fails + +- The process script forces HTTPS for git operations (`git config --global url."https://github.com/".insteadOf "git@github.com:"`) +- If you see SSH-related errors, check that this config is applied before plugin installs + +### PR creation fails + +- Verify the GitHub App has `Pull requests: Read & write` permission on the upstream repo +- Check that the fork is synced with upstream (the agent does this automatically) +- Ensure the branch name doesn't conflict with an existing branch + +### Rehearsal Testing + +To test your job in a PR to `openshift/release`, trigger a rehearsal with the full job name: + +``` +/pj-rehearse periodic-ci-openshift--main-periodic-jira-agent +``` + +Never run bare `/pj-rehearse` — always specify the full job name. + +## Example: HyperShift Configuration + +For a working example, see the HyperShift jira-agent workflow: +- Workflow YAML: `ci-operator/step-registry/hypershift/jira-agent/hypershift-jira-agent-workflow.yaml` +- Periodic config: `ci-operator/config/openshift/hypershift/openshift-hypershift-main__periodics.yaml` 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..ac05e086d9d66 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/README.md @@ -0,0 +1,79 @@ +# 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' +``` + +**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 + +| 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_TOOL_SETUP_SCRIPT` | `""` | Inline shell for project-specific tool/plugin 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-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/hypershift` docs for full setup instructions. diff --git a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.metadata.json b/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json similarity index 72% rename from ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.metadata.json rename to ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json index 59e74a6fdf8fb..9c077f1c492e8 100644 --- a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.metadata.json +++ b/ci-operator/step-registry/jira-agent/jira-agent-workflow.metadata.json @@ -1,5 +1,5 @@ { - "path": "hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml", + "path": "jira-agent/jira-agent-workflow.yaml", "owners": { "approvers": [ "bryan-cox", 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/hypershift/jira-agent/process/hypershift-jira-agent-process-commands.sh b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh old mode 100755 new mode 100644 similarity index 65% rename from ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-commands.sh rename to ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh index fee7b43f3bec0..35a6ef6b59531 --- a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-commands.sh +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-commands.sh @@ -1,7 +1,7 @@ #!/bin/bash set -euo pipefail -echo "=== HyperShift Jira Agent Process ===" +echo "=== Jira Agent Process ===" # Apply Gangway API overrides (MULTISTAGE_PARAM_OVERRIDE_* prefix) if [[ -n "${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY:-}" ]]; then @@ -9,54 +9,74 @@ if [[ -n "${MULTISTAGE_PARAM_OVERRIDE_JIRA_AGENT_ISSUE_KEY:-}" ]]; then 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; do + if [ -z "${!required_var:-}" ]; then + echo "ERROR: Required env var $required_var is not set" + exit 1 + fi +done +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 +FORK_ORG="${JIRA_AGENT_FORK_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:-}" +export 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 +# 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:" -# 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 +# Install the openshift-developer bundle (pulls in jira, ci, code-review, golang, git plugins) +echo "Installing Claude Code plugins..." +claude plugin marketplace add openshift-eng/ai-helpers +claude plugin install openshift-developer@ai-helpers + +# Resolve plugin install path for sourcing shell scripts +AI_HELPERS_DIR=$(claude plugin list --json 2>/dev/null \ + | jq -r '.[] | select(.name == "ci") | .path' 2>/dev/null) || true +if [[ -z "$AI_HELPERS_DIR" ]] || [[ ! -d "$AI_HELPERS_DIR" ]]; then + echo "Falling back to cloning ai-helpers for shell scripts..." + git clone --depth 1 https://github.com/openshift-eng/ai-helpers /tmp/ai-helpers + AI_HELPERS_DIR="/tmp/ai-helpers/plugins/ci" +fi -# 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 +# Source reusable CI shell scripts (bash functions, not Claude skills) +source "${AI_HELPERS_DIR}/skills/github-app-auth/github-app-auth.sh" +source "${AI_HELPERS_DIR}/skills/slack-pr-notify/slack-pr-notify.sh" -# 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" +# 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 -# 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 +# 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" +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 -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 +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 openshift/hypershift..." -git remote add upstream https://github.com/openshift/hypershift.git +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 @@ -67,11 +87,11 @@ 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" +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}/o-h-installation-id" +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}" @@ -80,67 +100,53 @@ if [ ! -f "$APP_ID_FILE" ] || [ ! -f "$INSTALLATION_ID_FILE" ] || [ ! -f "$PRIVA 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 " - ${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 + echo "ERROR: Required credentials are missing. Re-run once secrets are synced." + echo "no_credentials" > "${SHARED_DIR}/processed-issues.txt" + exit 1 fi -APP_ID=$(cat "$APP_ID_FILE") +# Disable tracing for credential handling +[[ $- == *x* ]] && _TOKEN_WAS_TRACING=true || _TOKEN_WAS_TRACING=false +set +x + 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_github_token() is provided by the ci plugin's github-app-auth.sh -# Generate token for fork (hypershift-community/hypershift) - for pushing branches +# 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" + $_TOKEN_WAS_TRACING && set -x exit 1 fi echo "Fork token generated successfully" -# Generate token for upstream (openshift/hypershift) - for creating PRs +# 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" + $_TOKEN_WAS_TRACING && set -x 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" +$_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" @@ -151,6 +157,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") @@ -162,18 +170,20 @@ 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" [[ $- == *x* ]] && _SLACK_WAS_TRACING=true || _SLACK_WAS_TRACING=false set +x if [ -f "$SLACK_WEBHOOK_FILE" ]; then + export SLACK_WEBHOOK_URL 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="" + export SLACK_WEBHOOK_URL="" fi $_SLACK_WAS_TRACING && set -x @@ -187,18 +197,11 @@ if [ -f "$GITHUB_SLACK_MAP_FILE" ]; then echo "Reviewer pings will use GitHub usernames instead of Slack mentions" GITHUB_SLACK_MAP="{}" fi + export GITHUB_SLACK_MAP 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" + export GITHUB_SLACK_MAP="{}" fi # Function to transition a Jira issue to a target status @@ -208,7 +211,7 @@ transition_issue() { # Get available transitions TRANSITIONS=$(curl -s \ - "https://redhat.atlassian.net/rest/api/3/issue/$ISSUE_KEY/transitions" \ + "${JIRA_BASE_URL}/rest/api/3/issue/$ISSUE_KEY/transitions" \ -H "Authorization: Basic $JIRA_AUTH" \ -H "Content-Type: application/json") @@ -217,12 +220,16 @@ transition_issue() { '.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" \ + 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 @@ -235,104 +242,13 @@ set_assignee() { local ACCOUNT_ID=$2 curl -s -w "\n%{http_code}" -X PUT \ - "https://redhat.atlassian.net/rest/api/3/issue/$ISSUE_KEY/assignee" \ + "${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 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 -} +# send_slack_notification() is provided by the ci plugin's slack-pr-notify.sh # Query Jira for issues (excluding already processed ones via label) echo "Querying Jira for issues..." @@ -340,11 +256,11 @@ 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' + 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}" "https://redhat.atlassian.net/rest/api/3/search/jql" \ +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" \ @@ -396,19 +312,23 @@ while IFS= read -r line; do 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) + # Load the jira-solve command content as system prompt (installed via openshift-developer bundle) + JIRA_PLUGIN_DIR=$(claude plugin list --json 2>/dev/null \ + | jq -r '.[] | select(.name == "jira") | .path' 2>/dev/null) || true + if [[ -z "$JIRA_PLUGIN_DIR" ]] || [[ ! -f "${JIRA_PLUGIN_DIR}/commands/solve.md" ]]; then + echo "ERROR: jira plugin solve.md not found — is openshift-developer bundle installed?" + exit 1 + fi + SKILL_CONTENT=$(cat "${JIRA_PLUGIN_DIR}/commands/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}" + 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..." @@ -450,7 +370,7 @@ while IFS= read -r line; do 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" + 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) @@ -475,12 +395,14 @@ while IFS= read -r line; do PHASE2_START=$(date +%s) - REVIEW_PROMPT="/code-review:pre-commit-review --language go --profile hypershift" + 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}" \ + --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 \ @@ -518,9 +440,9 @@ while IFS= read -r line; do 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" + 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 "Phase 2 (pre-commit review) failed for $ISSUE_KEY (exit code: $REVIEW_EXIT_CODE)" echo "Continuing with PR creation despite review failure..." fi @@ -538,15 +460,18 @@ while IFS= read -r line; do 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 + [[ $- == *x* ]] && _REFRESH3_TRACING=true || _REFRESH3_TRACING=false + set +x + 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 PHASE3_START=$(date +%s) @@ -561,7 +486,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 @@ -599,9 +524,9 @@ IMPORTANT: 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" + echo "Phase 3 (address review) completed for $ISSUE_KEY" else - echo "⚠️ Phase 3 (address review) failed (exit code: $FIX_EXIT_CODE)" + echo "Phase 3 (address review) failed (exit code: $FIX_EXIT_CODE)" echo "Continuing with PR creation..." fi else @@ -614,24 +539,27 @@ IMPORTANT: 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 + [[ $- == *x* ]] && _REFRESH4_TRACING=true || _REFRESH4_TRACING=false + set +x + 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" - 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 # === Phase 4: Create Pull Request === echo "" @@ -641,19 +569,9 @@ IMPORTANT: 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}" + PR_PROMPT="/openshift-developer:create-pr ${ISSUE_KEY} --upstream ${JIRA_AGENT_UPSTREAM_REPO} --head ${FORK_ORG}:${BRANCH_NAME} +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 claude -p "$PR_PROMPT" \ @@ -693,16 +611,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/openshift/hypershift/pull/[0-9]*' "/tmp/claude-${ISSUE_KEY}-pr.json" | head -1 || echo "") + 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" + echo "PR created: $PR_URL" else - echo "⚠️ Phase 4 completed but no PR URL found in output" + 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)" + 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 @@ -712,22 +633,22 @@ IMPORTANT: 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" + 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/hypershift-jira-agent-report/artifacts/jira-agent-report.html" + 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 openshift/hypershift --json body -q .body 2>/dev/null || echo "") + 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](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." +> **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 openshift/hypershift --body "$UPDATED_BODY" 2>/dev/null || echo "Warning: Failed to update PR #${PR_NUM} description" + 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 @@ -740,11 +661,11 @@ ${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 \ - "https://redhat.atlassian.net/rest/api/3/issue/$ISSUE_KEY" \ + "${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"}]}}') @@ -755,48 +676,55 @@ ${REPORT_SECTION}" 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" + # 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 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 + # 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 + 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" + 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" + 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)) diff --git a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.metadata.json b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json similarity index 72% rename from ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.metadata.json rename to ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json index 7b678563c9443..1b0e52b803db0 100644 --- a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-ref.metadata.json +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.metadata.json @@ -1,5 +1,5 @@ { - "path": "hypershift/jira-agent/report/hypershift-jira-agent-report-ref.yaml", + "path": "jira-agent/process/jira-agent-process-ref.yaml", "owners": { "approvers": [ "bryan-cox", 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..732e8619d7fd3 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/process/jira-agent-process-ref.yaml @@ -0,0 +1,113 @@ +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_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/hypershift/jira-agent/report/hypershift-jira-agent-report-commands.sh b/ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh old mode 100755 new mode 100644 similarity index 95% rename from ci-operator/step-registry/hypershift/jira-agent/report/hypershift-jira-agent-report-commands.sh rename to ci-operator/step-registry/jira-agent/report/jira-agent-report-commands.sh index ed1bebcd86cea..8c090fc24716b --- a/ci-operator/step-registry/hypershift/jira-agent/report/hypershift-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 @@ -95,6 +95,8 @@ format_duration() { fi } +JIRA_BASE_URL="${JIRA_BASE_URL:-https://redhat.atlassian.net}" + # Build issue rows for summary table and detail sections SUMMARY_ROWS="" DETAIL_SECTIONS="" @@ -199,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) @@ -220,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 @@ -256,11 +258,11 @@ while IFS= read -r line; do 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>" + 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=\"https://redhat.atlassian.net/browse/${ISSUE_KEY}\">${ISSUE_KEY}</a> <span class=\"status ${STATUS_CLASS}\">${STATUS_LABEL}</span></h2> + <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> diff --git a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.metadata.json b/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json similarity index 71% rename from ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.metadata.json rename to ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json index 22d12984f05fb..e2b373761e895 100644 --- a/ci-operator/step-registry/hypershift/jira-agent/process/hypershift-jira-agent-process-ref.metadata.json +++ b/ci-operator/step-registry/jira-agent/report/jira-agent-report-ref.metadata.json @@ -1,5 +1,5 @@ { - "path": "hypershift/jira-agent/process/hypershift-jira-agent-process-ref.yaml", + "path": "jira-agent/report/jira-agent-report-ref.yaml", "owners": { "approvers": [ "bryan-cox", 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/hypershift/jira-agent/setup/hypershift-jira-agent-setup-commands.sh b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh old mode 100755 new mode 100644 similarity index 51% rename from ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-commands.sh rename to ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh index 94cc7c5d99139..145cd3035294f --- a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-commands.sh +++ b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-commands.sh @@ -1,10 +1,16 @@ #!/bin/bash set -euo pipefail -echo "=== HyperShift Jira Agent Setup ===" +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 "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" 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..7615067e833c7 --- /dev/null +++ b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.metadata.json @@ -0,0 +1,19 @@ +{ + "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 diff --git a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.yaml similarity index 67% rename from ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml rename to ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.yaml index 7aa6338f82932..60c5d35fe39de 100644 --- a/ci-operator/step-registry/hypershift/jira-agent/setup/hypershift-jira-agent-setup-ref.yaml +++ b/ci-operator/step-registry/jira-agent/setup/jira-agent-setup-ref.yaml @@ -1,7 +1,7 @@ ref: - as: hypershift-jira-agent-setup + as: jira-agent-setup from: claude-ai-helpers - commands: hypershift-jira-agent-setup-commands.sh + commands: jira-agent-setup-commands.sh env: - name: CLAUDE_CODE_USE_VERTEX default: "1" @@ -28,10 +28,7 @@ ref: 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 + 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.