@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