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

dep-updater.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.

dep-updater.tsxBlame275 lines · 1 contributor
3cbe3d6Claude1/**
2 * Block D2 — AI dependency updater UI.
3 *
4 * GET /:owner/:repo/settings/dep-updater — run history + "Run now"
5 * POST /:owner/:repo/settings/dep-updater/run — kicks off a run (fire & forget)
6 *
7 * Owner-only. See `src/lib/dep-updater.ts` for the orchestrator.
8 */
9
10import { Hono } from "hono";
11import { eq, and, desc } from "drizzle-orm";
12import { db } from "../db";
13import { depUpdateRuns, repositories, users } from "../db/schema";
14import { Layout } from "../views/layout";
15import { RepoHeader } from "../views/components";
4127ecfClaude16import { formatRelative } from "../views/ui";
3cbe3d6Claude17import { IssueNav } from "./issues";
18import { softAuth, requireAuth } from "../middleware/auth";
19import type { AuthEnv } from "../middleware/auth";
20import { runDepUpdateRun, type Bump } from "../lib/dep-updater";
21
22const depUpdater = new Hono<AuthEnv>();
23
24depUpdater.use("*", softAuth);
25
26/**
27 * Resolve repo row + enforce owner-only access. Returns either a
28 * rendered Response (when unauthorised / missing) or `{ repo }`.
29 */
30async function resolveOwnerRepo(
31 c: any,
32 ownerName: string,
33 repoName: string
34): Promise<
35 | { kind: "ok"; repo: typeof repositories.$inferSelect }
36 | { kind: "response"; res: Response }
37> {
38 const user = c.get("user");
39 if (!user) {
40 return {
41 kind: "response",
42 res: c.redirect(`/login?redirect=${encodeURIComponent(c.req.path)}`),
43 };
44 }
45
46 try {
47 const [owner] = await db
48 .select()
49 .from(users)
50 .where(eq(users.username, ownerName))
51 .limit(1);
52 if (!owner) return { kind: "response", res: c.notFound() };
53 if (owner.id !== user.id) {
54 return {
55 kind: "response",
56 res: c.html(
57 <Layout title="Unauthorized" user={user}>
58 <div class="empty-state">
59 <h2>Unauthorized</h2>
60 <p>Only the repository owner can configure the dependency updater.</p>
61 </div>
62 </Layout>,
63 403
64 ),
65 };
66 }
67 const [repo] = await db
68 .select()
69 .from(repositories)
70 .where(
71 and(
72 eq(repositories.ownerId, owner.id),
73 eq(repositories.name, repoName)
74 )
75 )
76 .limit(1);
77 if (!repo) return { kind: "response", res: c.notFound() };
78 return { kind: "ok", repo };
79 } catch (err) {
80 // DB unreachable — let the global 503 / 500 handler cope.
81 return {
82 kind: "response",
83 res: c.html(
84 <Layout title="Error" user={user}>
85 <div class="empty-state">
86 <h2>Service unavailable</h2>
87 <p>The dependency updater is temporarily offline.</p>
88 </div>
89 </Layout>,
90 503
91 ),
92 };
93 }
94}
95
96function statusChip(status: string) {
97 const colorMap: Record<string, string> = {
98 success: "badge-open",
99 no_updates: "badge-closed",
100 failed: "badge-closed",
101 running: "badge-open",
102 pending: "badge-closed",
103 };
104 const cls = colorMap[status] || "badge-closed";
105 return <span class={`issue-badge ${cls}`}>{status}</span>;
106}
107
108function safeParseBumps(raw: string): Bump[] {
109 try {
110 const v = JSON.parse(raw || "[]");
111 return Array.isArray(v) ? v : [];
112 } catch {
113 return [];
114 }
115}
116
117// GET — run history + "Run now"
118depUpdater.get(
119 "/:owner/:repo/settings/dep-updater",
120 requireAuth,
121 async (c) => {
122 const { owner: ownerName, repo: repoName } = c.req.param();
123 const resolved = await resolveOwnerRepo(c, ownerName, repoName);
124 if (resolved.kind === "response") return resolved.res;
125 const { repo } = resolved;
126 const user = c.get("user")!;
127
128 let runs: Array<typeof depUpdateRuns.$inferSelect> = [];
129 try {
130 runs = await db
131 .select()
132 .from(depUpdateRuns)
133 .where(eq(depUpdateRuns.repositoryId, repo.id))
134 .orderBy(desc(depUpdateRuns.createdAt))
135 .limit(20);
136 } catch {
137 runs = [];
138 }
139
140 return c.html(
141 <Layout title={`Dep Updater — ${ownerName}/${repoName}`} user={user}>
142 <RepoHeader owner={ownerName} repo={repoName} />
143 <IssueNav owner={ownerName} repo={repoName} active="code" />
144 <div style="max-width: 900px">
145 <h2 style="margin-bottom: 8px">Dependency updater</h2>
146 <p style="color: var(--text-muted); margin-bottom: 20px">
147 Reads <code>package.json</code> on the default branch, queries the
148 npm registry for newer versions, and opens a pull request with the
149 bumped dependencies. Runs on-demand from this page; background
150 scheduling can be added later.
151 </p>
152
153 <form
9e2c6dfClaude154 method="post"
3cbe3d6Claude155 action={`/${ownerName}/${repoName}/settings/dep-updater/run`}
156 style="margin-bottom: 24px"
157 >
158 <button type="submit" class="btn btn-primary">
159 Run now
160 </button>
161 </form>
162
163 <h3 style="margin: 24px 0 8px; font-size: 16px">Recent runs</h3>
164 {runs.length === 0 ? (
165 <div class="empty-state">
166 <p>No runs yet. Click "Run now" to start your first scan.</p>
167 </div>
168 ) : (
169 <div class="issue-list">
170 {runs.map((r) => {
171 const applied = safeParseBumps(r.appliedBumps);
172 const attempted = safeParseBumps(r.attemptedBumps);
173 const bumps = applied.length > 0 ? applied : attempted;
4127ecfClaude174 const when = formatRelative(r.createdAt as unknown as string);
3cbe3d6Claude175 return (
176 <div
177 class="issue-row"
178 style="padding: 12px; border-bottom: 1px solid var(--border)"
179 >
180 <div style="display: flex; align-items: center; gap: 8px">
181 {statusChip(r.status)}
182 <span style="color: var(--text-muted); font-size: 13px">
183 {when}
184 </span>
185 {r.prNumber != null && (
186 <a
187 href={`/${ownerName}/${repoName}/pulls/${r.prNumber}`}
188 style="font-size: 13px"
189 >
190 PR #{r.prNumber}
191 </a>
192 )}
193 {r.branchName && (
194 <code style="font-size: 12px; color: var(--text-muted)">
195 {r.branchName}
196 </code>
197 )}
198 </div>
199 {r.errorMessage && (
200 <div
201 style="margin-top: 6px; font-size: 13px; color: var(--red)"
202 >
203 {r.errorMessage}
204 </div>
205 )}
206 {bumps.length > 0 && (
207 <details style="margin-top: 8px">
208 <summary style="cursor: pointer; font-size: 13px">
209 {bumps.length} bump{bumps.length === 1 ? "" : "s"}
210 </summary>
211 <table style="margin-top: 8px; font-size: 13px; width: 100%">
212 <thead>
213 <tr style="text-align: left; color: var(--text-muted)">
214 <th style="padding: 4px 8px">Package</th>
215 <th style="padding: 4px 8px">From</th>
216 <th style="padding: 4px 8px">To</th>
217 <th style="padding: 4px 8px">Kind</th>
218 <th style="padding: 4px 8px">Major?</th>
219 </tr>
220 </thead>
221 <tbody>
222 {bumps.map((b) => (
223 <tr>
224 <td style="padding: 4px 8px">
225 <code>{b.name}</code>
226 </td>
227 <td style="padding: 4px 8px">{b.from}</td>
228 <td style="padding: 4px 8px">{b.to}</td>
229 <td style="padding: 4px 8px">{b.kind}</td>
230 <td style="padding: 4px 8px">
231 {b.major ? "yes" : "no"}
232 </td>
233 </tr>
234 ))}
235 </tbody>
236 </table>
237 </details>
238 )}
239 </div>
240 );
241 })}
242 </div>
243 )}
244 </div>
245 </Layout>
246 );
247 }
248);
249
250// POST — fire and forget
251depUpdater.post(
252 "/:owner/:repo/settings/dep-updater/run",
253 requireAuth,
254 async (c) => {
255 const { owner: ownerName, repo: repoName } = c.req.param();
256 const resolved = await resolveOwnerRepo(c, ownerName, repoName);
257 if (resolved.kind === "response") return resolved.res;
258 const { repo } = resolved;
259 const user = c.get("user")!;
260
261 // Fire-and-forget. The run records its own failures to `dep_update_runs`.
262 runDepUpdateRun({
263 repositoryId: repo.id,
264 owner: ownerName,
265 repo: repoName,
266 userId: user.id,
267 }).catch((err) => {
268 console.error("[dep-updater] run failed:", err);
269 });
270
271 return c.redirect(`/${ownerName}/${repoName}/settings/dep-updater`);
272 }
273);
274
275export default depUpdater;