Skip to content

Commit

Permalink
feat(git): switch to per branch versioning
Browse files Browse the repository at this point in the history
  • Loading branch information
ChecksumDev authored Jul 16, 2024
1 parent 611713c commit 51a418c
Showing 1 changed file with 171 additions and 164 deletions.
335 changes: 171 additions & 164 deletions MBSS/Program.cs
Original file line number Diff line number Diff line change
@@ -1,119 +1,215 @@
using System.Diagnostics;
using System.Diagnostics;
using System.IO.Compression;
using System.Net;
using LibGit2Sharp;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;
using Spectre.Console;

namespace MBSS;

internal class BeatSaberVersion
namespace MBSS
{
[JsonProperty("version")] public string Version { get; set; } = string.Empty;
[JsonProperty("manifest")] public string Manifest { get; set; } = string.Empty;
}
internal class BeatSaberVersion
{
[JsonProperty("version")]
public string Version { get; set; } = string.Empty;

internal abstract class Program
{
public static async Task Main(string[] args)
[JsonProperty("manifest")]
public string Manifest { get; set; } = string.Empty;
}

internal abstract class Program
{
InitConsole();
private const string UserAgent = "MBSS";
private const string DepotDownloaderUrl = "https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest";
private const string GenericStripperUrl = "https://api.github.com/repos/beat-forge/GenericStripper/releases/latest";
private const string EnvFile = ".env";
private const string VersionsFile = "versions.json";
private const string DepotDownloaderExe = "bin/DepotDownloader.exe";
private const string GenericStripperExe = "bin/GenericStripper.exe";
private static readonly string[] RequiredEnvs = { "STEAM_USERNAME", "STEAM_PASSWORD", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GITHUB_TOKEN" };

public static async Task Main(string[] args)
{
InitConsole();

var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", "MBSS");
using var client = new HttpClient();
client.DefaultRequestHeaders.Add("User-Agent", UserAgent);

#region Arguments
HandleResetArgument(args);

if (args.Length > 0 && args[0] == "--reset")
{
AnsiConsole.MarkupLine("[red]Resetting MBSS and deleting all files...[/]");
if (Directory.Exists("versions")) Directory.Delete("versions", true);
if (Directory.Exists("downloads")) Directory.Delete("downloads", true);
if (Directory.Exists("bin")) Directory.Delete("bin", true);
}
if (!await LoadAndValidateVersions(VersionsFile)) return;

if (File.Exists(EnvFile)) await SetupDotEnv();

if (!ValidateEnvironmentVariables(RequiredEnvs)) return;

if (!await PerformPreflightChecks(client)) return;

#endregion
var versions = JsonConvert.DeserializeObject<List<BeatSaberVersion>>(await File.ReadAllTextAsync(VersionsFile));

#region Versions
foreach (var version in versions)

Check warning on line 50 in MBSS/Program.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.

Check warning on line 50 in MBSS/Program.cs

View workflow job for this annotation

GitHub Actions / build

Dereference of a possibly null reference.
{
var downloadPath = Path.Combine("downloads", version.Version);
var versionPath = Path.Combine(Directory.GetCurrentDirectory(), version.Version);

if (Directory.Exists(versionPath))
{
AnsiConsole.MarkupLine($"[yellow]Version {version.Version} already exists, skipping...[/]");
continue;
}

await GetAndStrip(version, downloadPath, versionPath);
AnsiConsole.MarkupLine($"[green]Version {version.Version} stripped![/]");

await CommitAndPushVersion(version);
}
}

private static void InitConsole()
{
AnsiConsole.MarkupLine("[bold yellow]MBSS - Mass Beat Saber Stripper[/]");
AnsiConsole.MarkupLine("[green]This program will download and strip the Beat Saber versions listed in versions.json.[/]");
AnsiConsole.MarkupLine("[green]It will then commit and push the stripped versions to the respective branches of the repository.[/]");
AnsiConsole.MarkupLine("[green]Ensure you are running MBSS inside the root of your desired versions repository![/]");
}

if (!File.Exists("versions.json"))
private static void HandleResetArgument(string[] args)
{
AnsiConsole.MarkupLine("[red]versions.json does not exist![/]");
return;
if (args.Length > 0 && args[0] == "--reset")
{
AnsiConsole.MarkupLine("[red]Resetting MBSS and deleting all files...[/]");
DeleteDirectoryIfExists("downloads");
DeleteDirectoryIfExists("bin");
}
}

var versions =
JsonConvert.DeserializeObject<List<BeatSaberVersion>>(await File.ReadAllTextAsync("versions.json"));
if (versions == null)
private static void DeleteDirectoryIfExists(string path)
{
AnsiConsole.MarkupLine("[red]Failed to parse versions.json![/]");
return;
if (Directory.Exists(path)) Directory.Delete(path, true);
}

#endregion
private static async Task<bool> LoadAndValidateVersions(string versionsFilePath)
{
if (!File.Exists(versionsFilePath))
{
AnsiConsole.MarkupLine("[red]versions.json does not exist![/]");
return false;
}

#region Environment Variables
var versions = JsonConvert.DeserializeObject<List<BeatSaberVersion>>(await File.ReadAllTextAsync(versionsFilePath));
if (versions == null)
{
AnsiConsole.MarkupLine("[red]Failed to parse versions.json![/]");
return false;
}

if (File.Exists(".env")) await SetupDotEnv();
return true;
}

var envs = new[] { "STEAM_USERNAME", "STEAM_PASSWORD", "GIT_AUTHOR_NAME", "GIT_AUTHOR_EMAIL", "GITHUB_TOKEN" };
foreach (var env in envs.Where(env => string.IsNullOrEmpty(Environment.GetEnvironmentVariable(env))))
private static bool ValidateEnvironmentVariables(string[] envs)
{
AnsiConsole.MarkupLine($"[red]Environment variable {env} is not set![/]");
return;
foreach (var env in envs)
{
if (string.IsNullOrEmpty(Environment.GetEnvironmentVariable(env)))
{
AnsiConsole.MarkupLine($"[red]Environment variable {env} is not set![/]");
return false;
}
}
return true;
}

#endregion
private static async Task<bool> PerformPreflightChecks(HttpClient client)
{
if (!Repository.IsValid(Directory.GetCurrentDirectory()))
{
AnsiConsole.MarkupLine("[red]MBSS is not running inside a Git repository, aborting.[/]");
return false;
}

#region Preflight Checks
if (!File.Exists(".gitignore"))
{
AnsiConsole.MarkupLine("[red]Git repository does not have a .gitignore, aborting.[/]");
AnsiConsole.MarkupLine("[red]It is absolutely necessary to ignore the bin/ and downloads/ directories![/]");
return false;
}

if (!Repository.IsValid(Directory.GetCurrentDirectory()))
{
AnsiConsole.MarkupLine("[red]MBSS is not running inside a Git repository, aborting.[/]");
return;
if (!File.Exists(DepotDownloaderExe)) await DownloadAndExtract(client, DepotDownloaderUrl, DepotDownloaderExe);
if (!File.Exists(GenericStripperExe)) await DownloadAndExtract(client, GenericStripperUrl, GenericStripperExe);

EnsureDirectoryExists("downloads");

return true;
}

if (!File.Exists(".gitignore"))
private static void EnsureDirectoryExists(string path)
{
AnsiConsole.MarkupLine("[red]Git repository does not have a .gitignore, aborting.[/]");
AnsiConsole.MarkupLine("[red]It is absolutely necessary to ignore the bin/ and downloads/ directories![/]");
return;
var dir = new DirectoryInfo(path);
if (!dir.Exists) dir.Create();
}

if (!File.Exists("bin/DepotDownloader.exe")) await GetDepotDownloader(client);
if (!File.Exists("bin/GenericStripper.exe")) await GetGenericStripper(client);
private static async Task DownloadAndExtract(HttpClient client, string url, string outputPath)
{
AnsiConsole.MarkupLine($"[yellow]{Path.GetFileName(outputPath)} does not exist, downloading...[/]");

var res = await client.GetAsync(url);
if (res.StatusCode != HttpStatusCode.OK) throw new Exception($"Failed to get {Path.GetFileName(outputPath)} release!");

var downloadDir = new DirectoryInfo("downloads");
var versionsDir = new DirectoryInfo("versions");
var latestRelease = JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(await res.Content.ReadAsStringAsync());
if (latestRelease == null) throw new Exception($"Failed to parse {Path.GetFileName(outputPath)} release!");

if (!downloadDir.Exists) downloadDir.Create();
if (!versionsDir.Exists) versionsDir.Create();
var assets = latestRelease["assets"] as JArray;
var asset = assets?.FirstOrDefault(x => x["name"]?.ToString().Contains("windows-x64") ?? false);
if (asset == null) throw new Exception($"Failed to find a {Path.GetFileName(outputPath)} asset for this system!");

#endregion
var assetRes = await client.GetAsync(asset["browser_download_url"]?.ToString());
if (assetRes.StatusCode != HttpStatusCode.OK) throw new Exception($"Failed to download {Path.GetFileName(outputPath)} asset!");

await using var assetStream = await assetRes.Content.ReadAsStreamAsync();
using var archive = new ZipArchive(assetStream);
archive.ExtractToDirectory(Path.Combine(Directory.GetCurrentDirectory(), "bin"));
}

private static async Task GetAndStrip(BeatSaberVersion version, string downloadPath, string versionPath)
{
await RunProcess(DepotDownloaderExe, $"-app 620980 -depot 620981 -manifest \"{version.Manifest}\" -dir {downloadPath} -remember-password -username \"{Environment.GetEnvironmentVariable("STEAM_USERNAME")}\" -password \"{Environment.GetEnvironmentVariable("STEAM_PASSWORD")}\"");
await RunProcess(GenericStripperExe, $"strip -m beatsaber -p \"{downloadPath}\" -o \"{versionPath}\"");

foreach (var version in versions)
if (Directory.Exists(downloadPath)) Directory.Delete(downloadPath, true);
}

private static async Task RunProcess(string fileName, string arguments)
{
var downloadPath = Path.Combine(downloadDir.FullName, $"{version.Version}");
var versionPath = Path.Combine(versionsDir.FullName, $"{version.Version}");
if (Directory.Exists(versionPath))
var process = new Process
{
AnsiConsole.MarkupLine($"[yellow]Version {version.Version} already exists, skipping...[/]");
continue;
}
StartInfo = new ProcessStartInfo
{
FileName = fileName,
Arguments = arguments,
RedirectStandardOutput = true,
RedirectStandardError = true,
UseShellExecute = false,
CreateNoWindow = true
}
};

await GetAndStrip(version, downloadPath, versionPath);
AnsiConsole.MarkupLine($"[green]Version {version.Version} stripped![/]");
process.Start();
await process.WaitForExitAsync();
}

private static async Task CommitAndPushVersion(BeatSaberVersion version)

Check warning on line 200 in MBSS/Program.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.

Check warning on line 200 in MBSS/Program.cs

View workflow job for this annotation

GitHub Actions / build

This async method lacks 'await' operators and will run synchronously. Consider using the 'await' operator to await non-blocking API calls, or 'await Task.Run(...)' to do CPU-bound work on a background thread.
{
using var repo = new Repository(Directory.GetCurrentDirectory());
var author = new Signature(Environment.GetEnvironmentVariable("GIT_AUTHOR_NAME"),
Environment.GetEnvironmentVariable("GIT_AUTHOR_EMAIL"), DateTimeOffset.Now);
var author = new Signature(Environment.GetEnvironmentVariable("GIT_AUTHOR_NAME"), Environment.GetEnvironmentVariable("GIT_AUTHOR_EMAIL"), DateTimeOffset.Now);

var branchName = $"version/{version.Version}";
var branch = repo.Branches[branchName] ?? repo.CreateBranch(branchName);
Commands.Checkout(repo, branch);

var status = repo.RetrieveStatus();
if (!status.IsDirty) continue; // No changes, skip
if (!status.IsDirty) return; // No changes, skip

Commands.Stage(repo, versionPath);
Commands.Stage(repo, Path.Combine(Directory.GetCurrentDirectory(), version.Version));
repo.Commit($"chore: v{version.Version}", author, author);

var remote = repo.Network.Remotes["origin"];
Expand All @@ -126,107 +222,18 @@ public static async Task Main(string[] args)
}
};

if (remote != null) repo.Network.Push(remote, @"refs/heads/main", options);
if (remote != null) repo.Network.Push(remote, $"refs/heads/{branchName}", options);
}
}

