diff --git a/demodata/example.zip b/demodata/example.zip new file mode 100644 index 0000000..119e684 Binary files /dev/null and b/demodata/example.zip differ diff --git a/src/GitHubReleaser/GitHubReleaser.args.json b/src/GitHubReleaser/GitHubReleaser.args.json index 56c9eb9..b7c4fab 100644 --- a/src/GitHubReleaser/GitHubReleaser.args.json +++ b/src/GitHubReleaser/GitHubReleaser.args.json @@ -6,6 +6,18 @@ "Id": "a6460bf8-07be-48ea-b2f4-11bca9e86648", "Command": "Test1", "Items": [ + { + "Id": "2762c393-a835-4357-8543-d0c1c1d45fdb", + "Command": "/github-repo:\"Suplanus/GitHubReleaser\"" + }, + { + "Id": "82ec0542-defa-498f-814d-b85df87b49b1", + "Command": "/github-token:\"Fill out Secrets.cs\"" + }, + { + "Id": "3735ff7a-efec-45ad-a02e-1f2fe65e5926", + "Command": "/file-for-version:\".\\GitHubReleaser.exe\"" + }, { "Id": "912b3373-c011-40ab-857c-1aa38c0b5042", "Command": "/create-changelog-file:\"true\"" @@ -16,7 +28,7 @@ }, { "Id": "3c1e4015-2cf8-4927-b596-4cb39742ddcb", - "Command": "/release-attachments:\"TODO\" \"TODO\"" + "Command": "/release-attachments:\".\\..\\..\\..\\..\\..\\demodata\\example.zip\"" }, { "Id": "29245bfd-5851-4c3d-9089-e6648a84e599", @@ -24,23 +36,15 @@ }, { "Id": "1da6e697-648d-495f-b2a4-27447f58ba81", - "Command": "/issue-labels:\"bug;Bug\" \"feature;Feature\"" + "Command": "/issue-labels:\"bug;Bug\" \"enhancement;Enhancement\"" }, { "Id": "a01d7c65-8590-4015-a84e-da00eb1e8ecd", "Command": "/pre-release:\"false\"" }, { - "Id": "3735ff7a-efec-45ad-a02e-1f2fe65e5926", - "Command": "/file-for-version:\"TODO\"" - }, - { - "Id": "82ec0542-defa-498f-814d-b85df87b49b1", - "Command": "/github-token:\"Fill out Secrets.cs\"" - }, - { - "Id": "2762c393-a835-4357-8543-d0c1c1d45fdb", - "Command": "/github-repo:\"Suplanus/GitHubReleaser\"" + "Id": "d9b72d73-4d90-4975-bae5-41bc95787a9a", + "Command": "/draft:\"true\"" } ] } diff --git a/src/GitHubReleaser/GitHubReleaser.csproj b/src/GitHubReleaser/GitHubReleaser.csproj index b8f9276..f9f1714 100644 --- a/src/GitHubReleaser/GitHubReleaser.csproj +++ b/src/GitHubReleaser/GitHubReleaser.csproj @@ -8,6 +8,7 @@ + diff --git a/src/GitHubReleaser/Model/CommandLineParameters.cs b/src/GitHubReleaser/Model/CommandLineParameters.cs index 832e212..dec5d4f 100644 --- a/src/GitHubReleaser/Model/CommandLineParameters.cs +++ b/src/GitHubReleaser/Model/CommandLineParameters.cs @@ -1,5 +1,5 @@ using System.Collections.Generic; -using System.Runtime.CompilerServices; +using System.Linq; using Fclp; namespace GitHubReleaser.Model @@ -8,7 +8,9 @@ internal class CommandLineParameters : ReleaserSettings { public ICommandLineParserResult Result { get; set; } - public static CommandLineParameters GetCommandLineParameters(string[] args) + public List IssueLabelsWithHeader { get; set; } + + public static CommandLineParameters FromArguments(string[] args) { var parser = new FluentCommandLineParser(); parser.Setup(arg => arg.GitHubRepo) @@ -26,7 +28,7 @@ public static CommandLineParameters GetCommandLineParameters(string[] args) parser.Setup(arg => arg.IsPreRelease) .As("pre-release"); - parser.Setup(arg => arg.IssueLabels) + parser.Setup(arg => arg.IssueLabelsWithHeader) .As("issue-labels"); parser.Setup(arg => arg.IssueFilterLabel) @@ -41,8 +43,22 @@ public static CommandLineParameters GetCommandLineParameters(string[] args) parser.Setup(arg => arg.IsChangelogFileCreationEnabled) .As("create-changelog-file"); + parser.Setup(arg => arg.IsDraft) + .As("draft"); + var result = parser.Parse(args); var commandLineArguments = parser.Object; + + // Manual map + if (parser.Object.IssueLabelsWithHeader != null) + { + foreach (var issueLabelWithHeader in parser.Object.IssueLabelsWithHeader) + { + var split = issueLabelWithHeader.Split(';'); + parser.Object.IssueLabels.Add(split.First(), split.Last()); + } + } + commandLineArguments.Result = result; #if DEBUG diff --git a/src/GitHubReleaser/Model/IssueWithLabel.cs b/src/GitHubReleaser/Model/IssueWithLabel.cs new file mode 100644 index 0000000..1f9cc74 --- /dev/null +++ b/src/GitHubReleaser/Model/IssueWithLabel.cs @@ -0,0 +1,16 @@ +using Octokit; + +namespace GitHubReleaser.Model +{ + internal class IssueWithLabel + { + public string Label { get; } + public Issue Issue { get; } + + public IssueWithLabel(string label, Issue issue) + { + Label = label; + Issue = issue; + } + } +} \ No newline at end of file diff --git a/src/GitHubReleaser/Model/Releaser.cs b/src/GitHubReleaser/Model/Releaser.cs index c85e005..2016c3b 100644 --- a/src/GitHubReleaser/Model/Releaser.cs +++ b/src/GitHubReleaser/Model/Releaser.cs @@ -1,17 +1,271 @@ -namespace GitHubReleaser.Model +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using System.Net; +using System.Reflection; +using System.Text; +using System.Threading.Tasks; +using Octokit; +using Serilog; + +namespace GitHubReleaser.Model { internal class Releaser { + public ReleaserSettings Settings { get; set; } + private readonly GitHubClient _client; + private readonly string ACCOUNT; + private readonly string REPO; + private readonly string VersionMilestone; + private string VersionFull; + public Releaser(ReleaserSettings releaserSettings) { Settings = releaserSettings; + var version = Assembly.LoadFile(Settings.FileForVersion).GetName().Version; + VersionMilestone = version.ToString(3); + VersionFull = version.ToString(); + + var split = Settings.GitHubRepo.Split('/'); + ACCOUNT = split.First(); + REPO = split.Last(); + + // GitHub + ServicePointManager.SecurityProtocol = + SecurityProtocolType.Tls12; // needed https://github.com/octokit/octokit.net/issues/1756 + var connection = new Connection(new ProductHeaderValue(REPO)); + _client = new GitHubClient(connection); + var tokenAuth = new Credentials(Settings.GitHubToken); + _client.Credentials = tokenAuth; } - public ReleaserSettings Settings { get; set; } + public async Task ExecuteAsync() + { + if (Settings.IsUpdateOnly) + { + // todo + } + else + { + var release = await CreateRelease(); + } + + if (Settings.IsChangelogFileCreationEnabled) + { + // todo + } + } - public void Execute() + private async Task CreateRelease() { - + // Remove existing + Release release = await _client.Repository.Release.Get(ACCOUNT, REPO, VersionFull); + if (release != null) + { + Log.Information("Remove release..."); + await _client.Repository.Release.Delete(ACCOUNT, REPO, release.Id); + } + + // Create + Log.Information("Create release..."); + NewRelease newRelease = new NewRelease(VersionFull); + newRelease.Name = VersionFull; + newRelease.Prerelease = Settings.IsPreRelease; + newRelease.Draft = Settings.IsDraft; + + // Changelog + var changelog = await GetReleaseChangelog(); + if (newRelease.Prerelease) + { + string alert = "| :warning: **PreRelease**: Bitte nur in Testumgebung nutzen! |" + + Environment.NewLine + + "| --- |"; + changelog = alert + Environment.NewLine + changelog; + } + newRelease.Body = changelog; + + release = await _client.Repository.Release.Create(ACCOUNT, REPO, newRelease); + + // Upload: Attachments + Log.Information("Upload attachments..."); + bool deleteFilesAfterUpload = true; + try + { + for (var index = 0; index < Settings.ReleaseAttachments.Count; index++) + { + Log.Information(index + 1 + " / " + Settings.ReleaseAttachments.Count); + + var setupFile = Settings.ReleaseAttachments[index]; + await using var archiveContents = File.OpenRead(setupFile); + string assetFilename = Path.GetFileName(setupFile); + var assetUpload = new ReleaseAssetUpload + { + FileName = assetFilename, + ContentType = "application/x-msdownload", + RawData = archiveContents + }; + await _client.Repository.Release.UploadAsset(release, assetUpload); + } + } + catch (Exception exception) + { + Log.Error(exception.ToString()); + deleteFilesAfterUpload = false; + } + + if (deleteFilesAfterUpload) + { + foreach (var attachment in Settings.ReleaseAttachments) + { + Directory.Delete(attachment, true); + } + } + + return release; + } + + private async Task GetReleaseChangelog() + { + var releases = await _client.Repository.Release.GetAll(ACCOUNT, REPO); + Release lastRelease = releases.OrderBy(obj => obj.CreatedAt.DateTime).Last(); + IssueRequest recently = new IssueRequest(); + recently.Filter = IssueFilter.All; + recently.State = ItemStateFilter.Closed; + + if (Settings.IsPreRelease) + { + var lastReleaseCreatedDate = lastRelease.PublishedAt; + recently.Since = lastReleaseCreatedDate; + } + + var allIssues = await _client.Issue.GetAllForCurrent(recently); + var issuesWithLabel = new List(); + foreach (var issue in allIssues) + { + if (issue.Milestone == null) + { + continue; + } + + if (!issue.Milestone.Title.Equals(VersionMilestone)) + { + continue; + } + + if (Settings.IssueFilterLabel != null) + { + if (issue.Labels.Any(obj => obj.Name.ToUpper().Equals(Settings.IssueFilterLabel))) + { + continue; + } + } + + // Filter by issue label + if (Settings.IssueLabels != null && + Settings.IssueLabels.Any()) + { + foreach (var label in issue.Labels) + { + if (Settings.IssueLabels.Any(obj => obj.Key.Equals(label.Name.ToLower()))) + { + issuesWithLabel.Add(new IssueWithLabel(label.Name, issue)); + break; + } + } + } + else + { + issuesWithLabel.Add(new IssueWithLabel(null, issue)); + } + } + + // Build changelog text + var issueGroups = issuesWithLabel.GroupBy(obj => obj.Label).OrderBy(obj => obj.Key); + var changelog = GetChangelogFromIssues(issueGroups); + + return changelog; + } + + private string GetChangelogFromIssues(IEnumerable> issueGroups) + { + var sb = new StringBuilder(); + foreach (var issueGroup in issueGroups) + { + sb.AppendLine(); + sb.AppendLine($"#### {issueGroup.Key}:"); + foreach (IssueWithLabel issueWithLabel in issueGroup.OrderBy(obj => obj.Issue.Title)) + { + sb.AppendLine($"- [{issueWithLabel.Issue.Title}]({issueWithLabel.Issue.HtmlUrl})"); + } + } + string changelog = sb.ToString().Trim(); + return changelog; + } + + public async Task CreateChangelogComplete() + { + Log.Information("Create Changelog..."); + + StringBuilder sb = new StringBuilder(); + sb.AppendLine("Changelog"); // needed for broken encoding + sb.AppendLine(); + + var releases = await _client.Repository.Release.GetAll(ACCOUNT, REPO); + foreach (var release in releases.OrderByDescending(obj => obj.CreatedAt.Date)) + { + if (release.Draft) + { + continue; + } + + var version = new Version(release.Name); + var versionToDisplay = $"{version.Major}.{version.Minor}.{version.Build}"; + var dateTime = release.CreatedAt.DateTime.ToLocalTime(); + dateTime = dateTime.AddDays(1); // Don't know why but this is needed + dateTime = dateTime.AddHours(1); // Think this is problem with summer & winter time + var dateTimeToDisplay = dateTime.ToString("yyyy-MM-dd HH:mm"); + + sb.AppendLine($"## [{versionToDisplay}]({release.HtmlUrl})"); + sb.AppendLine(); + + if (release.Prerelease) + { + sb.AppendLine($"#### Build: {version.Revision} | Date: {dateTimeToDisplay} | Prerelease"); + } + else + { + sb.AppendLine($"#### Build: {version.Revision} | Date: {dateTimeToDisplay}"); + } + + sb.AppendLine(); + + string changeLog = release.Body.Trim(); + var changeLogLines = changeLog.Split('\n'); + foreach (var line in changeLogLines) + { + if (!line.StartsWith("|")) // Ignore Tables + { + sb.AppendLine(line); + } + } + sb.AppendLine(); + } + + // Commit Changelog + var changelog = sb.ToString(); + string path = "CHANGELOG.md"; + IReadOnlyList contents = await _client + .Repository + .Content + .GetAllContents(ACCOUNT, REPO, path); + + await _client.Repository.Content.CreateFile( + ACCOUNT, + REPO, + path, + new UpdateFileRequest("Changelog", + changelog, contents.First().Sha)); } } } \ No newline at end of file diff --git a/src/GitHubReleaser/Model/ReleaserSettings.cs b/src/GitHubReleaser/Model/ReleaserSettings.cs index 24ebe98..ad5989f 100644 --- a/src/GitHubReleaser/Model/ReleaserSettings.cs +++ b/src/GitHubReleaser/Model/ReleaserSettings.cs @@ -12,10 +12,12 @@ internal class ReleaserSettings public string IssueFilterLabel { get; set; } - public List IssueLabels { get; set; } + public Dictionary IssueLabels { get; set; } public bool IsPreRelease { get; set; } + public bool IsDraft { get; set; } + public string FileForVersion { get; set; } public string GitHubToken { get; set; } diff --git a/src/GitHubReleaser/Program.cs b/src/GitHubReleaser/Program.cs index f92b716..c938173 100644 --- a/src/GitHubReleaser/Program.cs +++ b/src/GitHubReleaser/Program.cs @@ -1,6 +1,8 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; +using System.Threading.Tasks; using GitHubReleaser.Model; using Serilog; using Serilog.Sinks.SystemConsole.Themes; @@ -9,9 +11,7 @@ namespace GitHubReleaser { class Program { - public static CommandLineParameters CommandLineParameters { get; private set; } - - static void Main(string[] args) + static async Task Main(string[] args) { // Setup var argumentString = string.Join(" ", args.Skip(1)); @@ -22,25 +22,59 @@ static void Main(string[] args) .CreateLogger(); // CommandLineParameters - CommandLineParameters = CommandLineParameters.GetCommandLineParameters(args); - if (CommandLineParameters.Result.HasErrors) + var commandLine = GetCommandline(args); + + // Execute + Releaser releaser = new Releaser(commandLine); + await releaser.ExecuteAsync(); + } + + private static CommandLineParameters GetCommandline(string[] args) + { + var commandLineParameters = CommandLineParameters.FromArguments(args); + + // Checks + if (commandLineParameters.Result.HasErrors) { - Log.Error($"Error in parameters: {CommandLineParameters.Result.ErrorText}"); + Log.Error($"Error in parameters: {commandLineParameters.Result.ErrorText}"); Environment.Exit(160); } - LogParameter(nameof(CommandLineParameters.GitHubRepo), CommandLineParameters.GitHubRepo); - LogParameter(nameof(CommandLineParameters.GitHubToken), CommandLineParameters.GitHubToken); - LogParameter(nameof(CommandLineParameters.FileForVersion), CommandLineParameters.FileForVersion); - LogParameter(nameof(CommandLineParameters.IsChangelogFileCreationEnabled), CommandLineParameters.IsChangelogFileCreationEnabled); - LogParameter(nameof(CommandLineParameters.IsPreRelease), CommandLineParameters.IsPreRelease); - LogParameter(nameof(CommandLineParameters.IsUpdateOnly), CommandLineParameters.IsUpdateOnly); - LogParameter(nameof(CommandLineParameters.IssueFilterLabel), CommandLineParameters.IssueFilterLabel); - LogParameter(nameof(CommandLineParameters.IssueLabels), CommandLineParameters.IssueLabels); - LogParameter(nameof(CommandLineParameters.ReleaseAttachments), CommandLineParameters.ReleaseAttachments); - // Execute - Releaser releaser = new Releaser(CommandLineParameters); - releaser.Execute(); + if (!File.Exists(commandLineParameters.FileForVersion)) + { + Log.Error($"File not exists: {commandLineParameters.FileForVersion}"); + Environment.Exit(160); + } + + var extension = Path.GetExtension(commandLineParameters.FileForVersion)?.ToLower(); + if (extension != ".dll" && + extension != ".exe") + { + Log.Error($"File type not supported: {extension}"); + Environment.Exit(160); + } + + foreach (var attachment in commandLineParameters.ReleaseAttachments) + { + if (!File.Exists(attachment)) + { + Log.Error($"Attachment file not found: {attachment}"); + Environment.Exit(160); + } + } + + LogParameter(nameof(commandLineParameters.GitHubRepo), commandLineParameters.GitHubRepo); + LogParameter(nameof(commandLineParameters.GitHubToken), commandLineParameters.GitHubToken); + LogParameter(nameof(commandLineParameters.FileForVersion), commandLineParameters.FileForVersion); + LogParameter(nameof(commandLineParameters.IsChangelogFileCreationEnabled), + commandLineParameters.IsChangelogFileCreationEnabled); + LogParameter(nameof(commandLineParameters.IsPreRelease), commandLineParameters.IsPreRelease); + LogParameter(nameof(commandLineParameters.IsUpdateOnly), commandLineParameters.IsUpdateOnly); + LogParameter(nameof(commandLineParameters.IssueFilterLabel), commandLineParameters.IssueFilterLabel); + LogParameter(nameof(commandLineParameters.IssueLabels), commandLineParameters.IssueLabels); + LogParameter(nameof(commandLineParameters.ReleaseAttachments), commandLineParameters.ReleaseAttachments); + + return commandLineParameters; } private static void LogParameter(string name, object value) diff --git a/src/GitHubReleaser/Properties/launchSettings.json b/src/GitHubReleaser/Properties/launchSettings.json index 132fc58..4a47b2c 100644 --- a/src/GitHubReleaser/Properties/launchSettings.json +++ b/src/GitHubReleaser/Properties/launchSettings.json @@ -2,7 +2,7 @@ "profiles": { "GitHubReleaser": { "commandName": "Project", - "commandLineArgs": "/create-changelog-file:\"true\" /update-only:\"false\" /release-attachments:\"TODO\" \"TODO\" /issue-filter-label:\"not-in-changelog\" /issue-labels:\"bug;Bug\" \"feature;Feature\" /pre-release:\"false\" /file-for-version:\"TODO\" /github-token:\"TODO\" /github-repo:\"Suplanus/GitHubReleaser\"" + "commandLineArgs": "/github-repo:\"Suplanus/GitHubReleaser\" /github-token:\"Fill out Secrets.cs\" /file-for-version:\".\\GitHubReleaser.exe\" /create-changelog-file:\"true\" /update-only:\"false\" /release-attachments:\".\\..\\..\\..\\..\\..\\demodata\\example.zip\" /issue-filter-label:\"not-in-changelog\" /issue-labels:\"bug;Bug\" \"feature;Feature\" /pre-release:\"false\"" } } } \ No newline at end of file