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
122 changes: 122 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,122 @@
name: CI

on:
pull_request:
push:
branches: [main]

# Cancel superseded runs on the same ref to save runner minutes.
concurrency:
group: ci-${{ github.workflow }}-${{ github.ref }}
cancel-in-progress: true

permissions:
contents: read

env:
# Keep this in sync with openhealth/pubspec.yaml `environment.flutter`
# and the version used by developers locally.
FLUTTER_VERSION: "3.41.6"

jobs:
# Static analysis, formatting and unit tests. This is the PR quality gate.
analyze-and-test:
name: Analyze & Test
runs-on: ubuntu-latest
defaults:
run:
working-directory: openhealth
steps:
- name: Checkout
uses: actions/checkout@v4

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

- name: Install dependencies
run: flutter pub get

- name: Verify formatting
run: dart format --output=none --set-exit-if-changed .

- name: Analyze
run: flutter analyze

- name: Run tests with coverage
run: flutter test --coverage

- name: Upload coverage artifact
if: always()
uses: actions/upload-artifact@v4
with:
name: coverage
path: openhealth/coverage/lcov.info
if-no-files-found: ignore

# Android debug build sanity check.
build-android:
name: Build Android (debug)
runs-on: ubuntu-latest
defaults:
run:
working-directory: openhealth
steps:
- name: Checkout
uses: actions/checkout@v4

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

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

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

- name: Install dependencies
run: flutter pub get

- name: Build debug APK
run: flutter build apk --debug

# iOS build sanity check (no code signing).
build-ios:
name: Build iOS (no codesign)
runs-on: macos-latest
defaults:
run:
working-directory: openhealth
steps:
- name: Checkout
uses: actions/checkout@v4

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

- name: Install dependencies
run: flutter pub get

- name: Build iOS (no codesign)
run: flutter build ios --no-codesign
229 changes: 229 additions & 0 deletions .github/workflows/release.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,229 @@
name: Release

on:
push:
tags:
- "v*"
workflow_dispatch:

# Only one release run per tag/ref at a time.
concurrency:
group: release-${{ github.ref }}
cancel-in-progress: false

permissions:
contents: write # needed to create/attach assets to the GitHub Release

env:
# Keep this in sync with openhealth/pubspec.yaml `environment.flutter`.
FLUTTER_VERSION: "3.41.6"

jobs:
# --------------------------------------------------------------------------
# Android: build a signed release APK + AAB and attach to the GitHub Release.
# Skips gracefully (no-op) when signing secrets are not configured.
# Required secrets:
# ANDROID_KEYSTORE_BASE64 - base64 of the .jks/.keystore file
# ANDROID_KEYSTORE_PASSWORD
# ANDROID_KEY_ALIAS
# ANDROID_KEY_PASSWORD
# --------------------------------------------------------------------------
android:
name: Android (APK + AAB)
runs-on: ubuntu-latest
defaults:
run:
working-directory: openhealth
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Check Android signing secrets
id: secrets
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: |
if [ -n "$ANDROID_KEYSTORE_BASE64" ] \
&& [ -n "$ANDROID_KEYSTORE_PASSWORD" ] \
&& [ -n "$ANDROID_KEY_ALIAS" ] \
&& [ -n "$ANDROID_KEY_PASSWORD" ]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "::warning title=Android release skipped::Android signing secrets are not set (ANDROID_KEYSTORE_BASE64, ANDROID_KEYSTORE_PASSWORD, ANDROID_KEY_ALIAS, ANDROID_KEY_PASSWORD). See docs/RELEASING.md. Skipping signed Android build."
fi

- name: Set up Java
if: steps.secrets.outputs.configured == 'true'
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "17"

- name: Set up Flutter
if: steps.secrets.outputs.configured == 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: stable
cache: true

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

- name: Decode keystore
if: steps.secrets.outputs.configured == 'true'
env:
ANDROID_KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
run: |
echo "$ANDROID_KEYSTORE_BASE64" | base64 --decode > android/app/release-keystore.jks

- name: Write key.properties
if: steps.secrets.outputs.configured == 'true'
env:
ANDROID_KEYSTORE_PASSWORD: ${{ secrets.ANDROID_KEYSTORE_PASSWORD }}
ANDROID_KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
ANDROID_KEY_PASSWORD: ${{ secrets.ANDROID_KEY_PASSWORD }}
run: |
cat > android/key.properties <<EOF
storePassword=$ANDROID_KEYSTORE_PASSWORD
keyPassword=$ANDROID_KEY_PASSWORD
keyAlias=$ANDROID_KEY_ALIAS
storeFile=release-keystore.jks
EOF

- name: Install dependencies
if: steps.secrets.outputs.configured == 'true'
run: flutter pub get

