当サイトでは、
Semver とは
SemVer のMAJOR.MINOR.PATCH
それぞれの
- MAJOR
(メジャー) : 後方 互換性の ない 変更が 加えられた 場合に 更新される 番号。 例えば、 API の 仕様が 変更されて 互換性が 保てなくなった 場合などに 使用する - MINOR
(マイナー) : 後方 互換性を 保ったまま 新機能を 追加した 場合に 更新される 番号。 新しい 機能が 提供される ものの、 既存の 機能には 影響を 及ぼさない 場合などに 使用する - PATCH
(パッチ) : バグ修正など、 後方 互換性を 保ちながら 既存機能の 改善や 修正が 行われた 場合に 更新される 番号。 小さな バグ修正や セキュリティパッチなどに 使用する
これに
た2.3.1
と
2
: メジャーバージョン。後方 互換性の ない 変更が あった ことを 示す 3
: マイナーバージョン。新機能が 追加されている ことを 示す 1
: パッチバージョン。小さな バグ修正が 含まれている ことを 示す
当サイトでは
semantic-release は、
デフォルトで
と
以下が
コミットメッセージ例 | リリースタイプ |
---|---|
fix(scope): description | パッチリリース |
feat(scope): description | マイナーリリース |
perf(scope): description | メジャーリリース |
CalVer とは
CalVer とは、
この
- YYYY.MINOR: リリース年
(例: 「2025」)と、 その 年内の リリース回数を 示すマイナーバージョンを 使用。 例: 2025.1
,2025.2
。 - YYYY.MM.MICRO: リリース年、
月 (例: 「04」)、 バグ修正を 示す MICRO 番号を 使用。 例: 2025.04.3
。 - YYYYMMDD: 完全な
リリース日を バージョン番号と して 利用。 例: 20250412
。 - YY.MINOR: リリース年の
下 2 桁と マイナーバージョンを 組み合わせた 簡易的な 形式。 例: 25.4
。
JetBrains 系 IDE や、
変更したのか
なぜ CalVer にAngular Commit Message Conventions に
- マイナーバージョンを
上げたくないが ために、 コミットメッセージに feat
を使えない - 結果
的に コミットタイプに chore
を使うことが 多くなってしまう
- 結果
- メジャーバージョンを
どの タイミングで 上げるか 悩む - メジャーバージ
ョンは 頻繁に 上げるべきではないと いう 考えが ある
- メジャーバージ
- 当サイトでは
マイナーバージョンを 上げる たびに リリースノートを 書いていたが、 それも 辛くなってきた
特に
リリースノートを
何より、
と
※ Angular Commit Message Conventions とは
リリースを 自動化したい
CalVer の今までは
なぜなら、
とはgit tag
コマンドを
スクリプトの
- Git の
タグ付けと GitHub への リリース作成までを 自動化する - タグの
形式は YYYY.0M.0D
- タグが
重複する 場合は YYYY.0M.0D-MICRO
(MICRO の部分は インクリメント) - リリース本文には
前回の リリースとの コミット比較 URL を 含める - Bun で
実行する - dry run オプションを
つけると、 実際には リリースしない (ログを 出力する)
import { $ } from "bun";
const isDryRun = process.argv.includes("--dry-run");const GITHUB_ACCESS_TOKEN = process.env.GITHUB_ACCESS_TOKEN;
if (!GITHUB_ACCESS_TOKEN) { console.error( "🚨 GitHub token is missing. Set GITHUB_ACCESS_TOKEN in your environment variables.", ); process.exit(1);}
const now = new Date();const BASE_VERSION = `${now.getFullYear()}.${String(now.getMonth() + 1).padStart(2, "0")}.${String(now.getDate()).padStart(2, "0")}`;let version = BASE_VERSION;
const tags = (await $`git tag`.text()) .split("\n") .filter((t) => t.startsWith(BASE_VERSION));
if (tags.includes(version)) { let suffixCounter = 2; while (tags.includes(`${version}-${suffixCounter}`)) { suffixCounter++; } version = `${version}-${suffixCounter}`;}
console.log(`🔖 Creating tag: ${version}`);if (isDryRun) console.log("💡 Dry run mode is ON");
const CURRENT_BRANCH = (await $`git rev-parse --abbrev-ref HEAD`.text()).trim();
if (!isDryRun) { await $`git checkout main`; await $`git tag -f ${version}`; await $`git push -f origin ${version}`; await $`git checkout ${CURRENT_BRANCH}`; console.log(`✅ Released tag: ${version} and returned to ${CURRENT_BRANCH}`);} else { console.log("🚫 Would checkout main"); console.log(`🚫 Would tag -f ${version}`); console.log(`🚫 Would push -f origin ${version}`); console.log(`🚫 Would checkout ${CURRENT_BRANCH}`);}
const REPO_OWNER = "kkhys";const REPO_NAME = "me";
console.log(`🚀 Preparing to create GitHub release for ${version}`);
const getPreviousTag = async (): Promise<string | null> => { const response = await fetch( `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases`, { method: "GET", headers: { Authorization: `Bearer ${GITHUB_ACCESS_TOKEN}`, "Content-Type": "application/json", }, }, );
if (response.ok) { const releases: { tag_name: string }[] = (await response.json()) as { tag_name: string; }[]; if (releases && releases.length > 0 && releases[0]?.tag_name) { return releases[0].tag_name; } } else { console.error("❌ Failed to fetch releases from GitHub."); const errorData = (await response.json()) as { message: string }; console.error(`Error: ${errorData.message}`); }
return null;};
const createGitHubRelease = async () => { const previousTag = await getPreviousTag();
let body = `Automatic release for version ${version}.`; if (previousTag) { const compareUrl = `https://github.com/${REPO_OWNER}/${REPO_NAME}/compare/${previousTag}...${version}`; body += `\n\n[View changes since ${previousTag}](${compareUrl})`; } else { body += "\n\n(No previous release found for comparison.)"; }
const response = await fetch( `https://api.github.com/repos/${REPO_OWNER}/${REPO_NAME}/releases`, { method: "POST", headers: { Authorization: `Bearer ${GITHUB_ACCESS_TOKEN}`, "Content-Type": "application/json", }, body: JSON.stringify({ tag_name: version, name: version, body, draft: false, prerelease: false, }), }, );
if (response.ok) { const responseData = (await response.json()) as { html_url: string }; console.log(`✅ GitHub Release created: ${responseData.html_url}`); } else { const errorData = (await response.json()) as { message: string }; console.error("❌ Failed to create GitHub release."); console.error(`Error: ${errorData.message}`); }};
if (!isDryRun) { await createGitHubRelease();} else { console.log("🚫 Would create GitHub release"); console.log(`🚫 Release tag: ${version}`); console.log(`🚫 Release title: ${version}`); console.log("✅ Dry run completed for GitHub release process");}
bun run scripts/release.ts
上記を
dry run モードだと
bun run scripts/release.ts --dry-run
🔖 Creating tag: 2025.04.12-3💡 Dry run mode is ON🚫 Would checkout main🚫 Would tag -f 2025.04.12-3🚫 Would push -f origin 2025.04.12-3🚫 Would checkout develop🚀 Preparing to create GitHub release for 2025.04.12-3🚫 Would create GitHub release🚫 Release tag: 2025.04.12-3🚫 Release title: 2025.04.12-3✅ Dry run completed for GitHub release process
ごに
さいCalVer に