Skip to content

feat: 新增 --title-align 参数,支持 title 左/中/右对齐 - Bump version 0.1.9-beta.… #143

feat: 新增 --title-align 参数,支持 title 左/中/右对齐 - Bump version 0.1.9-beta.…

feat: 新增 --title-align 参数,支持 title 左/中/右对齐 - Bump version 0.1.9-beta.… #143

Workflow file for this run

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"