Adding initial support for GitHub Apps. (#1738)

* Added authentication using bearer token.

* Added Installation and AccessToken clients.

* Added new clients to Reactive project

* added support for DateTime serialized as FileTime

* added support for StatusEventPayload

* added support for StatusEventPayload

* Added test for StatusEventPayload and fixed serializer to return that event payload type.

* WIP - added ApplicationClient and related Api Urls.

* Continued implementing Installations support.

* Fixing build (WIP)

* fixed build

* added Account property to Installation. prefer nameof(x) over literal "x".

* fixed according to code review.

* fixed build.

* switched Installation ID from int to long.

* added Permissions and Events properties to Installation.

* added documentation to Application and Installation properties in IGitHubClient.

* wip - added tests to new clients

* wip - fix build

* wip - fixed build.

* added InstallationsClient tests.

* added integration test for InstallationsClient.

* changes requested in code review.

* add Get method for App

* Create GitHubApp response model instead of re-using existing Application response model

* add Get method to observable client

* fixed build (both locally and failed test).

* Fixed documentation and added some missing XML docs.

* added DebuggerDisplay to StatusEventPayload

* updated XML docs and added some missing bits. prefer nameof(x) over literal "x".

* Add xml comments to AccessToken response model and use DateTimeOffset rather than DateTime

* Tidy up XmlComments and make consistent across client and observable client and interfaces

* fixup unit tests to independently verify preview header

* Implement GetInstallation method

* revert commits unrelated to GitHubApps - these can be done on a separate PR if required

* this extra authenticator class doesnt appear to be used anywhere

* undo project file change as it doesnt appear to be necessary

* Revert "Merge remote-tracking branch 'remote/GitHubApps' into GitHubApps"

This reverts commit c53cc110b8d807f62fdfeaa7df19e1532d050007, reversing
changes made to 0c9e413d420a4725738644ea5b13af6ec102d456.

* Revert "Revert "Merge remote-tracking branch 'remote/GitHubApps' into GitHubApps""

This reverts commit 02d52b8adf814b6945c60cb59a907a8cd34b1ce7.

* add XmlDoc comments to response models and flesh out installation permissions

* name AcceptHeaders member consistently

* accidentally lost changes to Credentials.cs

* Enhance Intergation test framework to handle GitHubApp settings and discoer tests appropriately
Get code ready for GitHubJWT nuget package but for now just hardcode a JWT in ENV VAR
Add 1 integration test for each method and ensure they are working!

* fixed compiler warnings.

* Added support for Installation=>Id field that arrives in a Pull Request Event payload.

(See the last field in the sample JSON of https://developer.github.com/v3/activity/events/types/#pullrequestevent)

* Change integration test project to netcoreapp2.0 so we can use GitHubJwt nuget package in integration tests

* First cut at some GitHubApp doco

* update mkdocs config

* Moved the Installation property to ActivityPayload, so it's available in all payloads.

This feature is not undocumented, unfortunately, but valid:
https://platform.github.community/t/determining-which-installation-an-event-came-from/539/11

* Split Installation to Installation and InstallationId, and added a comfort method for gaining its AccessToken.

* fixed InstallationId CreateAccessToken to receive IGitHubAppsClient. added (and fixed) docs.

* reverted object-oriented style comfort method and it's docs.

* update all test projects to netcoreapp2.0

* tweak build configs to use 2.0.3 SDK

* also need to update cake frosting build to netcoreapp2.0

* tweak docs some more

* fix convention test failures

* test projects still had some old runtime parts in them!

* travis osx image needs to be at least 10.12 for .NET Core 2.0

* shell script might need the same argument tweak for cake

* more doc tweaks

* Make sure compiler warning output isnt somehow causing Linux and OSX builds to fail

* moar logging for linux/OSX builds

* stop sourcelink on linux/OSX builds to see if that is the problem

* set verbosity to detailed for the dotnet build step

* try new sourcelink and list out remotes

* is travis being weird with git clone?

* SourceLink may be defaulting to true on CI server so explicitly set it as false rather than omitting it

* detailed is a bit too verbose for travis, try normal

* turn sourcelink back on for Linux/OSX

* fix compiler warning

* Try SourceLink.Create.CommandLine instead of SourceLink.Create.GitHub

* CliToolReferences did not update to latest versions

* remove debug origin info

* turn off msbuild output

* go back to SourceLink.Create.GitHub!

* time diff between dev PC and API causes issues if specifying a full 600 second token

* handle extra date format that Installation end point now returns

* field needs to be protected in order to be deserialized

* provide even more buffer for client vs server clock drift

* Update to latest GitHubJwt reference

* go back to SDK 1 since SDK 2 is having sporadic travisCI faliures in TestSourceLink build step

* get appveyor working

* update sourcelink back to latest, and use SDK 1.04 (runtime 1.0.5)
This commit is contained in:
Itai Bar-Haim
2018-04-16 13:42:23 +03:00
committed by Ryan Gribble
parent 7aeea34fb2
commit 5ffc96995f
44 changed files with 1280 additions and 184 deletions

View File

@@ -7,7 +7,7 @@ matrix:
sudo: required
dotnet: 1.0.4
- os: osx
osx_image: xcode8
osx_image: xcode8.3
dotnet: 1.0.4
before_script:
@@ -15,5 +15,4 @@ before_script:
script:
- git fetch --unshallow --tags
- dotnet --info
- ./build.sh --linksources=true
- ./build.sh --linksources=true --verbosity=verbose

View File

@@ -0,0 +1,57 @@
using System;
namespace Octokit.Reactive
{
/// <summary>
/// A client for GitHub Applications API. Provides the methods required to get GitHub applications and installations.
/// </summary>
/// <remarks>
/// See the <a href="http://developer.github.com/v3/apps/">GitHub Apps API documentation</a> for more information.
/// </remarks>
public interface IObservableGitHubAppsClient
{
/// <summary>
/// Get a single GitHub App.
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-a-single-github-app</remarks>
/// <param name="slug">The URL-friendly name of your GitHub App. You can find this on the settings page for your GitHub App.</param>
IObservable<GitHubApp> Get(string slug);
/// <summary>
/// Returns the GitHub App associated with the authentication credentials used (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-the-authenticated-github-app</remarks>
IObservable<GitHubApp> GetCurrent();
/// <summary>
/// List installations of the authenticated GitHub App (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#find-installations</remarks>
IObservable<Installation> GetAllInstallationsForCurrent();
/// <summary>
/// List installations of the authenticated GitHub App (requires GitHubApp JWT token auth).
/// </summary>
/// <param name="options">Options for changing the API response</param>
/// <remarks>https://developer.github.com/v3/apps/#find-installations</remarks>
IObservable<Installation> GetAllInstallationsForCurrent(ApiOptions options);
/// <summary>
/// Get a single GitHub App Installation (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-a-single-installation</remarks>
/// <param name="installationId">The Id of the GitHub App Installation</param>
IObservable<Installation> GetInstallation(long installationId);
/// <summary>
/// Create a time bound access token for a GitHubApp Installation that can be used to access other API endpoints (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>
/// https://developer.github.com/v3/apps/#create-a-new-installation-token
/// https://developer.github.com/apps/building-github-apps/authentication-options-for-github-apps/#authenticating-as-an-installation
/// https://developer.github.com/v3/apps/available-endpoints/
/// </remarks>
/// <param name="installationId">The Id of the GitHub App Installation</param>
IObservable<AccessToken> CreateInstallationToken(long installationId);
}
}

View File

@@ -0,0 +1,85 @@
using Octokit.Reactive.Internal;
using System;
using System.Reactive.Threading.Tasks;
namespace Octokit.Reactive
{
/// <summary>
/// A client for GitHub Applications API. Provides the methods required to get GitHub applications and installations.
/// </summary>
/// <remarks>
/// See the <a href="http://developer.github.com/v3/apps/">GitHub Apps API documentation</a> for more information.
/// </remarks>
public class ObservableGitHubAppsClient : IObservableGitHubAppsClient
{
private IGitHubAppsClient _client;
private readonly IConnection _connection;
public ObservableGitHubAppsClient(IGitHubClient client)
{
Ensure.ArgumentNotNull(client, "client");
_client = client.GitHubApps;
_connection = client.Connection;
}
public IObservable<GitHubApp> Get(string slug)
{
return _client.Get(slug).ToObservable();
}
/// <summary>
/// Returns the GitHub App associated with the authentication credentials used (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-the-authenticated-github-app</remarks>
public IObservable<GitHubApp> GetCurrent()
{
return _client.GetCurrent().ToObservable();
}
/// <summary>
/// List installations of the authenticated GitHub App (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#find-installations</remarks>
public IObservable<Installation> GetAllInstallationsForCurrent()
{
return _connection.GetAndFlattenAllPages<Installation>(ApiUrls.Installations(), null, AcceptHeaders.GitHubAppsPreview);
}
/// <summary>
/// List installations of the authenticated GitHub App (requires GitHubApp JWT token auth).
/// </summary>
/// <param name="options">Options for changing the API response</param>
/// <remarks>https://developer.github.com/v3/apps/#find-installations</remarks>
public IObservable<Installation> GetAllInstallationsForCurrent(ApiOptions options)
{
Ensure.ArgumentNotNull(options, nameof(options));
return _connection.GetAndFlattenAllPages<Installation>(ApiUrls.Installations(), null, AcceptHeaders.GitHubAppsPreview, options);
}
/// <summary>
/// Get a single GitHub App Installation (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-a-single-installation</remarks>
/// <param name="installationId">The Id of the GitHub App Installation</param>
public IObservable<Installation> GetInstallation(long installationId)
{
return _client.GetInstallation(installationId).ToObservable();
}
/// <summary>
/// Create a time bound access token for a GitHubApp Installation that can be used to access other API endpoints (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>
/// https://developer.github.com/v3/apps/#create-a-new-installation-token
/// https://developer.github.com/apps/building-github-apps/authentication-options-for-github-apps/#authenticating-as-an-installation
/// https://developer.github.com/v3/apps/available-endpoints/
/// </remarks>
/// <param name="installationId">The Id of the GitHub App Installation</param>
public IObservable<AccessToken> CreateInstallationToken(long installationId)
{
return _client.CreateInstallationToken(installationId).ToObservable();
}
}
}

View File

@@ -18,6 +18,7 @@ namespace Octokit.Reactive
IObservableAuthorizationsClient Authorization { get; }
IObservableActivitiesClient Activity { get; }
IObservableGitHubAppsClient GitHubApps { get; }
IObservableIssuesClient Issue { get; }
IObservableMiscellaneousClient Miscellaneous { get; }
IObservableOauthClient Oauth { get; }

View File

@@ -33,6 +33,7 @@ namespace Octokit.Reactive
_gitHubClient = gitHubClient;
Authorization = new ObservableAuthorizationsClient(gitHubClient);
Activity = new ObservableActivitiesClient(gitHubClient);
GitHubApps = new ObservableGitHubAppsClient(gitHubClient);
Issue = new ObservableIssuesClient(gitHubClient);
Miscellaneous = new ObservableMiscellaneousClient(gitHubClient);
Oauth = new ObservableOauthClient(gitHubClient);
@@ -69,6 +70,7 @@ namespace Octokit.Reactive
public IObservableAuthorizationsClient Authorization { get; private set; }
public IObservableActivitiesClient Activity { get; private set; }
public IObservableGitHubAppsClient GitHubApps { get; private set; }
public IObservableIssuesClient Issue { get; private set; }
public IObservableMiscellaneousClient Miscellaneous { get; private set; }
public IObservableOauthClient Oauth { get; private set; }

View File

@@ -33,6 +33,9 @@
<ItemGroup>
<PackageReference Include="System.Reactive" Version="3.1.0" />
<PackageReference Include="SourceLink.Create.GitHub" Version="2.8.0" />
<DotNetCliToolReference Include="dotnet-sourcelink-git" Version="2.8.0" />
<DotNetCliToolReference Include="dotnet-sourcelink" Version="2.8.0" />
</ItemGroup>
<ItemGroup Condition=" '$(TargetFramework)' == 'net45' ">
@@ -40,10 +43,4 @@
<Reference Include="Microsoft.CSharp" />
</ItemGroup>
<ItemGroup>
<PackageReference Include="SourceLink.Create.GitHub" Version="2.1.0" PrivateAssets="All" />
<DotNetCliToolReference Include="dotnet-sourcelink-git" Version="2.1.0" />
<DotNetCliToolReference Include="dotnet-sourcelink" Version="2.1.0" />
</ItemGroup>
</Project>

View File

@@ -13,7 +13,7 @@ namespace Octokit.Tests.Conventions
static string CreateMessage(Type modelType, IEnumerable<PropertyInfo> propertiesWithInvalidType)
{
return string.Format("Model type '{0}' contains the following properties that are named or suffixed with 'Url' but are not of type String: {!}{2}",
return string.Format("Model type '{0}' contains the following properties that are named or suffixed with 'Url' but are not of type String: {1}{2}",
modelType.FullName,
Environment.NewLine,
string.Join(Environment.NewLine, propertiesWithInvalidType.Select(x => x.Name)));

View File

@@ -9,8 +9,6 @@
<AssemblyName>Octokit.Tests.Conventions</AssemblyName>
<PackageId>Octokit.Tests.Conventions</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<NetStandardImplicitPackageVersion>1.6.0</NetStandardImplicitPackageVersion>
<RuntimeFrameworkVersion>1.0.4</RuntimeFrameworkVersion>
<GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>

View File

@@ -0,0 +1,130 @@
using Octokit.Tests.Integration.Helpers;
using System;
using System.Linq;
using System.Threading.Tasks;
using Xunit;
namespace Octokit.Tests.Integration.Clients
{
public class GitHubAppsClientTests
{
public class TheGetMethod
{
IGitHubClient _github;
public TheGetMethod()
{
// Regular authentication
_github = Helper.GetAuthenticatedClient();
}
[GitHubAppsTest]
public async Task GetsApp()
{
var result = await _github.GitHubApps.Get(Helper.GitHubAppSlug);
Assert.Equal(Helper.GitHubAppId, result.Id);
Assert.False(string.IsNullOrEmpty(result.Name));
Assert.NotNull(result.Owner);
}
}
public class TheGetCurrentMethod
{
IGitHubClient _github;
public TheGetCurrentMethod()
{
// Authenticate as a GitHubApp
_github = Helper.GetAuthenticatedGitHubAppsClient();
}
[GitHubAppsTest]
public async Task GetsCurrentApp()
{
var result = await _github.GitHubApps.GetCurrent();
Assert.Equal(Helper.GitHubAppId, result.Id);
Assert.False(string.IsNullOrEmpty(result.Name));
Assert.NotNull(result.Owner);
}
}
public class TheGetAllInstallationsForCurrentMethod
{
IGitHubClient _github;
public TheGetAllInstallationsForCurrentMethod()
{
// Authenticate as a GitHubApp
_github = Helper.GetAuthenticatedGitHubAppsClient();
}
[GitHubAppsTest]
public async Task GetsAllInstallations()
{
var result = await _github.GitHubApps.GetAllInstallationsForCurrent();
Assert.True(result.Any(x => x.Id == Helper.GitHubAppInstallationId));
foreach (var installation in result)
{
Assert.Equal(Helper.GitHubAppId, installation.AppId);
Assert.NotNull(installation.Account);
Assert.NotNull(installation.Permissions);
Assert.Equal(InstallationPermissionLevel.Read, installation.Permissions.Metadata);
Assert.False(string.IsNullOrEmpty(installation.HtmlUrl));
Assert.NotEqual(0, installation.TargetId);
}
}
}
public class TheGetInstallationMethod
{
IGitHubClient _github;
public TheGetInstallationMethod()
{
// Authenticate as a GitHubApp
_github = Helper.GetAuthenticatedGitHubAppsClient();
}
[GitHubAppsTest]
public async Task GetsInstallation()
{
var installationId = Helper.GitHubAppInstallationId;
var result = await _github.GitHubApps.GetInstallation(installationId);
Assert.True(result.AppId == Helper.GitHubAppId);
Assert.Equal(Helper.GitHubAppId, result.AppId);
Assert.NotNull(result.Account);
Assert.NotNull(result.Permissions);
Assert.Equal(InstallationPermissionLevel.Read, result.Permissions.Metadata);
Assert.False(string.IsNullOrEmpty(result.HtmlUrl));
Assert.NotEqual(0, result.TargetId);
}
}
public class TheCreateInstallationTokenMethod
{
IGitHubClient _github;
public TheCreateInstallationTokenMethod()
{
// Authenticate as a GitHubApp
_github = Helper.GetAuthenticatedGitHubAppsClient();
}
[GitHubAppsTest]
public async Task GetsInstallation()
{
var installationId = Helper.GitHubAppInstallationId;
var result = await _github.GitHubApps.CreateInstallationToken(installationId);
Assert.NotNull(result.Token);
Assert.True(DateTimeOffset.Now < result.ExpiresAt);
}
}
}
}

View File

@@ -64,6 +64,34 @@ namespace Octokit.Tests.Integration
return new Credentials(githubUsername, githubPassword);
});
static readonly Lazy<bool> _gitHubAppsEnabled = new Lazy<bool>(() =>
{
string enabled = Environment.GetEnvironmentVariable("OCTOKIT_GITHUBAPP_ENABLED");
return !String.IsNullOrWhiteSpace(enabled);
});
static readonly Lazy<Credentials> _githubAppCredentials = new Lazy<Credentials>(() =>
{
// GitHubJwt nuget package only available for netstandard2.0+
#if GITHUBJWT_HELPER_AVAILABLE
var generator = new GitHubJwt.GitHubJwtFactory(
new GitHubJwt.FilePrivateKeySource(GitHubAppPemFile),
new GitHubJwt.GitHubJwtFactoryOptions
{
AppIntegrationId = Convert.ToInt32(GitHubAppId),
ExpirationSeconds = 500
}
);
var jwtToken = generator.CreateEncodedJwtToken();
return new Credentials(jwtToken, AuthenticationType.Bearer);
#else
// return null, which will cause the [GitHubAppTest]'s to not be discovered
return null;
#endif
});
static readonly Lazy<Uri> _customUrl = new Lazy<Uri>(() =>
{
string uri = Environment.GetEnvironmentVariable("OCTOKIT_CUSTOMURL");
@@ -96,6 +124,8 @@ namespace Octokit.Tests.Integration
public static Credentials BasicAuthCredentials { get { return _basicAuthCredentials.Value; } }
public static Credentials GitHubAppCredentials { get { return _githubAppCredentials.Value; } }
public static Uri CustomUrl { get { return _customUrl.Value; } }
public static Uri TargetUrl { get { return CustomUrl ?? GitHubClient.GitHubApiUrl; } }
@@ -126,6 +156,26 @@ namespace Octokit.Tests.Integration
get { return Environment.GetEnvironmentVariable("OCTOKIT_CLIENTSECRET"); }
}
public static long GitHubAppId
{
get { return Convert.ToInt64(Environment.GetEnvironmentVariable("OCTOKIT_GITHUBAPP_ID")); }
}
public static string GitHubAppPemFile
{
get { return Environment.GetEnvironmentVariable("OCTOKIT_GITHUBAPP_PEMFILE"); }
}
public static string GitHubAppSlug
{
get { return Environment.GetEnvironmentVariable("OCTOKIT_GITHUBAPP_SLUG"); }
}
public static long GitHubAppInstallationId
{
get { return Convert.ToInt64(Environment.GetEnvironmentVariable("OCTOKIT_GITHUBAPP_INSTALLATIONID")); }
}
public static void DeleteRepo(IConnection connection, Repository repository)
{
if (repository != null)
@@ -244,6 +294,16 @@ namespace Octokit.Tests.Integration
};
}
public static bool IsGitHubAppsEnabled { get { return _gitHubAppsEnabled.Value; } }
public static GitHubClient GetAuthenticatedGitHubAppsClient()
{
return new GitHubClient(new ProductHeaderValue("OctokitTests"), TargetUrl)
{
Credentials = GitHubAppCredentials
};
}
public static void DeleteInvitations(IConnection connection, List<string> invitees, int teamId)
{
try

View File

@@ -0,0 +1,46 @@
using System.Collections.Generic;
using System.Linq;
using Xunit;
using Xunit.Abstractions;
using Xunit.Sdk;
namespace Octokit.Tests.Integration
{
public class GitHubAppsTestDiscoverer : IXunitTestCaseDiscoverer
{
readonly IMessageSink diagnosticMessageSink;
public GitHubAppsTestDiscoverer(IMessageSink diagnosticMessageSink)
{
this.diagnosticMessageSink = diagnosticMessageSink;
}
public IEnumerable<IXunitTestCase> Discover(ITestFrameworkDiscoveryOptions discoveryOptions, ITestMethod testMethod, IAttributeInfo factAttribute)
{
if (!Helper.IsGitHubAppsEnabled)
return Enumerable.Empty<IXunitTestCase>();
Credentials creds = null;
try
{
// Make sure we can generate GitHub App credentials
creds = Helper.GitHubAppCredentials;
}
catch
{
}
if (creds == null)
{
return Enumerable.Empty<IXunitTestCase>();
}
return new[] { new XunitTestCase(diagnosticMessageSink, discoveryOptions.MethodDisplayOrDefault(), testMethod) };
}
}
[XunitTestCaseDiscoverer("Octokit.Tests.Integration.GitHubAppsTestDiscoverer", "Octokit.Tests.Integration")]
public class GitHubAppsTestAttribute : FactAttribute
{
}
}

View File

@@ -9,8 +9,6 @@
<AssemblyName>Octokit.Tests.Integration</AssemblyName>
<PackageId>Octokit.Tests.Integration</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<NetStandardImplicitPackageVersion>1.6.0</NetStandardImplicitPackageVersion>
<RuntimeFrameworkVersion>1.0.4</RuntimeFrameworkVersion>
<GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
@@ -21,6 +19,10 @@
<GenerateAssemblyFileVersionAttribute>false</GenerateAssemblyFileVersionAttribute>
</PropertyGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.0' ">
<DefineConstants>$(DefineConstants);GITHUBJWT_HELPER_AVAILABLE</DefineConstants>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\Octokit.Tests\Helpers\AssertEx.cs" />
<EmbeddedResource Include="fixtures\hello-world.txt;fixtures\hello-world.zip" Exclude="bin\**;obj\**;**\*.xproj;packages\**;@(EmbeddedResource)" />
@@ -43,4 +45,10 @@
<Service Include="{82a7f48d-3b50-4b1e-b82e-3ada8210c358}" />
</ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'netcoreapp1.0'">
<PackageReference Include="GitHubJwt">
<Version>0.0.2</Version>
</PackageReference>
</ItemGroup>
</Project>

View File

@@ -0,0 +1,105 @@
using System;
using System.Threading.Tasks;
using NSubstitute;
using Xunit;
namespace Octokit.Tests.Clients
{
public class GitHubAppsClientTests
{
public class TheCtor
{
[Fact]
public void EnsuresNonNullArguments()
{
Assert.Throws<ArgumentNullException>(() => new GitHubAppsClient(null));
}
}
public class TheGetCurrentMethod
{
[Fact]
public void GetsFromCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);
client.GetCurrent();
connection.Received().Get<GitHubApp>(Arg.Is<Uri>(u => u.ToString() == "app"), null, "application/vnd.github.machine-man-preview+json");
}
}
public class TheGetAllInstallationsForCurrentMethod
{
[Fact]
public async Task EnsuresNonNullArguments()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);
Assert.ThrowsAsync<ArgumentNullException>(() => client.GetAllInstallationsForCurrent(null));
}
[Fact]
public async Task RequestsCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);
client.GetAllInstallationsForCurrent();
connection.Received().GetAll<Installation>(Arg.Is<Uri>(u => u.ToString() == "app/installations"), null, "application/vnd.github.machine-man-preview+json");
}
[Fact]
public void RequestsTheCorrectUrlWithApiOptions()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);
var options = new ApiOptions
{
PageSize = 1,
PageCount = 1,
StartPage = 1
};
client.GetAllInstallationsForCurrent(options);
connection.Received().GetAll<Installation>(Arg.Is<Uri>(u => u.ToString() == "app/installations"), null, "application/vnd.github.machine-man-preview+json", options);
}
}
public class TheGetInstallationMethod
{
[Fact]
public void GetsFromCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);
client.GetInstallation(123);
connection.Received().Get<Installation>(Arg.Is<Uri>(u => u.ToString() == "app/installations/123"), null, "application/vnd.github.machine-man-preview+json");
}
}
public class TheCreateInstallationTokenMethod
{
[Fact]
public void PostsToCorrectUrl()
{
var connection = Substitute.For<IApiConnection>();
var client = new GitHubAppsClient(connection);
int fakeInstallationId = 3141;
client.CreateInstallationToken(fakeInstallationId);
connection.Received().Post<AccessToken>(Arg.Is<Uri>(u => u.ToString() == "installations/3141/access_tokens"), string.Empty, "application/vnd.github.machine-man-preview+json");
}
}
}
}

