add support for preview GitHub releases (#457)

This commit is contained in:
Jesse Plamondon-Willard 2018-03-15 19:52:18 -04:00
parent 9e052ae916
commit 436c071ba4
10 changed files with 78 additions and 29 deletions

View File

@ -9,9 +9,12 @@ namespace StardewModdingAPI.Common.Models
/// <summary>The mod name.</summary> /// <summary>The mod name.</summary>
public string Name { get; set; } public string Name { get; set; }
/// <summary>The mod's semantic version number.</summary> /// <summary>The semantic version for the mod's latest release.</summary>
public string Version { get; set; } public string Version { get; set; }
/// <summary>The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</summary>
public string PreviewVersion { get; set; }
/// <summary>The mod's web URL.</summary> /// <summary>The mod's web URL.</summary>
public string Url { get; set; } public string Url { get; set; }
@ -28,16 +31,17 @@ namespace StardewModdingAPI.Common.Models
// needed for JSON deserialising // needed for JSON deserialising
} }
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="name">The mod name.</param> /// <param name="name">The mod name.</param>
/// <param name="version">The mod's semantic version number.</param> /// <param name="version">The semantic version for the mod's latest release.</param>
/// <param name="previewVersion">The semantic version for the mod's latest preview release, if available and different from <see cref="Version"/>.</param>
/// <param name="url">The mod's web URL.</param> /// <param name="url">The mod's web URL.</param>
/// <param name="error">The error message indicating why the mod is invalid (if applicable).</param> /// <param name="error">The error message indicating why the mod is invalid (if applicable).</param>
public ModInfoModel(string name, string version, string url, string error = null) public ModInfoModel(string name, string version, string url, string previewVersion = null, string error = null)
{ {
this.Name = name; this.Name = name;
this.Version = version; this.Version = version;
this.PreviewVersion = previewVersion;
this.Url = url; this.Url = url;
this.Error = error; // mainly initialised here for the JSON deserialiser this.Error = error; // mainly initialised here for the JSON deserialiser
} }

View File

