diff --git a/.woodpecker/linux-release.yml b/.woodpecker/linux-release.yml index 762196d..4d90c63 100644 --- a/.woodpecker/linux-release.yml +++ b/.woodpecker/linux-release.yml @@ -18,28 +18,6 @@ depends_on: concurrency: 1 steps: - - name: linux-amd64-agent - image: bash - environment: - NUGET_PACKAGES: /home/justme/.nuget/packages - GITEA_BASE: http://192.168.2.162:3100 - GITEA_TOKEN: - from_secret: gitea_token - commands: - - bash ci/release-linux.sh amd64 agent - depends_on: [] - - - name: linux-amd64-probe - image: bash - environment: - NUGET_PACKAGES: /home/justme/.nuget/packages - GITEA_BASE: http://192.168.2.162:3100 - GITEA_TOKEN: - from_secret: gitea_token - commands: - - bash ci/release-linux.sh amd64 probe - depends_on: [] - - name: linux-amd64-backend image: bash environment: @@ -53,28 +31,6 @@ steps: - bash ci/release-linux.sh amd64 backend depends_on: [] - - name: linux-arm64-agent - image: bash - environment: - NUGET_PACKAGES: /home/justme/.nuget/packages - GITEA_BASE: http://192.168.2.162:3100 - GITEA_TOKEN: - from_secret: gitea_token - commands: - - bash ci/release-linux.sh arm64 agent - depends_on: [] - - - name: linux-arm64-probe - image: bash - environment: - NUGET_PACKAGES: /home/justme/.nuget/packages - GITEA_BASE: http://192.168.2.162:3100 - GITEA_TOKEN: - from_secret: gitea_token - commands: - - bash ci/release-linux.sh arm64 probe - depends_on: [] - - name: linux-arm64-backend image: bash environment: diff --git a/.woodpecker/windows-release.yml b/.woodpecker/windows-release.yml index a91b10d..5b0775c 100644 --- a/.woodpecker/windows-release.yml +++ b/.woodpecker/windows-release.yml @@ -18,46 +18,86 @@ depends_on: concurrency: 1 steps: - - name: windows-amd64-agent + - name: linux-x64 image: powershell - environment: + environment: &build-env GITEA_BASE: http://localhost:3100 GITEA_TOKEN: from_secret: gitea_token + NUGET_PACKAGES: 'C:\Users\JustMe-NAS\.nuget\packages' commands: - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force - - ./ci/release-windows.ps1 -Architecture amd64 -Role agent + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier linux-x64 depends_on: [] - - name: windows-amd64-probe + - name: linux-arm64 image: powershell - environment: - GITEA_BASE: http://localhost:3100 - GITEA_TOKEN: - from_secret: gitea_token + environment: *build-env commands: - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force - - ./ci/release-windows.ps1 -Architecture amd64 -Role probe + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier linux-arm64 depends_on: [] - - name: windows-arm64-agent + - name: linux-arm image: powershell - environment: - GITEA_BASE: http://localhost:3100 - GITEA_TOKEN: - from_secret: gitea_token + environment: *build-env commands: - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force - - ./ci/release-windows.ps1 -Architecture arm64 -Role agent + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier linux-arm depends_on: [] - - name: windows-arm64-probe + - name: linux-musl-x64 image: powershell - environment: - GITEA_BASE: http://localhost:3100 - GITEA_TOKEN: - from_secret: gitea_token + environment: *build-env commands: - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force - - ./ci/release-windows.ps1 -Architecture arm64 -Role probe + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier linux-musl-x64 + depends_on: [] + + - name: linux-musl-arm64 + image: powershell + environment: *build-env + commands: + - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier linux-musl-arm64 + depends_on: [] + + - name: linux-musl-arm + image: powershell + environment: *build-env + commands: + - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier linux-musl-arm + depends_on: [] + + - name: macos-x64 + image: powershell + environment: *build-env + commands: + - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier osx-x64 + depends_on: [] + + - name: macos-arm64 + image: powershell + environment: *build-env + commands: + - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier osx-arm64 + depends_on: [] + + - name: windows-x64 + image: powershell + environment: *build-env + commands: + - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier win-x64 + depends_on: [] + + - name: windows-arm64 + image: powershell + environment: *build-env + commands: + - Set-ExecutionPolicy -Scope Process -ExecutionPolicy Bypass -Force + - ./ci/release-cross-platform.ps1 -RuntimeIdentifier win-arm64 depends_on: [] diff --git a/README.md b/README.md index c844a83..4833812 100644 --- a/README.md +++ b/README.md @@ -42,9 +42,9 @@ npm ci --prefix web npm run build --prefix web ``` -前端产物位于 `web/display/dist/` 和 `web/admin/dist/`。NativeAOT 发布还需要对应平台的 C/C++ 工具链;Linux 发布脚本和 RID 说明见 [`docs/deployment.md`](docs/deployment.md)。 +前端产物位于 `web/display/dist/` 和 `web/admin/dist/`。NativeAOT 目标平台和 Windows 上使用 AotAnywhere 交叉构建的说明见 [`docs/deployment.md`](docs/deployment.md)。 -Backend 以 OCI 镜像发布,支持 Linux/amd64 与 Linux/arm64;Agent 和 Probe 以 Windows/Linux 的 amd64/arm64 二进制包发布。容器部署配置见 [`compose.yaml`](compose.yaml),发布说明见 [`docs/deployment.md`](docs/deployment.md)。 +Backend 以 OCI 镜像发布,支持 Linux/amd64 与 Linux/arm64;Agent 和 Probe 发布 Linux glibc/musl x64、arm64、arm,macOS x64/arm64,以及 Windows x64/arm64 二进制包。容器部署配置见 [`compose.yaml`](compose.yaml),发布说明见 [`docs/deployment.md`](docs/deployment.md)。 ### 本地运行 diff --git a/backend/agent/src/Monitor.Agent/Monitor.Agent.csproj b/backend/agent/src/Monitor.Agent/Monitor.Agent.csproj index 8ff3c29..86652a2 100644 --- a/backend/agent/src/Monitor.Agent/Monitor.Agent.csproj +++ b/backend/agent/src/Monitor.Agent/Monitor.Agent.csproj @@ -1,4 +1,7 @@ - + + + Exe monitor-agent @@ -11,4 +14,7 @@ + + diff --git a/backend/fleet/tools/fleet-enroll/fleet-enroll.csproj b/backend/fleet/tools/fleet-enroll/fleet-enroll.csproj index 96f2d53..08e149e 100644 --- a/backend/fleet/tools/fleet-enroll/fleet-enroll.csproj +++ b/backend/fleet/tools/fleet-enroll/fleet-enroll.csproj @@ -1,4 +1,7 @@ - + + + Exe fleet-enroll @@ -8,4 +11,7 @@ + + diff --git a/backend/global.json b/backend/global.json index a4e7375..2f5da59 100644 --- a/backend/global.json +++ b/backend/global.json @@ -3,6 +3,9 @@ "version": "10.0.200", "rollForward": "latestFeature" }, + "msbuild-sdks": { + "StuDev.AotAnywhere": "1.0.5" + }, "test": { "runner": "Microsoft.Testing.Platform" } diff --git a/backend/probe/src/Monitor.Probe/Monitor.Probe.csproj b/backend/probe/src/Monitor.Probe/Monitor.Probe.csproj index 9621c17..22b9da3 100644 --- a/backend/probe/src/Monitor.Probe/Monitor.Probe.csproj +++ b/backend/probe/src/Monitor.Probe/Monitor.Probe.csproj @@ -1,4 +1,7 @@ - + + + Exe monitor-probe @@ -11,4 +14,7 @@ + + diff --git a/backend/tests/Monitor.Backend.Tests/AgentEndToEndTests.cs b/backend/tests/Monitor.Backend.Tests/AgentEndToEndTests.cs index 5c41db0..cc4ae75 100644 --- a/backend/tests/Monitor.Backend.Tests/AgentEndToEndTests.cs +++ b/backend/tests/Monitor.Backend.Tests/AgentEndToEndTests.cs @@ -252,9 +252,32 @@ public sealed class AgentEndToEndTests await using ProbeSimulator probe = await agent.ConnectProbeAsync(); await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue()); - using HttpResponseMessage accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{agent.Id}~{probe.ProbeId}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "hy2_ladder", ["params"] = new JsonObject { ["size"] = 1000 } })); - Assert.True(accepted.StatusCode == System.Net.HttpStatusCode.Accepted, await accepted.Content.ReadAsStringAsync()); - string testId = JsonNode.Parse(await accepted.Content.ReadAsStringAsync())!["id"]!.GetValue(); + HttpResponseMessage? accepted = null; + string? startFailure = null; + for (int attempt = 0; attempt < 5; attempt++) + { + accepted = await _backend.Admin.PostAsync($"/api/admin/lines/{agent.Id}~{probe.ProbeId}/tests", BackendFixture.Json(new JsonObject { ["plan"] = "hy2_ladder", ["params"] = new JsonObject { ["size"] = 1000 } })); + if (accepted.StatusCode == System.Net.HttpStatusCode.Accepted) + { + break; + } + + startFailure = await accepted.Content.ReadAsStringAsync(); + string? code = JsonNode.Parse(startFailure)?["code"]?.GetValue(); + accepted.Dispose(); + accepted = null; + if (code != "probe_offline" || attempt == 4) + { + Assert.Fail($"The test request was rejected: {startFailure}"); + } + + await BackendFixture.EventuallyAsync(async () => (await AgentViewAsync(agent))["probe_connected"]!.GetValue(), TimeSpan.FromSeconds(15), "the probe should reconnect after a transient disconnect"); + } + + HttpResponseMessage acceptedResponse = accepted ?? throw new InvalidOperationException("The test request was not accepted."); + using (acceptedResponse) + { + string testId = JsonNode.Parse(await acceptedResponse.Content.ReadAsStringAsync())!["id"]!.GetValue(); JsonNode test = null!; await BackendFixture.EventuallyAsync( @@ -273,6 +296,7 @@ public sealed class AgentEndToEndTests await BackendFixture.EventuallyAsync( async () => (await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/raw/chunks?agent_id={agent.Id}"))["items"]!.AsArray().Count(c => c!["kind"]!.GetValue() == "udp_train") == agent.App.Counters.Fragments, because: (await _backend.GetJsonAsync(_backend.Admin, $"/api/admin/events?agent_id={agent.Id}")).ToJsonString() + $" fragments={agent.App.Counters.Fragments} discarded={agent.App.Counters.FragmentsDiscarded} pending={agent.App.Link.Outbox.PendingCount}/{agent.App.Link.Outbox.PendingBytes} dropped={agent.App.Link.Outbox.Dropped} link={agent.App.Link.IsConnected}"); + } } [Fact] diff --git a/ci/publish-gitea-release.sh b/ci/publish-gitea-release.sh index 63d71ee..1b3ba1d 100644 --- a/ci/publish-gitea-release.sh +++ b/ci/publish-gitea-release.sh @@ -26,14 +26,17 @@ printf 'machine %s login justme password %s\n' "$gitea_host" "$GITEA_TOKEN" > "$ chmod 600 "$netrc" trap 'rm -rf "$tmp"' EXIT +release_body="Agent and Probe builds: Linux glibc and musl (x64, arm64, arm), macOS (x64, arm64), and Windows (x64, arm64). macOS binaries are not Developer ID signed or notarized. Backend OCI images: registry.oeo.one/monitor/monitor-backend:$version (linux/amd64 and linux/arm64)." +payload="$(printf '{"tag_name":"%s","target_commitish":"%s","name":"MyMonitor %s","body":"%s","draft":false,"prerelease":false}' \ + "$tag" "${CI_COMMIT_SHA:-}" "$tag" "$release_body")" release_status="$(curl --silent --show-error --retry 3 --netrc-file "$netrc" \ --output "$release_json" --write-out '%{http_code}' "$api/releases/tags/$tag")" +release_created=false if [[ "$release_status" == 404 ]]; then - payload="$(printf '{"tag_name":"%s","target_commitish":"%s","name":"MyMonitor %s","body":"Agent and Probe archives for Linux and Windows amd64 and arm64. Backend OCI images: registry.oeo.one/monitor/monitor-backend:%s (linux/amd64 and linux/arm64).","draft":false,"prerelease":false}' \ - "$tag" "${CI_COMMIT_SHA:-}" "$tag" "$version")" release_status="$(curl --silent --show-error --retry 3 --netrc-file "$netrc" \ --header 'Content-Type: application/json' --data "$payload" \ --output "$release_json" --write-out '%{http_code}' "$api/releases")" + release_created=true fi [[ "$release_status" =~ ^2 ]] || { echo "Gitea Release lookup/create failed (HTTP $release_status)" >&2 @@ -42,6 +45,17 @@ fi } release_id="$(grep -o '"id":[[:space:]]*[0-9][0-9]*' "$release_json" | head -1 | tr -cd '0-9')" [[ -n "$release_id" ]] || { echo "Could not read Gitea Release ID" >&2; exit 1; } +if [[ "$release_created" == false ]]; then + update_response="$tmp/release-update.json" + update_status="$(curl --silent --show-error --retry 3 --netrc-file "$netrc" \ + --request PATCH --header 'Content-Type: application/json' --data "$payload" \ + --output "$update_response" --write-out '%{http_code}' "$api/releases/$release_id")" + [[ "$update_status" =~ ^2 ]] || { + echo "Gitea Release update failed (HTTP $update_status)" >&2 + cat "$update_response" >&2 + exit 1 + } +fi curl --fail-with-body --silent --show-error --retry 3 --netrc-file "$netrc" \ "$api/releases/$release_id/assets" -o "$assets_json" @@ -50,6 +64,18 @@ assets=( "monitor-agent-v$version-linux-arm64.tar.gz" "monitor-probe-v$version-linux-amd64.tar.gz" "monitor-probe-v$version-linux-arm64.tar.gz" + "monitor-agent-v$version-linux-arm.tar.gz" + "monitor-probe-v$version-linux-arm.tar.gz" + "monitor-agent-v$version-linux-musl-amd64.tar.gz" + "monitor-probe-v$version-linux-musl-amd64.tar.gz" + "monitor-agent-v$version-linux-musl-arm64.tar.gz" + "monitor-probe-v$version-linux-musl-arm64.tar.gz" + "monitor-agent-v$version-linux-musl-arm.tar.gz" + "monitor-probe-v$version-linux-musl-arm.tar.gz" + "monitor-agent-v$version-macos-x64.tar.gz" + "monitor-probe-v$version-macos-x64.tar.gz" + "monitor-agent-v$version-macos-arm64.tar.gz" + "monitor-probe-v$version-macos-arm64.tar.gz" "monitor-agent-v$version-windows-amd64.zip" "monitor-agent-v$version-windows-arm64.zip" "monitor-probe-v$version-windows-amd64.zip" diff --git a/ci/release-cross-platform.ps1 b/ci/release-cross-platform.ps1 new file mode 100644 index 0000000..662f090 --- /dev/null +++ b/ci/release-cross-platform.ps1 @@ -0,0 +1,209 @@ +param( + [Parameter(Mandatory)] + [ValidateSet( + 'linux-x64', 'linux-arm64', 'linux-arm', + 'linux-musl-x64', 'linux-musl-arm64', 'linux-musl-arm', + 'osx-x64', 'osx-arm64', 'win-x64', 'win-arm64' + )] + [string]$RuntimeIdentifier +) + +$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.' +} +$version = $Matches[1] +$root = Split-Path -Parent $PSScriptRoot +$backend = Join-Path $root 'backend' +$projectVersion = [regex]::Match((Get-Content (Join-Path $backend 'Directory.Build.props') -Raw), '([^<]+)').Groups[1].Value +if ($version -ne $projectVersion) { + throw "Tag version $version does not match project version $projectVersion." +} +if (-not $env:GITEA_TOKEN) { + throw 'GITEA_TOKEN is required.' +} + +$isWindows = $RuntimeIdentifier.StartsWith('win-', [StringComparison]::Ordinal) +$isMacOS = $RuntimeIdentifier.StartsWith('osx-', [StringComparison]::Ordinal) +$os = if ($isWindows) { 'windows' } elseif ($isMacOS) { 'macos' } else { 'linux' } +$arch = switch ($RuntimeIdentifier) { + 'linux-x64' { 'amd64' } + 'linux-arm64' { 'arm64' } + 'linux-arm' { 'arm' } + 'linux-musl-x64' { 'musl-amd64' } + 'linux-musl-arm64' { 'musl-arm64' } + 'linux-musl-arm' { 'musl-arm' } + 'osx-x64' { 'x64' } + 'osx-arm64' { 'arm64' } + 'win-x64' { 'amd64' } + 'win-arm64' { 'arm64' } +} + +if ($isWindows) { + $vcArchitecture = if ($RuntimeIdentifier -eq 'win-x64') { 'x64' } else { 'arm64' } + $vsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' + $vsPath = & $vsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath + if (-not $vsPath) { + throw 'Visual Studio C++ Build Tools were not found.' + } + $devCmd = Join-Path $vsPath 'Common7\Tools\VsDevCmd.bat' + $environmentLines = & cmd.exe /d /s /c "call `"$devCmd`" -no_logo -arch=$vcArchitecture -host_arch=x64 && set" + foreach ($line in $environmentLines) { + $separator = $line.IndexOf('=') + if ($separator -gt 0) { + [Environment]::SetEnvironmentVariable($line.Substring(0, $separator), $line.Substring($separator + 1), 'Process') + } + } +} + +$publishRoot = Join-Path $root ".release\$RuntimeIdentifier-$PID" +$buildRoot = Join-Path $root ".release\build\$RuntimeIdentifier-$PID" +$packageRoot = Join-Path $root '.release\packages' +$agentOut = Join-Path $publishRoot 'agent' +$probeOut = Join-Path $publishRoot 'probe' +$enrollOut = Join-Path $publishRoot 'fleet-enroll' +$null = New-Item -ItemType Directory -Force -Path $agentOut, $probeOut, $enrollOut, $packageRoot + +function Publish-Aot([string]$Project, [string]$Output) { + $arguments = @( + 'publish', $Project, + '-m:2', + '-c', 'Release', + '-r', $RuntimeIdentifier, + '--self-contained', 'true', + '-p:PublishAot=true', + '-p:StripSymbols=true', + '-p:DebugType=none', + '--artifacts-path', $buildRoot, + '-o', $Output + ) + Push-Location $backend + try { + & dotnet @arguments + if ($LASTEXITCODE -ne 0) { + throw "NativeAOT publish failed for $Project ($RuntimeIdentifier)." + } + } + finally { + Pop-Location + } +} + +function New-TarGzArchive([string]$SourceDirectory, [string]$ArchivePath, [string[]]$Executables) { + $tarFile = [IO.File]::Create($ArchivePath) + $gzip = [IO.Compression.GZipStream]::new($tarFile, [IO.Compression.CompressionLevel]::Optimal, $true) + $writer = [System.Formats.Tar.TarWriter]::new($gzip, [System.Formats.Tar.TarEntryFormat]::Pax, $true) + $executableMode = [System.IO.UnixFileMode]::UserRead -bor [System.IO.UnixFileMode]::UserWrite -bor [System.IO.UnixFileMode]::UserExecute -bor [System.IO.UnixFileMode]::GroupRead -bor [System.IO.UnixFileMode]::GroupExecute -bor [System.IO.UnixFileMode]::OtherRead -bor [System.IO.UnixFileMode]::OtherExecute + $regularMode = [System.IO.UnixFileMode]::UserRead -bor [System.IO.UnixFileMode]::UserWrite -bor [System.IO.UnixFileMode]::GroupRead -bor [System.IO.UnixFileMode]::OtherRead + try { + foreach ($file in Get-ChildItem -LiteralPath $SourceDirectory -File -Recurse) { + $relativePath = [IO.Path]::GetRelativePath($SourceDirectory, $file.FullName).Replace('\', '/') + $entry = [System.Formats.Tar.PaxTarEntry]::new([System.Formats.Tar.TarEntryType]::RegularFile, $relativePath) + $entry.Mode = if ($Executables -contains $relativePath) { $executableMode } else { $regularMode } + $entry.DataStream = [IO.File]::OpenRead($file.FullName) + try { + $writer.WriteEntry($entry) + } + finally { + $entry.DataStream.Dispose() + } + } + } + finally { + $writer.Dispose() + $gzip.Dispose() + $tarFile.Dispose() + } +} + +Publish-Aot 'agent/src/Monitor.Agent' $agentOut +Publish-Aot 'probe/src/Monitor.Probe' $probeOut +Publish-Aot 'fleet/tools/fleet-enroll' $enrollOut + +$giteaBase = if ($env:GITEA_BASE) { $env:GITEA_BASE.TrimEnd('/') } else { 'http://localhost:3100' } +$netrc = Join-Path $env:TEMP ("monitor-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 Publish-Package([string]$Role, [string]$BinaryDirectory) { + $stage = Join-Path $publishRoot "stage-$Role" + $null = New-Item -ItemType Directory -Force -Path $stage + $binaryName = if ($isWindows) { "monitor-$Role.exe" } else { "monitor-$Role" } + Copy-Item -LiteralPath (Join-Path $BinaryDirectory $binaryName) -Destination $stage + Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $stage + Copy-Item -LiteralPath (Join-Path $root "backend\$Role\README.md") -Destination $stage + + if ($Role -eq 'agent') { + $enrollName = if ($isWindows) { 'fleet-enroll.exe' } else { 'fleet-enroll' } + Copy-Item -LiteralPath (Join-Path $enrollOut $enrollName) -Destination $stage + } + if ($os -eq 'linux') { + Copy-Item -LiteralPath (Join-Path $root 'packaging/install.sh') -Destination $stage + $systemd = Join-Path $stage 'systemd' + $null = New-Item -ItemType Directory -Force -Path $systemd + Copy-Item -LiteralPath (Join-Path $root "packaging/systemd/monitor-$Role.service") -Destination $systemd + if ($Role -eq 'probe') { + $openwrt = Join-Path $stage 'openwrt' + $null = New-Item -ItemType Directory -Force -Path $openwrt + Copy-Item -LiteralPath (Join-Path $root 'packaging/openwrt/monitor-probe.init') -Destination $openwrt + Copy-Item -LiteralPath (Join-Path $root 'packaging/openwrt/monitor.keep') -Destination $openwrt + } + } + elseif ($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 ($isWindows) { + "monitor-$Role-v$version-windows-$arch.zip" + } + else { + "monitor-$Role-v$version-$os-$arch.tar.gz" + } + $archive = Join-Path $packageRoot $archiveName + if ($isWindows) { + Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $archive -CompressionLevel Optimal -Force + } + else { + $executables = if ($os -eq 'linux') { @("monitor-$Role", 'install.sh') } else { @("monitor-$Role") } + if ($Role -eq 'agent') { $executables += 'fleet-enroll' } + New-TarGzArchive $stage $archive $executables + } + + $hash = (Get-FileHash -LiteralPath $archive -Algorithm SHA256).Hash.ToLowerInvariant() + $checksumPath = "$archive.sha256" + [IO.File]::WriteAllText($checksumPath, "$hash $archiveName`n", [Text.UTF8Encoding]::new($false)) + + foreach ($file in @($archive, $checksumPath)) { + $name = Split-Path -Leaf $file + $url = "$giteaBase/api/packages/justme/generic/monitor-node/$version/$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." + } + if ($status -match '^2') { + Write-Output "uploaded Gitea package $name" + } + elseif ($status -eq '409') { + Write-Output "Gitea package $name already exists; reusing the existing release file" + } + else { + throw "Gitea package upload failed for $name (HTTP $status)." + } + } +} + +try { + Publish-Package 'agent' $agentOut + Publish-Package 'probe' $probeOut +} +finally { + Remove-Item -LiteralPath $netrc -Force -ErrorAction SilentlyContinue +} diff --git a/ci/release-linux.sh b/ci/release-linux.sh index c34cf0f..1504cae 100644 --- a/ci/release-linux.sh +++ b/ci/release-linux.sh @@ -1,127 +1,50 @@ #!/usr/bin/env bash -# Build one Linux architecture/component, then publish its package or OCI image. +# Publish one framework-dependent Backend OCI image for the requested arch. set -euo pipefail -arch="${1:?usage: release-linux.sh }" -component="${2:?usage: release-linux.sh }" +arch="${1:?usage: release-linux.sh }" root="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)" tag="${CI_COMMIT_TAG:-}" [[ "$tag" =~ ^v[0-9]+\.[0-9]+\.[0-9]+(-[0-9A-Za-z.-]+)?$ ]] || { echo "release must run from a vMAJOR.MINOR.PATCH tag" >&2 exit 2 } -version="${tag#v}" - +[[ -n "${ZOT_USERNAME:-}" && -n "${ZOT_PASSWORD:-}" ]] || { + echo "Zot credentials are required" >&2; exit 2; +} case "$arch" in - amd64) rid=linux-x64; objcopy=llvm-objcopy ;; - arm64) rid=linux-arm64; objcopy=aarch64-linux-gnu-objcopy ;; + amd64|arm64) ;; *) echo "unsupported architecture: $arch" >&2; exit 2 ;; esac -case "$component" in - agent|probe|backend) ;; - *) echo "unsupported component: $component" >&2; exit 2 ;; -esac +version="${tag#v}" project_version="$(sed -n 's#.*\(.*\).*#\1#p' "$root/backend/Directory.Build.props" | head -1)" if [[ "$version" != "$project_version" ]]; then echo "tag version $version does not match project version $project_version" >&2 exit 1 fi -output="$root/.release/linux-$arch/$component" -build_root="$root/.release/build/linux-$arch-$component" -stage_root="$root/.release/stage/linux-$arch-$component" -mkdir -p "$output" "$build_root" "$stage_root" +registry="${ZOT_REGISTRY:-192.168.2.162:5000}" +image="$registry/monitor/monitor-backend" +output="$root/.release/linux-$arch/backend" +build_root="$root/.release/build/linux-$arch-backend" +container_context="$root/.release/container/linux-$arch" +mkdir -p "$output" "$build_root" "$container_context/app" "$container_context/data" -publish_aot() { - local name="$1" project="$2" destination="$3" - (cd "$root/backend" && dotnet publish "$project" \ - -m:3 \ - -c Release -r "$rid" --self-contained true \ - -p:PublishAot=true -p:StripSymbols=true -p:DebugType=none \ - -p:ObjCopyName="$objcopy" \ - --artifacts-path "$build_root" \ - -o "$destination") -} +(cd "$root/backend" && dotnet publish src/Monitor.Backend \ + -m:3 -c Release --no-self-contained \ + -p:UseAppHost=false -p:OpenApiGenerateDocuments=false \ + --artifacts-path "$build_root" -o "$output") +cp -a "$output/." "$container_context/app/" +: > "$container_context/data/.keep" -if [[ "$component" == backend ]]; then - [[ -n "${ZOT_USERNAME:-}" && -n "${ZOT_PASSWORD:-}" ]] || { - echo "Zot credentials are required" >&2; exit 2; - } - registry="${ZOT_REGISTRY:-192.168.2.162:5000}" - image="$registry/monitor/monitor-backend" - publish_backend() { - (cd "$root/backend" && dotnet publish src/Monitor.Backend \ - -m:3 \ - -c Release --no-self-contained \ - -p:UseAppHost=false -p:OpenApiGenerateDocuments=false \ - --artifacts-path "$build_root" \ - -o "$output") - } - publish_backend - - container_context="$root/.release/container/linux-$arch" - mkdir -p "$container_context/app" "$container_context/data" - cp -a "$output/." "$container_context/app/" - : > "$container_context/data/.keep" - - export REGISTRY_AUTH_FILE="$root/.release/auth/zot-linux-$arch.json" - mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")" - buildah login --tls-verify=false --username "$ZOT_USERNAME" --password-stdin "$registry" <<< "$ZOT_PASSWORD" >/dev/null - local_image="localhost/monitor-backend:$version-$arch" - buildah bud --tls-verify=false --arch "$arch" \ - --build-arg "APP_VERSION=$version" \ - -f "$root/packaging/container/Backend.Containerfile" \ - -t "$local_image" "$container_context" >/dev/null - buildah push --tls-verify=false "$local_image" "docker://$image:$version-$arch" >/dev/null - echo "pushed Zot image $image:$version-$arch" - exit 0 -fi - -[[ -n "${GITEA_TOKEN:-}" ]] || { echo "GITEA_TOKEN is required" >&2; exit 2; } -gitea_base="${GITEA_BASE:-http://192.168.2.162:3100}" -gitea_host="${gitea_base#https://}" -gitea_host="${gitea_host#http://}" -gitea_host="${gitea_host%%/*}" -gitea_host="${gitea_host%%:*}" -netrc="$(mktemp)" -chmod 600 "$netrc" -printf 'machine %s login justme password %s\n' "$gitea_host" "$GITEA_TOKEN" > "$netrc" -trap 'rm -f "$netrc"' EXIT - -upload_package() { - local file="$1" name status - name="$(basename "$file")" - status="$(curl --silent --show-error --retry 3 --netrc-file "$netrc" \ - --upload-file "$file" --output /dev/null --write-out '%{http_code}' \ - "$gitea_base/api/packages/justme/generic/monitor-node/$version/$name")" - case "$status" in - 2*) echo "uploaded Gitea package $name" ;; - 409) echo "Gitea package $name already exists; reusing the existing release file" ;; - *) echo "Gitea package upload failed for $name (HTTP $status)" >&2; return 1 ;; - esac -} - -stage="$stage_root/$component" -mkdir -p "$stage/systemd" -publish_aot "$component" "$component/src/Monitor.$(tr '[:lower:]' '[:upper:]' <<< "${component:0:1}")${component:1}" "$output/$component" - -cp "$output/$component/monitor-$component" "$stage/monitor-$component" -cp "$root/packaging/install.sh" "$stage/install.sh" -cp "$root/LICENSE" "$stage/LICENSE" -cp "$root/backend/$component/README.md" "$stage/README.md" -cp "$root/packaging/systemd/monitor-$component.service" "$stage/systemd/monitor-$component.service" -if [[ "$component" == agent ]]; then - publish_aot fleet-enroll fleet/tools/fleet-enroll "$output/fleet-enroll" - cp "$output/fleet-enroll/fleet-enroll" "$stage/fleet-enroll" -else - mkdir -p "$stage/openwrt" - cp "$root/packaging/openwrt/monitor-probe.init" "$stage/openwrt/" - cp "$root/packaging/openwrt/monitor.keep" "$stage/openwrt/" -fi - -archive="$output/monitor-$component-v$version-linux-$arch.tar.gz" -tar -C "$stage" -czf "$archive" . -(cd "$output" && sha256sum "$(basename "$archive")" > "$(basename "$archive").sha256") -upload_package "$archive" -upload_package "$archive.sha256" +export REGISTRY_AUTH_FILE="$root/.release/auth/zot-linux-$arch.json" +mkdir -p "$(dirname "$REGISTRY_AUTH_FILE")" +buildah login --tls-verify=false --username "$ZOT_USERNAME" --password-stdin "$registry" <<< "$ZOT_PASSWORD" >/dev/null +local_image="localhost/monitor-backend:$version-$arch" +buildah bud --tls-verify=false --arch "$arch" \ + --build-arg "APP_VERSION=$version" \ + -f "$root/packaging/container/Backend.Containerfile" \ + -t "$local_image" "$container_context" >/dev/null +buildah push --tls-verify=false "$local_image" "docker://$image:$version-$arch" >/dev/null +echo "pushed Zot image $image:$version-$arch" diff --git a/ci/release-windows.ps1 b/ci/release-windows.ps1 deleted file mode 100644 index bbe8e79..0000000 --- a/ci/release-windows.ps1 +++ /dev/null @@ -1,134 +0,0 @@ -param( - [Parameter(Mandatory)] - [ValidateSet('amd64', 'arm64')] - [string]$Architecture, - [Parameter(Mandatory)] - [ValidateSet('agent', 'probe')] - [string]$Role -) - -$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.' -} -$version = $Matches[1] -$root = Split-Path -Parent $PSScriptRoot -$backend = Join-Path $root 'backend' -$projectVersion = [regex]::Match((Get-Content (Join-Path $backend 'Directory.Build.props') -Raw), '([^<]+)').Groups[1].Value -if ($version -ne $projectVersion) { - throw "Tag version $version does not match project version $projectVersion." -} -if (-not $env:GITEA_TOKEN) { - throw 'GITEA_TOKEN is required.' -} - -$rid = if ($Architecture -eq 'amd64') { 'win-x64' } else { 'win-arm64' } -$vcArchitecture = if ($Architecture -eq 'amd64') { 'x64' } else { 'arm64' } -$vsWhere = Join-Path ${env:ProgramFiles(x86)} 'Microsoft Visual Studio\Installer\vswhere.exe' -$vsPath = & $vsWhere -latest -products * -requires Microsoft.VisualStudio.Component.VC.Tools.x86.x64 -property installationPath -if (-not $vsPath) { - throw 'Visual Studio C++ Build Tools were not found.' -} - -$devCmd = Join-Path $vsPath 'Common7\Tools\VsDevCmd.bat' -$environmentLines = & cmd.exe /d /s /c "call `"$devCmd`" -no_logo -arch=$vcArchitecture -host_arch=x64 && set" -foreach ($line in $environmentLines) { - $separator = $line.IndexOf('=') - if ($separator -gt 0) { - [Environment]::SetEnvironmentVariable($line.Substring(0, $separator), $line.Substring($separator + 1), 'Process') - } -} - -$buildRoot = Join-Path $root ".release\build\windows-$Architecture-$Role-$PID" -$publishRoot = Join-Path $root ".release\windows-$Architecture-$Role-$PID" -$packageRoot = Join-Path $root '.release\packages' -$null = New-Item -ItemType Directory -Force -Path $publishRoot, $packageRoot - -function Publish-Aot([string]$Project, [string]$Output) { - $arguments = @( - 'publish', $Project, - '-m:3', - '-c', 'Release', - '-r', $rid, - '--self-contained', 'true', - '-p:PublishAot=true', - '-p:StripSymbols=true', - '-p:DebugType=none', - '--artifacts-path', $buildRoot, - '-o', $Output - ) - Push-Location $backend - try { - & dotnet @arguments - if ($LASTEXITCODE -ne 0) { - throw "NativeAOT publish failed for $Project ($rid)." - } - } - finally { - Pop-Location - } -} - -$roleOut = Join-Path $publishRoot $Role -$null = New-Item -ItemType Directory -Force -Path $roleOut -$enrollOut = $null -if ($Role -eq 'agent') { - Publish-Aot 'agent/src/Monitor.Agent' $roleOut - $enrollOut = Join-Path $publishRoot 'fleet-enroll' - $null = New-Item -ItemType Directory -Force -Path $enrollOut - Publish-Aot 'fleet/tools/fleet-enroll' $enrollOut -} -else { - Publish-Aot 'probe/src/Monitor.Probe' $roleOut -} - -$giteaBase = if ($env:GITEA_BASE) { $env:GITEA_BASE.TrimEnd('/') } else { 'http://localhost:3100' } -$netrc = Join-Path $env:TEMP ("monitor-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 Publish-Package([string]$Role, [string]$BinaryDirectory) { - $stage = Join-Path $publishRoot "stage-$Role" - $null = New-Item -ItemType Directory -Force -Path $stage - Copy-Item -LiteralPath (Join-Path $BinaryDirectory "monitor-$Role.exe") -Destination $stage - Copy-Item -LiteralPath (Join-Path $root 'LICENSE') -Destination $stage - Copy-Item -LiteralPath (Join-Path $root "backend\$Role\README.md") -Destination $stage - if ($Role -eq 'agent') { - Copy-Item -LiteralPath (Join-Path $enrollOut 'fleet-enroll.exe') -Destination $stage - } - - $archiveName = "monitor-$Role-v$version-windows-$Architecture.zip" - $archive = Join-Path $packageRoot $archiveName - Compress-Archive -Path (Join-Path $stage '*') -DestinationPath $archive -CompressionLevel Optimal -Force - $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)) - - foreach ($file in @($archive, $checksumPath)) { - $name = Split-Path -Leaf $file - $url = "$giteaBase/api/packages/justme/generic/monitor-node/$version/$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." - } - if ($status -match '^2') { - Write-Output "uploaded Gitea package $name" - } - elseif ($status -eq '409') { - Write-Output "Gitea package $name already exists; reusing the existing release file" - } - else { - throw "Gitea package upload failed for $name (HTTP $status)." - } - } -} - -try { - Publish-Package $Role $roleOut -} -finally { - Remove-Item -LiteralPath $netrc -Force -ErrorAction SilentlyContinue -} diff --git a/docs/deployment.md b/docs/deployment.md index 145fb30..401ee09 100644 --- a/docs/deployment.md +++ b/docs/deployment.md @@ -11,14 +11,14 @@ scripts/package.sh → backend/art | rid | 目标 | 构建方式 | | --- | --- | --- | -| `linux-musl-x64` | Alpine、OpenWrt x86 | Alpine 的 NativeAOT SDK 容器(任何有 Docker 的主机,包括 Windows) | -| `linux-x64` | Debian、Ubuntu 等 glibc 发行版 | Ubuntu noble 的 NativeAOT SDK 容器 | -| `win-x64` | Windows | 在 Windows 上直接 `dotnet publish -p:PublishAot=true`(需要 VS 的 C++ 工具链,`vswhere.exe` 所在目录要在 PATH 里) | -| `linux-musl-arm64`、`linux-arm64` | 路由器、树莓派 | 需要在 arm64 主机(或 CI 的 arm64 runner)上运行同一个脚本;脚本在 x64 主机上会拒绝并说明 | +| `linux-x64`、`linux-arm64`、`linux-arm` | Debian/Ubuntu glibc、ARMv7 | Windows CI 主机通过 AotAnywhere 交叉编译 | +| `linux-musl-x64`、`linux-musl-arm64`、`linux-musl-arm` | Alpine、OpenWrt、musl ARMv7 | Windows CI 主机通过 AotAnywhere 交叉编译 | +| `osx-x64`、`osx-arm64` | Intel Mac、Apple Silicon | Windows CI 主机通过 AotAnywhere 交叉编译;发布包未做 Developer ID 签名或 Apple 公证 | +| `win-x64`、`win-arm64` | Windows x64、ARM64 | Windows CI 主机上的 Visual Studio C++ 工具链 | -NativeAOT 不能交叉编译到别的操作系统;MIPS 不支持(见 `project-layout.md`)。 +macOS 的 Apple Silicon 产物带临时 ad-hoc 签名,Intel 产物未签名。通过浏览器下载时,macOS Gatekeeper 可能阻止启动;面向普通用户分发前需要 Developer ID 签名和 Apple 公证。MIPS 不在当前目标列表中(见 `project-layout.md`)。 -仓库内的 `scripts/publish.sh` 面向本机目标平台;tag 发布流水线另使用 Linux 交叉工具链生成 arm64 产物,并在 Windows runner 上构建 Windows x64/arm64 产物。 +仓库内的 `scripts/publish.sh` 面向本机目标平台;Woodpecker 的 tag 流水线在 Windows runner 上构建 Agent、Probe 和 `fleet-enroll` 的所有发布目标,Backend OCI 镜像仍在 Linux runner 上构建。 ## Backend 容器 @@ -36,7 +36,7 @@ Compose 将容器端口只绑定到宿主机 `127.0.0.1:8080`,适合由同机 ## 节点二进制发布 -Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上构建 Agent 和 Probe 的 Windows/Linux amd64/arm64 NativeAOT 程序,打包为 `.zip` 或 `.tar.gz` 并上传到 Gitea Generic Packages。发布前会确认该 tag 指向的 commit 已通过 push 验证;Agent、Probe 和 Backend 按架构并行构建。所有产物成功后,CI 会创建对应的 Gitea Release,并将四个平台的 Agent/Probe 压缩包及 SHA-256 校验文件作为附件。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 包带有安装脚本和 systemd/OpenWrt 单元;Windows 包包含可执行文件及对应 README。安装说明见 [`../backend/agent/README.md`](../backend/agent/README.md) 与 [`../backend/probe/README.md`](../backend/probe/README.md)。 +Woodpecker 在 `vMAJOR.MINOR.PATCH` tag 上,先确认 commit 已通过 push 验证,再由 Windows runner 使用 AotAnywhere 并行构建 Agent/Probe 的 Linux glibc、Linux musl、macOS 和 Windows 产物。Agent 包还包含一次性注册工具 `fleet-enroll`。Linux 归档带有安装脚本和 systemd/OpenWrt 单元;Windows 产物为 `.zip`,Linux/macOS 产物为 `.tar.gz`。所有平台产物和 Backend OCI 镜像成功后,CI 会创建对应的 Gitea Release,并附上压缩包和 SHA-256 校验文件。macOS 产物目前未签名/公证。安装说明见 [`../backend/agent/README.md`](../backend/agent/README.md) 与 [`../backend/probe/README.md`](../backend/probe/README.md)。 实测体积(`StackTraceSupport=false`、`IlcFoldIdenticalMethodBodies=true`、`OptimizationPreference=Size`):