feat(projects): PR + CI/checks status chip via gh (W3)

Per-project chip: PR state · N checks passing · mergeable — glance from the phone,
re-engage only when it's red, instead of dropping into a terminal for `gh pr checks`.

- src/http/gh.ts (new): single `gh pr view --json number,state,title,url,isDraft,
  mergeable,headRefName,baseRefName,statusCheckRollup` (execFile, no shell, cwd =
  isValidGitDir repo, timeout + maxBuffer). summarizeChecks rolls the mixed
  CheckRun/StatusContext rollup into {total,passing,failing,pending}. Never throws —
  degrades to not-installed (ENOENT) / unauthenticated / no-pr / error / disabled.
  Cache keyed by repoPath+branch (reuses projectScanTtlMs) with in-flight dedupe.
- src/types.ts: additive PrStatus/PrAvailability/PrCheckSummary; config GH_ENABLED
  (default on) + GH_TIMEOUT_MS (8s).
- GET /projects/pr?path= (read-only, isValidGitDir); public/gh-chip.ts render-only
  chip mounted in the project detail header (git repos only).

Read-only, host's own authed gh (same trust as the shell). No untrusted argv (only
the validated cwd); gh stdout/token never logged; the attacker-controllable PR title
is rendered inert via textContent (SEC-H4). Verified: typecheck + build:web clean,
1816 pass (gh tests 118). The 1 red is the known real-PTY ring-buffer timeout flake.
This commit is contained in:
Yaojia Wang
2026-07-12 21:01:18 +02:00
parent b119c31019
commit 7551f8a4b2
12 changed files with 1423 additions and 0 deletions

View File

@@ -1534,6 +1534,64 @@ body {
flex: none;
}
/* W3: PR + CI status chip (mirrors the .proj-branch chip look). */
.proj-pr-host {
display: inline-flex;
flex: none;
}
.proj-pr-chip {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 11px;
border-radius: 5px;
padding: 2px 7px;
white-space: nowrap;
max-width: 260px;
color: var(--text-faint);
background: var(--accent-soft);
}
.proj-pr-chip .proj-pr-title {
overflow: hidden;
text-overflow: ellipsis;
color: var(--text-faint);
}
.proj-pr-loading {
opacity: 0.6;
}
/* PR-state modifiers */
.proj-pr-open .proj-pr-label {
color: var(--green);
}
.proj-pr-draft .proj-pr-label {
color: var(--text-faint);
}
.proj-pr-merged .proj-pr-label {
color: var(--accent);
}
.proj-pr-closed .proj-pr-label {
color: var(--red);
}
.proj-pr-none,
.proj-pr-unavailable {
color: var(--text-faint);
background: transparent;
}
/* Checks-rollup modifiers (colour the whole chip label) */
.proj-pr-checks-ok .proj-pr-label {
color: var(--green);
}
.proj-pr-checks-fail .proj-pr-label {
color: var(--red);
}
.proj-pr-checks-pending .proj-pr-label {
color: var(--amber);
}
/* Mergeable = conflicting */
.proj-pr-conflict {
box-shadow: inset 0 0 0 1px var(--red);
}
/* Meta line (last-active time) */
.proj-meta {
font-size: 11px;