Skip to content

Commit 9f50dae

Browse files
committed
feat(build): implement Cake build system enhancement with Spectre.Console integration
- ✨ Add dynamic version generation from Directory.Build.props - Support --use-directory-props-version flag for automated CI/CD builds - Extract PackageMainVersion and PackageExtensionVersion properties - Generate build metadata with date, commit SHA, and branch name - NuGet-compliant version format: 2.0.0-preview1-20250716-080214 - �� Integrate Spectre.Console for rich developer experience - Beautiful FigletText headers with LocalStack.NET branding - Color-coded status messages (✅ success, ⚠️ warnings, ❌ errors) - Rich package information tables with rounded borders - Progress bars for package creation and publishing operations - Publication summary panels with download URLs - 🏗️ Refactor NugetPackTask with multi-package support - Support packing all packages when no specific ID provided - Eliminate magic strings with centralized constants - Use existing GetProjectTargetFrameworks methods instead of hardcoded values - Enhanced PrepareExtensionsProject with Cake built-in methods - Improved validation with dynamic version support - 🚀 Enhance NugetPushTask with better feedback - Rich console output for publishing operations - Dynamic version support for automated workflows - Upload progress indication and status reporting - Package URL generation for different sources - 🔧 Modernize BuildContext with enhanced capabilities - Add package/source constants (LocalStackClientProjName, GitHubPackageSource) - Implement GetPackageTargetFrameworks for better target framework handling - Add UseDirectoryPropsVersion and BranchName properties - Robust git commit SHA extraction with fallback - Default package source to github for better CI/CD integration - 📦 Replace manual process execution with Cake built-in methods - Use context.DotNetRemoveReference() instead of process calls - Use context.DotNetAddPackage() for package reference management - Proper working directory management and error handling - Type-safe FilePath handling for better reliability BREAKING CHANGE: Enhanced build system now defaults to github package source. Use --package-source flag to specify different targets. Implements Phases 1-4 of Cake Build Enhancement Plan. Maintains backward compatibility with existing workflows. Eliminates bash version generation complexity. Provides foundation for GitHub Actions workflow simplification.
1 parent db86a5e commit 9f50dae

File tree

6 files changed

+568
-87
lines changed

6 files changed

+568
-87
lines changed

.github/workflows/ci.yml

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,3 @@
1-
permissions:
2-
contents: read
3-
actions: write
41
name: "CI/CD Pipeline"
52

63
on:

build/LocalStack.Build/BuildContext.cs

Lines changed: 143 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -4,43 +4,53 @@ namespace LocalStack.Build;
44

