@ranger1/dx 0.1.48 → 0.1.49

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.
@@ -15,19 +15,70 @@
15
15
  # Stdout contract: print exactly one JSON object and nothing else.
16
16
 
17
17
  import json
18
- import os
19
18
  import re
20
- import secrets
21
19
  import subprocess
22
20
  import sys
23
21
  from urllib.parse import urlparse
24
22
  from pathlib import Path
25
23
 
26
24
 
25
+ _last_pr_number = None
26
+ _last_round = None
27
+
28
+
29
+ def emit_json(obj):
30
+ # Stdout contract: exactly one JSON line.
31
+ _ = sys.stdout.write(json.dumps(obj, separators=(",", ":"), ensure_ascii=True) + "\n")
32
+
33
+
34
+ def parse_args(argv):
35
+ pr = None
36
+ round_n = 1
37
+
38
+ positional = []
39
+ i = 1
40
+ while i < len(argv):
41
+ a = argv[i]
42
+ if a == "--pr":
43
+ i += 1
44
+ if i >= len(argv):
45
+ return None, None, "PR_NUMBER_NOT_PROVIDED"
46
+ pr = argv[i]
47
+ elif a.startswith("--pr="):
48
+ pr = a.split("=", 1)[1]
49
+ elif a == "--round":
50
+ i += 1
51
+ if i >= len(argv):
52
+ return None, None, "ROUND_INVALID"
53
+ round_n = argv[i]
54
+ elif a.startswith("--round="):
55
+ round_n = a.split("=", 1)[1]
56
+ elif a.startswith("-"):
57
+ return None, None, "INVALID_ARGS"
58
+ else:
59
+ positional.append(a)
60
+ i += 1
61
+
62
+ if pr is None and positional:
63
+ pr = positional[0]
64
+
65
+ pr = (pr or "").strip()
66
+ if not pr.isdigit():
67
+ return None, None, "PR_NUMBER_NOT_PROVIDED"
68
+
69
+ try:
70
+ round_int = int(str(round_n).strip())
71
+ except Exception:
72
+ return int(pr), None, "ROUND_INVALID"
73
+ if round_int < 1:
74
+ return int(pr), None, "ROUND_INVALID"
75
+
76
+ return int(pr), round_int, None
77
+
27
78
  def run(cmd, *, cwd=None, stdout_path=None, stderr_path=None):
28
79
  try:
29
80
  return _run(cmd, cwd=cwd, stdout_path=stdout_path, stderr_path=stderr_path)
30
- except FileNotFoundError as e:
81
+ except FileNotFoundError:
31
82
  # Match common shell semantics for "command not found".
32
83
  return 127
33
84
 
@@ -156,96 +207,197 @@ def write_fixfile(path, issues):
156
207
  sugg = it["suggestion"].replace("\n", "\\n")
157
208
  out.append(f" description: {desc}")
158
209
  out.append(f" suggestion: {sugg}")
159
- p.write_text("\n".join(out) + "\n")
210
+ _ = p.write_text("\n".join(out) + "\n")
160
211
 
161
212
 
162
213
  def main():
163
- if len(sys.argv) < 2:
164
- print(json.dumps({"error": "PR_NUMBER_NOT_PROVIDED"}))
214
+ global _last_pr_number
215
+ global _last_round
216
+
217
+ pr_number, round_n, arg_err = parse_args(sys.argv)
218
+ if arg_err:
219
+ err_obj: dict[str, object] = {"error": arg_err}
220
+ if pr_number is not None:
221
+ err_obj["prNumber"] = pr_number
222
+ if round_n is not None:
223
+ err_obj["round"] = round_n
224
+ emit_json(err_obj)
165
225
  return 1
166
226
 
167
- pr = sys.argv[1].strip()
168
- if not pr.isdigit():
169
- print(json.dumps({"error": "PR_NUMBER_NOT_PROVIDED"}))
170
- return 1
227
+ _last_pr_number = pr_number
228
+ _last_round = round_n
171
229
 
172
- rc, out, _ = run_capture(["git", "rev-parse", "--is-inside-work-tree"])
173
- if rc != 0 or out.strip() != "true":
174
- print(json.dumps({"error": "NOT_A_GIT_REPO"}))
230
+ pr = str(pr_number)
231
+ base_payload: dict[str, object] = {
232
+ "prNumber": pr_number,
233
+ "round": round_n,
234
+ }
235
+
236
+ rc, git_out, _ = run_capture(["git", "rev-parse", "--is-inside-work-tree"])
237
+ if rc != 0 or git_out.strip() != "true":
238
+ emit_json({
239
+ **base_payload,
240
+ "error": "NOT_A_GIT_REPO",
241
+ })
175
242
  return 1
176
243
 
177
244
  host = _detect_git_remote_host() or "github.com"
245
+
246
+ auth_host_used = None
178
247
  rc, gh_out, gh_err = run_capture(["gh", "auth", "status", "--hostname", host])
179
248
  if rc == 127:
