Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 26 additions & 5 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
Expand Up @@ -63,7 +63,7 @@ jobs:
"prerelease=$($v.Contains('-').ToString().ToLower())" >> $env:GITHUB_OUTPUT
"publish=$($publish.ToString().ToLower())" >> $env:GITHUB_OUTPUT

- name: Prepare release notes (English, with a link to the Portuguese changelog)
- name: Prepare release notes (English, a link to the Portuguese changelog, a summary per translated changelog)
if: steps.version.outputs.publish == 'true'
shell: pwsh
env:
Expand All @@ -79,10 +79,29 @@ jobs:
return $notes
}
# Both changelogs must have the section; the Portuguese one is linked, not pasted.
$null = Get-Notes "CHANGELOG.md"
$pt = Get-Notes "CHANGELOG.md"
$en = Get-Notes "CHANGELOG.en-US.md"
# English only, no language headings: ReleaseNotes.cs shows the first item to every
# language. A "(#NN)" at the end of an item becomes a link to that PR on GitHub.
# One hidden "<!-- summary <code>: ... -->" line per translated changelog (CHANGELOG.md is
# pt-BR, any CHANGELOG.<code>.md another language): the update notice shows it instead
# of the English first item (ReleaseNotes.cs). They go at the very end of the body, so
# clients up to 1.6.x, which show the first line they find, keep showing the English one.
function Get-FirstItem([string]$notes) {
$item = $notes -split "\r?\n" | Where-Object { $_ -match '^\s*[-*]\s+\S' } | Select-Object -First 1
if (-not $item) { return $null }
return ($item -replace '^\s*[-*]\s+', '' -replace '-->', '').Trim()
}
$summaries = @()
$ptItem = Get-FirstItem $pt
if ($ptItem) { $summaries += "<!-- summary pt-BR: $ptItem -->" }
foreach ($file in Get-ChildItem -Name -Filter "CHANGELOG.*.md" | Where-Object { $_ -ne "CHANGELOG.en-US.md" } | Sort-Object) {
$code = $file -replace '^CHANGELOG\.|\.md$', ''
# Optional: a translated changelog without this version's section is skipped, not fatal.
$notes = try { Get-Notes $file } catch { Write-Host "$file sem a seção $($env:VERSION); sem resumo em $code."; $null }
$item = if ($notes) { Get-FirstItem $notes }
if ($item) { $summaries += "<!-- summary ${code}: $item -->" }
}
# English, no language headings: ReleaseNotes.cs shows its first item to every
# language without a summary. A "(#NN)" at the end of an item becomes a link to that PR on GitHub.
$tag = "v$($env:VERSION)"
$releaseNotes = @(
$en
Expand All @@ -96,7 +115,9 @@ jobs:
"---"
""
"🇧🇷 Notas em português: [CHANGELOG.md](https://github.com/lippdev/consolemode/blob/$tag/CHANGELOG.md)"
) -join "`n"
)
if ($summaries.Count -gt 0) { $releaseNotes += @("") + $summaries }
$releaseNotes = $releaseNotes -join "`n"
Set-Content -LiteralPath "release-notes.md" -Value $releaseNotes -Encoding utf8

- name: Run tests
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.en-US.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
English (US) release notes, mirroring CHANGELOG.md (Brazilian Portuguese). Before publishing a version, add a `## [VERSION]` section to **both** files: the workflow publishes the section matching the tag from each one and fails if either is missing.

