feat(ft8): add native multistream QSO and WW Digi contest support #901
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 Electron App | |
| on: | |
| push: | |
| branches: [ main, develop ] | |
| pull_request: | |
| branches: [ main ] | |
| workflow_dispatch: | |
| inputs: | |
| version: | |
| description: 'Release version (e.g., v1.0.0, leave empty for nightly)' | |
| required: false | |
| default: '' | |
| # 授予 GITHUB_TOKEN 写入权限以创建 release | |
| permissions: | |
| contents: write | |
| jobs: | |
| build: | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| - os: windows-2022 | |
| platform: win32 | |
| arch: x64 | |
| artifact_name: tx-5dr-windows-x64 | |
| - os: macos-latest | |
| platform: darwin | |
| arch: arm64 | |
| artifact_name: tx-5dr-macos-arm64 | |
| - os: macos-15-intel | |
| platform: darwin | |
| arch: x64 | |
| artifact_name: tx-5dr-macos-x64 | |
| - os: ubuntu-latest | |
| platform: linux | |
| arch: x64 | |
| artifact_name: tx-5dr-linux-x64 | |
| - os: ubuntu-24.04-arm | |
| platform: linux | |
| arch: arm64 | |
| artifact_name: tx-5dr-linux-arm64 | |
| runs-on: ${{ matrix.os }} | |
| timeout-minutes: 60 | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 20 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22.15.1' | |
| - name: Enable Corepack and setup Yarn | |
| shell: bash | |
| run: | | |
| corepack enable | |
| corepack prepare yarn@4.9.1 --activate | |
| yarn --version || { | |
| echo "Corepack failed, installing Yarn 4.9.1 manually" | |
| npm install -g yarn@4.9.1 | |
| yarn --version | |
| } | |
| - name: Verify Yarn configuration | |
| shell: bash | |
| run: | | |
| echo "=== Verifying Yarn PnP is disabled ===" | |
| if [ -f .yarnrc.yml ]; then | |
| echo "Found .yarnrc.yml:" | |
| cat .yarnrc.yml | |
| if grep -q "nodeLinker: node-modules" .yarnrc.yml; then | |
| echo "✓ nodeLinker is set to node-modules (PnP disabled)" | |
| else | |
| echo "⚠ Warning: nodeLinker not set to node-modules" | |
| fi | |
| else | |
| echo "⚠ Warning: .yarnrc.yml not found" | |
| fi | |
| - name: Setup Python (for native modules) | |
| uses: actions/setup-python@v4 | |
| with: | |
| python-version: '3.11' | |
| - name: Install system dependencies (Linux) | |
| if: runner.os == 'Linux' | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y \ | |
| libasound2-dev libpulse-dev \ | |
| libx11-dev libxrandr-dev libxinerama-dev libxcursor-dev libxi-dev libxext-dev \ | |
| libopus-dev \ | |
| portaudio19-dev build-essential python3-dev pkg-config \ | |
| xz-utils rpm cpio | |
| - name: Verify Yarn version | |
| run: | | |
| echo "Node version: $(node --version)" | |
| echo "Yarn version: $(yarn --version)" | |
| echo "Package manager in package.json: $(grep packageManager package.json)" | |
| - name: Install dependencies | |
| shell: bash | |
| run: | | |
| echo "=== Installing dependencies with Yarn 4 (node-modules mode) ===" | |
| echo "Note: Using node-modules linker instead of PnP for better compatibility" | |
| yarn install || { | |
| echo "First install attempt failed, applying naudiodon2 fixes..." | |
| node scripts/fix-naudiodon.js || true | |
| echo "Clearing yarn cache and retrying..." | |
| yarn cache clean || true | |
| yarn install || { | |
| echo "Second install failed, trying with network concurrency limit..." | |
| yarn install --network-concurrency 1 || { | |
| echo "Final fallback: installing with inline builds..." | |
| yarn install --inline-builds | |
| } | |
| } | |
| } | |
| env: | |
| npm_config_cache: .npm | |
| ELECTRON_CACHE: .electron-cache | |
| ELECTRON_BUILDER_CACHE: .electron-builder-cache | |
| # Try to use prebuilt binaries first | |
| npm_config_build_from_source: false | |
| # Ensure proper build environment (only for non-Windows) | |
| CC: ${{ matrix.platform != 'win32' && 'gcc' || '' }} | |
| CXX: ${{ matrix.platform != 'win32' && 'g++' || '' }} | |
| # Force Yarn to use node-modules linker (should be in .yarnrc.yml) | |
| YARN_NODE_LINKER: node-modules | |
| - name: Generate ICO file (Windows) | |
| if: matrix.os == 'windows-latest' | |
| run: node scripts/generate-ico.js | |
| - name: Prepare desktop build metadata | |
| shell: bash | |
| env: | |
| TZ: UTC | |
| run: | | |
| CHANNEL="release" | |
| if [ -z "${{ github.event.inputs.version }}" ]; then | |
| CHANNEL="nightly" | |
| fi | |
| COMMIT_TIMESTAMP=$(git show -s --format=%cI "${{ github.sha }}") | |
| COMMIT_STAMP=$(git show -s --date=format-local:%Y%m%d%H%M --format=%cd "${{ github.sha }}") | |
| node scripts/prepare-electron-release.mjs \ | |
| --channel "$CHANNEL" \ | |
| --version "${{ github.event.inputs.version }}" \ | |
| --commit "${{ github.sha }}" \ | |
| --build-timestamp "$COMMIT_TIMESTAMP" \ | |
| --build-stamp "$COMMIT_STAMP" | |
| - name: Verify macOS signing secrets (release builds) | |
| if: matrix.platform == 'darwin' && github.event_name != 'pull_request' | |
| env: | |
| MACOS_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} | |
| MACOS_CERTIFICATE_PWD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| for name in MACOS_CERTIFICATE MACOS_CERTIFICATE_PWD APPLE_ID APPLE_APP_SPECIFIC_PASSWORD APPLE_TEAM_ID; do | |
| if [ -z "${!name:-}" ]; then | |
| echo "Missing required macOS signing secret: ${name}" >&2 | |
| exit 1 | |
| fi | |
| done | |
| - name: Import Code Signing Certificate (macOS) | |
| if: matrix.platform == 'darwin' && github.event_name != 'pull_request' | |
| env: | |
| MACOS_CERTIFICATE: ${{ secrets.APPLE_CERTIFICATE_BASE64 }} | |
| MACOS_CERTIFICATE_PWD: ${{ secrets.APPLE_CERTIFICATE_PASSWORD }} | |
| run: | | |
| echo "$MACOS_CERTIFICATE" | base64 --decode > certificate.p12 | |
| security create-keychain -p actions temp.keychain | |
| security default-keychain -s temp.keychain | |
| security unlock-keychain -p actions temp.keychain | |
| security import certificate.p12 -k temp.keychain -P "$MACOS_CERTIFICATE_PWD" -T /usr/bin/codesign -T /usr/bin/productbuild | |
| security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k actions temp.keychain | |
| security set-keychain-settings -t 3600 -u temp.keychain | |
| rm -f certificate.p12 | |
| echo "=== 验证证书导入 ===" | |
| security find-identity -v -p codesigning temp.keychain | |
| echo "✅ 证书导入成功" | |
| - name: Build application | |
| run: yarn build | |
| env: | |
| NODE_ENV: production | |
| TX5DR_CLUBLOG_API_KEY: ${{ secrets.CLUBLOG_API_KEY }} | |
| - name: Prepare portable Node runtime | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| NODE_VERSION="22.15.1" | |
| PLATFORM="${{ matrix.platform }}" | |
| ARCH="${{ matrix.arch }}" | |
| TRIPLET="${PLATFORM}-${ARCH}" | |
| echo "Preparing portable node ${NODE_VERSION} for ${TRIPLET}" | |
| mkdir -p resources/bin/${TRIPLET} | |
| mkdir -p resources/licenses | |
| if [ "${PLATFORM}" = "darwin" ]; then | |
| PKG="node-v${NODE_VERSION}-darwin-${ARCH}.tar.xz" | |
| URL="https://nodejs.org/dist/v${NODE_VERSION}/${PKG}" | |
| echo "Downloading ${URL}" | |
| curl -fsSL -o ${PKG} ${URL} | |
| tar -xJf ${PKG} | |
| SRC_DIR=$(echo node-v${NODE_VERSION}-darwin-${ARCH}) | |
| install -m 0755 ${SRC_DIR}/bin/node resources/bin/${TRIPLET}/node | |
| # macOS strip -x: 只删除本地符号,保留 N-API 等导出符号(native 模块需要) | |
| strip -x resources/bin/${TRIPLET}/node || true | |
| cp -f ${SRC_DIR}/LICENSE resources/licenses/nodejs-LICENSE || true | |
| # Clean up temporary archive and extracted directory to avoid packaging them | |
| rm -rf "${PKG}" "${SRC_DIR}" | |
| elif [ "${PLATFORM}" = "linux" ]; then | |
| PKG="node-v${NODE_VERSION}-linux-${ARCH}.tar.xz" | |
| URL="https://nodejs.org/dist/v${NODE_VERSION}/${PKG}" | |
| echo "Downloading ${URL}" | |
| curl -fsSL -o ${PKG} ${URL} | |
| tar -xJf ${PKG} | |
| SRC_DIR=$(echo node-v${NODE_VERSION}-linux-${ARCH}) | |
| install -m 0755 ${SRC_DIR}/bin/node resources/bin/${TRIPLET}/node | |
| strip resources/bin/${TRIPLET}/node || true | |
| cp -f ${SRC_DIR}/LICENSE resources/licenses/nodejs-LICENSE || true | |
| # Clean up temporary archive and extracted directory to avoid packaging them | |
| rm -rf "${PKG}" "${SRC_DIR}" | |
| else | |
| # Windows | |
| PKG="node-v${NODE_VERSION}-win-${ARCH}.zip" | |
| URL="https://nodejs.org/dist/v${NODE_VERSION}/${PKG}" | |
| echo "Downloading ${URL}" | |
| curl -fsSL -o ${PKG} ${URL} | |
| # Use 7z to extract zip on Windows runner (tar may not handle zip) | |
| 7z x -y "${PKG}" | |
| SRC_DIR=$(echo node-v${NODE_VERSION}-win-${ARCH}) | |
| mkdir -p resources/bin/${TRIPLET} | |
| cp -f ${SRC_DIR}/node.exe resources/bin/${TRIPLET}/node.exe | |
| cp -f ${SRC_DIR}/LICENSE resources/licenses/nodejs-LICENSE || true | |
| # Bundle MSVC runtime DLLs app-locally so portable archives work on clean Windows installs. | |
| case "${ARCH}" in | |
| x64) REDIST_ARCH="x64" ;; | |
| arm64) REDIST_ARCH="arm64" ;; | |
| *) | |
| echo "Unsupported Windows VC runtime arch: ${ARCH}" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| # Search for VC runtime in Visual Studio 2022 (pre-installed on windows-2022 runner) | |
| mapfile -t VC_REDIST_DIRS < <( | |
| find \ | |
| "/c/Program Files/Microsoft Visual Studio/2022" \ | |
| "/c/Program Files (x86)/Microsoft Visual Studio/2022" \ | |
| -type d \ | |
| -path "*/VC/Redist/MSVC/*/${REDIST_ARCH}/Microsoft.VC143.CRT" \ | |
| 2>/dev/null | sort -V | |
| ) | |
| if [ "${#VC_REDIST_DIRS[@]}" -eq 0 ]; then | |
| echo "Unable to find Visual C++ Redistributable CRT directory for ${REDIST_ARCH}." >&2 | |
| echo "Searched:" >&2 | |
| echo " /c/Program Files/Microsoft Visual Studio/2022/**/VC/Redist/MSVC/*/${REDIST_ARCH}/Microsoft.VC143.CRT" >&2 | |
| echo " /c/Program Files (x86)/Microsoft Visual Studio/2022/**/VC/Redist/MSVC/*/${REDIST_ARCH}/Microsoft.VC143.CRT" >&2 | |
| exit 1 | |
| fi | |
| VC_REDIST_DIR="${VC_REDIST_DIRS[$((${#VC_REDIST_DIRS[@]} - 1))]}" | |
| echo "Using VC runtime directory: ${VC_REDIST_DIR}" | |
| VC_RUNTIME_DLLS=( | |
| vcruntime140.dll | |
| vcruntime140_1.dll | |
| msvcp140.dll | |
| msvcp140_1.dll | |
| msvcp140_atomic_wait.dll | |
| ) | |
| for dll in "${VC_RUNTIME_DLLS[@]}"; do | |
| src="${VC_REDIST_DIR}/${dll}" | |
| if [ ! -f "${src}" ]; then | |
| echo "Required VC runtime DLL not found: ${src}" >&2 | |
| echo "Available files in ${VC_REDIST_DIR}:" >&2 | |
| ls -la "${VC_REDIST_DIR}" >&2 || true | |
| exit 1 | |
| fi | |
| cp -f "${src}" "resources/bin/${TRIPLET}/${dll}" | |
| done | |
| # Clean up temporary archive and extracted directory to avoid packaging them | |
| rm -rf "${PKG}" "${SRC_DIR}" | |
| fi | |
| echo "Portable node prepared:" | |
| ls -la resources/bin/${TRIPLET} | |
| # macOS: fix wsjtx-lib prebuilt dylib install_names to load from its own folder | |
| - name: Patch wsjtx-lib dylib install names (macOS) | |
| if: matrix.platform == 'darwin' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| echo "Patching wsjtx-lib install names..." | |
| ROOT=$(pwd) | |
| ARCH="${{ matrix.arch }}" | |
| PREBUILT_DIR="$ROOT/node_modules/wsjtx-lib/prebuilds/darwin-${ARCH}" | |
| if [ ! -d "$PREBUILT_DIR" ]; then | |
| echo "wsjtx-lib prebuild dir not found: $PREBUILT_DIR" | |
| exit 0 | |
| fi | |
| # 自动检测 Homebrew 路径(Intel: /usr/local, Apple Silicon: /opt/homebrew) | |
| if [ -d "/opt/homebrew" ]; then | |
| HOMEBREW_PREFIX="/opt/homebrew" | |
| elif [ -d "/usr/local/Homebrew" ]; then | |
| HOMEBREW_PREFIX="/usr/local" | |
| else | |
| echo "⚠️ Warning: Homebrew not found, trying default paths..." | |
| HOMEBREW_PREFIX="/opt/homebrew" # 默认使用 ARM64 路径 | |
| fi | |
| echo "✅ 使用 Homebrew 路径: $HOMEBREW_PREFIX" | |
| # Ensure libquadmath and libgcc_s are present (copy from Homebrew GCC) | |
| GCC_PREFIX=$(brew --prefix gcc@14 || brew --prefix gcc) | |
| echo "GCC prefix: $GCC_PREFIX" | |
| cp -Lv $(ls "$GCC_PREFIX"/lib/gcc/*/libquadmath.0.dylib | head -n1) "$PREBUILT_DIR" || true | |
| cp -Lv $(ls "$GCC_PREFIX"/lib/gcc/*/libgcc_s.1.1.dylib | head -n1) "$PREBUILT_DIR" || true | |
| pushd "$PREBUILT_DIR" >/dev/null | |
| set +e | |
| # Set ids to @loader_path | |
| install_name_tool -id @loader_path/libfftw3f.3.dylib libfftw3f.3.dylib 2>/dev/null || true | |
| install_name_tool -id @loader_path/libfftw3f_threads.3.dylib libfftw3f_threads.3.dylib 2>/dev/null || true | |
| install_name_tool -id @loader_path/libgfortran.5.dylib libgfortran.5.dylib 2>/dev/null || true | |
| [ -f libquadmath.0.dylib ] && install_name_tool -id @loader_path/libquadmath.0.dylib libquadmath.0.dylib || true | |
| [ -f libgcc_s.1.1.dylib ] && install_name_tool -id @loader_path/libgcc_s.1.1.dylib libgcc_s.1.1.dylib || true | |
| # Fix dependencies to @loader_path (支持两种路径:/opt/homebrew 和 /usr/local) | |
| install_name_tool -change ${HOMEBREW_PREFIX}/opt/fftw/lib/libfftw3f.3.dylib @loader_path/libfftw3f.3.dylib wsjtx_lib_nodejs.node 2>/dev/null || true | |
| install_name_tool -change ${HOMEBREW_PREFIX}/opt/fftw/lib/libfftw3f_threads.3.dylib @loader_path/libfftw3f_threads.3.dylib wsjtx_lib_nodejs.node 2>/dev/null || true | |
| # GCC dylibs may vary by version; try both gcc@14 and generic gcc paths | |
| install_name_tool -change ${HOMEBREW_PREFIX}/opt/gcc@14/lib/gcc/14/libgfortran.5.dylib @loader_path/libgfortran.5.dylib wsjtx_lib_nodejs.node 2>/dev/null || true | |
| install_name_tool -change ${HOMEBREW_PREFIX}/opt/gcc/lib/gcc/*/libgfortran.5.dylib @loader_path/libgfortran.5.dylib wsjtx_lib_nodejs.node 2>/dev/null || true | |
| install_name_tool -change ${HOMEBREW_PREFIX}/opt/gcc@14/lib/gcc/14/libquadmath.0.dylib @loader_path/libquadmath.0.dylib wsjtx_lib_nodejs.node 2>/dev/null || true | |
| install_name_tool -change ${HOMEBREW_PREFIX}/opt/gcc/lib/gcc/*/libquadmath.0.dylib @loader_path/libquadmath.0.dylib wsjtx_lib_nodejs.node 2>/dev/null || true | |
| # Fix secondary deps | |
| install_name_tool -change ${HOMEBREW_PREFIX}/opt/fftw/lib/libfftw3f.3.dylib @loader_path/libfftw3f.3.dylib libfftw3f_threads.3.dylib 2>/dev/null || true | |
| install_name_tool -change @rpath/libquadmath.0.dylib @loader_path/libquadmath.0.dylib libgfortran.5.dylib 2>/dev/null || true | |
| install_name_tool -change @rpath/libgcc_s.1.1.dylib @loader_path/libgcc_s.1.1.dylib libgfortran.5.dylib 2>/dev/null || true | |
| set -e | |
| popd >/dev/null | |
| - name: Package application | |
| shell: bash | |
| env: | |
| NODE_ENV: production | |
| PLATFORM: ${{ matrix.platform }} | |
| ARCH: ${{ matrix.arch }} | |
| GITHUB_TOKEN: ${{ github.token }} | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| IS_PULL_REQUEST: ${{ github.event_name == 'pull_request' }} | |
| DEBUG: electron-osx-sign* | |
| run: | | |
| set -euo pipefail | |
| if [ "${{ matrix.platform }}" = "darwin" ]; then | |
| echo "🔧 增加文件描述符限制..." | |
| ulimit -n 10240 | |
| echo "✅ 文件描述符限制已设置为: $(ulimit -n)" | |
| fi | |
| cleanup_macos_dmg_volume() { | |
| if [ "${{ matrix.platform }}" != "darwin" ]; then | |
| return 0 | |
| fi | |
| if [ -d "/Volumes/TX-5DR" ]; then | |
| hdiutil detach "/Volumes/TX-5DR" -force || true | |
| fi | |
| } | |
| package_with_retry() { | |
| local attempt=1 | |
| local max_attempts=2 | |
| if [ "${{ matrix.platform }}" = "darwin" ]; then | |
| max_attempts=4 | |
| fi | |
| while [ "$attempt" -le "$max_attempts" ]; do | |
| echo "📦 electron-builder 打包尝试 ${attempt}/${max_attempts}..." | |
| if "$@"; then | |
| return 0 | |
| fi | |
| if [ "$attempt" -ge "$max_attempts" ]; then | |
| echo "❌ electron-builder 打包失败,已达到最大重试次数" | |
| return 1 | |
| fi | |
| echo "⚠️ electron-builder 打包失败,清理输出后重试..." | |
| cleanup_macos_dmg_volume | |
| rm -rf out/electron-builder | |
| attempt=$((attempt + 1)) | |
| sleep $((attempt * 10)) | |
| done | |
| } | |
| if [ "${{ matrix.platform }}" = "darwin" ] && [ "${IS_PULL_REQUEST}" != "true" ]; then | |
| echo "🔓 解锁钥匙串并提取证书..." | |
| security unlock-keychain -p actions temp.keychain | |
| CERT_IDENTITY=$(security find-identity -v -p codesigning temp.keychain | grep "Developer ID Application" | head -1 | grep -o '"[^"]*"' | tr -d '"') | |
| if [ -z "$CERT_IDENTITY" ]; then | |
| echo "❌ 错误: 未找到 Developer ID Application 证书" | |
| exit 1 | |
| fi | |
| BUILDER_IDENTITY="${CERT_IDENTITY#Developer ID Application: }" | |
| if [ -z "$BUILDER_IDENTITY" ]; then | |
| echo "❌ 错误: 无法解析 electron-builder 签名身份" | |
| exit 1 | |
| fi | |
| echo "✅ 使用证书: $CERT_IDENTITY" | |
| echo "✅ electron-builder identity: $BUILDER_IDENTITY" | |
| package_with_retry env TX5DR_CODESIGN_IDENTITY_FULL="$CERT_IDENTITY" APPLE_IDENTITY="$BUILDER_IDENTITY" yarn make:autoupdate | |
| elif [ "${{ matrix.platform }}" = "darwin" ]; then | |
| echo "📦 PR macOS build: unsigned electron-builder package" | |
| package_with_retry env CSC_IDENTITY_AUTO_DISCOVERY=false yarn make:autoupdate | |
| else | |
| echo "📦 使用 electron-builder 构建 ${PLATFORM}-${ARCH} 全部桌面产物" | |
| package_with_retry yarn make:autoupdate | |
| fi | |
| - name: Verify auto-update package contents | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| PLATFORM="${{ matrix.platform }}" | |
| ARCH="${{ matrix.arch }}" | |
| TRIPLET="${PLATFORM}-${ARCH}" | |
| if [ "$PLATFORM" = "darwin" ]; then | |
| RESOURCES_DIR=$(find out/electron-builder -path "*.app/Contents/Resources" -type d | head -1) | |
| else | |
| APP_ROOT_CANDIDATE=$(find out/electron-builder -path "*/resources/app" -type d | head -1) | |
| RESOURCES_DIR=$(dirname "$APP_ROOT_CANDIDATE") | |
| fi | |
| if [ -z "${RESOURCES_DIR:-}" ] || [ ! -d "$RESOURCES_DIR" ]; then | |
| echo "Unable to locate electron-builder resources directory" >&2 | |
| find out/electron-builder -maxdepth 4 -type d >&2 || true | |
| exit 1 | |
| fi | |
| RESOURCES_DIR="$(cd "$RESOURCES_DIR" && pwd)" | |
| APP_ROOT="$RESOURCES_DIR/app" | |
| NODE_EXE="node" | |
| if [ "$PLATFORM" = "win32" ]; then | |
| NODE_EXE="node.exe" | |
| fi | |
| NODE_BIN="$RESOURCES_DIR/bin/$TRIPLET/$NODE_EXE" | |
| SERVER_SCRIPT_DIR="$APP_ROOT/packages/server/dist/scripts" | |
| required_paths=( | |
| "$RESOURCES_DIR/app-update.yml" | |
| "$RESOURCES_DIR/bin/$TRIPLET/$NODE_EXE" | |
| "$RESOURCES_DIR/models/deepcw" | |
| "$APP_ROOT/packages/electron-main/dist" | |
| "$APP_ROOT/packages/server/dist" | |
| "$APP_ROOT/packages/web/dist" | |
| "$APP_ROOT/packages/client-tools/src/proxy.js" | |
| ) | |
| for required in "${required_paths[@]}"; do | |
| if [ ! -e "$required" ]; then | |
| echo "Missing required packaged path: $required" >&2 | |
| echo "Resources dir: $RESOURCES_DIR" >&2 | |
| find "$RESOURCES_DIR" -maxdepth 4 -type d | sort >&2 || true | |
| exit 1 | |
| fi | |
| done | |
| if [ "$PLATFORM" = "darwin" ]; then | |
| python3 - "$APP_ROOT/node_modules" <<'PY' | |
| import pathlib | |
| import re | |
| import subprocess | |
| import sys | |
| root = pathlib.Path(sys.argv[1]) | |
| failed = False | |
| for binary in list(root.rglob('*.node')) + list(root.rglob('*.dylib')): | |
| output = subprocess.check_output(['otool', '-l', str(binary)], text=True) | |
| rpaths = re.findall(r'^\s*path\s+(.+?)\s+\(offset\s+\d+\)$', output, re.MULTILINE) | |
| duplicates = sorted({rpath for rpath in rpaths if rpaths.count(rpath) > 1}) | |
| if duplicates: | |
| failed = True | |
| print(f'Duplicate LC_RPATH in {binary}: {", ".join(duplicates)}', file=sys.stderr) | |
| if failed: | |
| sys.exit(1) | |
| PY | |
| fi | |
| if [ "$PLATFORM" = "linux" ]; then | |
| if find "$APP_ROOT/node_modules/wsjtx-lib/prebuilds" -path "*/linux-*/libstdc++.so.6" -type f | grep -q .; then | |
| echo "Packaged wsjtx-lib must not bundle libstdc++.so.6 on Linux Electron builds" >&2 | |
| find "$APP_ROOT/node_modules/wsjtx-lib/prebuilds" -path "*/linux-*/libstdc++.so.6" -type f >&2 | |
| exit 1 | |
| fi | |
| export NODE_ENV=production | |
| export APP_RESOURCES="$RESOURCES_DIR" | |
| export NODE_PATH="$APP_ROOT/node_modules" | |
| export LD_LIBRARY_PATH="$RESOURCES_DIR/native:${LD_LIBRARY_PATH:-}" | |
| echo "=== Linux packaged native module preflight ===" | |
| (cd "$SERVER_SCRIPT_DIR" && "$NODE_BIN" check-native-modules.js) | |
| run_packaged_import_order() { | |
| echo "=== Linux packaged native import order: $* ===" | |
| (cd "$SERVER_SCRIPT_DIR" && "$NODE_BIN" --input-type=module -e ' | |
| for (const mod of process.argv.slice(1)) { | |
| console.log(`import ${mod}`); | |
| await import(mod); | |
| } | |
| ' "$@") | |
| } | |
| run_packaged_import_order audify hamlib wsjtx-lib | |
| run_packaged_import_order wsjtx-lib audify hamlib | |
| fi | |
| echo "=== Packaged Image Radio runtime smoke ===" | |
| (cd "$SERVER_SCRIPT_DIR" && "$NODE_BIN" -e ' | |
| const { PNG } = require("pngjs"); | |
| if (typeof PNG.sync.read !== "function") throw new Error("pngjs unavailable"); | |
| const rasterwave = require("rasterwave-node"); | |
| if (rasterwave.sstvModes().length !== 31) throw new Error("rasterwave mode catalog incomplete"); | |
| if (typeof rasterwave.correctFaxPaper !== "function") throw new Error("radiofax correction unavailable"); | |
| for (const name of ["SstvDecoder", "FaxDecoder", "SstvEncoder"]) { | |
| if (typeof rasterwave[name] !== "function") throw new Error(`missing ${name}`); | |
| } | |
| const mode = rasterwave.sstvModes().find((item) => item.mode === "robot8Bw"); | |
| const pixels = new Uint8Array(mode.width * mode.height * 3); | |
| const encoders = [ | |
| { enhancedPreamble: true, stationId: { kind: "fsk", callsign: "N0CALL" }, postImageGapMs: 500, endGuardMs: 300 }, | |
| { stationId: { kind: "cw", callsign: "N0CALL", wpm: 20, toneHz: 800 }, postImageGapMs: 500, endGuardMs: 300 }, | |
| { stationId: { kind: "none" }, endGuardMs: 300 }, | |
| ].map((options) => new rasterwave.SstvEncoder(pixels, mode.mode, 12000, options)); | |
| if (encoders.some((encoder) => encoder.progress.rasterEndSample <= 0)) throw new Error("SSTV envelope progress unavailable"); | |
| Promise.all([ | |
| ...encoders.map((encoder) => encoder.dispose()), | |
| new rasterwave.SstvDecoder(12000, { outputMode: "continuousPaper", fallbackMode: "robot36", queueCapacitySamples: 24000 }, () => {}).dispose(), | |
| new rasterwave.FaxDecoder(12000, { outputMode: "continuousPaper", continuousAuto: true, queueCapacitySamples: 24000 }, () => {}).dispose(), | |
| rasterwave.correctFaxPaper(new Uint8Array(8), 4, 2, 0, []), | |
| ]).then(() => console.log("Packaged Image Radio runtime ok")).catch((error) => { console.error(error); process.exit(1); }); | |
| ') | |
| echo "✅ electron-builder package content verified: $RESOURCES_DIR" | |
| - name: Validate macOS signing and notarization | |
| if: matrix.platform == 'darwin' && github.event_name != 'pull_request' | |
| shell: bash | |
| env: | |
| APPLE_ID: ${{ secrets.APPLE_ID }} | |
| APPLE_APP_SPECIFIC_PASSWORD: ${{ secrets.APPLE_APP_SPECIFIC_PASSWORD }} | |
| APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }} | |
| run: | | |
| set -euo pipefail | |
| APP_PATH=$(find out/electron-builder -maxdepth 3 -name "*.app" -type d | head -1) | |
| if [ -z "${APP_PATH:-}" ] || [ ! -d "$APP_PATH" ]; then | |
| echo "Unable to locate packaged macOS app" >&2 | |
| find out/electron-builder -maxdepth 4 -type d >&2 || true | |
| exit 1 | |
| fi | |
| echo "=== macOS signing summary ===" | |
| echo "App: ${APP_PATH}" | |
| /usr/libexec/PlistBuddy -c 'Print :CFBundleIdentifier' "${APP_PATH}/Contents/Info.plist" | |
| /usr/libexec/PlistBuddy -c 'Print :CFBundleExecutable' "${APP_PATH}/Contents/Info.plist" | |
| codesign -dv --verbose=4 "${APP_PATH}" 2>&1 | sed -E 's/(TeamIdentifier=).*/\1***/' | |
| echo "=== Verify app code signature ===" | |
| codesign --verify --deep --strict --verbose=2 "${APP_PATH}" | |
| spctl --assess --type execute --verbose=4 "${APP_PATH}" | |
| xcrun stapler validate "${APP_PATH}" | |
| DMG_FILE=$(find out/electron-builder -maxdepth 1 -name "*.dmg" -type f | head -1) | |
| if [ -z "${DMG_FILE:-}" ] || [ ! -f "$DMG_FILE" ]; then | |
| echo "Unable to locate macOS DMG artifact" >&2 | |
| find out/electron-builder -maxdepth 1 -type f >&2 || true | |
| exit 1 | |
| fi | |
| echo "=== Notarize and staple DMG artifact ===" | |
| xcrun notarytool submit "${DMG_FILE}" \ | |
| --apple-id "${APPLE_ID}" \ | |
| --password "${APPLE_APP_SPECIFIC_PASSWORD}" \ | |
| --team-id "${APPLE_TEAM_ID}" \ | |
| --wait | |
| xcrun stapler staple "${DMG_FILE}" | |
| echo "=== Verify DMG notarization ticket ===" | |
| xcrun stapler validate "${DMG_FILE}" | |
| ZIP_FILE=$(find out/electron-builder -maxdepth 1 -name "*.zip" -type f | head -1) | |
| if [ -z "${ZIP_FILE:-}" ] || [ ! -f "$ZIP_FILE" ]; then | |
| echo "Unable to locate macOS ZIP artifact" >&2 | |
| find out/electron-builder -maxdepth 1 -type f >&2 || true | |
| exit 1 | |
| fi | |
| ZIP_CHECK_DIR=$(mktemp -d) | |
| unzip -q "${ZIP_FILE}" -d "${ZIP_CHECK_DIR}" | |
| ZIP_APP_PATH=$(find "${ZIP_CHECK_DIR}" -maxdepth 2 -name "*.app" -type d | head -1) | |
| if [ -z "${ZIP_APP_PATH:-}" ] || [ ! -d "$ZIP_APP_PATH" ]; then | |
| echo "Unable to locate app inside macOS ZIP artifact" >&2 | |
| find "${ZIP_CHECK_DIR}" -maxdepth 3 -type d >&2 || true | |
| exit 1 | |
| fi | |
| echo "=== Verify ZIP app signature and notarization ticket ===" | |
| codesign --verify --deep --strict --verbose=2 "${ZIP_APP_PATH}" | |
| spctl --assess --type execute --verbose=4 "${ZIP_APP_PATH}" | |
| xcrun stapler validate "${ZIP_APP_PATH}" | |
| rm -rf "${ZIP_CHECK_DIR}" | |
| - name: Verify Linux package metadata | |
| if: matrix.platform == 'linux' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| DEB_FILE=$(find out/electron-builder -maxdepth 1 -name "*.deb" -type f | head -1) | |
| RPM_FILE=$(find out/electron-builder -maxdepth 1 -name "*.rpm" -type f | head -1) | |
| if [ -z "${DEB_FILE}" ] || [ -z "${RPM_FILE}" ]; then | |
| echo "Missing Linux deb/rpm artifacts" >&2 | |
| find out/electron-builder -maxdepth 1 -type f >&2 || true | |
| exit 1 | |
| fi | |
| DEB_FILE=$(realpath "${DEB_FILE}") | |
| RPM_FILE=$(realpath "${RPM_FILE}") | |
| echo "Checking DEB metadata: ${DEB_FILE}" | |
| [ "$(dpkg-deb -f "${DEB_FILE}" Package)" = "tx-5dr" ] | |
| [ "$(dpkg-deb -f "${DEB_FILE}" Section)" = "utils" ] | |
| [ "$(dpkg-deb -f "${DEB_FILE}" Priority)" = "optional" ] | |
| dpkg-deb -f "${DEB_FILE}" Maintainer | grep -Fx 'BG5DRB <bg5drb@example.com>' | |
| dpkg-deb -f "${DEB_FILE}" Homepage | grep -Fx 'https://tx5dr.com' | |
| dpkg-deb -f "${DEB_FILE}" Description | head -1 | grep -Fx 'A modern digital radio station for amateur radio operators.' | |
| dpkg-deb -f "${DEB_FILE}" Description | grep -F 'A modern digital radio station for amateur radio operators. Operate FT8, FT4, and voice modes from any web browser - anywhere, anytime.' | |
| dpkg-deb -f "${DEB_FILE}" Depends | grep -F 'libgtk-3-0' | |
| dpkg-deb -f "${DEB_FILE}" Recommends | grep -F 'pulseaudio | libasound2' | |
| dpkg-deb -f "${DEB_FILE}" Suggests | grep -F 'lsb-release' | |
| DEB_TMP=$(mktemp -d) | |
| dpkg-deb -e "${DEB_FILE}" "${DEB_TMP}/control" | |
| dpkg-deb -x "${DEB_FILE}" "${DEB_TMP}/root" | |
| grep -F "update-alternatives --install '/usr/bin/tx-5dr'" "${DEB_TMP}/control/postinst" | |
| dpkg-deb -c "${DEB_FILE}" | grep -E '/opt/TX-5DR/tx-5dr$' | |
| DESKTOP_FILE="${DEB_TMP}/root/usr/share/applications/tx-5dr.desktop" | |
| grep -Fx 'Name=tx-5dr' "${DESKTOP_FILE}" | |
| grep -Fx 'Comment=A modern digital radio station for amateur radio operators. Operate FT8, FT4, and voice modes from any web browser - anywhere, anytime.' "${DESKTOP_FILE}" | |
| grep -Fx 'GenericName=Ham Radio Application' "${DESKTOP_FILE}" | |
| grep -Fx 'Exec=/opt/TX-5DR/tx-5dr %U' "${DESKTOP_FILE}" | |
| grep -Fx 'Icon=tx-5dr' "${DESKTOP_FILE}" | |
| grep -Fx 'Categories=Utility;AudioVideo;' "${DESKTOP_FILE}" | |
| echo "Checking RPM metadata: ${RPM_FILE}" | |
| rpm -qpi "${RPM_FILE}" | grep -F 'Name : tx-5dr' | |
| rpm -qpi "${RPM_FILE}" | grep -F 'Summary : A modern digital radio station for amateur radio operators.' | |
| rpm -qpi "${RPM_FILE}" | grep -F 'URL : https://tx5dr.com' | |
| rpm -qpl "${RPM_FILE}" | grep -Fx '/usr/share/applications/tx-5dr.desktop' | |
| rpm -qpl "${RPM_FILE}" | grep -Fx '/opt/TX-5DR/tx-5dr' | |
| rpm -qp --scripts "${RPM_FILE}" | grep -F "update-alternatives --install '/usr/bin/tx-5dr'" | |
| RPM_TMP=$(mktemp -d) | |
| (cd "${RPM_TMP}" && rpm2cpio "${RPM_FILE}" | cpio -id --quiet ./usr/share/applications/tx-5dr.desktop) | |
| RPM_DESKTOP="${RPM_TMP}/usr/share/applications/tx-5dr.desktop" | |
| grep -Fx 'Name=tx-5dr' "${RPM_DESKTOP}" | |
| grep -Fx 'Comment=A modern digital radio station for amateur radio operators. Operate FT8, FT4, and voice modes from any web browser - anywhere, anytime.' "${RPM_DESKTOP}" | |
| grep -Fx 'GenericName=Ham Radio Application' "${RPM_DESKTOP}" | |
| grep -Fx 'Exec=/opt/TX-5DR/tx-5dr %U' "${RPM_DESKTOP}" | |
| grep -Fx 'Icon=tx-5dr' "${RPM_DESKTOP}" | |
| grep -Fx 'Categories=Utility;AudioVideo;' "${RPM_DESKTOP}" | |
| echo "✅ Linux deb/rpm metadata verified" | |
| - name: Verify bundled VC runtime (Windows) | |
| if: matrix.platform == 'win32' | |
| shell: bash | |
| run: | | |
| set -euo pipefail | |
| ARCH="${{ matrix.arch }}" | |
| TRIPLET="win32-${ARCH}" | |
| case "${ARCH}" in | |
| x64) TOOL_ARCH="x64" ;; | |
| arm64) TOOL_ARCH="arm64" ;; | |
| *) | |
| echo "Unsupported Windows dependency scan arch: ${ARCH}" >&2 | |
| exit 1 | |
| ;; | |
| esac | |
| APP_DIR="out/electron-builder/win-unpacked" | |
| if [ ! -d "${APP_DIR}" ]; then | |
| echo "Unable to find electron-builder Windows unpacked app at ${APP_DIR}." >&2 | |
| find out/electron-builder -maxdepth 2 -type d >&2 || true | |
| exit 1 | |
| fi | |
| RUNTIME_DIR="${APP_DIR}/resources/bin/${TRIPLET}" | |
| REQUIRED_DLLS=( | |
| vcruntime140.dll | |
| vcruntime140_1.dll | |
| msvcp140.dll | |
| msvcp140_1.dll | |
| msvcp140_atomic_wait.dll | |
| ) | |
| for dll in "${REQUIRED_DLLS[@]}"; do | |
| if [ ! -f "${RUNTIME_DIR}/${dll}" ]; then | |
| echo "Missing bundled VC runtime DLL: ${RUNTIME_DIR}/${dll}" >&2 | |
| ls -la "${RUNTIME_DIR}" >&2 || true | |
| exit 1 | |
| fi | |
| done | |
| mapfile -t DUMPBIN_CANDIDATES < <( | |
| find \ | |
| "/c/Program Files/Microsoft Visual Studio/2022" \ | |
| "/c/Program Files (x86)/Microsoft Visual Studio/2022" \ | |
| -type f \ | |
| -path "*/VC/Tools/MSVC/*/bin/Hostx64/${TOOL_ARCH}/dumpbin.exe" \ | |
| 2>/dev/null | sort -V | |
| ) | |
| if [ "${#DUMPBIN_CANDIDATES[@]}" -eq 0 ]; then | |
| echo "Unable to find dumpbin.exe for ${TOOL_ARCH}; cannot verify VC dependencies." >&2 | |
| exit 1 | |
| fi | |
| DUMPBIN="${DUMPBIN_CANDIDATES[$((${#DUMPBIN_CANDIDATES[@]} - 1))]}" | |
| echo "Using dumpbin: ${DUMPBIN}" | |
| DEPS_WITH_SOURCE_FILE=$(mktemp) | |
| DEPS_FILE=$(mktemp) | |
| while IFS= read -r binary; do | |
| "${DUMPBIN}" //DEPENDENTS "${binary}" 2>/dev/null \ | |
| | grep -Eio '(MSVCP140[^[:space:]]*\.dll|VCRUNTIME140[^[:space:]]*\.dll)' \ | |
| | while IFS= read -r dll; do | |
| printf '%s\t%s\n' "${dll}" "${binary}" | |
| done || true | |
| done < <(find "${APP_DIR}" -type f \( -iname "*.exe" -o -iname "*.dll" -o -iname "*.node" \)) \ | |
| | tr '[:upper:]' '[:lower:]' \ | |
| | sort -u > "${DEPS_WITH_SOURCE_FILE}" | |
| cut -f1 "${DEPS_WITH_SOURCE_FILE}" | sort -u > "${DEPS_FILE}" | |
| echo "Detected MSVC runtime dependencies:" | |
| cat "${DEPS_FILE}" | |
| while IFS= read -r dll; do | |
| [ -z "${dll}" ] && continue | |
| if [ ! -f "${RUNTIME_DIR}/${dll}" ]; then | |
| echo "Detected VC dependency is not bundled app-locally: ${dll}" >&2 | |
| echo "Binaries requiring ${dll}:" >&2 | |
| grep -F "${dll}" "${DEPS_WITH_SOURCE_FILE}" | cut -f2- >&2 || true | |
| exit 1 | |
| fi | |
| done < "${DEPS_FILE}" | |
| - name: List build outputs (Debug) | |
| shell: bash | |
| run: | | |
| echo "=== Build Output Directory ===" | |
| ls -la out/ || echo "No out/ directory found" | |
| find out/ -type f \( -name "*.exe" -o -name "*.dmg" -o -name "*.AppImage" -o -name "*.deb" -o -name "*.rpm" -o -name "*.zip" -o -name "*.7z" -o -name "*.blockmap" \) || echo "No installable files found" | |
| - name: Prepare artifacts (rename for nightly if needed) | |
| shell: bash | |
| run: | | |
| PLATFORM="${{ matrix.platform }}" | |
| ARCH="${{ matrix.arch }}" | |
| VERSION="${{ github.event.inputs.version }}" | |
| # 创建统一的产物目录 | |
| mkdir -p release-artifacts | |
| # 判断是否为 nightly 版本(无版本号输入时) | |
| if [ -z "$VERSION" ]; then | |
| echo "📦 准备 Nightly 版本产物..." | |
| PREFIX="TX-5DR-nightly" | |
| else | |
| echo "📦 准备正式版本产物 ($VERSION)..." | |
| PREFIX="TX-5DR-${VERSION}" | |
| fi | |
| copy_single_artifact() { | |
| local search_dir="$1" | |
| local pattern="$2" | |
| local destination="$3" | |
| local require_blockmap="${4:-false}" | |
| local matches | |
| matches="$(find "$search_dir" -maxdepth 1 -name "$pattern" -type f 2>/dev/null | sort)" | |
| local count | |
| count="$(printf '%s\n' "$matches" | sed '/^$/d' | wc -l | tr -d ' ')" | |
| if [ "$count" -eq 0 ]; then | |
| echo "❌ 未找到产物: ${search_dir}/${pattern}" >&2 | |
| exit 1 | |
| fi | |
| if [ "$count" -gt 1 ]; then | |
| echo "❌ 找到多个匹配产物,请保持输出目录唯一: ${search_dir}/${pattern}" >&2 | |
| printf '%s\n' "$matches" | sed 's/^/ - /' >&2 | |
| exit 1 | |
| fi | |
| local file | |
| file="$(printf '%s\n' "$matches" | sed '/^$/d' | head -1)" | |
| cp "$file" "$destination" | |
| if [ "$require_blockmap" = "true" ]; then | |
| if [ ! -f "${file}.blockmap" ] && [[ "$file" == *.AppImage ]]; then | |
| echo "ℹ️ AppImage 仅内嵌 blockmap,额外生成外置 blockmap 供 latest.json/CI 校验使用" | |
| APP_BUILDER_BIN=$(node -p "require('app-builder-bin').appBuilderPath") | |
| "$APP_BUILDER_BIN" blockmap --input "$file" --output "${file}.blockmap" | |
| fi | |
| if [ ! -f "${file}.blockmap" ]; then | |
| echo "❌ 自动更新产物缺少 blockmap: ${file}.blockmap" >&2 | |
| exit 1 | |
| fi | |
| cp "${file}.blockmap" "${destination}.blockmap" | |
| fi | |
| } | |
| if [ "$PLATFORM" = "darwin" ]; then | |
| copy_single_artifact "out/electron-builder" "*.dmg" "release-artifacts/${PREFIX}-macos-${ARCH}.dmg" false | |
| echo "✅ 已准备: ${PREFIX}-macos-${ARCH}.dmg" | |
| copy_single_artifact "out/electron-builder" "*.zip" "release-artifacts/${PREFIX}-macos-${ARCH}.zip" true | |
| echo "✅ 已准备: ${PREFIX}-macos-${ARCH}.zip" | |
| elif [ "$PLATFORM" = "linux" ]; then | |
| copy_single_artifact "out/electron-builder" "*.deb" "release-artifacts/${PREFIX}-linux-${ARCH}.deb" false | |
| echo "✅ 已准备: ${PREFIX}-linux-${ARCH}.deb" | |
| copy_single_artifact "out/electron-builder" "*.rpm" "release-artifacts/${PREFIX}-linux-${ARCH}.rpm" false | |
| echo "✅ 已准备: ${PREFIX}-linux-${ARCH}.rpm" | |
| copy_single_artifact "out/electron-builder" "*.zip" "release-artifacts/${PREFIX}-linux-${ARCH}.zip" false | |
| echo "✅ 已准备: ${PREFIX}-linux-${ARCH}.zip" | |
| copy_single_artifact "out/electron-builder" "*.AppImage" "release-artifacts/${PREFIX}-linux-${ARCH}.AppImage" true | |
| echo "✅ 已准备: ${PREFIX}-linux-${ARCH}.AppImage" | |
| elif [ "$PLATFORM" = "win32" ]; then | |
| copy_single_artifact "out/electron-builder" "*-nsis.exe" "release-artifacts/${PREFIX}-windows-${ARCH}-nsis.exe" true | |
| echo "✅ 已准备: ${PREFIX}-windows-${ARCH}-nsis.exe ($(du -sh "release-artifacts/${PREFIX}-windows-${ARCH}-nsis.exe" | cut -f1))" | |
| copy_single_artifact "out/electron-builder" "*.zip" "release-artifacts/${PREFIX}-windows-${ARCH}.zip" false | |
| echo "✅ 已准备: ${PREFIX}-windows-${ARCH}.zip" | |
| copy_single_artifact "out/electron-builder" "*.7z" "release-artifacts/${PREFIX}-windows-${ARCH}.7z" false | |
| echo "✅ 已准备: ${PREFIX}-windows-${ARCH}.7z ($(du -sh "release-artifacts/${PREFIX}-windows-${ARCH}.7z" | cut -f1))" | |
| fi | |
| echo "=== 产物准备完成 ===" | |
| ls -lh release-artifacts/ | |
| - name: Upload artifacts (attempt 1) | |
| id: upload_artifacts_attempt_1 | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: release-artifacts/* | |
| retention-days: 30 | |
| overwrite: true | |
| - name: Wait before artifact upload retry | |
| if: steps.upload_artifacts_attempt_1.outcome == 'failure' | |
| shell: bash | |
| run: | | |
| echo "Artifact upload failed, retrying after transient backoff..." | |
| sleep 20 | |
| - name: Upload artifacts (attempt 2) | |
| if: steps.upload_artifacts_attempt_1.outcome == 'failure' | |
| id: upload_artifacts_attempt_2 | |
| continue-on-error: true | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: release-artifacts/* | |
| retention-days: 30 | |
| overwrite: true | |
| - name: Wait before final artifact upload retry | |
| if: steps.upload_artifacts_attempt_1.outcome == 'failure' && steps.upload_artifacts_attempt_2.outcome == 'failure' | |
| shell: bash | |
| run: | | |
| echo "Artifact upload failed twice, retrying one final time..." | |
| sleep 60 | |
| - name: Upload artifacts (final attempt) | |
| if: steps.upload_artifacts_attempt_1.outcome == 'failure' && steps.upload_artifacts_attempt_2.outcome == 'failure' | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ matrix.artifact_name }} | |
| path: release-artifacts/* | |
| retention-days: 30 | |
| overwrite: true | |
| create-release: | |
| needs: [build] | |
| runs-on: ubuntu-latest | |
| if: github.event_name != 'pull_request' | |
| env: | |
| OSS_ACCESS_KEY_ID: ${{ secrets.ALIYUN_ACCESS_KEY_ID }} | |
| OSS_ACCESS_KEY_SECRET: ${{ secrets.ALIYUN_ACCESS_KEY_SECRET }} | |
| OSS_BUCKET: ${{ secrets.OSS_BUCKET }} | |
| OSS_ENDPOINT: ${{ secrets.OSS_ENDPOINT }} | |
| OSS_BASE_URL: ${{ secrets.OSS_BASE_URL }} | |
| OSS_REGION: ${{ secrets.OSS_REGION }} | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 20 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '22' | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| path: dist/ | |
| - name: Prepare release assets | |
| shell: bash | |
| run: | | |
| echo "=== 准备 release 产物 ===" | |
| mkdir -p publish-assets | |
| mkdir -p release-assets | |
| find dist/ -type f \( -name "*.dmg" -o -name "*.zip" -o -name "*.deb" -o -name "*.rpm" -o -name "*.7z" -o -name "*.exe" -o -name "*.AppImage" -o -name "*.blockmap" \) -exec cp {} publish-assets/ \; | |
| find publish-assets/ -type f -exec cp {} release-assets/ \; | |
| echo "=== Release 产物列表 ===" | |
| ls -lh release-assets/ | |
| - name: Generate release info | |
| id: release_info | |
| env: | |
| TZ: UTC | |
| run: | | |
| echo "timestamp=$(date +'%Y-%m-%d %H:%M:%S UTC')" >> $GITHUB_OUTPUT | |
| echo "short_sha=$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT | |
| echo "commit_title=$(git show -s --format=%s "${{ github.sha }}")" >> $GITHUB_OUTPUT | |
| echo "date=$(date +'%Y-%m-%d')" >> $GITHUB_OUTPUT | |
| BASE_VERSION=$(node -p "require('./package.json').version.split('-')[0].split('+')[0]") | |
| COMMIT_STAMP=$(git show -s --date=format-local:%Y%m%d%H%M --format=%cd "${{ github.sha }}") | |
| if [ -z "${{ github.event.inputs.version }}" ]; then | |
| echo "tag=nightly-app" >> $GITHUB_OUTPUT | |
| echo "version=nightly" >> $GITHUB_OUTPUT | |
| echo "channel=nightly" >> $GITHUB_OUTPUT | |
| echo "channel_path=nightly" >> $GITHUB_OUTPUT | |
| echo "app_version=${BASE_VERSION}-nightly.${COMMIT_STAMP}+g$(echo ${{ github.sha }} | cut -c1-7)" >> $GITHUB_OUTPUT | |
| else | |
| echo "tag=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT | |
| echo "version=${{ github.event.inputs.version }}" >> $GITHUB_OUTPUT | |
| echo "channel=release" >> $GITHUB_OUTPUT | |
| echo "channel_path=release" >> $GITHUB_OUTPUT | |
| echo "app_version=${{ github.event.inputs.version }}" | sed 's/^app_version=v/app_version=/' >> $GITHUB_OUTPUT | |
| fi | |
| - name: Prepare app release metadata | |
| shell: bash | |
| run: | | |
| mkdir -p release-metadata | |
| cat > release-metadata/app-release-notes.md <<EOF | |
| Channel: ${{ steps.release_info.outputs.channel }} | |
| Version: ${{ steps.release_info.outputs.app_version }} | |
| Commit: ${{ steps.release_info.outputs.short_sha }} | |
| Built at: ${{ steps.release_info.outputs.timestamp }} | |
| EOF | |
| node - <<'EOF' > release-metadata/recent-commits.json | |
| const { execFileSync } = require('node:child_process'); | |
| const lines = execFileSync('git', [ | |
| 'log', | |
| '-n', | |
| '10', | |
| '--date=iso-strict', | |
| '--pretty=format:%H%x09%h%x09%s%x09%cI', | |
| process.env.GITHUB_SHA, | |
| ], { encoding: 'utf8' }).trim().split('\n').filter(Boolean); | |
| const commits = lines.map((line) => { | |
| const [id, shortId, title, publishedAt] = line.split('\t'); | |
| return { id, short_id: shortId, title, published_at: publishedAt }; | |
| }); | |
| process.stdout.write(JSON.stringify(commits)); | |
| EOF | |
| node scripts/generate-oss-manifest.mjs \ | |
| --product app \ | |
| --channel "${{ steps.release_info.outputs.channel }}" \ | |
| --tag "${{ steps.release_info.outputs.tag }}" \ | |
| --version "${{ steps.release_info.outputs.app_version }}" \ | |
| --commit "${{ steps.release_info.outputs.short_sha }}" \ | |
| --commit-title "${{ steps.release_info.outputs.commit_title }}" \ | |
| --published-at "${{ steps.release_info.outputs.timestamp }}" \ | |
| --base-url "${{ secrets.OSS_BASE_URL }}" \ | |
| --object-prefix "tx-5dr/app/${{ steps.release_info.outputs.tag }}" \ | |
| --oss-base-url "${{ secrets.OSS_BASE_URL }}" \ | |
| --oss-object-prefix "tx-5dr/app/${{ steps.release_info.outputs.tag }}" \ | |
| --github-base-url "https://github.com/${{ github.repository }}/releases/download" \ | |
| --github-object-prefix "${{ steps.release_info.outputs.tag }}" \ | |
| --assets-dir "publish-assets" \ | |
| --release-notes-file "release-metadata/app-release-notes.md" \ | |
| --recent-commits-json "$(cat release-metadata/recent-commits.json)" \ | |
| --output "release-assets/latest.json" | |
| - name: Validate app manifest | |
| shell: bash | |
| run: | | |
| node - <<'EOF' | |
| const fs = require('node:fs'); | |
| const manifest = JSON.parse(fs.readFileSync('release-assets/latest.json', 'utf8')); | |
| if (!manifest.version) throw new Error('app manifest missing version'); | |
| if (!manifest.commit_title) throw new Error('app manifest missing commit_title'); | |
| if (!Array.isArray(manifest.recent_commits) || manifest.recent_commits.length === 0 || manifest.recent_commits.length > 10) { | |
| throw new Error('app manifest missing recent_commits'); | |
| } | |
| if (manifest.recent_commits[0]?.short_id !== manifest.commit) { | |
| throw new Error('app manifest recent_commits[0] does not match top-level commit'); | |
| } | |
| if (manifest.recent_commits[0]?.title !== manifest.commit_title) { | |
| throw new Error('app manifest recent_commits[0] does not match top-level commit_title'); | |
| } | |
| if (!Array.isArray(manifest.assets) || manifest.assets.length === 0) { | |
| throw new Error('app manifest missing assets'); | |
| } | |
| const hasMacArmDmg = manifest.assets.some((asset) => asset.platform === 'macos' && asset.arch === 'arm64' && asset.package_type === 'dmg' && asset.url_cn && asset.url_global); | |
| if (!hasMacArmDmg) throw new Error('app manifest missing macOS arm64 dmg with dual urls'); | |
| const forbiddenMsi = manifest.assets.find((asset) => asset.package_type === 'msi' || asset.name.endsWith('.msi')); | |
| if (forbiddenMsi) throw new Error(`app manifest must not include MSI asset: ${forbiddenMsi.name}`); | |
| const requiredAssets = [ | |
| ['windows', 'x64', 'exe'], | |
| ['windows', 'x64', 'zip'], | |
| ['windows', 'x64', '7z'], | |
| ['macos', 'arm64', 'dmg'], | |
| ['macos', 'arm64', 'zip'], | |
| ['macos', 'x64', 'dmg'], | |
| ['macos', 'x64', 'zip'], | |
| ['linux', 'x64', 'AppImage'], | |
| ['linux', 'x64', 'deb'], | |
| ['linux', 'x64', 'rpm'], | |
| ['linux', 'x64', 'zip'], | |
| ['linux', 'arm64', 'AppImage'], | |
| ['linux', 'arm64', 'deb'], | |
| ['linux', 'arm64', 'rpm'], | |
| ['linux', 'arm64', 'zip'], | |
| ]; | |
| for (const [platform, arch, packageType] of requiredAssets) { | |
| if (!manifest.assets.some((asset) => asset.platform === platform && asset.arch === arch && asset.package_type === packageType)) { | |
| throw new Error(`app manifest missing ${platform}-${arch}-${packageType}`); | |
| } | |
| } | |
| const autoAssets = manifest.assets.filter((asset) => asset.auto_update?.supported); | |
| for (const asset of autoAssets) { | |
| if (!asset.size || !asset.sha512 || !asset.auto_update.sha512 || !asset.auto_update.installerFamily || !asset.auto_update.blockMapSize || !Array.isArray(asset.auto_update.files) || asset.auto_update.files.length === 0) { | |
| throw new Error(`app manifest missing auto_update metadata for ${asset.name}`); | |
| } | |
| for (const file of asset.auto_update.files) { | |
| if (!file.sha512 || !file.size || !file.blockMapSize) { | |
| throw new Error(`app manifest missing auto_update file metadata for ${asset.name}`); | |
| } | |
| } | |
| if (!fs.existsSync(`release-assets/${asset.name}.blockmap`)) { | |
| throw new Error(`app manifest missing blockmap asset for ${asset.name}`); | |
| } | |
| } | |
| for (const asset of manifest.assets) { | |
| if (!asset.url_cn || !asset.url_global) { | |
| throw new Error(`app manifest missing dual urls for ${asset.name}`); | |
| } | |
| } | |
| EOF | |
| # Nightly Release | |
| - name: Create or Update Nightly Release | |
| if: github.event.inputs.version == '' | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| tag: nightly-app | |
| name: "Nightly Build — Desktop App" | |
| body: | | |
| ## TX-5DR Nightly Build — Desktop App 桌面应用 | |
| **Built at** (构建时间): ${{ steps.release_info.outputs.timestamp }} | |
| **Commit**: [${{ steps.release_info.outputs.short_sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) | |
| **Branch**: ${{ github.ref_name }} | |
| ### 📦 Downloads 下载 | |
| #### Windows (x64) | |
| - [**NSIS Installer**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-windows-x64-nsis.exe) (安装包)— recommended for installation and auto-update | |
| - [**7z Archive**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-windows-x64.7z) (压缩包,推荐)— LZMA2 compression, ~100MB smaller than ZIP, requires [7-Zip](https://www.7-zip.org/) | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-windows-x64.zip) (压缩包)— larger file size | |
| #### macOS (ARM64 — Apple Silicon) | |
| - [**DMG Installer**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-macos-arm64.dmg) (安装包)— recommended | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-macos-arm64.zip) (压缩包) | |
| #### macOS (x64 — Intel) | |
| - [**DMG Installer**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-macos-x64.dmg) (安装包)— recommended | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-macos-x64.zip) (压缩包) | |
| #### Linux Desktop (x64) — Electron App | |
| - [**DEB Package**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-linux-x64.deb) (Ubuntu/Debian) | |
| - [**RPM Package**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-linux-x64.rpm) (Fedora/RHEL) | |
| - [**AppImage**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-linux-x64.AppImage) — supports in-app auto-update | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-linux-x64.zip) (压缩包) | |
| #### Linux Desktop (ARM64) — Electron App | |
| - [**DEB Package**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-linux-arm64.deb) (Ubuntu/Debian) | |
| - [**RPM Package**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-linux-arm64.rpm) (Fedora/RHEL) | |
| - [**AppImage**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-linux-arm64.AppImage) — supports in-app auto-update | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/nightly-app/TX-5DR-nightly-linux-arm64.zip) (压缩包) | |
| > Looking for **Linux Server** (headless, no Electron)? See [nightly-server](https://github.com/${{ github.repository }}/releases/tag/nightly-server). | |
| > 寻找 **Linux 服务器版**(无桌面环境)?请查看 [nightly-server](https://github.com/${{ github.repository }}/releases/tag/nightly-server)。 | |
| ### ⚠️ Notice 注意事项 | |
| - This is a development build and may contain untested features. (开发版本,可能包含未经充分测试的功能) | |
| - Automatically updated on every push to `main` / `develop`. (每次推送自动更新) | |
| ### 🔧 System Requirements 系统要求 | |
| - Node.js >= 22.15.1 (bundled / 已内置) | |
| - Windows 10+ / macOS 11+ / Ubuntu 20.04+ / Fedora 35+ | |
| allowUpdates: true | |
| removeArtifacts: true | |
| artifacts: "release-assets/*" | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| prerelease: true | |
| makeLatest: false | |
| # Official Release | |
| - name: Create Official Release | |
| if: github.event.inputs.version != '' | |
| uses: ncipollo/release-action@v1 | |
| with: | |
| tag: ${{ github.event.inputs.version }} | |
| name: "TX-5DR ${{ github.event.inputs.version }}" | |
| body: | | |
| ## TX-5DR ${{ github.event.inputs.version }} | |
| **Release Date** (发布日期): ${{ steps.release_info.outputs.date }} | |
| **Commit**: [${{ steps.release_info.outputs.short_sha }}](https://github.com/${{ github.repository }}/commit/${{ github.sha }}) | |
| ### 🚀 What's New 新功能 | |
| - See [CHANGELOG](https://github.com/${{ github.repository }}/commits/main) for details. | |
| ### 📦 Downloads 下载 | |
| #### Windows (x64) | |
| - [**NSIS Installer**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-windows-x64-nsis.exe) (安装包)— recommended for installation and auto-update | |
| - [**7z Archive**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-windows-x64.7z) (压缩包,推荐)— LZMA2 compression, ~100MB smaller than ZIP, requires [7-Zip](https://www.7-zip.org/) | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-windows-x64.zip) (压缩包)— larger file size | |
| #### macOS (ARM64 — Apple Silicon) | |
| - [**DMG Installer**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-macos-arm64.dmg) (安装包)— recommended | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-macos-arm64.zip) (压缩包) | |
| #### macOS (x64 — Intel) | |
| - [**DMG Installer**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-macos-x64.dmg) (安装包)— recommended | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-macos-x64.zip) (压缩包) | |
| #### Linux Desktop (x64) — Electron App | |
| - [**DEB Package**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-linux-x64.deb) (Ubuntu/Debian) | |
| - [**RPM Package**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-linux-x64.rpm) (Fedora/RHEL) | |
| - [**AppImage**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-linux-x64.AppImage) — supports in-app auto-update | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-linux-x64.zip) (压缩包) | |
| #### Linux Desktop (ARM64) — Electron App | |
| - [**DEB Package**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-linux-arm64.deb) (Ubuntu/Debian) | |
| - [**RPM Package**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-linux-arm64.rpm) (Fedora/RHEL) | |
| - [**AppImage**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-linux-arm64.AppImage) — supports in-app auto-update | |
| - [**ZIP Archive**](https://github.com/${{ github.repository }}/releases/download/${{ github.event.inputs.version }}/TX-5DR-${{ github.event.inputs.version }}-linux-arm64.zip) (压缩包) | |
| ### 🔧 System Requirements 系统要求 | |
| - Node.js >= 22.15.1 (bundled / 已内置) | |
| - Windows 10+ / macOS 11+ / Ubuntu 20.04+ / Fedora 35+ | |
| --- | |
| Built at (构建于): ${{ steps.release_info.outputs.timestamp }} | |
| artifacts: "release-assets/*" | |
| token: ${{ secrets.GITHUB_TOKEN }} | |
| draft: false | |
| prerelease: false | |
| makeLatest: true | |
| - name: Install ossutil | |
| if: env.OSS_ACCESS_KEY_ID != '' && env.OSS_ACCESS_KEY_SECRET != '' && env.OSS_BUCKET != '' && env.OSS_ENDPOINT != '' && env.OSS_BASE_URL != '' | |
| run: bash scripts/install-ossutil.sh | |
| - name: Configure ossutil | |
| if: env.OSS_ACCESS_KEY_ID != '' && env.OSS_ACCESS_KEY_SECRET != '' && env.OSS_BUCKET != '' && env.OSS_ENDPOINT != '' && env.OSS_BASE_URL != '' | |
| shell: bash | |
| run: | | |
| oss_region="${OSS_REGION:-}" | |
| oss_endpoint_host="${OSS_ENDPOINT#https://}" | |
| oss_endpoint_host="${oss_endpoint_host#http://}" | |
| oss_endpoint_host="${oss_endpoint_host%%/*}" | |
| if [ -z "${oss_region}" ]; then | |
| if [[ "${oss_endpoint_host}" =~ oss-([a-z0-9-]+)\.aliyuncs\.com$ ]]; then | |
| oss_region="${BASH_REMATCH[1]}" | |
| fi | |
| fi | |
| if [ -z "${oss_region}" ]; then | |
| echo "Failed to determine OSS region from OSS_ENDPOINT=${OSS_ENDPOINT}" >&2 | |
| echo "Please configure the OSS_REGION GitHub secret, for example: cn-hangzhou" >&2 | |
| exit 1 | |
| fi | |
| echo "Using OSS endpoint: ${OSS_ENDPOINT}" | |
| echo "Using OSS region: ${oss_region}" | |
| cat > "${RUNNER_TEMP}/.ossutilconfig" <<EOF | |
| [Credentials] | |
| language = EN | |
| endpoint = ${OSS_ENDPOINT} | |
| region = ${oss_region} | |
| accessKeyID = ${OSS_ACCESS_KEY_ID} | |
| accessKeySecret = ${OSS_ACCESS_KEY_SECRET} | |
| EOF | |
| - name: Generate OSS app manifest | |
| if: env.OSS_ACCESS_KEY_ID != '' && env.OSS_ACCESS_KEY_SECRET != '' && env.OSS_BUCKET != '' && env.OSS_ENDPOINT != '' && env.OSS_BASE_URL != '' | |
| run: | | |
| mkdir -p oss-metadata/app | |
| node scripts/generate-oss-manifest.mjs \ | |
| --product app \ | |
| --channel "${{ steps.release_info.outputs.channel }}" \ | |
| --tag "${{ steps.release_info.outputs.tag }}" \ | |
| --version "${{ steps.release_info.outputs.app_version }}" \ | |
| --commit "${{ steps.release_info.outputs.short_sha }}" \ | |
| --commit-title "${{ steps.release_info.outputs.commit_title }}" \ | |
| --published-at "${{ steps.release_info.outputs.timestamp }}" \ | |
| --base-url "${OSS_BASE_URL}" \ | |
| --object-prefix "tx-5dr/app/${{ steps.release_info.outputs.tag }}" \ | |
| --oss-base-url "${OSS_BASE_URL}" \ | |
| --oss-object-prefix "tx-5dr/app/${{ steps.release_info.outputs.tag }}" \ | |
| --github-base-url "https://github.com/${{ github.repository }}/releases/download" \ | |
| --github-object-prefix "${{ steps.release_info.outputs.tag }}" \ | |
| --assets-dir "publish-assets" \ | |
| --release-notes-file "release-metadata/app-release-notes.md" \ | |
| --recent-commits-json "$(cat release-metadata/recent-commits.json)" \ | |
| --output "oss-metadata/app/latest.json" | |
| - name: Validate OSS app manifest | |
| if: env.OSS_ACCESS_KEY_ID != '' && env.OSS_ACCESS_KEY_SECRET != '' && env.OSS_BUCKET != '' && env.OSS_ENDPOINT != '' && env.OSS_BASE_URL != '' | |
| shell: bash | |
| run: | | |
| node - <<'EOF' | |
| const fs = require('node:fs'); | |
| const manifest = JSON.parse(fs.readFileSync('oss-metadata/app/latest.json', 'utf8')); | |
| if (!Array.isArray(manifest.recent_commits) || manifest.recent_commits.length === 0 || manifest.recent_commits.length > 10) { | |
| throw new Error('oss app manifest missing recent_commits'); | |
| } | |
| if (manifest.recent_commits[0]?.short_id !== manifest.commit) { | |
| throw new Error('oss app manifest recent_commits[0] does not match top-level commit'); | |
| } | |
| if (manifest.recent_commits[0]?.title !== manifest.commit_title) { | |
| throw new Error('oss app manifest recent_commits[0] does not match top-level commit_title'); | |
| } | |
| const forbiddenMsi = (manifest.assets || []).find((asset) => asset.package_type === 'msi' || asset.name.endsWith('.msi')); | |
| if (forbiddenMsi) throw new Error(`oss app manifest must not include MSI asset: ${forbiddenMsi.name}`); | |
| const autoAssets = (manifest.assets || []).filter((asset) => asset.auto_update?.supported); | |
| for (const asset of autoAssets) { | |
| if (!asset.size || !asset.sha512 || !asset.auto_update.sha512 || !asset.auto_update.installerFamily || !asset.auto_update.blockMapSize || !Array.isArray(asset.auto_update.files) || asset.auto_update.files.length === 0) { | |
| throw new Error(`oss app manifest missing auto_update metadata for ${asset.name}`); | |
| } | |
| for (const file of asset.auto_update.files) { | |
| if (!file.sha512 || !file.size || !file.blockMapSize) { | |
| throw new Error(`oss app manifest missing auto_update file metadata for ${asset.name}`); | |
| } | |
| } | |
| if (!fs.existsSync(`publish-assets/${asset.name}.blockmap`)) { | |
| throw new Error(`oss app manifest missing blockmap asset for ${asset.name}`); | |
| } | |
| } | |
| EOF | |
| - name: Upload app assets to OSS | |
| if: env.OSS_ACCESS_KEY_ID != '' && env.OSS_ACCESS_KEY_SECRET != '' && env.OSS_BUCKET != '' && env.OSS_ENDPOINT != '' && env.OSS_BASE_URL != '' | |
| run: | | |
| for file in publish-assets/*; do | |
| ossutil cp "$file" "oss://${OSS_BUCKET}/tx-5dr/app/${{ steps.release_info.outputs.tag }}/$(basename "$file")" --force -c "${RUNNER_TEMP}/.ossutilconfig" | |
| done | |
| ossutil cp "oss-metadata/app/latest.json" "oss://${OSS_BUCKET}/tx-5dr/app/${{ steps.release_info.outputs.tag }}/latest.json" --force -c "${RUNNER_TEMP}/.ossutilconfig" | |
| ossutil cp "oss-metadata/app/latest.json" "oss://${OSS_BUCKET}/tx-5dr/app/${{ steps.release_info.outputs.channel_path }}/latest.json" --force -c "${RUNNER_TEMP}/.ossutilconfig" |