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

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
55 changes: 55 additions & 0 deletions .fullsend/harness/review.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
# .fullsend/harness/review.yaml
#
# Composes the upstream fullsend review harness (pinned to agents v0.38.0) and
# adds the local coderabbit-review skill as a complementary finding source.
# v0.38.0 review harness includes all built-in dimensions (correctness, protected
# paths, intent/coherence, challenger pass). This harness extends it to also
# ingest CodeRabbit findings via GitHub comments (S3 pattern) for synthesis.
#
# CodeRabbit findings are mapped with coderabbit-* category prefixes so the
# synthesizer can dedupe and attribute them properly. The built-in review
# dimensions remain authoritative for the verdict.
#
# Trigger: Inherits from base harness (no override needed - /fs-review works
# automatically for generated PRs per https://fullsend.sh/docs/guides/user/bugfix-workflow#slash-commands)
base: https://raw.githubusercontent.com/fullsend-ai/agents/48511880eaea5ef01f80b69ba4f228147611db33/harness/review.yaml#sha256=260bf4939f52ef173911d3d788f85bd62b26802f09711d8dcdcce8dbb24c5f04

allowed_remote_resources: [https://raw.githubusercontent.com/fullsend-ai/agents/]

# Extend base skills with repo-specific coderabbit-review skill
skills:
- skills/coderabbit-review

# Note: Required env vars should be set before running fullsend:
# export PRIOR_REVIEW_SHA=""
# export PRIOR_REVIEW_PROVENANCE="none"
# export REPO_FULL_NAME="openshift/ocm-agent-operator"
# export PR_NUMBER="123"
# export GH_TOKEN="$(gh auth token)"
#
# S2 (CLI mode) - CodeRabbit API key configuration:
# The CODERABBIT_API_KEY must be set as a CI secret in your pipeline
# (OpenShift Prow, Tekton, GitHub Actions, etc.) and exposed to the
# runner-side pre-script that generates coderabbit-findings.json.
# NEVER inject this secret into the sandbox - only the runner-side
# pre-script should have access to it.
#
# Example Tekton secret:
# apiVersion: v1
# kind: Secret
# metadata:
# name: coderabbit-api-key
# data:
# api-key: <base64-encoded-key>
#
# Example pre-script invocation in CI:
# export CODERABBIT_API_KEY="$(cat /path/to/secret)"
# export CODERABBIT_MODE="cli"
# .fullsend/skills/coderabbit-review/scripts/run-coderabbit.sh "$PR_NUMBER" \
# > /tmp/workspace/coderabbit-findings.json
#
# S2 (CLI mode) host_files mapping for CodeRabbit findings:
host_files:
- src: /tmp/workspace/coderabbit-findings.json
dest: /sandbox/workspace/coderabbit-findings.json
optional: true
107 changes: 107 additions & 0 deletions .fullsend/skills/coderabbit-review/SKILL.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,107 @@
---
name: coderabbit-review
description: >-
Ingest CodeRabbit findings for the current PR and map them into FullSend
review findings for synthesis. Use during /fs-review after the built-in
dimension sub-agents return, as a complementary (not replacement) source.
---

# CodeRabbit Review Skill

This skill adds CodeRabbit's AI review as an **extra finding source** for the
FullSend review agent. It does **not** replace `code-review` / `pr-review` —
those own protected-path checks, intent/coherence, the challenger pass, and the
JSON the post-script consumes. This skill only *gathers* CodeRabbit findings and
*maps* them into the same finding shape so the orchestrator can synthesise them.

> Naming: this skill is intentionally named `coderabbit-review` (a novel name).
> A repo skill named `code-review` or `pr-review` would be shadowed by the
> built-in and never invoked (Personal > Project precedence).

## When to use

- During `/fs-review` on a pull request, after the built-in review dimensions
have produced their findings, to fold in CodeRabbit's findings.
- Not for local pre-push (`code-review`) unless CodeRabbit results are already
available for the branch.

## Sources (in priority order)

The CodeRabbit CLI **cannot** run inside the review sandbox (no `coderabbit`
binary, no `curl`, `api.coderabbit.ai` is not in the network allowlist, and no
`CODERABBIT_API_KEY` is injected). So this skill never invokes the CLI directly.
Instead it reads findings that already exist:

1. **Injected file (S2, preferred for production):** if
`/sandbox/workspace/coderabbit-findings.json` exists, read it. A runner-side
pre-script produced it outside the sandbox; the API key never enters the
sandbox.

**CI Setup for S2 mode:**
- Store `CODERABBIT_API_KEY` as a secret in your CI system (OpenShift Prow,
Tekton, GitHub Actions, etc.)
- In your CI pipeline, before the review harness runs, execute the pre-script:
```bash
export CODERABBIT_API_KEY="$(cat /path/to/secret)"
export CODERABBIT_MODE="cli"
.fullsend/skills/coderabbit-review/scripts/run-coderabbit.sh "$PR_NUMBER" \
> /tmp/workspace/coderabbit-findings.json
```
- The `host_files` mapping in `review.yaml` copies this into the sandbox
- **Never** pass the API key into the sandbox environment

2. **GitHub ingest (S3, spike default):** otherwise run
`scripts/run-coderabbit.sh <PR_NUMBER>`, which uses the read-only `gh` client
already available to the review agent to pull CodeRabbit's existing PR review
comments. No extra network, binary, or secret required.

If neither source yields findings, emit a short informational note and continue.
**Do not fail the whole review because CodeRabbit was unavailable.**

## Step 1: Gather CodeRabbit findings

```bash
# Prefer the injected file; fall back to GitHub ingest.
if [ -f /sandbox/workspace/coderabbit-findings.json ]; then
cat /sandbox/workspace/coderabbit-findings.json
else
scripts/run-coderabbit.sh "$PR_NUMBER"
fi
```

The script emits a JSON array of `{source, path, line, body, url}` objects.

## Step 2: Map into FullSend review findings

For each CodeRabbit item, produce a finding object with these fields:

- `severity` — map CodeRabbit's severity to the review scale; default to a
low/`info` severity when CodeRabbit does not state one.
- `category` — prefix with `coderabbit-` (e.g. `coderabbit-correctness`,
`coderabbit-style`) so the challenger / synthesiser can dedupe against the
built-in dimensions.
- `file`, `line` — from the CodeRabbit comment (`null` for PR-level comments).
- `description` — CodeRabbit's finding text.
- `remediation` — CodeRabbit's suggested change, if present.
- `url` — link back to the CodeRabbit comment for human traceability.

## Step 3: Respect repo path exclusions

`.coderabbit.yaml` already excludes `boilerplate/**`, `hack/**`, `vendor/**`,
`**/testdata/**`, and generated `**/zz_generated.*.go`. Drop any CodeRabbit
finding whose `path` matches those globs so the review does not re-flag
generated or vendored code.

## Constraints

- **Do not** write `agent-result.json` / `review-result.json` — `pr-review` is
the sole producer of the schema-valid verdict. This skill only contributes
findings for synthesis.
- **Never** log, echo, or commit `CODERABBIT_API_KEY` or any token.
- Treat CodeRabbit findings as advisory input, not gate decisions — the
built-in dimensions and challenger remain authoritative.

## Related

- `scripts/run-coderabbit.sh` — GitHub ingest (S3) and the runner-only CLI (S2)
mode, with sandbox caveats documented inline.
133 changes: 133 additions & 0 deletions .fullsend/skills/coderabbit-review/scripts/run-coderabbit.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,133 @@
#!/usr/bin/env bash
# Collect CodeRabbit findings for a PR and emit them as JSON on stdout.
#
# Modes:
# comment (default, S3) - read CodeRabbit's existing PR review comments via
# `gh`. Sandbox-safe: uses only the read-only GitHub
# client already available to the review agent. No
# CodeRabbit credentials and no egress to
# coderabbit.ai.
# cli (S2) - run the CodeRabbit CLI for a fresh review. RUNNER
# ONLY. This will FAIL inside the FullSend review
# sandbox: no `coderabbit` binary, no `curl`,
# `api.coderabbit.ai` is not in the network allowlist,
# and CODERABBIT_API_KEY is not injected. Intended to
# run in a trusted pre-script whose output is copied
# into the sandbox via `host_files`.
#
# Usage:
# scripts/run-coderabbit.sh <PR_NUMBER>
# CODERABBIT_MODE=cli scripts/run-coderabbit.sh <PR_NUMBER>
set -euo pipefail

# Emit empty JSON array and exit (for non-fatal failures in comment mode)
emit_empty() {
echo "[]"
exit 0
}

REPO="${CODERABBIT_REPO:-openshift/ocm-agent-operator}"
MODE="${CODERABBIT_MODE:-comment}"
BOT="${CODERABBIT_BOT:-coderabbitai}"

die() { echo "error: $*" >&2; exit 1; }

PR="${1:-}"
[ -n "$PR" ] || die "usage: run-coderabbit.sh <PR_NUMBER>"

case "$MODE" in
comment)
# Sandbox-compatible: uses only gh and node (both allowlisted).
# Failures are non-fatal - emit empty array if CodeRabbit data unavailable.
command -v gh >/dev/null || { echo "warning: gh CLI not found" >&2; emit_empty; }
command -v node >/dev/null || { echo "warning: node not found" >&2; emit_empty; }

