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.
| 52ad8b1 | 1 | /** |
| 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 | ||
| 21 | import { and, eq, sql } from "drizzle-orm"; | |
| 22 | import { db } from "../db"; | |
| 23 | import { | |
| 24 | auditLog, | |
| 25 | issueLabels, | |
| 26 | issues, | |
| 27 | labels, | |
| 28 | prComments, | |
| 29 | pullRequests, | |
| 30 | repositories, | |
| 31 | users, | |
| 32 | } from "../db/schema"; | |
| 33 | import { DEMO_USERNAME } from "./demo-seed"; | |
| 34 | import { AI_REVIEW_MARKER } from "./ai-review"; | |
| 35 | ||
| 36 | const AI_BUILD_LABEL = "ai:build"; | |
| 37 | const DEMO_ACTIVITY_MARKER = "<!-- gluecron:demo-activity:v1 -->"; | |
| 38 | ||
| 39 | export 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 | ||
| 51 | interface DemoRepo { | |
| 52 | id: string; | |
| 53 | name: string; | |
| 54 | ownerId: string; | |
| 55 | } | |
| 56 | ||
| 57 | async 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 | ||
| 79 | async 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 | ||
| 105 | async 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 | ||
| 121 | async 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 | ||
| 144 | async 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 | */ | |
| 174 | export 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. */ | |
| 400 | export const __test = { | |
| 401 | AI_BUILD_LABEL, | |
| 402 | DEMO_ACTIVITY_MARKER, | |
| 403 | loadDemoRepos, | |
| 404 | }; |