Files
MyGoNavi/.github/workflows/dev-build.yml
2026-06-08 17:14:27 +08:00

1124 lines
45 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: 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') }}
- 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: 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 }}
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: 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"
}
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"
DRIVERS="$(bash ./tools/detect-changed-driver-agents.sh --base "$BASE_REF" --head "$GITHUB_SHA")"
if [[ "$BASE_REF" != "all" ]]; then
REVISION_DRIVERS=""
for PLATFORM in darwin/amd64 darwin/arm64 windows/amd64 windows/arm64 linux/amd64; do
PLATFORM_DRIVERS="$(bash ./tools/diff-driver-agent-revisions.sh --base "$BASE_REF" --head "$GITHUB_SHA" --platform "$PLATFORM")" || {
echo "⚠️ 平台 revision 差异对比失败(${PLATFORM}),保守回退为全量驱动重建"
DRIVERS="$(bash ./tools/detect-changed-driver-agents.sh --base all --head "$GITHUB_SHA")"
REVISION_DRIVERS=""
break
}
REVISION_DRIVERS="$(merge_csv "$REVISION_DRIVERS" "$PLATFORM_DRIVERS")"
done
if [[ -n "$REVISION_DRIVERS" ]]; then
echo "🧭 Revision diff union drivers: $REVISION_DRIVERS"
DRIVERS="$(merge_csv "$DRIVERS" "$REVISION_DRIVERS")"
fi
fi
FORCE_GLOBAL_DRIVER_BUILDS="$(bash ./tools/should-force-global-driver-builds.sh --base "$BASE_REF" --head "$GITHUB_SHA")"
echo "drivers=${DRIVERS}" >> "$GITHUB_OUTPUT"
echo "compare_base=${BASE_REF}" >> "$GITHUB_OUTPUT"
echo "force_global_driver_builds=${FORCE_GLOBAL_DRIVER_BUILDS}" >> "$GITHUB_OUTPUT"
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-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 UPX (Windows)
if: matrix.platform == 'windows/amd64'
shell: pwsh
run: |
$UPX_VERSION = "4.2.4"
$url = "https://github.com/upx/upx/releases/download/v${UPX_VERSION}/upx-${UPX_VERSION}-win64.zip"
$zipPath = "$env:RUNNER_TEMP\upx.zip"
$extractPath = "$env:RUNNER_TEMP\upx"
Write-Host "📥 从 GitHub Releases 下载 UPX v${UPX_VERSION} ..."
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force
$upxDir = Get-ChildItem -Path $extractPath -Directory | Select-Object -First 1
"$($upxDir.FullName)" | Out-File -FilePath $env:GITHUB_PATH -Append -Encoding utf8
$upxCmd = Join-Path $upxDir.FullName "upx.exe"
if (!(Test-Path $upxCmd)) {
Write-Error "❌ 未检测到 upx无法保证 Windows 产物经过压缩"
exit 1
}
& $upxCmd --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
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
- 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
# ---- 生成 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 -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 -platform ${{ matrix.platform }} -clean -o ${{ matrix.build_name }} -ldflags "-s -w -X GoNavi-Wails/internal/app.AppVersion=${DEV_VERSION}"
fi
- name: Resolve Platform Driver Revision Diff
id: revision_diff
if: ${{ matrix.build_optional_agents && needs.driver_agents.outputs.has_changes == 'true' }}
shell: bash
run: |
set -euo pipefail
BASE_REF="${{ needs.driver_agents.outputs.compare_base }}"
FALLBACK_DRIVERS="${{ needs.driver_agents.outputs.drivers }}"
FORCE_GLOBAL_DRIVER_BUILDS="${{ needs.driver_agents.outputs.force_global_driver_builds }}"
if [[ -z "$BASE_REF" || "$BASE_REF" == "all" || "$FORCE_GLOBAL_DRIVER_BUILDS" == "true" ]]; then
if [[ "$FORCE_GLOBAL_DRIVER_BUILDS" == "true" && -n "$BASE_REF" && "$BASE_REF" != "all" ]]; then
echo "⚠️ 当前提交涉及 driver 构建/发布链路,保留全局驱动重建结果:${FALLBACK_DRIVERS}"
else
echo "⚠️ 当前 driver 检测基线不可做平台 diff回退使用全局检测结果${FALLBACK_DRIVERS}"
fi
echo "drivers=${FALLBACK_DRIVERS}" >> "$GITHUB_OUTPUT"
else
echo "🧭 对比当前平台 revisionbase=${BASE_REF} head=${GITHUB_SHA} platform=${{ matrix.platform }}"
if DRIVERS="$(bash ./tools/diff-driver-agent-revisions.sh --base "$BASE_REF" --head "$GITHUB_SHA" --platform "${{ matrix.platform }}")"; then
echo "🧭 当前平台实际需要重建的 driver agents: ${DRIVERS:-<empty>}"
echo "drivers=${DRIVERS}" >> "$GITHUB_OUTPUT"
else
echo "⚠️ revision 差异对比失败,保守回退为全量重建"
ALL_DRIVERS="$(bash ./tools/detect-changed-driver-agents.sh --base all --head "$GITHUB_SHA")"
echo "drivers=${ALL_DRIVERS}" >> "$GITHUB_OUTPUT"
fi
fi
DRIVERS="$(sed -n 's/^drivers=//p' "$GITHUB_OUTPUT" | tail -n 1)"
if [[ -n "$DRIVERS" ]]; then
echo "has_changes=true" >> "$GITHUB_OUTPUT"
else
echo "has_changes=false" >> "$GITHUB_OUTPUT"
fi
- name: Build Optional Driver Agents
if: ${{ matrix.build_optional_agents && steps.revision_diff.outputs.has_changes == 'true' }}
shell: bash
env:
CHANGED_DRIVER_AGENTS: ${{ steps.revision_diff.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"
IFS=',' read -r -a DRIVERS <<< "$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/duckdb.lib" ]; then
echo "$lib_dir"
return 0
fi
mkdir -p "$lib_dir"
rm -rf "$extract_dir"
rm -f "$zip_path"
local attempt dll_path lib_path
for attempt in 1 2 3; do
echo "📥 下载 DuckDB Windows 动态库 (${attempt}/3): ${DUCKDB_WINDOWS_LIBRARY_URL}"
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)"
lib_path="$(find "$extract_dir" -type f -name duckdb.lib | head -n 1 || true)"
if [ -n "$dll_path" ] && [ -n "$lib_path" ]; then
cp "$dll_path" "$lib_dir/duckdb.dll"
cp "$lib_path" "$lib_dir/duckdb.lib"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.dll.a"
cp "$lib_dir/duckdb.lib" "$lib_dir/libduckdb.a"
rm -rf "$extract_dir"
echo "$lib_dir"
return 0
fi
echo "⚠️ DuckDB Windows 动态库压缩包缺少 duckdb.dll 或 duckdb.lib准备重试"
else
echo "⚠️ DuckDB Windows 动态库压缩包校验失败,准备重试"
fi
else
echo "⚠️ DuckDB Windows 动态库下载失败,准备重试"
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="$DRIVER"
if [ "$DRIVER" = "doris" ]; then
BUILD_DRIVER="diros"
fi
if [ "$DRIVER" = "duckdb" ] && [ "$GOOS" = "windows" ] && [ "$GOARCH" != "amd64" ]; then
echo "⚠️ 跳过 DuckDB driver当前平台 ${GOOS}/${GOARCH} 不受支持,仅支持 windows/amd64"
continue
fi
TAG="gonavi_${BUILD_DRIVER}_driver"
BUILD_TAGS="$TAG"
OUTPUT="${DRIVER}-driver-agent-${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
CGO_ENABLED=1 GOOS="$GOOS" GOARCH="$GOARCH" CGO_LDFLAGS="-L${DUCKDB_LIB_DIR} -lduckdb" PATH="${DUCKDB_LIB_DIR}:$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
bash ./tools/verify-driver-agent-revisions.sh \
--assets-dir drivers \
--platform "$TARGET_PLATFORM" \
--drivers "$CHANGED_DRIVER_AGENTS"
# 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")
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 EXE
if: contains(matrix.platform, 'windows')
shell: pwsh
run: |
Set-Location build/bin
$version = "${{ steps.version.outputs.version }}"
$target = "${{ matrix.build_name }}"
$finalExeName = "GoNavi-$version-${{ matrix.os_name }}-${{ matrix.arch_name }}${{ matrix.artifact_suffix }}.exe"
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
}
$isArm64Target = "${{ matrix.arch_name }}".ToLowerInvariant() -eq "arm64"
if ($isArm64Target) {
Write-Warning "⚠️ UPX 当前不支持 win64/arm64跳过压缩并保留原始 EXE。"
$LASTEXITCODE = 0
} else {
$upxCmd = Get-Command upx -ErrorAction SilentlyContinue
if ($null -eq $upxCmd) {
Write-Error "❌ 未找到 upx无法保证 Windows 产物经过压缩"
exit 1
}
$beforeBytes = (Get-Item -LiteralPath $finalExe).Length
Write-Host "🗜️ 使用 UPX 压缩 $finalExe ..."
& upx --best --lzma --force $finalExe | Out-Host
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ UPX 压缩失败($LASTEXITCODE"
exit 1
}
& upx -t $finalExe | Out-Host
if ($LASTEXITCODE -ne 0) {
Write-Error "❌ UPX 校验失败($LASTEXITCODE"
exit 1
}
$afterBytes = (Get-Item -LiteralPath $finalExe).Length
if ($afterBytes -lt $beforeBytes) {
$savedBytes = $beforeBytes - $afterBytes
Write-Host ("✅ UPX 压缩完成:{0:N2}MB -> {1:N2}MB减少 {2:N2}MB" -f ($beforeBytes / 1MB), ($afterBytes / 1MB), ($savedBytes / 1MB))
} else {
Write-Host (" UPX 压缩完成:{0:N2}MB -> {1:N2}MB" -f ($beforeBytes / 1MB), ($afterBytes / 1MB))
}
}
Write-Host "📦 输出 Windows 可执行文件 $finalExeName..."
Copy-Item -LiteralPath $finalExe -Destination "..\\..\\$finalExeName" -Force
# 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"
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/icons/hicolor/256x256/apps
cp "$TARGET" AppDir/usr/bin/gonavi
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-*.tar.gz
GoNavi-*.AppImage
drivers/**
retention-days: 7
# 汇总所有产物并发布为 Pre-release
release:
name: Publish Dev Pre-release
needs:
- build
- driver_agents
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v5
- name: Download All Artifacts
uses: actions/download-artifact@v7
with:
path: release-assets
pattern: dev-build-artifacts-*
merge-multiple: true
- name: List Assets
run: ls -R release-assets
- 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
python3 ../tools/generate-driver-release-manifest.py \
--assets-dir drivers \
--output ../driver-release-assets/GoNavi-DriverAgents-Manifest.json
rm -rf drivers
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
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 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'
uses: softprops/action-gh-release@v3
with:
repository: Syngnat/GoNavi-DriverAgents
tag_name: dev-latest
name: "GoNavi Driver Agents (${{ steps.version.outputs.version }})"
files: driver-release-assets/*
fail_on_unmatched_files: true
prerelease: true
draft: false
make_latest: false
body: |
GoNavi dev driver-agent assets.
**版本**: `${{ steps.version.outputs.version }}`
**来源仓库**: `${{ github.repository }}`
**提交**: [`${{ github.sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})
token: ${{ secrets.DRIVER_RELEASE_TOKEN }}
- 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: |
## 🧪 测试版本 (Dev Build)
**版本**: `${{ steps.version.outputs.version }}`
**分支**: `dev`
**提交**: [`${{ github.sha }}`](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }})
**构建时间**: ${{ steps.build_time.outputs.display }}
> ⚠️ 这是开发测试版本,仅供内部测试使用,不建议用于生产环境。
> 每次 push 到 `dev` 分支会自动覆盖此 release。
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}