# Get current PR head commit to filter stale comments.
head_sha=$(gh api "repos/${REPO}/pulls/${PR}" --jq '.head.sha' 2>/dev/null || true)

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] shell-injection

The $PR variable is interpolated into gh api URL paths without numeric validation. Mitigated by the sandbox environment and gh CLI URL encoding.

Suggested fix: Add validation: [[ "$PR" =~ ^[0-9]+$ ]] || die "PR_NUMBER must be numeric".

if [ -z "$head_sha" ]; then
echo "warning: failed to get PR head commit" >&2
emit_empty
fi

# Fetch review comments and issue comments (non-fatal on failure).
review_raw=$(gh api --paginate "repos/${REPO}/pulls/${PR}/comments" 2>/dev/null || echo "[]")
issue_raw=$(gh api --paginate "repos/${REPO}/issues/${PR}/comments" 2>/dev/null || echo "[]")

# Use node to filter and transform comments (sandbox-compatible).
node -e "
const bot = process.argv[1].toLowerCase();
const headSha = process.argv[2];
const reviewRaw = JSON.parse(process.argv[3]);
const issueRaw = JSON.parse(process.argv[4]);

// Filter review comments: exact bot login, current commit only.
const review = reviewRaw
.filter(c => c.user?.login?.toLowerCase() === bot)
.filter(c => c.commit_id === headSha)
.map(c => ({
source: 'coderabbit',
path: c.path,
line: c.line ?? c.original_line,
body: c.body,
url: c.html_url,
commit_id: c.commit_id
}));

