Skip to content
Closed
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
10 changes: 10 additions & 0 deletions .fallout/build.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -148,6 +148,16 @@
"type": "string",
"default": "Secrets must be entered via 'fallout :secrets [profile]'"
},
"NuGetSource": {
"type": "string"
},
"PublishTo": {
"type": "array",
"description": "Publish only to these named targets (default: all configured PublishTargets)",
"items": {
"type": "string"
}
},
"Solution": {
"type": "string",
"description": "Path to a solution file that is automatically loaded"
Expand Down
15 changes: 8 additions & 7 deletions AssemblyInfo.cs
Original file line number Diff line number Diff line change
Expand Up @@ -2,21 +2,22 @@

[assembly: InternalsVisibleTo("Fallout.Build")]
[assembly: InternalsVisibleTo("Fallout.Build.Shared")]
[assembly: InternalsVisibleTo("Fallout.Build.Tests")]
[assembly: InternalsVisibleTo("Fallout.Build.Specs")]
[assembly: InternalsVisibleTo("Fallout.Common")]
[assembly: InternalsVisibleTo("Fallout.Common.Tests")]
[assembly: InternalsVisibleTo("Fallout.Common.Specs")]
[assembly: InternalsVisibleTo("Fallout.Cli")]
[assembly: InternalsVisibleTo("Fallout.Cli.Tests")]
[assembly: InternalsVisibleTo("Fallout.ProjectModel.Tests")]
[assembly: InternalsVisibleTo("Fallout.Cli.Specs")]
[assembly: InternalsVisibleTo("Fallout.ProjectModel.Specs")]
[assembly: InternalsVisibleTo("Fallout.SourceGenerators")]
[assembly: InternalsVisibleTo("Fallout.Solution.Codegen")]
[assembly: InternalsVisibleTo("Fallout.Solution")]
[assembly: InternalsVisibleTo("Fallout.Solution.Tests")]
[assembly: InternalsVisibleTo("Fallout.Solution.Specs")]
[assembly: InternalsVisibleTo("Fallout.Persistence.Solution")]
[assembly: InternalsVisibleTo("Fallout.Persistence.Solution.Tests")]
[assembly: InternalsVisibleTo("Fallout.Tooling")]
[assembly: InternalsVisibleTo("Fallout.Tooling.Tests")]
[assembly: InternalsVisibleTo("Fallout.Tooling.Specs")]
[assembly: InternalsVisibleTo("Fallout.Utilities.IO.Globbing")]
[assembly: InternalsVisibleTo("Fallout.Utilities.Tests")]
[assembly: InternalsVisibleTo("Fallout.Utilities.Specs")]

// External extensions — kept as Nuke.* until those projects rebrand independently.
[assembly: InternalsVisibleTo("Nuke.VisualStudio")]
Expand Down
4 changes: 2 additions & 2 deletions Directory.Build.props
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@
<PackageReadmeFile>README.md</PackageReadmeFile>
</PropertyGroup>

<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('Tests'))">
<PropertyGroup Condition="$(MSBuildProjectName.EndsWith('Tests')) Or $(MSBuildProjectName.EndsWith('Specs'))">
<IsPackable>False</IsPackable>
</PropertyGroup>

Expand Down Expand Up @@ -53,7 +53,7 @@
<PackageReference Include="Nerdbank.GitVersioning" PrivateAssets="All" />
</ItemGroup>

<ItemGroup Condition="$(MSBuildProjectName.EndsWith('Tests'))">
<ItemGroup Condition="$(MSBuildProjectName.EndsWith('Tests')) Or $(MSBuildProjectName.EndsWith('Specs'))">
<PackageReference Include="coverlet.msbuild" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="GitHubActionsTestLogger" />
Expand Down
2 changes: 1 addition & 1 deletion build/Build.cs
Original file line number Diff line number Diff line change
Expand Up @@ -98,7 +98,7 @@ private static int ParseMajor(string informationalVersion)
from framework in project.GetTargetFrameworks()
select (project, framework);

IEnumerable<Fallout.Solutions.Project> ITest.TestProjects => Partition.GetCurrent(Solution.GetAllProjects("*.Tests"));
IEnumerable<Fallout.Solutions.Project> ITest.TestProjects => Partition.GetCurrent(Solution.GetAllProjects("*.Specs"));

[Parameter]
public int TestDegreeOfParallelism { get; } = 1;
Expand Down
9 changes: 9 additions & 0 deletions build/_build.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,14 @@
<IsPackable>false</IsPackable>
</PropertyGroup>