View File

@@ -9,8 +9,6 @@
<AssemblyName>Octokit.Tests</AssemblyName>
<PackageId>Octokit.Tests</PackageId>
<GenerateRuntimeConfigurationFiles>true</GenerateRuntimeConfigurationFiles>
<NetStandardImplicitPackageVersion>1.6.0</NetStandardImplicitPackageVersion>
<RuntimeFrameworkVersion>1.0.4</RuntimeFrameworkVersion>
<GenerateAssemblyTitleAttribute>false</GenerateAssemblyTitleAttribute>
<GenerateAssemblyDescriptionAttribute>false</GenerateAssemblyDescriptionAttribute>
<GenerateAssemblyConfigurationAttribute>false</GenerateAssemblyConfigurationAttribute>
@@ -31,7 +29,7 @@
<ItemGroup>
<ProjectReference Include="..\Octokit\Octokit.csproj" />
<ProjectReference Include="..\Octokit.Reactive\Octokit.Reactive.csproj" />
<PackageReference Include="NSubstitute" Version="2.0.2" />
<PackageReference Include="NSubstitute" Version="3.1.0" />
</ItemGroup>
<PropertyGroup Condition=" '$(TargetFramework)' == 'netcoreapp1.0' ">

View File

@@ -16,6 +16,10 @@
/// <summary>
/// Delegated access to a third party
/// </summary>
Oauth
Oauth,
/// <summary>
/// Credential for GitHub App using signed JWT
/// </summary>
Bearer
}
}

