diff --git a/.github/actions/sdk-update/action.yml b/.github/actions/sdk-update/action.yml index b51fc0fcd..3e69c9e4c 100644 --- a/.github/actions/sdk-update/action.yml +++ b/.github/actions/sdk-update/action.yml @@ -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" @@ -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'] }}" diff --git a/.github/sdk-update-exclude.json b/.github/sdk-update-exclude.json new file mode 100644 index 000000000..a5d9981fb --- /dev/null +++ b/.github/sdk-update-exclude.json @@ -0,0 +1,6 @@ +{ + "exclude": [ + "SvgSkia" + ] +} + diff --git a/tools/Uno.Sdk.Updater/Config/ExcludeConfig.cs b/tools/Uno.Sdk.Updater/Config/ExcludeConfig.cs new file mode 100644 index 000000000..c21b2400b --- /dev/null +++ b/tools/Uno.Sdk.Updater/Config/ExcludeConfig.cs @@ -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> _excluded = new(() => Load()); + public static HashSet Excluded => _excluded.Value; + + private static HashSet Load() + { + var set = new HashSet(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; + } + } +} diff --git a/tools/Uno.Sdk.Updater/Program.cs b/tools/Uno.Sdk.Updater/Program.cs index bc084bd4d..3f6803855 100644 --- a/tools/Uno.Sdk.Updater/Program.cs +++ b/tools/Uno.Sdk.Updater/Program.cs @@ -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"; @@ -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 @@ -53,7 +57,7 @@ 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"]; @@ -88,13 +92,12 @@ var inputManifest = await JsonSerializer.DeserializeAsync>(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(); - foreach(var group in inputManifest) + foreach (var group in inputManifest) { var updated = await UpdateGroup(group, unoVersion, client); manifest.Add(updated); @@ -135,7 +138,7 @@ var manifestJson = File.ReadAllText(packagesJsonPath); var manifest = JsonSerializer.Deserialize>(manifestJson) ?? []; - foreach(var group in manifest) + foreach (var group in manifest) { readMe = Regex.Replace(readMe, Regex.Escape($"${group.Group}$"), group.Version); } @@ -177,6 +180,49 @@ static IEnumerable MergeLocalManifest(IEnumerable 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 MergeLocalOverridesOnly(IEnumerable sdkManifest, string packagesJsonPath) +{ + var local = JsonSerializer.Deserialize>(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(StringComparer.OrdinalIgnoreCase) + : new Dictionary(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)) @@ -279,22 +325,28 @@ static string GetManifestGroupVersionOverride(IEnumerable manifes 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 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; } @@ -349,21 +401,33 @@ static async Task UpdateGroup(ManifestGroup group, NuGetVersion u if (group.VersionOverride is not null && group.VersionOverride.Count != 0) { var updatedOverrides = new Dictionary(); - 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); } @@ -387,4 +451,4 @@ public string ToXml() var attributes = string.Join(" ", attributeList); return $"<{ItemType} Include=\"{Include}\" {attributes} />"; } -} \ No newline at end of file +} diff --git a/tools/Uno.Sdk.Updater/Utils/Cli.cs b/tools/Uno.Sdk.Updater/Utils/Cli.cs new file mode 100644 index 000000000..01bf115ad --- /dev/null +++ b/tools/Uno.Sdk.Updater/Utils/Cli.cs @@ -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; + } + } +}