こんにちは、DX Solution事業本部 Devグループの髙橋です!
WebAssembly (Wasm) はフロントエンドのパフォーマンス改善策として注目されており、導入を考えている方も多いのではないでしょうか。
今週のAKARI Tech Blogでは、コストを抑えつつチーム開発を効率化する質実剛健なWasmのCI/CD構築術を紹介します!
- なぜWasm?
- 結論、Github Actions x Release Assets がオススメ!
- Github ActionsでWasmをビルド&配布(Rustの例)
- 依存管理ツールとの連携
- 最後に
- We’re Hiring!
なぜWasm?
燈では、ドメインに特化した複雑な計算やアルゴリズムをソフトウェアで使う場面が多々あります。
私が携わったプロジェクトでは、Pythonで〜数十秒かかるような計算結果を待ち時間なく表示するようなUI/UXが必要不可欠でした。
「計算負荷」と「操作性」を両立させるべく採用したのが、 WebAssembly (Wasm) でした。
しかし、チーム開発でWasmを導入すると、「ビルド成果物をどのように開発フローに組み込むか」 という運用上の課題が浮上します。
特に今回はアルゴリズムとソフトウェアとで担当者が異なっていたため、バージョンを管理できることが必須要件でした。
結論、Github Actions x Release Assets がオススメ!
今回紹介するのは、コストを抑えつつチーム開発を効率化する質実剛健なこちらの方法です!
1. GIthub ActionsでビルドしてRelease Assetsにバージョンごとにzip保存
2. フロントエンド依存管理ツール内でGitHub CLIを使い、Assetを自動取得
依存管理ツールにGitHub CLIを挟むことで、開発体験を全く変えずにWasmが導入できました!
※ 「GitHub Packages」も候補に挙がりましたが、ストレージやデータ転送量による従量課金や、認証周りの複雑さが懸念されます。対して「Release Assets」はバイナリ配布に特化しており、基本的に無料のため、小〜中規模開発におけるレジストリとして便利です。
GitHub Release と Assets について
GitHub Releaseは、Gitのタグ(バージョン)に紐づくリリースノートや成果物を管理する機能です。
変更履歴を記載したり、ソースコードには含めたくないビルド済みファイルをRelease Assetsとして添付したりできます。
今回の構成におけるメリットは以下の通りです。
- バイナリ配布に特化: ソースコードとは別に、ビルド済みのWasmパッケージ(zip)をAssetsとして管理可能
- セキュアな配布: リポジトリがPrivateであれば、ReleaseページやAssetsへのアクセスもGitHub認証(Token)によって保護されます。機密なロジックを含むWasmであっても、外部に公開されることなく安全にチーム内で配布可能です。
Github ActionsでWasmをビルド&配布(Rustの例)
WasmのビルドはGitHub Actionsで行うようにしました。例えばdevelopブランチへのマージ時に自動ビルド&配布するなど、GitHub Actions は CI/CD に欠かせません。
↓は、RustファイルをTypescript向けにビルドするworkflowのサンプルです。Releaseバージョンも自動で更新されるようにしてみました。
Rustをwasmにビルドするworkflow
name: Rust Wasm Release # 手動、もしくはdevelopブランチにマージされたらバージョン更新 on: workflow_dispatch: inputs: version_update_type: description: "アップデートするバージョンの種類 (v{major}.{minor}.{patch})" required: true type: choice options: - major - minor - patch default: patch push: branches: - develop paths: - 'path/to/rust-project/**' permissions: contents: write jobs: prepare-release: runs-on: ubuntu-latest outputs: version: ${{ steps.set_new_version.outputs.version }} steps: - name: Checkout develop branch uses: actions/checkout@v4 with: ref: develop fetch-depth: 0 # developにマージされた時は "major" を更新、手動実行時は入力値に従う - name: Calculate Next Version id: set_new_version env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | # 現バージョンの取得 LATEST_TAG=$(gh release list \ --exclude-pre-releases \ --exclude-drafts \ --json tagName \ --jq '.[0].tagName') || LATEST_TAG="" if [ -z "$LATEST_TAG" ]; then CURRENT_VERSION="0.0.0" else CURRENT_VERSION=${LATEST_TAG#v} fi VERSION_PARTS=$(echo "$CURRENT_VERSION" | tr '.' ' ') read -r MAJOR MINOR PATCH <<< "$VERSION_PARTS" # 手動実行か、Developブランチへのマージ時かを判定 if [ "${{ github.event_name }}" == "workflow_dispatch" ]; then UPDATE_TYPE=${{ inputs.version_update_type }} else UPDATE_TYPE="major" fi echo "Current: v$CURRENT_VERSION" echo "Update type: $UPDATE_TYPE" # 新バージョンの計算 if [ "$UPDATE_TYPE" = "major" ]; then MAJOR=$((MAJOR + 1)); MINOR=0; PATCH=0 elif [ "$UPDATE_TYPE" = "minor" ]; then MINOR=$((MINOR + 1)); PATCH=0 elif [ "$UPDATE_TYPE" = "patch" ]; then PATCH=$((PATCH + 1)) fi NEW_VERSION="v${MAJOR}.${MINOR}.${PATCH}" echo "New version: $NEW_VERSION" # outputにセット echo "version=$NEW_VERSION" >> "$GITHUB_OUTPUT" build-and-release: needs: prepare-release runs-on: ubuntu-latest defaults: run: working-directory: path/to/rust-project. env: RELEASE_VERSION: ${{ needs.prepare-release.outputs.version }} WASM_DIRECTORY_IN_ZIP: ${{ vars.WASM_DIRECTORY_IN_ZIP }} steps: - name: Checkout develop branch uses: actions/checkout@v4 with: ref: develop - name: Set up Rust uses: dtolnay/rust-toolchain@stable - name: Cache dependencies uses: Swatinem/rust-cache@v2 with: workspaces: path/to/rust-project. - name: Install wasm-pack run: cargo install wasm-pack - name: Build Wasm run: wasm-pack build --release --target web --out-dir ${{ env.WASM_DIRECTORY_IN_ZIP }} - name: Archive Wasm artifacts run: | zip -r "${{ github.workspace }}/wasm-pkg-${{ env.RELEASE_VERSION }}.zip" ${{ env.WASM_DIRECTORY_IN_ZIP }} - name: Create Release and Upload Assets uses: softprops/action-gh-release@v2 env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: tag_name: ${{ env.RELEASE_VERSION }} target_commitish: develop files: | wasm-pkg-${{ env.RELEASE_VERSION }}.zip fail_on_unmatched_files: true name: Release ${{ env.RELEASE_VERSION }} body: | Wasm package (target: web) for version ${{ env.RELEASE_VERSION }}.
依存管理ツールとの連携
フロントエンド側ではyarn install や npm install 時にAssetを自動で取得する仕組みを作り、開発体験を変えるWasmを利用可能にしました。
package.json の preinstall フック (npm install や yarn install が実行される直前に走るスクリプト) に、Asset取得用のシェルスクリプトを指定することで実現しました。
package.json
{ "name": "frontend", "private": true, "type": "module", "scripts": { ... "preinstall": "bash ./scripts/fetch-github-assets.sh" }, ... }
fetch-github-assets.sh
#!/bin/sh REPO="your-repository-name" VENDOR_DIR="./vendor" # zip内のディレクトリ名 # GitHub ActionsでLintチェックする場合も対応可能。 WASM_DIRECTORY_IN_ZIP="$WASM_DIRECTORY_IN_ZIP" # ghがインストールされているかチェック echo "Checking dependencies..." if ! command -v gh &> /dev/null then echo "Error: gh (GitHub CLI) could not be found." echo "Please install gh CLI and authenticate using 'gh auth login'." exit 1 fi # latestタグを取得 echo "Fetching latest release tag from $REPO..." LATEST_TAG=$(gh release view --repo "$REPO" --json tagName --jq .tagName) if [ $? -ne 0 ] || [ -z "$LATEST_TAG" ]; then echo "Failed to get the latest tag name from $REPO." exit 1 fi echo "Latest tag found: $LATEST_TAG" ASSET_NAME="wasm-pkg-${LATEST_TAG}.zip" # zipのダウンロード echo "Downloading $ASSET_NAME..." gh release download "$LATEST_TAG" \ --repo "$REPO" \ --pattern "$ASSET_NAME" \ --dir "." \ --clobber if [ $? -ne 0 ]; then echo "Failed to download Wasm package using gh." echo "Please check if you are authenticated ('gh auth status') and have access to the repo." rm -f "$ASSET_NAME" exit 1 fi if [ ! -f "$ASSET_NAME" ]; then echo "Error: Downloaded file $ASSET_NAME not found." exit 1 fi # zip解凍 echo "Unzipping $ASSET_NAME..." unzip -o "$ASSET_NAME" if [ $? -ne 0 ]; then echo "Failed to unzip Wasm package" rm -f "$ASSET_NAME" exit 1 fi # package.jsonからパッケージ名を取得して ./vendor/<package-name> にインストールする echo "Installing package to $TARGET_DIR..." PACKAGE_JSON_PATH="$WASM_DIRECTORY_IN_ZIP/package.json" if [ ! -f "$PACKAGE_JSON_PATH" ]; then echo "Error: $PACKAGE_JSON_PATH not found after unzip." rm -f "$ASSET_NAME" rm -rf "$WASM_DIRECTORY_IN_ZIP" exit 1 fi # jq があれば jq を使う if command -v jq &> /dev/null; then PACKAGE_NAME=$(jq -r .name "$PACKAGE_JSON_PATH") else PACKAGE_NAME=$(grep '"name"' "$PACKAGE_JSON_PATH" | head -n 1 | sed -E 's/.*"name"[[:space:]]*:[[:space:]]*"([^"]+)".*/\1/') fi if [ -z "$PACKAGE_NAME" ]; then echo "Failed to extract package name from $PACKAGE_JSON_PATH." rm -f "$ASSET_NAME" rm -rf "$WASM_DIRECTORY_IN_ZIP" exit 1 fi TARGET_DIR="$VENDOR_DIR/$PACKAGE_NAME" PACKAGE_JSON_CHECK="$TARGET_DIR/package.json" # インストール echo "Installing package to $TARGET_DIR..." mkdir -p "$TARGET_DIR" if [ -d "$WASM_DIRECTORY_IN_ZIP" ]; then echo "Moving contents from $WASM_DIRECTORY_IN_ZIP to $TARGET_DIR..." # * (通常ファイル) と .[!.]* (ドットファイル) の両方を移動対象にする mv "$WASM_DIRECTORY_IN_ZIP"/* "$TARGET_DIR/" 2>/dev/null || true mv "$WASM_DIRECTORY_IN_ZIP"/.[!.]* "$TARGET_DIR/" 2>/dev/null || true rmdir "$WASM_DIRECTORY_IN_ZIP" else echo "Error: $WASM_DIRECTORY_IN_ZIP not found. Installation failed." rm -f "$ASSET_NAME" rm -rf "$TARGET_DIR" exit 1 fi rm "$ASSET_NAME" echo "Wasm package installed successfully to $TARGET_DIR" exit 0
「GitHubから最新のWasmを取得して配置する処理」を挟むことで、いつも通り yarn insatll するだけで自動的にWasmのセットアップも完了するようになります。
最後に
今回は、コストを抑えつつ開発体験も損なわないWasmのCI/CD構築術を紹介しました。
燈では、Wasmのような新しい技術を単に導入するだけでなく、効率的に運用する方法も日々追求しています。
さらに、効率的な運用方法を仕組み化して横展開することで、事業部全体としての開発効率向上も図っています。
We’re Hiring!
燈では、新しい技術の導入だけでなく、チーム全体の開発効率を高めるための 「仕組み作り」 にも興味があるエンジニアを募集しております!
興味がある方は、ぜひカジュアル面談でお話しましょう!