Skip to main content
Glama
init.ts11.8 kB
/** * CI/CD Initialization */ import chalk from 'chalk'; import * as fs from 'fs'; import * as path from 'path'; import { execSync } from 'child_process'; import prompts from 'prompts'; import { Platform, ProjectType, TEMPLATES, getTemplate, getTemplateContent, TemplateVariables, } from './templates.js'; export interface CICDInitOptions { platform?: Platform; template?: ProjectType; mainBranch?: string; devBranch?: string; branchProtection?: boolean; interactive?: boolean; force?: boolean; } interface GitInfo { owner: string; repo: string; platform: Platform; remoteUrl: string; } /** * 检测 Git 信息 */ function detectGitInfo(): GitInfo | null { try { const remoteUrl = execSync('git remote get-url origin', { encoding: 'utf-8' }).trim(); // 解析 Git URL // 支持格式: // - https://github.com/owner/repo.git // - https://gitea.example.com/owner/repo.git // - git@github.com:owner/repo.git // - git@gitea.example.com:owner/repo.git let owner = ''; let repo = ''; let platform: Platform = 'gitea'; if (remoteUrl.includes('github.com')) { platform = 'github'; } if (remoteUrl.startsWith('https://')) { const urlPath = remoteUrl.replace(/^https:\/\/[^/]+\//, '').replace(/\.git$/, ''); const parts = urlPath.split('/'); if (parts.length >= 2) { owner = parts[0]; repo = parts[1]; } } else if (remoteUrl.startsWith('git@')) { const urlPath = remoteUrl.replace(/^git@[^:]+:/, '').replace(/\.git$/, ''); const parts = urlPath.split('/'); if (parts.length >= 2) { owner = parts[0]; repo = parts[1]; } } if (owner && repo) { return { owner, repo, platform, remoteUrl }; } } catch { // 忽略错误 } return null; } /** * 检测项目类型 */ function detectProjectType(): ProjectType | null { const cwd = process.cwd(); if (fs.existsSync(path.join(cwd, 'package.json'))) { return 'nodejs'; } if (fs.existsSync(path.join(cwd, 'go.mod'))) { return 'go'; } if ( fs.existsSync(path.join(cwd, 'requirements.txt')) || fs.existsSync(path.join(cwd, 'pyproject.toml')) || fs.existsSync(path.join(cwd, 'setup.py')) ) { return 'python'; } if (fs.existsSync(path.join(cwd, 'Cargo.toml'))) { return 'rust'; } if (fs.existsSync(path.join(cwd, 'Dockerfile'))) { return 'docker'; } return null; } /** * 获取项目名称 */ function getProjectName(): string { const cwd = process.cwd(); // 尝试从 package.json 获取 const packageJsonPath = path.join(cwd, 'package.json'); if (fs.existsSync(packageJsonPath)) { try { const pkg = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8')); if (pkg.name) { return pkg.name; } } catch { // 忽略 } } // 使用目录名 return path.basename(cwd); } /** * 获取工作流目录 */ function getWorkflowDir(platform: Platform): string { if (platform === 'github') { return '.github/workflows'; } return '.gitea/workflows'; } /** * 检查现有配置 */ function checkExistingConfig(): { gitea: boolean; github: boolean } { return { gitea: fs.existsSync('.gitea/workflows'), github: fs.existsSync('.github/workflows'), }; } /** * 初始化 CI/CD */ export async function initCICD(options: CICDInitOptions): Promise<void> { console.log(chalk.bold('\n🚀 CI/CD 配置初始化\n')); // 检测 Git 信息 const gitInfo = detectGitInfo(); if (!gitInfo) { console.log(chalk.yellow('⚠️ 未检测到 Git 仓库,请确保在 Git 仓库目录中运行')); } else { console.log(chalk.gray(`📁 仓库: ${gitInfo.owner}/${gitInfo.repo}`)); console.log(chalk.gray(`🔗 平台: ${gitInfo.platform === 'github' ? 'GitHub' : 'Gitea'}`)); } // 检测项目类型 const detectedType = detectProjectType(); if (detectedType) { console.log(chalk.gray(`📦 检测到项目类型: ${detectedType}`)); } // 检查现有配置 const existing = checkExistingConfig(); if (existing.gitea || existing.github) { const platforms = []; if (existing.gitea) platforms.push('Gitea'); if (existing.github) platforms.push('GitHub'); console.log(chalk.yellow(`\n⚠️ 检测到现有 CI/CD 配置: ${platforms.join(', ')}`)); if (!options.force) { const { confirm } = await prompts({ type: 'confirm', name: 'confirm', message: '是否覆盖现有配置?', initial: false, }); if (!confirm) { console.log(chalk.gray('\n已取消')); return; } } } // 交互式收集配置 let platform: Platform = options.platform || gitInfo?.platform || 'gitea'; let template: ProjectType = options.template || detectedType || 'nodejs'; let mainBranch = options.mainBranch || 'main'; let devBranch = options.devBranch || 'dev'; let setupBranchProtection = options.branchProtection !== false; if (options.interactive !== false) { const response = await prompts([ { type: 'select', name: 'platform', message: '选择 CI 平台', choices: [ { title: 'Gitea Actions', value: 'gitea' }, { title: 'GitHub Actions', value: 'github' }, ], initial: platform === 'github' ? 1 : 0, }, { type: 'select', name: 'template', message: '选择项目类型', choices: TEMPLATES.map((t) => ({ title: `${t.name} - ${t.description}`, value: t.id, })), initial: TEMPLATES.findIndex((t) => t.id === template) || 0, }, { type: 'text', name: 'mainBranch', message: '主分支名称(发布正式版)', initial: mainBranch, }, { type: 'text', name: 'devBranch', message: '开发分支名称(发布 Beta 版)', initial: devBranch, }, { type: 'confirm', name: 'branchProtection', message: '是否配置分支保护规则?', initial: setupBranchProtection, }, ]); if (!response.platform) { console.log(chalk.gray('\n已取消')); return; } platform = response.platform; template = response.template; mainBranch = response.mainBranch; devBranch = response.devBranch; setupBranchProtection = response.branchProtection; } // 准备模板变量 const variables: TemplateVariables = { projectName: getProjectName(), owner: gitInfo?.owner || 'owner', repo: gitInfo?.repo || 'repo', mainBranch, devBranch, nodeVersion: '20', goVersion: '1.21', pythonVersion: '3.11', rustVersion: 'stable', }; // 获取模板信息 const templateInfo = getTemplate(template); if (!templateInfo) { console.log(chalk.red(`\n❌ 未找到模板: ${template}`)); return; } // 创建工作流目录 const workflowDir = getWorkflowDir(platform); fs.mkdirSync(workflowDir, { recursive: true }); console.log(chalk.bold('\n📝 生成工作流文件...\n')); // 生成工作流文件 for (const fileName of templateInfo.files) { const filePath = path.join(workflowDir, fileName); const content = getTemplateContent(template, platform, fileName, variables); fs.writeFileSync(filePath, content); console.log(chalk.green(` ✓ ${filePath}`)); } // 创建 CONTRIBUTING.md(如果不存在) if (!fs.existsSync('CONTRIBUTING.md')) { const contributingContent = generateContributingDoc(mainBranch, devBranch); fs.writeFileSync('CONTRIBUTING.md', contributingContent); console.log(chalk.green(` ✓ CONTRIBUTING.md`)); } console.log(chalk.bold('\n✅ CI/CD 配置完成!\n')); // 显示后续步骤 console.log(chalk.bold('📋 后续步骤:\n')); if (template === 'nodejs') { console.log(chalk.cyan(' 1. 配置 NPM_TOKEN secret')); console.log(chalk.gray(` 在仓库设置中添加 NPM_TOKEN 用于发布到 npm`)); } else if (template === 'python') { console.log(chalk.cyan(' 1. 配置 PYPI_TOKEN secret')); console.log(chalk.gray(` 在仓库设置中添加 PYPI_TOKEN 用于发布到 PyPI`)); } else if (template === 'docker') { console.log(chalk.cyan(' 1. 配置 Docker secrets')); console.log(chalk.gray(` DOCKER_USERNAME 和 DOCKER_PASSWORD`)); } if (setupBranchProtection) { console.log(chalk.cyan('\n 2. 配置分支保护规则')); console.log(chalk.gray(` 运行以下命令配置分支保护:`)); console.log(chalk.white(` keactl cicd validate --fix`)); } console.log(chalk.cyan('\n 3. 创建开发分支')); console.log(chalk.gray(` git checkout -b ${devBranch}`)); console.log(chalk.gray(` git push -u origin ${devBranch}`)); console.log(chalk.cyan('\n 4. 开始开发')); console.log(chalk.gray(` 按照 CONTRIBUTING.md 中的流程进行开发`)); console.log(); } /** * 生成 CONTRIBUTING.md 文档 */ function generateContributingDoc(mainBranch: string, devBranch: string): string { return `# 贡献指南 ## 分支策略 \`\`\` feature/* ──┐ bugfix/* ──┼──→ ${devBranch} ──────→ ${mainBranch} fix/* ──┤ │ │ feat/* ──┘ ↓ ↓ beta版 正式版 hotfix/* ─────────────────→ ${mainBranch} (紧急修复) \`\`\` ### 分支说明 | 分支 | 用途 | 合并来源 | |------|------|----------| | \`${mainBranch}\` | 稳定版本,发布正式版 | \`${devBranch}\`, \`hotfix/*\` | | \`${devBranch}\` | 开发分支,发布 beta 版 | \`feature/*\`, \`bugfix/*\`, \`fix/*\`, \`feat/*\` | | \`feature/*\` | 新功能开发 | - | | \`bugfix/*\` / \`fix/*\` | Bug 修复 | - | | \`hotfix/*\` | 紧急修复 | - | ## 开发流程 ### 1. 创建功能分支 \`\`\`bash # 新功能 git checkout ${devBranch} git pull origin ${devBranch} git checkout -b feature/your-feature # Bug 修复 git checkout -b fix/issue-description \`\`\` ### 2. 开发和提交 \`\`\`bash # 提交代码 git add . git commit -m "feat: add new feature" # 推送分支 git push origin feature/your-feature \`\`\` ### 3. 创建 Pull Request - **目标分支**: \`${devBranch}\` - **标题格式**: \`feat: xxx\` 或 \`fix: xxx\` - 等待 CI 检查通过 - 请求 review(如需要) ### 4. 合并后自动发布 - 合并到 \`${devBranch}\` → 自动发布 beta 版本 - 合并到 \`${mainBranch}\` → 自动发布正式版本 ## Commit 规范 使用 [Conventional Commits](https://www.conventionalcommits.org/) 规范: \`\`\` <type>(<scope>): <subject> <body> <footer> \`\`\` ### Type 类型 | 类型 | 说明 | 版本影响 | |------|------|----------| | \`feat\` | 新功能 | minor (1.0.0 → 1.1.0) | | \`fix\` | Bug 修复 | patch (1.0.0 → 1.0.1) | | \`docs\` | 文档更新 | - | | \`style\` | 代码格式 | - | | \`refactor\` | 重构 | - | | \`test\` | 测试 | - | | \`chore\` | 构建/工具 | - | | \`breaking\` | 破坏性变更 | major (1.0.0 → 2.0.0) | ### 示例 \`\`\`bash # 新功能 git commit -m "feat(cli): add init command" # Bug 修复 git commit -m "fix(api): handle null response" # 破坏性变更 git commit -m "feat!: change API response format BREAKING CHANGE: response format changed from array to object" \`\`\` ## 紧急修复 \`\`\`bash # 创建 hotfix 分支 git checkout ${mainBranch} git checkout -b hotfix/critical-fix # 修复并提交 git commit -m "fix: critical bug fix" # 直接 PR 到 ${mainBranch} git push origin hotfix/critical-fix # 创建 PR: hotfix/critical-fix → ${mainBranch} \`\`\` `; }

Latest Blog Posts

MCP directory API

We provide all the information about MCP servers via our MCP API.

curl -X GET 'https://glama.ai/api/mcp/v1/servers/SupenBysz/gitea-mcp-tool'

If you have feedback or need assistance with the MCP directory API, please join our Discord server