View File

@@ -10,7 +10,8 @@ namespace Octokit.Internal
{
{ AuthenticationType.Anonymous, new AnonymousAuthenticator() },
{ AuthenticationType.Basic, new BasicAuthenticator() },
{ AuthenticationType.Oauth, new TokenAuthenticator() }
{ AuthenticationType.Oauth, new TokenAuthenticator() },
{ AuthenticationType.Bearer, new BearerTokenAuthenticator() }
};
public Authenticator(ICredentialStore credentialStore)

View File

@@ -0,0 +1,23 @@
using System;
using System.Globalization;
namespace Octokit.Internal
{
class BearerTokenAuthenticator: IAuthenticationHandler
{
public void Authenticate(IRequest request, Credentials credentials)
{
Ensure.ArgumentNotNull(request, nameof(request));
Ensure.ArgumentNotNull(credentials, nameof(credentials));
Ensure.ArgumentNotNull(credentials.Password, nameof(credentials.Password));
if (credentials.Login != null)
{
throw new InvalidOperationException("The Login is not null for a token authentication request. You " +
"probably did something wrong.");
}
request.Headers["Authorization"] = string.Format(CultureInfo.InvariantCulture, "Bearer {0}", credentials.Password);
}
}
}

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Octokit
{
/// <summary>
/// A client for GitHub Applications API. Provides the methods required to get GitHub applications and installations.
/// </summary>
/// <remarks>
/// See the <a href="http://developer.github.com/v3/apps/">GitHub Apps API documentation</a> for more information.
/// </remarks>
public class GitHubAppsClient : ApiClient, IGitHubAppsClient
{
public GitHubAppsClient(IApiConnection apiConnection) : base(apiConnection)
{
}
/// <summary>
/// Get a single GitHub App.
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-a-single-github-app</remarks>
/// <param name="slug">The URL-friendly name of your GitHub App. You can find this on the settings page for your GitHub App.</param>
public Task<GitHubApp> Get(string slug)
{
return ApiConnection.Get<GitHubApp>(ApiUrls.App(slug), null, AcceptHeaders.GitHubAppsPreview);
}
/// <summary>
/// Returns the GitHub App associated with the authentication credentials used (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-the-authenticated-github-app</remarks>
public Task<GitHubApp> GetCurrent()
{
return ApiConnection.Get<GitHubApp>(ApiUrls.App(), null, AcceptHeaders.GitHubAppsPreview);
}
/// <summary>
/// List installations of the authenticated GitHub App (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#find-installations</remarks>
public Task<IReadOnlyList<Installation>> GetAllInstallationsForCurrent()
{
return ApiConnection.GetAll<Installation>(ApiUrls.Installations(), null, AcceptHeaders.GitHubAppsPreview);
}
/// <summary>
/// List installations of the authenticated GitHub App (requires GitHubApp JWT token auth).
/// </summary>
/// <param name="options">Options for changing the API response</param>
/// <remarks>https://developer.github.com/v3/apps/#find-installations</remarks>
public Task<IReadOnlyList<Installation>> GetAllInstallationsForCurrent(ApiOptions options)
{
Ensure.ArgumentNotNull(options, nameof(options));
return ApiConnection.GetAll<Installation>(ApiUrls.Installations(), null, AcceptHeaders.GitHubAppsPreview, options);
}
/// <summary>
/// Get a single GitHub App Installation (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-a-single-installation</remarks>
/// <param name="installationId">The Id of the GitHub App Installation</param>
public Task<Installation> GetInstallation(long installationId)
{
return ApiConnection.Get<Installation>(ApiUrls.Installation(installationId), null, AcceptHeaders.GitHubAppsPreview);
}
/// <summary>
/// Create a time bound access token for a GitHubApp Installation that can be used to access other API endpoints (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>
/// https://developer.github.com/v3/apps/#create-a-new-installation-token
/// https://developer.github.com/apps/building-github-apps/authentication-options-for-github-apps/#authenticating-as-an-installation
/// https://developer.github.com/v3/apps/available-endpoints/
/// </remarks>
/// <param name="installationId">The Id of the GitHub App Installation</param>
public Task<AccessToken> CreateInstallationToken(long installationId)
{
return ApiConnection.Post<AccessToken>(ApiUrls.AccessTokens(installationId), string.Empty, AcceptHeaders.GitHubAppsPreview);
}
}
}

