Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: release automation #39

Merged
merged 12 commits into from
Dec 20, 2024
21 changes: 21 additions & 0 deletions .github/scripts/get-unity-package-names.csx
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
#!/usr/bin/env dotnet-script

#r "nuget: Newtonsoft.Json, 13.0.0"

using Newtonsoft.Json.Linq;

var srcPath = Path.GetFullPath("src");
var packages = Directory.EnumerateFiles(srcPath, "package.json", SearchOption.AllDirectories)
.Select(file =>
{
var content = File.ReadAllText(file);
var json = JObject.Parse(content);
return json["name"]?.ToString();
})
.Where(name => !string.IsNullOrEmpty(name))
.ToList();

foreach (var package in packages)
{
Console.WriteLine(package);
}
28 changes: 28 additions & 0 deletions .github/scripts/get-version.csx
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
#!/usr/bin/env dotnet-script

using System.Xml.Linq;

string GetVersion()
{
var currentDir = Directory.GetCurrentDirectory();
var rootDir = Path.GetFullPath(Path.Combine(currentDir, "src"));
var buildPropsPath = Path.Combine(rootDir, "Directory.Build.props");

if (!File.Exists(buildPropsPath))
throw new Exception($"Could not find Directory.Build.props at {buildPropsPath}");

var doc = XDocument.Load(buildPropsPath);

var version = doc.Descendants()
.Elements()
.FirstOrDefault(e => e.Name.LocalName == "DefaultVersion")
?.Value;

if (string.IsNullOrEmpty(version))
throw new Exception("DefaultVersion not found in Directory.Build.props");

return version;
}

var version = GetVersion();
Console.WriteLine(version);
29 changes: 29 additions & 0 deletions .github/scripts/logging.csx
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
public static class Log
{
public static void Header(string message)
{
Console.WriteLine();
Console.WriteLine($"=== {message} ===");
}

public static void SubHeader(string message)
{
Console.WriteLine();
Console.WriteLine($"--- {message} ---");
}

public static void Info(string message)
{
Console.WriteLine($" {message}");
}

public static void Success(string message)
{
Console.WriteLine($" ✓ {message}");
}

public static void Warning(string message)
{
Console.WriteLine($" ⚠ {message}");
}
}
310 changes: 310 additions & 0 deletions .github/scripts/sync-unity-version.csx
Original file line number Diff line number Diff line change
@@ -0,0 +1,310 @@
#!/usr/bin/env dotnet-script

// This scripts is used to sync the version of Unity packages and sample apps with the version of NuGet packages.
// It gets the version from the Directory.Build.props file and updates
// - the version in C# where the version field is marked with [VersionMarker] attribute
// - the version in ProjectSettings.asset of the Unity sample app
// - the version and dependency versions in package.json of all Unity packages
// - the version in packages-lock.json of all Unity packages in the sample app

#load "get-version.csx"
#load "logging.csx"

#r "nuget: Microsoft.CodeAnalysis.CSharp, 4.12.0"
#r "nuget: Newtonsoft.Json, 13.0.0"

using System.Xml.Linq;
using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using Microsoft.CodeAnalysis.CSharp.Syntax;
using Microsoft.CodeAnalysis.Text;
using System.Text.RegularExpressions;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

var srcPath = Path.GetFullPath("src");
Log.Header("Starting Unity Version Sync");
Log.Info($"Source path: {srcPath}");

var newVersion = GetVersion();
Log.Info($"Target version: {newVersion}");

// Update version fields in the code
Log.SubHeader("Updating Version Fields in Code");
var versionFields = await FindVersionFieldsAsync(srcPath);
if (!versionFields.Any())
{
Log.Warning("No version fields found");
}
foreach (var field in versionFields)
{
Log.Info($"Found: {field.fieldName} = {field.version} in {Path.GetRelativePath(".", field.filePath)}");
await UpdateVersionAsync(field.filePath, field.fieldName, newVersion);
Log.Success($"Updated {field.fieldName} to {newVersion}");
}

