Skip to content

Commit

Permalink
Adding public API to all manual projects (#779)
Browse files Browse the repository at this point in the history
* Adding public API to all manual projects

* Add `nuke shipapi`, `nuke ensureapideclared`, and `nuke declareapi`

* Attempt to make GitHub talk to you

* Add some logging, fix a potential regex issue

* Actually pass the GitHub Token

* The code was correct the first time

* Fix basic logic

* Try give the token more permissions?

* PR target

* Let's hope this PR mechanism works? Can't test until merged now!

* Install workload as well

* Make BuildTools exempt from public API
  • Loading branch information
Perksey committed Jan 16, 2022
1 parent 98d3a3c commit bbd27c4
Show file tree
Hide file tree
Showing 2 changed files with 243 additions and 2 deletions.
167 changes: 167 additions & 0 deletions src/infrastructure/Silk.NET.NUKE/Build.PublicApi.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.

using System;
using System.IO;
using System.Linq;
using Nuke.Common;
using Nuke.Common.CI.GitHubActions;
using Nuke.Common.IO;
using Nuke.Common.Tooling;
using Octokit;
using Octokit.Internal;
using static Nuke.Common.Tools.Git.GitTasks;
using static Nuke.Common.Tooling.ProcessTasks;
using static Nuke.Common.Tools.DotNet.DotNetTasks;

partial class Build
{
const string FormatDeclCmd =
"format analyzers {0} --diagnostics=RS0016 --severity=error -v=diag --include-generated";

Target ShipApi => CommonTarget
(
x => x.Executes
(
() =>
{
foreach (var unshippedFile in RootDirectory.GlobFiles("**/PublicAPI.Unshipped.txt"))
{
var shippedFile = unshippedFile.Parent / "PublicAPI.Shipped.txt";
if (!File.Exists(shippedFile))
{
// common.props should've made this file, so if it's not here then i'm guessing this isn't a
// public api after all.
continue;
}
var shippedLines = File.ReadAllLines(shippedFile).ToList();
var unshippedLines = File.ReadAllLines(unshippedFile).ToList();
for (var i = 0; i < unshippedLines.Count; i++)
{
var unshippedLine = unshippedLines[i];
if (unshippedLine.StartsWith("//") || unshippedLine.StartsWith("#"))
{
continue;
}
if (!shippedLines.Contains(unshippedLine))
{
shippedLines.Add(unshippedLine);
}
unshippedLines.RemoveAt(i);
i--; // so we don't skip the next element
}
File.WriteAllLines(unshippedFile, unshippedLines);
File.WriteAllLines(shippedFile, shippedLines);
}
MakePr();
}
)
);

Target DeclareApi => CommonTarget(x => x.Executes(() => DotNet(string.Format(FormatDeclCmd, "Silk.NET.sln"))));

Target EnsureApiDeclared => CommonTarget
(
x => x.Executes
(
async () =>
{
try
{
var cmd = string.Format
(
FormatDeclCmd,
GitHubActions.Instance.GitHubRef?.Contains("/pull/") ?? false
? "inbound_pr/Silk.NET.sln"
: "Silk.NET.sln"
);
// I have no trust of incoming code, so let's take the github token away from them before they think
// about adding dodgy MSBuild targets that could swipe it
var githubToken = EnvironmentInfo.GetVariable<string>("GITHUB_TOKEN");
EnvironmentInfo.SetVariable("GITHUB_TOKEN", string.Empty);
// run the format command
DotNet($"{cmd} --verify-no-changes");
// add our github token back
EnvironmentInfo.SetVariable("GITHUB_TOKEN", githubToken);
await AddOrUpdatePrComment("public_api", "public_api_declared", true);
}
catch (ProcessException)
{
await AddOrUpdatePrComment("public_api", "public_api_not_declared");
throw;
}
}
)
);

void MakePr()
{
var pushableToken = EnvironmentInfo.GetVariable<string>("PUSHABLE_GITHUB_TOKEN");
var curBranch = GitCurrentBranch(RootDirectory);
if (GitHubActions.Instance?.GitHubRepository == "dotnet/Silk.NET" &&
!string.IsNullOrWhiteSpace(pushableToken))
{
if (curBranch == "HEAD" || string.IsNullOrWhiteSpace(curBranch))
{
curBranch = "main"; // not a good assumption to make, but fine for now for our purposes
// (tags are created from main usually)
}

// it's assumed that the pushable token was used to checkout the repo
Git("fetch --all", RootDirectory);
Git("pull");
Git("add **/PublicAPI.*.txt", RootDirectory);
var newBranch = $"ci/{curBranch}/ship_apis";
var curCommit = GitCurrentCommit(RootDirectory);
var commitCmd = StartProcess
(
$"git commit -m \"Move unshipped APIs to shipped\""
)
.AssertWaitForExit();
if (!commitCmd.Output.Any(x => x.Text.Contains("nothing to commit", StringComparison.OrdinalIgnoreCase)))
{
commitCmd.AssertZeroExitCode();
}

// ensure there are no other changes
Git("checkout HEAD .nuke/", RootDirectory);
Git("reset --hard", RootDirectory);
if (GitCurrentCommit(RootDirectory) != curCommit) // might get "nothing to commit", you never know...
{
Logger.Info("Checking for existing branch...");
var exists = StartProcess("git", $"checkout \"{newBranch}\"", RootDirectory)
.AssertWaitForExit()
.ExitCode == 0;
if (!exists)
{
Logger.Info("None found, creating a new one...");
Git($"checkout -b \"{newBranch}\"");
}

Git($"merge -X theirs \"{curBranch}\" --allow-unrelated-histories");
Git($"push --set-upstream origin \"{newBranch}\"");
if (!exists)
{
var github = new GitHubClient
(
new ProductHeaderValue("Silk.NET-CI"),
new InMemoryCredentialStore(new Credentials(pushableToken))
);

var pr = github.PullRequest.Create
("dotnet", "Silk.NET", new("Move unshipped APIs to shipped", newBranch, curBranch))
.GetAwaiter()
.GetResult();
}
}
}
}
}
78 changes: 76 additions & 2 deletions src/infrastructure/Silk.NET.NUKE/Build.Support.cs
Original file line number Diff line number Diff line change
Expand Up @@ -4,15 +4,23 @@
using System;
using System.Collections.Concurrent;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Text.RegularExpressions;
using System.Threading.Tasks;
using JetBrains.Annotations;
using Microsoft.Build.Locator;
using Nuke.Common;
using Nuke.Common.CI.GitHubActions;
using Nuke.Common.IO;
using Nuke.Common.ProjectModel;
using Octokit;
using Octokit.Internal;

partial class Build
{
static readonly Regex PrRegex = new("refs\\/pull\\/([0-9]+).*", RegexOptions.Compiled);

/// Support plugins are available for:
/// - JetBrains ReSharper https://nuke.build/resharper
/// - JetBrains Rider https://nuke.build/rider
Expand All @@ -30,10 +38,10 @@ static int IndexOfOrThrow(string x, char y)
return idx;
}

[Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
[Nuke.Common.Parameter("Configuration to build - Default is 'Debug' (local) or 'Release' (server)")]
readonly string Configuration = IsLocalBuild ? "Debug" : "Release";

[Parameter("Extra properties passed to MSBuild commands")]
[Nuke.Common.Parameter("Extra properties passed to MSBuild commands")]
readonly string[] MsbuildProperties = Array.Empty<string>();

[Solution] readonly Solution OriginalSolution;
Expand Down Expand Up @@ -76,4 +84,70 @@ Target CommonTarget([CanBeNull] Target actualTarget = null) => Targets.GetOrAdd
return actualTarget is null ? def : actualTarget(def);
}
);

