@ruan-cat/release-toolkit
Version:
基于 changelogen 增强 changesets 工作流的发布工具包,提供语义化提交解析和 GitHub Release 同步功能。
557 lines (549 loc) • 19.8 kB
JavaScript
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