在Flutter项目上使用Github Actions for CI / CD的免费功能

GitHub Actions是一种用于自动执行带有存储库的例行操作并帮助您为项目创建CI / CD的工具。



GitHub用户每月有2000分钟的时间在服务基础架构上运行GitHub Actions。让我们充分利用这段空闲时间。



我为Flutter应用程序的开发人员提供了指导:如何使用GitHub Actions为每个请求请求运行测试和代码分析器,构建工件并将其部署在Firebase中进行测试。







一个人很快就会习惯美好的事物。而且他已经习惯了如此之多,以至于他甚至都没有想到事实并非总是如此。就像关于男人和山羊的古老轶事所说,当您被剥夺了所有快乐时,就会实现生活中所有快乐。



如果您的工作项目与CI / CD配合良好,那么您就是幸运的人。

也许您是在初创公司工作,并用自己的双手仔细配置了所有管道和挂钩。



也许整个DevOps团队都会照顾您的健康:每个月它都会通过新的集成,在我们眼前的构建时间融化以及将程序集部署到所有可想象和难以想象的地方的先进技术来取悦您。



不要紧。最主要的是,您始终要确保自己的构建是可行的,与此同时,您自己也可以摆脱许多非常无聊的例行任务,这种想法总是使开发人员陷入忧郁和沮丧之中。顺便说一句,如果您上次在Jira中手动更改问题的状态是在什么时候写评论,我将感到高兴。



离开舒适区



他们在哪里离开舒适区,最重要的是,为什么?原因很多。一个熟人请我帮忙为自己的酒吧编写一个小应用程序,您终于找到了实施自己梦pet以求的宠物项目的时间,或者决定释放一个偶然地作为项目一部分诞生的图书馆。最后,您和您的同事决定为研讨会编写一个小样本项目。



我敢打赌,在任何情况下,您从新的有趣任务中得到的灵感都会迅速与“无空气环境”中苛刻的软件开发现实相撞(是的,在某些时候,您将需要像空气一样的智能收集器)。



“ CI / CD很难...”



您通常在这种时候对自己说些什么?“我不明白!我只是在写前端/手机,对您的詹金斯一无所知!” 如果我告诉过您您不需要了解此类信息怎么办?



是的,是的,您只需要能够使用控制台命令来构建项目,仅此而已。即使这是一个个人小项目,而不是已经很难消化IDE的巨型多模块怪物,您也可以大大简化生活。



Github Actions非常简单,即使您的祖母也可以轻松完成设置。



那帖子是关于什么的呢?



如果一切都这么简单,为什么还要浪费时间阅读这份作品?我将用项目符号列表回答:



  • Flutter. CI . , Flutter- . ,

  • . Github Actions —  . 2 000 ( ). - , .

  • . Flutter Android iOS , - . , , , .



c CI/CD , . . ($).


Github Actions, !



Github Actions是一项服务,可让您自动执行存储库工作流程。您可以对项目手动执行的任何操作(除了直接编写代码外)都可以委托给Github Actions。如果您想立即了解主要资源,请转到官方文档



通常,我们甚至根本不知道我们需要自动化什么。团队没有时间了解服务的复杂API,然后从头开始编写和调试解决方案。Marketplace解决了这个问题:在那里发布了将近5000种现成的Action,可以解决许多典型任务(例如,发送有关Telegram中事件的通知分析技术债务的项目来源根据PR中更改的文件在PR上设置标签)。坏消息:其中许多是共享软件-使用限制非常严格。



工作过程



Github Actions中的所有内容都围绕工作流程。每个工作流程都回答两个问题:做什么和何时做。



怎么办。这里有无数的选项:您可以使用现成的或自行创建的脚本来构建,测试和部署构建。详细了解工作流程配置



何时做。您可以针对存储库中发生的事件触发工作流。创建一个拉取请求,推送一个提交标签,甚至为您的项目添加一个新星。挂钩的完整列表



如果工作流不是在某个事件上而是在某个时间或某个频率执行,则可以使用POSIX cron语法阅读有关常规事件



更多信息。存储库可以同时具有任意多个不同的工作流程。每个工作流程都在单独的YAML文件中进行了描述,每个文件都应存储在存储库根目录下的.github /工作流程目录中。了解有关工作流语法的更多信息



运行环境



