この記事では、GitHub で管理している JS ライブラリプロジェクトに対して、タグをpushするだけで VPS へ SSH + rsync で自動デプロイされる GitHub Actions ワークフローを最小構成で作った手順をまとめています。SSH 鍵の準備・GitHub Secrets の登録・deploy.yml の作成まで一通り実装でき、手作業でのデプロイが不要になります。CI/CD の仕組みが「GitHub上のLinuxでシェルを実行するしくみ」だと分かると、他のプロジェクトへの応用も簡単です。

はじめに

2025年の今回初めてGitHubのCI/CDを使ってみてとても便利だったので、やり方・手順を備忘録として残しました。

今回対象となるプロジェクトは個人で開発している小さなjsライブラリプロジェクトです。

GitHubで管理・公開しているこのnu.jsにタグをつけてプッシュした時点で、以下の処理をCI/CDで自動化させます。

  1. バージョンタグを取得する
  2. コンテンツをビルドする
  3. プレースホルダーに①を埋め込む
  4. 公開用のVPSサーバーへSSHで接続
  5. rsyncでコンテンツを同期する

と、こんな感じです。シェル一発でもできる作業なので、わざわざCI/CDで実現しなくても良さそうではありますが、実際やってみるとこれが結構便利で楽しかったです。なるほど、CI/CDというのはGitHub上にLinuxを立ち上げて、pushなどをトリガーにしてシェルを実行させるような仕組みなのですね! 仮想のmacOSなども実行できるので、iOSアプリのリリース作業もできそうです。ちょっと今まで使ってこなかったのが、損した気分になるほどCI/CDって便利かもです。

CI/CDとは

CI/CDは、ソフトウェアを「こまめに作って、こまめに届ける」ための自動化の仕組みで、まさに先に示した通りです。

CI(Continuous Integration)は、開発者がコードをpushするたびに、自動でビルド・テスト・静的解析を実行して、早い段階で不具合を見つけるしくみ。

CD(Continuous Delivery / Deployment)はテストを通った成果物を「いつでも本番に出せる状態」に自動で用意する(ステージング配置やアーティファクト化まで)。テスト通過後に「本番へ自動リリース」までやる。

こうすることで、早期にバグ検知、手作業ミス削減、リリース頻度向上、レビューと承認の見える化できます。

手順① サーバ側:デプロイ鍵を用意

ここからは実際に個人プロジェクトのデプロイをCI/CDで自動化させた手順をご紹介します。

ローカル(macOS)で鍵を作って、サーバーの authorized_keys に登録します。

# macOS 側で
ssh-keygen -t ed25519 -f ~/.ssh/nujs_deploy -C "nu-js deploy" -N ""

# cat 方式
cat ~/.ssh/nujs_deploy.pub | ssh [email protected] 'mkdir -p ~/.ssh && chmod 700 ~/.ssh && cat >> ~/.ssh/authorized_keys && chmod 600 ~/.ssh/authorized_keys'

手順② GitHub Secrets を登録

リポジトリ Settings → Secrets and variables → Actions に以下を追加します。

  • SSH_HOST : example.com
  • SSH_USER : xxx
  • SSH_PORT : 22(省略可)
  • SSH_PRIVATE_KEY : ~/.ssh/nujs_deploy中身 (秘密鍵テキスト)
  • REMOTE_PATH : /home/xxx/somewhere

注意点としては Environment secrets ではなく Repository secrets 側に設定するようにしてください。

手順③ ワークフローを作成(.github/workflows/deploy.yml)

ここまで準備ができましたら、いよいよ GitHub Actions を設定します。冒頭で説明した通り、大まかに次のような流れを実現してます。

  1. バージョンタグを取得する
  2. コンテンツをビルドする
  3. プレースホルダーに①を埋め込む
  4. 公開用のVPSサーバーへSSHで接続
  5. rsyncでコンテンツを同期する

タグ v* を push した時と、手動実行(workflow_dispatch)でこの Actions が走ります。バージョンはタグ名があれば優先して採用し、手動入力 or 日付でも埋めるようになってます。

なお、タグ名や手動入力値を run: のシェルへ直接展開(${{ github.ref_name }} などをそのまま埋め込む)するのは、コマンドインジェクションの原因になります。下の例では、いったん env: で環境変数として受け取り、シェル変数 $REF_NAME / $INPUT_VERSION 経由で利用しています(GitHub公式の推奨パターン )。

env: 経由にしてもまだ油断は禁物で、バージョン文字列に / & \ や改行が混ざると sed の区切りや置換特殊文字、GITHUB_OUTPUT の行構造を壊せます。下の例では、V^v[0-9A-Za-z._-]+$ だけ通す正規表現で検証してから GITHUB_OUTPUTsed に渡しています。タグ運用上は v2025.8.31.1 のような形だけ通れば十分なので、許容文字を絞っておくのが手っ取り早く安全です。

