mirror of
https://github.com/MervinPraison/PraisonAI.git
synced 2026-11-08 08:11:07 -10:00
Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
8a9d39342f | ||
|
|
74c9b2a1e8 | ||
|
|
d38987a535 |
No files matched your search
@@ -1,65 +1,71 @@
|
||||
framework: "praisonai"
|
||||
topic: "Autonomous Issue Resolution and PR Creation"
|
||||
planning: true
|
||||
process: workflow
|
||||
topic: "Autonomous Issue Resolution"
|
||||
planning: false
|
||||
|
||||
roles:
|
||||
issue_analyst:
|
||||
role: "GitHub Issue Analyst"
|
||||
goal: "Read the GitHub issue, understand the required changes, and map the problem to the local codebase"
|
||||
backstory: "You are an expert software analyst who reads bug reports and feature requests. You excel at taking a problem description and finding exactly which files in the codebase need to be modified. Your primary job is discovery."
|
||||
backstory: "You are an expert software analyst who reads bug reports and feature requests. You excel at taking a problem description and finding exactly which files in the codebase need to be modified. Your primary job is discovery and planning."
|
||||
tools:
|
||||
- "execute_command"
|
||||
|
||||
|
||||
software_engineer:
|
||||
role: "Autonomous Software Engineer"
|
||||
goal: "Checkout a new branch, rewrite the code to fix the issue, and ensure the changes are correct locally"
|
||||
backstory: "You are a senior software engineer who writes flawless code. You branch off main, use terminal commands to rewrite files using sed, echo, or other secure tools, and verify that your changes perfectly address the analyst's findings."
|
||||
role: "Autonomous Software Engineer"
|
||||
goal: "Implement required code changes on a branch, run tests, commit, push, and open a PR only if changes were made"
|
||||
backstory: "You are a senior software engineer. You always branch first, implement changes using shell commands, verify with git diff, run tests, then commit and push. You only open a Pull Request when you have actually committed code changes."
|
||||
tools:
|
||||
- "execute_command"
|
||||
- "github_create_branch"
|
||||
|
||||
release_manager:
|
||||
role: "Release and CI Manager"
|
||||
goal: "Commit the changes, push the branch to remote, and create the Pull Request linking back to the issue"
|
||||
backstory: "You are a DevOps Release Engineer. Your job is strictly to ensure that any code changes are properly committed, pushed to the GitHub repository, and framed as a clean Pull Request that automatically links to the issue."
|
||||
tools:
|
||||
- "github_commit_and_push"
|
||||
- "github_create_pull_request"
|
||||
- "github_create_branch"
|
||||
- "github_commit_and_push"
|
||||
- "execute_command"
|
||||
|
||||
steps:
|
||||
- name: analyze_issue
|
||||
- name: Analyze Issue
|
||||
agent: issue_analyst
|
||||
action: |
|
||||
Thoroughly analyze the Issue and the repository:
|
||||
0. CRITICAL: Your wrapper has already pre-fetched the complete issue description and Pull Request diff securely.
|
||||
1. Read the pre-fetched context natively using `execute_command` with the `command` argument set to `cat .github/triage-context.md`. Do NOT provide a `cwd` argument.
|
||||
2. Identify the exact problem or feature request.
|
||||
3. Use tools like `grep`, `find`, or `cat` to explore the codebase and find the specific files that need modification.
|
||||
4. Create a specific, file-level plan of what needs to change.
|
||||
expected_output: "A detailed analysis report including the files that must be changed and exactly what lines or logic must be altered to solve the Issue."
|
||||
|
||||
- name: implement_fix
|
||||
action: >
|
||||
Thoroughly analyze the issue and the repository. Follow these steps in order.
|
||||
|
||||
First, set up git identity: execute_command 'git config user.name PraisonAI' then
|
||||
execute_command 'git config user.email [email protected]'. Do NOT pass cwd.
|
||||
|
||||
Second, read the issue: execute_command 'cat .github/triage-context.md'. Do NOT pass cwd.
|
||||
|
||||
Third, read architecture guidelines: execute_command 'cat src/praisonai-agents/AGENTS.md'.
|
||||
|
||||
Fourth, decide if this issue requires code changes:
|
||||
- If YES: identify the exact files and lines that must change. Output a concrete file-level plan.
|
||||
- If NO (question, discussion, duplicate, invalid): say so clearly. No code changes needed.
|
||||
|
||||
Output: routing decision (Core SDK / Wrapper / Tools / No-change), validation result, and exact files/lines to change.
|
||||
expected_output: A decision on whether code changes are needed, and if so a concrete file-level plan.
|
||||
|
||||
- name: Implement Fix
|
||||
agent: software_engineer
|
||||
action: |
|
||||
Implement the changes required to solve the issue:
|
||||
1. Review the analyst's plan.
|
||||
2. Check out a dedicated fix branch by using the `github_create_branch` tool with `branch_name` set to `praisonai/issue-{{ISSUE_NUMBER}}`.
|
||||
3. Carefully modify the necessary files in the repository using bash commands (e.g. `cat`, `sed`, `echo`, or small inline scripts).
|
||||
4. Double-check your edits by inspecting the file outputs or running `git diff` to ensure you changed what was required.
|
||||
expected_output: "Confirmation that the git branch was successfully created and the files were reliably modified without syntax errors."
|
||||
|
||||
- name: create_pull_request
|
||||
agent: release_manager
|
||||
action: |
|
||||
Push the code and submit the PR:
|
||||
1. Use the native `github_commit_and_push` tool and provide a commit message like "PraisonAI Automated Fix".
|
||||
2. Use the native `github_create_pull_request` tool with the arguments:
|
||||
- title: "Fix Issue {{ISSUE_NUMBER}}"
|
||||
- body: "Automated triage by PraisonAI Native Issue Triage."
|
||||
- head_branch: "praisonai/issue-{{ISSUE_NUMBER}}"
|
||||
- base_branch: "main"
|
||||
3. Post a comment on the original issue letting the user know: `execute_command`: `gh issue comment {{ISSUE_NUMBER}} -b "I have autonomously analyzed the codebase and written a fix for this issue! I've opened a Pull Request for your review."`
|
||||
expected_output: "The URL of the successfully created Pull Request."
|
||||
dependencies: [analyze_issue, implement_fix]
|
||||
action: >
|
||||
You are working inside the repository at /Users/praison/praisonai-package.
|
||||
Do NOT ask for confirmation. Do NOT use placeholder paths. Act immediately.
|
||||
|
||||
Read the analyst's output first. If the analyst said NO code changes are needed, stop here and
|
||||
output a brief explanation — do NOT create a branch or PR.
|
||||
|
||||
If code changes ARE needed, follow these steps:
|
||||
|
||||
Step 1 — Create branch: github_create_branch with branch_name=praisonai/issue-{{ISSUE_NUMBER}}.
|
||||
|
||||
Step 2 — Implement changes using execute_command (no cwd argument).
|
||||
To insert a line: python3 -c "lines=open('FILE').readlines(); lines.insert(N,'LINE\n'); open('FILE','w').writelines(lines)"
|
||||
To replace text: python3 -c "t=open('FILE').read(); open('FILE','w').write(t.replace('OLD','NEW'))"
|
||||
After each edit verify: execute_command 'git diff'
|
||||
|
||||
Step 3 — Run tests: execute_command 'python3 -m pytest src/praisonai-agents/tests/ -x -q --timeout=30 -p no:warnings'
|
||||
|
||||
Step 4 — Commit and push: github_commit_and_push with a descriptive commit_message.
|
||||
|
||||
Step 5 — ONLY if commits were pushed: create PR using github_create_pull_request with
|
||||
title='fix: resolve issue #{{ISSUE_NUMBER}}',
|
||||
body='Fixes #{{ISSUE_NUMBER}}.\n\nAutonomously resolved by PraisonAI.',
|
||||
head_branch=praisonai/issue-{{ISSUE_NUMBER}}, base_branch=main.
|
||||
expected_output: Either "No code changes needed" or confirmation that branch was created, changes committed, pushed, and PR opened.
|
||||
dependencies: [Analyze Issue]
|
||||
@@ -1,3 +1,3 @@
|
||||
# GitHub Issue #1337: Big Task: Document CLI Router Architecture
|
||||
# GitHub Issue #1343: Add type hint to github_create_branch return type
|
||||
|
||||
Please thoroughly trace how src/praisonai/praisonai/cli/main.py interfaces with typer across all command modules. Produce a detailed architecture document in a new file docs/cli_architecture_test.md.
|
||||
The github_create_branch function in src/praisonai-agents/praisonaiagents/tools/github_tools.py is missing a return type annotation. Please add -> str to the function signature.
|
||||
@@ -51,17 +51,32 @@ class ShellTools:
|
||||
Dictionary with execution results
|
||||
"""
|
||||
try:
|
||||
# Strip wrapping quotes the LLM sometimes adds around the whole command string
|
||||
if command and len(command) >= 2 and command[0] == command[-1] and command[0] in ("'", '"'):
|
||||
command = command[1:-1]
|
||||
# Treat empty-string cwd same as None to avoid subprocess failure
|
||||
if not cwd:
|
||||
cwd = None
|
||||
# Always split command for safety (no shell execution)
|
||||
# Use shlex.split with appropriate posix flag
|
||||
if platform.system() == 'Windows':
|
||||
# Use shlex with posix=False for Windows to handle quotes properly
|
||||
command = shlex.split(command, posix=False)
|
||||
cmd_parts = shlex.split(command, posix=False)
|
||||
else:
|
||||
command = shlex.split(command)
|
||||
cmd_parts = shlex.split(command)
|
||||
# Guard against empty command list (e.g. LLM passed empty string)
|
||||
if not cmd_parts:
|
||||
return {
|
||||
'stdout': '',
|
||||
'stderr': 'Empty command',
|
||||
'exit_code': 1,
|
||||
'success': False,
|
||||
'execution_time': 0.0,
|
||||
}
|
||||
|
||||
# Expand tilde and environment variables in command arguments
|
||||
# (shell=False means the shell won't do this for us)
|
||||
command = [os.path.expanduser(os.path.expandvars(arg)) for arg in command]
|
||||
cmd_parts = [os.path.expanduser(os.path.expandvars(arg)) for arg in cmd_parts]
|
||||
|
||||
# Expand tilde in cwd (subprocess doesn't do this)
|
||||
if cwd:
|
||||
@@ -81,7 +96,7 @@ class ShellTools:
|
||||
# Start process
|
||||
start_time = time.time()
|
||||
process = subprocess.Popen(
|
||||
command,
|
||||
cmd_parts,
|
||||
stdout=subprocess.PIPE,
|
||||
stderr=subprocess.PIPE,
|
||||
cwd=cwd,
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
"""Unit tests for ShellTools.execute_command input-normalisation improvements.
|
||||
|
||||
Covers:
|
||||
1. Stripping wrapping quotes added by LLMs around the whole command string.
|
||||
2. Empty-command guard – consistent return-value shape.
|
||||
3. Empty/None cwd normalisation.
|
||||
"""
|
||||
|
||||
import os
|
||||
import pytest
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Helpers
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
def _make_shell():
|
||||
"""Return a ShellTools instance with auto-approval so tests can call it."""
|
||||
from praisonaiagents.tools.shell_tools import ShellTools
|
||||
return ShellTools()
|
||||
|
||||
|
||||
def _run(shell, command, **kwargs):
|
||||
"""Call execute_command with PRAISONAI_AUTO_APPROVE set."""
|
||||
with patch.dict(os.environ, {"PRAISONAI_AUTO_APPROVE": "true"}):
|
||||
return shell.execute_command(command, **kwargs)
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Expected result-shape helper
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
_REQUIRED_KEYS = {"stdout", "stderr", "exit_code", "success", "execution_time"}
|
||||
|
||||
|
||||
def _assert_shape(result: dict) -> None:
|
||||
"""Assert the result dict has all required keys."""
|
||||
missing = _REQUIRED_KEYS - result.keys()
|
||||
assert not missing, f"Result is missing keys: {missing}; got {result}"
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: empty command
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestEmptyCommandGuard:
|
||||
"""execute_command should return a consistent dict for empty commands."""
|
||||
|
||||
def test_empty_string_returns_required_keys(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, "")
|
||||
_assert_shape(result)
|
||||
|
||||
def test_empty_string_success_is_false(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, "")
|
||||
assert result["success"] is False
|
||||
|
||||
def test_empty_string_exit_code_nonzero(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, "")
|
||||
assert result["exit_code"] != 0
|
||||
|
||||
def test_empty_string_execution_time_present(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, "")
|
||||
assert isinstance(result["execution_time"], (int, float))
|
||||
|
||||
def test_whitespace_only_returns_required_keys(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, " ")
|
||||
_assert_shape(result)
|
||||
|
||||
def test_whitespace_only_success_is_false(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, " ")
|
||||
assert result["success"] is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: quote stripping
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestQuoteNormalisation:
|
||||
"""execute_command should strip wrapping quotes added by LLMs."""
|
||||
|
||||
def test_single_quoted_command_executes(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, "'echo hello'")
|
||||
_assert_shape(result)
|
||||
assert result["success"] is True
|
||||
assert "hello" in result["stdout"]
|
||||
|
||||
def test_double_quoted_command_executes(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, '"echo world"')
|
||||
_assert_shape(result)
|
||||
assert result["success"] is True
|
||||
assert "world" in result["stdout"]
|
||||
|
||||
def test_unquoted_command_executes_normally(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, "echo normal")
|
||||
_assert_shape(result)
|
||||
assert result["success"] is True
|
||||
assert "normal" in result["stdout"]
|
||||
|
||||
def test_mismatched_quotes_not_stripped(self):
|
||||
"""Mismatched quotes (e.g. 'cmd") are not stripped; shlex raises ValueError which is caught."""
|
||||
shell = _make_shell()
|
||||
# The outer try/except in execute_command catches ValueError from shlex.split
|
||||
result = _run(shell, "'echo bad\"")
|
||||
_assert_shape(result)
|
||||
assert result["success"] is False
|
||||
|
||||
|
||||
# ---------------------------------------------------------------------------
|
||||
# Tests: cwd normalisation
|
||||
# ---------------------------------------------------------------------------
|
||||
|
||||
class TestCwdNormalisation:
|
||||
"""Empty/None cwd should be treated as None (use current directory)."""
|
||||
|
||||
def test_empty_string_cwd_is_treated_as_none(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, "echo cwd_test", cwd="")
|
||||
_assert_shape(result)
|
||||
assert result["success"] is True
|
||||
|
||||
def test_none_cwd_works(self):
|
||||
shell = _make_shell()
|
||||
result = _run(shell, "echo no_cwd", cwd=None)
|
||||
_assert_shape(result)
|
||||
assert result["success"] is True
|
||||
@@ -1,4 +1,5 @@
|
||||
import os
|
||||
import re
|
||||
import sys
|
||||
import json
|
||||
import time
|
||||
@@ -59,15 +60,23 @@ class StickyComment:
|
||||
WORKING = "🔄"
|
||||
|
||||
def __init__(self, api_base: str, issue: int, token: str,
|
||||
task_type: str, title: str, run_url: str):
|
||||
task_type: str, title: str, run_url: str,
|
||||
repo_name: str = ""):
|
||||
self._api_base = api_base
|
||||
self._issue = issue
|
||||
self._token = token
|
||||
self._task_type = task_type
|
||||
self._title = title
|
||||
self._run_url = run_url
|
||||
self._repo_name = repo_name
|
||||
self._comment_id: Optional[int] = None
|
||||
|
||||
# Branch and PR tracking (set when tools fire)
|
||||
self._branch_name: Optional[str] = None
|
||||
self._pr_url: Optional[str] = None
|
||||
self._pr_number: Optional[int] = None
|
||||
self._summary_lines: List[str] = []
|
||||
|
||||
# Todo list: list of dicts {content, status} status ∈ pending/in_progress/completed
|
||||
self._todos: List[Dict[str, Any]] = []
|
||||
# Free-form activity log shown below the todo list (last N lines)
|
||||
@@ -154,6 +163,25 @@ class StickyComment:
|
||||
self._current_tool = None
|
||||
# no extra log for tool end – keeps comment clean
|
||||
|
||||
def set_branch(self, branch_name: str) -> None:
|
||||
"""Called when a branch is successfully created."""
|
||||
with self._lock:
|
||||
self._branch_name = branch_name
|
||||
self._schedule_push()
|
||||
|
||||
def set_pr(self, pr_url: str, pr_number: Optional[int] = None) -> None:
|
||||
"""Called when a PR is successfully opened."""
|
||||
with self._lock:
|
||||
self._pr_url = pr_url
|
||||
self._pr_number = pr_number
|
||||
self._schedule_push()
|
||||
|
||||
def add_summary_line(self, line: str) -> None:
|
||||
"""Append a line to the Summary section."""
|
||||
with self._lock:
|
||||
self._summary_lines.append(line)
|
||||
self._schedule_push()
|
||||
|
||||
def finalize(self, success: bool, summary: str) -> None:
|
||||
"""Push the final state synchronously."""
|
||||
if self._timer:
|
||||
@@ -164,7 +192,8 @@ class StickyComment:
|
||||
for t in self._todos:
|
||||
if t["status"] == "in_progress":
|
||||
t["status"] = "completed"
|
||||
self._logs.append(summary)
|
||||
if summary:
|
||||
self._summary_lines.append(summary)
|
||||
self._push_now(final=success)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
@@ -190,24 +219,40 @@ class StickyComment:
|
||||
return f"`{tool_name}`"
|
||||
|
||||
def _build_body(self, final: Optional[bool] = None) -> str:
|
||||
"""Render the full Markdown comment body."""
|
||||
"""Render the full Markdown comment body (Claude Code style)."""
|
||||
if final is True:
|
||||
icon = self.DONE
|
||||
status_icon = self.DONE
|
||||
verb = "finished"
|
||||
elif final is False:
|
||||
icon = self.FAIL
|
||||
status_icon = self.FAIL
|
||||
verb = "failed on"
|
||||
else:
|
||||
icon = self.SPINNER
|
||||
status_icon = self.SPINNER
|
||||
verb = "is working on"
|
||||
|
||||
lines: List[str] = []
|
||||
|
||||
# Title line
|
||||
lines.append(f"**{self._task_type} #{self._issue}: {self._title}** {icon}")
|
||||
lines.append("")
|
||||
# --- Header line (Claude Code style) ---
|
||||
# "PraisonAI finished @user's task — View job · branch-name · Create PR →"
|
||||
header_parts: List[str] = []
|
||||
header_parts.append(f"**PraisonAI {verb} #{self._issue}** {status_icon}")
|
||||
|
||||
# Job link
|
||||
meta_parts: List[str] = []
|
||||
if self._run_url:
|
||||
lines.append(f"[View job run]({self._run_url})")
|
||||
lines.append("")
|
||||
meta_parts.append(f"[View job]({self._run_url})")
|
||||
if self._branch_name:
|
||||
branch_url = f"https://github.com/{self._repo_name}/tree/{self._branch_name}" if self._repo_name else ""
|
||||
if branch_url:
|
||||
meta_parts.append(f"[`{self._branch_name}`]({branch_url})")
|
||||
else:
|
||||
meta_parts.append(f"`{self._branch_name}`")
|
||||
if self._pr_url:
|
||||
meta_parts.append(f"[Create PR →]({self._pr_url})")
|
||||
|
||||
lines.append(" · ".join([header_parts[0]] + meta_parts) if meta_parts else header_parts[0])
|
||||
lines.append("")
|
||||
lines.append(f"**{self._task_type} #{self._issue}: {self._title}**")
|
||||
lines.append("")
|
||||
|
||||
# --- Todo list ---
|
||||
if self._todos:
|
||||
@@ -218,20 +263,26 @@ class StickyComment:
|
||||
if s == "completed":
|
||||
box = "- [x]"
|
||||
elif s == "in_progress":
|
||||
box = "- [x]" # GitHub renders in_progress same as done; use ✅ marker
|
||||
box = "- [ ]"
|
||||
else:
|
||||
box = "- [ ]"
|
||||
content = t["content"]
|
||||
if s == "in_progress":
|
||||
content = f"{content} 🔄"
|
||||
elif s == "completed":
|
||||
content = f"~~{content}~~" if final else content
|
||||
lines.append(f"{box} {content}")
|
||||
lines.append("")
|
||||
|
||||
# --- Activity log (last 15 items) ---
|
||||
recent = self._logs[-15:]
|
||||
if recent:
|
||||
# --- Summary section ---
|
||||
if self._summary_lines:
|
||||
lines.append("**Summary**")
|
||||
lines.append("")
|
||||
for entry in self._summary_lines[-10:]:
|
||||
lines.append(f"{entry}")
|
||||
lines.append("")
|
||||
|
||||
# --- Activity log (last 8 items, only while running) ---
|
||||
if final is None and self._logs:
|
||||
recent = self._logs[-8:]
|
||||
lines.append("**Activity**")
|
||||
lines.append("")
|
||||
for entry in recent:
|
||||
@@ -308,11 +359,31 @@ def _make_context_sink(sticky: StickyComment):
|
||||
sticky.set_todos(todos)
|
||||
else:
|
||||
sticky.on_tool_start(agent_name, tool_name, tool_args)
|
||||
# Capture branch name from args immediately
|
||||
if tool_name == "github_create_branch":
|
||||
bn = tool_args.get("branch_name", "")
|
||||
if bn:
|
||||
sticky.set_branch(bn)
|
||||
|
||||
elif et_val == "tool_call_end":
|
||||
tool_name = data.get("tool_name", "")
|
||||
tool_result = data.get("tool_result") or {}
|
||||
if tool_name != "TodoWrite":
|
||||
sticky.on_tool_end(event.agent_name or "", tool_name)
|
||||
# Capture PR URL from result
|
||||
if tool_name == "github_create_pull_request":
|
||||
result_str = str(tool_result)
|
||||
m = re.search(r'https://github\.com/[^\s"\)]+/pull/\d+', result_str)
|
||||
if m:
|
||||
pr_url = m.group(0)
|
||||
pr_num_m = re.search(r'/pull/(\d+)', pr_url)
|
||||
sticky.set_pr(pr_url, int(pr_num_m.group(1)) if pr_num_m else None)
|
||||
# Also try dict result
|
||||
if isinstance(tool_result, dict):
|
||||
url = tool_result.get("url") or tool_result.get("html_url", "")
|
||||
if url and "/pull/" in url:
|
||||
pr_num_m = re.search(r'/pull/(\d+)', url)
|
||||
sticky.set_pr(url, int(pr_num_m.group(1)) if pr_num_m else None)
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
@@ -444,6 +515,7 @@ def github_triage(
|
||||
sticky = StickyComment(
|
||||
api_base=api_base, issue=issue, token=token,
|
||||
task_type=task_type, title=ctx_title, run_url=run_url,
|
||||
repo_name=repo_name,
|
||||
)
|
||||
sticky._logs.append(f"Context fetched for {task_type} #{issue}")
|
||||
|
||||
@@ -505,9 +577,9 @@ def github_triage(
|
||||
pass
|
||||
|
||||
if success:
|
||||
summary = f"✅ **Completed** – {task_type} #{issue} triaged successfully."
|
||||
summary = f"✅ Completed – {task_type} #{issue} triaged successfully."
|
||||
else:
|
||||
summary = f"❌ **Failed** – {error_msg[:200]}"
|
||||
summary = f"❌ Failed – {error_msg[:200]}"
|
||||
|
||||
sticky.finalize(success=success, summary=summary)
|
||||
print("[green]✅ Final sticky comment updated.[/green]")
|
||||
Reference in new issue
Block a user