Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions .github/workflows/publish-nuget.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
name: Publish NuGet

on:
workflow_dispatch:
inputs:
version:
description: 'NuGet package version (SemVer 2.0, e.g. 1.0.0, 1.0.0-beta.1)'
required: true
type: string
push-to-nuget:
description: 'Push package to NuGet.org'
required: false
type: boolean
default: true

permissions:
contents: write
id-token: write

jobs:
publish:
name: Build and Publish
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0

- name: Validate SemVer 2.0
run: |
if ! echo "${{ inputs.version }}" | grep -E '^[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?(\+[0-9A-Za-z-]+(\.[0-9A-Za-z-]+)*)?$' > /dev/null; then
echo "::error::Invalid SemVer 2.0 version: '${{ inputs.version }}'"
exit 1
fi
echo "Version ${{ inputs.version }} is valid SemVer 2.0"

- name: Setup .NET
uses: actions/setup-dotnet@v4
with:
dotnet-version: |
8.0.x
10.0.x

- name: Install Android workload
run: dotnet workload install android

- name: Restore
run: dotnet restore src/GeneralUpdate.Maui.sln

- name: Build
run: dotnet build src/GeneralUpdate.Maui.sln --configuration Release --no-restore /p:Version=${{ inputs.version }}

- name: Run tests
run: dotnet test src/GeneralUpdate.Maui.sln --configuration Release --no-restore --no-build /p:Version=${{ inputs.version }}

- name: Pack NuGet
run: dotnet pack src/GeneralUpdate.Maui.sln --configuration Release --no-restore --no-build -o artifacts /p:Version=${{ inputs.version }}

- name: List artifacts
run: ls -la artifacts/

- name: Create git tag
run: |
git config user.name "github-actions"
git config user.email "github-actions@github.com"
TAG="v${{ inputs.version }}"
if git rev-parse "$TAG" >/dev/null 2>&1; then
echo "::error::Tag $TAG already exists. Use a different version or delete the existing tag first."
exit 1
fi
git tag "$TAG"
git push --force-with-lease origin "$TAG"

