Skip to content
Merged
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
6 changes: 5 additions & 1 deletion .github/actions/sdk-update/action.yml
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,10 @@ inputs:
description: 'base branch'
required: true
default: 'main'
exclude-file: # packages to exclude for the update
description: 'Path to the JSON exclude file'
required: false
default: '.github/sdk-update-exclude.json'

runs:
using: "composite"
Expand All @@ -22,5 +26,5 @@ runs:
setAllVars: true

- name: Run Uno Sdk Updater
run: dotnet run -c Release --project tools/Uno.Sdk.Updater
shell: pwsh
run: dotnet run -c Release --project tools/Uno.Sdk.Updater -- --exclude-file "${{ inputs['exclude-file'] }}"
6 changes: 6 additions & 0 deletions .github/sdk-update-exclude.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
{
"exclude": [
"SvgSkia"
]
}

57 changes: 57 additions & 0 deletions tools/Uno.Sdk.Updater/Config/ExcludeConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
namespace Uno.Sdk.Updater.Config
{
// Exclusions (JSON via --exclude-file)
internal static class ExcludeConfig
{
public static string? ExcludeFilePath { get; set; }

private static readonly Lazy<HashSet<string>> _excluded = new(() => Load());
public static HashSet<string> Excluded => _excluded.Value;

private static HashSet<string> Load()
{
var set = new HashSet<string>(StringComparer.OrdinalIgnoreCase);

var path = ExcludeFilePath;
if (string.IsNullOrWhiteSpace(path))
return set; // no exclusions when not provided

// Normalize relative path
if (!Path.IsPathRooted(path))
path = Path.GetFullPath(path);

if (!File.Exists(path))
{
Console.WriteLine($"Exclude file not found: {path}");
return set;
}

try
{
using var fs = File.OpenRead(path);
using var doc = System.Text.Json.JsonDocument.Parse(fs);
if (doc.RootElement.TryGetProperty("exclude", out var arr) &&
arr.ValueKind == System.Text.Json.JsonValueKind.Array)
{
foreach (var el in arr.EnumerateArray())
{
if (el.ValueKind == System.Text.Json.JsonValueKind.String)
{
var s = el.GetString();
if (!string.IsNullOrWhiteSpace(s))
set.Add(s);
}
}
}

Console.WriteLine($"Loaded {set.Count} exclusion(s) from {path}");
}
catch (Exception ex)
{
Console.WriteLine($"Failed to read exclude file: {ex.Message}");
}

return set;
}
}
}
94 changes: 79 additions & 15 deletions tools/Uno.Sdk.Updater/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
using Uno.Sdk.Models;
using Uno.Sdk.Services;
using Uno.Sdk.Updater;
using Uno.Sdk.Updater.Config;
using Uno.Sdk.Updater.Utils;

const string UnoSdkPackageId = "Uno.Sdk.Private";
Expand All @@ -16,6 +17,9 @@
Console.WriteLine($"Maximum Search Version: {UpdaterBuildContext.MaxVersion}");
WriteBreak();

// Minimal CLI parsing for --exclude-file
ExcludeConfig.ExcludeFilePath = Cli.GetArgValue("--exclude-file");