55
public sealed class BuildContext : FrostingContext
66
{
7+
public const string LocalStackClientProjName = "LocalStack.Client";
8+
public const string LocalStackClientExtensionsProjName = "LocalStack.Client.Extensions";
9+
10+
public const string GitHubPackageSource = "github";
11+
public const string NuGetPackageSource = "nuget";
12+
public const string MyGetPackageSource = "myget";
13+
714
public BuildContext(ICakeContext context) : base(context)
815
{
916
BuildConfiguration = context.Argument("config", "Release");
10-
ForceBuild = context.Argument("force-build", false);
11-
ForceRestore = context.Argument("force-restore", false);
17+
ForceBuild = context.Argument("force-build", defaultValue: false);
18+
ForceRestore = context.Argument("force-restore", defaultValue: false);
1219
PackageVersion = context.Argument("package-version", "x.x.x");
1320
PackageId = context.Argument("package-id", default(string));
1421
PackageSecret = context.Argument("package-secret", default(string));
15-
PackageSource = context.Argument("package-source", default(string));
16-
SkipFunctionalTest = context.Argument("skipFunctionalTest", true);
22+
PackageSource = context.Argument("package-source", GitHubPackageSource);
23+
SkipFunctionalTest = context.Argument("skipFunctionalTest", defaultValue: true);
24+
25+
// New version generation arguments
26+
UseDirectoryPropsVersion = context.Argument("use-directory-props-version", defaultValue: false);
27+
BranchName = context.Argument("branch-name", "master");
1728

1829
var sourceBuilder = ImmutableDictionary.CreateBuilder<string, string>();
19-
sourceBuilder.AddRange(new[]
20-
{
21-
new KeyValuePair<string, string>("myget", "https://www.myget.org/F/localstack-dotnet-client/api/v3/index.json"),
22-
new KeyValuePair<string, string>("nuget", "https://api.nuget.org/v3/index.json"),
23-
new KeyValuePair<string, string>("github", "https://nuget.pkg.github.com/localstack-dotnet/index.json"),
24-
});
30+
sourceBuilder.AddRange([
31+
new KeyValuePair<string, string>(MyGetPackageSource, "https://www.myget.org/F/localstack-dotnet-client/api/v3/index.json"),
32+
new KeyValuePair<string, string>(NuGetPackageSource, "https://api.nuget.org/v3/index.json"),
33+
new KeyValuePair<string, string>(GitHubPackageSource, "https://nuget.pkg.github.com/localstack-dotnet/index.json"),
34+
]);
2535
PackageSourceMap = sourceBuilder.ToImmutable();
2636

2737
SolutionRoot = context.Directory("../../");
2838
SrcPath = SolutionRoot + context.Directory("src");
2939
TestsPath = SolutionRoot + context.Directory("tests");
3040
BuildPath = SolutionRoot + context.Directory("build");
3141
ArtifactOutput = SolutionRoot + context.Directory("artifacts");
32-
LocalStackClientFolder = SrcPath + context.Directory("LocalStack.Client");
33-
LocalStackClientExtFolder = SrcPath + context.Directory("LocalStack.Client.Extensions");
42+
LocalStackClientFolder = SrcPath + context.Directory(LocalStackClientProjName);
43+
LocalStackClientExtFolder = SrcPath + context.Directory(LocalStackClientExtensionsProjName);
3444
SlnFilePath = SolutionRoot + context.File("LocalStack.sln");
35-
LocalStackClientProjFile = LocalStackClientFolder + context.File("LocalStack.Client.csproj");
36-
LocalStackClientExtProjFile = LocalStackClientExtFolder + context.File("LocalStack.Client.Extensions.csproj");
45+
LocalStackClientProjFile = LocalStackClientFolder + context.File($"{LocalStackClientProjName}.csproj");
46+
LocalStackClientExtProjFile = LocalStackClientExtFolder + context.File($"{LocalStackClientExtensionsProjName}.csproj");
3747

3848
var packIdBuilder = ImmutableDictionary.CreateBuilder<string, FilePath>();
39-
packIdBuilder.AddRange(new[]
40-
{
41-
new KeyValuePair<string, FilePath>("LocalStack.Client", LocalStackClientProjFile),
42-
new KeyValuePair<string, FilePath>("LocalStack.Client.Extensions", LocalStackClientExtProjFile),
43-
});
49+
packIdBuilder.AddRange(
50+
[
51+
new KeyValuePair<string, FilePath>(LocalStackClientProjName, LocalStackClientProjFile),
52+
new KeyValuePair<string, FilePath>(LocalStackClientExtensionsProjName, LocalStackClientExtProjFile),
53+
]);
4454
PackageIdProjMap = packIdBuilder.ToImmutable();
4555
}
4656

@@ -60,6 +70,10 @@ public BuildContext(ICakeContext context) : base(context)
6070

6171
public string PackageSource { get; }
6272

73+
public bool UseDirectoryPropsVersion { get; }
74+
75+
public string BranchName { get; }
76+
6377
public ImmutableDictionary<string, string> PackageSourceMap { get; }
6478

6579
public ImmutableDictionary<string, FilePath> PackageIdProjMap { get; }
@@ -92,27 +106,10 @@ public static void ValidateArgument(string argumentName, string argument)
92106
}
93107
}
94108

95-
public void InstallXUnitNugetPackage()
96-
{
97-
if (!Directory.Exists("testrunner"))
98-
{
99-
Directory.CreateDirectory("testrunner");
100-
}
101-
102-
var nugetInstallSettings = new NuGetInstallSettings
103-
{
104-
Version = "2.8.1", Verbosity = NuGetVerbosity.Normal, OutputDirectory = "testrunner", WorkingDirectory = ".",
105-
};
106-
107-
this.NuGetInstall("xunit.runner.console", nugetInstallSettings);
108-
}
109-
110109
public IEnumerable<ProjMetadata> GetProjMetadata()
111110
{
112111
DirectoryPath testsRoot = this.Directory(TestsPath);
113-
List<FilePath> csProjFile = this.GetFiles($"{testsRoot}/**/*.csproj")
114-
.Where(fp => fp.FullPath.EndsWith("Tests.csproj", StringComparison.InvariantCulture))
115-
.ToList();
112+
List<FilePath> csProjFile = [.. this.GetFiles($"{testsRoot}/**/*.csproj").Where(fp => fp.FullPath.EndsWith("Tests.csproj", StringComparison.InvariantCulture))];
116113

117114
var projMetadata = new List<ProjMetadata>();
118115

@@ -194,49 +191,142 @@ public void InstallMonoOnLinux()
194191
this.Information("✅ Mono installation completed successfully");
195192
}
196193

