Skip to content

Commit bb81be7

Browse files
committed
Add CI Workflow for execution service
1 parent 6e2086e commit bb81be7

File tree

4 files changed

+302
-2
lines changed

4 files changed

+302
-2
lines changed
Lines changed: 68 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,68 @@
1+
name: CI (backend - execution service)
2+
3+
on:
4+
pull_request:
5+
branches: [dev, staging]
6+
paths:
7+
- "backend/execution-service/**"
8+
- "pnpm-lock.yaml"
9+
- "package.json"
10+
- ".github/workflows/ci-execution-service.yml"
11+
push:
12+
branches: [dev, staging]
13+
paths:
14+
- "backend/execution-service/**"
15+
- "pnpm-lock.yaml"
16+
- "package.json"
17+
- ".github/workflows/ci-execution-service.yml"
18+
19+
concurrency:
20+
group: ci-${{ github.workflow }}-${{ github.ref }}
21+
cancel-in-progress: true
22+
23+
jobs:
24+
build-test:
25+
name: Build
26+
runs-on: ubuntu-latest
27+
env:
28+
CI: true
29+
services:
30+
mongodb:
31+
image: mongo:8
32+
ports:
33+
- 27017:27017
34+
35+
steps:
36+
- name: Checkout
37+
uses: actions/checkout@v4
38+
39+
- name: Setup Node
40+
uses: actions/setup-node@v4
41+
with:
42+
node-version: "20.x"
43+
44+
- name: Enable Corepack & pin pnpm
45+
run: |
46+
corepack enable
47+
corepack prepare [email protected] --activate
48+
pnpm -v
49+
50+
- name: Install deps (workspace root)
51+
run: pnpm install --frozen-lockfile
52+
53+
54+
- name: Lint (execution service)
55+
run: pnpm --filter execution-service lint
56+
57+
- name: Set up Docker Buildx
58+
uses: docker/setup-buildx-action@v3
59+
60+
- name: Build Docker image
61+
uses: docker/build-push-action@v6
62+
with:
63+
context: backend/execution-service
64+
file: backend/execution-service/Dockerfile.execution
65+
push: false
66+
tags: execution-service:test
67+
cache-from: type=gha
68+
cache-to: type=gha,mode=max

docker-compose.yml

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,9 @@ services:
5353
- cs3219-peerprep-network
5454

