Skip to content
Open
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
23 changes: 19 additions & 4 deletions src/Fallout.Migrate/Steps/CleanupBootstrapScriptsStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand All @@ -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);
}
Expand Down Expand Up @@ -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)
Comment thread
ANcpLua marked this conversation as resolved.
{
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() == "")
{
Expand Down
39 changes: 38 additions & 1 deletion src/Fallout.Migrate/Steps/RenameNukeDirectoryStep.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.IO;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Fallout.Common.IO;
using Fallout.Migrate.Common;
Expand All @@ -7,16 +8,35 @@ namespace Fallout.Migrate.Steps;

/// <summary>
/// Renames the repository's <c>.nuke/</c> directory to <c>.fallout/</c>, or records a warning if
/// both already exist and need a manual merge.
/// both already exist and need a manual merge. The <c>build.schema.json</c> inside is rewritten
/// first, so the moved file names its base definition <c>FalloutBuild</c>.
/// </summary>
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 =
Comment thread
ANcpLua marked this conversation as resolved.
new(@"(?<=""|#/definitions/)NukeBuild(?="")", RegexOptions.Compiled);

/// <inheritdoc />
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;
Expand All @@ -43,4 +63,21 @@ public Task ExecuteAsync(MigrationContext context, Summary summary)
summary.DirectoriesRenamed++;
return Task.CompletedTask;
}

/// <summary>
/// Renames the <c>NukeBuild</c> definition and its <c>$ref</c> in <paramref name="original"/>.
/// </summary>
/// <param name="original">The original <c>build.schema.json</c> content.</param>
/// <returns>The rewritten content and the number of edits made.</returns>
private static RewriteResult RewriteSchema(string original)
{
var edits = 0;
var content = nukeBuildDefinition.Replace(original, _ =>
{
edits++;
return "FalloutBuild";
});

return new RewriteResult(content, edits);
}
}
6 changes: 3 additions & 3 deletions src/Fallout.Migrate/Steps/RewriteBootstrapScriptsStep.cs
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using Fallout.Common.IO;
using Fallout.Migrate.Common;

namespace Fallout.Migrate.Steps;
Expand Down Expand Up @@ -33,15 +32,16 @@ private static readonly (Regex Pattern, string Replacement)[] patterns =
/// <inheritdoc />
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",
"build.ps1",
"build.sh"
})
{
var path = context.RootDirectory / name;
if (path.FileExists())
foreach (var path in MigrationFileOperations.EnumerateFiles(context.RootDirectory, name))
Comment thread
ANcpLua marked this conversation as resolved.
{
MigrationFileOperations.ApplyRewrite(context, path, Rewrite, summary);
}
Expand Down
17 changes: 16 additions & 1 deletion src/Fallout.Migrate/Steps/RewriteCsFilesStep.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,8 @@ namespace Fallout.Migrate.Steps;
/// <summary>
/// Rewrites every <c>*.cs</c> file under the repository root: <c>Nuke.*</c> namespace prefixes become
/// <c>Fallout.</c>, the bare <c>NukeBuild</c>/<c>INukeBuild</c> types become
/// <c>FalloutBuild</c>/<c>IFalloutBuild</c>, and the solution-model namespace (which moved out of
/// <c>FalloutBuild</c>/<c>IFalloutBuild</c>, the <c>IHaz*</c> component interfaces become
/// <c>IHas*</c>, and the solution-model namespace (which moved out of
/// <c>*.Common.ProjectModel</c> in v11) becomes <c>Fallout.Solutions</c>.
/// </summary>
internal sealed class RewriteCsFilesStep : IMigrationStep
Expand All @@ -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(
Comment thread
ANcpLua marked this conversation as resolved.
@"\bIHaz(?=(?:Artifacts|Changelog|Configuration|GitRepository|GitVersion|NerdbankGitVersioning|Reports|Solution|TwitterCredentials)\b)",
RegexOptions.Compiled);

/// <inheritdoc />
public Task ExecuteAsync(MigrationContext context, Summary summary)
{
Expand Down Expand Up @@ -77,6 +86,12 @@ private static RewriteResult Rewrite(string original)
return "FalloutBuild";
});

content = iHazComponentType.Replace(content, _ =>
{
edits++;
return "IHas";
});

return new RewriteResult(content, edits);
}
}
109 changes: 109 additions & 0 deletions tests/Fallout.Migrate.Specs/RenameNukeDirectoryStepSpecs.cs
Original file line number Diff line number Diff line change
@@ -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();
}
}
86 changes: 86 additions & 0 deletions tests/Fallout.Migrate.Specs/RewriteBootstrapScriptsStepSpecs.cs
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
}
Loading
Loading