feat: add reusable Flutter build actions for android, ios, macos, windows, and linux
Update README Actions Table / update-readme (push) Successful in 7s
Update README Actions Table / update-readme (push) Successful in 7s
This commit is contained in:
@@ -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` |
|
||||||
|
|||||||
@@ -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 }}
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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 }}
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
```
|
||||||
@@ -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
|
||||||
@@ -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'
|
||||||
|
```
|
||||||
@@ -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
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user