Skip to content

Commit bbd27c4

Browse files
authored
Adding public API to all manual projects (#779)
* 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
1 parent 98d3a3c commit bbd27c4

File tree

2 files changed

+243
-2
lines changed

2 files changed

+243
-2
lines changed
Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
// Licensed to the .NET Foundation under one or more agreements.
2+
// The .NET Foundation licenses this file to you under the MIT license.
3+
4+
using System;
5+
using System.IO;
6+
using System.Linq;
7+
using Nuke.Common;
8+
using Nuke.Common.CI.GitHubActions;
9+
using Nuke.Common.IO;
10+
using Nuke.Common.Tooling;
11+
using Octokit;
12+
using Octokit.Internal;
13+
using static Nuke.Common.Tools.Git.GitTasks;
14+
using static Nuke.Common.Tooling.ProcessTasks;
15+
using static Nuke.Common.Tools.DotNet.DotNetTasks;
16+
17+
partial class Build
18+
{
19+
const string FormatDeclCmd =
20+
"format analyzers {0} --diagnostics=RS0016 --severity=error -v=diag --include-generated";
21+
22+
Target ShipApi => CommonTarget
23+
(
24+
x => x.Executes
25+
(
26+
() =>
27+
{
28+
foreach (var unshippedFile in RootDirectory.GlobFiles("**/PublicAPI.Unshipped.txt"))
29+
{
30+
var shippedFile = unshippedFile.Parent / "PublicAPI.Shipped.txt";
31+
if (!File.Exists(shippedFile))
32+
{
33+
// common.props should've made this file, so if it's not here then i'm guessing this isn't a
34+
// public api after all.
35+
continue;
36+
}
37+
38+
var shippedLines = File.ReadAllLines(shippedFile).ToList();
39+
var unshippedLines = File.ReadAllLines(unshippedFile).ToList();
40+
for (var i = 0; i < unshippedLines.Count; i++)
41+
{
42+
var unshippedLine = unshippedLines[i];
43+
if (unshippedLine.StartsWith("//") || unshippedLine.StartsWith("#"))
44+
{
45+
continue;
46+
}
47+
48+
if (!shippedLines.Contains(unshippedLine))
49+
{
50+
shippedLines.Add(unshippedLine);
51+
}
52+
53+
unshippedLines.RemoveAt(i);
54+
i--; // so we don't skip the next element
55+
}
56+
57+
File.WriteAllLines(unshippedFile, unshippedLines);
58+
File.WriteAllLines(shippedFile, shippedLines);
59+
}
60+
61+
MakePr();
62+
}
63+
)
64+
);
65+
66+
Target DeclareApi => CommonTarget(x => x.Executes(() => DotNet(string.Format(FormatDeclCmd, "Silk.NET.sln"))));
67+
68+
Target EnsureApiDeclared => CommonTarget
69+
(
70+
x => x.Executes
71+
(
72+
async () =>
73+
{
74+
try
75+
{
76+
var cmd = string.Format
77+
(
78+
FormatDeclCmd,
79+
GitHubActions.Instance.GitHubRef?.Contains("/pull/") ?? false
80+
? "inbound_pr/Silk.NET.sln"
81+
: "Silk.NET.sln"
82+
);
83+
84+
// I have no trust of incoming code, so let's take the github token away from them before they think
85+
// about adding dodgy MSBuild targets that could swipe it
86+
var githubToken = EnvironmentInfo.GetVariable<string>("GITHUB_TOKEN");
87+
EnvironmentInfo.SetVariable("GITHUB_TOKEN", string.Empty);
88+
89+
// run the format command
90+
DotNet($"{cmd} --verify-no-changes");
91+
92+
// add our github token back
93+
EnvironmentInfo.SetVariable("GITHUB_TOKEN", githubToken);
94+
await AddOrUpdatePrComment("public_api", "public_api_declared", true);
95+
}
96+
catch (ProcessException)
97+
{
98+
await AddOrUpdatePrComment("public_api", "public_api_not_declared");
99+
throw;
100+
}
101+
}
102+
)
103+
);
104+
105+
void MakePr()
106+
{
107+
var pushableToken = EnvironmentInfo.GetVariable<string>("PUSHABLE_GITHUB_TOKEN");
108+
var curBranch = GitCurrentBranch(RootDirectory);
109+
if (GitHubActions.Instance?.GitHubRepository == "dotnet/Silk.NET" &&
110+
!string.IsNullOrWhiteSpace(pushableToken))
111+
{
112+
if (curBranch == "HEAD" || string.IsNullOrWhiteSpace(curBranch))
113+
{
114+
curBranch = "main"; // not a good assumption to make, but fine for now for our purposes
115+
// (tags are created from main usually)
116+
}
117+
118+
// it's assumed that the pushable token was used to checkout the repo
119+
Git("fetch --all", RootDirectory);
120+
Git("pull");
121+
Git("add **/PublicAPI.*.txt", RootDirectory);
122+
var newBranch = $"ci/{curBranch}/ship_apis";
123+
var curCommit = GitCurrentCommit(RootDirectory);
124+
var commitCmd = StartProcess
125+
(
126+
$"git commit -m \"Move unshipped APIs to shipped\""
127+
)
128+
.AssertWaitForExit();
129+
if (!commitCmd.Output.Any(x => x.Text.Contains("nothing to commit", StringComparison.OrdinalIgnoreCase)))
130+
{
131+
commitCmd.AssertZeroExitCode();
132+
}
133+
134+
// ensure there are no other changes
135+
Git("checkout HEAD .nuke/", RootDirectory);
136+
Git("reset --hard", RootDirectory);
137+
if (GitCurrentCommit(RootDirectory) != curCommit) // might get "nothing to commit", you never know...
138+
{
139+
Logger.Info("Checking for existing branch...");
140+
var exists = StartProcess("git", $"checkout \"{newBranch}\"", RootDirectory)
141+
.AssertWaitForExit()
142+
.ExitCode == 0;
143+
if (!exists)
144+
{
145+
Logger.Info("None found, creating a new one...");
146+
Git($"checkout -b \"{newBranch}\"");
147+
}
148+
149+
Git($"merge -X theirs \"{curBranch}\" --allow-unrelated-histories");
150+
Git($"push --set-upstream origin \"{newBranch}\"");
151+
if (!exists)
152+
{
153+
var github = new GitHubClient
154+
(
155+
new ProductHeaderValue("Silk.NET-CI"),
156+
new InMemoryCredentialStore(new Credentials(pushableToken))
157+
);
158+
159+
var pr = github.PullRequest.Create
160+
("dotnet", "Silk.NET", new("Move unshipped APIs to shipped", newBranch, curBranch))
161+
.GetAwaiter()
162+
.GetResult();
163+
}
164+
}
165+
}
166+
}
167+
}

src/infrastructure/Silk.NET.NUKE/Build.Support.cs

Lines changed: 76 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,15 +4,23 @@
44
using System;
55
using System.Collections.Concurrent;
66
using System.Collections.Generic;
7+
using System.IO;
78
using System.Linq;
9+
using System.Text.RegularExpressions;
10+
using System.Threading.Tasks;
811
using JetBrains.Annotations;
912
using Microsoft.Build.Locator;
1013
using Nuke.Common;
14+
using Nuke.Common.CI.GitHubActions;
1115
using Nuke.Common.IO;
1216
using Nuke.Common.ProjectModel;
17+
using Octokit;
18+
using Octokit.Internal;
1319

1420
partial class Build
1521
{
22+
static readonly Regex PrRegex = new("refs\\/pull\\/([0-9]+).*", RegexOptions.Compiled);
23+
1624
/// Support plugins are available for:
1725
/// - JetBrains ReSharper https://nuke.build/resharper
1826
/// - JetBrains Rider https://nuke.build/rider
@@ -30,10 +38,10 @@ static int IndexOfOrThrow(string x, char y)
3038
return idx;
3139
}
3240

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

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

3947
[Solution] readonly Solution OriginalSolution;
@@ -76,4 +84,70 @@ Target CommonTarget([CanBeNull] Target actualTarget = null) => Targets.GetOrAdd
7684
return actualTarget is null ? def : actualTarget(def);
7785
}
7886
);
87+
88+
async Task AddOrUpdatePrComment(string type, string file, bool editOnly = false, params KeyValuePair<string, string>[] subs)
89+
{;
90+
var githubToken = EnvironmentInfo.GetVariable<string>("GITHUB_TOKEN");
91+
if (string.IsNullOrWhiteSpace(githubToken))
92+
{
93+
Logger.Info("GitHub token not found, skipping writing a comment.");
94+
return;
95+
}
96+
97+
var @ref = GitHubActions.Instance.GitHubRef;
98+
if (string.IsNullOrWhiteSpace(@ref))
99+
{
100+
Logger.Info("Not running in GitHub Actions, skipping writing a comment.");
101+
return;
102+
}
103+
104+
var prMatch = PrRegex.Match(@ref);
105+
if (!prMatch.Success || prMatch.Groups.Count < 2)
106+
{
107+
Logger.Info($"Couldn't match {@ref} to a PR, skipping writing a comment.");
108+
return;
109+
}
110+
111+
if (!int.TryParse(prMatch.Groups[1].Value, out var pr))
112+
{
113+
Logger.Info($"Couldn't parse {@prMatch.Groups[1].Value} as an int, skipping writing a comment.");
114+
return;
115+
}
116+
117+
var github = new GitHubClient
118+
(
119+
new ProductHeaderValue("Silk.NET-CI"),
120+
new InMemoryCredentialStore(new Credentials(githubToken))
121+
);
122+
123+
var existingComment = (await github.Issue.Comment.GetAllForIssue("dotnet", "Silk.NET", pr))
124+
.FirstOrDefault(x => x.Body.Contains($"`{type}`") && x.User.Name == "github-actions[bot]");
125+
if (existingComment is null && editOnly)
126+
{
127+
Logger.Info("Edit only mode is on and no existing comment found, skipping writing a comment.");
128+
return;
129+
}
130+
131+
var commentDir = RootDirectory / "build" / "comments";
132+
var commentText = await File.ReadAllTextAsync(commentDir / $"{file}.md") +
133+
await File.ReadAllTextAsync(commentDir / "footer.md");
134+
foreach (var (key, value) in subs)
135+
{
136+
commentText = commentText.Replace($"{{{key}}}", value);
137+
}
138+
139+
commentText = commentText.Replace("{actionsRun}", GitHubActions.Instance.GitHubRunNumber)
140+
.Replace("{typeId}", type);
141+
142+
if (existingComment is not null)
143+
{
144+
Logger.Info("Updated the comment on the PR.");
145+
await github.Issue.Comment.Update("dotnet", "Silk.NET", existingComment.Id, commentText);
146+
}
147+
else
148+
{
149+
Logger.Info("Added a comment to the PR.");
150+
await github.Issue.Comment.Create("dotnet", "Silk.NET", pr, commentText);
151+
}
152+
}
79153
}

0 commit comments

Comments
 (0)