UNPKG

@vfarcic/dot-ai

Version:

AI-powered development productivity platform that enhances software development workflows through intelligent automation and AI-driven assistance

326 lines (325 loc) 15.7 kB
"use strict"; /** * Push to Git Tool - Push generated manifests to a Git repository * * PRD #395: Git Push Recommend Integration * * This stage allows users to push generated manifests directly to a Git * repository, enabling GitOps workflows with Argo CD, Flux, etc. */ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); exports.PUSHTOGIT_TOOL_INPUT_SCHEMA = exports.PUSHTOGIT_TOOL_DESCRIPTION = exports.PUSHTOGIT_TOOL_NAME = void 0; exports.handlePushToGitTool = handlePushToGitTool; const zod_1 = require("zod"); const fs = __importStar(require("fs")); const os = __importStar(require("os")); const path = __importStar(require("path")); const crypto_1 = require("crypto"); const error_handling_1 = require("../core/error-handling"); const index_1 = require("../core/index"); const generic_session_manager_1 = require("../core/generic-session-manager"); const git_utils_1 = require("../core/git-utils"); const visualization_1 = require("../core/visualization"); exports.PUSHTOGIT_TOOL_NAME = 'pushToGit'; exports.PUSHTOGIT_TOOL_DESCRIPTION = 'Push generated manifests to a Git repository for GitOps workflows (Argo CD, Flux). Use after generateManifests stage.'; exports.PUSHTOGIT_TOOL_INPUT_SCHEMA = { solutionId: zod_1.z .string() .regex(/^sol-\d+-[a-f0-9]{8}$/) .describe('The solution ID to push manifests for'), repoUrl: zod_1.z.string().url().describe('Git repository URL (HTTPS)'), targetPath: zod_1.z .string() .describe('Path within repository where manifests will be stored (e.g., "apps/postgresql/")'), branch: zod_1.z.string().optional().describe('Git branch (default: main)'), commitMessage: zod_1.z .string() .optional() .describe('Commit message (default: "Add {resource} deployment")'), authorName: zod_1.z.string().optional().describe('Git author name'), authorEmail: zod_1.z.string().optional().describe('Git author email'), interaction_id: zod_1.z .string() .optional() .describe('INTERNAL ONLY - Do not populate. Used for evaluation dataset generation.'), }; async function handlePushToGitTool(args, dotAI, logger, requestId, sessionManager) { return await error_handling_1.ErrorHandler.withErrorHandling(async () => { logger.info('Handling pushToGit request', { requestId, solutionId: args.solutionId, repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl), targetPath: args.targetPath, branch: args.branch, }); const sm = sessionManager || new generic_session_manager_1.GenericSessionManager('sol'); const session = sm.getSession(args.solutionId); if (!session) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, `Solution not found: ${args.solutionId}`, { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { solutionId: args.solutionId }, suggestedActions: [ 'Verify the solution ID is correct', 'Ensure generateManifests stage was completed first', 'Check that the session has not expired', ], }); } const solution = session.data; if (!solution.generatedManifests) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, 'No manifests found. Run generateManifests stage first.', { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { solutionId: args.solutionId }, suggestedActions: [ 'Call recommend tool with stage: generateManifests first', 'Ensure the solution was fully configured', ], }); } if (solution.generatedManifests.type === 'helm') { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, 'GitOps push for Helm charts is not yet supported. Use the deployManifests stage to install directly, or wait for a future release with Argo CD Application / Flux HelmRelease support.', { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { solutionId: args.solutionId }, suggestedActions: [ 'Use deployManifests stage to install Helm chart directly', 'Wait for future release with GitOps Helm support (Argo CD Application / Flux HelmRelease)', ], }); } const authConfig = (0, git_utils_1.getGitAuthConfigFromEnv)(); if (!authConfig.pat && !authConfig.githubApp) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.CONFIGURATION, error_handling_1.ErrorSeverity.HIGH, 'No Git authentication configured. Set DOT_AI_GIT_TOKEN or configure GitHub App.', { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl) }, suggestedActions: [ 'Set DOT_AI_GIT_TOKEN environment variable with a valid PAT', 'Or configure GitHub App authentication (GITHUB_APP_ENABLED, GITHUB_APP_ID, GITHUB_APP_PRIVATE_KEY)', ], }); } const branch = args.branch || 'main'; const defaultCommitMessage = `Add ${solution.intent || 'deployment'} manifests`; const commitMessage = args.commitMessage || defaultCommitMessage; const rawTargetPath = args.targetPath.trim(); if (rawTargetPath === '' || rawTargetPath.startsWith('/') || rawTargetPath.startsWith('~') || rawTargetPath.includes('\\') || rawTargetPath.includes('..')) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, 'Invalid target path: use a relative repository path without "/", "~", "\\", or ".."', { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { targetPath: args.targetPath }, suggestedActions: [ 'Use a relative repository path such as "apps/postgresql"', ], }); } const targetPath = rawTargetPath.replace(/\/+$/, ''); const tmpDir = path.join(os.tmpdir(), `dot-ai-git-${args.solutionId}-${(0, crypto_1.randomUUID)()}`); logger.info('Cloning repository', { repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl), branch, tmpDir, }); try { fs.rmSync(tmpDir, { recursive: true, force: true }); try { await (0, git_utils_1.cloneRepo)(args.repoUrl, tmpDir, { branch, depth: 1 }); } catch (cloneError) { const errorMessage = cloneError instanceof Error ? cloneError.message : String(cloneError); logger.error('Failed to clone repository', cloneError, { repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl), branch, }); throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.NETWORK, error_handling_1.ErrorSeverity.HIGH, `Failed to clone repository: ${errorMessage}`, { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl), branch }, suggestedActions: [ 'Verify the repository URL is correct', 'Check that the branch exists', 'Ensure your token has read access to the repository', ], }); } const files = []; // Handle raw/kustomize manifests (Helm is rejected earlier in validation) const manifestFiles = solution.generatedManifests.files; if (manifestFiles && manifestFiles.length > 0) { for (const file of manifestFiles) { const sanitizedPath = (0, git_utils_1.sanitizeRelativePath)(file.relativePath); files.push({ path: path.posix.join(targetPath, sanitizedPath), content: file.content, }); } } if (files.length === 0) { throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.VALIDATION, error_handling_1.ErrorSeverity.HIGH, 'No files to push. Manifests may be empty or missing content.', { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { solutionId: args.solutionId }, suggestedActions: [ 'Verify generateManifests completed successfully', 'Check that manifests contain content', ], }); } logger.info('Pushing files to repository', { fileCount: files.length, targetPath, branch, }); const filesPreview = files.map(f => ({ path: f.path, size: f.content.length, lines: f.content.split('\n').length, })); let pushResult; try { pushResult = await (0, git_utils_1.pushRepo)(tmpDir, files, commitMessage, { branch, author: args.authorName ? { name: args.authorName, email: args.authorEmail || 'dot-ai@users.noreply.github.com', } : undefined, }); } catch (pushError) { const errorMessage = pushError instanceof Error ? pushError.message : String(pushError); logger.error('Failed to push to repository', pushError, { repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl), branch, targetPath, }); throw error_handling_1.ErrorHandler.createError(error_handling_1.ErrorCategory.NETWORK, error_handling_1.ErrorSeverity.HIGH, `Failed to push to repository: ${errorMessage}`, { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl), branch, targetPath, }, suggestedActions: [ 'Ensure your token has write access to the repository', 'Check for merge conflicts (pull latest changes first)', 'Verify the branch exists or can be created', ], }); } sm.updateSession(args.solutionId, { stage: 'pushed', gitPush: { repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl), path: targetPath, branch: pushResult.branch, commitSha: pushResult.commitSha, pushedAt: new Date().toISOString(), }, }); const visualizationUrl = (0, visualization_1.getVisualizationUrl)(args.solutionId); const response = { success: true, status: 'manifests_pushed', solutionId: args.solutionId, gitPush: { repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl), path: targetPath, branch: pushResult.branch, commitSha: pushResult.commitSha, filesPushed: pushResult.filesAdded, pushedAt: new Date().toISOString(), }, filesPreview, gitopsMessage: `Manifests pushed successfully. Your GitOps controller (Argo CD/Flux) will sync these changes automatically.`, timestamp: new Date().toISOString(), ...(visualizationUrl ? { visualizationUrl } : {}), }; logger.info('Push to Git completed successfully', { solutionId: args.solutionId, commitSha: pushResult.commitSha, branch: pushResult.branch, }); const content = [ { type: 'text', text: JSON.stringify(response, null, 2), }, ]; const agentDisplayBlock = (0, index_1.buildAgentDisplayBlock)({ visualizationUrl }); if (agentDisplayBlock) { content.push(agentDisplayBlock); } return { content }; } finally { try { fs.rmSync(tmpDir, { recursive: true, force: true }); } catch (cleanupError) { logger.warn('Failed to cleanup temporary git directory', { tmpDir, error: cleanupError instanceof Error ? cleanupError.message : String(cleanupError), }); } } }, { operation: 'push_to_git', component: 'PushToGitTool', requestId, input: { ...args, repoUrl: (0, git_utils_1.scrubCredentials)(args.repoUrl) }, }); }