forked from NSPG13/agent-bounties
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathgithub_issue_plan_comment.py
More file actions
264 lines (219 loc) · 8.1 KB
/
Copy pathgithub_issue_plan_comment.py
File metadata and controls
264 lines (219 loc) · 8.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
#!/usr/bin/env python3
"""Plan and publish the sticky paid-bounty validation comment for GitHub issues."""
from __future__ import annotations
import argparse
import io
import json
import os
import pathlib
import subprocess
import sys
from typing import Dict, List, Mapping, Optional, TextIO, Tuple
from _shared.github_actions import (
append_step_summary as append_github_summary,
cargo_body_path,
find_executable,
json_field,
publish_issue_comment,
repo_root,
)
MARKER = "<!-- agent-bounties-plan -->"
class UserError(RuntimeError):
pass
def script_repo_root() -> pathlib.Path:
return repo_root(__file__)
def read_json_field(value: object, field: str) -> object:
return json_field(value, field, UserError, "planner output missing field: {field}")
def write_issue_files(env: Mapping[str, str], tmp_dir: pathlib.Path) -> Tuple[Dict[str, object], pathlib.Path]:
event_path = env.get("GITHUB_EVENT_PATH")
if not event_path:
raise UserError("GITHUB_EVENT_PATH is required")
event = json.loads(pathlib.Path(event_path).read_text(encoding="utf-8"))
issue = event.get("issue") or {}
repository = event.get("repository") or {}
body_file = tmp_dir / "paid-bounty-issue-body.md"
body_file.write_text(issue.get("body") or "", encoding="utf-8")
meta: Dict[str, object] = {
"repo": env.get("GITHUB_REPOSITORY") or repository.get("full_name") or "",
"number": issue.get("number"),
"title": issue.get("title") or "",
"url": issue.get("html_url") or "",
}
missing = [key for key, value in meta.items() if value in ("", None)]
if missing:
raise UserError(f"issue event missing required metadata: {', '.join(missing)}")
meta_file = tmp_dir / "paid-bounty-issue-meta.json"
meta_file.write_text(json.dumps(meta), encoding="utf-8")
return meta, body_file
def run_github_plan(
env: Mapping[str, str],
workspace: pathlib.Path,
meta: Mapping[str, object],
body_file: pathlib.Path,
) -> str:
cargo_path = find_executable(["cargo", "cargo.exe"])
if not cargo_path:
raise UserError("cargo is required to plan a paid-bounty issue")
body_arg = cargo_body_path(body_file, cargo_path)
result = subprocess.run(
[
cargo_path,
"run",
"-p",
"cli",
"--",
"github-plan",
"--repository",
str(meta["repo"]),
"--issue-url",
str(meta["url"]),
"--title",
str(meta["title"]),
"--body-file",
body_arg,
],
cwd=workspace,
env=dict(env),
text=True,
stdout=subprocess.PIPE,
stderr=None,
check=False,
)
if result.returncode != 0:
raise UserError(f"github-plan failed with exit code {result.returncode}")
return result.stdout
def render_comment(plan: Mapping[str, object]) -> str:
conclusion = str(read_json_field(plan, "check.conclusion"))
title = str(read_json_field(plan, "check.title"))
summary = str(read_json_field(plan, "check.summary"))
details = str(read_json_field(plan, "check.text"))
ready = conclusion == "Success"
if ready and title == "Autonomous bounty metadata ready":
status_line = "This metadata is valid. Canonical contract events, not GitHub, control funding and claimability."
elif ready:
status_line = "This issue can be routed into a funded bounty."
else:
status_line = "This issue needs edits before it can be routed into a funded bounty."
return "\n".join(
[
MARKER,
f"### Agent bounty validation: {conclusion}",
"",
summary,
"",
status_line,
"",
"<details><summary>Planner output</summary>",
"",
"```",
details,
"```",
"",
"</details>",
"",
]
)
def append_step_summary(env: Mapping[str, str], comment: str) -> None:
append_github_summary(env, "Agent bounty validation", comment)
def publish_comment(env: Mapping[str, str], meta: Mapping[str, object], comment: str) -> None:
publish_issue_comment(
env,
meta["repo"],
meta["number"],
MARKER,
comment,
"paid-bounty-comment.md",
"gh is required to publish the paid-bounty validation comment",
UserError,
)
def run_from_env(env: Mapping[str, str], stdout: TextIO) -> int:
repo_root = script_repo_root()
workspace = pathlib.Path(env.get("GITHUB_WORKSPACE") or repo_root).resolve()
tmp_dir = pathlib.Path(env.get("RUNNER_TEMP") or workspace / "target" / "tmp").resolve()
tmp_dir.mkdir(parents=True, exist_ok=True)
meta, body_file = write_issue_files(env, tmp_dir)
plan_json = run_github_plan(env, workspace, meta, body_file)
plan_file = tmp_dir / "paid-bounty-plan.json"
plan_file.write_text(plan_json, encoding="utf-8")
plan = json.loads(plan_json)
comment = render_comment(plan)
comment_file = tmp_dir / "paid-bounty-comment.md"
comment_file.write_text(comment, encoding="utf-8")
append_step_summary(env, comment)
if env.get("DRY_RUN") == "1":
stdout.write(plan_json)
if not plan_json.endswith("\n"):
stdout.write("\n")
stdout.write("\n")
stdout.write(comment)
return 0
publish_comment(env, meta, comment)
return 0
def run_self_test() -> int:
repo_root = script_repo_root()
tmp_dir = repo_root / "target" / "tmp"
tmp_dir.mkdir(parents=True, exist_ok=True)
body = (repo_root / "examples" / "github-paid-bounty-issue.md").read_text(encoding="utf-8")
event = {
"repository": {"full_name": "agent-bounties/agent-bounties"},
"issue": {
"number": 1,
"title": "[bounty]: Fix CI",
"html_url": "https://github.com/agent-bounties/agent-bounties/issues/1",
"body": body,
},
}
event_path = tmp_dir / "github-issue-event.json"
event_path.write_text(json.dumps(event), encoding="utf-8")
env = dict(os.environ)
env.update(
{
"GITHUB_EVENT_PATH": str(event_path),
"GITHUB_REPOSITORY": "agent-bounties/agent-bounties",
"GITHUB_WORKSPACE": str(repo_root),
"RUNNER_TEMP": str(tmp_dir),
"DRY_RUN": "1",
}
)
buffer = io.StringIO()
run_from_env(env, buffer)
output = buffer.getvalue()
output_path = tmp_dir / "github-issue-plan-comment.out"
output_path.write_text(output, encoding="utf-8")
required = [MARKER, "Agent bounty validation: Success", "This issue can be routed into a funded bounty."]
missing = [needle for needle in required if needle not in output]
if missing:
raise UserError(f"self-test output missing: {', '.join(missing)}")
autonomous = render_comment(
{
"check": {
"conclusion": "Success",
"title": "Autonomous bounty metadata ready",
"summary": "Canonical contract events control funding and claims.",
"text": "Amount: 1 USDC\nCanonical contract: funding pending.",
}
}
)
if "Canonical contract events, not GitHub" not in autonomous:
raise UserError("self-test autonomous metadata implied that funding was ready")
if "can be routed into a funded bounty" in autonomous:
raise UserError("self-test autonomous metadata used legacy funding-ready copy")
print(f"GitHub issue plan comment dry-run passed: {output_path}")
return 0
def main() -> int:
parser = argparse.ArgumentParser(description=__doc__)
parser.add_argument(
"--self-test",
action="store_true",
help="run a deterministic dry-run using examples/github-paid-bounty-issue.md",
)
args = parser.parse_args()
try:
if args.self_test:
return run_self_test()
return run_from_env(os.environ, sys.stdout)
except UserError as error:
print(error, file=sys.stderr)
return 1
if __name__ == "__main__":
raise SystemExit(main())