async Task AddOrUpdatePrComment(string type, string file, bool editOnly = false, params KeyValuePair<string, string>[] subs)
{;
var githubToken = EnvironmentInfo.GetVariable<string>("GITHUB_TOKEN");
if (string.IsNullOrWhiteSpace(githubToken))
{
Logger.Info("GitHub token not found, skipping writing a comment.");
return;
}

var @ref = GitHubActions.Instance.GitHubRef;
if (string.IsNullOrWhiteSpace(@ref))
{
Logger.Info("Not running in GitHub Actions, skipping writing a comment.");
return;
}

var prMatch = PrRegex.Match(@ref);
if (!prMatch.Success || prMatch.Groups.Count < 2)
{
Logger.Info($"Couldn't match {@ref} to a PR, skipping writing a comment.");
return;
}

if (!int.TryParse(prMatch.Groups[1].Value, out var pr))
{
Logger.Info($"Couldn't parse {@prMatch.Groups[1].Value} as an int, skipping writing a comment.");
return;
}

var github = new GitHubClient
(
new ProductHeaderValue("Silk.NET-CI"),
new InMemoryCredentialStore(new Credentials(githubToken))
);

var existingComment = (await github.Issue.Comment.GetAllForIssue("dotnet", "Silk.NET", pr))
.FirstOrDefault(x => x.Body.Contains($"`{type}`") && x.User.Name == "github-actions[bot]");
if (existingComment is null && editOnly)
{
Logger.Info("Edit only mode is on and no existing comment found, skipping writing a comment.");
return;
}

var commentDir = RootDirectory / "build" / "comments";
var commentText = await File.ReadAllTextAsync(commentDir / $"{file}.md") +
await File.ReadAllTextAsync(commentDir / "footer.md");
foreach (var (key, value) in subs)
{
commentText = commentText.Replace($"{{{key}}}", value);
}

commentText = commentText.Replace("{actionsRun}", GitHubActions.Instance.GitHubRunNumber)
.Replace("{typeId}", type);

if (existingComment is not null)
{
Logger.Info("Updated the comment on the PR.");
await github.Issue.Comment.Update("dotnet", "Silk.NET", existingComment.Id, commentText);
}
else
{
Logger.Info("Added a comment to the PR.");
await github.Issue.Comment.Create("dotnet", "Silk.NET", pr, commentText);
}
}
}

0 comments on commit bbd27c4

Please sign in to comment.