Skip to content

Commit 04ff1fd

Browse files
committed
Get-ForgejoRelease: Add new cmdlet for retrieving releases from Forgejo instances (e.g., Codeberg)
1 parent 87df193 commit 04ff1fd

6 files changed

Lines changed: 149 additions & 91 deletions

File tree

app/Pog.Utils/Pog.Utils.psd1

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ This module is not exactly polished and might break sometimes.
2727
"Invoke-FileDownload"
2828

2929
# Env_UpdateRepository
30+
'Get-ForgejoRelease'
3031
'Get-GithubRelease'
3132
'Get-GithubAsset'
3233
'Get-UrlHash'

app/Pog/container/Env_UpdateRepository.psm1

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,9 @@ function RetrievePackageVersions($Package, $ExistingVersionSet) {
3939
$Version = if ($Version -is [Pog.PackageVersion]) {
4040
$Version
4141
} elseif ($Version) {
42-
[Pog.PackageVersion]$Version
42+
try {[Pog.PackageVersion]$Version} catch {
43+
throw "Version generator for package '$($Package.PackageName)' returned an unparseable package version: $Version"
44+
}
4345
} else {
4446
throw "Empty package version generated by the version generator for package '$($Package.PackageName)'."
4547
}
@@ -165,7 +167,7 @@ function Get-GithubAssetHash {
165167
return Get-HashFromChecksumFile $ChecksumAsset.Url $Asset.Name
166168
} else {
167169
# TODO: also print package name and version here
168-
Write-Information "Computing hash for '$($Asset.Name)' locally due to a missing checksum..."
170+
Write-Verbose "Computing hash for '$($Asset.Name)' locally due to a missing checksum..."
169171
return Get-UrlHash $Asset.Url
170172
}
171173
}
@@ -308,5 +310,5 @@ function Get-NuGetRelease {
308310

309311

310312
Export-ModuleMember `
311-
-Cmdlet Get-UrlHash, Get-GithubRelease, Get-GithubAsset `
312-
-Function __main, Get-GithubAssetHash, Get-HashFromChecksumText, Get-HashFromChecksumFile, Get-NuGetRelease
313+
-Cmdlet Get-UrlHash, Get-ForgejoRelease, Get-GithubRelease, Get-GithubAsset `
314+
-Function __main, Get-GithubAssetHash, Get-HashFromChecksumText, Get-HashFromChecksumFile, Get-NuGetRelease
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
using System.Management.Automation;
2+
using System.Security;
3+
using JetBrains.Annotations;
4+
using Pog.Utils.GitHub;
5+
6+
namespace Pog.Commands.ContainerCommands;
7+
8+
/// <summary>
9+
/// Lists all Forgejo releases for the passed repository and instance. Note that this cmdlet returns
10+
/// types compatible with <see cref="GetGitHubAssetCommand"/> and other cmdlets for GitHub.
11+
/// </summary>
12+
[PublicAPI]
13+
[Cmdlet(VerbsCommon.Get, "ForgejoRelease", DefaultParameterSetName = DefaultPS)]
14+
[OutputType(typeof(GitHubRelease), typeof(GitHubTag))]
15+
public sealed class GetForgejoReleaseCommand : GetReleaseCommandBase {
16+
/// Base URL of the Forgejo instance to use.
17+
/// For example, to use Codeberg, pass `https://codeberg.org`.
18+
[Parameter(Mandatory = true)]
19+
[ValidatePattern("^(http|https)://.*$")]
20+
public string Instance = null!;
21+
22+
/// API token to the selected instance to use. In the generator environment, this is set automatically through
23+
/// <c>$PSDefaultParameterValues</c>. Generators should not need to set this explicitly, this is primarily
24+
/// for interactive usage outside the container.
25+
[Parameter] public SecureString? AccessToken;
26+
27+
internal override GitHubApiClient CreateClient() {
28+
var apiToken = AccessToken == null ? null : UnprotectSecureString(AccessToken);
29+
if (apiToken != null) {
30+
WriteDebug($"Using an API token for '{Instance}'.");
31+
}
32+
return new(InternalState.HttpClient, apiToken, $"{Instance}/api/v1");
33+
}
34+
}
Lines changed: 7 additions & 74 deletions
Original file line numberDiff line numberDiff line change
@@ -1,91 +1,24 @@
1-
using System.Collections.Generic;
2-
using System.Management.Automation;
1+
using System.Management.Automation;
32
using System.Security;
43
using JetBrains.Annotations;
5-
using Pog.Commands.Common;
6-
using Pog.Utils;
74
using Pog.Utils.GitHub;
85

