// Cross-Gb5system package promotion — see SwPackagePromote.csproj's own header for context. // // Usage: // dotnet run -- \ // --package-file "/path/to/exported-package.bin" \ // --private-key-file "/path/to/private-key.pem" \ // --connection-string "Data Source=...;Initial Catalog=Gb5system;User Id=...;Password=...;TrustServerCertificate=True" \ // --dbmodel-code ENTITLEMENT_BASELINE_V1 \ // --branch-id 1 --branch-code MAIN \ // --tenant-id 1 --created-by-id -1 \ // [--sequence-in-chain 0] [--dry-run] // // The encrypted package file + private key are the SAME artifacts ExportOfflinePackage/ // SwOfflineApply already produce/consume — no export-side change needed. --dbmodel-code must // already be registered (by code) on the TARGET Gb5system; this tool never creates a DbModel, // since that's a real operational decision (which physical server/database the target uses), // not something to infer from a package file. // // Every write here is checksum-diff-aware (git-style): an object whose content already matches // the target's own current SW.MSWDDLOBJECTHISTORY tip is skipped, not blindly re-inserted — so // re-running this tool against a target it already partially/fully promoted to is safe and cheap. using System.Text.Json; using Microsoft.Data.SqlClient; using SwPackagePromote; var opts = PromoteOptions.Parse(args); if (opts is null) return 1; Console.WriteLine($"Package file : {opts.PackageFile}"); Console.WriteLine($"Target DbModel : code={opts.DbModelCode}"); Console.WriteLine($"Branch : id={opts.BranchId} code={opts.BranchCode}"); Console.WriteLine($"TenantId={opts.TenantId} CreatedById={opts.CreatedById} DryRun={opts.DryRun}"); Console.WriteLine(); var encryptedPackage = await File.ReadAllBytesAsync(opts.PackageFile); var privateKeyPem = await File.ReadAllTextAsync(opts.PrivateKeyFile); Console.WriteLine("Decrypting package..."); var plaintext = OfflinePackageDecryptor.Decrypt(encryptedPackage, privateKeyPem); var bundle = JsonSerializer.Deserialize(plaintext) ?? throw new InvalidOperationException("Package decrypted but content could not be parsed."); Console.WriteLine($"Package: {bundle.PackageCode} (source PackageId={bundle.PackageId}, {bundle.Scripts.Count} script(s))"); if (opts.DryRun) { Console.WriteLine("--dry-run: no database connection opened. Scripts in this package:"); foreach (var s in bundle.Scripts.OrderBy(s => s.Sequence)) Console.WriteLine($" [{s.Sequence}] {s.ObjectName} (ObjectType={s.ObjectType}, {s.SqlScript.Length} chars, checksum={s.Checksum?[..12]}...)"); return 0; } await using var conn = new SqlConnection(opts.ConnectionString); await conn.OpenAsync(); Console.WriteLine($"Connected to {conn.DataSource}/{conn.Database}."); var result = await GovernanceImporter.PromoteAsync(conn, bundle, opts); Console.WriteLine(); Console.WriteLine($"Target DbModelId : {result.DbModelId}"); Console.WriteLine($"Target PackageId : {result.PackageId} ({(result.PackageCreated ? "created" : "already existed, reused")})"); Console.WriteLine($"Objects unchanged (checksum matched target's current tip): {result.UnchangedCount}"); Console.WriteLine($"Objects promoted (new version registered) : {result.PromotedCount}"); Console.WriteLine($"Package links refreshed: {result.LinkedCount}"); Console.WriteLine(); Console.WriteLine("Done. The package is now a real, tracked, Approved/Released entry on this target's own"); Console.WriteLine("SW.MSWUPGRADEPACKAGE — deliver it to client databases the same way any other package is,"); Console.WriteLine("through this target's own ProvisionClientDatabase/RolloutCampaign mechanisms."); return 0; // ============================================================================================ public sealed class PromoteOptions { public required string ConnectionString; public required string PackageFile; public required string PrivateKeyFile; public required string DbModelCode; public required int BranchId; public required string BranchCode; public required int TenantId; public required int CreatedById; public required byte SequenceInChain; public bool DryRun; public static PromoteOptions? Parse(string[] argv) { string? Get(string name) => argv.SkipWhile(a => a != name).Skip(1).FirstOrDefault(); var dryRun = argv.Contains("--dry-run"); var packageFile = Get("--package-file"); var dbModelCode = Get("--dbmodel-code"); if (string.IsNullOrWhiteSpace(packageFile) || !File.Exists(packageFile)) { Console.Error.WriteLine("ERROR: --package-file is required and must point at an existing encrypted package file."); return null; } if (string.IsNullOrWhiteSpace(dbModelCode)) { Console.Error.WriteLine("ERROR: --dbmodel-code is required — the target Gb5system's own already-registered DbModelCode."); return null; } if (!dryRun) { var privateKeyFile = Get("--private-key-file"); var connStr = Get("--connection-string"); if (string.IsNullOrWhiteSpace(privateKeyFile) || !File.Exists(privateKeyFile)) { Console.Error.WriteLine("ERROR: --private-key-file is required (unless --dry-run) and must point at an existing PEM file."); return null; } if (string.IsNullOrWhiteSpace(connStr)) { Console.Error.WriteLine("ERROR: --connection-string is required unless --dry-run is passed."); return null; } } int ParseIntOr(string name, int def) => int.TryParse(Get(name), out var v) ? v : def; return new PromoteOptions { ConnectionString = Get("--connection-string") ?? string.Empty, PackageFile = packageFile, PrivateKeyFile = Get("--private-key-file") ?? string.Empty, DbModelCode = dbModelCode, BranchId = ParseIntOr("--branch-id", 1), BranchCode = Get("--branch-code") ?? "MAIN", TenantId = ParseIntOr("--tenant-id", 1), CreatedById = ParseIntOr("--created-by-id", -1), SequenceInChain = (byte)ParseIntOr("--sequence-in-chain", 0), DryRun = dryRun, }; } }