Github Actions提供了两个用于执行工作流程的选项:



  • Github-hosted runners — , . Windows, Linux macOS. , Codemagic, ( ). , , ;

  • Self-hosted runners — , . Github , .



在我的文章中,我将重点讨论第一个选项。我们正在遵循最简单的方法,对吧?



设置Flutter的基本工作流程



在开始配置工作流之前,我们需要达成两点共识。



首先,工作流主要作用将是使代码库的细分复杂化。没有构建,包含潜在问题或破坏测试的代码不应成为主流。



第二:我的配置中可能有些微妙之处与您的项目无关。我将尝试解释它们。但是,如果您以本文为指导,请慎重使用。



最后,让我们决定工作流应该做什么。我们需要一个计划来帮助我们朝正确的方向前进。



逐步完成组装



设置您自己的工作流程时,以上计划可用作清单。我们必须:



  1. 给工作流取一个有意义的名称;
  2. 指明我们的工作流程将在哪个事件开始;
  3. 确定将以哪种配置启动的机器;
  4. 确定我们的工作流程将包括的步骤:


  • 签出项目,
  • 安装Java;
  • 安装Flutter(您还记得,每次我们有干净的实例供我们使用时),
  • 下载项目包,
  • 启动静态分析仪,
  • 运行测试,
  • 构建装配本身,
  • 将构建版本部署到测试人员可以获取的位置。


现在,我们的工作已经形成了切实的形状。让我们继续执行。



我们的工作流程最终会是什么样子
— . , , .



name: Flutter PR

on:
 pull_request:
   branches:
     - "dev/sprint-**"
   paths-ignore:
     - "docs/**"
     - "openapi/**"
     - ".vscode/**"

jobs:
 build:
   runs-on: ubuntu-latest
   steps:
     - uses: actions/checkout@v1
     - uses: actions/setup-java@v1
       with:
         java-version: "12.x"

     - uses: subosito/flutter-action@v1
       with:
         channel: "stable"

     - run: sh ./scripts/flutter_pub_get.sh

     - run: sh ./scripts/flutter_analyze.sh

     - run: flutter test

     - run: flutter build apk --release

     - uses: actions/upload-artifact@v1
       with:
         name: APK for QA
         path: build/app/outputs/apk/dev/debug/apk_name.apk

     - name: Upload artifact to Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.FIREBASE_ANDROID_PROD_APP_ID }}
          token: ${{ secrets.FIREBASE_TOKEN }}
          groups: testers
          file: build/app/outputs/apk/dev/debug/apk_name.apk
          debug: true




名称



显然,我们需要命名工作流程,以便该名称尽可能准确地反映其本质。名称docs)是执行工作流时在Actions Console中看到的第一件事。为什么用这种方式命名我的工作流程,您很快就会发现。







name: Flutter PR


触发事件



“ on”docs)允许我们指定一个或多个事件,在注册时我们要启动工作流程。此外,可以对某些事件进行微调。 



选择哪个活动?为了不丢失故障,您可以至少指定所有现有事件。然后组装几乎会连续进行,但是我们要这样做吗?不可以,因为在这种情况下,我们免费关税计划的限制将很快终止。我们将寻找最佳解决方案。



让我们假设我们的项目遵守协议,根据协议,不能直接将代码直接推入项目的主分支,而只能通过创建拉取请求。如果我们的工作流响应创建请求并从修改后的代码库构建项目,这是合乎逻辑的:



on: pull_request


$这足以完成工作,但是解决方案还不是很理想。构建将在创建的每个拉取请求上触发。这是多余的,因为我们只对定向到项目主分支的拉取请求感兴趣。Github Actions的语法允许我们指定我们感兴趣的分支的名称(或掩码)。



on:
 pull_request:
   branches:
     - "dev/sprint-**"


$再一次,我们正在寻找优化流程的方法。有些文件,即使从理论上讲也不会损害您的项目:项目文档,Swagger,常规代码样式设置和IDE。幸运的是,我们能够通过路径掩码忽略此类文件。结果,“ on”块将如下所示:



on:
 pull_request:
   branches:
     - "dev/sprint-**"
   paths-ignore:
     - "docs/**"
     - "drz-swagger/**"
     - ".vscode/**"


重要提示:仅在准备好合并请求时才发出请求。下一次对已创建的拉取请求的推送将重新启动工作流程。

作业配置







最后,我们准备配置作业docs)。现在是时候澄清工作在工作流中扮演什么角色。



