@@ -0,0 +1,125 @@
|
|||||||
|
name: publish-winui-exe
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
- master
|
||||||
|
tags:
|
||||||
|
- "v*"
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
packages: write
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
publish:
|
||||||
|
runs-on: windows-latest
|
||||||
|
env:
|
||||||
|
DOTNET_CLI_TELEMETRY_OPTOUT: "1"
|
||||||
|
DOTNET_NOLOGO: "1"
|
||||||
|
GITEA_PACKAGE_NAME: ymhut-box-winui
|
||||||
|
GITEA_PACKAGE_OWNER: ${{ gitea.repository_owner }}
|
||||||
|
GITEA_PACKAGE_SERVER_URL: ${{ gitea.server_url }}
|
||||||
|
GITEA_PACKAGE_TOKEN: ${{ secrets.PACKAGE_TOKEN }}
|
||||||
|
GITEA_PACKAGE_USERNAME: ${{ vars.PACKAGE_USERNAME }}
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Setup .NET
|
||||||
|
uses: actions/setup-dotnet@v4
|
||||||
|
with:
|
||||||
|
dotnet-version: 10.0.x
|
||||||
|
|
||||||
|
- name: Install Inno Setup compiler
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
|
||||||
|
$existing = Get-Command ISCC.exe -ErrorAction SilentlyContinue
|
||||||
|
if ($existing) {
|
||||||
|
Write-Host "ISCC already available: $($existing.Source)"
|
||||||
|
exit 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$installer = Join-Path $env:RUNNER_TEMP 'innosetup-6.7.3.exe'
|
||||||
|
Invoke-WebRequest `
|
||||||
|
-Uri 'https://github.com/jrsoftware/issrc/releases/download/is-6_7_3/innosetup-6.7.3.exe' `
|
||||||
|
-OutFile $installer
|
||||||
|
|
||||||
|
Start-Process `
|
||||||
|
-FilePath $installer `
|
||||||
|
-ArgumentList '/VERYSILENT', '/SUPPRESSMSGBOXES', '/NORESTART', '/SP-' `
|
||||||
|
-Wait `
|
||||||
|
-PassThru |
|
||||||
|
ForEach-Object {
|
||||||
|
if ($_.ExitCode -ne 0) {
|
||||||
|
throw "Inno Setup installer failed with exit code $($_.ExitCode)."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$candidateRoots = @(
|
||||||
|
"${env:ProgramFiles(x86)}\Inno Setup 6",
|
||||||
|
"$env:ProgramFiles\Inno Setup 6",
|
||||||
|
"${env:ProgramFiles(x86)}\Inno Setup 7",
|
||||||
|
"$env:ProgramFiles\Inno Setup 7"
|
||||||
|
)
|
||||||
|
$iscc = $candidateRoots |
|
||||||
|
ForEach-Object { Join-Path $_ 'ISCC.exe' } |
|
||||||
|
Where-Object { Test-Path -LiteralPath $_ } |
|
||||||
|
Select-Object -First 1
|
||||||
|
|
||||||
|
if (-not $iscc) {
|
||||||
|
throw 'Inno Setup compiler was installed, but ISCC.exe was not found.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$isccRoot = Split-Path -Parent $iscc
|
||||||
|
"YMHUT_INNO_SETUP=$isccRoot" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
|
||||||
|
"$isccRoot" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
|
||||||
|
Write-Host "ISCC installed: $iscc"
|
||||||
|
|
||||||
|
- name: Build EXE installer
|
||||||
|
shell: pwsh
|
||||||
|
run: .\scripts\build-winui.ps1 --target=exe --no-pause
|
||||||
|
|
||||||
|
- name: Write installer checksum
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$installer = Get-ChildItem -LiteralPath installer_output -Filter 'YMhut_Box_WinUI_Setup_*.exe' -File |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $installer) {
|
||||||
|
throw 'EXE installer was not produced.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$hash = Get-FileHash -LiteralPath $installer.FullName -Algorithm SHA256
|
||||||
|
$checksumPath = "$($installer.FullName).sha256"
|
||||||
|
$checksumLine = "$($hash.Hash.ToLowerInvariant()) $($installer.Name)"
|
||||||
|
Set-Content -LiteralPath $checksumPath -Value $checksumLine -Encoding ASCII
|
||||||
|
Write-Host "Installer: $($installer.FullName)"
|
||||||
|
Write-Host "SHA256: $($hash.Hash.ToLowerInvariant())"
|
||||||
|
|
||||||
|
- name: Publish to Gitea Packages
|
||||||
|
shell: pwsh
|
||||||
|
run: |
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$installer = Get-ChildItem -LiteralPath installer_output -Filter 'YMhut_Box_WinUI_Setup_*.exe' -File |
|
||||||
|
Sort-Object LastWriteTime -Descending |
|
||||||
|
Select-Object -First 1
|
||||||
|
if (-not $installer) {
|
||||||
|
throw 'EXE installer was not produced.'
|
||||||
|
}
|
||||||
|
|
||||||
|
$checksumPath = "$($installer.FullName).sha256"
|
||||||
|
if (-not (Test-Path -LiteralPath $checksumPath)) {
|
||||||
|
throw "Installer checksum was not produced: $checksumPath"
|
||||||
|
}
|
||||||
|
|
||||||
|
.\scripts\publish-gitea-generic-package.ps1 `
|
||||||
|
-ArtifactPath @($installer.FullName, $checksumPath) `
|
||||||
|
-Overwrite
|
||||||
@@ -8,3 +8,29 @@
|
|||||||
6. Install, launch, upgrade and uninstall both MSIX and EXE packages.
|
6. Install, launch, upgrade and uninstall both MSIX and EXE packages.
|
||||||
|
|
||||||
The default signing path uses a self-signed certificate for sideload validation. Replace the certificate before public distribution if a commercial signing certificate is available.
|
The default signing path uses a self-signed certificate for sideload validation. Replace the certificate before public distribution if a commercial signing certificate is available.
|
||||||
|
|
||||||
|
## Gitea EXE Package Workflow
|
||||||
|
|
||||||
|
The Gitea Actions workflow at `.gitea/workflows/publish-winui-exe.yml` builds the Inno Setup installer with:
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
.\scripts\build-winui.ps1 --target=exe --no-pause
|
||||||
|
```
|
||||||
|
|
||||||
|
It then uploads the installer and a SHA-256 checksum file to Gitea Generic Packages:
|
||||||
|
|
||||||
|
```text
|
||||||
|
{gitea.server_url}/api/packages/{gitea.repository_owner}/generic/ymhut-box-winui/{version}/{file}
|
||||||
|
```
|
||||||
|
|
||||||
|
The package version is derived from `version.json` using the same rule as the local build script. For example, `version=2.0.7` and `build=06` publishes version `2.0.7.6`.
|
||||||
|
|
||||||
|
Required runner setup:
|
||||||
|
|
||||||
|
- A Windows Gitea runner labeled `windows-latest`.
|
||||||
|
- Internet access to download .NET workloads/NuGet packages and the pinned Inno Setup compiler when `ISCC.exe` is not already installed.
|
||||||
|
- A repository secret named `PACKAGE_TOKEN` with package write/delete access. Use a personal access token because Gitea's built-in Actions job token does not fully support package repository publishing on all versions.
|
||||||
|
|
||||||
|
Optional repository variables:
|
||||||
|
|
||||||
|
- `PACKAGE_USERNAME`: username that owns the package token. If omitted, the workflow actor is used.
|
||||||
|
|||||||
@@ -0,0 +1,172 @@
|
|||||||
|
[CmdletBinding()]
|
||||||
|
param(
|
||||||
|
[string] $ServerUrl = $env:GITEA_PACKAGE_SERVER_URL,
|
||||||
|
[string] $Owner = $env:GITEA_PACKAGE_OWNER,
|
||||||
|
[string] $PackageName = $(if ([string]::IsNullOrWhiteSpace($env:GITEA_PACKAGE_NAME)) { 'ymhut-box-winui' } else { $env:GITEA_PACKAGE_NAME }),
|
||||||
|
[string] $PackageVersion = $env:GITEA_PACKAGE_VERSION,
|
||||||
|
[string[]] $ArtifactPath = @('installer_output\YMhut_Box_WinUI_Setup_*.exe'),
|
||||||
|
[string] $Username = $env:GITEA_PACKAGE_USERNAME,
|
||||||
|
[string] $Token = $env:GITEA_PACKAGE_TOKEN,
|
||||||
|
[switch] $Overwrite
|
||||||
|
)
|
||||||
|
|
||||||
|
$ErrorActionPreference = 'Stop'
|
||||||
|
$ProgressPreference = 'SilentlyContinue'
|
||||||
|
|
||||||
|
Add-Type -AssemblyName System.Net.Http
|
||||||
|
|
||||||
|
$Root = (Resolve-Path (Join-Path $PSScriptRoot '..')).Path
|
||||||
|
|
||||||
|
function Get-DefaultPackageVersion {
|
||||||
|
$versionFile = Join-Path $Root 'version.json'
|
||||||
|
if (-not (Test-Path -LiteralPath $versionFile)) {
|
||||||
|
return '2.0.0.1'
|
||||||
|
}
|
||||||
|
|
||||||
|
$json = Get-Content -LiteralPath $versionFile -Raw -Encoding UTF8 | ConvertFrom-Json
|
||||||
|
$version = if ($json.version) { [string]$json.version } else { '2.0.0' }
|
||||||
|
$build = if ($json.build) { ([string]$json.build -replace '[^0-9]', '') } else { '1' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($build)) {
|
||||||
|
$build = '1'
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = @()
|
||||||
|
foreach ($part in ($version -split '\.')) {
|
||||||
|
$digits = ($part -replace '[^0-9]', '')
|
||||||
|
if ($digits) {
|
||||||
|
$parts += [int]$digits
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
while ($parts.Count -lt 3) {
|
||||||
|
$parts += 0
|
||||||
|
}
|
||||||
|
|
||||||
|
$parts = $parts | Select-Object -First 3
|
||||||
|
(($parts + [int]$build) | ForEach-Object { [Math]::Min($_, 65535) }) -join '.'
|
||||||
|
}
|
||||||
|
|
||||||
|
function ConvertTo-PathSegment([string] $Value) {
|
||||||
|
[Uri]::EscapeDataString($Value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function Assert-PackageName([string] $Value, [string] $Label) {
|
||||||
|
if ($Value -notmatch '^[A-Za-z0-9._+-]+$') {
|
||||||
|
throw "$Label '$Value' is invalid for Gitea Generic Packages. Use only letters, numbers, dots, hyphens, plus signs, and underscores."
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function Invoke-GiteaPackageRequest {
|
||||||
|
param(
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string] $Method,
|
||||||
|
[Parameter(Mandatory = $true)]
|
||||||
|
[string] $Uri,
|
||||||
|
[string] $FilePath
|
||||||
|
)
|
||||||
|
|
||||||
|
$request = [Net.Http.HttpRequestMessage]::new([Net.Http.HttpMethod]::new($Method), $Uri)
|
||||||
|
$request.Headers.Authorization = [Net.Http.Headers.AuthenticationHeaderValue]::new('Basic', $basicAuth)
|
||||||
|
|
||||||
|
$stream = $null
|
||||||
|
if (-not [string]::IsNullOrWhiteSpace($FilePath)) {
|
||||||
|
$stream = [IO.File]::OpenRead($FilePath)
|
||||||
|
$request.Content = [Net.Http.StreamContent]::new($stream)
|
||||||
|
$request.Content.Headers.ContentType = [Net.Http.Headers.MediaTypeHeaderValue]::Parse('application/octet-stream')
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
$response = $httpClient.SendAsync($request).GetAwaiter().GetResult()
|
||||||
|
return [int]$response.StatusCode
|
||||||
|
} finally {
|
||||||
|
if ($request) { $request.Dispose() }
|
||||||
|
if ($stream) { $stream.Dispose() }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ServerUrl)) {
|
||||||
|
$ServerUrl = $env:GITHUB_SERVER_URL
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Owner)) {
|
||||||
|
$Owner = $env:GITHUB_REPOSITORY_OWNER
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Owner) -and -not [string]::IsNullOrWhiteSpace($env:GITHUB_REPOSITORY)) {
|
||||||
|
$Owner = ($env:GITHUB_REPOSITORY -split '/', 2)[0]
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Username)) {
|
||||||
|
$Username = $env:GITEA_ACTOR
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Username)) {
|
||||||
|
$Username = $env:GITHUB_ACTOR
|
||||||
|
}
|
||||||
|
if ([string]::IsNullOrWhiteSpace($PackageVersion)) {
|
||||||
|
$PackageVersion = Get-DefaultPackageVersion
|
||||||
|
}
|
||||||
|
|
||||||
|
$ServerUrl = $ServerUrl.TrimEnd('/')
|
||||||
|
$rawPackageVersion = $PackageVersion
|
||||||
|
|
||||||
|
if ([string]::IsNullOrWhiteSpace($ServerUrl)) { throw 'Gitea server URL is required. Set GITEA_PACKAGE_SERVER_URL or run inside Gitea Actions.' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Owner)) { throw 'Gitea package owner is required. Set GITEA_PACKAGE_OWNER or run inside Gitea Actions.' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Username)) { throw 'Gitea package username is required. Set GITEA_PACKAGE_USERNAME or run inside Gitea Actions.' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($Token)) { throw 'Gitea package token is required. Set the PACKAGE_TOKEN secret or pass -Token.' }
|
||||||
|
if ([string]::IsNullOrWhiteSpace($PackageVersion)) { throw 'Gitea package version is required.' }
|
||||||
|
if ($rawPackageVersion -ne $rawPackageVersion.Trim()) { throw 'Gitea package version cannot have leading or trailing whitespace.' }
|
||||||
|
|
||||||
|
Assert-PackageName $PackageName 'Package name'
|
||||||
|
|
||||||
|
$artifacts = @()
|
||||||
|
foreach ($path in $ArtifactPath) {
|
||||||
|
$resolvedPattern = if ([IO.Path]::IsPathRooted($path)) { $path } else { Join-Path $Root $path }
|
||||||
|
$artifacts += @(Get-ChildItem -Path $resolvedPattern -File -ErrorAction SilentlyContinue)
|
||||||
|
}
|
||||||
|
|
||||||
|
$artifacts = @($artifacts | Sort-Object FullName -Unique)
|
||||||
|
if ($artifacts.Count -eq 0) {
|
||||||
|
throw "No artifact files matched: $($ArtifactPath -join ', ')"
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($artifact in $artifacts) {
|
||||||
|
Assert-PackageName $artifact.Name 'Artifact file name'
|
||||||
|
}
|
||||||
|
|
||||||
|
$basicAuth = [Convert]::ToBase64String([Text.Encoding]::UTF8.GetBytes("$Username`:$Token"))
|
||||||
|
$versionUri = "$ServerUrl/api/packages/$(ConvertTo-PathSegment $Owner)/generic/$(ConvertTo-PathSegment $PackageName)/$(ConvertTo-PathSegment $PackageVersion)"
|
||||||
|
$httpClient = [Net.Http.HttpClient]::new()
|
||||||
|
|
||||||
|
Write-Host "Publishing Gitea Generic Package"
|
||||||
|
Write-Host " Server: $ServerUrl"
|
||||||
|
Write-Host " Owner: $Owner"
|
||||||
|
Write-Host " Package: $PackageName"
|
||||||
|
Write-Host " Version: $PackageVersion"
|
||||||
|
|
||||||
|
try {
|
||||||
|
if ($Overwrite) {
|
||||||
|
$deleteStatus = Invoke-GiteaPackageRequest -Method 'DELETE' -Uri $versionUri
|
||||||
|
if ($deleteStatus -eq 204) {
|
||||||
|
Write-Host ' Existing package version deleted before upload.'
|
||||||
|
} elseif ($deleteStatus -eq 404) {
|
||||||
|
Write-Host ' No existing package version to delete.'
|
||||||
|
} else {
|
||||||
|
throw "Unexpected response while deleting existing package version: HTTP $deleteStatus"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
foreach ($artifact in $artifacts) {
|
||||||
|
$fileUri = "$versionUri/$(ConvertTo-PathSegment $artifact.Name)"
|
||||||
|
Write-Host " Uploading: $($artifact.Name)"
|
||||||
|
$uploadStatus = Invoke-GiteaPackageRequest -Method 'PUT' -Uri $fileUri -FilePath $artifact.FullName
|
||||||
|
|
||||||
|
if ($uploadStatus -eq 409 -and -not $Overwrite) {
|
||||||
|
throw "Package file already exists: $($artifact.Name). Re-run with -Overwrite to delete the package version first."
|
||||||
|
}
|
||||||
|
|
||||||
|
if ($uploadStatus -ne 201) {
|
||||||
|
throw "Unexpected response while uploading $($artifact.Name): HTTP $uploadStatus"
|
||||||
|
}
|
||||||
|
|
||||||
|
Write-Host " Uploaded: $fileUri"
|
||||||
|
}
|
||||||
|
} finally {
|
||||||
|
$httpClient.Dispose()
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user