Featured image of post 记一次 Flutter 工作流的搭建

记一次 Flutter 工作流的搭建

前两天在折腾 Flutter 工作流,记录一下踩坑过程。

kerrinz/pixiv-artvier#17 feat: GitHub action自动编译apk

或许可以写一下flutter配置安卓编译全家桶? 挖个坑先

apk签名

在发布 Android 应用时,必须对 APK 进行签名,但是签名密钥(keystore)是敏感信息,不能直接放在代码库中。常见的做法是将 keystore 文件进行 Base64 编码,然后作为 GitHub Secrets 存储在仓库中。在工作流中,可以通过解码该 Secret 并写入到文件系统来使用。

存储keystore 信息到 GitHub Secrets

需要在 GitHub 仓库的 Settings -> Secrets and variables -> Actions 中添加以下 Secrets

  • KEYSTORE_BASE64:Base64 编码后的 keystore 文件内容。
  • KEY_STORE_PASSWORD:keystore 的密码。
  • KEY_ALIAS:密钥别名。
  • KEY_PASSWORD:密钥的密码。

在工作流中使用keystore 信息进行签名

并将密钥库信息写入android/key.properties 文件,工作流中的代码如下所示:

1
2
3
4
5
6
7
8
9
- name: Create key.properties
    run: |
        # 创建 key.properties 文件
        echo "storePassword=${{ secrets.KEY_STORE_PASSWORD }}" >> android/key.properties
        echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
        echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
        echo "storeFile=ghaction-keystore.jks" >> android/key.properties
        # 解码 keystore 并保存到指定路径
        echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/ghaction-keystore.jks

配置 Android 项目使用签名

android/app/build.gradle(.kts) 文件中引用 key.properties 文件,以便在构建时使用正确的签名配置。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
val keystorePropertiesFile = rootProject.file("key.properties")
android {
    ...
        signingConfigs {
        create("release") {
            keyAlias = keystoreProperties["keyAlias"] as String
            keyPassword = keystoreProperties["keyPassword"] as String
            storeFile = keystoreProperties["storeFile"]?.let { file(it) }
            storePassword = keystoreProperties["storePassword"] as String
        }
    }
    ...
}

自动创建pre-release

在使用 actions/create-release@v1actions/upload-artifact@v4 时遇到了问题, 上传被拒绝,提示Error: Resource not accessible by integration github action 。 因为配置了精细权限(permissions),所以需要确保工作流有足够的权限来创建发布和上传资产,但依旧失败。

解决方法是使用 softprops/action-gh-release@v1,它可以更好地处理权限问题。

编译缓存优化

Flutter 项目的编译时间较长,尤其是首次构建时。每次workflow运行时都需要重新下载和配置Flutter SDK、Android SDK等,导致整体构建时间过长。因此,需要对编译cache进行优化。 可以使用 actions/cache@v4 来缓存 Gradle 依赖和 Android 构建输出,从而加快后续构建速度。

最后效果非常好,Build APK的时间从11分钟(首次编译)缩短到了1分钟左右(使用缓存)。

完整工作流配置

点击展开查看完整工作流配置
  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
name: Build and Release

permissions:
  contents: write
  pull-requests: read

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

