name: Rust
on:
push:
branches: [ "main" ]
tags:
- 'v*'
pull_request:
branches: [ "main" ]
workflow_dispatch:
permissions:
contents: write
env:
CARGO_TERM_COLOR: always
APP_NAME: superseedr
PROPTEST_CASES: 20000
jobs:
build_linux:
timeout-minutes: 120
name: Build & Test (Linux)
if: github.event_name == 'pull_request' || (github.event_name == 'push' && !startsWith(github.ref, 'refs/tags/'))
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6
- name: Set up Rust
uses: dtolnay/rust-toolchain@1.95.0
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install Dependencies
run: sudo apt-get update
- name: Check Formatting
run: cargo fmt --all --check
- name: Lint with Clippy
run: cargo clippy --all-targets --all-features -- -D warnings
- name: Run Tests
run: cargo test --all-targets --all-features
package_linux:
timeout-minutes: 120
name: Build Linux Artifacts (${{ matrix.suffix }})
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
strategy:
matrix:
include:
- suffix: "normal"
flags: ""
- suffix: "private"
flags: --no-default-features
steps:
- uses: actions/checkout@v6
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install Dependencies
run: |
sudo apt-get update
sudo apt-get install -y musl-tools libssl-dev pkg-config
cargo install cargo-bundle
rustup target add x86_64-unknown-linux-musl
- name: Create Staging Directory
run: mkdir staging
- name: Build Debian Package
run: cargo bundle --release --format deb ${{ matrix.flags }}
- name: Move Debian Package
run: |
DEB_FILE=$(find target/release/bundle/deb -name '*.deb')
if [ -z "$DEB_FILE" ]; then
echo "::error:: No .deb file found."
exit 1
fi
if [ "${{ matrix.suffix }}" = "private" ]; then
FILE_NAME="${APP_NAME}-private_${{ github.ref_name }}_amd64.deb"
else
FILE_NAME="${APP_NAME}_${{ github.ref_name }}_amd64.deb"
fi
echo "Moving $DEB_FILE to staging/$FILE_NAME"
mv "$DEB_FILE" "staging/$FILE_NAME"
- name: Upload Linux Artifacts
uses: actions/upload-artifact@v7
with:
name: superseedr-linux-amd64-${{ matrix.suffix }}-${{ github.ref_name }}
path: staging/*
bundle_macos:
timeout-minutes: 120
name: Build macOS Universal PKG (${{ matrix.suffix }})
if: startsWith(github.ref, 'refs/tags/')
runs-on: macos-latest
env:
KEYCHAIN_NAME: build.keychain
strategy:
matrix:
include:
- suffix: "normal"
flags: ""
- suffix: "private"
flags: --no-default-features
steps:
- uses: actions/checkout@v6
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install Rust Apple Targets
run: |
rustup target add aarch64-apple-darwin
rustup target add x86_64-apple-darwin
- name: Pre-compile Rust Binaries
run: |
echo "Starting pre-compilation to separate build time from signing time..."
echo "Building x86_64..."
cargo build --release --target x86_64-apple-darwin ${{ matrix.flags }}
echo "Building aarch64..."
cargo build --release --target aarch64-apple-darwin ${{ matrix.flags }}
- name: Setup macOS Keychain and Certificate
id: setup_keychain
env:
APPLE_INSTALLER_CERT_P12_BASE64: ${{ secrets.APPLE_INSTALLER_CERT_P12_BASE64 }}
APPLE_INSTALLER_CERT_PASSWORD: ${{ secrets.APPLE_INSTALLER_CERT_PASSWORD }}
run: |
# Create a new keychain
security create-keychain -p "$RUNNER_TEMP" "$KEYCHAIN_NAME"
security list-keychains -s "$KEYCHAIN_NAME"
security default-keychain -s "$KEYCHAIN_NAME"
security unlock-keychain -p "$RUNNER_TEMP" "$KEYCHAIN_NAME"
# Decode and import the .p12
echo $APPLE_INSTALLER_CERT_P12_BASE64 | base64 --decode > certificate.p12
security import certificate.p12 -k "$KEYCHAIN_NAME" -P "$APPLE_INSTALLER_CERT_PASSWORD" -T /usr/bin/codesign -T /usr/bin/productsign
rm certificate.p12
# Set keychain to allow signing
security set-key-partition-list -S apple-tool:,apple: -s -k "$RUNNER_TEMP" "$KEYCHAIN_NAME"
echo "Waiting for keychain to settle..."
sleep 2
# Find the certificate's Common Name (CN).
CERT_CN=$(security find-identity -v "$KEYCHAIN_NAME" | grep "Developer ID Installer" | head -n 1 | sed -E 's/.*"([^"]+)".*/\1/')
if [ -z "$CERT_CN" ]; then
echo "::error:: No valid codesigning identity found in keychain."
security find-identity -v "$KEYCHAIN_NAME" # Print all identities for debugging
exit 1
fi
echo "Using certificate: $CERT_CN"
echo "CERT_NAME=$CERT_CN" >> $GITHUB_ENV
- name: Execute Custom macOS Build Script
id: build_pkg
run: |
SCRIPT_PATH="scripts/build_osx_universal_pkg.sh"
chmod +x "$SCRIPT_PATH"
set -o pipefail
"$SCRIPT_PATH" \
${{ github.ref_name }} \
${{ matrix.suffix }} \
"${{ env.CERT_NAME }}" \
${{ matrix.flags }} \
2>&1 | tee build_log.txt
PKG_PATH=$(grep 'PKG_PATH=' build_log.txt | head -n 1 | sed -n 's/.*PKG_PATH=\(.*\)/\1/p' | tr -d '[:space:]')
if [ -z "$PKG_PATH" ]; then
echo "::error::Build script finished, but 'PKG_PATH=' was not found in the log."
exit 1
fi
echo "PKG_PATH found: $PKG_PATH"
echo "pkg_path=$PKG_PATH" >> $GITHUB_OUTPUT
- name: Notarize and Staple PKG
id: notarize
env:
APPLE_NOTARY_USERNAME: ${{ secrets.APPLE_NOTARY_USERNAME }}
APPLE_NOTARY_PASSWORD: ${{ secrets.APPLE_NOTARY_PASSWORD }}
APPLE_TEAM_ID: ${{ secrets.APPLE_TEAM_ID }}
run: |
PKG_FILE_PATH="${{ steps.build_pkg.outputs.pkg_path }}"
echo "Submitting $PKG_FILE_PATH for notarization..."
xcrun notarytool submit "$PKG_FILE_PATH" \
--apple-id "$APPLE_NOTARY_USERNAME" \
--password "$APPLE_NOTARY_PASSWORD" \
--team-id "$APPLE_TEAM_ID" \
--wait
echo "Notarization successful. Stapling ticket..."
xcrun stapler staple "$PKG_FILE_PATH"
- name: Stage macOS PKG
id: stage_pkg
run: |
mkdir -p staging
PKG_SRC_PATH="${{ steps.build_pkg.outputs.pkg_path }}"
VERSION_TAG="${{ github.ref_name }}"
SUFFIX="${{ matrix.suffix }}"
if [ "$SUFFIX" = "normal" ]; then
PKG_NAME="${{ env.APP_NAME }}-${VERSION_TAG}-universal-macos.pkg"
else
PKG_NAME="${{ env.APP_NAME }}-${VERSION_TAG}-${SUFFIX}-universal-macos.pkg"
fi
DEST_PATH="staging/$PKG_NAME"
echo "Moving $PKG_SRC_PATH to $DEST_PATH"
mv "$PKG_SRC_PATH" "$DEST_PATH"
echo "final_pkg_path=$DEST_PATH" >> $GITHUB_OUTPUT
- name: Cleanup Keychain
if: always() run: |
security delete-keychain "$KEYCHAIN_NAME"
- name: Upload macOS PKG Artifact
uses: actions/upload-artifact@v7
with:
name: superseedr-macos-${{ matrix.suffix }}-universal-${{ github.ref_name }}
path: ${{ steps.stage_pkg.outputs.final_pkg_path }}
build_windows:
timeout-minutes: 120
name: Build Windows MSI (${{ matrix.suffix }})
if: startsWith(github.ref, 'refs/tags/')
runs-on: windows-latest
strategy:
matrix:
include:
- suffix: "normal"
flags: ""
- suffix: "private"
flags: "--no-default-features"
steps:
- uses: actions/checkout@v6
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
target/
key: ${{ runner.os }}-cargo-${{ hashFiles('**/Cargo.lock') }}
- name: Install Rust MSVC Target
run: rustup target add x86_64-pc-windows-msvc
- name: Install WiX Toolset v3
run: choco install wixtoolset
- name: Install cargo-wix
run: cargo install cargo-wix
- name: Build MSI Installer (${{ matrix.suffix }})
id: build_msi
run: |
if ("${{ matrix.flags }}" -eq "") {
# For the "normal" build, just run the default command.
# This runs 'cargo build --release' AND packages the MSI.
echo "Running: cargo wix"
cargo wix
} else {
# For the "private" build, we must build manually first.
# 1. Run cargo build with our private flags
echo "Running: cargo build --release ${{ matrix.flags }}"
cargo build --release ${{ matrix.flags }}
# 2. Run cargo wix with '--no-build' to package the binaries we just made
echo "Running: cargo wix --no-build"
cargo wix --no-build
}
# Update the path: 'cargo wix' outputs to 'target/wix'
$MSI_FILE = Get-ChildItem -Path "target/wix" -Filter "*.msi" | Select-Object -First 1
if ($null -eq $MSI_FILE) { echo "::error:: No .msi file found"; exit 1; }
echo "msi_path=$($MSI_FILE.FullName)" >> $env:GITHUB_OUTPUT
- name: Sign MSI Installer (if secret is present)
if: env.WINDOWS_CERT_P12_BASE64 != ''
id: sign_msi
env:
WINDOWS_CERT_P12_BASE64: ${{ secrets.WINDOWS_CERT_P12_BASE64 }}
WINDOWS_CERT_PASSWORD: ${{ secrets.WINDOWS_CERT_PASSWORD }}
shell: pwsh
run: |
# Decode the certificate
[System.Text.Encoding]::UTF8.GetString([System.Convert]::FromBase64String($env:WINDOWS_CERT_P12_BASE64)) | Out-File -FilePath windows.pfx -Encoding OEM
$MSI_PATH = "${{ steps.build_msi.outputs.msi_path }}"
echo "Signing $MSI_PATH..."
# Find signtool.exe (it's part of the Windows SDK)
$SIGNTOOL_PATH = (Get-ChildItem -Path "C:\Program Files (x86)\Windows Kits\10\bin" -Filter "signtool.exe" -Recurse | Sort-Object VersionInfo -Descending | Select-Object -First 1).FullName
if ($null -eq $SIGNTOOL_PATH) {
echo "::error:: signtool.exe not found."
exit 1
}
echo "Using signtool at $SIGNTOOL_PATH"
# Sign the file
& $SIGNTOOL_PATH sign /f "windows.pfx" /p $env:WINDOWS_CERT_PASSWORD /tr http://timestamp.digicert.com /td SHA256 $MSI_PATH
# Clean up
Remove-Item windows.pfx
- name: Stage MSI
shell: pwsh
run: |
# Use the git tag for the version, not the Cargo.toml version
$VERSION_TAG = "${{ github.ref_name }}"
$MSI_FILE_PATH = "${{ steps.build_msi.outputs.msi_path }}"
$SUFFIX = "${{ matrix.suffix }}"
if ($SUFFIX -eq "normal") {
$MSI_NAME = "${{ env.APP_NAME }}_${VERSION_TAG}_x64_en-US.msi"
} else {
$MSI_NAME = "${{ env.APP_NAME }}-${SUFFIX}_${VERSION_TAG}_x64_en-US.msi"
}
mkdir staging
$DEST_PATH = "staging/$MSI_NAME"
echo "Moving $MSI_FILE_PATH to $DEST_PATH"
mv $MSI_FILE_PATH $DEST_PATH
# Output the final staged name for the release body
echo "final_msi_name=$MSI_NAME" >> $env:GITHUB_OUTPUT
- name: Upload Windows MSI Artifact
uses: actions/upload-artifact@v7
with:
name: superseedr-windows-${{ matrix.suffix }}-${{ github.ref_name }}
path: staging/*.msi
build_and_push_docker:
name: Docker (${{ matrix.flavor }})
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [package_linux, bundle_macos, build_windows]
strategy:
fail-fast: false
matrix:
flavor: [normal, private]
steps:
- name: Checkout
uses: actions/checkout@v6
- name: Set up QEMU
uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Log in to Docker Hub
uses: docker/login-action@v4
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Extract metadata
id: meta
uses: docker/metadata-action@v6
with:
images: jagatranvo/superseedr
tags: |
type=ref,event=tag${{ matrix.flavor == 'private' && ',suffix=-private' || '' }}
type=raw,value=${{ matrix.flavor == 'private' && 'private' || 'latest' }}
${{ matrix.flavor == 'normal' && 'type=ref,event=tag' || '' }}
- name: Build and push
uses: docker/build-push-action@v7
with:
context: .
file: ./Dockerfile
push: ${{ startsWith(github.ref, 'refs/tags/') }}
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
platforms: linux/amd64,linux/arm64
cache-from: type=gha,scope=docker-${{ matrix.flavor }}
cache-to: type=gha,mode=max,scope=docker-${{ matrix.flavor }}
build-args: |
PRIVATE_BUILD=${{ matrix.flavor == 'private' }}
- name: Update Docker Hub Description
if: matrix.flavor == 'normal' && startsWith(github.ref, 'refs/tags/')
uses: peter-evans/dockerhub-description@v5
with:
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
repository: jagatranvo/superseedr
readme-filepath: ./README.md
release:
timeout-minutes: 120
name: Create GitHub Release
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [package_linux, bundle_macos, build_windows, build_and_push_docker]
steps:
- name: Download all build artifacts
uses: actions/download-artifact@v8
with:
path: artifacts/
pattern: superseedr-*
- name: Set Release Version
run: echo "RELEASE_VERSION=${{ github.ref_name }}" >> $GITHUB_ENV
- name: Create Release and Upload Artifacts
uses: softprops/action-gh-release@v2
with:
name: ${{ github.ref_name }}
body: |
## Standard Builds (Recommended)
* **macOS Universal:** [superseedr-${{ env.RELEASE_VERSION }}-universal-macos.pkg](https://github.com/Jagalite/superseedr/releases/download/${{ github.ref_name }}/superseedr-${{ env.RELEASE_VERSION }}-universal-macos.pkg)
* **Linux (Debian):** [superseedr_${{ env.RELEASE_VERSION }}_amd64.deb](https://github.com/Jagalite/superseedr/releases/download/${{ github.ref_name }}/superseedr_${{ env.RELEASE_VERSION }}_amd64.deb)
* **Windows (MSI):** [superseedr_${{ env.RELEASE_VERSION }}_x64_en-US.msi](https://github.com/Jagalite/superseedr/releases/download/${{ github.ref_name }}/superseedr_${{ env.RELEASE_VERSION }}_x64_en-US.msi)
---
## Private Builds (Advanced)
These builds do not contain PEX or DHT in the final binary. Not recommended for normal users unless you have privacy requirements.
* **macOS Universal:** [superseedr-${{ env.RELEASE_VERSION }}-private-universal-macos.pkg](https://github.com/Jagalite/superseedr/releases/download/${{ github.ref_name }}/superseedr-${{ env.RELEASE_VERSION }}-private-universal-macos.pkg)
* **Linux (Debian):** [superseedr-private_${{ env.RELEASE_VERSION }}_amd64.deb](https://github.com/Jagalite/superseedr/releases/download/${{ github.ref_name }}/superseedr-private_${{ env.RELEASE_VERSION }}_amd64.deb)
* **Windows (MSI):** [superseedr-private_${{ env.RELEASE_VERSION }}_x64_en-US.msi](https://github.com/Jagalite/superseedr/releases/download/${{ github.ref_name }}/superseedr-private_${{ env.RELEASE_VERSION }}_x64_en-US.msi)
files: |
artifacts/superseedr-linux-amd64-*-${{ github.ref_name }}/*.deb
artifacts/superseedr-macos-*-universal-${{ github.ref_name }}/*.pkg
artifacts/superseedr-windows-*-${{ github.ref_name }}/*.msi
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
publish_crates_io:
timeout-minutes: 120
name: Publish to Crates.io
if: startsWith(github.ref, 'refs/tags/')
runs-on: ubuntu-latest
needs: [release]
steps:
- uses: actions/checkout@v6
- name: Cache cargo
uses: actions/cache@v5
with:
path: |
~/.cargo/bin/
~/.cargo/registry/index/
~/.cargo/registry/cache/
~/.cargo/git/db/
# target/ is intentionally omitted for cargo publish
key: ${{ runner.os }}-cargo-registry-${{ hashFiles('**/Cargo.lock') }}
- name: Publish to crates.io
run: cargo publish
env:
CARGO_REGISTRY_TOKEN: ${{ secrets.CRATES_IO_TOKEN }}