Skip to content

Commit c155d3b

Browse files
committed
Added ci cd
1 parent d92078f commit c155d3b

File tree

21 files changed

+263
-6183
lines changed

21 files changed

+263
-6183
lines changed
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
name: Docker Publish
2+
3+
on:
4+
push:
5+
branches: [ main ]
6+
workflow_dispatch:
7+
8+
jobs:
9+
build-and-push:
10+
runs-on: ubuntu-latest
11+
steps:
12+
- uses: actions/checkout@v4
13+
14+
- name: Login to Docker Hub
15+
uses: docker/login-action@v3
16+
with:
17+
username: ${{ secrets.DOCKERHUB_USERNAME }}
18+
password: ${{ secrets.DOCKERHUB_TOKEN }}
19+
20+
- name: Build and push code-server
21+
uses: docker/build-push-action@v5
22+
with:
23+
context: ./mobile-magic
24+
file: ./mobile-magic/docker/code-server.Dockerfile
25+
push: true
26+
tags: 100xdevs/antidevs-frontend:${{ github.sha }}

mobile-magic/apps/frontend/app/project/[projectId]/Project.tsx

Lines changed: 24 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,32 +1,33 @@
11
"use client";
22

3-
import { useState } from "react";
3+
import { useCallback, useEffect, useRef, useState } from "react";
44
import { Button } from "@/components/ui/button";
55
import { Textarea } from "@/components/ui/textarea";
66
import { Header } from "@/components/Header"
7-
import { FRONTEND_URL, WORKER_URL } from "@/config";
87
import { MoveUpRight, SquarePen } from "lucide-react";
98
import { usePrompts } from "@/hooks/usePrompts";
109
import { useAuth, useUser } from "@clerk/nextjs";
1110
import { SidebarInset } from "@/components/ui/sidebar"
12-
import { WORKER_API_URL } from "@/config";
13-
import { useRouter } from "next/navigation"
11+
import { useRouter, useSearchParams } from "next/navigation"
1412
import Image from "next/image";
1513
import axios from "axios";
1614

