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
128 changes: 128 additions & 0 deletions .github/workflows/android-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
# Publish Refetch to Google Play.
#
# Builds a signed App Bundle and uploads it through the Fastlane lanes in
# `android/fastlane/`. Manual only — there is no push trigger.
#
# Repository secrets (Settings → Secrets and variables → Actions):
#
# APP_IDENTIFIER Android applicationId (io.appwrite.refetch)
# PLAY_STORE_JSON_KEY_DATA Base64 Google Play service account JSON key
# ANDROID_KEYSTORE_BASE64 Base64 release keystore (.jks)
# ANDROID_KEYSTORE_PASSWORD Keystore password
# ANDROID_KEY_ALIAS Key alias inside the keystore
# ANDROID_KEY_PASSWORD Key password
#
# Encode the binary secrets with `base64 -w0 file` (Linux) or
# `base64 -i file` (macOS).

name: Android Release

on:
workflow_dispatch:
inputs:
release_type:
description: "Play Store track to publish to"
required: true
type: choice
default: internal
options:
- internal
- beta
- release
release_status:
description: "Release status (use 'draft' for the first-ever release on a draft app)"
required: false
type: choice
default: completed
options:
- completed
- draft
- halted
- inProgress

jobs:
publish:
name: Build and upload to Play Store (${{ inputs.release_type }})
runs-on: ubuntu-latest
timeout-minutes: 60

env:
# Read by android/fastlane/{Appfile,Fastfile} and the play_publisher plugin.
APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }}
PLAY_STORE_JSON_KEY_DATA: ${{ secrets.PLAY_STORE_JSON_KEY_DATA }}
SUPPLY_RELEASE_STATUS: ${{ inputs.release_status }}

steps:
- name: Check out repository
uses: actions/checkout@v4

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

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
# Pinned to the version we develop against locally — bump in lockstep.
flutter-version: "3.44.6"
channel: stable
cache: true

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
working-directory: android

- name: Decode release keystore
env:
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: |
mkdir -p android/app/keystore
KS="$PWD/android/app/keystore/upload-keystore.jks"
# `tr -d '\r\n'` defends against CRLF or wrapped secrets: base64
# silently folds stray bytes into the output and the resulting
# "keystore" then trips the JDK's ASN.1 parser inside Gradle.
printf '%s' "$KEYSTORE_BASE64" | tr -d '\r\n' | base64 -d > "$KS"
if [ "$(stat -c%s "$KS")" -lt 1000 ]; then
echo "Decoded keystore is implausibly small — check ANDROID_KEYSTORE_BASE64." >&2
exit 1
fi
# Absolute storeFile: build.gradle.kts resolves a relative path
# against android/app, an absolute one is passed through unchanged.
printf "storePassword=%s\nkeyPassword=%s\nkeyAlias=%s\nstoreFile=%s\n" \
"$ANDROID_KEYSTORE_PASSWORD" "$ANDROID_KEY_PASSWORD" "$ANDROID_KEY_ALIAS" \
"$KS" > android/key.properties

# Optional: android/app/build.gradle.kts only applies the FCM plugin when
# this file is present, so without the secret the build succeeds but
# ships without push notifications.
- name: Decode google-services.json
env:
GOOGLE_SERVICES_JSON: ${{ secrets.ANDROID_GOOGLE_SERVICES_JSON }}
run: |
if [ -z "$GOOGLE_SERVICES_JSON" ]; then
echo "::warning::ANDROID_GOOGLE_SERVICES_JSON is not set — building without FCM push."
exit 0
fi
printf '%s' "$GOOGLE_SERVICES_JSON" | tr -d '\r\n' | base64 -d > android/app/google-services.json
grep -q '"project_info"' android/app/google-services.json || {
echo "Decoded google-services.json does not look like a Firebase config." >&2
exit 1
}

- name: Install Flutter dependencies
run: flutter pub get

- name: Run tests
run: flutter test

