🏞️

GitHub Actions で CI/CD に入門した話: Web サイトのリリースブランチを自動生成する

2025/02/20に公開

ここしばらく重めの Web 制作の案件に関わっており、やっとこさリリースまでこぎつけることができました。

Sass をコンパイルしたり、PostCSS を使って未使用 CSS を Purge したりなどで、開発には使用するが、本番稼働時には不要となるファイルがリポジトリ内に大量にある状態でした。

当初は必要なファイルだけを抽出する .bat を作成してリリース用のパッケージを作成していたのですが、処理に時間もかかるし手間を感じていました。
今回の案件とは関係なく、CI/CD 自体は取り組みたいと考えていた一方で、中々検証とキャッチアップの時間確保が難しかったのですが、今回の課題のそもそもの処理内容自体は比較的易しいことと、業務的にも一区切りがついた段階で、何とか時間を確保することができたので GitHub Actions を利用して、リリース用のブランチを自動生成するワークフローを作成することにしました。

作業に使用したリポジトリ

https://github.com/ndjndj/action-release-package

やりたいこと

前提として、ブランチ戦略には GitLab Flow を採用していました。

最新のコードベースは main にあり、main のコードベースが最新になった時点、つまり main にプッシュされた時点で必要なファイル群を抽出して preproduction ブランチに反映させたい。

手順

とりあえず yaml を全部貼ります。
この yaml をリポジトリの、.github/workflows/ に置けば GitHub Actions が自動で有効になります。

流れとしては、preproduction ブランチのファイル/ディレクトリ群を削除して、必要なファイル/ディレクトリ群を main ブランチから引っ張ってきて反映させます。
ワークフロー自体はすべてのコード変更が起動トリガーになりますが、上記の preproduction ブランチへの反映は、本番環境で必要なファイルが変更されていたときだけ実行されます。

# main ブランチに最新コードが push された際に、
# 事前に用意した .github/include.txt で指定している必要なファイル、ディレクトリ群のみを
# preproduction ブランチにコミットする

name: Prepare release packages

on:
  workflow_dispatch: # 手動起動を有効化
  push:
    branches:
      - main # main ブランチの変更をトリガー
    paths: # すべてのファイルの変更をトリガー
      - '**'
      
defaults:
  run:
    shell: bash

