262 lines
8.4 KiB
Bash
262 lines
8.4 KiB
Bash
|
|
#!/usr/bin/env bash
|
||
|
|
#
|
||
|
|
# Task processor for the Forgejo → OpenClaw pipeline.
|
||
|
|
# Polls Forgejo for new issues in the tasks repo, sends them to OpenClaw
|
||
|
|
# for processing, and posts results back as comments.
|
||
|
|
#
|
||
|
|
# Runs inside the OpenClaw pod via kubectl exec from a CronJob.
|
||
|
|
#
|
||
|
|
# Environment:
|
||
|
|
# FORGEJO_TOKEN — Forgejo API token with repo access
|
||
|
|
# FORGEJO_URL — Forgejo base URL (default: https://forgejo.viktorbarzin.me)
|
||
|
|
# FORGEJO_REPO — Repo in format "owner/repo" (default: vbarzin/tasks)
|
||
|
|
# OPENCLAW_URL — OpenClaw gateway URL (default: http://127.0.0.1:18789)
|
||
|
|
# OPENCLAW_TOKEN — OpenClaw gateway token
|
||
|
|
# SLACK_WEBHOOK_URL — Optional Slack webhook for notifications
|
||
|
|
|
||
|
|
set -euo pipefail
|
||
|
|
|
||
|
|
FORGEJO_URL="${FORGEJO_URL:-https://forgejo.viktorbarzin.me}"
|
||
|
|
FORGEJO_REPO="${FORGEJO_REPO:-viktor/tasks}"
|
||
|
|
OPENCLAW_URL="${OPENCLAW_URL:-https://integrate.api.nvidia.com}"
|
||
|
|
SLACK_WEBHOOK_URL="${SLACK_WEBHOOK_URL:-}"
|
||
|
|
|
||
|
|
: "${FORGEJO_TOKEN:?FORGEJO_TOKEN is required}"
|
||
|
|
: "${OPENCLAW_TOKEN:?OPENCLAW_TOKEN is required}"
|
||
|
|
FORGEJO_BOT_USER="${FORGEJO_BOT_USER:-viktor}"
|
||
|
|
|
||
|
|
fg_api() {
|
||
|
|
curl -sf -H "Authorization: token $FORGEJO_TOKEN" -H "Content-Type: application/json" "$@"
|
||
|
|
}
|
||
|
|
|
||
|
|
get_label_id() {
|
||
|
|
local label_name="$1"
|
||
|
|
fg_api "$FORGEJO_URL/api/v1/repos/$FORGEJO_REPO/labels?limit=50" | \
|
||
|
|
python3 -c "
|
||
|
|
import sys, json
|
||
|
|
labels = json.load(sys.stdin)
|
||
|
|
name = sys.argv[1]
|
||
|
|
for l in labels:
|
||
|
|
if l['name'] == name:
|
||
|
|
print(l['id'])
|
||
|
|
break
|
||
|
|
else:
|
||
|
|
print(0)
|
||
|
|
" "$label_name"
|
||
|
|
}
|
||
|
|
|
||
|
|
add_label() {
|
||
|
|
local issue_id="$1" label_name="$2"
|
||
|
|
local label_id
|
||
|
|
label_id=$(get_label_id "$label_name")
|
||
|
|
if [ "$label_id" != "0" ]; then
|
||
|
|
fg_api "$FORGEJO_URL/api/v1/repos/$FORGEJO_REPO/issues/$issue_id/labels" \
|
||
|
|
-d "{\"labels\":[$label_id]}" > /dev/null 2>&1 || true
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
remove_label() {
|
||
|
|
local issue_id="$1" label_name="$2"
|
||
|
|
local label_id
|
||
|
|
label_id=$(get_label_id "$label_name")
|
||
|
|
if [ "$label_id" != "0" ]; then
|
||
|
|
fg_api -X DELETE "$FORGEJO_URL/api/v1/repos/$FORGEJO_REPO/issues/$issue_id/labels/$label_id" > /dev/null 2>&1 || true
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
post_comment() {
|
||
|
|
local issue_id="$1"
|
||
|
|
# Read comment body from stdin to avoid quoting issues
|
||
|
|
python3 -c "
|
||
|
|
import sys, json
|
||
|
|
body = sys.stdin.read()
|
||
|
|
print(json.dumps({'body': body}))
|
||
|
|
" | fg_api "$FORGEJO_URL/api/v1/repos/$FORGEJO_REPO/issues/$issue_id/comments" -d @- > /dev/null 2>&1
|
||
|
|
}
|
||
|
|
|
||
|
|
close_issue() {
|
||
|
|
local issue_id="$1"
|
||
|
|
fg_api "$FORGEJO_URL/api/v1/repos/$FORGEJO_REPO/issues/$issue_id" \
|
||
|
|
-X PATCH -d '{"state": "closed"}' > /dev/null 2>&1
|
||
|
|
}
|
||
|
|
|
||
|
|
get_comment_history() {
|
||
|
|
local issue_id="$1"
|
||
|
|
fg_api "$FORGEJO_URL/api/v1/repos/$FORGEJO_REPO/issues/$issue_id/comments?limit=20" 2>/dev/null | \
|
||
|
|
python3 -c "
|
||
|
|
import sys, json
|
||
|
|
bot_user = sys.argv[1]
|
||
|
|
comments = json.load(sys.stdin)
|
||
|
|
history = []
|
||
|
|
for c in comments:
|
||
|
|
user = c.get('user', {}).get('login', 'unknown')
|
||
|
|
body = c.get('body', '')
|
||
|
|
# Skip bot's own comments to keep context clean
|
||
|
|
if user == bot_user:
|
||
|
|
# Include a short summary of previous responses
|
||
|
|
if '## OpenClaw Task Result' in body:
|
||
|
|
# Extract just the result content (skip header/footer)
|
||
|
|
lines = body.split('\n')
|
||
|
|
content = [l for l in lines if not l.startswith('## ') and not l.startswith('---') and not l.startswith('*Processed')]
|
||
|
|
summary = '\n'.join(content).strip()[:500]
|
||
|
|
if summary:
|
||
|
|
history.append(f'[Previous AI response]: {summary}')
|
||
|
|
else:
|
||
|
|
history.append(f'[{user}]: {body}')
|
||
|
|
print('\n\n'.join(history))
|
||
|
|
" "$FORGEJO_BOT_USER" 2>/dev/null
|
||
|
|
}
|
||
|
|
|
||
|
|
notify_slack() {
|
||
|
|
if [ -n "$SLACK_WEBHOOK_URL" ]; then
|
||
|
|
python3 -c "
|
||
|
|
import json, sys
|
||
|
|
print(json.dumps({'text': sys.argv[1]}))
|
||
|
|
" "$1" | curl -sf -X POST "$SLACK_WEBHOOK_URL" \
|
||
|
|
-H "Content-Type: application/json" -d @- > /dev/null 2>&1 || true
|
||
|
|
fi
|
||
|
|
}
|
||
|
|
|
||
|
|
process_issue() {
|
||
|
|
local issue_id="$1" title="$2" body="$3" author="$4"
|
||
|
|
|
||
|
|
echo "Processing issue #$issue_id: $title (by $author)"
|
||
|
|
|
||
|
|
# Mark as processing
|
||
|
|
add_label "$issue_id" "processing"
|
||
|
|
remove_label "$issue_id" "pending"
|
||
|
|
remove_label "$issue_id" "completed"
|
||
|
|
|
||
|
|
# Fetch comment history for context
|
||
|
|
local comment_history
|
||
|
|
comment_history=$(get_comment_history "$issue_id")
|
||
|
|
|
||
|
|
# Call OpenClaw gateway API (OpenAI-compatible chat completions)
|
||
|
|
# Use python to safely build the JSON payload
|
||
|
|
local response
|
||
|
|
response=$(python3 -c "
|
||
|
|
import json, sys
|
||
|
|
title = sys.argv[1]
|
||
|
|
body = sys.argv[2]
|
||
|
|
author = sys.argv[3]
|
||
|
|
comment_history = sys.argv[4]
|
||
|
|
|
||
|
|
prompt = f'''You are processing a task submitted by {author} via the Forgejo task queue.
|
||
|
|
|
||
|
|
Task title: {title}
|
||
|
|
|
||
|
|
Task description:
|
||
|
|
{body}'''
|
||
|
|
|
||
|
|
if comment_history.strip():
|
||
|
|
prompt += f'''
|
||
|
|
|
||
|
|
Conversation history (follow-up comments):
|
||
|
|
{comment_history}
|
||
|
|
|
||
|
|
The latest comment is the most recent request. Address it in context of the original task and prior conversation.'''
|
||
|
|
|
||
|
|
prompt += '''
|
||
|
|
|
||
|
|
Please execute this task. When done, provide a clear summary of what was done and any results.
|
||
|
|
If the task requires infrastructure changes, describe what changes would be needed but do NOT apply them automatically — list the commands/changes for review.'''
|
||
|
|
|
||
|
|
payload = {
|
||
|
|
'model': 'mistralai/mistral-large-3-675b-instruct-2512',
|
||
|
|
'messages': [
|
||
|
|
{'role': 'system', 'content': 'You are an infrastructure AI assistant. Process the task and provide actionable results. Be concise.'},
|
||
|
|
{'role': 'user', 'content': prompt}
|
||
|
|
],
|
||
|
|
'max_tokens': 8192,
|
||
|
|
'temperature': 0.3
|
||
|
|
}
|
||
|
|
print(json.dumps(payload))
|
||
|
|
" "$title" "$body" "$author" "$comment_history" | \
|
||
|
|
curl -sf --max-time 300 \
|
||
|
|
-H "Authorization: Bearer $OPENCLAW_TOKEN" \
|
||
|
|
-H "Content-Type: application/json" \
|
||
|
|
"$OPENCLAW_URL/v1/chat/completions" \
|
||
|
|
-d @- 2>&1) || {
|
||
|
|
echo " ERROR: OpenClaw API call failed"
|
||
|
|
echo "Failed to process this task. OpenClaw API returned an error. Please check the CronJob logs or process manually." | \
|
||
|
|
post_comment "$issue_id"
|
||
|
|
add_label "$issue_id" "failed"
|
||
|
|
remove_label "$issue_id" "processing"
|
||
|
|
notify_slack ":x: Task #$issue_id failed: $title"
|
||
|
|
return 1
|
||
|
|
}
|
||
|
|
|
||
|
|
# Extract the response content and post as comment
|
||
|
|
python3 -c "
|
||
|
|
import sys, json
|
||
|
|
try:
|
||
|
|
data = json.load(sys.stdin)
|
||
|
|
msg = data['choices'][0]['message']
|
||
|
|
# Some models put content in reasoning_content instead of content
|
||
|
|
result = msg.get('content') or msg.get('reasoning_content') or msg.get('reasoning') or 'No response generated.'
|
||
|
|
except Exception as e:
|
||
|
|
result = f'Error parsing OpenClaw response: {e}'
|
||
|
|
|
||
|
|
body = f'## OpenClaw Task Result\n\n{result}\n\n---\n*Processed automatically by the OpenClaw task pipeline.*'
|
||
|
|
print(body)
|
||
|
|
" <<< "$response" | post_comment "$issue_id"
|
||
|
|
|
||
|
|
# Update labels and close
|
||
|
|
add_label "$issue_id" "completed"
|
||
|
|
remove_label "$issue_id" "processing"
|
||
|
|
close_issue "$issue_id"
|
||
|
|
|
||
|
|
echo " Issue #$issue_id processed and closed"
|
||
|
|
notify_slack ":white_check_mark: Task #$issue_id completed: $title"
|
||
|
|
}
|
||
|
|
|
||
|
|
# --- Main ---
|
||
|
|
|
||
|
|
echo "=== Task Processor $(date -u +%Y-%m-%dT%H:%M:%SZ) ==="
|
||
|
|
|
||
|
|
# List open issues
|
||
|
|
ISSUES=$(fg_api "$FORGEJO_URL/api/v1/repos/$FORGEJO_REPO/issues?state=open&type=issues&limit=10&sort=created&direction=asc" 2>/dev/null) || {
|
||
|
|
echo "ERROR: Could not fetch issues from Forgejo"
|
||
|
|
exit 1
|
||
|
|
}
|
||
|
|
|
||
|
|
# Parse pending issues into a temp file (avoids delimiter issues)
|
||
|
|
PENDING_FILE=$(mktemp)
|
||
|
|
trap 'rm -f "$PENDING_FILE"' EXIT
|
||
|
|
|
||
|
|
python3 -c "
|
||
|
|
import sys, json
|
||
|
|
issues = json.load(sys.stdin)
|
||
|
|
for issue in issues:
|
||
|
|
labels = [l['name'] for l in issue.get('labels', [])]
|
||
|
|
# Process if: no processing label AND (no completed label OR issue was reopened)
|
||
|
|
if 'processing' not in labels:
|
||
|
|
# Write each issue as a JSON line
|
||
|
|
print(json.dumps({
|
||
|
|
'id': issue['number'],
|
||
|
|
'title': issue['title'],
|
||
|
|
'body': (issue.get('body') or '')[:4000],
|
||
|
|
'author': issue['user']['login']
|
||
|
|
}))
|
||
|
|
" <<< "$ISSUES" > "$PENDING_FILE"
|
||
|
|
|
||
|
|
ISSUE_COUNT=$(wc -l < "$PENDING_FILE" | tr -d ' ')
|
||
|
|
|
||
|
|
if [ "$ISSUE_COUNT" = "0" ]; then
|
||
|
|
echo "No pending issues to process"
|
||
|
|
exit 0
|
||
|
|
fi
|
||
|
|
|
||
|
|
echo "Found $ISSUE_COUNT pending issue(s)"
|
||
|
|
|
||
|
|
# Process each pending issue (one JSON object per line)
|
||
|
|
while IFS= read -r line; do
|
||
|
|
issue_id=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['id'])" "$line")
|
||
|
|
title=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['title'])" "$line")
|
||
|
|
body=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['body'])" "$line")
|
||
|
|
author=$(python3 -c "import json,sys; print(json.loads(sys.argv[1])['author'])" "$line")
|
||
|
|
process_issue "$issue_id" "$title" "$body" "$author" || true
|
||
|
|
done < "$PENDING_FILE"
|
||
|
|
|
||
|
|
echo "=== Task processing complete ==="
|