Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
37 changes: 37 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,43 @@ jobs:
- name: Build iOS (release, no codesign)
run: flutter build ios --release --no-codesign

build-android:
name: Build Android
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v6

- uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

- uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true

- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: gradle-${{ runner.os }}-${{ hashFiles('android/**/*.gradle*', 'android/gradle/wrapper/gradle-wrapper.properties') }}
restore-keys: gradle-${{ runner.os }}-

- name: Install dependencies
run: flutter pub get

- name: Run code generation
run: dart run build_runner build --delete-conflicting-outputs

# No signing secrets on PRs: the release build falls back to the debug
# keystore (see android/app/build.gradle.kts), so this validates the
# whole release pipeline short of the Play upload.
- name: Build Android app bundle
run: flutter build appbundle --release

build-web:
name: Build Web
runs-on: ubuntu-latest
Expand Down
141 changes: 141 additions & 0 deletions .github/workflows/playstore.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
name: Deploy to Play Store

on:
push:
branches: [main]
paths:
- "lib/**"
- "android/**"
- "pubspec.yaml"
- "pubspec.lock"
- ".github/workflows/playstore.yml"
workflow_dispatch:
inputs:
track:
description: "Play track to upload to"
type: choice
options: [internal, alpha]
default: internal
release_status:
description: "Release status (a brand-new app must use draft until its first release is rolled out in the Play Console)"
type: choice
options: [draft, completed]
default: draft

# Serialize Play runs. The version code is max(track version codes) + 1, so two
# concurrent runs compute the SAME code and the second upload is rejected as a
# duplicate. Queue instead of racing, and never cancel an in-flight upload.
concurrency:
group: playstore-deploy
cancel-in-progress: false

env:
FLUTTER_VERSION: "3.41.4"

jobs:
deploy:
runs-on: ubuntu-latest

steps:
- name: Checkout
uses: actions/checkout@v6

