完整解析使用 Github Action 構(gòu)建和發(fā)布 Flutter 應(yīng)用

Github Actions 是 Github 提供的免費(fèi)自動(dòng)化構(gòu)建實(shí)現(xiàn),特別適用于持續(xù)集成和持續(xù)交付的場(chǎng)景,它具備自動(dòng)化完成許多不同任務(wù)的能力,例如構(gòu)建、測(cè)試和部署等等。

一、簡(jiǎn)單介紹

用戶只需要在自己 Github 的開(kāi)源項(xiàng)目下創(chuàng)建 .github/workflows 腳本就可以完成接入,另外針對(duì) Github Actions 官方還提供了 marketplace 用于開(kāi)發(fā)者提交或者引用別人寫好的 aciton ,所以很多時(shí)候開(kāi)發(fā)者在使用 Github Actions 時(shí),其實(shí)會(huì)變成了在 marketplace 里挑選和組合 action 的場(chǎng)景。當(dāng)然,這樣各有利弊,后面我們會(huì)講到 。

image.png

要在 Github 存儲(chǔ)庫(kù)中使用 Github Actions,首先需要?jiǎng)?chuàng)建目錄.github/workflows/,然后在 workflows 文件夾里創(chuàng)建不同的 .yml 文件用于響應(yīng)或者執(zhí)行不同的事件,比如 git push 、pull request 等,例如:

name: GitHub Actions Demo
on: [push]
jobs:
  Explore-GitHub-Actions:
    runs-on: ubuntu-latest
    steps:
      - run: echo "?? The job was automatically triggered by a ${{ github.event_name }} event."
      - run: echo "?? This job is now running on a ${{ runner.os }} server hosted by GitHub!"
      - run: echo "?? The name of your branch is ${{ github.ref }} and your repository is ${{ github.repository }}."
      - name: Check out repository code
        uses: actions/checkout@v2
      - run: echo "?? The ${{ github.repository }} repository has been cloned to the runner."
      - run: echo "??? The workflow is now ready to test your code on the runner."
      - name: List files in the repository
        run: |
          ls ${{ github.workspace }}
      - run: echo "?? This job's status is ${{ job.status }}."

上面是 Github doc 里關(guān)于 Action 的一個(gè)基本的工作流 yml 文件,具體參數(shù)含義 :

  • name:這表示該工作流文件的名稱,將在 Github 的 actions 選項(xiàng)卡作為名稱顯示 ;
  • on:這將觸發(fā)該工作流的事件名稱,它可以包含事件列表,例如這里監(jiān)聽(tīng)的事 push
  • jobs:每個(gè)工作流會(huì)包含一個(gè)或多個(gè) jobs ,在這里只有一個(gè),主要是用于表示不同工作任務(wù);
  • Explore-GitHub-Actions :這是工作 ID,你也可以根據(jù)自己的需要命名,會(huì)在 action 的執(zhí)行過(guò)程中顯示;
  • runs-on:jobs 需要運(yùn)行在虛擬機(jī)上,在這里中使用了 ubuntu-latest,當(dāng)然你也可以使用windows-latest 或者 macos-latest;
  • steps:每個(gè) jobs 可以將需要執(zhí)行的內(nèi)容劃分為不同步驟;
  • run:用于提供執(zhí)行命令,例如這里使用了echo 打印日志;
  • name: steps 里的 name 是可選項(xiàng),主要是在日志中用來(lái)做標(biāo)記的;
  • uses :使用一些官方或者第三方的 actions 來(lái)執(zhí)行,例如這里使用官方的 actions/checkout@v2,它會(huì)check-out 我們的 repo ,之后工作流可以直接訪問(wèn) repo 里的文件;

在 GitHub 倉(cāng)庫(kù)添加完對(duì)應(yīng)的 .github/workflows/ci.yml 文件之后,以后每次 push 都可以觸發(fā) action 的自動(dòng)執(zhí)行,以此來(lái)完成可持續(xù)的自動(dòng)集成和構(gòu)建能力。

二、構(gòu)建 Flutter 和發(fā)布到 Github Release

簡(jiǎn)單介紹完 Github Action ,接著我們介紹如何利用 Github Action 構(gòu)建 Flutter 和發(fā)布 apk 到 Github Release,如下代碼所示是 gsy_github_app_flutter 項(xiàng)目里使用到的 github action 腳本:

name: CI

on:
  push:
    branches:
      - master
    tags:
      - '*'
  pull_request:
    paths-ignore:
      - '**/*.md'
      - '**/*.txt'
      - '**/*.png'
      - '**/*.jpg'