View File

@@ -0,0 +1,58 @@
using System.Collections.Generic;
using System.Threading.Tasks;
namespace Octokit
{
/// <summary>
/// A client for GitHub Applications API. Provides the methods required to get GitHub applications and installations.
/// </summary>
/// <remarks>
/// See the <a href="http://developer.github.com/v3/apps/">GitHub Apps API documentation</a> for more information.
/// </remarks>
public interface IGitHubAppsClient
{
/// <summary>
/// Get a single GitHub App.
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-a-single-github-app</remarks>
/// <param name="slug">The URL-friendly name of your GitHub App. You can find this on the settings page for your GitHub App.</param>
Task<GitHubApp> Get(string slug);
/// <summary>
/// Returns the GitHub App associated with the authentication credentials used (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-the-authenticated-github-app</remarks>
Task<GitHubApp> GetCurrent();
/// <summary>
/// List installations of the authenticated GitHub App (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#find-installations</remarks>
Task<IReadOnlyList<Installation>> GetAllInstallationsForCurrent();
/// <summary>
/// List installations of the authenticated GitHub App (requires GitHubApp JWT token auth).
/// </summary>
/// <param name="options">Options for changing the API response</param>
/// <remarks>https://developer.github.com/v3/apps/#find-installations</remarks>
Task<IReadOnlyList<Installation>> GetAllInstallationsForCurrent(ApiOptions options);
/// <summary>
/// Get a single GitHub App Installation (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>https://developer.github.com/v3/apps/#get-a-single-installation</remarks>
/// <param name="installationId">The Id of the GitHub App Installation</param>
Task<Installation> GetInstallation(long installationId);
/// <summary>
/// Create a time bound access token for a GitHubApp Installation that can be used to access other API endpoints (requires GitHubApp JWT token auth).
/// </summary>
/// <remarks>
/// https://developer.github.com/v3/apps/#create-a-new-installation-token
/// https://developer.github.com/apps/building-github-apps/authentication-options-for-github-apps/#authenticating-as-an-installation
/// https://developer.github.com/v3/apps/available-endpoints/
/// </remarks>
/// <param name="installationId">The Id of the GitHub App Installation</param>
Task<AccessToken> CreateInstallationToken(long installationId);
}
}