private static async Task GetAndStrip(BeatSaberVersion version, string downloadPath, string versionPath)
{
var depotDownloader = new Process
private static async Task SetupDotEnv()
{
StartInfo =
var dotenv = await File.ReadAllLinesAsync(EnvFile);
foreach (var env in dotenv)
{
FileName = "bin/DepotDownloader.exe",
Arguments =
$"-app 620980 -depot 620981 -manifest \"{version.Manifest}\" -dir {downloadPath} -remember-password -username \"{Environment.GetEnvironmentVariable("STEAM_USERNAME")}\" -password \"{Environment.GetEnvironmentVariable("STEAM_PASSWORD")}\""
var split = env.Split('=', StringSplitOptions.RemoveEmptyEntries);
if (split.Length != 2) continue;
Environment.SetEnvironmentVariable(split[0], split[1]);
}
};

depotDownloader.Start();
await depotDownloader.WaitForExitAsync();

var genericStripper = new Process
{
StartInfo =
{
FileName = "bin/GenericStripper.exe",
Arguments = $"strip -m beatsaber -p \"{downloadPath}\" -o \"{versionPath}\""
}
};

genericStripper.Start();
await genericStripper.WaitForExitAsync();

if (Directory.Exists(downloadPath)) Directory.Delete(downloadPath, true);
}

