feat: 新增 --title-align 参数,支持 title 左/中/右对齐 - Bump version 0.1.9-beta.… #143
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 & Release | |
| on: | |
| push: | |
| branches: [main] | |
| pull_request: | |
| branches: [main] | |
| permissions: | |
| contents: write | |
| packages: write | |
| jobs: | |
| # ── 判断是否需要构建 / 发布 ────────────────────────────── | |
| check: | |
| name: Check commit message | |
| runs-on: ubuntu-latest | |
| outputs: | |
| should_build: ${{ steps.flags.outputs.should_build }} | |
| should_release: ${{ steps.flags.outputs.should_release }} | |
| should_publish: ${{ steps.flags.outputs.should_publish }} | |
| should_publish_pypi: ${{ steps.flags.outputs.should_publish_pypi }} | |
| should_publish_crates: ${{ steps.flags.outputs.should_publish_crates }} | |
| should_benchmark: ${{ steps.flags.outputs.should_benchmark }} | |
| version: ${{ steps.flags.outputs.version }} | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Parse commit message | |
| id: flags | |
| env: | |
| COMMIT_MSG: ${{ github.event.head_commit.message }} | |
| run: | | |
| MSG="$COMMIT_MSG" | |
| # 从 Cargo.toml 提取版本号 | |
| VERSION="v$(grep '^version' rust/Cargo.toml | head -1 | sed 's/.*"\(.*\)".*/\1/')" | |
| echo "version=$VERSION" >> "$GITHUB_OUTPUT" | |
| echo "📦 Version: $VERSION" | |
| # PR 始终构建 | |
| if [ "${{ github.event_name }}" = "pull_request" ]; then | |
| echo "should_build=true" >> "$GITHUB_OUTPUT" | |
| echo "should_release=false" >> "$GITHUB_OUTPUT" | |
| echo "should_publish=false" >> "$GITHUB_OUTPUT" | |
| echo "should_benchmark=false" >> "$GITHUB_OUTPUT" | |
| exit 0 | |
| fi | |
| # 初始化标志 | |
| BUILD=false | |
| RELEASE=false | |
| PUBLISH=false | |
| PUBLISH_PYPI=false | |
| PUBLISH_CRATES=false | |
| BENCHMARK=false | |
| # "build publish" = 构建 + Release + 发布到包管理器 (全套) | |
| if echo "$MSG" | grep -qi "build publish"; then | |
| BUILD=true | |
| RELEASE=true | |
| PUBLISH=true | |
| # "build release" = 构建 + GitHub Release | |
| elif echo "$MSG" | grep -qi "build release"; then | |
| BUILD=true | |
| RELEASE=true | |
| # "build action" = 仅构建 | |
| elif echo "$MSG" | grep -qi "build action"; then | |
| BUILD=true | |
| fi | |
| # "publish from release" = 从已有 Release 发布到包管理器 (不构建) | |
| if echo "$MSG" | grep -qi "publish from release"; then | |
| PUBLISH=true | |
| fi | |
| # "pypi publish" / "publish pypi" = 发布到 PyPI | |
| if echo "$MSG" | grep -qiE "pypi publish|publish pypi"; then | |
| PUBLISH_PYPI=true | |
| fi | |
| # "crates publish" / "publish crates" = 发布到 crates.io | |
| if echo "$MSG" | grep -qiE "crates publish|publish crates"; then | |
| PUBLISH_CRATES=true | |
| fi | |
| # "run benchmark" = 运行基准测试 | |
| if echo "$MSG" | grep -qi "run benchmark"; then | |
| BENCHMARK=true | |
| fi | |
| echo "should_build=$BUILD" >> "$GITHUB_OUTPUT" | |
| echo "should_release=$RELEASE" >> "$GITHUB_OUTPUT" | |
| echo "should_publish=$PUBLISH" >> "$GITHUB_OUTPUT" | |
| echo "should_publish_pypi=$PUBLISH_PYPI" >> "$GITHUB_OUTPUT" | |
| echo "should_publish_crates=$PUBLISH_CRATES" >> "$GITHUB_OUTPUT" | |
| echo "should_benchmark=$BENCHMARK" >> "$GITHUB_OUTPUT" | |
| echo "📋 Flags: build=$BUILD release=$RELEASE publish=$PUBLISH publish_pypi=$PUBLISH_PYPI publish_crates=$PUBLISH_CRATES benchmark=$BENCHMARK" | |
| # ── 同步代码到 Gitee ───────────────────────────────────── | |
| # 每次 push 都同步,与 check 并行运行 | |
| sync-gitee-code: | |
| name: 【Gitee码云】Sync Code Commit | |
| runs-on: ubuntu-latest | |
| if: github.event_name == 'push' | |
| steps: | |
| - name: Mirror to Gitee | |
| uses: Yikun/hub-mirror-action@v1.5 | |
| with: | |
| src: github/VincentZyuApps | |
| dst: gitee/vincent-zyu | |
| dst_key: ${{ secrets.GITEE_PRIVATE_KEY }} | |
| dst_token: ${{ secrets.GITEE_TOKEN }} | |
| static_list: "winload" | |
| force_update: true | |
| debug: true | |
| # ── Benchmark ─────────────────────────────────────────── | |
| benchmark: | |
| name: Run Benchmark | |
| needs: check | |
| if: needs.check.outputs.should_benchmark == 'true' | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install dependencies | |
| run: sudo apt-get update && sudo apt-get install -y time | |
| - name: Run benchmark | |
| run: | | |
| chmod +x benchmark_go/benchmark.sh | |
| cd benchmark_go | |
| ./benchmark.sh | |
| - name: Commit and push benchmark results | |
| run: | | |
| git config --global user.name "github-actions[bot]" | |
| git config --global user.email "github-actions[bot]@users.noreply.github.com" | |
| git add docs/benchmark/benchmark.svg | |
| git commit -m "docs: update benchmark results [skip ci]" || echo "No changes to commit" | |
| git push origin main | |
| # ── 多平台构建 ─────────────────────────────────────────── | |
| build: | |
| name: Build ${{ matrix.name }} | |
| needs: check | |
| if: needs.check.outputs.should_build == 'true' | |
| runs-on: ${{ matrix.os }} | |
| strategy: | |
| fail-fast: false | |
| matrix: | |
| include: | |
| # ── Windows x86_64 (MSVC, with Npcap) ── | |
| - target: x86_64-pc-windows-msvc | |
| os: windows-latest | |
| name: windows-x86_64-msvc | |
| binary: winload.exe | |
| asset: winload-windows-x86_64-msvc-npcap.exe | |
| # ── Windows x86_64 (MSVC, without Npcap) ── | |
| - target: x86_64-pc-windows-msvc | |
| os: windows-latest | |
| name: windows-x86_64-msvc-no-npcap | |
| binary: winload.exe | |
| asset: winload-windows-x86_64-msvc-no-npcap.exe | |
| no_default_features: true | |
| # ── Windows x86_64 (MinGW, no Npcap) ── | |
| - target: x86_64-pc-windows-gnu | |
| os: windows-latest | |
| name: windows-x86_64-mingw-no-npcap | |
| binary: winload.exe | |
| asset: winload-windows-x86_64-mingw-no-npcap.exe | |
| mingw: true | |
| no_default_features: true | |
| # ── Windows i686 (MSVC, without Npcap) ── | |
| - target: i686-pc-windows-msvc | |
| os: windows-latest | |
| name: windows-i686-msvc-no-npcap | |
| binary: winload.exe | |
| asset: winload-windows-i686-msvc-no-npcap.exe | |
| no_default_features: true | |
| # ── Windows i686 (MinGW, no Npcap) ── | |
| - target: i686-pc-windows-gnu | |
| os: windows-latest | |
| name: windows-i686-mingw-no-npcap | |
| binary: winload.exe | |
| asset: winload-windows-i686-mingw-no-npcap.exe | |
| mingw: true | |
| no_default_features: true | |
| # ── Linux x64 (musl 静态链接, 兼容所有 Linux) ── | |
| - target: x86_64-unknown-linux-musl | |
| os: ubuntu-latest | |
| name: linux-x86_64 | |
| binary: winload | |
| asset: winload-linux-x86_64 | |
| cross_packages: musl-tools | |
| # ── Linux i686 (musl 静态链接, 兼容所有 Linux) ── | |
| - target: i686-unknown-linux-musl | |
| os: ubuntu-latest | |
| name: linux-i686 | |
| binary: winload | |
| asset: winload-linux-i686 | |
| cross_packages: musl-tools | |
| # ── Windows ARM64 (MSVC, with Npcap) ── | |
| - target: aarch64-pc-windows-msvc | |
| os: windows-latest | |
| name: windows-aarch64-msvc | |
| binary: winload.exe | |
| asset: winload-windows-aarch64-msvc-npcap.exe | |
| # ── Windows ARM64 (MSVC, without Npcap) ── | |
| - target: aarch64-pc-windows-msvc | |
| os: windows-latest | |
| name: windows-aarch64-no-npcap | |
| binary: winload.exe | |
| asset: winload-windows-aarch64-msvc-no-npcap.exe | |
| no_default_features: true | |
| # ── Linux ARM64 (ubuntu-22.04 降低 GLIBC 要求) ── | |
| - target: aarch64-unknown-linux-gnu | |
| os: ubuntu-22.04 | |
| name: linux-aarch64 | |
| binary: winload | |
| asset: winload-linux-aarch64 | |
| cross_linker: aarch64-linux-gnu-gcc | |
| cross_packages: gcc-aarch64-linux-gnu | |
| # ── macOS x64 (build on Apple Silicon runner) ── | |
| - target: x86_64-apple-darwin | |
| os: macos-latest | |
| name: macos-x86_64 | |
| binary: winload | |
| asset: winload-macos-x86_64 | |
| # ── macOS ARM64 (Apple Silicon runner) ── | |
| - target: aarch64-apple-darwin | |
| os: macos-latest | |
| name: macos-aarch64 | |
| binary: winload | |
| asset: winload-macos-aarch64 | |
| # ── Android aarch64 (Termux ARM64) ── | |
| - target: aarch64-linux-android | |
| os: ubuntu-latest | |
| name: android-aarch64 | |
| binary: winload | |
| asset: winload-android-aarch64 | |
| # ── Android x86_64 (Termux x86_64, 模拟器 / Chromebook) ── | |
| - target: x86_64-linux-android | |
| os: ubuntu-latest | |
| name: android-x86_64 | |
| binary: winload | |
| asset: winload-android-x86_64 | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Rust toolchain | |
| uses: dtolnay/rust-toolchain@stable | |
| with: | |
| targets: ${{ matrix.target }} | |
| - name: Install MinGW-w64 | |
| if: matrix.mingw | |
| shell: pwsh | |
| run: | | |
| $arch = if ("${{ matrix.target }}" -like "*i686*") { "i686" } else { "x86_64" } | |
| $eh = if ($arch -eq "i686") { "dwarf" } else { "seh" } | |
| $url = "https://github.com/niXman/mingw-builds-binaries/releases/download/15.2.0-rt_v13-rev0/${arch}-15.2.0-release-posix-${eh}-ucrt-rt_v13-rev0.7z" | |
| $out = "$env:RUNNER_TEMP\mingw.7z" | |
| $extractDir = "$env:RUNNER_TEMP\mingw" | |
| Write-Host "Downloading MinGW ${arch}..." | |
| Invoke-WebRequest -Uri $url -OutFile $out | |
| Write-Host "Extracting..." | |
| 7z x $out -o"$extractDir" -y | Out-Null | |
| $mingwDir = (Get-ChildItem -Path $extractDir -Directory | Select-Object -First 1).FullName | |
| $binPath = Join-Path $mingwDir "bin" | |
| Write-Host "Adding to PATH: $binPath" | |
| echo "$binPath" | Out-File -Append $env:GITHUB_PATH | |
| - name: Configure MinGW linker | |
| if: matrix.mingw | |
| shell: pwsh | |
| run: | | |
| $arch = if ("${{ matrix.target }}" -like "*i686*") { "i686" } else { "x86_64" } | |
| $upper = $arch.ToUpper() | |
| echo "CARGO_TARGET_${upper}_PC_WINDOWS_GNU_LINKER=${arch}-w64-mingw32-gcc" >> $env:GITHUB_ENV | |
| echo "CC_${arch}_pc_windows_gnu=${arch}-w64-mingw32-gcc" >> $env:GITHUB_ENV | |
| - name: Cache cargo | |
| uses: Swatinem/rust-cache@v2 | |
| with: | |
| workspaces: rust -> target | |
| cache-on-failure: true | |
| key: ${{ matrix.target }} | |
| - name: Install platform toolchain | |
| if: matrix.cross_packages | |
| run: | | |
| sudo apt-get update | |
| sudo apt-get install -y ${{ matrix.cross_packages }} | |
| # i686 musl: download pre-built cross-compiler for static linking | |
| # (no GLIBC dependency — runs on ANY Linux, including old distros) | |
| - name: Setup i686 musl cross-toolchain | |
| if: matrix.target == 'i686-unknown-linux-musl' | |
| run: | | |
| # Try multiple musl mirrors until one works | |
| MUSL_MIRRORS=( | |
| "https://github.com/VincentZyuApps/winload/releases/download/i686-linux-musl-cross.tgz/i686-linux-musl-cross.tgz" | |
| "https://more.musl.cc/11/x86_64-linux-musl/i686-linux-musl-cross.tgz" | |
| ) | |
| USE_GLIBC_FALLBACK=false | |
| for mirror in "${MUSL_MIRRORS[@]}"; do | |
| echo "📥 Trying mirror: $mirror" | |
| if curl -fsSL --retry 2 --retry-delay 5 --connect-timeout 20 "$mirror" -o /tmp/musl-cross.tgz 2>&1; then | |
| echo "✅ Downloaded from $mirror" | |
| tar xzf /tmp/musl-cross.tgz -C /tmp | |
| GCC_PATH=$(find /tmp/i686-linux-musl-cross -name 'i686-linux-musl-gcc' -type f 2>/dev/null | head -1) | |
| if [ -n "$GCC_PATH" ]; then | |
| echo "CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER=$GCC_PATH" >> $GITHUB_ENV | |
| echo "✅ Using musl: $GCC_PATH" | |
| exit 0 | |
| fi | |
| fi | |
| echo "❌ Failed, trying next mirror..." | |
| done | |
| # All mirrors failed - skip this build instead of using incompatible glibc | |
| echo "⚠️ All musl mirrors failed, skipping i686 Linux build..." | |
| echo "I686_MUSL_SKIP=true" >> $GITHUB_ENV | |
| # Android: 配置 NDK 交叉编译工具链 (Termux 最低 API 24 = Android 7.0) | |
| - name: Setup Android NDK | |
| if: contains(matrix.target, 'android') | |
| shell: bash | |
| run: | | |
| NDK_HOME="${ANDROID_NDK_LATEST_HOME:-$ANDROID_NDK_HOME}" | |
| echo "📦 Using NDK: $NDK_HOME" | |
| TOOLCHAIN="$NDK_HOME/toolchains/llvm/prebuilt/linux-x86_64/bin" | |
| ls "$TOOLCHAIN" | head -20 | |
| if [[ "${{ matrix.target }}" == "aarch64-linux-android" ]]; then | |
| echo "CARGO_TARGET_AARCH64_LINUX_ANDROID_LINKER=${TOOLCHAIN}/aarch64-linux-android24-clang" >> "$GITHUB_ENV" | |
| echo "CC_aarch64_linux_android=${TOOLCHAIN}/aarch64-linux-android24-clang" >> "$GITHUB_ENV" | |
| echo "AR_aarch64_linux_android=${TOOLCHAIN}/llvm-ar" >> "$GITHUB_ENV" | |
| elif [[ "${{ matrix.target }}" == "x86_64-linux-android" ]]; then | |
| echo "CARGO_TARGET_X86_64_LINUX_ANDROID_LINKER=${TOOLCHAIN}/x86_64-linux-android24-clang" >> "$GITHUB_ENV" | |
| echo "CC_x86_64_linux_android=${TOOLCHAIN}/x86_64-linux-android24-clang" >> "$GITHUB_ENV" | |
| echo "AR_x86_64_linux_android=${TOOLCHAIN}/llvm-ar" >> "$GITHUB_ENV" | |
| fi | |
| echo "✅ Android NDK configured for ${{ matrix.target }}" | |
| # Windows: 下载 Npcap SDK,pcap crate 链接 wpcap.lib 需要 | |
| # 只在 MSVC + default-features (npcap) 构建时安装 (x86_64, aarch64) | |
| # MinGW 和 --no-default-features (no-npcap) 构建跳过 | |
| - name: Install Npcap SDK | |
| if: runner.os == 'Windows' && !matrix.no_default_features | |
| shell: pwsh | |
| run: | | |
| $sdkUrl = "https://npcap.com/dist/npcap-sdk-1.13.zip" | |
| $zipPath = "$env:RUNNER_TEMP\npcap-sdk.zip" | |
| $extractPath = "$env:RUNNER_TEMP\npcap-sdk" | |
| Write-Host "📥 Downloading Npcap SDK..." | |
| Invoke-WebRequest -Uri $sdkUrl -OutFile $zipPath | |
| Write-Host "📦 Extracting Npcap SDK..." | |
| Expand-Archive -Path $zipPath -DestinationPath $extractPath -Force | |
| # 根据 target 选择正确的 lib 目录 | |
| # Npcap SDK 1.13: x64 在 Lib/x64/, ARM64 在 Lib/ARM64/ | |
| $target = "${{ matrix.target }}" | |
| if ($target -like "*aarch64*") { | |
| $libDir = "$extractPath\Lib\ARM64" | |
| } else { | |
| $libDir = "$extractPath\Lib\x64" | |
| } | |
| Write-Host "📂 Npcap SDK lib dir: $libDir" | |
| Get-ChildItem $libDir | |
| echo "LIB=$libDir;$env:LIB" >> $env:GITHUB_ENV | |
| Write-Host "✅ Npcap SDK ready" | |
| - name: Install Linux packaging tools | |
| if: runner.os == 'Linux' && !contains(matrix.target, 'android') | |
| run: | | |
| cargo install cargo-deb cargo-generate-rpm | |
| - name: Set MinGW subsystem version | |
| if: matrix.mingw | |
| shell: bash | |
| run: | | |
| if [[ "${{ matrix.target }}" == *i686* ]]; then | |
| echo "RUSTFLAGS=-C link-args=-Wl,--major-subsystem-version,5,--minor-subsystem-version,1" >> $GITHUB_ENV | |
| else | |
| echo "RUSTFLAGS=-C link-args=-Wl,--major-subsystem-version,5,--minor-subsystem-version,2" >> $GITHUB_ENV | |
| fi | |
| - name: Build release binary | |
| if: matrix.target != 'i686-unknown-linux-musl' || !env.I686_MUSL_SKIP | |
| shell: bash | |
| working-directory: rust | |
| env: | |
| # 交叉编译时指定链接器 (仅 aarch64-linux-gnu 需要) | |
| CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: ${{ matrix.cross_linker || '' }} | |
| run: | | |
| cargo build --release --target ${{ matrix.target }} \ | |
| ${{ matrix.no_default_features && '--no-default-features' || '' }} | |
| # 验证 Linux musl 产物是否真的是静态链接 | |
| if [[ "${{ matrix.target }}" == *musl* ]]; then | |
| echo "🔍 Verifying static linking..." | |
| file target/${{ matrix.target }}/release/${{ matrix.binary }} | |
| ldd target/${{ matrix.target }}/release/${{ matrix.binary }} 2>&1 || echo "✅ Statically linked (no dynamic dependencies)" | |
| fi | |
| # Linux targets: 额外构建 DEB + RPM 包 (排除 Android, 排除 i686, 排除 musl 下载失败的情况) | |
| - name: Build DEB & RPM packages | |
| if: contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && !contains(matrix.target, 'i686') && (matrix.target != 'i686-unknown-linux-musl' || !env.I686_MUSL_SKIP) | |
| shell: bash | |
| working-directory: rust | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| echo "📦 Building DEB package for ${{ matrix.target }}..." | |
| cargo deb --target ${{ matrix.target }} --no-build | |
| # ── RPM 构建 ── | |
| # RPM 规范不允许版本号含连字符 '-',只允许字母、数字、'.', '_', '+', '~' 等字符。 | |
| # Cargo/SemVer 预发布版本用 '-'(如 0.1.8-rc.25),必须替换为 '~'(如 0.1.8~rc.25)。 | |
| # '~' 在 RPM 中表示"早于"同名正式版本,语义与预发布一致。 | |
| # DEB 自动将 '-' 转为 '~'(cargo-deb 内置处理),RPM 需要手动转换。 | |
| # cargo generate-rpm 的 --set-metadata 无法在版本校验前覆盖 version, | |
| # 因此需要临时 sed 替换 Cargo.toml 中的版本号,构建完成后 git restore 恢复。 | |
| # ⚠️ 用 tr 替代 bash 参数展开 ${var//-/~},因为 bash 在 CI 环境下 | |
| # 会将 '~' 展开为 $HOME(如 /home/runner),污染版本号。 | |
| RPM_VERSION="${VERSION#v}" | |
| RPM_VERSION=$(echo "$RPM_VERSION" | tr '-' '~') | |
| echo "🔍 RPM version (before sed): ${RPM_VERSION}" | |
| echo "🔍 Cargo.toml version (before): $(grep '^version' Cargo.toml)" | |
| sed -i "s|^version = .*|version = \"${RPM_VERSION}\"|" Cargo.toml | |
| echo "🔍 Cargo.toml version (after ): $(grep '^version' Cargo.toml)" | |
| echo "📦 Building RPM package for ${{ matrix.target }}..." | |
| cargo generate-rpm --target ${{ matrix.target }} | |
| git restore Cargo.toml | |
| echo "✅ Packages built (original names):" | |
| find target -name '*.deb' -o -name '*.rpm' | head -20 | |
| # 重命名为统一格式: winload-linux-<arch>-<version>.deb / .rpm | |
| mkdir -p renamed | |
| for f in $(find target -name '*.deb'); do | |
| cp "$f" "renamed/winload-${{ matrix.name }}-${VERSION}.deb" | |
| done | |
| for f in $(find target -name '*.rpm'); do | |
| cp "$f" "renamed/winload-${{ matrix.name }}-${VERSION}.rpm" | |
| done | |
| echo "📦 Renamed packages:" | |
| ls -lh renamed/ | |
| - name: Upload DEB artifact | |
| if: contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && !contains(matrix.target, 'i686') && (matrix.target != 'i686-unknown-linux-musl' || !env.I686_MUSL_SKIP) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: winload-${{ matrix.name }}-${{ needs.check.outputs.version }}.deb | |
| path: rust/renamed/*.deb | |
| - name: Upload RPM artifact | |
| if: contains(matrix.target, 'linux') && !contains(matrix.target, 'android') && !contains(matrix.target, 'i686') && (matrix.target != 'i686-unknown-linux-musl' || !env.I686_MUSL_SKIP) | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: winload-${{ matrix.name }}-${{ needs.check.outputs.version }}.rpm | |
| path: rust/renamed/*.rpm | |
| - name: Prepare artifact | |
| shell: bash | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| ASSET_BASE="${{ matrix.asset }}" | |
| # 在扩展名前插入版本号 | |
| # winload-linux-x86_64 -> winload-linux-x86_64-v0.1.0 | |
| # winload-windows-x86_64-msvc-npcap.exe -> winload-windows-x86_64-msvc-npcap-v0.1.0.exe | |
| if [[ "$ASSET_BASE" == *.* ]]; then | |
| # 有扩展名 | |
| BASE_NAME="${ASSET_BASE%.*}" | |
| EXTENSION="${ASSET_BASE##*.}" | |
| ASSET_WITH_VERSION="${BASE_NAME}-${VERSION}.${EXTENSION}" | |
| else | |
| # 无扩展名 | |
| ASSET_WITH_VERSION="${ASSET_BASE}-${VERSION}" | |
| fi | |
| echo "📦 Output: $ASSET_WITH_VERSION" | |
| cp "rust/target/${{ matrix.target }}/release/${{ matrix.binary }}" "$ASSET_WITH_VERSION" | |
| echo "asset_name=$ASSET_WITH_VERSION" >> "$GITHUB_ENV" | |
| - name: Upload build artifact | |
| uses: actions/upload-artifact@v4 | |
| with: | |
| name: ${{ env.asset_name }} | |
| path: ${{ env.asset_name }} | |
| # ── 发布到 GitHub Releases ───────────────────────────── | |
| release: | |
| name: Publish Release | |
| needs: [check, build] | |
| if: needs.check.outputs.should_release == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 | |
| - name: Download all artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| merge-multiple: true | |
| - name: List downloaded files | |
| run: | | |
| echo "Downloaded artifacts:" | |
| ls -lh | |
| - name: Delete existing release & tag (force fresh release) | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| echo "🗑️ Cleaning up existing release/tag: $VERSION" | |
| gh release delete "$VERSION" --yes --cleanup-tag 2>/dev/null || echo "No existing release to delete" | |
| # 等待 GitHub API 同步 | |
| sleep 3 | |
| - name: Generate release notes from commits | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| REPO="${{ github.repository }}" | |
| BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" | |
| PLAIN_VER="${VERSION#v}" | |
| PYPI_VER=$(echo "$PLAIN_VER" | sed 's/-rc\./rc/') | |
| # 用 sed 填充模板占位符,生成静态部分(下载表 + 安装指引) | |
| sed \ | |
| -e "s|__VERSION__|${VERSION}|g" \ | |
| -e "s|__PLAIN_VER__|${PLAIN_VER}|g" \ | |
| -e "s|__PYPI_VER__|${PYPI_VER}|g" \ | |
| -e "s|__REPO__|${REPO}|g" \ | |
| -e "s|__BASE_URL__|${BASE_URL}|g" \ | |
| .github/release_template.md > release_notes.md | |
| # 查找上一个 release tag(排除当前版本) | |
| PREV_TAG=$(git tag --sort=-v:refname | grep -v "^${VERSION}$" | head -1) | |
| # 追加 changelog + build info | |
| { | |
| echo "" | |
| echo "---" | |
| echo "" | |
| echo "### What's Changed" | |
| echo "" | |
| if [ -n "$PREV_TAG" ]; then | |
| echo "Changes since **${PREV_TAG}**:" | |
| echo "" | |
| git log "${PREV_TAG}..HEAD" --pretty=format:"- %s (\`%h\`) — @%an" --reverse | |
| else | |
| echo "All changes since repository creation:" | |
| echo "" | |
| git log --pretty=format:"- %s (\`%h\`) — @%an" --reverse | |
| fi | |
| echo "" | |
| echo "" | |
| echo "---" | |
| echo "" | |
| echo "### 📊 Build Info" | |
| echo "- **Build date**: $(date -u '+%Y-%m-%d %H:%M UTC')" | |
| echo "- **Commit**: [\`${GITHUB_SHA::7}\`](https://github.com/${REPO}/commit/${GITHUB_SHA})" | |
| echo "" | |
| if [ -n "$PREV_TAG" ]; then | |
| echo "**Full Changelog**: https://github.com/${REPO}/compare/${PREV_TAG}...${VERSION}" | |
| else | |
| echo "**Full Changelog**: https://github.com/${REPO}/commits/${VERSION}" | |
| fi | |
| } >> release_notes.md | |
| echo "📝 Generated release notes:" | |
| cat release_notes.md | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| # 收集所有要上传的文件(二进制 + DEB + RPM,统一 winload-*-VERSION* 格式) | |
| shopt -s nullglob | |
| FILES=(winload-*-${VERSION}*) | |
| echo "📤 Uploading ${#FILES[@]} files:" | |
| printf ' - %s\n' "${FILES[@]}" | |
| gh release create "$VERSION" \ | |
| --title "winload ${VERSION}" \ | |
| --notes-file release_notes.md \ | |
| --latest \ | |
| "${FILES[@]}" | |
| # ── 更新 Scoop Bucket (GitHub) ───────────────────────────── | |
| # 触发方式: | |
| # - "publish from release" → 从已有 Release 拉取二进制,仅更新 Scoop | |
| # - "build publish" → 构建 + Release + 更新 Scoop(全套) | |
| publish-scoop-github: | |
| name: Update Scoop Bucket | |
| needs: [check, release] | |
| # always() 让此 job 即使 release 被跳过也能运行 | |
| if: always() && needs.check.outputs.should_publish == 'true' && (needs.release.result == 'success' || needs.release.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Fetch latest release info | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| REPO="${{ github.repository }}" | |
| BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" | |
| echo "📦 Target version: ${VERSION}" | |
| echo "📥 Downloading Windows binaries from release..." | |
| # 下载 Windows x64 | |
| curl -fSL -o winload-windows-x86_64.exe \ | |
| "${BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe" \ | |
| || { echo "❌ Failed to download x64 binary"; exit 1; } | |
| # 下载 Windows ARM64 | |
| curl -fSL -o winload-windows-aarch64.exe \ | |
| "${BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe" \ | |
| || { echo "❌ Failed to download arm64 binary"; exit 1; } | |
| echo "✅ Downloaded:" | |
| ls -lh winload-windows-*.exe | |
| - name: Generate Scoop manifest | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| SCOOP_VERSION="${VERSION#v}" | |
| REPO="${{ github.repository }}" | |
| BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" | |
| # 计算 sha256 | |
| HASH_X64=$(sha256sum winload-windows-x86_64.exe | awk '{print $1}') | |
| HASH_ARM64=$(sha256sum winload-windows-aarch64.exe | awk '{print $1}') | |
| echo "📦 Version: $SCOOP_VERSION" | |
| echo "🔑 x64 hash: $HASH_X64" | |
| echo "🔑 arm64 hash: $HASH_ARM64" | |
| cat > winload.json << EOF | |
| { | |
| "version": "${SCOOP_VERSION}", | |
| "description": "Network Load Monitor - nload for Windows/Linux/macOS", | |
| "homepage": "https://github.com/${REPO}", | |
| "license": "MIT", | |
| "architecture": { | |
| "64bit": { | |
| "url": "${BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe", | |
| "hash": "${HASH_X64}", | |
| "bin": [["winload-windows-x86_64-msvc-npcap-${VERSION}.exe", "win-nload"]] | |
| }, | |
| "arm64": { | |
| "url": "${BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe", | |
| "hash": "${HASH_ARM64}", | |
| "bin": [["winload-windows-aarch64-msvc-npcap-${VERSION}.exe", "win-nload"]] | |
| } | |
| }, | |
| "checkver": { | |
| "github": "https://github.com/${REPO}" | |
| }, | |
| "autoupdate": { | |
| "architecture": { | |
| "64bit": { | |
| "url": "https://github.com/${REPO}/releases/download/v\$version/winload-windows-x86_64-msvc-npcap-v\$version.exe" | |
| }, | |
| "arm64": { | |
| "url": "https://github.com/${REPO}/releases/download/v\$version/winload-windows-aarch64-msvc-npcap-v\$version.exe" | |
| } | |
| } | |
| } | |
| } | |
| EOF | |
| echo "📝 Generated winload.json:" | |
| cat winload.json | |
| - name: Push to scoop-bucket repo | |
| env: | |
| SCOOP_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| git clone https://x-access-token:${SCOOP_TOKEN}@github.com/VincentZyuApps/scoop-bucket.git scoop-repo | |
| cp winload.json scoop-repo/bucket/winload.json | |
| cd scoop-repo | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add bucket/winload.json | |
| git commit -m "🍺 Update winload to ${VERSION}" || echo "No changes to commit" | |
| git push | |
| echo "✅ Scoop bucket updated!" | |
| # ── 更新 Scoop Bucket (Gitee) ────────────────────────────── | |
| # 生成指向 Gitee Releases 的 scoop manifest,推送到 Gitee | |
| publish-scoop-gitee: | |
| name: 【Gitee码云】Update Scoop Bucket | |
| needs: [check, release] | |
| if: always() && needs.check.outputs.should_publish == 'true' && (needs.release.result == 'success' || needs.release.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Download Windows binaries from GitHub Releases (for hash) | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| REPO="${{ github.repository }}" | |
| BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" | |
| echo "📦 Target version: ${VERSION}" | |
| echo "📥 Downloading Windows binaries from GitHub release..." | |
| curl -fSL -o winload-windows-x86_64.exe \ | |
| "${BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe" \ | |
| || { echo "❌ Failed to download x64 binary"; exit 1; } | |
| curl -fSL -o winload-windows-aarch64.exe \ | |
| "${BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe" \ | |
| || { echo "❌ Failed to download arm64 binary"; exit 1; } | |
| echo "✅ Downloaded:" | |
| ls -lh winload-windows-*.exe | |
| - name: Generate Gitee Scoop manifest | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| SCOOP_VERSION="${VERSION#v}" | |
| GITEE_BASE_URL="https://gitee.com/vincent-zyu/winload/releases/download/${VERSION}" | |
| # 计算 sha256 | |
| HASH_X64=$(sha256sum winload-windows-x86_64.exe | awk '{print $1}') | |
| HASH_ARM64=$(sha256sum winload-windows-aarch64.exe | awk '{print $1}') | |
| echo "📦 Version: $SCOOP_VERSION" | |
| echo "🔑 x64 hash: $HASH_X64" | |
| echo "🔑 arm64 hash: $HASH_ARM64" | |
| cat > winload.json << EOF | |
| { | |
| "version": "${SCOOP_VERSION}", | |
| "description": "Network Load Monitor - nload for Windows/Linux/macOS", | |
| "homepage": "https://gitee.com/vincent-zyu/winload", | |
| "license": "MIT", | |
| "architecture": { | |
| "64bit": { | |
| "url": "${GITEE_BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe", | |
| "hash": "${HASH_X64}", | |
| "bin": [["winload-windows-x86_64-msvc-npcap-${VERSION}.exe", "win-nload"]] | |
| }, | |
| "arm64": { | |
| "url": "${GITEE_BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe", | |
| "hash": "${HASH_ARM64}", | |
| "bin": [["winload-windows-aarch64-msvc-npcap-${VERSION}.exe", "win-nload"]] | |
| } | |
| }, | |
| "checkver": { | |
| "url": "https://gitee.com/vincent-zyu/winload/releases" | |
| }, | |
| "autoupdate": { | |
| "architecture": { | |
| "64bit": { | |
| "url": "https://gitee.com/vincent-zyu/winload/releases/download/v\$version/winload-windows-x86_64-msvc-npcap-v\$version.exe" | |
| }, | |
| "arm64": { | |
| "url": "https://gitee.com/vincent-zyu/winload/releases/download/v\$version/winload-windows-aarch64-msvc-npcap-v\$version.exe" | |
| } | |
| } | |
| } | |
| } | |
| EOF | |
| echo "📝 Generated Gitee winload.json:" | |
| cat winload.json | |
| - name: Push to Gitee scoop-bucket repo | |
| env: | |
| GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| git clone https://vincent-zyu:${GITEE_TOKEN}@gitee.com/vincent-zyu/scoop-bucket.git scoop-repo | |
| cp winload.json scoop-repo/bucket/winload.json | |
| cd scoop-repo | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add bucket/winload.json | |
| git commit -m "🍺 Update winload to ${VERSION}" || echo "No changes to commit" | |
| git push | |
| echo "✅ Gitee Scoop bucket updated!" | |
| # ── 更新 Homebrew Tap (GitHub) ───────────────────────────── | |
| # 生成指向 GitHub Releases 的 homebrew formula,推送到 GitHub homebrew-tap | |
| publish-homebrew-github: | |
| name: Update Homebrew Tap | |
| needs: [check, release] | |
| if: always() && needs.check.outputs.should_publish == 'true' && (needs.release.result == 'success' || needs.release.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Fetch latest release info | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| REPO="${{ github.repository }}" | |
| BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" | |
| echo "📦 Target version: ${VERSION}" | |
| # 下载所有平台的二进制用于计算 SHA256 | |
| PLATFORMS=( | |
| "winload-linux-x86_64:${BASE_URL}/winload-linux-x86_64-${VERSION}" | |
| "winload-linux-aarch64:${BASE_URL}/winload-linux-aarch64-${VERSION}" | |
| "winload-macos-x86_64:${BASE_URL}/winload-macos-x86_64-${VERSION}" | |
| "winload-macos-aarch64:${BASE_URL}/winload-macos-aarch64-${VERSION}" | |
| ) | |
| for entry in "${PLATFORMS[@]}"; do | |
| NAME="${entry%%:*}" | |
| URL="${entry##*:}" | |
| echo "📥 Downloading $NAME..." | |
| curl -fSL -o "$NAME" "$URL" || echo "⚠️ Failed to download $NAME (may not exist)" | |
| done | |
| echo "✅ Downloaded:" | |
| ls -lh winload-* 2>/dev/null || echo "No binaries downloaded" | |
| - name: Generate Homebrew formula | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| TAGVER="${VERSION#v}" # 去掉 v 前缀 | |
| REPO="${{ github.repository }}" | |
| BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" | |
| mkdir -p Formula | |
| # 计算各平台 SHA256 | |
| SHA_LINUX_X64=$(sha256sum winload-linux-x86_64 2>/dev/null | awk '{print $1}' || echo "") | |
| SHA_LINUX_ARM64=$(sha256sum winload-linux-aarch64 2>/dev/null | awk '{print $1}' || echo "") | |
| SHA_MACOS_X64=$(sha256sum winload-macos-x86_64 2>/dev/null | awk '{print $1}' || echo "") | |
| SHA_MACOS_ARM64=$(sha256sum winload-macos-aarch64 2>/dev/null | awk '{print $1}' || echo "") | |
| cat > Formula/winload.rb << HEREDOC | |
| class Winload < Formula | |
| desc "Network Load Monitor - nload-like TUI tool for Windows/Linux/macOS" | |
| homepage "https://github.com/${REPO}" | |
| license "MIT" | |
| version "${TAGVER}" | |
| # Dynamic URL generation per platform | |
| if OS.mac? && Hardware::CPU.intel? | |
| url "${BASE_URL}/winload-macos-x86_64-${VERSION}" | |
| sha256 "${SHA_MACOS_X64}" | |
| elsif OS.mac? | |
| url "${BASE_URL}/winload-macos-aarch64-${VERSION}" | |
| sha256 "${SHA_MACOS_ARM64}" | |
| elsif OS.linux? && Hardware::CPU.intel? | |
| url "${BASE_URL}/winload-linux-x86_64-${VERSION}" | |
| sha256 "${SHA_LINUX_X64}" | |
| elsif OS.linux? | |
| url "${BASE_URL}/winload-linux-aarch64-${VERSION}" | |
| sha256 "${SHA_LINUX_ARM64}" | |
| end | |
| def install | |
| bin.install Dir["winload-*"].first => "winload" | |
| end | |
| test do | |
| system "#{bin}/winload", "--version" | |
| end | |
| end | |
| HEREDOC | |
| # 移除 heredoc 缩进 | |
| sed -i 's/^ //' Formula/winload.rb | |
| echo "📝 Generated Formula/winload.rb:" | |
| cat Formula/winload.rb | |
| - name: Push to homebrew-tap repo | |
| env: | |
| GH_TOKEN: ${{ secrets.SCOOP_BUCKET_TOKEN }} # 复用已有 token | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| git clone https://x-access-token:${GH_TOKEN}@github.com/VincentZyuApps/homebrew-tap.git brew-repo | |
| cp Formula/winload.rb brew-repo/Formula/winload.rb | |
| cd brew-repo | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Formula/winload.rb | |
| git commit -m "🍺 Update winload to ${VERSION}" || echo "No changes to commit" | |
| git push | |
| echo "✅ Homebrew tap updated!" | |
| # ── 更新 Homebrew Tap (Gitee) ───────────────────────────── | |
| # 生成指向 Gitee Releases 的 homebrew formula,推送到 Gitee homebrew-tap | |
| publish-homebrew-gitee: | |
| name: 【Gitee码云】Update Homebrew Tap | |
| needs: [check, release] | |
| if: always() && needs.check.outputs.should_publish == 'true' && (needs.release.result == 'success' || needs.release.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Fetch latest release info from Gitee | |
| env: | |
| GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| GITEE_BASE_URL="https://gitee.com/vincent-zyu/winload/releases/download/${VERSION}" | |
| echo "📦 Target version: ${VERSION}" | |
| # 下载所有平台的二进制用于计算 SHA256 | |
| PLATFORMS=( | |
| "winload-linux-x86_64:${GITEE_BASE_URL}/winload-linux-x86_64-${VERSION}" | |
| "winload-linux-aarch64:${GITEE_BASE_URL}/winload-linux-aarch64-${VERSION}" | |
| "winload-macos-x86_64:${GITEE_BASE_URL}/winload-macos-x86_64-${VERSION}" | |
| "winload-macos-aarch64:${GITEE_BASE_URL}/winload-macos-aarch64-${VERSION}" | |
| ) | |
| for entry in "${PLATFORMS[@]}"; do | |
| NAME="${entry%%:*}" | |
| URL="${entry##*:}" | |
| echo "📥 Downloading $NAME from Gitee..." | |
| curl -fSL -o "$NAME" "$URL" || echo "⚠️ Failed to download $NAME (may not exist)" | |
| done | |
| echo "✅ Downloaded:" | |
| ls -lh winload-* 2>/dev/null || echo "No binaries downloaded" | |
| - name: Generate Homebrew formula (Gitee) | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| TAGVER="${VERSION#v}" | |
| GITEE_BASE_URL="https://gitee.com/vincent-zyu/winload/releases/download/${VERSION}" | |
| mkdir -p Formula | |
| # 计算各平台 SHA256 | |
| SHA_LINUX_X64=$(sha256sum winload-linux-x86_64 2>/dev/null | awk '{print $1}' || echo "") | |
| SHA_LINUX_ARM64=$(sha256sum winload-linux-aarch64 2>/dev/null | awk '{print $1}' || echo "") | |
| SHA_MACOS_X64=$(sha256sum winload-macos-x86_64 2>/dev/null | awk '{print $1}' || echo "") | |
| SHA_MACOS_ARM64=$(sha256sum winload-macos-aarch64 2>/dev/null | awk '{print $1}' || echo "") | |
| cat > Formula/winload.rb << HEREDOC | |
| class Winload < Formula | |
| desc "Network Load Monitor - nload-like TUI tool for Windows/Linux/macOS" | |
| homepage "https://gitee.com/vincent-zyu/winload" | |
| license "MIT" | |
| version "${TAGVER}" | |
| # Dynamic URL generation per platform (Gitee) | |
| if OS.mac? && Hardware::CPU.intel? | |
| url "${GITEE_BASE_URL}/winload-macos-x86_64-${VERSION}" | |
| sha256 "${SHA_MACOS_X64}" | |
| elsif OS.mac? | |
| url "${GITEE_BASE_URL}/winload-macos-aarch64-${VERSION}" | |
| sha256 "${SHA_MACOS_ARM64}" | |
| elsif OS.linux? && Hardware::CPU.intel? | |
| url "${GITEE_BASE_URL}/winload-linux-x86_64-${VERSION}" | |
| sha256 "${SHA_LINUX_X64}" | |
| elsif OS.linux? | |
| url "${GITEE_BASE_URL}/winload-linux-aarch64-${VERSION}" | |
| sha256 "${SHA_LINUX_ARM64}" | |
| end | |
| def install | |
| bin.install Dir["winload-*"].first => "winload" | |
| end | |
| test do | |
| system "#{bin}/winload", "--version" | |
| end | |
| end | |
| HEREDOC | |
| # 移除 heredoc 缩进 | |
| sed -i 's/^ //' Formula/winload.rb | |
| echo "📝 Generated Formula/winload.rb (Gitee):" | |
| cat Formula/winload.rb | |
| - name: Push to Gitee homebrew-tap repo | |
| env: | |
| GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| git clone https://vincent-zyu:${GITEE_TOKEN}@gitee.com/vincent-zyu/homebrew-tap.git brew-repo | |
| cp Formula/winload.rb brew-repo/Formula/winload.rb | |
| cd brew-repo | |
| git config user.name "github-actions[bot]" | |
| git config user.email "github-actions[bot]@users.noreply.github.com" | |
| git add Formula/winload.rb | |
| git commit -m "🍺 Update winload to ${VERSION}" || echo "No changes to commit" | |
| git push | |
| echo "✅ Gitee Homebrew tap updated!" | |
| # ── 更新 AUR 包(winload-rust-bin: 预编译二进制)───────────── | |
| publish-aur-bin: | |
| name: Update AUR Package (winload-rust-bin) | |
| needs: [check, release] | |
| if: always() && needs.check.outputs.should_publish == 'true' && (needs.release.result == 'success' || needs.release.result == 'skipped') | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Download Linux binaries from release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| REPO="${{ github.repository }}" | |
| BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" | |
| echo "📥 Downloading x86_64 binary..." | |
| curl -fSL -o winload-linux-x86_64 "${BASE_URL}/winload-linux-x86_64-${VERSION}" | |
| SHA256_X86=$(sha256sum winload-linux-x86_64 | awk '{print $1}') | |
| echo "🔑 x86_64 SHA256: $SHA256_X86" | |
| echo "SHA256_X86=$SHA256_X86" >> "$GITHUB_ENV" | |
| echo "📥 Downloading aarch64 binary..." | |
| curl -fSL -o winload-linux-aarch64 "${BASE_URL}/winload-linux-aarch64-${VERSION}" | |
| SHA256_ARM=$(sha256sum winload-linux-aarch64 | awk '{print $1}') | |
| echo "🔑 aarch64 SHA256: $SHA256_ARM" | |
| echo "SHA256_ARM=$SHA256_ARM" >> "$GITHUB_ENV" | |
| - name: Generate PKGBUILD | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| TAGVER="${VERSION#v}" # 0.1.6-beta.3 (for download URL) | |
| PKGVER="${TAGVER//-/.}" # 0.1.6.beta.3 (AUR compliant) | |
| cat > PKGBUILD << HEREDOC_END | |
| # Maintainer: VincentZyu <vincentzyu233@gmail.com> | |
| pkgname=winload-rust-bin | |
| pkgver=${PKGVER} | |
| pkgrel=1 | |
| pkgdesc="A lightweight, real-time CLI tool for monitoring network bandwidth and traffic" | |
| arch=('x86_64' 'aarch64') | |
| url="https://github.com/VincentZyuApps/winload" | |
| license=('MIT') | |
| provides=('winload') | |
| conflicts=('winload' 'winload-rust') | |
| _tagver=${TAGVER} | |
| _base_url="https://github.com/VincentZyuApps/winload/releases/download/v\${_tagver}" | |
| source_x86_64=("winload-linux-x86_64-v\${_tagver}::\${_base_url}/winload-linux-x86_64-v\${_tagver}") | |
| source_aarch64=("winload-linux-aarch64-v\${_tagver}::\${_base_url}/winload-linux-aarch64-v\${_tagver}") | |
| noextract=() | |
| sha256sums_x86_64=('${SHA256_X86}') | |
| sha256sums_aarch64=('${SHA256_ARM}') | |
| package() { | |
| if [[ "\$CARCH" == "x86_64" ]]; then | |
| install -Dm755 "\$srcdir/winload-linux-x86_64-v\${_tagver}" "\$pkgdir/usr/bin/winload" | |
| elif [[ "\$CARCH" == "aarch64" ]]; then | |
| install -Dm755 "\$srcdir/winload-linux-aarch64-v\${_tagver}" "\$pkgdir/usr/bin/winload" | |
| fi | |
| } | |
| HEREDOC_END | |
| # Remove leading whitespace (heredoc indentation) | |
| sed -i 's/^ //' PKGBUILD | |
| echo "📝 Generated PKGBUILD:" | |
| cat PKGBUILD | |
| - name: Generate .SRCINFO | |
| uses: docker://archlinux:latest | |
| with: | |
| args: bash -c "pacman -Sy --noconfirm pacman-contrib base-devel && useradd -m builder && cp PKGBUILD /home/builder/ && cd /home/builder && sudo -u builder makepkg --printsrcinfo > .SRCINFO && cp .SRCINFO /github/workspace/" | |
| - name: Push to AUR | |
| env: | |
| AUR_SSH_KEY: ${{ secrets.AUR_SSH_KEY }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| TAGVER="${VERSION#v}" # 0.1.6-beta.3 (for download URL) | |
| PKGVER="${TAGVER//-/.}" # 0.1.6.beta.3 (AUR compliant) | |
| # Setup SSH for AUR | |
| mkdir -p ~/.ssh | |
| echo "$AUR_SSH_KEY" > ~/.ssh/aur | |
| chmod 600 ~/.ssh/aur | |
| cat >> ~/.ssh/config << 'SSHEOF' | |
| Host aur.archlinux.org | |
| IdentityFile ~/.ssh/aur | |
| User aur | |
| StrictHostKeyChecking no | |
| SSHEOF | |
| # Clone AUR repo | |
| git clone ssh://aur@aur.archlinux.org/winload-rust-bin.git aur-repo || { | |
| echo "⚠️ AUR repo doesn't exist yet. Please create it manually first." | |
| echo " Run: ssh aur@aur.archlinux.org setup-repo winload-rust-bin" | |
| exit 1 | |
| } | |
| # Copy files | |
| cp PKGBUILD aur-repo/ | |
| cp .SRCINFO aur-repo/ | |
| # Commit and push | |
| cd aur-repo | |
| git config user.name "VincentZyu" | |
| git config user.email "vincentzyu233@gmail.com" | |
| git add PKGBUILD .SRCINFO | |
| git commit -m "Update to ${PKGVER}" || echo "No changes to commit" | |
| git push | |
| echo "✅ winload-rust-bin updated!" | |
| # ── 发布到 npm(@vincentzyuapps/winload: 预编译二进制)──────────── | |
| # 原理同 esbuild / @biomejs/biome / turbo: | |
| # 1. 为每个平台发布一个只含二进制的包 (os/cpu 字段让 npm 自动筛选) | |
| # 2. 主包 @vincentzyuapps/winload 通过 optionalDependencies 引用它们 | |
| # 3. 用户 npm install 时只下载匹配当前平台的那一个 | |
| # 4. 同时发布到 GitHub Packages (npm.pkg.github.com) | |
| publish-npm: | |
| name: Publish to npm | |
| needs: [check, release] | |
| if: always() && needs.check.outputs.should_publish == 'true' && (needs.release.result == 'success' || needs.release.result == 'skipped') | |
| permissions: | |
| id-token: write | |
| packages: write | |
| contents: read | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Setup Node.js | |
| uses: actions/setup-node@v4 | |
| with: | |
| node-version: '20' | |
| registry-url: 'https://registry.npmjs.org' | |
| - name: Download binaries from release | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| REPO="${{ github.repository }}" | |
| BASE_URL="https://github.com/${REPO}/releases/download/${VERSION}" | |
| mkdir -p artifacts | |
| echo "📥 Downloading binaries for npm publish..." | |
| curl -fSL -o artifacts/winload-windows-x86_64.exe "${BASE_URL}/winload-windows-x86_64-msvc-npcap-${VERSION}.exe" | |
| curl -fSL -o artifacts/winload-windows-aarch64.exe "${BASE_URL}/winload-windows-aarch64-msvc-npcap-${VERSION}.exe" | |
| curl -fSL -o artifacts/winload-linux-x86_64 "${BASE_URL}/winload-linux-x86_64-${VERSION}" | |
| curl -fSL -o artifacts/winload-linux-aarch64 "${BASE_URL}/winload-linux-aarch64-${VERSION}" | |
| curl -fSL -o artifacts/winload-macos-x86_64 "${BASE_URL}/winload-macos-x86_64-${VERSION}" | |
| curl -fSL -o artifacts/winload-macos-aarch64 "${BASE_URL}/winload-macos-aarch64-${VERSION}" | |
| echo "✅ Downloaded:" | |
| ls -lh artifacts/ | |
| - name: Publish platform packages | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| NPM_VERSION="${VERSION#v}" | |
| NPM_TAG="latest" | |
| echo "📦 npm version: ${NPM_VERSION} (tag: ${NPM_TAG})" | |
| # 平台定义: npm包名|os|cpu|源文件|二进制名 | |
| PLATFORMS=( | |
| "@vincentzyuapps/winload-win32-x64|win32|x64|artifacts/winload-windows-x86_64.exe|winload.exe" | |
| "@vincentzyuapps/winload-win32-arm64|win32|arm64|artifacts/winload-windows-aarch64.exe|winload.exe" | |
| "@vincentzyuapps/winload-linux-x64|linux|x64|artifacts/winload-linux-x86_64|winload" | |
| "@vincentzyuapps/winload-linux-arm64|linux|arm64|artifacts/winload-linux-aarch64|winload" | |
| "@vincentzyuapps/winload-darwin-x64|darwin|x64|artifacts/winload-macos-x86_64|winload" | |
| "@vincentzyuapps/winload-darwin-arm64|darwin|arm64|artifacts/winload-macos-aarch64|winload" | |
| ) | |
| for entry in "${PLATFORMS[@]}"; do | |
| IFS='|' read -r PKG_NAME PKG_OS PKG_CPU SOURCE_BIN BIN_NAME <<< "$entry" | |
| echo "" | |
| echo "📦 Publishing ${PKG_NAME}@${NPM_VERSION}..." | |
| PKG_DIR="npm-platforms/${PKG_NAME}" | |
| mkdir -p "${PKG_DIR}/bin" | |
| cp "${SOURCE_BIN}" "${PKG_DIR}/bin/${BIN_NAME}" | |
| chmod +x "${PKG_DIR}/bin/${BIN_NAME}" | |
| cat > "${PKG_DIR}/package.json" << PKGJSON | |
| { | |
| "name": "${PKG_NAME}", | |
| "version": "${NPM_VERSION}", | |
| "description": "winload binary for ${PKG_OS}-${PKG_CPU}", | |
| "license": "MIT", | |
| "repository": { | |
| "type": "git", | |
| "url": "https://github.com/${{ github.repository }}" | |
| }, | |
| "os": ["${PKG_OS}"], | |
| "cpu": ["${PKG_CPU}"], | |
| "files": ["bin/"] | |
| } | |
| PKGJSON | |
| cd "${PKG_DIR}" | |
| npm publish --provenance --access public --tag "${NPM_TAG}" 2>&1 || echo "⚠️ Failed to publish ${PKG_NAME} (may already exist)" | |
| cd "$GITHUB_WORKSPACE" | |
| done | |
| - name: Publish main package | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| NPM_VERSION="${VERSION#v}" | |
| NPM_TAG="latest" | |
| # 复制项目根目录的 readme.md 作为 npm 页面展示 | |
| cp readme.md npm/winload-rust-bin/readme.md | |
| cd npm/winload-rust-bin | |
| # 动态更新 package.json 中的版本号(版本以 Cargo.toml 为唯一真实源) | |
| node -e " | |
| const fs = require('fs'); | |
| const pkg = JSON.parse(fs.readFileSync('package.json', 'utf8')); | |
| pkg.version = '${NPM_VERSION}'; | |
| for (const dep of Object.keys(pkg.optionalDependencies || {})) { | |
| pkg.optionalDependencies[dep] = '${NPM_VERSION}'; | |
| } | |
| fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\n'); | |
| " | |
| echo "📦 Publishing @vincentzyuapps/winload@${NPM_VERSION} (tag: ${NPM_TAG})..." | |
| cat package.json | |
| npm publish --provenance --access public --tag "${NPM_TAG}" | |
| echo "✅ npm packages published!" | |
| # ── 同步发布到 GitHub Packages ── | |
| # 直接在每个包目录写入 .npmrc,确保 npm 能找到 npm.pkg.github.com 的认证。 | |
| # (actions/setup-node 生成的 .npmrc 只包含 registry.npmjs.org 的认证) | |
| - name: Publish platform packages to GitHub Packages | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| VERSION="${{ needs.check.outputs.version }}" | |
| NPM_VERSION="${VERSION#v}" | |
| NPM_TAG="latest" | |
| for dir in npm-platforms/@vincentzyuapps/*/; do | |
| [ -d "$dir" ] || continue | |
| echo "📦 Publishing $(basename $dir) to GitHub Packages..." | |
| cd "$dir" | |
| echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc | |
| npm publish --registry https://npm.pkg.github.com --tag "${NPM_TAG}" 2>&1 || echo "⚠️ Failed to publish $(basename $dir) to GitHub Packages (may already exist)" | |
| cd "$GITHUB_WORKSPACE" | |
| done | |
| - name: Publish main package to GitHub Packages | |
| env: | |
| NODE_AUTH_TOKEN: ${{ secrets.GITHUB_TOKEN }} | |
| run: | | |
| cd npm/winload-rust-bin | |
| echo "//npm.pkg.github.com/:_authToken=${NODE_AUTH_TOKEN}" > .npmrc | |
| npm publish --registry https://npm.pkg.github.com --tag "latest" | |
| echo "✅ GitHub Packages published!" | |
| # ── 发布到 PyPI (Python 包) ─────────────────────────────── | |
| publish-pypi: | |
| name: Publish to PyPI | |
| needs: check | |
| if: needs.check.outputs.should_publish_pypi == 'true' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install uv | |
| uses: astral-sh/setup-uv@v4 | |
| - name: Copy readme for PyPI | |
| run: cp readme.md py/readme.md | |
| - name: Build package | |
| working-directory: py | |
| run: | | |
| uv build | |
| - name: Publish to PyPI | |
| working-directory: py | |
| env: | |
| UV_PUBLISH_TOKEN: ${{ secrets.PYPI_TOKEN }} | |
| run: | | |
| uv publish --publish-url https://upload.pypi.org/legacy/ | |
| # ── 发布到 crates.io (Rust 包) ─────────────────────────────── | |
| publish-crates-io: | |
| name: Publish to crates.io | |
| needs: [check, build] | |
| if: needs.check.outputs.should_publish_crates == 'true' && needs.build.result == 'success' | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Install Rust | |
| uses: dtolnay/rust-toolchain@stable | |
| - name: Prepare README for crates.io | |
| run: | | |
| pwd | |
| ls -la | |
| cp ./readme.md ./rust/readme.md | |
| - name: Publish to crates.io | |
| env: | |
| CARGO_REGISTRY_TOKEN: ${{ secrets.CARGO_REGISTRY_TOKEN }} | |
| run: | | |
| cd rust | |
| cargo publish --allow-dirty --token "$CARGO_REGISTRY_TOKEN" | |
| echo "✅ Published to crates.io!" | |
| # ── 同步 Release 到 Gitee ───────────────────────────────── | |
| # 在 release 完成后运行,与 publish-scoop/aur/npm 并行 | |
| sync-gitee-release: | |
| name: 【Gitee码云】Sync Release File | |
| needs: [check, release] | |
| if: needs.check.outputs.should_release == 'true' && needs.release.result == 'success' | |
| runs-on: ubuntu-latest | |
| env: | |
| GITEE_TOKEN: ${{ secrets.GITEE_TOKEN }} | |
| GITEE_OWNER: vincent-zyu | |
| GITEE_REPO: winload | |
| steps: | |
| - name: Checkout | |
| uses: actions/checkout@v4 | |
| - name: Get release info from GitHub | |
| id: release_info | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TAG="${{ needs.check.outputs.version }}" | |
| echo "tag=$TAG" >> "$GITHUB_OUTPUT" | |
| # 获取 Release 信息 | |
| RELEASE_INFO=$(gh release view "$TAG" --json name,body,assets) | |
| echo "release_name=$(echo "$RELEASE_INFO" | jq -r '.name')" >> "$GITHUB_OUTPUT" | |
| echo "$RELEASE_INFO" | jq -r '.body' > release_body.md | |
| echo "$RELEASE_INFO" | jq -r '.assets[].url' > asset_urls.txt | |
| echo "📦 Release: $TAG" | |
| echo "📝 Assets:" | |
| cat asset_urls.txt | |
| - name: Download GitHub Release assets | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| run: | | |
| TAG="${{ steps.release_info.outputs.tag }}" | |
| mkdir -p assets | |
| cd assets | |
| gh release download "$TAG" --pattern "*" | |
| echo "📥 Downloaded files:" | |
| ls -la | |
| - name: Create or find Gitee Release | |
| id: gitee_release | |
| run: | | |
| TAG="${{ steps.release_info.outputs.tag }}" | |
| NAME="${{ steps.release_info.outputs.release_name }}" | |
| # JSON 安全转义 release body(处理换行、引号、反斜杠等) | |
| JSON_BODY=$(jq -R -s '.' < release_body.md) | |
| CURL="curl -s -k --retry 3 --retry-delay 5 --retry-all-errors" | |
| # 1. 确保 tag 存在于 Gitee | |
| if $CURL --fail "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/tags/${TAG}?access_token=${GITEE_TOKEN}" > /dev/null 2>&1; then | |
| echo "🏷️ Tag $TAG already exists on Gitee" | |
| else | |
| echo "🏷️ Creating tag $TAG on Gitee..." | |
| $CURL -X POST --header 'Content-Type: application/json;charset=UTF-8' \ | |
| "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/tags" \ | |
| -d "{\"access_token\":\"${GITEE_TOKEN}\",\"tag_name\":\"${TAG}\",\"refs\":\"main\",\"tag_message\":\"Release ${TAG}\"}" | |
| fi | |
| # 2. 检查是否已有对应 tag 的 Release | |
| EXISTING=$($CURL "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/tags/${TAG}?access_token=${GITEE_TOKEN}") | |
| RELEASE_ID=$(echo "$EXISTING" | jq -r '.id') | |
| if [ -n "$RELEASE_ID" ] && [ "$RELEASE_ID" != "null" ]; then | |
| ASSET_COUNT=$(echo "$EXISTING" | jq '.assets | length') | |
| echo "⚠️ Gitee release already exists (ID: $RELEASE_ID, assets: $ASSET_COUNT)" | |
| else | |
| echo "✅ No existing release, creating..." | |
| # 3. 创建 Release(注意 target_commitish 是必填字段) | |
| RELEASE_PAYLOAD=$(jq -n \ | |
| --arg token "$GITEE_TOKEN" \ | |
| --arg tag "$TAG" \ | |
| --arg name "$NAME" \ | |
| --argjson body "$JSON_BODY" \ | |
| '{ | |
| access_token: $token, | |
| tag_name: $tag, | |
| name: $name, | |
| body: $body, | |
| target_commitish: "main", | |
| prerelease: false | |
| }') | |
| CREATE_RESPONSE=$($CURL -X POST --header 'Content-Type: application/json;charset=UTF-8' \ | |
| "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/releases" \ | |
| -d "$RELEASE_PAYLOAD") | |
| RELEASE_ID=$(echo "$CREATE_RESPONSE" | jq -r '.id') | |
| ASSET_COUNT=0 | |
| if [ "$RELEASE_ID" = "null" ] || [ -z "$RELEASE_ID" ]; then | |
| echo "❌ Failed to create Gitee release" | |
| echo "Response: $CREATE_RESPONSE" | |
| exit 1 | |
| fi | |
| echo "✅ Created Gitee release (ID: $RELEASE_ID)" | |
| fi | |
| echo "release_id=$RELEASE_ID" >> "$GITHUB_OUTPUT" | |
| echo "asset_count=$ASSET_COUNT" >> "$GITHUB_OUTPUT" | |
| - name: Upload assets to Gitee | |
| if: steps.gitee_release.outputs.release_id != '' && steps.gitee_release.outputs.release_id != 'null' | |
| run: | | |
| RELEASE_ID="${{ steps.gitee_release.outputs.release_id }}" | |
| # 时间戳函数:同时输出 UTC 和 上海时间 | |
| ts() { | |
| echo "[$(date -u '+%H:%M:%S UTC') / $(TZ='Asia/Shanghai' date '+%H:%M:%S CST')]" | |
| } | |
| echo "$(ts) 📤 Uploading assets to Gitee release (ID: $RELEASE_ID)" | |
| SUCCESS=0 | |
| FAIL=0 | |
| TOTAL=$(ls -1 assets/ | wc -l) | |
| CURRENT=0 | |
| echo "$(ts) 📁 Total files to upload: $TOTAL" | |
| for FILE in assets/*; do | |
| if [ -f "$FILE" ]; then | |
| CURRENT=$((CURRENT + 1)) | |
| FILENAME=$(basename "$FILE") | |
| FILESIZE=$(stat -c%s "$FILE" 2>/dev/null || stat -f%z "$FILE" 2>/dev/null) | |
| FILESIZE_H=$(numfmt --to=iec $FILESIZE 2>/dev/null || echo "${FILESIZE} bytes") | |
| echo "" | |
| echo "$(ts) ──── [$CURRENT/$TOTAL] $FILENAME ($FILESIZE_H) ────" | |
| # 重试 3 次,超时 1200s (20min),间隔 10s | |
| # Gitee 上传速度约 10KB/s,3MB 文件需要 ~300s | |
| UPLOADED=false | |
| for ATTEMPT in 1 2 3; do | |
| echo "$(ts) 🔄 Attempt $ATTEMPT/3 — starting curl (max-time=1200s, connect-timeout=30s)" | |
| START_TIME=$(date +%s) | |
| HTTP_CODE_FILE=$(mktemp) | |
| RESPONSE=$(curl -s -k --retry 3 --retry-delay 10 --retry-all-errors \ | |
| --max-time 1200 --connect-timeout 30 \ | |
| -w "\n%{http_code}" \ | |
| -X POST --header "Content-Type: multipart/form-data" \ | |
| -F "access_token=${GITEE_TOKEN}" \ | |
| -F "file=@${FILE}" \ | |
| "https://gitee.com/api/v5/repos/${GITEE_OWNER}/${GITEE_REPO}/releases/${RELEASE_ID}/attach_files") | |
| CURL_EXIT=$? | |
| END_TIME=$(date +%s) | |
| ELAPSED=$((END_TIME - START_TIME)) | |
| # 分离 HTTP status code 和 response body | |
| HTTP_CODE=$(echo "$RESPONSE" | tail -1) | |
| BODY=$(echo "$RESPONSE" | sed '$d') | |
| echo "$(ts) 📡 curl exit=$CURL_EXIT, HTTP=$HTTP_CODE, took ${ELAPSED}s" | |
| if [ $CURL_EXIT -ne 0 ]; then | |
| case $CURL_EXIT in | |
| 28) echo "$(ts) ⏱️ TIMEOUT: curl exceeded max-time (${ELAPSED}s)" ;; | |
| 7) echo "$(ts) 🔌 CONNECTION REFUSED: Gitee server unreachable" ;; | |
| 56) echo "$(ts) 💥 RECV ERROR: connection reset during transfer" ;; | |
| *) echo "$(ts) ❓ CURL ERROR $CURL_EXIT (see https://curl.se/libcurl/c/libcurl-errors.html)" ;; | |
| esac | |
| elif echo "$BODY" | jq -e '.browser_download_url' > /dev/null 2>&1; then | |
| URL=$(echo "$BODY" | jq -r '.browser_download_url') | |
| echo "$(ts) ✅ SUCCESS: $URL" | |
| SUCCESS=$((SUCCESS + 1)) | |
| UPLOADED=true | |
| break | |
| else | |
| echo "$(ts) ⚠️ UNEXPECTED RESPONSE (HTTP $HTTP_CODE):" | |
| echo "$(ts) $(echo "$BODY" | head -c 300)" | |
| fi | |
| if [ $ATTEMPT -lt 3 ]; then | |
| echo "$(ts) 💤 Waiting 10s before retry..." | |
| sleep 10 | |
| fi | |
| done | |
| if [ "$UPLOADED" = false ]; then | |
| echo "$(ts) ❌ GAVE UP on $FILENAME after 3 attempts" | |
| FAIL=$((FAIL + 1)) | |
| fi | |
| fi | |
| done | |
| echo "" | |
| echo "$(ts) ════════════════════════════════════════" | |
| echo "$(ts) 📊 Upload summary: $SUCCESS/$TOTAL succeeded, $FAIL failed" | |
| echo "$(ts) ════════════════════════════════════════" | |
| echo "📊 Upload complete: $SUCCESS succeeded, $FAIL failed" |