每个工作流程必须至少包含一项工作。该工作包含对我们在项目中执行步骤的逐步说明一个工作流程中的作业数以及一个作业中的步骤数不受限制。默认情况下,所有作业并行执行,除非指定了一个作业对另一作业结果的依赖。我们的项目将只有负责构建项目的工作。



搭建环境



每次工作流在干净的虚拟机实例上运行。我们唯一可以选择的是将在此计算机上安装的操作系统。选择什么?



选择macOS是很诱人的,因为我们计划为目标平台(Android和iOS)构建Flutter应用程序。坏消息。在macOS上使用实例一分钟的费用为在Ubuntu上使用实例的十分钟(10 !!!)。在我们使用Windows的实例中,根本没有意义,因为在那里仍然无法组装iOS程序集,并且其使用时间是Ubuntu实例的两倍。更多关于计费



$我们如何确保我们的2,000分钟免费时间不会变成200分钟?没有好的解决方案。我决定在创建请求请求时跳过在iOS上进行构建。这可能会影响iOS版本的稳定性。还有一个折衷的选择-仅在更改pubspec.yaml/ ios目录中的任何文件时才在macOS上构建iOS构建,在其他情况下,仅在使用Ubuntu的实例上构建Android构建。可以类似于我们为“ on”块设置忽略文件的方式来完成此操作



jobs:
 build:
   runs-on: ubuntu-latest


您可以查看技术规格以及已安装的“开箱即用”软件的列表不幸的是,Flutter和Java没有包含在此列表中。每次执行工作流程时,都必须手动安装它们。



不要着急让自己难过。准备行动将对我们有所帮助,我们可以在工作步骤中使用它。我们将使用两个:



  • actions / setup-java-设置Java环境的官方动作;

  • subosito / flutter-action是用于下载和安装Flutter SDK的非正式动作。它本身已被很好地证明:它使您可以做所需的任何事情-例如,指定所需的框架通道或切换到特定版本的SDK。



steps:
      - uses: actions/setup-java@v1
        with:
          java-version: "12.x"

      - uses: subosito/flutter-action@v1
        with:
          channel: "stable"


克隆存储库



我们有一个干净的实例,是从Github租来的一台机器几分钟。在上一步中,我们在其上安装了所有必需的软件。现在,我们需要为项目克隆源存储库。为此,我们将使用现成的工具:



  • actions / checkout是克隆带有大量设置(大多数情况下不需要)的存储库的官方操作。由于工作流直接在我们要克隆的存储库上运行,因此不需要显式指定它。



- uses: actions/checkout@v1


加载依赖



到目前为止,我们还没有亲手执行步骤,仅使用了我提供的现成操作。现在,我们继续进行项目构建的活动阶段的实现,因此该自己编写该步骤的实现了。



在构建之前,我们需要下载pubspec.yaml文件的dependencies块中列出的所有软件包,以及它们的所有传递依赖。为此,Flutter SDK提供了一个简单的命令flutter pub get。步骤的实现可以包括调用一个终端命令。在这种情况下,仅在此命令完成后才调用下一步。



- run: flutter pub get


如果您的项目具有复杂的结构,并且包含许多在本地连接的d​​art程序包,那么您将面临问题。如果没有显式调用flutter pub get每个软件包,就无法构建项目。在我的项目中,此类软件包收集在根目录下的/ core文件夹中。下面是解决此问题的脚本。在同一根目录/ scripts文件夹中的flutter_pub_get.sh文件对此进行了描述



flutter pub get
cd core
for dir in */ ; do

    echo ${dir}
    cd ${dir}
    pwd
    flutter pub get
    cd ..
    pwd
    if [ "$#" -gt 0 ]; then shift; fi
    # shift
done


由于step的实现可以是任何终端命令,因此没有什么可以阻止我们执行shell脚本。



- run: sh ./scripts/flutter_pub_get.sh


静态代码分析



Flutter邀请我们使用内置命令flutter analyze来运行静态分析器。这将有助于在早期阶段识别我们的代码库的潜在问题:在bug产生之前,或者我们的代码变成不可读且不受支持的混乱。



我们本来可以利用盒装功能,而无需进行任何调整,但是可惜,标准团队行为flutter analyze存在一个缺陷,即在错误的时间破坏了我们的工作流程。 