jobs:
  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - uses: actions/setup-java@v2
        with:
          distribution: 'zulu'
          java-version: 11
      - uses: subosito/flutter-action@v1
        with:
          flutter-version: '2.8.1'
      - uses: finnp/create-file-action@master
        env:
          FILE_NAME: lib/common/config/ignoreConfig.dart
          FILE_DATA: class NetConfig { static const CLIENT_ID = "${{ secrets.CLIENT_ID }}"; static const CLIENT_SECRET = "${{ secrets.CLIENT_SECRET }}";}
      - run: flutter pub get
      - run: flutter build apk --release --target-platform=android-arm64 --no-shrink

  apk:
    name: Generate APK
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    steps:
      - name: Checkout
        uses: actions/checkout@v2
      - name: Setup JDK
        uses: actions/setup-java@v2
        with:
          distribution: 'zulu'
          java-version: 8
      - uses: subosito/flutter-action@v1
        with:
          flutter-version: '2.5.3'
      - uses: finnp/create-file-action@master
        env:
          FILE_NAME: lib/common/config/ignoreConfig.dart
          FILE_DATA: class NetConfig { static const CLIENT_ID = "${{ secrets.CLIENT_ID }}"; static const CLIENT_SECRET = "${{ secrets.CLIENT_SECRET }}";}
      - run: flutter pub get
      - run: flutter build apk --release --target-platform=android-arm64 --no-shrink
      - name: Upload APK
        uses: actions/upload-artifact@v2
        with:
          name: apk
          path: build/app/outputs/apk/release/app-release.apk
  release:
    name: Release APK
    needs: apk
    if: startsWith(github.ref, 'refs/tags/')
    runs-on: ubuntu-latest
    steps:
      - name: Download APK from build
        uses: actions/download-artifact@v2
        with:
          name: apk
      - name: Display structure of downloaded files
        run: ls -R

      - name: Create Release
        id: create_release
        uses: actions/create-release@v1.1.4
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          tag_name: ${{ github.ref }}
          release_name: ${{ github.ref }}
      - name: Upload Release APK
        id: upload_release_asset
        uses: actions/upload-release-asset@v1.0.1
        env:
          GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
        with:
          upload_url: ${{ steps.create_release.outputs.upload_url }}
          asset_path: ./app-release.apk
          asset_name: app-release.apk
          asset_content_type: application/zip

根據(jù)上述腳本,首先可以看到:

  • push 事件里我們指定了只監(jiān)聽(tīng) master 分支和 tags 相關(guān)的提交;

  • 然后在 pull_request 事件里忽略了關(guān)于 .md、 .text 和圖片相關(guān)的內(nèi)容,也就是這部分內(nèi)容提交不觸發(fā) action ,具體可以看你自己的需求;

  • 接著進(jìn)入到 jobs 里,首先不管是 push 還是 pull_request 都會(huì)執(zhí)行到 Build 事件,運(yùn)行在 ubuntu-latest 虛擬機(jī)上,之后利用 actions/checkout@v2 checkout 代碼;

  • 接著使用 actions/setup-java@v2 配置 java 環(huán)境,這里使用的是 Zulu OpenJDK 版本 11 ,下面表格是 setup-java 支持的可選 java 類型;

    Keyword Distribution Official site License
    temurin Eclipse Temurin Link Link
    zulu Zulu OpenJDK Link Link
    adopt or adopt-hotspot Adopt OpenJDK Hotspot Link Link
    adopt-openj9 Adopt OpenJDK OpenJ9 Link Link
    liberica Liberica JDK Link Link
    microsoft Microsoft Build of OpenJDK Link Link
  • 接著就是使用第三方的 subosito/flutter-action@v1 配置 flutter 環(huán)境,直接通過(guò) flutter-version: '2.8.1' 指定了 Flutter 版本;

  • 接著是使用第三方的 finnp/create-file-action@master 創(chuàng)建文件,因?yàn)?gsy_github_app_flutter 項(xiàng)目有一個(gè)配置文件是需要用戶根據(jù)自己的 ID 和 SECRET 手動(dòng)創(chuàng)建,所以這里通過(guò) create-file-action 創(chuàng)建文件并輸入內(nèi)容;

  • 在上述輸入內(nèi)容部分,有一個(gè) secrets.xxx 的參數(shù),因?yàn)闃?gòu)建時(shí)需要將自己的一些密鑰信息配置到 action 里,所以如下圖所示,可以在 SettingsSecrets 里添加對(duì)應(yīng)的內(nèi)容,就可以在 action 里通過(guò) secrets.xxx 讀??;

  • 接著配置好環(huán)境之后,就可以執(zhí)行 flutter pub getflutter build apk 執(zhí)行構(gòu)建;

完成 Build 任務(wù)的邏輯介紹之后,可以看到在 Build 任務(wù)下面還有一個(gè) apk 任務(wù),該任務(wù)基本和 Build 任務(wù)一直,不同之處在于:

  • 多了一個(gè) if: startsWith(github.ref, 'refs/tags/') ,也就是存在 tag 的時(shí)候才會(huì)觸發(fā)該任務(wù)執(zhí)行;
  • 多了一個(gè) actions/upload-artifact@v2 用于將構(gòu)建出來(lái)的 build/app/outputs/apk/release/app-release.apk上傳,并等到 release 任務(wù)內(nèi)使用;

