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:
36
src/types.ts
36
src/types.ts
@@ -61,6 +61,9 @@ export interface Config {
|
||||
readonly diffTimeoutMs: number; // DIFF_TIMEOUT_MS, default 2000
|
||||
readonly diffMaxBytes: number; // DIFF_MAX_BYTES, default 2MB
|
||||
readonly diffMaxFiles: number; // DIFF_MAX_FILES, default 300
|
||||
// W3 PR + CI status chip (gh)
|
||||
readonly ghEnabled: boolean; // GH_ENABLED, default true (false → never spawns gh)
|
||||
readonly ghTimeoutMs: number; // GH_TIMEOUT_MS, default 8000 (network — larger than diff)
|
||||
// B2 statusLine telemetry
|
||||
readonly statuslineTtlMs: number; // STATUSLINE_TTL_MS, default 30000
|
||||
// B3 git worktree creation
|
||||
@@ -535,6 +538,39 @@ export interface DiffResult {
|
||||
base?: string; // echoed when the diff was against a base revision (?base=<rev>)
|
||||
}
|
||||
|
||||
/* ── W3 PR + CI status chip (gh) ── */
|
||||
|
||||
/** Why a PrStatus has (or lacks) PR data. Drives the FE chip's degraded text. */
|
||||
export type PrAvailability =
|
||||
| 'ok' // a PR exists for the current branch; fields below are populated
|
||||
| 'no-pr' // gh works but the branch has no PR (or no remote/default repo)
|
||||
| 'not-installed' // `gh` binary not found on PATH (ENOENT)
|
||||
| 'unauthenticated' // gh present but not logged in (needs `gh auth login`)
|
||||
| 'disabled' // GH_ENABLED=0 — feature off, never spawns gh
|
||||
| 'error'; // gh spawned but failed for another reason (timeout, etc.)
|
||||
|
||||
/** Rolled-up CI check counts from gh's statusCheckRollup (CheckRun + StatusContext). */
|
||||
export interface PrCheckSummary {
|
||||
total: number;
|
||||
passing: number; // CheckRun conclusion SUCCESS/NEUTRAL/SKIPPED | StatusContext SUCCESS
|
||||
failing: number; // FAILURE/TIMED_OUT/CANCELLED/ACTION_REQUIRED | ERROR/FAILURE
|
||||
pending: number; // QUEUED/IN_PROGRESS/WAITING | PENDING/EXPECTED
|
||||
}
|
||||
|
||||
/** GET /projects/pr result. Only present-when-'ok' fields are optional. */
|
||||
export interface PrStatus {
|
||||
availability: PrAvailability;
|
||||
number?: number;
|
||||
title?: string;
|
||||
url?: string;
|
||||
state?: 'open' | 'closed' | 'merged'; // lower-cased from gh OPEN/CLOSED/MERGED
|
||||
isDraft?: boolean;
|
||||
mergeable?: 'mergeable' | 'conflicting' | 'unknown'; // lower-cased from gh
|
||||
headRefName?: string;
|
||||
baseRefName?: string;
|
||||
checks?: PrCheckSummary;
|
||||
}
|
||||
|
||||
/* ── B3 worktree creation (§3.5) ── */
|
||||
|
||||
/** Result of POST /projects/worktree (B3). `error` carries a safe message only
|
||||
|
||||
Reference in New Issue
Block a user