Created
February 18, 2025 15:52
-
-
Save AldeRoberge/20b58b26ece39da823c158886563bcaf to your computer and use it in GitHub Desktop.
Automatically publish projects to Nuget, ensure that their versions match
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
using System.Diagnostics; | |
using System.Text; | |
using System.Text.Json; | |
using System.Xml.Linq; | |
using AGX.Common.Logging.Serilog; | |
using Serilog; | |
namespace AGX.Common.Publishing | |
{ | |
internal class Program | |
{ | |
private static readonly string[] ProjectPaths = | |
{ | |
@"AGX.Common.Resources\AGX.Common.Resources.csproj", | |
@"AGX.Common.Logging.Serilog\AGX.Common.Logging.Serilog.csproj", | |
@"AGX.Common\AGX.Common.csproj", | |
@"AGX.Common.Unity\AGX.Common.Unity.csproj" | |
}; | |
private static void Main() | |
{ | |
LoggingConfig.ConfigureLogger(); | |
try | |
{ | |
ValidateEnvironment(); | |
// Check that all projects have a common version. | |
EnsureCommonVersion(); | |
foreach (var relativePath in ProjectPaths) | |
{ | |
ProcessProject(relativePath); | |
} | |
} | |
catch (Exception ex) | |
{ | |
Log.Fatal(ex, "An unhandled exception occurred."); | |
} | |
finally | |
{ | |
Log.CloseAndFlush(); | |
} | |
} | |
private static void ValidateEnvironment() | |
{ | |
var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY"); | |
if (string.IsNullOrWhiteSpace(apiKey)) | |
{ | |
throw new InvalidOperationException("NuGet API key not found. Set NUGET_API_KEY in your environment variables."); | |
} | |
} | |
/// <summary> | |
/// Ensures that all projects share the same version. | |
/// If they do not, update each project to the highest version found. | |
/// Also logs the current version on NuGet for each package. | |
/// </summary> | |
private static void EnsureCommonVersion() | |
{ | |
var baseDirectory = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); | |
var projectVersions = new Dictionary<string, Version>(); | |
// Read each project's version. | |
foreach (var relativePath in ProjectPaths) | |
{ | |
var projectPath = Path.Combine(baseDirectory, relativePath); | |
if (!File.Exists(projectPath)) | |
{ | |
Log.Warning("Project file not found: {ProjectPath}", projectPath); | |
continue; | |
} | |
var csprojXml = XDocument.Load(projectPath); | |
var versionElement = csprojXml.Descendants("Version").FirstOrDefault(); | |
if (versionElement == null) | |
{ | |
throw new InvalidOperationException($"No <Version> tag found in {projectPath}"); | |
} | |
if (!Version.TryParse(versionElement.Value, out var parsedVersion)) | |
{ | |
throw new InvalidOperationException($"Invalid version format in {projectPath}: {versionElement.Value}"); | |
} | |
var packageId = Path.GetFileNameWithoutExtension(projectPath); | |
projectVersions[packageId] = parsedVersion; | |
} | |
if (!projectVersions.Any()) | |
{ | |
throw new InvalidOperationException("No project versions were found."); | |
} | |
// Determine the highest version among all projects. | |
var highestVersion = projectVersions.Values.Max(); | |
// Update any projects that are not at the highest version. | |
foreach (var relativePath in ProjectPaths) | |
{ | |
var projectPath = Path.Combine(baseDirectory, relativePath); | |
if (!File.Exists(projectPath)) | |
{ | |
continue; | |
} | |
var csprojXml = XDocument.Load(projectPath); | |
var versionElement = csprojXml.Descendants("Version").FirstOrDefault(); | |
if (versionElement == null) | |
{ | |
continue; | |
} | |
if (Version.TryParse(versionElement.Value, out var currentVersion)) | |
{ | |
if (currentVersion != highestVersion) | |
{ | |
Log.Warning("Updating {ProjectPath} version from {OldVersion} to {NewVersion}", | |
projectPath, currentVersion, highestVersion); | |
versionElement.Value = highestVersion.ToString(); | |
csprojXml.Save(projectPath); | |
} | |
} | |
} | |
Log.Information("All projects are updated to the common version: {CommonVersion}", highestVersion); | |
// Log NuGet versions for each package. | |
foreach (var relativePath in ProjectPaths) | |
{ | |
var projectPath = Path.Combine(baseDirectory, relativePath); | |
var packageId = Path.GetFileNameWithoutExtension(projectPath); | |
var nugetVersion = GetNugetLatestVersion(packageId); | |
Log.Information("Package {PackageId}: Local version {LocalVersion}, NuGet version {NugetVersion}", | |
packageId, highestVersion.ToString(), nugetVersion ?? "not published"); | |
} | |
} | |
/// <summary> | |
/// Gets the latest published version for a package from NuGet. | |
/// </summary> | |
private static string? GetNugetLatestVersion(string packageId) | |
{ | |
try | |
{ | |
using var client = new HttpClient(); | |
// NuGet flat container API (package IDs must be lower-case) | |
var url = $"https://api.nuget.org/v3-flatcontainer/{packageId.ToLowerInvariant()}/index.json"; | |
var response = client.GetAsync(url).Result; | |
if (!response.IsSuccessStatusCode) | |
{ | |
// Package not found on NuGet | |
return null; | |
} | |
var jsonString = response.Content.ReadAsStringAsync().Result; | |
using var document = JsonDocument.Parse(jsonString); | |
if (document.RootElement.TryGetProperty("versions", out var versionsElement) && | |
versionsElement.ValueKind == JsonValueKind.Array) | |
{ | |
var versions = versionsElement.EnumerateArray() | |
.Select(v => v.GetString()) | |
.Where(v => !string.IsNullOrEmpty(v)) | |
.ToList(); | |
if (versions.Any()) | |
{ | |
// Assume the list is sorted; return the last version as the latest. | |
return versions.Last(); | |
} | |
} | |
} | |
catch (Exception ex) | |
{ | |
Log.Error(ex, "Error fetching NuGet version for package {PackageId}", packageId); | |
} | |
return null; | |
} | |
private static void ProcessProject(string relativePath) | |
{ | |
// The base directory is ../ from this project | |
var baseDirectory = Path.GetFullPath(Path.Combine(AppContext.BaseDirectory, "..", "..", "..", "..")); | |
var projectPath = Path.Combine(baseDirectory, relativePath); | |
if (!File.Exists(projectPath)) | |
{ | |
Log.Warning("Project file not found: {ProjectPath}", projectPath); | |
return; | |
} | |
try | |
{ | |
Log.Information("Processing project: {ProjectPath}", projectPath); | |
// Increment version (all projects now share the same version) | |
var newVersion = IncrementVersion(projectPath); | |
BuildProject(projectPath); | |
PublishPackage(projectPath, newVersion); | |
} | |
catch (Exception ex) | |
{ | |
Log.Error(ex, "Error processing project: {ProjectPath}", projectPath); | |
} | |
} | |
private static string IncrementVersion(string projectPath) | |
{ | |
var csprojXml = XDocument.Load(projectPath); | |
var versionElement = csprojXml.Descendants("Version").FirstOrDefault(); | |
if (versionElement == null) | |
{ | |
throw new InvalidOperationException($"No <Version> tag found in {projectPath}"); | |
} | |
var currentVersion = versionElement.Value; | |
if (!Version.TryParse(currentVersion, out var version)) | |
{ | |
throw new InvalidOperationException($"Invalid version format in {projectPath}: {currentVersion}"); | |
} | |
// Increment the minor version (adjust versioning strategy as needed) | |
var newVersion = new Version(version.Major, version.Minor + 1, version.Build).ToString(); | |
versionElement.Value = newVersion; | |
csprojXml.Save(projectPath); | |
Log.Information("Updated version in {ProjectPath} to {NewVersion}", projectPath, newVersion); | |
return newVersion; | |
} | |
private static void BuildProject(string projectPath) | |
{ | |
RunCommand("dotnet", $"pack \"{projectPath}\" --configuration Release"); | |
Log.Information("Build completed for {ProjectPath}", projectPath); | |
} | |
private static void PublishPackage(string projectPath, string version) | |
{ | |
var packagePath = Path.Combine( | |
Path.GetDirectoryName(projectPath)!, | |
$@"bin\Release\{Path.GetFileNameWithoutExtension(projectPath)}.{version}.nupkg" | |
); | |
if (!File.Exists(packagePath)) | |
{ | |
throw new FileNotFoundException($"Package not found: {packagePath}"); | |
} | |
var apiKey = Environment.GetEnvironmentVariable("NUGET_API_KEY"); | |
RunCommand("dotnet", $"nuget push \"{packagePath}\" --api-key {apiKey} --source https://api.nuget.org/v3/index.json"); | |
Log.Information("Package pushed: {PackagePath}", packagePath); | |
} | |
private static void RunCommand(string command, string arguments) | |
{ | |
Log.Information("Executing command: {Command} {Arguments}", command, arguments); | |
using var process = new Process(); | |
process.StartInfo = new ProcessStartInfo | |
{ | |
FileName = command, | |
Arguments = arguments, | |
RedirectStandardOutput = true, | |
RedirectStandardError = true, | |
UseShellExecute = false, | |
CreateNoWindow = true, | |
StandardOutputEncoding = Encoding.UTF8, | |
StandardErrorEncoding = Encoding.UTF8 | |
}; | |
process.OutputDataReceived += (sender, e) => | |
{ | |
if (!string.IsNullOrEmpty(e.Data)) | |
Log.Verbose(e.Data.Trim()); | |
}; | |
process.ErrorDataReceived += (sender, e) => | |
{ | |
if (!string.IsNullOrEmpty(e.Data)) | |
Log.Error(e.Data.Trim()); | |
}; | |
process.Start(); | |
process.BeginOutputReadLine(); | |
process.BeginErrorReadLine(); | |
process.WaitForExit(); | |
if (process.ExitCode != 0) | |
{ | |
throw new Exception($"Command failed: {command} {arguments}"); | |
} | |
} | |
} | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment