title: 玩转Qt(11)-github-Actions自动化发行 photos: /images/QtActions2/2.png tags:
在上一篇文章《github-Actions自动化编译》中,介绍了github-Actions的基本用法,
本文来介绍github-Actions的自动化发布。
先来回顾一下,上一篇文章中的Qt项目的编译流程
安装Qt环境
这一步用第三方Action模板:install-qt-action
获取项目代码
这一步用Actions官方核心模板:actions/checkout
执行qmake、make
这一步用自定义脚本,也可以换成cmake、qbs、gn、ninja等构建工具
执行test
这一步可以引入单元测试、自动化UI测试等。暂无完善的方案,以后再说。
发布
见下文。
Qt程序在编译完成后,发布的大致流程是:
1、 查找依赖库
2、制作压缩包或者安装包
3、上传压缩包或者安装包到网站、网盘。
Qt官方提供的查找依赖库的命令行工具,包括:Windows平台的Windeployqt、MacOS平台的Macosdeployqt。
在这两个平台,只使用Qt库的情况下,这两个工具足够了。
做压缩包比较简单。(我们常说的‘绿色软件’,就是一个压缩包)
一般安装7z、rar之类的压缩工具,用一条命令行就行了。
涛哥这里再说一下,github-Actions给所有平台都提供了PowerShell,而PowerShell内置了压缩命令Compress-Archive。
使用也很简单,只要路径和名字,例如:
Compress-Archive -Path .\MyFolder 'MyRelease.zip'
做安装包,Qt官方有功能很全面的安装包制作工具:QtInstallFrameWork, 稍微翻看一下文档或者例子即可。本文先不展开了。
github 本身提供了'Release'功能,每个仓库都有一个'Release'页面
可以将打包好的压缩包或者安装包,直接上传上去, 供他人下载。
github-Actions还提供了 创建'Release'、上传'Release'的模板
这两个模板的用法也很简单,在yml文件中直接use就行了,不赘述了。
前面介绍了一些简单的理论,接下来通过实例,教大家github-Actions的使用。
以HelloActions-Qt项目为例,做一些定制。
需求如下:
1、每次提交代码,同时在Windows、MacOS、Ubuntu、Android、IOS五个平台编译
2、每次提交tag,在windows和MacOS平台制作软件包,并发布到同一个github-'Release'
需求1已经实现了,着重讨论一下需求2:
'每次提交tag'限定了发布的时机。
涛哥尝试了一番,最终得到答案。
回顾一下, Windows平台的编译配置:
name: Windows
on: [push,pull_request]
jobs:
build:
name: Build
runs-on: windows-latest
strategy:
matrix:
qt_ver: [5.12.6]
qt_target: [desktop]
qt_arch: [win64_msvc2017_64, win32_msvc2017]
include:
- qt_arch: win64_msvc2017_64
msvc_arch: x64
- qt_arch: win32_msvc2017
msvc_arch: x86
# 步骤
steps:
# 安装Qt
- name: Install Qt
uses: jurplel/install-qt-action@v2.0.0
with:
version: ${{ matrix.qt_ver }}
target: ${{ matrix.qt_target }}
arch: ${{ matrix.qt_arch }}
# 拉取代码
- uses: actions/checkout@v1
with:
fetch-depth: 1
# 编译msvc
- name: build-msvc
shell: cmd
env:
vc_arch: ${{ matrix.msvc_arch }}
run: |
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" %vc_arch%
qmake
nmake
steps中的每一个步骤,可以有触发条件。我们可以在这里指定,只有github的事件为tag时才执行:
steps:
。。。
# tag 打包
- name: package
if: startsWith(github.event.ref, 'refs/tags/')
run: |
。。。
这里给出一个实际的打包步骤:
# tag 打包
- name: package
if: startsWith(github.event.ref, 'refs/tags/')
env:
VCINSTALLDIR: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC'
archiveName: ${{ matrix.qt_ver }}-${{ matrix.qt_target }}-${{ matrix.qt_arch }}
targetName: HelloActions-Qt.exe
shell: pwsh
run: |
# 创建文件夹
New-Item -ItemType Directory ${env:archiveName}
# 拷贝exe
Copy-Item bin\${env:targetName} ${env:archiveName}\
# 拷贝依赖
windeployqt --qmldir . ${env:archiveName}\${env:targetName}
# 打包zip
Compress-Archive -Path ${env:archiveName} ${env:archiveName}'.zip'
# 记录环境变量packageName给后续step
$name = ${env:archiveName}
echo "::set-env name=packageName::$name"
# 打印环境变量packageName
Write-Host 'packageName:'${env:packageName}
做一些说明:
其中的VCINSTALLDIR环境变量,是给windeployqt用的。有了这个环境变量,windeployqt会去msvc的安装路径提取‘运行时安装程序’。
打包完以后,将包名设置为环境变量,后续的步骤就可以通过环境变量拿到包名字了。
普通的设置环境变量,在步骤执行完成后就失效了,
这里使用github-Actions的‘记录命令’set-env ,具体可以参考文档github-Actions记录命令
文档说不要用双引号,应该都是针对linux的,我试出来的PowerShell用法如下:
$name = ${env:archiveName}
echo "::set-env name=packageName::$name"
先取环境变量到一个局部变量,再在‘记录命令’中引用局部变量。
如果只有一个平台、一种配置,直接用那两个模板就能解决问题。
这是官方给的例子upload-release-asset:
steps:
- name: Checkout code
uses: actions/checkout@master
- name: Build project # This would actually build your project, using zip for an example artifact
run: |
zip --junk-paths my-artifact README.md
- name: Create Release
id: create_release
uses: actions/create-release@v1.0.0
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
draft: false
prerelease: false
- name: Upload Release Asset
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 }} # This pulls from the CREATE RELEASE step above, referencing it's ID to get its outputs object, which include a `upload_url`. See this blog post for more info: https://jasonet.co/posts/new-features-of-github-actions/#passing-data-to-future-steps
asset_path: ./my-artifact.zip
asset_name: my-artifact.zip
asset_content_type: application/zip
在多平台 或者 多配置的情况下,同一个tag, 只有第一个执行create-release的任务可以成功,后续任务
再次执行create-release时,该tag下已经有了同名的‘Release’,所以会create失败。
这个问题折磨了涛哥好一阵子。找不到现成的解决方案,涛哥就自己实现了一种:
先用github的REST API去判断该tag下有没有‘Release’:
没有则执行create-release,并提取upload_url;
有则提取upload_url。
最后执行upload-release-asset
调用REST API,涛哥依旧使用了方便的PowerShell,
实际的配置如下:
# tag 查询github-Release
- name: queryReleaseWin
id: queryReleaseWin
if: startsWith(github.event.ref, 'refs/tags/')
shell: pwsh
env:
githubFullName: ${{ github.event.repository.full_name }}
ref: ${{ github.event.ref }}
run: |
[string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1)
[string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag}
$response={}
try {
$response = Invoke-RestMethod -Uri $url -Method Get
} catch {
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
# 没查到,输出
echo "::set-output name=needCreateRelease::true"
return
}
[string]$latestUpUrl = $response.upload_url
Write-Host 'latestUpUrl:'$latestUpUrl
if ($latestUpUrl.Length -eq 0) {
# 没查到,输出
echo "::set-output name=needCreateRelease::true"
}
# tag 创建github-Release
- name: createReleaseWin
id: createReleaseWin
if: startsWith(github.event.ref, 'refs/tags/') && steps.queryReleaseWin.outputs.needCreateRelease == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/create-release@v1.0.0
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ github.event.head_commit.message }}
draft: false
prerelease: false
# tag 重定向upload_url到环境变量uploadUrl。
- name: getLatestTagRelease
if: startsWith(github.event.ref, 'refs/tags/')
shell: pwsh
env:
githubFullName: ${{ github.event.repository.full_name }}
upUrl: ${{ steps.createReleaseWin.outputs.upload_url }}
ref: ${{ github.event.ref }}
run: |
# upUrl不为空,导出就完事
if (${env:upUrl}.Length -gt 0) {
$v=${env:upUrl}
echo "::set-env name=uploadUrl::$v"
return
}
# upUrl为空则重新获取
[string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1)
[string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag}
$response = Invoke-RestMethod -Uri $url -Method Get
[string]$latestUpUrl = $response.upload_url
Write-Host 'latestUpUrl:'$latestUpUrl
# 导出
echo "::set-env name=uploadUrl::$latestUpUrl"
Write-Host 'env uploadUrl:'${env:uploadUrl}
# tag 上传Release
- name: uploadRelease
id: uploadRelease
if: startsWith(github.event.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ env.uploadUrl }}
asset_path: ./${{ env.packageName }}.zip
asset_name: ${{ env.packageName }}.zip
asset_content_type: application/zip
name: Windows
on:
# push代码时触发workflow
push:
# 忽略README.md
paths-ignore:
- 'README.md'
- 'LICENSE'
# pull_request时触发workflow
pull_request:
# 忽略README.md
paths-ignore:
- 'README.md'
- 'LICENSE'
jobs:
build:
name: Build
# 运行平台, windows-latest目前是windows server 2019
runs-on: windows-latest
strategy:
# 矩阵配置
matrix:
qt_ver: [5.12.6]
qt_target: [desktop]
qt_arch: [win64_msvc2017_64, win32_msvc2017]
# 额外设置msvc_arch
include:
- qt_arch: win64_msvc2017_64
msvc_arch: x64
qt_arch_install: msvc2017_64
- qt_arch: win32_msvc2017
msvc_arch: x86
qt_arch_install: msvc2017
env:
targetName: HelloActions-Qt.exe
# 步骤
steps:
# 安装Qt
- name: Install Qt
# 使用外部action。这个action专门用来安装Qt
uses: jurplel/install-qt-action@v2.0.0
with:
# Version of Qt to install
version: ${{ matrix.qt_ver }}
# Target platform for build
target: ${{ matrix.qt_target }}
# Architecture for Windows/Android
arch: ${{ matrix.qt_arch }}
# 拉取代码
- uses: actions/checkout@v1
with:
fetch-depth: 1
# 编译msvc
- name: build-msvc
shell: cmd
env:
vc_arch: ${{ matrix.msvc_arch }}
run: |
call "C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC\Auxiliary\Build\vcvarsall.bat" %vc_arch%
qmake
nmake
# tag 打包
- name: package
if: startsWith(github.event.ref, 'refs/tags/')
env:
VCINSTALLDIR: 'C:\Program Files (x86)\Microsoft Visual Studio\2019\Enterprise\VC'
archiveName: ${{ matrix.qt_ver }}-${{ matrix.qt_target }}-${{ matrix.qt_arch }}
shell: pwsh
run: |
# 创建文件夹
New-Item -ItemType Directory ${env:archiveName}
# 拷贝exe
Copy-Item bin\${env:targetName} ${env:archiveName}\
# 拷贝依赖
windeployqt --qmldir . ${env:archiveName}\${env:targetName}
# 打包zip
Compress-Archive -Path ${env:archiveName} ${env:archiveName}'.zip'
# 记录环境变量packageName给后续step
$name = ${env:archiveName}
echo "::set-env name=packageName::$name"
# 打印环境变量packageName
Write-Host 'packageName:'${env:packageName}
# tag 查询github-Release
- name: queryReleaseWin
id: queryReleaseWin
if: startsWith(github.event.ref, 'refs/tags/')
shell: pwsh
env:
githubFullName: ${{ github.event.repository.full_name }}
ref: ${{ github.event.ref }}
run: |
[string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1)
[string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag}
$response={}
try {
$response = Invoke-RestMethod -Uri $url -Method Get
} catch {
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
# 没查到,输出
echo "::set-output name=needCreateRelease::true"
return
}
[string]$latestUpUrl = $response.upload_url
Write-Host 'latestUpUrl:'$latestUpUrl
if ($latestUpUrl.Length -eq 0) {
# 没查到,输出
echo "::set-output name=needCreateRelease::true"
}
# tag 创建github-Release
- name: createReleaseWin
id: createReleaseWin
if: startsWith(github.event.ref, 'refs/tags/') && steps.queryReleaseWin.outputs.needCreateRelease == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/create-release@v1.0.0
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ github.event.head_commit.message }}
draft: false
prerelease: false
# 重定向upload_url到环境变量uploadUrl。
- name: getLatestTagRelease
# tag 上一步无论成功还是失败都执行
if: startsWith(github.event.ref, 'refs/tags/')
shell: pwsh
env:
githubFullName: ${{ github.event.repository.full_name }}
upUrl: ${{ steps.createReleaseWin.outputs.upload_url }}
ref: ${{ github.event.ref }}
run: |
# upUrl不为空,导出就完事
if (${env:upUrl}.Length -gt 0) {
$v=${env:upUrl}
echo "::set-env name=uploadUrl::$v"
return
}
[string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1)
[string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag}
$response = Invoke-RestMethod -Uri $url -Method Get
[string]$latestUpUrl = $response.upload_url
Write-Host 'latestUpUrl:'$latestUpUrl
echo "::set-env name=uploadUrl::$latestUpUrl"
Write-Host 'env uploadUrl:'${env:uploadUrl}
# tag 上传Release
- name: uploadRelease
id: uploadRelease
if: startsWith(github.event.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ env.uploadUrl }}
asset_path: ./${{ env.packageName }}.zip
asset_name: ${{ env.packageName }}.zip
asset_content_type: application/zip
name: MacOS
on:
push:
paths-ignore:
- 'README.md'
- 'LICENSE'
pull_request:
paths-ignore:
- 'README.md'
- 'LICENSE'
jobs:
build:
name: Build
runs-on: ${{ matrix.os }}
strategy:
matrix:
os: [macos-latest]
qt_ver: [5.12.6]
qt_arch: [clang_64]
env:
targetName: HelloActions-Qt
steps:
- name: Install Qt
uses: jurplel/install-qt-action@v2.0.0
with:
version: ${{ matrix.qt_ver }}
- uses: actions/checkout@v1
with:
fetch-depth: 1
- name: build macos
run: |
qmake
make
# tag 打包
- name: package
if: startsWith(github.event.ref, 'refs/tags/')
run: |
# 拷贝依赖
macdeployqt bin/${targetName}.app -qmldir=. -verbose=1 -dmg
# tag 查询github-Release
- name: queryRelease
id: queryReleaseMacos
if: startsWith(github.event.ref, 'refs/tags/')
shell: pwsh
env:
githubFullName: ${{ github.event.repository.full_name }}
ref: ${{ github.event.ref }}
run: |
[string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1)
[string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag}
$response={}
try {
$response = Invoke-RestMethod -Uri $url -Method Get
} catch {
Write-Host "StatusCode:" $_.Exception.Response.StatusCode.value__
Write-Host "StatusDescription:" $_.Exception.Response.StatusDescription
# 没查到,输出
echo "::set-output name=needCreateRelease::true"
return
}
[string]$latestUpUrl = $response.upload_url
Write-Host 'latestUpUrl:'$latestUpUrl
if ($latestUpUrl.Length -eq 0) {
# 没查到,输出
echo "::set-output name=needCreateRelease::true"
}
# tag 创建github-Release
- name: createReleaseWin
id: createReleaseWin
if: startsWith(github.event.ref, 'refs/tags/') && steps.queryReleaseMacos.outputs.needCreateRelease == 'true'
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/create-release@v1.0.0
with:
tag_name: ${{ github.ref }}
release_name: Release ${{ github.ref }}
body: ${{ github.event.head_commit.message }}
draft: false
prerelease: false
# 重定向upload_url到环境变量uploadUrl。
- name: getLatestTagRelease
# tag 上一步无论成功还是失败都执行
if: startsWith(github.event.ref, 'refs/tags/')
shell: pwsh
env:
githubFullName: ${{ github.event.repository.full_name }}
upUrl: ${{ steps.queryReleaseMacos.outputs.upload_url }}
ref: ${{ github.event.ref }}
run: |
# upUrl不为空,导出就完事
if (${env:upUrl}.Length -gt 0) {
$v=${env:upUrl}
echo "::set-env name=uploadUrl::$v"
return
}
[string]$tag = ${env:ref}.Substring(${env:ref}.LastIndexOf('/') + 1)
[string]$url = 'https://api.github.com/repos/' + ${env:githubFullName} + '/releases/tags/' + ${tag}
$response = Invoke-RestMethod -Uri $url -Method Get
[string]$latestUpUrl = $response.upload_url
Write-Host 'latestUpUrl:'$latestUpUrl
echo "::set-env name=uploadUrl::$latestUpUrl"
Write-Host 'env uploadUrl:'${env:uploadUrl}
# tag 上传Release
- name: uploadRelease
id: uploadRelease
if: startsWith(github.event.ref, 'refs/tags/')
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
uses: actions/upload-release-asset@v1.0.1
with:
upload_url: ${{ env.uploadUrl }}
asset_path: ./bin/${{ env.targetName }}.dmg
asset_name: ${{ env.targetName }}.dmg
asset_content_type: application/applefile
代码在github HelloActions-Qt
另外在涛哥的Qml控件库TaoQuick,也使用了这一套配置