AKARI Tech Blog

燈株式会社のエンジニア・開発メンバーによる技術ブログです

GitHubを使った、質実剛健なWasmのCI/CD構築術

こんにちは、DX Solution事業本部 Devグループの髙橋です!

WebAssembly (Wasm) はフロントエンドのパフォーマンス改善策として注目されており、導入を考えている方も多いのではないでしょうか。

今週のAKARI Tech Blogでは、コストを抑えつつチーム開発を効率化する質実剛健なWasmのCI/CD構築術を紹介します!


今回紹介する Wasm CI/CDの概念図

なぜ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 }}.

手動で更新する時は、例えばpatchバージョンを選択するとpatchを1だけ上げた状態でリリース可能。

依存管理ツールとの連携

フロントエンド側ではyarn installnpm install 時にAssetを自動で取得する仕組みを作り、開発体験を変えるWasmを利用可能にしました。
package.jsonpreinstall フック (npm installyarn 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!

燈では、新しい技術の導入だけでなく、チーム全体の開発効率を高めるための 「仕組み作り」 にも興味があるエンジニアを募集しております!

興味がある方は、ぜひカジュアル面談でお話しましょう!

akariinc.co.jp