<!-- Dogfood the net10 pre-build [Solution] codegen (the StronglyTypedSolutionGenerator fallback
self-suppresses while this is Build). -->
<PropertyGroup>
<FalloutSolutionCodegenMode>Build</FalloutSolutionCodegenMode>
<FalloutSolutionCodegenProject>$(MSBuildProjectDirectory)\..\src\Fallout.Solution.Codegen\Fallout.Solution.Codegen.csproj</FalloutSolutionCodegenProject>
<FalloutBaseDirectory>$(MSBuildProjectDirectory)\..</FalloutBaseDirectory>
</PropertyGroup>

<!-- Test properties for MSBuild integration -->
<PropertyGroup>
<FalloutTasksEnabled Condition="'$(FalloutTasksEnabled)' == ''">False</FalloutTasksEnabled>
Expand Down Expand Up @@ -72,5 +80,6 @@
</ItemGroup>

<Import Project="..\src\Fallout.Common\Fallout.Common.targets" />
<Import Project="..\src\Fallout.Solution.Codegen\build\Fallout.Solution.Codegen.targets" />

</Project>
2 changes: 2 additions & 0 deletions docs/agents/conventions.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,8 @@ Three groups: conventions to respect, things never to do, and the tool-wrapper r
## Conventions worth respecting

