Files
ulthon_admin/extend/base/common/command/admin/VersionBase.php
2026-11-03 12:19:22 +00:00

358 lines
13 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
declare(strict_types=1);
namespace base\common\command\admin;
use app\common\console\Command;
use app\common\tools\PathTools;
use think\App as ThinkApp;
use think\console\Input;
use think\console\input\Option;
use think\console\Output;
/**
* 版本管理与发布命令.
*
* 发布流程:
* 1. 修改 VERSION 常量为新版本号
* 2. php think admin:version --generate-comment
* → 从 git log 自动生成更新说明,输出到临时目录
* → 复制 commit_comment.php 中的数组内容到 COMMENT 常量
* 2.1 提交本次修改的 VersionBase.php 到本地仓库
* 3. php think admin:version --push-tag
* → 创建 annotated tag 并推送到远程
* 4. [可选] php think admin:version --generate-release [--release-from-tag= vX.X.X]
* → 生成发布说明 Markdown,手动发布到目标平台
*/
class VersionBase extends Command
{
public const VERSION = 'v2.9.0';
public const PRODUCT_VERSION = '';
public const LAYUI_VERSION = 'v2.10.1';
public const COMMENT = [
'版本更新说明:',
'【新功能】',
'- 站内信与通知系统:NoticeService 发送核心/场景注册/订阅过滤与站内信驱动及测试',
'- 通知三表模型 Base/App 双层与 SystemAdmin email 字段 Scheme 及迁移',
'- 管理员表单电子邮箱字段',
'- PHPMailer 邮箱驱动与短信公众号渠道骨架',
'- NoticeSended 事件与登录提醒内置监听',
'- 通知管理 CURD 模块双层化',
'- 我的消息与订阅设置个人域页面',
'- timer 异步投递消费者与超时回收任务',
'- 投递明细/失败重发/只读场景总览',
'- 顶栏铃铛/个人入口/未读数轮询',
'- 系统配置通知渠道 Tab',
'- 通知菜单与配置默认值快照及对齐迁移',
'- 头部模块菜单宽度自适应折叠与更多下拉',
'- 全部模块顶部面板与星标收藏、收藏存储与置顶排序',
'【修复】',
'- 铃铛轮询时机/个人消息接口地址/订阅保存地址与详情接收时间',
'- 通知发送事务包裹与软删过滤加固',
'- 个人域软删过滤改用 withJoin 关系别名',
'- 工具栏按钮 tab 分支误引 operat 变量致表格渲染中断',
'- 头部更多按钮皮肤高度适配与空下拉气泡',
'- 内容tab切换对头部菜单class的全量重置并联动更多激活态',
'- 弹层iframe沿parent链继承皮肤类,修复弹层内单选复选回落原生绿(pro#7)',
'- 清爽皮肤primary复选框勾选文字不再有蓝底,改为无底蓝字(pro#7)',
'- MallGoods 价格字段去除冗余 length 声明消除 sync 振荡',
'【文档】',
'- 站内信与通知机制技能文档及变更日志',
'【其他】',
'- docker-dev 镜像内置 git,admin:version 发布命令容器内开箱即用',
'- 头部更多按钮空态隐藏下拉箭头与面板左上角定位打磨',
'- 通知模块规范修复与安装演练收尾',
];
protected const COMMIT_TYPES = [
'feat' => '新功能',
'fix' => '修复',
'docs' => '文档',
'style' => '样式',
'refactor' => '重构',
'perf' => '性能优化',
'test' => '测试',
'build' => '构建',
'ci' => '持续集成',
'chore' => '其他',
'revert' => '回退',
];
protected function configure()
{
parent::configure();
// 指令配置
$this->setName('admin:version')
->addOption('generate-comment', null, Option::VALUE_NONE, '使用git命令生成说明文件')
->addOption('generate-release', null, Option::VALUE_NONE, '使用git命令生成发布说明文件')
->addOption('release-from-tag', null, Option::VALUE_OPTIONAL, '从指定版本开始生成说明文件')
->addOption('push-tag', null, Option::VALUE_NONE, '使用git命令生成tag并推送')
->setDescription('查看当前版本号。可选 --generate-comment 生成更新说明、--push-tag 打tag推送、--generate-release 生成发布说明');
}
protected function execute(Input $input, Output $output)
{
// 文本模式输出
// 指令输出
if (!empty(static::PRODUCT_VERSION)) {
$output->info('当前版本号为:' . static::PRODUCT_VERSION);
}
$output->info('当前ulthon_admin版本号为:' . static::VERSION);
$output->info('当前Layui版本号为:' . static::LAYUI_VERSION);
$output->info('当前ThinkPHP版本号为:' . app()->version());
$output->writeln('当前的修改说明:');
$output->writeln('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
foreach (static::COMMENT as $comment) {
$output->info($comment);
}
$output->writeln('<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<');
$output->highlight('代码托管地址:https://gitee.com/ulthon/ulthon_admin');
$output->highlight('开发文档地址:http://doc.ulthon.com/home/read/ulthon_admin/home.html');
$is_push_tag = $input->hasOption('push-tag');
if ($is_push_tag) {
$this->pushTag();
}
$is_generate_comment = $input->hasOption('generate-comment');
if ($is_generate_comment) {
$this->generateComment();
}
$is_generate_release = $input->hasOption('generate-release');
if ($is_generate_release) {
$fromTag = $input->getOption('release-from-tag');
$this->generateRelease($fromTag);
}
}
protected function pushTag()
{
$input = $this->input;
$output = $this->output;
$output->writeln('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
$version = static::VERSION;
if (!empty(static::PRODUCT_VERSION)) {
$version = static::PRODUCT_VERSION;
}
// 将提交信息写入临时文件
$comment = implode("\n", static::COMMENT);
$output->info('生成标签:' . $version);
$output->info('标签描述:' . $comment);
// 使用项目的临时文件路径生成方法
$tempFile = PathTools::tempBuildPath('git_tag_message_' . uniqid() . '.txt');
file_put_contents($tempFile, $comment);
// 使用 -F 参数从文件读取提交信息
exec("git tag -a $version -F \"$tempFile\"");
// 删除临时文件
if (file_exists($tempFile)) {
unlink($tempFile);
}
$output->info('推送到远程仓库');
exec('git push --tags');
}
/**
* 生成版本更新说明
* 读取自上次tag到现在所有的提交说明.
* @return string
*/
protected function generateComment()
{
$this->output->writeln('>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>');
// 获取最新的tag
$lastTag = shell_exec('git describe --tags --abbrev=0');
$lastTag = trim($lastTag);
// 获取从最新tag到现在的所有提交
$commits = shell_exec("git log {$lastTag}..HEAD --pretty=format:\"%s\"");
if (empty($commits)) {
return '暂无更新说明';
}
$commits = explode("\n", $commits);
$groupedCommits = $this->groupCommitsByType($commits, function ($message) {
return trim($message);
});
// 生成更新说明
$comment = "版本更新说明:\n";
foreach (static::COMMIT_TYPES as $type => $desc) {
if (!empty($groupedCommits[$desc])) {
$comment .= "\n【{$desc}】\n";
foreach ($groupedCommits[$desc] as $message) {
$comment .= "- {$message}\n";
}
}
}
$this->output->writeln($comment);
// 生成数组格式文件
$comment_arr = explode("\n", $comment);
$comment_arr = array_filter($comment_arr, function ($item) {
return !empty($item);
});
$comment_arr = array_map(function ($item) {
return "'" . addslashes(trim($item)) . "',";
}, $comment_arr);
$comment_arr_code = implode("\n", $comment_arr);
$tmp_comment_file = PathTools::tempBuildPath('commit_comment.php');
file_put_contents($tmp_comment_file, "<?php\nreturn [\n" . $comment_arr_code . "\n];");
$this->output->info('已生成版本更新代码:' . $tmp_comment_file);
$tmp_comment_markdown_file = PathTools::tempBuildPath('commit_comment.md');
file_put_contents($tmp_comment_markdown_file, $comment);
$this->output->info('已生成版本更新Markdown:' . $tmp_comment_markdown_file);
}
/**
* 生成发布说明文件
* 从指定版本开始生成发布说明.
* @param string|null $fromTag 起始版本号,如果为空则使用最近的tag
* @return void
*/
public function generateRelease($fromTag = null)
{
$output = $this->output;
// 获取所有tags,按时间倒序排列
$tags = explode("\n", trim(shell_exec('git tag --sort=-creatordate')));
if (empty($tags)) {
$output->error('没有找到任何tag');
return;
}
$toTag = $tags[0]; // 最新的tag
if (empty($fromTag)) {
if (count($tags) < 2) {
$output->error('没有足够的tag来生成发布说明');
return;
}
$fromTag = $tags[1]; // 倒数第二个tag
$output->info('未指定起始版本,使用倒数第二个tag: ' . $fromTag);
} else {
if (!in_array($fromTag, $tags)) {
$output->error('指定的起始版本 ' . $fromTag . ' 不存在');
return;
}
if (version_compare($fromTag, $toTag, '>=')) {
$output->error('指定的起始版本 ' . $fromTag . ' 不早于最新版本 ' . $toTag);
return;
}
}
$output->info('从版本 ' . $fromTag . ' 生成发布说明');
$output->info('到最新tag: ' . $toTag);
// 获取当前版本
$currentVersion = static::PRODUCT_VERSION ?: static::VERSION;
// 获取从指定tag到目标tag的所有提交,包含提交哈希和提交日期
$commits = shell_exec("git log {$fromTag}..{$toTag} --pretty=format:\"%h|%ad|%s\" --date=short");
if (empty($commits)) {
$output->warning('从 ' . $fromTag . ' 到 ' . $toTag . ' 没有任何提交');
return;
}
$commits = explode("\n", $commits);
$groupedCommits = $this->groupCommitsByType($commits, function ($message, $fullCommit) {
$parts = explode('|', $fullCommit);
if (count($parts) >= 3) {
return [
'message' => trim($message),
'hash' => $parts[0],
'date' => $parts[1],
];
}
return null;
});
// 生成发布说明
$releaseTitle = "## {$currentVersion} 发布说明\n\n";
$releaseDate = '发布日期: ' . date('Y-m-d') . "\n\n";
$releaseContent = "本次发布包含了从 {$fromTag} 到 {$currentVersion} 的所有更新。\n\n";
foreach (static::COMMIT_TYPES as $type => $desc) {
if (!empty($groupedCommits[$desc])) {
$releaseContent .= "### {$desc}\n\n";
foreach ($groupedCommits[$desc] as $item) {
$releaseContent .= "- [{$item['date']}] {$item['message']} (#{$item['hash']})\n";
}
$releaseContent .= "\n";
}
}
$release = $releaseTitle . $releaseDate . $releaseContent;
// 生成发布说明文件
$releaseFile = PathTools::tempBuildPath("release_{$currentVersion}.md");
file_put_contents($releaseFile, $release);
$output->info('已生成发布说明文件: ' . $releaseFile);
}
protected function groupCommitsByType($commits, $formatCallback)
{
$groupedCommits = [];
foreach (static::COMMIT_TYPES as $type => $desc) {
$groupedCommits[$desc] = [];
}
$groupedCommits['其他'] = []; // 添加"其他"类别
foreach ($commits as $commit) {
$parts = explode('|', $commit);
$message = $parts[2] ?? $commit;
$matched = false;
foreach (static::COMMIT_TYPES as $type => $desc) {
// 支持 type: message 和 type(scope): message 两种格式
if (preg_match("/^{$type}(\([^)]*\))?\s*:/i", $message)) {
$cleanMessage = preg_replace("/^{$type}(\([^)]*\))?\s*:\s*/i", '', $message);
$groupedCommits[$desc][] = $formatCallback($cleanMessage, $commit);
$matched = true;
break;
}
}
// 如果没有匹配到任何类型,归类为"其他"
if (!$matched) {
$groupedCommits['其他'][] = $formatCallback($message, $commit);
}
}
// 清空空分组
foreach (array_keys($groupedCommits) as $key) {
if (empty($groupedCommits[$key])) {
unset($groupedCommits[$key]);
}
}
return $groupedCommits;
}
}