@ranger1/dx 0.1.28 → 0.1.29
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.
|
@@ -87,7 +87,6 @@ Round: 2
|
|
|
87
87
|
要求:只依赖 prompt 中的 `fixFile`;不要重新拉取/生成评审意见。
|
|
88
88
|
|
|
89
89
|
- 用 bash 读取 `fixFile`(例如 `cat "$fixFile"`)
|
|
90
|
-
- 从 `## IssuesToFix` 中解析条目,按 `priority` 排序并按 `id` 去重
|
|
91
90
|
- 解析失败则返回 `INVALID_FIX_FILE`
|
|
92
91
|
|
|
93
92
|
### 2. 逐项修复(No Scope Creep)
|
|
@@ -96,10 +95,6 @@ Round: 2
|
|
|
96
95
|
- 每个修复必须能明确对应到原问题的 `id`
|
|
97
96
|
- 无法修复时必须记录原因(例如:缺少上下文、超出本 PR 范围、需要产品决策、需要数据库迁移等)
|
|
98
97
|
|
|
99
|
-
执行前检查(强制):
|
|
100
|
-
|
|
101
|
-
- 当前分支禁止是 `main`/`master`(应已由 pr-context 切到 PR 分支)
|
|
102
|
-
|
|
103
98
|
### 3. 提交策略
|
|
104
99
|
|
|
105
100
|
- 强制:每个 findingId 单独一个提交(一个 findingId 对应一个 commit)
|
|
@@ -117,6 +112,7 @@ Round: 2
|
|
|
117
112
|
- 不确定的问题降级为拒绝修复,并写清 `reason`(不要“猜”)
|
|
118
113
|
- 修改尽量小:最小 diff、保持既有风格与约定
|
|
119
114
|
- 修改项目里的json/jsonc文件的时候,使用python脚本进行修改,禁止手动拼接字符串,防止格式错误
|
|
115
|
+
- 修复完成之后,调用 dx lint 和 dx build all 确保编译通过
|
|
120
116
|
|
|
121
117
|
## 重要约束(强制)
|
|
122
118
|
|
|
@@ -15,8 +15,266 @@ tools:
|
|
|
15
15
|
|
|
16
16
|
- `PR #<number>`
|
|
17
17
|
|
|
18
|
+
## 一键脚本(推荐,省 token)
|
|
19
|
+
|
|
20
|
+
把「环境/权限校验、PR 信息读取、checkout、base 分支 fetch、cache clear、lint+build、失败时写 fixFile、最终 JSON 输出」压到一次 `bash` 调用里执行。只有当脚本返回 merge 冲突相关错误时,才进入下面第 3 步做内容级合并。
|
|
21
|
+
|
|
22
|
+
注意:脚本会把所有命令输出写入 `~/.opencode/cache/`,stdout 只打印最终单一 JSON。
|
|
23
|
+
|
|
24
|
+
```bash
|
|
25
|
+
# 用法:把 PR 号填到 PR_NUMBER
|
|
26
|
+
PR_NUMBER=123 python3 - <<'PY'
|
|
27
|
+
import json
|
|
28
|
+
import os
|
|
29
|
+
import re
|
|
30
|
+
import secrets
|
|
31
|
+
import subprocess
|
|
32
|
+
from pathlib import Path
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def run(cmd, *, cwd=None, stdout_path=None, stderr_path=None):
|
|
36
|
+
if stdout_path and stderr_path and stdout_path == stderr_path:
|
|
37
|
+
f = open(stdout_path, "wb")
|
|
38
|
+
try:
|
|
39
|
+
p = subprocess.run(cmd, cwd=cwd, stdout=f, stderr=f)
|
|
40
|
+
return p.returncode
|
|
41
|
+
finally:
|
|
42
|
+
f.close()
|
|
43
|
+
|
|
44
|
+
stdout_f = open(stdout_path, "wb") if stdout_path else subprocess.DEVNULL
|
|
45
|
+
stderr_f = open(stderr_path, "wb") if stderr_path else subprocess.DEVNULL
|
|
46
|
+
try:
|
|
47
|
+
p = subprocess.run(cmd, cwd=cwd, stdout=stdout_f, stderr=stderr_f)
|
|
48
|
+
return p.returncode
|
|
49
|
+
finally:
|
|
50
|
+
if stdout_path:
|
|
51
|
+
stdout_f.close()
|
|
52
|
+
if stderr_path:
|
|
53
|
+
stderr_f.close()
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
def run_capture(cmd, *, cwd=None):
|
|
57
|
+
p = subprocess.run(cmd, cwd=cwd, stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True)
|
|
58
|
+
return p.returncode, p.stdout, p.stderr
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
def tail_text(path, max_lines=200, max_chars=12000):
|
|
62
|
+
try:
|
|
63
|
+
data = Path(path).read_text(errors="replace")
|
|
64
|
+
except Exception:
|
|
65
|
+
return "(failed to read log)"
|
|
66
|
+
lines = data.splitlines()
|
|
67
|
+
tail = "\n".join(lines[-max_lines:])
|
|
68
|
+
if len(tail) > max_chars:
|
|
69
|
+
tail = tail[-max_chars:]
|
|
70
|
+
return tail
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def first_file_line(text):
|
|
74
|
+
# Best-effort: match "path:line:col" or "path:line".
|
|
75
|
+
for m in re.finditer(r"^([^\s:]+\.[a-zA-Z0-9]+):(\d+)(?::(\d+))?\b", text, flags=re.M):
|
|
76
|
+
file = m.group(1)
|
|
77
|
+
line = int(m.group(2))
|
|
78
|
+
return file, line
|
|
79
|
+
return None, None
|
|
80
|
+
|
|
81
|
+
|
|
82
|
+
def write_fixfile(path, issues):
|
|
83
|
+
p = Path(path)
|
|
84
|
+
p.parent.mkdir(parents=True, exist_ok=True)
|
|
85
|
+
# Minimal schema for pr-fix parser.
|
|
86
|
+
out = ["## IssuesToFix", ""]
|
|
87
|
+
for it in issues:
|
|
88
|
+
out.append(f"- id: {it['id']}")
|
|
89
|
+
out.append(f" priority: {it['priority']}")
|
|
90
|
+
out.append(f" category: {it['category']}")
|
|
91
|
+
out.append(f" file: {it['file']}")
|
|
92
|
+
out.append(f" line: {it['line'] if it['line'] is not None else 'null'}")
|
|
93
|
+
out.append(f" title: {it['title']}")
|
|
94
|
+
# Keep as one line; caller should truncate.
|
|
95
|
+
desc = it["description"].replace("\n", "\\n")
|
|
96
|
+
sugg = it["suggestion"].replace("\n", "\\n")
|
|
97
|
+
out.append(f" description: {desc}")
|
|
98
|
+
out.append(f" suggestion: {sugg}")
|
|
99
|
+
p.write_text("\n".join(out) + "\n")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
def main():
|
|
103
|
+
pr = os.environ.get("PR_NUMBER", "").strip()
|
|
104
|
+
if not pr.isdigit():
|
|
105
|
+
print(json.dumps({"error": "PR_NUMBER_NOT_PROVIDED"}))
|
|
106
|
+
return
|
|
107
|
+
|
|
108
|
+
# Step 1: must be in git repo.
|
|
109
|
+
rc, out, _ = run_capture(["git", "rev-parse", "--is-inside-work-tree"])
|
|
110
|
+
if rc != 0 or out.strip() != "true":
|
|
111
|
+
print(json.dumps({"error": "NOT_A_GIT_REPO"}))
|
|
112
|
+
return
|
|
113
|
+
|
|
114
|
+
# Step 1: gh auth.
|
|
115
|
+
rc = run(["gh", "auth", "status"]) # devnull
|
|
116
|
+
if rc != 0:
|
|
117
|
+
print(json.dumps({"error": "GH_NOT_AUTHENTICATED"}))
|
|
118
|
+
return
|
|
119
|
+
|
|
120
|
+
# Read PR info.
|
|
121
|
+
rc, pr_json, _ = run_capture(["gh", "pr", "view", pr, "--json", "headRefName,baseRefName,mergeable"])
|
|
122
|
+
if rc != 0:
|
|
123
|
+
print(json.dumps({"error": "PR_NOT_FOUND_OR_NO_ACCESS"}))
|
|
124
|
+
return
|
|
125
|
+
try:
|
|
126
|
+
pr_info = json.loads(pr_json)
|
|
127
|
+
except Exception:
|
|
128
|
+
print(json.dumps({"error": "PR_NOT_FOUND_OR_NO_ACCESS"}))
|
|
129
|
+
return
|
|
130
|
+
|
|
131
|
+
head = (pr_info.get("headRefName") or "").strip()
|
|
132
|
+
base = (pr_info.get("baseRefName") or "").strip()
|
|
133
|
+
mergeable = (pr_info.get("mergeable") or "").strip()
|
|
134
|
+
|
|
135
|
+
# Step 2: checkout PR branch if needed.
|
|
136
|
+
rc, cur_branch, _ = run_capture(["git", "rev-parse", "--abbrev-ref", "HEAD"])
|
|
137
|
+
if rc != 0:
|
|
138
|
+
print(json.dumps({"error": "PR_CHECKOUT_FAILED"}))
|
|
139
|
+
return
|
|
140
|
+
if head and cur_branch.strip() != head:
|
|
141
|
+
if run(["gh", "pr", "checkout", pr]) != 0:
|
|
142
|
+
print(json.dumps({"error": "PR_CHECKOUT_FAILED"}))
|
|
143
|
+
return
|
|
144
|
+
|
|
145
|
+
# Step 3 pre-req: resolve base ref.
|
|
146
|
+
if not base:
|
|
147
|
+
rc, out, _ = run_capture(["gh", "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"])
|
|
148
|
+
if rc == 0:
|
|
149
|
+
base = out.strip()
|
|
150
|
+
if not base:
|
|
151
|
+
print(json.dumps({"error": "PR_BASE_REF_NOT_FOUND"}))
|
|
152
|
+
return
|
|
153
|
+
|
|
154
|
+
# Fetch base.
|
|
155
|
+
if run(["git", "fetch", "origin", base]) != 0:
|
|
156
|
+
ok = False
|
|
157
|
+
for fallback in ("main", "master"):
|
|
158
|
+
if fallback == base:
|
|
159
|
+
continue
|
|
160
|
+
if run(["git", "fetch", "origin", fallback]) == 0:
|
|
161
|
+
base = fallback
|
|
162
|
+
ok = True
|
|
163
|
+
break
|
|
164
|
+
if not ok:
|
|
165
|
+
print(json.dumps({"error": "PR_BASE_REF_FETCH_FAILED"}))
|
|
166
|
+
return
|
|
167
|
+
|
|
168
|
+
# If mergeable reports conflict, ask agent to go to conflict-resolution step.
|
|
169
|
+
if mergeable == "CONFLICTING":
|
|
170
|
+
print(json.dumps({"error": "PR_MERGE_CONFLICTS_UNRESOLVED"}))
|
|
171
|
+
return
|
|
172
|
+
|
|
173
|
+
# Step 4: cache clear then lint + build (in parallel), logs to cache.
|
|
174
|
+
run_id = secrets.token_hex(4)
|
|
175
|
+
cache = Path.home() / ".opencode" / "cache"
|
|
176
|
+
cache.mkdir(parents=True, exist_ok=True)
|
|
177
|
+
cache_clear_log = cache / f"precheck-pr{pr}-{run_id}-cache-clear.log"
|
|
178
|
+
lint_log = cache / f"precheck-pr{pr}-{run_id}-lint.log"
|
|
179
|
+
build_log = cache / f"precheck-pr{pr}-{run_id}-build.log"
|
|
180
|
+
meta_log = cache / f"precheck-pr{pr}-{run_id}-meta.json"
|
|
181
|
+
|
|
182
|
+
# Keep meta for debugging (not printed to stdout).
|
|
183
|
+
meta_log.write_text(json.dumps({
|
|
184
|
+
"pr": int(pr),
|
|
185
|
+
"headRefName": head,
|
|
186
|
+
"baseRefName": base,
|
|
187
|
+
"mergeable": mergeable,
|
|
188
|
+
"cacheClearLog": str(cache_clear_log),
|
|
189
|
+
"lintLog": str(lint_log),
|
|
190
|
+
"buildLog": str(build_log),
|
|
191
|
+
}, indent=2) + "\n")
|
|
192
|
+
|
|
193
|
+
cache_rc = run(["dx", "cache", "clear"], stdout_path=str(cache_clear_log), stderr_path=str(cache_clear_log))
|
|
194
|
+
if cache_rc != 0:
|
|
195
|
+
fix_file = f"~/.opencode/cache/precheck-fix-pr{pr}-{run_id}.md"
|
|
196
|
+
fix_path = str(cache / f"precheck-fix-pr{pr}-{run_id}.md")
|
|
197
|
+
log_tail = tail_text(cache_clear_log)
|
|
198
|
+
issues = [{
|
|
199
|
+
"id": "PRE-001",
|
|
200
|
+
"priority": "P1",
|
|
201
|
+
"category": "quality",
|
|
202
|
+
"file": "<unknown>",
|
|
203
|
+
"line": None,
|
|
204
|
+
"title": "dx cache clear failed",
|
|
205
|
+
"description": log_tail,
|
|
206
|
+
"suggestion": f"Open log: {cache_clear_log}",
|
|
207
|
+
}]
|
|
208
|
+
write_fixfile(fix_path, issues)
|
|
209
|
+
print(json.dumps({"ok": False, "fixFile": fix_file}))
|
|
210
|
+
return
|
|
211
|
+
|
|
212
|
+
import threading
|
|
213
|
+
|
|
214
|
+
results = {}
|
|
215
|
+
|
|
216
|
+
def worker(name, cmd, log_path):
|
|
217
|
+
results[name] = run(cmd, stdout_path=str(log_path), stderr_path=str(log_path))
|
|
218
|
+
|
|
219
|
+
t1 = threading.Thread(target=worker, args=("lint", ["dx", "lint"], lint_log))
|
|
220
|
+
t2 = threading.Thread(target=worker, args=("build", ["dx", "build", "all"], build_log))
|
|
221
|
+
t1.start(); t2.start(); t1.join(); t2.join()
|
|
222
|
+
|
|
223
|
+
if results.get("lint", 1) == 0 and results.get("build", 1) == 0:
|
|
224
|
+
print(json.dumps({"ok": True}))
|
|
225
|
+
return
|
|
226
|
+
|
|
227
|
+
fix_file = f"~/.opencode/cache/precheck-fix-pr{pr}-{run_id}.md"
|
|
228
|
+
fix_path = str(cache / f"precheck-fix-pr{pr}-{run_id}.md")
|
|
229
|
+
|
|
230
|
+
issues = []
|
|
231
|
+
i = 1
|
|
232
|
+
if results.get("lint", 1) != 0:
|
|
233
|
+
log_tail = tail_text(lint_log)
|
|
234
|
+
file, line = first_file_line(log_tail)
|
|
235
|
+
issues.append({
|
|
236
|
+
"id": f"PRE-{i:03d}",
|
|
237
|
+
"priority": "P1",
|
|
238
|
+
"category": "lint",
|
|
239
|
+
"file": file or "<unknown>",
|
|
240
|
+
"line": line,
|
|
241
|
+
"title": "dx lint failed",
|
|
242
|
+
"description": log_tail,
|
|
243
|
+
"suggestion": f"Open log: {lint_log}",
|
|
244
|
+
})
|
|
245
|
+
i += 1
|
|
246
|
+
if results.get("build", 1) != 0:
|
|
247
|
+
log_tail = tail_text(build_log)
|
|
248
|
+
file, line = first_file_line(log_tail)
|
|
249
|
+
issues.append({
|
|
250
|
+
"id": f"PRE-{i:03d}",
|
|
251
|
+
"priority": "P0",
|
|
252
|
+
"category": "build",
|
|
253
|
+
"file": file or "<unknown>",
|
|
254
|
+
"line": line,
|
|
255
|
+
"title": "dx build all failed",
|
|
256
|
+
"description": log_tail,
|
|
257
|
+
"suggestion": f"Open log: {build_log}",
|
|
258
|
+
})
|
|
259
|
+
|
|
260
|
+
write_fixfile(fix_path, issues)
|
|
261
|
+
print(json.dumps({"ok": False, "fixFile": fix_file}))
|
|
262
|
+
|
|
263
|
+
|
|
264
|
+
if __name__ == "__main__":
|
|
265
|
+
try:
|
|
266
|
+
main()
|
|
267
|
+
except Exception:
|
|
268
|
+
# Keep stdout contract.
|
|
269
|
+
print(json.dumps({"error": "PRECHECK_SCRIPT_FAILED"}))
|
|
270
|
+
|
|
271
|
+
PY
|
|
272
|
+
```
|
|
273
|
+
|
|
18
274
|
## 要做的事(按顺序)
|
|
19
275
|
|
|
276
|
+
优先使用上面的「一键脚本」完成第 1/2/4/5 步;仅当脚本返回 merge 冲突相关错误时,再进入第 3 步进行内容级合并(完成后重跑脚本)。
|
|
277
|
+
|
|
20
278
|
1. 校验环境/权限
|
|
21
279
|
|
|
22
280
|
- 必须在 git 仓库内,否则输出 `{"error":"NOT_A_GIT_REPO"}`
|
|
@@ -68,8 +326,9 @@ tools:
|
|
|
68
326
|
|
|
69
327
|
4. 预检:lint + build
|
|
70
328
|
|
|
329
|
+
- 运行 `dx cache clear`
|
|
71
330
|
- 运行 `dx lint`
|
|
72
|
-
- 运行 `dx build
|
|
331
|
+
- 运行 `dx build all`
|
|
73
332
|
|
|
74
333
|
5. 若 lint/build 失败:生成 fixFile(Markdown)并返回失败
|
|
75
334
|
|