feat: add reusable Flutter build actions for android, ios, macos, windows, and linux
Update README Actions Table / update-readme (push) Successful in 7s

This commit is contained in:
2026-10-18 13:55:08 -07:00
parent 56dd7d58b3
commit 7a12bb1cc6
11 changed files with 1669 additions and 0 deletions
+5
View File
@@ -10,6 +10,11 @@ This repository contains reusable composite actions for CI/CD workflows across o
| :--- | :--- | :--- | | :--- | :--- | :--- |
| [Deploy to Coolify](./coolify-deploy) | Triggers a Coolify deployment using a webhook and authorization token. | `rodydavis/shared-actions/coolify-deploy@main` | | [Deploy to Coolify](./coolify-deploy) | Triggers a Coolify deployment using a webhook and authorization token. | `rodydavis/shared-actions/coolify-deploy@main` |
| [Enforce Flutter Version Constraint](./enforce-flutter-version) | Scans all pubspec.yaml files to ensure they use the required Flutter SDK constraint. | `rodydavis/shared-actions/enforce-flutter-version@main` | | [Enforce Flutter Version Constraint](./enforce-flutter-version) | Scans all pubspec.yaml files to ensure they use the required Flutter SDK constraint. | `rodydavis/shared-actions/enforce-flutter-version@main` |
| [Flutter Build Android](./flutter-build-android) | Configures Android keystore signing, builds release AAB/APK artifacts, and optionally deploys to Google Play Console and Shorebird. | `rodydavis/shared-actions/flutter-build-android@main` |
| [Flutter Build iOS](./flutter-build-ios) | Installs Apple signing certificates and provisioning profiles in an isolated keychain, builds an iOS release IPA, and optionally uploads to TestFlight and Shorebird. | `rodydavis/shared-actions/flutter-build-ios@main` |
| [Flutter Build Linux](./flutter-build-linux) | Installs GTK3/Linux build dependencies, builds Linux Flutter desktop apps, packages .tar.gz and .deb installers, and generates release metadata. | `rodydavis/shared-actions/flutter-build-linux@main` |
| [Flutter Build macOS](./flutter-build-macos) | Builds, signs (Developer ID & Sparkle), packages (.dmg / .zip), notarizes, and generates release metadata for macOS Flutter desktop apps. | `rodydavis/shared-actions/flutter-build-macos@main` |
| [Flutter Build Windows](./flutter-build-windows) | Builds Windows Flutter desktop apps, compiles Inno Setup (.exe) and MSIX (.msix) installers, signs updates for WinSparkle (DSA), and generates release metadata. | `rodydavis/shared-actions/flutter-build-windows@main` |
| [Deploy Static Pages](./pages-deploy) | Packages static web assets into a hardened Nginx container, pushes to Gitea registry, and deploys to TrueNAS with Traefik ingress. | `rodydavis/shared-actions/pages-deploy@main` | | [Deploy Static Pages](./pages-deploy) | Packages static web assets into a hardened Nginx container, pushes to Gitea registry, and deploys to TrueNAS with Traefik ingress. | `rodydavis/shared-actions/pages-deploy@main` |
| [Setup Flutter](./setup-flutter) | Cross-platform setup for Flutter with local mirror and multi-runner concurrency support. | `rodydavis/shared-actions/setup-flutter@main` | | [Setup Flutter](./setup-flutter) | Cross-platform setup for Flutter with local mirror and multi-runner concurrency support. | `rodydavis/shared-actions/setup-flutter@main` |
| [Deploy to TrueNAS](./truenas-deploy) | Deploys a Docker Compose stack to TrueNAS SCALE over SSH and pulls latest container images. | `rodydavis/shared-actions/truenas-deploy@main` | | [Deploy to TrueNAS](./truenas-deploy) | Deploys a Docker Compose stack to TrueNAS SCALE over SSH and pulls latest container images. | `rodydavis/shared-actions/truenas-deploy@main` |
+52
View File
@@ -0,0 +1,52 @@
# Flutter Build Android
Composite action to configure Android keystore signing, build release AAB and APK artifacts, and optionally upload to Google Play Console and Shorebird.
## Inputs
| Name | Description | Required | Default |
| :--- | :--- | :--- | :--- |
| `working_directory` | Path to the Flutter app package directory. | No | `.` |
| `build_aab` | Whether to build an Android App Bundle (`.aab`). | No | `true` |
| `build_apk` | Whether to build a release APK (`.apk`). | No | `false` |
| `build_args` | Extra arguments passed to `flutter build`. | No | `''` |
| `artifact_prefix` | Optional prefix for copied versioned artifacts in `output_dir`. | No | `''` |
| `output_dir` | Directory to copy versioned artifacts into when `artifact_prefix` is set. | No | `artifacts` |
| `keystore_base64` | Base64-encoded Android release keystore file. | No | `''` |
| `key_alias` | Android signing key alias. | No | `''` |
| `key_password` | Android signing key password. | No | `''` |
| `store_password` | Android keystore password. | No | `''` |
| `google_play_service_account_json` | Google Play service account JSON content. | No | `''` |
| `google_play_service_account_file` | Fallback path to Google Play service account JSON file. | No | `''` |
| `google_play_track` | Google Play track (`internal`, `alpha`, `beta`, `production`). | No | `beta` |
| `google_play_status` | Google Play release status (`completed`, `draft`, `inProgress`, `halted`). | No | `completed` |
| `google_play_script` | Path to the Dart Google Play upload script. | No | `scripts/upload_play_store.dart` |
| `shorebird_token` | Shorebird token for optional Shorebird release. | No | `''` |
| `shorebird_artifact` | Shorebird artifact type (`apk` or `aab`). | No | `apk` |
## Outputs
| Name | Description |
| :--- | :--- |
| `version` | Resolved package version from `pubspec.yaml`. |
| `aab_path` | Path to the built `.aab` file. |
| `apk_path` | Path to the built `.apk` file. |
## Example Usage
```yaml
- name: Build & Release Android
uses: rodydavis/shared-actions/flutter-build-android@main
with:
working_directory: packages/hfw_flutter
build_aab: 'true'
build_apk: 'true'
artifact_prefix: hfw-android
keystore_base64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
key_alias: ${{ secrets.ANDROID_KEY_ALIAS }}
key_password: ${{ secrets.ANDROID_KEY_PASSWORD }}
store_password: ${{ secrets.ANDROID_STORE_PASSWORD }}
google_play_service_account_json: ${{ secrets.GOOGLE_PLAY_SERVICE_ACCOUNT_JSON }}
google_play_track: beta
shorebird_token: ${{ secrets.SHOREBIRD_TOKEN }}
```
+212
View File
@@ -0,0 +1,212 @@
name: 'Flutter Build Android'
description: 'Configures Android keystore signing, builds release AAB/APK artifacts, and optionally deploys to Google Play Console and Shorebird.'
inputs:
working_directory:
description: 'Path to the Flutter app package directory (e.g., packages/hfw_flutter or .)'
required: false
default: '.'
build_aab:
description: 'Whether to build an Android App Bundle (.aab)'
required: false
default: 'true'
build_apk:
description: 'Whether to build a release APK (.apk)'
required: false
default: 'false'
build_args:
description: 'Extra arguments passed to flutter build appbundle / apk (e.g., --dart-define=...)'
required: false
default: ''
artifact_prefix:
description: 'Optional prefix for copied versioned artifacts in output_dir (e.g., hfw-android -> hfw-android-v1.0.0.apk)'
required: false
default: ''
output_dir:
description: 'Directory to copy versioned artifacts into when artifact_prefix is set'
required: false
default: 'artifacts'
keystore_base64:
description: 'Base64-encoded Android release keystore file'
required: false
default: ''
key_alias:
description: 'Android signing key alias'
required: false
default: ''
key_password:
description: 'Android signing key password'
required: false
default: ''
store_password:
description: 'Android keystore password'
required: false
default: ''
google_play_service_account_json:
description: 'Google Play service account JSON content for Play Store upload'
required: false
default: ''
google_play_service_account_file:
description: 'Fallback path to Google Play service account JSON file (relative to repo root)'
required: false
default: ''
google_play_track:
description: 'Google Play track to deploy to (internal, alpha, beta, production)'
required: false
default: 'beta'
google_play_status:
description: 'Google Play release status (completed, draft, inProgress, halted)'
required: false
default: 'completed'
google_play_script:
description: 'Path to the Dart Google Play upload script (relative to repo root)'
required: false
default: 'scripts/upload_play_store.dart'
shorebird_token:
description: 'Shorebird token for optional Shorebird release'
required: false
default: ''
shorebird_artifact:
description: 'Shorebird artifact type (apk or aab)'
required: false
default: 'apk'
outputs:
version:
description: 'Resolved package version from pubspec.yaml'
value: ${{ steps.build.outputs.version }}
aab_path:
description: 'Path to the built AAB file'
value: ${{ steps.build.outputs.aab_path }}
apk_path:
description: 'Path to the built APK file'
value: ${{ steps.build.outputs.apk_path }}
runs:
using: 'composite'
steps:
- name: Configure Android Signing Keystore
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
KEYSTORE_BASE64: ${{ inputs.keystore_base64 }}
KEY_ALIAS: ${{ inputs.key_alias }}
KEY_PASSWORD: ${{ inputs.key_password }}
STORE_PASSWORD: ${{ inputs.store_password }}
run: |
if [ -n "$KEYSTORE_BASE64" ]; then
echo "Decoding Android Keystore..."
mkdir -p "$WORK_DIR/android/app"
echo "$KEYSTORE_BASE64" | base64 --decode > "$WORK_DIR/android/app/release.keystore"
printf "storePassword=%s\nkeyPassword=%s\nkeyAlias=%s\nstoreFile=release.keystore\n" \
"$STORE_PASSWORD" "$KEY_PASSWORD" "$KEY_ALIAS" > "$WORK_DIR/android/key.properties"
echo "Configured $WORK_DIR/android/key.properties"
else
echo "Notice: keystore_base64 not configured. Building unsigned / default debug signing."
fi
- name: Build Android Release Artifacts
id: build
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
BUILD_AAB: ${{ inputs.build_aab }}
BUILD_APK: ${{ inputs.build_apk }}
BUILD_ARGS: ${{ inputs.build_args }}
ARTIFACT_PREFIX: ${{ inputs.artifact_prefix }}
OUTPUT_DIR: ${{ inputs.output_dir }}
run: |
ROOT_DIR="$(pwd)"
VERSION=$(grep '^version: ' "$WORK_DIR/pubspec.yaml" | head -n 1 | cut -d ' ' -f 2 | tr -d '\r')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
cd "$WORK_DIR"
if [ "$BUILD_AAB" = "true" ]; then
echo "Building Android App Bundle (AAB)..."
# shellcheck disable=SC2086
flutter build appbundle --release $BUILD_ARGS
fi
if [ "$BUILD_APK" = "true" ]; then
echo "Building Android APK..."
# shellcheck disable=SC2086
flutter build apk --release $BUILD_ARGS
fi
cd "$ROOT_DIR"
AAB_REL="$WORK_DIR/build/app/outputs/bundle/release/app-release.aab"
APK_REL="$WORK_DIR/build/app/outputs/flutter-apk/app-release.apk"
AAB_REL="${AAB_REL#./}"
APK_REL="${APK_REL#./}"
if [ -n "$ARTIFACT_PREFIX" ]; then
mkdir -p "$OUTPUT_DIR"
if [ -f "$AAB_REL" ]; then
cp "$AAB_REL" "$OUTPUT_DIR/${ARTIFACT_PREFIX}-v${VERSION}.aab"
AAB_OUT="$OUTPUT_DIR/${ARTIFACT_PREFIX}-v${VERSION}.aab"
fi
if [ -f "$APK_REL" ]; then
cp "$APK_REL" "$OUTPUT_DIR/${ARTIFACT_PREFIX}-v${VERSION}.apk"
APK_OUT="$OUTPUT_DIR/${ARTIFACT_PREFIX}-v${VERSION}.apk"
fi
echo "Collected Android artifacts for version $VERSION in $OUTPUT_DIR"
fi
echo "aab_path=${AAB_OUT:-$AAB_REL}" >> "$GITHUB_OUTPUT"
echo "apk_path=${APK_OUT:-$APK_REL}" >> "$GITHUB_OUTPUT"
- name: Upload AAB to Google Play Console
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
PLAY_STORE_JSON: ${{ inputs.google_play_service_account_json }}
PLAY_STORE_FILE: ${{ inputs.google_play_service_account_file }}
PLAY_TRACK: ${{ inputs.google_play_track }}
PLAY_STATUS: ${{ inputs.google_play_status }}
PLAY_SCRIPT: ${{ inputs.google_play_script }}
run: |
AAB_FILE="$WORK_DIR/build/app/outputs/bundle/release/app-release.aab"
AAB_FILE="${AAB_FILE#./}"
if [ ! -f "$AAB_FILE" ]; then
echo "No AAB file found at $AAB_FILE; skipping Google Play upload."
exit 0
fi
DEFAULT_SA_FILE="$WORK_DIR/android/app/google_play.json"
DEFAULT_SA_FILE="${DEFAULT_SA_FILE#./}"
SA_FILE="${PLAY_STORE_FILE:-$DEFAULT_SA_FILE}"
if [ -n "$PLAY_STORE_JSON" ] && [ -f "$PLAY_SCRIPT" ]; then
echo "Google Play service account secret detected. Uploading AAB to track '$PLAY_TRACK'..."
echo "$PLAY_STORE_JSON" > /tmp/google_play_service_account.json
dart "$PLAY_SCRIPT" \
--aab "$AAB_FILE" \
--service-account /tmp/google_play_service_account.json \
--track "$PLAY_TRACK" \
--status "$PLAY_STATUS"
rm -f /tmp/google_play_service_account.json
elif [ -f "$SA_FILE" ] && [ -f "$PLAY_SCRIPT" ]; then
echo "Using repository service account file ($SA_FILE) for Google Play upload to track '$PLAY_TRACK'..."
dart "$PLAY_SCRIPT" \
--aab "$AAB_FILE" \
--service-account "$SA_FILE" \
--track "$PLAY_TRACK" \
--status "$PLAY_STATUS"
else
echo "Skipping Google Play upload (credentials or upload script not configured)."
fi
- name: Release via Shorebird (Optional)
if: inputs.shorebird_token != ''
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
SHOREBIRD_TOKEN: ${{ inputs.shorebird_token }}
SHOREBIRD_ARTIFACT: ${{ inputs.shorebird_artifact }}
run: |
if [ -n "$SHOREBIRD_TOKEN" ]; then
ROOT_DIR="$(pwd)"
cd "$WORK_DIR"
flutter pub global activate shorebird_cli
shorebird release android --artifact="$SHOREBIRD_ARTIFACT"
cd "$ROOT_DIR"
fi
+49
View File
@@ -0,0 +1,49 @@
# Flutter Build iOS
Composite action to install Apple signing certificates and provisioning profiles into an isolated temporary keychain, build a Flutter iOS `.ipa`, and optionally push to TestFlight and Shorebird.
## Inputs
| Name | Description | Required | Default |
| :--- | :--- | :--- | :--- |
| `working_directory` | Path to the Flutter app package directory. | No | `.` |
| `export_options_plist` | Relative path inside `working_directory` to `ExportOptions.plist`. | No | `ios/ExportOptions.plist` |
| `build_args` | Extra arguments passed to `flutter build ipa / ios`. | No | `''` |
| `disable_spm` | Disable Flutter Swift Package Manager before building. | No | `true` |
| `artifact_prefix` | Optional prefix for copied versioned IPA in `output_dir`. | No | `''` |
| `output_dir` | Directory to copy versioned IPA into when `artifact_prefix` is set. | No | `artifacts` |
| `certificate_base64` | Base64-encoded Apple iOS distribution `.p12` certificate. | No | `''` |
| `certificate_password` | Password for the Apple `.p12` certificate. | No | `''` |
| `provisioning_profile_base64` | Base64-encoded `.mobileprovision` profile. | No | `''` |
| `asc_key_id` | App Store Connect API Key ID. | No | `''` |
| `asc_issuer_id` | App Store Connect API Issuer ID. | No | `''` |
| `asc_private_key` | App Store Connect API Private Key (`.p8` content). | No | `''` |
| `apple_id_username` | Fallback Apple ID email for `altool` upload. | No | `''` |
| `apple_id_password` | Fallback Apple ID app-specific password for `altool` upload. | No | `''` |
| `upload_to_testflight` | Whether to upload signed IPA to TestFlight when credentials are provided. | No | `true` |
| `shorebird_token` | Shorebird token for optional Shorebird iOS release. | No | `''` |
## Outputs
| Name | Description |
| :--- | :--- |
| `version` | Resolved package version from `pubspec.yaml`. |
| `ipa_path` | Path to the built or packaged `.ipa` file. |
| `signed` | `'true'` if a signed App Store IPA was exported, `'false'` otherwise. |
## Example Usage
```yaml
- name: Build & Release iOS
uses: rodydavis/shared-actions/flutter-build-ios@main
with:
working_directory: packages/hfw_flutter
artifact_prefix: hfw-ios
certificate_base64: ${{ secrets.IOS_CERTIFICATE_BASE64 }}
certificate_password: ${{ secrets.IOS_CERTIFICATE_PWD }}
provisioning_profile_base64: ${{ secrets.IOS_PROVISIONING_PROFILE_BASE64 }}
asc_key_id: ${{ secrets.APP_STORE_CONNECT_KEY_ID }}
asc_issuer_id: ${{ secrets.APP_STORE_CONNECT_ISSUER_ID }}
asc_private_key: ${{ secrets.APP_STORE_CONNECT_PRIVATE_KEY }}
shorebird_token: ${{ secrets.SHOREBIRD_TOKEN }}
```
+244
View File
@@ -0,0 +1,244 @@
name: 'Flutter Build iOS'
description: 'Installs Apple signing certificates and provisioning profiles in an isolated keychain, builds an iOS release IPA, and optionally uploads to TestFlight and Shorebird.'
inputs:
working_directory:
description: 'Path to the Flutter app package directory (e.g., packages/hfw_flutter or .)'
required: false
default: '.'
export_options_plist:
description: 'Relative path inside working_directory to ExportOptions.plist'
required: false
default: 'ios/ExportOptions.plist'
build_args:
description: 'Extra arguments passed to flutter build ipa / ios (e.g., --dart-define=...)'
required: false
default: ''
disable_spm:
description: 'Disable Flutter Swift Package Manager before building'
required: false
default: 'true'
artifact_prefix:
description: 'Optional prefix for copied versioned IPA in output_dir (e.g., hfw-ios -> hfw-ios-v1.0.0.ipa)'
required: false
default: ''
output_dir:
description: 'Directory to copy versioned IPA into when artifact_prefix is set'
required: false
default: 'artifacts'
certificate_base64:
description: 'Base64-encoded Apple iOS distribution .p12 certificate'
required: false
default: ''
certificate_password:
description: 'Password for the Apple .p12 certificate'
required: false
default: ''
provisioning_profile_base64:
description: 'Base64-encoded .mobileprovision profile'
required: false
default: ''
asc_key_id:
description: 'App Store Connect API Key ID'
required: false
default: ''
asc_issuer_id:
description: 'App Store Connect API Issuer ID'
required: false
default: ''
asc_private_key:
description: 'App Store Connect API Private Key (.p8 content)'
required: false
default: ''
apple_id_username:
description: 'Fallback Apple ID email for altool upload'
required: false
default: ''
apple_id_password:
description: 'Fallback Apple ID app-specific password for altool upload'
required: false
default: ''
upload_to_testflight:
description: 'Whether to upload signed IPA to TestFlight when credentials are provided'
required: false
default: 'true'
shorebird_token:
description: 'Shorebird token for optional Shorebird iOS release'
required: false
default: ''
outputs:
version:
description: 'Resolved package version from pubspec.yaml'
value: ${{ steps.build.outputs.version }}
ipa_path:
description: 'Path to the built or packaged IPA file'
value: ${{ steps.build.outputs.ipa_path }}
signed:
description: 'Whether a signed IPA was exported by Xcode'
value: ${{ steps.build.outputs.signed }}
runs:
using: 'composite'
steps:
- name: Install Apple Certificate and Provisioning Profile
shell: bash
env:
IOS_CERT_BASE64: ${{ inputs.certificate_base64 }}
IOS_CERT_PWD: ${{ inputs.certificate_password }}
IOS_PROFILE_BASE64: ${{ inputs.provisioning_profile_base64 }}
run: |
if [ -n "$IOS_CERT_BASE64" ]; then
echo "Setting up temporary keychain for iOS signing..."
security delete-keychain signing_temp.keychain 2>/dev/null || true
rm -f "$HOME/Library/Keychains/signing_temp.keychain-db" || true
KEYCHAIN="$HOME/Library/Keychains/signing_temp.keychain-db"
KEYCHAIN_PWD="temp_password_123"
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" || true
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" || true
security set-keychain-settings -lut 21600 "$KEYCHAIN" || true
security list-keychains -d user -s "$KEYCHAIN" "$HOME/Library/Keychains/login.keychain-db" || true
security default-keychain -s "$HOME/Library/Keychains/login.keychain-db" || true
echo "$IOS_CERT_BASE64" | base64 --decode > /tmp/certificate.p12
security import "/tmp/certificate.p12" -k "$KEYCHAIN" -P "$IOS_CERT_PWD" -T /usr/bin/codesign -T /usr/bin/security -A || true
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PWD" "$KEYCHAIN" || true
if [ -n "$IOS_PROFILE_BASE64" ]; then
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
echo "$IOS_PROFILE_BASE64" | base64 --decode > /tmp/profile.mobileprovision
PROFILE_UUID=$(security cms -D -i /tmp/profile.mobileprovision 2>/dev/null | plutil -extract UUID raw - 2>/dev/null || echo "profile")
cp /tmp/profile.mobileprovision "$HOME/Library/MobileDevice/Provisioning Profiles/${PROFILE_UUID}.mobileprovision"
cp /tmp/profile.mobileprovision "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/${PROFILE_UUID}.mobileprovision"
fi
echo "iOS signing credentials installed successfully."
else
echo "Notice: certificate_base64 not configured."
fi
- name: Build iOS IPA
id: build
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
EXPORT_PLIST: ${{ inputs.export_options_plist }}
BUILD_ARGS: ${{ inputs.build_args }}
DISABLE_SPM: ${{ inputs.disable_spm }}
ARTIFACT_PREFIX: ${{ inputs.artifact_prefix }}
OUTPUT_DIR: ${{ inputs.output_dir }}
IOS_CERT_BASE64: ${{ inputs.certificate_base64 }}
ASC_KEY_ID: ${{ inputs.asc_key_id }}
ASC_ISSUER_ID: ${{ inputs.asc_issuer_id }}
ASC_KEY_CONTENT: ${{ inputs.asc_private_key }}
run: |
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:$PATH"
ROOT_DIR="$(pwd)"
VERSION=$(grep '^version: ' "$WORK_DIR/pubspec.yaml" | head -n 1 | cut -d ' ' -f 2 | tr -d '\r')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [ -n "$ASC_KEY_ID" ] && [ -n "$ASC_KEY_CONTENT" ]; then
mkdir -p "$HOME/.appstoreconnect/private_keys"
echo "$ASC_KEY_CONTENT" > "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8"
fi
cd "$WORK_DIR"
if [ "$DISABLE_SPM" = "true" ]; then
flutter config --no-enable-swift-package-manager
fi
if [ -f "$EXPORT_PLIST" ] && [ -n "$IOS_CERT_BASE64" ]; then
# shellcheck disable=SC2086
flutter build ipa --release --export-options-plist="$EXPORT_PLIST" $BUILD_ARGS || flutter build ios --release --no-codesign $BUILD_ARGS
else
# shellcheck disable=SC2086
flutter build ios --release --no-codesign $BUILD_ARGS
fi
cd "$ROOT_DIR"
SIGNED_IPA=$(ls "$WORK_DIR"/build/ios/ipa/*.ipa 2>/dev/null | head -n 1 || true)
if [ -n "$SIGNED_IPA" ]; then
echo "signed=true" >> "$GITHUB_OUTPUT"
else
echo "signed=false" >> "$GITHUB_OUTPUT"
fi
IPA_OUT="${SIGNED_IPA#./}"
if [ -n "$ARTIFACT_PREFIX" ]; then
mkdir -p "$OUTPUT_DIR"
TARGET_IPA="$OUTPUT_DIR/${ARTIFACT_PREFIX}-v${VERSION}.ipa"
if [ -n "$SIGNED_IPA" ]; then
cp "$SIGNED_IPA" "$TARGET_IPA"
elif [ -d "$WORK_DIR/build/ios/iphoneos/Runner.app" ]; then
rm -rf /tmp/Payload
mkdir -p /tmp/Payload
cp -r "$WORK_DIR/build/ios/iphoneos/Runner.app" /tmp/Payload/
(cd /tmp && zip -qr "$ROOT_DIR/$TARGET_IPA" Payload)
rm -rf /tmp/Payload
else
touch "$TARGET_IPA"
fi
IPA_OUT="$TARGET_IPA"
echo "Collected iOS artifact at $IPA_OUT"
fi
echo "ipa_path=$IPA_OUT" >> "$GITHUB_OUTPUT"
- name: Push to TestFlight via App Store Connect API
if: inputs.upload_to_testflight == 'true'
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
ASC_KEY_ID: ${{ inputs.asc_key_id }}
ASC_ISSUER_ID: ${{ inputs.asc_issuer_id }}
ASC_KEY_CONTENT: ${{ inputs.asc_private_key }}
AC_USERNAME: ${{ inputs.apple_id_username }}
AC_PASSWORD: ${{ inputs.apple_id_password }}
run: |
IPA_FILE=$(ls "$WORK_DIR"/build/ios/ipa/*.ipa 2>/dev/null | head -n 1 || true)
if [ -z "$IPA_FILE" ]; then
echo "Skipping TestFlight upload: no signed App Store IPA was exported in $WORK_DIR/build/ios/ipa/."
exit 0
fi
if [ -n "$ASC_KEY_ID" ] && [ -n "$ASC_ISSUER_ID" ] && [ -n "$ASC_KEY_CONTENT" ]; then
echo "Uploading signed IPA ($IPA_FILE) to TestFlight via App Store Connect API Key..."
mkdir -p "$HOME/.appstoreconnect/private_keys"
echo "$ASC_KEY_CONTENT" > "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8"
xcrun altool --upload-app \
-f "$IPA_FILE" \
--type ios \
--apiKey "$ASC_KEY_ID" \
--apiIssuer "$ASC_ISSUER_ID"
elif [ -n "$AC_USERNAME" ] && [ -n "$AC_PASSWORD" ]; then
echo "Uploading signed IPA ($IPA_FILE) to TestFlight via Apple ID App-Specific Password..."
xcrun altool --upload-app \
-f "$IPA_FILE" \
--type ios \
--u "$AC_USERNAME" \
--p "$AC_PASSWORD"
else
echo "Skipping TestFlight upload (App Store Connect credentials not configured)."
fi
- name: Release via Shorebird (Optional)
if: inputs.shorebird_token != ''
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
SHOREBIRD_TOKEN: ${{ inputs.shorebird_token }}
run: |
if [ -n "$SHOREBIRD_TOKEN" ]; then
ROOT_DIR="$(pwd)"
cd "$WORK_DIR"
flutter pub global activate shorebird_cli
shorebird release ios --artifact=ipa
cd "$ROOT_DIR"
fi
- name: Clean up keychain
if: always()
shell: bash
run: |
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" || true
security default-keychain -s "$HOME/Library/Keychains/login.keychain-db" || true
security delete-keychain "$HOME/Library/Keychains/signing_temp.keychain-db" 2>/dev/null || true
rm -f "$HOME/Library/Keychains/signing_temp.keychain-db" /tmp/certificate.p12 /tmp/profile.mobileprovision || true
+43
View File
@@ -0,0 +1,43 @@
# Flutter Build Linux
Composite action to install Linux GTK3 build dependencies, compile Linux Flutter desktop applications, package `.deb` and `.tar.gz` installers, and generate release metadata.
## Inputs
| Name | Description | Required | Default |
| :--- | :--- | :--- | :--- |
| `working_directory` | Path to the Flutter app package directory. | No | `.` |
| `arch` | Target Linux architecture (`amd64` / `x64` or `arm64`). | No | `amd64` |
| `build_args` | Extra arguments passed to `flutter build linux`. | No | `''` |
| `install_deps` | Install Linux GTK3 & CMake build dependencies via `apt-get`. | No | `true` |
| `auto_create_linux_runner` | Automatically run `flutter create --platforms=linux .` if `linux/` directory is missing. | No | `true` |
| `output_dir` | Output directory for packaged `.deb` and `.tar.gz` archives. | No | `dist` |
| `package_script` | Optional custom packaging script in repository (e.g., `scripts/package_linux.sh`). | No | `scripts/package_linux.sh` |
| `app_name` | Application display name for `.desktop` entry. | No | `Slides for Worship` |
| `package_name` | Debian package slug (e.g., `slides-for-worship`). | No | `slides-for-worship` |
| `binary_name` | Compiled Linux executable name inside bundle (auto-detected if empty). | No | `''` |
| `icon_path` | Path to app icon PNG relative to `working_directory`. | No | `assets/icon.png` |
| `maintainer` | Debian package maintainer string. | No | `Rody Davis <rody.davis.jr@gmail.com>` |
| `description` | Debian package description. | No | `Professional worship slide production and presentation studio.` |
| `generate_metadata_json` | Whether to generate `linux_<arch>_metadata.json`. | No | `false` |
## Outputs
| Name | Description |
| :--- | :--- |
| `version` | Resolved package version from `pubspec.yaml`. |
| `deb_path` | Path to generated `.deb` package. |
| `tar_path` | Path to generated `.tar.gz` archive. |
| `metadata_path` | Path to generated `linux_<arch>_metadata.json`. |
## Example Usage
```yaml
- name: Build & Package Linux Release
uses: rodydavis/shared-actions/flutter-build-linux@main
with:
working_directory: packages/sfw_desktop
arch: ${{ matrix.arch }}
build_args: --dart-define=API_BASE_URL=https://slidesforworship.app
generate_metadata_json: 'true'
```
+230
View File
@@ -0,0 +1,230 @@
name: 'Flutter Build Linux'
description: 'Installs GTK3/Linux build dependencies, builds Linux Flutter desktop apps, packages .tar.gz and .deb installers, and generates release metadata.'
inputs:
working_directory:
description: 'Path to the Flutter app package directory (e.g., packages/sfw_desktop)'
required: false
default: '.'
arch:
description: 'Target Linux architecture (amd64 / x64 or arm64)'
required: false
default: 'amd64'
build_args:
description: 'Extra arguments passed to flutter build linux (e.g., --dart-define=...)'
required: false
default: ''
install_deps:
description: 'Install Linux GTK3 & CMake build dependencies via apt-get'
required: false
default: 'true'
auto_create_linux_runner:
description: 'Automatically run flutter create --platforms=linux . if linux/ directory is missing'
required: false
default: 'true'
output_dir:
description: 'Output directory for packaged .deb and .tar.gz archives'
required: false
default: 'dist'
package_script:
description: 'Optional custom packaging script in repository (e.g., scripts/package_linux.sh)'
required: false
default: 'scripts/package_linux.sh'
app_name:
description: 'Application display name for .desktop entry (used if package_script is not present)'
required: false
default: 'Slides for Worship'
package_name:
description: 'Debian package slug (e.g., slides-for-worship)'
required: false
default: 'slides-for-worship'
binary_name:
description: 'Compiled Linux executable name inside bundle (auto-detected if empty)'
required: false
default: ''
icon_path:
description: 'Path to app icon PNG relative to working_directory'
required: false
default: 'assets/icon.png'
maintainer:
description: 'Debian package maintainer string'
required: false
default: 'Rody Davis <rody.davis.jr@gmail.com>'
description:
description: 'Debian package description'
required: false
default: 'Professional worship slide production and presentation studio.'
generate_metadata_json:
description: 'Whether to generate linux_<arch>_metadata.json'
required: false
default: 'false'
outputs:
version:
description: 'Resolved package version from pubspec.yaml'
value: ${{ steps.build.outputs.version }}
deb_path:
description: 'Path to generated .deb package'
value: ${{ steps.package.outputs.deb_path }}
tar_path:
description: 'Path to generated .tar.gz archive'
value: ${{ steps.package.outputs.tar_path }}
metadata_path:
description: 'Path to generated linux_<arch>_metadata.json'
value: ${{ steps.package.outputs.metadata_path }}
runs:
using: 'composite'
steps:
- name: Install Linux Build Dependencies
if: inputs.install_deps == 'true' && runner.os == 'Linux'
shell: bash
run: |
if command -v apt-get >/dev/null 2>&1; then
SUDO=""
if [ "$(id -u)" -ne 0 ] && command -v sudo >/dev/null 2>&1; then
SUDO="sudo"
fi
$SUDO apt-get update
$SUDO apt-get install -y clang cmake ninja-build pkg-config libgtk-3-dev liblzma-dev libstdc++-12-dev dpkg-dev
fi
- name: Build Linux Release
id: build
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
ARCH: ${{ inputs.arch }}
BUILD_ARGS: ${{ inputs.build_args }}
AUTO_CREATE: ${{ inputs.auto_create_linux_runner }}
run: |
ROOT_DIR="$(pwd)"
VERSION=$(grep '^version: ' "$WORK_DIR/pubspec.yaml" | head -n 1 | cut -d ' ' -f 2 | tr -d '\r')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
cd "$WORK_DIR"
flutter config --enable-linux-desktop
if [ ! -d "linux" ] && [ "$AUTO_CREATE" = "true" ]; then
echo "Notice: '$WORK_DIR/linux' directory not found. Scaffolding Linux runner via 'flutter create --platforms=linux .'..."
flutter create --platforms=linux .
fi
if [ "$ARCH" = "arm64" ] || [ "$ARCH" = "aarch64" ]; then
# shellcheck disable=SC2086
flutter build linux --release --target-platform=linux-arm64 $BUILD_ARGS
else
# shellcheck disable=SC2086
flutter build linux --release $BUILD_ARGS
fi
cd "$ROOT_DIR"
- name: Package Linux Installers (.deb & .tar.gz) & Metadata
id: package
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
ARCH_INPUT: ${{ inputs.arch }}
OUT_DIR: ${{ inputs.output_dir }}
PKG_SCRIPT: ${{ inputs.package_script }}
APP_NAME: ${{ inputs.app_name }}
PKG_NAME: ${{ inputs.package_name }}
BIN_NAME: ${{ inputs.binary_name }}
ICON_REL: ${{ inputs.icon_path }}
MAINTAINER: ${{ inputs.maintainer }}
DESC: ${{ inputs.description }}
VERSION: ${{ steps.build.outputs.version }}
GEN_META: ${{ inputs.generate_metadata_json }}
run: |
ROOT_DIR="$(pwd)"
if [ "$ARCH_INPUT" = "arm64" ] || [ "$ARCH_INPUT" = "aarch64" ]; then
DEB_ARCH="arm64"
BUNDLE_DIR="$WORK_DIR/build/linux/arm64/release/bundle"
else
DEB_ARCH="amd64"
BUNDLE_DIR="$WORK_DIR/build/linux/x64/release/bundle"
fi
mkdir -p "$OUT_DIR"
if [ -n "$PKG_SCRIPT" ] && [ -f "$PKG_SCRIPT" ]; then
echo "Running repository packaging script: $PKG_SCRIPT..."
bash "$PKG_SCRIPT" "$VERSION" "$DEB_ARCH" "$BUNDLE_DIR"
else
echo "Packaging .tar.gz and .deb directly..."
TAR_NAME="${PKG_NAME}-linux-${DEB_ARCH}.tar.gz"
tar -czf "$OUT_DIR/$TAR_NAME" -C "$BUNDLE_DIR" .
DEB_DIR="$WORK_DIR/build/deb_pkg_${DEB_ARCH}"
rm -rf "$DEB_DIR"
mkdir -p "$DEB_DIR/DEBIAN"
mkdir -p "$DEB_DIR/usr/bin"
mkdir -p "$DEB_DIR/usr/share/${PKG_NAME}"
mkdir -p "$DEB_DIR/usr/share/applications"
mkdir -p "$DEB_DIR/usr/share/icons/hicolor/512x512/apps"
cp -r "$BUNDLE_DIR"/* "$DEB_DIR/usr/share/${PKG_NAME}/"
if [ -z "$BIN_NAME" ]; then
BIN_NAME=$(grep '^name: ' "$WORK_DIR/pubspec.yaml" | head -n 1 | cut -d ' ' -f 2 | tr -d '\r')
fi
ln -sf "/usr/share/${PKG_NAME}/${BIN_NAME}" "$DEB_DIR/usr/bin/${PKG_NAME}"
if [ -f "$WORK_DIR/$ICON_REL" ]; then
cp "$WORK_DIR/$ICON_REL" "$DEB_DIR/usr/share/icons/hicolor/512x512/apps/${PKG_NAME}.png"
fi
cat <<EOF > "$DEB_DIR/usr/share/applications/${PKG_NAME}.desktop"
[Desktop Entry]
Name=$APP_NAME
Comment=$DESC
Exec=/usr/bin/$PKG_NAME
Icon=$PKG_NAME
Terminal=false
Type=Application
Categories=AudioVideo;Presentation;Utility;
EOF
CLEAN_VERSION=$(echo "$VERSION" | cut -d'+' -f1)
cat <<EOF > "$DEB_DIR/DEBIAN/control"
Package: $PKG_NAME
Version: $CLEAN_VERSION
Section: utils
Priority: optional
Architecture: $DEB_ARCH
Maintainer: $MAINTAINER
Description: $APP_NAME
$DESC
EOF
dpkg-deb --build "$DEB_DIR" "$OUT_DIR/${PKG_NAME}-linux-${DEB_ARCH}.deb"
fi
DEB_FILE=$(find "$OUT_DIR" -name "*-linux-${DEB_ARCH}.deb" 2>/dev/null | head -n 1 || true)
TAR_FILE=$(find "$OUT_DIR" -name "*-linux-${DEB_ARCH}.tar.gz" 2>/dev/null | head -n 1 || true)
echo "deb_path=$DEB_FILE" >> "$GITHUB_OUTPUT"
echo "tar_path=$TAR_FILE" >> "$GITHUB_OUTPUT"
if [ "$GEN_META" = "true" ]; then
PRIMARY_FILE="${DEB_FILE:-$TAR_FILE}"
SIZE="0"
FILENAME="${PKG_NAME}-linux-${DEB_ARCH}.deb"
if [ -n "$PRIMARY_FILE" ] && [ -f "$PRIMARY_FILE" ]; then
SIZE=$(stat -c%s "$PRIMARY_FILE" 2>/dev/null || stat -f%z "$PRIMARY_FILE" 2>/dev/null || echo "0")
FILENAME=$(basename "$PRIMARY_FILE")
fi
META_FILE="linux_${DEB_ARCH}_metadata.json"
cat <<EOF > "$META_FILE"
{
"version": "$VERSION",
"platform": "linux",
"arch": "$DEB_ARCH",
"filename": "$FILENAME",
"size": $SIZE,
"signature": "",
"signatureType": "none"
}
EOF
cat "$META_FILE"
echo "metadata_path=$META_FILE" >> "$GITHUB_OUTPUT"
fi
+63
View File
@@ -0,0 +1,63 @@
# Flutter Build macOS
Composite action to build, sign (Developer ID & Sparkle), package (`.dmg` or `.zip`), notarize, and generate auto-updater metadata for macOS Flutter desktop applications.
## Inputs
| Name | Description | Required | Default |
| :--- | :--- | :--- | :--- |
| `working_directory` | Path to the Flutter app package directory. | No | `.` |
| `app_name` | Display name of the `.app` bundle (auto-detected if empty). | No | `''` |
| `build_args` | Extra arguments passed to `flutter build macos`. | No | `''` |
| `disable_spm` | Disable Flutter Swift Package Manager before building. | No | `true` |
| `run_pod_install` | Run `pod install` inside `working_directory/macos` before building. | No | `false` |
| `package_type` | Packaging format to produce: `dmg`, `zip`, or `both`. | No | `dmg` |
| `dmg_filename` | Output DMG filename. | No | `''` |
| `zip_filename` | Output ZIP path/filename when `package_type` is `zip` or `both`. | No | `''` |
| `entitlements_path` | Path to entitlements plist relative to `working_directory`. | No | `macos/Runner/Release.entitlements` |
| `signing_identity` | Fallback Developer ID Application signing identity. | No | `''` |
| `certificate_base64` | Base64-encoded Apple `.p12` certificate. | No | `''` |
| `certificate_password` | Password for the Apple `.p12` certificate. | No | `''` |
| `certificate_file` | Fallback path to local `.p12` certificate file in repository. | No | `signing/macos/certificate.p12` |
| `metadata_txt_file` | Fallback path to `metadata.txt` containing p12/notarization credentials. | No | `signing/macos/metadata.txt` |
| `provisioning_profile_base64` | Optional Base64-encoded `.provisionprofile`. | No | `''` |
| `notarize_apple_id` | Apple ID email for DMG notarization. | No | `''` |
| `notarize_password` | Apple ID App-Specific Password for DMG notarization. | No | `''` |
| `notarize_team_id` | Apple Developer 10-character Team ID for DMG notarization. | No | `''` |
| `sparkle_private_key` | Sparkle EdDSA private key content for signing DMG updates. | No | `''` |
| `sparkle_key_file` | Fallback path to Sparkle EdDSA private key file. | No | `signing/sparkle/sparkle_key.pem` |
| `generate_metadata_json` | Whether to generate `macos_metadata.json` and `macos_signature.txt`. | No | `false` |
| `asc_key_id` | Optional App Store Connect API Key ID for `.pkg` upload. | No | `''` |
| `asc_issuer_id` | Optional App Store Connect API Issuer ID for `.pkg` upload. | No | `''` |
| `asc_private_key` | Optional App Store Connect API Private Key (`.p8`) for `.pkg` upload. | No | `''` |
## Outputs
| Name | Description |
| :--- | :--- |
| `version` | Resolved package version from `pubspec.yaml`. |
| `dmg_path` | Path to generated DMG installer. |
| `zip_path` | Path to generated ZIP archive. |
| `signature` | Sparkle EdDSA signature if generated. |
| `metadata_path` | Path to generated `macos_metadata.json`. |
## Example Usage
```yaml
- name: Build & Package macOS Release
uses: rodydavis/shared-actions/flutter-build-macos@main
with:
working_directory: packages/sfw_desktop
app_name: Slides for Worship
package_type: dmg
dmg_filename: slides-for-worship-macos-universal.dmg
run_pod_install: 'true'
build_args: --dart-define=API_BASE_URL=https://slidesforworship.app
certificate_base64: ${{ secrets.MACOS_CERTIFICATE }}
certificate_password: ${{ secrets.MACOS_CERTIFICATE_PWD }}
notarize_apple_id: ${{ secrets.AC_USERNAME }}
notarize_password: ${{ secrets.AC_PASSWORD }}
notarize_team_id: ${{ secrets.AC_TEAM_ID }}
sparkle_private_key: ${{ secrets.SPARKLE_PRIVATE_KEY }}
generate_metadata_json: 'true'
```
+463
View File
@@ -0,0 +1,463 @@
name: 'Flutter Build macOS'
description: 'Builds, signs (Developer ID & Sparkle), packages (.dmg / .zip), notarizes, and generates release metadata for macOS Flutter desktop apps.'
inputs:
working_directory:
description: 'Path to the Flutter app package directory (e.g., packages/sfw_desktop)'
required: false
default: '.'
app_name:
description: 'Display name of the .app bundle (e.g., "Slides for Worship" or auto-detected if empty)'
required: false
default: ''
build_args:
description: 'Extra arguments passed to flutter build macos (e.g., --dart-define=...)'
required: false
default: ''
disable_spm:
description: 'Disable Flutter Swift Package Manager before building'
required: false
default: 'true'
run_pod_install:
description: 'Run pod install inside working_directory/macos before building'
required: false
default: 'false'
package_type:
description: 'Packaging format to produce: dmg, zip, or both'
required: false
default: 'dmg'
dmg_filename:
description: 'Output DMG filename (e.g., slides-for-worship-macos-universal.dmg)'
required: false
default: ''
zip_filename:
description: 'Output ZIP path/filename when package_type is zip or both'
required: false
default: ''
entitlements_path:
description: 'Path to entitlements plist relative to working_directory'
required: false
default: 'macos/Runner/Release.entitlements'
signing_identity:
description: 'Fallback Developer ID Application signing identity if not auto-detected'
required: false
default: ''
certificate_base64:
description: 'Base64-encoded Apple .p12 certificate'
required: false
default: ''
certificate_password:
description: 'Password for the Apple .p12 certificate'
required: false
default: ''
certificate_file:
description: 'Fallback path to local .p12 certificate file in repository'
required: false
default: 'signing/macos/certificate.p12'
metadata_txt_file:
description: 'Fallback path to metadata.txt containing p12/notarization credentials'
required: false
default: 'signing/macos/metadata.txt'
provisioning_profile_base64:
description: 'Optional Base64-encoded .provisionprofile for Mac App Store / provisioning'
required: false
default: ''
notarize_apple_id:
description: 'Apple ID email for DMG notarization'
required: false
default: ''
notarize_password:
description: 'Apple ID App-Specific Password for DMG notarization'
required: false
default: ''
notarize_team_id:
description: 'Apple Developer 10-character Team ID for DMG notarization'
required: false
default: ''
sparkle_private_key:
description: 'Sparkle EdDSA private key content for signing DMG updates'
required: false
default: ''
sparkle_key_file:
description: 'Fallback path to Sparkle EdDSA private key file'
required: false
default: 'signing/sparkle/sparkle_key.pem'
generate_metadata_json:
description: 'Whether to generate macos_metadata.json and macos_signature.txt'
required: false
default: 'false'
asc_key_id:
description: 'Optional App Store Connect API Key ID for .pkg upload'
required: false
default: ''
asc_issuer_id:
description: 'Optional App Store Connect API Issuer ID for .pkg upload'
required: false
default: ''
asc_private_key:
description: 'Optional App Store Connect API Private Key (.p8) for .pkg upload'
required: false
default: ''
outputs:
version:
description: 'Resolved package version from pubspec.yaml'
value: ${{ steps.build.outputs.version }}
dmg_path:
description: 'Path to generated DMG installer'
value: ${{ steps.package.outputs.dmg_path }}
zip_path:
description: 'Path to generated ZIP archive'
value: ${{ steps.package.outputs.zip_path }}
signature:
description: 'Sparkle EdDSA signature if generated'
value: ${{ steps.sparkle.outputs.signature }}
metadata_path:
description: 'Path to generated macos_metadata.json'
value: ${{ steps.metadata.outputs.metadata_path }}
runs:
using: 'composite'
steps:
- name: Clean up previous temporary keychains
shell: bash
run: |
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" || true
security default-keychain -s "$HOME/Library/Keychains/login.keychain-db" || true
security delete-keychain "$HOME/Library/Keychains/signing_macos_temp.keychain-db" 2>/dev/null || true
rm -f "$HOME/Library/Keychains/signing_macos_temp.keychain-db" || true
- name: Import Code-Signing Certificates
shell: bash
env:
CERT_B64: ${{ inputs.certificate_base64 }}
CERT_PWD: ${{ inputs.certificate_password }}
CERT_FILE: ${{ inputs.certificate_file }}
METADATA_FILE: ${{ inputs.metadata_txt_file }}
PROFILE_B64: ${{ inputs.provisioning_profile_base64 }}
run: |
CERT_P12="$CERT_FILE"
if [ -z "$CERT_PWD" ] && [ -n "$METADATA_FILE" ] && [ -f "$METADATA_FILE" ]; then
CERT_PWD=$(grep -A 1 "p12 password:" "$METADATA_FILE" | tail -n 1 | tr -d '\r\n' || true)
fi
if [ -n "$CERT_B64" ]; then
echo "$CERT_B64" | base64 --decode > /tmp/macos_certificate.p12
CERT_P12="/tmp/macos_certificate.p12"
fi
if [ -f "$CERT_P12" ] && [ -n "$CERT_PWD" ]; then
echo "Importing code-signing certificate from $CERT_P12..."
KEYCHAIN="$HOME/Library/Keychains/signing_macos_temp.keychain-db"
KEYCHAIN_PWD="temp_password_123"
security create-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" || true
security unlock-keychain -p "$KEYCHAIN_PWD" "$KEYCHAIN" || true
security set-keychain-settings -lut 21600 "$KEYCHAIN" || true
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" "$KEYCHAIN" || true
security default-keychain -s "$HOME/Library/Keychains/login.keychain-db" || true
security import "$CERT_P12" -k "$KEYCHAIN" -P "$CERT_PWD" -T /usr/bin/codesign -T /usr/bin/security -T /usr/bin/productbuild -A || true
security set-key-partition-list -S apple-tool:,apple:,codesign: -s -k "$KEYCHAIN_PWD" "$KEYCHAIN" || true
echo "Certificate imported successfully. Available identities:"
security find-identity -v -p codesigning || true
else
echo "Notice: No certificate or password found; building unsigned or ad-hoc signed macOS bundle."
fi
if [ -n "$PROFILE_B64" ]; then
mkdir -p "$HOME/Library/MobileDevice/Provisioning Profiles" "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles"
echo "$PROFILE_B64" | base64 --decode > /tmp/macos_profile.provisionprofile
PROFILE_UUID=$(security cms -D -i /tmp/macos_profile.provisionprofile 2>/dev/null | plutil -extract UUID raw - 2>/dev/null || echo "macos_profile")
cp /tmp/macos_profile.provisionprofile "$HOME/Library/MobileDevice/Provisioning Profiles/${PROFILE_UUID}.provisionprofile"
cp /tmp/macos_profile.provisionprofile "$HOME/Library/Developer/Xcode/UserData/Provisioning Profiles/${PROFILE_UUID}.provisionprofile"
fi
- name: Install CocoaPods & Build macOS Release
id: build
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
RUN_PODS: ${{ inputs.run_pod_install }}
DISABLE_SPM: ${{ inputs.disable_spm }}
BUILD_ARGS: ${{ inputs.build_args }}
run: |
export PATH="/usr/bin:/bin:/usr/sbin:/sbin:/opt/homebrew/bin:/opt/homebrew/sbin:/usr/local/bin:$PATH"
ROOT_DIR="$(pwd)"
VERSION=$(grep '^version: ' "$WORK_DIR/pubspec.yaml" | head -n 1 | cut -d ' ' -f 2 | tr -d '\r')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
if [ "$RUN_PODS" = "true" ] && [ -d "$WORK_DIR/macos" ]; then
cd "$WORK_DIR/macos"
pod install || true
cd "$ROOT_DIR"
fi
cd "$WORK_DIR"
flutter config --enable-macos-desktop
if [ "$DISABLE_SPM" = "true" ]; then
flutter config --no-enable-swift-package-manager
fi
# shellcheck disable=SC2086
flutter build macos --release $BUILD_ARGS
cd "$ROOT_DIR"
- name: Sign App Bundle (Developer ID & Sparkle)
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
APP_NAME: ${{ inputs.app_name }}
ENTITLEMENTS_REL: ${{ inputs.entitlements_path }}
FALLBACK_IDENTITY: ${{ inputs.signing_identity }}
run: |
if [ -n "$APP_NAME" ]; then
APP_PATH="$WORK_DIR/build/macos/Build/Products/Release/${APP_NAME}.app"
else
APP_PATH=$(find "$WORK_DIR/build/macos/Build/Products/Release" -maxdepth 1 -name "*.app" | head -n 1)
fi
ENTITLEMENTS="$WORK_DIR/$ENTITLEMENTS_REL"
IDENTITY=$(security find-identity -v -p codesigning 2>/dev/null | grep "Developer ID Application" | head -1 | grep -oE '"[^"]+"' | sed 's/"//g' || true)
if [ -z "$IDENTITY" ] && [ -n "$FALLBACK_IDENTITY" ]; then
IDENTITY="$FALLBACK_IDENTITY"
fi
if [ -n "$APP_PATH" ] && [ -d "$APP_PATH" ] && [ -n "$IDENTITY" ]; then
echo "Signing Sparkle components, frameworks, and App Bundle at $APP_PATH with identity: $IDENTITY"
SPARKLE_FW="$APP_PATH/Contents/Frameworks/Sparkle.framework/Versions/B"
if [ -d "$SPARKLE_FW" ]; then
echo "Signing Sparkle internal components..."
[ -d "$SPARKLE_FW/XPCServices/Downloader.xpc" ] && codesign --force --options runtime --timestamp --sign "$IDENTITY" "$SPARKLE_FW/XPCServices/Downloader.xpc" || true
[ -d "$SPARKLE_FW/XPCServices/Installer.xpc" ] && codesign --force --options runtime --timestamp --sign "$IDENTITY" "$SPARKLE_FW/XPCServices/Installer.xpc" || true
[ -f "$SPARKLE_FW/Autoupdate" ] && codesign --force --options runtime --timestamp --sign "$IDENTITY" "$SPARKLE_FW/Autoupdate" || true
[ -d "$SPARKLE_FW/Updater.app" ] && codesign --force --options runtime --timestamp --sign "$IDENTITY" "$SPARKLE_FW/Updater.app" || true
codesign --force --options runtime --timestamp --sign "$IDENTITY" "$APP_PATH/Contents/Frameworks/Sparkle.framework" || true
fi
if [ -d "$APP_PATH/Contents/Frameworks" ]; then
find "$APP_PATH/Contents/Frameworks" -mindepth 1 -maxdepth 1 2>/dev/null | while read -r FW; do
if [ "$FW" != "$APP_PATH/Contents/Frameworks/Sparkle.framework" ]; then
codesign --force --options runtime --timestamp --sign "$IDENTITY" "$FW" || true
fi
done
fi
if [ -f "$ENTITLEMENTS" ]; then
codesign --force --options runtime --timestamp --entitlements "$ENTITLEMENTS" --sign "$IDENTITY" "$APP_PATH" || true
else
codesign --force --options runtime --timestamp --sign "$IDENTITY" "$APP_PATH" || true
fi
codesign -dv --verbose=4 "$APP_PATH" || true
fi
- name: Package macOS Installer (DMG / ZIP)
id: package
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
APP_NAME: ${{ inputs.app_name }}
PACKAGE_TYPE: ${{ inputs.package_type }}
DMG_FILENAME: ${{ inputs.dmg_filename }}
ZIP_FILENAME: ${{ inputs.zip_filename }}
run: |
if [ -n "$APP_NAME" ]; then
APP_PATH="$WORK_DIR/build/macos/Build/Products/Release/${APP_NAME}.app"
else
APP_PATH=$(find "$WORK_DIR/build/macos/Build/Products/Release" -maxdepth 1 -name "*.app" | head -n 1)
fi
if [ -z "$APP_PATH" ] || [ ! -d "$APP_PATH" ]; then
echo "::error::macOS .app bundle not found in $WORK_DIR/build/macos/Build/Products/Release"
exit 1
fi
BUNDLE_BASENAME=$(basename "$APP_PATH")
VOL_NAME="${APP_NAME:-${BUNDLE_BASENAME%.app}}"
if [ "$PACKAGE_TYPE" = "zip" ] || [ "$PACKAGE_TYPE" = "both" ]; then
ZIP_OUT="${ZIP_FILENAME:-$WORK_DIR/build/macos/release-archive/${VOL_NAME// /-}-macOS.zip}"
mkdir -p "$(dirname "$ZIP_OUT")"
ditto -c -k --sequesterRsrc --keepParent "$APP_PATH" "$ZIP_OUT"
echo "Packaged macOS ZIP archive: $ZIP_OUT"
echo "zip_path=$ZIP_OUT" >> "$GITHUB_OUTPUT"
fi
if [ "$PACKAGE_TYPE" = "dmg" ] || [ "$PACKAGE_TYPE" = "both" ]; then
DMG_OUT="${DMG_FILENAME:-${VOL_NAME// /-}-macos-universal.dmg}"
if ! command -v create-dmg >/dev/null 2>&1; then
brew install create-dmg || true
fi
echo "Detaching any leftover disk image mounts..."
for dev in $(hdiutil info 2>/dev/null | awk -v vol="$VOL_NAME" '$0 ~ "/Volumes/.*" vol "|/Volumes/.*Presentation-Creator" {print $1}'); do
[ -n "$dev" ] && hdiutil detach "$dev" -force || true
done
rm -f "$DMG_OUT"
DMG_STAGING="dmg_staging"
rm -rf "$DMG_STAGING"
mkdir -p "$DMG_STAGING"
cp -R "$APP_PATH" "$DMG_STAGING/"
create-dmg \
--volname "$VOL_NAME" \
--window-pos 200 120 \
--window-size 800 400 \
--icon-size 100 \
--icon "$BUNDLE_BASENAME" 200 190 \
--hide-extension "$BUNDLE_BASENAME" \
--app-drop-link 600 185 \
--no-internet-enable \
--sandbox-safe \
--skip-jenkins \
--overwrite \
"$DMG_OUT" \
"$DMG_STAGING" || true
if [ ! -f "$DMG_OUT" ]; then
echo "create-dmg did not generate DMG, running hdiutil fallback with Applications link..."
ln -s /Applications "$DMG_STAGING/Applications" 2>/dev/null || true
hdiutil create -volname "$VOL_NAME" -srcfolder "$DMG_STAGING" -ov -format UDZO "$DMG_OUT"
fi
rm -rf "$DMG_STAGING"
for dev in $(hdiutil info 2>/dev/null | awk -v vol="$VOL_NAME" '$0 ~ "/Volumes/.*" vol "|/Volumes/.*Presentation-Creator" {print $1}'); do
[ -n "$dev" ] && hdiutil detach "$dev" -force || true
done
test -f "$DMG_OUT"
echo "dmg_path=$DMG_OUT" >> "$GITHUB_OUTPUT"
IDENTITY=$(security find-identity -v -p codesigning 2>/dev/null | grep "Developer ID Application" | head -1 | grep -oE '"[^"]+"' | sed 's/"//g' || true)
if [ -n "$IDENTITY" ] && [ -f "$DMG_OUT" ]; then
echo "Signing DMG with identity: $IDENTITY"
codesign --force --timestamp --sign "$IDENTITY" "$DMG_OUT" || true
fi
fi
- name: Notarize & Staple DMG
if: inputs.package_type == 'dmg' || inputs.package_type == 'both'
shell: bash
env:
DMG_FILE: ${{ steps.package.outputs.dmg_path }}
AC_USER: ${{ inputs.notarize_apple_id }}
AC_PASS: ${{ inputs.notarize_password }}
AC_TEAM: ${{ inputs.notarize_team_id }}
METADATA_FILE: ${{ inputs.metadata_txt_file }}
run: |
if [ -z "$AC_USER" ] && [ -n "$METADATA_FILE" ] && [ -f "$METADATA_FILE" ]; then
AC_USER=$(grep -A 1 "AC username:" "$METADATA_FILE" | tail -n 1 | tr -d '\r\n' || true)
AC_PASS=$(grep -A 1 "AC password:" "$METADATA_FILE" | tail -n 1 | tr -d '\r\n' || true)
AC_TEAM=$(grep -A 1 "Team ID:" "$METADATA_FILE" | tail -n 1 | tr -d '\r\n' || true)
fi
if [ -n "$AC_USER" ] && [ -n "$AC_PASS" ] && [ -n "$AC_TEAM" ] && [ -n "$DMG_FILE" ] && [ -f "$DMG_FILE" ]; then
echo "Submitting $DMG_FILE to Apple Notary Service..."
xcrun notarytool submit "$DMG_FILE" \
--apple-id "$AC_USER" \
--password "$AC_PASS" \
--team-id "$AC_TEAM" \
--wait || true
echo "Stapling notarization ticket..."
for i in {1..5}; do
xcrun stapler staple "$DMG_FILE" && break || sleep 10
done || true
else
echo "Notarization credentials not configured; skipping notarization."
fi
- name: Sign DMG for Sparkle
id: sparkle
if: inputs.package_type == 'dmg' || inputs.package_type == 'both'
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
DMG_FILE: ${{ steps.package.outputs.dmg_path }}
SPARKLE_KEY: ${{ inputs.sparkle_private_key }}
SPARKLE_KEY_FILE: ${{ inputs.sparkle_key_file }}
run: |
ROOT_DIR="$(pwd)"
touch macos_signature.txt
if [ -z "$SPARKLE_KEY" ] && [ -n "$SPARKLE_KEY_FILE" ] && [ -f "$SPARKLE_KEY_FILE" ]; then
SPARKLE_KEY=$(cat "$SPARKLE_KEY_FILE")
fi
if [ -n "$SPARKLE_KEY" ] && [ -n "$DMG_FILE" ] && [ -f "$DMG_FILE" ]; then
echo "$SPARKLE_KEY" > "$ROOT_DIR/sparkle_key.pem"
echo "Signing $DMG_FILE for Sparkle..."
cd "$WORK_DIR"
OUTPUT=$(dart run auto_updater:sign_update "$ROOT_DIR/$DMG_FILE" --ed-key-file "$ROOT_DIR/sparkle_key.pem" 2>&1 || true)
echo "$OUTPUT"
cd "$ROOT_DIR"
SIG=$(echo "$OUTPUT" | grep -o 'Signature="[^"]*"' | head -n 1 | cut -d'"' -f2 || true)
if [ -n "$SIG" ]; then
echo "$SIG" > macos_signature.txt
echo "signature=$SIG" >> "$GITHUB_OUTPUT"
fi
rm -f "$ROOT_DIR/sparkle_key.pem"
fi
- name: Generate macOS Release Metadata
id: metadata
if: inputs.generate_metadata_json == 'true'
shell: bash
env:
VERSION: ${{ steps.build.outputs.version }}
DMG_FILE: ${{ steps.package.outputs.dmg_path }}
run: |
SIG=$(cat macos_signature.txt 2>/dev/null || true)
SIZE=$(stat -c%s "$DMG_FILE" 2>/dev/null || stat -f%z "$DMG_FILE" 2>/dev/null || echo "0")
FILENAME=$(basename "$DMG_FILE")
cat <<EOF > macos_metadata.json
{
"version": "$VERSION",
"platform": "macos",
"arch": "universal",
"filename": "$FILENAME",
"size": $SIZE,
"signature": "$SIG",
"signatureType": "edSignature"
}
EOF
cat macos_metadata.json
echo "metadata_path=macos_metadata.json" >> "$GITHUB_OUTPUT"
- name: Upload macOS Package to App Store Connect (Optional)
if: inputs.asc_key_id != '' && inputs.asc_issuer_id != '' && inputs.asc_private_key != ''
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
ASC_KEY_ID: ${{ inputs.asc_key_id }}
ASC_ISSUER_ID: ${{ inputs.asc_issuer_id }}
ASC_KEY_CONTENT: ${{ inputs.asc_private_key }}
run: |
PKG_FILE=$(ls "$WORK_DIR"/build/macos/*.pkg 2>/dev/null | head -n 1 || true)
if [ -z "$PKG_FILE" ]; then
echo "Skipping Mac App Store pkg upload (no signed .pkg generated)."
exit 0
fi
mkdir -p "$HOME/.appstoreconnect/private_keys"
echo "$ASC_KEY_CONTENT" > "$HOME/.appstoreconnect/private_keys/AuthKey_${ASC_KEY_ID}.p8"
xcrun altool --upload-app \
-f "$PKG_FILE" \
--type osx \
--apiKey "$ASC_KEY_ID" \
--apiIssuer "$ASC_ISSUER_ID"
- name: Eject DMG Mounts & Clean Up Keychain
if: always()
shell: bash
env:
APP_NAME: ${{ inputs.app_name }}
run: |
VOL_NAME="${APP_NAME:-Slides for Worship}"
for dev in $(hdiutil info 2>/dev/null | awk -v vol="$VOL_NAME" '$0 ~ "/Volumes/.*" vol "|/Volumes/.*Presentation-Creator" {print $1}'); do
[ -n "$dev" ] && hdiutil detach "$dev" -force || true
done
security list-keychains -d user -s "$HOME/Library/Keychains/login.keychain-db" || true
security default-keychain -s "$HOME/Library/Keychains/login.keychain-db" || true
security delete-keychain "$HOME/Library/Keychains/signing_macos_temp.keychain-db" 2>/dev/null || true
rm -f "$HOME/Library/Keychains/signing_macos_temp.keychain-db" /tmp/macos_certificate.p12 /tmp/macos_profile.provisionprofile || true
+48
View File
@@ -0,0 +1,48 @@
# Flutter Build Windows
Composite action to build Windows Flutter desktop applications, compile Inno Setup (`.exe`) and MSIX (`.msix`) installers, sign updates with WinSparkle DSA keys, and generate release metadata.
## Inputs
| Name | Description | Required | Default |
| :--- | :--- | :--- | :--- |
| `working_directory` | Path to the Flutter app package directory. | No | `.` |
| `arch` | Target Windows architecture (`x64` or `arm64`). | No | `x64` |
| `build_args` | Extra arguments passed to `flutter build windows`. | No | `''` |
| `build_inno_setup` | Whether to compile an Inno Setup installer (`.exe`). | No | `true` |
| `inno_setup_script` | Path to Inno Setup `.iss` script relative to `working_directory`. | No | `windows/installer/inno_setup.iss` |
| `output_dir` | Output directory for compiled Inno Setup installer. | No | `dist` |
| `exe_filename` | Expected installer filename for metadata JSON. | No | `''` |
| `build_msix` | Whether to build an MSIX package using `msix:create`. | No | `true` |
| `msix_output_name` | Output base name for the MSIX package. | No | `''` |
| `pfx_base64` | Base64-encoded Windows Authenticode `.pfx` certificate. | No | `''` |
| `pfx_password` | Password for the Windows `.pfx` certificate. | No | `''` |
| `sparkle_dsa_private_key` | WinSparkle DSA private key content (PEM) for signing installer. | No | `''` |
| `sparkle_dsa_key_file` | Fallback path to WinSparkle DSA private key file in repository. | No | `signing/sparkle/dsa_priv.pem` |
| `generate_metadata_json` | Whether to generate `windows_<arch>_metadata.json` and `windows_signature.txt`. | No | `false` |
## Outputs
| Name | Description |
| :--- | :--- |
| `version` | Resolved package version from `pubspec.yaml`. |
| `exe_path` | Path to compiled Inno Setup `.exe` installer. |
| `msix_path` | Path to generated `.msix` installer. |
| `signature` | WinSparkle DSA signature if generated. |
| `metadata_path` | Path to generated `windows_<arch>_metadata.json`. |
## Example Usage
```yaml
- name: Build & Package Windows Release
uses: rodydavis/shared-actions/flutter-build-windows@main
with:
working_directory: packages/sfw_desktop
arch: ${{ matrix.arch }}
build_args: --dart-define=API_BASE_URL=https://slidesforworship.app
msix_output_name: slides_for_worship_${{ matrix.arch }}
pfx_base64: ${{ secrets.WINDOWS_PFX_BASE64 }}
pfx_password: ${{ secrets.WINDOWS_PFX_PASSWORD }}
sparkle_dsa_private_key: ${{ secrets.SPARKLE_DSA_PRIVATE_KEY }}
generate_metadata_json: 'true'
```
+260
View File
@@ -0,0 +1,260 @@
name: 'Flutter Build Windows'
description: 'Builds Windows Flutter desktop apps, compiles Inno Setup (.exe) and MSIX (.msix) installers, signs updates for WinSparkle (DSA), and generates release metadata.'
inputs:
working_directory:
description: 'Path to the Flutter app package directory (e.g., packages/sfw_desktop)'
required: false
default: '.'
arch:
description: 'Target Windows architecture (x64 or arm64)'
required: false
default: 'x64'
build_args:
description: 'Extra arguments passed to flutter build windows (e.g., --dart-define=...)'
required: false
default: ''
build_inno_setup:
description: 'Whether to compile an Inno Setup installer (.exe)'
required: false
default: 'true'
inno_setup_script:
description: 'Path to Inno Setup .iss script relative to working_directory'
required: false
default: 'windows/installer/inno_setup.iss'
output_dir:
description: 'Output directory for compiled Inno Setup installer'
required: false
default: 'dist'
exe_filename:
description: 'Expected installer filename for metadata JSON (e.g., slides-for-worship-windows-x64-setup.exe)'
required: false
default: ''
build_msix:
description: 'Whether to build an MSIX package using msix:create'
required: false
default: 'true'
msix_output_name:
description: 'Output base name for the MSIX package (e.g., slides_for_worship_x64)'
required: false
default: ''
pfx_base64:
description: 'Base64-encoded Windows Authenticode .pfx certificate'
required: false
default: ''
pfx_password:
description: 'Password for the Windows .pfx certificate'
required: false
default: ''
sparkle_dsa_private_key:
description: 'WinSparkle DSA private key content (PEM) for signing installer'
required: false
default: ''
sparkle_dsa_key_file:
description: 'Fallback path to WinSparkle DSA private key file in repository'
required: false
default: 'signing/sparkle/dsa_priv.pem'
generate_metadata_json:
description: 'Whether to generate windows_<arch>_metadata.json and windows_signature.txt'
required: false
default: 'false'
outputs:
version:
description: 'Resolved package version from pubspec.yaml'
value: ${{ steps.build.outputs.version }}
exe_path:
description: 'Path to compiled Inno Setup .exe installer'
value: ${{ steps.package_meta.outputs.exe_path }}
msix_path:
description: 'Path to generated .msix installer'
value: ${{ steps.package_meta.outputs.msix_path }}
signature:
description: 'WinSparkle DSA signature if generated'
value: ${{ steps.package_meta.outputs.signature }}
metadata_path:
description: 'Path to generated windows_<arch>_metadata.json'
value: ${{ steps.package_meta.outputs.metadata_path }}
runs:
using: 'composite'
steps:
- name: Configure Windows Tooling PATH
shell: powershell
run: |
$toolPaths = @(
"C:\Program Files\Git\bin",
"C:\Program Files\Git\usr\bin",
"C:\Git\bin",
"C:\Git\usr\bin",
"C:\Users\rodyd\AppData\Local\Programs\Inno Setup 6",
"C:\Program Files (x86)\Inno Setup 6",
"C:\Program Files\Inno Setup 6",
"C:\flutter\bin"
)
foreach ($p in $toolPaths) {
if (Test-Path $p) {
$env:PATH = "$p;$env:PATH"
if (Test-Path env:GITHUB_PATH) {
[System.IO.File]::AppendAllText($env:GITHUB_PATH, "$p`r`n")
}
}
}
- name: Decode Windows PFX Certificate
if: inputs.pfx_base64 != ''
shell: powershell
env:
WINDOWS_PFX_BASE64: ${{ inputs.pfx_base64 }}
run: |
if ($env:WINDOWS_PFX_BASE64) {
$pfx_cert_byte = [System.Convert]::FromBase64String($env:WINDOWS_PFX_BASE64)
[IO.File]::WriteAllBytes("cert.pfx", $pfx_cert_byte)
Write-Output "Decoded cert.pfx"
}
- name: Build Windows Release
id: build
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
ARCH: ${{ inputs.arch }}
BUILD_ARGS: ${{ inputs.build_args }}
run: |
ROOT_DIR="$(pwd)"
VERSION=$(grep '^version: ' "$WORK_DIR/pubspec.yaml" | head -n 1 | cut -d ' ' -f 2 | tr -d '\r')
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
cd "$WORK_DIR"
if [ "$ARCH" = "arm64" ]; then
# shellcheck disable=SC2086
flutter build windows --release --target-platform=windows-arm64 $BUILD_ARGS
else
# shellcheck disable=SC2086
flutter build windows --release $BUILD_ARGS
fi
cd "$ROOT_DIR"
- name: Compile Inno Setup Installer
if: inputs.build_inno_setup == 'true'
shell: powershell
env:
WORK_DIR: ${{ inputs.working_directory }}
ARCH: ${{ inputs.arch }}
ISS_REL: ${{ inputs.inno_setup_script }}
OUT_DIR: ${{ inputs.output_dir }}
VERSION: ${{ steps.build.outputs.version }}
run: |
New-Item -ItemType Directory -Force -Path $env:OUT_DIR | Out-Null
$sourceRel = Join-Path $env:WORK_DIR "build\windows\$($env:ARCH)\runner\Release"
if (-not (Test-Path $sourceRel)) {
$sourceRel = Join-Path $env:WORK_DIR "build\windows\x64\runner\Release"
}
$sourceDir = (Resolve-Path $sourceRel).Path
$outputDir = (Resolve-Path $env:OUT_DIR).Path
$issScript = Join-Path $env:WORK_DIR $env:ISS_REL
if (-not (Test-Path $issScript)) {
Write-Warning "Inno Setup script not found at $issScript; skipping Inno Setup compilation."
exit 0
}
$iscc = (Get-Command iscc.exe -ErrorAction SilentlyContinue).Source
if (-not $iscc -and (Test-Path "C:\Users\rodyd\AppData\Local\Programs\Inno Setup 6\ISCC.exe")) {
$iscc = "C:\Users\rodyd\AppData\Local\Programs\Inno Setup 6\ISCC.exe"
}
if ($iscc) {
& $iscc /DMyAppVersion="$($env:VERSION)" /DAppArch="$($env:ARCH)" /DSourceDir="$sourceDir" /DOutputDir="$outputDir" "$issScript"
} else {
Write-Warning "ISCC.exe not found on runner; skipping Inno Setup installer."
}
- name: Create MSIX Package
if: inputs.build_msix == 'true'
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
ARCH: ${{ inputs.arch }}
MSIX_NAME: ${{ inputs.msix_output_name }}
PFX_PASS: ${{ inputs.pfx_password }}
run: |
ROOT_DIR="$(pwd)"
OUT_NAME="${MSIX_NAME:-slides_for_worship_${ARCH}}"
cd "$WORK_DIR"
if [ -f "$ROOT_DIR/cert.pfx" ]; then
flutter pub run msix:create --certificate-path "$ROOT_DIR/cert.pfx" --certificate-password "$PFX_PASS" --install-certificate false --output-name "$OUT_NAME"
else
flutter pub run msix:create --install-certificate false --output-name "$OUT_NAME"
fi
cd "$ROOT_DIR"
- name: Sign for WinSparkle & Generate Metadata
id: package_meta
shell: bash
env:
WORK_DIR: ${{ inputs.working_directory }}
ARCH: ${{ inputs.arch }}
OUT_DIR: ${{ inputs.output_dir }}
VERSION: ${{ steps.build.outputs.version }}
EXE_FILENAME_INPUT: ${{ inputs.exe_filename }}
SPARKLE_DSA_KEY: ${{ inputs.sparkle_dsa_private_key }}
SPARKLE_DSA_FILE: ${{ inputs.sparkle_dsa_key_file }}
GEN_META: ${{ inputs.generate_metadata_json }}
run: |
EXE_FILE=$(find "$OUT_DIR" -name "*.exe" 2>/dev/null | head -n 1 || true)
MSIX_FILE=$(find "$WORK_DIR/build/windows" -name "*.msix" 2>/dev/null | head -n 1 || true)
echo "exe_path=$EXE_FILE" >> "$GITHUB_OUTPUT"
echo "msix_path=$MSIX_FILE" >> "$GITHUB_OUTPUT"
SETUP_FILE="${EXE_FILE:-$MSIX_FILE}"
if [ -z "$SPARKLE_DSA_KEY" ] && [ -n "$SPARKLE_DSA_FILE" ] && [ -f "$SPARKLE_DSA_FILE" ]; then
SPARKLE_DSA_KEY=$(cat "$SPARKLE_DSA_FILE")
fi
WIN_SIG=""
if [ -n "$SPARKLE_DSA_KEY" ] && [ -n "$SETUP_FILE" ] && [ -f "$SETUP_FILE" ]; then
echo "$SPARKLE_DSA_KEY" > dsa_priv.pem
echo "Signing $SETUP_FILE with DSA key..."
WIN_SIG=$(openssl dgst -sha1 -binary < "$SETUP_FILE" | openssl dgst -sha1 -sign dsa_priv.pem | openssl enc -base64 | tr -d '\n\r' || true)
if [ -n "$WIN_SIG" ]; then
echo "$WIN_SIG" > windows_signature.txt
echo "signature=$WIN_SIG" >> "$GITHUB_OUTPUT"
fi
rm -f dsa_priv.pem
fi
if [ "$GEN_META" = "true" ]; then
SIZE="0"
if [ -n "$EXE_FILE" ] && [ -f "$EXE_FILE" ]; then
SIZE=$(stat -c%s "$EXE_FILE" 2>/dev/null || stat -f%z "$EXE_FILE" 2>/dev/null || echo "0")
fi
if [ -n "$EXE_FILENAME_INPUT" ]; then
FILENAME="$EXE_FILENAME_INPUT"
elif [ -n "$EXE_FILE" ]; then
FILENAME=$(basename "$EXE_FILE")
else
FILENAME="slides-for-worship-windows-${ARCH}-setup.exe"
fi
META_FILE="windows_${ARCH}_metadata.json"
cat <<EOF > "$META_FILE"
{
"version": "$VERSION",
"platform": "windows",
"arch": "$ARCH",
"filename": "$FILENAME",
"size": $SIZE,
"signature": "$WIN_SIG",
"signatureType": "dsaSignature"
}
EOF
cat "$META_FILE"
echo "metadata_path=$META_FILE" >> "$GITHUB_OUTPUT"
fi
- name: Clean up PFX Certificate
if: always()
shell: powershell
run: |
if (Test-Path "cert.pfx") {
Remove-Item -Force "cert.pfx" -ErrorAction SilentlyContinue
}