5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Gitのタグを作る・直す・自動化する

5
Posted at

👋 はじめに

リリースのたびに手作業でGitタグとGitHub Releaseを作っていたのですが、
「たまに付け忘れる」「過去タグの形式がバラバラ」という、
地味だけど無視できない課題が溜まっていました。

タグ作成作業はリリース時しかやらないので、Gitのタグってそもそもどうやって作るんだっけ?となり今回記事に整理することとしました。

1. シンプルにタグを作る
2. 間違えたタグを正しく入れ替える
3. 特定ブランチのマージをトリガーに、タグ付けを自動化する(CI)

この記事では、その過程で使ったコマンドとワークフローをまとめます。
(コマンド例のリポジトリ名・製品名はすべて仮のものに置き換えています)

🏷️ その1:シンプルにタグを作る

まずは基本。特定のコミットにタグを打って、リモートへ送るだけです。

# 現在のHEADに軽量タグを作成
git tag v1.2.3

# メッセージ付きの注釈付き(annotated)タグにする場合
git tag -a v1.2.3 -m "MyApp v1.2.3"

# 特定のコミットを指定して打つこともできる
git tag v1.2.3 <commit-sha>

# タグはpushしないとリモートに反映されない
git push origin v1.2.3

ポイントは タグはpushして初めて共有される こと。
ローカルで打っただけだと自分の手元にしか存在しません。

GitHub Releaseまで一気に作りたいなら、GitHub CLI(gh)が便利です。

# タグ作成 + Release作成をまとめて実行(タグが無ければ同時に作られる)
gh release create v1.2.3 \
  --title "MyApp V1.2.3" \
  --generate-notes

--generate-notes を付けると、前のタグからの変更を自動でリリースノートにしてくれます。

🔧 その2:間違えたタグを正しく入れ替える

過去に独自命名のタグ(例:V1.23.00 のような非セマンティックな形式)を
打ってしまっていて、これを v1.2.3 のようなセマンティックバージョンへ
統一したくなりました。

ここで注意なのが、Gitにはタグの「リネーム」コマンドが無い こと。
「新しいタグを同じコミットに作る → 古いタグを消す」の組み合わせで実現します。

さらに厄介なのが、GitHub Releaseがタグに紐づいている 場合。
先に古いタグを消すとReleaseが迷子になるので、順番が重要です。

1タグ分の手順はこうなります。

OLD=V1.23.00     # 古いタグ
NEW=v1.2.3       # 正しいタグ

# 事前に全タグを取得しておく
git fetch origin --tags

# ① 古いタグと同じコミットに、新しいタグを作成
git tag "$NEW" "$OLD^{}"

# ② 新しいタグをpush
git push origin "$NEW"

# ③ Releaseを新しいタグへ付け替える(★削除より先にやるのが肝)
gh release edit "$OLD" --repo your-org/your-repo --tag "$NEW"

# ④ 古いタグをリモートから削除
git push origin --delete "$OLD"

# ⑤ 古いタグをローカルから削除(任意・掃除)
git tag -d "$OLD"

"$OLD^{}" は「そのタグが指すコミット」を表す記法です。
これで、新タグを旧タグとまったく同じコミットに打てます。

複数タグをまとめて処理したくなりますが、
共有リポジトリのタグ削除は巻き戻しにくい破壊的操作 なので、
私は1つずつ、結果を確認しながら実行しました。

実行前のチェックポイント:

  • 古いタグを参照しているもの(CI・デプロイスクリプト・ドキュメント)が無いか
  • Release本体(ノートや公開日)は gh release edit --tag で付け替えられ、消えない
  • ローカルに旧タグを持っている人は、各自 git fetch --prune --prune-tags するまで残る

🤖 その3:特定ブランチのマージで自動タグ付け(GitHub Actions)

