-
-
Notifications
You must be signed in to change notification settings - Fork 18
Expand file tree
/
Copy pathBuild.cs
More file actions
298 lines (258 loc) · 13.3 KB
/
Copy pathBuild.cs
File metadata and controls
298 lines (258 loc) · 13.3 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
using System;
using System.Collections.Generic;
using System.Linq;
using NuGet.Packaging;
using Fallout.Common;
using Fallout.Common.CI;
using Fallout.Common.CI.GitHubActions;
using Fallout.Common.Execution;
using Fallout.Common.Git;
using Fallout.Common.IO;
using Fallout.Solutions;
using Fallout.Common.Tooling;
using Fallout.Common.Tools.DotNet;
using Fallout.Common.Tools.GitHub;
using Fallout.Common.Utilities;
using Fallout.Components;
using static Fallout.Common.ControlFlow;
using static Fallout.Common.Tools.DotNet.DotNetTasks;
[DotNetVerbosityMapping]
[ShutdownDotNetAfterServerBuild]
partial class Build
: FalloutBuild,
IHasChangelog,
IHasGitRepository,
IHasSolution,
IRestore,
ICompile,
IPack,
ITest,
IReportCoverage,
IPublish,
ICreateGitHubRelease
{
public static int Main() => Execute<Build>(x => ((IPack)x).Pack);
[CI] readonly GitHubActions GitHubActions;
GitRepository GitRepository => From<IHasGitRepository>().GitRepository;
[Solution(GenerateProjects = true)] readonly Solution Solution;
Fallout.Solutions.Solution IHasSolution.Solution => Solution;
AbsolutePath OutputDirectory => RootDirectory / "output";
AbsolutePath SourceDirectory => RootDirectory / "source";
// The integration trunk and sole prerelease lane (ADR-0008). Per-commit
// -preview prereleases to GitHub Packages (see .github/workflows/preview.yml).
// Long-lived + protected, so it needs ubuntu-latest PR/push validation.
const string MainBranch = "main";
// Glob matching the long-lived release-branch family: CalVer production lines
// (release/2026, release/2027, ...).
// Used by Build.CI.GitHubActions to extend PR-validation workflows to cover PRs
// targeting any release branch — branch protection on a release branch requires the
// ubuntu-latest status check, which only fires if the workflow's branches: list
// matches the PR's base ref. See ADR-0004 (calendar versioning) / milestone #13.
const string ReleaseBranchPattern = "release/*";
// Glob matching the gitflow support-branch family (ADR-0004, amended 2026-05-30):
// the legacy semver line (support/v10, ...) and retired CalVer years (support/2026, ...).
// Same protected-branch validation rationale as ReleaseBranchPattern.
const string SupportBranchPattern = "support/*";
// Trunk-based versioning. Nerdbank.GitVersioning computes the canonical version
// (major.minor from version.json + git-height as patch). ThisAssembly is generated by
// NB.GV at compile time; AssemblyInformationalVersion looks like "10.3.5+abc1234" so we
// strip the "+commitsha" tail. #81.
[Parameter] readonly bool Major;
string MajorMinorPatchVersion => Major
? $"{ParseMajor(ThisAssembly.AssemblyInformationalVersion) + 1}.0.0"
: ThisAssembly.AssemblyInformationalVersion.Split('+')[0];
string MilestoneTitle => $"v{MajorMinorPatchVersion}";
private static int ParseMajor(string informationalVersion)
=> int.Parse(informationalVersion.Split('.')[0]);
AbsolutePath IHasArtifacts.ArtifactsDirectory => RootDirectory / "output";
Target Clean => _ => _
.Before<IRestore>()
.Executes(() =>
{
SourceDirectory.GlobDirectories("*/bin", "*/obj").DeleteDirectories();
OutputDirectory.CreateOrCleanDirectory();
});
Configure<DotNetBuildSettings> ICompile.CompileSettings => _ => _
.When(!ScheduledTargets.Contains(((IPublish)this).Publish) && !ScheduledTargets.Contains(Install), _ => _
.ClearProperties());
Configure<DotNetPublishSettings> ICompile.PublishSettings => _ => _
.When(!ScheduledTargets.Contains(((IPublish)this).Publish) && !ScheduledTargets.Contains(Install), _ => _
.ClearProperties());
IEnumerable<(Fallout.Solutions.Project Project, string Framework)> ICompile.PublishConfigurations =>
from project in new[] { Solution.Fallout_Cli, Solution.Fallout_MSBuildTasks }
from framework in project.GetTargetFrameworks()
select (project, framework);
IEnumerable<Fallout.Solutions.Project> ITest.TestProjects => Partition.GetCurrent(Solution.GetAllProjects("*.Specs"));
[Parameter]
public int TestDegreeOfParallelism { get; } = 1;
Target ITest.Test => _ => _
.Inherit<ITest>()
.Partition(2);
bool IReportCoverage.CreateCoverageHtmlReport => true;
bool IReportCoverage.ReportToCodecov => false;
// Local Terminal runs use a placeholder version so packed nupkgs don't
// collide with real releases. CI runs let Nerdbank.GitVersioning inject the
// real version via MSBuild ($PackageVersion).
Configure<DotNetPackSettings> IPack.PackSettings => _ => _
.When(Host is Terminal, _ => _
.SetVersion(DefaultDeploymentVersion));
string DefaultDeploymentVersion => "9999.0.0";
[Parameter] [Secret] readonly string NuGetApiKey;
// Two publish channels (FALLOUT001 — see IPublish.PublishTargets). Routing replaces the
// old single-feed push + the hand-rolled `dotnet nuget push` in the workflows (#333):
// - github-packages: EVERY package, incl. the Nuke.* shims — that ID is owned by the
// original NUKE maintainer (#47), so the shims only ever go here. Keyed by the GitHub token.
// - nuget.org: Fallout.* ONLY, never the Nuke.* shims. Keyed by NUGET_API_KEY.
// Select per run from CI with `dotnet fallout Publish --publish-to <name>`. PublishTarget.SkipDuplicate
// (default true) keeps re-runs idempotent if a version already exists on a feed.
#pragma warning disable FALLOUT001 // opting our own build into the experimental multi-channel publish surface
IEnumerable<PublishTarget> IPublish.PublishTargets => new[]
{
new PublishTarget
{
Name = "github-packages",
Source = "https://nuget.pkg.github.com/Fallout-build/index.json",
ApiKey = From<ICreateGitHubRelease>().GitHubToken,
},
new PublishTarget
{
Name = "nuget.org",
Source = "https://api.nuget.org/v3/index.json",
ApiKey = NuGetApiKey,
IncludePackages = new[] { "Fallout.*" },
ExcludePackages = new[] { "Nuke.*" },
},
};
#pragma warning restore FALLOUT001
// The workflows now gate *which* channel publishes (via --publish-to); the on-branch
// requirement is gone. We keep a CI guard, though: GitHubToken binds from the
// GITHUB_TOKEN env var (ICreateGitHubRelease.GitHubToken), which many developers have
// exported — without this, a stray local `dotnet fallout Publish` (no --publish-to →
// all targets) would actually push to GitHub Packages. Requiring GitHubActions blocks
// local pushes while still allowing every CI lane (preview/release).
Target IPublish.Publish => _ => _
.Inherit<IPublish>()
.Consumes(From<IPack>().Pack)
.Requires(() => Host is GitHubActions)
.WhenSkipped(DependencyBehavior.Execute);
IEnumerable<AbsolutePath> NuGetPackageFiles
=> From<IPack>().PackagesDirectory.GlobFiles("*.nupkg");
Target DeletePackages => _ => _
.DependentFor<IPublish>()
.After<IPack>()
.OnlyWhenStatic(() => Host is Terminal)
.Executes(() =>
{
var packagesDirectory = NuGetPackageResolver.GetPackagesDirectory(packagesConfigFile: BuildProjectFile);
var packageDirectories = packagesDirectory.GlobDirectories($"fallout.*/{DefaultDeploymentVersion}");
packageDirectories.DeleteDirectories();
});
string ICreateGitHubRelease.Name => MajorMinorPatchVersion;
IEnumerable<AbsolutePath> ICreateGitHubRelease.AssetFiles => NuGetPackageFiles;
// Rewritten override (not inheriting the base) so we can build a rich
// release body that lists the milestone contents. The base implementation
// reads CHANGELOG.md's [vNext] section, which we don't maintain on this
// fork. We rely on milestones + GitHub's auto-generated PR list instead.
Target ICreateGitHubRelease.CreateGitHubRelease => _ => _
.TriggeredBy<IPublish>()
.ProceedAfterFailure()
.Requires(() => From<ICreateGitHubRelease>().GitHubToken)
// Only cut a GitHub Release in the release workflow — never as a side-effect of a
// preview publish. This target is TriggeredBy<IPublish>, and since #333 the
// preview lane dogfoods `Publish`; gating on IsOnMainBranch alone would
// fire it on every preview push to main (it doesn't — GitHub Releases are production-
// only, and release.yml hand-rolls them via `gh release create`).
.OnlyWhenStatic(() => GitHubActions?.Workflow == ReleaseWorkflow)
.Executes(async () =>
{
var client = GitHubTasks.GitHubClient;
client.Credentials = new global::Octokit.Credentials(From<ICreateGitHubRelease>().GitHubToken.NotNull());
var owner = GitRepository.GetGitHubOwner();
var name = GitRepository.GetGitHubName();
var version = MajorMinorPatchVersion;
// Find a milestone whose title starts with this release's version
// (matches both our "10.2.0 - <desc>" convention and the legacy
// "v10.2.0" form). First match wins.
var milestones = await client.Issue.Milestone.GetAllForRepository(
owner, name,
new global::Octokit.MilestoneRequest { State = global::Octokit.ItemStateFilter.All });
var milestone = milestones.FirstOrDefault(m =>
m.Title.StartsWith(version, StringComparison.Ordinal) ||
m.Title.StartsWith($"v{version}", StringComparison.Ordinal));
var milestoneItems = milestone == null
? Array.Empty<global::Octokit.Issue>()
: (await client.Issue.GetAllForRepository(owner, name, new global::Octokit.RepositoryIssueRequest
{
Milestone = milestone.Number.ToString(),
State = global::Octokit.ItemStateFilter.Closed
})).ToArray();
// Build the release body. GitHub will append its auto-generated
// PR-since-last-tag list because GenerateReleaseNotes=true.
var body = new System.Text.StringBuilder();
if (milestone != null)
{
body.AppendLine($"**Milestone:** [{milestone.Title}]({milestone.HtmlUrl})");
body.AppendLine();
var pulls = milestoneItems.Where(i => i.PullRequest != null).OrderBy(i => i.Number).ToArray();
var issues = milestoneItems.Where(i => i.PullRequest == null).OrderBy(i => i.Number).ToArray();
if (pulls.Length > 0)
{
body.AppendLine("### Pull requests in this milestone");
foreach (var pr in pulls)
body.AppendLine($"- {pr.Title} (#{pr.Number}) — @{pr.User.Login}");
body.AppendLine();
}
if (issues.Length > 0)
{
body.AppendLine("### Issues closed in this milestone");
foreach (var issue in issues)
body.AppendLine($"- {issue.Title} (#{issue.Number})");
body.AppendLine();
}
}
global::Octokit.Release release;
try
{
release = await client.Repository.Release.Create(owner, name,
new global::Octokit.NewRelease(version)
{
Name = version,
Body = body.ToString(),
Prerelease = false,
Draft = false,
GenerateReleaseNotes = true
});
}
catch
{
release = await client.Repository.Release.Get(owner, name, version);
}
foreach (var assetPath in NuGetPackageFiles)
{
await using var assetStream = System.IO.File.OpenRead(assetPath);
await client.Repository.Release.UploadAsset(release, new global::Octokit.ReleaseAssetUpload
{
FileName = assetPath.Name,
ContentType = "application/octet-stream",
RawData = assetStream
});
}
// Notify each milestone item that it's shipped.
foreach (var item in milestoneItems)
await client.Issue.Comment.Create(owner, name, item.Number, $"Released in {version}! 🎉");
});
Target Install => _ => _
.DependsOn<IPack>()
.Executes(() =>
{
// Read the id off the csproj rather than hardcoding it: the tool's PackageId is
// deliberately decoupled from its project/assembly name (Fallout.Cli packs as
// Fallout.GlobalTool), so Solution.Fallout_Cli.Name would install the wrong package.
var packageId = Solution.Fallout_Cli.GetProperty("PackageId");
SuppressErrors(() => DotNet($"tool uninstall -g {packageId}"), logWarning: false);
DotNet($"tool install -g {packageId} --add-source {OutputDirectory} --version {DefaultDeploymentVersion}");
});
T From<T>()
where T : IFalloutBuild
=> (T)(object)this;
}