private static void InitConsole()
{
AnsiConsole.MarkupLine("[bold yellow]MBSS - Mass Beat Saber Stripper[/]");
AnsiConsole.MarkupLine(
"[green]This program will download and strip the Beat Saber versions listed in versions.json.[/]");
AnsiConsole.MarkupLine(
"[green]It will then commit and push the stripped versions to the main branch of the repository.[/]");
AnsiConsole.MarkupLine(
"[green]Ensure you are running MBSS inside the root of your desired versions repository![/]");
}

private static async Task SetupDotEnv()
{
var dotenv = await File.ReadAllLinesAsync(".env");
foreach (var env in dotenv)
{
var split = env.Split('=', StringSplitOptions.RemoveEmptyEntries);
if (split.Length != 2) continue;
Environment.SetEnvironmentVariable(split[0], split[1]);
}
}

private static async Task GetDepotDownloader(HttpClient client)
{
AnsiConsole.MarkupLine("[yellow]DepotDownloader.exe does not exist, downloading...[/]");

var res = await client.GetAsync("https://api.github.com/repos/SteamRE/DepotDownloader/releases/latest");
if (res.StatusCode != HttpStatusCode.OK) throw new Exception("Failed to get DepotDownloader release!");

var latestRelease =
JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(res.Content.ReadAsStringAsync().Result);
if (latestRelease == null) throw new Exception("Failed to parse DepotDownloader release!");

var assets = latestRelease["assets"] as JArray;
var asset = assets?.FirstOrDefault(x => x["name"]?.ToString().Contains("windows-x64") ?? false);
if (asset == null) throw new Exception("Failed to find a DepotDownloader asset for this system!");

var assetRes = client.GetAsync(asset["browser_download_url"]?.ToString()).Result;
if (assetRes.StatusCode != HttpStatusCode.OK)
throw new Exception("Failed to download DepotDownloader asset!");

await using var assetStream = assetRes.Content.ReadAsStreamAsync().Result;
using var archive = new ZipArchive(assetStream);
archive.ExtractToDirectory(Path.Combine(Directory.GetCurrentDirectory(), "bin"));
}