197-
public void RunXUnitUsingMono(string targetFramework, string assemblyPath)
194+
public string GetProjectVersion()
198195
{
199-
int exitCode = this.StartProcess(
200-
"mono", new ProcessSettings { Arguments = $"./testrunner/xunit.runner.console.2.8.1/tools/{targetFramework}/xunit.console.exe {assemblyPath}" });
201-
202-
if (exitCode != 0)
196+
if (UseDirectoryPropsVersion)
203197
{
204-
throw new InvalidOperationException($"Exit code: {exitCode}");
198+
return GetDynamicVersionFromProps("PackageMainVersion");
205199
}
206-
}
207200

208-
public string GetProjectVersion()
209-
{
201+
// Original logic for backward compatibility
210202
FilePath file = this.File("./src/Directory.Build.props");
211-
212203
this.Information(file.FullPath);
213204

214205
string project = File.ReadAllText(file.FullPath, Encoding.UTF8);
215206
int startIndex = project.IndexOf("<Version>", StringComparison.Ordinal) + "<Version>".Length;
216207
int endIndex = project.IndexOf("</Version>", startIndex, StringComparison.Ordinal);
217208

218-
string version = project.Substring(startIndex, endIndex - startIndex);
209+
string version = project[startIndex..endIndex];
219210
version = $"{version}.{PackageVersion}";
220211

221212
return version;
222213
}
223214

224215
public string GetExtensionProjectVersion()
225216
{
226-
FilePath file = this.File(LocalStackClientExtProjFile);
217+
if (UseDirectoryPropsVersion)
218+
{
219+
return GetDynamicVersionFromProps("PackageExtensionVersion");
220+
}
227221

222+
// Original logic for backward compatibility
223+
FilePath file = this.File(LocalStackClientExtProjFile);
228224
this.Information(file.FullPath);
229225

230226
string project = File.ReadAllText(file.FullPath, Encoding.UTF8);
231227
int startIndex = project.IndexOf("<Version>", StringComparison.Ordinal) + "<Version>".Length;
232228
int endIndex = project.IndexOf("</Version>", startIndex, StringComparison.Ordinal);
233229

234-
string version = project.Substring(startIndex, endIndex - startIndex);
230+
string version = project[startIndex..endIndex];
235231
version = $"{version}.{PackageVersion}";
236232

237233
return version;
238234
}
239235

