Release Automation #18
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: 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 | |
| release_ui: | |
| description: 'Release JEngine.UI?' | |
| required: true | |
| type: boolean | |
| default: false | |
| ui_version: | |
| description: 'New UI version (e.g., 1.0.0)' | |
| required: false | |
| type: string | |
| manual_changelog: | |
| description: 'Manual changelog entries (optional)' | |
| required: false | |
| type: string | |
| permissions: read-all | |
| jobs: | |
| validate: | |
| name: Validate Inputs | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| outputs: | |
| core_version: ${{ steps.validate.outputs.core_version }} | |
| util_version: ${{ steps.validate.outputs.util_version }} | |
| ui_version: ${{ steps.validate.outputs.ui_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" ] && [ "${{ inputs.release_ui }}" != "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) | |
| CURRENT_UI_VERSION=$(jq -r '.version' UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json) | |
| echo "Current Core version: $CURRENT_CORE_VERSION" | |
| echo "Current Util version: $CURRENT_UTIL_VERSION" | |
| echo "Current UI version: $CURRENT_UI_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 | |
| # Validate UI version if releasing | |
| if [ "${{ inputs.release_ui }}" == "true" ]; then | |
| if [ -z "${{ inputs.ui_version }}" ]; then | |
| echo "Error: UI version is required when releasing UI package" | |
| exit 1 | |
| fi | |
| validate_version "${{ inputs.ui_version }}" | |
| if [ "${{ inputs.ui_version }}" == "$CURRENT_UI_VERSION" ]; then | |
| echo "Error: New UI version must be different from current version" | |
| exit 1 | |
| fi | |
| echo "ui_version=${{ inputs.ui_version }}" >> $GITHUB_OUTPUT | |
| else | |
| echo "ui_version=$CURRENT_UI_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 | |
| permissions: | |
| contents: read | |
| checks: write | |
| uses: ./.github/workflows/unity-tests.yml | |
| secrets: inherit | |
| upload-coverage: | |
| name: Upload Coverage | |
| needs: run-tests | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: read | |
| steps: | |
| - name: Checkout repository | |
| uses: actions/checkout@v4 | |
| - name: Download coverage artifacts | |
| uses: actions/download-artifact@v4 | |
| with: | |
| name: Coverage-results-2022.3.55f1 | |
| path: coverage | |
| - name: List coverage files | |
| run: | | |
| echo "Coverage directory structure:" | |
| find coverage -type f -name "*.xml" 2>/dev/null || echo "No XML files found" | |
| - name: Fix coverage paths | |
| run: | | |
| # Unity test runner generates paths with /github/workspace/ prefix (Docker container path) | |
| # Strip this prefix so Codecov can match paths to repository files | |
| echo "Fixing coverage paths..." | |
| find coverage -name "*.xml" -exec sed -i 's|/github/workspace/||g' {} \; | |
| echo "Path fix complete. Sample paths after fix:" | |
| find coverage -name "TestCoverageResults*.xml" -exec grep -h "fullPath=" {} \; | head -5 || true | |
| - name: Upload coverage to Codecov (util package) | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: coverage/**/TestCoverageResults*.xml | |
| flags: util | |
| name: jengine-util | |
| fail_ci_if_error: true | |
| verbose: true | |
| - name: Upload coverage to Codecov (ui package) | |
| uses: codecov/codecov-action@v4 | |
| with: | |
| token: ${{ secrets.CODECOV_TOKEN }} | |
| files: coverage/**/TestCoverageResults*.xml | |
| flags: ui | |
| name: jengine-ui | |
| fail_ci_if_error: true | |
| verbose: true | |
| prepare-release: | |
| name: Prepare Release | |
| needs: [validate, run-tests] | |
| runs-on: ubuntu-latest | |
| permissions: | |
| contents: write | |
| 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: | | |
| # Find the latest existing semantic version tag (e.g., 1.0.5, not v1.0.5) | |
| # Sort by version number and get the latest one | |
| BASE_TAG=$(git tag -l '[0-9]*.[0-9]*.[0-9]*' | sort -V | tail -n 1) | |
| if [ -z "$BASE_TAG" ]; then | |
| echo "Warning: No existing version tags found" | |
| BASE_TAG="" | |
| else | |
| echo "Found latest tag: $BASE_TAG" | |
| fi | |
| echo "base_tag=$BASE_TAG" >> $GITHUB_OUTPUT | |
| echo "Using base tag for changelog: ${BASE_TAG:-'(none - will use all commits)'}" | |
| # Generate changelog from conventional commits | |
| - name: Generate changelog | |
| id: generate-changelog | |
| env: | |
| GH_TOKEN: ${{ github.token }} | |
| REPO: ${{ github.repository }} | |
| run: | | |
| BASE_TAG="${{ steps.base-tag.outputs.base_tag }}" | |
| # Get commits since last tag | |
| if [ -n "$BASE_TAG" ] && git rev-parse "$BASE_TAG" >/dev/null 2>&1; then | |
| echo "Getting commits since tag: $BASE_TAG" | |
| COMMITS=$(git log ${BASE_TAG}..HEAD --pretty=format:"%H|%s" --no-merges) | |
| else | |
| echo "Warning: No valid base tag, using all commits" | |
| COMMITS=$(git log --pretty=format:"%H|%s" --no-merges) | |
| fi | |
| # Parse conventional commits | |
| FEATURES="" | |
| FIXES="" | |
| BREAKING="" | |
| OTHER="" | |
| declare -A CONTRIBUTORS_MAP | |
| # Store regex in variable to avoid bash parsing issues with special characters | |
| COMMIT_PATTERN='^([a-z]+)(\(([^)]+)\))?!?:[[:space:]](.+)$' | |
| # Pattern to extract PR number from commit message like "message (#123)" | |
| PR_PATTERN='\(#([0-9]+)\)$' | |
| while IFS='|' read -r hash subject; do | |
| [ -z "$hash" ] && continue | |
| # Extract PR number if present and convert to hyperlink | |
| pr_link="" | |
| if [[ $subject =~ $PR_PATTERN ]]; then | |
| pr_num="${BASH_REMATCH[1]}" | |
| pr_link="[#${pr_num}](https://github.com/${REPO}/pull/${pr_num})" | |
| # Replace (#123) with the hyperlink (use | as delimiter to avoid conflict with URL slashes) | |
| subject=$(echo "$subject" | sed "s|(#${pr_num})|(${pr_link})|") | |
| # Get contributor from PR using GitHub API | |
| pr_author=$(gh api "repos/${REPO}/pulls/${pr_num}" --jq '.user.login' 2>/dev/null || echo "") | |
| if [ -n "$pr_author" ] && [ "$pr_author" != "null" ]; then | |
| CONTRIBUTORS_MAP["$pr_author"]=1 | |
| fi | |
| fi | |
| # 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 | |
| done <<< "$COMMITS" | |
| # Build contributors list from the associative array (excludes bots) | |
| CONTRIBUTORS="" | |
| for contributor in "${!CONTRIBUTORS_MAP[@]}"; do | |
| # Skip bot accounts | |
| if [[ "$contributor" != *"[bot]"* ]] && [[ "$contributor" != *"-bot"* ]]; then | |
| CONTRIBUTORS="${CONTRIBUTORS}[@${contributor}](https://github.com/${contributor}), " | |
| fi | |
| done | |
| # Remove trailing comma | |
| CONTRIBUTORS=$(echo "$CONTRIBUTORS" | sed 's/, $//') | |
| # Build changelog | |
| CHANGELOG="" | |
| # Add package release info | |
| RELEASED_PACKAGES="" | |
| UNCHANGED_PACKAGES="" | |
| if [ "${{ inputs.release_core }}" == "true" ]; then | |
| RELEASED_PACKAGES="${RELEASED_PACKAGES}JEngine.Core v${{ needs.validate.outputs.core_version }}, " | |
| else | |
| UNCHANGED_PACKAGES="${UNCHANGED_PACKAGES}Core v${{ needs.validate.outputs.core_version }}, " | |
| fi | |
| if [ "${{ inputs.release_util }}" == "true" ]; then | |
| RELEASED_PACKAGES="${RELEASED_PACKAGES}JEngine.Util v${{ needs.validate.outputs.util_version }}, " | |
| else | |
| UNCHANGED_PACKAGES="${UNCHANGED_PACKAGES}Util v${{ needs.validate.outputs.util_version }}, " | |
| fi | |
| if [ "${{ inputs.release_ui }}" == "true" ]; then | |
| RELEASED_PACKAGES="${RELEASED_PACKAGES}JEngine.UI v${{ needs.validate.outputs.ui_version }}, " | |
| else | |
| UNCHANGED_PACKAGES="${UNCHANGED_PACKAGES}UI v${{ needs.validate.outputs.ui_version }}, " | |
| fi | |
| # Remove trailing comma and space | |
| RELEASED_PACKAGES=$(echo "$RELEASED_PACKAGES" | sed 's/, $//') | |
| UNCHANGED_PACKAGES=$(echo "$UNCHANGED_PACKAGES" | sed 's/, $//') | |
| if [ -n "$UNCHANGED_PACKAGES" ]; then | |
| CHANGELOG="${CHANGELOG}**Released**: ${RELEASED_PACKAGES} (${UNCHANGED_PACKAGES} unchanged)\n\n" | |
| else | |
| CHANGELOG="${CHANGELOG}**Released**: ${RELEASED_PACKAGES}\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 }}" | |
| - name: Update UI package.json | |
| if: inputs.release_ui == true | |
| run: | | |
| jq '.version = "${{ needs.validate.outputs.ui_version }}"' \ | |
| UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json > /tmp/package.json | |
| mv /tmp/package.json UnityProject/Packages/com.jasonxudeveloper.jengine.ui/package.json | |
| echo "✅ Updated UI package.json to v${{ needs.validate.outputs.ui_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 | |
| env: | |
| REPO: ${{ github.repository }} | |
| 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 (already contains PR hyperlinks) | |
| # Format: "- Description (scope)" for conventional commits, "- Description" for others | |
| if echo "$CHANGELOG" | grep -q "### ✨ Features"; then | |
| FEATURES=$(echo "$CHANGELOG" | sed -n '/### ✨ Features/,/###/p' | grep "^- " || true) | |
| if [ -n "$FEATURES" ]; then | |
| while IFS= read -r line; do | |
| # Convert **scope**: format to "Description (scope)" format | |
| if [[ $line =~ ^\-\ \*\*([^*]+)\*\*:\ (.+)$ ]]; then | |
| scope="${BASH_REMATCH[1]}" | |
| desc="${BASH_REMATCH[2]}" | |
| # Capitalize first letter of description | |
| desc_cap="$(echo "${desc:0:1}" | tr '[:lower:]' '[:upper:]')${desc:1}" | |
| CHANGE_ENTRY="${CHANGE_ENTRY}- ${desc_cap} (${scope})\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 "^- " || true) | |
| if [ -n "$FIXES" ]; then | |
| while IFS= read -r line; do | |
| if [[ $line =~ ^\-\ \*\*([^*]+)\*\*:\ (.+)$ ]]; then | |
| scope="${BASH_REMATCH[1]}" | |
| desc="${BASH_REMATCH[2]}" | |
| desc_cap="$(echo "${desc:0:1}" | tr '[:lower:]' '[:upper:]')${desc:1}" | |
| CHANGE_ENTRY="${CHANGE_ENTRY}- ${desc_cap} (${scope})\n" | |
| else | |
| CHANGE_ENTRY="${CHANGE_ENTRY}${line}\n" | |
| fi | |
| done <<< "$FIXES" | |
| fi | |
| fi | |
| # Extract other changes (non-conventional commits) - already contains PR hyperlinks | |
| if echo "$CHANGELOG" | grep -q "### 📦 Other Changes"; then | |
| OTHERS=$(echo "$CHANGELOG" | sed -n '/### 📦 Other Changes/,/###/p' | grep "^- " || 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 | |
| run: | | |
| # Use GitHub's bot email format so the app avatar shows on commits | |
| # The user ID (257041894) is from: gh api '/users/jengine-release-bot[bot]' --jq '.id' | |
| # This is different from the App ID - it's the bot account's user ID | |
| git config user.name "jengine-release-bot[bot]" | |
| git config user.email "257041894+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 | |
| if [ "${{ inputs.release_ui }}" == "true" ]; then | |
| echo "✅ **JEngine.UI**: v${{ needs.validate.outputs.ui_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 (non-Core 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: | |
| # Generate GitHub App token so the release is created by JEngine Release Bot | |
| - 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: | |
| ref: ${{ needs.validate.outputs.release_tag }} | |
| - name: Create GitHub Release | |
| env: | |
| GH_TOKEN: ${{ steps.generate-token.outputs.token }} | |
| run: | | |
| # Create release body | |
| cat > /tmp/release_body.md << 'RELEASE_EOF' | |
| ${{ 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 | |
| openupm add com.jasonxudeveloper.jengine.ui # Optional: UI utilities | |
| ``` | |
| ## 📖 Documentation | |
| - [English Documentation](https://jengine.xgamedev.net/) | |
| - [中文文档](https://jengine.xgamedev.net/zh/) | |
| --- | |
| *This release was automatically created by the JEngine Release Bot* | |
| RELEASE_EOF | |
| # Create the release using gh CLI (no deprecated set-output) | |
| gh release create "${{ needs.validate.outputs.release_tag }}" \ | |
| --title "v${{ needs.validate.outputs.release_tag }}" \ | |
| --notes-file /tmp/release_body.md | |
| - 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 | |
| if [ "${{ inputs.release_ui }}" == "true" ]; then | |
| echo "✅ **JEngine.UI**: v${{ needs.validate.outputs.ui_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 |