# Without Play credentials the workflow still builds and attaches the signed
# AAB (the very first upload of a new app must happen manually in the Play
# Console anyway - the publisher API can't create it).
- name: Check Play credentials
id: play
env:
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
run: |
if [ -n "$PLAY_SERVICE_ACCOUNT_JSON" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "::notice::PLAY_SERVICE_ACCOUNT_JSON is not set - building the signed AAB as an artifact and skipping the Play upload."
fi

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"

- name: Calculate build number
id: build_number
env:
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
run: |
VERSION=$(grep '^version:' pubspec.yaml | sed 's/version: //' | sed 's/+.*//')
echo "version=${VERSION}" >> "$GITHUB_OUTPUT"
if [ "${{ steps.play.outputs.available }}" = "true" ]; then
printf '%s' "$PLAY_SERVICE_ACCOUNT_JSON" > "$RUNNER_TEMP/play-service-account.json"
export PLAY_JSON_KEY_PATH="$RUNNER_TEMP/play-service-account.json"
cd android
gem install fastlane
fastlane next_build_number
else
echo "build_number=${{ github.run_number }}" >> "$GITHUB_OUTPUT"
fi

- name: Set up Java
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true

- name: Install dependencies
run: flutter pub get

- name: Run code generation
run: dart run build_runner build --delete-conflicting-outputs

- name: Configure signing
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
printf '%s' "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/upload-keystore.jks
cat > android/key.properties << EOF
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyPassword=$ANDROID_KEY_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
storeFile=../upload-keystore.jks
EOF

- name: Build AAB
run: |
flutter build appbundle \
--build-number=${{ steps.build_number.outputs.build_number }} \
--build-name=${{ steps.build_number.outputs.version }}

- name: Upload AAB artifact
uses: actions/upload-artifact@v4
with:
name: nullfeed-${{ steps.build_number.outputs.version }}-${{ steps.build_number.outputs.build_number }}-aab
path: build/app/outputs/bundle/release/app-release.aab
if-no-files-found: error

- name: Upload to Play Store
if: steps.play.outputs.available == 'true'
env:
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
PLAY_TRACK: ${{ inputs.track || 'internal' }}
PLAY_RELEASE_STATUS: ${{ inputs.release_status || 'draft' }}
run: |
printf '%s' "$PLAY_SERVICE_ACCOUNT_JSON" > "$RUNNER_TEMP/play-service-account.json"
export PLAY_JSON_KEY_PATH="$RUNNER_TEMP/play-service-account.json"
cd android
fastlane beta

- name: Cleanup signing material
if: always()
run: |
rm -f android/key.properties android/upload-keystore.jks "$RUNNER_TEMP/play-service-account.json"
57 changes: 57 additions & 0 deletions .github/workflows/storelisting.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
name: Sync Store Listing

# Play listing copy/graphics live in-repo (android/fastlane/metadata); merging a
# change to them pushes the listing to the Play Console. No binaries here.
on:
push:
branches: [main]
paths:
- "android/fastlane/metadata/**"
- ".github/workflows/storelisting.yml"
workflow_dispatch:

concurrency:
group: storelisting
cancel-in-progress: false

jobs:
play-listing:
runs-on: ubuntu-latest
defaults:
run:
working-directory: android
steps:
- name: Checkout
uses: actions/checkout@v6

- name: Check Play credentials
id: play
env:
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
run: |
if [ -n "$PLAY_SERVICE_ACCOUNT_JSON" ]; then
echo "available=true" >> "$GITHUB_OUTPUT"
else
echo "available=false" >> "$GITHUB_OUTPUT"
echo "::notice::PLAY_SERVICE_ACCOUNT_JSON is not set - skipping the Play listing sync."
fi

- name: Set up Ruby
if: steps.play.outputs.available == 'true'
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.3"

- name: Push listing
if: steps.play.outputs.available == 'true'
env:
PLAY_SERVICE_ACCOUNT_JSON: ${{ secrets.PLAY_SERVICE_ACCOUNT_JSON }}
run: |
printf '%s' "$PLAY_SERVICE_ACCOUNT_JSON" > "$RUNNER_TEMP/play-service-account.json"
export PLAY_JSON_KEY_PATH="$RUNNER_TEMP/play-service-account.json"
gem install fastlane
fastlane listing

- name: Cleanup credentials
if: always()
run: rm -f "$RUNNER_TEMP/play-service-account.json"
2 changes: 1 addition & 1 deletion .metadata
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ migration:
- platform: root
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
- platform: web
- platform: android
create_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a
base_revision: 90673a4eef275d1a6692c26ac80d6d746d41a73a

Expand Down
14 changes: 14 additions & 0 deletions android/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
gradle-wrapper.jar
/.gradle
/captures/
/gradlew
/gradlew.bat
/local.properties
GeneratedPluginRegistrant.java
.cxx/

# Remember to never publicly share your keystore.
# See https://flutter.dev/to/reference-keystore
key.properties
**/*.keystore
**/*.jks
65 changes: 65 additions & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
import java.io.FileInputStream
import java.util.Properties

plugins {
id("com.android.application")
id("kotlin-android")
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id("dev.flutter.flutter-gradle-plugin")
}

// CI decodes the upload keystore and writes android/key.properties; local builds
// without it fall back to debug signing so `flutter run --release` still works.
val keystorePropertiesFile = rootProject.file("key.properties")
val keystoreProperties = Properties()
if (keystorePropertiesFile.exists()) {
FileInputStream(keystorePropertiesFile).use { keystoreProperties.load(it) }
}

android {
namespace = "codes.julian.nullfeed"
compileSdk = flutter.compileSdkVersion
ndkVersion = flutter.ndkVersion

compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}

kotlinOptions {
jvmTarget = JavaVersion.VERSION_17.toString()
}

defaultConfig {
applicationId = "codes.julian.nullfeed"
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
}

signingConfigs {
if (keystorePropertiesFile.exists()) {
create("release") {
keyAlias = keystoreProperties["keyAlias"] as String
keyPassword = keystoreProperties["keyPassword"] as String
storeFile = file(keystoreProperties["storeFile"] as String)
storePassword = keystoreProperties["storePassword"] as String
}
}
}

buildTypes {
release {
signingConfig = if (keystorePropertiesFile.exists()) {
signingConfigs.getByName("release")
} else {
signingConfigs.getByName("debug")
}
}
}
}

flutter {
source = "../.."
}
7 changes: 7 additions & 0 deletions android/app/src/debug/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<!-- The INTERNET permission is required for development. Specifically,
the Flutter tool needs it to communicate with the running application
to allow setting breakpoints, to provide hot reload, etc.
-->
<uses-permission android:name="android.permission.INTERNET"/>
</manifest>
50 changes: 50 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<uses-permission android:name="android.permission.INTERNET"/>
<!-- Keep media playing with the screen off (wakelock_plus). -->
<uses-permission android:name="android.permission.WAKE_LOCK"/>
<!-- Self-hosted NullFeed servers commonly run plain HTTP on the LAN. -->
<application
android:label="NullFeed"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher"
android:usesCleartextTraffic="true">
<activity
android:name=".MainActivity"
android:exported="true"
android:launchMode="singleTop"
android:taskAffinity=""
android:theme="@style/LaunchTheme"
android:configChanges="orientation|keyboardHidden|keyboard|screenSize|smallestScreenSize|locale|layoutDirection|fontScale|screenLayout|density|uiMode"
android:hardwareAccelerated="true"
android:windowSoftInputMode="adjustResize">
<!-- Specifies an Android theme to apply to this Activity as soon as
the Android process has started. This theme is visible to the user
while the Flutter UI initializes. After that, this theme continues
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.

In particular, this is used by the Flutter engine in io.flutter.plugin.text.ProcessTextPlugin. -->
<queries>
<intent>
<action android:name="android.intent.action.PROCESS_TEXT"/>
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package codes.julian.nullfeed

import io.flutter.embedding.android.FlutterActivity

class MainActivity : FlutterActivity()
Loading
Loading