- **Centralized package versions** — add new packages to `Directory.Packages.props`, never inline. Adding a *meaningful* library (not a tiny transitive helper)? Add a row to [docs/dependencies.md](https://github.com/Fallout-build/Fallout/blob/main/docs/dependencies.md) in the same PR — reviewers will ask.
- **Test naming follows AV1600 behavior-focused style.** Write test names as short, present-tense descriptions of observable behavior, not method calls. Example good: `Missing_changelog_does_not_add_a_full_changelog_link_to_release_notes`. Example bad: `GetNuGetReleaseNotes_WithMissingChangelog_AndGitHubRepository_DoesNotThrow`. Focus on "what happens" not "what method is called" (see [AV1600](references/testability.md#AV1620)).
- **Test files and classes use `Specs` suffix.** Name test projects `Foo.Specs`, test files `FooSpecs.cs`, and test classes `FooSpecs`. Use `Specs` not `Test` or `Tests` — it clarifies that the class describes the expected behavior (specification) of the subject under test.
- **Tool wrappers**: copy/paste from neighbours; cover full commands; use `<c>`, `<a>`, `<ul>`/`<ol>`, `<em>`, `<para/>` in `help`; don't write `secret: false` or `default: xxx`. See [Tool wrapper recipe](#tool-wrapper-recipe) below.
- **Tests next to code, separate folder**: every `Foo` project under `src/` has a sibling `Foo.Tests` project under `tests/`. Mirror the namespace.
- **No IDE-specific style files committed.** `.editorconfig` and `*.DotSettings` were removed during the takeover — relying on `dotnet format` defaults and review.
Expand Down
1 change: 1 addition & 0 deletions docs/agents/issue-and-pr-style.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,6 +75,7 @@ Closes #<issue>
in a series). Summarize the need in a line — don't recite the issue's
Problem/Outcome/criteria back; the reader can click through. The PR explains
*the change*; the issue holds *the requirement*.
- **Create PRs as draft by default.** Use `gh pr create --draft` unless the user explicitly asks for a ready-for-review PR. Convert to ready only when the user explicitly requests it. This keeps incomplete work from accidentally entering review and ensures work stays flexible during early development.
- Add the `⚠️ Breaking change` callout **only** when the change is breaking —
see the [PR-creation flow](release-and-versioning.md#pr-creation-flow) for
what that requires.
Expand Down
6 changes: 5 additions & 1 deletion docs/dependencies.md
Original file line number Diff line number Diff line change
Expand Up @@ -90,13 +90,17 @@ Still on Matt's personal NuGet account; supply-chain SPOF, high-priority to repl
|---|---|
| `xunit` (+ `runner.visualstudio`) | Test framework |
| `Microsoft.NET.Test.Sdk` | Test host wiring |
| `FluentAssertions` | Readable assertion DSL |
| `FluentAssertions` | Readable assertion DSL — see [licensing note](#fluentassertions-licensing) below |
| `Verify.Xunit` (+ `.DiffPlex`, `.SourceGenerators`) | Snapshot-based testing (the `*.verified.txt` / `*.received.txt` pattern) |
| `coverlet.msbuild` | Code coverage |
| `GitHubActionsTestLogger` | Format test output for GitHub Actions annotations |
| `Basic.Reference.Assemblies.NetStandard20` | Reference assemblies for source-generator compile tests |
| `NetArchTest.Rules` | Architecture-fitness tests (e.g. `Fallout.Core` purity); broader suite tracked in #95 |

### FluentAssertions licensing

As of **v8.0** (Jan 2025) FluentAssertions dropped Apache 2.0 for the proprietary **Xceed Community License**: free for open-source / non-commercial use, paid (per-seat) for commercial use. v7.x remains Apache 2.0. We pin **8.x** (`Directory.Packages.props`) and stay current — this is fine for Fallout because it's (a) an OSS project covered by the free community license, and (b) a **test-only / dev-time** dependency that is never redistributed to consumers of the framework. The standard assertion convention (xUnit + FluentAssertions + Verify) is unchanged — see [conventions.md](agents/conventions.md).

## Build-time CLI tools (`PackageDownload`)

These are downloaded by `build/_build.csproj` for use during this repo's own build — not shipped to consumers.
Expand Down
29 changes: 15 additions & 14 deletions fallout.slnx
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@
<Project Path="src\Persistence\Fallout.Persistence.Solution\Fallout.Persistence.Solution.csproj" />
<Project Path="src\Persistence\Fallout.Solution\Fallout.Solution.csproj" />
<Project Path="src\Fallout.SourceGenerators\Fallout.SourceGenerators.csproj" />
<Project Path="src\Fallout.Solution.Codegen\Fallout.Solution.Codegen.csproj" />
<Project Path="src\Fallout.Tooling.Generator\Fallout.Tooling.Generator.csproj" />
<Project Path="src\Fallout.Tooling\Fallout.Tooling.csproj" />
<Project Path="src\Fallout.Utilities.IO.Compression\Fallout.Utilities.IO.Compression.csproj" />
Expand All @@ -35,22 +36,22 @@
<Project Path="src\Fallout.Utilities.Text.Json\Fallout.Utilities.Text.Json.csproj" />
<Project Path="src\Fallout.Utilities.Text.Yaml\Fallout.Utilities.Text.Yaml.csproj" />
<Project Path="src\Fallout.Utilities\Fallout.Utilities.csproj" />
<Project Path="tests\Fallout.Build.Tests\Fallout.Build.Tests.csproj" />
<Project Path="tests\Fallout.Common.Tests\Fallout.Common.Tests.csproj" />
<Project Path="tests\Fallout.Components.Tests\Fallout.Components.Tests.csproj" />
<Project Path="tests\Fallout.Core.Tests\Fallout.Core.Tests.csproj" />
<Project Path="tests\Fallout.Cli.Tests\Fallout.Cli.Tests.csproj" />
<Project Path="tests\Fallout.Migrate.Tests\Fallout.Migrate.Tests.csproj" />
<Project Path="tests\Fallout.Migrate.Analyzers.Tests\Fallout.Migrate.Analyzers.Tests.csproj" />
<Project Path="tests\Nuke.Common.Shim.Tests\Nuke.Common.Shim.Tests.csproj" />
<Project Path="tests\Nuke.Components.Shim.Tests\Nuke.Components.Shim.Tests.csproj" />
<Project Path="tests\Fallout.ProjectModel.Tests\Fallout.ProjectModel.Tests.csproj" />
<Project Path="tests\Fallout.Solution.Tests\Fallout.Solution.Tests.csproj" />
<Project Path="tests\Fallout.SourceGenerators.Tests\Fallout.SourceGenerators.Tests.csproj" />
<Project Path="tests\Fallout.Build.Specs\Fallout.Build.Specs.csproj" />
<Project Path="tests\Fallout.Common.Specs\Fallout.Common.Specs.csproj" />
<Project Path="tests\Fallout.Components.Specs\Fallout.Components.Specs.csproj" />
<Project Path="tests\Fallout.Core.Specs\Fallout.Core.Specs.csproj" />
<Project Path="tests\Fallout.Cli.Specs\Fallout.Cli.Specs.csproj" />
<Project Path="tests\Fallout.Migrate.Specs\Fallout.Migrate.Specs.csproj" />
<Project Path="tests\Fallout.Migrate.Analyzers.Specs\Fallout.Migrate.Analyzers.Specs.csproj" />
<Project Path="tests\Nuke.Common.Shim.Specs\Nuke.Common.Shim.Specs.csproj" />
<Project Path="tests\Nuke.Components.Shim.Specs\Nuke.Components.Shim.Specs.csproj" />
<Project Path="tests\Fallout.ProjectModel.Specs\Fallout.ProjectModel.Specs.csproj" />
<Project Path="tests\Fallout.Solution.Specs\Fallout.Solution.Specs.csproj" />
<Project Path="tests\Fallout.SourceGenerators.Specs\Fallout.SourceGenerators.Specs.csproj" />
<Project Path="tests\Benchmarks\Fallout.Persistence.Solution.Benchmarks\Fallout.Persistence.Solution.Benchmarks.csproj" />
<Project Path="tests\Consumers\Nuke.Consumer\Nuke.Consumer.csproj" />
<Project Path="tests\Consumers\Fallout.Consumer.Local\Fallout.Consumer.Local.csproj" />
<Project Path="tests\Consumers\Fallout.Consumer.NuGet\Fallout.Consumer.NuGet.csproj" />
<Project Path="tests\Fallout.Tooling.Tests\Fallout.Tooling.Tests.csproj" />
<Project Path="tests\Fallout.Utilities.Tests\Fallout.Utilities.Tests.csproj" />
<Project Path="tests\Fallout.Tooling.Specs\Fallout.Tooling.Specs.csproj" />
<Project Path="tests\Fallout.Utilities.Specs\Fallout.Utilities.Specs.csproj" />
</Solution>
12 changes: 9 additions & 3 deletions src/Fallout.Build/Host.Activation.cs
Original file line number Diff line number Diff line change
Expand Up @@ -29,9 +29,15 @@ internal static IEnumerable<Type> AvailableTypes
private static bool IsRunning(Type hostType)
{
var propertyName = $"IsRunning{hostType.Name}";
var member = hostType.GetProperty(propertyName, ReflectionUtility.Static)
.NotNull($"Host type '{hostType.Name}' defines no property '{propertyName}'");
return member.GetValue<bool>();
var member = hostType.GetProperty(propertyName, ReflectionUtility.Static);

// A public Host subclass that doesn't follow the `IsRunning{Name}` convention is
// not a runnable host — treat it as not-running rather than throwing. Host
// auto-detection runs in FalloutBuild's static ctor, so an exception here crashes
// every build that merely references such an assembly. The Nuke.* transition shim
// emits exactly such a subclass (`Nuke.Common.Host`), which made any shim-referencing
// build abort on startup. See Fallout.Canary#3.
return member is not null && member.GetValue<bool>();
}

private static Host CreateHost(Type hostType)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ public class GitHubActionsJob : ConfigurationEntity
{
public string Name { get; set; }
public GitHubActionsImage Image { get; set; }
public string[] RunsOnLabels { get; set; } = new string[0];
public int TimeoutMinutes { get; set; }
public string ConcurrencyGroup { get; set; }
public string EnvironmentName { get; set; }
Expand All @@ -24,7 +25,14 @@ public override void Write(CustomFileWriter writer)
using (writer.Indent())
{
writer.WriteLine($"name: {Name}");
writer.WriteLine($"runs-on: {Image.GetValue()}");
if (RunsOnLabels.Length > 0)
{
writer.WriteLine($"runs-on: [{RunsOnLabels.JoinCommaSpace()}]");
}
else
{
writer.WriteLine($"runs-on: {Image.GetValue()}");
}

if (TimeoutMinutes > 0)
{
Expand Down
15 changes: 15 additions & 0 deletions src/Fallout.Common/CI/GitHubActions/GitHubActionsAttribute.cs
Original file line number Diff line number Diff line change
Expand Up @@ -93,6 +93,16 @@ public GitHubActionsAttribute(

public string[] InvokedTargets { get; set; } = new string[0];

/// <summary>
/// Runner labels emitted verbatim as <c>runs-on: [label1, label2, ...]</c>, for selecting a
/// self-hosted runner pool by OS/arch/capability (e.g. <c>["self-hosted", "linux", "x64"]</c>).
/// <para/>
/// When non-empty this replaces the <c>runs-on:</c> image for the job and requires exactly one
/// image (no matrix). The constructor-mandated <see cref="GitHubActionsImage"/> is then ignored
/// for <c>runs-on:</c> and only names the job.
/// </summary>
public string[] RunsOnLabels { get; set; } = new string[0];

public GitHubActionsSubmodules Submodules
{
set => _submodules = value;
Expand Down Expand Up @@ -176,6 +186,10 @@ public override ConfigurationEntity GetConfiguration(IReadOnlyCollection<Executa
$"Workflows can only define either shorthand '{nameof(On)}' or '{nameof(On)}*' triggers");
Assert.True(configuration.ShortTriggers.Length > 0 || configuration.DetailedTriggers.Length > 0,
$"Workflows must define either shorthand '{nameof(On)}' or '{nameof(On)}*' triggers");
Assert.True(RunsOnLabels.Length == 0 || _images.Length == 1,
$"Cannot use '{nameof(RunsOnLabels)}' with multiple images; labels resolve a single job's runner");
Assert.True(RunsOnLabels.All(x => !x.IsNullOrWhiteSpace()),
$"'{nameof(RunsOnLabels)}' entries must not be null, empty, or whitespace");

return configuration;
}
Expand All @@ -185,6 +199,7 @@ protected virtual GitHubActionsJob GetJobs(GitHubActionsImage image, IReadOnlyCo
return new GitHubActionsJob
{
Name = image.GetValue().Replace(".", "_"),
RunsOnLabels = RunsOnLabels,
EnvironmentName = EnvironmentName,
EnvironmentUrl = EnvironmentUrl,
Steps = GetSteps(relevantTargets).ToArray(),
Expand Down
8 changes: 5 additions & 3 deletions src/Fallout.Common/ChangeLog/ChangeLogTasks.cs
Original file line number Diff line number Diff line change
Expand Up @@ -21,21 +21,23 @@ public static class ChangelogTasks
{
public static string GetNuGetReleaseNotes(string changelogFile, GitRepository repository = null)
{
AbsolutePath changelogPath = changelogFile;

// URL-encode characters MSBuild treats as command-line/property metacharacters.
// Without this, MSBuild's property parser splits on ; (turning a long release note
// into multiple bogus arguments) and chokes on stray " in the value.
var changelogSectionNotes = ExtractChangelogSectionNotes(changelogFile)
var changelogSectionNotes = ExtractChangelogSectionNotes(changelogPath)
.Select(x => x.Replace("- ", "\u2022 ")
.Replace("* ", "\u2022 ")
.Replace("+ ", "\u2022 ")
.Replace("`", string.Empty)
.Replace(",", "%2C")
.Replace(";", "%3B")).ToList();

if (repository.IsGitHubRepository())
if (repository.IsGitHubRepository() && changelogPath.FileExists())
{
changelogSectionNotes.Add(string.Empty);
changelogSectionNotes.Add($"Full changelog at {repository.GetGitHubBrowseUrl(changelogFile)}");
changelogSectionNotes.Add($"Full changelog at {repository.GetGitHubBrowseUrl(changelogPath, itemType: GitHubItemType.File)}");
}

return changelogSectionNotes.JoinNewLine();
Expand Down
2 changes: 1 addition & 1 deletion src/Fallout.Migrate/Fallout.Migrate.csproj
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
</PropertyGroup>

<ItemGroup>
<InternalsVisibleTo Include="Fallout.Migrate.Tests" />
<InternalsVisibleTo Include="Fallout.Migrate.Specs" />
</ItemGroup>

</Project>
26 changes: 26 additions & 0 deletions src/Fallout.Solution.Codegen/Fallout.Solution.Codegen.csproj
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">

<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFramework>net10.0</TargetFramework>
<IsPackable>false</IsPackable>
<Description>Pre-build codegen for the strongly-typed [Solution] accessor (Solution.g.cs). The net10 default that frees the Roslyn generator from the solution-parser dependency cone; reuses the real Fallout.Persistence.Solution parser and the shared SolutionEmitter.</Description>
</PropertyGroup>

<ItemGroup>
<!-- Shared with the in-compiler StronglyTypedSolutionGenerator fallback -> identical output. -->
<Compile Include="..\shared\SolutionEmitter.cs" Link="SolutionEmitter.cs" />
</ItemGroup>

<ItemGroup>
<ProjectReference Include="..\Persistence\Fallout.Solution\Fallout.Solution.csproj" />
<ProjectReference Include="..\Fallout.Build.Shared\Fallout.Build.Shared.csproj" />
<ProjectReference Include="..\Fallout.Utilities\Fallout.Utilities.csproj" />
</ItemGroup>

<ItemGroup>
<PackageReference Include="Scriban" />
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
</ItemGroup>

</Project>
Loading
Loading