name: deploy-nu-js

on:
  workflow_dispatch:
    inputs:
      version:
        description: 'VERSION(例: v2025.8.11.1)未入力なら実行時刻で生成'
        required: false
  push:
    tags:
      - 'v*'

jobs:
  buildAndDeploy:
    runs-on: ubuntu-latest
    env:
      SSH_HOST: ${{ secrets.SSH_HOST }}
      SSH_USER: ${{ secrets.SSH_USER }}
      SSH_PORT: ${{ secrets.SSH_PORT }}
      REMOTE_PATH: ${{ secrets.REMOTE_PATH }}

    steps:
      - name: Checkout
        uses: actions/checkout@v4

      - name: Decide version string
        id: ver
        shell: bash
        env:
          REF_TYPE: ${{ github.ref_type }}
          REF_NAME: ${{ github.ref_name }}
          INPUT_VERSION: ${{ inputs.version }}
        run: |
          if [ "$REF_TYPE" = "tag" ] && [ -n "$REF_NAME" ]; then
            V="$REF_NAME"
          elif [ -n "$INPUT_VERSION" ]; then
            V="$INPUT_VERSION"
          else
            V="v$(date +%Y.%m.%d.%H%M)"
          fi
          # / & \ や改行が混ざると sed や GITHUB_OUTPUT が壊れるので、許容文字を絞る
          if [[ ! "$V" =~ ^v[0-9A-Za-z._-]+$ ]]; then
            echo "❌ VERSION '$V' must match ^v[0-9A-Za-z._-]+$" >&2
            exit 1
          fi
          echo "version=$V" >> "$GITHUB_OUTPUT"

      - name: Build (same as your script)
        shell: bash
        env:
          VERSION: ${{ steps.ver.outputs.version }}
        run: |
          set -e
          find ./ -name '.DS_Store' -delete -print || true
          echo "👉 build/ ディレクトリを再作成..."
          rm -rf build
          mkdir -p build/src build/static build/sample build/lib

          echo "📁 必要なファイルをコピー中..."
          cp index.html build/
          cp -R src build/
          cp -R static build/
          cp -R sample build/
          cp -R lib build/

          # バージョン埋め込み(Linux sed は -i 拡張子不要)
          # 直前のステップで VERSION の文字種を検証済みなので、sed 区切りや置換特殊文字との衝突は起きない
          sed -i "s/__VERSION__/${VERSION}/g" "build/index.html"

      # 入力の事前チェック(空なら明示的に fail)
      - name: Preflight check
        shell: bash
        run: |
          [ -n "${{ secrets.SSH_HOST }}" ] || { echo "❌ SSH_HOST is empty. Set repository secret or variable."; exit 1; }
          [ -n "${{ secrets.SSH_USER }}" ] || { echo "❌ SSH_USER is empty."; exit 1; }
          [ -n "${{ secrets.SSH_PRIVATE_KEY || '' }}" ] || { echo "❌ SSH_PRIVATE_KEY is empty."; exit 1; }
          [ -n "${{ secrets.REMOTE_PATH }}" ] || { echo "❌ REMOTE_PATH is empty."; exit 1; }

      - name: Setup SSH key
        shell: bash
        run: |
          set -e
          install -m 700 -d ~/.ssh
          echo "${{ secrets.SSH_PRIVATE_KEY }}" > ~/.ssh/id_deploy
          chmod 600 ~/.ssh/id_deploy
          # known_hosts 登録(ホスト鍵検証)
          ssh-keyscan -p "${SSH_PORT:-22}" "${SSH_HOST}" >> ~/.ssh/known_hosts

      - name: Prepare remote directory
        shell: bash
        run: |
          ssh -i ~/.ssh/id_deploy -p "${SSH_PORT:-22}" \
            -o StrictHostKeyChecking=yes \
            "${SSH_USER}@${SSH_HOST}" "mkdir -p '${REMOTE_PATH}'"

      - name: Deploy (rsync over SSH)
        shell: bash
        run: |
          rsync -avu --delete --exclude='.sass-cache' \
            -e "ssh -i ~/.ssh/id_deploy -p ${SSH_PORT:-22} -o StrictHostKeyChecking=yes" \
            build/ "${SSH_USER}@${SSH_HOST}:${REMOTE_PATH}"

      - name: Done
        run: echo "Done🎉 https://apppppp.com/kit/nu-js/"

使い方

完成した GitHub Actions が動くか試してみましょう。

タグのプッシュがトリガーで発火する設定なので、以下のように実行してみます。

git tag v2025.8.31.1 && git push origin v2025.8.31.1

公開サーバーへアクセスすると、最新が反映されてました。CI/CDがはじめての場合、結構感動しますね!

手動実行も可能です。Actionsdeploy-nu-jsRun workflow でプロンプトが表示されますので version を自由入力して実行できます。

関連記事

関連アイテム