完成 apk 任務(wù)之后,會(huì)進(jìn)入到 release 任務(wù),該任務(wù)同樣通過(guò) if 指定了只在 tag 提交時(shí)運(yùn)行:

  • 任務(wù)首先會(huì)通過(guò) actions/download-artifact@v2 下載剛剛上傳的 apk;
  • 然后就通過(guò) actions/create-release@v1.1.4 創(chuàng)建一個(gè) release 版本,這里使用的 secrets.GITHUB_TOKEN 是官方內(nèi)置的 secrets ,我們直接使用就可以了;
  • 最后通過(guò) actions/upload-release-asset@v1.0.1 將 apk 上傳到剛剛創(chuàng)建的 release 版本里,自此就完成了 action 的發(fā)布流程;

可以看到整個(gè)過(guò)程其實(shí)都是在組合不同的 action ,可以很靈活方便地配置構(gòu)建邏輯,例如如果你的項(xiàng)目是單純的 android sdk 項(xiàng)目,那同樣可以通過(guò)如下腳本進(jìn)行發(fā)布管理:

name: CI

on:
  push:
    branches:
      - master
    paths-ignore:
      - '.idea/**'
      - '.gitattributes'
      - '.github/**.json'
      - '.gitignore'
      - '.gitmodules'
      - '**.md'
      - '**/*.txt'
      - '**/*.png'
      - '**/*.jpg'
      - 'LICENSE'
      - 'NOTICE'
  pull_request:
    paths-ignore:
      - '.idea/**'
      - '.gitattributes'
      - '.github/**.json'
      - '.gitignore'
      - '.gitmodules'
      - '**.md'
      - '**/*.txt'
      - '**/*.png'
      - '**/*.jpg'
      - 'LICENSE'
      - 'NOTICE'

jobs:
  publish:
    name: Publish to MavenLocal
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: 17
      - uses: gradle/gradle-build-action@v2
        with:
          arguments: publishToMavenLocal

  build:
    name: Build
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v3
      - uses: actions/setup-java@v3
        with:
          distribution: 'zulu'
          java-version: 17
      - uses: gradle/gradle-build-action@v2
        with:
          arguments: app:assembleDebug

當(dāng)然,如果你需要打包的是 iOS ,那么你就需要使用 macos-latest 的環(huán)境,另外還需要配置相關(guān)的開(kāi)發(fā)者證書(shū),這個(gè)過(guò)程可能會(huì)比較難受,相關(guān)可以參考 《Flutter 搭建 iOS 命令行服務(wù)打包發(fā)布全保姆式流程》 。

三、隱私安全問(wèn)題

最后,關(guān)于 Github Actions 之前存在過(guò)出現(xiàn)泄露敏感數(shù)據(jù)的問(wèn)題,比如 Github 的 Token 等 ,舉個(gè)例子,如上面的腳本,它在執(zhí)行任務(wù)時(shí)都會(huì)需要秘鑰 ,如果你使用的第三方 action 在執(zhí)行過(guò)程中獲取了你的密鑰并干了一些“非法” 的事情,就可能出現(xiàn)異常泄漏問(wèn)題。

所以一般情況下建議大家都要去看下非官方的腳本實(shí)現(xiàn)里是否安全,但是由于 tag 和 branch 是可以修改,所以建議不要@分支或tag,而是應(yīng)該 checkout 對(duì)應(yīng)的提交哈希,這樣有利于你審查使用時(shí)的腳本是否安全。

另外,例如還有人提到可以通過(guò) pull_request 來(lái)惡意攻擊獲取對(duì)應(yīng)隱私:

  • 1、fork 一個(gè)正在使用 GitHub Actions 的公開(kāi)代碼庫(kù);

  • 2、創(chuàng)建一個(gè)基于該項(xiàng)目的 pull 請(qǐng)求;

  • 3、使用 pull_request_target 事件創(chuàng)建一個(gè)惡意 Actions 工作流,然后單獨(dú)向該 fork 庫(kù) commit;

  • 4、將第二步基分支的 pull 請(qǐng)求更新為第三步的 commit 哈希;

之后惡意 Actions 工作流就會(huì)運(yùn)行,并從目標(biāo) repos 里獲取到執(zhí)行過(guò)程的敏感數(shù)據(jù),此時(shí)攻擊者將擁有對(duì)目標(biāo)存儲(chǔ)庫(kù)的寫訪問(wèn)權(quán)限,除此之外他們還可以通過(guò) GitHub 訪問(wèn)與倉(cāng)庫(kù)之成的任何服務(wù)。

所以雖然 GitHub Action 很便捷,但是如果出于商業(yè)考慮的話,還需要謹(jǐn)慎抉擇安全問(wèn)題。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容