| | | 1 | | using System.Text.RegularExpressions; |
| | | 2 | | |
| | | 3 | | namespace Services.ApiServices.TelegramBotApi; |
| | | 4 | | |
| | | 5 | | [System.Text.Json.Serialization.JsonConverter(typeof(System.Text.Json.Serialization.JsonStringEnumConverter))] |
| | | 6 | | public enum DeployStage |
| | | 7 | | { |
| | | 8 | | Started, |
| | | 9 | | Finished, |
| | | 10 | | Failed |
| | | 11 | | } |
| | | 12 | | |
| | | 13 | | /// <summary>Коммит из обновления (первый родитель ветки деплоя: прямые коммиты и merge-коммиты PR).</summary> |
| | | 14 | | public sealed record DeployCommit(string Sha, string? Author, string? Subject, string? Body); |
| | | 15 | | |
| | | 16 | | /// <summary>Событие деплоя, которое присылает workflow GitHub Actions.</summary> |
| | 5 | 17 | | public sealed record DeployEvent( |
| | 30 | 18 | | DeployStage Stage, |
| | 12 | 19 | | string Sha, |
| | 4 | 20 | | string? CommitDate, |
| | 5 | 21 | | string? Repository, |
| | 6 | 22 | | string? RunUrl, |
| | 5 | 23 | | string? Actor, |
| | 4 | 24 | | int? DurationSeconds, |
| | 10 | 25 | | IReadOnlyList<DeployCommit> Commits); |
| | | 26 | | |
| | | 27 | | /// <summary>Изменение для списка «что в обновлении»: заголовок, коммит и (если есть) номер PR.</summary> |
| | | 28 | | public sealed record DeployChange(string Title, string Sha, int? PullRequest); |
| | | 29 | | |
| | | 30 | | /// <summary> |
| | | 31 | | /// Превращает коммиты деплоя в список изменений: merge-коммит PR заменяется заголовком PR, |
| | | 32 | | /// squash-коммит «заголовок (#N)» — номером PR; технические merge веток пропускаются. |
| | | 33 | | /// </summary> |
| | | 34 | | public static partial class DeployChangeParser |
| | | 35 | | { |
| | | 36 | | [GeneratedRegex(@"^Merge pull request #(?<n>\d+)\b", RegexOptions.IgnoreCase)] |
| | | 37 | | private static partial Regex MergePullRequest(); |
| | | 38 | | |
| | | 39 | | [GeneratedRegex(@"\s*\(#(?<n>\d+)\)\s*$")] |
| | | 40 | | private static partial Regex SquashSuffix(); |
| | | 41 | | |
| | | 42 | | [GeneratedRegex(@"^Merge (branch|remote-tracking branch|tag)\b", RegexOptions.IgnoreCase)] |
| | | 43 | | private static partial Regex TechnicalMerge(); |
| | | 44 | | |
| | | 45 | | public static IReadOnlyList<DeployChange> Parse(IEnumerable<DeployCommit> commits) |
| | | 46 | | { |
| | | 47 | | var result = new List<DeployChange>(); |
| | | 48 | | var seenPr = new HashSet<int>(); |
| | | 49 | | foreach (var commit in commits) |
| | | 50 | | { |
| | | 51 | | var subject = (commit.Subject ?? "").Trim(); |
| | | 52 | | if (subject.Length == 0) continue; |
| | | 53 | | |
| | | 54 | | var merge = MergePullRequest().Match(subject); |
| | | 55 | | if (merge.Success) |
| | | 56 | | { |
| | | 57 | | var pr = int.Parse(merge.Groups["n"].Value); |
| | | 58 | | if (!seenPr.Add(pr)) continue; |
| | | 59 | | var title = FirstLine(commit.Body); |
| | | 60 | | result.Add(new DeployChange(string.IsNullOrWhiteSpace(title) ? subject : title, commit.Sha, pr)); |
| | | 61 | | continue; |
| | | 62 | | } |
| | | 63 | | |
| | | 64 | | if (TechnicalMerge().IsMatch(subject)) continue; |
| | | 65 | | |
| | | 66 | | var squash = SquashSuffix().Match(subject); |
| | | 67 | | if (squash.Success) |
| | | 68 | | { |
| | | 69 | | var pr = int.Parse(squash.Groups["n"].Value); |
| | | 70 | | if (!seenPr.Add(pr)) continue; |
| | | 71 | | result.Add(new DeployChange(subject[..squash.Index].Trim(), commit.Sha, pr)); |
| | | 72 | | continue; |
| | | 73 | | } |
| | | 74 | | |
| | | 75 | | result.Add(new DeployChange(subject, commit.Sha, null)); |
| | | 76 | | } |
| | | 77 | | |
| | | 78 | | return result; |
| | | 79 | | } |
| | | 80 | | |
| | | 81 | | private static string? FirstLine(string? text) => |
| | | 82 | | text?.Split('\n', StringSplitOptions.RemoveEmptyEntries | StringSplitOptions.TrimEntries).FirstOrDefault(); |
| | | 83 | | } |