Skip to content

Commit

Permalink
Don't require GH CLI for specific account sync
Browse files Browse the repository at this point in the history
This makes the sync operation way more friendly. We can probably provide a global tool too since the CLI wouldn't be strictly required.
  • Loading branch information
kzu committed Jun 11, 2024
1 parent 336044a commit af2647d
Show file tree
Hide file tree
Showing 5 changed files with 49 additions and 21 deletions.
35 changes: 28 additions & 7 deletions src/Commands/SyncCommand.cs
Original file line number Diff line number Diff line change
Expand Up @@ -55,15 +55,19 @@ public class SyncSettings : CommandSettings

public override async Task<int> ExecuteAsync(CommandContext context, SyncSettings settings)
{
var result = await base.ExecuteAsync(context, settings);
if (result != 0)
return result;
var firstRunCompleted = config.TryGetBoolean("sponsorlink", "firstrun", out var completed) && completed;

if (!firstRunCompleted &&
app.Run(["welcome"]) is var welcome &&
welcome < 0)
{
return welcome;
}

var result = 0;
var ghDir = Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sponsorlink", "github");
Directory.CreateDirectory(ghDir);

Debug.Assert(Account != null, "After authentication, Account should never be null.");

var sponsorables = new HashSet<string>();
if (settings.Sponsorable?.Length > 0)
{
Expand All @@ -76,6 +80,11 @@ public override async Task<int> ExecuteAsync(CommandContext context, SyncSetting
}
else
{
// In order to run discovery, we need an authenticated user
result = await base.ExecuteAsync(context, settings);
if (result != 0)
return result;

// Discover all candidate sponsorables for the current user
if (await Status().StartAsync(Sync.QueryingUserSponsorships, async _ =>
{
Expand Down Expand Up @@ -133,8 +142,21 @@ await Status().StartAsync(Sync.FetchingManifests(sponsorables.Count), async ctx
{
ctx.Status = Sync.DetectingManifest(sponsorable);
using var http = httpFactory.CreateClient();
var branch = await client.QueryAsync(GraphQueries.DefaultBranch(sponsorable, ".github"));

// First try default branch
var branch = "main";
var (status, manifest) = await SponsorableManifest.FetchAsync(sponsorable, branch, http);
if (status == SponsorableManifest.Status.NotFound)
{
// We can directly query via the default HTTP client since the .github repository must be public.
branch = await httpFactory.GetQueryClient().QueryAsync(new GraphQuery(
$"/repos/{sponsorable}/.github", ".default_branch") { IsLegacy = true });

if (branch != null && branch != "main")
// Retry discovery with non-'main' branch
(status, manifest) = await SponsorableManifest.FetchAsync(sponsorable, branch, http);
}

switch (status)
{
case SponsorableManifest.Status.OK when manifest != null:
Expand Down Expand Up @@ -226,7 +248,6 @@ await Status().StartAsync(Sync.Synchronizing(manifest.Sponsorable), async ctx =>
}
}

var config = DotNetConfig.Config.Build(Path.Combine(Environment.GetFolderPath(Environment.SpecialFolder.UserProfile), ".sponsorlink"));
var autosync = settings.AutoSync;

if (!settings.Unattended &&
Expand Down
3 changes: 3 additions & 0 deletions src/Core/HttpGraphQueryClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,9 @@ public static class GraphQueryClientExtensions
public static IServiceCollection AddGraphQueryClient(this IServiceCollection services)
=> services.AddSingleton<IGraphQueryClientFactory, GraphQueryClientFactory>();

public static IGraphQueryClient GetQueryClient(this IHttpClientFactory factory, string name = "")
=> new HttpGraphQueryClient(factory, name);

class GraphQueryClientFactory(IHttpClientFactory http) : IGraphQueryClientFactory
{
public IGraphQueryClient CreateClient(string name) => new HttpGraphQueryClient(http, name);
Expand Down
12 changes: 9 additions & 3 deletions src/Core/SponsorableManifest.cs
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,12 @@ public static SponsorableManifest Create(Uri issuer, Uri[] audience, string clie
{
// Try to detect sponsorlink manifest in the sponsorable .github repo
var url = $"https://github.com/{sponsorable}/.github/raw/{branch ?? "main"}/sponsorlink.jwt";

var disposeHttp = http == null;

// Manifest should be public, so no need for any special HTTP client.
using (http ??= new HttpClient())
try
{
var response = await http.GetAsync(url);
var response = await (http ?? new HttpClient()).GetAsync(url);
if (!response.IsSuccessStatusCode)
return (Status.NotFound, default);

Expand All @@ -67,6 +68,11 @@ public static SponsorableManifest Create(Uri issuer, Uri[] audience, string clie

return (Status.OK, manifest);
}
finally
{
if (disposeHttp)
http?.Dispose();
}
}

/// <summary>
Expand Down
10 changes: 6 additions & 4 deletions src/Tests/Helpers.cs
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,12 @@ class Helpers
static Helpers()
{
var collection = new ServiceCollection()
.AddHttpClient()
.AddHttpClient().ConfigureHttpClientDefaults(defaults => defaults.ConfigureHttpClient(http =>
{
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(ThisAssembly.Info.Product, ThisAssembly.Info.InformationalVersion));
if (Debugger.IsAttached)
http.Timeout = TimeSpan.FromMinutes(10);
}))
.AddLogging()
.AddSingleton<IConfiguration>(Configuration)
.AddSingleton(_ => AsyncLazy.Create(async () =>
Expand All @@ -49,14 +54,12 @@ static Helpers()
collection.AddHttpClient("GitHub", http =>
{
http.BaseAddress = new Uri("https://api.github.com");
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(ThisAssembly.Info.Product, ThisAssembly.Info.InformationalVersion));
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ghtoken);
});

collection.AddHttpClient("GitHub:Token", http =>
{
http.BaseAddress = new Uri("https://api.github.com");
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(ThisAssembly.Info.Product, ThisAssembly.Info.InformationalVersion));
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ghtoken);
});
}
Expand All @@ -66,7 +69,6 @@ static Helpers()
collection.AddHttpClient("GitHub:Sponsorable", http =>
{
http.BaseAddress = new Uri("https://api.github.com");
http.DefaultRequestHeaders.UserAgent.Add(new ProductInfoHeaderValue(ThisAssembly.Info.Product, ThisAssembly.Info.InformationalVersion));
http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", ghsponsorable);
});
}
Expand Down
10 changes: 3 additions & 7 deletions src/Tests/SyncCommandTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -134,24 +134,20 @@ public async Task ExplicitSponsorableSync_NoSponsorableManifest()
}

[LocalFact("GitHub:Token")]
public async Task ExplicitSponsorableSync_InvalidSponsorableManifest()
public async Task ExplicitSponsorableSync_InvalidSponsorableManifestNonMainBranch()
{
EnsureAuthenticated();

var graph = new Mock<IGraphQueryClient>();
// Return default 'main' branch from GraphQueries.DefaultBranch
graph.Setup(x => x.QueryAsync(GraphQueries.DefaultBranch("kzu", ".github"))).ReturnsAsync("sponsorlink");

var command = new SyncCommand(
Mock.Of<ICommandApp>(MockBehavior.Strict),
config,
graph.Object,
Mock.Of<IGraphQueryClient>(MockBehavior.Strict),
Mock.Of<IGitHubAppAuthenticator>(MockBehavior.Strict),
Services.GetRequiredService<IHttpClientFactory>());

var settings = new SyncCommand.SyncSettings
{
Sponsorable = ["kzu"],
Sponsorable = ["devlooped-bot"],
Unattended = true,
};

Expand Down

0 comments on commit af2647d

Please sign in to comment.