@trim21/personal-pi-extensions 0.0.183 → 0.0.185

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/gh-readonly.ts +117 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.0.183",
3
+ "version": "0.0.185",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -206,11 +206,22 @@ const jobRunSchema = Type.Object({
206
206
  name: Type.String(),
207
207
  status: Type.String(),
208
208
  conclusion: Type.Union([Type.String(), Type.Null()]),
209
+ html_url: Type.Optional(Type.String()),
209
210
  steps: Type.Array(stepSchema),
210
211
  });
211
212
 
212
213
  const jobsResponseSchema = Type.Object({ jobs: Type.Array(jobRunSchema) });
213
214
 
215
+ const prHeadSchema = Type.Object({ headRefOid: Type.String() });
216
+
217
+ const workflowRunSchema = Type.Object({
218
+ id: Type.Number(),
219
+ name: Type.String(),
220
+ html_url: Type.String(),
221
+ });
222
+
223
+ const workflowRunsSchema = Type.Object({ workflow_runs: Type.Array(workflowRunSchema) });
224
+
214
225
  function truncate(
215
226
  text: string,
216
227
  maxLines = 2000,
@@ -401,6 +412,15 @@ async function resolveRepo(
401
412
  return nameWithOwner;
402
413
  }
403
414
 
415
+ /** Job conclusions that count as "did not succeed" for CI result reporting. */
416
+ const FAILED_JOB_CONCLUSIONS = new Set([
417
+ "failure",
418
+ "timed_out",
419
+ "action_required",
420
+ "startup_failure",
421
+ "cancelled",
422
+ ]);
423
+
404
424
  export function statusIcon(conclusion: string | null): string {
405
425
  switch (conclusion) {
406
426
  case "success": {
@@ -1311,29 +1331,115 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
1311
1331
  const args = ["pr", "checks", String(number), ...repoArgs(repo), "--watch"];
1312
1332
  if (fail_fast) args.push("--fail-fast");
1313
1333
 
1334
+ // gh 的退出码语义不可靠:checks 失败时返回 exit 1(SilentError),挂起时
1335
+ // 返回 exit 8(PendingError),且失败详情只在非结构化的 stdout 表格里。
1336
+ // 因此 watch 退出后直接用 Actions API 抓取该 PR head 提交关联的所有
1337
+ // workflow job,以 job 的真实 conclusion 为准判断成功/失败。
1314
1338
  const result = await runGh(args, { cwd: ctx.cwd, signal, timeout: 600_000 });
1339
+ if (result.killed) {
1340
+ throw new Error(
1341
+ result.reason === "timeout"
1342
+ ? "gh pr checks --watch timed out after 10 minutes"
1343
+ : "gh pr checks --watch was aborted",
1344
+ );
1345
+ }
1315
1346
 
1316
- const exitCode = result.code;
1317
- const stdout = result.stdout;
1318
- const stderr = result.stderr;
1347
+ const effectiveRepo = await resolveRepo(repo, signal, ctx.cwd, params);
1348
+ const prOut = await ghExec(
1349
+ ["pr", "view", String(number), "--repo", effectiveRepo, "--json", "headRefOid"],
1350
+ { cwd: ctx.cwd, signal, input: params },
1351
+ );
1352
+ const { headRefOid } = Value.Parse(prHeadSchema, JSON.parse(prOut));
1353
+
1354
+ onUpdate?.({
1355
+ content: [{ type: "text", text: `Fetching workflow jobs for PR #${number}...` }],
1356
+ details: {},
1357
+ });
1358
+
1359
+ const runsOut = await ghExec(
1360
+ ["api", `/repos/${effectiveRepo}/actions/runs?head_sha=${headRefOid}&per_page=100`],
1361
+ { cwd: ctx.cwd, signal, input: params },
1362
+ );
1363
+ const { workflow_runs } = Value.Parse(workflowRunsSchema, JSON.parse(runsOut));
1364
+
1365
+ const failedJobs: {
1366
+ runId: number;
1367
+ runName: string;
1368
+ runUrl: string;
1369
+ jobId: number;
1370
+ jobName: string;
1371
+ conclusion: string;
1372
+ jobUrl?: string;
1373
+ }[] = [];
1374
+ let totalJobs = 0;
1375
+
1376
+ for (const run of workflow_runs) {
1377
+ const jobsOut = await ghExec(
1378
+ ["api", `/repos/${effectiveRepo}/actions/runs/${run.id}/jobs?per_page=100`],
1379
+ { cwd: ctx.cwd, signal, input: params },
1380
+ );
1381
+ const { jobs } = Value.Parse(jobsResponseSchema, JSON.parse(jobsOut));
1382
+ totalJobs += jobs.length;
1383
+
1384
+ for (const job of jobs) {
1385
+ if (!job.conclusion || FAILED_JOB_CONCLUSIONS.has(job.conclusion)) {
1386
+ failedJobs.push({
1387
+ runId: run.id,
1388
+ runName: run.name,
1389
+ runUrl: run.html_url,
1390
+ jobId: job.id,
1391
+ jobName: job.name,
1392
+ conclusion: job.conclusion ?? "in_progress",
1393
+ jobUrl: job.html_url,
1394
+ });
1395
+ }
1396
+ }
1397
+ }
1319
1398
 
1320
- // Exit code 2 means one or more checks failed
1321
- if (exitCode === 2) {
1399
+ if (totalJobs === 0) {
1322
1400
  return {
1323
1401
  content: [
1324
- { type: "text", text: `## PR #${number} CI Checks - FAILED\n\n${stdout}\n${stderr}` },
1402
+ {
1403
+ type: "text",
1404
+ text: `## PR #${number} CI Checks\n\nNo workflow runs found for head commit ${headRefOid.slice(0, 7)}.`,
1405
+ },
1325
1406
  ],
1326
- details: { status: "failure", exitCode, input: params },
1407
+ details: { status: "no-jobs", totalJobs: 0, input: params },
1327
1408
  };
1328
1409
  }
1329
1410
 
1330
- if (exitCode !== 0) {
1331
- throw new Error(`gh pr checks --watch failed: ${stderr || `exit code ${exitCode}`}`);
1411
+ if (failedJobs.length > 0) {
1412
+ const lines = failedJobs.map(
1413
+ (j) =>
1414
+ `- ${statusIcon(j.conclusion)} **${j.jobName}** (${j.conclusion}) — [job #${j.jobId}](${j.jobUrl ?? j.runUrl})\n` +
1415
+ ` - workflow: [${j.runName} (#${j.runId})](${j.runUrl})`,
1416
+ );
1417
+ return {
1418
+ content: [
1419
+ {
1420
+ type: "text",
1421
+ text:
1422
+ `## PR #${number} CI Checks - FAILED\n\n` +
1423
+ `${failedJobs.length} of ${totalJobs} job(s) did not succeed:\n\n${lines.join("\n")}`,
1424
+ },
1425
+ ],
1426
+ details: {
1427
+ status: "failure",
1428
+ totalJobs,
1429
+ failedJobs,
1430
+ input: params,
1431
+ },
1432
+ };
1332
1433
  }
1333
1434
 
1334
1435
  return {
1335
- content: [{ type: "text", text: `## PR #${number} CI Checks - PASSED\n\n${stdout}` }],
1336
- details: { status: "success", exitCode: 0, input: params },
1436
+ content: [
1437
+ {
1438
+ type: "text",
1439
+ text: `## PR #${number} CI Checks - PASSED\n\nAll ${totalJobs} job(s) succeeded.`,
1440
+ },
1441
+ ],
1442
+ details: { status: "success", totalJobs, input: params },
1337
1443
  };
1338
1444
  },
1339
1445
  });