GitHub Actions
Deploy from GitHub Actions [dev] - 2025-10-31 07:28:50
68f7925
name: 'Create Release with Tag'
description: 'Creates a Git tag and GitHub Release with categorized PR notes'
inputs:
branch:
description: 'Branch name (e.g., main, stg)'
required: true
tag-prefix:
description: 'Tag prefix (e.g., main-, stg-)'
required: true
prerelease:
description: 'Whether this is a pre-release (true/false)'
required: true
release-name-prefix:
description: 'Release name prefix (e.g., Production Release, Staging Pre-release)'
required: true
runs:
using: 'composite'
steps:
- name: Create Tag and Release
uses: actions/github-script@v7
with:
script: |
const now = new Date();
const year = now.getFullYear();
const month = String(now.getMonth() + 1).padStart(2, '0');
const day = String(now.getDate()).padStart(2, '0');
const hours = String(now.getHours()).padStart(2, '0');
const minutes = String(now.getMinutes()).padStart(2, '0');
const seconds = String(now.getSeconds()).padStart(2, '0');
const tagName = `${{ inputs.tag-prefix }}${year}${month}${day}_${hours}${minutes}${seconds}`;
// 前回のタグを取得
let previousTag = null;
try {
const tags = await github.rest.repos.listTags({
owner: context.repo.owner,
repo: context.repo.repo,
per_page: 100
});
previousTag = tags.data.find(t => t.name.startsWith('${{ inputs.tag-prefix }}'));
} catch (e) {
console.log('No previous tags found or error:', e.message);
}
// カテゴリ定義
const categories = {
'feat': { emoji: '✨', title: 'Features', order: 1 },
'fix': { emoji: '🐛', title: 'Bug Fixes', order: 2 },
'hotfix': { emoji: '🚑', title: 'Hotfixes', order: 3 },
'refactor': { emoji: '♻️', title: 'Refactoring', order: 4 },
'docs': { emoji: '📝', title: 'Documentation', order: 5 },
'test': { emoji: '✅', title: 'Tests', order: 6 },
'chore': { emoji: '🔧', title: 'Chore', order: 7 },
'other': { emoji: '🔄', title: 'Other Changes', order: 8 }
};
// リリースノート生成
let releaseBody = '';
if (previousTag) {
console.log(`Previous tag found: ${previousTag.name}`);
try {
// PRタイトル一覧を取得
const commits = await github.rest.repos.compareCommits({
owner: context.repo.owner,
repo: context.repo.repo,
base: previousTag.name,
head: '${{ inputs.branch }}'
});
const prNumbers = new Set();
for (const commit of commits.data.commits) {
const match = commit.commit.message.match(/#(\d+)/);
if (match) prNumbers.add(match[1]);
}
// PRをカテゴリごとに分類
const categorizedPrs = {};
for (const num of prNumbers) {
try {
const pr = await github.rest.pulls.get({
owner: context.repo.owner,
repo: context.repo.repo,
pull_number: parseInt(num)
});
if (pr.data.merged_at) {
// プレフィックスを抽出(PRタイトル優先、次にブランチ名)
const titleMatch = pr.data.title.match(/^(feat|fix|hotfix|refactor|docs|test|chore):/);
const branchMatch = pr.data.head.ref.match(/^(feature|feat|fix|hotfix|refactor|docs|test|chore)\//);
let category = 'other';
if (titleMatch) {
category = titleMatch[1];
} else if (branchMatch) {
// feature/ feat にマッピング
category = branchMatch[1] === 'feature' ? 'feat' : branchMatch[1];
}
if (!categorizedPrs[category]) {
categorizedPrs[category] = [];
}
categorizedPrs[category].push(`- ${pr.data.title} (#${num})`);
}
} catch (e) {
console.log(`Failed to get PR #${num}:`, e.message);
}
}
// カテゴリ順にリリースノートを生成
const sortedCategories = Object.keys(categorizedPrs).sort((a, b) => {
return categories[a].order - categories[b].order;
});
const sections = [];
for (const category of sortedCategories) {
const cat = categories[category];
sections.push(`## ${cat.emoji} ${cat.title}\n${categorizedPrs[category].join('\n')}`);
}
releaseBody = sections.length > 0 ? sections.join('\n\n') : 'No PRs merged in this release';
} catch (e) {
console.log('Error generating release notes:', e.message);
releaseBody = 'Error generating release notes';
}
} else {
console.log('No previous tag found - this is the initial release');
releaseBody = `Initial Release - ${now.toISOString()}`;
}
// タグ作成
try {
await github.rest.git.createRef({
owner: context.repo.owner,
repo: context.repo.repo,
ref: `refs/tags/${tagName}`,
sha: context.sha
});
console.log(`Tag created: ${tagName}`);
} catch (e) {
console.log('Error creating tag:', e.message);
throw e;
}
// リリース作成
try {
await github.rest.repos.createRelease({
owner: context.repo.owner,
repo: context.repo.repo,
tag_name: tagName,
name: `${{ inputs.release-name-prefix }} ${tagName}`,
body: releaseBody,
prerelease: ${{ inputs.prerelease }}
});
console.log(`Release created: ${{ inputs.release-name-prefix }} ${tagName}`);
} catch (e) {
console.log('Error creating release:', e.message);
throw e;
}