// Update Unity sample app version
Log.SubHeader("Updating Unity Sample App Version");
var projectSettingsPath = Path.GetFullPath("sample/Reown.AppKit.Unity/ProjectSettings/ProjectSettings.asset");
if (File.Exists(projectSettingsPath))
{
await UpdateProjectSettingsVersionAsync(projectSettingsPath, newVersion);
Log.Success($"Updated ProjectSettings.asset to {newVersion}");
}
else
{
Log.Warning($"ProjectSettings.asset not found at {projectSettingsPath}");
}

// Update Unity packages
Log.SubHeader("Updating Package Versions");
await UpdatePackageVersionsAsync(srcPath, newVersion);
await UpdatePackagesLockVersionsAsync(Path.GetFullPath("sample"), newVersion);

private static async Task<IEnumerable<(string filePath, string fieldName, string version)>> FindVersionFieldsAsync(string path)
{
var results = new List<(string filePath, string fieldName, string version)>();
foreach (var file in Directory.EnumerateFiles(path, "*.cs", SearchOption.AllDirectories))
{
var sourceText = SourceText.From(await File.ReadAllTextAsync(file));
var tree = CSharpSyntaxTree.ParseText(sourceText);
var root = await tree.GetRootAsync();

var fields = root.DescendantNodes()
.OfType<FieldDeclarationSyntax>()
.Where(field => field.AttributeLists
.SelectMany(list => list.Attributes)
.Any(attr => attr.Name.ToString() == "VersionMarker"));

foreach (var field in fields)
{
var variable = field.Declaration.Variables.First();
if (variable.Initializer?.Value is LiteralExpressionSyntax literal)
{
results.Add((
filePath: file,
fieldName: variable.Identifier.Text,
version: literal.Token.ValueText
));
}
}
}

return results;
}

async Task UpdateVersionAsync(string filePath, string fieldName, string newVersion)
{
var sourceText = SourceText.From(await File.ReadAllTextAsync(filePath));
var tree = CSharpSyntaxTree.ParseText(sourceText);
var root = await tree.GetRootAsync();

// Find the specific field to update
var fieldToUpdate = root.DescendantNodes()
.OfType<FieldDeclarationSyntax>()
.First(field =>
field.AttributeLists
.SelectMany(list => list.Attributes)
.Any(attr => attr.Name.ToString() == "VersionMarker") &&
field.Declaration.Variables
.Any(v => v.Identifier.Text == fieldName));

var variable = fieldToUpdate.Declaration.Variables.First();

if (variable.Initializer?.Value is LiteralExpressionSyntax literal)
{
var currentValue = literal.Token.ValueText;
var newValue = UpdateVersionString(currentValue, newVersion);

// Create new string literal while preserving trivia
var newLiteral = SyntaxFactory.LiteralExpression(
SyntaxKind.StringLiteralExpression,
SyntaxFactory.Literal(literal.Token.LeadingTrivia, $"\"{newValue}\"", newValue, literal.Token.TrailingTrivia)
).WithTriviaFrom(literal);

// Create new variable with updated initializer, preserving trivia
var newVariable = variable
.WithInitializer(variable.Initializer.WithValue(newLiteral))
.WithTriviaFrom(variable);

// Create new field declaration preserving trivia
var newField = fieldToUpdate
.WithDeclaration(fieldToUpdate.Declaration.WithVariables(
SyntaxFactory.SingletonSeparatedList(newVariable)))
.WithTriviaFrom(fieldToUpdate);

// Replace the old field with the new one
var newRoot = root.ReplaceNode(fieldToUpdate, newField);

// Write back preserving original encoding and line endings
await File.WriteAllTextAsync(filePath, newRoot.ToFullString());
}
}

string UpdateVersionString(string currentValue, string newVersion)
{
var semVerPattern = new Regex(
@"^(?<prefix>.*?)?(?:v)?(?<version>\d+\.\d+\.\d+)$",
RegexOptions.Compiled
);
var match = semVerPattern.Match(currentValue);
if (!match.Success)
{
throw new ArgumentException(
$"Current value '{currentValue}' doesn't contain a valid version number pattern");
}

var prefix = match.Groups["prefix"].Value;
var hasV = currentValue.Contains('v') &&
(prefix.Length == 0 || !prefix.EndsWith('v'));

return $"{prefix}{(hasV ? "v" : "")}{newVersion}";
}