private static async Task GetGenericStripper(HttpClient client)
{
AnsiConsole.MarkupLine("[yellow]GenericStripper.exe does not exist, downloading...[/]");

var res = await client.GetAsync("https://api.github.com/repos/beat-forge/GenericStripper/releases/latest");
if (res.StatusCode != HttpStatusCode.OK) throw new Exception("Failed to get GenericStripper release!");

var latestRelease =
JsonConvert.DeserializeObject<Dictionary<string, dynamic>>(res.Content.ReadAsStringAsync().Result);
if (latestRelease == null) throw new Exception("Failed to parse GenericStripper release!");

var assets = latestRelease["assets"] as JArray;
var asset = assets?.FirstOrDefault(x => x["name"]?.ToString().Contains("GenericStripper") ?? false);
if (asset == null) throw new Exception("Failed to find a GenericStripper asset for this system!");

var assetRes = client.GetAsync(asset["browser_download_url"]?.ToString()).Result;
if (assetRes.StatusCode != HttpStatusCode.OK)
throw new Exception("Failed to download GenericStripper asset!");

await using var assetStream = assetRes.Content.ReadAsStreamAsync().Result;
using var archive = new ZipArchive(assetStream);
archive.ExtractToDirectory(Path.Combine(Directory.GetCurrentDirectory(), "bin"));
}
}

0 comments on commit 51a418c

Please sign in to comment.