name: CI # Section 10.4 requires every PR to pass these checks. This workflow does not # reimplement them: it runs `scripts/ci.sh`, which is the single source of truth # for what must pass. Reimplementing the steps here is how CI and local checks # drift apart, and then one of them is lying to you. on: push: branches: [main] pull_request: # Superseded PR runs are cancelled rather than left to finish and report on a # commit that no longer exists. concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: ci: # Matches the act_runner label in .gitea/workflows/README.md. Change both # together if the runner is registered under a different label. runs-on: ubuntu-latest timeout-minutes: 30 steps: - name: Check out uses: actions/checkout@v4.1.7 # Pinned to an exact version, never a floating tag. A floating tag lets # upstream change what runs in your CI without review, which is a supply # chain problem, not a convenience. - name: Install Rust uses: dtolnay/rust-toolchain@stable # rust-toolchain.toml pins the channel and components, so this action # installs exactly what the project asks for and CI matches local. - name: Cache cargo uses: Swatinem/rust-cache@v2.7.5 - name: Cache cargo registry uses: actions/cache@v4.1.2 with: path: | ~/.cargo/registry/index ~/.cargo/registry/cache ~/.cargo/git/db key: ${{ runner.os }}-cargo-${{ hashFiles('Cargo.lock') }} restore-keys: ${{ runner.os }}-cargo- - name: Run all checks # scripts/ci.sh runs: fmt --check, clippy -D warnings, cargo test, the 5 MB # size gate, Android lint (skipped loudly without an SDK), and gitleaks. # # A skip inside ci.sh prints SKIP with the reason and is listed in the # summary as "NOT a pass", so an unverified Android build can never be # mistaken for a verified one. run: bash scripts/ci.sh - name: Upload the release daemon # Section 10.5 requires releases to carry SHA-256 checksums. Producing # the checksum here means the release pipeline never has to trust an # artifact it did not build itself. if: always() run: | set -euo pipefail BIN="${CARGO_TARGET_DIR:-$HOME/.blocker-target}/release/blocker-daemon" mkdir -p dist if [ -f "$BIN" ]; then cp "$BIN" dist/blocker-daemon sha256sum dist/blocker-daemon > dist/blocker-daemon.sha256 else echo "release daemon not built; nothing to upload" fi - uses: actions/upload-artifact@v4.4.3 if: always() with: name: blocker-daemon-linux path: dist/ if-no-files-found: warn # A separate job, rather than a step, for two reasons: it needs a different # runner label (a runner with the Android SDK), and its failure should be # legible on its own rather than buried in the middle of the CI output. # # Note the asymmetry with ci.sh, which is deliberate. ci.sh SKIPS Android when # no SDK is present, because a developer running checks locally should not be # blocked by a missing toolchain. This job FAILS when the SDK is absent, # because a silently skipped Android build in CI means Android regressions ship # unnoticed. Same check, opposite defaults, for opposite contexts. android: runs-on: android-build timeout-minutes: 60 if: github.event_name == 'pull_request' || github.ref == 'refs/heads/main' steps: - uses: actions/checkout@v4.1.7 - uses: actions/setup-java@v4.4.0 with: distribution: temurin java-version: '17' - uses: android-actions/setup-android@v3.2.2 - name: Accept SDK licences run: yes | sdkmanager --licenses > /dev/null 2>&1 || true - name: Cache Gradle uses: actions/cache@v4.1.2 with: path: | ~/.gradle/caches ~/.gradle/wrapper key: ${{ runner.os }}-gradle-${{ hashFiles('android/**/*.gradle.kts') }} restore-keys: ${{ runner.os }}-gradle- # Sideload flavour only. There is deliberately no Play Store signing path # in CI: sections 6.8 and 4.1 record that the strongest friction # (Stubborn level, Device Admin uninstall block) is hostile to Play Store # policy and is intended for sideload or F-Droid distribution. - name: Lint and assemble release working-directory: android run: ./gradlew lint assembleRelease