Spaces:
Sleeping
Sleeping
| 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; | |
| } | |