Skip to content

chore: bump portable-pty from 0.8.1 to 0.9.0 #430

chore: bump portable-pty from 0.8.1 to 0.9.0

chore: bump portable-pty from 0.8.1 to 0.9.0 #430

Workflow file for this run

name: CI
on:
push:
branches: [main]
pull_request:
branches: [main]
workflow_dispatch:
inputs:
release:
description: "Cut a release from main (forces a patch bump when there is nothing new to release)"
type: boolean
default: false
concurrency:
group: ${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true
env:
CARGO_TERM_COLOR: always
CARGO_INCREMENTAL: 0
IMAGE_NAME: mearman/cascade-relay
jobs:
# ── Change detection ────────────────────────────────────────────────
# Gates PWA and Pages jobs on apps/web/** changes so Rust-only pushes
# don't burn runner minutes on unrelated web tooling.
changes:
name: Detect changed paths
runs-on: ubuntu-latest
timeout-minutes: 5
outputs:
web: ${{ steps.check.outputs.web }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
- id: check
shell: bash
run: |
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
echo "web=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if [[ "${{ github.event_name }}" == "pull_request" ]]; then
BASE="${{ github.event.pull_request.base.sha }}"
HEAD="${{ github.event.pull_request.head.sha }}"
else
BASE="${{ github.event.before }}"
HEAD="${{ github.event.after }}"
fi
if [[ "$BASE" == *"0000000000000000000000000000000000000000"* ]]; then
echo "web=true" >> "$GITHUB_OUTPUT"
exit 0
fi
if git diff --name-only "$BASE" "$HEAD" -- apps/web/ | grep -q .; then
echo "web=true" >> "$GITHUB_OUTPUT"
else
echo "web=false" >> "$GITHUB_OUTPUT"
fi
# Lints the browser crates against the wasm32 target. The host clippy in
# `check` cannot see wasm-only code paths (and the crate-root test-cfg allow
# masks lints like string_slice on the host), so wasm-only regressions —
# e.g. a forbidden string slice or a non-Send future pulled into the wasm
# build — only surface here. Runs unconditionally; it is cheap (three small
# crates).
wasm-clippy:
name: Clippy (wasm32)
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.96.0"
components: clippy
targets: wasm32-unknown-unknown
- name: Cache Cargo output
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry
target/wasm32-unknown-unknown
key: wasm32-clippy-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
restore-keys: wasm32-clippy-${{ runner.os }}-
- name: Clippy the browser crates (wasm32)
run: >
cargo clippy --target wasm32-unknown-unknown
-p cascade-wasm -p cascade-backend-fsaccess -p cascade-p2p-webrtc
-- -D warnings
# Runs the wasm-bindgen-test suite for the two browser bridge crates against
# JS stub modules under Node.js. Kept separate from wasm-clippy so a
# stub/runtime failure is distinguishable from a lint failure.
wasm-test:
name: wasm-bindgen-test (--node)
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.96.0"
targets: wasm32-unknown-unknown
- name: Setup Node 20
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '20'
- name: Cache Cargo output
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry
target/wasm32-unknown-unknown
key: wasm32-test-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
restore-keys: wasm32-test-${{ runner.os }}-
- name: Install wasm-pack
# Pin to 0.15.0 so the lane is reproducible. wasm-pack uses the
# wasm-bindgen version from Cargo.lock so it stays in sync with the
# workspace pin automatically.
run: cargo install wasm-pack --version 0.15.0 --locked
- name: wasm-bindgen-test (cascade-backend-fsaccess)
run: wasm-pack test --node crates/cascade-backend-fsaccess --features js-test-stub
- name: wasm-bindgen-test (cascade-p2p-webrtc)
run: wasm-pack test --node crates/cascade-p2p-webrtc --features js-test-stub
# Builds, lints, and TESTS the engine under the `portable` feature (the
# wasm/mobile foundation). `cargo test --workspace` runs default features
# only, so without this the portable config — RuntimeHandle/StateStorage/
# HttpClient/FileSystem/Clock contracts and the wasm adapters — has no CI
# coverage and regresses silently. Bare `portable` is the wasm config; the
# `portable,p2p` and `portable,exec` builds guard the mobile cross-compile
# feature combinations (test-only, since those configs pull native back in).
portable:
name: Engine (portable)
runs-on: ubuntu-latest
timeout-minutes: 20
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.96.0"
components: clippy
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: portable
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build (portable)
run: cargo build -p cascade-engine --no-default-features --features portable
- name: Test (portable)
run: cargo test -p cascade-engine --no-default-features --features portable --lib
- name: Clippy (portable)
run: cargo clippy -p cascade-engine --no-default-features --features portable -- -D warnings
- name: Build (portable,p2p)
run: cargo build -p cascade-engine --no-default-features --features portable,p2p
- name: Build (portable,exec)
run: cargo build -p cascade-engine --no-default-features --features portable,exec
source-length:
name: Source file length
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Check no production source file exceeds the line cap
run: python3 scripts/check-source-file-length.py
# Builds the iOS File Provider extension: cross-compiles the cascade-ffi
# static slices, generates the UniFFI Swift bindings, and ad-hoc-signs the
# Simulator build via xcodebuild. Catches FFI/extension regressions that the
# host workspace build cannot. macOS runner (Xcode), so it is the costliest
# gate here; the rust-cache keeps the warm path short.
ios-app:
name: Build iOS File Provider extension
runs-on: macos-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.96.0"
targets: aarch64-apple-ios,aarch64-apple-ios-sim
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: ios-ffi
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Cache Homebrew
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/Library/Caches/Homebrew
key: homebrew-${{ runner.os }}-xcodegen
restore-keys: homebrew-${{ runner.os }}-
- name: Install xcodegen
run: brew install xcodegen
- name: Bootstrap (build FFI slices, generate bindings, generate project)
run: ./swift/CascadeFileProvideriOS/bootstrap.sh
- name: Build the extension (Simulator, ad-hoc signed)
run: >
xcodebuild
-project swift/CascadeFileProvideriOS/CascadeFileProvideriOS.xcodeproj
-target CascadeHostApp -sdk iphonesimulator -configuration Debug
ARCHS=arm64 ONLY_ACTIVE_ARCH=NO build
- name: Run the logic tests (Simulator)
# Hostless unit tests for the path<->identifier mapping; the macOS
# runner ships an iOS Simulator runtime (the dev machine does not), so
# this is where they actually execute.
run: >
xcodebuild
-project swift/CascadeFileProvideriOS/CascadeFileProvideriOS.xcodeproj
-scheme FileProviderLogicTests -sdk iphonesimulator
-destination 'platform=iOS Simulator,name=iPhone 16,arch=arm64'
-configuration Debug test
- name: Run the integration tests (Simulator)
# Drives the REAL CascadeNode through the FFI on the simulator — the
# FFI -> engine -> local-backend stack on the actual iOS target.
run: >
xcodebuild
-project swift/CascadeFileProvideriOS/CascadeFileProvideriOS.xcodeproj
-scheme CascadeIntegrationTests -sdk iphonesimulator
-destination 'platform=iOS Simulator,name=iPhone 16,arch=arm64'
-configuration Debug test
- name: Run the File Provider OS-registration e2e (Simulator)
# Registers a domain via NSFileProviderManager and enumerates through
# the OS-routed enumerator — proving the OS instantiates the extension
# and routes enumeration to it. Timing-sensitive; keep going even if it
# flakes so a slow domain activation does not block the run.
continue-on-error: true
run: >
xcodebuild
-project swift/CascadeFileProvideriOS/CascadeFileProvideriOS.xcodeproj
-scheme FileProviderRegistrationE2ETests -sdk iphonesimulator
-destination 'platform=iOS Simulator,name=iPhone 16,arch=arm64'
-configuration Debug test
- name: Package the Simulator app
# The ad-hoc signed Simulator .app — drag into a booted simulator to run.
# Not a device build: a real iPhone needs a signed .ipa (see release-ios-ipa).
run: >
ditto -c -k --keepParent
swift/CascadeFileProvideriOS/build/Debug-iphonesimulator/CascadeHostApp.app
"$RUNNER_TEMP/cascade-ios-simulator-app.zip"
- name: Upload the Simulator app (CI artifact)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cascade-ios-simulator-app
path: ${{ runner.temp }}/cascade-ios-simulator-app.zip
if-no-files-found: error
# Builds the Android DocumentsProvider app: cross-compiles the cascade-ffi
# shared library for both ABIs into jniLibs and runs gradle assembleDebug.
# ubuntu-latest ships the Android SDK, an NDK, and JDK 17.
android-app:
name: Build Android DocumentsProvider app
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.96.0"
targets: aarch64-linux-android,x86_64-linux-android
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: android-ffi
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Resolve the preinstalled NDK and JDK 17
shell: bash
run: |
echo "ANDROID_NDK=$(ls -d "$ANDROID_SDK_ROOT"/ndk/*/ | sort -V | tail -1)" >> "$GITHUB_ENV"
echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
- name: Cache Gradle
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties', 'android/gradle.properties') }}
restore-keys: gradle-${{ runner.os }}-
- name: Cache Android SDK platform and build-tools
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.ANDROID_SDK_ROOT }}/platforms/android-34
${{ env.ANDROID_SDK_ROOT }}/build-tools/34.0.0
key: android-sdk-34-bt34-${{ runner.os }}
- name: Ensure the SDK platform and build-tools the app targets
shell: bash
run: |
# `set +o pipefail` so `yes` getting SIGPIPE when sdkmanager exits
# (the runner default is `-o pipefail`) does not fail the step; the
# pipeline's status is then sdkmanager's own exit code. Idempotent:
# already-installed packages (restored from cache) are a fast no-op.
set +o pipefail
yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" "platforms;android-34" "build-tools;34.0.0" >/dev/null
- name: Bootstrap (cross-compile the .so slices into jniLibs)
run: ./android/bootstrap.sh
- name: Assemble the debug APK
working-directory: android
run: ./gradlew :app:assembleDebug --no-daemon
- name: Run JVM unit tests
working-directory: android
run: ./gradlew :app:testDebugUnitTest --no-daemon
- name: Upload the debug APK (CI artifact)
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: cascade-android-debug-apk
path: android/app/build/outputs/apk/debug/app-debug.apk
if-no-files-found: error
# On-device e2e for the Android DocumentsProvider: boots an x86_64 emulator
# and runs the instrumented test, which drives the REAL provider through a
# ContentResolver (provider -> CascadeNode -> FFI -> engine -> local backend,
# native .so and all). `continue-on-error` because emulator boots on GitHub's
# free runners are flaky; flip to blocking once it's proven green reliably.
android-e2e:
name: Android DocumentsProvider e2e (emulator)
runs-on: ubuntu-latest
timeout-minutes: 45
continue-on-error: true
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: actions/setup-java@c1e323688fd81a25caa38c78aa6df2d33d3e20d9 # v4
with:
distribution: temurin
java-version: '17'
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.96.0"
targets: aarch64-linux-android,x86_64-linux-android
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: android-ffi
- name: Resolve the preinstalled NDK and SDK
shell: bash
run: |
echo "ANDROID_NDK=$(ls -d "$ANDROID_SDK_ROOT"/ndk/*/ | sort -V | tail -1)" >> "$GITHUB_ENV"
echo "sdk.dir=$ANDROID_SDK_ROOT" > android/local.properties
- name: Cache Gradle
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties', 'android/gradle.properties') }}
restore-keys: gradle-${{ runner.os }}-
- name: Cache Android SDK platform and build-tools
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.ANDROID_SDK_ROOT }}/platforms/android-34
${{ env.ANDROID_SDK_ROOT }}/build-tools/34.0.0
key: android-sdk-34-bt34-${{ runner.os }}
- name: Ensure the SDK platform and build-tools the app targets
shell: bash
run: |
set +o pipefail
yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" "platforms;android-34" "build-tools;34.0.0" >/dev/null
- name: Bootstrap (cross-compile the .so slices into jniLibs)
run: ./android/bootstrap.sh
- name: Run the instrumented tests on the emulator
uses: reactivecircus/android-emulator-runner@e89f39f1abbbd05b1113a29cf4db69e7540cae5a # v2.37.0
with:
api-level: 34
arch: x86_64
target: default
emulator-options: -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim
# Disable the package verifier before installing the test APK. The
# emulator's integrity verification stalls intermittently under load
# and surfaces as INSTALL_FAILED_VERIFICATION_FAILURE, which fails the
# instrumented test run before any test executes. There is nothing to
# verify on a self-signed debug APK on a throwaway emulator.
script: >-
adb shell settings put global package_verifier_enable 0 &&
adb shell settings put global verifier_verify_adb_installs 0 &&
./gradlew :app:connectedDebugAndroidTest --no-daemon
working-directory: android
# Attaches the Android debug APK to the GitHub release. Runs only when the
# release job actually cut a release, mirroring the cascade-binary release
# jobs. Debug-signed (no keystore secret needed); installable for testing,
# though MANAGE_DOCUMENTS only registers the provider on a system install.
release-android:
name: Release Android APK
needs: release
if: needs.release.outputs.released == 'true'
runs-on: ubuntu-latest
timeout-minutes: 30
permissions:
contents: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: aarch64-linux-android,x86_64-linux-android
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: android-ffi
- name: Resolve the preinstalled NDK and JDK 17
shell: bash
run: |
echo "ANDROID_NDK=$(ls -d "$ANDROID_SDK_ROOT"/ndk/*/ | sort -V | tail -1)" >> "$GITHUB_ENV"
echo "JAVA_HOME=$JAVA_HOME_17_X64" >> "$GITHUB_ENV"
- name: Cache Gradle
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties', 'android/gradle.properties') }}
restore-keys: gradle-${{ runner.os }}-
- name: Cache Android SDK platform and build-tools
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
${{ env.ANDROID_SDK_ROOT }}/platforms/android-34
${{ env.ANDROID_SDK_ROOT }}/build-tools/34.0.0
key: android-sdk-34-bt34-${{ runner.os }}
- name: Ensure the SDK platform and build-tools the app targets
shell: bash
run: |
set +o pipefail
yes | "$ANDROID_SDK_ROOT/cmdline-tools/latest/bin/sdkmanager" "platforms;android-34" "build-tools;34.0.0" >/dev/null
- name: Bootstrap (cross-compile the .so slices into jniLibs)
run: ./android/bootstrap.sh
- name: Assemble the debug APK
working-directory: android
run: ./gradlew :app:assembleDebug --no-daemon
- name: Package the APK with a versioned name
run: |
mkdir -p dist
cp android/app/build/outputs/apk/debug/app-debug.apk dist/cascade-android.apk
shasum -a 256 dist/cascade-android.apk | awk '{print $1}' > dist/cascade-android.apk.sha256
- name: Upload to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release.outputs.version }}
files: dist/cascade-android.apk*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Builds and attaches a signed iOS .ipa to the release. iOS requires a
# provisioning profile to install on any device, so this needs Apple signing
# secrets and is skipped (without failing) when they are absent. Required
# secrets (see swift/CascadeFileProvideriOS/README.md): IOS_DIST_CERT_P12
# (base64 .p12), IOS_DIST_CERT_PASSWORD, IOS_KEYCHAIN_PASSWORD,
# IOS_PROVISIONING_PROFILE (base64 .mobileprovision), IOS_TEAM_ID,
# IOS_EXPORT_METHOD (e.g. development / ad-hoc / app-store).
release-ios-ipa:
name: Release iOS IPA (signed)
needs: release
if: needs.release.outputs.released == 'true'
runs-on: macos-latest
timeout-minutes: 40
permissions:
contents: write
steps:
- name: Detect signing secrets
id: signing
env:
CERT: ${{ secrets.IOS_DIST_CERT_P12 }}
run: |
if [ -n "$CERT" ]; then echo "have=true" >> "$GITHUB_OUTPUT"; else echo "have=false" >> "$GITHUB_OUTPUT"; fi
- name: Note skipped (no signing secrets)
if: steps.signing.outputs.have != 'true'
run: echo "iOS signing secrets are not configured; skipping the IPA build. See swift/CascadeFileProvideriOS/README.md."
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
if: steps.signing.outputs.have == 'true'
with:
ref: ${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
if: steps.signing.outputs.have == 'true'
with:
targets: aarch64-apple-ios
- name: Install xcodegen
if: steps.signing.outputs.have == 'true'
run: brew install xcodegen
- name: Import signing certificate
if: steps.signing.outputs.have == 'true'
env:
CERT_P12: ${{ secrets.IOS_DIST_CERT_P12 }}
CERT_PW: ${{ secrets.IOS_DIST_CERT_PASSWORD }}
KEYCHAIN_PW: ${{ secrets.IOS_KEYCHAIN_PASSWORD }}
run: |
echo "$CERT_P12" | base64 --decode > "$RUNNER_TEMP/cert.p12"
security create-keychain -p "$KEYCHAIN_PW" build.keychain
security default-keychain -s build.keychain
security unlock-keychain -p "$KEYCHAIN_PW" build.keychain
security import "$RUNNER_TEMP/cert.p12" -k build.keychain -P "$CERT_PW" -T /usr/bin/codesign
security set-key-partition-list -S apple-tool:,apple: -s -k "$KEYCHAIN_PW" build.keychain
- name: Install provisioning profile
if: steps.signing.outputs.have == 'true'
env:
PROFILE: ${{ secrets.IOS_PROVISIONING_PROFILE }}
run: |
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles"
echo "$PROFILE" | base64 --decode > "$HOME/Library/MobileDevice/Provisioning Profiles/cascade.mobileprovision"
- name: Bootstrap (build FFI device slice, bindings, project)
if: steps.signing.outputs.have == 'true'
run: ./swift/CascadeFileProvideriOS/bootstrap.sh
- name: Archive and export the signed IPA
if: steps.signing.outputs.have == 'true'
env:
TEAM_ID: ${{ secrets.IOS_TEAM_ID }}
EXPORT_METHOD: ${{ secrets.IOS_EXPORT_METHOD }}
working-directory: swift/CascadeFileProvideriOS
run: |
cat > "$RUNNER_TEMP/ExportOptions.plist" <<PLIST
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0"><dict>
<key>method</key><string>${EXPORT_METHOD:-development}</string>
<key>teamID</key><string>${TEAM_ID}</string>
<key>signingStyle</key><string>manual</string>
</dict></plist>
PLIST
xcodebuild -project CascadeFileProvideriOS.xcodeproj -scheme CascadeHostApp \
-sdk iphoneos -configuration Release -archivePath "$RUNNER_TEMP/Cascade.xcarchive" \
DEVELOPMENT_TEAM="$TEAM_ID" archive
xcodebuild -exportArchive -archivePath "$RUNNER_TEMP/Cascade.xcarchive" \
-exportOptionsPlist "$RUNNER_TEMP/ExportOptions.plist" -exportPath "$RUNNER_TEMP/ipa"
mkdir -p dist && cp "$RUNNER_TEMP"/ipa/*.ipa dist/cascade-ios.ipa
- name: Upload to release
if: steps.signing.outputs.have == 'true'
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release.outputs.version }}
files: swift/CascadeFileProvideriOS/dist/cascade-ios.ipa
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Attaches the ad-hoc-signed iOS Simulator app to the release — the
# counterpart to the Android APK, runnable in a booted simulator with no
# Apple account (unzip, then drag into the simulator). A real-device build is
# the separate, secrets-gated release-ios-ipa job.
release-ios-simulator:
name: Release iOS Simulator app
needs: release
if: needs.release.outputs.released == 'true'
runs-on: macos-latest
timeout-minutes: 40
permissions:
contents: write
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: aarch64-apple-ios,aarch64-apple-ios-sim
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: ios-ffi
- name: Cache Homebrew
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/Library/Caches/Homebrew
key: homebrew-${{ runner.os }}-xcodegen
restore-keys: homebrew-${{ runner.os }}-
- name: Install xcodegen
run: brew install xcodegen
- name: Bootstrap (build FFI slices, generate bindings, generate project)
run: ./swift/CascadeFileProvideriOS/bootstrap.sh
- name: Build the extension (Simulator, ad-hoc signed)
run: >
xcodebuild
-project swift/CascadeFileProvideriOS/CascadeFileProvideriOS.xcodeproj
-target CascadeHostApp -sdk iphonesimulator -configuration Debug
ARCHS=arm64 ONLY_ACTIVE_ARCH=NO build
- name: Package the Simulator app
run: |
mkdir -p dist
ditto -c -k --keepParent \
swift/CascadeFileProvideriOS/build/Debug-iphonesimulator/CascadeHostApp.app \
dist/cascade-ios-simulator.zip
shasum -a 256 dist/cascade-ios-simulator.zip | awk '{print $1}' > dist/cascade-ios-simulator.zip.sha256
- name: Upload to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release.outputs.version }}
files: dist/cascade-ios-simulator.zip*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
# Verifies the iOS *archive* half of the IPA pipeline with no signing
# identity: `xcodebuild archive` builds a valid .xcarchive unsigned (only the
# final `exportArchive` step needs a profile). Runs always — no secrets — so
# the build path of release-ios-ipa is proven green, leaving only the signed
# export gated on the IOS_* secrets.
ios-archive-check:
name: iOS archive (unsigned)
runs-on: macos-latest
timeout-minutes: 40
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.96.0"
targets: aarch64-apple-ios
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: ios-ffi
- name: Cache Homebrew
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/Library/Caches/Homebrew
key: homebrew-${{ runner.os }}-xcodegen
restore-keys: homebrew-${{ runner.os }}-
- name: Install xcodegen
run: brew install xcodegen
- name: Bootstrap (build FFI device slice, bindings, project)
run: ./swift/CascadeFileProvideriOS/bootstrap.sh
- name: Archive the app (unsigned)
run: >
xcodebuild
-project swift/CascadeFileProvideriOS/CascadeFileProvideriOS.xcodeproj
-scheme CascadeHostApp -sdk iphoneos -configuration Release
-archivePath "$RUNNER_TEMP/Cascade.xcarchive"
CODE_SIGNING_ALLOWED=NO CODE_SIGNING_REQUIRED=NO DEVELOPMENT_TEAM=""
archive
check:
name: Check (${{ matrix.target }})
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
# x86_64-apple-darwin is covered at release time by the
# cross-compiled build-macos-x86 job; running native tests on
# macos-13 (Intel) here is value-low and macos-13 runners are
# heavily oversubscribed.
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-2025-vs2026
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
# sccache wraps rustc so individual compilation units survive release-plz's
# per-release Cargo.lock churn (which invalidates rust-cache's target/
# wholesale). rust-cache below still carries the registry/git/deps.
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
components: rustfmt, clippy
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
id: sccache
with:
path: ${{ runner.temp }}/sccache
key: sccache-check-${{ matrix.target }}-${{ github.sha }}
restore-keys: sccache-check-${{ matrix.target }}-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# Share the compiled-dependency cache with the `test` job on the same
# target — dependency builds dominate, and clippy vs test only differ
# in the workspace crates' own artifacts. `save-if` restricts cache
# writes to main so feature-branch churn can't evict the warm
# main-line cache (GHA's 10 GB LRU).
shared-key: ${{ matrix.target }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libfuse3-dev
- name: Format
run: cargo fmt --check
- name: Clippy
# --all-targets so test and bench code is linted too, not just lib/bin.
# -D warnings promotes every clippy warning (including default-level
# lints the workspace table does not explicitly deny) to an error.
# The four restriction lints (unwrap_used/expect_used/indexing_slicing/
# string_slice) are scoped out of test code at each crate root — see
# the comment in the root Cargo.toml's [workspace.lints.clippy].
run: cargo clippy --workspace --all-targets -- -D warnings
- name: Clippy (feature-gated discovery + integration code)
# The Clippy step above builds default features only, so the
# dht / announce / nat-integration code is never linted there. Lint it
# explicitly — once, on Linux, since the gated code compiles the same on
# every platform — so a regression in those paths fails CI rather than
# only surfacing for whoever happens to build the feature.
if: runner.os == 'Linux'
run: cargo clippy -p cascade-p2p --all-targets --features "dht announce nat-integration" -- -D warnings
- name: Docs
run: cargo doc --workspace --no-deps --document-private-items
env:
RUSTDOCFLAGS: "-D warnings"
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
# Mirror rust-cache's save-if policy: only main writes the cache so
# feature-branch churn can't evict the warm main-line entry. Also
# gate on a non-empty sccache dir: actions/cache/save emits a
# 'Cache save failed' run-summary annotation whenever the path is
# empty, which is noisy and not informative — a build that didn't
# touch sccache has nothing to save and should just skip the step.
if: github.ref == 'refs/heads/main' && hashFiles('${{ runner.temp }}/sccache/**') != ''

Check warning on line 715 in .github/workflows/ci.yml

View workflow run for this annotation

GitHub Actions / CI

Workflow syntax warning

.github/workflows/ci.yml (Line: 715, Col: 13): Conditional expression contains literal text outside replacement tokens. This will cause the expression to always evaluate to truthy. Did you mean to put the entire expression inside ${{ }}?
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-check-${{ matrix.target }}-${{ github.sha }}
test:
name: Test (${{ matrix.target }})
timeout-minutes: 30
strategy:
fail-fast: false
matrix:
include:
- os: ubuntu-latest
target: x86_64-unknown-linux-gnu
- os: ubuntu-24.04-arm
target: aarch64-unknown-linux-gnu
- os: macos-latest
target: aarch64-apple-darwin
- os: windows-2025-vs2026
target: x86_64-pc-windows-msvc
runs-on: ${{ matrix.os }}
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-test-${{ matrix.target }}-${{ github.sha }}
restore-keys: sccache-test-${{ matrix.target }}-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
shared-key: ${{ matrix.target }}
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install system dependencies (Linux)
if: runner.os == 'Linux'
run: sudo apt-get update && sudo apt-get install -y libfuse3-dev
- name: Run tests
run: cargo test --workspace
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-test-${{ matrix.target }}-${{ github.sha }}
nat-integration:
name: NAT integration (Linux netns)
needs: [check, test]
timeout-minutes: 20
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-nat-integration-${{ github.sha }}
restore-keys: sccache-nat-integration-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
# rust-cache rather than a lockfile-hash blob: it keys on rustc
# version, prunes stale deps, and survives release-plz's constant
# Cargo.lock bumps that fully invalidated the old hashFiles key.
key: nat-integration
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libfuse3-dev iproute2 iptables
- name: Build nat-integration test binaries
# Build the operated-relay test and the serverless-rung tests up front
# (without sudo) so the sudo invocations below only have to run, not
# compile — keeping the privileged surface as small as possible. This
# non-privileged build is where sccache earns its keep; the sudo steps
# below run as root and deliberately bypass sccache (RUSTC_WRAPPER=)
# so they never write root-owned files into the runner-owned cache dir.
run: |
cargo test -p cascade-p2p --features nat-integration --test nat_integration --no-run
cargo test -p cascade-p2p --features nat-integration --test serverless_rungs --no-run
- name: Run NAT integration tests (requires root for CAP_NET_ADMIN)
# sudo -E forwards the full environment (including PATH so cargo is
# found) and HOME so the Cargo registry cache is accessible.
# RUSTC_WRAPPER= disables sccache under root so the cache dir stays
# owned by the runner user and the save step can read it.
run: |
sudo -E env "PATH=$PATH" "HOME=$HOME" "RUSTC_WRAPPER=" \
cargo test -p cascade-p2p \
--features nat-integration \
--test nat_integration \
-- --test-threads=1 --nocapture
- name: Run serverless connectivity-rung tests (peer-as-STUN + peer relay)
# Proves the fully serverless rungs of the connectivity ladder with no
# operated servers of any kind: (a) a cone-NAT pair connects via
# peer-as-STUN observed-address learning plus hole punch, and (b) a
# symmetric-NAT pair bridges through a third open peer acting as a peer
# relay. Both assert a block transfer completes over the expected rung.
run: |
sudo -E env "PATH=$PATH" "HOME=$HOME" "RUSTC_WRAPPER=" \
cargo test -p cascade-p2p \
--features nat-integration \
--test serverless_rungs \
-- --test-threads=1 --nocapture
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-nat-integration-${{ github.sha }}
dht-live:
name: DHT live tests (local mainline testnet)
needs: [check, test]
runs-on: ubuntu-latest
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-dht-live-${{ github.sha }}
restore-keys: sccache-dht-live-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: dht-live
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Clippy the live DHT tests under their feature
# The standard workspace clippy job builds default features only, so the
# `#[cfg(feature = "dht")]` live_tests module is never linted there. The
# "Clippy (feature-gated discovery + integration code)" step in the check
# job lints it under `dht` already; re-assert it here, scoped to the same
# crate and feature, so this job fails on a lint regression in the live
# test code before it spends time bootstrapping a swarm.
run: cargo clippy -p cascade-p2p --all-targets --features dht -- -D warnings
- name: Build DHT live test binary
# Compile up front (without running) so the run step below only has to
# bootstrap the swarm, not build — keeping the timed run step lean.
run: cargo test -p cascade-p2p --features dht --lib discovery::dht::live_tests --no-run
- name: Run DHT live tests against an ephemeral local testnet
# The live_tests module is `#[ignore]`'d so the standard offline
# `cargo test --workspace` stays green; this job opts them in with
# `--ignored`. They spin up an in-process `mainline::Testnet` — a small
# swarm of DHT nodes bound to local UDP sockets — and never touch the
# public BitTorrent bootstrap routers, so the run is fully hermetic.
# Single-threaded so the swarms do not contend, and `timeout-minutes`
# bounds the whole step so a wedged lookup fails the job rather than
# hanging it (each resolve is itself bounded by DHT_RESOLVE_TIMEOUT).
timeout-minutes: 10
run: |
cargo test -p cascade-p2p \
--features dht \
--lib discovery::dht::live_tests \
-- --ignored --test-threads=1 --nocapture
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-dht-live-${{ github.sha }}
projfs-probe:
name: ProjFS availability probe (Windows)
needs: [check, test]
timeout-minutes: 20
runs-on: windows-2025-vs2026
steps:
# Diagnostic only: confirms whether the Windows Projected File System
# (Client-ProjFS) optional feature is enabled on the hosted runner and
# whether its PrjFlt filter driver can be brought up without a reboot.
# This de-risks implementing the ProjFS read/write callbacks (roadmap
# v8) by establishing — empirically, on the exact image CI gets —
# whether a real ProjFS integration test could run here at all. The
# runner-images toolset lists Client-ProjFS in its windowsFeatures, but
# the published manifest can lag the live image and the 2025 migration
# trimmed some components, so we check rather than assume.
- name: Report Windows build
shell: pwsh
run: |
Get-ComputerInfo -Property OsName, OsVersion, OsBuildNumber |
Format-List
- name: Probe Client-ProjFS optional feature
shell: pwsh
run: |
$feature = Get-WindowsOptionalFeature -Online -FeatureName Client-ProjFS -ErrorAction SilentlyContinue
if ($null -eq $feature) {
Write-Error "Client-ProjFS optional feature is not present on this image."
exit 1
}
Write-Host "Client-ProjFS state: $($feature.State)"
if ($feature.State -ne 'Enabled') {
Write-Error "Client-ProjFS is present but not Enabled (state: $($feature.State)). A ProjFS integration test would need to enable it, which may require a reboot CI cannot perform."
exit 1
}
- name: Probe PrjFlt filter driver
shell: pwsh
# The projfs callbacks run behind the PrjFlt minifilter. Even with the
# optional feature Enabled, confirm the driver is loadable so a future
# integration test can attach a virtualization root. `fltmc` needs an
# elevated context — GitHub runners run as admin by default.
run: |
$existing = fltmc filters | Select-String -Quiet 'PrjFlt'
if ($existing) {
Write-Host "PrjFlt minifilter already loaded."
} else {
Write-Host "PrjFlt not loaded; attempting fltmc load PrjFlt..."
fltmc load PrjFlt
if ($LASTEXITCODE -ne 0) {
Write-Error "fltmc load PrjFlt failed (exit $LASTEXITCODE) — driver not loadable without a reboot on this image."
exit 1
}
Write-Host "PrjFlt loaded successfully."
}
fltmc filters | Select-String 'PrjFlt'
- name: Summary
shell: pwsh
run: Write-Host "ProjFS is available on this runner — a Windows-only ProjFS integration test is feasible here."
projfs-live:
name: ProjFS live integration (Windows)
needs: [check, test]
timeout-minutes: 20
runs-on: windows-2025-vs2026
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: projfs-live
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Verify ProjFS prerequisites
shell: pwsh
# Re-check the same preconditions the projfs-probe job confirms, so a
# regression in the moving windows-latest image fails this job with a
# clear message rather than surfacing an opaque PrjStartVirtualizing
# HRESULT out of Rust. The separate probe job de-risks the image; this
# guard protects this job's own run.
run: |
$feature = Get-WindowsOptionalFeature -Online -FeatureName Client-ProjFS -ErrorAction SilentlyContinue
if ($null -eq $feature -or $feature.State -ne 'Enabled') {
$state = if ($null -eq $feature) { 'absent' } else { $feature.State }
Write-Error "Client-ProjFS is not Enabled (state: $state) — the live ProjFS test cannot run on this image."
exit 1
}
Write-Host "Client-ProjFS state: $($feature.State)"
$loaded = fltmc filters | Select-String -Quiet 'PrjFlt'
if (-not $loaded) {
Write-Host "PrjFlt not loaded; attempting fltmc load PrjFlt..."
fltmc load PrjFlt
if ($LASTEXITCODE -ne 0) {
Write-Error "fltmc load PrjFlt failed (exit $LASTEXITCODE) — the PrjFlt minifilter is not loadable on this image."
exit 1
}
}
Write-Host "PrjFlt minifilter is loaded."
- name: Clippy live ProjFS test under its feature
# The projfs-live test file is gated behind the feature + Windows, so
# the workspace clippy job never lints it. Lint it here under the same
# gate so the test code is actually held to the workspace lint bar.
run: cargo clippy -p cascade-presenter-projfs --features projfs-live --all-targets -- -D warnings
- name: Build live ProjFS test binary
run: cargo test -p cascade-presenter-projfs --features projfs-live --test projfs_live --no-run
- name: Run live ProjFS integration test
# Single-threaded so the live mount and its OS callbacks are not raced
# by parallel test functions. No elevation step is needed beyond what
# fltmc requires: GitHub Windows runners run elevated by default, which
# is sufficient for PrjStartVirtualizing on a runner-owned directory.
run: cargo test -p cascade-presenter-projfs --features projfs-live --test projfs_live -- --test-threads=1 --nocapture
e2e-p2p:
name: E2E (P2P mesh in Docker Compose)
needs: [check, test]
timeout-minutes: 30
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Verify Docker Compose
run: docker compose version
- name: Run P2P e2e harness
run: test/e2e/p2p/run.sh
env:
# Larger build context fits comfortably in 7 GB-class runners.
DOCKER_BUILDKIT: "1"
- name: Dump compose logs on failure
if: failure()
working-directory: test/e2e/p2p
run: docker compose logs --tail=200 || true
# ── PWA (was .github/workflows/pwa.yml) ────────────────────────────
pwa-check:
name: PWA typecheck
needs: [changes]
if: needs.changes.outputs.web == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 11
- name: Cache node_modules
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.pnpm-store
key: pnpm-${{ runner.os }}-${{ hashFiles('apps/web/pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install
working-directory: apps/web
- name: TypeScript type check
run: pnpm run typecheck
working-directory: apps/web
- name: Lint
run: pnpm run lint
working-directory: apps/web
- name: Unit tests
run: pnpm run test:run
working-directory: apps/web
pwa-build:
name: PWA build
needs: [changes, pwa-check]
if: needs.changes.outputs.web == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- uses: pnpm/action-setup@0ebf47130e4866e96fce0953f49152a61190b271 # v6.0.9
with:
version: 11
- name: Cache node_modules
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.pnpm-store
key: pnpm-${{ runner.os }}-${{ hashFiles('apps/web/pnpm-lock.yaml') }}
restore-keys: pnpm-${{ runner.os }}-
- name: Install dependencies
run: pnpm install
working-directory: apps/web
- name: Ensure WASM placeholder
run: rm -f public/wasm && mkdir -p public/wasm
working-directory: apps/web
- name: Build
run: pnpm run build
working-directory: apps/web
- name: Upload dist artifact
uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1
with:
name: pwa-dist
path: apps/web/dist/
retention-days: 1
release:
name: Release
needs: [check, test]
# Auto on push to main, or on demand via `workflow_dispatch` with the
# `release` input ticked (a dispatch without it just runs CI).
if: >
github.ref == 'refs/heads/main' &&
(github.event_name == 'push' ||
(github.event_name == 'workflow_dispatch' && inputs.release))
# release-plz bumps Cargo.lock on every release, which invalidates
# rust-cache's target/ wholesale, and the sccache Save steps only run on a
# main push — so the first release after a cold/evicted cache rebuilds the
# full workspace from scratch. 60 minutes gives that cold path headroom;
# warm releases finish well inside it.
timeout-minutes: 60
runs-on: ubuntu-latest
outputs:
released: ${{ steps.release.outputs.released }}
version: ${{ steps.release.outputs.version }}
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
fetch-depth: 0
token: ${{ secrets.GITHUB_TOKEN }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-release-${{ github.sha }}
restore-keys: sccache-release-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install release-plz
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: release-plz
- name: Bump versions
run: |
release-plz update 2>&1
- name: Commit and push version bump
id: bump
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add -A
if git diff --cached --quiet; then
# release-plz found nothing to release. On a manual dispatch the
# operator explicitly asked for a release, so force a patch bump
# rather than no-op; on a push, leave it as a clean no-release.
if [ "${{ github.event_name }}" != "workflow_dispatch" ]; then
echo 'No version bump to commit'
echo "bumped=false" >> "$GITHUB_OUTPUT"
exit 0
fi
CUR=$(grep '^version = ' Cargo.toml | head -1 | sed -E 's/version = "(.*)"/\1/')
NEW=$(echo "$CUR" | awk -F. '{printf "%d.%d.%d", $1, $2, $3 + 1}')
awk -v new="$NEW" '!done && /^version = "[^"]*"/ {sub(/"[^"]*"/, "\"" new "\""); done=1} {print}' Cargo.toml > Cargo.toml.tmp
mv Cargo.toml.tmp Cargo.toml
# Sync the workspace crates' versions in Cargo.lock (not their deps).
cargo update --workspace
git add -A
echo "Forced patch bump v${CUR} -> v${NEW} (manual release, no new releasable commits)"
fi
VERSION=$(grep '^version = ' Cargo.toml | head -1 | sed -E 's/version = "(.*)"/\1/')
# [skip ci] keeps this bot bump from spawning a second, redundant CI
# run: the release and its asset builds already ran as downstream
# jobs of the push that triggered this one, so re-running the full
# matrix here only burns minutes and cancels the original run's
# trailing jobs via the cancel-in-progress concurrency group.
git commit -m "chore: release v${VERSION} [skip ci]"
git push
echo "bumped=true" >> "$GITHUB_OUTPUT"
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
echo "Released v${VERSION}"
- name: Create releases
id: release
if: steps.bump.outputs.bumped == 'true'
run: |
set +e
OUTPUT=$(release-plz release --git-token ${{ secrets.GITHUB_TOKEN }} 2>&1)
RC=$?
set -e
echo "$OUTPUT"
# release-plz logs lines like "released cascade 0.1.1 (git-only)".
# With multiple packages in the workspace, release-plz publishes
# crate-prefixed tags (cascade-v0.1.1), not the bare v0.1.1.
if echo "$OUTPUT" | grep -q 'released cascade ';
then
VERSION=$(echo "$OUTPUT" | grep 'released cascade ' | head -1 | grep -oP 'released cascade \K[^ ]+')
TAG="cascade-v${VERSION}"
echo "released=true" >> "$GITHUB_OUTPUT"
echo "version=${TAG}" >> "$GITHUB_OUTPUT"
echo "Detected release: ${TAG}"
else
echo "released=false" >> "$GITHUB_OUTPUT"
echo 'No cascade binary release detected'
fi
exit $RC
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-release-${{ github.sha }}
build-macos-arm:
name: Build macOS ARM
needs: release
if: needs.release.outputs.released == 'true'
# Full-workspace release build gated on a release: see the release job —
# the same Cargo.lock churn + cold sccache means a near-clean compile of
# the whole dependency graph. 60 minutes covers the cold path on
# oversubscribed macOS runners; warm builds finish well inside it.
timeout-minutes: 60
runs-on: macos-latest
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: aarch64-apple-darwin
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-aarch64-apple-darwin-${{ github.sha }}
restore-keys: sccache-build-aarch64-apple-darwin-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: build-aarch64-apple-darwin
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build
run: cargo build --release --target aarch64-apple-darwin -p cascade
env:
CASCADE_GDRIVE_CLIENT_ID: ${{ secrets.CASCADE_GDRIVE_CLIENT_ID }}
CASCADE_GDRIVE_CLIENT_SECRET: ${{ secrets.CASCADE_GDRIVE_CLIENT_SECRET }}
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-aarch64-apple-darwin-${{ github.sha }}
- name: Package archive
run: |
mkdir -p dist
cp target/aarch64-apple-darwin/release/cascade dist/cascade
cd dist
tar czf cascade-aarch64-macos.tar.gz cascade
shasum -a 256 cascade-aarch64-macos.tar.gz | awk '{print $1}' > cascade-aarch64-macos.tar.gz.sha256
- name: Upload to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release.outputs.version }}
files: dist/cascade-aarch64-macos.tar.gz*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-macos-x86:
name: Build macOS x86_64
needs: release
if: needs.release.outputs.released == 'true'
# Full-workspace cross-compile gated on a release — cold sccache plus the
# release-plz Cargo.lock bump means a near-clean build of the whole
# dependency graph. 60 minutes covers the cold path; warm builds finish
# well inside it.
timeout-minutes: 60
runs-on: macos-latest
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: x86_64-apple-darwin
- run: rustup target add x86_64-apple-darwin
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-x86_64-apple-darwin-${{ github.sha }}
restore-keys: sccache-build-x86_64-apple-darwin-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: build-x86_64-apple-darwin
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build
run: cargo build --release --target x86_64-apple-darwin -p cascade
env:
CASCADE_GDRIVE_CLIENT_ID: ${{ secrets.CASCADE_GDRIVE_CLIENT_ID }}
CASCADE_GDRIVE_CLIENT_SECRET: ${{ secrets.CASCADE_GDRIVE_CLIENT_SECRET }}
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-x86_64-apple-darwin-${{ github.sha }}
- name: Package archive
run: |
mkdir -p dist
cp target/x86_64-apple-darwin/release/cascade dist/cascade
cd dist
tar czf cascade-x86_64-macos.tar.gz cascade
shasum -a 256 cascade-x86_64-macos.tar.gz | awk '{print $1}' > cascade-x86_64-macos.tar.gz.sha256
- name: Upload to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release.outputs.version }}
files: dist/cascade-x86_64-macos.tar.gz*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-linux-x86:
name: Build Linux x86_64
needs: release
if: needs.release.outputs.released == 'true'
# Full-workspace release build gated on a release — cold sccache plus the
# release-plz Cargo.lock bump means a near-clean build of the whole
# dependency graph. 60 minutes covers the cold path; warm builds finish
# well inside it.
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-x86_64-unknown-linux-gnu-${{ github.sha }}
restore-keys: sccache-build-x86_64-unknown-linux-gnu-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: build-x86_64-unknown-linux-gnu
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libfuse3-dev
- name: Build
run: cargo build --release --target x86_64-unknown-linux-gnu -p cascade
env:
CASCADE_GDRIVE_CLIENT_ID: ${{ secrets.CASCADE_GDRIVE_CLIENT_ID }}
CASCADE_GDRIVE_CLIENT_SECRET: ${{ secrets.CASCADE_GDRIVE_CLIENT_SECRET }}
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-x86_64-unknown-linux-gnu-${{ github.sha }}
- name: Package archive
run: |
mkdir -p dist
cp target/x86_64-unknown-linux-gnu/release/cascade dist/cascade
cd dist
tar czf cascade-x86_64-linux.tar.gz cascade
sha256sum cascade-x86_64-linux.tar.gz | awk '{print $1}' > cascade-x86_64-linux.tar.gz.sha256
- name: Upload to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release.outputs.version }}
files: dist/cascade-x86_64-linux.tar.gz*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-linux-arm:
name: Build Linux ARM64
needs: release
if: needs.release.outputs.released == 'true'
# Full-workspace cross-compile gated on a release — cold sccache plus the
# release-plz Cargo.lock bump means a near-clean build of the whole
# dependency graph. 60 minutes covers the cold path; warm builds finish
# well inside it.
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: aarch64-unknown-linux-gnu
- run: rustup target add aarch64-unknown-linux-gnu
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-aarch64-unknown-linux-gnu-${{ github.sha }}
restore-keys: sccache-build-aarch64-unknown-linux-gnu-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: build-aarch64-unknown-linux-gnu
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cross-compilation toolchain
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu
- name: Build
run: cargo build --release --target aarch64-unknown-linux-gnu -p cascade
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
CASCADE_GDRIVE_CLIENT_ID: ${{ secrets.CASCADE_GDRIVE_CLIENT_ID }}
CASCADE_GDRIVE_CLIENT_SECRET: ${{ secrets.CASCADE_GDRIVE_CLIENT_SECRET }}
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-aarch64-unknown-linux-gnu-${{ github.sha }}
- name: Package archive
run: |
mkdir -p dist
cp target/aarch64-unknown-linux-gnu/release/cascade dist/cascade
cd dist
tar czf cascade-aarch64-linux.tar.gz cascade
sha256sum cascade-aarch64-linux.tar.gz | awk '{print $1}' > cascade-aarch64-linux.tar.gz.sha256
- name: Upload to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release.outputs.version }}
files: dist/cascade-aarch64-linux.tar.gz*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
build-windows-x86:
name: Build Windows x86_64
needs: release
if: needs.release.outputs.released == 'true'
# Full-workspace release build gated on a release — cold sccache plus the
# release-plz Cargo.lock bump means a near-clean build of the whole
# dependency graph, and Windows linking is the slowest of the matrix.
# 60 minutes covers the cold path; warm builds finish well inside it.
timeout-minutes: 60
runs-on: windows-2025-vs2026
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
targets: x86_64-pc-windows-msvc
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-x86_64-pc-windows-msvc-${{ github.sha }}
restore-keys: sccache-build-x86_64-pc-windows-msvc-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: build-x86_64-pc-windows-msvc
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build
run: cargo build --release --target x86_64-pc-windows-msvc -p cascade
env:
CASCADE_GDRIVE_CLIENT_ID: ${{ secrets.CASCADE_GDRIVE_CLIENT_ID }}
CASCADE_GDRIVE_CLIENT_SECRET: ${{ secrets.CASCADE_GDRIVE_CLIENT_SECRET }}
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-build-x86_64-pc-windows-msvc-${{ github.sha }}
- name: Package archive
shell: pwsh
run: |
New-Item -ItemType Directory -Force -Path dist | Out-Null
Copy-Item target/x86_64-pc-windows-msvc/release/cascade.exe dist/cascade.exe
Compress-Archive -Path dist/cascade.exe -DestinationPath dist/cascade-x86_64-windows.zip
$hash = (Get-FileHash -Algorithm SHA256 dist/cascade-x86_64-windows.zip).Hash.ToLower()
$hash | Out-File -Encoding ascii -NoNewline dist/cascade-x86_64-windows.zip.sha256
- name: Upload to release
uses: softprops/action-gh-release@b4309332981a82ec1c5618f44dd2e27cc8bfbfda # v3.0.0
with:
tag_name: ${{ needs.release.outputs.version }}
files: dist/cascade-x86_64-windows.zip*
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
update-tap:
name: Update Homebrew tap
needs: [release, build-macos-arm, build-macos-x86, build-linux-x86, build-linux-arm]
if: needs.release.outputs.released == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout tap via SSH deploy key
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: Mearman/homebrew-cascade
ssh-key: ${{ secrets.TAP_DEPLOY_KEY }}
- name: Download checksums
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
TAG_URL="https://github.com/Mearman/cascade/releases/download/${VERSION}"
# `--fail` makes curl exit non-zero on 4xx/5xx so a Not-Found body
# doesn't get piped through awk and end up in the formula (see the
# v0.1.33 Scoop bucket regression where `Hash: Not` shipped).
# `--retry` covers the small propagation window between
# `softprops/action-gh-release` returning success and the asset
# being downloadable from the public CDN. Validate that what we
# got is a 64-char lowercase hex sha256 before letting it through.
fetch_sha() {
local name="$1"
local out="$2"
curl -sL --fail --retry 6 --retry-delay 5 --retry-all-errors \
"${TAG_URL}/${name}.sha256" | awk '{print $1}' > "${out}"
if ! [[ "$(cat "${out}")" =~ ^[0-9a-f]{64}$ ]]; then
echo "::error::checksum for ${name} not a 64-char hex digest: $(cat "${out}")"
exit 1
fi
}
fetch_sha cascade-aarch64-macos.tar.gz sha-macos-arm
fetch_sha cascade-x86_64-macos.tar.gz sha-macos-x86
fetch_sha cascade-x86_64-linux.tar.gz sha-linux-x86
fetch_sha cascade-aarch64-linux.tar.gz sha-linux-arm
- name: Generate formula
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
VERSION_NUM="${VERSION#cascade-v}"
MACOS_ARM_SHA=$(cat sha-macos-arm)
MACOS_X86_SHA=$(cat sha-macos-x86)
LINUX_X86_SHA=$(cat sha-linux-x86)
LINUX_ARM_SHA=$(cat sha-linux-arm)
mkdir -p Formula
cat > Formula/cascade.rb <<EOF
# Auto-generated by CI — do not edit manually.
# Regenerated on each semantic release from Mearman/cascade.
class Cascade < Formula
desc "Cross-platform cloud storage filesystem client"
homepage "https://github.com/Mearman/cascade"
version "${VERSION_NUM}"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/Mearman/cascade/releases/download/${VERSION}/cascade-aarch64-macos.tar.gz"
sha256 "${MACOS_ARM_SHA}"
else
url "https://github.com/Mearman/cascade/releases/download/${VERSION}/cascade-x86_64-macos.tar.gz"
sha256 "${MACOS_X86_SHA}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/Mearman/cascade/releases/download/${VERSION}/cascade-aarch64-linux.tar.gz"
sha256 "${LINUX_ARM_SHA}"
else
url "https://github.com/Mearman/cascade/releases/download/${VERSION}/cascade-x86_64-linux.tar.gz"
sha256 "${LINUX_X86_SHA}"
end
end
def install
bin.install "cascade"
end
service do
run [opt_bin/"cascade", "start"]
keep_alive true
log_path var/"log/cascade.log"
error_log_path var/"log/cascade.err.log"
end
test do
assert_match version.to_s, shell_output("\#{bin}/cascade --version")
end
end
EOF
- name: Commit and push
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/cascade.rb
git diff --cached --quiet || git commit -m "chore: update formula to ${VERSION}"
git push
update-scoop:
name: Update Scoop bucket
needs: [release, build-windows-x86]
if: needs.release.outputs.released == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout bucket via SSH deploy key
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: Mearman/scoop-cascade
ssh-key: ${{ secrets.SCOOP_DEPLOY_KEY }}
- name: Download checksum
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
TAG_URL="https://github.com/Mearman/cascade/releases/download/${VERSION}"
# See the matching block in `update-tap` for the rationale —
# this is the regression that landed in v0.1.33 as `Hash: Not`.
curl -sL --fail --retry 6 --retry-delay 5 --retry-all-errors \
"${TAG_URL}/cascade-x86_64-windows.zip.sha256" | awk '{print $1}' > sha-windows-x86
if ! [[ "$(cat sha-windows-x86)" =~ ^[0-9a-f]{64}$ ]]; then
echo "::error::Windows checksum not a 64-char hex digest: $(cat sha-windows-x86)"
exit 1
fi
- name: Generate manifest
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
VERSION_NUM="${VERSION#cascade-v}"
WINDOWS_X86_SHA=$(cat sha-windows-x86)
mkdir -p bucket
cat > bucket/cascade.json <<EOF
{
"version": "${VERSION_NUM}",
"description": "Cross-platform cloud storage filesystem client",
"homepage": "https://github.com/Mearman/cascade",
"license": "MIT",
"architecture": {
"64bit": {
"url": "https://github.com/Mearman/cascade/releases/download/${VERSION}/cascade-x86_64-windows.zip",
"hash": "${WINDOWS_X86_SHA}"
}
},
"bin": "cascade.exe",
"checkver": {
"github": "https://github.com/Mearman/cascade"
},
"autoupdate": {
"architecture": {
"64bit": {
"url": "https://github.com/Mearman/cascade/releases/download/v\$version/cascade-x86_64-windows.zip"
}
}
}
}
EOF
- name: Commit and push
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add bucket/cascade.json
git diff --cached --quiet || git commit -m "chore: update manifest to ${VERSION}"
git push
verify-brew-macos:
name: Verify Homebrew install (macOS)
needs: [release, update-tap]
if: needs.release.outputs.released == 'true'
timeout-minutes: 15
runs-on: macos-latest
steps:
- name: Cache Homebrew downloads
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/Library/Caches/Homebrew/downloads
key: brew-cache-macos-${{ needs.release.outputs.version }}
restore-keys: brew-cache-macos-
- name: Tap Homebrew formula
run: brew tap Mearman/cascade
- name: Install via Homebrew
run: brew install Mearman/cascade/cascade
- name: Run cascade --version
run: |
cascade --version
# Verify the installed version matches the release tag.
INSTALLED=$(cascade --version | awk '{print $2}')
EXPECTED="${VERSION#cascade-v}"
if [ "$INSTALLED" != "$EXPECTED" ]; then
echo "version mismatch: installed=$INSTALLED expected=$EXPECTED"
exit 1
fi
env:
VERSION: ${{ needs.release.outputs.version }}
verify-brew-linux:
name: Verify Homebrew install (Linux)
needs: [release, update-tap]
if: needs.release.outputs.released == 'true'
timeout-minutes: 15
runs-on: ubuntu-latest
steps:
- name: Cache Linuxbrew downloads
# Only cache the bottle download dir — caching the full prefix
# would restore an older cascade binary across versions and
# `brew install` would no-op, causing the version check below
# to fail with the previous release's version.
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/Homebrew/downloads
key: linuxbrew-downloads-${{ needs.release.outputs.version }}
restore-keys: linuxbrew-downloads-
- name: Install Homebrew on Linux
run: |
if ! command -v brew >/dev/null 2>&1 && ! [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" </dev/null
fi
echo "/home/linuxbrew/.linuxbrew/bin" >> $GITHUB_PATH
- name: Tap Homebrew formula
# brew tap can trigger SIGPIPE (exit 141) on Linux when its verbose
# output overflows the Actions runner's stdout pipe. The tap itself
# succeeds — the failure is in output flushing. Suppress stdout and
# verify the tap directory exists instead of relying on the exit code.
run: |
brew tap Mearman/cascade >/dev/null || true
test -d /home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/mearman/homebrew-cascade
- name: Install via Homebrew
run: brew install Mearman/cascade/cascade
- name: Run cascade --version
run: |
cascade --version
INSTALLED=$(cascade --version | awk '{print $2}')
EXPECTED="${VERSION#cascade-v}"
if [ "$INSTALLED" != "$EXPECTED" ]; then
echo "version mismatch: installed=$INSTALLED expected=$EXPECTED"
exit 1
fi
env:
VERSION: ${{ needs.release.outputs.version }}
verify-scoop:
name: Verify Scoop install (Windows)
needs: [release, update-scoop]
if: needs.release.outputs.released == 'true'
timeout-minutes: 15
runs-on: windows-2025-vs2026
steps:
- name: Cache Scoop downloads
# Caching the full ~/scoop dir restores an older cascade across
# releases, so `scoop install` becomes a no-op and the version
# check fails. Cache only the bottle download dir.
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~\scoop\cache
key: scoop-downloads-${{ needs.release.outputs.version }}
restore-keys: scoop-downloads-
- name: Install Scoop
shell: pwsh
run: |
if (-not (Test-Path "$env:USERPROFILE\scoop\shims\scoop.ps1")) {
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
}
Add-Content $env:GITHUB_PATH "$env:USERPROFILE\scoop\shims"
- name: Add bucket and install cascade
shell: pwsh
run: |
if (-not (scoop bucket list | Select-String -Quiet '^cascade')) {
scoop bucket add cascade https://github.com/Mearman/scoop-cascade
}
scoop install cascade
- name: Run cascade --version
shell: pwsh
env:
VERSION: ${{ needs.release.outputs.version }}
run: |
$output = cascade --version
Write-Host $output
$installed = ($output -split '\s+')[1]
$expected = $env:VERSION -replace '^cascade-v',''
if ($installed -ne $expected) {
Write-Host "version mismatch: installed=$installed expected=$expected"
exit 1
}
smoke-macos:
name: Smoke test (macOS — daemon + mount)
needs: [release, update-tap]
if: needs.release.outputs.released == 'true'
timeout-minutes: 20
runs-on: macos-latest
steps:
- name: Cache Homebrew downloads
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/Library/Caches/Homebrew/downloads
key: brew-cache-macos-${{ needs.release.outputs.version }}
restore-keys: brew-cache-macos-
- name: Tap Homebrew formula
run: brew tap Mearman/cascade
- name: Install via Homebrew
run: brew install Mearman/cascade/cascade
- name: Prepare smoke config (local backend)
run: |
set -eux
SMOKE="$RUNNER_TEMP/cascade-smoke"
mkdir -p "$SMOKE/config" "$SMOKE/storage" "$SMOKE/mount"
cat > "$SMOKE/config/config.toml" <<TOML
[backends.smoke]
type = "local"
root_path = "$SMOKE/storage"
[mount]
point = "$SMOKE/mount"
TOML
cat > "$SMOKE/config/smoke.toml" <<TOML
type = "local"
root_path = "$SMOKE/storage"
id = "smoke"
display_name = "Smoke"
TOML
echo "SMOKE=$SMOKE" >> "$GITHUB_ENV"
- name: Tier 2 — daemon up with --no-mount
run: |
set -eux
cascade --config "$SMOKE/config" start --no-mount > "$SMOKE/daemon.log" 2>&1 &
DAEMON=$!
echo "DAEMON=$DAEMON" >> "$GITHUB_ENV"
for i in 1 2 3 4 5 6 7 8 9 10; do
sleep 1
cascade --config "$SMOKE/config" status 2>/dev/null | grep -q "Running: true" && break
done
cascade --config "$SMOKE/config" status | tee /tmp/status1.txt
grep -q "Running: true" /tmp/status1.txt
grep -q "smoke" /tmp/status1.txt
cascade --config "$SMOKE/config" stop
wait $DAEMON 2>/dev/null || true
- name: Tier 3 — actual WebDAV mount + file round-trip
run: |
set -eux
echo "pre-seeded" > "$SMOKE/storage/hello.txt"
cascade --config "$SMOKE/config" start > "$SMOKE/mount.log" 2>&1 &
DAEMON=$!
for i in $(seq 1 30); do
sleep 1
mount | grep -q "$SMOKE/mount" && break
done
mount | grep "$SMOKE/mount"
ls -la "$SMOKE/mount/smoke/"
cat "$SMOKE/mount/smoke/hello.txt" | grep -q "pre-seeded"
cascade --config "$SMOKE/config" stop
wait $DAEMON 2>/dev/null || true
- name: Dump daemon log on failure
if: failure()
run: |
echo "=== daemon.log ==="; cat "$SMOKE/daemon.log" || true
echo "=== mount.log ==="; cat "$SMOKE/mount.log" || true
smoke-linux:
name: Smoke test (Linux — daemon + FUSE mount)
needs: [release, update-tap]
if: needs.release.outputs.released == 'true'
timeout-minutes: 20
runs-on: ubuntu-latest
steps:
- name: Install FUSE
run: |
sudo apt-get update
sudo apt-get install -y fuse3 libfuse3-3
- name: Cache Linuxbrew downloads
# See verify-brew-linux above: caching the prefix breaks the
# per-release install.
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~/.cache/Homebrew/downloads
key: linuxbrew-downloads-${{ needs.release.outputs.version }}
restore-keys: linuxbrew-downloads-
- name: Install Homebrew on Linux
run: |
if ! command -v brew >/dev/null 2>&1 && ! [ -x /home/linuxbrew/.linuxbrew/bin/brew ]; then
/bin/bash -c "$(curl -fsSL https://raw.githubusercontent.com/Homebrew/install/HEAD/install.sh)" </dev/null
fi
echo "/home/linuxbrew/.linuxbrew/bin" >> "$GITHUB_PATH"
- name: Tap Homebrew formula
run: |
brew tap Mearman/cascade >/dev/null || true
test -d /home/linuxbrew/.linuxbrew/Homebrew/Library/Taps/mearman/homebrew-cascade
- name: Install via Homebrew
run: brew install Mearman/cascade/cascade
- name: Prepare smoke config (local backend)
run: |
set -eux
SMOKE="$RUNNER_TEMP/cascade-smoke"
mkdir -p "$SMOKE/config" "$SMOKE/storage" "$SMOKE/mount"
cat > "$SMOKE/config/config.toml" <<TOML
[backends.smoke]
type = "local"
root_path = "$SMOKE/storage"
[mount]
point = "$SMOKE/mount"
TOML
cat > "$SMOKE/config/smoke.toml" <<TOML
type = "local"
root_path = "$SMOKE/storage"
id = "smoke"
display_name = "Smoke"
TOML
echo "SMOKE=$SMOKE" >> "$GITHUB_ENV"
- name: Tier 2 — daemon up with --no-mount
run: |
set -eux
cascade --config "$SMOKE/config" start --no-mount > "$SMOKE/daemon.log" 2>&1 &
DAEMON=$!
for i in 1 2 3 4 5 6 7 8 9 10; do
sleep 1
cascade --config "$SMOKE/config" status 2>/dev/null | grep -q "Running: true" && break
done
cascade --config "$SMOKE/config" status | tee /tmp/status1.txt
grep -q "Running: true" /tmp/status1.txt
grep -q "smoke" /tmp/status1.txt
cascade --config "$SMOKE/config" stop
wait $DAEMON 2>/dev/null || true
- name: Tier 3 — actual FUSE mount + file read
run: |
set -eux
echo "pre-seeded" > "$SMOKE/storage/hello.txt"
cascade --config "$SMOKE/config" start > "$SMOKE/mount.log" 2>&1 &
DAEMON=$!
for i in $(seq 1 30); do
sleep 1
mount | grep -q "$SMOKE/mount" && break
done
mount | grep "$SMOKE/mount" || (echo "FUSE mount did not appear within 30s"; cat "$SMOKE/mount.log"; exit 1)
ls -la "$SMOKE/mount/" || true
cascade --config "$SMOKE/config" stop
wait $DAEMON 2>/dev/null || true
- name: Dump daemon log on failure
if: failure()
run: |
echo "=== daemon.log ==="; cat "$SMOKE/daemon.log" || true
echo "=== mount.log ==="; cat "$SMOKE/mount.log" || true
smoke-windows:
name: Smoke test (Windows — daemon + WebDAV mount)
needs: [release, update-scoop]
if: needs.release.outputs.released == 'true'
timeout-minutes: 20
runs-on: windows-2025-vs2026
steps:
- name: Cache Scoop downloads
# Caching the full ~/scoop dir restores an older cascade across
# releases, so `scoop install` becomes a no-op and the version
# check fails. Cache only the bottle download dir.
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ~\scoop\cache
key: scoop-downloads-${{ needs.release.outputs.version }}
restore-keys: scoop-downloads-
- name: Install Scoop
shell: pwsh
run: |
if (-not (Test-Path "$env:USERPROFILE\scoop\shims\scoop.ps1")) {
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
Invoke-RestMethod -Uri https://get.scoop.sh | Invoke-Expression
}
Add-Content $env:GITHUB_PATH "$env:USERPROFILE\scoop\shims"
- name: Add bucket and install cascade
shell: pwsh
run: |
if (-not (scoop bucket list | Select-String -Quiet '^cascade')) {
scoop bucket add cascade https://github.com/Mearman/scoop-cascade
}
scoop install cascade
- name: Note WebClient service availability
shell: pwsh
# windows-latest is Windows Server, which does not ship the
# WebClient service by default (it's part of Desktop Experience).
# The cascade-managed `net use` mount path therefore can't be
# exercised here. We instead verify the WebDAV server cascade
# runs in-process is reachable — the `net use` call itself is
# a thin wrapper around the Windows redirector that requires
# WebClient to work.
run: |
$svc = Get-Service -Name WebClient -ErrorAction SilentlyContinue
if ($null -eq $svc) {
Write-Host "WebClient service not installed on this runner — using HTTP probe instead of net use for tier 3."
} else {
Set-Service -Name WebClient -StartupType Automatic
Start-Service -Name WebClient
Get-Service -Name WebClient
}
- name: Prepare smoke config (local backend)
shell: pwsh
run: |
$smoke = "$env:RUNNER_TEMP\cascade-smoke"
$storage = "$smoke\storage"
$mount = "$smoke\mount"
$config = "$smoke\config"
New-Item -ItemType Directory -Force -Path $config, $storage, $mount | Out-Null
$storageFwd = $storage -replace '\\','/'
$mountFwd = $mount -replace '\\','/'
@"
[backends.smoke]
type = "local"
root_path = "$storageFwd"
[mount]
point = "$mountFwd"
"@ | Out-File -Encoding utf8 -NoNewline "$config\config.toml"
@"
type = "local"
root_path = "$storageFwd"
id = "smoke"
display_name = "Smoke"
"@ | Out-File -Encoding utf8 -NoNewline "$config\smoke.toml"
"SMOKE=$smoke" | Add-Content -Path $env:GITHUB_ENV
- name: Tier 2 — daemon up with --no-mount
shell: pwsh
run: |
$smoke = $env:SMOKE
$proc = Start-Process -FilePath cascade -ArgumentList "--config","$smoke\config","start","--no-mount" -PassThru -RedirectStandardOutput "$smoke\daemon.log" -RedirectStandardError "$smoke\daemon.err"
for ($i = 0; $i -lt 10; $i++) {
Start-Sleep -Seconds 1
$status = (cascade --config "$smoke\config" status 2>$null)
if ($status -match "Running: true") { break }
}
$status = cascade --config "$smoke\config" status
Write-Host $status
if (-not ($status -match "Running: true")) { throw "daemon did not report running" }
if (-not ($status -match "smoke")) { throw "backend not registered" }
cascade --config "$smoke\config" stop
Wait-Process -Id $proc.Id -Timeout 10 -ErrorAction SilentlyContinue
- name: Tier 3 — WebDAV server reachable via HTTP
shell: pwsh
# We can't exercise `net use` without the WebClient service, but
# the WebDAV server cascade runs in-process is testable directly:
# PROPFIND on the root should return 207 Multi-Status with the
# registered backends as collections.
run: |
$smoke = $env:SMOKE
"pre-seeded" | Out-File -Encoding ascii -NoNewline "$smoke\storage\hello.txt"
$proc = Start-Process -FilePath cascade -ArgumentList "--config","$smoke\config","start","--no-mount" -PassThru -RedirectStandardOutput "$smoke\mount.log" -RedirectStandardError "$smoke\mount.err"
$port = $null
for ($i = 0; $i -lt 30; $i++) {
Start-Sleep -Seconds 1
if (Test-Path "$smoke\mount.log") {
$log = Get-Content "$smoke\mount.log" -Raw
if ($log -match 'WebDAV server started.*port[= ]+(\d+)') {
$port = $matches[1]; break
}
if ($log -match 'WebDAV URL: http://localhost:(\d+)') {
$port = $matches[1]; break
}
}
}
if (-not $port) {
Write-Host (Get-Content "$smoke\mount.log" -ErrorAction SilentlyContinue)
throw "WebDAV server port not detected in daemon log"
}
Write-Host "WebDAV port: $port"
# Invoke-WebRequest only knows standard HTTP verbs; WebDAV's
# PROPFIND isn't in its WebRequestMethod enum. curl.exe ships
# with Windows 10+/Server 2019+ and accepts any verb.
$body = curl.exe -sS -X PROPFIND -H "Depth: 1" -w "%{http_code}" -o response.xml "http://localhost:$port/"
Write-Host "HTTP $body"
if ($body -ne '207') { throw "expected 207, got $body" }
$content = Get-Content response.xml -Raw
if ($content -notmatch 'smoke') { throw "registered backend not in PROPFIND response" }
Write-Host "PROPFIND returned 207 with backend present."
cascade --config "$smoke\config" stop
Wait-Process -Id $proc.Id -Timeout 10 -ErrorAction SilentlyContinue
- name: Dump daemon log on failure
if: failure()
shell: pwsh
run: |
$smoke = $env:SMOKE
if (Test-Path "$smoke\daemon.log") { Write-Host "=== daemon.log ==="; Get-Content "$smoke\daemon.log" }
if (Test-Path "$smoke\daemon.err") { Write-Host "=== daemon.err ==="; Get-Content "$smoke\daemon.err" }
if (Test-Path "$smoke\mount.log") { Write-Host "=== mount.log ==="; Get-Content "$smoke\mount.log" }
if (Test-Path "$smoke\mount.err") { Write-Host "=== mount.err ==="; Get-Content "$smoke\mount.err" }
# ===========================================================================
# Relay artifacts — built and attached to the SAME cascade release.
#
# Single-release model: the relay binaries + checksums become assets of the
# cascade-vX release (named cascade-relay-* so they never collide with the
# cascade-* assets), and the Docker image is pushed to
# ghcr.io/mearman/cascade-relay. Every job gates on the cascade release and
# uploads to the release release-plz already created — so there is no separate
# cascade-relay-vX tag and no create-before-upload race. needs.release.outputs
# .version is the cascade tag (e.g. cascade-v0.1.57); the bare version is
# derived where needed.
# ===========================================================================
docker-amd64:
name: Docker image (amd64)
needs: [release]
if: needs.release.outputs.released == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.export.outputs.digest }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- name: Set up buildx
run: |
docker buildx create --use --name relay-builder --driver docker-container
docker buildx inspect --bootstrap
- name: Log in to ghcr.io
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Build and push by digest
env:
TAG: ${{ needs.release.outputs.version }}
run: |
VERSION="${TAG#cascade-v}"
docker buildx build \
--platform linux/amd64 \
--file crates/relay-server/Dockerfile \
--label "org.opencontainers.image.source=https://github.com/Mearman/cascade" \
--label "org.opencontainers.image.version=${VERSION}" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.description=cascade-relay — opaque byte-pipe relay for Cascade peers behind NATs" \
--label "org.opencontainers.image.licenses=MIT" \
--cache-from "type=registry,ref=ghcr.io/${IMAGE_NAME}:buildcache-amd64" \
--cache-to "type=registry,ref=ghcr.io/${IMAGE_NAME}:buildcache-amd64,mode=max" \
--output "type=image,name=ghcr.io/${IMAGE_NAME},push-by-digest=true,name-canonical=true,push=true" \
--metadata-file metadata.json \
.
- name: Export digest
id: export
run: |
DIGEST=$(python3 -c "import json; print(json.load(open('metadata.json'))['containerimage.digest'])")
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
docker-arm64:
name: Docker image (arm64)
needs: [release]
if: needs.release.outputs.released == 'true'
timeout-minutes: 30
runs-on: ubuntu-24.04-arm
permissions:
contents: read
packages: write
outputs:
digest: ${{ steps.export.outputs.digest }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- name: Set up buildx
run: |
docker buildx create --use --name relay-builder --driver docker-container
docker buildx inspect --bootstrap
- name: Log in to ghcr.io
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Build and push by digest
env:
TAG: ${{ needs.release.outputs.version }}
run: |
VERSION="${TAG#cascade-v}"
docker buildx build \
--platform linux/arm64 \
--file crates/relay-server/Dockerfile \
--label "org.opencontainers.image.source=https://github.com/Mearman/cascade" \
--label "org.opencontainers.image.version=${VERSION}" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.description=cascade-relay — opaque byte-pipe relay for Cascade peers behind NATs" \
--label "org.opencontainers.image.licenses=MIT" \
--cache-from "type=registry,ref=ghcr.io/${IMAGE_NAME}:buildcache-arm64" \
--cache-to "type=registry,ref=ghcr.io/${IMAGE_NAME}:buildcache-arm64,mode=max" \
--output "type=image,name=ghcr.io/${IMAGE_NAME},push-by-digest=true,name-canonical=true,push=true" \
--metadata-file metadata.json \
.
- name: Export digest
id: export
run: |
DIGEST=$(python3 -c "import json; print(json.load(open('metadata.json'))['containerimage.digest'])")
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
docker-manifest:
name: Docker manifest (ghcr.io)
needs: [release, docker-amd64, docker-arm64]
if: needs.release.outputs.released == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
steps:
- name: Log in to ghcr.io
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Create and push multi-arch manifest
env:
TAG: ${{ needs.release.outputs.version }}
DIGEST_AMD64: ${{ needs.docker-amd64.outputs.digest }}
DIGEST_ARM64: ${{ needs.docker-arm64.outputs.digest }}
run: |
VERSION="${TAG#cascade-v}"
docker buildx imagetools create \
--tag "ghcr.io/${IMAGE_NAME}:${VERSION}" \
--tag "ghcr.io/${IMAGE_NAME}:latest" \
"ghcr.io/${IMAGE_NAME}@${DIGEST_AMD64}" \
"ghcr.io/${IMAGE_NAME}@${DIGEST_ARM64}"
# ===========================================================================
# Daemon Docker image — multi-arch (linux/amd64 + linux/arm64).
#
# Mirrors docker-amd64 / docker-arm64 / docker-manifest above but targets
# ghcr.io/mearman/cascade (the cascade daemon) rather than the relay image.
# Per-job IMAGE_NAME overrides the workflow-level mearman/cascade-relay so
# the buildcache refs, manifest tags, and push targets do not collide.
# ===========================================================================
docker-daemon-amd64:
name: Docker daemon image (amd64)
needs: [release]
if: needs.release.outputs.released == 'true'
timeout-minutes: 60
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
IMAGE_NAME: mearman/cascade
outputs:
digest: ${{ steps.export.outputs.digest }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- name: Set up buildx
run: |
docker buildx create --use --name daemon-builder --driver docker-container
docker buildx inspect --bootstrap
- name: Log in to ghcr.io
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Build and push by digest
env:
TAG: ${{ needs.release.outputs.version }}
run: |
VERSION="${TAG#cascade-v}"
docker buildx build \
--platform linux/amd64 \
--file crates/cascade/Dockerfile \
--label "org.opencontainers.image.source=https://github.com/Mearman/cascade" \
--label "org.opencontainers.image.version=${VERSION}" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.description=cascade daemon — cross-platform cloud storage filesystem client" \
--label "org.opencontainers.image.licenses=MIT" \
--cache-from "type=registry,ref=ghcr.io/${IMAGE_NAME}:buildcache-cascade-amd64" \
--cache-to "type=registry,ref=ghcr.io/${IMAGE_NAME}:buildcache-cascade-amd64,mode=max" \
--output "type=image,name=ghcr.io/${IMAGE_NAME},push-by-digest=true,name-canonical=true,push=true" \
--metadata-file metadata.json \
.
- name: Export digest
id: export
run: |
DIGEST=$(python3 -c "import json; print(json.load(open('metadata.json'))['containerimage.digest'])")
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
docker-daemon-arm64:
name: Docker daemon image (arm64)
needs: [release]
if: needs.release.outputs.released == 'true'
timeout-minutes: 60
runs-on: ubuntu-24.04-arm
permissions:
contents: read
packages: write
env:
IMAGE_NAME: mearman/cascade
outputs:
digest: ${{ steps.export.outputs.digest }}
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- name: Set up buildx
run: |
docker buildx create --use --name daemon-builder --driver docker-container
docker buildx inspect --bootstrap
- name: Log in to ghcr.io
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Build and push by digest
env:
TAG: ${{ needs.release.outputs.version }}
run: |
VERSION="${TAG#cascade-v}"
docker buildx build \
--platform linux/arm64 \
--file crates/cascade/Dockerfile \
--label "org.opencontainers.image.source=https://github.com/Mearman/cascade" \
--label "org.opencontainers.image.version=${VERSION}" \
--label "org.opencontainers.image.revision=${{ github.sha }}" \
--label "org.opencontainers.image.description=cascade daemon — cross-platform cloud storage filesystem client" \
--label "org.opencontainers.image.licenses=MIT" \
--cache-from "type=registry,ref=ghcr.io/${IMAGE_NAME}:buildcache-cascade-arm64" \
--cache-to "type=registry,ref=ghcr.io/${IMAGE_NAME}:buildcache-cascade-arm64,mode=max" \
--output "type=image,name=ghcr.io/${IMAGE_NAME},push-by-digest=true,name-canonical=true,push=true" \
--metadata-file metadata.json \
.
- name: Export digest
id: export
run: |
DIGEST=$(python3 -c "import json; print(json.load(open('metadata.json'))['containerimage.digest'])")
echo "digest=${DIGEST}" >> "$GITHUB_OUTPUT"
docker-daemon-manifest:
name: Docker daemon manifest (ghcr.io)
needs: [release, docker-daemon-amd64, docker-daemon-arm64]
if: needs.release.outputs.released == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: read
packages: write
env:
IMAGE_NAME: mearman/cascade
steps:
- name: Log in to ghcr.io
run: |
echo "${{ secrets.GITHUB_TOKEN }}" | \
docker login ghcr.io --username "${{ github.actor }}" --password-stdin
- name: Create and push multi-arch manifest
env:
TAG: ${{ needs.release.outputs.version }}
DIGEST_AMD64: ${{ needs.docker-daemon-amd64.outputs.digest }}
DIGEST_ARM64: ${{ needs.docker-daemon-arm64.outputs.digest }}
run: |
VERSION="${TAG#cascade-v}"
docker buildx imagetools create \
--tag "ghcr.io/${IMAGE_NAME}:${VERSION}" \
--tag "ghcr.io/${IMAGE_NAME}:latest" \
"ghcr.io/${IMAGE_NAME}@${DIGEST_AMD64}" \
"ghcr.io/${IMAGE_NAME}@${DIGEST_ARM64}"
build-relay-macos-arm:
name: Build relay macOS ARM
needs: [release]
if: needs.release.outputs.released == 'true'
timeout-minutes: 30
runs-on: macos-latest
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable --target aarch64-apple-darwin
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-relay-aarch64-apple-darwin-${{ github.sha }}
restore-keys: sccache-relay-aarch64-apple-darwin-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: relay-aarch64-apple-darwin
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build
# Retry on transient cargo registry network failures. The last
# attempt's exit code propagates, so permanent build errors
# still fail the job.
shell: bash
run: |
status=1
for i in 1 2 3; do
if cargo build --release --target aarch64-apple-darwin -p cascade-relay-server; then
status=0
break
fi
echo "::warning::Build attempt $i failed, retrying..."
sleep 10
done
exit $status
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-relay-aarch64-apple-darwin-${{ github.sha }}
- name: Package archive
run: |
mkdir -p dist
cp target/aarch64-apple-darwin/release/cascade-relay dist/cascade-relay
cd dist
tar czf cascade-relay-aarch64-macos.tar.gz cascade-relay
shasum -a 256 cascade-relay-aarch64-macos.tar.gz | awk '{print $1}' \
> cascade-relay-aarch64-macos.tar.gz.sha256
- name: Upload to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
TAG: ${{ needs.release.outputs.version }}
run: |
gh release upload "$TAG" \
dist/cascade-relay-aarch64-macos.tar.gz \
dist/cascade-relay-aarch64-macos.tar.gz.sha256 \
--repo Mearman/cascade \
--clobber
build-relay-macos-x86:
name: Build relay macOS x86_64
needs: [release]
if: needs.release.outputs.released == 'true'
timeout-minutes: 30
runs-on: macos-latest
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable --target x86_64-apple-darwin
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
rustup target add x86_64-apple-darwin
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-relay-x86_64-apple-darwin-${{ github.sha }}
restore-keys: sccache-relay-x86_64-apple-darwin-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: relay-x86_64-apple-darwin
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Build
# Retry on transient cargo registry network failures. The last
# attempt's exit code propagates, so permanent build errors
# still fail the job.
shell: bash
run: |
status=1
for i in 1 2 3; do
if cargo build --release --target x86_64-apple-darwin -p cascade-relay-server; then
status=0
break
fi
echo "::warning::Build attempt $i failed, retrying..."
sleep 10
done
exit $status
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-relay-x86_64-apple-darwin-${{ github.sha }}
- name: Package archive
run: |
mkdir -p dist
cp target/x86_64-apple-darwin/release/cascade-relay dist/cascade-relay
cd dist
tar czf cascade-relay-x86_64-macos.tar.gz cascade-relay
shasum -a 256 cascade-relay-x86_64-macos.tar.gz | awk '{print $1}' \
> cascade-relay-x86_64-macos.tar.gz.sha256
- name: Upload to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELAY_TAG: ${{ needs.release.outputs.version }}
run: |
gh release upload "$RELAY_TAG" \
dist/cascade-relay-x86_64-macos.tar.gz \
dist/cascade-relay-x86_64-macos.tar.gz.sha256 \
--repo Mearman/cascade \
--clobber
build-relay-linux-x86:
name: Build relay Linux x86_64
needs: [release]
if: needs.release.outputs.released == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- name: Install Rust toolchain
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-relay-x86_64-unknown-linux-gnu-${{ github.sha }}
restore-keys: sccache-relay-x86_64-unknown-linux-gnu-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: relay-x86_64-unknown-linux-gnu
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install system dependencies
run: sudo apt-get update && sudo apt-get install -y libfuse3-dev
- name: Build
# Retry on transient cargo registry network failures. The last
# attempt's exit code propagates, so permanent build errors
# still fail the job.
shell: bash
run: |
status=1
for i in 1 2 3; do
if cargo build --release --target x86_64-unknown-linux-gnu -p cascade-relay-server; then
status=0
break
fi
echo "::warning::Build attempt $i failed, retrying..."
sleep 10
done
exit $status
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-relay-x86_64-unknown-linux-gnu-${{ github.sha }}
- name: Package archive
run: |
mkdir -p dist
cp target/x86_64-unknown-linux-gnu/release/cascade-relay dist/cascade-relay
cd dist
tar czf cascade-relay-x86_64-linux.tar.gz cascade-relay
sha256sum cascade-relay-x86_64-linux.tar.gz | awk '{print $1}' \
> cascade-relay-x86_64-linux.tar.gz.sha256
- name: Upload to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELAY_TAG: ${{ needs.release.outputs.version }}
run: |
gh release upload "$RELAY_TAG" \
dist/cascade-relay-x86_64-linux.tar.gz \
dist/cascade-relay-x86_64-linux.tar.gz.sha256 \
--repo Mearman/cascade \
--clobber
build-relay-linux-arm:
name: Build relay Linux ARM64
needs: [release]
if: needs.release.outputs.released == 'true'
timeout-minutes: 30
runs-on: ubuntu-latest
permissions:
contents: write
env:
RUSTC_WRAPPER: sccache
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
ref: ${{ needs.release.outputs.version }}
- name: Install Rust toolchain and cross target
run: |
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | \
sh -s -- -y --default-toolchain stable
echo "$HOME/.cargo/bin" >> "$GITHUB_PATH"
rustup target add aarch64-unknown-linux-gnu
- name: Install sccache
uses: taiki-e/install-action@15449e3094499af05d8d964a1c884208e4b8b595 # v2.81.11
with:
tool: sccache
- name: Configure sccache dir
# runner.temp is not available in job-level env, so export SCCACHE_DIR
# here (bash is present on every runner OS, so this is uniform across
# Linux/macOS/Windows). It matches the actions/cache path below.
shell: bash
run: echo "SCCACHE_DIR=$RUNNER_TEMP/sccache" >> "$GITHUB_ENV"
- name: Restore sccache
uses: actions/cache/restore@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-relay-aarch64-unknown-linux-gnu-${{ github.sha }}
restore-keys: sccache-relay-aarch64-unknown-linux-gnu-
- uses: Swatinem/rust-cache@c19371144df3bb44fab255c43d04cbc2ab54d1c4 # v2.9.1
with:
key: relay-aarch64-unknown-linux-gnu
save-if: ${{ github.ref == 'refs/heads/main' }}
- name: Install cross-compilation toolchain
run: |
sudo apt-get update
sudo apt-get install -y gcc-aarch64-linux-gnu libfuse3-dev
- name: Build
# Retry on transient cargo registry network failures. The last
# attempt's exit code propagates, so permanent build errors
# still fail the job.
shell: bash
run: |
status=1
for i in 1 2 3; do
if cargo build --release --target aarch64-unknown-linux-gnu -p cascade-relay-server; then
status=0
break
fi
echo "::warning::Build attempt $i failed, retrying..."
sleep 10
done
exit $status
env:
CARGO_TARGET_AARCH64_UNKNOWN_LINUX_GNU_LINKER: aarch64-linux-gnu-gcc
- name: sccache stats
run: sccache --show-stats
- name: Save sccache
if: github.ref == 'refs/heads/main'
# sccache is a build-time accelerator; an empty or failed cache save
# is a noisy warning, not a real failure. Continue on error so the
# post-build upload doesn't pollute the run summary.
continue-on-error: true
uses: actions/cache/save@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: ${{ runner.temp }}/sccache
key: sccache-relay-aarch64-unknown-linux-gnu-${{ github.sha }}
- name: Package archive
run: |
mkdir -p dist
cp target/aarch64-unknown-linux-gnu/release/cascade-relay dist/cascade-relay
cd dist
tar czf cascade-relay-aarch64-linux.tar.gz cascade-relay
sha256sum cascade-relay-aarch64-linux.tar.gz | awk '{print $1}' \
> cascade-relay-aarch64-linux.tar.gz.sha256
- name: Upload to release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
RELAY_TAG: ${{ needs.release.outputs.version }}
run: |
gh release upload "$RELAY_TAG" \
dist/cascade-relay-aarch64-linux.tar.gz \
dist/cascade-relay-aarch64-linux.tar.gz.sha256 \
--repo Mearman/cascade \
--clobber
update-tap-relay:
name: Update Homebrew tap (relay)
needs:
- release
- build-relay-macos-arm
- build-relay-macos-x86
- build-relay-linux-x86
- build-relay-linux-arm
if: needs.release.outputs.released == 'true'
timeout-minutes: 10
runs-on: ubuntu-latest
steps:
- name: Checkout tap via SSH deploy key
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
with:
repository: Mearman/homebrew-cascade
ssh-key: ${{ secrets.TAP_DEPLOY_KEY }}
- name: Download checksums
env:
RELAY_TAG: ${{ needs.release.outputs.version }}
run: |
TAG_URL="https://github.com/Mearman/cascade/releases/download/${RELAY_TAG}"
fetch_sha() {
local name="$1"
local out="$2"
curl -sL --fail --retry 6 --retry-delay 5 --retry-all-errors \
"${TAG_URL}/${name}.sha256" | awk '{print $1}' > "${out}"
if ! [[ "$(cat "${out}")" =~ ^[0-9a-f]{64}$ ]]; then
echo "::error::checksum for ${name} not a 64-char hex digest: $(cat "${out}")"
exit 1
fi
}
fetch_sha cascade-relay-aarch64-macos.tar.gz sha-macos-arm
fetch_sha cascade-relay-x86_64-macos.tar.gz sha-macos-x86
fetch_sha cascade-relay-x86_64-linux.tar.gz sha-linux-x86
fetch_sha cascade-relay-aarch64-linux.tar.gz sha-linux-arm
- name: Generate formula
env:
RELAY_TAG: ${{ needs.release.outputs.version }}
run: |
VERSION="${RELAY_TAG#cascade-v}"
MACOS_ARM_SHA=$(cat sha-macos-arm)
MACOS_X86_SHA=$(cat sha-macos-x86)
LINUX_X86_SHA=$(cat sha-linux-x86)
LINUX_ARM_SHA=$(cat sha-linux-arm)
mkdir -p Formula
cat > Formula/cascade-relay.rb <<EOF
# Auto-generated by CI — do not edit manually.
# Regenerated on each semantic release from Mearman/cascade.
class CascadeRelay < Formula
desc "Opaque byte-pipe relay server for Cascade peers behind NATs"
homepage "https://github.com/Mearman/cascade"
version "${VERSION}"
license "MIT"
on_macos do
if Hardware::CPU.arm?
url "https://github.com/Mearman/cascade/releases/download/${RELAY_TAG}/cascade-relay-aarch64-macos.tar.gz"
sha256 "${MACOS_ARM_SHA}"
else
url "https://github.com/Mearman/cascade/releases/download/${RELAY_TAG}/cascade-relay-x86_64-macos.tar.gz"
sha256 "${MACOS_X86_SHA}"
end
end
on_linux do
if Hardware::CPU.arm?
url "https://github.com/Mearman/cascade/releases/download/${RELAY_TAG}/cascade-relay-aarch64-linux.tar.gz"
sha256 "${LINUX_ARM_SHA}"
else
url "https://github.com/Mearman/cascade/releases/download/${RELAY_TAG}/cascade-relay-x86_64-linux.tar.gz"
sha256 "${LINUX_X86_SHA}"
end
end
def install
bin.install "cascade-relay"
end
test do
assert_match version.to_s, shell_output("#{bin}/cascade-relay --version")
end
end
EOF
- name: Commit and push
env:
RELAY_TAG: ${{ needs.release.outputs.version }}
run: |
git config user.name "github-actions[bot]"
git config user.email "github-actions[bot]@users.noreply.github.com"
git add Formula/cascade-relay.rb
git diff --cached --quiet || git commit -m "chore: update cascade-relay formula to ${RELAY_TAG}"
git push
# ── GitHub Pages (was .github/workflows/pages.yml) ──────────────────
pages-build:
name: Build PWA for Pages
needs: [changes]
if: needs.changes.outputs.web == 'true' && (github.event_name == 'push' && github.ref == 'refs/heads/main' || github.event_name == 'workflow_dispatch')
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6.0.3
- name: Setup Node 22
uses: actions/setup-node@48b55a011bda9f5d6aeb4c2d9c7362e8dae4041e # v6.4.0
with:
node-version: '22'
- name: Enable pnpm
run: corepack enable
- name: Install Rust toolchain
uses: dtolnay/rust-toolchain@29eef336d9b2848a0b548edc03f92a220660cdb8 # stable
with:
toolchain: "1.96.0"
components: rustfmt, clippy
- name: Add wasm32 target
run: rustup target add wasm32-unknown-unknown
- name: Cache Cargo output
uses: actions/cache@27d5ce7f107fe9357f9df03efb73ab90386fccae # v5.0.5
with:
path: |
~/.cargo/registry
target/wasm32-unknown-unknown
key: wasm32-${{ runner.os }}-${{ hashFiles('Cargo.lock') }}
restore-keys: wasm32-${{ runner.os }}-
- name: Install wasm-bindgen CLI
run: |
VERSION=$(grep '^name = "wasm-bindgen"$' Cargo.lock -A1 | grep '^version' | head -1 | sed 's/.*"\(.*\)"/\1/')
cargo install wasm-bindgen-cli --version "$VERSION"
- name: Build WASM module
run: bash scripts/build-wasm.sh
- name: Install dependencies
working-directory: apps/web
run: pnpm install --frozen-lockfile
- name: Build
working-directory: apps/web
run: pnpm build
- name: Upload Pages artifact
uses: actions/upload-pages-artifact@fc324d3547104276b827a68afc52ff2a11cc49c9 # v5.0.0
with:
path: apps/web/dist
pages-deploy:
name: Deploy to GitHub Pages
needs: [pages-build]
if: github.ref == 'refs/heads/main'
runs-on: ubuntu-latest
permissions:
pages: write
id-token: write
environment:
name: github-pages
url: ${{ steps.deploy.outputs.page_url }}
steps:
- name: Deploy to GitHub Pages
id: deploy
uses: actions/deploy-pages@cd2ce8fcbc39b97be8ca5fce6e763baed58fa128 # v5.0.0