180
- print(json.dumps({
249
+ emit_json({
250
+ **base_payload,
181
251
  "error": "GH_CLI_NOT_FOUND",
182
252
  "detail": "gh not found in PATH",
183
253
  "suggestion": "Install GitHub CLI: https://cli.github.com/",
184
- }))
254
+ })
185
255
  return 1
256
+
257
+ if rc == 0:
258
+ auth_host_used = host
259
+ else:
260
+ # If hostname auth fails (e.g. SSH host alias), fall back to default host.
261
+ rc_default, gh_out_default, gh_err_default = run_capture(["gh", "auth", "status"])
262
+ if rc_default == 0:
263
+ # Proceed using default gh auth context; avoid false GH_NOT_AUTHENTICATED.
264
+ auth_host_used = "default"
265
+ rc, gh_out, gh_err = rc_default, gh_out_default, gh_err_default
266
+
186
267
  if rc != 0:
187
268
  detail = (gh_err or gh_out or "").strip()
188
269
  if len(detail) > 4000:
189
270
  detail = detail[-4000:]
190
- print(json.dumps({
271
+ emit_json({
272
+ **base_payload,
191
273
  "error": "GH_NOT_AUTHENTICATED",
192
274
  "host": host,
193
275
  "detail": detail,
194
276
  "suggestion": f"Run: gh auth login --hostname {host}",
195
- }))
277
+ })
196
278
  return 1
197
279
 
198
- rc, pr_json, _ = run_capture(["gh", "pr", "view", pr, "--json", "headRefName,baseRefName,mergeable"])
280
+ if auth_host_used == "default":
281
+ base_payload["authHostUsed"] = auth_host_used
282
+
283
+ rc, pr_json, _ = run_capture([
284
+ "gh",
285
+ "pr",
286
+ "view",
287
+ pr,
288
+ "--json",
289
+ "headRefName,baseRefName,mergeable,headRefOid",
290
+ ])
199
291
  if rc != 0:
200
- print(json.dumps({"error": "PR_NOT_FOUND_OR_NO_ACCESS"}))
292
+ emit_json({
293
+ **base_payload,
294
+ "error": "PR_NOT_FOUND_OR_NO_ACCESS",
295
+ })
201
296
  return 1
202
297
  try:
203
298
  pr_info = json.loads(pr_json)
204
299
  except Exception:
205
- print(json.dumps({"error": "PR_NOT_FOUND_OR_NO_ACCESS"}))
300
+ emit_json({
301
+ **base_payload,
302
+ "error": "PR_NOT_FOUND_OR_NO_ACCESS",
303
+ })
206
304
  return 1
207
305
 
208
306
  head = (pr_info.get("headRefName") or "").strip()
209
307
  base = (pr_info.get("baseRefName") or "").strip()
210
308
  mergeable = (pr_info.get("mergeable") or "").strip()
211
309
 
310
+ head_oid = (pr_info.get("headRefOid") or "").strip()
311
+ if not head_oid:
312
+ emit_json({
313
+ **base_payload,
314
+ "error": "PR_HEAD_OID_NOT_FOUND",
315
+ "headRefName": head,
316
+ "baseRefName": base,
317
+ "mergeable": mergeable,
318
+ })
319
+ return 1
320
+
321
+ head_short = head_oid[:7]
322
+ run_id = f"{pr_number}-{round_n}-{head_short}"
323
+
324
+ payload: dict[str, object] = {
325
+ **base_payload,
326
+ "runId": run_id,
327
+ "headOid": head_oid,
328
+ "headShort": head_short,
329
+ "headRefName": head,
330
+ "baseRefName": base,
331
+ "mergeable": mergeable,
332
+ }
333
+
212
334
  rc, cur_branch, _ = run_capture(["git", "rev-parse", "--abbrev-ref", "HEAD"])
213
335
  if rc != 0:
214
- print(json.dumps({"error": "PR_CHECKOUT_FAILED"}))
336
+ emit_json({
337
+ **payload,
338
+ "error": "PR_CHECKOUT_FAILED",
339
+ })
215
340
  return 1
216
341
  if head and cur_branch.strip() != head:
217
342
  if run(["gh", "pr", "checkout", pr]) != 0:
218
- print(json.dumps({"error": "PR_CHECKOUT_FAILED"}))
343
+ emit_json({
344
+ **payload,
345
+ "error": "PR_CHECKOUT_FAILED",
346
+ })
219
347
  return 1
220
348
 
221
349
  if not base:
222
- rc, out, _ = run_capture(["gh", "repo", "view", "--json", "defaultBranchRef", "--jq", ".defaultBranchRef.name"])
350
+ rc, default_branch_out, _ = run_capture([
351
+ "gh",
352
+ "repo",
353
+ "view",
354
+ "--json",
355
+ "defaultBranchRef",
356
+ "--jq",
357
+ ".defaultBranchRef.name",
358
+ ])
223
359
  if rc == 0:
224
- base = out.strip()
360
+ base = default_branch_out.strip()
225
361
  if not base:
226
- print(json.dumps({"error": "PR_BASE_REF_NOT_FOUND"}))
362
+ emit_json({
363
+ **payload,
364
+ "error": "PR_BASE_REF_NOT_FOUND",
365
+ })
227
366
  return 1