分析仪发现的问题分为三个严重级别:信息,警告,错误。在这个问题上据描述,即使在分析过程中仅发现了信息类的问题(并且花在这里和现在修复它们并不总是值得花时间),该命令仍返回代码“ 1”,结果程序集将崩溃。



我建议使用以下脚本作为临时解决方案。从现在开始,仅当错误级别存在问题时,程序集才会崩溃



OUTPUT="$(flutter analyze)"
echo "$OUTPUT"
echo
if grep -q "error •" echo "$OUTPUT"; then
    echo "flutter analyze found errors"
    exit 1
else
    echo "flutter analyze didn't find any errors"
    exit 0
fi


我们在工作流程的下一步中执行shell脚本:



- run: sh ./scripts/flutter_analyze.sh


运行测试



如果您的项目中有测试,那么您就走对了!要使测试有效,编写它们是不够的-需要定期运行它们,以便及时纠正实施缺陷或在必要时进行更新。因此,在下一步中,我们将实施



- run: flutter test


注意:不包含任何已实现测试的空测试类将使整个工作流程崩溃。只有一种方法:在准备好在其中实现至少一个测试之前,不要声明测试类。



建立并签名



所有准备工作都结束了。我们已经验证该代码很可能不包含明显的问题。现在,我们进入最重要的阶段-工件的生产。换句话说,我们将构建APK。



组件本身非常容易实现。我们可以使用终端命令抖动构建,它可以进行深度配置,并允许您针对特定的风味,主文件ABI构建构件。我们不会在本文中讨论这些细微差别,因此如有必要,请使用其他命令标志。



- run: flutter build apk --release


我们的目标是使装配体具有释放键。在这个阶段,我们将必须解决安全性问题,因为我们需要将发布密钥库及其所有别名和密码存储在某个地方。



Github允许您将字符串值安全地存储在专用的Secrets存储库中。此处可用的数据存储在适当的存储库中,并且可以从工作流的任何步骤以编程方式读取。同时,无法通过Github Web界面查看值。仅允许删除或覆盖。







对于别名和密码,这似乎是一个很好的解决方案,特别是如果您是自己的安全服务,那么* .jks文件本身呢?即使您的存储库是私有的,将其推送到存储库也不是一个好主意。不幸的是,Github没有提供任何安全的文件存储方式,因此您必须躲避。



将我们的密钥库文件表示为字符串会很好。这是真实的-您只需要在base64中对其进行编码。为此,请在包含我们的* .jks文件的目录中打开一个终端,然后执行以下命令。接下来,将创建一个文本文件,您可以从中复制我们密钥库的base64表示形式,然后将其保存到Github Secrets。



openssl base64 < key_store_filename.jks | tr -d '\n' | tee keystore.jks.base64.txt


现在,我们已经具有成功签署装配的所有必要要素,我们将继续进行该步骤的配置。在env块中,我们声明该特定步骤的所有环境变量。我们将从Secrets中获取这些变量的值。



- run: flutter build apk --release
        env:
          STORE_PASSWORD: ${{ secrets.STORE_PASSWORD }}
          KEY_PASSWORD: ${{ secrets.KEY_PASSWORD }}
          KEY_ALIAS: ${{ secrets.KEY_ALIAS }}
          STORE_FILE: ${{ secrets.STORE_FILE }}


在我们的Android主机中,我们必须以一种方式描述程序集配置,以便能够在CI中对* .apk文件进行签名,而又不会失去在本地构建已签名程序集的能力。此刻负责keystoreConfig.gradle文件



如果找到keystore_release.properties文件,则知道该组装在本地进行,这意味着您可以通过简单地从文件中读取所有keystoreConfig属性来对其进行初始化。否则,汇编将在CI中进行,这意味着敏感数据的唯一来源是Github Secrets。



ext {
   def releaseKeystorePropsFile = rootProject.file("keystore/keystore_release.properties")
   if (releaseKeystorePropsFile.exists()) {
       println "Start extract release keystore config from keystore_release.properties"
       def keystoreProps = new Properties()
       keystoreProps.load(new FileInputStream(releaseKeystorePropsFile))
       keystoreConfig = [
               storePassword: keystoreProps['storePassword'],
               keyPassword  : keystoreProps['keyPassword'],
               keyAlias     : keystoreProps['keyAlias'],
               storeFile    : keystoreProps['storeFile']
       ]
   } else {
       println "Start extract release keystore config from global vars"
       keystoreConfig = [
               storePassword: "$System.env.STORE_PASSWORD",
               keyPassword  : "$System.env.KEY_PASSWORD",
               keyAlias     : "$System.env.KEY_ALIAS",
               storeFile    : "$System.env.STORE_FILE"
       ]
   }
   println "Extracted keystore config: $keystoreConfig"
}


