CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history
demo-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.
| 988380a | 1 | /** |
| 2 | * Demo seed — idempotently creates a `demo` user plus three public demo repos | |
| 3 | * (`hello-python`, `todo-api`, `design-docs`) each with an initial commit, | |
| 4 | * one open issue, and (for `todo-api`) a closed pull request. | |
| 5 | * | |
| 6 | * Requirements: | |
| 7 | * - Never throws. Every DB insert + subprocess call is wrapped in try/catch | |
| 8 | * and errors are pushed into the `errors` array on the result. | |
| 9 | * - Idempotent. A second call returns `created.user = false`, `repos = []`. | |
| 10 | * - Fast-path: when the demo user already exists and all three repos are | |
| 11 | * already recorded in the DB, returns immediately without side effects | |
| 12 | * (unless `opts.force === true`). | |
| 13 | * - Imports (never modifies) locked helpers: `hashPassword`, `initBareRepo`, | |
| 14 | * `bootstrapRepository`. | |
| 15 | * | |
| 16 | * Content builders (`buildHelloPythonFiles`, `buildTodoApiFiles`, | |
| 17 | * `buildDesignDocsFiles`) are pure — they return file-path → file-contents | |
| 18 | * records and are unit-tested directly. They're re-exported via `__test` | |
| 19 | * for convenience. | |
| 20 | */ | |
| 21 | ||
| 22 | import { and, eq } from "drizzle-orm"; | |
| 23 | import { db } from "../db"; | |
| 24 | import { | |
| 25 | users, | |
| 26 | repositories, | |
| 27 | issues, | |
| 28 | pullRequests, | |
| 29 | } from "../db/schema"; | |
| 30 | import { hashPassword } from "./auth"; | |
| 31 | import { initBareRepo, getRepoPath } from "../git/repository"; | |
| 32 | import { bootstrapRepository } from "./repo-bootstrap"; | |
| 33 | ||
| 34 | export const DEMO_USERNAME = "demo" as const; | |
| 35 | const DEMO_EMAIL = "demo@gluecron.local"; | |
| 36 | const DEMO_DISPLAY_NAME = "Demo Account"; | |
| 37 | const DEMO_AUTHOR_NAME = "Demo"; | |
| 38 | const DEMO_AUTHOR_EMAIL = "demo@gluecron.local"; | |
| 39 | ||
| 40 | export interface DemoSeedResult { | |
| 41 | demoUser: { id: string; username: string } | null; | |
| 42 | repos: Array<{ name: string; url: string }>; | |
| 43 | created: { | |
| 44 | user: boolean; | |
| 45 | repos: string[]; | |
| 46 | issues: number; | |
| 47 | prs: number; | |
| 48 | }; | |
| 49 | errors: string[]; | |
| 50 | } | |
| 51 | ||
| 52 | /* ------------------------------------------------------------------------ */ | |
| 53 | /* Content builders (pure) */ | |
| 54 | /* ------------------------------------------------------------------------ */ | |
| 55 | ||
| 56 | export function buildHelloPythonFiles(): Record<string, string> { | |
| 57 | return { | |
| 58 | "README.md": `# hello-python | |
| 59 | ||
| 60 | A tiny demo Python app, seeded by GlueCron to showcase the UI. | |
| 61 | ||
| 62 | ## Run | |
| 63 | ||
| 64 | \`\`\`bash | |
| 65 | pip install -r requirements.txt | |
| 66 | python main.py | |
| 67 | \`\`\` | |
| 68 | ||
| 69 | This repo belongs to the \`demo\` account. Feel free to browse — it's | |
| 70 | regenerated on demand from the demo seeder. | |
| 71 | `, | |
| 72 | "main.py": `"""hello-python — demo entrypoint.""" | |
| 73 | ||
| 74 | ||
| 75 | def greet(name: str) -> str: | |
| 76 | return f"Hello, {name}!" | |
| 77 | ||
| 78 | ||
| 79 | def main() -> None: | |
| 80 | print(greet("GlueCron")) | |
| 81 | ||
| 82 | ||
| 83 | if __name__ == "__main__": | |
| 84 | main() | |
| 85 | `, | |
| 86 | "requirements.txt": `# No third-party deps yet — kept intentionally minimal. | |
| 87 | # Add packages below as "name==version" once needed. | |
| 88 | `, | |
| 89 | }; | |
| 90 | } | |
| 91 | ||
| 92 | export function buildTodoApiFiles(): Record<string, string> { | |
| 93 | const pkg = { | |
| 94 | name: "todo-api", | |
| 95 | version: "0.1.0", | |
| 96 | private: true, | |
| 97 | description: "Demo todo API — GlueCron seeded sample", | |
| 98 | main: "src/index.ts", | |
| 99 | scripts: { | |
| 100 | dev: "bun run src/index.ts", | |
| 101 | start: "bun run src/index.ts", | |
| 102 | }, | |
| 103 | dependencies: { | |
| 104 | hono: "^4.6.0", | |
| 105 | }, | |
| 106 | devDependencies: { | |
| 107 | typescript: "^5.4.0", | |
| 108 | }, | |
| 109 | }; | |
| 110 | ||
| 111 | return { | |
| 112 | "README.md": `# todo-api | |
| 113 | ||
| 114 | A minimal Hono-based todo API, seeded by GlueCron as a demo. | |
| 115 | ||
| 116 | ## Endpoints | |
| 117 | ||
| 118 | - \`GET /todos\` — list todos | |
| 119 | - \`POST /todos\` — create todo | |
| 120 | - \`GET /health\` — health probe | |
| 121 | ||
| 122 | ## Run | |
| 123 | ||
| 124 | \`\`\`bash | |
| 125 | bun install | |
| 126 | bun run dev | |
| 127 | \`\`\` | |
| 128 | `, | |
| 129 | "package.json": JSON.stringify(pkg, null, 2) + "\n", | |
| 130 | "src/index.ts": `import { Hono } from "hono"; | |
| 131 | ||
| 132 | type Todo = { id: number; title: string; done: boolean }; | |
| 133 | ||
| 134 | const app = new Hono(); | |
| 135 | const todos: Todo[] = [ | |
| 136 | { id: 1, title: "Try GlueCron", done: false }, | |
| 137 | { id: 2, title: "Push a commit", done: false }, | |
| 138 | ]; | |
| 139 | ||
| 140 | app.get("/health", (c) => c.json({ ok: true })); | |
| 141 | app.get("/todos", (c) => c.json(todos)); | |
| 142 | ||
| 143 | app.post("/todos", async (c) => { | |
| 144 | const body = await c.req.json<{ title?: string }>(); | |
| 145 | if (!body?.title) return c.json({ error: "title required" }, 400); | |
| 146 | const todo: Todo = { id: todos.length + 1, title: body.title, done: false }; | |
| 147 | todos.push(todo); | |
| 148 | return c.json(todo, 201); | |
| 149 | }); | |
| 150 | ||
| 151 | export default app; | |
| 152 | `, | |
| 153 | }; | |
| 154 | } | |
| 155 | ||
| 156 | export function buildDesignDocsFiles(): Record<string, string> { | |
| 157 | return { | |
| 158 | "README.md": `# design-docs | |
| 159 | ||
| 160 | Architecture notes and ADRs for the \`demo\` org's sample project. | |
| 161 | ||
| 162 | Browse: | |
| 163 | ||
| 164 | - [Architecture overview](docs/architecture.md) | |
| 165 | - [ADR-001 — Choose Hono for the HTTP layer](docs/adr-001.md) | |
| 166 | `, | |
| 167 | "docs/architecture.md": `# Architecture overview | |
| 168 | ||
| 169 | ## Goals | |
| 170 | ||
| 171 | - Keep the surface small. | |
| 172 | - Prefer boring, well-understood primitives. | |
| 173 | - Fast cold-start on Bun. | |
| 174 | ||
| 175 | ## Components | |
| 176 | ||
| 177 | - **HTTP layer:** Hono. | |
| 178 | - **Data:** PostgreSQL via Drizzle. | |
| 179 | - **Jobs:** in-process, cron-driven. | |
| 180 | ||
| 181 | ## Non-goals | |
| 182 | ||
| 183 | - Multi-tenant isolation at the data layer. | |
| 184 | - Horizontal scale-out (v1 is single-node). | |
| 185 | `, | |
| 186 | "docs/adr-001.md": `# ADR-001 — Choose Hono for the HTTP layer | |
| 187 | ||
| 188 | - **Status:** accepted | |
| 189 | - **Date:** 2026-01-15 | |
| 190 | ||
| 191 | ## Context | |
| 192 | ||
| 193 | We need an HTTP framework that runs on Bun natively, has JSX server-side | |
| 194 | rendering, and minimal dependencies. | |
| 195 | ||
| 196 | ## Decision | |
| 197 | ||
| 198 | Adopt Hono as the HTTP layer. | |
| 199 | ||
| 200 | ## Consequences | |
| 201 | ||
| 202 | - Tiny runtime footprint. | |
| 203 | - Ecosystem is smaller than Express; we'll write middleware ourselves | |
| 204 | where nothing exists. | |
| 205 | ||
| 206 | ## Rollout | |
| 207 | ||
| 208 | Migrate the existing Express routes to Hono over two sprints. Keep the | |
| 209 | legacy handler importable until all routes are ported. | |
| 210 | `, | |
| 211 | }; | |
| 212 | } | |
| 213 | ||
| 214 | /* ------------------------------------------------------------------------ */ | |
| 215 | /* Git plumbing — write an initial commit from a file map */ | |
| 216 | /* ------------------------------------------------------------------------ */ | |
| 217 | ||
| 218 | interface SpawnResult { | |
| 219 | stdout: string; | |
| 220 | stderr: string; | |
| 221 | exitCode: number; | |
| 222 | } | |
| 223 | ||
| 224 | async function spawnSafe( | |
| 225 | cmd: string[], | |
| 226 | cwd: string, | |
| 227 | stdin?: string | Uint8Array, | |
| 228 | env?: Record<string, string> | |
| 229 | ): Promise<SpawnResult> { | |
| 230 | try { | |
| 231 | const proc = Bun.spawn(cmd, { | |
| 232 | cwd, | |
| 233 | stdout: "pipe", | |
| 234 | stderr: "pipe", | |
| 235 | stdin: stdin !== undefined ? "pipe" : undefined, | |
| 236 | env: { ...process.env, ...(env || {}) }, | |
| 237 | }); | |
| 238 | if (stdin !== undefined && proc.stdin) { | |
| 239 | const bytes = | |
| 240 | typeof stdin === "string" ? new TextEncoder().encode(stdin) : stdin; | |
| 241 | (proc.stdin as any).write(bytes); | |
| 242 | (proc.stdin as any).end(); | |
| 243 | } | |
| 244 | const [stdout, stderr] = await Promise.all([ | |
| 245 | new Response(proc.stdout).text(), | |
| 246 | new Response(proc.stderr).text(), | |
| 247 | ]); | |
| 248 | const exitCode = await proc.exited; | |
| 249 | return { stdout: stdout.trim(), stderr, exitCode }; | |
| 250 | } catch (err: any) { | |
| 251 | return { stdout: "", stderr: String(err?.message || err), exitCode: -1 }; | |
| 252 | } | |
| 253 | } | |
| 254 | ||
| 255 | /** | |
| 256 | * Write an initial commit to the bare repo at `repoDir` on branch `main` | |
| 257 | * containing the given file map. Uses git plumbing (hash-object + update-index | |
| 258 | * via a transient index + write-tree + commit-tree + update-ref). Mirrors the | |
| 259 | * pattern in `dep-updater.ts` / `createOrUpdateFileOnBranch`. Returns the | |
| 260 | * new commit sha on success. | |
| 261 | */ | |
| 262 | async function writeInitialCommit( | |
| 263 | repoDir: string, | |
| 264 | files: Record<string, string>, | |
| 265 | message: string, | |
| 266 | authorName: string, | |
| 267 | authorEmail: string | |
| 268 | ): Promise<{ commitSha: string } | { error: string }> { | |
| 2c3ba6e | 269 | const tmpIndex = `${repoDir}/index.demo-seed.${process.pid}.${Date.now()}.${crypto.randomUUID().replace(/-/g, '').slice(0, 8)}`; |
| 988380a | 270 | const baseEnv = { |
| 271 | GIT_INDEX_FILE: tmpIndex, | |
| 272 | GIT_AUTHOR_NAME: authorName, | |
| 273 | GIT_AUTHOR_EMAIL: authorEmail, | |
| 274 | GIT_COMMITTER_NAME: authorName, | |
| 275 | GIT_COMMITTER_EMAIL: authorEmail, | |
| 276 | }; | |
| 277 | ||
| 278 | const cleanup = async () => { | |
| 279 | try { | |
| 280 | const { unlink } = await import("fs/promises"); | |
| 281 | await unlink(tmpIndex); | |
| 282 | } catch { | |
| 283 | /* ignore */ | |
| 284 | } | |
| 285 | }; | |
| 286 | ||
| 287 | try { | |
| 288 | // 1. Hash each file → blob sha, then stage via update-index --cacheinfo. | |
| 289 | for (const [path, contents] of Object.entries(files)) { | |
| 290 | const hashed = await spawnSafe( | |
| 291 | ["git", "hash-object", "-w", "--stdin"], | |
| 292 | repoDir, | |
| 293 | contents | |
| 294 | ); | |
| 295 | if (hashed.exitCode !== 0 || !/^[0-9a-f]{40}$/.test(hashed.stdout)) { | |
| 296 | await cleanup(); | |
| 297 | return { error: `hash-object failed for ${path}: ${hashed.stderr}` }; | |
| 298 | } | |
| 299 | const blobSha = hashed.stdout; | |
| 300 | ||
| 301 | const upd = await spawnSafe( | |
| 302 | [ | |
| 303 | "git", | |
| 304 | "update-index", | |
| 305 | "--add", | |
| 306 | "--cacheinfo", | |
| 307 | `100644,${blobSha},${path}`, | |
| 308 | ], | |
| 309 | repoDir, | |
| 310 | undefined, | |
| 311 | baseEnv | |
| 312 | ); | |
| 313 | if (upd.exitCode !== 0) { | |
| 314 | await cleanup(); | |
| 315 | return { error: `update-index failed for ${path}: ${upd.stderr}` }; | |
| 316 | } | |
| 317 | } | |
| 318 | ||
| 319 | // 2. write-tree → tree sha. | |
| 320 | const wt = await spawnSafe( | |
| 321 | ["git", "write-tree"], | |
| 322 | repoDir, | |
| 323 | undefined, | |
| 324 | baseEnv | |
| 325 | ); | |
| 326 | if (wt.exitCode !== 0 || !/^[0-9a-f]{40}$/.test(wt.stdout)) { | |
| 327 | await cleanup(); | |
| 328 | return { error: `write-tree failed: ${wt.stderr}` }; | |
| 329 | } | |
| 330 | const treeSha = wt.stdout; | |
| 331 | ||
| 332 | // 3. commit-tree (no parent — initial commit). | |
| 333 | const commit = await spawnSafe( | |
| 334 | ["git", "commit-tree", treeSha, "-m", message], | |
| 335 | repoDir, | |
| 336 | undefined, | |
| 337 | baseEnv | |
| 338 | ); | |
| 339 | if (commit.exitCode !== 0 || !/^[0-9a-f]{40}$/.test(commit.stdout)) { | |
| 340 | await cleanup(); | |
| 341 | return { error: `commit-tree failed: ${commit.stderr}` }; | |
| 342 | } | |
| 343 | const commitSha = commit.stdout; | |
| 344 | ||
| 345 | // 4. update-ref refs/heads/main. | |
| 346 | const upd = await spawnSafe( | |
| 347 | ["git", "update-ref", "refs/heads/main", commitSha], | |
| 348 | repoDir | |
| 349 | ); | |
| 350 | if (upd.exitCode !== 0) { | |
| 351 | await cleanup(); | |
| 352 | return { error: `update-ref failed: ${upd.stderr}` }; | |
| 353 | } | |
| 354 | ||
| 355 | await cleanup(); | |
| 356 | return { commitSha }; | |
| 357 | } catch (err: any) { | |
| 358 | await cleanup(); | |
| 359 | return { error: String(err?.message || err) }; | |
| 360 | } | |
| 361 | } | |
| 362 | ||
| 363 | /* ------------------------------------------------------------------------ */ | |
| 364 | /* Seed orchestration */ | |
| 365 | /* ------------------------------------------------------------------------ */ | |
| 366 | ||
| 367 | interface DemoRepoSpec { | |
| 368 | name: string; | |
| 369 | description: string; | |
| 370 | files: Record<string, string>; | |
| 371 | issueTitle: string; | |
| 372 | issueBody?: string; | |
| 373 | seedClosedPr?: { title: string; body?: string }; | |
| 374 | } | |
| 375 | ||
| 376 | function demoRepoSpecs(): DemoRepoSpec[] { | |
| 377 | return [ | |
| 378 | { | |
| 379 | name: "hello-python", | |
| 380 | description: "Tiny Python demo app seeded by GlueCron.", | |
| 381 | files: buildHelloPythonFiles(), | |
| 382 | issueTitle: "Add rate limiting", | |
| 383 | issueBody: | |
| 384 | "The `/greet` endpoint has no rate limiting. We should add a simple token-bucket.", | |
| 385 | }, | |
| 386 | { | |
| 387 | name: "todo-api", | |
| 388 | description: "Minimal Hono todo API, seeded as a demo.", | |
| 389 | files: buildTodoApiFiles(), | |
| 390 | issueTitle: "Dark mode broken on mobile", | |
| 391 | issueBody: | |
| 392 | "On iOS Safari, the dark-mode toggle flickers to light for ~200ms on first paint.", | |
| 393 | seedClosedPr: { | |
| 394 | title: "feat: add /health endpoint", | |
| 395 | body: "Adds a trivial liveness probe at `GET /health` returning `{ ok: true }`.", | |
| 396 | }, | |
| 397 | }, | |
| 398 | { | |
| 399 | name: "design-docs", | |
| 400 | description: "Architecture notes + ADRs for the demo project.", | |
| 401 | files: buildDesignDocsFiles(), | |
| 402 | issueTitle: "Clarify ADR-001 rollout section", | |
| 403 | issueBody: | |
| 404 | "The rollout section of ADR-001 mentions 'two sprints' — we should pin a concrete date range.", | |
| 405 | }, | |
| 406 | ]; | |
| 407 | } | |
| 408 | ||
| 409 | async function findDemoUser(): Promise<{ id: string; username: string } | null> { | |
| 410 | try { | |
| 411 | const [row] = await db | |
| 412 | .select({ id: users.id, username: users.username }) | |
| 413 | .from(users) | |
| 414 | .where(eq(users.username, DEMO_USERNAME)) | |
| 415 | .limit(1); | |
| 416 | return row ?? null; | |
| 417 | } catch { | |
| 418 | return null; | |
| 419 | } | |
| 420 | } | |
| 421 | ||
| 422 | async function findDemoRepo( | |
| 423 | ownerId: string, | |
| 424 | name: string | |
| 425 | ): Promise<{ id: string } | null> { | |
| 426 | try { | |
| 427 | const [row] = await db | |
| 428 | .select({ id: repositories.id }) | |
| 429 | .from(repositories) | |
| 430 | .where( | |
| 431 | and(eq(repositories.ownerId, ownerId), eq(repositories.name, name)) | |
| 432 | ) | |
| 433 | .limit(1); | |
| 434 | return row ?? null; | |
| 435 | } catch { | |
| 436 | return null; | |
| 437 | } | |
| 438 | } | |
| 439 | ||
| 440 | /** | |
| 441 | * Idempotently create the demo user + three demo repos. Never throws. | |
| 442 | */ | |
| 443 | export async function ensureDemoContent(opts?: { | |
| 444 | force?: boolean; | |
| 445 | }): Promise<DemoSeedResult> { | |
| 446 | const force = !!opts?.force; | |
| 447 | const result: DemoSeedResult = { | |
| 448 | demoUser: null, | |
| 449 | repos: [], | |
| 450 | created: { user: false, repos: [], issues: 0, prs: 0 }, | |
| 451 | errors: [], | |
| 452 | }; | |
| 453 | ||
| 454 | const specs = demoRepoSpecs(); | |
| 455 | ||
| 456 | // 1. Fast-path — demo user + all three repos already exist and !force. | |
| 457 | if (!force) { | |
| 458 | const existing = await findDemoUser(); | |
| 459 | if (existing) { | |
| 460 | let allPresent = true; | |
| 461 | for (const spec of specs) { | |
| 462 | const repo = await findDemoRepo(existing.id, spec.name); | |
| 463 | if (!repo) { | |
| 464 | allPresent = false; | |
| 465 | break; | |
| 466 | } | |
| 467 | } | |
| 468 | if (allPresent) { | |
| 469 | result.demoUser = existing; | |
| 470 | result.repos = specs.map((s) => ({ | |
| 471 | name: s.name, | |
| 472 | url: `/${DEMO_USERNAME}/${s.name}`, | |
| 473 | })); | |
| 474 | return result; | |
| 475 | } | |
| 476 | } | |
| 477 | } | |
| 478 | ||
| 479 | // 2. Resolve or create the demo user. | |
| 480 | let demoUser = await findDemoUser(); | |
| 481 | if (!demoUser) { | |
| 482 | try { | |
| 483 | // Random password → login effectively disabled. | |
| 484 | const randBytes = crypto.getRandomValues(new Uint8Array(32)); | |
| 485 | const randomPassword = Array.from(randBytes) | |
| 486 | .map((b) => b.toString(16).padStart(2, "0")) | |
| 487 | .join(""); | |
| 488 | const passwordHash = await hashPassword(randomPassword); | |
| 489 | const [inserted] = await db | |
| 490 | .insert(users) | |
| 491 | .values({ | |
| 492 | username: DEMO_USERNAME, | |
| 493 | email: DEMO_EMAIL, | |
| 494 | displayName: DEMO_DISPLAY_NAME, | |
| 495 | passwordHash, | |
| 496 | }) | |
| 497 | .returning({ id: users.id, username: users.username }); | |
| 498 | if (inserted) { | |
| 499 | demoUser = inserted; | |
| 500 | result.created.user = true; | |
| 501 | } | |
| 502 | } catch (err: any) { | |
| 503 | result.errors.push( | |
| 504 | `create demo user: ${String(err?.message || err)}` | |
| 505 | ); | |
| 506 | } | |
| 507 | } | |
| 508 | ||
| 509 | if (!demoUser) { | |
| 510 | // Can't proceed without a user row. | |
| 511 | return result; | |
| 512 | } | |
| 513 | result.demoUser = demoUser; | |
| 514 | ||
| 515 | // 3. For each spec: ensure bare repo + initial commit + DB row + bootstrap | |
| 516 | // + one open issue (+ closed PR on todo-api). | |
| 517 | for (const spec of specs) { | |
| 518 | result.repos.push({ | |
| 519 | name: spec.name, | |
| 520 | url: `/${DEMO_USERNAME}/${spec.name}`, | |
| 521 | }); | |
| 522 | ||
| 523 | const existingRepo = await findDemoRepo(demoUser.id, spec.name); | |
| 524 | if (existingRepo && !force) { | |
| 525 | continue; | |
| 526 | } | |
| 527 | ||
| 528 | // Create bare repo on disk (ok if already present — initBareRepo is | |
| 529 | // git init --bare which is idempotent). | |
| 530 | let diskPath: string; | |
| 531 | try { | |
| 532 | diskPath = await initBareRepo(DEMO_USERNAME, spec.name); | |
| 533 | } catch (err: any) { | |
| 534 | result.errors.push( | |
| 535 | `initBareRepo(${spec.name}): ${String(err?.message || err)}` | |
| 536 | ); | |
| 537 | continue; | |
| 538 | } | |
| 539 | ||
| 540 | // Write initial commit only if HEAD doesn't already resolve. | |
| 541 | const repoDir = getRepoPath(DEMO_USERNAME, spec.name); | |
| 542 | const headCheck = await spawnSafe( | |
| 543 | ["git", "rev-parse", "--verify", "refs/heads/main"], | |
| 544 | repoDir | |
| 545 | ); | |
| 546 | if (headCheck.exitCode !== 0) { | |
| 547 | const wrote = await writeInitialCommit( | |
| 548 | repoDir, | |
| 549 | spec.files, | |
| 550 | "Initial commit", | |
| 551 | DEMO_AUTHOR_NAME, | |
| 552 | DEMO_AUTHOR_EMAIL | |
| 553 | ); | |
| 554 | if ("error" in wrote) { | |
| 555 | result.errors.push( | |
| 556 | `writeInitialCommit(${spec.name}): ${wrote.error}` | |
| 557 | ); | |
| 558 | // Continue — we still want the DB row so the UI can show it. | |
| 559 | } | |
| 560 | } | |
| 561 | ||
| 562 | // Insert DB row (skip if it already exists — e.g. partial prior run). | |
| 563 | let repoId: string | null = existingRepo?.id ?? null; | |
| 564 | if (!repoId) { | |
| 565 | try { | |
| 566 | const [inserted] = await db | |
| 567 | .insert(repositories) | |
| 568 | .values({ | |
| 569 | name: spec.name, | |
| 570 | ownerId: demoUser.id, | |
| 571 | description: spec.description, | |
| 572 | isPrivate: false, | |
| 573 | defaultBranch: "main", | |
| 574 | diskPath, | |
| 575 | }) | |
| 576 | .returning({ id: repositories.id }); | |
| 577 | if (inserted) { | |
| 578 | repoId = inserted.id; | |
| 579 | result.created.repos.push(spec.name); | |
| 580 | } | |
| 581 | } catch (err: any) { | |
| 582 | result.errors.push( | |
| 583 | `insert repo(${spec.name}): ${String(err?.message || err)}` | |
| 584 | ); | |
| 585 | } | |
| 586 | } | |
| 587 | ||
| 588 | if (!repoId) continue; | |
| 589 | ||
| 590 | // Green-ecosystem bootstrap (labels, settings, branch protection, welcome | |
| 591 | // issue). Wrapped — bootstrap internally tolerates duplicates but we | |
| 592 | // don't want any surprise throw to poison the seeder. | |
| 593 | try { | |
| 594 | await bootstrapRepository({ | |
| 595 | repositoryId: repoId, | |
| 596 | ownerUserId: demoUser.id, | |
| 597 | }); | |
| 598 | } catch (err: any) { | |
| 599 | result.errors.push( | |
| 600 | `bootstrapRepository(${spec.name}): ${String(err?.message || err)}` | |
| 601 | ); | |
| 602 | } | |
| 603 | ||
| 604 | // One open issue per repo. | |
| 605 | try { | |
| 606 | await db.insert(issues).values({ | |
| 607 | repositoryId: repoId, | |
| 608 | authorId: demoUser.id, | |
| 609 | title: spec.issueTitle, | |
| 610 | body: spec.issueBody ?? null, | |
| 611 | state: "open", | |
| 612 | }); | |
| 613 | result.created.issues += 1; | |
| 614 | } catch (err: any) { | |
| 615 | result.errors.push( | |
| 616 | `insert issue(${spec.name}): ${String(err?.message || err)}` | |
| 617 | ); | |
| 618 | } | |
| 619 | ||
| 620 | // Closed PR on todo-api. | |
| 621 | if (spec.seedClosedPr) { | |
| 622 | try { | |
| 623 | await db.insert(pullRequests).values({ | |
| 624 | repositoryId: repoId, | |
| 625 | authorId: demoUser.id, | |
| 626 | title: spec.seedClosedPr.title, | |
| 627 | body: spec.seedClosedPr.body ?? null, | |
| 628 | state: "closed", | |
| 629 | baseBranch: "main", | |
| 630 | headBranch: "demo/health-endpoint", | |
| 631 | closedAt: new Date(), | |
| 632 | }); | |
| 633 | result.created.prs += 1; | |
| 634 | } catch (err: any) { | |
| 635 | result.errors.push( | |
| 636 | `insert PR(${spec.name}): ${String(err?.message || err)}` | |
| 637 | ); | |
| 638 | } | |
| 639 | } | |
| 640 | } | |
| 641 | ||
| 642 | return result; | |
| 643 | } | |
| 644 | ||
| 645 | /* ------------------------------------------------------------------------ */ | |
| 646 | /* Test-only exports */ | |
| 647 | /* ------------------------------------------------------------------------ */ | |
| 648 | ||
| 649 | export const __test = { | |
| 650 | buildHelloPythonFiles, | |
| 651 | buildTodoApiFiles, | |
| 652 | buildDesignDocsFiles, | |
| 653 | demoRepoSpecs, | |
| 654 | }; |