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
13 changes: 13 additions & 0 deletions .github/workflows/CI-CD.yml
Original file line number Diff line number Diff line change
Expand Up @@ -25,16 +25,29 @@ jobs:
os: [windows-latest, ubuntu-latest]
runs-on: ${{ matrix.os }}

env:
DOTNET_INSTALL_DIR: "./.dotnet"

steps:
- name: Checkout
uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2
with:
fetch-depth: '0'

- name: Install Mono (Linux only)
if: matrix.os == 'ubuntu-latest'
run: |
sudo apt update
sudo apt install -y mono-complete

- name: Install .NET Core
uses: actions/setup-dotnet@3e891b0cb619bf60e2c25674b222b8940e2c1c25 # v4.1.0
with:
dotnet-version: 6.0.x

- name: Add .NET to PATH (Linux only)
if: matrix.os == 'ubuntu-latest'
run: echo "$DOTNET_INSTALL_DIR:$DOTNET_INSTALL_DIR/tools" >> $GITHUB_PATH

- name: Set up Visual Studio shell
uses: egor-tensin/vs-shell@9a932a62d05192eae18ca370155cf877eecc2202 # v2
Expand Down
1 change: 0 additions & 1 deletion SIL.BuildTasks/UnitTestTasks/NUnit3.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
using System.IO;
using System.Text;

namespace SIL.BuildTasks.UnitTestTasks
Expand Down
185 changes: 111 additions & 74 deletions SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs
Original file line number Diff line number Diff line change
@@ -1,54 +1,67 @@
// Copyright (c) 2018 SIL Global
// Copyright (c) 2018-2025 SIL Global
// This software is licensed under the MIT License (http://opensource.org/licenses/MIT)

using System;
using System.Collections.Generic;
using System.IO;
using NUnit.Framework;
using static System.IO.Path;

