name: Release # This workflow builds and packages Rustnet for multiple platforms. # # For macOS code signing and notarization, configure these GitHub repository secrets: # - APPLE_DEVELOPER_CERTIFICATE_P12: Base64-encoded .p12 certificate file # (Export from Keychain: Developer ID Application certificate → Export as .p12 → base64 encode) # - APPLE_DEVELOPER_CERTIFICATE_PASSWORD: Password for the .p12 file # - APPLE_ID_ISSUER_ID: App Store Connect API issuer ID (from App Store Connect → Users and Access → Keys) # - APPLE_ID_KEY_ID: App Store Connect API key ID # - APPLE_ID_KEY: App Store Connect API key content (.p8 file contents) # # Without these secrets, the DMG will be unsigned and users must use "Open Anyway" in # System Settings → Privacy & Security to bypass Gatekeeper protection. # # To obtain Apple Developer credentials: # 1. Join the Apple Developer Program ($99/year): https://developer.apple.com/programs/ # 2. Create a Developer ID Application certificate in your Apple Developer account # 3. Create an App Store Connect API key with "Developer" role # 4. Export certificate as .p12, encode with: base64 -i certificate.p12 | pbcopy on: push: tags: - "v[0-9]+.[0-9]+.[0-9]+" workflow_dispatch: permissions: contents: write packages: write attestations: write id-token: write env: RUST_BACKTRACE: 1 RUSTNET_ASSET_DIR: assets jobs: # Build all platforms using shared workflow build: uses: ./.github/workflows/build-platforms.yml with: create-archives: true strip-symbols: true version: ${{ github.ref_name }} create-release: name: create-release runs-on: ubuntu-latest needs: build steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Download all artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: artifacts - name: Extract changelog for release run: | # Extract the version-specific section from CHANGELOG.md VERSION="${{ github.ref_name }}" # Remove 'v' prefix if present for matching changelog headers VERSION_NUMBER="${VERSION#v}" # Extract content between version headers awk -v version="$VERSION_NUMBER" ' /^## \[/ { if (found) exit if ($0 ~ "\\[" version "\\]") { found=1 next } } found { print } ' CHANGELOG.md > release_notes.md # Check if we extracted any content if [ ! -s release_notes.md ]; then echo "⚠️ No changelog entry found for $VERSION, will use auto-generated notes" echo "USE_GENERATED_NOTES=true" >> "$GITHUB_ENV" else echo "✅ Extracted changelog content for $VERSION" cat release_notes.md fi - name: Create Release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Create the release if it doesn't exist if ! gh release view ${{ github.ref_name }} 2>/dev/null; then if [ "$USE_GENERATED_NOTES" = "true" ]; then # Fallback to auto-generated notes if changelog extraction failed gh release create ${{ github.ref_name }} \ --title "Release ${{ github.ref_name }}" \ --draft \ --generate-notes else # Use extracted changelog content gh release create ${{ github.ref_name }} \ --title "Release ${{ github.ref_name }}" \ --draft \ --notes-file release_notes.md fi fi # Upload all build artifacts for artifact in artifacts/build-*/rustnet-*; do gh release upload ${{ github.ref_name }} "$artifact" --clobber done # aarch64 static: JS actions don't work in Alpine containers on ARM runners, # so the reusable workflow can't upload artifacts. Build and upload here instead, # where we have contents: write from the workflow-level permissions. upload-arm-static: name: upload-arm-static runs-on: ubuntu-24.04-arm needs: create-release container: image: rust:alpine steps: - name: Checkout repository (ARM workaround) run: | apk add --no-cache git git config --global --add safe.directory /__w/rustnet/rustnet git clone --depth 1 "https://github.com/${{ github.repository }}.git" . # shellcheck disable=SC2193 if [[ "${{ github.ref }}" == refs/tags/* ]]; then git fetch --depth 1 origin tag "${{ github.ref_name }}" git checkout "${{ github.ref_name }}" fi - name: Install dependencies run: | apk add --no-cache \ musl-dev libpcap-dev pkgconfig build-base perl \ elfutils-dev zlib-dev zlib-static zstd-dev zstd-static \ clang llvm linux-headers rustup component add rustfmt - name: Build static binary env: CFLAGS: "-mno-outline-atomics" CXXFLAGS: "-mno-outline-atomics" RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" run: cargo build --release - name: Verify static linking run: | file target/release/rustnet file target/release/rustnet | grep -q "static.* linked" || \ (echo "ERROR: Binary is not statically linked" && exit 1) - name: Create and upload release archive run: | staging="rustnet-${{ github.ref_name }}-aarch64-unknown-linux-musl" mkdir -p "$staging/assets" cp target/release/rustnet "$staging/" cp crates/rustnet-core/assets/services "$staging/assets/" 2>/dev/null || true cp README.md "$staging/" cp LICENSE "$staging/" 2>/dev/null || true tar czf "$staging.tar.gz" "$staging" apk add --no-cache github-cli gh release upload "${{ github.ref_name }}" "$staging.tar.gz" --clobber env: GH_TOKEN: ${{ github.token }} upload-android-static: name: upload-${{ matrix.arch }}-android-static runs-on: ${{ matrix.runner }} needs: create-release container: image: rust:alpine strategy: fail-fast: false matrix: include: - arch: aarch64 runner: ubuntu-24.04-arm artifact: aarch64-linux-android-musl - arch: x86_64 runner: ubuntu-latest artifact: x86_64-linux-android-musl - arch: armv7 runner: ubuntu-latest artifact: armv7-linux-android-musl - arch: x86 runner: ubuntu-latest artifact: i686-linux-android-musl steps: # aarch64: JS actions don't work in Alpine containers on ARM runners - name: Checkout repository (aarch64 workaround) if: matrix.arch == 'aarch64' run: | apk add --no-cache git git config --global --add safe.directory /__w/rustnet/rustnet git clone --depth 1 "https://github.com/${{ github.repository }}.git" . # shellcheck disable=SC2193 if [[ "${{ github.ref }}" == refs/tags/* ]]; then git fetch --depth 1 origin tag "${{ github.ref_name }}" git checkout "${{ github.ref_name }}" fi - name: Checkout repository if: matrix.arch != 'aarch64' uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install common dependencies run: | apk add --no-cache \ musl-dev pkgconfig build-base perl \ zlib-dev zlib-static \ clang llvm linux-headers github-cli curl rustup component add rustfmt # aarch64/x86_64: libpcap-dev provides a native static libpcap. # zstd-static is required because Alpine's libpcap-dev links against zstd. - name: Install libpcap (native) if: matrix.arch == 'aarch64' || matrix.arch == 'x86_64' run: apk add --no-cache libpcap-dev elfutils-dev zstd-dev zstd-static # armv7: use zig cc as the cross-compiler (no external toolchain download needed). # zig bundles musl libc for all targets, so no -l:libzstd.a needed. - name: Install cross-compiler and libpcap (armv7) if: matrix.arch == 'armv7' run: | rustup target add armv7-unknown-linux-musleabihf apk add zig flex bison bash # zig cc wrapper: cc-rs detects it as clang and passes --target=armv7-* # which zig rejects (zig uses 'arm' not 'armv7'). Filter those args out. # shellcheck disable=SC2016 printf '%s\n' \ '#!/bin/bash' \ 'args=()' \ 'for arg in "$@"; do' \ ' case "$arg" in --target=*) ;; *) args+=("$arg") ;; esac' \ 'done' \ 'exec zig cc -target arm-linux-musleabihf "${args[@]}"' \ > /usr/local/bin/arm-linux-musleabihf-gcc chmod +x /usr/local/bin/arm-linux-musleabihf-gcc # libpcap is pinned by version + sha256 (Dependabot does not track # tarballs fetched in run steps, so bump the URL and the hash together). curl -fsSL --retry 3 --retry-delay 5 \ -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz echo "37ced90a19a302a7f32e458224a00c365c117905c2cd35ac544b6880a81488f0 libpcap.tar.gz" | sha256sum -c - tar xzf libpcap.tar.gz cd libpcap-1.10.5 CC=arm-linux-musleabihf-gcc \ ./configure --host=arm-linux-musleabihf \ --disable-shared --enable-static \ --disable-usb --disable-dbus --disable-bluetooth --disable-remote \ --prefix=/arm-sysroot make -j"$(nproc)" && make install cd .. # x86: use zig cc as the cross-compiler (no external toolchain download needed). # zig bundles musl libc for all targets, so no -l:libzstd.a needed. - name: Install cross-compiler and libpcap (x86/i686) if: matrix.arch == 'x86' run: | rustup target add i686-unknown-linux-musl apk add zig flex bison bash # zig cc wrapper: cc-rs passes --target=i686-* which zig doesn't accept; # filter those args and use -target x86-linux-musl instead. # shellcheck disable=SC2016 printf '%s\n' \ '#!/bin/bash' \ 'args=()' \ 'for arg in "$@"; do' \ ' case "$arg" in --target=*) ;; -Wl,-melf_i386) ;; *) args+=("$arg") ;; esac' \ 'done' \ 'exec zig cc -target x86-linux-musl "${args[@]}"' \ > /usr/local/bin/i686-linux-musl-gcc chmod +x /usr/local/bin/i686-linux-musl-gcc # libpcap is pinned by version + sha256 (Dependabot does not track # tarballs fetched in run steps, so bump the URL and the hash together). curl -fsSL --retry 3 --retry-delay 5 \ -o libpcap.tar.gz https://www.tcpdump.org/release/libpcap-1.10.5.tar.gz echo "37ced90a19a302a7f32e458224a00c365c117905c2cd35ac544b6880a81488f0 libpcap.tar.gz" | sha256sum -c - tar xzf libpcap.tar.gz cd libpcap-1.10.5 CC=i686-linux-musl-gcc \ ./configure --host=i686-linux-musl \ --disable-shared --enable-static \ --disable-usb --disable-dbus --disable-bluetooth --disable-remote \ --prefix=/x86-sysroot make -j"$(nproc)" && make install cd .. - name: Build static binary (aarch64) if: matrix.arch == 'aarch64' env: CFLAGS: "-mno-outline-atomics" CXXFLAGS: "-mno-outline-atomics" RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" run: cargo build --release --no-default-features - name: Build static binary (x86_64) if: matrix.arch == 'x86_64' env: RUSTFLAGS: "-C strip=symbols -C link-arg=-l:libzstd.a" run: cargo build --release --no-default-features - name: Build static binary (armv7) if: matrix.arch == 'armv7' env: # link-self-contained=no: let zig provide musl CRT (avoid duplicate _start # when both Rust's self-contained crt1.o and zig's crt1.o are linked) RUSTFLAGS: "-C strip=symbols -C link-self-contained=no" CARGO_TARGET_ARMV7_UNKNOWN_LINUX_MUSLEABIHF_LINKER: arm-linux-musleabihf-gcc PKG_CONFIG_PATH: /arm-sysroot/lib/pkgconfig PKG_CONFIG_SYSROOT_DIR: /arm-sysroot PKG_CONFIG_ALLOW_CROSS: "1" run: cargo build --release --target armv7-unknown-linux-musleabihf --no-default-features - name: Build static binary (x86/i686) if: matrix.arch == 'x86' env: RUSTFLAGS: "-C strip=symbols -C link-self-contained=no" CARGO_TARGET_I686_UNKNOWN_LINUX_MUSL_LINKER: i686-linux-musl-gcc PKG_CONFIG_PATH: /x86-sysroot/lib/pkgconfig PKG_CONFIG_SYSROOT_DIR: /x86-sysroot PKG_CONFIG_ALLOW_CROSS: "1" run: cargo build --release --target i686-unknown-linux-musl --no-default-features - name: Verify static linking run: | BIN="${{ (matrix.arch == 'armv7' && 'target/armv7-unknown-linux-musleabihf/release/rustnet') || (matrix.arch == 'x86' && 'target/i686-unknown-linux-musl/release/rustnet') || 'target/release/rustnet' }}" file "$BIN" file "$BIN" | grep -q "static.* linked" || \ (echo "ERROR: Binary is not statically linked" && exit 1) - name: Create release archive run: | VERSION="${{ github.ref_name }}" ARTIFACT="${{ matrix.artifact }}" case "${{ matrix.arch }}" in armv7) BIN="target/armv7-unknown-linux-musleabihf/release/rustnet" ;; x86) BIN="target/i686-unknown-linux-musl/release/rustnet" ;; *) BIN="target/release/rustnet" ;; esac staging="rustnet-${VERSION}-${ARTIFACT}" mkdir -p "$staging/assets" cp "$BIN" "$staging/rustnet" cp crates/rustnet-core/assets/services "$staging/assets/" 2>/dev/null || true cp README.md "$staging/" cp LICENSE "$staging/" 2>/dev/null || true tar czf "$staging.tar.gz" "$staging" - name: Upload to release run: | VERSION="${{ github.ref_name }}" ARCHIVE="rustnet-${VERSION}-${{ matrix.artifact }}.tar.gz" gh release upload "$VERSION" "$ARCHIVE" --repo "${{ github.repository }}" --clobber env: GH_TOKEN: ${{ github.token }} # Publish the release (un-draft) after all assets are uploaded. # This ensures downstream jobs (Homebrew, Chocolatey) can download assets. publish-release: name: publish-release runs-on: ubuntu-latest needs: - create-release - upload-arm-static - upload-android-static - package-installers - package-macos - package-windows if: always() && needs.create-release.result == 'success' steps: - name: Publish release env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: gh release edit ${{ github.ref_name }} --draft=false --repo ${{ github.repository }} # Gate job that waits for all publish steps to finish before firing # downstream package-manager triggers. Without this gate, triggers that # run as soon as publish-release completes race against publish-crates, # publish-docker, update-copr, and release-ppa — and the downstream # Homebrew/Chocolatey workflows bail because their "is release.yml still # running?" safety check sees the parent run as in-progress. all-published: name: all-published runs-on: ubuntu-latest needs: - publish-release - publish-crates - publish-docker - update-copr - release-ppa - release-obs steps: - name: All publish steps complete run: echo "Release pipeline fully published; safe to fire downstream triggers." trigger-bsd-build: name: trigger-bsd-build runs-on: ubuntu-latest needs: all-published steps: - name: Trigger FreeBSD build run: | gh api repos/domcyrus/rustnet-bsd/dispatches \ -f event_type=freebsd-build \ -f "client_payload[tag]=${{ github.ref_name }}" \ -f "client_payload[rustnet_ref]=${{ github.sha }}" env: GH_TOKEN: ${{ secrets.BSD_DISPATCH_TOKEN }} trigger-homebrew-update: name: trigger-homebrew-update runs-on: ubuntu-latest needs: all-published steps: - name: Trigger Homebrew formula update run: | gh workflow run update-formula.yml \ --repo domcyrus/homebrew-rustnet \ -f version=${{ github.ref_name }} env: GH_TOKEN: ${{ secrets.HOMEBREW_PAT }} trigger-chocolatey-update: name: trigger-chocolatey-update runs-on: ubuntu-latest needs: all-published steps: - name: Trigger Chocolatey package update run: | gh workflow run update-package.yml \ --repo domcyrus/rustnet-chocolatey \ -f version=${{ github.ref_name }} env: GH_TOKEN: ${{ secrets.CHOCOLATEY_PAT }} # Dispatching a same-repo workflow via gh workflow run requires # actions: write on the token. The top-level permissions block grants # contents/packages/attestations/id-token only, so explicitly opt in here. trigger-aur-update: name: trigger-aur-update runs-on: ubuntu-latest needs: all-published permissions: actions: write contents: read steps: - name: Trigger AUR package update run: | gh workflow run aur-update.yml \ --repo domcyrus/rustnet env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} package-installers: name: package-installers runs-on: ubuntu-22.04 needs: create-release steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Download build artifacts uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: path: artifacts - name: Setup Linux dependencies uses: ./.github/actions/setup-linux-deps - name: Install Rust and tools uses: dtolnay/rust-toolchain@stable # unpinned: maintained branch by Rust team member with: targets: x86_64-unknown-linux-gnu,aarch64-unknown-linux-gnu,armv7-unknown-linux-gnueabihf components: rustfmt - name: Install packaging tools run: | cargo install cargo-deb cargo-generate-rpm - name: Package Debian packages run: | mkdir -p installers for arch in amd64 arm64 armhf; do case "$arch" in amd64) target="x86_64-unknown-linux-gnu" ;; arm64) target="aarch64-unknown-linux-gnu" ;; armhf) target="armv7-unknown-linux-gnueabihf" ;; esac # Extract binary and assets from artifact tar -xzf "artifacts/build-$target/rustnet-${{ github.ref_name }}-$target.tar.gz" mkdir -p "target/$target/release" cp "rustnet-${{ github.ref_name }}-$target/rustnet" "target/$target/release/" # Ensure services file exists for packaging mkdir -p crates/rustnet-core/assets if [ -f "rustnet-${{ github.ref_name }}-$target/assets/services" ]; then cp "rustnet-${{ github.ref_name }}-$target/assets/services" crates/rustnet-core/assets/ fi # Create deb package cargo deb --no-build --no-strip --target "$target" mv "target/$target/debian"/*.deb "installers/Rustnet_LinuxDEB_$arch.deb" # Clean up for next iteration rm -rf "rustnet-${{ github.ref_name }}-$target" "target/$target" done - name: Package RPM packages run: | for arch in x86_64 aarch64; do target="$arch-unknown-linux-gnu" # Extract binary and assets from artifact tar -xzf "artifacts/build-$target/rustnet-${{ github.ref_name }}-$target.tar.gz" mkdir -p "target/$target/release" cp "rustnet-${{ github.ref_name }}-$target/rustnet" "target/$target/release/" # Ensure services file exists for packaging mkdir -p crates/rustnet-core/assets if [ -f "rustnet-${{ github.ref_name }}-$target/assets/services" ]; then cp "rustnet-${{ github.ref_name }}-$target/assets/services" crates/rustnet-core/assets/ fi # Fix library linking if needed patchelf --replace-needed libpcap.so.0.8 libpcap.so.1 "target/$target/release/rustnet" 2>/dev/null || true # Create rpm package cargo generate-rpm --target "$target" mv "target/$target/generate-rpm"/*.rpm "installers/Rustnet_LinuxRPM_$arch.rpm" # Clean up for next iteration rm -rf "rustnet-${{ github.ref_name }}-$target" "target/$target" done - name: Upload installer packages env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # Upload all installer packages for installer in installers/*; do gh release upload ${{ github.ref_name }} "$installer" --clobber done package-macos: name: package-macos runs-on: macos-latest needs: create-release strategy: matrix: include: - arch: Intel target: x86_64-apple-darwin - arch: AppleSilicon target: aarch64-apple-darwin steps: - name: Checkout repository uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - name: Install packaging tools run: | cargo install toml-cli cargo install apple-codesign brew install create-dmg - name: Download build artifact uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8 with: name: build-${{ matrix.target }} path: artifacts - name: Package for macOS run: | # Extract binary tar -xzf "artifacts/rustnet-${{ github.ref_name }}-${{ matrix.target }}.tar.gz" # Get version and update plist VERSION=$(toml get Cargo.toml package.version --raw) sed -i'.bak' -e "s/0\.0\.0/${VERSION}/g" -e "s/fffffff/${GITHUB_SHA:0:7}/g" resources/packaging/macos/Info.plist # Create app bundle mkdir -p "Rustnet.app/Contents/"{MacOS,Resources/assets} cp resources/packaging/macos/Info.plist "Rustnet.app/Contents/" cp resources/packaging/macos/graphics/rustnet.icns "Rustnet.app/Contents/Resources/" cp "rustnet-${{ github.ref_name }}-${{ matrix.target }}/rustnet" "Rustnet.app/Contents/MacOS/" cp resources/packaging/macos/wrapper.sh "Rustnet.app/Contents/MacOS/" cp "rustnet-${{ github.ref_name }}-${{ matrix.target }}/assets/services" "Rustnet.app/Contents/Resources/assets/" chmod +x "Rustnet.app/Contents/MacOS/"{rustnet,wrapper.sh} - name: Code sign and notarize app bundle env: APPLE_CERTIFICATE_P12: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_P12 }} APPLE_CERTIFICATE_PASSWORD: ${{ secrets.APPLE_DEVELOPER_CERTIFICATE_PASSWORD }} APPLE_API_ISSUER: ${{ secrets.APPLE_ID_ISSUER_ID }} APPLE_API_KEY_ID: ${{ secrets.APPLE_ID_KEY_ID }} APPLE_API_KEY: ${{ secrets.APPLE_ID_KEY }} run: | # Skip signing if secrets are not configured if [ -z "$APPLE_CERTIFICATE_P12" ]; then echo "⚠️ Code signing secrets not configured. DMG will be unsigned." echo "⚠️ Users will need to use 'Open Anyway' in System Settings > Privacy & Security" echo "SKIP_SIGNING=true" >> "$GITHUB_ENV" exit 0 fi # Decode certificate and save to file echo "$APPLE_CERTIFICATE_P12" | base64 --decode > certificate.p12 # Create temporary JSON for API credentials cat > api-key.json <