Swadhin Biswas

[email protected]
2026-04-22 01:32:40 +0600 committed NaN years ago
18087d6
commit 18087d6
34 files changed
+784 -178
▼scripts/drills/backup-restore-rehearsal.sh
+30-0
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
+OUT_DIR="${OUT_DIR:-$ROOT_DIR/test-results/drills}"
6
+mkdir -p "$OUT_DIR"
7
+
8
+echo "[backup-restore-drill] Running backup..."
9
+cd "$ROOT_DIR"
10
+bunx tsx scripts/backup.ts | tee "$OUT_DIR/backup-drill.log"
11
+
12
+LATEST_BACKUP="$(ls -1t backups/backup-*.tar.gz backups/backup-*.tar.gz.enc 2>/dev/null | head -n1 || true)"
13
+if [[ -z "$LATEST_BACKUP" ]]; then
14
+ echo "[backup-restore-drill] No backup artifact found after backup run."
15
+ exit 1
16
+fi
17
+
18
+echo "[backup-restore-drill] Verifying backup artifact: $LATEST_BACKUP"
19
+bunx tsx scripts/backup-verify.ts "$LATEST_BACKUP" | tee "$OUT_DIR/backup-verify-drill.log"
20
+
21
+RESTORE_MODE="${RESTORE_MODE:-safe}"
22
+if [[ "$RESTORE_MODE" == "full" ]]; then
23
+ echo "[backup-restore-drill] Running FULL restore rehearsal (RESTORE_MODE=full)."
24
+ bunx tsx scripts/restore.ts "$LATEST_BACKUP" | tee "$OUT_DIR/restore-drill.log"
25
+else
26
+ echo "[backup-restore-drill] Running SAFE restore rehearsal (non-destructive)."
27
+ RESTORE_DB=false RESTORE_REPOS=false RESTORE_ENV=false bunx tsx scripts/restore.ts "$LATEST_BACKUP" | tee "$OUT_DIR/restore-drill.log"
28
+fi
29
+
30
+echo "[backup-restore-drill] ✅ Drill complete. Artifacts in $OUT_DIR"
0
31
▼scripts/drills/postgres-reconnect-drill.sh
+41-0
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
5
+APP_URL="${APP_URL:-http://localhost:4321/api/health}"
6
+OUT_DIR="${OUT_DIR:-./test-results/drills}"
7
+mkdir -p "$OUT_DIR"
8
+
9
+if docker compose version >/dev/null 2>&1; then
10
+ DC=(docker compose)
11
+elif docker-compose version >/dev/null 2>&1; then
12
+ DC=(docker-compose)
13
+else
14
+ echo "[postgres-drill] Docker Compose is required."
15
+ exit 1
16
+fi
17
+
18
+echo "[postgres-drill] Starting services (postgres, redis, app)..."
19
+"${DC[@]}" -f "$COMPOSE_FILE" up -d postgres redis app
20
+
21
+echo "[postgres-drill] Capturing baseline health..."
22
+curl -sS "$APP_URL" | tee "$OUT_DIR/postgres-drill-baseline-health.json" >/dev/null
23
+
24
+echo "[postgres-drill] Stopping postgres to simulate DB outage..."
25
+"${DC[@]}" -f "$COMPOSE_FILE" stop postgres
26
+sleep 8
27
+
28
+echo "[postgres-drill] Capturing degraded health..."
29
+curl -sS "$APP_URL" | tee "$OUT_DIR/postgres-drill-degraded-health.json" >/dev/null || true
30
+
31
+echo "[postgres-drill] Starting postgres again..."
32
+"${DC[@]}" -f "$COMPOSE_FILE" start postgres
33
+sleep 10
34
+
35
+echo "[postgres-drill] Capturing recovered health..."
36
+curl -sS "$APP_URL" | tee "$OUT_DIR/postgres-drill-recovered-health.json" >/dev/null
37
+
38
+echo "[postgres-drill] Collecting app logs (last 5m)..."
39
+"${DC[@]}" -f "$COMPOSE_FILE" logs --since 5m app > "$OUT_DIR/postgres-drill-app.log" || true
40
+
41
+echo "[postgres-drill] ✅ Drill complete. Artifacts in $OUT_DIR"
0
42
▼scripts/drills/redis-outage-drill.sh
+41-0
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+COMPOSE_FILE="${COMPOSE_FILE:-docker-compose.yml}"
5
+APP_URL="${APP_URL:-http://localhost:4321/api/health}"
6
+OUT_DIR="${OUT_DIR:-./test-results/drills}"
7
+mkdir -p "$OUT_DIR"
8
+
9
+if docker compose version >/dev/null 2>&1; then
10
+ DC=(docker compose)
11
+elif docker-compose version >/dev/null 2>&1; then
12
+ DC=(docker-compose)
13
+else
14
+ echo "[redis-drill] Docker Compose is required."
15
+ exit 1
16
+fi
17
+
18
+echo "[redis-drill] Starting services (postgres, redis, app)..."
19
+"${DC[@]}" -f "$COMPOSE_FILE" up -d postgres redis app
20
+
21
+echo "[redis-drill] Capturing baseline health..."
22
+curl -sS "$APP_URL" | tee "$OUT_DIR/redis-drill-baseline-health.json" >/dev/null
23
+
24
+echo "[redis-drill] Stopping redis to simulate outage..."
25
+"${DC[@]}" -f "$COMPOSE_FILE" stop redis
26
+sleep 8
27
+
28
+echo "[redis-drill] Capturing degraded health..."
29
+curl -sS "$APP_URL" | tee "$OUT_DIR/redis-drill-degraded-health.json" >/dev/null || true
30
+
31
+echo "[redis-drill] Bringing redis back..."
32
+"${DC[@]}" -f "$COMPOSE_FILE" start redis
33
+sleep 8
34
+
35
+echo "[redis-drill] Capturing recovered health..."
36
+curl -sS "$APP_URL" | tee "$OUT_DIR/redis-drill-recovered-health.json" >/dev/null
37
+
38
+echo "[redis-drill] Collecting app logs (last 5m)..."
39
+"${DC[@]}" -f "$COMPOSE_FILE" logs --since 5m app > "$OUT_DIR/redis-drill-app.log" || true
40
+
41
+echo "[redis-drill] ✅ Drill complete. Artifacts in $OUT_DIR"
0
42
▼scripts/perf/load-baseline.mjs
+173-0
1
+#!/usr/bin/env node
2
+import { writeFileSync } from "node:fs";
3
+import { performance } from "node:perf_hooks";
4
+
5
+const baseUrl = (process.env.PERF_BASE_URL || "http://127.0.0.1:3000").replace(
6
+ /\/$/,
7
+ "",
8
+);
9
+const paths = (process.env.PERF_PATHS || "/api/health,/api/metrics")
10
+ .split(",")
11
+ .map((path) => path.trim())
12
+ .filter(Boolean);
13
+const concurrency = Math.max(
14
+ 1,
15
+ Number.parseInt(process.env.PERF_CONCURRENCY || "8", 10) || 8,
16
+);
17
+const requestsPerPath = Math.max(
18
+ 1,
19
+ Number.parseInt(process.env.PERF_REQUESTS || "100", 10) || 100,
20
+);
21
+const timeoutMs = Math.max(
22
+ 1000,
23
+ Number.parseInt(process.env.PERF_TIMEOUT_MS || "10000", 10) || 10000,
24
+);
25
+const maxP95Ms = Math.max(
26
+ 1,
27
+ Number.parseFloat(process.env.PERF_MAX_P95_MS || "500") || 500,
28
+);
29
+const outputPath = process.env.PERF_OUTPUT || "";
30
+
31
+function percentile(values, pct) {
32
+ if (!values.length) return 0;
33
+ const sorted = [...values].sort((a, b) => a - b);
34
+ const index = Math.min(
35
+ sorted.length - 1,
36
+ Math.max(0, Math.ceil((pct / 100) * sorted.length) - 1),
37
+ );
38
+ return sorted[index];
39
+}
40
+
41
+function stats(samples) {
42
+ const avg = samples.reduce((sum, value) => sum + value, 0) / samples.length;
43
+ return {
44
+ count: samples.length,
45
+ avgMs: Number(avg.toFixed(2)),
46
+ p50Ms: Number(percentile(samples, 50).toFixed(2)),
47
+ p95Ms: Number(percentile(samples, 95).toFixed(2)),
48
+ p99Ms: Number(percentile(samples, 99).toFixed(2)),
49
+ minMs: Number(Math.min(...samples).toFixed(2)),
50
+ maxMs: Number(Math.max(...samples).toFixed(2)),
51
+ };
52
+}
53
+
54
+async function fetchWithTimeout(url) {
55
+ const controller = new AbortController();
56
+ const timer = setTimeout(
57
+ () => controller.abort(new Error(`Timed out after ${timeoutMs}ms`)),
58
+ timeoutMs,
59
+ );
60
+ const started = performance.now();
61
+
62
+ try {
63
+ const response = await fetch(url, {
64
+ method: "GET",
65
+ headers: {
66
+ Accept: "application/json, text/plain, */*",
67
+ },
68
+ signal: controller.signal,
69
+ });
70
+
71
+ await response.arrayBuffer();
72
+ return {
73
+ ok: response.ok,
74
+ status: response.status,
75
+ durationMs: performance.now() - started,
76
+ };
77
+ } finally {
78
+ clearTimeout(timer);
79
+ }
80
+}
81
+
82
+async function benchmarkPath(path) {
83
+ const url = `${baseUrl}${path.startsWith("/") ? path : `/${path}`}`;
84
+ const durations = [];
85
+ const statuses = new Map();
86
+ const failures = [];
87
+ let nextIndex = 0;
88
+
89
+ async function worker() {
90
+ while (nextIndex < requestsPerPath) {
91
+ const current = nextIndex++;
92
+ try {
93
+ const result = await fetchWithTimeout(url);
94
+ durations.push(result.durationMs);
95
+ statuses.set(result.status, (statuses.get(result.status) || 0) + 1);
96
+ if (!result.ok) {
97
+ failures.push({ index: current, status: result.status });
98
+ }
99
+ } catch (error) {
100
+ failures.push({
101
+ index: current,
102
+ error: error instanceof Error ? error.message : String(error),
103
+ });
104
+ }
105
+ }
106
+ }
107
+
108
+ const workers = Array.from({ length: concurrency }, () => worker());
109
+ await Promise.all(workers);
110
+
111
+ const summary = stats(durations);
112
+ return {
113
+ path,
114
+ url,
115
+ concurrency,
116
+ requests: requestsPerPath,
117
+ statuses: Object.fromEntries(statuses.entries()),
118
+ failures,
119
+ ...summary,
120
+ thresholdMs: maxP95Ms,
121
+ passed: failures.length === 0 && summary.p95Ms <= maxP95Ms,
122
+ };
123
+}
124
+
125
+async function main() {
126
+ console.log(`[perf] Base URL: ${baseUrl}`);
127
+ console.log(`[perf] Paths: ${paths.join(", ")}`);
128
+ console.log(
129
+ `[perf] Concurrency: ${concurrency}, requests/path: ${requestsPerPath}, timeout: ${timeoutMs}ms`,
130
+ );
131
+ console.log(`[perf] P95 threshold: ${maxP95Ms}ms`);
132
+
133
+ const results = [];
134
+ for (const path of paths) {
135
+ const result = await benchmarkPath(path);
136
+ results.push(result);
137
+ const verdict = result.passed ? "PASS" : "FAIL";
138
+ console.log(
139
+ `[perf] ${verdict} ${path} p95=${result.p95Ms}ms avg=${result.avgMs}ms max=${result.maxMs}ms statuses=${JSON.stringify(result.statuses)}`,
140
+ );
141
+ if (result.failures.length > 0) {
142
+ console.log(`[perf] failures: ${result.failures.length}`);
143
+ }
144
+ }
145
+
146
+ const report = {
147
+ baseUrl,
148
+ concurrency,
149
+ requestsPerPath,
150
+ timeoutMs,
151
+ maxP95Ms,
152
+ generatedAt: new Date().toISOString(),
153
+ results,
154
+ };
155
+
156
+ if (outputPath) {
157
+ writeFileSync(outputPath, JSON.stringify(report, null, 2));
158
+ console.log(`[perf] Report written to ${outputPath}`);
159
+ }
160
+
161
+ const failed = results.filter((result) => !result.passed);
162
+ if (failed.length > 0) {
163
+ console.error(
164
+ `[perf] ${failed.length} path(s) exceeded the threshold or returned errors.`,
165
+ );
166
+ process.exitCode = 1;
167
+ }
168
+}
169
+
170
+main().catch((error) => {
171
+ console.error("[perf] Benchmark failed:", error);
172
+ process.exitCode = 1;
173
+});
0
174
▼scripts/security/install-hooks.sh
+12-0
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
+cd "$ROOT_DIR"
6
+
7
+chmod +x .githooks/pre-push scripts/security/pre-push-secret-scan.sh
8
+
9
+git config core.hooksPath .githooks
10
+
11
+echo "[hooks] configured core.hooksPath=.githooks"
12
+echo "[hooks] installed pre-push secret scan guard"
0
13
▼scripts/security/pre-push-secret-scan.sh
+91-0
1
+#!/usr/bin/env bash
2
+set -euo pipefail
3
+
4
+ROOT_DIR="$(cd "$(dirname "$0")/../.." && pwd)"
5
+cd "$ROOT_DIR"
6
+
7
+RANGES=("$@")
8
+if [[ ${#RANGES[@]} -eq 0 ]]; then
9
+ RANGES=("HEAD~20..HEAD")
10
+fi
11
+
12
+if [[ "${OCH_SECRET_SCAN_BYPASS:-false}" == "true" ]]; then
13
+ echo "[secret-scan] bypass enabled via OCH_SECRET_SCAN_BYPASS=true"
14
+ exit 0
15
+fi
16
+
17
+run_gitleaks_for_range() {
18
+ local range="$1"
19
+ echo "[secret-scan] gitleaks range: $range"
20
+ gitleaks detect \
21
+ --source . \
22
+ --config .gitleaks.toml \
23
+ --no-banner \
24
+ --redact \
25
+ --log-opts "$range" \
26
+ --exit-code 1
27
+}
28
+
29
+run_gitleaks_full_repo() {
30
+ echo "[secret-scan] gitleaks full-repo fallback"
31
+ gitleaks detect \
32
+ --source . \
33
+ --config .gitleaks.toml \
34
+ --no-banner \
35
+ --redact \
36
+ --exit-code 1
37
+}
38
+
39
+run_heuristic_scan() {
40
+ local range="$1"
41
+ local diff_out
42
+ if ! diff_out="$(git diff "$range" 2>/dev/null || true)"; then
43
+ diff_out=""
44
+ fi
45
+
46
+ if [[ -z "$diff_out" ]]; then
47
+ return 0
48
+ fi
49
+
50
+ # Lightweight high-risk pattern scan for environments where gitleaks is unavailable.
51
+ if echo "$diff_out" | grep -E -i \
52
+ '(AKIA[0-9A-Z]{16}|ASIA[0-9A-Z]{16}|ghp_[A-Za-z0-9]{36,}|github_pat_[A-Za-z0-9_]{20,}|xox[baprs]-[A-Za-z0-9-]{10,}|-----BEGIN (RSA|EC|OPENSSH|PRIVATE) KEY-----|GOCSPX-[A-Za-z0-9_-]{10,}|AIza[0-9A-Za-z\-_]{20,}|glpat-[A-Za-z0-9\-_]{20,}|postgres://[^[:space:]]+:[^[:space:]]+@|mongodb(\+srv)?://[^[:space:]]+:[^[:space:]]+@|redis://[^[:space:]]+:[^[:space:]]+@)' >/dev/null; then
53
+ echo "[secret-scan] possible secret detected in range: $range"
54
+ echo "[secret-scan] install gitleaks for precise scanning: https://github.com/gitleaks/gitleaks"
55
+ return 1
56
+ fi
57
+
58
+ return 0
59
+}
60
+
61
+main() {
62
+ local has_gitleaks=false
63
+ if command -v gitleaks >/dev/null 2>&1; then
64
+ has_gitleaks=true
65
+ fi
66
+
67
+ if [[ "$has_gitleaks" == true ]]; then
68
+ for range in "${RANGES[@]}"; do
69
+ if ! run_gitleaks_for_range "$range"; then
70
+ echo "[secret-scan] blocked push because secret findings were detected."
71
+ echo "[secret-scan] if this is a false positive, use an allowlist rule and re-run scan."
72
+ exit 1
73
+ fi
74
+ done
75
+
76
+ echo "[secret-scan] passed (gitleaks)"
77
+ exit 0
78
+ fi
79
+
80
+ echo "[secret-scan] gitleaks not found, running heuristic fallback scan"
81
+ for range in "${RANGES[@]}"; do
82
+ if ! run_heuristic_scan "$range"; then
83
+ echo "[secret-scan] blocked push due to high-risk secret pattern(s)."
84
+ exit 1
85
+ fi
86
+ done
87
+
88
+ echo "[secret-scan] passed (heuristic fallback)"
89
+}
90
+
91
+main
0
92
▼src/db/adapter/types.ts
+10-0
8
8
| "postgres"
9
9
| "turso"
10
10
| "cockroachdb"
11
+ | "mysql"
12
+ | "mariadb"
13
+ | "mongodb"
14
+ | "redis"
15
+ | "firestore"
16
+ | "dynamodb"
17
+ | "neo4j"
18
+ | "cassandra"
19
+ | "scylladb"
20
+ | "surrealdb"
11
21
| "custom";
12
22
13
23
export interface DatabaseConfig {
14
24
▼src/lib/analytics-advanced.ts
+3-4
9
9
import { compareBranches } from "@/lib/git";
10
10
import { resolveRepoPath } from "@/lib/git-storage";
11
11
import { and, asc, desc, eq, gte, inArray, lte } from "drizzle-orm";
12
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
12
13
import {
13
14
boolean,
14
15
integer,
127
128
> = new Map();
128
129
129
130
// Replace previous snapshot for this repository
130
- // @ts-expect-error - Drizzle multi-db union type issue
131
- await db
131
+ await (db as NodePgDatabase<typeof schema>)
132
132
.delete(schema.fileHotspots)
133
133
.where(eq(schema.fileHotspots.repositoryId, repositoryId));
134
134
191
191
calculatedAt: new Date(),
192
192
};
193
193
194
- // @ts-expect-error - Drizzle multi-db union type issue
195
- await db.insert(schema.fileHotspots).values(hotspot);
194
+ await (db as NodePgDatabase<typeof schema>).insert(schema.fileHotspots).values(hotspot);
196
195
hotspots.push(hotspot as FileHotspot);
197
196
}
198
197
199
198
▼src/lib/api.ts
+1-1
211
211
try {
212
212
return await handler(context);
213
213
} catch (err) {
214
- logger.error(err);
214
+ logger.error(err instanceof Error ? err.message : String(err));
215
215
return serverError(err instanceof Error ? err.message : "Unknown error");
216
216
}
217
217
};
218
218
▼src/lib/artifacts.ts
+3-4
5
5
6
6
import { getDatabase, schema } from "@/db";
7
7
import { and, eq } from "drizzle-orm";
8
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
8
9
import { createReadStream, createWriteStream } from "fs";
9
10
import fs from "fs/promises";
10
11
import path from "path";
207
208
}
208
209
209
210
// Increment download count
210
- // @ts-expect-error - Drizzle multi-db union type issue
211
- await db
211
+ await (db as NodePgDatabase<typeof schema>)
212
212
.update(schema.workflowArtifacts)
213
213
.set({ downloadCount: (record.downloadCount ?? 0) + 1 })
214
214
.where(eq(schema.workflowArtifacts.id, record.id));
289
289
}
290
290
291
291
// Remove DB record
292
- // @ts-expect-error - Drizzle multi-db union type issue
293
- await db
292
+ await (db as NodePgDatabase<typeof schema>)
294
293
.delete(schema.workflowArtifacts)
295
294
.where(eq(schema.workflowArtifacts.id, artifactId));
296
295
297
296
▼src/lib/ci-logs.ts
+2-2
5
5
6
6
import { getDatabase, schema } from "@/db";
7
7
import { eq } from "drizzle-orm";
8
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
8
9
import fs from "fs/promises";
9
10
import path from "path";
10
11
import { logger } from "./logger";
92
93
async persistStep(step: StepRecord): Promise<void> {
93
94
try {
94
95
const db = getDatabase();
95
- // @ts-expect-error - Drizzle multi-db union type issue
96
- await db
96
+ await (db as NodePgDatabase<typeof schema>)
97
97
.insert(schema.workflowSteps)
98
98
.values({
99
99
id: step.id,
100
100
▼src/lib/env-config.ts
+83-0
1
+import { getDatabase, schema } from "@/db";
2
+import { eq } from "drizzle-orm";
3
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+
5
+// Short-lived cache so we don't query the DB on every call,
6
+// but updates in the dashboard propagate quickly (TTL 30s).
7
+let configCache: Record<string, string> | null = null;
8
+let lastCacheTime = 0;
9
+const CACHE_TTL = 30000;
10
+
11
+export async function fetchSystemConfig(): Promise<Record<string, string>> {
12
+ const now = Date.now();
13
+ if (configCache && now - lastCacheTime < CACHE_TTL) {
14
+ return configCache;
15
+ }
16
+
17
+ try {
18
+ const db = getDatabase() as NodePgDatabase<typeof schema>;
19
+ const configs = await db.query.systemConfig.findMany({
20
+ where: eq(schema.systemConfig.key, "general_config"),
21
+ });
22
+
23
+ if (configs.length > 0 && configs[0].value) {
24
+ try {
25
+ configCache = JSON.parse(configs[0].value);
26
+ lastCacheTime = now;
27
+ return configCache as Record<string, string>;
28
+ } catch {
29
+ configCache = null;
30
+ lastCacheTime = now;
31
+ }
32
+ }
33
+
34
+ configCache = {};
35
+ lastCacheTime = now;
36
+ return configCache;
37
+ } catch {
38
+ configCache = null;
39
+ return {};
40
+ }
41
+}
42
+
43
+/**
44
+ * Supercharges standard process.env to check the Database Admin Settings first.
45
+ * If the environment setting (e.g. OPENAI_API_KEY) is declared in the Admin UI,
46
+ * it safely overrides the .env file.
47
+ */
48
+export async function getEnv(
49
+ key:
50
+ | "OPENAI_API_KEY"
51
+ | "ANTHROPIC_API_KEY"
52
+ | "GROQ_API_KEY"
53
+ | "SMTP_HOST"
54
+ | "SMTP_PORT"
55
+ | "SMTP_USER"
56
+ | "SMTP_PASSWORD"
57
+ | "SMTP_FROM"
58
+ | string,
59
+): Promise<string | undefined> {
60
+ const dbConfig = await fetchSystemConfig();
61
+
62
+ // Mapping standard env keys to the JSON shape we use in settings.astro
63
+ const dbMappedKeys: Record<string, string> = {
64
+ OPENAI_API_KEY: "openaiKey",
65
+ ANTHROPIC_API_KEY: "anthropicKey",
66
+ GROQ_API_KEY: "groqKey",
67
+ SMTP_HOST: "smtpHost",
68
+ SMTP_PORT: "smtpPort",
69
+ SMTP_USER: "smtpUser",
70
+ SMTP_PASSWORD: "smtpPass",
71
+ SMTP_FROM: "smtpFrom",
72
+ };
73
+
74
+ const dashboardKey = dbMappedKeys[key];
75
+
76
+ // 1. Fallback to App Settings Override
77
+ if (dashboardKey && dbConfig[dashboardKey]) {
78
+ return dbConfig[dashboardKey] as string;
79
+ }
80
+
81
+ // 2. Primary .env Standard
82
+ return process.env[key];
83
+}
0
84
▼src/lib/env-validation.ts
+2-0
3
3
* Run this at application startup to ensure all required configuration is present
4
4
*/
5
5
6
+import { logger } from "@/lib/logger";
7
+
6
8
interface EnvConfig {
7
9
name: string;
8
10
required: boolean;
9
11
▼src/lib/epics.ts
+5-8
5
5
6
6
import { getDatabase, schema } from "@/db";
7
7
import { and, eq, inArray } from "drizzle-orm";
8
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
8
9
import { logger } from "./logger";
9
10
10
11
export interface EpicInfo {
202
203
203
204
if (!epic) return false;
204
205
205
- // @ts-expect-error - Drizzle multi-db union type issue
206
- await db
206
+ await (db as NodePgDatabase<typeof schema>)
207
207
.update(schema.issues)
208
208
.set({ parentId: epicId, updatedAt: new Date() })
209
209
.where(eq(schema.issues.id, issueId));
217
217
export async function removeFromEpic(issueId: string): Promise<boolean> {
218
218
const db = getDatabase();
219
219
220
- // @ts-expect-error - Drizzle multi-db union type issue
221
- await db
220
+ await (db as NodePgDatabase<typeof schema>)
222
221
.update(schema.issues)
223
222
.set({ parentId: null, updatedAt: new Date() })
224
223
.where(eq(schema.issues.id, issueId));
257
256
export async function convertToEpic(issueId: string): Promise<boolean> {
258
257
const db = getDatabase();
259
258
260
- // @ts-expect-error - Drizzle multi-db union type issue
261
- await db
259
+ await (db as NodePgDatabase<typeof schema>)
262
260
.update(schema.issues)
263
261
.set({ type: "epic", parentId: null, updatedAt: new Date() })
264
262
.where(eq(schema.issues.id, issueId));
275
273
): Promise<boolean> {
276
274
const db = getDatabase();
277
275
278
- // @ts-expect-error - Drizzle multi-db union type issue
279
- await db
276
+ await (db as NodePgDatabase<typeof schema>)
280
277
.update(schema.issues)
281
278
.set({
282
279
type: "task",
283
280
▼src/lib/external-ci.ts
+5-10
6
6
import { getDatabase, schema } from "@/db";
7
7
import { externalBuilds, externalCIConfigs } from "@/db/schema/integrations";
8
8
import { and, eq } from "drizzle-orm";
9
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
9
10
import { assertOnlineFeature } from "./air-gapped";
10
11
import { logger } from "./logger";
11
12
283
284
const completedAt =
284
285
status !== "pending" && status !== "running" ? new Date() : null;
285
286
286
- // @ts-expect-error - Drizzle multi-db union type issue
287
- await db
287
+ await (db as NodePgDatabase<typeof schema>)
288
288
.update(schema.externalBuilds)
289
289
.set({
290
290
status,
405
405
});
406
406
407
407
if (existing) {
408
- // @ts-expect-error - Drizzle multi-db union type issue
409
- await db
408
+ await (db as NodePgDatabase<typeof schema>)
410
409
.update(schema.pullRequestChecks)
411
410
.set({
412
411
headSha: pr.headSha,
413
412
status: status === "running" ? "in_progress" : "completed",
414
413
conclusion,
415
- updatedAt: new Date(),
416
414
})
417
415
.where(eq(schema.pullRequestChecks.id, existing.id));
418
416
} else {
419
- // @ts-expect-error - Drizzle multi-db union type issue
420
- await db.insert(schema.pullRequestChecks).values({
417
+ await (db as NodePgDatabase<typeof schema>).insert(schema.pullRequestChecks).values({
421
418
id: crypto.randomUUID(),
422
419
pullRequestId: prId,
423
420
name: checkName,
425
422
status: status === "running" ? "in_progress" : "completed",
426
423
conclusion,
427
424
createdAt: new Date(),
428
- updatedAt: new Date(),
429
425
});
430
426
}
431
427
}
469
465
if (build) {
470
466
const normalizedStatus = normalizeStatus(config.provider, String(status));
471
467
472
- // @ts-expect-error - Drizzle multi-db union type issue
473
- await db
468
+ await (db as NodePgDatabase<typeof schema>)
474
469
.update(schema.externalBuilds)
475
470
.set({
476
471
status: normalizedStatus,
477
472
▼src/lib/inbox-shortcuts.ts
+2-0
3
3
* Graphite-style keyboard navigation for PR inbox
4
4
*/
5
5
6
+import { logger } from "@/lib/logger";
7
+
6
8
// Keyboard shortcut handler
7
9
export function initInboxKeyboardShortcuts() {
8
10
let selectedIndex = 0;
9
11
▼src/lib/metrics.ts
+1-1
180
180
];
181
181
182
182
for (const metric of allMetrics) {
183
- register.registerMetric(metric);
183
+ register.registerMetric(metric as typeof httpRequestsTotal);
184
184
}
185
185
186
186
export { register };
187
187
▼src/lib/packages.ts
+7-11
6
6
import { getDatabase, schema } from "@/db";
7
7
import type { Package, PackageVersion } from "@/db/schema/packages";
8
8
import { and, desc, eq, ilike, sql } from "drizzle-orm";
9
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
9
10
import { logger } from "./logger";
10
11
11
12
// ============================================================================
109
110
})) || [];
110
111
111
112
// Count total
112
- const countResult = await db
113
+ const countResult = await (db as NodePgDatabase<typeof schema>)
113
114
.select({ count: sql<number>`count(*)` })
114
115
.from(schema.packages)
115
116
.where(and(...conditions));
158
159
createdAt: new Date(),
159
160
};
160
161
161
- // @ts-expect-error - Drizzle multi-db union type issue
162
- await db.insert(schema.packageVersions).values(ver);
162
+ await (db as NodePgDatabase<typeof schema>).insert(schema.packageVersions).values(ver);
163
163
164
164
// Update package timestamp & handle "latest" tag demotion
165
- // @ts-expect-error - Drizzle multi-db union type issue
166
- await db
165
+ await (db as NodePgDatabase<typeof schema>)
167
166
.update(schema.packages)
168
167
.set({ updatedAt: new Date() })
169
168
.where(eq(schema.packages.id, options.packageId));
206
205
version: string,
207
206
): Promise<void> {
208
207
const db = getDatabase();
209
- // @ts-expect-error - Drizzle multi-db union type issue
210
- await db
208
+ await (db as NodePgDatabase<typeof schema>)
211
209
.update(schema.packageVersions)
212
210
.set({ yanked: true })
213
211
.where(
224
222
versionId: string,
225
223
): Promise<void> {
226
224
const db = getDatabase();
227
- // @ts-expect-error - Drizzle multi-db union type issue
228
- await db
225
+ await (db as NodePgDatabase<typeof schema>)
229
226
.update(schema.packageVersions)
230
227
.set({ downloadCount: sql`${schema.packageVersions.downloadCount} + 1` })
231
228
.where(eq(schema.packageVersions.id, versionId));
232
- // @ts-expect-error - Drizzle multi-db union type issue
233
- await db
229
+ await (db as NodePgDatabase<typeof schema>)
234
230
.update(schema.packages)
235
231
.set({ downloadCount: sql`${schema.packages.downloadCount} + 1` })
236
232
.where(eq(schema.packages.id, packageId));
237
233
▼src/lib/pipeline.ts
+1-0
11
11
import path from "path";
12
12
import { parse as parseYaml } from "yaml";
13
13
import { generateId } from "./utils";
14
+import { logger } from "@/lib/logger";
14
15
15
16
// Types
16
17
export interface WorkflowConfig {
17
18
▼src/lib/pr-issue-linking.ts
+4-6
7
7
import { issues } from "@/db/schema/issues";
8
8
import { pullRequests } from "@/db/schema/pull-requests";
9
9
import { and, eq, inArray, or } from "drizzle-orm";
10
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
10
11
import { pgTable, text, timestamp } from "drizzle-orm/pg-core";
11
12
import { logger } from "./logger";
12
13
119
120
120
121
if (existing) {
121
122
// Update link type
122
- // @ts-expect-error - Drizzle multi-db union type issue
123
- await db
123
+ await (db as NodePgDatabase<typeof schema>)
124
124
.update(schema.prIssueLinks)
125
125
.set({ linkType: options.linkType })
126
126
.where(eq(schema.prIssueLinks.id, existing.id));
286
286
})) || [];
287
287
288
288
for (const link of links) {
289
- // @ts-expect-error - Drizzle multi-db union type issue
290
- await db
289
+ await (db as NodePgDatabase<typeof schema>)
291
290
.update(schema.issues)
292
291
.set({
293
292
state: "closed",
320
319
const db = getDatabase();
321
320
322
321
try {
323
- // @ts-expect-error - Drizzle multi-db union type issue
324
- await db
322
+ await (db as NodePgDatabase<typeof schema>)
325
323
.delete(schema.prIssueLinks)
326
324
.where(eq(schema.prIssueLinks.id, linkId));
327
325
return true;
328
326
▼src/lib/pull-requests.ts
+78-0
1
+import { getDatabase, schema } from "@/db";
2
+import { eq } from "drizzle-orm";
3
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
4
+import { evaluateGates } from "./ci-gates";
5
+import { mergeBranch } from "./git";
6
+import { resolveRepoPath } from "./git-storage";
7
+import { closeLinkedIssuesOnMerge } from "./pr-issue-linking";
8
+
9
+export async function mergePullRequest(
10
+ pullRequestId: string,
11
+ mergedById: string,
12
+ mergeMethod: "merge" | "squash" | "rebase" = "merge",
13
+) {
14
+ const db = getDatabase() as NodePgDatabase<typeof schema>;
15
+
16
+ const pr = await db.query.pullRequests.findFirst({
17
+ where: eq(schema.pullRequests.id, pullRequestId),
18
+ });
19
+
20
+ if (!pr) {
21
+ throw new Error("Pull request not found");
22
+ }
23
+
24
+ if (pr.state !== "open") {
25
+ throw new Error("Pull request is not open");
26
+ }
27
+
28
+ const gateResult = await evaluateGates(pr.id);
29
+ if (!gateResult.canMerge) {
30
+ const failed = gateResult.results
31
+ .filter((result) => !result.passed)
32
+ .map((result) => result.message)
33
+ .join("; ");
34
+ throw new Error(`Merge blocked: ${failed}`);
35
+ }
36
+
37
+ const repo = await db.query.repositories.findFirst({
38
+ where: eq(schema.repositories.id, pr.repositoryId),
39
+ });
40
+
41
+ if (!repo) {
42
+ throw new Error("Repository not found");
43
+ }
44
+
45
+ const repoPath = await resolveRepoPath(repo.diskPath);
46
+ const commitTitle = `Merge pull request #${pr.number} from ${pr.headBranch}`;
47
+ const mergeResult = await mergeBranch(
48
+ repoPath,
49
+ pr.baseBranch,
50
+ pr.headBranch,
51
+ commitTitle,
52
+ );
53
+
54
+ if (!mergeResult.success) {
55
+ throw new Error(mergeResult.message || "Merge failed");
56
+ }
57
+
58
+ const now = new Date();
59
+ await db
60
+ .update(schema.pullRequests)
61
+ .set({
62
+ state: "merged",
63
+ isMerged: true,
64
+ mergedAt: now,
65
+ mergedById,
66
+ mergeCommitSha: mergeResult.sha || null,
67
+ mergeSha: mergeResult.sha || null,
68
+ mergeMethod,
69
+ updatedAt: now,
70
+ })
71
+ .where(eq(schema.pullRequests.id, pullRequestId));
72
+
73
+ await closeLinkedIssuesOnMerge(pr.id, mergedById);
74
+
75
+ return db.query.pullRequests.findFirst({
76
+ where: eq(schema.pullRequests.id, pullRequestId),
77
+ });
78
+}
0
79
▼src/lib/realtime-client.ts
+2-0
1
+import { logger } from "@/lib/logger";
2
+
1
3
export class RealtimeClient {
2
4
private eventSource: EventSource | null = null;
3
5
private listeners: Map<string, Function[]> = new Map();
4
6
▼src/lib/security-advanced.ts
+5-8
12
12
secretScanResults,
13
13
} from "@/db/schema/integrations";
14
14
import { and, eq, gt, gte, lte } from "drizzle-orm";
15
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
15
16
import { logger } from "./logger";
16
17
17
18
// Re-export types from schema for consumers
168
169
const db = getDatabase();
169
170
170
171
try {
171
- // @ts-expect-error - Drizzle multi-db union type issue
172
- await db
172
+ await (db as NodePgDatabase<typeof schema>)
173
173
.update(schema.secretScanResults)
174
174
.set({
175
175
status: resolution,
366
366
);
367
367
368
368
// Increment counter
369
- // @ts-expect-error - Drizzle multi-db union type issue
370
- await db
369
+ await (db as NodePgDatabase<typeof schema>)
371
370
.update(schema.rateLimitLogs)
372
371
.set({
373
372
requestCount: existing.requestCount + 1,
378
377
return { allowed, remaining: remaining - 1, resetAt };
379
378
} else {
380
379
// Create new window
381
- // @ts-expect-error - Drizzle multi-db union type issue
382
- await db.insert(schema.rateLimitLogs).values({
380
+ await (db as NodePgDatabase<typeof schema>).insert(schema.rateLimitLogs).values({
383
381
id: crypto.randomUUID(),
384
382
ruleId: matchingRule.id,
385
383
key: options.key,
672
670
syncedTeams.push(team.name);
673
671
} else if (!isGroupMember && existingMembership) {
674
672
// Remove from team (SAML is authoritative source)
675
- // @ts-expect-error - Drizzle multi-db union type issue
676
- await db
673
+ await (db as NodePgDatabase<typeof schema>)
677
674
.delete(schema.teamMembers)
678
675
.where(
679
676
and(
680
677
▼src/pages/[owner]/[repo]/pulls/[number].astro
+3-2
91
91
});
92
92
93
93
// Fetch Stack Info
94
-let stackInfo = null;
94
+let stackInfo: { baseBranch: string; stackId: string; entries: { order: number; pr: any }[] } | null = null;
95
95
try {
96
96
const stackEntry = await db.query.prStackEntries.findFirst({
97
97
where: eq(schema.prStackEntries.pullRequestId, pr.id),
111
111
112
112
stackInfo = {
113
113
baseBranch: stackEntry.stack.baseBranch,
114
+ stackId: stackEntry.stackId,
114
115
entries: entries.map((e) => ({
115
116
order: e.stackOrder,
116
117
pr: e.pullRequest,
792
793
isMerged: entry.pr.isMerged,
793
794
order: entry.order,
794
795
})) : [],
795
- stackId: stackInfo ? stackInfo.stack.id : null,
796
+ stackId: stackInfo ? stackInfo.stackId : null,
796
797
currentBuiltinState: pr.state,
797
798
currentCustomStateId: pr.stateId || null,
798
799
}}
799
800
▼src/pages/[owner]/[repo]/pulls/index.astro
+1-1
93
93
number: pr.number,
94
94
title: pr.title,
95
95
state: pr.state as "open" | "closed" | "merged",
96
- createdAt: pr.createdAt,
96
+ createdAt: pr.createdAt.toISOString(),
97
97
commentCount: pr.commentCount || 0,
98
98
sourceBranch: pr.headBranch || "",
99
99
targetBranch: pr.baseBranch || "",
100
100
▼src/pages/[owner]/[repo]/settings/workflow.astro
+1-1
109
109
repo={repo.name}
110
110
/>
111
111
112
- <div className="my-8"></div>
112
+ <div class="my-8"></div>
113
113
114
114
<IssueWorkflowSettings
115
115
client:load
116
116
▼src/pages/api/repos/[owner]/[repo]/pr/[id]/stack-graph.ts
+12-8
1
1
import type { APIRoute } from "astro";
2
-import { db } from "../../../../../../../db/index";
3
-import { stackedPrs } from "../../../../../../../db/schema/stacked-prs";
2
+import { getDatabase, schema } from "@/db";
4
3
import { eq } from "drizzle-orm";
5
4
6
5
export const GET: APIRoute = async ({ params }) => {
7
- const prId = Number(params.id);
6
+ const prId = params.id;
8
7
9
- if (isNaN(prId)) {
8
+ if (!prId) {
10
9
return new Response("Invalid PR ID", { status: 400 });
11
10
}
12
11
12
+ const db = getDatabase();
13
13
try {
14
- const selfPr = await db.select().from(stackedPrs).where(eq(stackedPrs.prId, prId)).limit(1);
14
+ const selfPr = await db.query.prStackEntries.findFirst({
15
+ where: eq(schema.prStackEntries.pullRequestId, prId),
16
+ });
15
17
16
- if (selfPr.length === 0) {
18
+ if (!selfPr) {
17
19
return new Response(JSON.stringify({ isStacked: false }), { status: 200, headers: { "Content-Type": "application/json" } });
18
20
}
19
21
20
- const stackId = selfPr[0].stackId;
21
- const fullStack = await db.select().from(stackedPrs).where(eq(stackedPrs.stackId, stackId));
22
+ const stackId = selfPr.stackId;
23
+ const fullStack = await db.query.prStackEntries.findMany({
24
+ where: eq(schema.prStackEntries.stackId, stackId),
25
+ });
22
26
23
27
return new Response(JSON.stringify({ isStacked: true, stackId, items: fullStack }), {
24
28
status: 200,
25
29
▼src/pages/api/repos/[owner]/[repo]/pulls/bulk-queue-manage.ts
+2-2
7
7
unauthorized,
8
8
} from "@/lib/api";
9
9
import { withErrorHandler } from "@/lib/errors";
10
-import { removeFromMergeQueue, reprioritizeQueue } from "@/lib/merge-queue";
10
+import { removeFromMergeQueue, updateQueuePriority } from "@/lib/merge-queue";
11
11
import { canWriteRepo } from "@/lib/permissions";
12
12
import type { APIRoute } from "astro";
13
13
import { and, eq, inArray } from "drizzle-orm";
112
112
if (parsed.data.action === "remove") {
113
113
await removeFromMergeQueue(pr.id);
114
114
} else {
115
- await reprioritizeQueue(entry.id, parsed.data.priority!);
115
+ await updateQueuePriority(entry.id, parsed.data.priority!);
116
116
}
117
117
118
118
updated.push({
119
119
▼src/pages/api/repos/[owner]/[repo]/queue.ts
+2-2
29
29
import {
30
30
processNextInQueue,
31
31
removeFromMergeQueue,
32
- reprioritizeQueue,
32
+ updateQueuePriority,
33
33
} from "@/lib/merge-queue";
34
34
import { canWriteRepo } from "@/lib/permissions";
35
35
281
281
return badRequest("Priority is required");
282
282
}
283
283
284
- await reprioritizeQueue(entry.id, priority);
284
+ await updateQueuePriority(entry.id, priority);
285
285
logger.info(
286
286
{ userId: actor.id, repoId: repository.id, entryId, priority },
287
287
"Reprioritized merge queue entry",
288
288
▼src/pages/api/repos/[owner]/[repo]/stacks/submit.ts
+153-0
1
+import type { APIRoute } from "astro";
2
+import { getDatabase, schema } from "@/db";
3
+import { badRequest, notFound, success, unauthorized } from "@/lib/api";
4
+import { getUserFromRequest } from "@/lib/auth";
5
+import { createStack } from "@/lib/stacks";
6
+import { generateId } from "@/lib/utils";
7
+import { and, desc, eq } from "drizzle-orm";
8
+import type { NodePgDatabase } from "drizzle-orm/node-postgres";
9
+import { z } from "zod";
10
+
11
+const SubmitStackSchema = z.object({
12
+ branches: z.array(
13
+ z.object({
14
+ name: z.string(),
15
+ parent: z.string(),
16
+ commitHash: z.string(),
17
+ title: z.string().optional(),
18
+ description: z.string().optional(),
19
+ isDraft: z.boolean().default(false),
20
+ }),
21
+ ),
22
+});
23
+
24
+export const POST: APIRoute = async ({ params, request }) => {
25
+ const { owner, repo } = params;
26
+ if (!owner || !repo) return badRequest("Missing repo info");
27
+
28
+ try {
29
+ const actor = await getUserFromRequest(request);
30
+ if (!actor) return unauthorized();
31
+
32
+ const db = getDatabase() as NodePgDatabase<typeof schema>;
33
+
34
+ const ownerUser = await db.query.users.findFirst({
35
+ where: eq(schema.users.username, owner),
36
+ columns: { id: true },
37
+ });
38
+
39
+ if (!ownerUser) return notFound("Repository owner not found");
40
+
41
+ const repository = await db.query.repositories.findFirst({
42
+ where: and(
43
+ eq(schema.repositories.ownerId, ownerUser.id),
44
+ eq(schema.repositories.name, repo),
45
+ ),
46
+ });
47
+
48
+ if (!repository) return notFound("Repository not found");
49
+
50
+ const body = await request.json();
51
+ const parsed = SubmitStackSchema.parse(body);
52
+
53
+ if (parsed.branches.length === 0) {
54
+ return badRequest("At least one branch is required");
55
+ }
56
+
57
+ const dbPr = db;
58
+
59
+ const latestPr = await dbPr
60
+ .select({ number: schema.pullRequests.number })
61
+ .from(schema.pullRequests)
62
+ .where(eq(schema.pullRequests.repositoryId, repository.id))
63
+ .orderBy(desc(schema.pullRequests.number))
64
+ .limit(1);
65
+
66
+ let nextNumber = (latestPr[0]?.number ?? 0) + 1;
67
+ const stack = await createStack({
68
+ repositoryId: repository.id,
69
+ baseBranch: parsed.branches[0]?.parent || repository.defaultBranch,
70
+ name: `Submitted stack (${parsed.branches.length} PRs)`,
71
+ createdById: actor.userId,
72
+ });
73
+
74
+ const createdPrs: Array<{ prId: string; branch: string }> = [];
75
+
76
+ await db.transaction(async (tx) => {
77
+ for (let i = 0; i < parsed.branches.length; i++) {
78
+ const branchData = parsed.branches[i];
79
+
80
+ const existingPr = await tx.query.pullRequests.findFirst({
81
+ where: and(
82
+ eq(schema.pullRequests.repositoryId, repository.id),
83
+ eq(schema.pullRequests.headBranch, branchData.name),
84
+ ),
85
+ });
86
+
87
+ let prId = existingPr?.id;
88
+
89
+ if (existingPr) {
90
+ await tx
91
+ .update(schema.pullRequests)
92
+ .set({
93
+ baseBranch: branchData.parent,
94
+ headSha: branchData.commitHash,
95
+ baseSha: branchData.commitHash,
96
+ title: branchData.title || existingPr.title,
97
+ body: branchData.description || existingPr.body,
98
+ isDraft: branchData.isDraft,
99
+ updatedAt: new Date(),
100
+ })
101
+ .where(eq(schema.pullRequests.id, existingPr.id));
102
+ } else {
103
+ const inserted = await tx
104
+ .insert(schema.pullRequests)
105
+ .values({
106
+ id: generateId(),
107
+ repositoryId: repository.id,
108
+ number: nextNumber++,
109
+ title: branchData.title || `Update ${branchData.name}`,
110
+ body: branchData.description || "",
111
+ state: "open",
112
+ authorId: actor.userId,
113
+ headBranch: branchData.name,
114
+ headSha: branchData.commitHash,
115
+ baseBranch: branchData.parent,
116
+ baseSha: branchData.commitHash,
117
+ isDraft: branchData.isDraft,
118
+ createdAt: new Date(),
119
+ updatedAt: new Date(),
120
+ })
121
+ .returning({ id: schema.pullRequests.id });
122
+
123
+ prId = inserted[0]?.id;
124
+ }
125
+
126
+ if (!prId) {
127
+ throw new Error(
128
+ `Failed to resolve pull request for branch ${branchData.name}`,
129
+ );
130
+ }
131
+
132
+ await tx
133
+ .delete(schema.prStackEntries)
134
+ .where(eq(schema.prStackEntries.pullRequestId, prId));
135
+
136
+ await tx.insert(schema.prStackEntries).values({
137
+ id: generateId(),
138
+ stackId: stack.id,
139
+ pullRequestId: prId,
140
+ stackOrder: i + 1,
141
+ parentPrId: i > 0 ? (createdPrs[i - 1]?.prId ?? null) : null,
142
+ createdAt: new Date(),
143
+ });
144
+
145
+ createdPrs.push({ prId, branch: branchData.name });
146
+ }
147
+ });
148
+
149
+ return success({ success: true, stackId: stack.id, createdPrs });
150
+ } catch (error: any) {
151
+ return badRequest(error?.message || "Failed to submit stack");
152
+ }
153
+};
0
154
▼
+0-99
1
-import type { APIRoute } from "astro";
2
-import { db } from "../../../../../../../db/index";
3
-import { pullRequests } from "../../../../../../../db/schema/pull-requests";
4
-import { stackedPrs } from "../../../../../../../db/schema/stacked-prs";
5
-import { repositories } from "../../../../../../../db/schema/repositories";
6
-import { eq, and } from "drizzle-orm";
7
-import { z } from "zod";
8
-
9
-const SubmitStackSchema = z.object({
10
- branches: z.array(z.object({
11
- name: z.string(),
12
- parent: z.string(),
13
- commitHash: z.string(),
14
- title: z.string().optional(),
15
- description: z.string().optional(),
16
- isDraft: z.boolean().default(false)
17
- }))
18
-});
19
-
20
-export const POST: APIRoute = async ({ params, request }) => {
21
- const { owner, repo } = params;
22
- if (!owner || !repo) return new Response("Missing repo info", { status: 400 });
23
-
24
- try {
25
- const body = await request.json();
26
- const parsed = SubmitStackSchema.parse(body);
27
-
28
- const repository = await db.query.repositories.findFirst({
29
- where: and(eq(repositories.owner, owner), eq(repositories.name, repo))
30
- });
31
-
32
- if (!repository) return new Response("Repository not found", { status: 404 });
33
-
34
- // Perform the entire stack creation transactionally
35
- const result = await db.transaction(async (tx) => {
36
- let createdPrs = [];
37
- let stackIdentifier = \`stack_$\{Date.now()\}\`; // Unique ID for this push session
38
-
39
- for (let i = 0; i < parsed.branches.length; i++) {
40
- const branchData = parsed.branches[i];
41
-
42
- // 1. Create or Find the Pull Request
43
- let prId;
44
- const existingPr = await tx.select().from(pullRequests).where(
45
- and(eq(pullRequests.repositoryId, repository.id), eq(pullRequests.headBranch, branchData.name))
46
- ).limit(1);
47
-
48
- if (existingPr.length > 0) {
49
- prId = existingPr[0].id;
50
- // Update PR if needed
51
- await tx.update(pullRequests).set({
52
- baseBranch: branchData.parent,
53
- title: branchData.title || existingPr[0].title,
54
- body: branchData.description || existingPr[0].body
55
- }).where(eq(pullRequests.id, prId));
56
- } else {
57
- // Insert new PR
58
- const newPr = await tx.insert(pullRequests).values({
59
- repositoryId: repository.id,
60
- number: Math.floor(Math.random() * 10000), // Should use standard auto-increment or sequence
61
- headBranch: branchData.name,
62
- baseBranch: branchData.parent,
63
- title: branchData.title || \`Update \$\{branchData.name\}\`,
64
- body: branchData.description || "",
65
- status: "OPEN",
66
- isDraft: branchData.isDraft,
67
- authorId: 1 // Stubbed to admin
68
- }).returning();
69
- prId = newPr[0].id;
70
- }
71
-
72
- createdPrs.push({ prId, branch: branchData.name });
73
-
74
- // 2. Manage Stack Tracking Relation
75
- // We delete any old stack relation for this PR and re-insert
76
- // to handle re-ordering or moving branches dynamically.
77
- await tx.delete(stackedPrs).where(eq(stackedPrs.prId, prId));
78
-
79
- await tx.insert(stackedPrs).values({
80
- prId: prId,
81
- stackId: stackIdentifier,
82
- parentBranch: branchData.parent,
83
- childBranch: branchData.name,
84
- position: i + 1
85
- });
86
- }
87
-
88
- return createdPrs;
89
- });
90
-
91
- return new Response(JSON.stringify({ success: true, createdPrs: result }), {
92
- status: 200,
93
- headers: { "Content-Type": "application/json" }
94
- });
95
-
96
- } catch (error: any) {
97
- return new Response(JSON.stringify({ error: error.message }), { status: 400 });
98
- }
99
-};
100
0
▼src/pages/inbox/index.astro
+3-3
1
1
---
2
-import Layout from "../../layouts/Layout.astro";
2
+import BaseLayout from "../../layouts/BaseLayout.astro";
3
3
import { KeyboardReviewInbox } from "../../components/inbox/KeyboardReviewInbox";
4
4
5
5
// Typically we'd fetch the first PR needing review from the DB here
13
13
};
14
14
---
15
15
16
-<Layout title="Inbox - OpenCodeHub">
16
+<BaseLayout title="Inbox - OpenCodeHub">
17
17
<main class="h-screen w-full bg-slate-950 overflow-hidden pt-16">
18
18
<!-- React Island loaded instantly for client interaction -->
19
19
<KeyboardReviewInbox client:only="react" initialPr={mockInitialPr} />
20
20
</main>
21
-</Layout>
21
+</BaseLayout>
22
22
▼src/pages/insights.astro
+3-3
60
60
const myPrs = await db.query.pullRequests.findMany({
61
61
where: and(
62
62
eq(schema.pullRequests.authorId, user.id),
63
- gte(schema.pullRequests.createdAt, startDate.toISOString())
63
+ gte(schema.pullRequests.createdAt, startDate)
64
64
),
65
65
});
66
66
82
82
const myReviews = await db.query.pullRequestReviews.findMany({
83
83
where: and(
84
84
eq(schema.pullRequestReviews.reviewerId, user.id),
85
- gte(schema.pullRequestReviews.createdAt, startDate.toISOString())
85
+ gte(schema.pullRequestReviews.createdAt, startDate)
86
86
),
87
87
});
88
88
124
124
// Top contributors (if viewing team)
125
125
if (view === "team") {
126
126
const allPrs = await db.query.pullRequests.findMany({
127
- where: gte(schema.pullRequests.createdAt, startDate.toISOString()),
127
+ where: gte(schema.pullRequests.createdAt, startDate),
128
128
with: { author: true },
129
129
limit: 100,
130
130
});
131
131
▼src/pages/merge-queue.astro
+2-2
138
138
id: item.id,
139
139
position: currentPosition,
140
140
status: item.status,
141
- priority: item.priority,
141
+ priority: item.priority ?? 0,
142
142
mergeMethod: item.mergeMethod || "merge",
143
- addedAt: item.addedAt,
143
+ addedAt: item.addedAt?.toISOString() ?? new Date().toISOString(),
144
144
pr: item.pullRequest
145
145
? {
146
146
number: item.pullRequest.number,
147
147