namespace SIL.ReleaseTasks.Tests
{
[TestFixture]
public class CreateReleaseNotesHtmlTests
{
private const string kNotReleaseNotesClassName = "notmarkdown";
private static readonly string NotReleaseNotesDiv =
$"<div class='{kNotReleaseNotesClassName}'/>";
private static readonly string NotReleaseNotesDivXpath =
$"//div[contains(@class, '{kNotReleaseNotesClassName}')]";
private static readonly string ReleaseNotesClassAttribute =
$"class='{CreateReleaseNotesHtml.kReleaseNotesClassName}'";

private static string GetRandomFileEndingWith(string ending) => Combine(GetTempPath(), GetRandomFileName() + ending);

[Test]
public void MissingMarkdownReturnsFalse()
{
var mockEngine = new MockEngine();
var testMarkdown = new CreateReleaseNotesHtml();
testMarkdown.ChangelogFile = Path.GetRandomFileName();
testMarkdown.BuildEngine = mockEngine;
Assert.That(testMarkdown.Execute(), Is.False);
var sut = new CreateReleaseNotesHtml
{
ChangelogFile = GetRandomFileName(),
BuildEngine = mockEngine
};
Assert.That(sut.Execute(), Is.False);
Assert.That(mockEngine.LoggedMessages[0], Does.StartWith("The given markdown file (").And.EndsWith(") does not exist."));
}

[Test]
public void SimpleMdResultsInSimpleHtml()
{
var testMarkdown = new CreateReleaseNotesHtml();
using(
var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"),
Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm")))
var sut = new CreateReleaseNotesHtml();
using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"),
GetRandomFileEndingWith(".Test.htm")))
{
File.WriteAllLines(filesForTest.FirstFile,
new[]
{"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", "* more", "## 2.2.2", "* things"});
testMarkdown.ChangelogFile = filesForTest.FirstFile;
testMarkdown.HtmlFile = filesForTest.SecondFile;
Assert.That(testMarkdown.Execute(), Is.True);
sut.ChangelogFile = filesForTest.FirstFile;
sut.HtmlFile = filesForTest.SecondFile;
Assert.That(sut.Execute(), Is.True);
}
}

[Test]
public void RemovesKACHead()
public void RemovesKeepAChangelogHead()
{
var testMarkdown = new CreateReleaseNotesHtml();
using(
var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"),
Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm")))
var sut = new CreateReleaseNotesHtml();
using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"),
GetRandomFileEndingWith(".Test.htm")))
{
string changelogContent =
@"# Change Log

All notable changes to this project will be documented in this file.
All notable changes to this project will be documented in this file.

The format is based on [Keep a Changelog](http://keepachangelog.com/)
and this project adheres to [Semantic Versioning](http://semver.org/).
Expand Down Expand Up @@ -78,8 +91,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
- All bugs.";

string expectedHtml =
@"<html><head></head><body><div class='releasenotes'>
<h2>[2.3.4] - 2020-12-09</h2>
$"<html><head><meta charset=\"UTF-8\"/></head><body><div {ReleaseNotesClassAttribute}>" +
Environment.NewLine +
@"<h2>[2.3.4] - 2020-12-09</h2>
<h3>Changed</h3>
<ul>
<li>This to that.</li>
Expand All @@ -96,87 +110,110 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
</div></body></html>";

File.WriteAllText(filesForTest.FirstFile, changelogContent);
testMarkdown.ChangelogFile = filesForTest.FirstFile;
testMarkdown.HtmlFile = filesForTest.SecondFile;
sut.ChangelogFile = filesForTest.FirstFile;
sut.HtmlFile = filesForTest.SecondFile;
// SUT
Assert.That(testMarkdown.Execute(), Is.True);
Assert.That(sut.Execute(), Is.True);
string actualHtml = File.ReadAllText(filesForTest.SecondFile);
Assert.That(actualHtml, Is.EqualTo(expectedHtml));
}
}

[Test]
public void HtmlWithNoReleaseNotesElementIsCompletelyReplaced()
[TestCase(null)]
[TestCase("UTF-8")]
[TestCase("ISO-8859-1")]
public void HtmlWithNoReleaseNotesElement_IsCompletelyReplaced(string existingCharset)
{
var testMarkdown = new CreateReleaseNotesHtml();
using(
var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"),
Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm")))
var sut = new CreateReleaseNotesHtml();
using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"),
GetRandomFileEndingWith(".Test.htm")))
{
var ChangelogFile = filesForTest.FirstFile;
var changelogFile = filesForTest.FirstFile;
var htmlFile = filesForTest.SecondFile;
File.WriteAllLines(ChangelogFile,
new[]
{"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", "* more", "## 2.2.2", "* things"});
File.WriteAllLines(htmlFile,
new[] {"<html>", "<body>", "<div class='notmarkdown'/>", "</body>", "</html>"});
testMarkdown.ChangelogFile = ChangelogFile;
testMarkdown.HtmlFile = htmlFile;
Assert.That(testMarkdown.Execute(), Is.True);
AssertThatXmlIn.File(htmlFile).HasNoMatchForXpath("//div[@notmarkdown]");
File.WriteAllLines(changelogFile, new[]
{
"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7",
"* more", "## 2.2.2", "* things"
});
var htmlLines = new List<string>(new[]
{ "<html>", "<body>", NotReleaseNotesDiv, "</body>", "</html>" });
if (existingCharset != null)
{
htmlLines.Insert(1, "<head>");
htmlLines.Insert(2, $"<meta charset = \"{existingCharset}\"/>");
htmlLines.Insert(3, "</head>");
}

File.WriteAllLines(htmlFile, htmlLines);
sut.ChangelogFile = changelogFile;
sut.HtmlFile = htmlFile;
Assert.That(sut.Execute(), Is.True);
AssertThatXmlIn.File(htmlFile).HasNoMatchForXpath(NotReleaseNotesDivXpath);
var expectedCharset = "UTF-8";
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath($"/html/head/meta[@charset='{expectedCharset}']", 1);
}
}

[Test]
public void HtmlWithReleaseNotesElementHasOnlyReleaseNoteElementChanged()
public void HtmlWithReleaseNotesElement_HasOnlyReleaseNoteElementChanged()
{
var testMarkdown = new CreateReleaseNotesHtml();
using(
var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"),
Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm")))
var sut = new CreateReleaseNotesHtml();
using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"),
GetRandomFileEndingWith(".Test.htm")))
{
var ChangelogFile = filesForTest.FirstFile;
var changelogFile = filesForTest.FirstFile;
var htmlFile = filesForTest.SecondFile;
File.WriteAllLines(ChangelogFile,
new[]
{"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", "* more", "## 2.2.2", "* things"});
File.WriteAllLines(htmlFile,
new[] {"<html>", "<body>", "<div class='notmarkdown'/>", "<div class='releasenotes'/>", "</body>", "</html>"});
testMarkdown.ChangelogFile = ChangelogFile;
testMarkdown.HtmlFile = htmlFile;
Assert.That(testMarkdown.Execute(), Is.True);
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath("//*[@class='notmarkdown']", 1);
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath("//*[@class='releasenotes']", 1);
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath("//*[@class='releasenotes']//*[text()[contains(., 'does some things')]]", 1);
File.WriteAllLines(changelogFile, new[]
{
"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7",
"* more", "## 2.2.2", "* things"
});
File.WriteAllLines(htmlFile, new[]
{
"<html>", "<body>", NotReleaseNotesDiv,
$"<div {ReleaseNotesClassAttribute}/>", "</body>", "</html>"
});
sut.ChangelogFile = changelogFile;
sut.HtmlFile = htmlFile;
Assert.That(sut.Execute(), Is.True);
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath(
NotReleaseNotesDivXpath, 1);
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath(
$"//*[@{ReleaseNotesClassAttribute}]", 1);
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath(
$"//*[@{ReleaseNotesClassAttribute}]//*[text()[contains(., 'does some things')]]", 1);
}
}