jobs: 
  prepare-release-package:
    runs-on: ubuntu-latest
    timeout-minutes: 10 # 10分たっても終わらなかったらタイムアウト起こす
    steps: 
      - name: Checkout repository 
        uses: actions/checkout@v4
        with: 
          fetch-depth: 0

      - name: Configure Git 
        run: |
          # git user の設定
          git config --global user.name 'github-actions[bot]'
          git config --global user.email 'github-actions[bot]@github.com'

      - name: Check if changes are in included paths 
        id: check-paths 
        run: |
          # 取り込み対象のファイル/ディレクトリ群を読み込む
          INCLUDE_PATHS=$(cat '.github/include.txt')
          # push された際に変更があったファイルを読み込む
          CHANGED_FILES=$(git diff --name-only HEAD^)

          # 変更があるファイル群が取り込み対象に含まれているかをサーチする
          SHOULD_SYNC="false"
          while IFS= read -r file; do 
            while IFS= read -r include_path; do 
              echo "Checking file: $file"
              echo "Against include_path: $include_path"
              if [[ "$file" == "$include_path" ]] || [[ $file =~ ^$include_path* ]]; then
                SHOULD_SYNC="true"
                echo "match found."
                break 2 
              fi 
            done <<< "$INCLUDE_PATHS"
          done <<< "$CHANGED_FILES"

          echo "should_sync=$SHOULD_SYNC" >> $GITHUB_OUTPUT

      - name: Sync specific files to preproduction
        if: steps.check-paths.outputs.should_sync == 'true'
        run: |
          INCLUDE_PATHS=$(cat '.github/include.txt')
          git fetch origin main
          git checkout -b preproduction 
          git checkout -b tmp-sync # 作業用に作成

          # .git 以外をすべて削除
          find . -mindepth 1 -maxdepth 1 -not -name '.git' -exec rm -rf {} + 

          # 取り込みに必要なファイル/ディレクトリのみを main から反映させる
          while IFS= read -r path; do 
            normalized_path="${path//$'\r'/}"
            echo "checking include_path: $path"
            echo "checking normalized_path: $normalized_path"
            # 空でない & コメントじゃない行
            if [[ -n $normalized_path && ! $normalized_path =~ ^[[:space:]]*# ]]; then 
              echo "match found."
              git checkout main -- $normalized_path* || echo "WARNING: Failed to checkout $path"
            fi 

          done <<< "$INCLUDE_PATHS"

          # main から必要なファイル/ディレクトリ群だけを commit
          # 既に .git しかない状態だったので、必要なものだけが残る  
          git add -A 
          git commit -m "SYNC: Update from main branch" || echo "No Changes to commit"
          # 作業用ブランチを preproduction に反映
          git push origin tmp-sync:preproduction -f 

      - name: Cleanup 
        if: always()
        run: |
          # 後片付け  
          git checkout main 
          git branch -D tmp-sync || true 
          git branch -D preproduction || true

また、取り込む対象のファイル/ディレクトリ群は .github/include.txt に保存しています

src/
static/
index.html
.htaccess

actions/checkout@v4 について

これ
リポジトリを作業環境(ランナー上)に落としてくるもので、その後のステップでリポジトリに対して何かアクションをしたいのなら必須のステップ。
fetch-depth は、どの範囲までブランチを取得するかのオプションで、0 を指定することですべてのコミット履歴を取得します。デフォルトは 1 で、最新のコミット履歴のみを取得します。

$GITHUB_OUTPUT について

GitHub Actions で定義されている環境変数で、後続のステップに変数の状態を渡したいときに使用できます。

- name: Check if changes are in included paths 
        id: check-paths 
        run: |
            SHOULD_SYNC="false"
            # 中略
            echo "should_sync=$SHOULD_SYNC" >> $GITHUB_OUTPUT

- name: Sync specific files to preproduction
        if: steps.check-paths.outputs.should_sync == 'true'

.git 以外をすべて削除する方法について

参考
一度 preproduction ブランチをきれいにするために、.git 以外のファイル/ディレクトリをすべて削除しています。

困ったこと

git push できない

GitHub Action の Workflow permission で Read と Write を許可していなかったことが原因でした。
リポジトリ → Settings → Actions → General → Workflow permissions → Read and write permissions にチェックを入れる必要がありました。

ファイルから読み込んだパスだと git checkout できない

ファイルから読み込んだ文字列に CR が含まれているためでした。
ローカルでは Windows を使用しており、改行に CR が含まれるため、あらかじめ取り除く必要がありました。
bash のパラメータ展開という機能を使用して、CR を削除しています。

normalized_path="${path//$'\r'/}"

参考

現状の課題

  • すべてのファイルの変更を監視しているが、ディレクトリ指定と拡張子指定で済みそう
    • ディレクトリはそんなに増えない
    • 拡張子もそんなに増えない
    • 同様に include.txt での運用も本当に必要かどうか
  • preproduction ブランチを一度きれいにしているが差分のあるファイルだけを上書きすればよいのではないか
    • ファイルが削除される場合もあるので要検討
    • というか main ブランチで上書きして exclude 対象だけ消せばいい気もする

参考

概要については以下の記事が参考になりました。
CI/CDとは何かをわかりやすく図解、具体的なツールや取り組み方とともに紹介する
GitHub Actions ドキュメント
【初心者向け】【入門】GitHub Actionsとは?書き方、デバッグ設定、runs-onやcheckoutなどの仕組みや構造も含めて徹底解説
GitHub Actions入門
なんとなくから脱却するGitHub Actionsグッドプラクティス11選
GitHub Actions でプライベートリポジトリの Action を共有できるようになったので試してみる

最後に

Claude と壁打ちしながら検索ワードを拾いつつ組み上げていくという感じで進めました。
シェルでがっつり手続きを書くのは初めてだったのでいい経験になりました。
また、とりあえずですが GitHub Actions を用いた CI/CD に入門できたのはよかったです。
次はホスティングにレンタルサーバーを利用していたので、Production ブランチから自動で FTP をつなげてリリースまでやってくれる Action を作成したいです。

GitHubで編集を提案

Discussion