Skip to content

Commit e2b1017

Browse files
committed
feat(ci): add Husky pre-commit hooks to prevent CI failures
1 parent 8d599b5 commit e2b1017

File tree

4 files changed

+254
-1
lines changed

4 files changed

+254
-1
lines changed

.husky/pre-commit

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
#!/usr/bin/env bash
2+
set -e # Exit on any command failure
3+
4+
echo "🔍 Running pre-commit checks..."
5+
6+
echo "📝 Running lint..."
7+
if ! pnpm run lint; then
8+
echo "❌ Lint failed! Please fix the issues before committing."
9+
exit 1
10+
fi
11+
12+
echo "🏗️ Running build..."
13+
if ! NODE_OPTIONS="--max-old-space-size=8192" pnpm run build; then
14+
echo "❌ Build failed! Please fix the issues before committing."
15+
exit 1
16+
fi
17+
18+
echo "✅ Pre-commit checks passed!"

package.json

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,8 @@
3333
"db:generate": "drizzle-kit generate",
3434
"db:push": "drizzle-kit push",
3535
"db:studio": "drizzle-kit studio",
36-
"db:test": "tsx scripts/test-db.ts"
36+
"db:test": "tsx scripts/test-db.ts",
37+
"prepare": "husky"
3738
},
3839
"dependencies": {
3940
"@clerk/backend": "^2.5.0",
@@ -63,6 +64,7 @@
6364
"esbuild": "^0.25.9",
6465
"eslint": "^8.57.1",
6566
"globals": "^16.3.0",
67+
"husky": "^9.1.7",
6668
"jest": "^30.0.3",
6769
"prettier": "^3.4.2",
6870
"semantic-release": "^24.2.7",

pnpm-lock.yaml

Lines changed: 10 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

src/routes/code.ts

Lines changed: 223 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,223 @@
1+
import { Hono } from "hono";
2+
import { zValidator } from "@hono/zod-validator";
3+
import { HTTPException } from "hono/http-exception";
4+
import { z } from "zod";
5+
6+
const codeRouter = new Hono();
7+
8+
// Environment variables
9+
const JUDGE0_API_URL = process.env.JUDGE0_API_URL || "https://judge0-ce.p.rapidapi.com";
10+
const JUDGE0_API_KEY = process.env.JUDGE0_API_KEY;
11+
const JUDGE0_API_HOST = process.env.JUDGE0_API_HOST || "judge0-ce.p.rapidapi.com";
12+
13+
if (!JUDGE0_API_KEY) {
14+
console.error("❌ JUDGE0_API_KEY environment variable is required");
15+
process.exit(1);
16+
}
17+
18+
// Validation schemas with proper security limits
19+
const executeCodeSchema = z.object({
20+
language_id: z.number().int().min(1).max(200), // Reasonable language ID range
21+
source_code: z.string().min(1).max(50000), // 50KB max source code
22+
stdin: z.string().max(10000).optional().default(""), // 10KB max stdin
23+
cpu_time_limit: z.number().min(1).max(30).optional().default(5), // 1-30 seconds
24+
memory_limit: z.number().min(16384).max(512000).optional().default(128000), // 16MB-512MB
25+
wall_time_limit: z.number().min(1).max(60).optional().default(10), // 1-60 seconds
26+
});
27+
28+
const tokenSchema = z.object({
29+
token: z.string().min(1),
30+
});
31+
32+
// Helper function to make Judge0 API requests
33+
async function makeJudge0Request(endpoint: string, options: RequestInit = {}) {
34+
const url = `${JUDGE0_API_URL}${endpoint}`;
35+
36+
// Add timeout protection
37+
const controller = new AbortController();
38+
const timeoutId = setTimeout(() => controller.abort(), 30000); // 30s timeout
39+
40+
try {
41+
const response = await fetch(url, {
42+
...options,
43+
signal: controller.signal,
44+
headers: {
45+
"X-RapidAPI-Key": JUDGE0_API_KEY,
46+
"X-RapidAPI-Host": JUDGE0_API_HOST,
47+
"Content-Type": "application/json",
48+
...options.headers,
49+
},
50+
});
51+
52+
clearTimeout(timeoutId);
53+
54+
if (!response.ok) {
55+
// Log full error details server-side only
56+
const errorBody = await response.text().catch(() => '');
57+
console.error(`Judge0 API Error: ${response.status} ${response.statusText} - ${errorBody}`);
58+
59+
// Return sanitized error messages to frontend
60+
let clientMessage = "Code execution failed. Please try again.";
61+
let statusCode = response.status;
62+
63+
if (response.status === 429) {
64+
clientMessage = "Code execution service is temporarily busy. Please try again in a few minutes.";
65+
} else if (response.status === 401 || response.status === 403) {
66+
clientMessage = "Code execution service is temporarily unavailable. Please contact support.";
67+
statusCode = 503; // Don't expose auth issues
68+
} else if (response.status >= 500) {
69+
clientMessage = "Code execution service is temporarily unavailable. Please try again later.";
70+
statusCode = 503;
71+
}
72+
73+
throw new HTTPException(statusCode, {
74+
message: clientMessage,
75+
});
76+
}
77+
78+
return response;
79+
} catch (error) {
80+
clearTimeout(timeoutId);
81+
82+
if (error instanceof HTTPException) {
83+
throw error;
84+
}
85+
86+
if (error.name === 'AbortError') {
87+
console.error("Judge0 API timeout");
88+
throw new HTTPException(504, {
89+
message: "Code execution timed out. Please try again."
90+
});
91+
}
92+
93+
console.error("Judge0 API request failed:", error);
94+
throw new HTTPException(503, {
95+
message: "Code execution service temporarily unavailable",
96+
});
97+
}
98+
}
99+
100+
// POST /api/code/execute - Submit code for execution
101+
codeRouter.post(
102+
"/execute",
103+
zValidator("json", executeCodeSchema),
104+
async (c) => {
105+
try {
106+
const body = c.req.valid("json");
107+
108+
// Base64 encode the source code and stdin for Judge0
109+
const submissionData = {
110+
...body,
111+
source_code: Buffer.from(body.source_code).toString("base64"),
112+
stdin: Buffer.from(body.stdin || "").toString("base64"),
113+
};
114+
115+
const response = await makeJudge0Request("/submissions?base64_encoded=true", {
116+
method: "POST",
117+
body: JSON.stringify(submissionData),
118+
});
119+
120+
const result = await response.json();
121+
return c.json(result);
122+
} catch (error) {
123+
if (error instanceof HTTPException) {
124+
throw error;
125+
}
126+
127+
console.error("Code execution error:", error);
128+
throw new HTTPException(500, {
129+
message: "Failed to submit code for execution",
130+
});
131+
}
132+
}
133+
);
134+
135+
// GET /api/code/status/:token - Get execution status and results
136+
codeRouter.get(
137+
"/status/:token",
138+
zValidator("param", tokenSchema),
139+
async (c) => {
140+
try {
141+
const { token } = c.req.valid("param");
142+
143+
const response = await makeJudge0Request(
144+
`/submissions/${token}?base64_encoded=true`
145+
);
146+
147+
const result = await response.json();
148+
149+
// Decode base64 encoded fields if they exist
150+
if (result.stdout) {
151+
result.stdout = Buffer.from(result.stdout, "base64").toString("utf-8");
152+
}
153+
if (result.stderr) {
154+
result.stderr = Buffer.from(result.stderr, "base64").toString("utf-8");
155+
}
156+
if (result.compile_output) {
157+
result.compile_output = Buffer.from(result.compile_output, "base64").toString("utf-8");
158+
}
159+
if (result.message) {
160+
result.message = Buffer.from(result.message, "base64").toString("utf-8");
161+
}
162+
163+
return c.json(result);
164+
} catch (error) {
165+
if (error instanceof HTTPException) {
166+
throw error;
167+
}
168+
169+
console.error("Status check error:", error);
170+
throw new HTTPException(500, {
171+
message: "Failed to check execution status",
172+
});
173+
}
174+
}
175+
);
176+
177+
// GET /api/code/languages - Get supported languages (optional endpoint)
178+
codeRouter.get("/languages", async (c) => {
179+
try {
180+
const response = await makeJudge0Request("/languages");
181+
const result = await response.json();
182+
return c.json(result);
183+
} catch (error) {
184+
if (error instanceof HTTPException) {
185+
throw error;
186+
}
187+
188+
console.error("Languages fetch error:", error);
189+
throw new HTTPException(500, {
190+
message: "Failed to fetch supported languages",
191+
});
192+
}
193+
});
194+
195+
// GET /api/code/health - Health check for Judge0 service connectivity
196+
codeRouter.get("/health", async (c) => {
197+
try {
198+
const response = await makeJudge0Request("/languages");
199+
200+
if (response.ok) {
201+
return c.json({
202+
status: "healthy",
203+
judge0: "connected",
204+
timestamp: new Date().toISOString(),
205+
});
206+
} else {
207+
return c.json({
208+
status: "degraded",
209+
judge0: "partial_connectivity",
210+
timestamp: new Date().toISOString(),
211+
}, 207); // Multi-status
212+
}
213+
} catch (error) {
214+
console.error("Judge0 health check failed:", error);
215+
return c.json({
216+
status: "unhealthy",
217+
judge0: "disconnected",
218+
timestamp: new Date().toISOString(),
219+
}, 503);
220+
}
221+
});
222+
223+
export default codeRouter;

0 commit comments

Comments
 (0)