diff --git a/.fallout/build.schema.json b/.fallout/build.schema.json index 47c6a4f95..34edd2473 100644 --- a/.fallout/build.schema.json +++ b/.fallout/build.schema.json @@ -41,7 +41,8 @@ "RunTargetInDockerImageTest", "Test", "UpdateContributors", - "UpdateStargazers" + "UpdateStargazers", + "VerifyGeneratedTools" ] }, "Verbosity": { diff --git a/.github/workflows/ubuntu-latest.yml b/.github/workflows/ubuntu-latest.yml index 45f548d6e..ef2e3ec0b 100644 --- a/.github/workflows/ubuntu-latest.yml +++ b/.github/workflows/ubuntu-latest.yml @@ -55,5 +55,5 @@ jobs: global-json-file: global.json - name: 'Restore: dotnet tools' run: dotnet tool restore - - name: 'Run: Test, Pack' - run: dotnet fallout Test Pack + - name: 'Run: VerifyGeneratedTools, Test, Pack' + run: dotnet fallout VerifyGeneratedTools Test Pack diff --git a/build/Build.CI.GitHubActions.cs b/build/Build.CI.GitHubActions.cs index 6a272c419..090c5fe7b 100644 --- a/build/Build.CI.GitHubActions.cs +++ b/build/Build.CI.GitHubActions.cs @@ -51,7 +51,7 @@ // branch — all are long-lived and protected; all require the ubuntu-latest check. OnPullRequestBranches = new[] { ExperimentalBranch, MainBranch, ReleaseBranchPattern, SupportBranchPattern }, OnPullRequestExcludePaths = new[] { "docs/**", ".assets/**", "**/*.md" }, - InvokedTargets = new[] { nameof(ITest.Test), nameof(IPack.Pack) }, + InvokedTargets = new[] { nameof(VerifyGeneratedTools), nameof(ITest.Test), nameof(IPack.Pack) }, PublishArtifacts = false)] partial class Build { diff --git a/build/Build.CodeGeneration.cs b/build/Build.CodeGeneration.cs index f8ab28165..29aa95616 100644 --- a/build/Build.CodeGeneration.cs +++ b/build/Build.CodeGeneration.cs @@ -22,12 +22,29 @@ partial class Build }); Target GenerateTools => _ => _ + .Executes(() => GenerateAllTools()); + + // CI gate: `GenerateTools` only runs when a contributor remembers to invoke it, so a .json + // spec edited without regenerating its .Generated.cs could merge silently and ship stale + // wrapper code. Regenerating from a known-clean checkout and asserting the working copy is + // still clean afterward catches that drift before merge. + Target VerifyGeneratedTools => _ => _ + .Requires(() => GitHasCleanWorkingCopy()) .Executes(() => { - SpecificationsDirectory.GlobFiles("*/*.json").ForEach(x => - GenerateCode( - x, - namespaceProvider: x => $"Fallout.Common.Tools.{x.Name}", - sourceFileProvider: x => GitRepository.SetBranch(MainBranch).GetGitHubBrowseUrl(x.SpecificationFile))); + GenerateAllTools(); + + Assert.True( + GitHasCleanWorkingCopy(), + "Generated tool wrappers are out of sync with their .json specs. Run './build.ps1 GenerateTools' locally and commit the result."); }); + + void GenerateAllTools() + { + SpecificationsDirectory.GlobFiles("*/*.json").ForEach(x => + GenerateCode( + x, + namespaceProvider: x => $"Fallout.Common.Tools.{x.Name}", + sourceFileProvider: x => GitRepository.SetBranch(MainBranch).GetGitHubBrowseUrl(x.SpecificationFile))); + } }