View File

@@ -99,6 +99,7 @@ namespace Octokit
Enterprise = new EnterpriseClient(apiConnection);
Gist = new GistsClient(apiConnection);
Git = new GitDatabaseClient(apiConnection);
GitHubApps = new GitHubAppsClient(apiConnection);
Issue = new IssuesClient(apiConnection);
Migration = new MigrationClient(apiConnection);
Miscellaneous = new MiscellaneousClient(connection);
@@ -263,6 +264,14 @@ namespace Octokit
/// </remarks>
public IGitDatabaseClient Git { get; private set; }
/// <summary>
/// Access GitHub's Apps API.
/// </summary>
/// <remarks>
/// Refer to the API documentation for more information: https://developer.github.com/v3/apps/
/// </remarks>
public IGitHubAppsClient GitHubApps { get; private set; }
/// <summary>
/// Access GitHub's Search API.
/// </summary>

View File

@@ -55,6 +55,8 @@ namespace Octokit
public const string NestedTeamsPreview = "application/vnd.github.hellcat-preview+json";
public const string GitHubAppsPreview = "application/vnd.github.machine-man-preview+json";
/// <summary>
/// Combines multiple preview headers. GitHub API supports Accept header with multiple
/// values separated by comma.

View File

@@ -285,12 +285,54 @@ namespace Octokit
/// Returns the <see cref="Uri"/> for the specified notification's subscription status.
/// </summary>
/// <param name="id">The Id of the notification.</param>
/// <returns></returns>
public static Uri NotificationSubscription(int id)
{
return "notifications/threads/{0}/subscription".FormatUri(id);
}
/// <summary>
/// Returns the <see cref="Uri"/> for creating a new installation token.
/// </summary>
/// <param name="installationId">The Id of the GitHub App installation.</param>
public static Uri AccessTokens(long installationId)
{
return "installations/{0}/access_tokens".FormatUri(installationId);
}
/// <summary>
/// Returns the <see cref="Uri"/> that creates a github app.
/// </summary>
public static Uri App()
{
return "app".FormatUri();
}
/// <summary>
/// Returns the <see cref="Uri"/> that creates a github app.
/// </summary>
public static Uri App(string slug)
{
return "apps/{0}".FormatUri(slug);
}
/// <summary>
/// Returns the <see cref="Uri"/> that returns all the installations of the authenticated application.
/// </summary>
/// <returns></returns>
public static Uri Installations()
{
return "app/installations".FormatUri();
}
/// <summary>
/// Returns the <see cref="Uri"/> that returns a single installation of the authenticated application.
/// </summary>
/// <returns></returns>
public static Uri Installation(long installationId)
{
return "app/installations/{0}".FormatUri(installationId);
}
/// <summary>
/// Returns the <see cref="Uri"/> that returns all of the issues across all the authenticated users visible
/// repositories including owned repositories, member repositories, and organization repositories:

View File

@@ -148,7 +148,7 @@ namespace Octokit
{
throw new ArgumentException(
string.Format(CultureInfo.InvariantCulture, "The base address '{0}' must be an absolute URI",
baseAddress), "baseAddress");
baseAddress), nameof(baseAddress));
}
UserAgent = FormatUserAgent(productInformation);
@@ -264,6 +264,14 @@ namespace Octokit
return SendData<T>(uri, HttpMethod.Post, body, accepts, contentType, CancellationToken.None);
}
public Task<IApiResponse<T>> Post<T>(Uri uri, object body, string accepts, string contentType, IDictionary<string, string> parameters)
{
Ensure.ArgumentNotNull(uri, nameof(uri));
Ensure.ArgumentNotNull(body, nameof(body));
return SendData<T>(uri.ApplyParameters(parameters), HttpMethod.Post, body, accepts, contentType, CancellationToken.None);
}
/// <summary>
/// Performs an asynchronous HTTP POST request.
/// Attempts to map the response body to an object of type <typeparamref name="T"/>

View File

@@ -13,23 +13,27 @@ namespace Octokit
AuthenticationType = AuthenticationType.Anonymous;
}
public Credentials(string token)
{
Ensure.ArgumentNotNullOrEmptyString(token, nameof(token));
public Credentials(string login, string password) : this(login, password, AuthenticationType.Basic) { }
Login = null;
Password = token;
AuthenticationType = AuthenticationType.Oauth;
}
public Credentials(string login, string password)
public Credentials(string login, string password, AuthenticationType authenticationType)
{
Ensure.ArgumentNotNullOrEmptyString(login, nameof(login));
Ensure.ArgumentNotNullOrEmptyString(password, nameof(password));
Login = login;
Password = password;
AuthenticationType = AuthenticationType.Basic;
AuthenticationType = authenticationType;
}
public Credentials(string token) : this(token, AuthenticationType.Oauth) { }
public Credentials(string token, AuthenticationType authenticationType)
{
Ensure.ArgumentNotNullOrEmptyString(token, nameof(token));
Login = null;
Password = token;
AuthenticationType = authenticationType;
}
public string Login

View File

@@ -126,8 +126,9 @@ namespace Octokit
/// <param name="body">The object to serialize as the body of the request</param>
/// <param name="accepts">Specifies accepted response media types.</param>
/// <param name="contentType">Specifies the media type of the request body</param>
/// <param name="parameters">Extra parameters for authentication.</param>
/// <returns><seealso cref="IResponse"/> representing the received HTTP response</returns>
Task<IApiResponse<T>> Post<T>(Uri uri, object body, string accepts, string contentType);
Task<IApiResponse<T>> Post<T>(Uri uri, object body, string accepts, string contentType, IDictionary<string, string> parameters = null);
/// <summary>
/// Performs an asynchronous HTTP POST request.

View File

@@ -38,6 +38,14 @@ namespace Octokit
/// </remarks>
IActivitiesClient Activity { get; }
/// <summary>
/// Access GitHub's Application API.
/// </summary>
/// <remarks>
/// Refer to the API documentation for more information: https://developer.github.com/v3/apps/
/// </remarks>
IGitHubAppsClient GitHubApps { get; }
/// <summary>
/// Access GitHub's Issue API.
/// </summary>

View File

@@ -0,0 +1,36 @@
using System;
using System.Diagnostics;
using System.Globalization;
namespace Octokit
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class AccessToken
{
public AccessToken() { }
public AccessToken(string token, DateTimeOffset expiresAt)
{
Token = token;
ExpiresAt = expiresAt;
}
/// <summary>
/// The access token
/// </summary>
public string Token { get; protected set; }
/// <summary>
/// The expiration date
/// </summary>
public DateTimeOffset ExpiresAt { get; protected set; }
internal string DebuggerDisplay
{
get
{
return string.Format(CultureInfo.InvariantCulture, "Token: {0}, ExpiresAt: {1}", Token, ExpiresAt);
}
}
}
}

View File