228
367
 
368
+ # baseRefName can be resolved from default branch; keep payload in sync.
369
+ payload["baseRefName"] = base
370
+
229
371
  if run(["git", "fetch", "origin", base]) != 0:
230
- print(json.dumps({"error": "PR_BASE_REF_FETCH_FAILED", "baseRefName": base}))
372
+ emit_json({
373
+ **payload,
374
+ "error": "PR_BASE_REF_FETCH_FAILED",
375
+ "baseRefName": base,
376
+ })
231
377
  return 1
232
378
 
233
379
  if mergeable == "CONFLICTING":
234
- print(json.dumps({"error": "PR_MERGE_CONFLICTS_UNRESOLVED"}))
380
+ emit_json({
381
+ **payload,
382
+ "error": "PR_MERGE_CONFLICTS_UNRESOLVED",
383
+ })
235
384
  return 1
236
385
 
237
- run_id = secrets.token_hex(4)
238
386
  root = repo_root()
239
387
  cache = cache_dir(root)
240
388
  cache.mkdir(parents=True, exist_ok=True)
241
389
 
242
- cache_clear_log = cache / f"precheck-pr{pr}-{run_id}-cache-clear.log"
243
- lint_log = cache / f"precheck-pr{pr}-{run_id}-lint.log"
244
- build_log = cache / f"precheck-pr{pr}-{run_id}-build.log"
245
- meta_log = cache / f"precheck-pr{pr}-{run_id}-meta.json"
246
-
247
- meta_log.write_text(json.dumps({
248
- "pr": int(pr),
390
+ cache_clear_log = cache / f"precheck-{run_id}-cache-clear.log"
391
+ lint_log = cache / f"precheck-{run_id}-lint.log"
392
+ build_log = cache / f"precheck-{run_id}-build.log"
393
+ meta_log = cache / f"precheck-{run_id}-meta.json"
394
+
395
+ _ = meta_log.write_text(json.dumps({
396
+ "prNumber": pr_number,
397
+ "round": round_n,
398
+ "runId": run_id,
399
+ "headOid": head_oid,
400
+ "headShort": head_short,
249
401
  "headRefName": head,
250
402
  "baseRefName": base,
251
403
  "mergeable": mergeable,
@@ -256,7 +408,7 @@ def main():
256
408
 
257
409
  cache_rc = run(["dx", "cache", "clear"], stdout_path=str(cache_clear_log), stderr_path=str(cache_clear_log))
258
410
  if cache_rc != 0:
259
- fix_file = f"precheck-fix-pr{pr}-{run_id}.md"
411
+ fix_file = f"precheck-fix-{run_id}.md"
260
412
  fix_path = cache / fix_file
261
413
  log_tail = tail_text(cache_clear_log)
262
414
  issues = [{
@@ -270,7 +422,11 @@ def main():
270
422
  "suggestion": f"Open log: {repo_relpath(root, cache_clear_log)}",
271
423
  }]
272
424
  write_fixfile(str(fix_path), issues)
273
- print(json.dumps({"ok": False, "fixFile": repo_relpath(root, fix_path)}))
425
+ emit_json({
426
+ **payload,
427
+ "ok": False,
428
+ "fixFile": repo_relpath(root, fix_path),
429
+ })
274
430
  return 1
275
431
 
276
432
  import threading
@@ -288,10 +444,13 @@ def main():
288
444
  t2.join()
289
445
 
290
446
  if results.get("lint", 1) == 0 and results.get("build", 1) == 0:
291
- print(json.dumps({"ok": True}))
447
+ emit_json({
448
+ **payload,
449
+ "ok": True,
450
+ })
292
451
  return 0
293
452
 
294
- fix_file = f"precheck-fix-pr{pr}-{run_id}.md"
453
+ fix_file = f"precheck-fix-{run_id}.md"
295
454
  fix_path = cache / fix_file
296
455
 
297
456
  issues = []
@@ -325,13 +484,22 @@ def main():
325
484
  })
326
485
 
327
486
  write_fixfile(str(fix_path), issues)
328
- print(json.dumps({"ok": False, "fixFile": repo_relpath(root, fix_path)}))
487
+ emit_json({
488
+ **payload,
489
+ "ok": False,
490
+ "fixFile": repo_relpath(root, fix_path),
491
+ })
329
492
  return 1
330
493
 
331
494
 
332
495
  if __name__ == "__main__":
333
496
  try:
334
497
  sys.exit(main())
335
- except Exception as e:
336
- print(json.dumps({"error": "PRECHECK_SCRIPT_FAILED"}))
498
+ except Exception:
499
+ err_obj: dict[str, object] = {"error": "PRECHECK_SCRIPT_FAILED"}
500
+ if _last_pr_number is not None:
501
+ err_obj["prNumber"] = _last_pr_number
502
+ if _last_round is not None:
503
+ err_obj["round"] = _last_round
504
+ emit_json(err_obj)
337
505
  sys.exit(1)