var jsonOptions = new JsonSerializerOptions(JsonSerializerDefaults.Web)
{
// We want to keep the output Human Readable
Expand Down Expand Up @@ -49,11 +53,11 @@
string? readMePath = null;
string? packagesJsonPath = null;
string? relativePackagesJsonPath = null;
string? description = null;

Check warning on line 56 in tools/Uno.Sdk.Updater/Program.cs

View workflow job for this annotation

GitHub Actions / manifest-update

The variable 'description' is assigned but its value is never used
string? tags = null;
bool wroteChanges = false;

foreach(var entry in sdkZip.Entries)
foreach (var entry in sdkZip.Entries)
{
var extension = Path.GetExtension(entry.FullName);
string[] allowedExtensions = [".md", ".json", ".nuspec"];
Expand Down Expand Up @@ -88,13 +92,12 @@
var inputManifest = await JsonSerializer.DeserializeAsync<IEnumerable<ManifestGroup>>(packageStream)
?? throw new InvalidOperationException("Unable to parse the packages.json from the Sdk.");

if (!unoVersion.IsPreview)
{
inputManifest = MergeLocalManifest(inputManifest, outputPath);
}
inputManifest = unoVersion.IsPreview
? MergeLocalOverridesOnly(inputManifest, outputPath) // keep only local VersionOverride entries
: MergeLocalManifest(inputManifest, outputPath); // full merge as before

var manifest = new List<ManifestGroup>();
foreach(var group in inputManifest)
foreach (var group in inputManifest)
{
var updated = await UpdateGroup(group, unoVersion, client);
manifest.Add(updated);
Expand Down Expand Up @@ -135,7 +138,7 @@
var manifestJson = File.ReadAllText(packagesJsonPath);
var manifest = JsonSerializer.Deserialize<IEnumerable<ManifestGroup>>(manifestJson) ?? [];

foreach(var group in manifest)
foreach (var group in manifest)
{
readMe = Regex.Replace(readMe, Regex.Escape($"${group.Group}$"), group.Version);
}
Expand Down Expand Up @@ -177,6 +180,49 @@
return mergedManifest;
}

// Keep local versions as baseline. Only ensure local VersionOverride are present,
// and bring in any brand-new groups that exist in the SDK manifest.
static IEnumerable<ManifestGroup> MergeLocalOverridesOnly(IEnumerable<ManifestGroup> sdkManifest, string packagesJsonPath)
{
var local = JsonSerializer.Deserialize<IEnumerable<ManifestGroup>>(File.ReadAllText(packagesJsonPath))
?? throw new InvalidOperationException($"Unable to parse local packages.json at '{packagesJsonPath}'.");

// Start from LOCAL manifest -> prevents downgrades of existing groups
var map = local.ToDictionary(g => g.Group, g => g, StringComparer.OrdinalIgnoreCase);

// Keep local versions as the baseline to prevent downgrades.
// For groups that have local VersionOverride entries, merge them over the
// existing overrides in the map (local wins). This preserves any SDK overrides
// that local did not specify, and ensures a case-insensitive dictionary.
foreach (var lg in local)
{
if (lg.VersionOverride is { Count: > 0 } && map.TryGetValue(lg.Group, out var existing))
{
// Start from existing overrides so we don't drop SDK-provided keys
var merged = existing.VersionOverride is null
? new Dictionary<string, string>(StringComparer.OrdinalIgnoreCase)
: new Dictionary<string, string>(existing.VersionOverride, StringComparer.OrdinalIgnoreCase);

foreach (var kv in lg.VersionOverride)
{
// Local overrides win (overwrite)
merged[kv.Key] = kv.Value;
}

map[lg.Group] = existing with { VersionOverride = merged };
}
}

// Add brand-new groups that exist in SDK but not locally
foreach (var sg in sdkManifest)
{
if (!map.ContainsKey(sg.Group))
map[sg.Group] = sg;
}

return map.Values;
}

static void WriteIfDifferent(string filePath, string content, ref bool didWriteChanges)
{
if (!File.Exists(filePath) || !File.ReadAllText(filePath).Equals(content))
Expand Down Expand Up @@ -279,22 +325,28 @@
return group.VersionOverride[overrideKey];
}

throw new InvalidOperationException($"No Version Overrides were fround for {groupId} or the key {overrideKey}.");
throw new InvalidOperationException($"No Version Overrides were found for {groupId} or the key {overrideKey}.");
}

static async Task<ManifestGroup> UpdateGroup(ManifestGroup group, NuGetVersion unoVersion, NuGetApiClient client)
{
if (ExcludeConfig.Excluded.Contains(group.Group))
{
Console.WriteLine($"Skipping '{group.Group}' due to exclusion list.");
return group;
}

if (group.Group == "Core")
{
Console.WriteLine($"Setting Core group to: {unoVersion.OriginalVersion}");
return group with { Version = unoVersion };
}
// Skip AndroidX packages to avoid Java misalignment
else if (group.Packages.Any(x => x.StartsWith("Xamarin"))
else if (group.Packages.Any(x => x.StartsWith("Xamarin"))
// Skip Maui on Release branch to avoid AndroidX package misalignment
|| (!unoVersion.IsPreview && group.Group == "Maui"))
{
Console.WriteLine("Skipping " + group.Group + " to avoid Java misalignment.");
Console.WriteLine($"Skipping '{group.Group}' to avoid Java misalignment.");
return group;
}

Expand Down Expand Up @@ -349,21 +401,33 @@
if (group.VersionOverride is not null && group.VersionOverride.Count != 0)
{
var updatedOverrides = new Dictionary<string, string>();
foreach((var key, var versionOverrideString) in group.VersionOverride)
foreach ((var key, var versionOverrideString) in group.VersionOverride)
{
if (!NuGetVersion.TryParse(versionOverrideString, out var versionOverride))
{
Console.WriteLine($"Could not parse version '{versionOverrideString} for {group.Group}.");
Console.WriteLine($"Could not parse version '{versionOverrideString}' for group '{group.Group}', package '{packageId}'.");
continue;
}

version = await client.GetVersionAsync(packageId, versionOverride.IsPreview, noMajorUpgrade, versionOverride.OriginalVersion);
// Explicit VersionOverrides should always allow major upgrades
version = await client.GetVersionAsync(packageId, versionOverride.IsPreview, false, versionOverride.OriginalVersion);
if (version != versionOverrideString)
{
Console.WriteLine($"Updated Version Override for '{group.Group}' - '{key}' to '{version}'.");
}

version = NuGetVersion.Parse(version) < versionOverride ? versionOverrideString : version;
if (!NuGetVersion.TryParse(version, out var parsedVersion))
{
Console.WriteLine($"Could not parse version '{version}' for {group.Group}.");
// Fall back to the local override as a safe default
version = versionOverride.OriginalVersion;
}
else if (parsedVersion < versionOverride)
{
// Keep the local override (e.g., 10.0.0-preview) if the feed returns a lower version (e.g., 9.x)
version = versionOverride.OriginalVersion;
}

updatedOverrides.Add(key, version);
}

Expand All @@ -387,4 +451,4 @@
var attributes = string.Join(" ", attributeList);
return $"<{ItemType} Include=\"{Include}\" {attributes} />";
}
}
}
17 changes: 17 additions & 0 deletions tools/Uno.Sdk.Updater/Utils/Cli.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
namespace Uno.Sdk.Updater.Utils
{
/// Minimal helper to retrieve a CLI argument value (e.g. --exclude-file).
internal static class Cli
{
public static string? GetArgValue(string name)
{
var av = Environment.GetCommandLineArgs();
for (int i = 0; i < av.Length; i++)
{
if (string.Equals(av[i], name, StringComparison.OrdinalIgnoreCase) && i + 1 < av.Length)
return av[i + 1];
}
return null;
}
}
}
Loading