// Filter issue comments: exact bot login.
const issue = issueRaw
.filter(c => c.user?.login?.toLowerCase() === bot)
.map(c => ({
source: 'coderabbit',
path: null,
line: null,
body: c.body,
url: c.html_url
}));

console.log(JSON.stringify([...review, ...issue]));
" "$BOT" "$head_sha" "$review_raw" "$issue_raw"

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] argument-injection

GitHub API response bodies containing untrusted comment text are passed as CLI arguments to node -e. For PRs with many comments, this could exceed ARG_MAX.

Suggested fix: Pipe JSON through stdin instead of command-line arguments.

;;
cli)
# RUNNER ONLY - see header. Blocked inside the review sandbox.
# CODERABBIT_API_KEY must be set as a CI secret and exposed to this pre-script.
# See SKILL.md and review.yaml for CI setup instructions.
command -v coderabbit >/dev/null || die "coderabbit CLI not found (runner-only; use MODE=comment in-sandbox)"
command -v node >/dev/null || die "node not found (needed to normalize CLI output)"
[ -n "${CODERABBIT_API_KEY:-}" ] || die "CODERABBIT_API_KEY not set (must be configured as CI secret; see SKILL.md)"

# Get CLI output (try JSON format first, fall back to plain text parsing if needed).
# Adjust flags based on your installed CLI version's capabilities.
cli_output=$(coderabbit review --format json --pr "$PR" --api-key "$CODERABBIT_API_KEY" 2>/dev/null \
|| coderabbit review --plain --pr "$PR" --api-key "$CODERABBIT_API_KEY" 2>&1 \
|| die "coderabbit CLI failed")

# Normalize CLI output to {source, path, line, body, url}[] format.
node -e "
const raw = process.argv[1];
let findings = [];

try {
// Try parsing as JSON first
const parsed = JSON.parse(raw);
findings = (Array.isArray(parsed) ? parsed : [parsed])
.filter(f => f.file && f.line && f.message)
.map(f => ({
source: 'coderabbit',
path: f.file || f.path || null,
line: f.line || null,
body: f.message || f.body || f.comment || '',
url: f.url || null
}));
} catch (e) {
// Plain text fallback: emit empty array (CLI output not parseable)
console.error('warning: CLI output not in expected JSON format', e.message);
findings = [];
}

