Skip to content

🔀 Merge pull request #18 from ChenXu233/dev #35

🔀 Merge pull request #18 from ChenXu233/dev

🔀 Merge pull request #18 from ChenXu233/dev #35

Workflow file for this run

name: Flutter Multi-Platform Release
on:
push:
branches:
- main
workflow_dispatch:
env:
FLUTTER_VERSION: "3.38.4"
BUILD_DIR: "build"
ANDROID_KEYSTORE_PATH: "android/app/keystore.jks"
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
jobs:
bump-version:
name: Auto-Generate Version Tag
runs-on: ubuntu-latest
permissions:
contents: write # 允许删除/推送标签
outputs:
new_tag: ${{ steps.tag-generation.outputs.NEW_TAG }}
steps:
- name: Checkout code
uses: actions/checkout@v4
with:
fetch-depth: 0 # 需要完整历史记录检查标签
persist-credentials: true
- name: Setup Git user
run: |
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
- name: Get Flutter project version
id: get-version
run: |
VERSION=$(grep '^version:' pubspec.yaml | awk '{print $2}' | tr -d '\n')
echo "PROJECT_VERSION=$VERSION" >> $GITHUB_OUTPUT
- name: Generate and push Git tag
id: tag-generation
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: |
set -e
NEW_TAG="v${{ steps.get-version.outputs.PROJECT_VERSION }}"
echo "Generating tag: $NEW_TAG"
# Delete local tag if exists
if git rev-parse "$NEW_TAG" >/dev/null 2>&1; then
git tag -d "$NEW_TAG" || true
fi
# Delete remote tag if exists
if git ls-remote --tags origin "$NEW_TAG" | grep -q "$NEW_TAG"; then
git push origin --delete "refs/tags/$NEW_TAG" || true
fi
# Create and push new tag
git tag "$NEW_TAG"
git push origin "$NEW_TAG"
echo "NEW_TAG=$NEW_TAG" >> $GITHUB_OUTPUT
build-ios:
name: Build iOS
runs-on: macos-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Verify Flutter & Dart
run: |
flutter --version
dart --version
- name: Build iOS
run: |
cd .
flutter pub get
flutter build ios --release --no-codesign
cd build/ios/iphoneos
zip -r ios-release.zip Runner.app
- name: Upload iOS Artifacts
uses: actions/upload-artifact@v4
with:
name: ios-artifacts
path: build/ios/iphoneos/ios-release.zip
build-macos:
name: Build macOS
runs-on: macos-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Verify Flutter & Dart
run: |
flutter --version
dart --version
- name: Build macOS
run: |
cd .
flutter pub get
flutter build macos --release
cd build/macos/Build/Products/Release
zip -r macos-release.zip *.app
- name: Upload macOS Artifacts
uses: actions/upload-artifact@v4
with:
name: macos-artifacts
path: build/macos/Build/Products/Release/macos-release.zip
build-android-with-impeller:
name: Build Android (With Impeller)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
JAVA_VERSION: "17"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: ${{ env.JAVA_VERSION }}
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Verify Flutter & Dart
run: |
flutter --version
dart --version
- name: Install Dependencies
run: |
sudo apt update -y
sudo apt install -y ninja-build libgtk-3-dev clang cmake libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio
- name: Configure Android Signing
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
STORE_PASS: ${{ secrets.ANDROID_STORE_PASSWORD }}
KEY_PASS: ${{ secrets.ANDROID_KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
set -e
echo "Decoding keystore..."
if [ ! -f android/app/keystore.jks ] || ! cmp -s <(echo "$KEYSTORE_BASE64") <(base64 -w0 android/app/keystore.jks 2>/dev/null); then
echo "$KEYSTORE_BASE64" | base64 -d > android/app/keystore.jks
chmod 600 android/app/keystore.jks
echo "Keystore decoded and permissions set."
else
echo "Keystore already exists and matches secret."
fi
echo "Generating keystore.properties..."
{
echo "storePassword=$STORE_PASS"
echo "keyPassword=$KEY_PASS"
echo "keyAlias=$KEY_ALIAS"
echo "storeFile=keystore.jks"
} > android/keystore.properties
chmod 600 android/keystore.properties
echo "keystore.properties generated."
- name: Build APK with Impeller
run: |
cd .
flutter pub get
flutter build apk --release --flavor with_impeller
- name: Upload Impeller Artifact
uses: actions/upload-artifact@v4
with:
name: android-with-impeller
path: build/app/outputs/flutter-apk/app-with_impeller-release.apk
build-android-without-impeller:
name: Build Android (Without Impeller)
runs-on: ubuntu-latest
timeout-minutes: 30
env:
JAVA_VERSION: "17"
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Java
uses: actions/setup-java@v4
with:
distribution: "zulu"
java-version: ${{ env.JAVA_VERSION }}
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Verify Flutter & Dart
run: |
flutter --version
dart --version
- name: Install Dependencies
run: |
sudo apt update -y
sudo apt install -y ninja-build libgtk-3-dev clang cmake libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio
- name: Configure Android Signing
env:
KEYSTORE_BASE64: ${{ secrets.ANDROID_KEYSTORE_BASE64 }}
STORE_PASS: ${{ secrets.ANDROID_STORE_PASSWORD }}
KEY_PASS: ${{ secrets.ANDROID_KEY_PASSWORD }}
KEY_ALIAS: ${{ secrets.ANDROID_KEY_ALIAS }}
run: |
set -e
echo "Decoding keystore..."
if [ ! -f android/app/keystore.jks ] || ! cmp -s <(echo "$KEYSTORE_BASE64") <(base64 -w0 android/app/keystore.jks 2>/dev/null); then
echo "$KEYSTORE_BASE64" | base64 -d > android/app/keystore.jks
chmod 600 android/app/keystore.jks
echo "Keystore decoded and permissions set."
else
echo "Keystore already exists and matches secret."
fi
echo "Generating keystore.properties..."
{
echo "storePassword=$STORE_PASS"
echo "keyPassword=$KEY_PASS"
echo "keyAlias=$KEY_ALIAS"
echo "storeFile=keystore.jks"
} > android/keystore.properties
chmod 600 android/keystore.properties
echo "keystore.properties generated."
- name: Build APK without Impeller
run: |
cd .
flutter pub get
flutter build apk --release --flavor without_impeller
- name: Upload Non-Impeller Artifact
uses: actions/upload-artifact@v4
with:
name: android-without-impeller
path: build/app/outputs/flutter-apk/app-without_impeller-release.apk
build-web:
name: Build Web
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Verify Flutter & Dart
run: |
flutter --version
dart --version
- name: Build Web
run: |
cd .
flutter pub get
flutter build web --release
cd build/web
zip -r web-release.zip .
- name: Upload Web Artifacts
uses: actions/upload-artifact@v4
with:
name: web-artifacts
path: build/web/web-release.zip
build-linux:
name: Build Linux
runs-on: ubuntu-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Verify Flutter & Dart
run: |
flutter --version
dart --version
- name: Install Dependencies
run: |
sudo apt update -y
sudo apt install -y ninja-build libgtk-3-dev clang cmake libgstreamer1.0-dev libgstreamer-plugins-base1.0-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-base gstreamer1.0-plugins-good gstreamer1.0-plugins-bad gstreamer1.0-plugins-ugly gstreamer1.0-libav gstreamer1.0-tools gstreamer1.0-x gstreamer1.0-alsa gstreamer1.0-gl gstreamer1.0-gtk3 gstreamer1.0-qt5 gstreamer1.0-pulseaudio # 跨平台编译依赖
- name: Build Linux
run: |
cd .
flutter pub get
flutter build linux --release
chmod +x build/linux/x64/release/bundle/*
- name: Package Linux bundle (zip & tar.gz & AppImage)
run: |
cd build/linux/x64/release
set +e
echo "Creating zip archive..."
zip -r linux-x64-release.zip bundle
set -e
echo "Creating tar.gz archive..."
tar -czf linux-x64-release.tar.gz bundle
echo "Archive sizes:"
ls -lh linux-x64-release.* || true
# Create a minimal AppDir and AppImage
APPDIR=AppDir
rm -rf "$APPDIR"
mkdir -p "$APPDIR/usr/bin"
cp -r bundle/* "$APPDIR/usr/bin/"
# find an executable binary inside AppDir (first match)
EXE=$(find "$APPDIR/usr/bin" -maxdepth 1 -type f -executable | head -n 1 || true)
if [ -z "$EXE" ]; then
# try to find any ELF file
EXE=$(find "$APPDIR/usr/bin" -maxdepth 1 -type f -exec file --brief --mime-type {} \; | awk -F: '/application\/x-executable|application\/x-mach-binary|application\/x-pie-executable/{print NR}' | head -n1 || true)
fi
# Create AppRun wrapper (use printf to avoid YAML parsing issues)
printf '%s\n' '#!/bin/sh' 'HERE="$(dirname "$(readlink -f "$0")")"' 'exec "$HERE/usr/bin/"* "$@"' > "$APPDIR/AppRun"
chmod +x "$APPDIR/AppRun"
# Minimal .desktop (use printf to avoid YAML/heredoc issues)
printf '%s\n' '[Desktop Entry]' 'Name=ToDoTimeSquare' 'Exec=AppRun' 'Type=Application' 'Categories=Utility;' > "$APPDIR/ToDoTimeSquare.desktop"
# Download appimagetool and build AppImage
APPIMAGE_TOOL=appimagetool-x86_64.AppImage
wget -q -O "$APPIMAGE_TOOL" "https://github.com/AppImage/appimagetool/releases/download/continuous/appimagetool-x86_64.AppImage"
chmod +x "$APPIMAGE_TOOL"
./"$APPIMAGE_TOOL" "$APPDIR" ToDoTimeSquare.AppImage || true
# 确保总是成功退出
exit 0
shell: bash
- name: Upload Linux Artifacts
uses: actions/upload-artifact@v4
with:
name: linux-artifacts
path: |
build/linux/x64/release/linux-x64-release.zip
build/linux/x64/release/linux-x64-release.tar.gz
build/linux/x64/release/ToDoTimeSquare.AppImage
build-windows:
name: Build Windows
runs-on: windows-latest
timeout-minutes: 30
steps:
- name: Checkout code
uses: actions/checkout@v4
- name: Setup Flutter
uses: subosito/flutter-action@v2
with:
channel: stable
flutter-version: ${{ env.FLUTTER_VERSION }}
cache: true
- name: Verify Flutter & Dart
run: |
flutter --version
dart --version
- name: Build Windows
run: |
cd .
flutter pub get
flutter build windows --release
- name: Zip Windows bundle
run: |
pwsh -Command "Compress-Archive -Path 'build/windows/x64/runner/Release/*' -DestinationPath 'build/windows/x64/runner/Release/windows-release.zip' -Force"
shell: pwsh
- name: Upload Windows ZIP
uses: actions/upload-artifact@v4
with:
name: windows-artifacts
path: build/windows/x64/runner/Release/windows-release.zip
- name: Install Inno Setup
run: |
choco install innosetup -y
- name: Generate Inno Setup Script
shell: pwsh
run: |
$version = (Select-String '^version:' pubspec.yaml).Line.Split(':')[1].Trim()
Set-Content -Path installer.iss -Value @"
[Setup]
AppName=ToDoTimeSquare
AppVersion=$version
DefaultDirName={pf}\ToDoTimeSquare
DefaultGroupName=ToDoTimeSquare
OutputDir=build\windows\installer
OutputBaseFilename=ToDoTimeSquareInstaller
Compression=lzma
SolidCompression=yes
SetupIconFile=windows\runner\resources\app_icon.ico
[Files]
Source: "build\windows\x64\runner\Release\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
[Icons]
Name: "{group}\ToDoTimeSquare"; Filename: "{app}\ToDoTimeSquare.exe"
Name: "{group}\Uninstall ToDoTimeSquare"; Filename: "{uninstallexe}"
[Run]
Filename: "{app}\todo_time_square.exe"; Description: "Launch ToDoTimeSquare"; Flags: nowait postinstall skipifsilent
"@
- name: Generate Installer
run: |
iscc installer.iss
shell: cmd
- name: Upload Installer
uses: actions/upload-artifact@v4
with:
name: windows-installer
path: build/windows/installer/ToDoTimeSquareInstaller.exe
release:
name: Create GitHub Release
needs:
- bump-version
- build-android-with-impeller
- build-android-without-impeller
- build-web
- build-linux
- build-windows
- build-ios
- build-macos
if: ${{ always() && needs.bump-version.result == 'success' }}
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Download All Artifacts
uses: actions/download-artifact@v4
with:
path: release-artifacts
- name: List files for debugging
run: ls -R release-artifacts
- name: Publish Release
uses: softprops/action-gh-release@v2
with:
tag_name: ${{ needs.bump-version.outputs.new_tag }}
name: 🔖${{ needs.bump-version.outputs.new_tag }}
body: ${{ github.event.head_commit.message }}
files: |
release-artifacts/**/*.apk
release-artifacts/**/*.zip
release-artifacts/**/*.tar.gz
release-artifacts/**/*.AppImage
release-artifacts/**/*.exe