手作業をやめたい本命がこれ。
release/** ブランチが main にマージされたら、
自動でタグとReleaseを作るワークフローを組みました。

やりたいこと:

  • トリガー:release/**main のPRが マージされて クローズされたときだけ
  • タグ:ブランチ名からセマンティックバージョンを導出(例:release/v1.2v1.2.0
  • Release名:MyApp V1.2.0 の形式で作成
name: Create Release Tag and GitHub Release

on:
  pull_request:
    types: [closed]
    branches: [main]

permissions:
  contents: write

jobs:
  create-tag-and-release:
    # release/** が main に「マージされて」クローズされたときだけ実行
    # (マージせずにクローズした場合や、release/ 以外のブランチでは動かさない)
    if: >-
      github.event.pull_request.merged == true &&
      startsWith(github.event.pull_request.head.ref, 'release/')
    runs-on: ubuntu-latest
    steps:
      - name: Checkout repository
        uses: actions/checkout@v4
        with:
          fetch-depth: 0

      - name: Determine tag name and release name
        id: meta
        run: |
          set -euo pipefail
          HEAD_BRANCH="${{ github.event.pull_request.head.ref }}"   # 例: release/v3.4
          VERSION_PART="${HEAD_BRANCH#release/}"
          VERSION_NUMBER="${VERSION_PART#[vV]}"

          # 想定外のブランチ名で不正なタグを作らないよう検証する
          if ! printf '%s' "$VERSION_NUMBER" | grep -Eq '^[0-9]+\.[0-9]+(\.[0-9]+)?$'; then
            echo "::error::Unexpected version from branch '${HEAD_BRANCH}': '${VERSION_NUMBER}'."
            exit 1
          fi

          # major.minor.patch に分解し、省略された要素は 0 で補完
          MAJOR="$(printf '%s' "$VERSION_NUMBER" | cut -d. -f1)"
          MINOR="$(printf '%s' "$VERSION_NUMBER" | cut -d. -f2)"
          PATCH="$(printf '%s' "$VERSION_NUMBER" | cut -d. -f3)"
          MINOR="${MINOR:-0}"
          PATCH="${PATCH:-0}"

          FULL_VERSION="${MAJOR}.${MINOR}.${PATCH}"
          TAG_NAME="v${FULL_VERSION}"                 # 例: v3.4.0
          RELEASE_NAME="MyApp V${FULL_VERSION}"       # 例: MyApp V3.4.0

          {
            echo "tag_name=${TAG_NAME}"
            echo "release_name=${RELEASE_NAME}"
          } >> "$GITHUB_OUTPUT"

      - name: Create tag and GitHub Release
        env:
          GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
          TAG_NAME: ${{ steps.meta.outputs.tag_name }}
          RELEASE_NAME: ${{ steps.meta.outputs.release_name }}
          MERGE_SHA: ${{ github.event.pull_request.merge_commit_sha }}
        run: |
          set -euo pipefail

          # マージコミットSHAが空なら停止(マージ方式によっては空になり得る)
          if [ -z "${MERGE_SHA}" ]; then
            echo "::error::merge_commit_sha is empty."
            exit 1
          fi

          # 冪等性:同名Releaseが既にあれば何もしない
          if gh release view "${TAG_NAME}" >/dev/null 2>&1; then
            echo "Release '${TAG_NAME}' already exists. Skip."
            exit 0
          fi

          # Releaseは無いが同名タグだけある場合は、意図しない付け替えを避けて停止
          if gh api "repos/${GITHUB_REPOSITORY}/git/ref/tags/${TAG_NAME}" >/dev/null 2>&1; then
            echo "::error::Tag '${TAG_NAME}' already exists without a release."
            exit 1
          fi

          # タグ(マージコミットを指す)とReleaseを同時に作成
          gh release create "${TAG_NAME}" \
            --target "${MERGE_SHA}" \
            --title "${RELEASE_NAME}" \
            --generate-notes

つまずきやすいポイントを、あらかじめガードとして入れてあります。

  • merged == true で判定:マージせずに閉じたPRでは動かさない
  • バージョン形式を検証release/hotfix-xxx のような想定外ブランチで
    不正なタグを作らないよう、正規表現でチェックして明示的に失敗させる
  • merge_commit_sha の空チェック:作成先コミットが定まらないときは停止
  • 同名タグの存在チェック:Releaseは無いのにタグだけある状態を検知して停止
  • permissions: contents: write:タグ・Release作成に必要な権限

🎯 まとめ

  • タグは「作る」だけならシンプル。ただし push して初めて共有される
  • 「直す」はリネーム不可なので 作り直し+古いタグ削除
    Releaseが絡むなら 付け替えを削除より先に やる
  • 「自動化」はGitHub Actionsで、マージ判定・バージョン検証・冪等性
    ガードとして入れておくと安心

次回リリース時に、タグ作成されているかをチェックする評価項目を一つ減らせそうです。

本記事に掲載しているコード例のリポジトリ名・製品名・バージョン表記はすべて説明用の仮の値です。
また本記事の内容は筆者個人の見解であり、所属組織の立場を代表するものではありません。

5
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
4

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?