using Dpz.Core.EnumLibrary;
namespace Dpz.Core.Web.Jobs.Hangfire.Build;
/// <summary>
/// 探测各发布目标当前运行版本,并与即将发布的版本比较
/// </summary>
public class PipelineVersionProbe(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<PipelineVersionProbe> logger
)
{
public const string HttpClientName = "pipeline-version";
public const string WaitCacheKeyPrefix = "pipeline:version-wait:";
public const string DecisionCacheKeyPrefix = "pipeline:version-decision:";
public const string DecisionConfirm = "confirm";
public const string DecisionCancel = "cancel";
public static readonly TimeSpan ConfirmTimeout = TimeSpan.FromMinutes(2);
public static string WaitCacheKey(string runId) => $"{WaitCacheKeyPrefix}{runId}";
public static string DecisionCacheKey(string runId) => $"{DecisionCacheKeyPrefix}{runId}";
/// <summary>
/// 找出即将发布版本与线上运行版本相同的项目。
/// 未配置 host、探测失败的项目会被跳过,不阻断发布。
/// </summary>
public async Task<List<PipelineVersionMatch>> FindSameVersionsAsync(
IReadOnlyDictionary<BuildProject, string> publishVersions,
CancellationToken cancellationToken = default
)
{
var hosts = ReadVersionHosts();
var matches = new List<PipelineVersionMatch>();
foreach (var (project, publishVersion) in publishVersions)
{
if (project == BuildProject.None)
{
continue;
}
if (!hosts.TryGetValue(project.ToString(), out var host))
{
logger.LogWarning("未配置 {Project} 的版本探测地址,已跳过同版本检查", project);
continue;
}
var liveVersion = await ProbeLiveVersionAsync(project, host, cancellationToken);
if (string.IsNullOrWhiteSpace(liveVersion))
{
continue;
}
if (!IsSameVersion(liveVersion, publishVersion))
{
continue;
}
matches.Add(
new PipelineVersionMatch
{
Project = project,
Host = host,
LiveVersion = liveVersion,
PublishVersion = publishVersion,
}
);
}
return matches;
}
private Dictionary<string, string> ReadVersionHosts()
{
var section = configuration.GetSection("Pipeline:VersionHosts");
var hosts = section.Get<Dictionary<string, string>>() ?? [];
var lookup = new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase);
foreach (var (key, value) in hosts)
{
if (string.IsNullOrWhiteSpace(key) || string.IsNullOrWhiteSpace(value))
{
continue;
}
lookup[key.Trim()] = value.Trim();
}
return lookup;
}
private async Task<string?> ProbeLiveVersionAsync(
BuildProject project,
string host,
CancellationToken cancellationToken
)
{
if (
!Uri.TryCreate(host, UriKind.Absolute, out var baseUri)
|| (baseUri.Scheme != Uri.UriSchemeHttp && baseUri.Scheme != Uri.UriSchemeHttps)
)
{
logger.LogWarning(
"{Project} 的版本探测地址无效,已跳过同版本检查:{Host}",
project,
host
);
return null;
}
var url = new Uri(baseUri, "/api/version");
try
{
var client = httpClientFactory.CreateClient(HttpClientName);
var raw = (await client.GetStringAsync(url, cancellationToken)).Trim().Trim('"');
if (string.IsNullOrWhiteSpace(raw))
{
logger.LogWarning("{Project} 线上版本为空,已跳过同版本检查:{Url}", project, url);
return null;
}
return raw;
}
catch (Exception e)
{
logger.LogWarning(e, "探测 {Project} 线上版本失败:{Url}", project, url);
return null;
}
}
private static bool IsSameVersion(string liveVersion, string publishVersion)
{
if (
Version.TryParse(liveVersion, out var live)
&& Version.TryParse(publishVersion, out var publish)
)
{
return live == publish;
}
return string.Equals(liveVersion, publishVersion, StringComparison.OrdinalIgnoreCase);
}
}
/// <summary>
/// 用户未确认或超时未确认同版本发布
/// </summary>
public sealed class PipelineVersionNotConfirmedException(string message) : Exception(message);
⚠⚠ 以下内容为AI分析的结果,请根据实际情况进行判断。
这段代码是 .NET 中的一个服务类,用于在发布流水线中检测各个发布目标当前正在运行的版本,并与即将发布的版本进行比较,从而判断是否存在“重复发布”的情况。下面详细解释各组成部分。
1. 类的基本结构
public class PipelineVersionProbe(
IHttpClientFactory httpClientFactory,
IConfiguration configuration,
ILogger<PipelineVersionProbe> logger
)
- 这是 C# 12 的主构造函数语法,依赖注入时会自动传入:
IHttpClientFactory:用于创建 HTTP 客户端,探测线上版本接口。IConfiguration:读取配置(如版本探测地址)。ILogger<PipelineVersionProbe>:日志记录器。
类本身被设计为通过依赖注入使用,无额外状态。
2. 常量与静态成员
这些常量用于缓存键和消息标识:
HttpClientName = "pipeline-version":HTTP 客户端名称,可供IHttpClientFactory创建特定配置的客户端。WaitCacheKeyPrefix、DecisionCacheKeyPrefix:用于在缓存中记录等待确认和用户决策的键前缀。DecisionConfirm = "confirm"、DecisionCancel = "cancel":表示用户确认或取消同版本发布的决策值。ConfirmTimeout = TimeSpan.FromMinutes(2):等待用户确认的超时时间。- 两个静态方法
WaitCacheKey和DecisionCacheKey用来构造带有特定runId的缓存键。
这些缓存键通常与 Hangfire 或外部状态存储配合使用,用于协调发布流程中的人工确认步骤。
3. 主要方法:FindSameVersionsAsync
这是类的核心逻辑,作用是找出“即将发布的版本”与“线上运行版本”相同的项目。
public async Task<List<PipelineVersionMatch>> FindSameVersionsAsync(
IReadOnlyDictionary<BuildProject, string> publishVersions,
CancellationToken cancellationToken = default
)
publishVersions:要发布的项目及其版本号(BuildProject是枚举,string为版本号)。- 返回值:
List<PipelineVersionMatch>,用于保存已匹配上的项目信息(项目名、Host、线上版本、发布版本)。
执行流程
- 读取配置中的版本探测地址(
ReadVersionHosts)。 - 遍历待发布项目列表。
- 跳过
BuildProject.None(无效项目)。 - 如果项目没有配置探测地址,则记录警告并跳过(不阻断发布)。
- 尝试探测线上版本(
ProbeLiveVersionAsync)。- 如果探测失败或返回空值,跳过该项目(也不阻断发布)。
- 比较线上版本和发布版本是否一致(
IsSameVersion)。 - 相同则把信息加入
matches列表。
这个方法的设计思路是宽容处理异常:探测失败、未配置等都不会直接导致发布失败,只会跳过同版本检查;只有明确发现相同版本时才会返回结果,供上层决定是否阻断或要求确认。
4. 辅助方法:ReadVersionHosts
private Dictionary<string, string> ReadVersionHosts()
- 从配置节
"Pipeline:VersionHosts"读取一个字典,例如:
{
"Pipeline": {
"VersionHosts": {
"WebApp": "https://example.com"
}
}
}
- 对读取到的字典做规范化:忽略大小写、去除键和值的空格,过滤空值。
- 返回一个
StringComparer.OrdinalIgnoreCase比较器的字典,便于后续按项目名查找。
5. 辅助方法:ProbeLiveVersionAsync
private async Task<string?> ProbeLiveVersionAsync(
BuildProject project,
string host,
CancellationToken cancellationToken
)
- 验证 host 是否为合法的绝对 HTTP/HTTPS URL,否则记录警告并返回
null。 - 拼接
/api/version作为探测接口。 - 通过
httpClientFactory.CreateClient(HttpClientName)获取 HTTP 客户端。 - 执行 GET 请求,获取字符串响应。
- 对响应做
.Trim().Trim('"'),去掉首尾空格和可能的引号(很多版本接口可能返回带引号的 JSON 字符串,例如"1.2.3")。 - 如果响应为空,记录警告并返回
null。 - 如果请求异常,记录警告并返回
null(不抛出异常,保证主流程不中断)。
这是一个典型的“尽力而为”探测方法:任何失败都视为无法获得线上版本,从而不参与同版本判断。
6. 辅助方法:IsSameVersion
private static bool IsSameVersion(string liveVersion, string publishVersion)
- 首先尝试用
Version.TryParse解析两个版本号,如果都能解析成功,则比较两个Version对象的相等性(例如1.0.0与1.0.0.0会被视为相同)。 - 如果其中任意一个无法解析为
Version,则退化为忽略大小写的字符串比较(例如v1.0.0与V1.0.0会被视为相同)。
这种策略兼顾了“语义化版本号”和“非标准版本号”两种情况。
7. 异常类型:PipelineVersionNotConfirmedException
public sealed class PipelineVersionNotConfirmedException(string message) : Exception(message);
- 这是一个自定义异常,用于表示“用户未确认或超时未确认同版本发布”。
- 主构造函数接收错误信息并传给基类
Exception。 - 该异常可能在发布流程中使用,当发现某些项目存在同版本且需要用户确认时,若用户未确认或超过
ConfirmTimeout,则抛出此异常终止或暂停发布。
总结
这个类是一个发布流水线中的“版本探测”组件,职责是:
- 从配置读取各项目对应的线上探测 URL。
- 并发或顺序地探测线上当前版本。
- 与即将发布的版本进行比较,找出哪些项目实际上已经运行了相同版本。
- 对于探测失败或未配置的项目,采取“跳过”策略,不阻断发布。
- 返回匹配列表,供上层决定是否要求用户确认或直接终止发布。
代码风格简洁、健壮,充分考虑了各种边界情况(无效 URL、空响应、解析异常、大小写差异等),并利用 .NET 的类型和依赖注入特性实现清晰的职责分离。
AI 正在分析代码…
评论加载中...