@ -1,4 +1,5 @@
using System; using System;
using System.Linq;
using System.Net; using System.Net;
using System.Threading.Tasks; using System.Threading.Tasks;
using Pathoschild.Http.Client; using Pathoschild.Http.Client;
@ -11,8 +12,11 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/********* /*********
** Properties ** Properties
*********/ *********/
/// <summary>The URL for a GitHub releases API query excluding the base URL, where {0} is the repository owner and name.</summary> /// <summary>The URL for a GitHub API query for the latest stable release, excluding the base URL, where {0} is the organisation and project name.</summary>
private readonly string ReleaseUrlFormat; private readonly string StableReleaseUrlFormat;
/// <summary>The URL for a GitHub API query for the latest release (including prerelease), excluding the base URL, where {0} is the organisation and project name.</summary>
private readonly string AnyReleaseUrlFormat;
/// <summary>The underlying HTTP client.</summary> /// <summary>The underlying HTTP client.</summary>
private readonly IClient Client; private readonly IClient Client;
@ -23,14 +27,16 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
*********/ *********/
/// <summary>Construct an instance.</summary> /// <summary>Construct an instance.</summary>
/// <param name="baseUrl">The base URL for the GitHub API.</param> /// <param name="baseUrl">The base URL for the GitHub API.</param>
/// <param name="releaseUrlFormat">The URL for a GitHub releases API query excluding the <paramref name="baseUrl"/>, where {0} is the repository owner and name.</param> /// <param name="stableReleaseUrlFormat">The URL for a GitHub API query for the latest stable release, excluding the <paramref name="baseUrl"/>, where {0} is the organisation and project name.</param>
/// <param name="anyReleaseUrlFormat">The URL for a GitHub API query for the latest release (including prerelease), excluding the <paramref name="baseUrl"/>, where {0} is the organisation and project name.</param>
/// <param name="userAgent">The user agent for the API client.</param> /// <param name="userAgent">The user agent for the API client.</param>
/// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param> /// <param name="acceptHeader">The Accept header value expected by the GitHub API.</param>
/// <param name="username">The username with which to authenticate to the GitHub API.</param> /// <param name="username">The username with which to authenticate to the GitHub API.</param>
/// <param name="password">The password with which to authenticate to the GitHub API.</param> /// <param name="password">The password with which to authenticate to the GitHub API.</param>
public GitHubClient(string baseUrl, string releaseUrlFormat, string userAgent, string acceptHeader, string username, string password) public GitHubClient(string baseUrl, string stableReleaseUrlFormat, string anyReleaseUrlFormat, string userAgent, string acceptHeader, string username, string password)
{ {
this.ReleaseUrlFormat = releaseUrlFormat; this.StableReleaseUrlFormat = stableReleaseUrlFormat;
this.AnyReleaseUrlFormat = anyReleaseUrlFormat;
this.Client = new FluentClient(baseUrl) this.Client = new FluentClient(baseUrl)
.SetUserAgent(userAgent) .SetUserAgent(userAgent)
@ -41,18 +47,23 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>Get the latest release for a GitHub repository.</summary> /// <summary>Get the latest release for a GitHub repository.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param> /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <returns>Returns the latest release if found, else <c>null</c>.</returns> /// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
public async Task<GitRelease> GetLatestReleaseAsync(string repo) /// <returns>Returns the release if found, else <c>null</c>.</returns>
public async Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false)
{ {
// validate key format this.AssetKeyFormat(repo);
if (!repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo));
// fetch info
try try
{ {
if (includePrerelease)
{
GitRelease[] results = await this.Client
.GetAsync(string.Format(this.AnyReleaseUrlFormat, repo))
.AsArray<GitRelease>();
return results.FirstOrDefault();
}
return await this.Client return await this.Client
.GetAsync(string.Format(this.ReleaseUrlFormat, repo)) .GetAsync(string.Format(this.StableReleaseUrlFormat, repo))
.As<GitRelease>(); .As<GitRelease>();
} }
catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound) catch (ApiException ex) when (ex.Status == HttpStatusCode.NotFound)
@ -66,5 +77,18 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
{ {
this.Client?.Dispose(); this.Client?.Dispose();
} }
/*********
** Private methods
*********/
/// <summary>Assert that a repository key is formatted correctly.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <exception cref="ArgumentException">The repository key is invalid.</exception>
private void AssetKeyFormat(string repo)
{
if (repo == null || !repo.Contains("/") || repo.IndexOf("/", StringComparison.InvariantCultureIgnoreCase) != repo.LastIndexOf("/", StringComparison.InvariantCultureIgnoreCase))
throw new ArgumentException($"The value '{repo}' isn't a valid GitHub repository key, must be a username and project name like 'Pathoschild/SMAPI'.", nameof(repo));
}
} }
} }

View File

@ -19,6 +19,10 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
/// <summary>The Markdown description for the release.</summary> /// <summary>The Markdown description for the release.</summary>
public string Body { get; set; } public string Body { get; set; }
/// <summary>Whether this is a prerelease version.</summary>
[JsonProperty("prerelease")]
public bool IsPrerelease { get; set; }
/// <summary>The attached files.</summary> /// <summary>The attached files.</summary>
public GitAsset[] Assets { get; set; } public GitAsset[] Assets { get; set; }
} }

View File

@ -11,7 +11,8 @@ namespace StardewModdingAPI.Web.Framework.Clients.GitHub
*********/ *********/
/// <summary>Get the latest release for a GitHub repository.</summary> /// <summary>Get the latest release for a GitHub repository.</summary>
/// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param> /// <param name="repo">The repository key (like <c>Pathoschild/SMAPI</c>).</param>
/// <returns>Returns the latest release if found, else <c>null</c>.</returns> /// <param name="includePrerelease">Whether to return a prerelease version if it's latest.</param>
Task<GitRelease> GetLatestReleaseAsync(string repo); /// <returns>Returns the release if found, else <c>null</c>.</returns>
Task<GitRelease> GetLatestReleaseAsync(string repo, bool includePrerelease = false);
} }
} }

View File

