Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

demo-activity-seed.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

demo-activity-seed.tsBlame404 lines · 1 contributor
52ad8b1Claude1/**
2 * Block L3 — extra seeded content + audit rows so the live `/demo` page
3 * has something to render the first time a visitor lands.
4 *
5 * This module is STRICTLY ADDITIVE to `src/lib/demo-seed.ts` (which is
6 * locked under §4.5). The locked seed creates the `demo` user + 3 sample
7 * repos + a handful of issues and one closed PR; this helper layers on:
8 *
9 * - 2 additional issues per repo (one labelled `ai:build`).
10 * - One open PR + one merged PR on `todo-api`.
11 * - One AI-review comment on the open PR (with `AI_REVIEW_MARKER`).
12 * - One `auto_merge.merged` audit row for the merged PR.
13 *
14 * All operations are idempotent — a marker comment, label-name, or
15 * audit-action equality check is consulted before each insert. Re-running
16 * is a no-op. Never throws.
17 *
18 * Wired from `src/index.ts` immediately after `ensureDemoContent()`.
19 */
20
21import { and, eq, sql } from "drizzle-orm";
22import { db } from "../db";
23import {
24 auditLog,
25 issueLabels,
26 issues,
27 labels,
28 prComments,
29 pullRequests,
30 repositories,
31 users,
32} from "../db/schema";
33import { DEMO_USERNAME } from "./demo-seed";
34import { AI_REVIEW_MARKER } from "./ai-review";
35
36const AI_BUILD_LABEL = "ai:build";
37const DEMO_ACTIVITY_MARKER = "<!-- gluecron:demo-activity:v1 -->";
38
39export interface DemoActivitySeedResult {
40 added: {
41 issues: number;
42 labels: number;
43 issueLabels: number;
44 prs: number;
45 prComments: number;
46 auditRows: number;
47 };
48 errors: string[];
49}
50
51interface DemoRepo {
52 id: string;
53 name: string;
54 ownerId: string;
55}
56
57async function loadDemoRepos(): Promise<DemoRepo[]> {
58 try {
59 const [demo] = await db
60 .select({ id: users.id })
61 .from(users)
62 .where(eq(users.username, DEMO_USERNAME))
63 .limit(1);
64 if (!demo) return [];
65 const rows = await db
66 .select({
67 id: repositories.id,
68 name: repositories.name,
69 ownerId: repositories.ownerId,
70 })
71 .from(repositories)
72 .where(eq(repositories.ownerId, demo.id));
73 return rows;
74 } catch {
75 return [];
76 }
77}
78
79async function ensureLabel(
80 repoId: string,
81 name: string,
82 color: string
83): Promise<string | null> {
84 try {
85 const [existing] = await db
86 .select({ id: labels.id })
87 .from(labels)
88 .where(and(eq(labels.repositoryId, repoId), eq(labels.name, name)))
89 .limit(1);
90 if (existing) return existing.id;
91 const [inserted] = await db
92 .insert(labels)
93 .values({
94 repositoryId: repoId,
95 name,
96 color,
97 })
98 .returning({ id: labels.id });
99 return inserted?.id ?? null;
100 } catch {
101 return null;
102 }
103}
104
105async function findIssueByTitle(
106 repoId: string,
107 title: string
108): Promise<{ id: string } | null> {
109 try {
110 const [row] = await db
111 .select({ id: issues.id })
112 .from(issues)
113 .where(and(eq(issues.repositoryId, repoId), eq(issues.title, title)))
114 .limit(1);
115 return row ?? null;
116 } catch {
117 return null;
118 }
119}
120
121async function ensureIssueLabel(
122 issueId: string,
123 labelId: string
124): Promise<boolean> {
125 try {
126 const [existing] = await db
127 .select({ id: issueLabels.id })
128 .from(issueLabels)
129 .where(
130 and(
131 eq(issueLabels.issueId, issueId),
132 eq(issueLabels.labelId, labelId)
133 )
134 )
135 .limit(1);
136 if (existing) return false;
137 await db.insert(issueLabels).values({ issueId, labelId });
138 return true;
139 } catch {
140 return false;
141 }
142}
143
144async function findPrByTitle(
145 repoId: string,
146 title: string
147): Promise<{ id: string; number: number; state: string; mergedAt: Date | null } | null> {
148 try {
149 const [row] = await db
150 .select({
151 id: pullRequests.id,
152 number: pullRequests.number,
153 state: pullRequests.state,
154 mergedAt: pullRequests.mergedAt,
155 })
156 .from(pullRequests)
157 .where(
158 and(
159 eq(pullRequests.repositoryId, repoId),
160 eq(pullRequests.title, title)
161 )
162 )
163 .limit(1);
164 return row ?? null;
165 } catch {
166 return null;
167 }
168}
169
170/**
171 * Idempotently seed extra demo content + activity for the live `/demo` page.
172 * Never throws. Re-runnable.
173 */
174export async function ensureDemoActivity(): Promise<DemoActivitySeedResult> {
175 const result: DemoActivitySeedResult = {
176 added: {
177 issues: 0,
178 labels: 0,
179 issueLabels: 0,
180 prs: 0,
181 prComments: 0,
182 auditRows: 0,
183 },
184 errors: [],
185 };
186
187 const repos = await loadDemoRepos();
188 if (repos.length === 0) return result;
189
190 // Find the demo user id for `authorId` on issues/PRs/comments.
191 const demoUserId = repos[0].ownerId;
192
193 for (const repo of repos) {
194 // ── Issue 1: an `ai:build`-labelled issue per repo.
195 const aiIssueTitle = `[AI] Add /metrics endpoint to ${repo.name}`;
196 const aiIssueBody =
197 `Expose a Prometheus-style \`/metrics\` endpoint with request counts ` +
198 `and p95 latency. Auto-build candidate; the gluecron autopilot should ` +
199 `pick this up and open a draft PR.`;
200 try {
201 let existing = await findIssueByTitle(repo.id, aiIssueTitle);
202 if (!existing) {
203 const [inserted] = await db
204 .insert(issues)
205 .values({
206 repositoryId: repo.id,
207 authorId: demoUserId,
208 title: aiIssueTitle,
209 body: aiIssueBody,
210 state: "open",
211 })
212 .returning({ id: issues.id });
213 if (inserted) {
214 existing = { id: inserted.id };
215 result.added.issues += 1;
216 }
217 }
218 if (existing) {
219 const labelId = await ensureLabel(repo.id, AI_BUILD_LABEL, "#8c6dff");
220 if (labelId) {
221 // Track label count only on first insert per repo.
222 // ensureLabel doesn't expose "newly inserted" so this is an
223 // approximation; the counter is for observability only.
224 if (await ensureIssueLabel(existing.id, labelId)) {
225 result.added.issueLabels += 1;
226 }
227 }
228 }
229 } catch (err: any) {
230 result.errors.push(
231 `ai:build issue(${repo.name}): ${String(err?.message || err)}`
232 );
233 }
234
235 // ── Issue 2: a plain triage issue (one extra per repo so each repo has
236 // 3+ issues total once the locked seed's single issue is counted).
237 const triageTitle = `[triage] Investigate flaky tests in ${repo.name}`;
238 try {
239 const existing = await findIssueByTitle(repo.id, triageTitle);
240 if (!existing) {
241 await db.insert(issues).values({
242 repositoryId: repo.id,
243 authorId: demoUserId,
244 title: triageTitle,
245 body:
246 "Several CI runs have shown intermittent failures. Likely a " +
247 "timing-sensitive assertion. Worth a 15-minute investigation.",
248 state: "open",
249 });
250 result.added.issues += 1;
251 }
252 } catch (err: any) {
253 result.errors.push(
254 `triage issue(${repo.name}): ${String(err?.message || err)}`
255 );
256 }
257 }
258
259 // ── PR seeding + AI review + audit row are scoped to todo-api.
260 const todoApi = repos.find((r) => r.name === "todo-api");
261 if (todoApi) {
262 // Open PR (carries an AI review comment).
263 const openPrTitle = "feat: add /metrics endpoint";
264 try {
265 let openPr = await findPrByTitle(todoApi.id, openPrTitle);
266 if (!openPr) {
267 const [inserted] = await db
268 .insert(pullRequests)
269 .values({
270 repositoryId: todoApi.id,
271 authorId: demoUserId,
272 title: openPrTitle,
273 body:
274 "Adds a Prometheus-style `/metrics` endpoint. " +
275 DEMO_ACTIVITY_MARKER,
276 state: "open",
277 baseBranch: "main",
278 headBranch: "demo/metrics-endpoint",
279 })
280 .returning({
281 id: pullRequests.id,
282 number: pullRequests.number,
283 state: pullRequests.state,
284 mergedAt: pullRequests.mergedAt,
285 });
286 if (inserted) {
287 openPr = inserted;
288 result.added.prs += 1;
289 }
290 }
291
292 if (openPr) {
293 // Add an AI review comment with the canonical marker so the page's
294 // "AI reviews posted today" tile has content. Idempotent — we
295 // refuse to add a second AI review on this PR.
296 const [existingReview] = await db
297 .select({ id: prComments.id })
298 .from(prComments)
299 .where(
300 and(
301 eq(prComments.pullRequestId, openPr.id),
302 eq(prComments.isAiReview, true)
303 )
304 )
305 .limit(1);
306 if (!existingReview) {
307 await db.insert(prComments).values({
308 pullRequestId: openPr.id,
309 authorId: demoUserId,
310 isAiReview: true,
311 body:
312 `${AI_REVIEW_MARKER}\n## AI Code Review\n\n` +
313 "**Verdict: looks good.** The new `/metrics` handler is small, " +
314 "side-effect-free, and adds a Prometheus-format counter for " +
315 "request totals. No blocking findings.",
316 });
317 result.added.prComments += 1;
318 }
319 }
320 } catch (err: any) {
321 result.errors.push(
322 `open PR(todo-api): ${String(err?.message || err)}`
323 );
324 }
325
326 // Merged PR + matching audit row.
327 const mergedPrTitle = "chore: bump hono to ^4.6.0";
328 try {
329 let mergedPr = await findPrByTitle(todoApi.id, mergedPrTitle);
330 if (!mergedPr) {
331 const now = new Date();
332 const [inserted] = await db
333 .insert(pullRequests)
334 .values({
335 repositoryId: todoApi.id,
336 authorId: demoUserId,
337 title: mergedPrTitle,
338 body:
339 "Routine dep bump — Hono 4.6.0 ships small bugfixes. " +
340 DEMO_ACTIVITY_MARKER,
341 state: "merged",
342 baseBranch: "main",
343 headBranch: "demo/hono-4-6",
344 mergedAt: now,
345 mergedBy: demoUserId,
346 closedAt: now,
347 })
348 .returning({
349 id: pullRequests.id,
350 number: pullRequests.number,
351 state: pullRequests.state,
352 mergedAt: pullRequests.mergedAt,
353 });
354 if (inserted) {
355 mergedPr = inserted;
356 result.added.prs += 1;
357 }
358 }
359
360 if (mergedPr) {
361 // Check for an existing auto_merge.merged audit row on this PR.
362 const [existingAudit] = await db
363 .select({ id: auditLog.id })
364 .from(auditLog)
365 .where(
366 and(
367 eq(auditLog.action, "auto_merge.merged"),
368 eq(auditLog.repositoryId, todoApi.id),
369 eq(auditLog.targetId, mergedPr.id)
370 )
371 )
372 .limit(1);
373 if (!existingAudit) {
374 await db.insert(auditLog).values({
375 repositoryId: todoApi.id,
376 action: "auto_merge.merged",
377 targetType: "pull_request",
378 targetId: mergedPr.id,
379 metadata: JSON.stringify({
380 prNumber: mergedPr.number,
381 baseBranch: "main",
382 headBranch: "demo/hono-4-6",
383 source: "demo-activity-seed",
384 }),
385 });
386 result.added.auditRows += 1;
387 }
388 }
389 } catch (err: any) {
390 result.errors.push(
391 `merged PR(todo-api): ${String(err?.message || err)}`
392 );
393 }
394 }
395
396 return result;
397}
398
399/** Test-only re-exports. */
400export const __test = {
401 AI_BUILD_LABEL,
402 DEMO_ACTIVITY_MARKER,
403 loadDemoRepos,
404};