mirror of
https://gitee.com/ulthon/ulthon_admin.git
synced 2026-10-29 17:08:21 +08:00
392 lines
16 KiB
PHP
392 lines
16 KiB
PHP
<?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.7.0';
|
||
|
||
public const PRODUCT_VERSION = '';
|
||
|
||
public const LAYUI_VERSION = 'v2.10.1';
|
||
|
||
public const COMMENT = [
|
||
'版本更新说明:',
|
||
'【新功能】',
|
||
'- 配色专属标准皮肤,清爽全包观感与面板交互重构',
|
||
'- 商品列表默认形态改为商品模板',
|
||
'- 新增清爽皮肤(36px 紧凑头部)并设为默认',
|
||
'- 名片模板视觉升级,状态开关移至头部',
|
||
'- 商品卡模板视觉升级,状态开关移至图面右上',
|
||
'- 商品列表新增商品自定义卡片模板三形态示例',
|
||
'- 用户管理示例升级三形态并示范按端默认配置',
|
||
'- 默认形态支持按端配置,layoutDefault 演进对象式并兼容旧写法',
|
||
'- 列表双形态注入双端生效,PC 端默认可切卡片',
|
||
'- 受控数据流转为默认,controlled:false 作为兼容逃生口',
|
||
'- 用户管理名片卡片官方示例',
|
||
'- Vue 模板钩子支持',
|
||
'- laytpl 模板钩子支持',
|
||
'- 手机端默认卡片与销毁重建式视图切换',
|
||
'- 内置卡片形态与本地化样式',
|
||
'- 实现五级模板协议与行级渲染上下文',
|
||
'- 灰度第五批 app 层五页切换受控模式',
|
||
'- 灰度第四批 mall 三页切换受控模式',
|
||
'- 灰度第三批三页切换并收编业务直呼',
|
||
'- 灰度第二批三页切换受控模式',
|
||
'- 灰度第一批四页切换受控模式',
|
||
'- 支持 controlled 受控模式(灰度开关)',
|
||
'- 装载 reloadData shim 与 checkStatus 分发',
|
||
'- 新增受控数据层 store(dataProvider/pager/selection/reload)',
|
||
'- 移除寄生多模板实现,保留搜索与核心封装',
|
||
'【修复】',
|
||
'- 清爽皮肤内容容器padding归零',
|
||
'- 清爽皮肤弹框头部清新化与logo图片高度调整',
|
||
'- 清爽侧栏收窄至160并缩进三收紧(8/16/24)',
|
||
'- 清爽侧栏收窄至170并缩进二收紧(10/20/30)',
|
||
'- 头部与收起态细节修正(控制按钮收窄居中、收起图标居中、logo 高度贴线)',
|
||
'- 清爽侧栏图标与菜单标题间距收紧',
|
||
'- 清爽侧栏细节修正(导航树宽度对齐180、内边距收紧、缩进层级10/25/40)',
|
||
'- 清爽侧栏宽度再收窄至 180px',
|
||
'- 清爽侧栏减宽 190px 与三级菜单名完整显示',
|
||
'- 清爽皮肤侧栏菜单行高紧凑化(40px→32px)',
|
||
'- 修复刷新后 iframe 内容页皮肤类不生效(onInitElemStyle 早于 body 解析执行)',
|
||
'- 清爽皮肤按钮配色适配',
|
||
'- 配色面板矮视口显示滚动条,修复元素风格无法选择',
|
||
'- 清爽皮肤文字色交还配色系统,修复深色配色对比度',
|
||
'- 自定义操作按钮配置对象渲染中被就地污染,切换形态后退化为文本',
|
||
'- 手机端未配置页面自动启用内置卡片形态',
|
||
'- 失败通道摘除模板容器加载态并明示 where 白名单契约',
|
||
'【文档】',
|
||
'- 修正队列设计技能审核问题(phpredis Stream API 用法、redis store 环境前提、示例缺陷)',
|
||
'- 拆分表格与卡片双技能并同步最新代码',
|
||
'- 修正定时器技能数据消费竞态示例,补充分片索引失效警示',
|
||
'- 新增队列设计技能(DB 原子抢占/区间游标/Redis Stream 生产者消费者三模式)',
|
||
'- 新增自定义卡片模板定制指南技能',
|
||
'- 清空项目信息节留给下游,runtime 权限提示迁至 stack 文档',
|
||
'- 补充清爽皮肤说明与预览图',
|
||
'- 多形态章节改写为默认数据流与逃生口语义',
|
||
'- 修正对已退役 AdminInitServiceBase 与 AGENTS.md 旧章节名的失效引用',
|
||
'- 更新表格技能文档覆盖受控模式与模板协议',
|
||
'【样式】',
|
||
'- 清爽皮肤侧栏菜单项行高收紧为 32px',
|
||
'【重构】',
|
||
'- 清理十八页冗余的受控显式声明',
|
||
'- 跨窗口刷新显式走 ua.table.reloadData 分发入口',
|
||
'【测试】',
|
||
'- 模板形态行为集成验证与行选中委托统一',
|
||
'【其他】',
|
||
'- 删除未定义引用的死函数 refreshTable/reloadTable',
|
||
'- 删除第一代 uaTable 原型死代码',
|
||
];
|
||
|
||
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;
|
||
}
|
||
}
|