Skip to content

Instantly share code, notes, and snippets.

@AldeRoberge
Created February 18, 2025 15:52
Show Gist options
  • Save AldeRoberge/20b58b26ece39da823c158886563bcaf to your computer and use it in GitHub Desktop.
Save AldeRoberge/20b58b26ece39da823c158886563bcaf to your computer and use it in GitHub Desktop.
Automatically publish projects to Nuget, ensure that their versions match
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