- name: Run Fastlane ${{ inputs.release_type }}
working-directory: android
run: bundle exec fastlane ${{ inputs.release_type }}
110 changes: 110 additions & 0 deletions .github/workflows/ios-release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
# Publish Refetch to TestFlight / the App Store.
#
# Builds a signed IPA and uploads it through the Fastlane lanes in
# `ios/fastlane/`. Manual only — there is no push trigger.
#
# Repository secrets (Settings → Secrets and variables → Actions):
#
# APP_IDENTIFIER iOS bundle id (io.appwrite.refetch)
# APPLE_ID Apple Developer account email
# TEAM_ID Apple Developer Team ID (10 chars)
# ITC_TEAM_ID App Store Connect team id (numeric)
# APP_STORE_CONNECT_KEY_ID App Store Connect API key id
# APP_STORE_CONNECT_ISSUER_ID App Store Connect issuer id
# APP_STORE_CONNECT_KEY_CONTENT Base64-encoded .p8 private key
# MATCH_GIT_URL HTTPS URL of the private certificates repo
# MATCH_PASSWORD Passphrase the match repo is encrypted with
# MATCH_GIT_BASIC_AUTHORIZATION Base64 of "user:personal-access-token",
# used to clone MATCH_GIT_URL over HTTPS
#
# `bundle exec fastlane fetch_apple_info` prints ITC_TEAM_ID locally.
# If you prefer an SSH match URL, drop MATCH_GIT_BASIC_AUTHORIZATION and add
# an ssh-agent step instead.

name: iOS Release

on:
workflow_dispatch:
inputs:
release_type:
description: "Where to send the build"
required: true
type: choice
default: beta
options:
- beta # TestFlight
- release # App Store review
- build # signed IPA only, no upload
build_number:
description: "Override the build number (TestFlight rejects a re-upload at the same one). Blank uses pubspec.yaml."
required: false
type: string

jobs:
publish:
name: Build and upload to App Store Connect (${{ inputs.release_type }})
runs-on: macos-latest
timeout-minutes: 90

env:
# Read by ios/fastlane/{Appfile,Matchfile,Fastfile}.
APP_IDENTIFIER: ${{ secrets.APP_IDENTIFIER }}
APPLE_ID: ${{ secrets.APPLE_ID }}
TEAM_ID: ${{ secrets.TEAM_ID }}
ITC_TEAM_ID: ${{ secrets.ITC_TEAM_ID }}
APP_STORE_CONNECT_KEY_ID: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
APP_STORE_CONNECT_ISSUER_ID: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
APP_STORE_CONNECT_KEY_CONTENT: ${{ secrets.APP_STORE_CONNECT_KEY_CONTENT }}
MATCH_GIT_URL: ${{ secrets.MATCH_GIT_URL }}
MATCH_PASSWORD: ${{ secrets.MATCH_PASSWORD }}
MATCH_GIT_BASIC_AUTHORIZATION: ${{ secrets.MATCH_GIT_BASIC_AUTHORIZATION }}
# Keep fastlane's output readable in the Actions log.
FASTLANE_SKIP_UPDATE_CHECK: "1"
FASTLANE_HIDE_CHANGELOG: "1"

steps:
- name: Check out repository
uses: actions/checkout@v4

- name: Set up Flutter
uses: subosito/flutter-action@v2
with:
# Pinned to the version we develop against locally — bump in lockstep.
flutter-version: "3.44.6"
channel: stable
cache: true

- name: Set up Ruby
uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
bundler-cache: true
working-directory: ios

- name: Install Flutter dependencies
run: flutter pub get

- name: Run tests
run: flutter test

# Generates Runner.xcworkspace and installs the pods that fastlane's
# build_app then archives with the match-provisioned signing identity.
- name: Build Flutter iOS
run: |
if [ -n "${{ inputs.build_number }}" ]; then
flutter build ios --release --no-codesign --build-number="${{ inputs.build_number }}"
else
flutter build ios --release --no-codesign
fi

- name: Run Fastlane ${{ inputs.release_type }}
working-directory: ios
run: bundle exec fastlane ${{ inputs.release_type }}