5555
execution-api:
56-
build: ./backend/execution-service
56+
build:
57+
context: ./backend/execution-service
58+
dockerfile: Dockerfile.execution
5759
container_name: execution-api
5860
ports:
5961
- "8010:8010"
@@ -65,7 +67,9 @@ services:
6567
- cs3219-peerprep-network
6668
command: pnpm run start:api
6769
execution-worker:
68-
build: ./backend/execution-service
70+
build:
71+
context: ./backend/execution-service
72+
dockerfile: Dockerfile.execution
6973
container_name: execution-worker
7074
env_file:
7175
- ./backend/execution-service/.env
Lines changed: 228 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,228 @@
1+
"use client";
2+
3+
import { useRef, useEffect, useState } from "react";
4+
import { useParams } from "next/navigation";
5+
import { Editor } from "@monaco-editor/react";
6+
import type { editor } from "monaco-editor";
7+
import * as monaco from "monaco-editor";
8+
import * as Y from "yjs";
9+
import { WebsocketProvider } from "y-websocket";
10+
import { MonacoBinding } from "y-monaco";
11+
import { toast } from "react-toastify";
12+
import { Separator } from "@/components/ui/separator";
13+
import { Card, CardTitle, CardHeader } from "@/components/ui/card";
14+
import {
15+
Select,
16+
SelectContent,
17+
SelectGroup,
18+
SelectItem,
19+
SelectTrigger,
20+
SelectValue,
21+
} from "@/components/ui/select";
22+
import { useCollaborationState, useCollaborationActions } from "@/stores/collaboration-store";
23+
import { ProgrammingLanguage, ConnectionState } from "@/utils/enums";
24+
import { collaborationConfig } from "@/utils/config";
25+
26+
interface CodeEditorPanelProps {
27+
readOnly?: boolean;
28+
}
29+
30+
const programmingLanguageMonacoMap: Record<ProgrammingLanguage, string> = {
31+
[ProgrammingLanguage.PYTHON]: "python",
32+
[ProgrammingLanguage.JAVASCRIPT]: "javascript",
33+
[ProgrammingLanguage.JAVA]: "java",
34+
[ProgrammingLanguage.CPP]: "cpp",
35+
};
36+
37+
const programmingLanguageDisplayMap: Record<ProgrammingLanguage, string> = {
38+
[ProgrammingLanguage.PYTHON]: "Python",
39+
[ProgrammingLanguage.JAVASCRIPT]: "JavaScript",
40+
[ProgrammingLanguage.JAVA]: "Java",
41+
[ProgrammingLanguage.CPP]: "C++",
42+
};
43+
44+
export default function CodeEditorPanel({ readOnly = false }: CodeEditorPanelProps) {
45+
const params = useParams();
46+
const roomId = params?.id as string;
47+
48+
const { roomDetails, documentContent } = useCollaborationState();
49+
const { changeLanguage, updateLanguage } = useCollaborationActions();
50+
51+
const [editorInstance, setEditorInstance] = useState<editor.IStandaloneCodeEditor | null>(null);
52+
const [monacoInstance, setMonacoInstance] = useState<typeof monaco | null>(null);
53+
const [connectionStatus, setConnectionStatus] = useState<ConnectionState>(
54+
ConnectionState.DISCONNECTED,
55+
);
56+
57+
const ydocRef = useRef<Y.Doc | null>(null);
58+
const providerRef = useRef<WebsocketProvider | null>(null);
59+
const bindingRef = useRef<MonacoBinding | null>(null);
60+
61+
const currentLanguage = roomDetails?.programmingLanguage || ProgrammingLanguage.PYTHON;
62+
const monacoLanguage = programmingLanguageMonacoMap[currentLanguage];
63+
64+
// Initialize Yjs and WebSocket provider for active rooms
65+
useEffect(() => {
66+
if (!roomId || !roomDetails?.isActive || !editorInstance || providerRef.current) {
67+
return;
68+
}
69+
70+
// Create Yjs document
71+
const ydoc = new Y.Doc();
72+
ydocRef.current = ydoc;
73+
74+
// Create WebSocket provider
75+
const wsUrl = collaborationConfig.WS_URL;
76+
const provider = new WebsocketProvider(wsUrl, roomId, ydoc);
77+
providerRef.current = provider;
78+
79+
// Create Monaco binding
80+
const binding = new MonacoBinding(
81+
ydoc.getText("monaco"),
82+
editorInstance.getModel()!,
83+
new Set([editorInstance]),
84+
provider.awareness,
85+
);
86+
bindingRef.current = binding;
87+
88+
setConnectionStatus(ConnectionState.CONNECTING);
89+
90+
// Listen for connection status
91+
provider.on("status", (event: { status: string }) => {
92+
if (event.status === ConnectionState.CONNECTED) {
93+
setConnectionStatus(ConnectionState.CONNECTED);
94+
toast.success("Connected to collaboration session");
95+
} else if (event.status === ConnectionState.DISCONNECTED) {
96+
setConnectionStatus(ConnectionState.DISCONNECTED);
97+
toast.warn("Disconnected from collaboration session");
98+
}
99+
});
100+
101+
// Listen for custom messages
102+
const handleMessage = (event: MessageEvent) => {
103+
try {
104+
const message = JSON.parse(event.data);
105+
if (message.type === "language-change-notification") {
106+
const programmingLanguage = message.data.language as ProgrammingLanguage;
107+
updateLanguage(programmingLanguage);
108+
toast.info(`Language changed to ${programmingLanguageDisplayMap[programmingLanguage]}`);
109+
} else if (message.type === "room-close-notification") {
110+
toast.warn("The collaboration room has been closed");
111+
}
112+
} catch {
113+
// Ignore non-JSON messages (e.g., Yjs updates)
114+
}
115+
};
116+
provider.ws?.addEventListener("message", handleMessage);
117+
118+
// Cleanup
119+
return () => {
120+
provider.ws?.removeEventListener("message", handleMessage);
121+
binding.destroy();
122+
provider.destroy();
123+
ydoc.destroy();
124+
};
125+
}, [roomId, roomDetails?.isActive, editorInstance, updateLanguage]);
126+
127+
// Update Monaco language when room language changes
128+
useEffect(() => {
129+
if (editorInstance && monacoLanguage && monacoInstance) {
130+
const model = editorInstance.getModel();
131+
if (model) {
132+
monacoInstance.editor.setModelLanguage(model, monacoLanguage);
133+
}
134+
}
135+
}, [editorInstance, monacoLanguage, monacoInstance]);
136+
137+
// Handle language change from dropdown
138+
const handleLanguageChange = async (language: ProgrammingLanguage) => {
139+
if (!roomId || readOnly) return;
140+
changeLanguage(roomId, language).catch((err) => {
141+
console.error("Failed to change language:", err);
142+
});
143+
};
144+
145+
// Handle editor mount
146+
const handleEditorDidMount = (
147+
editorRef: editor.IStandaloneCodeEditor,
148+
monacoRef: typeof monaco,
149+
) => {
150+
setEditorInstance(editorRef);
151+
setMonacoInstance(monacoRef);
152+
153+
// Set document content for read-only mode
154+
if (readOnly && documentContent) {
155+
editorRef.setValue(documentContent);
156+
}
157+
};
158+
159+
// Connection status badge
160+
const getConnectionBadge = () => {
161+
if (readOnly || !roomDetails?.isActive) {
162+
return (
163+
<div className="flex items-center gap-2">
164+
<div className="w-2 h-2 rounded-full bg-gray-500" />
165+
<span className="text-sm text-muted-foreground">Read-Only</span>
166+
</div>
167+
);
168+
}
169+
170+
const statusConfig = {
171+
[ConnectionState.CONNECTING]: { color: "bg-yellow-500", text: "Connecting..." },
172+
[ConnectionState.CONNECTED]: { color: "bg-green-500", text: "Connected" },
173+
[ConnectionState.DISCONNECTED]: { color: "bg-red-500", text: "Disconnected" },
174+
[ConnectionState.RECONNECTING]: { color: "bg-yellow-500", text: "Reconnecting..." },
175+
};
176+
177+
const config = statusConfig[connectionStatus];
178+
179+
return (
180+
<div className="flex items-center gap-2">
181+
<div className={`w-2 h-2 rounded-full ${config.color}`} />
182+
<span className="text-sm text-muted-foreground">{config.text}</span>
183+
</div>
184+
);
185+
};
186+
187+
return (
188+
<Card className="rounded-none min-h-full h-auto w-full">
189+
<CardHeader>
190+
<div className="flex items-center justify-between">
191+
<div className="flex items-center gap-4">
192+
<CardTitle>Editor</CardTitle>
193+
<Select
194+
value={currentLanguage}
195+
onValueChange={handleLanguageChange}
196+
disabled={readOnly}
197+
>
198+
<SelectTrigger className="w-[220px]">
199+
<SelectValue placeholder="Language" />
200+
</SelectTrigger>
201+
<SelectContent className="max-h-[300px] overflow-y-auto">
202+
<SelectGroup>
203+
{Object.values(ProgrammingLanguage).map((lang) => (
204+
<SelectItem key={lang} value={lang}>
205+
{programmingLanguageDisplayMap[lang]}
206+
</SelectItem>
207+
))}
208+
</SelectGroup>
209+
</SelectContent>
210+
</Select>
211+
</div>
212+
{getConnectionBadge()}
213+
</div>
214+
</CardHeader>
215+
<Separator />
216+
<Editor
217+
height="60vh"
218+
language={monacoLanguage}
219+
theme="vs-dark"
220+
options={{
221+
readOnly: readOnly,
222+
fontSize: 14,
223+
}}
224+
onMount={handleEditorDidMount}
225+
/>
226+
</Card>
227+
);
228+
}

0 commit comments

Comments
 (0)