17-
export const Project: React.FC<{ projectId: string }> = ({ projectId }) => {
15+
export const Project: React.FC<{ projectId: string, sessionUrl: string, previewUrl: string, workerUrl: string }> = ({projectId, sessionUrl, previewUrl, workerUrl }) => {
16+
const searchParams = useSearchParams()
17+
const initPrompt = searchParams.get('initPrompt');
18+
1819
const router = useRouter()
1920
const { prompts } = usePrompts(projectId);
20-
const [prompt, setPrompt] = useState("");
21+
const prompt = useRef(initPrompt || "");
2122
const { getToken } = useAuth();
2223
const { user } = useUser()
2324
const [tab, setTab] = useState("code");
2425

25-
const onSubmit = async (e: React.FormEvent<HTMLFormElement>) => {
26+
const onSubmit = useCallback(async (e: React.FormEvent<HTMLFormElement>) => {
2627
e.preventDefault()
2728
const token = await getToken();
2829
axios.post(
29-
`${WORKER_API_URL}/prompt`,
30+
`${workerUrl}/prompt`,
3031
{
3132
projectId: projectId,
3233
prompt: prompt,
@@ -37,8 +38,15 @@ export const Project: React.FC<{ projectId: string }> = ({ projectId }) => {
3738
},
3839
},
3940
);
40-
setPrompt("");
41-
}
41+
prompt.current = "";
42+
}, [projectId, workerUrl, getToken]);
43+
44+
useEffect(() => {
45+
if (initPrompt) {
46+
prompt.current = initPrompt;
47+
onSubmit({ preventDefault: () => {} } as React.FormEvent<HTMLFormElement>);
48+
}
49+
}, [onSubmit, initPrompt]);
4250

4351
return (
4452
<SidebarInset className="bg-transparent">
@@ -54,7 +62,7 @@ export const Project: React.FC<{ projectId: string }> = ({ projectId }) => {
5462
<div className="flex flex-col space-y-4 justify-center">
5563
{/* we are filtering only user prompts and rendering it */}
5664
{prompts.filter((prompt) => prompt.type === "USER").map((prompt) => (
57-
<div>
65+
<div key={prompt.id}>
5866
<span key={prompt.id} className="flex text-lg gap-2">
5967
<Image src={user?.imageUrl || ""} width={10} height={10} alt="Profile picture" className="rounded-full w-6 h-6" />
6068
{prompt.content}
@@ -78,17 +86,17 @@ export const Project: React.FC<{ projectId: string }> = ({ projectId }) => {
7886
<form onSubmit={(e) => onSubmit(e)} className="relative w-full border-2 bg-gray-500/10 focus-within:outline-1 focus-within:outline-teal-300/30 rounded-xl">
7987
<div className="p-2">
8088
<Textarea
81-
value={prompt}
89+
value={prompt.current}
8290
placeholder="Write a prompt..."
83-
onChange={(e) => setPrompt(e.target.value)}
91+
onChange={(e) => prompt.current = e.target.value}
8492
className="w-full placeholder:text-gray-400/60 shadow-none bg-transparent border-none text-md rounded-none focus-visible:ring-0 min-h-16 max-h-80 resize-none outline-none"
8593
/>
8694
</div>
8795
<div className="p-2 flex items-center justify-end">
8896
<Button
8997
type="submit"
9098
className="h-10 w-10 cursor-pointer rounded-full bg-teal-200/10 hover:bg-teal-300/20 flex items-center justify-center"
91-
disabled={!prompt}
99+
disabled={!prompt.current}
92100
>
93101
<MoveUpRight className="w-10 h-10 text-teal-300/70" />
94102
</Button>
@@ -105,14 +113,14 @@ export const Project: React.FC<{ projectId: string }> = ({ projectId }) => {
105113
<div className="flex gap-2 h-full">
106114
<div className={`${tab === "code" ? "left-0 flex-1" : tab === "split" ? "left-0 flex-1" : "left-full flex-0"} position-absolute transition-all duration-300 h-full w-full`}>
107115
<iframe
108-
src={`${WORKER_URL}/`}
116+
src={`${sessionUrl}/`}
109117
className="w-full h-full rounded-lg"
110118
title="Project Worker"
111119
/>
112120
</div>
113121
<div className={`${tab === "preview" ? "left-0 flex-1" : tab === "split" ? "left-0 flex-1" : "left-full flex-0"} position-absolute transition-all duration-300 h-full w-full`}>
114122
<iframe
115-
src={`${FRONTEND_URL}/`}
123+
src={`${previewUrl}/`}
116124
className="w-full h-full rounded-lg"
117125
title="Project Worker"
118126
/>
Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
1+
import axios from "axios";
12
import { Project } from "./Project"
3+
import { K8S_ORCHESTRATOR_URL } from "@/config";
24

35
interface Params {
46
params: Promise<{ projectId: string }>
@@ -7,5 +9,13 @@ interface Params {
79
export default async function ProjectPage({ params }: Params) {
810
const projectId = (await params).projectId
911

10-
return <Project projectId={projectId} />
12+
const response = await axios.get(`${K8S_ORCHESTRATOR_URL}/worker/${projectId}`);
13+
14+
const { sessionUrl, previewUrl, workerUrl } = response.data;
15+
return <Project
16+
projectId={projectId}
17+
sessionUrl={sessionUrl}
18+
previewUrl={previewUrl}
19+
workerUrl={workerUrl}
20+
/>
1121
}

mobile-magic/apps/frontend/components/Prompt.tsx

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
"use client";
22

33
import { useState, useEffect, useRef } from "react";
4-
import { BACKEND_URL, WORKER_API_URL } from "@/config";
4+
import { BACKEND_URL } from "@/config";
55
import { useRouter } from "next/navigation";
66
import { Textarea } from "./ui/textarea";
77
import { useAuth } from "@clerk/nextjs";
@@ -27,7 +27,7 @@ export function Prompt() {
2727
const [isSignedIn, setIsSignedIn] = useState<boolean>(false);
2828
const promptRef = useRef<HTMLTextAreaElement>(null);
2929
const [prompt, setPrompt] = useState("");
30-
const [type, setType] = useState<"NEXTJS" | "REACT_NATIVE">("NEXTJS");
30+
const [type, setType] = useState<"NEXTJS" | "REACT_NATIVE" | "REACT">("NEXTJS");
3131

3232
const { getToken } = useAuth();
3333
const router = useRouter();
@@ -53,13 +53,8 @@ export function Prompt() {
5353
"Authorization": `Bearer ${token}`
5454
}
5555
})
56-
// You should get the worker url here.
57-
await axios.post(`${WORKER_API_URL}/prompt`, {
58-
projectId: response.data.projectId,
59-
prompt: prompt,
60-
});
6156

62-
router.push(`/project/${response.data.projectId}`);
57+
router.push(`/project/${response.data.projectId}?initPrompt=${prompt}`);
6358
}
6459

6560
return (
@@ -70,6 +65,9 @@ export function Prompt() {
7065
>
7166
<div className="px-4 py-2 sm:static sm:w-auto fixed bottom-0 left-0 w-full">
7267
<div className="flex flex-row gap-2 mb-4">
68+
<Button variant={type === "REACT" ? "default" : "outline"} onClick={() => setType("REACT")}>
69+
React
70+
</Button>
7371
<Button variant={type === "NEXTJS" ? "default" : "outline"} onClick={() => setType("NEXTJS")}>
7472
NextJS
7573
</Button>
Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11

2-
export const BACKEND_URL = "http://localhost:9090";
2+
export const BACKEND_URL = process.env.NEXT_PUBLIC_BACKEND_URL || "http://localhost:9090";
33
// TODO: This should come from the router when we add it eventually
4-
export const WORKER_URL = "http://localhost:8080";
5-
export const FRONTEND_URL = "http://localhost:8081";
6-
export const WORKER_API_URL = "http://localhost:9091";
4+
5+
export const K8S_ORCHESTRATOR_URL = process.env.NEXT_PUBLIC_K8S_ORCHESTRATOR_URL || "http://localhost:7001";
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,2 +1,2 @@
11

2-
export const DOMAIN = "cloud.100xdevs.com";
2+
export const DOMAIN = "cloud.antidevs.com";

mobile-magic/apps/k8s-orchestrator/index.ts

Lines changed: 79 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { KubeConfig, CoreV1Api } from "@kubernetes/client-node";
44
import * as k8s from "@kubernetes/client-node";
55
import { getRandomName } from "./names";
66
import cors from 'cors';
7+
import { Writable } from 'stream';
78
import { addNewPod, addProjectToPod, redisClient, removePod } from './redis';
89
import { getAllPods } from './redis';
910
import { DOMAIN } from './config';
@@ -34,7 +35,7 @@ async function listPods(): Promise<string[]> {
3435
return res.items.filter(pod => pod.status?.phase === 'Running' || pod.status?.phase === 'Pending').filter(pod => pod.metadata?.name).map((pod) => pod.metadata?.name as string);
3536
}
3637

37-
async function createPod(projectType: "NEXTJS" | "REACT_NATIVE") {
38+
async function createPod() {
3839
const name = getRandomName();
3940

4041
await k8sApi.createNamespacedPod({ namespace: 'user-apps', body: {
@@ -45,16 +46,30 @@ async function createPod(projectType: "NEXTJS" | "REACT_NATIVE") {
4546
},
4647
},
4748
spec: {
48-
initContainers: [{
49-
name: 'init',
50-
image: '100xdevs/code-server-base:latest',
51-
command: ["/bin/sh", "-c"],
52-
args: [`cp -r ${PROJECT_TYPE_TO_BASE_FOLDER[projectType]} /app`]
53-
}],
5449
containers: [{
55-
name,
56-
image: '100xdevs/code-server-base:latest',
50+
name: "code-server",
51+
image: '100xdevs/code-server-base:v2',
5752
ports: [{ containerPort: 8080 }, { containerPort: 8081 }],
53+
}, {
54+
name: "ws-relayer",
55+
image: "100xdevs/ws-relayer:latest",
56+
ports: [{ containerPort: 9093 }],
57+
}, {
58+
name: "worker",
59+
image: "100xdevs/worker:latest",
60+
ports: [{ containerPort: 9091 }],
61+
env: [{
62+
name: "WS_RELAYER_URL",
63+
value: `ws://localhost:9091`,
64+
}, {
65+
name: "ANTHROPIC_API_KEY",
66+
valueFrom: {
67+
secretKeyRef: {
68+
name: "worker-secret",
69+
key: "ANTHROPIC_API_KEY",
70+
}
71+
}
72+
}]
5873
}],
5974
},
6075
}});
@@ -82,15 +97,59 @@ async function createPod(projectType: "NEXTJS" | "REACT_NATIVE") {
8297
ports: [{ port: 8080, targetPort: 8081, protocol: 'TCP', name: 'preview' }],
8398
},
8499
}});
100+
101+
await k8sApi.createNamespacedService({ namespace: 'user-apps', body: {
102+
metadata: {
103+
name: `worker-${name}`,
104+
},
105+
spec: {
106+
selector: {
107+
app: name,
108+
},
109+
ports: [{ port: 8080, targetPort: 9091, protocol: 'TCP', name: 'preview' }],
110+
},
111+
}});
85112
}
86113

87-
async function assignPodToProject(projectId: string, projectType: "NEXTJS" | "REACT_NATIVE") {
114+
async function assignPodToProject(projectId: string, projectType: "NEXTJS" | "REACT_NATIVE" | "REACT") {
88115
const pods = await getAllPods();
89-
const pod = Object.keys(pods).find((pod) => pods[pod] === "empty");
116+
const pod = Object.keys(pods).find((pod) => pods[pod] === "empty");
90117
if (!pod) {
91-
await createPod(projectType);
118+
await createPod();
92119
return assignPodToProject(projectId, projectType);
93120
}
121+
122+
const exec = new k8s.Exec(kc);
123+
console.log(pod);
124+
let stdout = "";
125+
let stderr = "";
126+
console.log("executing")
127+
exec.exec("user-apps", pod, "code-server", ["/bin/sh", "-c", `mv ${PROJECT_TYPE_TO_BASE_FOLDER[projectType]}/* /app`],
128+
new Writable({
129+
write: (chunk: Buffer, encoding: BufferEncoding, callback: () => void) => {
130+
stdout += chunk;
131+
callback();
132+
}
133+
}),
134+
new Writable({
135+
write: (chunk: Buffer, encoding: BufferEncoding, callback: () => void) => {
136+
stderr += chunk;
137+
callback();
138+
}
139+
}),
140+
null,
141+
false,
142+
(status) => {
143+
console.log(status);
144+
console.log(stdout);
145+
console.log(stderr);
146+
}
147+
);
148+
149+
await new Promise((resolve) => setTimeout(resolve, 1000));
150+
console.log(stdout);
151+
console.log(stderr);
152+
94153
addProjectToPod(projectId, pod);
95154
console.log(`Assigned project ${projectId} to pod ${pod}`);
96155
return pod;
@@ -105,12 +164,17 @@ app.get("/worker/:projectId", async (req, res) => {
105164
});
106165

107166
if (!project) {
108-
return res.status(404).json({ error: "Project not found" });
167+
res.status(404).json({ error: "Project not found" });
168+
return;
109169
}
110170

111-
const pod = await assignPodToProject(projectId, project.type);
171+
const pod = await assignPodToProject(projectId, "REACT"); // project.type);
112172

113-
res.json({ workerUrl: `https://session-${pod}.${DOMAIN}`, previewUrl: `https://preview-${pod}.${DOMAIN}` });
173+
res.json({
174+
sessionUrl: `https://session-${pod}.${DOMAIN}`,
175+
previewUrl: `https://preview-${pod}.${DOMAIN}`,
176+
workerUrl: `http://worker-${pod}.${DOMAIN}`
177+
});
114178
});
115179

116180
app.listen(7001, () => {

mobile-magic/apps/k8s-orchestrator/redis.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,4 +22,5 @@ export function getAllPods() {
2222
export async function addProjectToPod(projectId: string, pod: string) {
2323
await redisClient.hSet("POD_MAPPING", pod, projectId);
2424
await redisClient.expire("POD_MAPPING", 300); // expire after 5 minutes
25-
}
25+
}
26+

mobile-magic/apps/primary-backend/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -41,5 +41,5 @@ app.get("/prompts/:projectId", authMiddleware, async (req, res) => {
4141
});
4242

4343
app.listen(9090, () => {
44-
console.log("Server is running on port 3000");
44+
console.log("Server is running on port 9090");
4545
});

0 commit comments

Comments
 (0)