#feature: improve Jira planning and release branch creation

This commit is contained in:
2026-05-29 14:25:43 +08:00
parent ade18a0aa8
commit 4875031cc3
4 changed files with 213 additions and 31 deletions
+21 -20
View File
@@ -620,6 +620,8 @@ class GitMonitorService
private function ensureReleaseBranchExists(string $repoKey, array $repoConfig, string $branch, ?string $description): void
{
$path = $this->resolveProjectPath($repoKey, $repoConfig);
$worktreePath = null;
$worktreeCreated = false;
if (!is_dir($path) || !is_dir($path . DIRECTORY_SEPARATOR . '.git')) {
Log::warning('Invalid git repository path for branch creation', ['repository' => $repoKey, 'path' => $path]);
@@ -636,34 +638,27 @@ class GitMonitorService
$version = str_replace('release/', '', $branch);
try {
// 从 origin/master 创建新分支
// 在临时 worktree 中创建并推送分支,避免切换或修改用户正在工作的仓库。
$this->runGit($path, ['git', 'fetch', 'origin', 'master']);
// 创建本地分支(基于 origin/master
try {
// 先尝试删除可能存在的本地分支
$this->runGit($path, ['git', 'branch', '-D', $branch]);
} catch (ProcessFailedException) {
// 忽略,分支可能不存在
}
$this->runGit($path, ['git', 'checkout', '-b', $branch, 'origin/master']);
$worktreePath = sys_get_temp_dir() . DIRECTORY_SEPARATOR . 'toolbox-release-' . str_replace(['/', '\\'], '-', $repoKey . '-' . $version) . '-' . bin2hex(random_bytes(4));
$this->runGit($path, ['git', 'worktree', 'add', '--detach', $worktreePath, 'origin/master']);
$worktreeCreated = true;
// 修改 version.txt 文件
$versionFile = $path . DIRECTORY_SEPARATOR . 'version.txt';
$versionFile = $worktreePath . DIRECTORY_SEPARATOR . 'version.txt';
if (!file_put_contents($versionFile, $version)) {
throw new \RuntimeException("Failed to write version.txt");
}
// 添加并提交更改
$this->runGit($path, ['git', 'add', 'version.txt']);
$this->runGit($worktreePath, ['git', 'add', 'version.txt']);
// 构建提交信息:分支名 + 空格 + Jira 描述
$commitMessage = $branch . ($description ? ' ' . $description : '');
$this->runGit($path, ['git', 'commit', '-m', $commitMessage]);
$this->runGit($worktreePath, ['git', 'commit', '-m', $commitMessage]);
// 推送到远程
$this->runGit($path, ['git', 'push', '-u', 'origin', $branch]);
$this->runGit($worktreePath, ['git', 'push', 'origin', 'HEAD:refs/heads/' . $branch]);
Log::info('Created and pushed release branch', [
'repository' => $repoKey,
@@ -687,11 +682,17 @@ class GitMonitorService
'error' => $e->getMessage(),
]);
} finally {
// 切回 develop 分支,避免影响后续操作
try {
$this->runGit($path, ['git', 'checkout', self::DEVELOP_BRANCH]);
} catch (ProcessFailedException) {
// 忽略
if ($worktreeCreated && $worktreePath !== null) {
try {
$this->runGit($path, ['git', 'worktree', 'remove', '--force', $worktreePath]);
} catch (ProcessFailedException $e) {
Log::warning('Failed to remove temporary release worktree', [
'repository' => $repoKey,
'branch' => $branch,
'path' => $worktreePath,
'error' => $e->getMessage(),
]);
}
}
}
}
+51 -11
View File
@@ -138,19 +138,18 @@ class JiraService
throw new \InvalidArgumentException('用户名不能为空');
}
// 定义已完成的状态列表(需要排除的状态)
$completedStatuses = [
'Done',
'已上线',
'已完成',
// 未来任务只展示已进入评审、排期或开发阶段,且已归入 Sprint 的需求。
$nextWeekStatuses = [
'需求已评审',
'需求已排期',
'开发中',
];
$statusExclusion = implode('", "', $completedStatuses);
// 查询分配给用户且未完成的需求(Story/需求类型,不包括子任务)
$jql = sprintf(
'assignee = "%s" AND status NOT IN ("%s") AND issuetype in ("Story", "需求") ORDER BY created ASC',
// 查询未来一周需要继续处理的需求(Story/需求类型,不包括子任务)。
// 需求既可能分给当前用户,也可能通过研发 owner 归属当前用户。
$jql = $this->buildNextWeekTasksJql(
$username,
$statusExclusion
$nextWeekStatuses,
$this->resolveDeveloperOwnerJqlField()
);
try {
@@ -172,7 +171,45 @@ class JiraService
return collect();
}
private function buildNextWeekTasksJql(string $username, array $allowedStatuses, ?string $developerOwnerField): string
{
$statusInclusion = implode('", "', $allowedStatuses);
$ownerClause = sprintf('assignee = "%s"', $username);
if ($developerOwnerField) {
$ownerClause = sprintf('(%s OR %s = "%s")', $ownerClause, $developerOwnerField, $username);
}
return sprintf(
'%s AND status IN ("%s") AND issuetype in ("Story", "需求") AND Sprint is not EMPTY ORDER BY created ASC',
$ownerClause,
$statusInclusion
);
}
private function resolveDeveloperOwnerJqlField(): ?string
{
try {
foreach ($this->fieldService->getAllFields() as $field) {
$name = $field->name ?? '';
$id = $field->id ?? '';
if ($name === '' || ! $this->isDeveloperOwnerFieldName($name)) {
continue;
}
if (preg_match('/^customfield_(\d+)$/', $id, $matches)) {
return 'cf['.$matches[1].']';
}
return '"'.str_replace('"', '\"', $name).'"';
}
} catch (\Exception) {
// 字段发现失败时退回 assignee 查询,避免周报生成整体失败。
}
return null;
}
/**
* 获取 Jira Sprint 下拉选项(从最近 Story Sprint 字段中提取)
@@ -245,6 +282,7 @@ class JiraService
$startAt += $maxResults;
$total = $result->total ?? $issues->count();
} while ($issues->count() < $total && ! empty($pageIssues));
return $issues;
} catch (JiraException $e) {
throw new \RuntimeException('获取 Sprint 需求失败: '.$e->getMessage());
@@ -597,6 +635,7 @@ class JiraService
if (is_array($item)) {
return $item['value'] ?? $item['name'] ?? $item['displayName'] ?? '';
}
return '';
})->filter()->implode(', ');
}
@@ -618,6 +657,7 @@ class JiraService
return null;
}
/**
* 生成 Markdown 格式的周报
*/