[Test]
public void HtmlWithReleaseNotesElementWithContentsIsChanged()
public void HtmlWithReleaseNotesElementWithContents_IsChanged()
{
var testMarkdown = new CreateReleaseNotesHtml();
using(
var filesForTest = new TwoTempFilesForTest(Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.md"),
Path.Combine(Path.GetTempPath(), Path.GetRandomFileName()+".Test.htm")))
var sut = new CreateReleaseNotesHtml();
using (var filesForTest = new TwoTempFilesForTest(GetRandomFileEndingWith(".Test.md"),
GetRandomFileEndingWith(".Test.htm")))
{
var ChangelogFile = filesForTest.FirstFile;
var changelogFile = filesForTest.FirstFile;
var htmlFile = filesForTest.SecondFile;
File.WriteAllLines(ChangelogFile,
new[]
{"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7", "* more", "## 2.2.2", "* things"});
File.WriteAllLines(htmlFile,
new[]
File.WriteAllLines(changelogFile, new[]
{
"## 2.3.9", "* with some random content", "* does some things", "## 2.3.7",
"* more", "## 2.2.2", "* things"
});
File.WriteAllLines(htmlFile, new[]
{
"<html>", "<body>", "<div class='releasenotes'>", "<span class='note'/>", "</div>",
"</body>", "</html>"
"<html>", "<head>", "<meta charset='UTF-8'/>", "</head>", "<body>",
$"<div {ReleaseNotesClassAttribute}>",
"<span class='note'/>", "</div>", "</body>", "</html>"
});
testMarkdown.ChangelogFile = ChangelogFile;
testMarkdown.HtmlFile = htmlFile;
Assert.That(testMarkdown.Execute(), Is.True);
sut.ChangelogFile = changelogFile;
sut.HtmlFile = htmlFile;
Assert.That(sut.Execute(), Is.True);
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath(
"/html/head/meta[@charset='UTF-8']", 1);
AssertThatXmlIn.File(htmlFile).HasNoMatchForXpath("//span[@class='note']");
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath("//*[@class='releasenotes']", 1);
AssertThatXmlIn.File(htmlFile).HasSpecifiedNumberOfMatchesForXpath(
$"//*[@{ReleaseNotesClassAttribute}]", 1);
}
}

}
}
45 changes: 25 additions & 20 deletions SIL.ReleaseTasks/CreateReleaseNotesHtml.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
// Copyright (c) 2018 SIL Global
// Copyright (c) 2018-2025 SIL Global
// This software is licensed under the MIT License (http://opensource.org/licenses/MIT)

