name: build on: push: branches: - '**' tags: - 'v*' 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.1' jobs: test: name: Test runs-on: ubuntu-latest steps: - name: Checkout uses: actions/checkout@v4 with: submodules: recursive fetch-depth: 0 - name: Test commit message hook shell: bash run: bash tool/check_commit_msg_test.sh - name: Test comment density hook shell: bash run: bash tool/check_comment_density_test.sh - 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: Verify changelog if: github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/') run: dart run tool/changelog.dart verify - name: Setup Go uses: actions/setup-go@v5 with: go-version: '1.26.4' cache-dependency-path: core/go.sum - name: Run tests run: flutter test --reporter expanded --coverage - name: Check coverage run: dart run tool/check_coverage.dart coverage/lcov.info 75 - name: Validate local Flutter packages shell: bash 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 - name: Validate Go core wrapper working-directory: core shell: bash env: CGO_ENABLED: 0 run: | # `go test` succeeds silently when a package has no test files, so # assert the wrapper actually ships tests before trusting the result. 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 . # `go vet .` only reaches the root package. `platform` and `tun` are # behind build constraints, so they need an explicit GOOS to compile # at all; without this they ship unvetted. GOOS=linux go vet ./platform - name: Setup NDK uses: nttld/setup-ndk@v1 id: core-ndk with: ndk-version: r28c - name: Validate the Android core library working-directory: core shell: bash env: ANDROID_NDK_HOME: ${{ steps.core-ndk.outputs.ndk-path }} run: | # The step above pins CGO_ENABLED=0, which excludes every `android && # cgo` file: lib.go, bride.go, tun/ and platform/limit.go. The job # that builds them needs the NDK and only runs on a `v*` tag, so # without this the entire Android surface of the core first compiles # at release time. 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; } # Same tags the shipped library is built with; read from the build # config at the repository root so the two cannot drift. 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 - name: Setup Java uses: actions/setup-java@v4 with: distribution: temurin java-version: '17' - name: Run Android unit tests working-directory: android shell: bash run: | # settings.gradle.kts resolves the Flutter tooling through local.properties, # which only the Flutter build itself would otherwise generate. { 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 # Modules are listed explicitly: the aggregate testDebugUnitTest task would # also run third-party Flutter plugin suites. `:core` is omitted because its # only class loads the native library through System.loadLibrary. ./gradlew :common:testDebugUnitTest :service:testDebugUnitTest :app:testDebugUnitTest - name: Validate Rust components run: | cargo fmt --manifest-path services/helper/Cargo.toml -- --check cargo test --manifest-path services/helper/Cargo.toml cargo fmt --manifest-path plugins/rust_api/rust/Cargo.toml -- --check cargo test --manifest-path plugins/rust_api/rust/Cargo.toml windows-helper-test: name: Windows helper test runs-on: windows-2022 steps: - name: Checkout uses: actions/checkout@v4 - 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: if: github.event_name == 'push' && startsWith(github.ref, 'refs/tags/v') needs: [ test, 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 fetch-depth: 0 - 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: '1.26.4' cache-dependency-path: | core/go.sum - name: Enable git long paths (Windows) if: startsWith(matrix.os, 'windows') run: git config --global core.longpaths true - 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 with: ndk-version: r28c add-to-path: true link-to-sdk: true - name: Get Flutter Dependency run: | flutter --version flutter pub get - name: Setup 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: 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/