デスクトップアプリのデリバリーを自動化する
はじめに
こんにちは、PKSHA Communication の林良祐です。
私が担当している PKSHA Speech Insight(PSI)というプロダクトでは、Windows デスクトップアプリをお客様に提供しています。
▼PSI のプロダクト紹介記事はこちら
デスクトップアプリはブラウザで動く Web アプリとは異なり、お客様の環境にアプリをインストールしていただく必要があります。Microsoft Store で配布されるアプリもありますが、我々のデスクトップアプリは契約いただいているお客様にインストーラーを配布し、それをインストールしていただく方式になっています。
新機能開発などを積極的に行っているので、多いときは約 2 週間に一回くらいのペースでバージョンアップを行っています。iOS アプリや Android アプリなどは、IDE の機能や fastlane を用いることで配布でき、アプリの自動アップデートなどはプラットフォーム側が自動で行ってくれます。しかし、インストーラーを配布する方式では、インストーラーの作成や配布、自動アップデートの仕組みなどは自身で構築する必要があります。
本記事では、GitHub Actions によるデスクトップアプリのインストーラー作成の仕組みについて記述します。
アプリの概要
デスクトップアプリの実装の詳細や、運用については本筋ではないので詳細には記述しませんが、ビルドや配布、自動アップデートの仕組みとは不可分なので、概要を記述します。
デスクトップアプリは Flutter と C++(Visual C++)で開発されています。
ほとんどの機能は Flutter で実装されていますが、一部 Windows API を使う必要がある部分は C++ で実装されています。以下、psi_app は Flutter のアプリケーションで、 native_app は C++ のアプリケーションです。native_app.exe は Flutter の asset に置かれ、psi_app 側から起動されます。それぞれのアプリケーションプロセスは gRPC で通信をしています。
API なども含め、サービスの環境としては development(dev), staging(stg), production(prd)の3つがあります。dev 環境は主にエンジニアが開発のために使う環境で、stg 環境は QA を行ったり、BizDev がリリース前に検証を行ったりするための環境です。prd 環境が実際にお客様に提供している環境になります。Flutter ビルドの際は、Flavor で環境を切り替え、設定値などを切り替えています。
ビルド & インストーラー作成
インストーラーの作成は GitHub Actions で行っており、actions と workflows は以下のような構成になっています。
.github
|- actions/build-app
| |- actions.yaml
|
|- workflows
|- build-dev.yaml
|- build-prd.yaml
|- build-stg.yaml
build-dev は main branch に PR がマージされたタイミング、もしくは PR コメントをトリガーに走るようになっており、 build-stg と build-prd は tag が push されたタイミングでトリガーされるようになっています。
actions では PR コメントをトリガーにしてビルドしたり、環境名をアプリのアイコンに入れたりなど、いくつか工夫をしているのですが、本筋とは外れるので付録を参照ください。
build-prd はアプリをビルドするジョブと、インストーラーを作成する 2 つのジョブで構成されています。
ビルド
まず、ビルドするジョブは、以下のようになっています。
実際にはもう少し処理を行っていますが、重要な部分だけを載せています。
jobs:
build:
runs-on:
labels: windows-latest-x64-32core
steps:
- uses: actions/checkout@v4
- name: install yq
run: choco install yq
- uses: ./.github/actions/build-app
with:
flavor: prd
- name: make iss file
run: |
$version = yq '.version' psi_app\\pubspec.yaml
$iss = Get-Content inno\\setup.iss.template
$iss = $iss.Replace('$APP_NAME_SUFFIX', '')
$iss = $iss.Replace('$APP_VERSION', $version)
$iss = $iss.Replace('$SIGN_TOOL', 'signtool')
Set-Content setup.iss $iss
- name: make build-artifact
run: |
mkdir build
mv setup.iss build
mv psi_app\\build\\windows\\x64\\runner\\Release build\\dist
mkdir build\\misc
curl.exe "https://aka.ms/vs/17/release/vc_redist.x86.exe" -o .\\build\\misc\\vc_redist.x86.exe -L -vvv
- uses: actions/upload-artifact@v4
with:
name: prd-build
path: build
Windows アプリをビルドするので、runner は windows です。
標準の hosted runner ではビルドに時間がかかるので、larger runner を用いており、windows-latest-x64-32core はそのラベルです。
実際にビルドする処理は Composite Actions(./.github/actions/build-app)に切り出しています。
name: build app
inputs:
flavor:
required: true
runs:
using: 'composite'
steps:
- id: flutter-version
run: |
$VERSION=(cat psi_app/.fvmrc | jq -r .flutter)
echo "flutter-version=$VERSION" | Out-File -FilePath $Env:GITHUB_OUTPUT -Encoding utf8 -Append
shell: pwsh
- id: flutter-action
uses: subosito/flutter-action@v2
with:
flutter-version: ${{ steps.flutter-version.outputs.flutter-version }}
channel: "stable"
cache: true
- uses: microsoft/setup-msbuild@v1.3
- name: cache
id: cache-native
uses: actions/cache@v4
with:
path: psi_app/assets/native_app
key: native_app-${{ hashFiles('native_app') }}
- name: build
if: steps.cache-native.outputs.cache-hit != 'true'
run: |
vcpkg integrate install
msbuild native/native.vcxproj -p:Configuration=Release -p:Platform=x86 -p:DebugSymbols=false -p:DebugType=None -p:OutDir=Release -p:VcpkgTriplet=x86-windows -p:VcpkgHostTriplet=x86-windows
working-directory: native_app
shell: pwsh
- name: copy native_app
if: steps.cache-native.outputs.cache-hit != 'true'
run: |
cp -r "native_app/native_app/Release/*" "psi_app/assets/native_app"
shell: pwsh
- run: flutter pub get
shell: pwsh
working-directory: psi_app
- run: flutter pub run build_runner build --delete-conflicting-outputs
shell: pwsh
working-directory: psi_app
- name: build app
run: flutter build windows --verbose --target lib/main.dart --dart-define-from-file=env/${{ inputs.flavor }}.json
shell: pwsh
working-directory: psi_app
ビルド処理は、C++ で記述された native_app.exe のビルドと、Flutter アプリのビルドの大きく 2 つを行っています。Visual C++ のビルド処理は、 microsoft/setup-msbuild を用いて行っています。Platform=x86 となっているのは、一部の依存系が 32bit でしか動かないためです。
ビルド後、Inno setup でインストーラーを作成するための iss ファイルを作成し、それらを artifact としてアップロードしています。
インストーラー作成
jobs:
build:
# 省略
make-installer:
needs: build
runs-on:
labels: windows-latest-8core
environment:
name: production
permissions:
id-token: write
contents: read
steps:
- uses: actions/download-artifact@v4
with:
name: prd-build
path: build
- name: install azure sign tool
run: dotnet tool install --no-cache --global AzureSignTool --version 4.0.1
- name: azure login
uses: azure/login@v2
with:
client-id: ${{ secrets.AZURE_CLIENT_ID }}
tenant-id: ${{ secrets.AZURE_TENANT_ID }}
subscription-id: ${{ secrets.AZURE_SUBSCRIPTION_ID }}
- name: azure token
run: |
$az_token=$(az account get-access-token --scope "https://vault.azure.net/.default" --query accessToken --output tsv)
echo "::add-mask::$az_token"
echo "AZ_TOKEN=$az_token" | Out-File -FilePath $env:GITHUB_ENV -Encoding utf8 -Append
- name: install Inno setup
run: choco install innosetup
- name: build singed installer
run: 'iscc /Ssigntool="azuresigntool.exe sign --verbose -kvu https://code-signing-xxxxxx.vault.azure.net/ -kvc xxxxxx -kva %AZ_TOKEN% -fd sha256 -tr http://timestamp.globalsign.com/tsa/r6advanced1 -s $f" setup.iss'
shell: cmd
working-directory: build
- name: make installer-artifact
run: |
mkdir installer
mv build\\setup-psi-*.exe installer
- uses: actions/upload-artifact@v4
with:
name: prd-installer
path: installer
インストーラーの作成は Inno Setup を用いています。
以下は、iss ファイルのテンプレートで、ビルドの際に $APP_NAME_SUFFIX や $APP_VERSION を書き換えるようにしています。
#define AppId "{{xxxxxxxx-xxxx-xxxx-xxxx-xxxxxxxxxxxx}" + "$APP_NAME_SUFFIX"
#define AppName "PKSHA Speech Insight" + "$APP_NAME_SUFFIX"
#define AppVersion "$APP_VERSION"
#define AppPublisher "PKSHA Communication Inc."
#define AppURL "<https://aisaas.pkshatech.com/speechinsight/>"
#define AppExeName "psi_app.exe"
[Setup]
AppId={#AppId}
AppName={#AppName}
AppVersion={#AppVersion}
AppVerName={#AppName} {#AppVersion}
AppPublisher={#AppPublisher}
AppPublisherURL={#AppURL}
AppSupportURL={#AppURL}
AppUpdatesURL={#AppURL}
DefaultDirName={autopf}\\{#AppName}
DisableProgramGroupPage=yes
OutputDir=.
OutputBaseFilename=setup-psi-{#AppVersion}
Compression=lzma
SolidCompression=yes
WizardStyle=modern
PrivilegesRequiredOverridesAllowed=dialog
#if '$SIGN_TOOL' != ''
SignTool=$SIGN_TOOL
#endif
[Languages]
Name: "english"; MessagesFile: "compiler:Default.isl"
Name: "japanese"; MessagesFile: "compiler:Languages\\Japanese.isl"
[Tasks]
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}"; Flags: unchecked
[Files]
Source: "dist\\{#AppExeName}"; DestDir: "{app}"; Flags: ignoreversion
Source: "dist\\*"; DestDir: "{app}"; Flags: ignoreversion recursesubdirs createallsubdirs
Source: "misc\\vc_redist.x86.exe"; DestDir: "{tmp}"; Flags: dontcopy
[Icons]
Name: "{autoprograms}\\{#AppName}"; Filename: "{app}\\{#AppExeName}"
Name: "{autodesktop}\\{#AppName}"; Filename: "{app}\\{#AppExeName}"; Tasks: desktopicon
[Run]
Filename: "{app}\\{#AppExeName}"; Description: "{cm:LaunchProgram,{#StringChange(AppName, '&', '&&')}}"; Flags: nowait postinstall
Filename: "{tmp}\\vc_redist.x86.exe"; StatusMsg: "Installing VC++ Redistributables..."; Parameters: "/quiet"; Check: VC2017RedistNeedsInstall; Flags: waituntilterminated
[Code]
function VC2017RedistNeedsInstall: Boolean;
var
Version: String;
begin
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\x64', 'Version', Version) then
begin
Log('VC Redist Version check : found ' + Version);
Result := (CompareStr(Version, 'v14.40.33810.0')<0);
end
else
begin
Result := True;
end;
if (Result) then
begin
ExtractTemporaryFile('vc_redist.x86.exe');
end;
end;
dev, stg ではインストーラーに署名を行っていないので $SIGN_TOOL='' を、prd のときは署名を行うため $SIGN_TOOL=signtool を指定します。
必要なファイルを集めたら、iscc setup.iss を実行してインストーラーを作成します。prd の場合は、後述の azuresigntool によるコード署名のため、/S オプションで署名処理を指定しています。
作成されたインストーラーは artifact としてアップロードし、リリース前の最終動作確認などを行った後に、S3 にアップロードし、お客様の下に配信されます。
コードサイニング
コードサイニングは、デジタル署名技術を使用してソフトウェアの配布元がそのソフトウェアを正式に認証し、改ざんされていないことを保証するプロセスです。
このプロセスにより、ソフトウェアが信頼できるソースから来ていることをエンドユーザーが確認できます。Windows の場合、署名されていないインストーラーでソフトウェアをインストールしようとすると、SmartScreen が表示されます。
コードサイニングするためにはコードサイニング証明書が必要です。
コードサイニング証明書は外部の機関から購入するのですが、コードサイニング証明書の秘密鍵は、FIPS140 Level2、 Common Criteria EAL 4+、または同等のセキュリティ要件を満たすハードウェアに格納されて提供されます。
なので、.pfx ファイルなどではなく、USB トークンや HSM(Hardware Security Module)の形で提供されます。
以前は USB トークンで署名を行っていたのですが、この部分は手動で作業する必要があり、USB トークンの管理なども含め、非常に手間がかかっていました。なので、GlobalSign のコードサイニング証明書と、Microsoft Azure Key Vault を利用し、この署名も GitHub Actions 内で行うようにしました。
まず、GlobalSign のコードサイニング証明書を以下の手順で Azure Key Vault にインポートします。
Action では、Azure にログインしてトークンを取得し、そのトークンを使って、azuresigntool.exe sign で署名することができます。
azuresigntool.exe sign --verbose -kvu <azure-key-vault-url> -kvc <friendly-name> -kva <azure-token> -fd sha256 -tr http://timestamp.globalsign.com/tsa/r6advanced1 -s <target-file>
Inno setup で署名を行うときは、/S で上記コマンドを指定することで、azuresigntool による署名を行うことができます。
まとめ
この記事では、GitHub Actions 上で PSI デスクトップアプリをビルドし、署名されたインストーラーを作成する手順について紹介しました。
我々は、開発したプロダクトを、お客様に安定して早く、安全にデリバリするために、自動化できるところは可能な限り自動化するようにしています。
インストーラー作成までの処理を自動化することで、開発や QA のイテレーションを早く回すことができるようになりました。
また、コードサイニングまで含めて Actions 上で行えるようになったことで、USB トークンの管理が不要になり、いつでも安全に prd アプリの作成ができるようになりました。
付録
コメントをトリガーにしてアプリをビルドする
PR に /build-dev とコメントすると、そのブランチで dev のアプリをビルドするようにしています。
name: build-dev
on:
push:
branches:
- develop
tags-ignore:
- "*"
issue_comment:
types:
- created
jobs:
build:
runs-on:
labels: windows-latest-x64-32core
permissions:
contents: write
issues: read
pull-requests: read
if: |
github.event_name == 'push' ||
(
github.event_name == 'issue_comment'
&& github.event.action == 'created'
&& github.event.issue.pull_request != null
&& startsWith(github.event.comment.body, '/build-dev')
)
steps:
- name: Get PR branch
uses: xt0rted/pull-request-comment-branch@v2
id: get-branch
if: github.event_name == 'issue_comment'
- name: checkout
uses: actions/checkout@v4
with:
ref: ${{ steps.get-branch.outputs.head_ref }}
if: github.event_name == 'issue_comment'
- uses: actions/checkout@v4
if: github.event_name == 'push'
# 省略
- name: Comment version.json
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
run: gh pr comment "${{ github.event.issue.number }}" -R PKSHATechnology/psi-app -F info.txt
if: github.event_name == 'issue_comment'
develop ブランチにマージされたときは github.event_name が push になり、PR にコメントされたときは issue_comment となります。checkout する際に、issue_comment の場合はブランチを指定する必要があるので、いくつか分岐させています。また、issue_comment の場合はアップロード先などを PR にコメントするようにしています。
dev/stg 環境のアプリはアイコンに環境名を入れる
imagemagick でアイコンに文字を入れることができます。
- run: magick .\\psi_app\\windows\\runner\\resources\\app_icon.ico -pointsize 96 -fill red -annotate +80+80 'dev' .\\psi_app\\windows\\runner\\resources\\app_icon.ico
タグで指定された version と pubspec.yaml の version が一致しているか確認する
stg/prd アプリを作成するときは、prd-v1.2.3 のようにタグを打っているのですが、pubspec.yaml のバージョンの修正を忘れることがあります。
そのまま出してしまうとややこしいことになってしまうので、以下のチェックを入れています。
name: build-prd
on:
push:
tags:
- "prd-v*"
jobs:
# 省略
steps:
- name: check version
run: |
$version = yq '.version' psi_app\\pubspec.yaml
$tag_version = $ENV:GITHUB_REF.Replace("refs/tags/prd-v", "")
If ($version -ne $tag_version) {
exit 1
}
インストール時に Visual C++ 再頒布可能パッケージがインストールされていないときにインストールする
Visual C++ 再頒布可能パッケージは Visual C++ で構築されたアプリケーションで必要になるライブラリです。
環境によって既にインストールされていたり、されていなかったりなので、インストール時に確認して、インストールされていない場合はインストールするようにしています。
Visual C++ 再頒布可能パッケージをインストールするために、 iss ファイルに以下の内容を記述しています。
vc_redist.x86.exe は MS の web ページからダウンロードしてきたインストーラーです。
Code ブロックに、Visual C++ 再頒布可能パッケージがインストールされているかを確認する関数が定義されています。
VCRedistNeedsInstall が true のとき、 [Run] にあるインストール処理が実行されます。
[Files]
...
Source: "misc\\vc_redist.x86.exe"; DestDir: "{tmp}"; Flags: dontcopy
[Run]
...
Filename: "{tmp}\\vc_redist.x86.exe"; StatusMsg: "Installing VC++ Redistributables..."; Parameters: "/quiet"; Check: VCRedistNeedsInstall; Flags: waituntilterminated
[Code]
function VCRedistNeedsInstall: Boolean;
var
Version: String;
begin
if RegQueryStringValue(HKEY_LOCAL_MACHINE, 'SOFTWARE\\Microsoft\\VisualStudio\\14.0\\VC\\Runtimes\\x64', 'Version', Version) then
begin
Log('VC Redist Version check : found ' + Version);
Result := (CompareStr(Version, 'v14.40.33810.0')<0);
end
else
begin
Result := True;
end;
if (Result) then
begin
ExtractTemporaryFile('vc_redist.x86.exe');
end;
end;
おわりに
PKSHA Speech Insight をはじめ、AI SaaS の各プロダクトでは社会実装に取り組んでいく仲間を募集中です。
お客様に快適にプロダクトを使っていただけるように、新機能開発はもちろんのこと、本記事で紹介したようなデリバリープロセスの改善なども積極的に行っています。
興味を持っていただけた方は、ぜひカジュアル面談しましょう!
―INFORMATION―
▼ 中途採用:ソフトウエアエンジニアの募集要項はこちら
▼ 中途採用:全職種の募集要項はこちら
▼ PKSHA 採用サイト
▼ Wantedly はこちら