🎃

CI/CD 目指すはメンテフリー🗽~GitHub Actionsによる自動アップデートとWebhooksによるDiscordへの自動通知~

2024/10/15に公開
Changelog
  • 10/16/2024
    • 初版公開
  • 10/21/2024
    • mainブランチのRulesetsに関する項を追加
    • 定期マージのworkflow(例)を修正

はじめに

この記事ではCI/CDをフル活用して、メンテナンスコストを限りなくゼロに近づける方法を紹介する。

対象とする読者

  • メンテナンスコストを限りなくゼロに近づけたい(メンテナンスフリー)
  • ライブラリやフレームワークのアップデートを自動化したい
  • アップデートやデプロイの通知を受け取りたい
  • Staging/Production環境とCI/CDを上手く組み合わせたい

CI/CD とは?

CI/CDの必要性やDevOpsについて:CI/CDのリンク

https://zenn.dev/moko_poi/articles/ab52dfc83a213d

前準備(前提)

  • 環境として開発(developブランチ)と本番(mainブランチ)がある
  • 一般的なCI (lint, test, build)は既に用意されている

アップデートの自動化

まずはアップデートの自動化を行う。

CI (GitHub Actions)

例)Lint workflow
.github/workflows/lint.yml
name: Lint
on:
  pull_request:
    branches:
      - develop

jobs:
  lint:
    name: npm run lint
    runs-on: ubuntu-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v4

      - name: Set up Node
        uses: actions/setup-node@v4
        with:
          node-version: "20"
          cache: "npm"

      - name: Cache packages
        uses: actions/cache@v4
        id: cache_npm_packages
        env:
          cache-name: cache-npm-packages
        with:
          path: "**/node_modules"
          key: ${{ runner.os }}-build-${{ env.cache-name }}-${{ hashFiles('**/package-lock.json') }}

      - name: Install packages
        if: ${{ steps.cache_npm_packages.outputs.cache-hit != 'true' }}
        run: npm i
      - name: Run lint
        run: npm run lint

Dependabot

アップデートの自動化にはDependabotを使う。

セキュリティアップデート

Settings > Code security

Code security

バージョンアップデート

.github/dependabot.yml
# https://docs.github.com/code-security/dependabot/dependabot-version-updates/configuration-options-for-the-dependabot.yml-file

version: 2
updates:
  - package-ecosystem: "github-actions"
    directory: "/"
    schedule:
      interval: "monthly"
    target-branch: "develop"

  - package-ecosystem: "npm"
    directory: "/"
    schedule:
      interval: "daily"
    open-pull-requests-limit: 10
    target-branch: "develop"
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-major", "version-update:semver-minor"]
例)グループ化すると関連するパッケージをまとめたPRにできる

Pull request

.github/dependabot.yml
...
    ignore:
      - dependency-name: "*"
        update-types: ["version-update:semver-major", "version-update:semver-minor"]
    groups:
      next:
        applies-to: version-updates
        update-types:
          - "patch"
          - "minor"
        patterns:
          - "@next/*"
          - "next"
          - "eslint-config-next"
      lint:
        applies-to: version-updates
        update-types:
          - "patch"
          - "minor"
        patterns:
          - "@typescript-eslint/*"
          - "eslint*"
          - "prettier*"
          - "stylelint*"
        exclude-patterns:
          - "eslint-config-next"
      deps:
        applies-to: version-updates
        update-types:
          - "patch"
          - "minor"
        dependency-type: production
        patterns:
          - "*"
        exclude-patterns:
          - "@next/*"
          - "next"
          - "eslint-config-next"
          - "@typescript-eslint/*"
          - "eslint*"
          - "prettier*"
          - "stylelint*"
      dev-deps:
        applies-to: version-updates
        update-types:
          - "patch"
          - "minor"
        dependency-type: development
        patterns:
          - "*"
        exclude-patterns:
          - "@next/*"
          - "next"
          - "eslint-config-next"
          - "@typescript-eslint/*"
          - "eslint*"
          - "prettier*"
          - "stylelint*"

GitHub Settings

Settings > Rulesets

Rulesets

Rulesetsの設定により、CI (lint)をパスしたPRのみマージできるようにする。コード品質の検証やテストに失敗した場合にマージを止めることができる。