236+
/// <summary>
237+
/// Gets the target frameworks for a specific package using the existing proven method
238+
/// </summary>
239+
/// <param name="packageId">The package identifier</param>
240+
/// <returns>Comma-separated target frameworks</returns>
241+
public string GetPackageTargetFrameworks(string packageId)
242+
{
243+
if (!PackageIdProjMap.TryGetValue(packageId, out FilePath? projectFile) || projectFile == null)
244+
{
245+
throw new ArgumentException($"Unknown package ID: {packageId}", nameof(packageId));
246+
}
247+
248+
string[] frameworks = GetProjectTargetFrameworks(projectFile.FullPath);
249+
return string.Join(", ", frameworks);
250+
}
251+
252+
/// <summary>
253+
/// Generates dynamic version from Directory.Build.props with build metadata
254+
/// </summary>
255+
/// <param name="versionPropertyName">The property name to extract (PackageMainVersion or PackageExtensionVersion)</param>
256+
/// <returns>Version with build metadata (e.g., 2.0.0-preview1.20240715.a1b2c3d)</returns>
257+
private string GetDynamicVersionFromProps(string versionPropertyName)
258+
{
259+
// Extract base version from Directory.Build.props
260+
FilePath propsFile = this.File("../../Directory.Build.props");
261+
string content = File.ReadAllText(propsFile.FullPath, Encoding.UTF8);
262+
263+
string startElement = $"<{versionPropertyName}>";
264+
string endElement = $"</{versionPropertyName}>";
265+
266+
int startIndex = content.IndexOf(startElement, StringComparison.Ordinal) + startElement.Length;
267+
int endIndex = content.IndexOf(endElement, startIndex, StringComparison.Ordinal);
268+
269+
if (startIndex < startElement.Length || endIndex < 0)
270+
{
271+
throw new InvalidOperationException($"Could not find {versionPropertyName} in Directory.Build.props");
272+
}
273+
274+
string baseVersion = content[startIndex..endIndex];
275+
276+
// Generate build metadata
277+
string buildDate = DateTime.UtcNow.ToString("yyyyMMdd", System.Globalization.CultureInfo.InvariantCulture);
278+
string commitSha = GetGitCommitSha();
279+
string safeBranchName = BranchName.Replace('/', '-').Replace('_', '-');
280+
281+
// Simplified NuGet-compliant version format
282+
if (BranchName == "master")
283+
{
284+
// For master: 2.0.0-preview1-20240715-a1b2c3d
285+
return $"{baseVersion}-{buildDate}-{commitSha}";
286+
}
287+
else
288+
{
289+
// For feature branches: 2.0.0-preview1-feature-branch-20240715-a1b2c3d
290+
return $"{baseVersion}-{safeBranchName}-{buildDate}-{commitSha}";
291+
}
292+
}
293+
294+
/// <summary>
295+
/// Gets the short git commit SHA for version metadata
296+
/// </summary>
297+
/// <returns>Short commit SHA or timestamp fallback</returns>
298+
private string GetGitCommitSha()
299+
{
300+
try
301+
{
302+
var processSettings = new ProcessSettings
303+
{
304+
Arguments = "rev-parse --short HEAD",
305+
RedirectStandardOutput = true,
306+
RedirectStandardError = true,
307+
Silent = true,
308+
};
309+
310+
var exitCode = this.StartProcess("git", processSettings, out IEnumerable<string> output);
311+
312+
if (exitCode == 0 && output?.Any() == true)
313+
{
314+
string? commitSha = output.FirstOrDefault()?.Trim();
315+
if (!string.IsNullOrEmpty(commitSha))
316+
{
317+
return commitSha;
318+
}
319+
}
320+
}
321+
catch (Exception ex)
322+
{
323+
this.Warning($"Failed to get git commit SHA: {ex.Message}");
324+
}
325+
326+
// Fallback to timestamp-based identifier
327+
return DateTime.UtcNow.ToString("HHmmss", System.Globalization.CultureInfo.InvariantCulture);
328+
}
329+
240330
private string[] GetProjectTargetFrameworks(string csprojPath)
241331
{
242332
FilePath file = this.File(csprojPath);
@@ -249,7 +339,7 @@ private string[] GetProjectTargetFrameworks(string csprojPath)
249339
int startIndex = project.IndexOf(startElement, StringComparison.Ordinal) + startElement.Length;
250340
int endIndex = project.IndexOf(endElement, startIndex, StringComparison.Ordinal);
251341

252-
string targetFrameworks = project.Substring(startIndex, endIndex - startIndex);
342+
string targetFrameworks = project[startIndex..endIndex];
253343

254344
return targetFrameworks.Split(';');
255345
}
@@ -268,14 +358,14 @@ private string GetAssemblyName(string csprojPath)
268358
int startIndex = project.IndexOf("<AssemblyName>", StringComparison.Ordinal) + "<AssemblyName>".Length;
269359
int endIndex = project.IndexOf("</AssemblyName>", startIndex, StringComparison.Ordinal);
270360

271-
assemblyName = project.Substring(startIndex, endIndex - startIndex);
361+
assemblyName = project[startIndex..endIndex];
272362
}
273363
else
274364
{
275365
int startIndex = csprojPath.LastIndexOf('/') + 1;
276366
int endIndex = csprojPath.IndexOf(".csproj", startIndex, StringComparison.Ordinal);
277367

278-
assemblyName = csprojPath.Substring(startIndex, endIndex - startIndex);
368+
assemblyName = csprojPath[startIndex..endIndex];
279369
}
280370

281371
return assemblyName;

0 commit comments

Comments
 (0)