Prepare Release #15
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
name: Prepare Release | |
on: | |
workflow_dispatch: | |
inputs: | |
bump: | |
type: choice | |
description: "Version bump type" | |
required: true | |
options: | |
- patch | |
- minor | |
- major | |
preid: | |
description: "(Optional) prerelease identifier (e.g. alpha, beta, rc)" | |
required: false | |
dry_run: | |
type: boolean | |
description: "Dry run (do not push/PR)" | |
default: false | |
required: true | |
force_update_existing: | |
type: boolean | |
description: "If release branch already exists, force update it" | |
default: false | |
required: false | |
permissions: | |
contents: write | |
pull-requests: write | |
jobs: | |
prepare: | |
runs-on: ubuntu-latest | |
steps: | |
- name: Checkout dev | |
uses: actions/checkout@v4 | |
with: | |
ref: dev | |
fetch-depth: 0 | |
- name: Set up Git user | |
run: | | |
git config user.name "github-actions[bot]" | |
git config user.email "41898282+github-actions[bot]@users.noreply.github.com" | |
- name: Set up Flutter | |
uses: subosito/flutter-action@v2 | |
with: | |
channel: stable | |
cache: true | |
- name: Flutter pub get | |
run: flutter pub get | |
- name: Read current version | |
id: current | |
run: | | |
CURR=$(grep '^version:' pubspec.yaml | head -1 | awk '{print $2}') | |
echo "current=$CURR" >> $GITHUB_OUTPUT | |
- name: Compute next version | |
id: next | |
run: | | |
set -e | |
bump='${{ github.event.inputs.bump }}' | |
curr='${{ steps.current.outputs.current }}' | |
pre='${{ github.event.inputs.preid }}' | |
base=${curr%%-*} # strip prerelease if any | |
IFS='.' read -r MA MI PA <<< "$base" | |
case "$bump" in | |
patch) PA=$((PA+1));; | |
minor) MI=$((MI+1)); PA=0;; | |
major) MA=$((MA+1)); MI=0; PA=0;; | |
esac | |
next="$MA.$MI.$PA" | |
if [ -n "$pre" ]; then | |
# If current already has same pre id, increment numeric suffix | |
if [[ $curr == *"$pre"* ]]; then | |
# Extract trailing digits | |
suffix=$(echo "$curr" | sed -n "s/.*$pre\.\([0-9]\+\)$/\1/p") | |
if [ -n "$suffix" ]; then | |
next="$MA.$MI.$PA-$pre.$((suffix+1))" | |
else | |
next="$MA.$MI.$PA-$pre.1" | |
fi | |
else | |
next="$MA.$MI.$PA-$pre.1" | |
fi | |
fi | |
echo "value=$next" >> $GITHUB_OUTPUT | |
echo "Next version: $next" | |
- name: Collect merged commit / PR titles since last release | |
id: changes | |
run: | | |
set -e | |
LAST_TAG=$(git describe --tags --abbrev=0 --match 'v[0-9]*' 2>/dev/null || echo '') | |
echo "Last tag: $LAST_TAG" | |
RANGE="" | |
if [ -n "$LAST_TAG" ]; then RANGE="$LAST_TAG..HEAD"; fi | |
RAW=$(git log --pretty=format:%s $RANGE) | |
echo "Raw subjects:\n$RAW" | head -40 | |
FILTERED=$(echo "$RAW" | grep -v -E '^chore\(release\):' | grep -v -E '^Merge pull request' || true) | |
if [ -z "$FILTERED" ]; then | |
CHANGES='- (変更点なし / No notable changes)' | |
else | |
CHANGES=$(echo "$FILTERED" | awk 'function map(line){ | |
if(line ~ /^feat/){return "- ✨ " line} | |
if(line ~ /^fix/){return "- 🐛 " line} | |
if(line ~ /^perf/){return "- 🚀 " line} | |
if(line ~ /^docs/){return "- 📝 " line} | |
if(line ~ /^refactor/){return "- ♻️ " line} | |
if(line ~ /^test/){return "- ✅ " line} | |
if(line ~ /^build/){return "- 🧱 " line} | |
if(line ~ /^chore/){return "- 🔧 " line} | |
return "- " line } | |
{ print map($0) }') | |
fi | |
echo "--- Generated list ---" | |
echo "$CHANGES" | head -40 | |
esc=$(printf "%s" "$CHANGES" | sed 's/%/%25/g; s/\r/%0D/g; s/\n/%0A/g') | |
echo "list=$esc" >> $GITHUB_OUTPUT | |
- name: Update pubspec.yaml version | |
run: | | |
next='${{ steps.next.outputs.value }}' | |
sed -i "s/^version: .*/version: $next/" pubspec.yaml | |
echo "Updated pubspec.yaml to $next" | |
- name: Generate OSS license file | |
run: | | |
# generate lib/generated/oss_licenses.dart (flutter_oss_licenses) | |
dart run flutter_oss_licenses:generate --output lib/generated/oss_licenses.dart | |
echo "Generated OSS licenses" | |
- name: Update CHANGELOG.md (prepend Unreleased section) | |
run: | | |
next='${{ steps.next.outputs.value }}' | |
date=$(date +%Y-%m-%d) | |
if [ ! -f CHANGELOG.md ]; then | |
echo "# Changelog" > CHANGELOG.md | |
echo >> CHANGELOG.md | |
fi | |
# Ensure Changelog header exists | |
if ! grep -q '^# Changelog' CHANGELOG.md; then | |
{ echo '# Changelog'; echo; cat CHANGELOG.md; } > /tmp/_ch && mv /tmp/_ch CHANGELOG.md | |
fi | |
# Ensure Unreleased section exists | |
if ! grep -q '^## \[Unreleased\]' CHANGELOG.md; then | |
{ echo '## [Unreleased]'; echo; cat CHANGELOG.md; } > /tmp/_unrel && mv /tmp/_unrel CHANGELOG.md | |
fi | |
# Update or add the release section | |
if ! grep -q "^## \[$next\]" CHANGELOG.md; then | |
awk -v ver="$next" -v d="$date" 'BEGIN{added=0} { | |
if(!added && /^## \[Unreleased\]/){ | |
print $0; print ""; print "## [" ver "] - " d; print ""; print "__AUTO_CHANGELOG_PLACEHOLDER__"; print ""; added=1; next | |
} | |
} END{ if(!added){ print "## [" ver "] - " d; print ""; print "- (placeholder) Describe changes here"; print "" } }' CHANGELOG.md > /tmp/_new && mv /tmp/_new CHANGELOG.md | |
fi | |
echo "Updated CHANGELOG.md" | |
- name: Inject generated changes into CHANGELOG | |
run: | | |
list='${{ steps.changes.outputs.list }}' | |
if [ -z "$list" ]; then echo "No list to inject"; exit 0; fi | |
perl -0777 -pe 's/__AUTO_CHANGELOG_PLACEHOLDER__/'"$list"'/g' CHANGELOG.md > /tmp/_chg && mv /tmp/_chg CHANGELOG.md | |
echo "Injected generated changes into CHANGELOG.md" | |
- name: Show diff | |
run: git --no-pager diff --name-only && git --no-pager diff | head -200 | |
- name: Commit changes | |
if: ${{ github.event.inputs.dry_run == 'false' }} | |
run: | | |
next='${{ steps.next.outputs.value }}' | |
git add pubspec.yaml CHANGELOG.md lib/generated/oss_licenses.dart | |
if git diff --cached --quiet; then | |
echo "No changes to commit (version already set?)" | |
else | |
git commit -m "chore(release): v$next\n\nPrepare release from dev" | |
fi | |
- name: Push dev (ensure version bump recorded) | |
if: ${{ github.event.inputs.dry_run == 'false' }} | |
run: | | |
# Push dev so that origin/dev always reflects latest version bump | |
git push origin dev | |
- name: Push release branch (safe) | |
if: ${{ github.event.inputs.dry_run == 'false' }} | |
id: push | |
run: | | |
set -e | |
next='${{ steps.next.outputs.value }}' | |
BRANCH="release/v$next" | |
FORCE='${{ github.event.inputs.force_update_existing }}' | |
git fetch origin "$BRANCH" || true | |
if git rev-parse -q --verify "origin/$BRANCH" >/dev/null; then | |
echo "Remote branch exists: $BRANCH" | |
if [ "$FORCE" = "true" ]; then | |
echo "Force updating remote branch $BRANCH" | |
git push origin HEAD:$BRANCH --force | |
else | |
# Check divergence | |
LOCAL=$(git rev-parse HEAD) | |
REMOTE=$(git rev-parse "origin/$BRANCH") | |
if [ "$LOCAL" = "$REMOTE" ]; then | |
echo "Remote branch already up-to-date." | |
else | |
echo "Branch diverged. (Set force_update_existing=true to overwrite)" | |
echo "branch=$BRANCH" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
fi | |
else | |
echo "Creating new remote branch $BRANCH" | |
git push origin HEAD:$BRANCH | |
fi | |
echo "branch=$BRANCH" >> $GITHUB_OUTPUT | |
- name: Determine target branch | |
id: target | |
run: | | |
pre='${{ github.event.inputs.preid }}' | |
if [ -n "$pre" ]; then | |
echo "base=dev" >> $GITHUB_OUTPUT | |
echo "pr_body=自動生成された **プレリリース (Beta)** 準備 PR です。" >> $GITHUB_OUTPUT | |
else | |
echo "base=main" >> $GITHUB_OUTPUT | |
echo "pr_body=自動生成された **正式リリース** 準備 PR です。" >> $GITHUB_OUTPUT | |
fi | |
- name: Check diff with base | |
if: ${{ github.event.inputs.dry_run == 'false' && steps.push.outputs.branch != '' }} | |
id: diff | |
run: | | |
BASE='${{ steps.target.outputs.base }}' | |
HEAD_BR='${{ steps.push.outputs.branch }}' | |
set -e | |
echo "Fetching refs for base=$BASE head=$HEAD_BR ..." | |
# Fetch only the required branches (full history already available if checkout used fetch-depth:0) | |
git fetch origin "$BASE" "$HEAD_BR" --no-tags --prune || true | |
# Verify remote release branch exists | |
if ! git ls-remote --exit-code origin "$HEAD_BR" >/dev/null 2>&1; then | |
echo "Release branch '$HEAD_BR' not found on remote yet (skipping PR creation check)." >&2 | |
echo "ahead_commits=0" >> $GITHUB_OUTPUT | |
echo "file_diff=0" >> $GITHUB_OUTPUT | |
echo "release_files_changed=0" >> $GITHUB_OUTPUT | |
echo "different=false" >> $GITHUB_OUTPUT | |
exit 0 | |
fi | |
BASE_REF="origin/$BASE" | |
HEAD_REF="origin/$HEAD_BR" | |
git rev-parse "$BASE_REF" >/dev/null | |
git rev-parse "$HEAD_REF" >/dev/null || true | |
# Primary commit ahead count (commits reachable from HEAD_REF not in base) | |
AHEAD=$(git rev-list --right-only --count "$BASE_REF...$HEAD_REF" 2>/dev/null || echo 0) | |
echo "ahead_commits=$AHEAD" >> $GITHUB_OUTPUT | |
# Secondary: file diff check | |
if git diff --quiet "$BASE_REF...$HEAD_REF"; then | |
FILE_DIFF=0 | |
else | |
FILE_DIFF=1 | |
fi | |
echo "file_diff=$FILE_DIFF" >> $GITHUB_OUTPUT | |
# Tertiary: specific release related files changed | |
if git diff --name-only "$BASE_REF...$HEAD_REF" | grep -E '^(pubspec.yaml|CHANGELOG.md)$' >/dev/null; then | |
REL_FILES_CHANGED=1 | |
else | |
REL_FILES_CHANGED=0 | |
fi | |
echo "release_files_changed=$REL_FILES_CHANGED" >> $GITHUB_OUTPUT | |
if [ "$AHEAD" -gt 0 ] || [ "$FILE_DIFF" -eq 1 ] || [ "$REL_FILES_CHANGED" -eq 1 ]; then | |
echo "different=true" >> $GITHUB_OUTPUT | |
echo "Diff detected (ahead=$AHEAD file_diff=$FILE_DIFF release_files=$REL_FILES_CHANGED)" | |
else | |
echo "different=false" >> $GITHUB_OUTPUT | |
echo "No diff between $HEAD_BR and $BASE (ahead=$AHEAD file_diff=$FILE_DIFF release_files=$REL_FILES_CHANGED). PR will be skipped." | |
fi | |
- name: Create / Reuse PR (github-script) | |
if: ${{ github.event.inputs.dry_run == 'false' && steps.push.outputs.branch != '' && steps.diff.outputs.different == 'true' }} | |
uses: actions/github-script@v7 | |
id: create_pr | |
with: | |
github-token: ${{ secrets.GITHUB_TOKEN }} | |
script: | | |
const next = process.env.NEXT_VERSION = `${{ steps.next.outputs.value }}`; | |
const head = `${{ steps.push.outputs.branch }}`; // already pushed 'release/vX.Y.Z' | |
const base = `${{ steps.target.outputs.base }}`; | |
const prTitle = `chore(release): v${{ steps.next.outputs.value }}`; | |
const body = ( | |
`${{ steps.target.outputs.pr_body }}\n\n` + | |
`- 元ブランチ: dev\n` + | |
`- 次バージョン: v${{ steps.next.outputs.value }}\n` + | |
`- CHANGELOG は placeholder を含む場合があります。必要に応じて編集してください。\n\n` + | |
`**【重要】**\n` + | |
`- **正式リリース (main向け)**: マージ後、publish-release ワークフローがタグと GitHub Release を作成します。\n` + | |
`- **プレリリース (dev向け)**: マージ後、タグやリリースは作成されません。バージョン番号のみ取り込みます。` | |
).trim(); | |
const {owner, repo} = context.repo; | |
// Check existing open PR from head -> base | |
const existing = await github.rest.pulls.list({owner, repo, state: 'open', head: `${owner}:${head}`}); | |
if (existing.data.length > 0) { | |
core.info(`Existing PR found: ${existing.data[0].html_url}`); | |
core.setOutput('url', existing.data[0].html_url); | |
return; | |
} | |
// Create new PR | |
const pr = await github.rest.pulls.create({owner, repo, head, base, title: prTitle, body}); | |
core.info(`Created PR: ${pr.data.html_url}`); | |
core.setOutput('url', pr.data.html_url); | |
- name: Skip note (no diff) | |
if: ${{ github.event.inputs.dry_run == 'false' && steps.push.outputs.branch != '' && steps.diff.outputs.different == 'false' }} | |
run: echo "Base と release ブランチに差分が無いため PR 作成をスキップしました。" | |
- name: Dry run note | |
if: ${{ github.event.inputs.dry_run == 'true' }} | |
run: echo "Dry run finished. No push/PR performed." |