使用GitHub Actions自动发布应用
GitHub Actions是GitHub提供的一项功能,通过创建工作流程(workflow)文件,开发人员可以实现对应用自动化的测试、构建和发布等任务。同时,GitHub Actions支持自定义流程工作的平台,因此跨平台的构建也成为了可能。
在GitHub Actions中可以考虑使用语义化版本和Conventional Commits,实现自动化的版本管理和发布流程,提高开发效率;下面将先对这二者进行介绍。
语义化版本和Conventional Commits
语义化版本
考虑以下的版本号,Major.Minor.Patch
,其中:
- **Major:**当应用出现了重大的,破坏性的变更时,这个部分+1
- **Minor:**当应用出现了功能性的更新(包括废弃某些API),但是没有不向后兼容的变更时,这个部分+1
- **Patch:**应用没有任何功能性的更新,只是修复漏洞时,这个部分+1
除了以上的几点,语义化版本还有以下的要求:
- 版本号的数字一定是按照数字顺序递增。比如,对于1.9.0版本,如果有功能性的更新,那么他之后应该是1.10.0、1.11.0等。
- 对于Major为0的版本号(比如0.10.2),这表示应用还在初始开发阶段,此时应用仍然不稳定,版本的变化不一定符合语义化版本的特征。
- 当应用的版本为1.0.0时,应用正式发布,此后的版本号变化将严格反映语义化版本的特征。
- 预发布版本需要在正式版本号后添加横线,形如X.Y.Z-alpha、X.Y.Z-alpha.1等,并且它们在版本号的比较上小于X.Y.Z。
其他的语义化版本规范请参见官网。
Conventional Commits
Conventional Commits是一个轻量级的提交信息的格式约定,它规定了每次提交的修改范围、描述以及附加的信息的格式。使用Conventional Commits具有以下好处:
- 简单清晰地描述每次提交的修改内容,方便开发人员查看和管理。
- 方便自动工具审查,避免多种不同提交格式所带来的混乱。
- 可以借助自动化工具,在每次发布时自动生成发布文档,介绍应用更新内容。
Conventional Commits的格式如下:
<type>[optional scope]: <description>
[optional body]
[optional footer(s)]
用尖括号括起来的表示必选项,方括号括起来的表示可选项,下面对其中一些进行说明:
- **fix:**当一个提交信息的type是
fix
时,表示修复一个漏洞,对应语义化版本中的Patch
。 - **feat:**当一个提价信息的type是
feat
时,表示应用引入一个新的功能,对应语义化版本中的Minor
。 - **BREAKING CHANGE:**提交信息的footer中具有
BREAKING CHANGE:
,或者在type后面跟随一个!
时,表示应用具有不向后兼容的破坏性更新,对应语义化版本的Major
。当应用具有BREAKING CHANGE时,它可以具有任何的类型。 - 除了
fix
和feat
以外,提交信息的type还可以取以下值:build、chore、ci、docs、style、refactor、prefer、test等。 - 除了
BREAKING CHANGE:
以外,提交信息的footer也被允许,其格式应当为Key: Value
,例如:Closes: #123,可以表示关闭Issue #123。 scope
可以用来给提交信息添加额外的上下文,比如:feat(parser): add ability to parse arrays
.
常用的提交类型说明如下:
- **docs:**只有文档被更新
- **style:**不影响代码含义的修改,比如空格、缩进、语句末尾的分号等。
- **refactor:**针对代码的修改,但是不修复bug,也不引入新功能。
- **pref:**为了提高代码性能所做的修改。
- **test:**添加新的测试,或者修改已有的测试。
- **build:**针对构建系统的修改,比如npm、glup等。
- **ci:**修改持续继承相关的文件和脚本等,比如GitHub Actions。
- **chore:**既不修改源文件,也不修改测试文件,比如发布新版本等。
- **revert:**撤回一次已有的更新。
GitHub Actions介绍
GitHub Actions是GitHub提供的持续集成服务,允许开发者自动化地测试、构建和发布代码。将GitHub Actions与前面提到的语义化版本和Conventional Commits结合,可以提高开发效率,使开发流程更加规范,便于理解和追踪代码更改。下面是一段GitHub Actions的“Hello world”代码:
name: hello-world # 1
on: # 2
push:
branches: main
jobs: # 3
hello-world:
runs-on: ubuntu-latest # 4
steps: # 5
- name: checkout-repository
uses: actions/checkout@v4 # 6
- run: echo "代码仓库 ${{ github.repository }}已被克隆到runner中" # 7
- 指定Action的名称,在执行Actions时会被现实在侧边栏中显示。
- 指定Action触发的条件(Trigger),这里是在main分支推送代码时执行。
- 每个Action至少会有一个Job,所有的Job都在
jobs
中定义。 runs-on
指定了这个Job运行的平台,可以指定一个平台,也可以指定多个不同的平台同时执行任务。- 每个Job都至少会有一个Step,所有的step都在
steps
中定义,每个Step可以使使用uses
调用其他的GitHub Actions,或者使用run
来执行一段代码。 - 使用actions/checkout@v4,可以将代码从仓库中克隆到执行Action的容器中。如果需要对代码进行操作(执行测试、构建等任务时),那么必须要先使用这个Action。
- 在Step中使用
run
来执行一段shell命令,在不同平台上使用的shell不一样,比如Windows上默认命令行是Powershel。
Trigger
Trigger用来表示这个Action在什么时机被执行,可能是一次推送、一个新的Issue或者一个新的Pull Request。同时我们也可以借助Filter来实现对触发条件的筛选。下面列出几个常用的触发条件:
-
最简单的触发条件:
on: push # 只有一个事件可以触发Action执行 on: [push, fork, issue, pull_request] # 有多个事件可以触发Action执行
-
对代码推送或Pull Request等进行筛选:
on: push: branches: # 针对推送的代码分支进行过滤 - main tags: - "v**" - "releases/**" # 使用通配符进行匹配 - "!releases/**-alpha" # 在前面添加感叹号表示排除掉这个筛选条件 issue: # 对Issue进行筛选 types: [opened, labeled]
-
手动执行每个Action
on: workflow_dispatch: # 添加后可以手动执行Action,但是只能在默认分支上执行 inputs: # 可选项
手动执行Actions时,还可以通过
input
定义手动执行时的输入,语法参见Workflow syntax for GitHub Actions - GitHub Docs。
Jobs
在GitHub Actions中,jobs
定义了需要执行的任务。一个简单的示例如下:
jobs:
job_id:
name: Job Id # 可选项,指定当前步骤的名称,在执行时会被现实在GitHub界面上
runs-on: ubuntu-latest # 每个Job必须要指定运行的平台,后续代码中为了简便将省略这一部分
定义所需要的权限
在Action或者Job执行过程中,可能会需要不同的权限,比如contents: write
权限可以让开发人员发布一个新的Release。权限信息可以被直接定义在Action中,表示全局需要的权限;也可以单独定义在每个Job中,表示当前Job所需要的权限,参见Workflow syntax for GitHub Actions - GitHub Docs。
name: Permission Demo
on: push
permissions:
contents: write
jobs:
make-a-pull-request:
permissions:
pull_requests: write # 表示创建一个新的Pull Request
# 后续省略
定义每个Job的执行条件和依赖关系
每个Job在运行时都是并行执行的,但是我们有时同样需要让这些Job按顺序执行,或者在某些条件下执行,这时就可以用到needs
和if
标签:
jobs:
job1:
job2:
needs: job1
job3:
needs: [job1, job2]
在上面的代码中,job2依赖于job1,而job3又依赖于job2和job1,因此他们三个的执行顺序是:job1→job2→job3。
on: push
jobs:
process-tag:
if: ${{ startsWith(github.ref, 'refs/tags/') }}
上述代码表示只有在推送一个Tag的时候才会执行process-tag这个job。
在不同平台执行任务
有时同一个任务需要在不同的平台下执行,比如跨平台的构建,这时可以使用matrix
实现:
jobs:
build-multi-platform:
strategy:
matrix:
platform: [macos-latest, ubuntu-latest, windows-latest] # 定义所需要运行的平台
runs-on: ${{ matrix.platform }}
steps:
step_1:
if: matrix.platform == 'ubuntu-latest' # 在ubuntu-latest环境下执行这个step
Steps
每个Job都会有至少一个Step,用来表示需要执行的具体行为。与Job不同的是,Step是按照定义顺序执行的,不存在并行执行的问题。
jobs:
job1:
rus-on: ubuntu-latest
steps:
- name: Step 1 # 1
id: st1 # 2
if: condition == true # 3
shell: pwsh # 4
uses: actions/checkout@v4 # 5
run: echo 'Hello, world!' # 6
with: # 7
key: value
env: # 8
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
- 可选项,用于指定GitHub上显示的名称。
- 可选项,用于在不同的上下文中传递参数,参见Contexts - GitHub Docs。
- 可选项,用于指定当前Step的执行条件。
- 可选项,用于指定使用的命令行,在Windows上默认是
pwsh
,使用PowerShell,其他平台上默认是bash
,使用Bash Shell。参见Workflow syntax for GitHub Actions - GitHub Docs。 - 必选项之一,使用其他的GitHub Actions。
- 必选项之一,执行一段命令,
uses
和run
可以同时使用。 - 可选项,用于向
uses
使用的GitHub Action传递参数。 - 用于指定运行环境中的值,这属于上下文(Context)的一部分,参见Contexts - GitHub Docs。
使用Release Please自动更新版本号
Release Please是Google推出的自动工具,可以实现基于Conventional Commit的提交信息自动发布更新、自动更新项目的版本号。
注意:Release Please并不会自动将代码上传到包管理器(比如npm、Maven Repository、crates.io等),也不能处理复杂的分支管理,这些需要开发人员手动实现。
Release Please的工作是基于Release PR实现的。在被触发而工作时,Release Please会首先分析Git的提交历史,从提交信息中按照Conventional Commits的格式整理出此次更新的内容和级别(Major、Minor或Patch),然后修改版本号和修改文件(一般是CHANGELOG.md),最后向仓库中发起一个Pull Request。如果开发人员合并了这个PR,那么修改就会被合并进分支中,然后发布一个新的Release。
Release Please支持的项目类型主要有以下几种,完整列表参见GitHub:
类型 | 描述 |
---|---|
node | 包含一个package.json和CHANGELOG.md。 |
rust | 包含Cargo.toml和CHANGELOG.md。 |
dart | 包含pubspec.yaml和CHANGELOG.md。 |
php | 包含composer.json和CHANGELOG.md。 |
maven | 在每次发布后生成一个SNAPSHOT版本号并且自动更新pom.xml。 |
simple | 包含一个version.txt和CHANGELOG.md。 |
下面是一个简单的示例:
name: release-please-demo
on:
push:
branches:
- main
permissions: # 1
contents: write
pull-requests: write
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- uses: google-github-actions/release-please-action@v4 # 2
with:
release-type: node # 3
token: ${{ secrets.GITHUB_TOKEN }} # 4
- Release Please需要
contents: write
权限来发布新版本,使用pull-request: write
权限来发布Release PR。 - Release Please的GitHub Actions名称为:
google-github-actions/release-please-action@v4
。 - 指定发布类型,这里以
node
为例,要求仓库中有一个package.json
文件和CHANGELOG.md
文件(这个如果没有会自动创建)。 - 附带上GitHub Token,这是必须的。
GITHUB_TOKEN
是GitHub在每个Action执行时自动生成的token,开发人员也可以使用自定义的Token。
Release Please的输出
上面的实例只做了检查提交信息、发起Release PR和在合并后自动发布新版本的功能,有时我们需要在成功发布版本之后做一些后续工作,这时就可以借助Release Please的输出来实现。常用的输出结果有以下几条:
- **release_created:**当根目录下有新的发布产生时为true。
- **upload_url:**上传文件的URL,与GitHub APIReleases - GitHub Docs有关。
- **html_url:**同样与GItHub Release API有关。
- **tag_name:**新发布版本的Tag,同样与GitHub Release API有关。
- **major、minor、patch:**新发布版本的版本号。
其他的输入输出参见README。
当发布被合并时,同样会在main分支产生一次推送,因此上面的GitHub Actions会被再次执行,因此我们可以在后面根据release_created
输出内容来判断Release PR是否被合并,即是否会有新发布产生:
# 上面的内容不变
jobs:
release-please:
runs-on: ubuntu-latest
steps:
- id: release # 1
uses: google-github-actions/release-please-action@v4
with:
release-type: node
token: ${{ secrets.GITHUB_TOKEN }}
outputs: # 2
release_created: ${{ steps.release.outputs.release_created }}
tag_name: ${{ steps.release.outputs.release_created }}
build-release:
needs: release-please # 3
runs-on: ubuntu-latest
if: ${{ needs.release-please.outputs.release_created }} # 4
steps:
- name: Echo tag name
run: echo ${{ needs.release-please.outputs.tag_name }}
- 指定这个Step的id,便于传递参数。
- 将Release Please的输出结果暴露出来,在后面继续使用。
- 因为这个
build-release
需要在release-please
之后执行,因此需要指定依赖关系。 - 判断是否产生新的Release,用到了前面Job的输出。
附:使用Tauri构建应用并上传
最后附带一个完整的示例代码,用于在不同平台构建Tauri应用并上传至Release:
name: Create release # 指定名称
on:
push:
branches:
- master # 在master分支推送代码时触发,新项目可能是main分支
permissions: # 指定需要的权限
contents: write
pull-requests: write
jobs:
create-release:
runs-on: ubuntu-latest
steps:
- name: Create release
id: release
uses: google-github-actions/release-please-action@v3 # 使用Release Please Action,并指定类型为node
with:
release-type: node
token: ${{ secrets.GITHUB_TOKEN }} # 使用GitHub自动生成的token
outputs: # 将输出暴露除去
release-created: ${{ steps.release.outputs.release_created }}
tag-name: ${{ steps.release.outputs.tag_name }}
build-tauri:
needs: create-release
if: ${{ needs.create-release.outputs.release-created }}
env: # 指定GH_TOKEN,gh是GitHub自带的CLI,用于实现一些自动化任务,比如向Release上传构建产物等
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
strategy: # 1
fail-fast: false
matrix:
platform: [macos-latest, ubuntu-20.04, windows-latest]
runs-on: ${{ matrix.platform }}
steps:
- name: Echo tag name
run: echo "Build Application ${{ needs.create-release.outputs.tag-name }} for platform ${{ matrix.platform }}."
- name: Checkout repository
uses: actions/checkout@v4
- name: Install dependencies(Ubuntu only) # 安装Linux特定的依赖
if: matrix.platform == 'ubuntu-20.04'
run: |
sudo apt-get update
sudo apt-get install -y libgtk-3-dev libwebkit2gtk-4.0-dev libayatana-appindicator3-dev librsvg2-dev
- name: Rust setup
uses: dtolnay/rust-toolchain@stable
- name: Rust cache
uses: swatinem/rust-cache@v2
with:
workspaces: "./src-tauri -> target"
- name: Sync node version and setup cache
uses: actions/setup-node@v4
with:
node-version: "lts/*"
cache: "yarn"
- name: Install frontend dependencies
run: yarn install
- name: Build Application
id: build
uses: tauri-apps/tauri-action@v0 # 使用tauri-action来构建应用
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} # 传递GITHUB_TOKEN,这是必须的
- name: Upload artifacts(Linux)
if: matrix.platform == 'ubuntu-20.04'
run: | # 在这里使用竖线,将后面的每行代码通过\n连接起来
gh release upload ${{ needs.create-release.outputs.tag-name }} ${{ github.workspace }}/src-tauri/target/release/bundle/deb/*.deb
gh release upload ${{ needs.create-release.outputs.tag-name }} ${{ github.workspace }}/src-tauri/target/release/bundle/appimage/*.AppImage
- name: Upload artifacts(macOS)
if: matrix.platform == 'macos-latest'
run: |
cd ${{ github.workspace }}/src-tauri/target/release/bundle/macos
zip -r Application.app.zip Application.app # 在macOS中,xxx.app是一个文件夹,因此需要打包成一个压缩包
gh release upload ${{ needs.create-release.outputs.tag-name }} ${{ github.workspace }}/src-tauri/target/release/bundle/dmg/*.dmg
gh release upload ${{ needs.create-release.outputs.tag-name }} ${{ github.workspace }}/src-tauri/target/release/bundle/macos/*.app.zip
- name: Upload artifacts(Windows)
if: matrix.platform == 'windows-latest'
run: | # 2
Get-ChildItem -Path "${{ github.workspace }}\src-tauri\target\release\bundle\msi\*.msi" | ForEach-Object {
gh release upload ${{ needs.create-release.outputs.tag-name }} $_.FullName
}
Get-ChildItem -Path "${{ github.workspace }}\src-tauri\target\release\bundle\nsis\*.exe" | ForEach-Object {
gh release upload ${{ needs.create-release.outputs.tag-name }} $_.FullName
}
关于GitHub Actions Matrix
GitHub Actions允许同样的工作在不同的参数下执行,可以是使用的操作系统,也可以是自定义的其他参数。示例代码如下:
jobs:
example_matrix:
strategy:
matrix:
version: [10, 12, 14]
os: [ubuntu-latest, windows-latest]
这段代码会产生6个不同的运行平台,分别是:
- { version: 10, os: ubuntu-latest }
- { version: 10, os: windows-latest }
- { version: 12, os: ubuntu-latest }
- { version: 12, os: windows-latest }
- { version: 14, os: ubuntu-latest }
- { version: 14, os: windows-latest }
每个Job最多能创建256个Matrix,同样可以使用include和exclude来添加或删除运行矩阵,参见添加矩阵 和排除矩阵。
fail-fast
参数默认为true
,当它为true时,只要运行的矩阵中有一个失败,那么所有的矩阵全部停止运行,在这里我们不希望在某个平台构建失败后影响到其他平台的构建结果,因此需要将其设置为false
。
关于运行矩阵的完整内容,参见对作业使用矩阵 - GitHub 文档。
Job运行时使用的Shell
在非Windows平台下的命令行是Bash,在Windows平台下的命令行默认为PowerShell,二者语法有较大差异,因此在跨平台执行任务时需要注意区分。下面列出所支持的全部命令行:
支持的平台 | 命令行选项 | 执行参数 | 是否为默认 |
---|---|---|---|
Linux/macOS | bash -e {0} | 是(非Windows平台下) | |
All | bash | bash --noprofile --norc -eo pipefail {0} | 是(非Windows平台下),没有时使用sh |
All | pwsh | pwsh -command ". ‘{0}’” | |
All | python | python {0} | |
Linux/macOS | sh | sh -e {0} | 非Windows平台下缺失bash时的默认 |
Windows | cmd | %ComSpec% /D /E:ON /V:OFF /S /C "CALL “{0}”” | |
Windows | pwsh | pwsh -command ". ‘{0}’” | 是,Windows平台下 |
Windows | powershell | powershell -command ". ‘{0}’” | 否,旧版PowerShell |
另一个常用的命令行是gh
,即GitHub CLI,关于他的使用方法,可以参见用户手册。