From 57fbe2bce54ae56589f768fe6015fba2ad4d9363 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sun, 19 Jul 2026 00:02:30 +1200 Subject: [PATCH 1/3] Refactor build steps into component interfaces MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Replace the single Build.Maintenance.cs partial with one component interface per step under build/Steps/, mirroring the migrator's "each unit is its own type" shape but on the framework's native grain (like IRestore/ICompile): - IGenerateTools (References, GenerateTools), IGeneratePublicApi, IDownloadLicenses, IHandleExternalRepositories, IUpdateContributors, IUpdateStargazers. - Build.cs implements them — its base list is the "wire up the steps" seam. - Each target stays CLI-invocable and graph-aware: Pack still gains DownloadLicenses via DependentFor; UpdateContributors inherits IHandleExternalRepositories for the external-repo directory. - UseHttps moves from a readonly [Parameter] field to a [Parameter] property (the component idiom); MainBranch stays a const on Build (it feeds a [GitHubActions] attribute), so IGenerateTools carries its own ToolsSourceBranch. No behaviour change: dotnet fallout --help lists every target as before. Co-Authored-By: Claude Opus 4.8 (1M context) --- build/Build.Maintenance.cs | 270 --------------------- build/Build.cs | 9 +- build/Steps/IDownloadLicenses.cs | 45 ++++ build/Steps/IGeneratePublicApi.cs | 89 +++++++ build/Steps/IGenerateTools.cs | 39 +++ build/Steps/IHandleExternalRepositories.cs | 51 ++++ build/Steps/IUpdateContributors.cs | 47 ++++ build/Steps/IUpdateStargazers.cs | 41 ++++ 8 files changed, 320 insertions(+), 271 deletions(-) delete mode 100644 build/Build.Maintenance.cs create mode 100644 build/Steps/IDownloadLicenses.cs create mode 100644 build/Steps/IGeneratePublicApi.cs create mode 100644 build/Steps/IGenerateTools.cs create mode 100644 build/Steps/IHandleExternalRepositories.cs create mode 100644 build/Steps/IUpdateContributors.cs create mode 100644 build/Steps/IUpdateStargazers.cs diff --git a/build/Build.Maintenance.cs b/build/Build.Maintenance.cs deleted file mode 100644 index 53f9d0309..000000000 --- a/build/Build.Maintenance.cs +++ /dev/null @@ -1,270 +0,0 @@ -using System; -using System.Collections.Generic; -using System.IO; -using System.Linq; -using System.Reflection; -using System.Text; -using System.Threading.Tasks; -using Fallout.Common; -using Fallout.Common.Git; -using Fallout.Common.IO; -using Fallout.Common.Tools.GitHub; -using Fallout.Common.Utilities; -using Fallout.Common.Utilities.Collections; -using Fallout.Components; -using Fallout.Solutions; -using Fallout.Utilities.Text.Yaml; -using Serilog; -using static Fallout.CodeGeneration.CodeGenerator; -using static Fallout.CodeGeneration.ReferenceUpdater; -using static Fallout.Common.ControlFlow; -using static Fallout.Common.IO.HttpTasks; -using static Fallout.Common.Tools.Git.GitTasks; - -// Out-of-band maintenance/generation targets: not part of the default -// Restore -> Compile -> Test -> Pack -> Publish pipeline. Consolidated here from -// the former per-target partials (Build.CodeGeneration / .PublicApi / .Licenses / -// .Contributors / .Stargazers / .GlobalSolution) to keep the build project's file -// count down. The attribute-bearing partials (Build.CI.GitHubActions, Build.Terminal) -// stay separate because class-level attributes can't live on an interface or be merged. -partial class Build -{ - // --- Tool wrappers & CLI reference generation ------------------------------- - - AbsolutePath SpecificationsDirectory => RootDirectory / "src" / "Fallout.Common" / "Tools"; - AbsolutePath ReferencesDirectory => RootDirectory / "docs" / "cli-tools"; - - Target References => _ => _ - .Requires(() => GitHasCleanWorkingCopy()) - .Executes(() => - { - ReferencesDirectory.CreateOrCleanDirectory(); - - UpdateReferences(SpecificationsDirectory, ReferencesDirectory); - }); - - Target GenerateTools => _ => _ - .Executes(() => - { - SpecificationsDirectory.GlobFiles("*/*.json").ForEach(x => - GenerateCode( - x, - namespaceProvider: x => $"Fallout.Common.Tools.{x.Name}", - sourceFileProvider: x => GitRepository.SetBranch(MainBranch).GetGitHubBrowseUrl(x.SpecificationFile))); - }); - - // --- Public API surface dump ------------------------------------------------ - - AbsolutePath PublicApiFile => RootDirectory / "PUBLIC_API.md"; - - Target GeneratePublicApi => _ => _ - .Executes(() => - { - var types = typeof(FalloutBuild).Assembly - .GetTypes() - .SelectMany(x => x.DescendantsAndSelf(y => y.GetNestedTypes())) - .Where(x => x.IsPublic || x.IsNestedPublic) - .Distinct() - .OrderBy(x => x.FullName).ToList(); - - var builder = new StringBuilder(); - - builder - .AppendLine("# Public API") - .AppendLine() - .AppendLine("## Namespaces & Types") - .AppendLine(); - - var groups = types.GroupBy(x => x.Namespace); - - foreach (var group in groups) - { - builder.AppendLine($"### {group.Key}"); - builder.AppendLine(); - group.ForEach(x => builder.AppendLine($"- {x.GetDisplayName()}")); - builder.AppendLine(); - } - - builder - .AppendLine("## Types & Methods") - .AppendLine(); - - foreach (var type in types) - { - builder - .AppendLine($"### {type.Namespace}.{type.GetDisplayName()}") - .AppendLine(); - - var memberInfos = type - .GetMembers(ReflectionUtility.All | BindingFlags.DeclaredOnly); - - bool DefaultFilter(MemberInfo member) - { - if (member is PropertyInfo) - return false; - - if (member is Type && !member.IsPublic()) - return false; - - if (!(member.IsPublic() || member.IsFamily() && !member.DeclaringType.NotNull().IsSealed)) - return false; - - if (member is FieldInfo { IsSpecialName: true }) - return false; - - return true; - } - - var members = memberInfos - .Where(DefaultFilter) - .OrderByDescending(x => x is FieldInfo) - .ThenByDescending(x => x is ConstructorInfo) - .ThenByDescending(x => x is MethodInfo) - .ThenByDescending(x => x.Name.StartsWith("get_") || x.Name.StartsWith("set_")) - .ThenBy(x => x.Name); - - foreach (var member in members) - builder.AppendLine($"- {member.GetDisplayText()}"); - - builder.AppendLine(); - } - - PublicApiFile.WriteAllText(builder.ToString()); - }); - - // --- Third-party license bundling (part of Pack) ---------------------------- - - AbsolutePath LicensesDirectory => TemporaryDirectory / "licenses"; - - IEnumerable<(string Project, string Url)> Licenses - => new[] - { - ("Glob", "https://raw.githubusercontent.com/kthompson/glob/develop/LICENSE"), - ("ICSharpCode.SharpZipLib", "https://raw.githubusercontent.com/icsharpcode/SharpZipLib/master/LICENSE.txt"), - ("Microsoft.Build", "https://raw.githubusercontent.com/dotnet/msbuild/main/LICENSE"), - ("Microsoft.CodeAnalysis", "https://raw.githubusercontent.com/dotnet/roslyn/main/License.txt"), - ("Newtonsoft.Json", "https://raw.githubusercontent.com/JamesNK/Newtonsoft.Json/master/LICENSE.md"), - ("NuGet", "https://raw.githubusercontent.com/NuGet/NuGet.Client/dev/LICENSE.txt"), - ("Octokit", "https://raw.githubusercontent.com/octokit/octokit.net/main/LICENSE.txt"), - ("Serilog", "https://raw.githubusercontent.com/serilog/serilog/dev/LICENSE"), - ("Spectre.Console", "https://raw.githubusercontent.com/spectreconsole/spectre.console/main/LICENSE.md"), - ("YamlDotNet", "https://raw.githubusercontent.com/aaubry/YamlDotNet/master/LICENSE.txt") - }; - - Target DownloadLicenses => _ => _ - .After() - .DependentFor() - .Executes(() => - { - LicensesDirectory.CreateOrCleanDirectory(); - - var downloadTasks = Licenses.Select(async x => - { - await HttpDownloadFileAsync(x.Url, LicensesDirectory / $"{x.Project}.txt"); - Log.Information("Downloaded license for {Project}", x.Project); - }); - Task.WaitAll(downloadTasks.ToArray()); - }); - - // --- Contributors & stargazers ---------------------------------------------- - - AbsolutePath ContributorsFile => RootDirectory / "CONTRIBUTORS.md"; - AbsolutePath ContributorsCacheFile => TemporaryDirectory / "contributors.dat"; - - Target UpdateContributors => _ => _ - .Executes(() => - { - var previousContributors = ContributorsCacheFile.Existing()?.ReadAllLines() ?? []; - - var repositoryDirectories = new[] { RootDirectory / ".git" } - .Concat(ExternalRepositoriesDirectory.GlobDirectories("*/.git")); - var contributors = repositoryDirectories - .SelectMany(x => Git(@"log --pretty=""%an|%ae%n%cn|%ce""", workingDirectory: x, logOutput: false)) - .Select(x => x.Text) - .Distinct().ToList() - .Select(x => x.Split('|')) - .ForEachLazy(x => Assert.Count(x, length: 2)) - .Select(x => new { Name = x[0], Email = x[1] }).ToList(); - - var newContributors = contributors.Where(x => !previousContributors.Contains(x.Email)); - - foreach (var newContributor in newContributors) - { - var content = (ContributorsFile.Existing()?.ReadAllLines() ?? []) - .Concat($"- {newContributor.Name}").OrderBy(x => x); - ContributorsFile.WriteAllLines(content, Encoding.Default); - Git($"add {ContributorsFile}"); - - var message = $"Add {newContributor.Name} as contributor".DoubleQuote(); - var author = $"{newContributor.Name} <{newContributor.Email}>".DoubleQuote(); - Git($"commit -m {message} --author {author}"); - } - - ContributorsCacheFile.WriteAllLines(contributors.Select(x => x.Email).ToList()); - }); - - AbsolutePath StargazersFile => TemporaryDirectory / "stargazers.csv"; - - Target UpdateStargazers => _ => _ - .Executes(async () => - { - var stargazerUsers = await GitHubTasks.GitHubClient.Activity.Starring.GetAllStargazers( - GitRepository.GetGitHubOwner(), - GitRepository.GetGitHubName()); - var stargazerEntries = stargazerUsers.Select(async x => - { - var user = await GitHubTasks.GitHubClient.User.Get(x.Login); - return new[] - { - user.Login.DoubleQuote(), - user.Name.DoubleQuote(), - user.Company.DoubleQuote(), - user.Location.DoubleQuote(), - user.Email.DoubleQuote(), - user.Blog.DoubleQuote() - }; - }).ToList(); - - await Task.WhenAll(stargazerEntries); - - StargazersFile.WriteAllLines( - new[] { new[] { "Login", "Name", "Company", "Location", "Email", "Blog" } } - .Concat(stargazerEntries.Select(x => x.Result).OrderBy(x => x.First())) - .Select(x => x.JoinComma())); - }); - - // --- External repositories / global solution -------------------------------- - - [Parameter] readonly bool UseHttps; - - AbsolutePath GlobalSolution => RootDirectory / "fallout-global.sln"; - AbsolutePath ExternalRepositoriesDirectory => RootDirectory / "external"; - AbsolutePath ExternalRepositoriesFile => ExternalRepositoriesDirectory / "repositories.yml"; - - IEnumerable ExternalSolutions - => ExternalRepositories - .Select(x => ExternalRepositoriesDirectory / x.GetGitHubName()) - .Select(x => x.GlobFiles("*.sln").Single()) - .Select(x => x.ReadSolution()); - - IEnumerable ExternalRepositories - => ExternalRepositoriesFile.ReadYaml().Select(x => GitRepository.FromUrl(x)); - - Target CheckoutExternalRepositories => _ => _ - .Executes(() => - { - foreach (var repository in ExternalRepositories) - { - var repositoryDirectory = ExternalRepositoriesDirectory / repository.GetGitHubName(); - var origin = UseHttps ? repository.HttpsUrl : repository.SshUrl; - - if (!Directory.Exists(repositoryDirectory)) - Git($"clone {origin} {repositoryDirectory} --progress"); - else - { - SuppressErrors(() => Git($"remote add origin {origin}", repositoryDirectory)); - Git($"remote set-url origin {origin}", repositoryDirectory); - } - } - }); -} diff --git a/build/Build.cs b/build/Build.cs index 227c06f03..04fbb10d5 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -30,7 +30,14 @@ partial class Build ITest, IReportCoverage, IPublish, - ICreateGitHubRelease + ICreateGitHubRelease, + // Build-local steps (build/Steps/) — each concern is its own component. + IGenerateTools, + IGeneratePublicApi, + IDownloadLicenses, + IHandleExternalRepositories, + IUpdateContributors, + IUpdateStargazers { public static int Main() => Execute(x => ((IPack)x).Pack); diff --git a/build/Steps/IDownloadLicenses.cs b/build/Steps/IDownloadLicenses.cs new file mode 100644 index 000000000..dee4eeed9 --- /dev/null +++ b/build/Steps/IDownloadLicenses.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Fallout.Common; +using Fallout.Common.IO; +using Fallout.Components; +using Serilog; +using static Fallout.Common.IO.HttpTasks; + +// Step: fetch the third-party licenses bundled into the packages. Hooks into Pack via +// DependentFor so it runs as part of the default pipeline, not just on demand. +interface IDownloadLicenses : IFalloutBuild +{ + AbsolutePath LicensesDirectory => TemporaryDirectory / "licenses"; + + IEnumerable<(string Project, string Url)> Licenses + => new[] + { + ("Glob", "https://raw.githubusercontent.com/kthompson/glob/develop/LICENSE"), + ("ICSharpCode.SharpZipLib", "https://raw.githubusercontent.com/icsharpcode/SharpZipLib/master/LICENSE.txt"), + ("Microsoft.Build", "https://raw.githubusercontent.com/dotnet/msbuild/main/LICENSE"), + ("Microsoft.CodeAnalysis", "https://raw.githubusercontent.com/dotnet/roslyn/main/License.txt"), + ("Newtonsoft.Json", "https://raw.githubusercontent.com/JamesNK/Newtonsoft.Json/master/LICENSE.md"), + ("NuGet", "https://raw.githubusercontent.com/NuGet/NuGet.Client/dev/LICENSE.txt"), + ("Octokit", "https://raw.githubusercontent.com/octokit/octokit.net/main/LICENSE.txt"), + ("Serilog", "https://raw.githubusercontent.com/serilog/serilog/dev/LICENSE"), + ("Spectre.Console", "https://raw.githubusercontent.com/spectreconsole/spectre.console/main/LICENSE.md"), + ("YamlDotNet", "https://raw.githubusercontent.com/aaubry/YamlDotNet/master/LICENSE.txt") + }; + + Target DownloadLicenses => _ => _ + .After() + .DependentFor() + .Executes(() => + { + LicensesDirectory.CreateOrCleanDirectory(); + + var downloadTasks = Licenses.Select(async x => + { + await HttpDownloadFileAsync(x.Url, LicensesDirectory / $"{x.Project}.txt"); + Log.Information("Downloaded license for {Project}", x.Project); + }); + Task.WaitAll(downloadTasks.ToArray()); + }); +} diff --git a/build/Steps/IGeneratePublicApi.cs b/build/Steps/IGeneratePublicApi.cs new file mode 100644 index 000000000..ca9295d14 --- /dev/null +++ b/build/Steps/IGeneratePublicApi.cs @@ -0,0 +1,89 @@ +using System; +using System.Linq; +using System.Reflection; +using System.Text; +using Fallout.Common; +using Fallout.Common.IO; +using Fallout.Common.Utilities; +using Fallout.Common.Utilities.Collections; + +// Step: dump the framework's public API surface to PUBLIC_API.md. +interface IGeneratePublicApi : IFalloutBuild +{ + AbsolutePath PublicApiFile => RootDirectory / "PUBLIC_API.md"; + + Target GeneratePublicApi => _ => _ + .Executes(() => + { + var types = typeof(FalloutBuild).Assembly + .GetTypes() + .SelectMany(x => x.DescendantsAndSelf(y => y.GetNestedTypes())) + .Where(x => x.IsPublic || x.IsNestedPublic) + .Distinct() + .OrderBy(x => x.FullName).ToList(); + + var builder = new StringBuilder(); + + builder + .AppendLine("# Public API") + .AppendLine() + .AppendLine("## Namespaces & Types") + .AppendLine(); + + var groups = types.GroupBy(x => x.Namespace); + + foreach (var group in groups) + { + builder.AppendLine($"### {group.Key}"); + builder.AppendLine(); + group.ForEach(x => builder.AppendLine($"- {x.GetDisplayName()}")); + builder.AppendLine(); + } + + builder + .AppendLine("## Types & Methods") + .AppendLine(); + + foreach (var type in types) + { + builder + .AppendLine($"### {type.Namespace}.{type.GetDisplayName()}") + .AppendLine(); + + var memberInfos = type + .GetMembers(ReflectionUtility.All | BindingFlags.DeclaredOnly); + + bool DefaultFilter(MemberInfo member) + { + if (member is PropertyInfo) + return false; + + if (member is Type && !member.IsPublic()) + return false; + + if (!(member.IsPublic() || member.IsFamily() && !member.DeclaringType.NotNull().IsSealed)) + return false; + + if (member is FieldInfo { IsSpecialName: true }) + return false; + + return true; + } + + var members = memberInfos + .Where(DefaultFilter) + .OrderByDescending(x => x is FieldInfo) + .ThenByDescending(x => x is ConstructorInfo) + .ThenByDescending(x => x is MethodInfo) + .ThenByDescending(x => x.Name.StartsWith("get_") || x.Name.StartsWith("set_")) + .ThenBy(x => x.Name); + + foreach (var member in members) + builder.AppendLine($"- {member.GetDisplayText()}"); + + builder.AppendLine(); + } + + PublicApiFile.WriteAllText(builder.ToString()); + }); +} diff --git a/build/Steps/IGenerateTools.cs b/build/Steps/IGenerateTools.cs new file mode 100644 index 000000000..9ece9aefe --- /dev/null +++ b/build/Steps/IGenerateTools.cs @@ -0,0 +1,39 @@ +using System; +using Fallout.Common; +using Fallout.Common.IO; +using Fallout.Common.Tools.GitHub; +using Fallout.Common.Utilities.Collections; +using Fallout.Components; +using static Fallout.CodeGeneration.CodeGenerator; +using static Fallout.CodeGeneration.ReferenceUpdater; +using static Fallout.Common.Tools.Git.GitTasks; + +// Step: (re)generate the tool wrappers from their JSON specs and the CLI reference docs. +interface IGenerateTools : IFalloutBuild, IHasGitRepository +{ + AbsolutePath SpecificationsDirectory => RootDirectory / "src" / "Fallout.Common" / "Tools"; + AbsolutePath ReferencesDirectory => RootDirectory / "docs" / "cli-tools"; + + // Branch that generated source links point at. Mirrors Build.MainBranch, which must + // stay a const there (it feeds a [GitHubActions] attribute and so can't be shared here). + string ToolsSourceBranch => "main"; + + Target References => _ => _ + .Requires(() => GitHasCleanWorkingCopy()) + .Executes(() => + { + ReferencesDirectory.CreateOrCleanDirectory(); + + UpdateReferences(SpecificationsDirectory, ReferencesDirectory); + }); + + Target GenerateTools => _ => _ + .Executes(() => + { + SpecificationsDirectory.GlobFiles("*/*.json").ForEach(x => + GenerateCode( + x, + namespaceProvider: x => $"Fallout.Common.Tools.{x.Name}", + sourceFileProvider: x => GitRepository.SetBranch(ToolsSourceBranch).GetGitHubBrowseUrl(x.SpecificationFile))); + }); +} diff --git a/build/Steps/IHandleExternalRepositories.cs b/build/Steps/IHandleExternalRepositories.cs new file mode 100644 index 000000000..005860337 --- /dev/null +++ b/build/Steps/IHandleExternalRepositories.cs @@ -0,0 +1,51 @@ +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Fallout.Common; +using Fallout.Common.Git; +using Fallout.Common.IO; +using Fallout.Common.Tools.GitHub; +using Fallout.Common.Utilities; +using Fallout.Solutions; +using Fallout.Utilities.Text.Yaml; +using static Fallout.Common.ControlFlow; +using static Fallout.Common.Tools.Git.GitTasks; + +// Step: check out the companion repositories declared in external/repositories.yml and +// expose their solutions. Depended on by IUpdateContributors, which mines their git log. +interface IHandleExternalRepositories : IFalloutBuild +{ + [Parameter] bool UseHttps => TryGetValue(() => UseHttps) ?? false; + + AbsolutePath GlobalSolution => RootDirectory / "fallout-global.sln"; + AbsolutePath ExternalRepositoriesDirectory => RootDirectory / "external"; + AbsolutePath ExternalRepositoriesFile => ExternalRepositoriesDirectory / "repositories.yml"; + + IEnumerable ExternalSolutions + => ExternalRepositories + .Select(x => ExternalRepositoriesDirectory / x.GetGitHubName()) + .Select(x => x.GlobFiles("*.sln").Single()) + .Select(x => x.ReadSolution()); + + IEnumerable ExternalRepositories + => ExternalRepositoriesFile.ReadYaml().Select(x => GitRepository.FromUrl(x)); + + Target CheckoutExternalRepositories => _ => _ + .Executes(() => + { + foreach (var repository in ExternalRepositories) + { + var repositoryDirectory = ExternalRepositoriesDirectory / repository.GetGitHubName(); + var origin = UseHttps ? repository.HttpsUrl : repository.SshUrl; + + if (!Directory.Exists(repositoryDirectory)) + Git($"clone {origin} {repositoryDirectory} --progress"); + else + { + SuppressErrors(() => Git($"remote add origin {origin}", repositoryDirectory)); + Git($"remote set-url origin {origin}", repositoryDirectory); + } + } + }); +} diff --git a/build/Steps/IUpdateContributors.cs b/build/Steps/IUpdateContributors.cs new file mode 100644 index 000000000..c0e76c5cb --- /dev/null +++ b/build/Steps/IUpdateContributors.cs @@ -0,0 +1,47 @@ +using System.Linq; +using System.Text; +using Fallout.Common; +using Fallout.Common.IO; +using Fallout.Common.Utilities; +using Fallout.Common.Utilities.Collections; +using static Fallout.Common.Tools.Git.GitTasks; + +// Step: append newly-seen authors from this repo (and the external companions) to +// CONTRIBUTORS.md. Inherits IHandleExternalRepositories for ExternalRepositoriesDirectory. +interface IUpdateContributors : IFalloutBuild, IHandleExternalRepositories +{ + AbsolutePath ContributorsFile => RootDirectory / "CONTRIBUTORS.md"; + AbsolutePath ContributorsCacheFile => TemporaryDirectory / "contributors.dat"; + + Target UpdateContributors => _ => _ + .Executes(() => + { + var previousContributors = ContributorsCacheFile.Existing()?.ReadAllLines() ?? []; + + var repositoryDirectories = new[] { RootDirectory / ".git" } + .Concat(ExternalRepositoriesDirectory.GlobDirectories("*/.git")); + var contributors = repositoryDirectories + .SelectMany(x => Git(@"log --pretty=""%an|%ae%n%cn|%ce""", workingDirectory: x, logOutput: false)) + .Select(x => x.Text) + .Distinct().ToList() + .Select(x => x.Split('|')) + .ForEachLazy(x => Assert.Count(x, length: 2)) + .Select(x => new { Name = x[0], Email = x[1] }).ToList(); + + var newContributors = contributors.Where(x => !previousContributors.Contains(x.Email)); + + foreach (var newContributor in newContributors) + { + var content = (ContributorsFile.Existing()?.ReadAllLines() ?? []) + .Concat($"- {newContributor.Name}").OrderBy(x => x); + ContributorsFile.WriteAllLines(content, Encoding.Default); + Git($"add {ContributorsFile}"); + + var message = $"Add {newContributor.Name} as contributor".DoubleQuote(); + var author = $"{newContributor.Name} <{newContributor.Email}>".DoubleQuote(); + Git($"commit -m {message} --author {author}"); + } + + ContributorsCacheFile.WriteAllLines(contributors.Select(x => x.Email).ToList()); + }); +} diff --git a/build/Steps/IUpdateStargazers.cs b/build/Steps/IUpdateStargazers.cs new file mode 100644 index 000000000..d4f52d738 --- /dev/null +++ b/build/Steps/IUpdateStargazers.cs @@ -0,0 +1,41 @@ +using System.Linq; +using System.Threading.Tasks; +using Fallout.Common; +using Fallout.Common.IO; +using Fallout.Common.Tools.GitHub; +using Fallout.Common.Utilities; +using Fallout.Components; + +// Step: snapshot the repository's stargazers to a CSV in the temp directory. +interface IUpdateStargazers : IFalloutBuild, IHasGitRepository +{ + AbsolutePath StargazersFile => TemporaryDirectory / "stargazers.csv"; + + Target UpdateStargazers => _ => _ + .Executes(async () => + { + var stargazerUsers = await GitHubTasks.GitHubClient.Activity.Starring.GetAllStargazers( + GitRepository.GetGitHubOwner(), + GitRepository.GetGitHubName()); + var stargazerEntries = stargazerUsers.Select(async x => + { + var user = await GitHubTasks.GitHubClient.User.Get(x.Login); + return new[] + { + user.Login.DoubleQuote(), + user.Name.DoubleQuote(), + user.Company.DoubleQuote(), + user.Location.DoubleQuote(), + user.Email.DoubleQuote(), + user.Blog.DoubleQuote() + }; + }).ToList(); + + await Task.WhenAll(stargazerEntries); + + StargazersFile.WriteAllLines( + new[] { new[] { "Login", "Name", "Company", "Location", "Email", "Blog" } } + .Concat(stargazerEntries.Select(x => x.Result).OrderBy(x => x.First())) + .Select(x => x.JoinComma())); + }); +} From ae3a1fdc890753288d4a096e83870cee519964ac Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sun, 19 Jul 2026 00:03:14 +1200 Subject: [PATCH 2/3] Sync build.schema.json after dropping the Docker demo target Regenerated schema drops the removed RunTargetInDockerImageTest entry so the build's target list matches reality. Co-Authored-By: Claude Opus 4.8 (1M context) --- .fallout/build.schema.json | 1 - 1 file changed, 1 deletion(-) diff --git a/.fallout/build.schema.json b/.fallout/build.schema.json index fde15dc2b..9dc2c0b76 100644 --- a/.fallout/build.schema.json +++ b/.fallout/build.schema.json @@ -38,7 +38,6 @@ "References", "ReportCoverage", "Restore", - "RunTargetInDockerImageTest", "Test", "UpdateContributors", "UpdateStargazers" From a703180f0e23ee7b2498052f19b68e6c94405c48 Mon Sep 17 00:00:00 2001 From: Chrison Simtian Date: Sun, 19 Jul 2026 00:21:40 +1200 Subject: [PATCH 3/3] Drop dead members and inherited template cruft from the build - Remove unused members carried over from upstream NUKE: Build.MilestoneTitle, and GlobalSolution / ExternalSolutions on IHandleExternalRepositories (no references anywhere; they were for a global-solution target this build lacks). - Delete the commented-out example blocks in _build.csproj (PackAsTool, PublishSingleFile, NukeExternalFiles, source-generator scaffolding). Co-Authored-By: Claude Opus 4.8 (1M context) --- build/Build.cs | 1 - build/Steps/IHandleExternalRepositories.cs | 9 -------- build/_build.csproj | 27 ---------------------- 3 files changed, 37 deletions(-) diff --git a/build/Build.cs b/build/Build.cs index 04fbb10d5..c79c808eb 100644 --- a/build/Build.cs +++ b/build/Build.cs @@ -77,7 +77,6 @@ partial class Build string MajorMinorPatchVersion => Major ? $"{ParseMajor(ThisAssembly.AssemblyInformationalVersion) + 1}.0.0" : ThisAssembly.AssemblyInformationalVersion.Split('+')[0]; - string MilestoneTitle => $"v{MajorMinorPatchVersion}"; static int ParseMajor(string informationalVersion) => int.Parse(informationalVersion.Split('.')[0]); diff --git a/build/Steps/IHandleExternalRepositories.cs b/build/Steps/IHandleExternalRepositories.cs index 005860337..b9c00adf3 100644 --- a/build/Steps/IHandleExternalRepositories.cs +++ b/build/Steps/IHandleExternalRepositories.cs @@ -6,8 +6,6 @@ using Fallout.Common.Git; using Fallout.Common.IO; using Fallout.Common.Tools.GitHub; -using Fallout.Common.Utilities; -using Fallout.Solutions; using Fallout.Utilities.Text.Yaml; using static Fallout.Common.ControlFlow; using static Fallout.Common.Tools.Git.GitTasks; @@ -18,16 +16,9 @@ interface IHandleExternalRepositories : IFalloutBuild { [Parameter] bool UseHttps => TryGetValue(() => UseHttps) ?? false; - AbsolutePath GlobalSolution => RootDirectory / "fallout-global.sln"; AbsolutePath ExternalRepositoriesDirectory => RootDirectory / "external"; AbsolutePath ExternalRepositoriesFile => ExternalRepositoriesDirectory / "repositories.yml"; - IEnumerable ExternalSolutions - => ExternalRepositories - .Select(x => ExternalRepositoriesDirectory / x.GetGitHubName()) - .Select(x => x.GlobFiles("*.sln").Single()) - .Select(x => x.ReadSolution()); - IEnumerable ExternalRepositories => ExternalRepositoriesFile.ReadYaml().Select(x => GitRepository.FromUrl(x)); diff --git a/build/_build.csproj b/build/_build.csproj index 0491b6ce2..ca6479beb 100644 --- a/build/_build.csproj +++ b/build/_build.csproj @@ -18,35 +18,8 @@ False $(MSBuildThisFileDirectory)\..\src\Fallout.MSBuildTasks\bin\Debug\net8.0\publish - - - - - - - - - - - - - - - - - - - - - - - - - - - $(Configuration.ToUpper()) $(DefineConstants);WIN