96
namespace Pog.Commands.ContainerCommands;
107

118
/// <summary>Lists all GitHub releases for the passed repository.</summary>
129
[PublicAPI]
13-
[Cmdlet(VerbsCommon.Get, "GitHubRelease")]
10+
[Cmdlet(VerbsCommon.Get, "GitHubRelease", DefaultParameterSetName = DefaultPS)]
1411
[OutputType(typeof(GitHubRelease), typeof(GitHubTag))]
15-
public sealed class GetGitHubReleaseCommand : PogCmdlet {
16-
private const string VersionPS = "Version";
17-
private const string TagPrefixPS = "Version";
18-
19-
[Parameter(Mandatory = true, Position = 0)]
20-
[ValidatePattern(@"^[^/\s]+/[^/\s]+$")]
21-
public string Repository = null!;
22-
23-
/// ScriptBlock that parses the raw tag name into a version string. Typically, this is not necessary.
24-
[Parameter(ParameterSetName = VersionPS)]
25-
public ScriptBlock? Version;
26-
27-
/// Tag name prefix to remove to get the raw version. By default, "v" prefix or no prefix is accepted.
28-
[Parameter(ParameterSetName = TagPrefixPS)]
29-
[Parameter] public string? TagPrefix;
30-
31-
/// Retrieve tags instead of releases.
32-
[Parameter] public SwitchParameter Tags;
33-
12+
public sealed class GetGitHubReleaseCommand : GetReleaseCommandBase {
3413
/// GitHub API token to use. In the generator environment, this is set automatically through <c>$PSDefaultParameterValues</c>.
3514
/// Generators should not need to set this explicitly, this is primarily for interactive usage outside the container.
3615
[Parameter] public SecureString? AccessToken;
3716

38-
private readonly GitHubApiClient _client = new(InternalState.HttpClient);
39-
private string? _apiToken;
40-
41-
protected override void BeginProcessing() {
42-
base.BeginProcessing();
43-
44-
_apiToken = AccessToken == null ? null : UnprotectSecureString(AccessToken);
45-
if (_apiToken != null) {
17+
internal override GitHubApiClient CreateClient() {
18+
var apiToken = AccessToken == null ? null : UnprotectSecureString(AccessToken);
19+
if (apiToken != null) {
4620
WriteDebug("Using a GitHub API token.");
4721
}
48-
}
49-
50-
protected override void ProcessRecord() {
51-
base.ProcessRecord();
52-
WriteObjectEnumerable(Tags ? EnumerateTags() : EnumerateReleases());
53-
}
54-
55-
private string? ParseVersion(GitHubObject obj) {
56-
if (Version != null) {
57-
var rawResult = Version.InvokeWithContext(null, [new("_", obj)]);
58-
var result = LanguagePrimitives.ConvertTo<string?>(rawResult);
59-
return string.IsNullOrEmpty(result) ? null : result;
60-
} else {
61-
var tag = obj.GetTagName();
62-
// if TagPrefix is not explicitly set, also allow versions with no prefix (starting with a number)
63-
tag = tag.StripPrefix(TagPrefix ?? "v") ?? (TagPrefix == null ? tag : null);
64-
// if the prefix is missing or the resulting version does not start with a number, ignore it
65-
return string.IsNullOrEmpty(tag) || !char.IsDigit(tag![0]) ? null : tag;
66-
}
67-
}
68-
69-
private IEnumerable<T> FilterEnumerable<T>(IAsyncEnumerable<T> enumerable) where T : GitHubObject {
70-
foreach (var obj in enumerable.ToBlockingEnumerable(CancellationToken)) {
71-
// parse tag name to generate version
72-
obj.SetVersion(ParseVersion(obj));
73-
// ignore releases with unparseable versions
74-
if (obj.VersionStr != null) {
75-
yield return obj;
76-
} else {
77-
WriteVerbose($"Skipping release '{obj.GetTagName()}' in repository '{Repository}', " +
78-
$"could not parse tag as a version. To change how the tag is parsed, " +
79-
$"pass either `-Version` or `-TagPrefix`.");
80-
}
81-
}
82-
}
83-
84-
private IEnumerable<GitHubRelease> EnumerateReleases() {
85-
return FilterEnumerable(_client.EnumerateReleasesAsync(Repository, _apiToken, CancellationToken));
86-
}
87-
88-
private IEnumerable<GitHubTag> EnumerateTags() {
89-
return FilterEnumerable(_client.EnumerateTagsAsync(Repository, _apiToken, CancellationToken));
22+
return new(InternalState.HttpClient, apiToken);
9023
}
9124
}
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
using System.Collections.Generic;
2+
using System.Management.Automation;
3+
using JetBrains.Annotations;
4+
using Pog.Commands.Common;
5+
using Pog.Utils;
6+
using Pog.Utils.GitHub;
7+
8+
namespace Pog.Commands.ContainerCommands;
9+
10+
[PublicAPI]
11+
public abstract class GetReleaseCommandBase : PogCmdlet {
12+
protected const string DefaultPS = VersionPS;
13+
private const string VersionPS = "Version";
14+
private const string TagPrefixPS = "TagPrefix";
15+
16+
[Parameter(Mandatory = true, Position = 0)]
17+
[ValidatePattern(@"^[^/\s]+/[^/\s]+$")]
18+
public string Repository = null!;
19+
20+
/// ScriptBlock that parses the raw tag name into a version string. Typically, this is not necessary.
21+
[Parameter(ParameterSetName = VersionPS)]
22+
public ScriptBlock? Version;
23+
24+
/// Tag name prefix to remove to get the raw version. By default, "v" prefix or no prefix is accepted.
25+
[Parameter(ParameterSetName = TagPrefixPS)]
26+
[Parameter] public string? TagPrefix;
27+
28+
/// Retrieve tags instead of releases.
29+
[Parameter] public SwitchParameter Tags;
30+
31+
private GitHubApiClient _client = null!;
32+
33+
internal abstract GitHubApiClient CreateClient();
34+
35+
protected override void BeginProcessing() {
36+
base.BeginProcessing();
37+
_client = CreateClient();
38+
}
39+
40+
protected override void ProcessRecord() {
41+
base.ProcessRecord();
42+
WriteObjectEnumerable(Tags ? EnumerateTags() : EnumerateReleases());
43+
}
44+
45+
private string? ParseVersion(GitHubObject obj) {
46+
if (Version != null) {
47+
var rawResult = Version.InvokeWithContext(null, [new("_", obj)]);
48+
var result = LanguagePrimitives.ConvertTo<string?>(rawResult);
49+
return string.IsNullOrEmpty(result) ? null : result;
50+
} else {
51+
var tag = obj.GetTagName();
52+
// if TagPrefix is not explicitly set, also allow versions with no prefix (starting with a number)
53+
tag = tag.StripPrefix(TagPrefix ?? "v") ?? (TagPrefix == null ? tag : null);
54+
// if the prefix is missing or the resulting version does not start with a number, ignore it
55+
return string.IsNullOrEmpty(tag) || !char.IsDigit(tag![0]) ? null : tag;
56+
}
57+
}
58+
59+
private IEnumerable<T> FilterEnumerable<T>(IAsyncEnumerable<T> enumerable) where T : GitHubObject {
60+
foreach (var obj in enumerable.ToBlockingEnumerable(CancellationToken)) {
61+
// parse tag name to generate version
62+
obj.SetVersion(ParseVersion(obj));
63+
// ignore releases with unparseable versions
64+
if (obj.VersionStr != null) {
65+
yield return obj;
66+
} else {
67+
WriteVerbose($"Skipping release '{obj.GetTagName()}' in repository '{Repository}', " +
68+
$"could not parse tag as a version. To change how the tag is parsed, " +
69+
$"pass either `-Version` or `-TagPrefix`.");
70+
}
71+
}
72+
}
73+
74+
private IEnumerable<GitHubRelease> EnumerateReleases() {
75+
return FilterEnumerable(_client.EnumerateReleasesAsync(Repository, CancellationToken));
76+
}
77+
78+
private IEnumerable<GitHubTag> EnumerateTags() {
79+
return FilterEnumerable(_client.EnumerateTagsAsync(Repository, CancellationToken));
80+
}
81+
}

app/Pog/lib_compiled/Pog/src/Utils/GitHub/GitHubApiClient.cs

Lines changed: 20 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,25 +16,32 @@ public class GitHubRequestException(string message) : HttpRequestException(messa
1616

1717
public class GitHubRateLimitException(string message) : GitHubRequestException(message);
1818

19-
internal class GitHubApiClient(HttpClient httpClient) {
20-
public IAsyncEnumerable<GitHubRelease> EnumerateReleasesAsync(
21-
string repo, string? apiToken = null, CancellationToken token = default) {
19+
/// API client for GitHub and mostly compatible services like Forgejo.
20+
internal class GitHubApiClient(HttpClient httpClient, string? apiToken = null, string baseUrl = "https://api.github.com") {
21+
private readonly bool _isGitHub = baseUrl == "https://api.github.com";
22+
23+
public IAsyncEnumerable<GitHubRelease> EnumerateReleasesAsync(string repo, CancellationToken token = default) {
2224
return EnumerateFeedAsync<GitHubRelease>(
23-
$"Cannot list releases for GitHub repository '{repo}'",
25+
GetErrorMsg("releases", repo),
2426
// 100 releases per page is the maximum: https://docs.github.com/en/rest/releases/releases#list-releases
25-
new($"https://api.github.com/repos/{repo}/releases?per_page=100"),
27+
new($"{baseUrl}/repos/{repo}/releases?per_page=100"),
2628
// TODO: when System.Text.Json is updated to 9.0.0, add RespectRequiredConstructorParameters and RespectNullableAnnotations
27-
new JsonSerializerOptions {PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower},
28-
apiToken, token);
29+
new() {PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower},
30+
token);
2931
}
3032

31-
public IAsyncEnumerable<GitHubTag> EnumerateTagsAsync(
32-
string repo, string? apiToken = null, CancellationToken token = default) {
33+
public IAsyncEnumerable<GitHubTag> EnumerateTagsAsync(string repo, CancellationToken token = default) {
3334
return EnumerateFeedAsync<GitHubTag>(
34-
$"Cannot list tags for GitHub repository '{repo}'",
35-
new($"https://api.github.com/repos/{repo}/tags?per_page=100"),
35+
GetErrorMsg("tags", repo),
36+
new($"{baseUrl}/repos/{repo}/tags?per_page=100"),
3637
new() {PropertyNamingPolicy = JsonNamingPolicy.SnakeCaseLower},
37-
apiToken, token);
38+
token);
39+
}
40+
41+
private string GetErrorMsg(string subject, string repo) {
42+
return _isGitHub
43+
? $"Cannot list {subject} for GitHub repository '{repo}'"
44+
: $"Cannot list {subject} for repository '{repo}' at instance '{baseUrl}'";
3845
}
3946

4047
private static HttpResponseMessage ValidateApiResponse(string errorMsg, HttpResponseMessage response) {
@@ -68,7 +75,7 @@ private static HttpResponseMessage ValidateApiResponse(string errorMsg, HttpResp
6875
}
6976

7077
private async IAsyncEnumerable<T> EnumerateFeedAsync<T>(
71-
string errorMsg, Uri? uri, JsonSerializerOptions options, string? apiToken = null,
78+
string errorMsg, Uri? uri, JsonSerializerOptions options,
7279
[EnumeratorCancellation] CancellationToken token = default) {
7380
// TODO: this could be optimized by querying the "last" rel link and then requesting all pages in between in parallel
7481
while (uri != null) {

0 commit comments

Comments
 (0)