Files
MyGoNavi/.github/workflows/dev-build.yml
Syngnat 3fe41f3700 feat(update): 将更新日志写入静态清单并打通 CI 生成
- 扩展 latest.json / UpdateInfo 支持 releaseNotes 字段
- latest 发版将 changelog 同步写入清单与 GitHub Release
- dev 构建按上一版基线自动生成相对更新日志
- 补充清单生成与后端映射测试
2026-08-02 13:07:26 +08:00

1588 lines
67 KiB
YAML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
name: Dev Build
on:
push:
branches:
- dev
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
permissions:
contents: write
jobs:
frontend:
name: Build frontend
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Test release asset contracts
run: |
bash tools/detect-changed-driver-agents.test.sh
bash tools/generate-driver-agent-revisions.test.sh
python3 tools/generate-driver-release-manifest.test.py
python3 tools/generate-update-latest-manifest.test.py
python3 tools/prepare-vps-release-payload.test.py
bash tools/vps-release-commit.test.sh
python3 tools/package-driver-release-assets.test.py
python3 tools/legal-release-assets.test.py
python3 tools/windows-release-artifacts.test.py
python3 tools/generate-release-notes.test.py
- name: Setup Node
uses: actions/setup-node@v5
with:
node-version: '20'
cache: 'npm'
cache-dependency-path: frontend/package-lock.json
- name: Cache frontend node_modules
uses: actions/cache@v5
with:
path: frontend/node_modules
key: ${{ runner.os }}-node20-frontend-${{ hashFiles('frontend/package-lock.json', 'frontend/patches/*.patch') }}
- name: Install Wails
run: go install -v github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
- name: Build frontend dist
shell: bash
run: |
set -euo pipefail
mkdir -p frontend/dist
printf '<!doctype html><title>GoNavi</title>\n' > frontend/dist/index.html
wails generate module
node frontend/scripts/wails-frontend-install.mjs
npm --prefix frontend run build
- name: Test frontend virtual scrolling
run: npm --prefix frontend test -- src/components/dataGridVirtualScroll.test.ts src/components/DataGrid.layout.test.tsx
- name: Pack frontend dist
shell: bash
run: tar -cf frontend-dist.tar -C frontend/dist .
- name: Upload frontend dist
uses: actions/upload-artifact@v6
with:
name: frontend-dist
path: frontend-dist.tar
if-no-files-found: error
retention-days: 1
driver_agents:
name: Detect changed driver agents
runs-on: ubuntu-latest
outputs:
drivers: ${{ steps.detect.outputs.drivers }}
drivers_darwin_amd64: ${{ steps.detect.outputs.drivers_darwin_amd64 }}
drivers_darwin_arm64: ${{ steps.detect.outputs.drivers_darwin_arm64 }}
drivers_windows_amd64: ${{ steps.detect.outputs.drivers_windows_amd64 }}
drivers_windows_arm64: ${{ steps.detect.outputs.drivers_windows_arm64 }}
drivers_linux_amd64: ${{ steps.detect.outputs.drivers_linux_amd64 }}
drivers_linux_arm64: ${{ steps.detect.outputs.drivers_linux_arm64 }}
has_changes: ${{ steps.detect.outputs.has_changes }}
release_source: ${{ steps.detect.outputs.release_source }}
compare_base: ${{ steps.detect.outputs.compare_base }}
force_global_driver_builds: ${{ steps.detect.outputs.force_global_driver_builds }}
source_commit: ${{ steps.published_source.outputs.source_commit }}
has_manifest: ${{ steps.published_source.outputs.has_manifest }}
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
cache: true
- name: Resolve published driver release source
id: published_source
env:
DRIVER_RELEASE_TOKEN: ${{ secrets.DRIVER_RELEASE_TOKEN }}
shell: bash
run: |
set -euo pipefail
manifest_path="$RUNNER_TEMP/published-driver-manifest.json"
SOURCE_COMMIT="$(python3 tools/resolve-driver-release-source.py --repo Syngnat/GoNavi-DriverAgents --tag dev-latest --manifest-output "$manifest_path")"
echo "source_commit=${SOURCE_COMMIT}" >> "$GITHUB_OUTPUT"
if [[ -s "$manifest_path" ]]; then
echo "has_manifest=true" >> "$GITHUB_OUTPUT"
echo "🧭 Published dev driver release exposes revision manifest"
else
echo "has_manifest=false" >> "$GITHUB_OUTPUT"
echo "🧭 Published dev driver release has no revision manifest"
fi
if [[ -n "$SOURCE_COMMIT" && -s "$manifest_path" ]]; then
if bash ./tools/validate-driver-release-manifest.sh --commit "$SOURCE_COMMIT" --manifest "$manifest_path"; then
if python3 tools/validate-driver-release-assets.py --repo Syngnat/GoNavi-DriverAgents --tag dev-latest; then
echo "manifest_valid=true" >> "$GITHUB_OUTPUT"
echo "🧭 Published dev driver release manifest and actual assets are consistent with its source commit"
else
echo "manifest_valid=false" >> "$GITHUB_OUTPUT"
echo "⚠️ Published dev driver release assets do not match manifest; forcing full rebuild"
fi
else
echo "manifest_valid=false" >> "$GITHUB_OUTPUT"
echo "⚠️ Published dev driver release manifest is stale; forcing full rebuild"
fi
else
echo "manifest_valid=false" >> "$GITHUB_OUTPUT"
fi
if [[ -n "$SOURCE_COMMIT" ]]; then
echo "🧭 Last published dev driver release source commit: $SOURCE_COMMIT"
else
echo "🧭 Unable to resolve published dev driver release source commit; fallback to push diff base"
fi
- name: Detect changed driver agents
id: detect
shell: bash
run: |
set -euo pipefail
merge_csv() {
local current="$1"
local incoming="$2"
local merged="$current"
local seen=",${current},"
local item
IFS=',' read -r -a items <<< "$incoming"
for item in "${items[@]}"; do
item="$(printf '%s' "$item" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
if [[ -z "$item" ]]; then
continue
fi
if [[ "$seen" == *",$item,"* ]]; then
continue
fi
if [[ -n "$merged" ]]; then
merged="${merged},${item}"
else
merged="$item"
fi
seen="${seen}${item},"
done
printf '%s\n' "$merged"
}
platform_output_key() {
case "$1" in
darwin/amd64) echo "darwin_amd64" ;;
darwin/arm64) echo "darwin_arm64" ;;
windows/amd64) echo "windows_amd64" ;;
windows/arm64) echo "windows_arm64" ;;
linux/amd64) echo "linux_amd64" ;;
linux/arm64) echo "linux_arm64" ;;
*)
echo "unknown platform: $1" >&2
return 1
;;
esac
}
BASE_REF="${{ steps.published_source.outputs.source_commit }}"
HAS_MANIFEST="${{ steps.published_source.outputs.has_manifest }}"
MANIFEST_VALID="${{ steps.published_source.outputs.manifest_valid }}"
if [[ "$HAS_MANIFEST" != "true" ]]; then
echo "⚠️ Published driver release lacks revision manifest; forcing full rebuild to self-heal old assets"
BASE_REF="all"
elif [[ "$MANIFEST_VALID" != "true" ]]; then
echo "⚠️ Published driver release manifest is stale; forcing full rebuild to self-heal old assets"
BASE_REF="all"
fi
if [[ -n "$BASE_REF" ]]; then
if git rev-parse --verify "${BASE_REF}^{commit}" >/dev/null 2>&1 && git merge-base --is-ancestor "$BASE_REF" "$GITHUB_SHA"; then
echo "🧭 Using last published driver release source commit as detection base: $BASE_REF"
else
echo "⚠️ Published driver release source commit is unavailable or not an ancestor of $GITHUB_SHA: $BASE_REF"
BASE_REF=""
fi
fi
if [[ -z "$BASE_REF" ]]; then
echo "⚠️ Falling back to full driver rebuild because published driver release source commit is unavailable"
BASE_REF="all"
fi
echo "🧭 Final driver detection base: $BASE_REF"
FORCE_GLOBAL_DRIVER_BUILDS="$(bash ./tools/should-force-global-driver-builds.sh --base "$BASE_REF" --head "$GITHUB_SHA")"
DRIVERS="$(bash ./tools/detect-changed-driver-agents.sh --base "$BASE_REF" --head "$GITHUB_SHA")"
PLATFORM_OUTPUTS=()
PLATFORM_DIFF_FAILED=false
if [[ "$BASE_REF" != "all" && "$FORCE_GLOBAL_DRIVER_BUILDS" != "true" ]]; then
REVISION_DRIVERS=""
for PLATFORM in darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64; do
PLATFORM_KEY="$(platform_output_key "$PLATFORM")"
DIFF_ARGS=(bash ./tools/diff-driver-agent-revisions.sh --base "$BASE_REF" --head "$GITHUB_SHA" --platform "$PLATFORM")
if [[ -n "$DRIVERS" ]]; then
DIFF_ARGS+=(--drivers "$DRIVERS")
fi
PLATFORM_DRIVERS="$("${DIFF_ARGS[@]}")" || {
echo "⚠️ 平台 revision 差异对比失败(${PLATFORM}),保守回退为全量驱动重建"
DRIVERS="$(bash ./tools/detect-changed-driver-agents.sh --base all --head "$GITHUB_SHA")"
REVISION_DRIVERS=""
PLATFORM_DIFF_FAILED=true
PLATFORM_OUTPUTS=()
break
}
PLATFORM_OUTPUTS+=("drivers_${PLATFORM_KEY}=${PLATFORM_DRIVERS}")
REVISION_DRIVERS="$(merge_csv "$REVISION_DRIVERS" "$PLATFORM_DRIVERS")"
done
if [[ "$PLATFORM_DIFF_FAILED" == "false" && -n "$REVISION_DRIVERS" ]]; then
echo "🧭 Revision diff union drivers: $REVISION_DRIVERS"
DRIVERS="$(merge_csv "$DRIVERS" "$REVISION_DRIVERS")"
fi
fi
if [[ "$BASE_REF" == "all" || "$FORCE_GLOBAL_DRIVER_BUILDS" == "true" || "$PLATFORM_DIFF_FAILED" == "true" ]]; then
PLATFORM_OUTPUTS=()
for PLATFORM in darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64; do
PLATFORM_KEY="$(platform_output_key "$PLATFORM")"
PLATFORM_OUTPUTS+=("drivers_${PLATFORM_KEY}=${DRIVERS}")
done
elif [[ ${#PLATFORM_OUTPUTS[@]} -eq 0 ]]; then
for PLATFORM in darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64 linux/arm64; do
PLATFORM_KEY="$(platform_output_key "$PLATFORM")"
PLATFORM_OUTPUTS+=("drivers_${PLATFORM_KEY}=")
done
fi
echo "drivers=${DRIVERS}" >> "$GITHUB_OUTPUT"
echo "compare_base=${BASE_REF}" >> "$GITHUB_OUTPUT"
echo "force_global_driver_builds=${FORCE_GLOBAL_DRIVER_BUILDS}" >> "$GITHUB_OUTPUT"
for OUTPUT_LINE in "${PLATFORM_OUTPUTS[@]}"; do
echo "$OUTPUT_LINE" >> "$GITHUB_OUTPUT"
done
if [ -n "$DRIVERS" ]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "🧭 Changed driver agents: $DRIVERS"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "🧭 No driver-agent changes detected"
fi
if [[ "$FORCE_GLOBAL_DRIVER_BUILDS" == "true" ]]; then
echo "🧭 Driver build/release plumbing changed; preserve global driver rebuild set on every platform"
fi
echo "release_source=dev-latest" >> "$GITHUB_OUTPUT"
build:
name: Build ${{ matrix.platform }}
needs:
- frontend
- driver_agents
runs-on: ${{ matrix.os }}
strategy:
fail-fast: false
matrix:
include:
- os: macos-latest
platform: darwin/amd64
os_name: MacOS
arch_name: Amd64
build_name: gonavi-build-darwin-amd64
wails_tags: ""
artifact_suffix: ""
build_optional_agents: true
linux_webkit: ""
- os: macos-latest
platform: darwin/arm64
os_name: MacOS
arch_name: Arm64
build_name: gonavi-build-darwin-arm64
wails_tags: ""
artifact_suffix: ""
build_optional_agents: true
linux_webkit: ""
- os: windows-2025-vs2026
platform: windows/amd64
os_name: Windows
arch_name: Amd64
build_name: gonavi-build-windows-amd64
wails_tags: ""
artifact_suffix: ""
build_optional_agents: true
linux_webkit: ""
- os: windows-2025-vs2026
platform: windows/arm64
os_name: Windows
arch_name: Arm64
build_name: gonavi-build-windows-arm64
wails_tags: ""
artifact_suffix: ""
build_optional_agents: true
linux_webkit: ""
- os: ubuntu-22.04
platform: linux/amd64
os_name: Linux
arch_name: Amd64
build_name: gonavi-build-linux-amd64
wails_tags: ""
artifact_suffix: ""
build_optional_agents: true
linux_webkit: "4.0"
- os: ubuntu-22.04-arm
platform: linux/arm64
os_name: Linux
arch_name: Arm64
build_name: gonavi-build-linux-arm64
wails_tags: ""
artifact_suffix: ""
build_optional_agents: true
linux_webkit: "4.0"
- os: ubuntu-24.04
platform: linux/amd64
os_name: Linux
arch_name: Amd64
build_name: gonavi-build-linux-amd64-webkit41
wails_tags: "webkit2_41"
artifact_suffix: "-WebKit41"
build_optional_agents: false
linux_webkit: "4.1"
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Download frontend dist
id: download_frontend_dist
continue-on-error: true
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: frontend-artifact
- name: Reset failed frontend dist download
if: steps.download_frontend_dist.outcome != 'success'
shell: bash
run: rm -rf frontend-artifact
- name: Retry frontend dist download
if: steps.download_frontend_dist.outcome != 'success'
uses: actions/download-artifact@v7
with:
name: frontend-dist
path: frontend-artifact
- name: Extract frontend dist
shell: bash
run: |
set -euo pipefail
mkdir -p frontend/dist
test -s frontend-artifact/frontend-dist.tar
tar -xf frontend-artifact/frontend-dist.tar -C frontend/dist
test -s frontend/dist/index.html
- name: Install WiX Toolset (Windows)
if: contains(matrix.platform, 'windows')
shell: pwsh
run: |
$wixVersion = "6.0.2"
$wixTools = Join-Path $env:RUNNER_TEMP "wix-tools"
dotnet tool install wix --tool-path $wixTools --version $wixVersion
if ($LASTEXITCODE -ne 0) {
throw "WiX Toolset 安装失败,退出码: $LASTEXITCODE"
}
$wixTools | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
$env:PATH = "$wixTools;$env:PATH"
wix extension add --global "WixToolset.UI.wixext/$wixVersion"
if ($LASTEXITCODE -ne 0) {
throw "WiX UI 扩展安装失败,退出码: $LASTEXITCODE"
}
wix --version
- name: Install Linux Dependencies
if: contains(matrix.platform, 'linux')
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev
if [ "${{ matrix.linux_webkit }}" = "4.1" ]; then
sudo apt-get install -y libwebkit2gtk-4.1-dev libsoup-3.0-dev
else
sudo apt-get install -y libwebkit2gtk-4.0-dev
fi
sudo apt-get install -y upx-ucl || sudo apt-get install -y upx
upx --version
sudo apt-get install -y libfuse2 || sudo apt-get install -y libfuse2t64 || true
if [ "${{ matrix.arch_name }}" != "Amd64" ]; then
echo "⚠️ Linux arm64 暂不生成 AppImage仅产出 tar.gz"
touch /tmp/skip-appimage
exit 0
fi
LINUXDEPLOY_URL="https://github.com/linuxdeploy/linuxdeploy/releases/download/continuous/linuxdeploy-x86_64.AppImage"
PLUGIN_URL="https://github.com/linuxdeploy/linuxdeploy-plugin-gtk/releases/download/continuous/linuxdeploy-plugin-gtk-x86_64.AppImage"
echo "📥 下载 linuxdeploy..."
wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries=3 \
-O /tmp/linuxdeploy "$LINUXDEPLOY_URL" || {
echo "⚠️ linuxdeploy 下载失败AppImage 打包将跳过"
touch /tmp/skip-appimage
}
echo "📥 下载 linuxdeploy-plugin-gtk..."
wget --retry-connrefused --waitretry=1 --read-timeout=20 --timeout=15 --tries=3 \
-O /tmp/linuxdeploy-plugin-gtk "$PLUGIN_URL" || {
echo "⚠️ linuxdeploy-plugin-gtk 下载失败AppImage 打包将跳过"
touch /tmp/skip-appimage
}
if [ ! -f /tmp/skip-appimage ]; then
chmod +x /tmp/linuxdeploy /tmp/linuxdeploy-plugin-gtk
echo "✅ AppImage 工具准备完成"
fi
- name: Install Wails
run: go install -v github.com/wailsapp/wails/v2/cmd/wails@v2.11.0
- name: Setup MSYS2 Toolchain For DuckDB (Windows AMD64)
id: msys2_duckdb
if: ${{ matrix.platform == 'windows/amd64' }}
continue-on-error: true
uses: msys2/setup-msys2@v2
with:
msystem: UCRT64
update: true
install: >-
mingw-w64-ucrt-x86_64-gcc
mingw-w64-ucrt-x86_64-binutils
- name: Configure DuckDB CGO Toolchain (Windows AMD64)
if: ${{ matrix.platform == 'windows/amd64' }}
shell: pwsh
run: |
function Find-MingwBin([string[]]$candidates) {
foreach ($bin in $candidates) {
if ([string]::IsNullOrWhiteSpace($bin)) {
continue
}
$gcc = Join-Path $bin 'gcc.exe'
$gxx = Join-Path $bin 'g++.exe'
if ((Test-Path $gcc) -and (Test-Path $gxx)) {
return $bin
}
}
return $null
}
$msys2Outcome = "${{ steps.msys2_duckdb.outcome }}"
$msys2Location = "${{ steps.msys2_duckdb.outputs['msys2-location'] }}"
$candidateBins = @()
if (-not [string]::IsNullOrWhiteSpace($msys2Location)) {
$candidateBins += Join-Path $msys2Location 'ucrt64\bin'
}
$candidateBins += @(
'C:\msys64\ucrt64\bin',
'D:\a\_temp\msys64\ucrt64\bin'
)
$candidateBins = @($candidateBins | Select-Object -Unique)
$mingwBin = Find-MingwBin $candidateBins
if (-not $mingwBin) {
if ($msys2Outcome -ne 'success') {
Write-Warning "⚠️ MSYS2 安装步骤结果为 $msys2Outcome回退到 UCRT64 本机路径探测"
} else {
Write-Warning "⚠️ MSYS2 已执行,但未找到 UCRT64 gcc/g++,回退到本机路径探测"
}
$mingwBin = Find-MingwBin $candidateBins
}
if (-not $mingwBin) {
Write-Error "❌ 未找到可用的 DuckDB UCRT64 编译器。已检查:$($candidateBins -join ', ')"
exit 1
}
$gcc = (Join-Path $mingwBin 'gcc.exe')
$gxx = (Join-Path $mingwBin 'g++.exe')
if (!(Test-Path $gcc) -or !(Test-Path $gxx)) {
Write-Error "❌ DuckDB 编译器缺失gcc=$gcc g++=$gxx"
exit 1
}
"$mingwBin" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
"CC=$gcc" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
"CXX=$gxx" | Out-File -FilePath $env:GITHUB_ENV -Append -Encoding utf8
Write-Host "✅ 已配置 DuckDB cgo 编译器: gcc=$gcc g++=$gxx"
- name: Verify DuckDB CGO Toolchain (Windows AMD64)
if: ${{ matrix.platform == 'windows/amd64' }}
shell: pwsh
run: |
& "$env:CC" --version
& "$env:CXX" --version
- name: Test Windows Online Updater (Windows AMD64)
if: ${{ matrix.platform == 'windows/amd64' }}
shell: pwsh
run: go test . ./internal/app -run '^(TestShouldEnableWindowsMSISingleInstanceOnlyForInstalledMainGUI|TestPrimaryWindowActivatorQueuesRequestsUntilRuntimeStartup|TestAcquireWindowsMSISingleInstance|TestResolveWindowsUpdateMaintenanceName|TestAcquireWindowsUpdateMaintenance|TestPrepareWindowsUpdateHandoff|TestWindowsPowerShellUpdater|TestBuildWindowsLaunchCommandUsesHiddenPowerShellFile|TestExpectedAssetNameForExecutableUsesWindowsPortableSuffix|TestShouldWindowsUpdateLaunchDownloadedAssetDirectly|TestResolveUpdateInstallMode|TestExpectedAssetNameForWindowsInstallMode|TestPortableUpdatePackageAcceptsZipAndLegacyExe|TestResolveReusableStagedUpdateForPlatformReusesPortableZipInsideStagedDir|TestResolveWindowsUpdateFinalTargetPathMapsDownloadedPortableZipToExe|TestResolveUpdateWorkspaceDirForPlatformSeparatesMSIFromInstallDirectory|TestValidateUpdatePackageForCurrentInstallMode|TestWindowsUpdateRequiresExplicitCloseConfirmation|TestInstallUpdateAndRestartRequiresCloseConfirmationOnWindows|TestInstallUpdateAndRestartSkipsCloseConfirmationForSingleWindowsInstance|TestInstallUpdateAndRestartMSI|TestFindOtherWindowsUpdateInstances|TestCloseWindowsUpdateInstances|TestInstallUpdateAndRestartClosesOtherTargetInstances|TestBuildWindowsMSIUpdatePowerShellScript|TestBuildWindowsMSILaunchCommand|TestResolveWindowsMSIExecPath)' -count=1 -timeout=3m
# ---- 生成 dev 版本号 ----
- name: Generate Dev Version
id: version
shell: bash
run: |
SHORT_SHA=$(git rev-parse --short HEAD)
DEV_VERSION="dev-${SHORT_SHA}"
echo "version=${DEV_VERSION}" >> "$GITHUB_OUTPUT"
echo "📌 Dev 版本号: ${DEV_VERSION}"
- name: Build
shell: bash
run: |
set -euo pipefail
DEV_VERSION="${{ steps.version.outputs.version }}"
echo "🧭 为 ${{ matrix.platform }} 全量生成 driver-agent revision 指纹,避免跨平台沿用旧 revision"
./tools/generate-driver-agent-revisions.sh --platform "${{ matrix.platform }}"
REVISION_HASH="$(python3 - <<'PY'
import hashlib
from pathlib import Path
print(hashlib.sha256(Path("internal/db/driver_agent_revisions_gen.go").read_bytes()).hexdigest())
PY
)"
export GOCACHE="${RUNNER_TEMP}/go-build-${{ matrix.os_name }}-${{ matrix.arch_name }}-${REVISION_HASH}"
mkdir -p "$GOCACHE"
echo "🧭 使用隔离 GOCACHE$GOCACHE"
if [ -n "${{ matrix.wails_tags }}" ]; then
wails build -s -skipbindings -trimpath -platform ${{ matrix.platform }} -clean -o ${{ matrix.build_name }} -tags "${{ matrix.wails_tags }}" -ldflags "-s -w -X GoNavi-Wails/internal/app.AppVersion=${DEV_VERSION}"
else
wails build -s -skipbindings -trimpath -platform ${{ matrix.platform }} -clean -o ${{ matrix.build_name }} -ldflags "-s -w -X GoNavi-Wails/internal/app.AppVersion=${DEV_VERSION}"
fi
- name: Select Platform Driver Set
id: platform_drivers
if: ${{ matrix.build_optional_agents && needs.driver_agents.outputs.has_changes == 'true' }}
shell: bash
run: |
set -euo pipefail
case "${{ matrix.platform }}" in
darwin/amd64)
DRIVERS='${{ needs.driver_agents.outputs.drivers_darwin_amd64 }}'
;;
darwin/arm64)
DRIVERS='${{ needs.driver_agents.outputs.drivers_darwin_arm64 }}'
;;
windows/amd64)
DRIVERS='${{ needs.driver_agents.outputs.drivers_windows_amd64 }}'
;;
windows/arm64)
DRIVERS='${{ needs.driver_agents.outputs.drivers_windows_arm64 }}'
;;
linux/amd64)
DRIVERS='${{ needs.driver_agents.outputs.drivers_linux_amd64 }}'
;;
linux/arm64)
DRIVERS='${{ needs.driver_agents.outputs.drivers_linux_arm64 }}'
;;
*)
echo "unsupported platform: ${{ matrix.platform }}" >&2
exit 1
;;
esac
echo "drivers=${DRIVERS}" >> "$GITHUB_OUTPUT"
if [[ -n "$DRIVERS" ]]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
echo "🧭 当前平台最终需要重建的 driver agents: ${DRIVERS}"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
echo "🧭 当前平台无需重建 driver agents"
fi
- name: Build Optional Driver Agents
if: ${{ matrix.build_optional_agents && steps.platform_drivers.outputs.has_changes == 'true' }}
shell: bash
env:
CHANGED_DRIVER_AGENTS: ${{ steps.platform_drivers.outputs.drivers }}
run: |
set -euo pipefail
TARGET_PLATFORM="${{ matrix.platform }}"
GOOS="${TARGET_PLATFORM%%/*}"
GOARCH="${TARGET_PLATFORM##*/}"
REVISION_HASH="$(python3 - <<'PY'
import hashlib
from pathlib import Path
print(hashlib.sha256(Path("internal/db/driver_agent_revisions_gen.go").read_bytes()).hexdigest())
PY
)"
export GOCACHE="${RUNNER_TEMP}/go-build-${{ matrix.os_name }}-${{ matrix.arch_name }}-${REVISION_HASH}"
mkdir -p "$GOCACHE"
echo "🧭 可选驱动使用隔离 GOCACHE$GOCACHE"
normalize_driver_token() {
local value
value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]' | tr -d '[:space:]')"
case "$value" in
"") return 1 ;;
doris|diros) echo "doris" ;;
open_gauss|open-gauss) echo "opengauss" ;;
gaussdb|gauss_db|gauss-db) echo "gaussdb" ;;
elastic|elasticsearch) echo "elasticsearch" ;;
mariadb|oceanbase|starrocks|sphinx|sqlserver|sqlite|duckdb|dameng|kingbase|highgo|vastbase|opengauss|gaussdb|iris|mongodb|tdengine|iotdb|clickhouse|trino)
echo "$value"
;;
*)
echo "❌ 不支持的 driver-agent${1:-}" >&2
return 1
;;
esac
}
build_driver_name() {
case "$1" in
doris) echo "diros" ;;
*) echo "$1" ;;
esac
}
declare -a RAW_DRIVERS=()
declare -a DRIVERS=()
IFS=',' read -r -a RAW_DRIVERS <<< "$CHANGED_DRIVER_AGENTS"
for RAW_DRIVER in "${RAW_DRIVERS[@]}"; do
DRIVER="$(normalize_driver_token "$RAW_DRIVER")" || continue
DRIVERS+=("$DRIVER")
done
if [ ${#DRIVERS[@]} -eq 0 ]; then
echo "🧭 没有需要构建的 driver-agent"
exit 0
fi
NORMALIZED_CHANGED_DRIVER_AGENTS="$(IFS=,; echo "${DRIVERS[*]}")"
echo "🧭 归一后的 driver-agent 构建列表:${NORMALIZED_CHANGED_DRIVER_AGENTS}"
OUTDIR="drivers/${{ matrix.os_name }}"
mkdir -p "$OUTDIR"
DUCKDB_WINDOWS_LIBRARY_VERSION="v1.4.4"
DUCKDB_WINDOWS_LIBRARY_URL="https://github.com/duckdb/duckdb/releases/download/${DUCKDB_WINDOWS_LIBRARY_VERSION}/libduckdb-windows-amd64.zip"
prepare_duckdb_windows_library() {
local lib_dir="$RUNNER_TEMP/duckdb-windows-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
local extract_dir="$RUNNER_TEMP/duckdb-windows-extract-${DUCKDB_WINDOWS_LIBRARY_VERSION}"
local zip_path="$RUNNER_TEMP/libduckdb-windows-amd64.zip"
if [ -f "$lib_dir/duckdb.dll" ] && [ -f "$lib_dir/libduckdb.dll.a" ] && [ -f "$lib_dir/libduckdb.a" ]; then
echo "$lib_dir"
return 0
fi
mkdir -p "$lib_dir"
rm -rf "$extract_dir"
rm -f "$zip_path"
local attempt dll_path
for attempt in 1 2 3; do
echo "📥 下载 DuckDB Windows 动态库 (${attempt}/3): ${DUCKDB_WINDOWS_LIBRARY_URL}" >&2
if curl --retry 3 --retry-delay 2 --retry-all-errors --connect-timeout 20 --max-time 300 -fsSL "$DUCKDB_WINDOWS_LIBRARY_URL" -o "$zip_path"; then
if unzip -tq "$zip_path" >/dev/null 2>&1; then
rm -rf "$lib_dir"
rm -rf "$extract_dir"
mkdir -p "$lib_dir"
mkdir -p "$extract_dir"
unzip -qo "$zip_path" -d "$extract_dir"
dll_path="$(find "$extract_dir" -type f -name duckdb.dll | head -n 1 || true)"
if [ -n "$dll_path" ]; then
cp "$dll_path" "$lib_dir/duckdb.dll"
go run ./cmd/mingw-import-lib \
--dll "$lib_dir/duckdb.dll" \
--output-lib "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/libduckdb.dll.a" "$lib_dir/libduckdb.a"
rm -rf "$extract_dir"
echo "$lib_dir"
return 0
fi
echo "⚠️ DuckDB Windows 动态库压缩包缺少 duckdb.dll准备重试" >&2
else
echo "⚠️ DuckDB Windows 动态库压缩包校验失败,准备重试" >&2
fi
else
echo "⚠️ DuckDB Windows 动态库下载失败,准备重试" >&2
fi
rm -f "$zip_path"
rm -rf "$lib_dir"
rm -rf "$extract_dir"
mkdir -p "$lib_dir"
sleep $((attempt * 2))
done
echo "❌ 无法准备 DuckDB Windows 动态库:${DUCKDB_WINDOWS_LIBRARY_URL}" >&2
return 1
}
for DRIVER in "${DRIVERS[@]}"; do
BUILD_DRIVER="$(build_driver_name "$DRIVER")"
if [ "$DRIVER" = "duckdb" ] && [ "$GOOS" = "windows" ] && [ "$GOARCH" != "amd64" ]; then
echo "⚠️ 跳过 DuckDB driver当前平台 ${GOOS}/${GOARCH} 不受支持,仅支持 windows/amd64"
continue
fi
DRIVER_VARIANTS=("")
if [ "$DRIVER" = "mongodb" ]; then
DRIVER_VARIANTS=("v1" "v2" "")
fi
for DRIVER_VARIANT in "${DRIVER_VARIANTS[@]}"; do
TAG="gonavi_${BUILD_DRIVER}_driver"
BUILD_TAGS="$TAG"
OUTPUT_STEM="${DRIVER}-driver-agent"
if [ "$DRIVER" = "mongodb" ]; then
if [ "$DRIVER_VARIANT" = "v1" ]; then
BUILD_TAGS="gonavi_mongodb_driver_v1"
OUTPUT_STEM="mongodb-driver-agent-v1"
elif [ "$DRIVER_VARIANT" = "v2" ]; then
BUILD_TAGS="gonavi_mongodb_driver"
OUTPUT_STEM="mongodb-driver-agent-v2"
else
BUILD_TAGS="gonavi_mongodb_driver"
fi
fi
OUTPUT="${OUTPUT_STEM}-${GOOS}-${GOARCH}"
if [ "$GOOS" = "windows" ]; then
OUTPUT="${OUTPUT}.exe"
fi
OUTPUT_PATH="${OUTDIR}/${OUTPUT}"
DUCKDB_LIB_DIR=""
if [ "$DRIVER" = "duckdb" ] && [ "$GOOS" = "windows" ] && [ "$GOARCH" = "amd64" ]; then
DUCKDB_LIB_DIR="$(prepare_duckdb_windows_library)"
BUILD_TAGS="${BUILD_TAGS} duckdb_use_lib"
fi
echo "🔧 构建 ${OUTPUT_PATH} (tags=${BUILD_TAGS})"
if [ "$DRIVER" = "duckdb" ]; then
if [ -n "$DUCKDB_LIB_DIR" ]; then
DUCKDB_LIB_DIR_GCC="$(cygpath -m "$DUCKDB_LIB_DIR")"
DUCKDB_LIB_DIR_PATH="$(cygpath -u "$DUCKDB_LIB_DIR")"
# cgo 会把每个 CGO_LDFLAGS 片段转成 //go:cgo_ldflag-L 参数不能再额外包引号。
DUCKDB_WINDOWS_CGO_LDFLAGS="-L${DUCKDB_LIB_DIR_GCC} -lduckdb -lstdc++ -lm -lws2_32 -lwsock32 -lrstrtmgr"
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CGO_LDFLAGS="${DUCKDB_WINDOWS_CGO_LDFLAGS}" PATH="${DUCKDB_LIB_DIR_PATH}:$PATH" go build \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
cp "$DUCKDB_LIB_DIR/duckdb.dll" "$OUTDIR/duckdb.dll"
bash ./tools/compress-driver-artifact.sh "$OUTDIR/duckdb.dll" "$TARGET_PLATFORM" "${{ matrix.os_name }}/duckdb.dll"
else
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
fi
else
CGO_ENABLED=0 GOOS="$GOOS" GOARCH="$GOARCH" go build \
-tags "${BUILD_TAGS}" \
-trimpath \
-ldflags "-s -w" \
-o "${OUTPUT_PATH}" \
./cmd/optional-driver-agent
fi
bash ./tools/compress-driver-artifact.sh "${OUTPUT_PATH}" "$TARGET_PLATFORM" "${{ matrix.os_name }}/${OUTPUT}"
if [ "$DRIVER" = "duckdb" ] && [ -n "$DUCKDB_LIB_DIR" ]; then
DUCKDB_ZIP_PATH="${OUTDIR}/duckdb-driver.zip"
export DUCKDB_ZIP_PATH
export DUCKDB_AGENT_PATH="${OUTPUT_PATH}"
export DUCKDB_DLL_PATH="${OUTDIR}/duckdb.dll"
python3 - <<'PY'
import os
import zipfile
zip_path = os.environ["DUCKDB_ZIP_PATH"]
entries = [
("Windows/duckdb-driver-agent-windows-amd64.exe", os.environ["DUCKDB_AGENT_PATH"]),
("Windows/duckdb.dll", os.environ["DUCKDB_DLL_PATH"]),
]
with zipfile.ZipFile(zip_path, "w", compression=zipfile.ZIP_DEFLATED) as zf:
for arcname, src in entries:
if not os.path.isfile(src):
raise FileNotFoundError(src)
zf.write(src, arcname)
PY
echo "📦 已生成 DuckDB Windows 专属驱动包: ${DUCKDB_ZIP_PATH}"
fi
done
done
bash ./tools/verify-driver-agent-revisions.sh \
--assets-dir drivers \
--platform "$TARGET_PLATFORM" \
--drivers "$NORMALIZED_CHANGED_DRIVER_AGENTS"
mkdir -p driver-provenance
python3 ./tools/generate-driver-release-manifest.py \
--assets-dir drivers \
--provenance-output "driver-provenance/${GOOS}-${GOARCH}.json"
# macOS Packaging
- name: Package macOS DMG
if: contains(matrix.platform, 'darwin')
run: |
brew install create-dmg
VERSION="${{ steps.version.outputs.version }}"
cd build/bin
APP_PATH=$(find . -maxdepth 1 -name "*.app" | head -n 1)
if [ -z "$APP_PATH" ]; then
echo "❌ 未找到 .app 应用包!"
exit 1
fi
APP_NAME=$(basename "$APP_PATH")
mkdir -p "$APP_NAME/Contents/Resources"
cp ../../LICENSE "$APP_NAME/Contents/Resources/LICENSE"
cp ../../NOTICE "$APP_NAME/Contents/Resources/NOTICE"
APP_BIN=$(find "$APP_PATH/Contents/MacOS" -maxdepth 1 -type f | head -n 1)
if [ -z "$APP_BIN" ]; then
echo "❌ 未找到 macOS 应用主程序!"
exit 1
fi
echo " macOS 产物不执行 UPX 压缩,保留原始主程序。"
echo "🔏 正在进行 Ad-hoc 签名..."
if command -v xattr >/dev/null 2>&1; then
xattr -cr "$APP_NAME" || true
fi
codesign --force --deep --sign - "$APP_NAME"
DMG_NAME="${{ matrix.build_name }}.dmg"
FINAL_NAME="GoNavi-${VERSION}-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.dmg"
echo "📦 正在生成 DMG: $DMG_NAME..."
create-dmg \
--volname "GoNavi Dev Build" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "$APP_NAME" 200 190 \
--hide-extension "$APP_NAME" \
--app-drop-link 600 185 \
"$DMG_NAME" \
"$APP_NAME"
VERIFY_MOUNT_DIR=$(mktemp -d "${TMPDIR:-/tmp}/gonavi-dev-verify.XXXXXX")
hdiutil attach -nobrowse -readonly -mountpoint "$VERIFY_MOUNT_DIR" "$DMG_NAME" >/dev/null
PACKAGED_APP=$(find "$VERIFY_MOUNT_DIR" -maxdepth 1 -name "*.app" | head -n 1)
if [ -z "$PACKAGED_APP" ]; then
echo "❌ DMG 内未找到 .app 应用包!"
hdiutil detach "$VERIFY_MOUNT_DIR" -quiet >/dev/null 2>&1 || true
exit 1
fi
codesign --verify --deep --strict --verbose=4 "$PACKAGED_APP"
hdiutil detach "$VERIFY_MOUNT_DIR" -quiet >/dev/null 2>&1 || true
mv "$DMG_NAME" "../../$FINAL_NAME"
# Windows Packaging
- name: Package Windows Portable ZIP, bridge EXE, and MSI
if: contains(matrix.platform, 'windows')
shell: pwsh
run: |
Set-Location build/bin
$version = "${{ steps.version.outputs.version }}"
$target = "${{ matrix.build_name }}"
$archName = "${{ matrix.arch_name }}"
$portableExeName = "GoNavi-$version-${{ matrix.os_name }}-$archName${{ matrix.artifact_suffix }}-Portable.exe"
$portableZipName = "GoNavi-$version-${{ matrix.os_name }}-$archName${{ matrix.artifact_suffix }}-Portable.zip"
$msiName = "GoNavi-$version-${{ matrix.os_name }}-$archName${{ matrix.artifact_suffix }}-Installer.msi"
if (Test-Path "$target.exe") {
$finalExe = "$target.exe"
} elseif (Test-Path "$target") {
Rename-Item -Path "$target" -NewName "$target.exe"
$finalExe = "$target.exe"
} else {
Write-Error "❌ 未找到构建产物 '$target'!"
exit 1
}
Write-Host " 保留 Wails 原始 Windows EXE避免可执行文件打包器增加杀毒软件误报。"
$releaseRoot = (Resolve-Path -LiteralPath "..\\..").Path
$licenseFile = (Resolve-Path -LiteralPath "..\\..\\LICENSE").Path
$noticeFile = (Resolve-Path -LiteralPath "..\\..\\NOTICE").Path
Write-Host "📦 输出旧客户端兼容 EXE $portableExeName..."
Copy-Item -LiteralPath $finalExe -Destination (Join-Path $releaseRoot $portableExeName) -Force
$portableDir = Join-Path $env:RUNNER_TEMP "gonavi-portable-$archName"
if (Test-Path -LiteralPath $portableDir) {
Remove-Item -LiteralPath $portableDir -Recurse -Force
}
New-Item -ItemType Directory -Path $portableDir -Force | Out-Null
$portableApp = Join-Path $portableDir "GoNavi.exe"
$portableLicense = Join-Path $portableDir "LICENSE"
$portableNotice = Join-Path $portableDir "NOTICE"
Copy-Item -LiteralPath $finalExe -Destination $portableApp -Force
Copy-Item -LiteralPath $licenseFile -Destination $portableLicense -Force
Copy-Item -LiteralPath $noticeFile -Destination $portableNotice -Force
$portableFiles = @($portableApp, $portableLicense, $portableNotice)
$portableZipPath = Join-Path $releaseRoot $portableZipName
Write-Host "📦 生成普通 Windows Portable ZIP $portableZipName..."
Compress-Archive -LiteralPath $portableFiles -DestinationPath $portableZipPath -CompressionLevel Optimal -Force
if (-not (Test-Path -LiteralPath $portableZipPath -PathType Leaf) -or (Get-Item -LiteralPath $portableZipPath).Length -le 0) {
throw "Windows Portable ZIP 生成失败: $portableZipPath"
}
$runNumber = [int64]$env:GITHUB_RUN_NUMBER
if ($runNumber -le 0 -or $runNumber -gt 65535) {
throw "GitHub run number 无法映射为 MSI 版本号: $runNumber"
}
$productVersion = "255.0.$runNumber"
$wixArch = if ($archName -eq "Arm64") { "arm64" } else { "x64" }
$componentGuid = if ($archName -eq "Arm64") {
"7F4C7757-329F-42B0-B312-D4B8AD50415E"
} else {
"0BCEE70B-9CF2-449C-9ADE-190188493234"
}
$desktopShortcutComponentGuid = if ($archName -eq "Arm64") {
"3CF7DCD2-2658-432D-8677-AB6BB34A9659"
} else {
"114030DD-DB5F-460A-9CAC-997680E9F938"
}
$licenseText = Get-Content -LiteralPath $licenseFile -Raw
$escapedLicense = $licenseText.Replace('\', '\\').Replace('{', '\{').Replace('}', '\}')
$escapedLicense = $escapedLicense -replace "`r?`n", "\par`r`n"
$licenseRtf = Join-Path $env:RUNNER_TEMP "GoNavi-License.rtf"
$rtfContent = "{\rtf1\ansi\deff0{\fonttbl{\f0 Segoe UI;}}\f0\fs18 $escapedLicense}"
Set-Content -LiteralPath $licenseRtf -Value $rtfContent -Encoding ascii
$sourceExe = (Resolve-Path -LiteralPath $finalExe).Path
$wxsPath = (Resolve-Path -LiteralPath "..\\windows\\installer.wxs").Path
$iconPath = (Resolve-Path -LiteralPath "..\\windows\\icon.ico").Path
$installMarkerPath = Join-Path $env:RUNNER_TEMP ".gonavi-msi-install"
Set-Content -LiteralPath $installMarkerPath -Value "MSI" -Encoding ascii -NoNewline
$installMarkerPath = (Resolve-Path -LiteralPath $installMarkerPath).Path
$wixIntermediateDir = Join-Path $env:RUNNER_TEMP "wixobj-$archName"
Write-Host "📦 生成 Windows MSI 安装程序 $msiName (version=$productVersion arch=$wixArch)..."
wix build $wxsPath `
-arch $wixArch `
-ext "WixToolset.UI.wixext/6.0.2" `
-d "ProductName=GoNavi" `
-d "ProductVersion=$productVersion" `
-d "UpgradeCode=CDD6BF2F-ED1E-4345-A0AB-DCDB7E15FB23" `
-d "ComponentGuid=$componentGuid" `
-d "DesktopShortcutComponentGuid=$desktopShortcutComponentGuid" `
-d "InstallFolderName=GoNavi" `
-d "RegistryKeyName=GoNavi" `
-d "SourceExe=$sourceExe" `
-d "InstallMarker=$installMarkerPath" `
-d "IconPath=$iconPath" `
-d "LicenseRtf=$licenseRtf" `
-d "LicenseFile=$licenseFile" `
-d "NoticeFile=$noticeFile" `
-pdbtype none `
-intermediatefolder $wixIntermediateDir `
-out "..\\..\\$msiName"
if ($LASTEXITCODE -ne 0) {
throw "WiX MSI 构建失败,退出码: $LASTEXITCODE"
}
# Linux Packaging
- name: Package Linux
if: contains(matrix.platform, 'linux')
run: |
VERSION="${{ steps.version.outputs.version }}"
cd build/bin
TARGET="${{ matrix.build_name }}"
TAR_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.tar.gz"
APPIMAGE_NAME="GoNavi-$VERSION-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.AppImage"
if [ ! -f "$TARGET" ]; then
echo "❌ 未找到构建产物 '$TARGET'!"
exit 1
fi
chmod +x "$TARGET"
BEFORE_BYTES=$(wc -c <"$TARGET" | tr -d '[:space:]')
echo "🗜️ 正在使用 UPX 压缩 Linux 可执行文件: $TARGET ..."
upx --best --lzma --force "$TARGET"
upx -t "$TARGET"
AFTER_BYTES=$(wc -c <"$TARGET" | tr -d '[:space:]')
if [ "$AFTER_BYTES" -lt "$BEFORE_BYTES" ]; then
SAVED_BYTES=$((BEFORE_BYTES - AFTER_BYTES))
awk -v b="$BEFORE_BYTES" -v a="$AFTER_BYTES" -v s="$SAVED_BYTES" 'BEGIN { printf "✅ Linux UPX 压缩完成:%.2fMB -> %.2fMB,减少 %.2fMB\n", b/1024/1024, a/1024/1024, s/1024/1024 }'
else
awk -v b="$BEFORE_BYTES" -v a="$AFTER_BYTES" 'BEGIN { printf " Linux UPX 压缩完成:%.2fMB -> %.2fMB\n", b/1024/1024, a/1024/1024 }'
fi
echo "📦 正在打包 $TAR_NAME..."
tar -czvf "$TAR_NAME" "$TARGET" -C ../.. LICENSE NOTICE
mv "$TAR_NAME" ../../
if [ -f /tmp/skip-appimage ]; then
echo "⚠️ 跳过 AppImage 打包"
exit 0
fi
echo "📦 正在生成 AppImage..."
mkdir -p AppDir/usr/bin
mkdir -p AppDir/usr/share/applications
mkdir -p AppDir/usr/share/doc/gonavi
mkdir -p AppDir/usr/share/icons/hicolor/256x256/apps
cp "$TARGET" AppDir/usr/bin/gonavi
cp ../../LICENSE AppDir/usr/share/doc/gonavi/LICENSE
cp ../../NOTICE AppDir/usr/share/doc/gonavi/NOTICE
printf '%s\n' \
'[Desktop Entry]' \
'Name=GoNavi' \
'Exec=gonavi' \
'Icon=gonavi' \
'Type=Application' \
'Categories=Development;Database;' \
'Comment=Database Management Tool' \
> AppDir/usr/share/applications/gonavi.desktop
cp AppDir/usr/share/applications/gonavi.desktop AppDir/gonavi.desktop
if [ -f "../../build/appicon.png" ]; then
cp "../../build/appicon.png" AppDir/usr/share/icons/hicolor/256x256/apps/gonavi.png
cp "../../build/appicon.png" AppDir/gonavi.png
else
convert -size 256x256 xc:#336791 -fill white -gravity center -pointsize 48 -annotate 0 "GoNavi" AppDir/gonavi.png || \
wget -q "https://via.placeholder.com/256/336791/FFFFFF?text=GoNavi" -O AppDir/gonavi.png || \
touch AppDir/gonavi.png
cp AppDir/gonavi.png AppDir/usr/share/icons/hicolor/256x256/apps/gonavi.png
fi
export DEPLOY_GTK_VERSION=3
/tmp/linuxdeploy --appdir AppDir --plugin gtk --output appimage || {
echo "⚠️ AppImage 生成失败,但 tar.gz 已成功生成"
exit 0
}
mv GoNavi*.AppImage "$APPIMAGE_NAME" 2>/dev/null || {
echo "⚠️ AppImage 重命名失败"
exit 0
}
if [ -f "$APPIMAGE_NAME" ]; then
mv "$APPIMAGE_NAME" ../../
echo "✅ AppImage 生成成功"
fi
- name: Upload Artifact
uses: actions/upload-artifact@v6
with:
name: dev-build-artifacts-${{ strategy.job-index }}
path: |
GoNavi-*.dmg
GoNavi-*.exe
GoNavi-*.zip
GoNavi-*.msi
GoNavi-*.tar.gz
GoNavi-*.AppImage
drivers/**
driver-provenance/**
retention-days: 7
# 汇总所有产物并发布为 Pre-release
release:
name: Publish Dev Pre-release
needs:
- build
- driver_agents
# Serialize only the publication stage with stable releases so both
# channels cannot publish shared mirror targets concurrently.
concurrency:
group: gonavi-release-publication
cancel-in-progress: false
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
with:
# dev 更新日志需要 prev..HEAD 提交范围
fetch-depth: 0
- name: Setup Go
uses: actions/setup-go@v6
with:
go-version-file: 'go.mod'
- name: Download All Artifacts
uses: actions/download-artifact@v7
with:
path: release-assets
pattern: dev-build-artifacts-*
merge-multiple: true
- name: Add legal documents
shell: bash
run: install -m 0644 LICENSE NOTICE release-assets/
- name: List Assets
run: ls -R release-assets
- name: Download Previous Driver Manifest
if: needs.driver_agents.outputs.has_changes == 'true' && needs.driver_agents.outputs.release_source != 'all'
env:
DRIVER_RELEASE_SOURCE: ${{ needs.driver_agents.outputs.release_source }}
shell: bash
run: |
set -euo pipefail
manifest_path="$RUNNER_TEMP/previous-driver-manifest.json"
manifest_url="https://github.com/Syngnat/GoNavi-DriverAgents/releases/download/${DRIVER_RELEASE_SOURCE}/GoNavi-DriverAgents-Manifest.json"
if curl --retry 3 --retry-delay 2 --retry-all-errors -fsSL "$manifest_url" -o "$manifest_path"; then
echo "🧭 已下载上一版 driver manifest 作为复用资产 provenance"
else
rm -f "$manifest_path"
echo "⚠️ 上一版 driver manifest 不可用;未重建资产将由 manifest 生成阶段拒绝"
fi
- name: Complete Driver Agent Assets
if: needs.driver_agents.outputs.has_changes == 'true'
env:
DRIVER_RELEASE_TOKEN: ${{ secrets.DRIVER_RELEASE_TOKEN }}
run: |
python3 tools/complete-driver-release-assets.py \
--assets-dir release-assets \
--source "${{ needs.driver_agents.outputs.release_source }}" \
--require-complete
- name: Package Driver Agents Bundle
id: driver_assets
shell: bash
run: |
set -euo pipefail
cd release-assets
if [ ! -d drivers ]; then
echo "⚠️ 未找到 drivers 目录,跳过驱动总包打包"
echo "has_driver_assets=false" >> "$GITHUB_OUTPUT"
exit 0
fi
if [ -z "$(find drivers -type f 2>/dev/null | head -n 1)" ]; then
echo "⚠️ drivers 目录为空,跳过驱动总包打包"
rm -rf drivers
echo "has_driver_assets=false" >> "$GITHUB_OUTPUT"
exit 0
fi
echo "📦 打包驱动总包GoNavi-DriverAgents.zip"
python3 ../tools/package-driver-release-assets.py \
drivers \
../driver-release-assets
MANIFEST_ARGS=(
--assets-dir drivers
--output ../driver-release-assets/GoNavi-DriverAgents-Manifest.json
)
if [ -d driver-provenance ]; then
MANIFEST_ARGS+=(--provenance driver-provenance)
fi
if [ -s "$RUNNER_TEMP/previous-driver-manifest.json" ]; then
MANIFEST_ARGS+=(--provenance "$RUNNER_TEMP/previous-driver-manifest.json")
fi
python3 ../tools/generate-driver-release-manifest.py "${MANIFEST_ARGS[@]}"
rm -rf drivers driver-provenance
echo "has_driver_assets=true" >> "$GITHUB_OUTPUT"
- name: Generate SHA256SUMS
shell: bash
run: |
cd release-assets
FILES=()
while IFS= read -r file; do
if [ -n "$file" ]; then
case "$file" in
SHA256SUMS|latest.json|latest-dev.json) continue ;;
esac
FILES+=("$file")
fi
done < <(find . -maxdepth 1 -type f ! -name SHA256SUMS -exec basename {} \; | sort)
if [ ${#FILES[@]} -eq 0 ]; then
echo "⚠️ 未找到可签名资产,生成空 SHA256SUMS"
: > SHA256SUMS
else
sha256sum "${FILES[@]}" > SHA256SUMS
fi
- name: Generate Driver SHA256SUMS
if: steps.driver_assets.outputs.has_driver_assets == 'true'
shell: bash
run: |
cd driver-release-assets
FILES=()
while IFS= read -r file; do
if [ -n "$file" ]; then
FILES+=("$file")
fi
done < <(find . -maxdepth 1 -type f ! -name SHA256SUMS -exec basename {} \; | sort)
if [ ${#FILES[@]} -eq 0 ]; then
echo "❌ 未找到驱动发布资产"
exit 1
fi
sha256sum "${FILES[@]}" > SHA256SUMS
- name: Generate Dev Version
id: version
run: |
SHORT_SHA="${GITHUB_SHA:0:7}"
DEV_VERSION="dev-${SHORT_SHA}"
echo "version=${DEV_VERSION}" >> "$GITHUB_OUTPUT"
- name: Format Build Time
id: build_time
shell: bash
run: |
python3 - <<'PY' >> "$GITHUB_OUTPUT"
from datetime import datetime, timezone, timedelta
raw = "${{ github.event.head_commit.timestamp }}"
dt = datetime.fromisoformat(raw)
china_tz = timezone(timedelta(hours=8))
formatted = dt.astimezone(china_tz).strftime("%Y-%m-%d %H:%M:%S")
print(f"display={formatted}")
PY
# 解析上一版 dev 基线(线上 latest-dev.json 的 version → 短 SHA用于生成相对变更日志。
- name: Resolve previous dev baseline
id: prev_dev
shell: bash
run: |
set -euo pipefail
PREV_SHA=""
for url in \
"https://download.syngnat.top/gonavi/dev/releases/latest/latest-dev.json" \
"https://github.com/${{ github.repository }}/releases/download/dev-latest/latest-dev.json"
do
if curl -fsSL --max-time 20 "$url" -o "$RUNNER_TEMP/prev-latest-dev.json"; then
PREV_VERSION="$(jq -r '.version // empty' "$RUNNER_TEMP/prev-latest-dev.json" 2>/dev/null || true)"
if [[ "$PREV_VERSION" =~ ^dev-([0-9a-fA-F]{7,40})$ ]]; then
CANDIDATE="${BASH_REMATCH[1]}"
if git rev-parse --verify "${CANDIDATE}^{commit}" >/dev/null 2>&1; then
PREV_SHA="$(git rev-parse "${CANDIDATE}^{commit}")"
echo "resolved previous dev baseline: version=${PREV_VERSION} sha=${PREV_SHA:0:12} (from ${url})"
break
fi
fi
fi
done
echo "sha=${PREV_SHA}" >> "$GITHUB_OUTPUT"
- name: Generate Dev Changelog
id: changelog
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
shell: bash
run: |
set -euo pipefail
DEV_VERSION="${{ steps.version.outputs.version }}"
BUILD_TIME="${{ steps.build_time.outputs.display }}"
PREV_SHA="${{ steps.prev_dev.outputs.sha }}"
CHANGELOG_FILE="$RUNNER_TEMP/dev-changelog.md"
BODY_FILE="$RUNNER_TEMP/dev-release-body.md"
NOTES_CORE="$RUNNER_TEMP/dev-changelog-core.md"
HEADER_FILE="$RUNNER_TEMP/dev-changelog-header.md"
cat > "$HEADER_FILE" <<EOF
## 🧪 测试版本 (Dev Build)
**版本**: \`${DEV_VERSION}\`
**分支**: \`dev\`
**提交**: [\`${{ github.sha }}\`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})
**构建时间**: ${BUILD_TIME}
> ⚠️ 这是开发测试版本,仅供内部测试使用,不建议用于生产环境。
> 每次 push 到 \`dev\` 分支会自动覆盖此 release。
EOF
if [[ -n "$PREV_SHA" ]]; then
python3 tools/generate-release-notes.py \
--repo "${{ github.repository }}" \
--tag "${{ github.sha }}" \
--previous-tag "$PREV_SHA" \
--max-commits 100 \
--repository-url "${{ github.server_url }}/${{ github.repository }}" \
--output "$NOTES_CORE" || {
echo "warning: generate-release-notes failed; falling back to empty commit list" >&2
echo "(未能生成提交变更列表)" > "$NOTES_CORE"
}
else
cat > "$NOTES_CORE" <<EOF
## 📝 变更说明
- 未能解析上一测试版基线,本次仅发布当前构建。
- 当前提交:\`${{ github.sha }}\`
EOF
fi
{
cat "$HEADER_FILE"
echo "## 更新日志(相对上一测试版)"
echo
cat "$NOTES_CORE"
} > "$BODY_FILE"
# 应用内弹窗使用完整 body含 dev 警告头)
cp "$BODY_FILE" "$CHANGELOG_FILE"
echo "changelog_file=$CHANGELOG_FILE" >> "$GITHUB_OUTPUT"
echo "body_file=$BODY_FILE" >> "$GITHUB_OUTPUT"
echo "📄 dev changelog ready ($(wc -c < "$BODY_FILE") bytes)"
# 静态更新清单dev 通道读
# https://github.com/Syngnat/GoNavi/releases/download/dev-latest/latest-dev.json
- name: Generate static update manifest (latest-dev.json)
shell: bash
run: |
set -euo pipefail
DEV_VERSION="${{ steps.version.outputs.version }}"
CHANGELOG_FILE="${{ steps.changelog.outputs.changelog_file }}"
python3 tools/generate-update-latest-manifest.py \
--assets-dir release-assets \
--version "$DEV_VERSION" \
--tag dev-latest \
--channel dev \
--name "Dev Build (${DEV_VERSION})" \
--download-base-url "https://download.syngnat.top/gonavi/dev/releases/download" \
--download-tag "$DEV_VERSION" \
--release-notes-file "$CHANGELOG_FILE" \
--output release-assets/latest-dev.json
test -s release-assets/latest-dev.json
jq -e '(.releaseNotes | type == "string" and length > 0)' release-assets/latest-dev.json >/dev/null
echo "📄 latest-dev.json ready:"
head -n 40 release-assets/latest-dev.json
# 删除旧的 dev pre-release保持只有最新一个
- name: Reset Previous Dev Release
uses: actions/github-script@v8
with:
github-token: ${{ secrets.GITHUB_TOKEN }}
script: |
const tag = 'dev-latest';
const ref = `tags/${tag}`;
const { owner, repo } = context.repo;
const releases = await github.paginate(github.rest.repos.listReleases, {
owner,
repo,
per_page: 100,
});
const matchedReleases = releases.filter((release) => release.tag_name === tag);
if (matchedReleases.length === 0) {
core.info(`No existing releases found for tag ${tag}`);
} else {
for (const release of matchedReleases) {
core.info(`Deleting release ${release.id} (${release.name || 'unnamed'}) for tag ${tag}`);
await github.rest.repos.deleteRelease({
owner,
repo,
release_id: release.id,
});
}
}
try {
await github.rest.git.deleteRef({
owner,
repo,
ref,
});
core.info(`Deleted ref ${ref}`);
} catch (error) {
const message = String(error.response?.data?.message || error.message || '');
if (error.status === 404 || (error.status === 422 && message.includes('Reference does not exist'))) {
core.info(`No existing ref found for ${ref}`);
} else {
throw error;
}
}
- name: Reset Previous Driver Dev Release
if: steps.driver_assets.outputs.has_driver_assets == 'true'
uses: actions/github-script@v8
with:
github-token: ${{ secrets.DRIVER_RELEASE_TOKEN }}
script: |
const tag = 'dev-latest';
const ref = `tags/${tag}`;
const [owner, repo] = 'Syngnat/GoNavi-DriverAgents'.split('/');
const releases = await github.paginate(github.rest.repos.listReleases, {
owner,
repo,
per_page: 100,
});
const matchedReleases = releases.filter((release) => release.tag_name === tag);
if (matchedReleases.length === 0) {
core.info(`No existing driver releases found for tag ${tag}`);
} else {
for (const release of matchedReleases) {
core.info(`Deleting driver release ${release.id} (${release.name || 'unnamed'}) for tag ${tag}`);
await github.rest.repos.deleteRelease({
owner,
repo,
release_id: release.id,
});
}
}
try {
await github.rest.git.deleteRef({
owner,
repo,
ref,
});
core.info(`Deleted driver ref ${ref}`);
} catch (error) {
const message = String(error.response?.data?.message || error.message || '');
if (error.status === 404 || (error.status === 422 && message.includes('Reference does not exist'))) {
core.info(`No existing driver ref found for ${ref}`);
} else {
throw error;
}
}
- name: Create Dev Driver Agents Pre-release
if: steps.driver_assets.outputs.has_driver_assets == 'true'
env:
GH_TOKEN: ${{ secrets.DRIVER_RELEASE_TOKEN }}
shell: bash
run: |
set -euo pipefail
mapfile -t DRIVER_RELEASE_ASSETS < <(find driver-release-assets -maxdepth 1 -type f | sort)
if [ ${#DRIVER_RELEASE_ASSETS[@]} -eq 0 ]; then
echo "未找到 driver release assets"
exit 1
fi
NOTES_FILE="$RUNNER_TEMP/dev-driver-release-notes.md"
cat > "$NOTES_FILE" <<'EOF'
GoNavi dev driver-agent assets.
**版本**: `${{ steps.version.outputs.version }}`
**来源仓库**: `${{ github.repository }}`
**提交**: [`${{ github.sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})
EOF
gh release create dev-latest "${DRIVER_RELEASE_ASSETS[@]}" \
--repo Syngnat/GoNavi-DriverAgents \
--title "GoNavi Driver Agents (${{ steps.version.outputs.version }})" \
--notes-file "$NOTES_FILE" \
--prerelease \
--latest=false
- name: Create Dev Pre-release
uses: softprops/action-gh-release@v3
with:
tag_name: dev-latest
name: "🧪 Dev Build (${{ steps.version.outputs.version }})"
target_commitish: ${{ github.sha }}
files: release-assets/*
prerelease: true
draft: false
body_path: ${{ steps.changelog.outputs.body_file }}
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- name: Prepare and verify dev mirror payload
env:
DEV_VERSION: ${{ steps.version.outputs.version }}
HAS_DRIVER_ASSETS: ${{ steps.driver_assets.outputs.has_driver_assets }}
shell: bash
run: |
set -euo pipefail
app_dir="release-assets"
driver_dir="driver-release-assets"
jq -e \
--arg version "${DEV_VERSION}" \
--arg mirror_base "https://download.syngnat.top/gonavi/dev/releases/download" \
--arg github_base "https://github.com/${GITHUB_REPOSITORY}/releases/download/dev-latest" \
'
.channel == "dev"
and .tagName == "dev-latest"
and .version == $version
and (.assets | type == "array" and length > 0)
and all(.assets[];
(.url | type == "string" and startswith($mirror_base + "/" + $version + "/"))
and (.apiUrl | type == "string" and startswith($github_base + "/"))
and (.sha256 | type == "string" and test("^[0-9a-fA-F]{64}$"))
)
' "${app_dir}/latest-dev.json" >/dev/null
driver_latest_file="${RUNNER_TEMP}/driver-dev-latest-index.json"
if [[ "${HAS_DRIVER_ASSETS}" == "true" ]]; then
[[ -s "${driver_dir}/SHA256SUMS" ]] || {
echo "DriverAgents SHA256SUMS is missing" >&2
exit 1
}
(cd "${driver_dir}" && sha256sum --check SHA256SUMS)
jq \
--arg tag "dev-latest" \
--arg mirror_tag "${DEV_VERSION}" \
'.tagName = $tag | .mirrorTagName = $mirror_tag | del(.assets["GoNavi-DriverAgents.zip"])' \
"${driver_dir}/GoNavi-DriverAgents-Index.json" \
> "${driver_latest_file}"
jq -e '.assets | type == "object" and length > 0' "${driver_latest_file}" >/dev/null
fi
while IFS=$'\t' read -r name expected_size expected_sha; do
[[ "${name}" == "$(basename "${name}")" ]] || {
echo "Invalid dev app asset name: ${name}" >&2
exit 1
}
file="${app_dir}/${name}"
[[ -f "${file}" ]] || {
echo "Dev app asset is missing: ${name}" >&2
exit 1
}
actual_size="$(stat -c '%s' "${file}")"
actual_sha="$(sha256sum "${file}" | awk '{print $1}')"
if [[ "${actual_size}" != "${expected_size}" || "${actual_sha}" != "${expected_sha,,}" ]]; then
echo "Dev app asset does not match latest-dev.json: ${name}" >&2
exit 1
fi
done < <(jq -r '.assets[] | [.name, (.size | tostring), .sha256] | @tsv' "${app_dir}/latest-dev.json")
if [[ "${HAS_DRIVER_ASSETS}" == "true" ]]; then
while IFS=$'\t' read -r name expected_size; do
[[ "${name}" == "$(basename "${name}")" ]] || {
echo "Invalid dev driver asset name: ${name}" >&2
exit 1
}
file="${driver_dir}/${name}"
[[ -f "${file}" ]] || {
echo "Dev driver asset is missing: ${name}" >&2
exit 1
}
actual_size="$(stat -c '%s' "${file}")"
if [[ "${actual_size}" != "${expected_size}" ]]; then
echo "Dev driver asset size does not match index: ${name}" >&2
exit 1
fi
done < <(jq -r '.assets | to_entries[] | [.key, (.value | tostring)] | @tsv' "${driver_latest_file}")
fi
- name: Mirror dev release to Gatewaysentry
uses: ./.github/actions/publish-vps-mirror
with:
channel: dev
app-tag: ${{ steps.version.outputs.version }}
app-dir: release-assets
app-manifest: release-assets/latest-dev.json
driver-enabled: ${{ steps.driver_assets.outputs.has_driver_assets }}
driver-tag: ${{ steps.version.outputs.version }}
driver-dir: driver-release-assets
driver-version-index: ${{ runner.temp }}/driver-dev-latest-index.json
driver-latest-index: ${{ runner.temp }}/driver-dev-latest-index.json
ssh-host: ${{ secrets.MIRROR_SSH_HOST }}
ssh-port: ${{ secrets.MIRROR_SSH_PORT }}
ssh-user: ${{ secrets.MIRROR_SSH_USER }}
ssh-private-key: ${{ secrets.MIRROR_SSH_PRIVATE_KEY }}
ssh-known-hosts: ${{ secrets.MIRROR_SSH_KNOWN_HOSTS }}