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
32 changes: 32 additions & 0 deletions .github/pull_request_template.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
### Summary

### Risk & Scope
- [ ] Low risk
- [ ] Medium risk
- [ ] High risk (requires ai:required or risk:high label)

### Architecture Compliance
- [ ] No transport logic added in React components
- [ ] No server-only modules imported into client code
- [ ] Service/store boundaries preserved (service -> store -> hooks -> components)

### Service/Store Impact
- [ ] Touches service layer (list files):
- [ ] Touches state store/reducer (list files):
- [ ] Migration/backward compatibility considered

### Auth/Security
- [ ] Affects auth/session/token flow
- [ ] WebSocket upgrade/auth assumptions reviewed
- [ ] No secrets introduced in code/config/logs

### Testing
- [ ] Lint/type/knip pass locally
- [ ] Unit tests updated
- [ ] Integration tests updated
- [ ] VRT/E2E impact assessed

### Accessibility
- [ ] Realtime announcements use correct aria-live strategy
- [ ] No high-frequency screen-reader spam introduced
- [ ] Contrast/accessibility checks considered
76 changes: 74 additions & 2 deletions .github/scripts/jules_ops.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,22 @@
import requests
import argparse

def create_jules_session(prompt, branch, title, owner, repo_name, jules_api_url):
def print_summary(summary):
"""
Prints a structured summary to GITHUB_STEP_SUMMARY.
"""
if 'GITHUB_STEP_SUMMARY' in os.environ:
with open(os.environ['GITHUB_STEP_SUMMARY'], 'a') as f:
f.write("### 🤖 Jules Operation Summary\n")
f.write(f"- **Mode:** `{summary['mode']}`\n")
f.write(f"- **Issue Created:** {'✅' if summary['issue_created'] else '❌'}\n")
f.write(f"- **PR Created:** {'✅' if summary['pr_created'] else '❌'}\n")
if summary['skipped_reason']:
f.write(f"- **Skipped Reason:** `{summary['skipped_reason']}`\n")
else:
print(f"Summary: {summary}")

def create_jules_session(prompt, branch, title, owner, repo_name, jules_api_url, mode="audit"):
"""
Creates a new Jules session via the API and returns the session ID.
"""
Expand All @@ -24,6 +39,7 @@ def create_jules_session(prompt, branch, title, owner, repo_name, jules_api_url)
"title": title,
"owner": owner,
"repo_name": repo_name,
"mode": mode,
}

try:
Expand Down Expand Up @@ -85,21 +101,77 @@ def main():
parser.add_argument("--owner", help="The owner of the repository.")
parser.add_argument("--repo-name", help="The name of the repository.")
parser.add_argument("--jules-api-url", default="https://api.jules.ai/v1/sessions", help="The URL of the Jules API.")
parser.add_argument("--mode", choices=['audit', 'direct'], default='audit', help="The operation mode.")
parser.add_argument("--direct", action="store_true", help="Alias for --mode direct.")
parser.add_argument("--allow-risk-paths", action="store_true", help="Allow direct mode on high-risk paths.")
parser.add_argument("--deterministic-passed", default="false", help="Whether deterministic checks passed.")
parser.add_argument("--changed-files", help="Comma-separated list of changed files.")

args = parser.parse_args()

mode = args.mode
if args.direct:
mode = 'direct'

if args.command == 'new':
if not all([args.prompt, args.branch, args.title, args.owner, args.repo_name]):
sys.stderr.write("Error: --prompt, --branch, --title, --owner, and --repo-name are required for the 'new' command.\n")
sys.exit(1)

summary = {
"mode": mode,
"issue_created": mode == "audit",
"pr_created": mode == "direct",
"skipped_reason": None
}

# Safety gates for direct mode
if mode == 'direct':
deterministic_passed = str(args.deterministic_passed).strip().lower() == "true"
if not deterministic_passed:
summary["skipped_reason"] = "deterministic_failed"
print_summary(summary)
sys.stderr.write("Error: Direct mode blocked on deterministic failure.\n")
sys.exit(1)

if not args.changed_files and not args.allow_risk_paths:
summary["skipped_reason"] = "missing_changed_files"
print_summary(summary)
sys.stderr.write(
"Error: Direct mode blocked. --changed-files is required unless --allow-risk-paths is provided.\n"
)
sys.exit(1)

# High-risk path detection
if args.changed_files and not args.allow_risk_paths:
risk_paths = [
"server.ts", "middleware.ts",
"context/WebSocketContext.tsx", "context/webSocketReducer.ts",
"hooks/useBluetoothHRM.ts", ".github/workflows/",
"package.json", "pnpm-lock.yaml"
]
changed_files = args.changed_files.split(',')
for cf in changed_files:
cf = cf.strip()
for rp in risk_paths:
if cf == rp or (rp.endswith('/') and cf.startswith(rp)):
summary["skipped_reason"] = "risk_path_touched"
print_summary(summary)
sys.stderr.write(f"Error: Direct mode blocked. Risk path touched: {cf}. Use --allow-risk-paths to override.\n")
sys.exit(1)

