From e7dd87bf7eaefb26ae8448193c82716cd8b165dc Mon Sep 17 00:00:00 2001 From: QWQLwToo <2467013926@qq.com> Date: Tue, 30 Jun 2026 14:25:59 +0800 Subject: [PATCH] =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E5=B7=A5=E4=BD=9C=E6=B5=81?= =?UTF-8?q?=E6=9E=84=E5=BB=BA?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitea/workflows/publish-winui-exe.yml | 125 ++++++++++++++++ docs/release-process.md | 26 ++++ scripts/publish-gitea-generic-package.ps1 | 172 ++++++++++++++++++++++ 3 files changed, 323 insertions(+) create mode 100644 .gitea/workflows/publish-winui-exe.yml create mode 100644 scripts/publish-gitea-generic-package.ps1 diff --git a/.gitea/workflows/publish-winui-exe.yml b/.gitea/workflows/publish-winui-exe.yml new file mode 100644 index 0000000..34d80b7 --- /dev/null +++ b/.gitea/workflows/publish-winui-exe.yml @@ -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 diff --git a/docs/release-process.md b/docs/release-process.md index d1e4f04..322b56d 100644 --- a/docs/release-process.md +++ b/docs/release-process.md @@ -8,3 +8,29 @@ 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. + +## 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. diff --git a/scripts/publish-gitea-generic-package.ps1 b/scripts/publish-gitea-generic-package.ps1 new file mode 100644 index 0000000..ce287fc --- /dev/null +++ b/scripts/publish-gitea-generic-package.ps1 @@ -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() +}