@ -29,8 +29,11 @@ namespace StardewModdingAPI.Web.Framework.ConfigModels
/// <summary>The base URL for the GitHub API.</summary> /// <summary>The base URL for the GitHub API.</summary>
public string GitHubBaseUrl { get; set; } public string GitHubBaseUrl { get; set; }
/// <summary>The URL for a GitHub API latest-release query excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary> /// <summary>The URL for a GitHub API query for the latest stable release, excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
public string GitHubReleaseUrlFormat { get; set; } public string GitHubStableReleaseUrlFormat { get; set; }
/// <summary>The URL for a GitHub API query for the latest release (including prerelease), excluding the <see cref="GitHubBaseUrl"/>, where {0} is the organisation and project name.</summary>
public string GitHubAnyReleaseUrlFormat { get; set; }
/// <summary>The Accept header value expected by the GitHub API.</summary> /// <summary>The Accept header value expected by the GitHub API.</summary>
public string GitHubAcceptHeader { get; set; } public string GitHubAcceptHeader { get; set; }

View File

@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
return new ModInfoModel("Found no mod with this ID."); return new ModInfoModel("Found no mod with this ID.");
// create model // create model
return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url); return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -38,10 +38,21 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
// fetch info // fetch info
try try
{ {
GitRelease release = await this.Client.GetLatestReleaseAsync(id); // get latest release
return release != null GitRelease latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: true);
? new ModInfoModel(id, this.NormaliseVersion(release.Tag), $"https://github.com/{id}/releases") GitRelease preview = null;
: new ModInfoModel("Found no mod with this ID."); if (latest == null)
return new ModInfoModel("Found no mod with this ID.");
// get latest stable release (if not latest)
if (latest.IsPrerelease)
{
preview = latest;
latest = await this.Client.GetLatestReleaseAsync(id, includePrerelease: false);
}
// return data
return new ModInfoModel(name: id, version: this.NormaliseVersion(latest?.Tag), previewVersion: this.NormaliseVersion(preview?.Tag), url: $"https://github.com/{id}/releases");
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -43,7 +43,7 @@ namespace StardewModdingAPI.Web.Framework.ModRepositories
return new ModInfoModel("Found no mod with this ID."); return new ModInfoModel("Found no mod with this ID.");
if (mod.Error != null) if (mod.Error != null)
return new ModInfoModel(mod.Error); return new ModInfoModel(mod.Error);
return new ModInfoModel(mod.Name, this.NormaliseVersion(mod.Version), mod.Url); return new ModInfoModel(name: mod.Name, version: this.NormaliseVersion(mod.Version), url: mod.Url);
} }
catch (Exception ex) catch (Exception ex)
{ {

View File

@ -74,7 +74,8 @@ namespace StardewModdingAPI.Web
services.AddSingleton<IGitHubClient>(new GitHubClient( services.AddSingleton<IGitHubClient>(new GitHubClient(
baseUrl: api.GitHubBaseUrl, baseUrl: api.GitHubBaseUrl,
releaseUrlFormat: api.GitHubReleaseUrlFormat, stableReleaseUrlFormat: api.GitHubStableReleaseUrlFormat,
anyReleaseUrlFormat: api.GitHubAnyReleaseUrlFormat,
userAgent: userAgent, userAgent: userAgent,
acceptHeader: api.GitHubAcceptHeader, acceptHeader: api.GitHubAcceptHeader,
username: api.GitHubUsername, username: api.GitHubUsername,

View File

@ -24,7 +24,8 @@
"ChucklefishModPageUrlFormat": "resources/{0}", "ChucklefishModPageUrlFormat": "resources/{0}",
"GitHubBaseUrl": "https://api.github.com", "GitHubBaseUrl": "https://api.github.com",
"GitHubReleaseUrlFormat": "repos/{0}/releases/latest", "GitHubStableReleaseUrlFormat": "repos/{0}/releases/latest",
"GitHubAnyReleaseUrlFormat": "repos/{0}/releases?per_page=1",
"GitHubAcceptHeader": "application/vnd.github.v3+json", "GitHubAcceptHeader": "application/vnd.github.v3+json",
"GitHubUsername": null, // see top note "GitHubUsername": null, // see top note
"GitHubPassword": null, // see top note "GitHubPassword": null, // see top note