session_id = create_jules_session(
prompt=args.prompt,
branch=args.branch,
title=args.title,
owner=args.owner,
repo_name=args.repo_name,
jules_api_url=args.jules_api_url
jules_api_url=args.jules_api_url,
mode=mode
)

print_summary(summary)

if 'GITHUB_OUTPUT' in os.environ:
with open(os.environ['GITHUB_OUTPUT'], 'a') as f:
f.write(f"session_id={session_id}\n")
Expand Down
157 changes: 62 additions & 95 deletions .github/scripts/manage-pr-labels.sh
Original file line number Diff line number Diff line change
@@ -1,141 +1,108 @@
#!/bin/bash
set -e # Exit immediately if a command exits with a non-zero status.
set -o pipefail # Return value of a pipeline is the value of the last command to exit with a non-zero status
# .github/scripts/manage-pr-labels.sh
# Robust label management with retries and graceful handling of missing review results

# Logging functions
# To enable debug logging (using the debug() function), set the DEBUG environment variable to "true".
log() { echo "$*"; }
warn() { echo "::warning::$*"; }
error() { echo "::error::$*"; exit 1; }
debug() { if [ "$DEBUG" = "true" ]; then echo "::debug::$*"; fi; }
group() { echo "::group::$1"; }
endgroup() { echo "::endgroup::"; }

# Check for required environment variables
if [ -z "$GH_TOKEN" ] || [ -z "$PR_NUMBER" ]; then
error "GH_TOKEN and PR_NUMBER environment variables are required."
fi

# =================================================================
# Ensure all managed labels exist in the repository
# =================================================================
# Helper function to ensure a label exists
# Function to ensure a label exists with retries
ensure_label_exists() {
local name=$1
local description=$2
local color=$3

# Trim whitespace
local clean_name=$(echo "$name" | xargs)
if [ -z "$clean_name" ]; then return; fi

# GitHub labels are case-insensitive, so we use grep -i for the check.
# We assume EXISTING_LABELS is populated in the calling scope.
if echo "$EXISTING_LABELS" | grep -iFxq -- "$clean_name"; then
debug "Label '$clean_name' already exists."
else
log "Creating label '$clean_name'..."
# We capture errors and check for "already exists" specifically,
# providing a safety net if the existence check missed a label (e.g. due to race conditions).
local cmd=("gh" "label" "create" "$clean_name")
if [ -n "$description" ]; then cmd+=("--description" "$description"); fi
if [ -n "$color" ]; then cmd+=("--color" "$color"); fi

set +e
local error_msg
error_msg=$("${cmd[@]}" 2>&1)
local exit_code=$?
set -e
if echo "$EXISTING_LABELS" | grep -iFxq -- "$clean_name" > /dev/null; then
return
fi

if [ $exit_code -ne 0 ] && ! echo "$error_msg" | grep -qi "already exists"; then
error "Failed to create label '$clean_name': $error_msg"
log "Creating label '$clean_name'..."
for i in {1..3}; do
if gh label create "$clean_name" ${description:+--description "$description"} ${color:+--color "$color"} 2>/dev/null; then
return
fi
fi
if gh label list --limit 1000 --json name --jq '.[].name' | grep -iFxq -- "$clean_name" > /dev/null; then
return
fi
warn "Attempt $i to create label '$clean_name' failed, retrying..."
sleep 5
done
}

# =================================================================
# Ensure all managed labels exist in the repository
# =================================================================
group "Ensuring all managed labels exist"
# Get all existing labels once to avoid redundant API calls.
# We strip quotes and carriage returns to ensure reliable matching regardless of gh version or environment.
EXISTING_LABELS=$(gh label list --limit 1000 --json name --jq '.[].name' | tr -d '"\r')
jq -r '.[] | .name + "|" + .description + "|" + .color' .github/pr-labels.json | while IFS='|' read -r name description color; do
ensure_label_exists "$name" "$description" "$color"
# Get existing labels with retry
for i in {1..3}; do
EXISTING_LABELS=$(gh label list --limit 1000 --json name --jq '.[].name' 2>/dev/null | tr -d '"\r') && break || sleep 5
done
endgroup

# =================================================================
# Proceed with label management on the PR
# =================================================================

# Extract new labels from the review result JSON
NEW_LABELS=""
if [ -f "review_result.json" ] && [ "$(jq 'has("labels")' review_result.json)" == "true" ]; then
NEW_LABELS=$(jq -r '.labels | .[]' review_result.json | tr '\n' ',' | sed 's/,$//')
fi

