name: Build and Release to F-Droid on: push: tags: - '*' workflow_dispatch: jobs: ci: runs-on: docker env: ANDROID_HOME: /opt/android-sdk ANDROID_SDK_ROOT: /opt/android-sdk steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Install Android SDK packages run: | sdkmanager --licenses >/dev/null <<'EOF' y y y y y y y y y y EOF sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0" - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Trust Flutter SDK git directory run: | set -e FLUTTER_BIN_DIR="$(dirname "$(command -v flutter)")" FLUTTER_SDK_DIR="$(cd "$FLUTTER_BIN_DIR/.." && pwd -P)" git config --global --add safe.directory "$FLUTTER_SDK_DIR" if [ -n "${FLUTTER_ROOT:-}" ]; then git config --global --add safe.directory "$FLUTTER_ROOT" fi git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.4-x64 || true - name: Verify Android + Flutter toolchain run: flutter doctor -v - name: Install dependencies run: flutter pub get - name: Static analysis run: flutter analyze --no-pub - name: Run tests run: flutter test - name: Check outdated dependencies run: dart pub outdated continue-on-error: true - name: Security audit run: dart pub audit - name: Trivy filesystem scan run: | set -e SUDO="" if command -v sudo >/dev/null 2>&1; then SUDO="sudo" fi if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update $SUDO apt-get install -y wget apt-transport-https gnupg lsb-release wget -qO - https://aquasecurity.github.io/trivy-repo/deb/public.key | gpg --dearmor | $SUDO tee /usr/share/keyrings/trivy.gpg > /dev/null echo "deb [signed-by=/usr/share/keyrings/trivy.gpg] https://aquasecurity.github.io/trivy-repo/deb generic main" | $SUDO tee /etc/apt/sources.list.d/trivy.list $SUDO apt-get update $SUDO apt-get install -y trivy elif command -v apk >/dev/null 2>&1; then $SUDO apk add --no-cache trivy || (wget -qO trivy.tar.gz https://github.com/aquasecurity/trivy/releases/latest/download/trivy_0.62.1_Linux-64bit.tar.gz && tar xzf trivy.tar.gz trivy && $SUDO mv trivy /usr/local/bin/) fi trivy filesystem --severity HIGH,CRITICAL --exit-code 0 . continue-on-error: true - name: Build debug APK run: flutter build apk --debug build-and-deploy: needs: ci runs-on: docker env: ANDROID_HOME: /opt/android-sdk ANDROID_SDK_ROOT: /opt/android-sdk steps: - name: Checkout uses: actions/checkout@v4 - name: Setup Java uses: actions/setup-java@v4 with: distribution: 'zulu' java-version: '17' - name: Setup Android SDK uses: android-actions/setup-android@v3 - name: Install Android SDK packages run: | sdkmanager --licenses >/dev/null <<'EOF' y y y y y y y y y y EOF sdkmanager "platform-tools" "platforms;android-36" "build-tools;36.0.0" - name: Install jq run: | set -e SUDO="" if command -v sudo >/dev/null 2>&1; then SUDO="sudo" fi if command -v apt-get >/dev/null 2>&1; then $SUDO apt-get update $SUDO apt-get install -y jq elif command -v apk >/dev/null 2>&1; then $SUDO apk add --no-cache jq elif command -v dnf >/dev/null 2>&1; then $SUDO dnf install -y jq elif command -v yum >/dev/null 2>&1; then $SUDO yum install -y jq else echo "Could not find a supported package manager to install jq" exit 1 fi - name: Setup Flutter uses: subosito/flutter-action@v2 with: channel: 'stable' - name: Trust Flutter SDK git directory run: | set -e FLUTTER_BIN_DIR="$(dirname "$(command -v flutter)")" FLUTTER_SDK_DIR="$(cd "$FLUTTER_BIN_DIR/.." && pwd -P)" git config --global --add safe.directory "$FLUTTER_SDK_DIR" if [ -n "${FLUTTER_ROOT:-}" ]; then git config --global --add safe.directory "$FLUTTER_ROOT" fi # Runner-specific fallback observed in failing logs git config --global --add safe.directory /opt/hostedtoolcache/flutter/stable-3.41.4-x64 || true - name: Verify Android + Flutter toolchain run: flutter doctor -v - name: Install dependencies run: flutter pub get - name: Set version from git tag run: | set -e RAW_TAG="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" # Strip leading 'v' if present (v1.2.3 -> 1.2.3) VERSION="${RAW_TAG#v}" # Extract a numeric build number for versionCode. # Converts semver x.y.z into a single integer: x*10000 + y*100 + z # e.g. 1.1.1 -> 10101, 2.3.15 -> 20315 MAJOR=$(echo "$VERSION" | cut -d. -f1) MINOR=$(echo "$VERSION" | cut -d. -f2) PATCH=$(echo "$VERSION" | cut -d. -f3) MAJOR=${MAJOR:-0}; MINOR=${MINOR:-0}; PATCH=${PATCH:-0} VERSION_CODE=$(( MAJOR * 10000 + MINOR * 100 + PATCH )) echo "Version: $VERSION, VersionCode: $VERSION_CODE" # Update pubspec.yaml: replace the version line sed -i "s/^version: .*/version: ${VERSION}+${VERSION_CODE}/" pubspec.yaml grep '^version:' pubspec.yaml # ADD THIS NEW STEP - name: Setup Android Keystore env: KEYSTORE_BASE64: ${{ secrets.KEYSTORE_BASE64 }} KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }} KEY_ALIAS: ${{ secrets.KEY_ALIAS }} run: | # Decode the base64 string back into the binary .jks file echo "$KEYSTORE_BASE64" | base64 --decode > android/app/upload-keystore.jks # Create the key.properties file that build.gradle expects echo "storePassword=$KEY_PASSWORD" > android/key.properties echo "keyPassword=$KEY_PASSWORD" >> android/key.properties echo "keyAlias=$KEY_ALIAS" >> android/key.properties echo "storeFile=upload-keystore.jks" >> android/key.properties - name: Build APK run: flutter build apk --release - name: Setup F-Droid Server Tools run: | SUDO="" if command -v sudo >/dev/null 2>&1; then SUDO="sudo" fi $SUDO apt-get update # sshpass from apt, fdroidserver via pip to get a newer androguard that # can parse modern Flutter/AGP APKs (apt ships fdroidserver 2.2.1 which crashes) $SUDO apt-get install -y sshpass python3-pip pip3 install --break-system-packages --upgrade fdroidserver - name: Initialize or fetch F-Droid Repository env: HOST: ${{ secrets.HETZNER_HOST }} USER: ${{ secrets.HETZNER_USER }} PASS: ${{ secrets.HETZNER_PASS }} run: | mkdir -p fdroid # Ensure remote path exists (sftp mkdir, ignoring errors if already present). sshpass -p "$PASS" sftp -o StrictHostKeyChecking=no "$USER@$HOST" <<'SFTP' -mkdir dev -mkdir dev/fdroid -mkdir dev/fdroid/repo SFTP # Try to download the entire fdroid/ directory from Hetzner to keep # older APKs, the repo keystore, and config.yml across runs. # If it fails (first time), initialize a new local repo. sshpass -p "$PASS" scp -o StrictHostKeyChecking=no -r "$USER@$HOST:dev/fdroid/." fdroid/ || (cd fdroid && fdroid init) - name: Ensure F-Droid repo signing key and icon run: | cd fdroid # Ensure repo icon exists (use app launcher icon) mkdir -p repo/icons if [ ! -f repo/icons/icon.png ]; then cp ../android/app/src/main/res/mipmap-xxxhdpi/ic_launcher.png repo/icons/icon.png fi # If keystore doesn't exist, create the signing key. # This only runs on the very first deployment; subsequent runs # download the keystore from Hetzner via the scp step above. if [ ! -f keystore.p12 ]; then fdroid update --create-key fi - name: Copy new APK to repo run: | set -e mkdir -p fdroid/repo # Prefer tag name for release builds; fallback to ref name for manual runs. REF_NAME="${GITHUB_REF_NAME:-${GITHUB_REF##*/}}" SAFE_REF_NAME="$(echo "$REF_NAME" | tr '/ ' '__' | tr -cd '[:alnum:]_.-')" if [ -z "$SAFE_REF_NAME" ]; then SAFE_REF_NAME="${GITHUB_SHA:-manual}" fi cp build/app/outputs/flutter-apk/app-release.apk "fdroid/repo/my_flutter_app_${SAFE_REF_NAME}.apk" - name: Copy metadata to F-Droid repo run: | cp -r fdroid-metadata/* fdroid/metadata/ - name: Generate F-Droid Index run: | cd fdroid fdroid update -c - name: Upload Repo to Hetzner env: HOST: ${{ secrets.HETZNER_HOST }} USER: ${{ secrets.HETZNER_USER }} PASS: ${{ secrets.HETZNER_PASS }} run: | set -euo pipefail SSH_OPTS="-o StrictHostKeyChecking=no -o ConnectTimeout=20" # Create remote directory tree via SFTP batch (no exec channel needed). # Leading '-' on each mkdir means "ignore error if already exists". sshpass -p "$PASS" sftp $SSH_OPTS "$USER@$HOST" <<'SFTP' -mkdir dev -mkdir dev/fdroid -mkdir dev/fdroid/repo SFTP # Upload the entire fdroid/ directory (repo + keystore + config) # so the signing key persists across runs. sshpass -p "$PASS" scp $SSH_OPTS -r fdroid/. "$USER@$HOST:dev/fdroid/"