feat(cert,scripts,docs): Linux 原生安装包与证书申请三处修复(DNS 解析器 / Buypass 下线 / ZeroSSL 报错与密钥脱敏)(v1.2.13)
CI / verify (push) Canceled after 0s

- Linux 安装包:scripts/build-linux.ps1 交叉编译出 fonu-<版本>-linux-{amd64,arm64}.tar.gz(二进制 + migrations + systemd 安装脚本 + 运行时资源 + sha256),scripts/fonu-install.sh 负责装机(/opt/fonu、/var/lib/fonu、/etc/fonu/fonu.env 首次生成随机会话密钥)并新增 --reset-admin;装完直接打印初始管理员口令

- 证书 DNS-01:lego 默认读系统 resolv.conf(容器里是 Docker 的 127.0.0.11,不返回权威 NS)导致 could not determine authoritative nameservers;现显式传 AddRecursiveNameservers,默认 223.5.5.5/1.1.1.1/8.8.8.8,设置项 acme_dns_resolvers 可覆盖(system = 回到系统解析器)

- Buypass 已停止签发(2025-10-15)并终止服务(2026-04-15):从可选 CA 移除,历史记录仍可展示,直接提交会被拦下并给出中文原因

- ZeroSSL:422 时带出 error.type/error.code(非 JSON 退回截断正文);同时修掉网络错误把含 access_key 的完整 URL 写进任务日志与 app.log 的密钥泄露

- 测试:解析器列表解析 3 例 + ZeroSSL EAB 4 例(含不泄露密钥断言);gofmt / go build / go vet / go test 全绿
This commit is contained in:
2026-09-27 10:10:15 +08:00
parent 2236259a52
commit dda0eb876e
18 changed files with 1353 additions and 32 deletions
+111
View File
@@ -0,0 +1,111 @@
<#
.SYNOPSIS
构建 Fonu 的 Linux 安装包(tar.gz:二进制 + migrations + systemd 安装脚本)。
.DESCRIPTION
与 Dockerfile 同源的三步:
1) 前端构建(VITE_APP_VERSION 取根目录 VERSION,dist 会被 //go:embed 进二进制)
2) 交叉编译 fonu-agent 双架构到 internal/frp/agentbin(主程序构建时内嵌)
3) CGO_ENABLED=0 交叉编译主程序并打包
依赖是纯 Go 的(modernc.org/sqlite),所以 Windows 上就能产出 Linux 包,不需要 Docker/WSL。
.EXAMPLE
./scripts/build-linux.ps1 # 双架构
./scripts/build-linux.ps1 -Arch amd64 # 只出 amd64
./scripts/build-linux.ps1 -SkipWeb # 前端已构建过,跳过 npm
#>
param(
[ValidateSet('amd64', 'arm64', 'both')] [string]$Arch = 'both',
[string]$OutDir = 'dist',
[switch]$SkipWeb,
[switch]$SkipAgent
)
$ErrorActionPreference = 'Stop'
$root = Split-Path -Parent $PSScriptRoot
Set-Location $root
$version = (Get-Content (Join-Path $root 'VERSION') -Raw).Trim()
if (-not $version) { throw 'VERSION 文件为空,无法确定版本号' }
Write-Host "==> Fonu $version" -ForegroundColor Cyan
# ---- 1) 前端:dist 缺失会让主程序编译失败(//go:embed all:web/dist)
if (-not $SkipWeb) {
Push-Location (Join-Path $root 'web')
try {
if (-not (Test-Path 'node_modules')) {
Write-Host '==> npm ci(首次构建,需要网络)'
npm ci
if ($LASTEXITCODE -ne 0) { throw "npm ci 失败(exit $LASTEXITCODE)" }
}
$env:VITE_APP_VERSION = $version
npm run build
if ($LASTEXITCODE -ne 0) { throw "前端构建失败(exit $LASTEXITCODE)" }
} finally {
Pop-Location
Remove-Item Env:VITE_APP_VERSION -ErrorAction SilentlyContinue
}
}
if (-not (Test-Path (Join-Path $root 'cmd/fonu/web/dist/index.html'))) {
throw '缺少 cmd/fonu/web/dist/index.html:请先构建前端(或去掉 -SkipWeb)'
}
# ---- 2) fonu-agent 双架构:主程序会内嵌这两个文件,必须在构建主程序前就位
if (-not $SkipAgent) {
Write-Host '==> 交叉编译 fonu-agent(amd64/arm64)'
$env:CGO_ENABLED = '0'
$env:GOOS = 'linux'
foreach ($a in @('amd64', 'arm64')) {
$env:GOARCH = $a
go build -trimpath -o "internal/frp/agentbin/fonu-agent-linux-$a" ./cmd/fonu-agent
if ($LASTEXITCODE -ne 0) { throw "fonu-agent($a) 构建失败(exit $LASTEXITCODE)" }
}
}
# ---- 3) 主程序 + 打包
$archs = if ($Arch -eq 'both') { @('amd64', 'arm64') } else { @($Arch) }
$outPath = Join-Path $root $OutDir
New-Item -ItemType Directory -Force -Path $outPath | Out-Null
try {
foreach ($a in $archs) {
$name = "fonu-$version-linux-$a"
$stage = Join-Path $outPath $name
if (Test-Path $stage) { Remove-Item -Recurse -Force $stage }
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'migrations') | Out-Null
Write-Host "==> 交叉编译 fonu(linux/$a)"
$env:CGO_ENABLED = '0'
$env:GOOS = 'linux'
$env:GOARCH = $a
go build -trimpath `
-ldflags "-s -w -X github.com/fonu/fonu/internal/version.Version=$version" `
-o (Join-Path $stage 'fonu') ./cmd/fonu
if ($LASTEXITCODE -ne 0) { throw "fonu($a) 构建失败(exit $LASTEXITCODE)" }
# 包内容与 Dockerfile 的运行时 stage 对齐:migrations 必须与二进制同级(程序按工作目录找)
Copy-Item (Join-Path $root 'migrations/*.sql') (Join-Path $stage 'migrations') -Force
Copy-Item (Join-Path $root 'scripts/fonu-install.sh') (Join-Path $stage 'install.sh') -Force
Copy-Item (Join-Path $root 'VERSION') $stage -Force
Copy-Item (Join-Path $root 'LICENSE') $stage -Force
if (Test-Path (Join-Path $root 'web/assets/image')) {
New-Item -ItemType Directory -Force -Path (Join-Path $stage 'web/assets') | Out-Null
Copy-Item -Recurse -Force (Join-Path $root 'web/assets/image') (Join-Path $stage 'web/assets')
}
$tarball = Join-Path $outPath "$name.tar.gz"
if (Test-Path $tarball) { Remove-Item -Force $tarball }
tar -czf $tarball -C $outPath $name
if ($LASTEXITCODE -ne 0) { throw "打包失败(exit $LASTEXITCODE)" }
$hash = (Get-FileHash $tarball -Algorithm SHA256).Hash.ToLower()
"$hash $name.tar.gz" | Out-File -Encoding ascii "$tarball.sha256"
$sizeMB = [math]::Round((Get-Item $tarball).Length / 1MB, 2)
Write-Host (" {0}.tar.gz {1} MB sha256 {2}" -f $name, $sizeMB, $hash.Substring(0, 16)) -ForegroundColor Green
}
} finally {
# 交叉编译用的环境变量必须复位,否则后续 go vet/test 会继续按 linux 目标跑
Remove-Item Env:GOOS, Env:GOARCH, Env:CGO_ENABLED -ErrorAction SilentlyContinue
}
Write-Host "==> 产物在 $outPath" -ForegroundColor Cyan