Settings > General > Pull Requests

Allow auto-merge

Allow auto-mergeを有効にすると、先ほどRulesets設定した条件(CIやPRのRequirements)をパスした場合に自動マージできる。

Settings > Actions > Workflow permissions

Workflow permissions

GitHub Actions

.github/workflows/dependabot-auto-merge.yml
# https://docs.github.com/code-security/dependabot/working-with-dependabot/automating-dependabot-with-github-actions

name: Dependabot auto-merge
on: pull_request

permissions:
  contents: write
  pull-requests: write

jobs:
  dependabot:
    runs-on: ubuntu-latest
    if: ${{ github.actor == 'dependabot[bot]' }}
    steps:
      - name: Dependabot metadata
        id: metadata
        uses: dependabot/fetch-metadata@v2
        with:
          github-token: "${{ secrets.GITHUB_TOKEN }}"
      - name: Approve a PR
        run: gh pr review --approve "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GH_TOKEN: ${{secrets.GITHUB_TOKEN}}
      - name: Enable auto-merge for Dependabot PRs
        if: ${{ steps.metadata.outputs.update-type == 'version-update:semver-patch' }}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{github.event.pull_request.html_url}}
          GH_TOKEN: ${{secrets.GITHUB_TOKEN}}

CD(リリース・デプロイの自動化)

定期的なマージと、Vercel, Netlify, Cloudflare Pages等のPaaSや、デプロイ用のworkflowを組み合わせることでリリース(デプロイ)を自動化する。

定期マージ

前述したアップデートの自動化developブランチに対して行う。これらの変更をmainブランチ/本番環境(prod)にデプロイするには、定期的に変更をマージするworkflowを作る。

例)毎週月曜日の00:00(JST)に定期マージするworkflow
.github/workflows/schedule-merge-main.yml
name: Schedule merge from develop into main
on:
  schedule:
    - cron: "0 15 * * 0" # Every Monday at 00:00 JST
  workflow_dispatch:

permissions:
  contents: write
  pull-requests: write

jobs:
  merge:
    runs-on: ubuntu-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0
          ref: develop
      - name: Diff develop and main
        id: diff
        run: git diff origin/main origin/develop --exit-code
        continue-on-error: true
      - name: Create a PR
        id: pull-request
        # If diff is exist, create a PR
        if: ${{ steps.diff.outcome == 'failure' }}
        run: |
          PR_URL=$(gh pr create -B main -t "release(prod): weekly merge from develop into main" -l 'Type: Release' -b "Regular merge every Monday at 00:00.")
          echo "PR_URL=$PR_URL" >> "$GITHUB_OUTPUT"
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
      - name: Enable auto-merge for PRs
        # Only merge if PR has diff
        if: ${{ steps.diff.outcome == 'failure' }}
        run: gh pr merge --auto --merge "$PR_URL"
        env:
          PR_URL: ${{ steps.pull-request.outputs.PR_URL }}
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
例)mainブランチのRulesets

Rulesets

例)Vercel Ignored Build Stepのscript(なくてもよい)
vercel-build-ignore.sh
#!/bin/bash

BRANCH=$VERCEL_GIT_COMMIT_REF

echo "🚀 Starting build for branch $BRANCH"

# Only build for main, preview, and develop branches
if [ "$BRANCH" != "main" ] && [ "$BRANCH" != "preview" ] && [ "$BRANCH" != "develop" ]; then
  echo "🛑 Skipping build for branch $BRANCH"
  exit 0
fi

# Only build if the commit does not include .md files
if git diff --name-only HEAD~1 HEAD | grep -q '\.md$'; then
  echo "🛑 Skipping build because commit includes .md files"
  exit 0
fi

# Proceed with the build
echo "✅ Proceeding with the build"
exit 1

リリース(デプロイ)

Vercel, Netlify, Cloudflare Pages等のPaaSを使う場合は、mainブランチをProduction Branchに設定する。
デプロイ用のworkflowを作る場合は、GitHub Actionsでmainブランチへのpushイベントをトリガーにすると良い。

通知の自動化

GitHubのIssuesやPull requests、GitHub Actionsのworkflowsやリリース(デプロイ)の通知を自動化する。

Discord Webhook

