Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
Commit240c477unknown_key

feat(pr+issue): CI status checks on PR detail, linked PRs on issue detail

feat(pr+issue): CI status checks on PR detail, linked PRs on issue detail

- PR detail shows external CI statuses (Block J8 commit_statuses table)
  in a new card below gate checks. Loaded in parallel with gate check
  evaluation. Shows context, description, state pill (pending/success/
  failure/error), and "Details" link when targetUrl is set.
- Issue detail shows linked pull requests: scans PR title+body for
  #<issueNumber> references (ILIKE search scoped to the repo) and
  renders them as a linked-PR panel below the comment thread, with
  open/draft/merged/closed state icons and badges.

https://claude.ai/code/session_01ACsT2Pc8GRoZwZRF8SK68Y
Claude committed on May 28, 2026Parent: b558f23
2 files changed+1254240c477613f99bbfc4f2a39b4f82f4eacf3a6e7b
2 changed files+125−4
Modifiedsrc/routes/issues.tsx+66−1View fileUnifiedSplit
33 */
44
55import { Hono } from "hono";
6import { eq, and, desc, asc, sql, ilike, inArray } from "drizzle-orm";
6import { eq, and, desc, asc, sql, ilike, inArray, or } from "drizzle-orm";
77import { db } from "../db";
88import {
99 issues,
1212 users,
1313 labels,
1414 issueLabels,
15 pullRequests,
1516} from "../db/schema";
1617import { Layout } from "../views/layout";
1718import { RepoHeader, RepoNav } from "../views/components";
628629 color: var(--text);
629630 font-size: 13.5px;
630631 }
632
633 /* ─── Linked PRs ─── */
634 .iss-linked-prs { margin-top: 16px; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
635 .iss-linked-prs-head { display: flex; align-items: center; justify-content: space-between; padding: 10px 16px; background: var(--bg-elevated); border-bottom: 1px solid var(--border); font-size: 13px; font-weight: 600; }
636 .iss-linked-pr-row { display: flex; align-items: center; gap: 10px; padding: 9px 16px; border-bottom: 1px solid var(--border); font-size: 13px; text-decoration: none; color: inherit; }
637 .iss-linked-pr-row:last-child { border-bottom: none; }
638 .iss-linked-pr-row:hover { background: var(--bg-hover); }
639 .iss-linked-pr-icon.is-open { color: #34d399; }
640 .iss-linked-pr-icon.is-merged { color: #a78bfa; }
641 .iss-linked-pr-icon.is-closed { color: #8b949e; }
642 .iss-linked-pr-icon.is-draft { color: #8b949e; }
643 .iss-linked-pr-title { flex: 1 1 auto; min-width: 0; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
644 .iss-linked-pr-num { color: var(--text-muted); font-size: 12px; }
645 .iss-linked-pr-state { font-size: 11px; font-weight: 600; padding: 1px 7px; border-radius: 9999px; }
646 .iss-linked-pr-state.is-open { color: #34d399; background: rgba(52,211,153,0.10); }
647 .iss-linked-pr-state.is-merged { color: #a78bfa; background: rgba(167,139,250,0.10); }
648 .iss-linked-pr-state.is-closed { color: #8b949e; background: var(--bg-tertiary); }
649 .iss-linked-pr-state.is-draft { color: #8b949e; background: var(--bg-tertiary); }
631650`;
632651
633652// Pre-rendered <style> tag (constant, reused per request).
11901209 (user.id === resolved.owner.id || user.id === issue.authorId);
11911210 const info = c.req.query("info");
11921211
1212 // Linked PRs — find PRs in this repo whose title or body reference this issue number.
1213 const issueRefPattern = `%#${issue.number}%`;
1214 const linkedPrs = await db
1215 .select({
1216 number: pullRequests.number,
1217 title: pullRequests.title,
1218 state: pullRequests.state,
1219 isDraft: pullRequests.isDraft,
1220 })
1221 .from(pullRequests)
1222 .where(
1223 and(
1224 eq(pullRequests.repositoryId, resolved.repo.id),
1225 or(
1226 ilike(pullRequests.title, issueRefPattern),
1227 ilike(pullRequests.body, issueRefPattern),
1228 )
1229 )
1230 )
1231 .orderBy(desc(pullRequests.createdAt))
1232 .limit(8);
1233
11931234 return c.html(
11941235 <Layout
11951236 title={`${issue.title} #${issue.number} — ${ownerName}/${repoName}`}
13101351 })}
13111352 </div>
13121353
1354 {linkedPrs.length > 0 && (
1355 <div class="iss-linked-prs">
1356 <div class="iss-linked-prs-head">
1357 <span>Linked pull requests</span>
1358 <span>{linkedPrs.length}</span>
1359 </div>
1360 {linkedPrs.map((lpr) => {
1361 const prState = lpr.isDraft ? "draft" : lpr.state;
1362 const prIcon = prState === "merged" ? "⮬" : prState === "closed" ? "✕" : prState === "draft" ? "◌" : "○";
1363 return (
1364 <a
1365 href={`/${ownerName}/${repoName}/pulls/${lpr.number}`}
1366 class="iss-linked-pr-row"
1367 >
1368 <span class={`iss-linked-pr-icon is-${prState}`} aria-hidden="true">{prIcon}</span>
1369 <span class="iss-linked-pr-title">{lpr.title}</span>
1370 <span class="iss-linked-pr-num">#{lpr.number}</span>
1371 <span class={`iss-linked-pr-state is-${prState}`}>{prState}</span>
1372 </a>
1373 );
1374 })}
1375 </div>
1376 )}
1377
13131378 {user && (
13141379 <form
13151380 class="issues-composer"
Modifiedsrc/routes/pulls.tsx+59−3View fileUnifiedSplit
8585 commitsBetween,
8686} from "../git/repository";
8787import type { GitDiffFile, GitCommit } from "../git/repository";
88import { listStatuses } from "../lib/commit-statuses";
89import type { CommitStatus } from "../db/schema";
8890import { html } from "hono/html";
8991import {
9092 getPreviewForBranch,
13421344 .prs-edit-actions { display: flex; gap: 8px; }
13431345 .prs-edit-save-btn { padding: 7px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; background: var(--accent); color: #fff; border: none; cursor: pointer; }
13441346 .prs-edit-cancel-btn { padding: 7px 16px; border-radius: 8px; font-size: 13px; font-weight: 600; background: var(--bg-elevated); color: var(--text); border: 1px solid var(--border); cursor: pointer; }
1347
1348 /* ─── CI status checks ─── */
1349 .prs-ci-card { margin-top: 14px; border: 1px solid var(--border); border-radius: 12px; overflow: hidden; }
1350 .prs-ci-head { display: flex; align-items: center; justify-content: space-between; padding: 12px 16px; background: var(--bg-elevated); border-bottom: 1px solid var(--border); }
1351 .prs-ci-head h3 { margin: 0; font-size: 14px; font-weight: 600; color: var(--text); }
1352 .prs-ci-summary { font-size: 12px; color: var(--text-muted); }
1353 .prs-ci-row { display: flex; align-items: center; gap: 12px; padding: 10px 16px; border-bottom: 1px solid var(--border); }
1354 .prs-ci-row:last-child { border-bottom: none; }
1355 .prs-ci-icon { flex: 0 0 auto; width: 18px; height: 18px; display: inline-flex; align-items: center; justify-content: center; border-radius: 50%; font-size: 11px; font-weight: 700; }
1356 .prs-ci-icon.is-success { background: rgba(52,211,153,0.20); color: #34d399; }
1357 .prs-ci-icon.is-pending { background: rgba(251,191,36,0.20); color: #fbbf24; }
1358 .prs-ci-icon.is-failure, .prs-ci-icon.is-error { background: rgba(248,113,113,0.20); color: #f87171; }
1359 .prs-ci-context { flex: 1 1 auto; font-size: 13px; font-weight: 500; color: var(--text); }
1360 .prs-ci-desc { font-size: 12px; color: var(--text-muted); }
1361 .prs-ci-pill { font-size: 11px; font-weight: 700; padding: 2px 8px; border-radius: 9999px; }
1362 .prs-ci-pill.is-success { color: #34d399; background: rgba(52,211,153,0.10); }
1363 .prs-ci-pill.is-pending { color: #fbbf24; background: rgba(251,191,36,0.10); }
1364 .prs-ci-pill.is-failure, .prs-ci-pill.is-error { color: #f87171; background: rgba(248,113,113,0.10); }
1365 .prs-ci-link { font-size: 12px; color: var(--accent); text-decoration: none; }
1366 .prs-ci-link:hover { text-decoration: underline; }
13451367`;
13461368
13471369/**
30903112
30913113 // Get gate check status for open PRs
30923114 let gateChecks: GateCheckResult[] = [];
3115 let ciStatuses: CommitStatus[] = [];
30933116 if (pr.state === "open") {
30943117 const headSha = await resolveRef(ownerName, repoName, pr.headBranch);
30953118 if (headSha) {
30973120 const aiApproved = aiComments.length === 0 || aiComments.some(
30983121 ({ comment }) => comment.body.includes("**Approved**")
30993122 );
3100 const gateResult = await runAllGateChecks(
3101 ownerName, repoName, pr.baseBranch, pr.headBranch, headSha, aiApproved
3102 );
3123 const [gateResult, fetchedCiStatuses] = await Promise.all([
3124 runAllGateChecks(
3125 ownerName, repoName, pr.baseBranch, pr.headBranch, headSha, aiApproved
3126 ),
3127 listStatuses(resolved.repo.id, headSha).catch(() => [] as CommitStatus[]),
3128 ]);
31033129 gateChecks = gateResult.checks;
3130 ciStatuses = fetchedCiStatuses;
31043131 }
31053132 }
31063133
37933820 </div>
37943821 )}
37953822
3823 {pr.state === "open" && ciStatuses.length > 0 && (
3824 <div class="prs-ci-card">
3825 <div class="prs-ci-head">
3826 <h3>CI checks</h3>
3827 <span class="prs-ci-summary">
3828 {ciStatuses.filter(s => s.state === "success").length}/{ciStatuses.length} passing
3829 </span>
3830 </div>
3831 {ciStatuses.map((status) => {
3832 const iconGlyph = status.state === "success" ? "✓" : status.state === "pending" ? "…" : "✗";
3833 return (
3834 <div class="prs-ci-row">
3835 <span class={`prs-ci-icon is-${status.state}`} aria-hidden="true">{iconGlyph}</span>
3836 <span class="prs-ci-context">{status.context}</span>
3837 {status.description && (
3838 <span class="prs-ci-desc">{status.description}</span>
3839 )}
3840 <span class={`prs-ci-pill is-${status.state}`}>
3841 {status.state}
3842 </span>
3843 {status.targetUrl && (
3844 <a href={status.targetUrl} class="prs-ci-link" target="_blank" rel="noopener noreferrer">Details</a>
3845 )}
3846 </div>
3847 );
3848 })}
3849 </div>
3850 )}
3851
37963852 {/* ─── Merge area / state-aware action card ─────────────── */}
37973853 {user && pr.state === "open" && (
37983854 <div
37993855