async Task UpdateProjectSettingsVersionAsync(string filePath, string newVersion)
{
if (!File.Exists(filePath))
{
Log.Warning($"ProjectSettings.asset not found at {filePath}");
return;
}

var content = await File.ReadAllTextAsync(filePath);

var pattern = @"(bundleVersion:)\s*(.+)";
var replacement = $"$1 {newVersion}";

var updatedContent = Regex.Replace(content, pattern, replacement);

await File.WriteAllTextAsync(filePath, updatedContent);
Log.Success($"Updated bundleVersion in ProjectSettings.asset to {newVersion}");
}

async Task UpdatePackageVersionsAsync(string srcPath, string newVersion)
{
var files = Directory.EnumerateFiles(srcPath, "package.json", SearchOption.AllDirectories).ToList();
if (!files.Any())
{
Log.Warning($"No package.json files found in {Path.GetRelativePath(".", srcPath)}");
return;
}

foreach (var file in files)
{
Log.Info($"Processing: {Path.GetRelativePath(".", file)}");
var content = await File.ReadAllTextAsync(file);
var packageJson = JObject.Parse(content);
var updated = false;

using var jsonWriter = new StringWriter();
using var writer = new JsonTextWriter(jsonWriter)
{
Formatting = Formatting.Indented
};
using var jsonReader = new JsonTextReader(new StringReader(content));

while (jsonReader.Read())
{
if (jsonReader.TokenType == JsonToken.PropertyName)
{
var propertyName = jsonReader.Value?.ToString();
writer.WriteToken(jsonReader.TokenType, jsonReader.Value);

jsonReader.Read();
if (propertyName == "version" || (propertyName?.StartsWith("com.reown.") ?? false))
{
if (jsonReader.Value?.ToString() != newVersion)
{
writer.WriteValue(newVersion);
updated = true;
continue;
}
}
writer.WriteToken(jsonReader.TokenType, jsonReader.Value);
}
else
{
writer.WriteToken(jsonReader.TokenType, jsonReader.Value);
}
}

if (updated)
{
await File.WriteAllTextAsync(file, jsonWriter.ToString());
Log.Success($"Updated versions in {file}");
}
}
}

async Task UpdatePackagesLockVersionsAsync(string basePath, string newVersion)
{
var files = Directory.EnumerateFiles(basePath, "packages-lock.json", SearchOption.AllDirectories).ToList();
if (!files.Any())
{
Log.Warning($"No packages-lock.json files found in {Path.GetRelativePath(".", basePath)}");
return;
}

foreach (var file in files)
{
Log.Info($"Processing: {Path.GetRelativePath(".", file)}");
var content = await File.ReadAllTextAsync(file);
var packageJson = JObject.Parse(content);
var updated = false;
var updatedPackages = new List<(string package, string from, string to)>();

// Update versions in dependencies
if (packageJson.TryGetValue("dependencies", out var dependencies) && dependencies is JObject depsObj)
{
foreach (var dep in depsObj.Properties())
{
if (dep.Name.StartsWith("com.reown."))
{
var depObj = dep.Value as JObject;
if (depObj != null)
{
// Update dependencies of the package
if (depObj.TryGetValue("dependencies", out var nestedDeps) && nestedDeps is JObject nestedDepsObj)
{
foreach (var nestedDep in nestedDepsObj.Properties())
{
if (nestedDep.Name.StartsWith("com.reown."))
{
var oldVersion = nestedDepsObj[nestedDep.Name].ToString();
if (oldVersion != newVersion)
{
nestedDepsObj[nestedDep.Name] = newVersion;
updated = true;
updatedPackages.Add((nestedDep.Name, oldVersion, newVersion));
}
}
}
}

// Update the package version itself if it's not a file reference
if (depObj.TryGetValue("version", out var versionToken))
{
var currentVersion = versionToken.ToString();
if (!currentVersion.StartsWith("file:") && currentVersion != newVersion)
{
depObj["version"] = newVersion;
updated = true;
updatedPackages.Add((dep.Name, currentVersion, newVersion));
}
}
}
}
}
}

if (updated)
{
await File.WriteAllTextAsync(file, packageJson.ToString(Formatting.Indented));
Log.Success($"Updated {updatedPackages.Count} package versions:");
foreach (var (package, from, to) in updatedPackages)
{
Log.Info($" {package}: {from} → {to}");
}
}
}
}
Loading
Loading