canvasxpress-cli 71.1.0 → 71.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
|
Binary file
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Triage a freshly opened GitHub issue with Claude.
|
|
3
|
+
|
|
4
|
+
Reads the issue from the environment (populated by the workflow), asks Claude to
|
|
5
|
+
decide whether it is a well-defined, meritorious issue, then acts via the `gh`
|
|
6
|
+
CLI:
|
|
7
|
+
|
|
8
|
+
* needs-info -> post a comment asking for the specific missing detail and add
|
|
9
|
+
the `needs-more-info` label.
|
|
10
|
+
* valid -> add the `bug` or `enhancement` label and assign @neuhausi.
|
|
11
|
+
|
|
12
|
+
The model is instructed to return a single strict-JSON object so we never parse
|
|
13
|
+
prose. Any failure (missing key, bad JSON, API error) is non-fatal: we leave the
|
|
14
|
+
issue untouched and exit 0 so a transient hiccup never blocks a contributor.
|
|
15
|
+
"""
|
|
16
|
+
|
|
17
|
+
import json
|
|
18
|
+
import os
|
|
19
|
+
import subprocess
|
|
20
|
+
import sys
|
|
21
|
+
|
|
22
|
+
import anthropic
|
|
23
|
+
|
|
24
|
+
# ---- Decision contract ------------------------------------------------------
|
|
25
|
+
# Claude must return exactly this shape. Keeping it tiny makes it reliable.
|
|
26
|
+
SYSTEM_PROMPT = """\
|
|
27
|
+
You triage incoming GitHub issues for an open-source software project. Judge ONLY
|
|
28
|
+
the issue as written against the repository context. Decide:
|
|
29
|
+
|
|
30
|
+
1. Is it well-defined? A well-defined issue states what happened or what is wanted
|
|
31
|
+
clearly enough to act on. A BUG report needs: what was done, what was expected,
|
|
32
|
+
and what happened instead (ideally a reproducer, code, version, or error text).
|
|
33
|
+
A FEATURE request needs: the capability wanted and why / the use case.
|
|
34
|
+
2. Does it have merit? It is on-topic for THIS repository and is not spam, a
|
|
35
|
+
duplicate-looking one-liner, a support question better asked elsewhere, or empty.
|
|
36
|
+
|
|
37
|
+
Return a SINGLE JSON object, no markdown fence, with these fields:
|
|
38
|
+
"decision": "valid" | "needs_info"
|
|
39
|
+
"type": "bug" | "feature" | null // the kind, when decision is "valid"; else null
|
|
40
|
+
"reasons": [string, ...] // brief, why you decided this
|
|
41
|
+
"comment": string // the GitHub comment body to post (see below)
|
|
42
|
+
|
|
43
|
+
Rules for "comment":
|
|
44
|
+
* If decision is "needs_info": write a friendly, specific request listing exactly
|
|
45
|
+
what the reporter should add (e.g. a minimal reproducer, the exact input/prompt,
|
|
46
|
+
expected vs actual output, versions, error text). Reference what IS present so it
|
|
47
|
+
does not feel generic. Markdown is fine.
|
|
48
|
+
* If decision is "valid": write a short acknowledgement confirming it was triaged
|
|
49
|
+
as a bug or feature and assigned for review. One or two sentences.
|
|
50
|
+
Never invent facts about the project. Be concise and kind.
|
|
51
|
+
"""
|
|
52
|
+
|
|
53
|
+
|
|
54
|
+
def getenv(name, default=""):
|
|
55
|
+
return os.environ.get(name, default) or default
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
def run_gh(args):
|
|
59
|
+
"""Run a `gh` command, returning (ok, output). Never raises."""
|
|
60
|
+
try:
|
|
61
|
+
result = subprocess.run(
|
|
62
|
+
["gh"] + args,
|
|
63
|
+
capture_output=True,
|
|
64
|
+
text=True,
|
|
65
|
+
check=False,
|
|
66
|
+
)
|
|
67
|
+
if result.returncode != 0:
|
|
68
|
+
sys.stderr.write("gh %s failed: %s\n" % (" ".join(args), result.stderr.strip()))
|
|
69
|
+
return False, result.stderr
|
|
70
|
+
return True, result.stdout
|
|
71
|
+
except FileNotFoundError:
|
|
72
|
+
sys.stderr.write("gh CLI not found on runner\n")
|
|
73
|
+
return False, ""
|
|
74
|
+
|
|
75
|
+
|
|
76
|
+
def ensure_label(repo, name, color, description):
|
|
77
|
+
"""Create the label if it does not already exist (idempotent)."""
|
|
78
|
+
run_gh([
|
|
79
|
+
"label", "create", name,
|
|
80
|
+
"--repo", repo,
|
|
81
|
+
"--color", color,
|
|
82
|
+
"--description", description,
|
|
83
|
+
"--force", # update color/description if it exists, don't error
|
|
84
|
+
])
|
|
85
|
+
|
|
86
|
+
|
|
87
|
+
def ask_claude(model, repo_context, title, body):
|
|
88
|
+
"""Return the parsed decision dict, or None on any failure."""
|
|
89
|
+
client = anthropic.Anthropic() # reads ANTHROPIC_API_KEY from env
|
|
90
|
+
user_content = (
|
|
91
|
+
"Repository context:\n%s\n\n"
|
|
92
|
+
"Issue title:\n%s\n\n"
|
|
93
|
+
"Issue body:\n%s\n"
|
|
94
|
+
) % (repo_context, title, body or "(empty)")
|
|
95
|
+
|
|
96
|
+
try:
|
|
97
|
+
message = client.messages.create(
|
|
98
|
+
model=model,
|
|
99
|
+
max_tokens=1024,
|
|
100
|
+
system=SYSTEM_PROMPT,
|
|
101
|
+
messages=[{"role": "user", "content": user_content}],
|
|
102
|
+
)
|
|
103
|
+
except Exception as exc: # noqa: BLE001 - triage must never hard-fail
|
|
104
|
+
sys.stderr.write("Anthropic API error: %s\n" % exc)
|
|
105
|
+
return None
|
|
106
|
+
|
|
107
|
+
text = "".join(block.text for block in message.content if block.type == "text").strip()
|
|
108
|
+
# Tolerate an accidental ```json fence.
|
|
109
|
+
if text.startswith("```"):
|
|
110
|
+
text = text.strip("`")
|
|
111
|
+
text = text[text.find("{"): text.rfind("}") + 1]
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
decision = json.loads(text)
|
|
115
|
+
except json.JSONDecodeError:
|
|
116
|
+
sys.stderr.write("Could not parse model JSON:\n%s\n" % text)
|
|
117
|
+
return None
|
|
118
|
+
|
|
119
|
+
if decision.get("decision") not in ("valid", "needs_info"):
|
|
120
|
+
sys.stderr.write("Unexpected decision value: %r\n" % decision.get("decision"))
|
|
121
|
+
return None
|
|
122
|
+
return decision
|
|
123
|
+
|
|
124
|
+
|
|
125
|
+
def main():
|
|
126
|
+
repo = getenv("GITHUB_REPOSITORY")
|
|
127
|
+
number = getenv("ISSUE_NUMBER")
|
|
128
|
+
title = getenv("ISSUE_TITLE")
|
|
129
|
+
body = getenv("ISSUE_BODY")
|
|
130
|
+
assignee = getenv("ASSIGNEE", "neuhausi")
|
|
131
|
+
model = getenv("TRIAGE_MODEL", "claude-sonnet-5-5")
|
|
132
|
+
repo_context = getenv("REPO_CONTEXT", "A CanvasXpress companion repository.")
|
|
133
|
+
|
|
134
|
+
if not repo or not number:
|
|
135
|
+
sys.stderr.write("Missing GITHUB_REPOSITORY or ISSUE_NUMBER; nothing to do.\n")
|
|
136
|
+
return 0
|
|
137
|
+
|
|
138
|
+
decision = ask_claude(model, repo_context, title, body)
|
|
139
|
+
if decision is None:
|
|
140
|
+
# Soft-fail: leave the issue for a human rather than guessing.
|
|
141
|
+
return 0
|
|
142
|
+
|
|
143
|
+
comment = decision.get("comment") or ""
|
|
144
|
+
print("Decision: %s (type=%s)" % (decision.get("decision"), decision.get("type")))
|
|
145
|
+
for reason in decision.get("reasons", []):
|
|
146
|
+
print(" - %s" % reason)
|
|
147
|
+
|
|
148
|
+
# Make sure the labels we use exist, with friendly colors.
|
|
149
|
+
ensure_label(repo, "needs-more-info", "d4c5f9", "Awaiting more detail from the reporter")
|
|
150
|
+
ensure_label(repo, "bug", "d73a4a", "Something isn't working")
|
|
151
|
+
ensure_label(repo, "enhancement", "a2eeef", "New feature or request")
|
|
152
|
+
|
|
153
|
+
if decision["decision"] == "needs_info":
|
|
154
|
+
if comment:
|
|
155
|
+
run_gh(["issue", "comment", number, "--repo", repo, "--body", comment])
|
|
156
|
+
run_gh(["issue", "edit", number, "--repo", repo, "--add-label", "needs-more-info"])
|
|
157
|
+
return 0
|
|
158
|
+
|
|
159
|
+
# decision == "valid"
|
|
160
|
+
label = "bug" if decision.get("type") == "bug" else "enhancement"
|
|
161
|
+
run_gh(["issue", "edit", number, "--repo", repo, "--add-label", label])
|
|
162
|
+
run_gh(["issue", "edit", number, "--repo", repo, "--add-assignee", assignee])
|
|
163
|
+
if comment:
|
|
164
|
+
run_gh(["issue", "comment", number, "--repo", repo, "--body", comment])
|
|
165
|
+
return 0
|
|
166
|
+
|
|
167
|
+
|
|
168
|
+
if __name__ == "__main__":
|
|
169
|
+
sys.exit(main())
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
name: Issue triage
|
|
2
|
+
|
|
3
|
+
# Prototype: when an issue is opened (or reopened), ask Claude whether it is a
|
|
4
|
+
# well-defined, meritorious issue. If not, post a comment asking for the missing
|
|
5
|
+
# detail and label it `needs-more-info`. If it is, assign it to @neuhausi and
|
|
6
|
+
# label it `bug` or `enhancement`.
|
|
7
|
+
#
|
|
8
|
+
# Requires repo secret ANTHROPIC_API_KEY. GITHUB_TOKEN is provided automatically.
|
|
9
|
+
|
|
10
|
+
on:
|
|
11
|
+
issues:
|
|
12
|
+
types: [opened, reopened]
|
|
13
|
+
|
|
14
|
+
# Least-privilege: we only read repo contents and write to the issue.
|
|
15
|
+
permissions:
|
|
16
|
+
contents: read
|
|
17
|
+
issues: write
|
|
18
|
+
|
|
19
|
+
concurrency:
|
|
20
|
+
# One triage per issue at a time; a reopen supersedes an in-flight run.
|
|
21
|
+
group: issue-triage-${{ github.event.issue.number }}
|
|
22
|
+
cancel-in-progress: true
|
|
23
|
+
|
|
24
|
+
jobs:
|
|
25
|
+
triage:
|
|
26
|
+
runs-on: ubuntu-latest
|
|
27
|
+
# Never triage pull requests (they also surface as "issues" in some events).
|
|
28
|
+
if: ${{ github.event.issue.pull_request == null }}
|
|
29
|
+
steps:
|
|
30
|
+
- name: Checkout (for the triage script)
|
|
31
|
+
uses: actions/checkout@v4
|
|
32
|
+
|
|
33
|
+
- name: Set up Python
|
|
34
|
+
uses: actions/setup-python@v5
|
|
35
|
+
with:
|
|
36
|
+
python-version: "3.12"
|
|
37
|
+
|
|
38
|
+
- name: Install the Anthropic SDK
|
|
39
|
+
run: pip install --quiet "anthropic>=0.40"
|
|
40
|
+
|
|
41
|
+
- name: Triage the issue
|
|
42
|
+
env:
|
|
43
|
+
ANTHROPIC_API_KEY: ${{ secrets.ANTHROPIC_API_KEY }}
|
|
44
|
+
GH_TOKEN: ${{ github.token }}
|
|
45
|
+
GITHUB_REPOSITORY: ${{ github.repository }}
|
|
46
|
+
ISSUE_NUMBER: ${{ github.event.issue.number }}
|
|
47
|
+
ISSUE_TITLE: ${{ github.event.issue.title }}
|
|
48
|
+
ISSUE_BODY: ${{ github.event.issue.body }}
|
|
49
|
+
ISSUE_AUTHOR: ${{ github.event.issue.user.login }}
|
|
50
|
+
# Override per-repo if desired; defaults are sensible.
|
|
51
|
+
ASSIGNEE: neuhausi
|
|
52
|
+
TRIAGE_MODEL: claude-sonnet-5-5
|
|
53
|
+
# One-line description of THIS repo so Claude judges merit in context.
|
|
54
|
+
REPO_CONTEXT: >-
|
|
55
|
+
canvasxpress-cli is the command-line tool for generating CanvasXpress charts from data files.
|
|
56
|
+
run: python .github/scripts/triage_issue.py
|