diff --git a/src/Fallout.Migrate/Steps/CleanupBootstrapScriptsStep.cs b/src/Fallout.Migrate/Steps/CleanupBootstrapScriptsStep.cs
index a3fc79bdc..3b8faaa6b 100644
--- a/src/Fallout.Migrate/Steps/CleanupBootstrapScriptsStep.cs
+++ b/src/Fallout.Migrate/Steps/CleanupBootstrapScriptsStep.cs
@@ -2,7 +2,6 @@
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
-using Fallout.Common.IO;
using Fallout.Migrate.Common;
namespace Fallout.Migrate.Steps;
@@ -15,14 +14,14 @@ internal partial class CleanupBootstrapScriptsStep : IMigrationStep
{
public Task ExecuteAsync(MigrationContext context, Summary summary)
{
- foreach (var file in new[]
+ // The scripts can sit in a subdirectory (NukeScriptDirectory), so search the whole tree.
+ foreach (var name in new[]
{
"build.sh",
"build.ps1"
})
{
- var path = context.RootDirectory / file;
- if (path.FileExists())
+ foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, name))
{
MigrationFileOperations.ApplyRewrite(context, path, Cleanup, summary);
}
@@ -50,11 +49,27 @@ private static RewriteResult Cleanup(string content)
// here, we get the index of the line that contains the environment variable check
var indexOfEnterpriseEnvVarCheck = lines.FindIndex(line => line.Contains(envVarToCheck));
+ // The generated block starts with the `if` line that checks the token. Any other first use,
+ // for example `export NUKE_ENTERPRISE_TOKEN=...`, is not the generated block, so leave the
+ // file alone. Otherwise an unrelated `fi`/`}` later in the file would end the removal.
+ var checkLine = lines[indexOfEnterpriseEnvVarCheck].TrimStart();
+ if (!checkLine.StartsWith("if ", StringComparison.OrdinalIgnoreCase) &&
+ !checkLine.StartsWith("if(", StringComparison.OrdinalIgnoreCase))
+ {
+ return new RewriteResult(content, 0);
+ }
+
// here, we get the index of the line that ends the if block
// which is fi on bash and a simple "}" in powershell
var endOfIfBlock = lines.FindIndex(indexOfEnterpriseEnvVarCheck,
line => line.Trim() == "}" || line.Trim() == "fi");
+ // No closing `fi`/`}` after the token: not the generated block, so leave the file alone.
+ if (endOfIfBlock < 0)
+ {
+ return new RewriteResult(content, 0);
+ }
+
// this is "just" to remove the empty line after the if block
if (lines.Count > endOfIfBlock + 1 && lines[endOfIfBlock + 1].Trim() == "")
{
diff --git a/src/Fallout.Migrate/Steps/RenameNukeDirectoryStep.cs b/src/Fallout.Migrate/Steps/RenameNukeDirectoryStep.cs
index a473895ea..2f8e43c2b 100644
--- a/src/Fallout.Migrate/Steps/RenameNukeDirectoryStep.cs
+++ b/src/Fallout.Migrate/Steps/RenameNukeDirectoryStep.cs
@@ -1,4 +1,5 @@
using System.IO;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Fallout.Common.IO;
using Fallout.Migrate.Common;
@@ -7,16 +8,35 @@ namespace Fallout.Migrate.Steps;
///
/// Renames the repository's .nuke/ directory to .fallout/, or records a warning if
-/// both already exist and need a manual merge.
+/// both already exist and need a manual merge. The build.schema.json inside is rewritten
+/// first, so the moved file names its base definition FalloutBuild.
///
internal sealed class RenameNukeDirectoryStep : IMigrationStep
{
+ // NUKE's schema has the layout Fallout reads; only the base definition is named
+ // NukeBuild. CompletionUtility (fallout completion and :secrets) looks up FalloutBuild,
+ // so rename the definition key and its $ref. The next build rewrites the whole file.
+ // The lookbehind matches any quoted "NukeBuild", not only these two. That is safe because
+ // the schema is machine-generated, and NukeBuild appears only as the key and in the $ref.
+ private static readonly Regex nukeBuildDefinition =
+ new(@"(?<=""|#/definitions/)NukeBuild(?="")", RegexOptions.Compiled);
+
///
public Task ExecuteAsync(MigrationContext context, Summary summary)
{
var legacy = context.RootDirectory / ".nuke";
var canonical = context.RootDirectory / ".fallout";
+ // Rewrite the schema in the directory that ends up as .fallout/: .nuke/ when it is moved,
+ // otherwise .fallout/. When both exist, .nuke/ is left for the manual merge. This also
+ // repairs a repository that an earlier fallout-migrate moved without rewriting.
+ var moveLegacy = legacy.DirectoryExists() && !canonical.DirectoryExists();
+ var schema = (moveLegacy ? legacy : canonical) / "build.schema.json";
+ if (schema.FileExists())
+ {
+ MigrationFileOperations.ApplyRewrite(context, schema, RewriteSchema, summary);
+ }
+
if (!legacy.DirectoryExists())
{
return Task.CompletedTask;
@@ -43,4 +63,21 @@ public Task ExecuteAsync(MigrationContext context, Summary summary)
summary.DirectoriesRenamed++;
return Task.CompletedTask;
}
+
+ ///
+ /// Renames the NukeBuild definition and its $ref in .
+ ///
+ /// The original build.schema.json content.
+ /// The rewritten content and the number of edits made.
+ private static RewriteResult RewriteSchema(string original)
+ {
+ var edits = 0;
+ var content = nukeBuildDefinition.Replace(original, _ =>
+ {
+ edits++;
+ return "FalloutBuild";
+ });
+
+ return new RewriteResult(content, edits);
+ }
}
diff --git a/src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs b/src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs
index 741b118bc..c0bfc01bd 100644
--- a/src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs
+++ b/src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs
@@ -1,6 +1,5 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
-using Fallout.Common.IO;
using Fallout.Migrate.Common;
namespace Fallout.Migrate.Steps;
@@ -33,6 +32,8 @@ private static readonly (Regex Pattern, string Replacement)[] patterns =
///
public Task ExecuteAsync(MigrationContext context, Summary summary)
{
+ // The scripts can sit in a subdirectory (NukeScriptDirectory), so search the whole tree.
+ // The patterns only match NUKE-specific text, and a script without any is not written.
foreach (var name in new[]
{
"build.cmd",
@@ -40,8 +41,7 @@ public Task ExecuteAsync(MigrationContext context, Summary summary)
"build.sh"
})
{
- var path = context.RootDirectory / name;
- if (path.FileExists())
+ foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, name))
{
MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary);
}
diff --git a/src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs b/src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs
index 2b6064b12..dd6ad9315 100644
--- a/src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs
+++ b/src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs
@@ -7,7 +7,8 @@ namespace Fallout.Migrate.Steps;
///
/// Rewrites every *.cs file under the repository root: Nuke.* namespace prefixes become
/// Fallout., the bare NukeBuild/INukeBuild types become
-/// FalloutBuild/IFalloutBuild, and the solution-model namespace (which moved out of
+/// FalloutBuild/IFalloutBuild, the IHaz* component interfaces become
+/// IHas*, and the solution-model namespace (which moved out of
/// *.Common.ProjectModel in v11) becomes Fallout.Solutions.
///
internal sealed class RewriteCsFilesStep : IMigrationStep
@@ -32,6 +33,14 @@ internal sealed class RewriteCsFilesStep : IMigrationStep
private static readonly Regex nukeBuildType = new(@"\bNukeBuild\b", RegexOptions.Compiled);
private static readonly Regex iNukeBuildType = new(@"\bINukeBuild\b", RegexOptions.Compiled);
+ // NUKE's IHaz* component interfaces are IHas* in Fallout.Components. The [Obsolete]
+ // IHaz* aliases ship only in the Nuke.Components shim, which the namespace swap above
+ // moves the code away from. Limited to the shipped names, so a user-defined IHaz*
+ // interface keeps its name.
+ private static readonly Regex iHazComponentType = new(
+ @"\bIHaz(?=(?:Artifacts|Changelog|Configuration|GitRepository|GitVersion|NerdbankGitVersioning|Reports|Solution|TwitterCredentials)\b)",
+ RegexOptions.Compiled);
+
///
public Task ExecuteAsync(MigrationContext context, Summary summary)
{
@@ -77,6 +86,12 @@ private static RewriteResult Rewrite(string original)
return "FalloutBuild";
});
+ content = iHazComponentType.Replace(content, _ =>
+ {
+ edits++;
+ return "IHas";
+ });
+
return new RewriteResult(content, edits);
}
}
diff --git a/tests/Fallout.Migrate.Specs/RenameNukeDirectoryStepSpecs.cs b/tests/Fallout.Migrate.Specs/RenameNukeDirectoryStepSpecs.cs
new file mode 100644
index 000000000..21278365d
--- /dev/null
+++ b/tests/Fallout.Migrate.Specs/RenameNukeDirectoryStepSpecs.cs
@@ -0,0 +1,109 @@
+using System;
+using System.IO;
+using System.Threading.Tasks;
+using Fallout.Common.IO;
+using Fallout.Migrate.Common;
+using Fallout.Migrate.Steps;
+using FluentAssertions;
+using Xunit;
+
+namespace Fallout.Migrate.Specs;
+
+public class RenameNukeDirectoryStepSpecs : IDisposable
+{
+ private const string NukeSchema = """
+ {
+ "definitions": {
+ "Host": { "type": "string" },
+ "NukeBuild": { "properties": { "Verbosity": { "type": "string" } } }
+ },
+ "allOf": [
+ { "properties": { "Configuration": { "type": "string" } } },
+ { "$ref": "#/definitions/NukeBuild" }
+ ]
+ }
+ """;
+
+ private readonly AbsolutePath tempDirectory;
+ private readonly Summary summary = new();
+
+ public RenameNukeDirectoryStepSpecs()
+ {
+ tempDirectory = AbsolutePath.Temp("fallout-migrate-test");
+ }
+
+ public void Dispose()
+ {
+ tempDirectory.DeleteDirectory();
+ }
+
+ [Fact]
+ public async Task Moved_schema_names_its_base_definition_FalloutBuild()
+ {
+ // Arrange
+ (tempDirectory / ".nuke" / "build.schema.json").WriteAllText(NukeSchema, eofLineBreak: false);
+ var context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null);
+
+ // Act
+ await new RenameNukeDirectoryStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.DirectoriesRenamed.Should().Be(1);
+ summary.EditCount.Should().Be(2);
+ var schema = (tempDirectory / ".fallout" / "build.schema.json").ReadAllText();
+ schema.Should().Contain("\"FalloutBuild\": {");
+ schema.Should().Contain("\"$ref\": \"#/definitions/FalloutBuild\"");
+ schema.Should().NotContain("NukeBuild");
+ }
+
+ [Fact]
+ public async Task Schema_of_an_already_migrated_repo_is_repaired()
+ {
+ // Arrange
+ (tempDirectory / ".fallout" / "build.schema.json").WriteAllText(NukeSchema, eofLineBreak: false);
+ var context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null);
+
+ // Act
+ await new RenameNukeDirectoryStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.DirectoriesRenamed.Should().Be(0);
+ summary.EditCount.Should().Be(2);
+ (tempDirectory / ".fallout" / "build.schema.json").ReadAllText().Should().NotContain("NukeBuild");
+ }
+
+ [Fact]
+ public async Task Only_the_fallout_schema_is_rewritten_when_both_directories_exist()
+ {
+ // Arrange
+ (tempDirectory / ".nuke" / "build.schema.json").WriteAllText(NukeSchema, eofLineBreak: false);
+ (tempDirectory / ".fallout" / "build.schema.json").WriteAllText(NukeSchema, eofLineBreak: false);
+ var context = new MigrationContext(tempDirectory, dryRun: false, TextWriter.Null);
+
+ // Act
+ await new RenameNukeDirectoryStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.DirectoriesRenamed.Should().Be(0);
+ summary.EditCount.Should().Be(2);
+ summary.Warnings.Should().ContainSingle().Which.Should().Contain("merge their contents manually");
+ (tempDirectory / ".fallout" / "build.schema.json").ReadAllText().Should().NotContain("NukeBuild");
+ (tempDirectory / ".nuke" / "build.schema.json").ReadAllText().Should().Be(NukeSchema);
+ }
+
+ [Fact]
+ public async Task Dry_run_leaves_the_schema_unchanged()
+ {
+ // Arrange
+ (tempDirectory / ".nuke" / "build.schema.json").WriteAllText(NukeSchema, eofLineBreak: false);
+ var context = new MigrationContext(tempDirectory, dryRun: true, TextWriter.Null);
+
+ // Act
+ await new RenameNukeDirectoryStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.EditCount.Should().Be(2);
+ (tempDirectory / ".nuke" / "build.schema.json").ReadAllText().Should().Be(NukeSchema);
+ (tempDirectory / ".fallout").DirectoryExists().Should().BeFalse();
+ }
+}
diff --git a/tests/Fallout.Migrate.Specs/RewriteBootstrapScriptsStepSpecs.cs b/tests/Fallout.Migrate.Specs/RewriteBootstrapScriptsStepSpecs.cs
index b437f0995..57247ed40 100644
--- a/tests/Fallout.Migrate.Specs/RewriteBootstrapScriptsStepSpecs.cs
+++ b/tests/Fallout.Migrate.Specs/RewriteBootstrapScriptsStepSpecs.cs
@@ -196,4 +196,90 @@ public async Task Leaves_bootstrapper_scripts_without_leftovers_alone()
"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" --no-build -- "$@"
""");
}
+
+ [Fact]
+ public async Task Bootstrap_scripts_in_a_subdirectory_are_rewritten()
+ {
+ // Arrange
+ (tempDirectory / "eng" / "build.sh").WriteAllText("""
+ TEMP_DIRECTORY="$SCRIPT_DIR/../.nuke/temp"
+ dotnet nuke "$@"
+ """, eofLineBreak: false);
+
+ // Act
+ await new RewriteBootstrapScriptsStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.EditCount.Should().Be(2);
+ var buildSh = (tempDirectory / "eng" / "build.sh").ReadAllText();
+ buildSh.Should().Contain("/.fallout/temp");
+ buildSh.Should().Contain("dotnet fallout");
+ }
+
+ [Fact]
+ public async Task Unrelated_build_scripts_are_left_untouched()
+ {
+ // Arrange
+ var original = "#!/bin/sh\nmake all\n";
+ (tempDirectory / "tools" / "build.sh").WriteAllText(original, eofLineBreak: false);
+
+ // Act
+ await new RewriteBootstrapScriptsStep().ExecuteAsync(context, summary);
+ await new CleanupBootstrapScriptsStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.EditCount.Should().Be(0);
+ (tempDirectory / "tools" / "build.sh").ReadAllText().Should().Be(original);
+ }
+
+ [Fact]
+ public async Task Enterprise_feed_block_in_a_subdirectory_script_is_removed()
+ {
+ // Arrange
+ (tempDirectory / "eng" / "build.ps1").WriteAllText("""
+ if (Test-Path env:NUKE_ENTERPRISE_TOKEN) {
+ & $env:DOTNET_EXE nuget add source "https://f.feedz.io/nuke/enterprise/nuget" --name "nuke-enterprise" --username "PAT" --password $env:NUKE_ENTERPRISE_TOKEN > $null
+ }
+
+ ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }
+ """, eofLineBreak: false);
+
+ // Act
+ await new CleanupBootstrapScriptsStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.EditCount.Should().Be(1);
+ (tempDirectory / "eng" / "build.ps1").ReadAllText().Should().Be(
+ "ExecSafe { & $env:DOTNET_EXE run --project $BuildProjectFile --no-build -- $BuildArguments }");
+ }
+
+ [Fact]
+ public async Task Enterprise_token_without_a_closing_block_is_left_alone()
+ {
+ // Arrange
+ var original = "export NUKE_ENTERPRISE_TOKEN=\"$1\"\ndotnet nuke \"$@\"";
+ (tempDirectory / "build.sh").WriteAllText(original, eofLineBreak: false);
+
+ // Act
+ await new CleanupBootstrapScriptsStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.EditCount.Should().Be(0);
+ (tempDirectory / "build.sh").ReadAllText().Should().Be(original);
+ }
+
+ [Fact]
+ public async Task Enterprise_token_outside_an_if_line_is_left_alone()
+ {
+ // Arrange
+ var original = "export NUKE_ENTERPRISE_TOKEN=\"$1\"\necho \"start\"\nif [ -f x ]; then echo y; fi\ndotnet nuke \"$@\"";
+ (tempDirectory / "build.sh").WriteAllText(original, eofLineBreak: false);
+
+ // Act
+ await new CleanupBootstrapScriptsStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.EditCount.Should().Be(0);
+ (tempDirectory / "build.sh").ReadAllText().Should().Be(original);
+ }
}
diff --git a/tests/Fallout.Migrate.Specs/RewriteCsFilesStepSpecs.cs b/tests/Fallout.Migrate.Specs/RewriteCsFilesStepSpecs.cs
index 8cc688983..86e56c0f5 100644
--- a/tests/Fallout.Migrate.Specs/RewriteCsFilesStepSpecs.cs
+++ b/tests/Fallout.Migrate.Specs/RewriteCsFilesStepSpecs.cs
@@ -1,5 +1,7 @@
using System;
using System.IO;
+using System.Linq;
+using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Fallout.Common.IO;
using Fallout.Migrate.Common;
@@ -157,4 +159,62 @@ public async Task ProjectModel_as_part_of_another_identifier_is_not_truncated()
var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim();
buildCs.Should().Be("using Fallout.Common.ProjectModelFoo;");
}
+
+ [Fact]
+ public async Task IHaz_component_interfaces_are_renamed_to_IHas()
+ {
+ // Arrange
+ (tempDirectory / "Build.cs").WriteAllText("""
+ interface IBuildPaths : IHazSolution, IHazArtifacts;
+ var solution = From().Solution;
+ """);
+
+ // Act
+ await new RewriteCsFilesStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.EditCount.Should().Be(3);
+ var buildCs = (tempDirectory / "Build.cs").ReadAllText();
+ buildCs.Should().Contain("IBuildPaths : IHasSolution, IHasArtifacts;");
+ buildCs.Should().Contain("From()");
+ buildCs.Should().NotContain("IHaz");
+ }
+
+ [Fact]
+ public async Task User_defined_IHaz_interfaces_keep_their_name()
+ {
+ // Arrange
+ (tempDirectory / "Build.cs").WriteAllText("interface IHazSourcePaths : IHazSolution;");
+
+ // Act
+ await new RewriteCsFilesStep().ExecuteAsync(context, summary);
+
+ // Assert
+ summary.EditCount.Should().Be(1);
+ var buildCs = (tempDirectory / "Build.cs").ReadAllText().Trim();
+ buildCs.Should().Be("interface IHazSourcePaths : IHasSolution;");
+ }
+
+ [Fact]
+ public async Task Every_IHaz_alias_in_the_Nuke_Components_shim_is_renamed()
+ {
+ // The rename list is hard-coded. This spec fails when the shim gains an IHaz* alias
+ // that the list doesn't have.
+ // Arrange
+ var shimDirectory = AbsolutePath.Create(AppContext.BaseDirectory)
+ .FindParentOrSelf(x => (x / "fallout.slnx").FileExists()) / "src" / "Shims" / "Nuke.Components";
+ var aliases = shimDirectory.GetFiles("*.cs")
+ .SelectMany(x => Regex.Matches(x.ReadAllText(), @"\binterface\s+(?IHaz\w+)"))
+ .Select(x => x.Groups["name"].Value)
+ .ToList();
+ (tempDirectory / "Build.cs").WriteAllText(string.Join("\n", aliases), eofLineBreak: false);
+
+ // Act
+ await new RewriteCsFilesStep().ExecuteAsync(context, summary);
+
+ // Assert
+ aliases.Should().NotBeEmpty();
+ (tempDirectory / "Build.cs").ReadAllText()
+ .Should().Be(string.Join("\n", aliases.Select(x => "IHas" + x["IHaz".Length..])));
+ }
}