- name: Build signed APK
if: steps.secrets.outputs.configured == 'true'
run: flutter build apk --release

- name: Build signed AAB
if: steps.secrets.outputs.configured == 'true'
run: flutter build appbundle --release

- name: Upload build artifacts (workflow)
if: steps.secrets.outputs.configured == 'true'
uses: actions/upload-artifact@v4
with:
name: android-release
path: |
openhealth/build/app/outputs/flutter-apk/app-release.apk
openhealth/build/app/outputs/bundle/release/app-release.aab
if-no-files-found: error

- name: Attach to GitHub Release
if: steps.secrets.outputs.configured == 'true' && startsWith(github.ref, 'refs/tags/')
uses: softprops/action-gh-release@v2
with:
files: |
openhealth/build/app/outputs/flutter-apk/app-release.apk
openhealth/build/app/outputs/bundle/release/app-release.aab
fail_on_unmatched_files: true

# --------------------------------------------------------------------------
# iOS: build a signed IPA and upload it to TestFlight.
# Skips gracefully (no-op) when App Store Connect secrets are not configured.
# Required secrets:
# APPSTORE_API_KEY_ID - App Store Connect API Key ID
# APPSTORE_API_ISSUER_ID - App Store Connect API Issuer ID
# APPSTORE_API_KEY_BASE64 - base64 of the AuthKey_XXXX.p8 private key
# Optional secrets (manual signing instead of automatic):
# IOS_DIST_CERT_BASE64 - base64 of the distribution .p12
# IOS_DIST_CERT_PASSWORD - password for the .p12
# IOS_PROVISIONING_PROFILE_BASE64 - base64 of the .mobileprovision
# --------------------------------------------------------------------------
ios:
name: iOS (TestFlight)
runs-on: macos-latest
defaults:
run:
working-directory: openhealth
steps:
- name: Checkout
uses: actions/checkout@v4

- name: Check App Store Connect secrets
id: secrets
env:
APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
APPSTORE_API_KEY_BASE64: ${{ secrets.APPSTORE_API_KEY_BASE64 }}
run: |
if [ -n "$APPSTORE_API_KEY_ID" ] \
&& [ -n "$APPSTORE_API_ISSUER_ID" ] \
&& [ -n "$APPSTORE_API_KEY_BASE64" ]; then
echo "configured=true" >> "$GITHUB_OUTPUT"
else
echo "configured=false" >> "$GITHUB_OUTPUT"
echo "::warning title=iOS release skipped::App Store Connect secrets are not set (APPSTORE_API_KEY_ID, APPSTORE_API_ISSUER_ID, APPSTORE_API_KEY_BASE64). See docs/RELEASING.md. Skipping TestFlight upload."
fi

- name: Set up Flutter
if: steps.secrets.outputs.configured == 'true'
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ env.FLUTTER_VERSION }}
channel: stable
cache: true

- name: Install dependencies
if: steps.secrets.outputs.configured == 'true'
run: flutter pub get

- name: Set up App Store Connect API key
if: steps.secrets.outputs.configured == 'true'
env:
APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APPSTORE_API_KEY_BASE64: ${{ secrets.APPSTORE_API_KEY_BASE64 }}
run: |
mkdir -p "$HOME/private_keys"
echo "$APPSTORE_API_KEY_BASE64" | base64 --decode \
> "$HOME/private_keys/AuthKey_${APPSTORE_API_KEY_ID}.p8"

# Build the IPA. ios/ExportOptions.plist uses automatic signing with the
# team (YLK778Z528); xcodebuild authenticates to fetch signing assets via
# the App Store Connect API key.
- name: Build signed IPA
if: steps.secrets.outputs.configured == 'true'
env:
APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
run: |
flutter build ipa --release \
--export-options-plist=ios/ExportOptions.plist

- name: Upload to TestFlight
if: steps.secrets.outputs.configured == 'true'
env:
APPSTORE_API_KEY_ID: ${{ secrets.APPSTORE_API_KEY_ID }}
APPSTORE_API_ISSUER_ID: ${{ secrets.APPSTORE_API_ISSUER_ID }}
run: |
IPA_PATH="$(find build/ios/ipa -name '*.ipa' | head -n 1)"
echo "Uploading $IPA_PATH to TestFlight..."
xcrun altool --upload-app \
--type ios \
--file "$IPA_PATH" \
--apiKey "$APPSTORE_API_KEY_ID" \
--apiIssuer "$APPSTORE_API_ISSUER_ID"

- name: Upload IPA artifact (workflow)
if: steps.secrets.outputs.configured == 'true'
uses: actions/upload-artifact@v4
with:
name: ios-release
path: openhealth/build/ios/ipa/*.ipa
if-no-files-found: error
Loading
Loading