@@ -7,6 +7,7 @@ namespace Octokit
{
public Repository Repository { get; protected set; }
public User Sender { get; protected set; }
public InstallationId Installation { get; protected set; }
internal string DebuggerDisplay
{

View File

@@ -0,0 +1,72 @@
using System;
using System.Diagnostics;
using System.Globalization;
namespace Octokit
{
/// <summary>
/// Represents a GitHub application.
/// </summary>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class GitHubApp
{
public GitHubApp() { }
public GitHubApp(long id, string name, User owner, string description, string externalUrl, string htmlUrl, DateTimeOffset createdAt, DateTimeOffset updatedAt)
{
Id = id;
Name = name;
Owner = owner;
Description = description;
ExternalUrl = externalUrl;
HtmlUrl = htmlUrl;
CreatedAt = createdAt;
UpdatedAt = updatedAt;
}
/// <summary>
/// The Id of the GitHub App.
/// </summary>
public long Id { get; protected set; }
/// <summary>
/// The Name of the GitHub App.
/// </summary>
public string Name { get; protected set; }
/// <summary>
/// The Owner of the GitHub App.
/// </summary>
public User Owner { get; protected set; }
/// <summary>
/// The Description of the GitHub App.
/// </summary>
public string Description { get; protected set; }
/// <summary>
/// The URL to the GitHub App's external website.
/// </summary>
public string ExternalUrl { get; protected set; }
/// <summary>
/// The URL to view the GitHub App on GitHub
/// </summary>
public string HtmlUrl { get; protected set; }
/// <summary>
/// Date the GitHub App was created.
/// </summary>
public DateTimeOffset CreatedAt { get; protected set; }
/// <summary>
/// Date the GitHub App was last updated.
/// </summary>
public DateTimeOffset UpdatedAt { get; protected set; }
internal string DebuggerDisplay
{
get { return string.Format(CultureInfo.InvariantCulture, "Id: {0} Name: {1}", Id, Name); }
}
}
}

View File

@@ -0,0 +1,91 @@
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using Octokit.Internal;
namespace Octokit
{
/// <summary>
/// Represents an application installation.
/// </summary>
/// <remarks>
/// For more information see https://developer.github.com/v3/apps/#find-installations
/// </remarks>
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class Installation : InstallationId
{
public Installation() { }
public Installation(long id, User account, string accessTokenUrl, string repositoriesUrl, string htmlUrl, long appId, long targetId, AccountType targetType, InstallationPermissions permissions, IReadOnlyList<string> events, string singleFileName, string repositorySelection) : base(id)
{
Account = account;
HtmlUrl = htmlUrl;
AppId = appId;
TargetId = targetId;
TargetType = targetType;
Permissions = permissions;
Events = events;
SingleFileName = singleFileName;
RepositorySelection = repositorySelection;
}
/// <summary>
/// The user who owns the Installation.
/// </summary>
public User Account { get; protected set; }
/// <summary>
/// The URL to view the Installation on GitHub.
/// </summary>
public string HtmlUrl { get; protected set; }
/// <summary>
/// The Id of the associated GitHub App.
/// </summary>
public long AppId { get; protected set; }
/// <summary>
/// The Id of the User/Organization the Installation is installed in
/// </summary>
public long TargetId { get; protected set; }
/// <summary>
/// The type of the target (User or Organization)
/// </summary>
public StringEnum<AccountType> TargetType { get; protected set; }
/// <summary>
/// The Permissions granted to the Installation
/// </summary>
public InstallationPermissions Permissions { get; private set; }
/// <summary>
/// The Events subscribed to by the Installation
/// </summary>
public IReadOnlyList<string> Events { get; private set; }
/// <summary>
/// The single file the GitHub App can managem (when Permissions.SingleFile is set to read or write)
/// </summary>
public string SingleFileName { get; protected set; }
/// <summary>
/// The choice of repositories the installation is on. Can be either "selected" or "all".
/// </summary>
public StringEnum<InstallationRepositorySelection> RepositorySelection { get; protected set; }
internal new string DebuggerDisplay
{
get { return string.Format(CultureInfo.InvariantCulture, "Id: {0} AppId: {1}", Id, AppId); }
}
}
public enum InstallationRepositorySelection
{
[Parameter(Value = "all")]
All,
[Parameter(Value = "selected")]
Selected
}
}

View File

@@ -0,0 +1,26 @@
using System.Diagnostics;
using System.Globalization;
namespace Octokit
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class InstallationId
{
public InstallationId() { }
public InstallationId(long id)
{
Id = id;
}
/// <summary>
/// The Installation Id.
/// </summary>
public long Id { get; protected set; }
internal string DebuggerDisplay
{
get { return string.Format(CultureInfo.InvariantCulture, "Id: {0}", Id); }
}
}
}

View File

@@ -0,0 +1,121 @@
using System.Diagnostics;
using System.Globalization;
using Octokit.Internal;
namespace Octokit
{
[DebuggerDisplay("{DebuggerDisplay,nq}")]
public class InstallationPermissions
{
public InstallationPermissions() { }
public InstallationPermissions(InstallationPermissionLevel? metadata, InstallationPermissionLevel? administration, InstallationPermissionLevel? statuses, InstallationPermissionLevel? deployments, InstallationPermissionLevel? issues, InstallationPermissionLevel? pages, InstallationPermissionLevel? pullRequests, InstallationPermissionLevel? contents, InstallationPermissionLevel? singleFile, InstallationPermissionLevel? repositoryProjects, InstallationPermissionLevel? members, InstallationPermissionLevel? organizationProjects, InstallationPermissionLevel? teamDiscussions)
{
Metadata = metadata;
Administration = administration;
Statuses = statuses;
Deployments = deployments;
Issues = issues;
Pages = pages;
PullRequests = pullRequests;
Contents = contents;
SingleFile = singleFile;
RepositoryProjects = repositoryProjects;
Members = members;
OrganizationProjects = organizationProjects;
TeamDiscussions = teamDiscussions;
}
/// <summary>
/// Repository metadata
/// Search repositories, list collaborators, and access repository metadata.
/// </summary>
public StringEnum<InstallationPermissionLevel>? Metadata { get; protected set; }
/// <summary>
/// Repository administration
/// Repository creation, deletion, settings, teams, and collaborators.
/// </summary>
public StringEnum<InstallationPermissionLevel>? Administration { get; protected set; }
/// <summary>
/// Commit statuses
/// Commit statuses.
/// </summary>
public StringEnum<InstallationPermissionLevel>? Statuses { get; protected set; }
/// <summary>
/// Deployments
/// Deployments and deployment statuses.
/// </summary>
public StringEnum<InstallationPermissionLevel>? Deployments { get; protected set; }
/// <summary>
/// Issues
/// Issues and related comments, assignees, labels, and milestones.
/// </summary>
public StringEnum<InstallationPermissionLevel>? Issues { get; protected set; }
/// <summary>
/// Pages
/// Retrieve Pages statuses, configuration, and builds, as well as create new builds.
/// </summary>
public StringEnum<InstallationPermissionLevel>? Pages { get; protected set; }
/// <summary>
/// Pull requests
/// Pull requests and related comments, assignees, labels, milestones, and merges.
/// </summary>
public StringEnum<InstallationPermissionLevel>? PullRequests { get; protected set; }
/// <summary>
/// Repository contents
/// Repository contents, commits, branches, downloads, releases, and merges.
/// </summary>
public StringEnum<InstallationPermissionLevel>? Contents { get; protected set; }
/// <summary>
/// Single file
/// Manage just a single file.
/// </summary>
public StringEnum<InstallationPermissionLevel>? SingleFile { get; protected set; }
/// <summary>
/// Repository projects
/// Manage repository projects, columns, and cards.
/// </summary>
public StringEnum<InstallationPermissionLevel>? RepositoryProjects { get; protected set; }
/// <summary>
/// Organization members (only applicable when installed for an Organization )
/// Organization members and teams.
/// </summary>
public StringEnum<InstallationPermissionLevel>? Members { get; protected set; }
/// <summary>
/// Organization projects (only applicable when installed for an Organization )
/// Manage organization projects, columns, and cards.
/// </summary>
public StringEnum<InstallationPermissionLevel>? OrganizationProjects { get; protected set; }
/// <summary>
/// Team discussions (only applicable when installed for an Organization )
/// Team discussions.
/// </summary>
public StringEnum<InstallationPermissionLevel>? TeamDiscussions { get; protected set; }
internal string DebuggerDisplay
{
get { return string.Format(CultureInfo.InvariantCulture, "Metadata: {0}, Contents: {1}, Issues: {2}, Single File: {3}", Metadata, Contents, Issues, SingleFile); }
}
}
public enum InstallationPermissionLevel
{
[Parameter(Value = "read")]
Read,
[Parameter(Value = "write")]
Write
}
}

View File

@@ -37,9 +37,9 @@
</ItemGroup>
<ItemGroup>
<PackageReference Include="SourceLink.Create.GitHub" Version="2.1.0" PrivateAssets="All" />
<DotNetCliToolReference Include="dotnet-sourcelink-git" Version="2.1.0" />
<DotNetCliToolReference Include="dotnet-sourcelink" Version="2.1.0" />
<PackageReference Include="SourceLink.Create.GitHub" Version="2.8.0" />
<DotNetCliToolReference Include="dotnet-sourcelink-git" Version="2.8.0" />
<DotNetCliToolReference Include="dotnet-sourcelink" Version="2.8.0" />
</ItemGroup>
</Project>

View File

@@ -1293,6 +1293,7 @@ namespace Octokit
private static readonly string[] Iso8601Format = new string[]
{
@"yyyy-MM-dd\THH:mm:ss.FFFFFFF\Z",
@"yyyy-MM-dd\THH:mm:ss.FFFFFFFK",
@"yyyy-MM-dd\THH:mm:ss\Z",
@"yyyy-MM-dd\THH:mm:ssK"
};

View File

@@ -5,7 +5,7 @@ init:
build_script:
- dotnet --info
- ps: .\build.ps1 -LinkSources
- ps: .\build.ps1 -LinkSources -Verbosity Verbose
test: off

View File

@@ -26,14 +26,14 @@ Param(
[string]$Configuration = "Release",
[switch]$LinkSources,
[ValidateSet("Quiet", "Minimal", "Normal", "Verbose", "Diagnostic")]
[string]$Verbosity = "Verbose",
[string]$Verbosity = "Normal",
[switch]$WhatIf,
[Parameter(Position=0,Mandatory=$false,ValueFromRemainingArguments=$true)]
[string[]]$ScriptArgs
)
$DotNetChannel = "preview";
$DotNetVersion = "1.0.1";
$DotNetVersion = "1.0.4";
$DotNetInstallerUri = "https://raw.githubusercontent.com/dotnet/cli/rel/1.0.1/scripts/obtain/dotnet-install.ps1";
$NugetUrl = "https://dist.nuget.org/win-x86-commandline/latest/nuget.exe"
@@ -106,14 +106,14 @@ $Arguments = @{
# Start Cake
Push-Location
Set-Location build
Write-Host "Restoring packages..."
Write-Host "Preparing Cake.Frosting build runner..."
Invoke-Expression "dotnet restore"
if($LASTEXITCODE -ne 0) {
Pop-Location;
exit $LASTEXITCODE;
}
Write-Host "Running build..."
Invoke-Expression "dotnet run -- $Arguments"
Write-Host "Running Cake.Frosting build runner..."
Invoke-Expression "dotnet run $Arguments"
if($LASTEXITCODE -ne 0) {
Pop-Location;
exit $LASTEXITCODE;

View File

@@ -7,5 +7,8 @@ if [[ "$TRAVIS_OS_NAME" == "osx" ]]; then
fi
cd build
echo "Preparing Cake.Frosting build runner..."
dotnet restore
dotnet run -- "$@"
echo "Executing Cake.Frosting build runner..."
dotnet run "$@"

View File

@@ -1,4 +1,7 @@
{
// Use IntelliSense to find out which attributes exist for C# debugging
// Use hover for the description of the existing attributes
// For further information visit https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md
"version": "0.2.0",
"configurations": [
{
@@ -6,10 +9,12 @@
"type": "coreclr",
"request": "launch",
"preLaunchTask": "build",
"program": "${workspaceRoot}\\bin\\Debug\\netcoreapp1.0\\build.dll",
// If you have changed target frameworks, make sure to update the program path.
"program": "${workspaceFolder}/bin/Debug/netcoreapp2.0/Build.dll",
"args": [],
"cwd": "${workspaceRoot}",
"externalConsole": false,
"cwd": "${workspaceFolder}",
// For more information about the 'console' field, see https://github.com/OmniSharp/omnisharp-vscode/blob/master/debugger-launchjson.md#console-terminal-window
"console": "internalConsole",
"stopAtEntry": false,
"internalConsoleOptions": "openOnSessionStart"
},
@@ -17,7 +22,7 @@
"name": ".NET Core Attach",
"type": "coreclr",
"request": "attach",
"processId": "${command.pickProcess}"
"processId": "${command:pickProcess}"
}
]
,]
}

View File

@@ -1,15 +1,14 @@
{
"version": "0.1.0",
"command": "dotnet",
"isShellCommand": true,
"args": [],
"version": "2.0.0",
"tasks": [
{
"taskName": "build",
"label": "build",
"command": "dotnet",
"type": "process",
"args": [
"${workspaceRoot}\\project.json"
"build",
"${workspaceFolder}/Build.csproj"
],
"isBuildCommand": true,
"problemMatcher": "$msCompile"
}
]

View File

@@ -13,7 +13,7 @@ public class Build : FrostingTask<Context>
Configuration = context.Configuration,
ArgumentCustomization = args => args
.Append("/p:Version={0}", context.Version.GetSemanticVersion())
.AppendIfTrue(context.LinkSources, "/p:SourceLinkCreate=true")
.Append("/p:SourceLinkCreate={0}", context.LinkSources.ToString().ToLower())
});
}
}