这就是keystore_release.properties文件的样子



storePassword={___}
keyPassword={___}
keyAlias={___}
storeFile=../keystore/keystore.jks


Android主机build.gradle文件的最后一步是将keystoreConfig文件应用于我们的发布版本签名配置:



android {
   signingConfigs {
       release {
           apply from: '../keystore/keystoreConfig.gradle'

           keyAlias keystoreConfig.keyAlias
           keyPassword keystoreConfig.keyPassword
           storeFile file(keystoreConfig.storeFile)
           storePassword keystoreConfig.storePassword
       }
   }
}


已签名的程序集已经在我们手中!但是,您如何将其扩展到您的同事进行测试?



卸货



Github Actions允许您将上传工件设置为几乎任何已知的用于分发程序集的工具,但是我们将仅考虑两个选项:



  • Github存储-将程序集上传到您自己的Github存储的最简单方法,它可以立即使用,但是有一些限制;

  • Firebase App Distribution是Firebase生态系统中的一项服务,已由Crashlytics取代Beta。集成的配置比较困难,但是服务本身使用起来更加方便。





Github存储

Github存储可以通过官方操作轻松集成。您只需要指定程序集的名称即可,因为您的同事将在Web界面中看到它,以及CI中已组装的* .apk文件的路径。



- uses: actions/upload-artifact@v1
        with:
          name: APK for QA
          path: build/app/outputs/apk/dev/debug/apk_name.apk


主要问题是有限的存储空间。在免费计划中,仅提供500 MB。最奇怪的是,我没有找到任何方法可以通过Web界面立即手动清除整个存储,因此我通过编写一个单独的工作流仅负责清除生苔的工件来摆脱这种情况。



工作流每天凌晨1点运行,并删除所有超过一周的工件:



name: Github Storage clear

on:
  schedule:
    - cron: '0 1 * * *'

jobs:
  remove-old-artifacts:
    runs-on: ubuntu-latest
    timeout-minutes: 10

    steps:
      - name: Remove old artifacts
        uses: c-hive/gha-remove-artifacts@v1
        with:
          age: '1 week'


Firebase应用程序分发

关于Firebase应用程序分发,我使用现成的动作wzieba / Firebase-Distribution-Github-Action与之集成。 



- name: Upload artifact to Firebase App Distribution
        uses: wzieba/Firebase-Distribution-Github-Action@v1
        with:
          appId: ${{ secrets.FIREBASE_ANDROID_PROD_APP_ID }}
          token: ${{ secrets.FIREBASE_TOKEN }}
          groups: testers
          file: build/app/outputs/apk/dev/debug/apk_name.apk
          debug: true


为了使操作正常运行,它需要传递参数:



  • appId-应用程序标识符,可以在Firebase项目的设置中找到;







  • 令牌-用于对FIrebase项目进行身份验证的令牌,需要将程序集加载到服务中。您只能通过Firebase CLI获取令牌,有关令牌,您可以在官方文档中了解更多信息

  • 文件-CI上已编译的* .apk文件的路径;

  • groups-此参数是可选的,但允许您指定测试程序组的别名,上载的程序集将自动与该测试程序组共享。



发射和观察



我们最简单的工作流程已准备就绪!我们剩下要做的就是触发触发事件并观察工作流程的进度。



忠告和离别词



现在,无论Flutter项目的大小,开发强度或钱包如何,您都可以在其Flutter项目上享受简单CI / CD机制的所有好处。



最后,这是我在处理此工作流程时想到的一些技巧和观察结果:



  • workflow . workflow , , . - . , - workflow , .

  • step’ shell-. workflow . . .

  • Run Duration workflow. workflow , . workflow , step’. . Flutter SDK . — 5-6 .



未来仍有大量潜在的改进和改进。在评论中写下您的想法,以改善工作流程。您个人缺乏什么?读者最有趣的想法的实现也许将成为下一篇关于该主题的文章的基础。



可以在测试应用程序的存储库中找到所有脚本和工作流



感谢您的关注。



PS我们的Surf团队为Flutter发布了许多有用的库。我们将它们上传到SurfGear存储库



All Articles