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 'GoNavi\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 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 # ---- 生成 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 "🧭 对比当前平台 revision:base=${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:-}" 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/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="$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 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 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' 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: | ## 🧪 测试版本 (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 }}