Release binaries #41
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Build and release | |
| on: | |
| push: | |
| tags: | |
| - "v*" | |
| workflow_dispatch: | |
| inputs: | |
| publish_release: | |
| description: "Create or update a GitHub Release after CI passes" | |
| required: true | |
| default: false | |
| type: boolean | |
| tag_name: | |
| description: "Release tag when publish_release is enabled, for example v2.8.0" | |
| required: false | |
| type: string | |
| permissions: | |
| contents: read | |
| concurrency: | |
| group: "${{ github.workflow }}-${{ github.ref }}" | |
| cancel-in-progress: ${{ github.event_name != 'workflow_dispatch' && github.ref_type != 'tag' }} | |
| env: | |
| BUILD_CONFIG: Release | |
| CI: "true" | |
| VCPKG_BINARY_SOURCES: "clear;files,${{ github.workspace }}/.cache/vcpkg-binaries,readwrite" | |
| VCPKG_DOWNLOADS: "${{ github.workspace }}/.cache/vcpkg-downloads" | |
| jobs: | |
| metadata: | |
| name: Resolve build metadata | |
| runs-on: ubuntu-latest | |
| outputs: | |
| version: ${{ steps.release.outputs.version }} | |
| is_release: ${{ steps.release.outputs.is_release }} | |
| steps: | |
| - name: Resolve version and release mode | |
| id: release | |
| shell: pwsh | |
| run: | | |
| $manualTag = "${{ inputs.tag_name }}" | |
| $isTag = "${{ github.ref_type }}" -eq "tag" | |
| $isManual = "${{ github.event_name }}" -eq "workflow_dispatch" | |
| $publishManualRelease = "${{ inputs.publish_release }}" -eq "true" | |
| if ($isTag) { | |
| $version = "${{ github.ref_name }}" | |
| $isRelease = $true | |
| } elseif ($isManual -and $publishManualRelease) { | |
| if ($manualTag -notmatch '^v[0-9A-Za-z][0-9A-Za-z._-]*$') { | |
| throw "tag_name is required when publish_release is enabled; it must start with v and contain only letters, numbers, dots, underscores, or hyphens." | |
| } | |
| $version = $manualTag | |
| $isRelease = $true | |
| } else { | |
| $sha = "${{ github.sha }}" | |
| $version = "dev-$($sha.Substring(0, 7))" | |
| $isRelease = $false | |
| } | |
| "version=$version" >> $env:GITHUB_OUTPUT | |
| "is_release=$($isRelease.ToString().ToLowerInvariant())" >> $env:GITHUB_OUTPUT | |
| build: | |
| name: Build ${{ matrix.platform }} | |
| needs: metadata | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 360 | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - platform: windows-x64 | |
| os: windows-2022 | |
| test_preset: windows-ninja-release | |
| archive_ext: zip | |
| - platform: linux | |
| os: ubuntu-24.04 | |
| test_preset: linux-release | |
| archive_ext: tar.gz | |
| - platform: macos | |
| os: macos-latest | |
| test_preset: macos-release | |
| archive_ext: tar.gz | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v7 | |
| with: | |
| persist-credentials: false | |
| - name: Resolve pinned vcpkg revision | |
| shell: pwsh | |
| run: | | |
| $mainManifest = Get-Content "kbe/src/vcpkg.json" -Raw | ConvertFrom-Json | |
| $mainBaseline = $mainManifest.'builtin-baseline' | |
| if (-not $mainBaseline) { | |
| throw "kbe/src/vcpkg.json does not define builtin-baseline." | |
| } | |
| "KBE_VCPKG_REF=$mainBaseline" >> $env:GITHUB_ENV | |
| - name: Restore vcpkg caches | |
| uses: actions/cache@v6 | |
| with: | |
| path: | | |
| .cache/vcpkg-binaries | |
| .cache/vcpkg-downloads | |
| key: vcpkg-${{ runner.os }}-${{ runner.arch }}-${{ hashFiles('kbe/src/vcpkg.json', 'kbe/src/cmake/vcpkg-triplets/*.cmake') }} | |
| restore-keys: | | |
| vcpkg-${{ runner.os }}-${{ runner.arch }}- | |
| - name: Prepare vcpkg cache directories | |
| shell: pwsh | |
| run: | | |
| $binaryCache = Join-Path $env:GITHUB_WORKSPACE ".cache/vcpkg-binaries" | |
| foreach ($path in @($env:VCPKG_DOWNLOADS, $binaryCache)) { | |
| if (-not $path) { | |
| throw "A vcpkg cache path is empty." | |
| } | |
| New-Item -ItemType Directory -Path $path -Force | Out-Null | |
| } | |
| - name: Build on Linux | |
| if: runner.os == 'Linux' | |
| shell: sh | |
| run: sh ./install/install_linux.sh "$BUILD_CONFIG" | |
| - name: Build on macOS | |
| if: runner.os == 'macOS' | |
| shell: sh | |
| run: sh ./install/install_macos.sh "$BUILD_CONFIG" | |
| - name: Build on Windows | |
| if: runner.os == 'Windows' | |
| shell: cmd | |
| run: | | |
| set "VSDEVCMD=%ProgramFiles%\Microsoft Visual Studio\2022\Enterprise\Common7\Tools\VsDevCmd.bat" | |
| if not exist "%VSDEVCMD%" set "VSDEVCMD=%ProgramFiles%\Microsoft Visual Studio\2022\Professional\Common7\Tools\VsDevCmd.bat" | |
| if not exist "%VSDEVCMD%" set "VSDEVCMD=%ProgramFiles%\Microsoft Visual Studio\2022\Community\Common7\Tools\VsDevCmd.bat" | |
| if not exist "%VSDEVCMD%" ( | |
| echo [ERROR] Visual Studio 2022 with C++ tools was not found. | |
| exit /b 1 | |
| ) | |
| call "%VSDEVCMD%" -no_logo -arch=x64 -host_arch=x64 | |
| if errorlevel 1 exit /b %errorlevel% | |
| call install\install_windows.bat %BUILD_CONFIG% | |
| if errorlevel 1 exit /b %errorlevel% | |
| - name: Run CTest | |
| shell: pwsh | |
| run: | | |
| Push-Location "kbe/src" | |
| try { | |
| ctest --preset "${{ matrix.test_preset }}" | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "CTest failed with exit code $LASTEXITCODE." | |
| } | |
| } finally { | |
| Pop-Location | |
| } | |
| - name: Upload test diagnostics | |
| if: failure() | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: test-diagnostics-${{ matrix.platform }} | |
| path: | | |
| kbe/src/out/build/**/Testing/Temporary/ | |
| kbe/src/out/build/**/Testing/**/*.xml | |
| if-no-files-found: ignore | |
| retention-days: 7 | |
| - name: Verify server binaries | |
| shell: pwsh | |
| run: | | |
| $suffix = if ("${{ runner.os }}" -eq "Windows") { ".exe" } else { "" } | |
| $required = @( | |
| "machine", "baseappmgr", "cellappmgr", "dbmgr", "loginapp", | |
| "baseapp", "cellapp", "bots", "logger", "interfaces", "kbcmd" | |
| ) | |
| foreach ($name in $required) { | |
| $path = Join-Path "kbe/bin/server" "$name$suffix" | |
| if (-not (Test-Path -LiteralPath $path -PathType Leaf)) { | |
| throw "Missing server binary: $path" | |
| } | |
| } | |
| Get-ChildItem -LiteralPath "kbe/bin/server" -File | | |
| Sort-Object Name | | |
| Select-Object Name, Length | |
| - name: Audit Linux runtime dependencies | |
| if: runner.os == 'Linux' | |
| shell: sh | |
| run: | | |
| set -eu | |
| for binary in kbe/bin/server/*; do | |
| if [ -f "$binary" ] && file "$binary" | grep -q "ELF"; then | |
| echo "::group::ldd $binary" | |
| dependencies="$(ldd "$binary")" | |
| printf '%s\n' "$dependencies" | |
| echo "::endgroup::" | |
| if printf '%s\n' "$dependencies" | grep -q "not found"; then | |
| echo "[ERROR] Missing runtime dependency for $binary" >&2 | |
| exit 1 | |
| fi | |
| dynamic_tags="$(readelf -d "$binary")" | |
| if printf '%s\n' "$dynamic_tags" | grep -Eq 'vcpkg_installed|/home/runner/work'; then | |
| echo "[ERROR] Non-relocatable Linux runtime path in $binary" >&2 | |
| exit 1 | |
| fi | |
| case "$(basename "$binary")" in | |
| libpython3*.so*) ;; | |
| *) | |
| if ! printf '%s\n' "$dynamic_tags" | grep -E '(RPATH|RUNPATH)' | grep -Fq '$ORIGIN'; then | |
| echo "[ERROR] Missing relocatable \$ORIGIN runtime path in $binary" >&2 | |
| exit 1 | |
| fi | |
| ;; | |
| esac | |
| fi | |
| done | |
| - name: Bundle Linux runtime libraries | |
| if: runner.os == 'Linux' && needs.metadata.outputs.is_release == 'true' | |
| shell: sh | |
| run: | | |
| set -eu | |
| sudo apt-get update | |
| sudo apt-get install -y patchelf | |
| lib_dir="kbe/bin/server/lib" | |
| mkdir -p "$lib_dir" | |
| : > /tmp/kbe-runtime-libs.txt | |
| for binary in kbe/bin/server/*; do | |
| if [ -f "$binary" ] && file "$binary" | grep -q "ELF"; then | |
| ldd "$binary" | awk ' | |
| /=> \// { print $3 } | |
| /^[[:space:]]*\// { print $1 } | |
| ' >> /tmp/kbe-runtime-libs.txt | |
| fi | |
| done | |
| sort -u /tmp/kbe-runtime-libs.txt | while IFS= read -r library; do | |
| [ -n "$library" ] || continue | |
| [ -f "$library" ] || continue | |
| base="$(basename "$library")" | |
| case "$base" in | |
| libpython3*.so*) | |
| # Python 已由构建部署到可执行文件同目录,不在 server/lib 中保留重复副本。 | |
| # Python is already deployed beside the executables; do not duplicate it in server/lib. | |
| continue | |
| ;; | |
| ld-linux*.so.*|libc.so.*|libm.so.*|libdl.so.*|libpthread.so.*|librt.so.*|libresolv.so.*|linux-vdso*.so.*) | |
| continue | |
| ;; | |
| esac | |
| cp -L "$library" "$lib_dir/" | |
| done | |
| for binary in kbe/bin/server/*; do | |
| if [ -f "$binary" ] && file "$binary" | grep -q "ELF"; then | |
| patchelf --set-rpath '$ORIGIN/lib:$ORIGIN' "$binary" | |
| fi | |
| done | |
| echo "::group::Bundled Linux runtime libraries" | |
| find "$lib_dir" -maxdepth 1 -type f -printf '%f\n' | sort | |
| echo "::endgroup::" | |
| - name: Audit macOS runtime dependencies | |
| if: runner.os == 'macOS' | |
| shell: sh | |
| run: | | |
| set -eu | |
| for binary in kbe/bin/server/*; do | |
| if [ -f "$binary" ] && file "$binary" | grep -q "Mach-O"; then | |
| echo "::group::otool -L $binary" | |
| dependencies="$(otool -L "$binary")" | |
| printf '%s\n' "$dependencies" | |
| echo "::endgroup::" | |
| if printf '%s\n' "$dependencies" | grep -Eq 'vcpkg_installed|/Users/runner/work'; then | |
| echo "[ERROR] Non-relocatable macOS dependency in $binary" >&2 | |
| exit 1 | |
| fi | |
| fi | |
| done | |
| - name: Package release | |
| if: needs.metadata.outputs.is_release == 'true' | |
| id: package | |
| shell: pwsh | |
| run: | | |
| $version = "${{ needs.metadata.outputs.version }}" | |
| $platform = "${{ matrix.platform }}" | |
| if ("${{ runner.os }}" -ne "Windows") { | |
| $arch = (& uname -m).Trim() | |
| if ($LASTEXITCODE -ne 0 -or -not $arch) { | |
| throw "Unable to resolve the runner architecture." | |
| } | |
| $platform = "$platform-$arch" | |
| } | |
| $packageName = "KBEngine-Nex-1x-$version-$platform" | |
| $stageRoot = Join-Path $PWD "dist" | |
| $stageDir = Join-Path $stageRoot $packageName | |
| $kbeDir = Join-Path $stageDir "kbe" | |
| New-Item -ItemType Directory -Force -Path $kbeDir | Out-Null | |
| if (Test-Path "docs") { | |
| Copy-Item "docs" -Destination $stageDir -Recurse -Force | |
| } | |
| foreach ($dir in @("bin", "res", "tools")) { | |
| $source = Join-Path "kbe" $dir | |
| if (Test-Path $source) { | |
| Copy-Item $source -Destination $kbeDir -Recurse -Force | |
| } | |
| } | |
| foreach ($file in @( | |
| "new_assets.bat", "new_assets.sh", "UPDATE.md", "README.md", | |
| "GPL-LICENSE.txt", "LGPL-LICENSE.txt" | |
| )) { | |
| if (Test-Path $file) { | |
| Copy-Item $file -Destination $stageDir -Force | |
| } | |
| } | |
| $archivePath = Join-Path $stageRoot "$packageName.${{ matrix.archive_ext }}" | |
| if ("${{ matrix.archive_ext }}" -eq "zip") { | |
| Compress-Archive -Path $stageDir -DestinationPath $archivePath -Force | |
| } else { | |
| tar -czf $archivePath -C $stageRoot $packageName | |
| if ($LASTEXITCODE -ne 0) { | |
| throw "tar failed with exit code $LASTEXITCODE." | |
| } | |
| } | |
| "archive=$archivePath" >> $env:GITHUB_OUTPUT | |
| "archive_name=$(Split-Path $archivePath -Leaf)" >> $env:GITHUB_OUTPUT | |
| - name: Upload release artifact | |
| if: needs.metadata.outputs.is_release == 'true' | |
| uses: actions/upload-artifact@v7 | |
| with: | |
| name: ${{ steps.package.outputs.archive_name }} | |
| path: ${{ steps.package.outputs.archive }} | |
| archive: false | |
| if-no-files-found: error | |
| retention-days: 14 | |
| publish: | |
| name: Publish GitHub Release | |
| if: needs.metadata.outputs.is_release == 'true' | |
| needs: | |
| - metadata | |
| - build | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Download release artifacts | |
| uses: actions/download-artifact@v8 | |
| with: | |
| path: dist | |
| merge-multiple: true | |
| skip-decompress: true | |
| - name: Generate checksums | |
| shell: sh | |
| run: | | |
| set -eu | |
| cd dist | |
| find . -maxdepth 1 -type f \ | |
| \( -name 'KBEngine-Nex-1x-*.zip' -o -name 'KBEngine-Nex-1x-*.tar.gz' \) \ | |
| -printf '%f\n' | sort > release-archives.txt | |
| archive_count="$(wc -l < release-archives.txt | tr -d '[:space:]')" | |
| if [ "$archive_count" -ne 3 ]; then | |
| echo "[ERROR] Expected 3 release archives, found $archive_count." >&2 | |
| find . -maxdepth 2 -print >&2 | |
| exit 1 | |
| fi | |
| xargs sha256sum < release-archives.txt > SHA256SUMS.txt | |
| rm release-archives.txt | |
| cat SHA256SUMS.txt | |
| - name: Create or update GitHub Release | |
| shell: sh | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| GH_REPO: ${{ github.repository }} | |
| VERSION: ${{ needs.metadata.outputs.version }} | |
| run: | | |
| set -eu | |
| if gh release view "$VERSION" >/dev/null 2>&1; then | |
| gh release upload "$VERSION" \ | |
| dist/KBEngine-Nex-1x-*.zip \ | |
| dist/KBEngine-Nex-1x-*.tar.gz \ | |
| dist/SHA256SUMS.txt \ | |
| --clobber | |
| else | |
| gh release create "$VERSION" \ | |
| dist/KBEngine-Nex-1x-*.zip \ | |
| dist/KBEngine-Nex-1x-*.tar.gz \ | |
| dist/SHA256SUMS.txt \ | |
| --target "$GITHUB_SHA" \ | |
| --title "KBEngine Nex 1.x $VERSION" \ | |
| --notes "Release binaries for Windows, Linux, and macOS." | |
| fi |