- name: Upload IPA artifact
if: success()
uses: actions/upload-artifact@v4
with:
name: refetch-ios-${{ inputs.release_type }}
path: build/ios/*.ipa
if-no-files-found: warn
56 changes: 56 additions & 0 deletions .github/workflows/refresh-gemfile-lock.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
# Regenerate android/Gemfile.lock and ios/Gemfile.lock on a runner and commit
# them back. Needed after any Gemfile change: Bundler runs frozen by default in
# CI, so the release workflows fail on a stale lock.

name: Refresh Gemfile.lock

on:
workflow_dispatch:

permissions:
contents: write

jobs:
refresh:
name: Resolve ${{ matrix.platform }}/Gemfile.lock
runs-on: ubuntu-latest
strategy:
# Serial: both jobs commit to the same branch.
max-parallel: 1
matrix:
platform: [android, ios]

steps:
- uses: actions/checkout@v4

- uses: ruby/setup-ruby@v1
with:
ruby-version: "3.2"
# Deliberately no bundler-cache: that is what enforces frozen mode,
# and resolving a new lockfile is the entire point of this job.
bundler-cache: false

- name: Resolve dependencies
working-directory: ${{ matrix.platform }}
run: |
bundle config set --local frozen false
bundle lock
# The iOS lanes run on macOS runners and developers are on macOS too,
# so keep the generic "ruby" platform in the lock alongside Linux.
bundle lock --add-platform ruby x86_64-linux

- name: Commit the updated lockfile
working-directory: ${{ matrix.platform }}
run: |
# Stage first: `git diff` ignores untracked files, so a missing
# Gemfile.lock would look unchanged and be silently dropped.
git add Gemfile.lock
if git diff --cached --quiet -- Gemfile.lock; then
echo "${{ matrix.platform }}/Gemfile.lock already up to date — nothing to commit."
exit 0
fi
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git commit -m "chore(fastlane): refresh ${{ matrix.platform }}/Gemfile.lock"
git pull --rebase
git push
14 changes: 14 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,22 @@ app.*.map.json
# Firebase config files contain project keys — each developer/CI supplies their own.
**/google-services.json
**/GoogleService-Info.plist
# Google Play service account keys — point fastlane at one with
# PLAY_STORE_JSON_KEY_PATH, or hand it to CI as PLAY_STORE_JSON_KEY_DATA.
**/*service_account*.json
**/*service-account*.json
**/play-store-*.json
.env
*.env

# Local-only documentation (design specs, setup notes) — kept out of the repo.
docs/

# Fastlane — generated docs, run reports and locally installed gems.
**/fastlane/README.md
**/fastlane/report.xml
**/fastlane/Preview.html
**/fastlane/test_output/
**/.bundle/
android/vendor/
ios/vendor/
38 changes: 38 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,44 @@ Launcher icons are generated from `assets/icon/` (sources also kept as SVG):
dart run flutter_launcher_icons
```

## Releasing

Both stores are driven by [fastlane](https://fastlane.tools), configured per
platform under `android/fastlane/` and `ios/fastlane/`. Store listing text lives
in `fastlane/metadata/` alongside the app.

```bash
cd android && bundle install # or: cd ios && bundle install
bundle exec fastlane lanes
```

Shared lane names: `build` (no upload), `beta` (Play open testing / TestFlight),
`release` (Play production / App Store review), plus `upload_metadata`,
`upload_screenshots`, `upload_listing`, `download_metadata` and `screenshots`.
Android also has `internal` for the Play internal track.

Everything project-specific is read from environment variables, so no secrets
are committed — set them in your CI provider or a local, gitignored `.env`:

- **Android** — `APP_IDENTIFIER`, plus `PLAY_STORE_JSON_KEY_PATH` or
`PLAY_STORE_JSON_KEY_DATA`. Release signing still comes from
`android/key.properties`. The build/upload logic lives in
[fastlane-plugin-play_publisher](https://github.com/popupbits/fastlane-plugin-play_publisher),
which documents the optional vars (`SUPPLY_RELEASE_STATUS=draft` for a
first-ever release, `SKIP_FLUTTER_BUILD=1`, …).
- **iOS** — `APP_IDENTIFIER`, `APPLE_ID`, `TEAM_ID`, `ITC_TEAM_ID`,
`APP_STORE_CONNECT_KEY_ID`, `APP_STORE_CONNECT_ISSUER_ID`,
`APP_STORE_CONNECT_KEY_CONTENT` (base64 `.p8`), and `MATCH_GIT_URL` /
`MATCH_PASSWORD` for the private
[match](https://docs.fastlane.tools/actions/match/) certificates repo.
`fastlane fetch_apple_info` prints your `ITC_TEAM_ID`.

In CI the same lanes run from **Actions → Android Release / iOS Release**, both
manual (`workflow_dispatch`) — pick a track and go. The secrets each one needs
are listed at the top of `.github/workflows/*.yml`. After changing either
`Gemfile`, run **Refresh Gemfile.lock** so the committed locks stay in sync;
Bundler runs frozen in CI and a stale lock fails the release.

## License

[MIT](LICENSE) — consistent with the
Expand Down
6 changes: 6 additions & 0 deletions android/Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -2,5 +2,11 @@ source "https://rubygems.org"

gem "fastlane", "~> 2.225"

# All Android build/publish logic lives in this plugin — the Fastfile in
# this repo is only lane names.
gem "fastlane-plugin-play_publisher",
git: "https://github.com/popupbits/fastlane-plugin-play_publisher",
tag: "v1.0.0"

# Optional plugins — uncomment as needed
# gem "fastlane-plugin-firebase_app_distribution"
Loading