diff --git a/server/routes/pipelineCommit.js b/server/routes/pipelineCommit.js new file mode 100644 index 0000000..3310982 --- /dev/null +++ b/server/routes/pipelineCommit.js @@ -0,0 +1,67 @@ +import { Router } from 'express'; +import { requireSession } from '../lib/requireSession.js'; +import { getGithubAccessTokenForUser } from '../lib/github-token.js'; +import { upsertWorkflowFile } from '../tools/github_adapter.js'; + +const router = Router(); + +/** + * POST /mcp/v1/pipeline_commit + * Body: + * { + * "repoFullName": "owner/repo", + * "branch": "main", + * "yaml": "name: CI/CD Pipeline ...", + * "path": ".github/workflows/ci.yml" + * } + */ +router.post('/pipeline_commit', requireSession, async (req, res) => { + try { + const { repoFullName, branch, yaml, path } = req.body || {}; + if (!repoFullName || !yaml) { + return res + .status(400) + .json({ error: 'repoFullName and yaml are required' }); + } + + const userId = req.user?.user_id; + if (!userId) + return res.status(401).json({ error: 'User session missing or invalid' }); + + const token = await getGithubAccessTokenForUser(userId); + if (!token) + return res.status(401).json({ error: 'Missing GitHub token for user' }); + + const [owner, repo] = repoFullName.split('/'); + const workflowPath = path || '.github/workflows/ci.yml'; + const branchName = branch || 'main'; + + console.log( + `[pipeline_commit] Committing workflow to ${repoFullName}:${workflowPath}` + ); + + const result = await upsertWorkflowFile({ + token, + owner, + repo, + path: workflowPath, + content: yaml, + branch: branchName, + message: 'Add CI workflow via OSP', + }); + + return res.status(201).json({ + ok: true, + message: 'Workflow committed successfully', + data: result, + }); + } catch (err) { + console.error('[pipeline_commit] error:', err); + const status = err.status || 500; + return res + .status(status) + .json({ error: err.message, details: err.details || undefined }); + } +}); + +export default router; diff --git a/server/server.js b/server/server.js index dbe1f25..29118c8 100644 --- a/server/server.js +++ b/server/server.js @@ -4,17 +4,19 @@ import cors from 'cors'; import helmet from 'helmet'; import morgan from 'morgan'; import { healthCheck } from './db.js'; -import githubAuthRouter from './routes/auth.github.js'; -import userRouter from './routes/usersRoutes.js'; import mcpRoutes from './routes/mcp.js'; import agentRoutes from './routes/agent.js'; -import cookieParser from 'cookie-parser'; +import githubAuthRouter from './routes/auth.github.js'; import deploymentsRouter from './routes/deployments.js'; +import authRoutes from './routes/authRoutes.js'; +import userRouter from './routes/usersRoutes.js'; +import cookieParser from 'cookie-parser'; import authAws from './routes/auth.aws.js'; import authGoogle from './routes/auth.google.js'; import { z } from 'zod'; import { query } from './db.js'; -import jenkinsRouter from "./routes/jenkins.js"; +import jenkinsRouter from './routes/jenkins.js'; +import pipelineCommitRouter from './routes/pipelineCommit.js'; // app.use(authRoutes); const app = express(); @@ -137,12 +139,12 @@ app.get('/connections', async (_req, res) => { // -- Agent entry point app.use('/deployments', deploymentsRouter); app.use('/agent', agentRoutes); +app.use('/mcp/v1', pipelineCommitRouter); app.use('/mcp/v1', mcpRoutes); // Mount GitHub OAuth routes at /auth/github app.use('/auth/github', githubAuthRouter); - - +app.use(authRoutes); // Mount AWS SSO routes app.use('/auth/aws', authAws); @@ -150,8 +152,6 @@ app.use('/auth/aws', authAws); app.use('/auth/google', authGoogle); app.use('/jenkins', jenkinsRouter); -// // Mount GitHub OAuth routes at /auth/github -// app.use('/auth/github', githubAuthRouter); // --- Global Error Handler --- app.use((err, req, res, next) => { @@ -163,7 +163,5 @@ app.use((err, req, res, next) => { }); }); - - const port = process.env.PORT || 4000; app.listen(port, () => console.log(`API on http://localhost:${port}`)); diff --git a/server/tools/github_adapter.js b/server/tools/github_adapter.js index 6b622fe..2907020 100644 --- a/server/tools/github_adapter.js +++ b/server/tools/github_adapter.js @@ -255,3 +255,69 @@ export async function dispatchWorkflow({ } catch {} throw err; } + +// Helper function to create and update a workflow file in a repo +export async function upsertWorkflowFile({ + token, + owner, + repo, + path, + content, + branch, + message = 'Add CI workflow via AutoDeploy', +}) { + const baseUrl = `https://api.github.com/repos/${owner}/${repo}/contents/${encodeURIComponent( + path + )}`; + + const headers = { + Authorization: `Bearer ${token}`, + Accept: 'application/vnd.github+json', + 'User-Agent': 'OSP-CI-Builder', + 'Content-Type': 'application/json', + }; + + //check if the file exists to get the SHA + let sha = undefined; + const getRes = await fetch(`${baseUrl}?ref=${encodeURIComponent(branch)}`, { + headers, + }); + + if (getRes.status === 200) { + const existing = await getRes.json(); + sha = existing.sha; + } else if (getRes.status === 400) { + const text = await getRes.text().catch(() => ''); + throw new Error( + `Get existing workflow failed: ${getRes.status} ${getRes.statusText} ${text}` + ); + } + + //Encode content as base64 character data + const encoded = Buffer.from(content, 'utf-8').toString('base64'); + + //PUT to creat/update the file + const body = { + message, + content: encoded, + branch, + }; + + if (sha) body.sha = sha; + + const putRes = await fetch(baseUrl, { + method: 'PUT', + headers, + body: JSON.stringify(body), + }); + + if (!putRes.ok) { + const text = await putRes.text().catch(() => ''); + throw new Error( + `Upsert workflow file failed: ${putRes.status} ${putRes.statusText} ${text}` + ); + } + + const data = await putRes.json(); + return data; +} diff --git a/server/tools/pipeline_generator.js b/server/tools/pipeline_generator.js index d121617..2b9c9a8 100644 --- a/server/tools/pipeline_generator.js +++ b/server/tools/pipeline_generator.js @@ -1,22 +1,23 @@ -import { github_adapter } from "./github_adapter.js"; +import { github_adapter } from './github_adapter.js'; -import { pool } from "../db.js"; +import { pool } from '../db.js'; -import jwt from "jsonwebtoken"; +import jwt from 'jsonwebtoken'; -import { z } from "zod"; -import { requireSession } from "../lib/requireSession.js"; +import { z } from 'zod'; +import { requireSession } from '../lib/requireSession.js'; export const pipeline_generator = { - name: "pipeline_generator", - description: "Generate a mock CI/CD YAML configuration for a given repository and provider.", + name: 'pipeline_generator', + description: + 'Generate a mock CI/CD YAML configuration for a given repository and provider.', // ✅ Input schema for validation input_schema: z.object({ repo: z.string(), - branch: z.string().default("main"), - provider: z.enum(["aws", "jenkins"]), - template: z.enum(["node_app", "python_app", "container_service"]), + branch: z.string().default('main'), + provider: z.enum(['aws', 'jenkins']), + template: z.enum(['node_app', 'python_app', 'container_service']), options: z .object({ run_tests: z.boolean().default(true), @@ -27,7 +28,7 @@ export const pipeline_generator = { }), // Real handler (queries github_adapter for repo info and generates pipeline config) - handler: async ({ repo, branch = "main", provider, template, options }) => { + handler: async ({ repo, branch = 'main', provider, template, options }) => { const sessionToken = process.env.MCP_SESSION_TOKEN; let decoded = {}; let userId = null; @@ -37,10 +38,10 @@ export const pipeline_generator = { const sessionData = await requireSession(); if (sessionData?.user?.id) { userId = sessionData.user.id; - console.log("🧩 Resolved user_id from session:", userId); + console.log('🧩 Resolved user_id from session:', userId); } } catch { - console.warn("âš ī¸ No active session found, trying token decode..."); + console.warn('âš ī¸ No active session found, trying token decode...'); } // Fallback: decode MCP_SESSION_TOKEN if no user found @@ -48,20 +49,22 @@ export const pipeline_generator = { try { decoded = jwt.decode(sessionToken); userId = decoded?.user?.id || decoded?.sub || null; - if (userId) console.log("🧠 Resolved user_id from decoded token:", userId); + if (userId) + console.log('🧠 Resolved user_id from decoded token:', userId); } catch (err) { - console.warn("âš ī¸ Could not decode MCP_SESSION_TOKEN:", err.message); + console.warn('âš ī¸ Could not decode MCP_SESSION_TOKEN:', err.message); } } if (!userId) { - console.warn("âš ī¸ Could not resolve user_id — defaulting to anonymous."); - userId = "00000000-0000-0000-0000-000000000000"; + console.warn('âš ī¸ Could not resolve user_id — defaulting to anonymous.'); + userId = '00000000-0000-0000-0000-000000000000'; } // 🧠 Try to resolve user_id from GitHub username if still anonymous - if (userId === "00000000-0000-0000-0000-000000000000") { - let githubUsername = decoded?.github_username || process.env.GITHUB_USERNAME || null; + if (userId === '00000000-0000-0000-0000-000000000000') { + let githubUsername = + decoded?.github_username || process.env.GITHUB_USERNAME || null; if (githubUsername) { try { @@ -72,15 +75,21 @@ export const pipeline_generator = { if (userRows.length > 0) { userId = userRows[0].id; - console.log("🔄 Resolved user_id from github_username:", userId); + console.log('🔄 Resolved user_id from github_username:', userId); } else { - console.warn("âš ī¸ No user found in DB matching github_username:", githubUsername); + console.warn( + 'âš ī¸ No user found in DB matching github_username:', + githubUsername + ); } } catch (err) { - console.warn("âš ī¸ Failed to resolve user_id from github_username:", err.message); + console.warn( + 'âš ī¸ Failed to resolve user_id from github_username:', + err.message + ); } } else { - console.warn("âš ī¸ No GitHub username available to resolve user_id."); + console.warn('âš ī¸ No GitHub username available to resolve user_id.'); } } @@ -98,12 +107,12 @@ export const pipeline_generator = { if (rows.length > 0 && rows[0].access_token) { githubToken = rows[0].access_token; - console.log("đŸ—ī¸ GitHub token retrieved from DB for user:", userId); + console.log('đŸ—ī¸ GitHub token retrieved from DB for user:', userId); } else { - console.warn("âš ī¸ No GitHub access token found for user:", userId); + console.warn('âš ī¸ No GitHub access token found for user:', userId); } } catch (dbErr) { - console.warn("âš ī¸ DB lookup failed:", dbErr.message); + console.warn('âš ī¸ DB lookup failed:', dbErr.message); } if (!githubToken) { @@ -113,18 +122,22 @@ export const pipeline_generator = { (globalThis.MCP_SESSION?.github_token ?? null); } - console.log("đŸĒļ Using GitHub token from source:", githubToken ? "available" : "missing"); + console.log( + 'đŸĒļ Using GitHub token from source:', + githubToken ? 'available' : 'missing' + ); if (!githubToken) { return { success: false, - error: "No GitHub access token found for this user.", + error: 'No GitHub access token found for this user.', }; } // ✅ Normalize repo path to include username/repo format - if (!repo.includes("/")) { - let githubUsername = decoded?.github_username || process.env.GITHUB_USERNAME; + if (!repo.includes('/')) { + let githubUsername = + decoded?.github_username || process.env.GITHUB_USERNAME; if (!githubUsername) { try { const { rows: userRows } = await pool.query( @@ -133,20 +146,28 @@ export const pipeline_generator = { ); if (userRows.length > 0) { githubUsername = userRows[0].github_username; - console.log("🧠 Retrieved GitHub username from DB:", githubUsername); + console.log( + '🧠 Retrieved GitHub username from DB:', + githubUsername + ); } else { - console.warn("âš ī¸ No GitHub username found in DB for user:", userId); + console.warn('âš ī¸ No GitHub username found in DB for user:', userId); } } catch (dbErr) { - console.warn("âš ī¸ Failed to query DB for GitHub username:", dbErr.message); + console.warn( + 'âš ī¸ Failed to query DB for GitHub username:', + dbErr.message + ); } } if (githubUsername) { repo = `${githubUsername}/${repo}`; - console.log("🧩 Normalized repo path:", repo); + console.log('🧩 Normalized repo path:', repo); } else { - console.warn("âš ī¸ Cannot normalize repo path: no GitHub username found."); + console.warn( + 'âš ī¸ Cannot normalize repo path: no GitHub username found.' + ); } } @@ -154,13 +175,13 @@ export const pipeline_generator = { let repoInfo; try { repoInfo = await github_adapter.handler({ - action: "get_repo", + action: 'get_repo', user_id: userId, repo, - token: githubToken + token: githubToken, }); } catch (err) { - console.warn("âš ī¸ GitHub API fetch failed:", err.message); + console.warn('âš ī¸ GitHub API fetch failed:', err.message); repoInfo = null; } @@ -171,38 +192,45 @@ export const pipeline_generator = { // If repo info failed, try fallback to github_adapter info or mock data if (!repoInfo) { - console.warn(`âš ī¸ Could not retrieve repository information for ${repo}, attempting fallback...`); + console.warn( + `âš ī¸ Could not retrieve repository information for ${repo}, attempting fallback...` + ); try { repoInfo = await github_adapter.handler({ - action: "info", + action: 'info', user_id: userId, repo, - token: githubToken + token: githubToken, }); - console.log("🧠 Fallback repo info retrieved successfully."); + console.log('🧠 Fallback repo info retrieved successfully.'); } catch (fallbackErr) { - console.warn("âš ī¸ Fallback GitHub info retrieval failed:", fallbackErr.message); + console.warn( + 'âš ī¸ Fallback GitHub info retrieval failed:', + fallbackErr.message + ); } } // Final safeguard: if still missing, create mock repo info to continue pipeline generation if (!repoInfo) { - console.warn("âš ī¸ Both primary and fallback repo info unavailable — using minimal mock data."); + console.warn( + 'âš ī¸ Both primary and fallback repo info unavailable — using minimal mock data.' + ); repoInfo = { - language: "JavaScript", - visibility: "public", + language: 'JavaScript', + visibility: 'public', default_branch: branch, }; } // Determine defaults dynamically - const language = repoInfo.language || "JavaScript"; - const inferredTemplate = language.toLowerCase().includes("python") - ? "python_app" - : "node_app"; + const language = repoInfo.language || 'JavaScript'; + const inferredTemplate = language.toLowerCase().includes('python') + ? 'python_app' + : 'node_app'; const inferredProvider = - repoInfo.visibility === "private" ? "jenkins" : "aws"; + repoInfo.visibility === 'private' ? 'jenkins' : 'aws'; const selectedProvider = provider || inferredProvider; const selectedTemplate = template || inferredTemplate; @@ -220,12 +248,18 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 - - name: Setup ${selectedTemplate === "node_app" ? "Node.js" : "Python"} - uses: actions/setup-${selectedTemplate === "node_app" ? "node" : "python"}@v4 + - name: Setup ${selectedTemplate === 'node_app' ? 'Node.js' : 'Python'} + uses: actions/setup-${ + selectedTemplate === 'node_app' ? 'node' : 'python' + }@v4 - name: Install Dependencies - run: ${selectedTemplate === "node_app" ? "npm ci" : "pip install -r requirements.txt"} + run: ${ + selectedTemplate === 'node_app' + ? 'npm ci' + : 'pip install -r requirements.txt' + } - name: Run Tests - run: ${selectedTemplate === "node_app" ? "npm test" : "pytest"} + run: ${selectedTemplate === 'node_app' ? 'npm test' : 'pytest'} deploy: needs: build runs-on: ubuntu-latest @@ -245,11 +279,11 @@ jobs: provider: selectedProvider, template: selectedTemplate, options: options || {}, - stages: ["build", "test", "deploy"], + stages: ['build', 'test', 'deploy'], generated_yaml, repo_info: repoInfo, created_at: new Date().toISOString(), }, }; }, -}; \ No newline at end of file +};