## [Unreleased]
### What's new
- Update notice in Portuguese: the new version's highlight shows in the app's language when there is a changelog in it (today, Portuguese); other languages still see it in English. Applies to updates published after this version. (#158)

## [1.6.2]
### What's new
Expand Down
2 changes: 2 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@
Notas em português do Brasil; a versão em inglês (Estados Unidos) fica em `CHANGELOG.en-US.md`. Antes de publicar uma versão, adicione uma seção `## [VERSÃO]` com o changelog daquela versão **nos dois arquivos**. O workflow publica as duas seções correspondentes à tag na mesma release e falha se faltar alguma.

## [Unreleased]
### Novidades
- Aviso de atualização em português: o destaque da nova versão aparece no idioma do app quando há um changelog nele (hoje, português); os outros idiomas continuam vendo o destaque em inglês. Vale a partir das atualizações publicadas depois desta versão. (#158)

## [1.6.2]
### Novidades
Expand Down
39 changes: 36 additions & 3 deletions src/ConsoleMode/Services/ReleaseNotes.cs
Original file line number Diff line number Diff line change
@@ -1,25 +1,37 @@
using System.Text.RegularExpressions;

namespace ConsoleMode.Services;

/// <summary>
/// Reads the GitHub release body written by .github/workflows/release.yml.
/// Since 1.5.0 the body has one section per language, each opened by a fixed
/// "## …" heading; older releases are pt-BR only, with no language headings.
/// Since 1.7.0 the body ends with one hidden summary per translated changelog
/// (<see cref="SummaryPrefix"/>), which wins over the English notes.
/// </summary>
public static class ReleaseNotes
{
public const string PortugueseHeading = "## Português (Brasil)";
public const string EnglishHeading = "## English (US)";

/// <summary>"&lt;!-- summary pt-BR: first item --&gt;": an HTML comment, so GitHub doesn't show it.</summary>
public const string SummaryPrefix = "<!-- summary ";

private static readonly Regex MarkdownLink = new(@"\[([^\]]+)\]\([^)]*\)", RegexOptions.Compiled);

/// <summary>
/// First real line of the notes in <paramref name="language"/>, skipping headings and
/// list markers, or null when the release has no notes in that language.
/// </summary>
public static string? FirstLine(string? notes, string language)
{
if (string.IsNullOrWhiteSpace(notes)) return null;
var lines = notes.Replace("\r\n", "\n").Split('\n');
var summary = Summary(lines, language);
if (summary is not null) return summary;

// The release body has pt-BR and en-US sections; every other language reads the English one.
var isEnglish = !string.Equals(language, LocalizationService.PortugueseBrazil, StringComparison.Ordinal);
var lines = notes.Replace("\r\n", "\n").Split('\n');
var bilingual = lines.Any(l => IsHeading(l, PortugueseHeading) || IsHeading(l, EnglishHeading));

// Since 1.5.0-beta.6 the body is English only (Portuguese lives in CHANGELOG.md, linked
Expand All @@ -38,12 +50,33 @@ public static class ReleaseNotes

var trimmed = raw.Trim();
if (trimmed.Length == 0 || trimmed.StartsWith('#') || trimmed == "---") continue;
var line = trimmed.TrimStart('-', '*', ' ').Replace("**", "").Trim();
if (line.Length > 0) return line.Length > 140 ? line[..140] + "…" : line;
var line = Clean(trimmed);
if (line.Length > 0) return line;
}
return null;
}

/// <summary>The hidden summary for exactly <paramref name="language"/>, if the release has one.</summary>
private static string? Summary(string[] lines, string language)
{
var prefix = SummaryPrefix + language + ":";
foreach (var raw in lines)
{
var trimmed = raw.Trim();
if (!trimmed.StartsWith(prefix, StringComparison.Ordinal) || !trimmed.EndsWith("-->", StringComparison.Ordinal)) continue;
var line = Clean(trimmed[prefix.Length..^3]);
if (line.Length > 0) return line;
}
return null;
}

/// <summary>Plain text for the notice: no list marker, bold or link syntax, at most 140 characters.</summary>
private static string Clean(string text)
{
var line = MarkdownLink.Replace(text.Trim().TrimStart('-', '*', ' ').Replace("**", ""), "$1").Trim();
return line.Length > 140 ? line[..140] + "…" : line;
}

private static bool IsHeading(string line, string heading) =>
string.Equals(line.Trim().TrimStart(''), heading, StringComparison.Ordinal);
}
45 changes: 45 additions & 0 deletions tests/ConsoleMode.Tests/ReleaseNotesTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -80,6 +80,51 @@ public void FirstLine_EnglishOnlyBodyIsSharedByEveryLanguage()
Assert.Equal("Session menu over the game. (#46)", ReleaseNotes.FirstLine(englishOnly, LocalizationService.SpanishSpain));
}

private const string WithSummaries = """
### What's new
- **Minimize to the system tray:** a new option in Settings. (#150)

---

🇧🇷 Notas em português: [CHANGELOG.md](https://example.com/CHANGELOG.md)

<!-- summary pt-BR: **Minimizar para a bandeja:** traduzida por [u/Skinner99](https://www.reddit.com/user/Skinner99/). (#150) -->
""";

[Fact]
public void FirstLine_HiddenSummaryWinsForItsLanguage()
{
Assert.Equal("Minimizar para a bandeja: traduzida por u/Skinner99. (#150)",
ReleaseNotes.FirstLine(WithSummaries, LocalizationService.PortugueseBrazil));
Assert.Equal("Minimizar para a bandeja: traduzida por u/Skinner99. (#150)",
ReleaseNotes.FirstLine(WithSummaries.Replace("\n", "\r\n"), LocalizationService.PortugueseBrazil));
}

[Fact]
public void FirstLine_LanguagesWithoutASummaryReadTheEnglishNotes()
{
Assert.Equal("Minimize to the system tray: a new option in Settings. (#150)",
ReleaseNotes.FirstLine(WithSummaries, LocalizationService.EnglishUnitedStates));
Assert.Equal("Minimize to the system tray: a new option in Settings. (#150)",
ReleaseNotes.FirstLine(WithSummaries, LocalizationService.GermanGermany));
}

[Fact]
public void FirstLine_EmptyOrUnclosedSummaryFallsBackToTheNotes()
{
Assert.Equal("Item.", ReleaseNotes.FirstLine("- Item.\n<!-- summary pt-BR: -->", LocalizationService.PortugueseBrazil));
Assert.Equal("Item.", ReleaseNotes.FirstLine("- Item.\n<!-- summary pt-BR: sem fim", LocalizationService.PortugueseBrazil));
}

/// <summary>Clients up to 1.6.x show the first line they find; the summaries sit at the end so they keep showing the English item.</summary>
[Fact]
public void Summaries_StayAfterTheNotesForOlderClients()
{
var firstLine = WithSummaries.Split('\n').Select(l => l.Trim())
.First(l => l.Length > 0 && !l.StartsWith('#') && l != "---");
Assert.StartsWith("- **Minimize", firstLine);
}

[Fact]
public void FirstLine_EmptyBodyIsNull()
{
Expand Down
Loading