131
docs/github-apps.md Normal file
View File

@@ -0,0 +1,131 @@
# Working with GitHub Apps
## Overview
GitHub Apps are a new type of integration offering narrow, specific permissions, compared to OAuth apps or user authentication.
To learn more, head to the GitHub Apps section under the GitHub Developer [Getting started with Building Apps](https://developer.github.com/apps/getting-started-with-building-apps/#using-github-apps) documentation.
A GitHub App (known in Octokit as a `GitHubApp`) specifies permissions (read, write, none) it will be granted for various scopes and also registers for various webhook events.
A GitHub App is installed in an `Organization` or `User` account (known in Octokit as an `Installation`) where it is further limited to nominated (or all) repositories for that account.
The [GitHub Api Documentation](https://developer.github.com/v3/apps/) on GitHub Apps contains more detailed information.
## Authentication
The below walkthrough outlines how to authenticate as a `GitHubApp` and an `Installation` using Octokit.net.
Be sure to see the [GitHub Api Documentation](https://developer.github.com/apps/building-github-apps/authentication-options-for-github-apps/#authentication-options-for-github-apps) on GitHub Apps authentication, if you require more details.
## GitHub App Walkthrough
Each GitHub App has a private certificate (PEM file) generated through the GitHub website and possessed by the owner of the GitHub App. Authentication occurs using a time based JWT token, signed by the GitHub App's private certificate.
``` csharp
// A time based JWT token, signed by the GitHub App's private certificate
var jwtToken = "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJpYXQiOjE1MjA0Mjc3MTQsImV4cCI6MTUyMDQyODMxNCwiaXNzIjo5NzM1fQ.K-d3FKWKddMygFqvPZYWQusqhbF1LYfcIM0VbBq4uJsS9VkjhyXALlHmTJWjdblzx-U55lkZc_KWdJd6GlDxvoRb5w_9nrLcIFRbYVgi9XTYpCc3o5j7Qh3FvKxA1bzEs8XGrxjjE7-WJn_xi85ugFKTy9tlIRPa-PHeIOvNp4fz4ru8SFPoD4epiraeEyLfpU_ke-HYF7Ws7ar19zQkfJKRHSIFm1LxJ5MGKWT8pQBBUSGxGPgEG_tYI83aYw6cVx-DLV290bpr23LRUC684Wv_XabUDzXjPUYynAc01APZF6aN8B0LHdPbG8I6Yd74sQfmN-aHz5moz8ZNWLNm8Q";
// Use the JWT as a Bearer token
var gitHubClient = new GitHubClient(new ProductHeaderValue("MyApp"))
{
Credentials = new Credentials(jwtToken, AuthenticationType.Bearer)
};
```
The authenticated `GitHubApp` can query various top level information about itself:
``` csharp
// Get the current authenticated GitHubApp
var app = await appClient.GetCurrent();
// Get a list of installations for the authenticated GitHubApp
var installations = await appClient.GetAllInstallationsForCurrent();
// Get a specific installation of the authenticated GitHubApp by it's installation Id
var installation = await appClient.GetInstallation(123);
```
In order to do more than top level calls, a `GitHubApp` needs to authenticate as a specific `Installation` by creating a temporary installation token, and using that for authentication:
``` csharp
// Create an Installation token for Insallation Id 123
var response = await appClient.CreateInstallationToken(123);
// NOTE - the token will expire!
response.ExpiresAt;
// Create a new GitHubClient using the installation token as authentication
var installationClient = new GitHubClient(new ProductHeaderValue("MyApp-Installation123"))
{
Credentials = new Credentials(response.Token)
};
```
Once authenticated as an `Installation`, a [subset of regular API endpoints](https://developer.github.com/v3/apps/available-endpoints/) can be queried, using the permissions of the `GitHubApp` and repository settings of the `Installation`:
``` csharp
// Create a Comment on an Issue
// - Assuming the GitHub App has read/write permission for issues scope
// - Assuming we are operating on a repository that the Installation has access to
var response = await installationClient.Issue.Comment.Create("owner", "repo", 1, "Hello from my GitHubApp Installation!");
```
## A Note on identifying Installation Id's
GitHub Apps can be registered for webhook events.
WebHook payloads sent to these registrations now include an extra field to indicate the Id of the GitHub App Installation that is associated with the received webhook.
Example webhook for an opened Pull Request:
``` json
{
"action": "opened",
"number": 1,
"pull_request": {
...
},
"repository": {
...
},
"sender": {
...
},
"installation": {
"id": 1234
}
}
```
You can retrieve this `installation.id` from your webhook payload, and use it to create the Installation token as above, to further interact with the repository as that Installation of the GitHub App.
Although Octokit.net doesn't have explicit webhook support, the `ActivityPayload` response classes do generally align with webhook payloads (assuming the octokit.net custom deserializer is used), so we have added the field to these:
``` csharp
// json payload from the received webhook
var json = "...";
// Deserialize the pull_request event
var serializer = new Octokit.Internal.SimpleJsonSerializer();
var payload = serializer_.Deserialize<PullRequestEventPayload>(json);
// Create an Installation token for the associated Insallation Id
var response = await appClient.CreateInstallationToken(payload.Installation.Id);
```
## A Note on JWT Tokens
Octokit.net expects that you will pass in the appropriately signed JWT token required to authenticate the `GitHubApp`.
Luckily one of our contributors [@adriangodong](https://github.com/adriangodong) has created a library `GitHubJwt` ( [GitHub](https://github.com/adriangodong/githubjwt) | [NuGet](https://www.nuget.org/packages/githubjwt) ) which you can use to help with this (as long as you are targetting `netstandard2.0` or above).
``` csharp
var generator = new GitHubJwt.GitHubJwtFactory(
new GitHubJwt.FilePrivateKeySource("/path/to/pem_file"),
new GitHubJwt.GitHubJwtFactoryOptions
{
AppIntegrationId = 123, // The GitHub App Id
ExpirationSeconds = 600 // 10 minutes is the maximum time allowed
}
);
var jwtToken = generator.CreateEncodedJwtToken();
```

View File

@@ -24,12 +24,14 @@ pages:
- 'Releases' : 'releases.md'
- 'Search' : 'search.md'
- 'Git Database' : 'git-database.md'
- 'GitHub Apps' : 'github-apps.md'
- Samples:
- 'Exploring Pull Requests' : 'demos/exploring-pull-requests.md'
- Advanced:
- 'API Options': 'extensibility.md'
- 'Working with Enums': 'working-with-enums.md'
- 'Debugging from Source': 'debugging-source.md'
- 'OAuth Flow': 'oauth-flow.md'
- 'HttpClient': 'http-client.md'

View File

@@ -1,127 +0,0 @@
<#
.SYNOPSIS
Builds and tests Octokit
.DESCRIPTION
Janky runs this script after checking out a revision and cleaning its
working tree.
.PARAMETER Clean
When true, all untracked (and ignored) files will be removed from the work
tree. Defaults to false.
#>
Param(
[switch]
$Clean = $false
)
Set-StrictMode -Version Latest
try {
# Make the output width reeeeeaaaaaly wide so our output doesn't get hard-wrapped.
# <http://stackoverflow.com/questions/978777/powershell-output-column-width>
$Host.UI.RawUI.BufferSize = New-Object Management.Automation.Host.Size -ArgumentList 5000, 25
} catch {
# Most likely we were run in a cmd.exe terminal which won't allow setting
# the BufferSize to such a strange size.
}
$rootDirectory = Split-Path (Split-Path $MyInvocation.MyCommand.Path)
Push-Location $rootDirectory
function Die-WithOutput($exitCode, $output) {
Write-Output $output
Write-Output ""
exit $exitCode
}
function Dump-Error($output) {
$exitCode = $LastExitCode
$errors = $output | Select-String ": error"
if ($errors) {
$output = "Likely errors:", $errors, "", "Full output:", $output
}
Die-WithOutput $exitCode $output
}
function Run-Command([scriptblock]$Command, [switch]$Fatal, [switch]$Quiet) {
$output = ""
if ($Quiet) {
$output = & $Command 2>&1
} else {
& $Command
}
if (!$Fatal) {
return
}
$exitCode = 0
if ($LastExitCode -ne 0) {
$exitCode = $LastExitCode
} elseif (!$?) {
$exitCode = 1
} else {
return
}
$error = "Error executing command ``$Command``."
if ($output) {
$error = "$error Output:", $output
}
Die-WithOutput $exitCode $error
}
if ($Clean) {
Write-Output "Cleaning work tree..."
Write-Output ""
Run-Command -Quiet -Fatal { git clean -xdf }
}
Write-Output "Installing FAKE..."
Write-Output ""
.\tools\nuget\nuget.exe "install" "FAKE.Core" "-OutputDirectory" "tools" "-ExcludeVersion" "-Version" "2.18.1"
Write-Output "Building Octokit..."
Write-Output ""
$output = & .\tools\FAKE.Core\tools\Fake.exe "build.fsx" "target=BuildApp" "buildMode=Release"
if ($LastExitCode -ne 0) {
Dump-Error($output)
}
Write-Output "Running unit tests..."
Write-Output ""
$output = & .\tools\FAKE.Core\tools\Fake.exe "build.fsx" "target=UnitTests" "buildMode=Release"
if ($LastExitCode -ne 0) {
Dump-Error($output)
}
Write-Output "Running convention tests..."
Write-Output ""
$output = & .\tools\FAKE.Core\tools\Fake.exe "build.fsx" "target=ConventionTests" "buildMode=Release"
if ($LastExitCode -ne 0) {
Dump-Error($output)
}
Write-Output "Running integration tests..."
Write-Output ""
$output = & .\tools\FAKE.Core\tools\Fake.exe "build.fsx" "target=IntegrationTests" "buildMode=Release"
if ($LastExitCode -ne 0) {
Dump-Error($output)
}
Write-Output "Creating NuGet packages..."
Write-Output ""
$output = & .\tools\FAKE.Core\tools\Fake.exe "build.fsx" "target=CreatePackages" "buildMode=Release"
if ($LastExitCode -ne 0) {
Dump-Error($output)
}
$exitCode = 0
exit $exitCode

View File

@@ -98,6 +98,13 @@ VerifyEnvironmentVariable "Override GitHub URL" "OCTOKIT_CUSTOMURL" $true
VerifyEnvironmentVariable "application ClientID" "OCTOKIT_CLIENTID" $true
VerifyEnvironmentVariable "application Secret" "OCTOKIT_CLIENTSECRET" $true
if (AskYesNoQuestion "Do you wish to setup GitHubApps integration test settings?" "OCTOKIT_GITHUBAPP_ENABLED")
{
VerifyEnvironmentVariable "GitHub App ID" "OCTOKIT_GITHUBAPP_ID"
VerifyEnvironmentVariable "GitHub App SLUG" "OCTOKIT_GITHUBAPP_SLUG"
VerifyEnvironmentVariable "GitHub App Installation ID" "OCTOKIT_GITHUBAPP_INSTALLATIONID"
VerifyEnvironmentVariable "GitHub App Pem File" "OCTOKIT_GITHUBAPP_PEMFILE"
}
if (AskYesNoQuestion "Do you wish to enable GitHub Enterprise (GHE) Integration Tests?" "OCTOKIT_GHE_ENABLED")
{