name: build on: push: branches: - '**' tags: - 'v*' workflow_dispatch: concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/v') }} env: IS_STABLE: ${{ !contains(github.ref, '-') }} FLUTTER_VERSION: '3.47.4' GO_VERSION: '1.26.4' NDK_VERSION: r28c jobs: dart: name: Dart # A private copy pays for its Actions minutes, so it runs the checks only when # started by hand. if: &checks github.event_name == 'workflow_dispatch' || !github.event.repository.private runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 submodules: true - name: Test commit message hook run: bash tool/check_commit_msg_test.sh - name: Test comment density hook run: bash tool/check_comment_density_test.sh - name: Setup Rust cache uses: Swatinem/rust-cache@v2 with: workspaces: plugins/rust_api/rust cache-directories: .dart_tool/hooks_runner - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - name: Install dependencies run: flutter pub get - name: Check format run: dart format --output=none --set-exit-if-changed lib test tool plugins setup.dart - name: Analyze run: flutter analyze --no-fatal-infos - name: Skip the Go core build hook run: | yq -i '.hooks.user_defines.setup.build_assets = false' pubspec.yaml - name: Verify changelog if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') run: dart run tool/changelog.dart verify - name: Run tests run: flutter test --reporter expanded --coverage - name: Check coverage run: dart run tool/check_coverage.dart coverage/lcov.info 75 plugins: name: Plugins if: *checks runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - name: Validate local Flutter packages run: bash tool/check_plugins.sh - name: Validate setup hooks package working-directory: plugins/setup/setup_hooks run: | dart pub get dart analyze dart test desktop: name: Desktop ${{ matrix.platform }} if: *checks runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: include: - platform: linux os: ubuntu-latest - platform: windows os: windows-2022 - platform: macos os: macos-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: core/go.sum - name: Setup Rust cache uses: Swatinem/rust-cache@v2 with: key: desktop-${{ matrix.platform }} workspaces: | services/helper plugins/rust_api/rust cache-directories: .dart_tool/hooks_runner - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - name: Install Linux build dependencies if: matrix.platform == 'linux' run: | sudo apt-get update sudo apt-get install -y ninja-build libgtk-3-dev libayatana-appindicator3-dev libsecret-1-dev - name: Setup Xcode if: matrix.platform == 'macos' run: | sudo xcode-select -s /Applications/Xcode_26.3.app || sudo xcode-select -s /Applications/Xcode_26.2.app xcodebuild -version - name: Enable git long paths (Windows) if: matrix.platform == 'windows' run: git config --global core.longpaths true - name: Install dependencies run: flutter pub get # Compiles the desktop runners and the native plugin code that only the # tagged release matrix built before; the bundle itself is discarded. - name: Build debug bundle run: flutter build ${{ matrix.platform }} --debug go: name: Go core if: *checks runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: core/go.sum - name: Validate Go core wrapper working-directory: core env: CGO_ENABLED: 0 run: | if ! compgen -G '*_test.go' > /dev/null; then echo 'No Go tests found in core/; the test step would pass vacuously.' >&2 exit 1 fi test -z "$(gofmt -l ./*.go ./platform ./tun)" || { gofmt -l ./*.go ./platform ./tun >&2 exit 1 } go test -count=1 . go vet . GOOS=linux go vet ./platform - name: Setup NDK uses: nttld/setup-ndk@v1 id: ndk with: ndk-version: ${{ env.NDK_VERSION }} - name: Validate the Android core library working-directory: core env: ANDROID_NDK_HOME: ${{ steps.ndk.outputs.ndk-path }} run: | # Nothing else compiles the `android && cgo` files before a `v*` tag. CC="$(echo "$ANDROID_NDK_HOME"/toolchains/llvm/prebuilt/*/bin/aarch64-linux-android21-clang)" test -x "$CC" || { echo "NDK clang not found at $CC" >&2; exit 1; } tags="$(sed -n 's/^tags: *//p' ../build_config.yaml)" test -n "$tags" || { echo 'Could not read tags from build_config.yaml' >&2; exit 1; } export CGO_ENABLED=1 GOOS=android GOARCH=arm64 CC go vet "-tags=$tags" . ./tun ./platform android: name: Android unit tests if: *checks runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - name: Install dependencies run: flutter pub get - name: Setup NDK uses: nttld/setup-ndk@v1 id: ndk with: ndk-version: ${{ env.NDK_VERSION }} - name: Setup Java uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' - name: Setup Gradle uses: gradle/actions/setup-gradle@v6 - name: Run Android unit tests working-directory: android env: ANDROID_NDK_HOME: ${{ steps.ndk.outputs.ndk-path }} run: | { echo "flutter.sdk=$FLUTTER_ROOT" echo "sdk.dir=$ANDROID_HOME" } > local.properties wrapper="$FLUTTER_ROOT/bin/cache/artifacts/gradle_wrapper" test -d "$wrapper" || flutter precache cp -R "$wrapper/." . chmod +x gradlew # `:core` only calls System.loadLibrary; the excluded Flutter tasks would run # the native build hooks for every debug ABI, which no JVM unit test consumes. ./gradlew :common:testDebugUnitTest :service:testDebugUnitTest :app:testDebugUnitTest \ -x :app:compileFlutterBuildDebug -x :app:copyFlutterAssetsDebug rust: name: Rust if: *checks runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Rust cache uses: Swatinem/rust-cache@v2 with: workspaces: | services/helper plugins/rust_api/rust - name: Check format run: | cargo fmt --manifest-path services/helper/Cargo.toml -- --check cargo fmt --manifest-path plugins/rust_api/rust/Cargo.toml -- --check - name: Run tests run: | cargo test --manifest-path services/helper/Cargo.toml cargo test --manifest-path plugins/rust_api/rust/Cargo.toml windows-helper-test: name: Windows helper test if: *checks runs-on: windows-2022 steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Rust cache uses: Swatinem/rust-cache@v2 with: workspaces: services/helper - name: Check format run: cargo fmt --manifest-path services/helper/Cargo.toml -- --check - name: Run Windows service tests run: cargo test --manifest-path services/helper/Cargo.toml --features windows-service build: # Releases publish where the release links point: the public repository. if: github.repository == 'chen08209/FlClash' && github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') needs: [ dart, plugins, go, android, rust, windows-helper-test ] runs-on: ${{ matrix.os }} strategy: matrix: include: - platform: android os: ubuntu-latest - platform: windows os: Windows-2022 arch: amd64 - platform: linux os: ubuntu-22.04 arch: amd64 - platform: macos os: macos-15-intel arch: amd64 - platform: macos os: macos-latest arch: arm64 - platform: windows os: windows-11-arm arch: arm64 - platform: linux os: ubuntu-24.04-arm arch: arm64 steps: - name: Setup rust if: startsWith(matrix.os, 'windows-11-arm') shell: pwsh run: | Invoke-WebRequest -Uri "https://win.rustup.rs/aarch64" -OutFile rustup-init.exe .\rustup-init.exe -y --default-toolchain stable $cargoPath = "$env:USERPROFILE\.cargo\bin" Add-Content $env:GITHUB_PATH $cargoPath - name: Checkout uses: actions/checkout@v4 with: submodules: recursive - name: Setup Android Signing if: startsWith(matrix.platform,'android') run: | echo "${{ secrets.KEYSTORE }}" | base64 --decode > android/app/keystore.jks echo "${{ secrets.SERVICE_JSON }}" | base64 --decode > android/app/google-services.json { echo "keyAlias=${{ secrets.KEY_ALIAS }}" echo "storePassword=${{ secrets.STORE_PASSWORD }}" echo "keyPassword=${{ secrets.KEY_PASSWORD }}" } >> android/local.properties - name: Setup Go uses: actions/setup-go@v5 with: go-version: ${{ env.GO_VERSION }} cache-dependency-path: core/go.sum - name: Setup Rust cache uses: Swatinem/rust-cache@v2 with: key: ${{ matrix.platform }}-${{ matrix.arch || 'all' }} workspaces: services/helper cache-directories: .dart_tool/hooks_runner - name: Setup Gradle if: startsWith(matrix.platform,'android') uses: gradle/actions/setup-gradle@v6 - name: Enable git long paths (Windows) if: startsWith(matrix.os, 'windows') run: git config --global core.longpaths true # Stable publishes no Windows or Linux arm64 archive; master makes the action # clone the SDK at the pinned tag and let it fetch the host's own Dart SDK. - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: master flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - name: Setup Xcode if: startsWith(matrix.platform,'macos') run: | sudo xcode-select -s /Applications/Xcode_26.3.app || sudo xcode-select -s /Applications/Xcode_26.2.app xcodebuild -version - name: Setup NDK if: startsWith(matrix.platform,'android') uses: nttld/setup-ndk@v1 id: build-ndk with: ndk-version: ${{ env.NDK_VERSION }} add-to-path: true link-to-sdk: true - name: Get Flutter Dependency run: | flutter --version flutter pub get # See .agents/flutter-upgrade.md before bumping FLUTTER_VERSION. - name: Disable Impeller on Intel macOS if: matrix.platform == 'macos' && matrix.arch == 'amd64' run: plutil -replace FLTEnableImpeller -bool NO macos/Runner/Info.plist - name: Setup env: ANDROID_NDK_HOME: ${{ steps.build-ndk.outputs.ndk-path }} run: dart setup.dart ${{ matrix.platform }} ${{ env.IS_STABLE == 'true' && '--env stable' || '' }} -v - name: Upload uses: actions/upload-artifact@v4 with: name: artifact-${{ matrix.platform }}${{ matrix.arch && format('-{0}', matrix.arch) }} path: ./dist overwrite: true upload: permissions: contents: write needs: [ build ] runs-on: ubuntu-latest services: telegram-bot-api: image: aiogram/telegram-bot-api:latest env: TELEGRAM_API_ID: ${{ secrets.TELEGRAM_API_ID }} TELEGRAM_API_HASH: ${{ secrets.TELEGRAM_API_HASH }} ports: - 8081:8081 steps: - name: Checkout uses: actions/checkout@v4 with: fetch-depth: 0 - name: Download uses: actions/download-artifact@v4 with: path: ./dist/ pattern: artifact-* merge-multiple: true - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: stable flutter-version: ${{ env.FLUTTER_VERSION }} cache: true - name: Install dependencies run: flutter pub get - name: Skip native build hooks run: | yq -i '.hooks.user_defines.setup.build_assets = false | .hooks.user_defines.rust_api.build_assets = false' pubspec.yaml - name: Generate release notes shell: bash env: IS_STABLE: ${{ env.IS_STABLE }} TAG: ${{ github.ref_name }} run: | target=() if [[ "$IS_STABLE" == 'true' ]]; then target=(--tag "$TAG") else dart run tool/changelog.dart build --unreleased fi dart run tool/changelog.dart render release "${target[@]}" --out release.md dart run tool/changelog.dart render telegram "${target[@]}" --out telegram.md - name: Push to telegram env: TELEGRAM_BOT_TOKEN: ${{ secrets.TELEGRAM_BOT_TOKEN }} TAG: ${{ github.ref_name }} RUN_ID: ${{ github.run_id }} run: | python -m pip install --upgrade pip pip install requests python release_telegram.py - name: Patch release.md run: | version=$(echo "${{ github.ref_name }}" | sed 's/^v//') sed "s|VERSION|$version|g" ./.github/release_template.md >> release.md - name: Generate sha256 if: ${{ env.IS_STABLE == 'true' }} run: | cd ./dist sha256sum ./* > SHA256SUMS - name: Release if: ${{ env.IS_STABLE == 'true' }} uses: softprops/action-gh-release@v2 with: files: ./dist/* body_path: './release.md' - name: Create Homebrew Cask Source Dir if: ${{ env.IS_STABLE == 'true' }} run: | version=$(echo "${{ github.ref_name }}" | sed 's/^v//') arm_sha=$(sha256sum "./dist/FlClash-${version}-macos-arm64.dmg" | awk '{print $1}') amd_sha=$(sha256sum "./dist/FlClash-${version}-macos-amd64.dmg" | awk '{print $1}') mkdir -p ./brew/Casks sed \ -e "s|VERSION|$version|g" \ -e "s|ARM_SHA256|$arm_sha|g" \ -e "s|AMD_SHA256|$amd_sha|g" \ ./.github/homebrew_cask_template.rb > ./brew/Casks/flclash.rb - name: Push to Homebrew tap if: ${{ env.IS_STABLE == 'true' }} uses: cpina/github-action-push-to-another-repository@v1.7.2 env: SSH_DEPLOY_KEY: ${{ secrets.HOMEBREW_SSH_DEPLOY_KEY }} with: source-directory: ./brew/ destination-github-username: chen08209 destination-repository-name: homebrew-tap user-name: 'chen08029' user-email: 'chen08209@gmail.com' target-branch: main commit-message: Update Homebrew cask from ${{ github.ref_name }} target-directory: / - name: Create Fdroid Source Dir if: ${{ env.IS_STABLE == 'true' }} run: | mkdir -p ./tmp cp ./dist/*android-arm64-v8a* ./tmp/ || true echo "Files copied successfully" - name: Push to fdroid repo if: ${{ env.IS_STABLE == 'true' }} uses: cpina/github-action-push-to-another-repository@v1.7.2 env: SSH_DEPLOY_KEY: ${{ secrets.SSH_DEPLOY_KEY }} with: source-directory: ./tmp/ destination-github-username: chen08209 destination-repository-name: FlClash-fdroid-repo user-name: 'chen08029' user-email: 'chen08209@gmail.com' target-branch: main commit-message: Update from ${{ github.ref_name }} target-directory: /tmp/