Skip to content

Release Automation

Release Automation #7

Workflow file for this run

name: Release Automation
on:
workflow_dispatch:
inputs:
release_core:
description: 'Release JEngine.Core?'
required: true
type: boolean
default: false
core_version:
description: 'New Core version (e.g., 1.0.6)'
required: false
type: string
release_util:
description: 'Release JEngine.Util?'
required: true
type: boolean
default: false
util_version:
description: 'New Util version (e.g., 1.0.1)'
required: false
type: string
manual_changelog:
description: 'Manual changelog entries (optional)'
required: false
type: string
jobs:
validate:
name: Validate Inputs
runs-on: ubuntu-latest
outputs:
core_version: ${{ steps.validate.outputs.core_version }}
util_version: ${{ steps.validate.outputs.util_version }}
release_tag: ${{ steps.validate.outputs.release_tag }}
create_github_release: ${{ steps.validate.outputs.create_github_release }}
steps:
- name: Checkout repository
uses: actions/checkout@v4
- name: Validate inputs
id: validate
run: |
# Check at least one package is selected
if [ "${{ inputs.release_core }}" != "true" ] && [ "${{ inputs.release_util }}" != "true" ]; then
echo "Error: At least one package must be selected for release"
exit 1
fi
# Validate semantic version format
validate_version() {
local version=$1
if ! [[ $version =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
echo "Error: Invalid version format '$version'. Must be X.Y.Z (e.g., 1.0.6)"
exit 1
fi
}
# Get current versions from package.json
CURRENT_CORE_VERSION=$(jq -r '.version' UnityProject/Packages/com.jasonxudeveloper.jengine.core/package.json)
CURRENT_UTIL_VERSION=$(jq -r '.version' UnityProject/Packages/com.jasonxudeveloper.jengine.util/package.json)
echo "Current Core version: $CURRENT_CORE_VERSION"
echo "Current Util version: $CURRENT_UTIL_VERSION"
# Validate Core version if releasing
if [ "${{ inputs.release_core }}" == "true" ]; then
if [ -z "${{ inputs.core_version }}" ]; then
echo "Error: Core version is required when releasing Core package"
exit 1
fi
validate_version "${{ inputs.core_version }}"
# Compare versions (simple string comparison for semantic versions)
if [ "${{ inputs.core_version }}" == "$CURRENT_CORE_VERSION" ]; then
echo "Error: New Core version must be different from current version"
exit 1
fi
echo "core_version=${{ inputs.core_version }}" >> $GITHUB_OUTPUT
else
echo "core_version=$CURRENT_CORE_VERSION" >> $GITHUB_OUTPUT
fi
# Validate Util version if releasing
if [ "${{ inputs.release_util }}" == "true" ]; then
if [ -z "${{ inputs.util_version }}" ]; then
echo "Error: Util version is required when releasing Util package"
exit 1
fi
validate_version "${{ inputs.util_version }}"
if [ "${{ inputs.util_version }}" == "$CURRENT_UTIL_VERSION" ]; then
echo "Error: New Util version must be different from current version"
exit 1
fi
echo "util_version=${{ inputs.util_version }}" >> $GITHUB_OUTPUT
else
echo "util_version=$CURRENT_UTIL_VERSION" >> $GITHUB_OUTPUT
fi
# Release tag always follows Core version
# GitHub releases are only created when Core is released
if [ "${{ inputs.release_core }}" == "true" ]; then
# No 'v' prefix to match existing tag format (1.0.5, not v1.0.5)
echo "release_tag=${{ inputs.core_version }}" >> $GITHUB_OUTPUT
echo "create_github_release=true" >> $GITHUB_OUTPUT
else
# If only Util is released, create tag for OpenUPM but no GitHub release
# No 'v' prefix to match existing tag format
echo "release_tag=util-${{ inputs.util_version }}" >> $GITHUB_OUTPUT
echo "create_github_release=false" >> $GITHUB_OUTPUT
fi
echo "✅ Validation passed"
run-tests:
name: Run Unity Tests
needs: validate
uses: ./.github/workflows/unity-tests.yml
secrets: inherit
prepare-release:
name: Prepare Release
needs: [validate, run-tests]
runs-on: ubuntu-latest
outputs:
changelog: ${{ steps.generate-changelog.outputs.changelog }}
steps:
# Generate GitHub App token for authenticated commits
- name: Generate GitHub App Token
id: generate-token
uses: actions/create-github-app-token@v1
with:
app-id: ${{ secrets.RELEASE_APP_ID }}
private-key: ${{ secrets.RELEASE_APP_PRIVATE_KEY }}
- name: Checkout repository
uses: actions/checkout@v4
with:
token: ${{ steps.generate-token.outputs.token }}
fetch-depth: 0 # Full history for changelog generation
# Determine the tag to use for changelog generation
- name: Determine changelog base tag
id: base-tag
run: |
# Get current Core version
CURRENT_CORE=$(jq -r '.version' UnityProject/Packages/com.jasonxudeveloper.jengine.core/package.json)
# Always use Core version for changelog base (releases follow Core version)
# Note: existing tags don't have 'v' prefix (e.g., 1.0.5 not v1.0.5)
BASE_TAG="$CURRENT_CORE"
echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT
echo "Using base tag for changelog: $BASE_TAG"
# Generate changelog from conventional commits
- name: Generate changelog
id: generate-changelog
run: |
BASE_TAG="${{ steps.base-tag.outputs.base_tag }}"
# Get commits since last tag
if git rev-parse "$BASE_TAG" >/dev/null 2>&1; then
COMMITS=$(git log $BASE_TAG..HEAD --pretty=format:"%H|%s|%an" --no-merges)
else
echo "Warning: Tag $BASE_TAG not found, using all commits"
COMMITS=$(git log --pretty=format:"%H|%s|%an" --no-merges)
fi
# Parse conventional commits
FEATURES=""
FIXES=""
BREAKING=""
OTHER=""
CONTRIBUTORS=""
# Store regex in variable to avoid bash parsing issues with special characters
COMMIT_PATTERN='^([a-z]+)(\(([^)]+)\))?!?:[[:space:]](.+)$'
while IFS='|' read -r hash subject author; do
[ -z "$hash" ] && continue
# Extract commit type and scope
if [[ $subject =~ $COMMIT_PATTERN ]]; then
type="${BASH_REMATCH[1]}"
scope="${BASH_REMATCH[3]}"
description="${BASH_REMATCH[4]}"
is_breaking="${subject//[^!]/}"
# Format with scope if present
if [ -n "$scope" ]; then
entry="**$scope**: $description"
else
entry="$description"
fi
case $type in
feat)
FEATURES="${FEATURES}- $entry\n"
;;
fix)
FIXES="${FIXES}- $entry\n"
;;
# Other conventional types (chore, docs, refactor, etc.) are intentionally excluded
esac
# Check for breaking changes
if [ -n "$is_breaking" ] || git show -s --format=%B $hash | grep -q "BREAKING CHANGE:"; then
breaking_desc=$(git show -s --format=%B $hash | sed -n '/BREAKING CHANGE:/,/^$/p' | tail -n +2 | head -n 1)
if [ -z "$breaking_desc" ]; then
breaking_desc="$description"
fi
BREAKING="${BREAKING}- $breaking_desc\n"
fi
else
# Non-conventional commit - add to Other Changes
# Clean up the subject (remove quotes if present)
clean_subject=$(echo "$subject" | sed 's/^"//;s/"$//')
if [ -n "$clean_subject" ]; then
OTHER="${OTHER}- $clean_subject\n"
fi
fi
# Collect unique contributors
if ! echo "$CONTRIBUTORS" | grep -q "@$author"; then
CONTRIBUTORS="${CONTRIBUTORS}@$author, "
fi
done <<< "$COMMITS"
# Remove trailing comma from contributors
CONTRIBUTORS=$(echo "$CONTRIBUTORS" | sed 's/, $//')
# Build changelog
CHANGELOG=""
# Add package release info (always show both versions for clarity)
if [ "${{ inputs.release_core }}" == "true" ] && [ "${{ inputs.release_util }}" == "true" ]; then
CHANGELOG="${CHANGELOG}**Released**: JEngine.Core v${{ needs.validate.outputs.core_version }}, JEngine.Util v${{ needs.validate.outputs.util_version }}\n\n"
elif [ "${{ inputs.release_core }}" == "true" ]; then
CHANGELOG="${CHANGELOG}**Released**: JEngine.Core v${{ needs.validate.outputs.core_version }} (Util remains v${{ needs.validate.outputs.util_version }})\n\n"
else
CHANGELOG="${CHANGELOG}**Released**: JEngine.Util v${{ needs.validate.outputs.util_version }} (Core remains v${{ needs.validate.outputs.core_version }})\n\n"
fi
if [ -n "$BREAKING" ]; then
CHANGELOG="${CHANGELOG}### ⚠️ BREAKING CHANGES\n\n${BREAKING}\n"
fi
if [ -n "$FEATURES" ]; then
CHANGELOG="${CHANGELOG}### ✨ Features\n\n${FEATURES}\n"
fi
if [ -n "$FIXES" ]; then
CHANGELOG="${CHANGELOG}### 🐛 Bug Fixes\n\n${FIXES}\n"
fi
# Add other changes (non-feat/fix conventional commits and non-conventional commits)
if [ -n "$OTHER" ]; then
CHANGELOG="${CHANGELOG}### 📦 Other Changes\n\n${OTHER}\n"
fi
# Add manual changelog if provided
if [ -n "${{ inputs.manual_changelog }}" ]; then
CHANGELOG="${CHANGELOG}### 📝 Additional Changes\n\n${{ inputs.manual_changelog }}\n\n"
fi
# Add contributors
if [ -n "$CONTRIBUTORS" ]; then
CHANGELOG="${CHANGELOG}### 👥 Contributors\n\n${CONTRIBUTORS}\n"
fi
# If no changelog content, add placeholder
if [ -z "$FEATURES" ] && [ -z "$FIXES" ] && [ -z "$BREAKING" ] && [ -z "$OTHER" ] && [ -z "${{ inputs.manual_changelog }}" ]; then
CHANGELOG="${CHANGELOG}Minor updates and improvements.\n"
fi
echo "changelog<<EOF" >> $GITHUB_OUTPUT
echo -e "$CHANGELOG" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
# Save to file for CHANGE.md update
echo -e "$CHANGELOG" > /tmp/changelog.txt
# Update package.json files
- name: Update Core package.json
if: inputs.release_core == true
run: |
jq '.version = "${{ needs.validate.outputs.core_version }}"' \
UnityProject/Packages/com.jasonxudeveloper.jengine.core/package.json > /tmp/package.json
mv /tmp/package.json UnityProject/Packages/com.jasonxudeveloper.jengine.core/package.json
echo "✅ Updated Core package.json to v${{ needs.validate.outputs.core_version }}"
- name: Update Util package.json
if: inputs.release_util == true
run: |
jq '.version = "${{ needs.validate.outputs.util_version }}"' \
UnityProject/Packages/com.jasonxudeveloper.jengine.util/package.json > /tmp/package.json
mv /tmp/package.json UnityProject/Packages/com.jasonxudeveloper.jengine.util/package.json
echo "✅ Updated Util package.json to v${{ needs.validate.outputs.util_version }}"
# Update README files (only when releasing Core)
- name: Update README.md
if: inputs.release_core == true
run: |
VERSION="${{ needs.validate.outputs.core_version }}"
CHANGELOG=$(cat /tmp/changelog.txt)
# Extract feature bullet points from changelog for README
FEATURES=""
if echo "$CHANGELOG" | grep -q "### ✨ Features"; then
FEATURES=$(echo "$CHANGELOG" | sed -n '/### ✨ Features/,/^###/p' | grep "^- " || true)
fi
if echo "$CHANGELOG" | grep -q "### 🐛 Bug Fixes"; then
FIXES=$(echo "$CHANGELOG" | sed -n '/### 🐛 Bug Fixes/,/^###/p' | grep "^- " || true)
if [ -n "$FEATURES" ] && [ -n "$FIXES" ]; then
FEATURES="${FEATURES}"$'\n'"${FIXES}"
elif [ -n "$FIXES" ]; then
FEATURES="$FIXES"
fi
fi
# Trim leading/trailing whitespace and empty lines
FEATURES=$(echo "$FEATURES" | sed '/^$/d')
# If no features/fixes, use a generic message
if [ -z "$FEATURES" ]; then
FEATURES="- Minor updates and improvements"
fi
# Write replacement content to temp file (avoids sed multiline issues)
{
echo "## 🎉 Latest Features (v$VERSION)"
echo ""
echo "$FEATURES"
echo ""
echo "[📋 View Complete Changelog](CHANGE.md)"
} > /tmp/new_section.txt
# Use awk for reliable multiline replacement
awk '
/^## 🎉 Latest Features/ { skip=1; while((getline line < "/tmp/new_section.txt") > 0) print line; close("/tmp/new_section.txt") }
/^\[📋 View Complete Changelog\]/ { skip=0; next }
!skip { print }
' README.md > /tmp/README.md.new
mv /tmp/README.md.new README.md
echo "✅ Updated README.md with new features"
- name: Update README_zh_cn.md
if: inputs.release_core == true
run: |
VERSION="${{ needs.validate.outputs.core_version }}"
CHANGELOG=$(cat /tmp/changelog.txt)
# Extract feature bullet points from changelog for README
FEATURES=""
if echo "$CHANGELOG" | grep -q "### ✨ Features"; then
FEATURES=$(echo "$CHANGELOG" | sed -n '/### ✨ Features/,/^###/p' | grep "^- " || true)
fi
if echo "$CHANGELOG" | grep -q "### 🐛 Bug Fixes"; then
FIXES=$(echo "$CHANGELOG" | sed -n '/### 🐛 Bug Fixes/,/^###/p' | grep "^- " || true)
if [ -n "$FEATURES" ] && [ -n "$FIXES" ]; then
FEATURES="${FEATURES}"$'\n'"${FIXES}"
elif [ -n "$FIXES" ]; then
FEATURES="$FIXES"
fi
fi
# Trim leading/trailing whitespace and empty lines
FEATURES=$(echo "$FEATURES" | sed '/^$/d')
# If no features/fixes, use a generic message
if [ -z "$FEATURES" ]; then
FEATURES="- 小更新和改进"
fi
# Write replacement content to temp file (avoids sed multiline issues)
{
echo "## 🎉 最新功能 (v$VERSION)"
echo ""
echo "$FEATURES"
echo ""
echo "[📋 查看完整更新日志](CHANGE.md)"
} > /tmp/new_section_zh.txt
# Use awk for reliable multiline replacement
awk '
/^## 🎉 最新功能/ { skip=1; while((getline line < "/tmp/new_section_zh.txt") > 0) print line; close("/tmp/new_section_zh.txt") }
/^\[📋 查看完整更新日志\]/ { skip=0; next }
!skip { print }
' README_zh_cn.md > /tmp/README_zh_cn.md.new
mv /tmp/README_zh_cn.md.new README_zh_cn.md
echo "✅ Updated README_zh_cn.md with new features"
# Update CHANGE.md
- name: Update CHANGE.md
run: |
DATE=$(date +"%B %d %Y")
# Read the generated changelog
CHANGELOG=$(cat /tmp/changelog.txt)
# Convert changelog to CHANGE.md format
# For Core releases, use Core version. For Util-only, use Core version with note
if [ "${{ inputs.release_core }}" == "true" ]; then
VERSION="${{ needs.validate.outputs.core_version }}"
CHANGE_ENTRY="## $VERSION ($DATE)\n\n"
else
VERSION="${{ needs.validate.outputs.core_version }}"
CHANGE_ENTRY="## $VERSION ($DATE) - Util v${{ needs.validate.outputs.util_version }}\n\n"
fi
# Extract features and fixes from changelog
if echo "$CHANGELOG" | grep -q "### ✨ Features"; then
FEATURES=$(echo "$CHANGELOG" | sed -n '/### ✨ Features/,/###/p' | grep "^- " | sed 's/^- /- /' || true)
if [ -n "$FEATURES" ]; then
while IFS= read -r line; do
# Convert **scope**: format to prefix format
if [[ $line =~ ^\-\ \*\*([^*]+)\*\*:\ (.+)$ ]]; then
CHANGE_ENTRY="${CHANGE_ENTRY}- **$(echo ${BASH_REMATCH[2]} | sed 's/^./\u&/')** (${BASH_REMATCH[1]})\n"
else
CHANGE_ENTRY="${CHANGE_ENTRY}${line}\n"
fi
done <<< "$FEATURES"
fi
fi
if echo "$CHANGELOG" | grep -q "### 🐛 Bug Fixes"; then
FIXES=$(echo "$CHANGELOG" | sed -n '/### 🐛 Bug Fixes/,/###/p' | grep "^- " | sed 's/^- /- /' || true)
if [ -n "$FIXES" ]; then
while IFS= read -r line; do
if [[ $line =~ ^\-\ \*\*([^*]+)\*\*:\ (.+)$ ]]; then
CHANGE_ENTRY="${CHANGE_ENTRY}- **$(echo ${BASH_REMATCH[2]} | sed 's/^./\u&/')** (${BASH_REMATCH[1]})\n"
else
CHANGE_ENTRY="${CHANGE_ENTRY}${line}\n"
fi
done <<< "$FIXES"
fi
fi
# Extract other changes (non-conventional commits)
if echo "$CHANGELOG" | grep -q "### 📦 Other Changes"; then
OTHERS=$(echo "$CHANGELOG" | sed -n '/### 📦 Other Changes/,/###/p' | grep "^- " | sed 's/^- /- /' || true)
if [ -n "$OTHERS" ]; then
while IFS= read -r line; do
CHANGE_ENTRY="${CHANGE_ENTRY}${line}\n"
done <<< "$OTHERS"
fi
fi
# Add manual changelog entries
if [ -n "${{ inputs.manual_changelog }}" ]; then
CHANGE_ENTRY="${CHANGE_ENTRY}${{ inputs.manual_changelog }}\n"
fi
CHANGE_ENTRY="${CHANGE_ENTRY}\n"
# Prepend to CHANGE.md (after "## All Versions" line)
sed -i "2i\\$CHANGE_ENTRY" CHANGE.md
echo "✅ Updated CHANGE.md"
# Commit and push changes
- name: Commit and push changes
env:
APP_ID: ${{ secrets.RELEASE_APP_ID }}
run: |
# Use GitHub's bot email format so the app avatar shows on commits
git config user.name "jengine-release-bot[bot]"
git config user.email "${APP_ID}+jengine-release-bot[bot]@users.noreply.github.com"
git add UnityProject/Packages/*/package.json README*.md CHANGE.md
# Different commit message based on what's being released
if [ "${{ inputs.release_core }}" == "true" ]; then
git commit -m "chore(release): ${{ needs.validate.outputs.release_tag }}"
else
git commit -m "chore(util): update to v${{ needs.validate.outputs.util_version }}"
fi
git push origin ${{ github.ref_name }}
echo "✅ Committed and pushed changes"
# Create Git tag (always - needed for OpenUPM detection)
- name: Create Git tag
run: |
git tag ${{ needs.validate.outputs.release_tag }}
git push origin ${{ needs.validate.outputs.release_tag }}
echo "✅ Created and pushed tag ${{ needs.validate.outputs.release_tag }}"
# Summary
- name: Release Summary
run: |
echo "## 📦 Package Update Summary" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ inputs.release_core }}" == "true" ]; then
echo "✅ **JEngine.Core**: v${{ needs.validate.outputs.core_version }}" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ inputs.release_util }}" == "true" ]; then
echo "✅ **JEngine.Util**: v${{ needs.validate.outputs.util_version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "🏷️ **Git Tag**: ${{ needs.validate.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY
if [ "${{ needs.validate.outputs.create_github_release }}" == "true" ]; then
echo "📋 **GitHub Release**: Will be created" >> $GITHUB_STEP_SUMMARY
else
echo "ℹ️ **GitHub Release**: Not created (Util-only update)" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "📦 **OpenUPM**: Will detect update from git tag \`${{ needs.validate.outputs.release_tag }}\`" >> $GITHUB_STEP_SUMMARY
create-release:
name: Create GitHub Release
needs: [validate, prepare-release]
if: needs.validate.outputs.create_github_release == 'true'
runs-on: ubuntu-latest
permissions:
contents: write
steps:
- name: Checkout repository
uses: actions/checkout@v4
with:
ref: ${{ needs.validate.outputs.release_tag }}
- name: Create GitHub Release
uses: actions/create-release@v1
env:
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
with:
tag_name: ${{ needs.validate.outputs.release_tag }}
release_name: v${{ needs.validate.outputs.release_tag }}
body: |
${{ needs.prepare-release.outputs.changelog }}
---
## 📦 Installation
Install via [OpenUPM](https://openupm.com/):
```bash
openupm add com.jasonxudeveloper.jengine.core
openupm add com.jasonxudeveloper.jengine.util
```
## 📖 Documentation
- [English Documentation](https://jengine.xgamedev.net/)
- [中文文档](https://jengine.xgamedev.net/zh/)
---
*This release was automatically created by the JEngine Release Bot*
draft: false
prerelease: false
- name: Summary
run: |
echo "## 🎉 Release Created Successfully!" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "**Tag**: ${{ needs.validate.outputs.release_tag }}" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
if [ "${{ inputs.release_core }}" == "true" ]; then
echo "✅ **JEngine.Core**: v${{ needs.validate.outputs.core_version }}" >> $GITHUB_STEP_SUMMARY
fi
if [ "${{ inputs.release_util }}" == "true" ]; then
echo "✅ **JEngine.Util**: v${{ needs.validate.outputs.util_version }}" >> $GITHUB_STEP_SUMMARY
fi
echo "" >> $GITHUB_STEP_SUMMARY
echo "**OpenUPM will automatically detect and build the packages within 10-15 minutes.**" >> $GITHUB_STEP_SUMMARY
echo "" >> $GITHUB_STEP_SUMMARY
echo "📋 [View Release](https://github.com/${{ github.repository }}/releases/tag/${{ needs.validate.outputs.release_tag }})" >> $GITHUB_STEP_SUMMARY