UNPKG

@ruan-cat/release-toolkit

Version:

基于 changelogen 增强 changesets 工作流的发布工具包,提供语义化提交解析和 GitHub Release 同步功能。

557 lines (549 loc) 19.8 kB
var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var index_exports = {}; __export(index_exports, { GitHubReleaseSync: () => GitHubReleaseSync, changelogConfig: () => changelogen_config_default, changelogFunctions: () => changelog_with_changelogen_default, generateChangelogFromGitHistory: () => generateChangelogFromGitHistory, generateHybridChangelog: () => generateHybridChangelog, runSync: () => runSync }); module.exports = __toCommonJS(index_exports); // src/plugins/changelog-with-changelogen.ts var import_consola = require("consola"); var import_commitlint_config2 = require("@ruan-cat/commitlint-config"); var import_changelogen = require("changelogen"); // src/configs/changelogen.config.ts var import_commitlint_config = require("@ruan-cat/commitlint-config"); var createCompleteTypeMapping = () => { const typeMapping = {}; import_commitlint_config.commitTypes.forEach(({ type, description, emoji }) => { typeMapping[type] = { title: emoji ? `${emoji} ${description}` : description, semver: getSemverByType(type) }; }); const gitmojiMapping = { // 新功能类 sparkles: { title: "\u2728 \u65B0\u589E\u529F\u80FD", semver: "minor" }, zap: { title: "\u26A1 \u6027\u80FD\u4F18\u5316", semver: "patch" }, // 修复类 bug: { title: "\u{1F41E} \u4FEE\u590D\u95EE\u9898", semver: "patch" }, ambulance: { title: "\u{1F691} \u7D27\u6025\u4FEE\u590D", semver: "patch" }, // 文档类 memo: { title: "\u{1F4DD} \u66F4\u65B0\u6587\u6863", semver: "patch" }, // 构建类 package: { title: "\u{1F4E6} \u6784\u5EFA\u7CFB\u7EDF", semver: "patch" }, rocket: { title: "\u{1F680} \u90E8\u7F72\u529F\u80FD", semver: "patch" }, // 其他 other: { title: "\u5176\u4ED6\u66F4\u6539", semver: "patch" } }; return { ...typeMapping, ...gitmojiMapping }; }; var config = { // 仓库配置 repo: { provider: "github", repo: "ruan-cat/monorepo" }, // 完整的提交类型映射 - 支持 emoji + conventional commits types: createCompleteTypeMapping(), // 作用域映射 - 增强 scope 显示,支持中文映射 scopeMap: { api: "\u63A5\u53E3", ui: "\u754C\u9762", docs: "\u6587\u6863", test: "\u6D4B\u8BD5", config: "\u914D\u7F6E", deps: "\u4F9D\u8D56", release: "\u53D1\u5E03" }, // 默认配置参数 cwd: process.cwd(), from: "", to: "HEAD", // 排除的作者(包括机器人账号) excludeAuthors: ["renovate[bot]", "dependabot[bot]", "github-actions[bot]"], // GitHub token 配置 - 将通过环境变量读取 tokens: {}, // 输出配置 - 生成 CHANGELOG.md 文件 output: "CHANGELOG.md", // 发布配置 publish: { args: [], private: false }, // Git 标签配置 signTags: false, // 模板配置 - 自定义提交和标签消息格式 templates: { commitMessage: "\u{1F4E2} publish: release package(s) {{newVersion}}", tagMessage: "{{newVersion}}", tagBody: "Released on {{date}}" } }; function getSemverByType(type) { switch (type) { case "feat": return "minor"; // 新功能 -> 次版本号 case "fix": return "patch"; // 修复 -> 补丁版本号 case "perf": return "patch"; // 性能优化 -> 补丁版本号 case "revert": return "patch"; // 回滚 -> 补丁版本号 case "docs": case "style": case "refactor": case "test": case "chore": case "build": case "ci": default: return "patch"; } } var changelogen_config_default = config; // src/plugins/changelog-with-changelogen.ts function createTypeEmojiMap() { const map = /* @__PURE__ */ new Map(); import_commitlint_config2.commitTypes.forEach(({ type, emoji, description }) => { map.set(type, { emoji, description }); }); return map; } function createEmojiTypeMap() { const map = /* @__PURE__ */ new Map(); import_commitlint_config2.commitTypes.forEach(({ type, emoji }) => { map.set(emoji, type); }); return map; } async function getCommitsFromGitHistory(from, to) { try { const config2 = await (0, import_changelogen.loadChangelogConfig)(process.cwd(), { ...changelogen_config_default, from: from || "", to: to || "HEAD" }); import_consola.consola.debug("Loaded changelogen config:", config2); const rawCommits = await (0, import_changelogen.getGitDiff)(config2.from, config2.to); import_consola.consola.debug(`Found ${rawCommits.length} raw commits from git history`); const parsedCommits = (0, import_changelogen.parseCommits)(rawCommits, config2); import_consola.consola.debug(`Parsed ${parsedCommits.length} semantic commits`); return parsedCommits; } catch (error) { import_consola.consola.error("Error getting commits from git history:", error); return []; } } function formatCommitToChangelogLine(commit, repoUrl) { let line = "- "; const typeEmojiMap = createTypeEmojiMap(); const typeInfo = typeEmojiMap.get(commit.type); if (typeInfo == null ? void 0 : typeInfo.emoji) { line += `${typeInfo.emoji} `; } if (commit.type && commit.type !== "other") { line += `**${commit.type}**`; if (commit.scope) { line += `(${commit.scope})`; } line += ": "; } if (commit.isBreaking) { line += "**BREAKING**: "; } line += commit.description; if (repoUrl) { const commitUrl = `${repoUrl}/commit/${commit.shortHash}`; line += ` ([${commit.shortHash}](${commitUrl}))`; } return line; } var getReleaseLine = async (changeset, type, changelogOpts) => { try { const repoUrl = `https://github.com/${(changelogOpts == null ? void 0 : changelogOpts.repo) || "ruan-cat/monorepo"}`; if (changeset.commit) { import_consola.consola.debug(`Processing changeset ${changeset.id} with commit ${changeset.commit}`); const commits = await getCommitsFromGitHistory(changeset.commit, changeset.commit); if (commits.length > 0) { const commit = commits[0]; const line2 = formatCommitToChangelogLine(commit, repoUrl); import_consola.consola.debug(`Generated changelog line from git commit for ${changeset.id}:`, line2); return line2; } import_consola.consola.warn(`Could not find git commit ${changeset.commit}, falling back to changeset parsing`); } import_consola.consola.debug(`Processing changeset ${changeset.id} without commit, using changeset content`); const firstLine = changeset.summary.split("\n")[0]; const typeEmojiMap = createTypeEmojiMap(); const emojiTypeMap = createEmojiTypeMap(); let line = "- "; let emoji = ""; let commitType = ""; let scope = ""; let description = firstLine; let isBreaking = false; isBreaking = firstLine.includes("!:") || firstLine.toLowerCase().includes("breaking"); const emojiConventionalMatch = firstLine.match( /^([\u{1f000}-\u{1f9ff}|\u{2600}-\u{27bf}|\u{2700}-\u{27BF}|\u{1F600}-\u{1F64F}|\u{1F300}-\u{1F5FF}|\u{1F680}-\u{1F6FF}|\u{1F1E0}-\u{1F1FF}|\u{2600}-\u{26FF}|\u{2700}-\u{27BF}])\s+(\w+)(\([^)]+\))?(!)?\s*:\s*(.+)$/u ); if (emojiConventionalMatch) { [, emoji, commitType, scope, , description] = emojiConventionalMatch; scope = scope ? scope.slice(1, -1) : ""; } else { const conventionalMatch = firstLine.match(/^(\w+)(\([^)]+\))?(!)?\s*:\s*(.+)$/); if (conventionalMatch) { [, commitType, scope, , description] = conventionalMatch; scope = scope ? scope.slice(1, -1) : ""; const typeInfo = typeEmojiMap.get(commitType); emoji = (typeInfo == null ? void 0 : typeInfo.emoji) || ""; } } if (emoji) { line += `${emoji} `; } if (commitType && commitType !== "other") { line += `**${commitType}**`; if (scope) { line += `(${scope})`; } line += ": "; } if (isBreaking) { line += "**BREAKING**: "; } line += description; if (changeset.commit) { const commitUrl = `${repoUrl}/commit/${changeset.commit}`; line += ` ([${changeset.commit.substring(0, 7)}](${commitUrl}))`; } import_consola.consola.debug(`Generated changelog line for ${changeset.id}:`, line); return line; } catch (error) { import_consola.consola.error(`Error processing changeset ${changeset.id}:`, error); return `- ${changeset.summary}`; } }; var getDependencyReleaseLine = async (changesets, dependenciesUpdated, changelogOpts) => { if (dependenciesUpdated.length === 0) return ""; const updatedDependencies = dependenciesUpdated.map((dependency) => { const type = dependency.type === "patch" ? "Patch" : dependency.type === "minor" ? "Minor" : "Major"; return ` - ${dependency.name}@${dependency.newVersion} (${type})`; }); return `- Updated dependencies: ${updatedDependencies.join("\n")}`; }; async function generateChangelogFromGitHistory(from, to, options) { try { import_consola.consola.info("Generating changelog from git commit history..."); const commits = await getCommitsFromGitHistory(from, to); if (commits.length === 0) { import_consola.consola.warn("No commits found in the specified range"); return ""; } const repoUrl = (options == null ? void 0 : options.repo) ? `https://github.com/${options.repo}` : void 0; let changelog = ""; if (options == null ? void 0 : options.groupByType) { const commitsByType = /* @__PURE__ */ new Map(); commits.forEach((commit) => { const type = commit.type || "other"; if (!commitsByType.has(type)) { commitsByType.set(type, []); } commitsByType.get(type).push(commit); }); const typeOrder = [ "feat", "fix", "perf", "revert", "docs", "style", "refactor", "test", "build", "ci", "chore", "other" ]; const sortedTypes = Array.from(commitsByType.keys()).sort((a, b) => { const indexA = typeOrder.indexOf(a); const indexB = typeOrder.indexOf(b); return (indexA === -1 ? 999 : indexA) - (indexB === -1 ? 999 : indexB); }); for (const type of sortedTypes) { const typeCommits = commitsByType.get(type); if (typeCommits.length === 0) continue; const typeEmojiMap = createTypeEmojiMap(); const typeInfo = typeEmojiMap.get(type); const typeTitle = typeInfo ? `${typeInfo.emoji} ${typeInfo.description}` : type.toUpperCase(); changelog += ` ### ${typeTitle} `; typeCommits.forEach((commit) => { changelog += formatCommitToChangelogLine(commit, repoUrl) + "\n"; }); } } else { commits.forEach((commit) => { changelog += formatCommitToChangelogLine(commit, repoUrl) + "\n"; }); } if (options == null ? void 0 : options.includeAuthors) { const authors = /* @__PURE__ */ new Set(); commits.forEach((commit) => { commit.authors.forEach((author) => { authors.add(author.name); }); }); if (authors.size > 0) { changelog += ` ### Contributors `; Array.from(authors).sort().forEach((author) => { changelog += `- ${author} `; }); } } import_consola.consola.success(`Generated changelog with ${commits.length} commits`); return changelog; } catch (error) { import_consola.consola.error("Error generating changelog from git history:", error); return ""; } } async function generateHybridChangelog(changesets, options) { try { let changelog = ""; if (changesets && changesets.length > 0) { import_consola.consola.info(`Processing ${changesets.length} changesets...`); for (const changeset of changesets) { const line = await getReleaseLine(changeset, "patch", { repo: options == null ? void 0 : options.repo }); changelog += line + "\n"; } } if ((options == null ? void 0 : options.fallbackToGit) && (!changesets || changesets.length === 0)) { import_consola.consola.info("No changesets found, falling back to git commit history..."); const gitChangelog = await generateChangelogFromGitHistory(options.from, options.to, { repo: options.repo, groupByType: true, includeAuthors: true }); changelog += gitChangelog; } return changelog; } catch (error) { import_consola.consola.error("Error generating hybrid changelog:", error); return ""; } } var changelogFunctions = { getReleaseLine, getDependencyReleaseLine }; var changelog_with_changelogen_default = changelogFunctions; // src/scripts/sync-github-release.ts var import_rest = require("@octokit/rest"); var import_fs = require("fs"); var import_path = require("path"); var import_consola2 = require("consola"); var import_meta = {}; var GitHubReleaseSync = class { constructor(options) { this.options = options; this.octokit = new import_rest.Octokit({ auth: options.token, log: import_consola2.consola }); const [owner, repo] = options.repository.split("/"); if (!owner || !repo) { throw new Error(`Invalid repository format: ${options.repository}. Expected "owner/repo"`); } this.repo = { owner, repo }; import_consola2.consola.info(`Initialized GitHub Release Sync for ${owner}/${repo}`); } octokit; repo; /** * 解析 CHANGELOG.md 获取最新版本的发布说明 */ parseLatestChangelog(changelogPath) { try { if (!(0, import_fs.existsSync)(changelogPath)) { import_consola2.consola.warn(`Changelog not found: ${changelogPath}`); return null; } const content = (0, import_fs.readFileSync)(changelogPath, "utf-8"); const lines = content.split("\n"); let version = ""; let releaseNotes = ""; let isInLatestVersion = false; let hasFoundFirstVersion = false; for (const line of lines) { const versionMatch = line.match(/^##\s+(\[)?([^\]]+)(\])?/); if (versionMatch) { if (!hasFoundFirstVersion) { version = versionMatch[2]; isInLatestVersion = true; hasFoundFirstVersion = true; continue; } else { break; } } if (isInLatestVersion && line.trim()) { releaseNotes += line + "\n"; } } if (!version) { import_consola2.consola.warn(`No version found in changelog: ${changelogPath}`); return null; } return { version: version.trim(), releaseNotes: releaseNotes.trim() }; } catch (error) { import_consola2.consola.error(`Error parsing changelog ${changelogPath}:`, error); return null; } } /** * 创建或更新 GitHub Release */ async createOrUpdateRelease(tagName, name, body, targetCommitish = "main") { try { try { const { data: existingRelease } = await this.octokit.rest.repos.getReleaseByTag({ ...this.repo, tag: tagName }); import_consola2.consola.info(`Updating existing release: ${tagName}`); await this.octokit.rest.repos.updateRelease({ ...this.repo, release_id: existingRelease.id, name, body, draft: false, prerelease: false }); import_consola2.consola.success(`Updated GitHub release: ${tagName}`); } catch (error) { if (error.status === 404) { import_consola2.consola.info(`Creating new release: ${tagName}`); await this.octokit.rest.repos.createRelease({ ...this.repo, tag_name: tagName, target_commitish: targetCommitish, name, body, draft: false, prerelease: false }); import_consola2.consola.success(`Created GitHub release: ${tagName}`); } else { throw error; } } } catch (error) { import_consola2.consola.error(`Failed to create/update release ${tagName}:`, error); throw error; } } /** * 从已发布的包列表同步到 GitHub Release */ async syncFromChangesets(publishedPackages) { if (!(publishedPackages == null ? void 0 : publishedPackages.length)) { import_consola2.consola.info("No packages were published, skipping GitHub release sync"); return; } import_consola2.consola.info(`Syncing ${publishedPackages.length} published packages to GitHub Releases`); for (const pkg of publishedPackages) { try { import_consola2.consola.info(`Processing package: ${pkg.name}@${pkg.version}`); const packageDir = pkg.name.startsWith("@") ? pkg.name.split("/")[1] : pkg.name; const changelogPath = (0, import_path.resolve)(process.cwd(), "packages", packageDir, "CHANGELOG.md"); const changelog = this.parseLatestChangelog(changelogPath); if (!changelog) { import_consola2.consola.warn(`Skipping ${pkg.name} - no changelog found or parsed`); continue; } if (changelog.version !== pkg.version) { import_consola2.consola.warn(`Version mismatch for ${pkg.name}: changelog=${changelog.version}, published=${pkg.version}`); } const tagName = `${pkg.name}@${pkg.version}`; const releaseName = `${pkg.name} v${pkg.version}`; const releaseBody = `# ${pkg.name} v${pkg.version} ${changelog.releaseNotes}`; await this.createOrUpdateRelease(tagName, releaseName, releaseBody); } catch (error) { import_consola2.consola.error(`Failed to sync package ${pkg.name}:`, error); continue; } } import_consola2.consola.success("GitHub release sync completed"); } }; async function runSync() { try { const token = process.env.GITHUB_TOKEN; const publishedPackagesJson = process.env.PUBLISHED_PACKAGES; const repository = process.env.GITHUB_REPOSITORY || "ruan-cat/monorepo"; if (!token) { throw new Error("GITHUB_TOKEN environment variable is required"); } if (!publishedPackagesJson) { import_consola2.consola.info("PUBLISHED_PACKAGES is empty, no packages to sync"); return; } let publishedPackages; try { publishedPackages = JSON.parse(publishedPackagesJson); } catch (error) { throw new Error(`Failed to parse PUBLISHED_PACKAGES JSON: ${error}`); } const sync = new GitHubReleaseSync({ token, repository }); await sync.syncFromChangesets(publishedPackages); } catch (error) { import_consola2.consola.error("GitHub Release sync failed:", error); process.exit(1); } } if (typeof import_meta.url !== "undefined" && import_meta.url === `file://${process.argv[1]}`) { runSync(); } // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { GitHubReleaseSync, changelogConfig, changelogFunctions, generateChangelogFromGitHistory, generateHybridChangelog, runSync }); //# sourceMappingURL=index.cjs.map