console.log(JSON.stringify(findings));
" "$cli_output"
;;
*)
die "unknown CODERABBIT_MODE: $MODE (expected 'comment' or 'cli')"
;;
esac
2 changes: 1 addition & 1 deletion api/v1alpha1/managedfleetnotification_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ type FleetNotification struct {
// References useful for context or remediation - this could be links to documentation, KB articles, etc
References []NotificationReferenceType `json:"references,omitempty"`

// +kubebuilder:validation:Enum={"Debug","Info","Warning","Major","Critical","Error","Fatal"}
// +kubebuilder:validation:Enum={"Debug","Info","Warning","Major","Critical","Error","Fatal","Important","Moderate","Low"}
// Re-use the severity definitation in managednotification_types
Severity NotificationSeverity `json:"severity"`

Expand Down
12 changes: 8 additions & 4 deletions api/v1alpha1/managednotification_types.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,12 +28,16 @@ type NotificationSeverity string

const (
SeverityDebug NotificationSeverity = "Debug"
SeverityWarning NotificationSeverity = "Warning"
SeverityInfo NotificationSeverity = "Info"
SeverityMajor NotificationSeverity = "Major"
SeverityWarning NotificationSeverity = "Warning" // Deprecated: use SeverityModerate

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] deprecation-contract

Deprecation of SeverityWarning, SeverityInfo, and SeverityMajor is communicated only via Go source comments. YAML consumers creating CRs have no visibility into the deprecation.

SeverityInfo NotificationSeverity = "Info" // Deprecated: use SeverityLow
SeverityMajor NotificationSeverity = "Major" // Deprecated: use SeverityImportant
SeverityCritical NotificationSeverity = "Critical"
SeverityError NotificationSeverity = "Error"
SeverityFatal NotificationSeverity = "Fatal"

SeverityImportant NotificationSeverity = "Important"
SeverityModerate NotificationSeverity = "Moderate"
SeverityLow NotificationSeverity = "Low"
)

// +kubebuilder:validation:Pattern=`^https?:\/\/.+$`
Expand All @@ -59,7 +63,7 @@ type Notification struct {
// References useful for context or remediation - this could be links to documentation, KB articles, etc
References []NotificationReferenceType `json:"references,omitempty"`

// +kubebuilder:validation:Enum={"Debug","Info","Warning","Major","Critical","Error","Fatal"}
// +kubebuilder:validation:Enum={"Debug","Info","Warning","Major","Critical","Error","Fatal","Important","Moderate","Low"}
// The severity of the Service Log notification
Severity NotificationSeverity `json:"severity"`

Expand Down
4 changes: 2 additions & 2 deletions api/v1alpha1/managednotification_types_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ var _ = Describe("OCMAgent Controller", func() {
Summary: "Test Summary",
ActiveDesc: "Test Firing",
ResolvedDesc: "Test Resolved",
Severity: "Info",
Severity: "Low",
ResendWait: 1,
},
},
Expand Down Expand Up @@ -82,7 +82,7 @@ var _ = Describe("OCMAgent Controller", func() {
Name: testNotificationNameWrb,
Summary: "Test Summary",
ActiveDesc: "Test Firing",
Severity: "Info",
Severity: "Low",
ResendWait: 1,
},
},
Expand Down
7 changes: 6 additions & 1 deletion controllers/ocmagent/ocmagent_controller.go
Original file line number Diff line number Diff line change
Expand Up @@ -74,13 +74,18 @@ func (r *OcmAgentReconciler) Reconcile(ctx context.Context, request reconcile.Re
// Request object not found, could have been deleted after reconcile request.
// Owned objects are automatically garbage collected. For additional cleanup logic use finalizers.
// Return and don't requeue
reqLogger.V(2).Info("OCMAgent resource not found, marking as absent")

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

[low] scope-creep

The V(2) debug logging additions are orthogonal to the other changes in this PR and should be tracked separately.

localmetrics.UpdateMetricOcmAgentResourceAbsent()
return reconcile.Result{}, nil
}
// Error reading the object - requeue the request.
reqLogger.Error(err, "Failed to retrieve OCMAgent. Will retry on next reconcile.")
reqLogger.Error(err, "Failed to retrieve OCMAgent from API server. Will retry on next reconcile.",
"namespace", request.Namespace, "name", request.Name)
return reconcile.Result{}, err
}
reqLogger.V(2).Info("Successfully retrieved OCMAgent resource",
"replicas", instance.Spec.Replicas,
"fleetMode", instance.Spec.FleetMode)
localmetrics.ResetMetricOcmAgentResourceAbsent()

oaohandler, err := r.OCMAgentHandlerBuilder.New()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ spec:
- Critical
- Error
- Fatal
- Important
- Moderate
- Low
type: string
summary:
description: The summary line of the notification
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,6 +83,9 @@ spec:
- Critical
- Error
- Fatal
- Important
- Moderate
- Low
type: string
summary:
description: The summary line of the Service Log notification
Expand Down
Loading
Loading