jobs:
  build-android:
    runs-on: ubuntu-latest

    steps:
      # 检出代码
      - uses: actions/checkout@v3

      #  设置 Java 环境
      - name: Setup Java
        uses: actions/setup-java@v3
        with:
          distribution: "zulu"
          java-version: "17"

      # 设置 Flutter 环境
      - name: Setup Flutter
        uses: subosito/flutter-action@v2
        with:
          channel: "stable"
          cache: true

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

      # 缓存 Android 构建输出(包括 NDK、CMake 编译产物等)
      - name: Cache Android build
        uses: actions/cache@v4
        with:
          path: |
            build
            android/.gradle
            android/app/build
            ~/.android/build-cache
          key: ${{ runner.os }}-android-build-${{ hashFiles('**/*.dart', 'pubspec.yaml', 'android/**/*.gradle*') }}
          restore-keys: |
            ${{ runner.os }}-android-build-

      # 设置 Android SDK 环境并预安装组件
      - name: Setup Android SDK components
        run: |
          export ANDROID_SDK_ROOT=$ANDROID_HOME
          export PATH=$PATH:$ANDROID_SDK_ROOT/cmdline-tools/latest/bin:$ANDROID_SDK_ROOT/platform-tools
          yes | sdkmanager --licenses || true
          sdkmanager --install "platforms;android-33" "build-tools;33.0.2" "cmake;3.22.1" "ndk;26.1.10909125" || true

      # 获取依赖
      - name: Get dependencies
        run: flutter pub get
      # 构建 APK
      - name: Create key.properties
        run: |
          echo "storePassword=${{ secrets.KEY_STORE_PASSWORD }}" >> android/key.properties
          echo "keyPassword=${{ secrets.KEY_PASSWORD }}" >> android/key.properties
          echo "keyAlias=${{ secrets.KEY_ALIAS }}" >> android/key.properties
          echo "storeFile=ghaction-keystore.jks" >> android/key.properties
          echo "${{ secrets.KEYSTORE_BASE64 }}" | base64 --decode > android/app/ghaction-keystore.jks

      - name: Build APK
        run: flutter build apk --split-per-abi --release

      - name: Build Universal APK
        run: flutter build apk --release

      - name: Upload Android artifacts
        uses: actions/upload-artifact@v4
        with:
          name: android-build
          path: |
            build/app/outputs/flutter-apk/app-release.apk
            build/app/outputs/flutter-apk/app-armeabi-v7a-release.apk
            build/app/outputs/flutter-apk/app-arm64-v8a-release.apk
            build/app/outputs/flutter-apk/app-x86_64-release.apk
  create-release:
    needs: build-android
    runs-on: ubuntu-latest

    steps:
      - name: Checkout repository
        uses: actions/checkout@v3
        with:
          fetch-depth: 0

      # 下载构建产物
      - name: Download artifacts
        uses: actions/download-artifact@v4
        with:
          path: ./dist/

      # 提取 tag 名称
      - name: Extract tag name
        id: extract_tag
        run: |
          TAG_NAME=${GITHUB_REF#refs/tags/}
          echo "tag_name=$TAG_NAME" >> $GITHUB_ENV
          echo "Release tag: $TAG_NAME"

      # 生成 Changelog
      - name: Generate Changelog
        id: changelog
        run: |
          # 获取当前 tag
          CURRENT_TAG="${{ env.tag_name }}"
          
          # 获取上一个 tag
          PREVIOUS_TAG=$(git tag --sort=-v:refname | grep -A1 "^${CURRENT_TAG}$" | tail -n1)
          
          echo "Current tag: $CURRENT_TAG"
          echo "Previous tag: $PREVIOUS_TAG"
          
          # 如果没有上一个 tag,则从第一个 commit 开始
          if [ -z "$PREVIOUS_TAG" ] || [ "$PREVIOUS_TAG" == "$CURRENT_TAG" ]; then
            echo "No previous tag found, generating changelog from first commit"
            CHANGELOG=$(git log --pretty=format:"- %s (%h)" --no-merges)
          else
            # 生成两个 tag 之间的 changelog
            CHANGELOG=$(git log ${PREVIOUS_TAG}..${CURRENT_TAG} --pretty=format:"- %s (%h)" --no-merges)
          fi
          
          # 如果 changelog 为空
          if [ -z "$CHANGELOG" ]; then
            CHANGELOG="No changes"
          fi
          
          # 保存到文件
          echo "## What's Changed" > changelog.md
          echo "" >> changelog.md
          echo "$CHANGELOG" >> changelog.md
          echo "" >> changelog.md
          echo "**Full Changelog**: https://github.com/${{ github.repository }}/compare/${PREVIOUS_TAG}...${CURRENT_TAG}" >> changelog.md
          
          # 输出 changelog 用于 release
          cat changelog.md
          
          # 将 changelog 设置为输出(处理多行)
          echo "changelog<<EOF" >> $GITHUB_OUTPUT
          cat changelog.md >> $GITHUB_OUTPUT
          echo "EOF" >> $GITHUB_OUTPUT

      # 创建 GitHub Release 并上传 APK 文件
      - name: Create Release and Upload Assets
        uses: softprops/action-gh-release@v1
        with:
          tag_name: ${{ env.tag_name }}
          name: ${{ env.tag_name }}
          body: ${{ steps.changelog.outputs.changelog }}
          draft: false
          prerelease: false
          files: |
            ./dist/android-build/app-release.apk
            ./dist/android-build/app-armeabi-v7a-release.apk
            ./dist/android-build/app-arm64-v8a-release.apk
            ./dist/android-build/app-x86_64-release.apk

IIIA

2
IIIA 2 AI参与制作
人类主导,AI用于提升效果
?
使用 Hugo 构建
主题 StackJimmy 设计