using System;
Expand All @@ -15,13 +15,17 @@ namespace SIL.ReleaseTasks
/// <inheritdoc />
/// <summary>
/// Given a markdown-style changelog file, this class will generate a release notes HTML file.
/// If the HTML file already exists, the task will look for a section with `class="releasenotes"`
/// and replace it with the current release notes.
/// The developer-oriented and [Unreleased] beginning will be removed from a Keep a Changelog style changelog.
/// If the HTML file already exists, the task will look for a section with
/// `class="<see cref="kReleaseNotesClassName"/>>"` and replace it with the current release
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
/// `class="<see cref="kReleaseNotesClassName"/>>"` and replace it with the current release
/// `class="<see cref="kReleaseNotesClassName"/>"` and replace it with the current release

/// notes.
/// The developer-oriented and [Unreleased] beginning will be removed from a Keep a Changelog
/// style changelog.
/// </summary>
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class CreateReleaseNotesHtml : Task
{
public const string kReleaseNotesClassName = "releasenotes";

[Required]
public string HtmlFile { get; set; }

Expand All @@ -39,27 +43,28 @@ public override bool Execute()
try
{
string inputMarkdown = File.ReadAllText(ChangelogFile);
CreateReleaseNotesHtml.RemoveKeepAChangelogHeadIfPresent(ref inputMarkdown);
// MarkdownDeep appears to use \n for newlines. Rather than mix those with platform line-endings, just
// convert them to platform line-endings if needed.
RemoveKeepAChangelogHeadIfPresent(ref inputMarkdown);
// MarkDig appears to use \n for newlines. Rather than mix those with platform
// line-endings, just convert them to platform line-endings if needed.
var markdownHtml = Markdown.ToHtml(inputMarkdown).Replace("\n", Environment.NewLine);
if(File.Exists(HtmlFile))
XElement releaseNotesElement = null;
XDocument htmlDoc = null;
if (File.Exists(HtmlFile))
{
htmlDoc = XDocument.Load(HtmlFile);
var releaseNotesElementXpath = $"//*[@class='{kReleaseNotesClassName}']";
releaseNotesElement = htmlDoc.XPathSelectElement(releaseNotesElementXpath);
}
if (releaseNotesElement == null)
WriteBasicHtmlFromMarkdown(markdownHtml);
else
{
var htmlDoc = XDocument.Load(HtmlFile);
var releaseNotesElement = htmlDoc.XPathSelectElement("//*[@class='releasenotes']");
if (releaseNotesElement == null)
return true;

releaseNotesElement.RemoveNodes();
var mdDocument = XDocument.Parse($"<div>{markdownHtml}</div>");
// ReSharper disable once PossibleNullReferenceException - Will either throw or work
releaseNotesElement.Add(mdDocument.Root.Elements());
htmlDoc.Save(HtmlFile);
}
else
{
WriteBasicHtmlFromMarkdown(markdownHtml);
}
return true;
}
catch(Exception e)
Expand All @@ -71,7 +76,9 @@ public override bool Execute()

private void WriteBasicHtmlFromMarkdown(string markdownHtml)
{
File.WriteAllText(HtmlFile, $"<html><head></head><body><div class='releasenotes'>{Environment.NewLine}{markdownHtml}</div></body></html>");
File.WriteAllText(HtmlFile, "<html><head><meta charset=\"UTF-8\"/></head><body>" +
$"<div class='{kReleaseNotesClassName}'>" +
$"{Environment.NewLine}{markdownHtml}</div></body></html>");
}

/// <summary>
Expand All @@ -81,9 +88,7 @@ private void WriteBasicHtmlFromMarkdown(string markdownHtml)
public static bool RemoveKeepAChangelogHeadIfPresent(ref string md)
{
if (!md.Contains("[Unreleased]"))
{
return false;
}
string unreleasedHeader = $"## [Unreleased]{Environment.NewLine}{Environment.NewLine}";
int unreleasedHeaderLocation = md.IndexOf(unreleasedHeader);
md = md.Substring(unreleasedHeaderLocation + unreleasedHeader.Length);
Expand Down
Loading