# Determine if a status label (approved/not approved/not reviewed) is present
HAS_STATUS_LABEL=false
if echo "$NEW_LABELS" | grep -qE "approved|not approved|not reviewed"; then
HAS_STATUS_LABEL=true
if [ -f ".github/pr-labels.json" ]; then
while IFS='|' read -r name description color; do
ensure_label_exists "$name" "$description" "$color"
done < <(jq -r '.[] | .name + "|" + .description + "|" + .color' .github/pr-labels.json)
fi
endgroup

# If no status label is present, and we are forced to have one, we need to decide.
# If review_result.json is missing or review was skipped, use 'not reviewed'.
if [ "$HAS_STATUS_LABEL" = false ]; then
if [ ! -f "review_result.json" ] || [ "$NEEDS_REVIEW" = "false" ]; then
if [ -n "$NEW_LABELS" ]; then
NEW_LABELS="$NEW_LABELS,not reviewed"
else
NEW_LABELS="not reviewed"
fi
else
# This shouldn't happen with the updated gemini-client.ts, but as a fallback:
warn "No status label found in review result. Defaulting to 'not reviewed'."
if [ -n "$NEW_LABELS" ]; then
NEW_LABELS="$NEW_LABELS,not reviewed"
else
NEW_LABELS="not reviewed"
fi
NEW_LABELS=""
if [ -f "review_result.json" ]; then
if [ "$(jq 'has("labels")' review_result.json 2>/dev/null)" == "true" ]; then
NEW_LABELS=$(jq -r '.labels | .[]' review_result.json | tr '\n' ',' | sed 's/,$//')
fi
fi

# Get the list of managed labels from the pr-labels.json file
MANAGED_LABELS=$(jq -r '.[].name' .github/pr-labels.json)

# Get current labels on the PR
CURRENT_LABELS=$(gh pr view $PR_NUMBER --json labels --jq '.labels[].name')
# Determine if a status label is present
if ! echo "$NEW_LABELS" | grep -qE "approved|not approved|not reviewed"; then
NEW_LABELS="${NEW_LABELS:+$NEW_LABELS,}not reviewed"
fi

group "Label Details for PR #$PR_NUMBER"
log "Current labels on PR:"
log "$CURRENT_LABELS"
log "---"
log "All managed labels (from .github/pr-labels.json):"
debug "$MANAGED_LABELS"
log "---"
log "New labels to apply from Gemini review:"
log "$NEW_LABELS"
# Retry PR view
for i in {1..3}; do
CURRENT_LABELS=$(gh pr view "$PR_NUMBER" --json labels --jq '.labels[].name' 2>/dev/null) && break || sleep 5
done
log "Current labels on PR: $CURRENT_LABELS"
log "New labels to apply: $NEW_LABELS"
endgroup

# Call the universal cleanup script to remove automated review and obsolete labels.
# This script is located at scripts/ci/cleanup-pr-labels.sh
group "Cleaning up automated review and obsolete labels"
GH_TOKEN="$GH_TOKEN" ./scripts/ci/cleanup-pr-labels.sh "$PR_NUMBER" review
if [ -x "./scripts/ci/cleanup-pr-labels.sh" ]; then
for i in {1..3}; do
if GH_TOKEN="$GH_TOKEN" ./scripts/ci/cleanup-pr-labels.sh "$PR_NUMBER" review; then
break
fi
warn "Cleanup attempt $i failed, retrying..."
sleep 5
done
fi
endgroup

# Add the new labels from the Gemini review
if [ -n "$NEW_LABELS" ]; then
# Before adding, ensure all new labels exist.
group "Ensuring new labels exist before applying"
IFS=',' read -ra LABELS <<< "$NEW_LABELS"
# Refresh existing labels to include any created in the first step.
EXISTING_LABELS=$(gh label list --limit 1000 --json name --jq '.[].name' | tr -d '"\r')
for label in "${LABELS[@]}"; do
IFS=',' read -ra LABELS_ARR <<< "$NEW_LABELS"
# Refresh existing labels
for i in {1..3}; do
EXISTING_LABELS=$(gh label list --limit 1000 --json name --jq '.[].name' 2>/dev/null | tr -d '"\r') && break || sleep 5
done
for label in "${LABELS_ARR[@]}"; do
ensure_label_exists "$label"
done
endgroup

log "Adding labels: $NEW_LABELS"
gh pr edit $PR_NUMBER --add-label "$NEW_LABELS"
for i in {1..3}; do
if gh pr edit "$PR_NUMBER" --add-label "$NEW_LABELS"; then
log "Successfully updated labels."
exit 0
fi
warn "Label add attempt $i failed, retrying..."
sleep 10
done
error "Failed to add labels after 3 attempts."
fi
45 changes: 0 additions & 45 deletions .github/workflows/auto-fix.yml

This file was deleted.

Loading