この記事では、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で自動化させます。
- バージョンタグを取得する
- コンテンツをビルドする
- プレースホルダーに①を埋め込む
- 公開用のVPSサーバーへSSHで接続
- 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.comSSH_USER:xxxSSH_PORT:22(省略可)SSH_PRIVATE_KEY:~/.ssh/nujs_deployの 中身 (秘密鍵テキスト)REMOTE_PATH:/home/xxx/somewhere
注意点としては Environment secrets ではなく Repository secrets 側に設定するようにしてください。

手順③ ワークフローを作成(.github/workflows/deploy.yml)
ここまで準備ができましたら、いよいよ GitHub Actions を設定します。冒頭で説明した通り、大まかに次のような流れを実現してます。
- バージョンタグを取得する
- コンテンツをビルドする
- プレースホルダーに①を埋め込む
- 公開用のVPSサーバーへSSHで接続
- 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_OUTPUT と sed に渡しています。タグ運用上は 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がはじめての場合、結構感動しますね!

手動実行も可能です。Actions → deploy-nu-js → Run workflow でプロンプトが表示されますので version を自由入力して実行できます。
関連記事
- リモートサーバーへ root ログインしてシェルを実行するまでを半自動化する
- git worktreeでAI時代の並列開発を試す。Codexに別ブランチを同時に任せるには
- はじめての Spring Boot 〜 JavaでWebアプリケーション