diff --git a/.github/workflows/CI-CD.yml b/.github/workflows/CI-CD.yml
index eccc67d9..3e6030c6 100644
--- a/.github/workflows/CI-CD.yml
+++ b/.github/workflows/CI-CD.yml
@@ -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
diff --git a/SIL.BuildTasks/UnitTestTasks/NUnit3.cs b/SIL.BuildTasks/UnitTestTasks/NUnit3.cs
index 08036c4d..02f4eee5 100644
--- a/SIL.BuildTasks/UnitTestTasks/NUnit3.cs
+++ b/SIL.BuildTasks/UnitTestTasks/NUnit3.cs
@@ -4,7 +4,6 @@
using System;
using System.Diagnostics;
using System.Diagnostics.CodeAnalysis;
-using System.IO;
using System.Text;
namespace SIL.BuildTasks.UnitTestTasks
diff --git a/SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs b/SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs
index 62d3bdfa..f193a18c 100644
--- a/SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs
+++ b/SIL.ReleaseTasks.Tests/CreateReleaseNotesHtmlTests.cs
@@ -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 =
+ $"
-
[2.3.4] - 2020-12-09
+$"
" +
+Environment.NewLine +
+@"
[2.3.4] - 2020-12-09
Changed
- This to that.
@@ -96,87 +110,110 @@ and this project adheres to [Semantic Versioning](http://semver.org/).
";
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[] {"", "", "
", "", ""});
- 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
(new[]
+ { "", "", NotReleaseNotesDiv, "", "" });
+ if (existingCharset != null)
+ {
+ htmlLines.Insert(1, "");
+ htmlLines.Insert(2, $"");
+ htmlLines.Insert(3, "");
+ }
+
+ 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[] {"", "", "", "", "", ""});
- 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[]
+ {
+ "", "", NotReleaseNotesDiv,
+ $"", "", ""
+ });
+ 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[]
{
- "", "", "", "", "
",
- "", ""
+ "", "", "", "", "",
+ $"",
+ "", "
", "", ""
});
- 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);
}
}
-
}
}
diff --git a/SIL.ReleaseTasks/CreateReleaseNotesHtml.cs b/SIL.ReleaseTasks/CreateReleaseNotesHtml.cs
index 32cded4a..910bbf41 100644
--- a/SIL.ReleaseTasks/CreateReleaseNotesHtml.cs
+++ b/SIL.ReleaseTasks/CreateReleaseNotesHtml.cs
@@ -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;
@@ -15,13 +15,17 @@ namespace SIL.ReleaseTasks
///
///
/// 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=">"` and replace it with the current release
+ /// notes.
+ /// The developer-oriented and [Unreleased] beginning will be removed from a Keep a Changelog
+ /// style changelog.
///
[SuppressMessage("ReSharper", "MemberCanBePrivate.Global")]
public class CreateReleaseNotesHtml : Task
{
+ public const string kReleaseNotesClassName = "releasenotes";
+
[Required]
public string HtmlFile { get; set; }
@@ -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($"{markdownHtml}
");
// 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)
@@ -71,7 +76,9 @@ public override bool Execute()
private void WriteBasicHtmlFromMarkdown(string markdownHtml)
{
- File.WriteAllText(HtmlFile, $"{Environment.NewLine}{markdownHtml}
");
+ File.WriteAllText(HtmlFile, "" +
+ $"" +
+ $"{Environment.NewLine}{markdownHtml}
");
}
///
@@ -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);