# CI runs first; build only proceeds if all checks pass. # # Push to dev: flutter analyze + flutter test # Tag v* (release): gates + signed APK build + attach to Forgejo Release # # main pushes are NOT gated here: a merge to main only happens after # dev has already passed CI, and the release tag is the sole trigger # for a signed APK build. Re-running analyze+test on the merge commit # just burns runner time without changing the outcome. # # To cut a release: # Create a release via the Forgejo UI on main with a v* tag name. # The tag push triggers this workflow; the build job attaches the APK. # # Note: JavaScript actions (actions/checkout, actions/upload-artifact) cannot run # inside the Flutter container because it has no Node.js. All steps use shell # commands directly instead. # # Required secrets (repo → Settings → Secrets → Actions): # RELEASE_TOKEN — Forgejo PAT with write:repository scope # RELEASE_KEYSTORE_BASE64 — base64 of the signing keystore # RELEASE_KEYSTORE_PASSWORD — keystore + key password # RELEASE_KEY_ALIAS — key alias within the keystore name: CI & Build on: push: branches: [dev] tags: ["v*"] # Cancel older runs on the same branch when a newer push lands. Tag runs # are never cancelled so a release build can't kill itself mid-flight. concurrency: group: ci-${{ github.ref }} cancel-in-progress: ${{ !startsWith(github.ref, 'refs/tags/') }} # Least-privilege default. The build job upgrades to contents: write # so it can attach the APK to a Forgejo release. permissions: contents: read jobs: analyze: name: Analyze & test runs-on: ci-runner container: # Pinned to a specific Flutter version for reproducible builds. # Floating :stable means a random Flutter minor bump could change # analyzer output or break the build without any commit landing. # Bump this line (and verify locally with `flutter --version`) # when you intentionally want a newer Flutter. image: ghcr.io/cirruslabs/flutter:3.41.6 steps: - name: Checkout run: | git clone "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" . git checkout "$GITHUB_SHA" - name: Install dependencies run: flutter pub get - name: Analyze run: flutter analyze - name: Test run: flutter test build: name: Build release APK needs: [analyze] # Only tag pushes produce a signed release build. dev pushes # run the gates above and stop there. if: startsWith(github.ref, 'refs/tags/') runs-on: ci-runner container: image: ghcr.io/cirruslabs/flutter:3.41.6 permissions: contents: write steps: - name: Checkout run: | git clone "$GITHUB_SERVER_URL/$GITHUB_REPOSITORY" . git checkout "$GITHUB_SHA" - name: Install dependencies run: flutter pub get - name: Set up release signing env: KEYSTORE_B64: ${{ secrets.RELEASE_KEYSTORE_BASE64 }} STORE_PASSWORD: ${{ secrets.RELEASE_KEYSTORE_PASSWORD }} KEY_ALIAS: ${{ secrets.RELEASE_KEY_ALIAS }} run: | echo "$KEYSTORE_B64" | base64 -d > android/app/release.jks printf 'storePassword=%s\nkeyPassword=%s\nkeyAlias=%s\nstoreFile=release.jks\n' \ "$STORE_PASSWORD" "$STORE_PASSWORD" "$KEY_ALIAS" > android/key.properties - name: Build release APK run: | # Derive version from the tag (e.g. v26.03.12 → name=26.03.12 number=260312) TAG="${{ github.ref_name }}" BUILD_NAME="${TAG#v}" BUILD_NUMBER=$(echo "$BUILD_NAME" | tr -d '.') flutter build apk --release \ --build-name="$BUILD_NAME" \ --build-number="$BUILD_NUMBER" - name: Publish Forgejo release # Release-publish logic lives in a shell script so it's # testable locally (bash -x scripts/publish_apk_release.sh # with env vars set) instead of trapped in YAML. env: RELEASE_TOKEN: ${{ secrets.RELEASE_TOKEN }} TAG: ${{ github.ref_name }} APK: build/app/outputs/flutter-apk/app-release.apk run: bash scripts/publish_apk_release.sh