- name: Create GitHub Release
env:
GH_TOKEN: ${{ github.token }}
run: |
TAG="v${{ inputs.version }}"
if gh release view "$TAG" --json tagName &>/dev/null; then
gh release delete "$TAG" --yes
fi
gh release create "$TAG" \
artifacts/*.nupkg \
--title "$TAG" \
--generate-notes \
--verify-tag

- name: NuGet login via Trusted Publishing (OIDC → temp API key)
if: ${{ inputs.push-to-nuget == true }}
id: nuget-login
uses: NuGet/login@v1
with:
user: juster.chu

- name: Push NuGet package to NuGet.org
if: ${{ inputs.push-to-nuget == true }}
run: |
dotnet nuget push artifacts/*.nupkg \
--source https://api.nuget.org/v3/index.json \
--api-key "${{ steps.nuget-login.outputs.NUGET_API_KEY }}" \
--skip-duplicate
11 changes: 11 additions & 0 deletions src/GeneralUpdate.Maui.Android/Abstractions/IHttpAuthProvider.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace GeneralUpdate.Maui.Android.Abstractions;

/// <summary>
/// Provides authentication for HTTP requests.
/// Implementations can add headers, modify the request, or perform
/// any other authentication flow before the request is sent.
/// </summary>
public interface IHttpAuthProvider
{
Task ApplyAuthAsync(HttpRequestMessage request, CancellationToken token = default);
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
using System.Net.Security;
using System.Security.Cryptography.X509Certificates;

namespace GeneralUpdate.Maui.Android.Abstractions;

/// <summary>
/// Provides custom SSL/TLS certificate validation logic.
/// Used to configure <see cref="System.Net.Http.HttpClientHandler.ServerCertificateCustomValidationCallback"/>.
/// </summary>
public interface ISslValidationPolicy
{
bool ValidateCertificate(
X509Certificate2? certificate,
X509Chain? chain,
SslPolicyErrors sslPolicyErrors);
}
28 changes: 28 additions & 0 deletions src/GeneralUpdate.Maui.Android/Enums/AuthScheme.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,28 @@
namespace GeneralUpdate.Maui.Android.Enums;

/// <summary>
/// Defines the supported HTTP authentication schemes for update downloads.
/// </summary>
public enum AuthScheme
{
/// <summary>
/// HMAC-SHA256 signature-based authentication.
/// Adds X-Update-Timestamp and X-Update-Signature headers.
/// </summary>
Hmac = 0,

/// <summary>
/// Bearer token authentication via Authorization header.
/// </summary>
Bearer = 1,

/// <summary>
/// API key authentication via a custom header (default: X-Api-Key).
/// </summary>
ApiKey = 2,

/// <summary>
/// HTTP Basic authentication via Authorization header.
/// </summary>
Basic = 3
}
77 changes: 77 additions & 0 deletions src/GeneralUpdate.Maui.Android/Models/HttpDownloadOptions.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
using System.Net;
using GeneralUpdate.Maui.Android.Abstractions;

namespace GeneralUpdate.Maui.Android.Models;

/// <summary>
/// Configures HTTP transport behavior for update downloads:
/// SSL/TLS certificate validation, proxy, timeouts, and authentication.
/// <para>
/// When provided to <see cref="Services.GeneralUpdateBootstrap.CreateDefault"/>,
/// the library constructs an internal <see cref="HttpClient"/> from these settings.
/// When null, the existing behavior is preserved (bare HttpClient, no auth, system SSL).
/// </para>
/// </summary>
public sealed record HttpDownloadOptions
{
/// <summary>
/// Custom SSL/TLS certificate validation policy.
/// Defaults to null, which uses the system's default certificate validation.
/// Set to <see cref="Services.AllowAllSslValidationPolicy"/> for self-signed certificates
/// in development environments only.
/// </summary>
public ISslValidationPolicy? SslValidationPolicy { get; init; }

/// <summary>
/// Overall timeout for the entire download operation.
/// Default is 10 minutes.
/// </summary>
public TimeSpan DownloadTimeout { get; init; } = TimeSpan.FromMinutes(10);

/// <summary>
/// Optional web proxy for HTTP requests.
/// When set, <see cref="UseProxy"/> must also be true for the proxy to take effect.
/// </summary>
public IWebProxy? Proxy { get; init; }

/// <summary>
/// Whether to use the configured <see cref="Proxy"/>.
/// Default is false.
/// </summary>
public bool UseProxy { get; init; }

/// <summary>
/// Global authentication provider applied to all download requests.
/// Per-package authentication on <see cref="UpdatePackageInfo"/> takes precedence.
/// When <see cref="UpdatePackageInfo.AuthScheme"/> is explicitly set,
/// per-package credentials are used exclusively (no fallback to global).
/// </summary>
public IHttpAuthProvider? AuthProvider { get; init; }

/// <summary>
/// Builds an <see cref="HttpClientHandler"/> from the configured options.
/// Applies SSL validation policy and proxy settings.
/// </summary>
internal HttpClientHandler BuildHandler()
{
var handler = new HttpClientHandler();

if (SslValidationPolicy != null)
{
handler.ServerCertificateCustomValidationCallback =
(_, cert, chain, errors) => SslValidationPolicy.ValidateCertificate(cert, chain, errors);
}

if (UseProxy && Proxy != null)
{
handler.Proxy = Proxy;
handler.UseProxy = true;
}
else
{
handler.UseProxy = false;
}

return handler;
}
}
30 changes: 30 additions & 0 deletions src/GeneralUpdate.Maui.Android/Models/UpdatePackageInfo.cs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
using GeneralUpdate.Maui.Android.Enums;

namespace GeneralUpdate.Maui.Android.Models;

/// <summary>
Expand All @@ -22,4 +24,32 @@ public sealed class UpdatePackageInfo
public bool ForceUpdate { get; init; }

public string? ApkFileName { get; init; }

/// <summary>
/// Per-package authentication scheme.
/// When set, takes precedence over the global <see cref="HttpDownloadOptions.AuthProvider"/>.
/// </summary>
public AuthScheme? AuthScheme { get; init; }

/// <summary>
/// Token value used by Bearer or ApiKey authentication.
/// For Bearer: the Bearer token string.
/// For ApiKey: the API key value.
/// </summary>
public string? AuthToken { get; init; }

/// <summary>
/// Secret key used by HMAC-SHA256 signature authentication.
/// </summary>
public string? AuthSecretKey { get; init; }

/// <summary>
/// Username used by Basic authentication.
/// </summary>
public string? BasicUsername { get; init; }

/// <summary>
/// Password used by Basic authentication.
/// </summary>
public string? BasicPassword { get; init; }
}
16 changes: 15 additions & 1 deletion src/GeneralUpdate.Maui.Android/Services/AndroidBootstrap.cs
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ namespace GeneralUpdate.Maui.Android.Services;
/// <summary>
/// Orchestrates update discovery, package download, integrity validation, and APK installation triggering.
/// </summary>
public sealed class AndroidBootstrap : IAndroidBootstrap
public sealed class AndroidBootstrap : IAndroidBootstrap, IDisposable
{
Comment on lines +11 to 12
private const string UpdateInProgressMessage = "An update execution is already in progress.";
private readonly IUpdateDownloader _downloader;
Expand Down Expand Up @@ -222,4 +222,18 @@ private static UpdateFailureReason MapFailureReason(Exception ex)
_ => UpdateFailureReason.Unknown
};
}

private bool _disposed;

public void Dispose()
{
if (_disposed) return;

if (_downloader is IDisposable disposableDownloader)
{
disposableDownloader.Dispose();
}

_disposed = true;
}
}
Loading
Loading