Skip to content

Commit 02b1727

Browse files
authored
chore(triage-skill): Increase num_turns and add script to post summary (#19456)
With the new addition of security measures (e.g. stopping immediately with non-zero exit code), the action just stops when reaching 20 turns. Before, the action ran for around 35 turns, so the limit is increased to 40. Additionally (because strictly stopping execution and we no longer get a Job Summary), a script was added to take the script output and post it. So far, we no longer have tool call errors, just non-zero exit codes because of too many turns. Now, we can at least see the summary from the `claude-execution-output.json` (this is saved as an [`output` by the claude action](https://github.com/anthropics/claude-code-action/blob/edd85d61533cbba7b57ed0ca4af1750b1fdfd3c4/base-action/README.md?plain=1#L119)). Previous `exit code 1` logs: ``` { "type": "result", "subtype": "error_max_turns", "is_error": false, ... } Log saved to /home/runner/work/_temp/claude-execution-output.json Error: Execution failed: Error: Action failed with error: Claude execution failed: Error: Process completed with exit code 1. ``` Closes #19457 (added automatically)
1 parent a67374f commit 02b1727

File tree

3 files changed

+133
-1
lines changed

3 files changed

+133
-1
lines changed

.claude/skills/triage-issue/scripts/README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,3 +18,7 @@ Parses `gh api` JSON output (single issue or search results) into a readable sum
1818
## post_linear_comment.py
1919

2020
Posts the triage report to an existing Linear issue. Reads `LINEAR_CLIENT_ID` and `LINEAR_CLIENT_SECRET` from environment variables — never pass secrets as CLI arguments.
21+
22+
## write_job_summary.py
23+
24+
Reads Claude Code execution output JSON (from the triage GitHub Action) and prints Markdown for the job summary: duration, turns, cost, and a note when the run stopped due to `error_max_turns`. Used by the workflow step that runs `if: always()` so the summary is posted even when the triage step fails (e.g. max turns reached).
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
#!/usr/bin/env python3
2+
"""
3+
Read Claude Code execution output JSON and write duration, cost, and status
4+
to stdout as Markdown for GitHub Actions job summary (GITHUB_STEP_SUMMARY).
5+
6+
Usage:
7+
python3 write_job_summary.py <path-to-claude-execution-output.json>
8+
9+
Handles single JSON object or NDJSON (one JSON object per line).
10+
Uses the last object with type "result" when multiple are present.
11+
12+
Job summary has a ~1MB limit; raw JSON is truncated if needed to avoid job abort.
13+
"""
14+
15+
import json
16+
import sys
17+
18+
# Stay under GITHUB_STEP_SUMMARY ~1MB limit; leave room for the table and text
19+
MAX_RAW_BYTES = 800_000
20+
21+
22+
def _append_raw_json_section(content: str, lines: list[str]) -> None:
23+
"""Append a 'Full execution output' json block to lines, with truncation and fence escaping."""
24+
raw = content.strip()
25+
encoded = raw.encode("utf-8")
26+
if len(encoded) > MAX_RAW_BYTES:
27+
raw = encoded[:MAX_RAW_BYTES].decode("utf-8", errors="replace") + "\n\n... (truncated due to job summary size limit)"
28+
raw = raw.replace("```", "`\u200b``")
29+
lines.extend(["", "### Full execution output", "", "```json", raw, "```"])
30+
31+
32+
def main() -> int:
33+
if len(sys.argv) < 2:
34+
print("Usage: write_job_summary.py <execution-output.json>", file=sys.stderr)
35+
return 1
36+
37+
path = sys.argv[1]
38+
try:
39+
with open(path, encoding="utf-8") as f:
40+
content = f.read()
41+
except OSError as e:
42+
msg = f"## Claude Triage Run\n\nCould not read execution output: {e}"
43+
print(msg, file=sys.stderr)
44+
print(msg) # Also to stdout so job summary shows something
45+
return 1
46+
47+
# Support single JSON or NDJSON (one object per line)
48+
results = []
49+
for line in content.strip().splitlines():
50+
line = line.strip()
51+
if not line:
52+
continue
53+
try:
54+
obj = json.loads(line)
55+
if obj.get("type") == "result":
56+
results.append(obj)
57+
except json.JSONDecodeError:
58+
continue
59+
60+
if not results:
61+
# Try parsing whole content as single JSON
62+
try:
63+
obj = json.loads(content)
64+
if obj.get("type") == "result":
65+
results = [obj]
66+
except json.JSONDecodeError:
67+
pass
68+
69+
if not results:
70+
no_result_lines = ["## Claude Triage Run", "", "No execution result found in output."]
71+
_append_raw_json_section(content, no_result_lines)
72+
print("\n".join(no_result_lines))
73+
return 0
74+
75+
last = results[-1]
76+
duration_ms = last.get("duration_ms")
77+
num_turns = last.get("num_turns")
78+
total_cost = last.get("total_cost_usd")
79+
subtype = last.get("subtype", "")
80+
81+
cost_str = f"${total_cost:.4f} USD" if isinstance(total_cost, (int, float)) else "n/a"
82+
lines = [
83+
"## Claude Triage Run",
84+
"",
85+
"| Metric | Value |",
86+
"|--------|-------|",
87+
f"| Duration | {duration_ms if duration_ms is not None else 'n/a'} ms |",
88+
f"| Turns | {num_turns if num_turns is not None else 'n/a'} |",
89+
f"| Cost (USD) | {cost_str} |",
90+
]
91+
if subtype == "error_max_turns":
92+
lines.extend([
93+
"",
94+
"⚠️ **Run stopped:** maximum turns reached. Consider increasing `max-turns` in the workflow or simplifying the issue scope.",
95+
])
96+
elif subtype and subtype != "success":
97+
lines.extend([
98+
"",
99+
f"Result: `{subtype}`",
100+
])
101+
102+
_append_raw_json_section(content, lines)
103+
104+
print("\n".join(lines))
105+
return 0
106+
107+
108+
if __name__ == "__main__":
109+
sys.exit(main())

.github/workflows/triage-issue.yml

Lines changed: 20 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,6 +53,7 @@ jobs:
5353
ref: develop
5454

5555
- name: Run Claude triage
56+
id: triage
5657
uses: anthropics/claude-code-action@v1
5758
with:
5859
anthropic_api_key: ${{ secrets.ANTHROPIC_API_KEY }}
@@ -71,4 +72,22 @@ jobs:
7172
Do NOT use `python3 -c` or other inline Python in Bash, only the provided scripts are allowed.
7273
Do NOT attempt to delete (`rm`) temporary files you create.
7374
claude_args: |
74-
--max-turns 20 --allowedTools "Write,Bash(gh api *),Bash(gh pr list *),Bash(npm info *),Bash(npm ls *),Bash(python3 .claude/skills/triage-issue/scripts/post_linear_comment.py *),Bash(python3 .claude/skills/triage-issue/scripts/parse_gh_issues.py *),Bash(python3 .claude/skills/triage-issue/scripts/detect_prompt_injection.py *)"
75+
--max-turns 40 --allowedTools "Write,Bash(gh api *),Bash(gh pr list *),Bash(npm info *),Bash(npm ls *),Bash(python3 .claude/skills/triage-issue/scripts/post_linear_comment.py *),Bash(python3 .claude/skills/triage-issue/scripts/parse_gh_issues.py *),Bash(python3 .claude/skills/triage-issue/scripts/detect_prompt_injection.py *),Bash(python3 .claude/skills/triage-issue/scripts/write_job_summary.py *)"
76+
77+
- name: Post triage job summary
78+
if: always()
79+
run: |
80+
EXEC_FILE="${{ steps.triage.outputs.execution_file }}"
81+
if [ -z "$EXEC_FILE" ] || [ ! -f "$EXEC_FILE" ]; then
82+
EXEC_FILE="${RUNNER_TEMP}/claude-execution-output.json"
83+
fi
84+
if [ ! -f "$EXEC_FILE" ]; then
85+
EXEC_FILE="${GITHUB_WORKSPACE}/../../_temp/claude-execution-output.json"
86+
fi
87+
if [ -f "$EXEC_FILE" ]; then
88+
python3 .claude/skills/triage-issue/scripts/write_job_summary.py "$EXEC_FILE" >> "$GITHUB_STEP_SUMMARY"
89+
else
90+
echo "## Claude Triage Run" >> "$GITHUB_STEP_SUMMARY"
91+
echo "" >> "$GITHUB_STEP_SUMMARY"
92+
echo "No execution output file found. Run may have been skipped or failed before writing output." >> "$GITHUB_STEP_SUMMARY"
93+
fi

0 commit comments

Comments
 (0)