Server Settings (Edit Channel) > Integrations > Webhooks > New Webhook

New Webhook

GitHub Webhook

Settings > Webhooks > Add webhook

Manage webhook

Webhook URLに/githubを付けることで、Discordが公式に対応しているGitHubイベントを通知できる。
それ以外のGitHubイベントはGitHub Actionsでイベントに対応するworkflowsを作る必要がある。これにはcurlDiscord Webhookの実行もしくは、Discord Webhook Actionを使う。

Discord Webhook Action (GitHub Actions)

Discord Webhook Actionを使って、Discordが対応していないGitHubイベントの通知を行う。(curlを使わない場合)

GitHub Settings

Settings > Actions secrets and variables > New repository secrets

Repository secrets

GitHub Actions

例)CI (workflows)の通知
.github/workflows/discord-notify-workflow.yml
# https://github.com/marketplace/actions/discord-webhook-action

name: Discord Notify Workflow
on:
  workflow_run:
    workflows: ["Lint"]
    types: [requested, completed]
    branches:
      - "*"

jobs:
  discord-notify-workflow:
    runs-on: ubuntu-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v4

      - name: Discord Webhook Action
        uses: tsickert/discord-webhook@v6.0.0
        with:
          webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL_WORKFLOW }}
          username: ${{ github.event.workflow_run.actor.login }}
          avatar-url: ${{ github.event.workflow_run.actor.avatar_url }}
          embed-title: "[${{ github.repository }}] Workflow ${{ github.event.workflow_run.name }}: ${{ github.event.workflow_run.pull_requests.number || github.event.workflow_run.head_branch }} 🤖"
          embed-description: "Branch: ${{ github.repository }}/${{ github.event.workflow_run.head_branch }}\nWorkflow: ${{ github.event.workflow_run.name }}\nStatus: ${{ job.status }} ${{ job.status == 'success' && '✅' || job.status == 'failure' && '🛑' || '🟨' }}\nEvent: ${{ github.event.workflow_run.event }}"
          embed-color: ${{ job.status == 'success' && '2278750' || job.status == 'failure' && '15680580' || '15381256' }}
          embed-author-icon-url: ${{ github.event.workflow_run.actor.avatar_url }}
          embed-author-name: ${{ github.event.workflow_run.actor.login }}
          embed-author-url: ${{ github.event.workflow_run.actor.html_url }}
          embed-url: ${{ github.event.workflow_run.html_url }}
例)Deploy (Release)の通知
.github/workflows/discord-notify-deploy.yml
# https://github.com/marketplace/actions/discord-webhook-action

name: Discord Notify Deploy
on: deployment

jobs:
  discord-notify-deploy:
    runs-on: ubuntu-latest
    steps:
      - name: Clone repository
        uses: actions/checkout@v4

      - name: Discord Webhook Action
        uses: tsickert/discord-webhook@v6.0.0
        with:
          webhook-url: ${{ secrets.DISCORD_WEBHOOK_URL_WORKFLOW }}
          username: ${{ github.event.deployment.creator.login }}
          avatar-url: ${{ github.event.deployment.creator.avatar_url }}
          embed-title: "[${{ github.repository }}] Deploy ${{ github.event.deployment.environment }} 🛠️"
          embed-description: "Repository: ${{ github.repository }}\nDeploy: ${{ github.event.deployment.environment }}\nStatus: ${{ job.status }} ${{ job.status == 'success' && '✅' || job.status == 'failure' && '🛑' || '🟨' }}\n${{github.event.deployment.description}}"
          embed-color: ${{ job.status == 'success' && '2278750' || job.status == 'failure' && '15680580' || '15381256' }}
          embed-author-icon-url: ${{ github.event.deployment.creator.avatar_url }}
          embed-author-name: ${{ github.event.deployment.creator.login }}
          embed-author-url: ${{ github.event.deployment.creator.html_url }}
          embed-url: ${{ github.event.deployment.url }}

通知(表示例)

こんな感じのembeddedでリッチなカードの通知が来る。

Discord deployment embed

例)CI (workflows)の表示

Discord workflow embed

さいごに

お役に立てれば幸いです!

以上『CI/CDの完全自動化でリアルライフを充実させよう!』のコーナーでした 🎃

アプリ開発サークル@IPUT

Discussion