Files
MoShiro/ci/package-cross-platform.ps1
T
justme 490b008a57
ci/woodpecker/push/dev-image Pipeline is running
ci/woodpecker/push/web-build Pipeline was successful
ci/woodpecker/push/verify Pipeline was successful
scripts: unify Linux native builds and retain restore caches [CI]
Build musl and glibc for amd64 and arm64 through one native package path. Reuse musl outputs for OCI images and OpenWrt installers, and export binaries inside a rootless unshare session.

Restore the executable dependency graph into a shared persistent NuGet cache, build managed dependencies once, and run isolated native publishes in parallel.

Clean temporary outputs and expired image layers under store locks, retire inactive legacy stores after seven days, and retain two toolchain exports. Add regression coverage for staging, extraction, packaging and cleanup boundaries.
2026-10-21 12:29:00 +08:00

158 lines
7.3 KiB
PowerShell

$ErrorActionPreference = 'Stop'
Set-StrictMode -Version Latest
if (-not $env:CI_COMMIT_TAG -or $env:CI_COMMIT_TAG -notmatch '^v(\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?)$') {
throw 'Release must run from a vMAJOR.MINOR.PATCH tag.'
}
if (-not $env:GITEA_TOKEN) {
throw 'GITEA_TOKEN is required.'
}
$version = $Matches[1]
$root = Split-Path -Parent $PSScriptRoot
$backend = Join-Path $root 'backend'
# 版本取自 tag:写进工作区的 Directory.Build.props(仓库里的 <Version> 只是占位,发版不用再提交 bump)
$propsPath = Join-Path $backend 'Directory.Build.props'
$props = Get-Content $propsPath -Raw
Set-Content -LiteralPath $propsPath -Value ([regex]::Replace($props, '<Version>[^<]*</Version>', "<Version>$version</Version>")) -NoNewline
$tarProject = Join-Path $PSScriptRoot 'MoShiro.Tar\MoShiro.Tar.csproj'
$tarTool = Join-Path $PSScriptRoot 'MoShiro.Tar\bin\Release\net10.0\MoShiro.Tar.dll'
& dotnet build $tarProject -c Release --nologo
if ($LASTEXITCODE -ne 0) {
throw 'Could not build the cross-platform tar helper.'
}
$giteaBase = if ($env:GITEA_BASE) { $env:GITEA_BASE.TrimEnd('/') } else { 'http://localhost:3100' }
$netrc = Join-Path $env:TEMP ("moshiro-gitea-$([guid]::NewGuid().ToString('N')).netrc")
$giteaHost = ([Uri]$giteaBase).Host
Set-Content -LiteralPath $netrc -Value "machine $giteaHost login justme password $($env:GITEA_TOKEN)" -Encoding ascii -NoNewline
function New-TarGzArchive([string]$SourceDirectory, [string]$ArchivePath, [string[]]$Executables) {
& dotnet $tarTool $SourceDirectory $ArchivePath @Executables
if ($LASTEXITCODE -ne 0) {
throw "Could not create tar archive $ArchivePath."
}
}
try {
$targets = @(
@{ Rid = 'osx-x64'; Os = 'macos'; Arch = 'x64'; Windows = $false },
@{ Rid = 'osx-arm64'; Os = 'macos'; Arch = 'arm64'; Windows = $false },
@{ Rid = 'win-x64'; Os = 'windows'; Arch = 'amd64'; Windows = $true },
@{ Rid = 'win-arm64'; Os = 'windows'; Arch = 'arm64'; Windows = $true }
)
$packageRoot = Join-Path $root '.release\packages'
$null = New-Item -ItemType Directory -Force -Path $packageRoot
$uploadFiles = [System.Collections.Generic.List[string]]::new()
foreach ($target in $targets) {
$agentDirectory = Join-Path $root ".release\publish\$($target.Rid)\agent"
$enrollDirectory = Join-Path $root ".release\publish\$($target.Rid)\fleet-enroll"
foreach ($path in @($agentDirectory, $enrollDirectory)) {
if (-not (Test-Path -LiteralPath $path -PathType Container)) {
throw "Build output is missing: $path"
}
}
foreach ($role in @('agent')) {
$stage = Join-Path $root ".release\stage\$($target.Rid)\$role"
$null = New-Item -ItemType Directory -Force -Path $stage
$extension = if ($target.Windows) { '.exe' } else { '' }
Copy-Item -LiteralPath (Join-Path (Join-Path $root ".release\publish\$($target.Rid)\$role") "moshiro-$role$extension") -Destination $stage
Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $stage
Copy-Item -LiteralPath (Join-Path $root "backend\$role\README.md") -Destination $stage
$executables = @("moshiro-$role$extension")
$enrollName = if ($target.Windows) { 'fleet-enroll.exe' } else { 'fleet-enroll' }
Copy-Item -LiteralPath (Join-Path $enrollDirectory $enrollName) -Destination $stage
$executables += $enrollName
if ($target.Os -eq 'macos') {
@'
This macOS NativeAOT binary is not signed with a Developer ID certificate and
has not been notarized by Apple. Apple Silicon builds receive an ad-hoc code
signature; Intel builds are unsigned. Gatekeeper may block browser-downloaded
files. A Developer ID signature and notarization are required for a seamless
consumer install.
'@ | Set-Content -LiteralPath (Join-Path $stage 'MACOS-SECURITY.txt') -Encoding utf8
}
$archiveName = if ($target.Windows) { "moshiro-$role-v$version-windows-$($target.Arch).zip" } else { "moshiro-$role-v$version-$($target.Os)-$($target.Arch).tar.gz" }
$archive = Join-Path $packageRoot $archiveName
if ($target.Windows) {
Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $archive -CompressionLevel Optimal -Force
}
else {
New-TarGzArchive $stage $archive $executables
}
$hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant()
$checksum = "$hash $archiveName`n"
$checksumPath = "$archive.sha256"
[IO.File]::WriteAllText($checksumPath, $checksum, [Text.UTF8Encoding]::new($false))
[void]$uploadFiles.Add($archive)
[void]$uploadFiles.Add($checksumPath)
}
}
$uploadScript = @'
param([string]$File, [string]$Base, [string]$PackageVersion, [string]$Netrc)
$ErrorActionPreference = 'Stop'
$name = [IO.Path]::GetFileName($File)
$url = "$Base/api/packages/justme/generic/moshiro-node/$PackageVersion/$name"
$status = & curl.exe --silent --show-error --retry 3 --netrc-file $Netrc --upload-file $File --output NUL --write-out '%{http_code}' $url
if ($LASTEXITCODE -ne 0) {
throw "Gitea package upload failed for $name (curl exit $LASTEXITCODE)."
}
if ($status -match '^2') {
"uploaded Gitea package $name"
}
elseif ($status -eq '409') {
"Gitea package $name already exists; reusing the existing release file"
}
else {
throw "Gitea package upload failed for $name (HTTP $status)."
}
'@
$runspacePool = [RunspaceFactory]::CreateRunspacePool(1, 8)
$runspacePool.Open()
$pendingUploads = [System.Collections.Generic.List[object]]::new()
try {
foreach ($file in $uploadFiles) {
$powerShell = [PowerShell]::Create()
$powerShell.RunspacePool = $runspacePool
[void]$powerShell.AddScript($uploadScript).AddArgument($file).AddArgument($giteaBase).AddArgument($version).AddArgument($netrc)
$pendingUploads.Add([pscustomobject]@{ PowerShell = $powerShell; Handle = $powerShell.BeginInvoke() })
}
$uploadErrors = [System.Collections.Generic.List[string]]::new()
foreach ($pending in $pendingUploads) {
try {
foreach ($message in $pending.PowerShell.EndInvoke($pending.Handle)) {
Write-Output $message
}
foreach ($errorRecord in $pending.PowerShell.Streams.Error) {
$uploadErrors.Add($errorRecord.ToString())
}
}
catch {
$uploadErrors.Add($_.Exception.Message)
}
}
if ($uploadErrors.Count -gt 0) {
throw "One or more Gitea package uploads failed:`n$($uploadErrors -join "`n")"
}
}
finally {
foreach ($pending in $pendingUploads) {
$pending.PowerShell.Dispose()
}
$runspacePool.Close()
$runspacePool.Dispose()
}
# Packaging starts after every producer has finished; uploaded outputs are disposable.
foreach ($directory in @('publish', 'build', 'stage', 'packages')) {
$path = Join-Path $root ".release\$directory"
if (Test-Path -LiteralPath $path) { Remove-Item -LiteralPath $path -Recurse -Force }
}
}
finally {
Remove-Item -LiteralPath $netrc -Force -ErrorAction SilentlyContinue
}