feat: integrate Bugtracker module, UpdateService enhancements & Token hierarchy
This commit is contained in:
@@ -0,0 +1,21 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<OutputType>Exe</OutputType>
|
||||
<TargetFramework>net8.0</TargetFramework>
|
||||
<ImplicitUsings>enable</ImplicitUsings>
|
||||
<Nullable>enable</Nullable>
|
||||
<RootNamespace>Deploymentcenter.Packager</RootNamespace>
|
||||
<AssemblyName>pack-and-deploy</AssemblyName>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentFTP" Version="54.1.2" />
|
||||
<PackageReference Include="System.CommandLine" Version="2.0.0-beta4.22272.1" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\Deploymentcenter.Client\Deploymentcenter.Client.csproj" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
@@ -0,0 +1,377 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Diagnostics;
|
||||
using System.Formats.Tar;
|
||||
using System.IO;
|
||||
using System.IO.Compression;
|
||||
using System.Linq;
|
||||
using System.Net.Http;
|
||||
using System.Security.Cryptography;
|
||||
using System.Text;
|
||||
using System.Text.Json;
|
||||
using System.Threading.Tasks;
|
||||
using Deploymentcenter.Client.Models;
|
||||
using FluentFTP;
|
||||
|
||||
namespace Deploymentcenter.Packager
|
||||
{
|
||||
public class PackagerConfig
|
||||
{
|
||||
public string FtpHost { get; set; } = "www531.your-server.de";
|
||||
public int FtpPort { get; set; } = 21;
|
||||
public string FtpUser { get; set; } = "bergisnu_4";
|
||||
public string FtpPass { get; set; } = "o2#M*NN^5EsT";
|
||||
public string FtpRemoteBaseDir { get; set; } = "/public_html/releases";
|
||||
public string ApiBaseUrl { get; set; } = "https://dc.mhdf.de";
|
||||
public List<string> ExcludePatterns { get; set; } = new List<string>
|
||||
{
|
||||
"*.pdb", "*.xml", "appsettings.Development.json", "appsettings.Staging.json", "*.log", "logs/*"
|
||||
};
|
||||
}
|
||||
|
||||
class Program
|
||||
{
|
||||
static async Task<int> Main(string[] args)
|
||||
{
|
||||
Console.WriteLine("=================================================");
|
||||
Console.WriteLine(" Deploymentcenter Packager & Deploy Tool v1.0 ");
|
||||
Console.WriteLine("=================================================");
|
||||
|
||||
string project = GetArg(args, "--project", "-p") ?? "myapp";
|
||||
string version = GetArg(args, "--version", "-v") ?? "1.0.0";
|
||||
string channel = GetArg(args, "--channel", "-c") ?? "prod";
|
||||
string publishDir = GetArg(args, "--publish-dir", "-d") ?? Directory.GetCurrentDirectory();
|
||||
string changelog = GetArg(args, "--changelog") ?? $"Release v{version}";
|
||||
bool isCritical = HasFlag(args, "--critical");
|
||||
string configFile = GetArg(args, "--config") ?? Path.Combine(AppDomain.CurrentDomain.BaseDirectory, "packager.config.json");
|
||||
|
||||
PackagerConfig config = LoadConfig(configFile);
|
||||
|
||||
// Override config with explicit CLI args if provided
|
||||
string ftpHost = GetArg(args, "--ftp-host") ?? config.FtpHost;
|
||||
string ftpUser = GetArg(args, "--ftp-user") ?? config.FtpUser;
|
||||
string ftpPass = GetArg(args, "--ftp-pass") ?? config.FtpPass;
|
||||
string remoteBase = GetArg(args, "--remote-dir") ?? config.FtpRemoteBaseDir;
|
||||
|
||||
publishDir = Path.GetFullPath(publishDir);
|
||||
if (!Directory.Exists(publishDir))
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Red;
|
||||
Console.WriteLine($"[ERROR] Publish directory does not exist: {publishDir}");
|
||||
Console.ResetColor();
|
||||
return 1;
|
||||
}
|
||||
|
||||
Console.WriteLine($"[INFO] Packaging Project : {project}");
|
||||
Console.WriteLine($"[INFO] Version : {version}");
|
||||
Console.WriteLine($"[INFO] Channel : {channel}");
|
||||
Console.WriteLine($"[INFO] Publish Directory : {publishDir}");
|
||||
|
||||
// 1. Gather files and filter exclusions
|
||||
var allFiles = Directory.GetFiles(publishDir, "*", SearchOption.AllDirectories);
|
||||
var filteredFiles = new List<string>();
|
||||
|
||||
foreach (var file in allFiles)
|
||||
{
|
||||
string relPath = Path.GetRelativePath(publishDir, file).Replace('\\', '/');
|
||||
if (IsExcluded(relPath, config.ExcludePatterns))
|
||||
{
|
||||
Console.WriteLine($" [EXCLUDED] {relPath}");
|
||||
continue;
|
||||
}
|
||||
filteredFiles.Add(file);
|
||||
}
|
||||
|
||||
Console.WriteLine($"[INFO] Total files selected for package: {filteredFiles.Count}");
|
||||
|
||||
// 2. Prepare staging directory
|
||||
string outputTempDir = Path.Combine(Path.GetTempPath(), "dc_packager_" + Guid.NewGuid().ToString("N"));
|
||||
Directory.CreateDirectory(outputTempDir);
|
||||
|
||||
string packageTarGzPath = Path.Combine(outputTempDir, "package.tar.gz");
|
||||
string manifestJsonPath = Path.Combine(outputTempDir, "manifest.json");
|
||||
string sha256FilePath = Path.Combine(outputTempDir, "package.tar.gz.sha256");
|
||||
|
||||
string gitCommit = GetGitCommitLong();
|
||||
string gitCommitShort = GetGitCommitShort();
|
||||
string buildDateUtc = DateTime.UtcNow.ToString("o");
|
||||
|
||||
// Build Manifest
|
||||
var packageManifest = new PackageManifest
|
||||
{
|
||||
ProjectId = project,
|
||||
Version = version,
|
||||
Channel = channel,
|
||||
BuildDate = buildDateUtc,
|
||||
GitCommit = gitCommit,
|
||||
GitCommitShort = gitCommitShort,
|
||||
Changelog = changelog,
|
||||
Files = new List<PackageFileEntry>()
|
||||
};
|
||||
|
||||
foreach (var file in filteredFiles)
|
||||
{
|
||||
string relPath = Path.GetRelativePath(publishDir, file).Replace('\\', '/');
|
||||
long size = new FileInfo(file).Length;
|
||||
string hash = ComputeSha256(file);
|
||||
packageManifest.Files.Add(new PackageFileEntry
|
||||
{
|
||||
Path = relPath,
|
||||
SizeBytes = size,
|
||||
Sha256 = hash
|
||||
});
|
||||
}
|
||||
|
||||
// Write manifest.json
|
||||
string manifestJson = JsonSerializer.Serialize(packageManifest, new JsonSerializerOptions { WriteIndented = true });
|
||||
await File.WriteAllTextAsync(manifestJsonPath, manifestJson);
|
||||
|
||||
// 3. Create package.tar.gz
|
||||
Console.WriteLine("[INFO] Creating package.tar.gz archive...");
|
||||
string archiveStaging = Path.Combine(outputTempDir, "archive_root");
|
||||
Directory.CreateDirectory(archiveStaging);
|
||||
|
||||
foreach (var file in filteredFiles)
|
||||
{
|
||||
string relPath = Path.GetRelativePath(publishDir, file);
|
||||
string targetFile = Path.Combine(archiveStaging, relPath);
|
||||
Directory.CreateDirectory(Path.GetDirectoryName(targetFile)!);
|
||||
File.Copy(file, targetFile, true);
|
||||
}
|
||||
|
||||
// Also place manifest.json inside archive root
|
||||
File.Copy(manifestJsonPath, Path.Combine(archiveStaging, "manifest.json"), true);
|
||||
|
||||
// Compress to tar.gz using System.Formats.Tar + GZipStream
|
||||
using (var fs = File.Create(packageTarGzPath))
|
||||
using (var gz = new GZipStream(fs, CompressionLevel.Optimal))
|
||||
{
|
||||
TarFile.CreateFromDirectory(archiveStaging, gz, includeBaseDirectory: false);
|
||||
}
|
||||
|
||||
long packageSizeBytes = new FileInfo(packageTarGzPath).Length;
|
||||
string packageSha256 = ComputeSha256(packageTarGzPath);
|
||||
await File.WriteAllTextAsync(sha256FilePath, packageSha256);
|
||||
|
||||
Console.WriteLine($"[SUCCESS] Package created successfully! ({packageSizeBytes} bytes)");
|
||||
Console.WriteLine($"[INFO] Package SHA256: {packageSha256}");
|
||||
|
||||
// 4. FTP Upload to LEMP Release Server
|
||||
string remoteChannelPath = $"{remoteBase.TrimEnd('/')}/{project}/{channel}";
|
||||
string remoteVersionPath = $"{remoteChannelPath}/{version}";
|
||||
|
||||
Console.WriteLine($"[INFO] Uploading via FTP to {ftpHost}:{config.FtpPort} ({remoteVersionPath})...");
|
||||
|
||||
try
|
||||
{
|
||||
using var ftp = new AsyncFtpClient(ftpHost, ftpUser, ftpPass, config.FtpPort);
|
||||
await ftp.Connect();
|
||||
|
||||
await ftp.CreateDirectory(remoteVersionPath);
|
||||
|
||||
// Upload package.tar.gz, package.tar.gz.sha256, and manifest.json
|
||||
await ftp.UploadFile(packageTarGzPath, $"{remoteVersionPath}/package.tar.gz", FtpRemoteExists.Overwrite);
|
||||
await ftp.UploadFile(sha256FilePath, $"{remoteVersionPath}/package.tar.gz.sha256", FtpRemoteExists.Overwrite);
|
||||
await ftp.UploadFile(manifestJsonPath, $"{remoteVersionPath}/manifest.json", FtpRemoteExists.Overwrite);
|
||||
|
||||
Console.WriteLine("[SUCCESS] Files uploaded to version directory!");
|
||||
|
||||
// 5. Update remote channel latest.json
|
||||
string remoteLatestJsonPath = $"{remoteChannelPath}/latest.json";
|
||||
ReleaseManifest channelManifest = new ReleaseManifest
|
||||
{
|
||||
ProjectId = project,
|
||||
Channel = channel,
|
||||
Versions = new List<VersionInfo>()
|
||||
};
|
||||
|
||||
// Read existing latest.json if present on FTP
|
||||
if (await ftp.FileExists(remoteLatestJsonPath))
|
||||
{
|
||||
string tempLatestLocal = Path.Combine(outputTempDir, "existing_latest.json");
|
||||
var status = await ftp.DownloadFile(tempLatestLocal, remoteLatestJsonPath, FtpLocalExists.Overwrite);
|
||||
if (status == FtpStatus.Success && File.Exists(tempLatestLocal))
|
||||
{
|
||||
try
|
||||
{
|
||||
string existingJson = await File.ReadAllTextAsync(tempLatestLocal);
|
||||
var existingManifest = JsonSerializer.Deserialize<ReleaseManifest>(existingJson);
|
||||
if (existingManifest != null && existingManifest.Versions != null)
|
||||
{
|
||||
channelManifest.Versions = existingManifest.Versions;
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
}
|
||||
|
||||
// Construct new version info
|
||||
string packagePublicUrl = $"{config.ApiBaseUrl.TrimEnd('/')}/releases/{project}/{channel}/{version}/package.tar.gz";
|
||||
|
||||
var newVersionInfo = new VersionInfo
|
||||
{
|
||||
Version = version,
|
||||
BuildDate = buildDateUtc,
|
||||
GitCommit = gitCommit,
|
||||
GitCommitShort = gitCommitShort,
|
||||
PackageUrl = packagePublicUrl,
|
||||
Sha256 = packageSha256,
|
||||
SizeBytes = packageSizeBytes,
|
||||
Changelog = changelog,
|
||||
IsCritical = isCritical
|
||||
};
|
||||
|
||||
// Remove duplicate version entry if re-publishing same version
|
||||
channelManifest.Versions.RemoveAll(v => v.Version.Equals(version, StringComparison.OrdinalIgnoreCase));
|
||||
channelManifest.Versions.Insert(0, newVersionInfo);
|
||||
|
||||
// Keep last 15 releases
|
||||
if (channelManifest.Versions.Count > 15)
|
||||
{
|
||||
channelManifest.Versions = channelManifest.Versions.Take(15).ToList();
|
||||
}
|
||||
|
||||
channelManifest.Latest = channelManifest.Versions.FirstOrDefault();
|
||||
|
||||
string updatedLatestJson = JsonSerializer.Serialize(channelManifest, new JsonSerializerOptions { WriteIndented = true });
|
||||
string localLatestJsonPath = Path.Combine(outputTempDir, "latest.json");
|
||||
await File.WriteAllTextAsync(localLatestJsonPath, updatedLatestJson);
|
||||
|
||||
await ftp.UploadFile(localLatestJsonPath, remoteLatestJsonPath, FtpRemoteExists.Overwrite);
|
||||
Console.WriteLine("[SUCCESS] Updated latest.json on FTP server!");
|
||||
|
||||
await ftp.Disconnect();
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
Console.ForegroundColor = ConsoleColor.Yellow;
|
||||
Console.WriteLine($"[WARNING] FTP upload encountered error: {ex.Message}");
|
||||
Console.ResetColor();
|
||||
}
|
||||
|
||||
// 6. Notify Deploymentcenter Web API
|
||||
try
|
||||
{
|
||||
using var http = new HttpClient();
|
||||
string apiPublishUrl = $"{config.ApiBaseUrl.TrimEnd('/')}/api/updateservice/v1/index.php";
|
||||
var payload = new
|
||||
{
|
||||
action = "publish_release",
|
||||
product_slug = project,
|
||||
version = version,
|
||||
channel = channel,
|
||||
release_notes = changelog,
|
||||
download_url = $"{config.ApiBaseUrl.TrimEnd('/')}/releases/{project}/{channel}/{version}/package.tar.gz",
|
||||
sha256_hash = packageSha256,
|
||||
git_commit = gitCommitShort,
|
||||
size_bytes = packageSizeBytes,
|
||||
is_critical = isCritical ? 1 : 0
|
||||
};
|
||||
|
||||
string jsonContent = JsonSerializer.Serialize(payload);
|
||||
var content = new StringContent(jsonContent, Encoding.UTF8, "application/json");
|
||||
var response = await http.PostAsync(apiPublishUrl, content);
|
||||
if (response.IsSuccessStatusCode)
|
||||
{
|
||||
Console.WriteLine("[SUCCESS] Notified Deploymentcenter Web API of new release.");
|
||||
}
|
||||
}
|
||||
catch { }
|
||||
|
||||
// Cleanup temp
|
||||
try { Directory.Delete(outputTempDir, true); } catch { }
|
||||
|
||||
Console.ForegroundColor = ConsoleColor.Green;
|
||||
Console.WriteLine($"\n[FINISHED] Release v{version} for {project} ({channel}) successfully published!");
|
||||
Console.ResetColor();
|
||||
return 0;
|
||||
}
|
||||
|
||||
static PackagerConfig LoadConfig(string path)
|
||||
{
|
||||
if (File.Exists(path))
|
||||
{
|
||||
try
|
||||
{
|
||||
string json = File.ReadAllText(path);
|
||||
var cfg = JsonSerializer.Deserialize<PackagerConfig>(json);
|
||||
if (cfg != null) return cfg;
|
||||
}
|
||||
catch { }
|
||||
}
|
||||
return new PackagerConfig();
|
||||
}
|
||||
|
||||
static bool IsExcluded(string relPath, List<string> patterns)
|
||||
{
|
||||
string fileName = Path.GetFileName(relPath);
|
||||
foreach (var pattern in patterns)
|
||||
{
|
||||
if (pattern.StartsWith("*."))
|
||||
{
|
||||
string ext = pattern.Substring(1);
|
||||
if (fileName.EndsWith(ext, StringComparison.OrdinalIgnoreCase)) return true;
|
||||
}
|
||||
else if (pattern.Equals(relPath, StringComparison.OrdinalIgnoreCase) || pattern.Equals(fileName, StringComparison.OrdinalIgnoreCase))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
static string ComputeSha256(string file)
|
||||
{
|
||||
using var sha256 = SHA256.Create();
|
||||
using var stream = File.OpenRead(file);
|
||||
byte[] hash = sha256.ComputeHash(stream);
|
||||
return BitConverter.ToString(hash).Replace("-", "").ToLowerInvariant();
|
||||
}
|
||||
|
||||
static string? GetArg(string[] args, string flagLong, string? flagShort = null)
|
||||
{
|
||||
for (int i = 0; i < args.Length - 1; i++)
|
||||
{
|
||||
if (args[i].Equals(flagLong, StringComparison.OrdinalIgnoreCase) ||
|
||||
(flagShort != null && args[i].Equals(flagShort, StringComparison.OrdinalIgnoreCase)))
|
||||
{
|
||||
return args[i + 1];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
static bool HasFlag(string[] args, string flag)
|
||||
{
|
||||
return args.Any(a => a.Equals(flag, StringComparison.OrdinalIgnoreCase));
|
||||
}
|
||||
|
||||
static string GetGitCommitLong()
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("git", "rev-parse HEAD") { RedirectStandardOutput = true, UseShellExecute = false };
|
||||
using var p = Process.Start(psi);
|
||||
string outStr = p?.StandardOutput.ReadToEnd().Trim() ?? "";
|
||||
p?.WaitForExit();
|
||||
if (!string.IsNullOrEmpty(outStr)) return outStr;
|
||||
}
|
||||
catch { }
|
||||
return "UNKNOWN_COMMIT";
|
||||
}
|
||||
|
||||
static string GetGitCommitShort()
|
||||
{
|
||||
try
|
||||
{
|
||||
var psi = new ProcessStartInfo("git", "rev-parse --short HEAD") { RedirectStandardOutput = true, UseShellExecute = false };
|
||||
using var p = Process.Start(psi);
|
||||
string outStr = p?.StandardOutput.ReadToEnd().Trim() ?? "";
|
||||
p?.WaitForExit();
|
||||
if (!string.IsNullOrEmpty(outStr)) return outStr;
|
||||
}
|
||||
catch { }
|
||||
return "UNKNOWN";
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"ftpHost": "www531.your-server.de",
|
||||
"ftpPort": 21,
|
||||
"ftpUser": "bergisnu_4",
|
||||
"ftpPass": "o2#M*NN^5EsT",
|
||||
"ftpRemoteBaseDir": "/public_html/releases",
|
||||
"apiBaseUrl": "https://dc.mhdf.de",
|
||||
"excludePatterns": [
|
||||
"*.pdb",
|
||||
"*.xml",
|
||||
"appsettings.Development.json",
|
||||
"appsettings.Staging.json",
|
||||
"*.log",
|
||||
"logs/**",
|
||||
"scratch/**",
|
||||
"*.tmp"
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user