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

migrations.tsx

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

migrations.tsxBlame363 lines · 1 contributor
14c3cc8Claude1/**
2 * Migration history — tracks repos imported from GitHub (bulk org import + single
3 * repo import) and lets owners re-run the post-migration verifier on demand.
4 *
5 * The `repositories` table does NOT currently carry an `importedAt` /
6 * `importSource` / `mirrorUpstreamUrl` column (see `src/db/schema.ts`), so
7 * we fall back to a best-effort derivation: list every repo owned by the
8 * current user and surface `createdAt` as the "imported at" timestamp. When
9 * the schema eventually grows an `importedAt` column we can switch the
10 * filter to `isNotNull(repositories.importedAt)` without changing the UI.
11 *
12 * The verifier itself lives in `src/lib/import-verify.ts` and is being
13 * supplied by a parallel agent. We load it via dynamic import inside a
14 * try/catch so a missing module produces a helpful "verifier not available"
15 * note instead of a 500.
16 */
17
18import { Hono } from "hono";
19import { desc, eq } from "drizzle-orm";
20import { db } from "../db";
21import { repositories } from "../db/schema";
22import { Layout } from "../views/layout";
4127ecfClaude23import { formatRelative } from "../views/ui";
14c3cc8Claude24import { requireAuth } from "../middleware/auth";
25import type { AuthEnv } from "../middleware/auth";
26
27const migrations = new Hono<AuthEnv>();
28
29migrations.use("/migrations", requireAuth);
30migrations.use("/migrations/*", requireAuth);
31
32// ─── Verifier loader ─────────────────────────────────────────
33//
34// The verifier is optional at app boot — a parallel agent owns the file.
35// We load it dynamically so this route works whether or not the module
36// is present on disk. The expected interface is:
37//
38// export async function verifyMigration(repoId: number): Promise<{
39// repoId: number;
40// clonable: boolean;
41// hasDefaultBranch: boolean;
42// commitCount: number;
43// issues: string[];
44// }>
45//
46type VerifyResult = {
47 repoId: number;
48 clonable: boolean;
49 hasDefaultBranch: boolean;
50 commitCount: number;
51 issues: string[];
52};
53
54async function loadVerifier(): Promise<
55 ((repoId: number) => Promise<VerifyResult>) | null
56> {
57 try {
58 // eslint-disable-next-line @typescript-eslint/no-var-requires
59 const mod: any = await import("../lib/import-verify");
60 if (mod && typeof mod.verifyMigration === "function") {
61 return mod.verifyMigration as (id: number) => Promise<VerifyResult>;
62 }
63 return null;
64 } catch {
65 return null;
66 }
67}
68
69// ─── GET /migrations ─────────────────────────────────────────
70migrations.get("/migrations", async (c) => {
71 const user = c.get("user")!;
72
73 // Best-effort listing: all repos owned by the user, newest first.
74 // When an `importedAt` column is added later, narrow this WHERE to
75 // `and(eq(ownerId, user.id), isNotNull(importedAt))`.
76 let rows: Array<{
77 id: string;
78 name: string;
79 createdAt: Date;
80 description: string | null;
81 }> = [];
82 try {
83 const result = await db
84 .select({
85 id: repositories.id,
86 name: repositories.name,
87 createdAt: repositories.createdAt,
88 description: repositories.description,
89 })
90 .from(repositories)
91 .where(eq(repositories.ownerId, user.id))
92 .orderBy(desc(repositories.createdAt));
93 rows = result as any;
94 } catch {
95 rows = [];
96 }
97
98 return c.html(
99 <Layout title="Migration history" user={user}>
100 <div style="max-width: 900px; margin: 0 auto; padding: 24px">
101 <div style="display:flex;justify-content:space-between;align-items:center;margin-bottom:16px">
102 <h1 style="margin:0">Migration history</h1>
103 <div style="display:flex;gap:8px">
104 <a class="btn" href="/import">
105 Import
106 </a>
107 <a class="btn" href="/import/bulk">
108 Bulk import
109 </a>
110 </div>
111 </div>
112
113 <p style="color: var(--text-muted); margin-bottom: 16px">
114 Repositories you've migrated to gluecron. Use <strong>Verify</strong>
115 {" "}to re-run the post-migration check (clonability, default branch,
116 commit count).
117 </p>
118
119 {rows.length === 0 ? (
120 <div class="panel-empty" style="padding: 32px; text-align: center">
121 You haven't migrated any repos yet. Try{" "}
122 <a href="/import">/import</a> or{" "}
123 <a href="/import/bulk">/import/bulk</a>.
124 </div>
125 ) : (
126 <div class="panel">
127 <div
128 class="panel-item"
129 style="font-weight:600;background:var(--bg-subtle)"
130 >
131 <div style="flex:2;min-width:0">Repo</div>
132 <div style="flex:2;min-width:0">Source</div>
133 <div style="flex:1;min-width:0">Imported at</div>
134 <div style="width:120px;text-align:right">Action</div>
135 </div>
136 {rows.map((r) => (
137 <div class="panel-item">
138 <div style="flex:2;min-width:0;overflow:hidden;text-overflow:ellipsis">
139 <a href={`/${user.username}/${r.name}`}>
140 <strong>{r.name}</strong>
141 </a>
142 {r.description && (
143 <div
144 style="font-size:12px;color:var(--text-muted);white-space:nowrap;overflow:hidden;text-overflow:ellipsis"
145 >
146 {r.description}
147 </div>
148 )}
149 </div>
150 <div
151 style="flex:2;min-width:0;font-size:12px;color:var(--text-muted);font-family:var(--font-mono);overflow:hidden;text-overflow:ellipsis"
152 >
153 {/* Source URL column — we don't persist the upstream URL
154 yet, so show a neutral placeholder. */}
155
156 </div>
157 <div style="flex:1;min-width:0;font-size:12px;color:var(--text-muted)">
4127ecfClaude158 {r.createdAt ? formatRelative(r.createdAt as unknown as string) : "—"}
14c3cc8Claude159 </div>
160 <div style="width:120px;text-align:right">
161 <a
162 class="btn btn-primary"
163 href={`/migrations/verify/${r.id}`}
164 >
165 Verify
166 </a>
167 </div>
168 </div>
169 ))}
170 </div>
171 )}
172 </div>
173 </Layout>
174 );
175});
176
177// ─── GET /migrations/verify/:repoId ──────────────────────────
178migrations.get("/migrations/verify/:repoId", async (c) => {
179 const user = c.get("user")!;
180 const repoId = c.req.param("repoId");
181
182 // Ownership check: the verifier must only run for repos this user owns.
183 let repo:
184 | {
185 id: string;
186 name: string;
187 ownerId: string;
188 defaultBranch: string;
189 }
190 | null = null;
191 try {
192 const [row] = await db
193 .select({
194 id: repositories.id,
195 name: repositories.name,
196 ownerId: repositories.ownerId,
197 defaultBranch: repositories.defaultBranch,
198 })
199 .from(repositories)
200 .where(eq(repositories.id, repoId))
201 .limit(1);
202 repo = (row as any) || null;
203 } catch {
204 repo = null;
205 }
206
207 if (!repo) {
208 return c.html(
209 <Layout title="Verify migration" user={user}>
210 <div style="max-width: 700px; margin: 0 auto; padding: 24px">
211 <h1>Repository not found</h1>
212 <p style="color:var(--text-muted)">
213 <a href="/migrations">Back to migration history</a>
214 </p>
215 </div>
216 </Layout>,
217 404
218 );
219 }
220
221 if (repo.ownerId !== user.id) {
222 return c.html(
223 <Layout title="Verify migration" user={user}>
224 <div style="max-width: 700px; margin: 0 auto; padding: 24px">
225 <h1>Forbidden</h1>
226 <p style="color:var(--text-muted)">
227 You can only verify repositories you own.
228 </p>
229 <p>
230 <a href="/migrations">Back to migration history</a>
231 </p>
232 </div>
233 </Layout>,
234 403
235 );
236 }
237
238 const verify = await loadVerifier();
239 let result: VerifyResult | null = null;
240 let verifierError: string | null = null;
241 if (!verify) {
242 verifierError =
243 "Verifier not available. The import-verify module is not installed yet.";
244 } else {
245 try {
246 // Schema stores repo id as uuid string; the verifier interface
247 // types it as `number` but many callers pass through strings. Cast
248 // defensively so we don't crash on either shape.
249 result = await verify(repo.id as unknown as number);
250 } catch (err: any) {
251 verifierError =
252 "Verifier failed: " + (err && err.message ? err.message : String(err));
253 }
254 }
255
256 const indicator = (ok: boolean) => (
257 <span
258 style={`display:inline-block;width:10px;height:10px;border-radius:50%;margin-right:6px;background:${
259 ok ? "var(--green, #2ea043)" : "var(--red, #f85149)"
260 }`}
261 />
262 );
263
264 return c.html(
265 <Layout title={`Verify ${repo.name}`} user={user}>
266 <div style="max-width: 700px; margin: 0 auto; padding: 24px">
267 <div style="margin-bottom: 12px">
268 <a href="/migrations" style="font-size:12px">
269 ← Migration history
270 </a>
271 </div>
272 <h1 style="margin:0 0 4px 0">Verify migration</h1>
273 <div style="color:var(--text-muted);font-family:var(--font-mono);margin-bottom:16px">
274 {user.username}/{repo.name}
275 </div>
276
277 {verifierError && (
278 <div
279 class="panel-empty"
280 style="padding:16px;border-left:3px solid var(--red, #f85149)"
281 >
282 {verifierError}
283 </div>
284 )}
285
286 {result && (
287 <div class="panel">
288 <div class="panel-item">
289 <div style="flex:1">
290 {indicator(result.clonable)}
291 <strong>Clonable</strong>
292 </div>
293 <div style="color:var(--text-muted);font-size:12px">
294 {result.clonable ? "Repository responds to git clone" : "Clone failed"}
295 </div>
296 </div>
297 <div class="panel-item">
298 <div style="flex:1">
299 {indicator(result.hasDefaultBranch)}
300 <strong>Default branch</strong>
301 </div>
302 <div style="color:var(--text-muted);font-size:12px">
303 {result.hasDefaultBranch
304 ? `Found ${repo.defaultBranch}`
305 : `Missing ${repo.defaultBranch}`}
306 </div>
307 </div>
308 <div class="panel-item">
309 <div style="flex:1">
310 {indicator(result.commitCount > 0)}
311 <strong>Commits</strong>
312 </div>
313 <div style="color:var(--text-muted);font-size:12px">
314 {result.commitCount} commit
315 {result.commitCount === 1 ? "" : "s"}
316 </div>
317 </div>
318 {result.issues && result.issues.length > 0 && (
319 <div
320 class="panel-item"
321 style="flex-direction:column;align-items:stretch;gap:4px"
322 >
323 <div>
324 {indicator(false)}
325 <strong>Issues</strong>
326 </div>
327 <ul style="margin:0;padding-left:20px;color:var(--text-muted);font-size:13px">
328 {result.issues.map((i) => (
329 <li>{i}</li>
330 ))}
331 </ul>
332 </div>
333 )}
334 {(!result.issues || result.issues.length === 0) &&
335 result.clonable &&
336 result.hasDefaultBranch &&
337 result.commitCount > 0 && (
338 <div class="panel-item">
339 <div style="flex:1;color:var(--green, #2ea043)">
340 All checks passed.
341 </div>
342 </div>
343 )}
344 </div>
345 )}
346
347 <div style="margin-top:16px;display:flex;gap:8px">
348 <a
349 class="btn btn-primary"
350 href={`/migrations/verify/${repo.id}`}
351 >
352 Re-run verification
353 </a>
354 <a class="btn" href="/migrations">
355 Back
356 </a>
357 </div>
358 </div>
359 </Layout>
360 );
361});
362
363export default migrations;