Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
node_modules
npm-debug.log
Dockerfile
Dockerfile.jenkins
docker-compose*.yml
.git
.gitignore
.env
.vscode
dist
54 changes: 17 additions & 37 deletions Dockerfile
Original file line number Diff line number Diff line change
@@ -1,37 +1,17 @@
# Jenkins LTS (with JDK 17). The current LTS line (>= 2.492.3) meets the MCP plugin minimum requirement.
FROM jenkins/jenkins:lts-jdk17

USER root

# Install base tools (git, curl, certificates).
# If you use dedicated Jenkins agents, also install git inside your agent images.
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl ca-certificates && \
rm -rf /var/lib/apt/lists/*

# Switch back to jenkins user
USER jenkins

# Preinstall plugins:
# MCP Server, Git, Git Client, GitHub integration, Pipeline, and Credentials
# Note: jenkins-plugin-cli is included in the official Jenkins image.
RUN jenkins-plugin-cli --plugins \
mcp-server \
git \
git-client \
github \
github-branch-source \
workflow-aggregator \
credentials \
ssh-credentials \
configuration-as-code

# Expose ports
EXPOSE 8080 50000

# (Optional) Jenkins startup parameters
# Disable the setup wizard on first startup:
# ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false"

# (Optional) Mount JCasC configuration file
# ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc.yaml
FROM node:20-alpine

WORKDIR /app

ENV NODE_ENV=production

ENV PORT=3000

COPY package*.json ./

RUN npm ci --omit=dev

COPY . .

EXPOSE 3000

CMD ["node", "server/server.js"]
37 changes: 37 additions & 0 deletions Dockerfile.jenkins
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
# Jenkins LTS (with JDK 17). The current LTS line (>= 2.492.3) meets the MCP plugin minimum requirement.
FROM jenkins/jenkins:lts-jdk17

USER root

# Install base tools (git, curl, certificates).
# If you use dedicated Jenkins agents, also install git inside your agent images.
RUN apt-get update && apt-get install -y --no-install-recommends \
git curl ca-certificates && \
rm -rf /var/lib/apt/lists/*

# Switch back to jenkins user
USER jenkins

# Preinstall plugins:
# MCP Server, Git, Git Client, GitHub integration, Pipeline, and Credentials
# Note: jenkins-plugin-cli is included in the official Jenkins image.
RUN jenkins-plugin-cli --plugins \
mcp-server \
git \
git-client \
github \
github-branch-source \
workflow-aggregator \
credentials \
ssh-credentials \
configuration-as-code

# Expose ports
EXPOSE 8080 50000

# (Optional) Jenkins startup parameters
# Disable the setup wizard on first startup:
# ENV JAVA_OPTS="-Djenkins.install.runSetupWizard=false"

# (Optional) Mount JCasC configuration file
# ENV CASC_JENKINS_CONFIG=/var/jenkins_home/casc.yaml
109 changes: 72 additions & 37 deletions server/agent/wizardAgent.js
Original file line number Diff line number Diff line change
@@ -1,15 +1,29 @@
import OpenAI from "openai";
import dotenv from "dotenv";
import fetch from "node-fetch";
import OpenAI from 'openai';
import dotenv from 'dotenv';
import fetch from 'node-fetch';

dotenv.config();
const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });
//const client = new OpenAI({ apiKey: process.env.OPENAI_API_KEY });

// Construct the OpenAI client lazily so that the server does not shut down completely when we build the container
let client = null;

function getClient() {
if (!client) {
const apiKey = process.env.OPENAI_API_KEY;
if (!apiKey) {
throw new Error('Missing OPENAI_API_KEY - cannot run Wizard Agent');
}
client = new OpenAI({ apiKey });
}
return client;
}

// Helper: call MCP routes dynamically, with error handling
async function callMCPTool(tool, input, cookie) {
try {
const response = await fetch(`http://localhost:3000/mcp/v1/${tool}`, {
method: "POST",
method: 'POST',
headers: {
"Content-Type": "application/json",
"Cookie": cookie || (process.env.MCP_SESSION_TOKEN ? `mcp_session=${process.env.MCP_SESSION_TOKEN}` : ""),
Expand All @@ -18,8 +32,8 @@ async function callMCPTool(tool, input, cookie) {
});
return await response.json();
} catch (err) {
console.warn("⚠️ MCP call failed:", err.message || err);
return { error: "MCP server unreachable" };
console.warn('⚠️ MCP call failed:', err.message || err);
return { error: 'MCP server unreachable' };
}
}

Expand Down Expand Up @@ -59,16 +73,18 @@ export async function runWizardAgent(userPrompt) {
Never invent new template names. If unsure, default to "node_app".
`;

const client = getClient();

const completion = await client.chat.completions.create({
model: "gpt-4o-mini",
model: 'gpt-4o-mini',
messages: [
{ role: "system", content: systemPrompt },
{ role: "user", content: typeof userPrompt === "string" ? userPrompt : userPrompt.prompt },
],
});

const decision = completion.choices[0].message.content;
console.log("\n🤖 Agent decided:", decision);
console.log('\n🤖 Agent decided:', decision);

let agentMeta = {
agent_decision: decision,
Expand Down Expand Up @@ -117,7 +133,7 @@ export async function runWizardAgent(userPrompt) {
if (usernameMatch) payload.username = usernameMatch[1];
if (userIdMatch) payload.user_id = userIdMatch[1];
if (repoMatch) {
const [username, repo] = repoMatch[1].split("/");
const [username, repo] = repoMatch[1].split('/');
payload.username = username;
payload.repo = `${username}/${repo}`;
}
Expand All @@ -132,12 +148,13 @@ export async function runWizardAgent(userPrompt) {
};
}

if (toolName === "pipeline_generator") {
if (toolName === 'pipeline_generator') {
if (!repo) {
console.warn("⚠️ Missing repo context for pipeline generation.");
return {
success: false,
error: "I couldn’t determine which repository you meant. Please specify it, e.g., 'generate pipeline for user/repo'."
console.warn('⚠️ Missing repo context for pipeline generation.');
return {
success: false,
error:
"I couldn’t determine which repository you meant. Please specify it, e.g., 'generate pipeline for user/repo'.",
};
}

Expand All @@ -154,29 +171,42 @@ export async function runWizardAgent(userPrompt) {
console.log(`📦 Retrieved repo info from GitHub:`, repoInfo);
}
} catch (err) {
console.warn("⚠️ Failed to fetch GitHub info before pipeline generation:", err.message);
console.warn(
'⚠️ Failed to fetch GitHub info before pipeline generation:',
err.message
);
}

// Merge language or visibility into payload if available
if (repoInfo?.language && !payload.language) payload.language = repoInfo.language.toLowerCase();
if (repoInfo?.visibility && !payload.visibility) payload.visibility = repoInfo.visibility;
if (repoInfo?.language && !payload.language)
payload.language = repoInfo.language.toLowerCase();
if (repoInfo?.visibility && !payload.visibility)
payload.visibility = repoInfo.visibility;

// Infer template if still missing
if (!payload.template) {
if (repoInfo?.language?.toLowerCase().includes("javascript") || repoInfo?.language?.toLowerCase().includes("typescript") || /js|ts|node|javascript/i.test(repo)) {
payload.template = "node_app";
} else if (repoInfo?.language?.toLowerCase().includes("python") || /py|flask|django/i.test(repo)) {
payload.template = "python_app";
if (
repoInfo?.language?.toLowerCase().includes('javascript') ||
repoInfo?.language?.toLowerCase().includes('typescript') ||
/js|ts|node|javascript/i.test(repo)
) {
payload.template = 'node_app';
} else if (
repoInfo?.language?.toLowerCase().includes('python') ||
/py|flask|django/i.test(repo)
) {
payload.template = 'python_app';
} else {
payload.template = "container_service";
payload.template = 'container_service';
}
console.log(`🪄 Inferred template: ${payload.template}`);
}

// --- Auto-correct short template names ---
if (payload.template === "node") payload.template = "node_app";
if (payload.template === "python") payload.template = "python_app";
if (payload.template === "container") payload.template = "container_service";
if (payload.template === 'node') payload.template = 'node_app';
if (payload.template === 'python') payload.template = 'python_app';
if (payload.template === 'container')
payload.template = 'container_service';

// --- Validate template against allowed values ---
const allowedTemplates = ["node_app", "python_app", "container_service"];
Expand All @@ -193,9 +223,13 @@ export async function runWizardAgent(userPrompt) {
}

// ✅ Ensure provider is valid before sending payload
if (!payload.provider || !["aws", "jenkins"].includes(payload.provider)) {
if (
!payload.provider ||
!['aws', 'jenkins'].includes(payload.provider)
) {
// Infer from repo visibility or fallback to AWS
payload.provider = repoInfo?.visibility === "private" ? "jenkins" : "aws";
payload.provider =
repoInfo?.visibility === 'private' ? 'jenkins' : 'aws';
console.log(`🧭 Inferred provider: ${payload.provider}`);
}

Expand Down Expand Up @@ -298,7 +332,7 @@ export async function runWizardAgent(userPrompt) {
};
}

if (toolName === "oidc_adapter") {
if (toolName === 'oidc_adapter') {
const payload = provider ? { provider } : {};
agentMeta.tool_called = "oidc_adapter";
const output = await callMCPTool("oidc_adapter", payload, cookie);
Expand All @@ -310,7 +344,7 @@ export async function runWizardAgent(userPrompt) {
};
}

if (toolName === "github_adapter") {
if (toolName === 'github_adapter') {
if (repo) {
agentMeta.tool_called = "github_adapter";
const output = await callMCPTool("github/info", { repo }, cookie);
Expand All @@ -321,10 +355,11 @@ export async function runWizardAgent(userPrompt) {
tool_output: output
};
} else {
console.warn("⚠️ Missing repo for GitHub info retrieval.");
return {
success: false,
error: "Couldn’t determine which repository to fetch. Please include it in your request (e.g., 'tell me about user/repo')."
console.warn('⚠️ Missing repo for GitHub info retrieval.');
return {
success: false,
error:
"Couldn’t determine which repository to fetch. Please include it in your request (e.g., 'tell me about user/repo').",
};
}
}
Expand All @@ -341,10 +376,10 @@ export async function runWizardAgent(userPrompt) {

// Example local test (can comment out for production)
if (process.argv[2]) {
const input = process.argv.slice(2).join(" ");
const input = process.argv.slice(2).join(' ');
runWizardAgent(input)
.then((res) => {
console.log("\n📦 Tool Output:\n", JSON.stringify(res, null, 2));
console.log('\n📦 Tool Output:\n', JSON.stringify(res, null, 2));
})
.catch(console.error);
}
}
6 changes: 5 additions & 1 deletion server/routes/auth.github.js
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,10 @@ const {
JWT_SECRET,
} = process.env;

// URL to redirect user to the apropiate endpoint after GitHub authentication success
const FRONTEND_URL =
process.env.FRONTEND_URL || 'http://localhost:5173/connect';

if (!GITHUB_CLIENT_ID || !GITHUB_CLIENT_SECRET || !GITHUB_OAUTH_REDIRECT_URI) {
console.warn('[WARN] Missing GitHub OAuth env vars');
}
Expand Down Expand Up @@ -158,7 +162,7 @@ router.get('/callback', async (req, res) => {
// secure: true, // enable on HTTPS
});

return res.redirect('/');
return res.redirect(FRONTEND_URL);
} catch (e) {
console.error('[OAuth callback] error:', e);
return res.status(500).send(`OAuth failed: ${e.message}`);
Expand Down
2 changes: 1 addition & 1 deletion server/server.js
Original file line number Diff line number Diff line change
Expand Up @@ -75,7 +75,7 @@ const UserBody = z.object({

// Create or upsert user by email
app.post('/users', async (req, res) => {
const parse = UserBody.safeParse(req.body); // love that you are doing this. great.
const parse = UserBody.safeParse(req.body);
if (!parse.success)
return res.status(400).json({ error: parse.error.message });
const { email, github_username } = parse.data;
Expand Down