Skip to content

Release Management

Release Management #17

name: Release Management
on:
# Using pull_request_target instead of pull_request for security reasons:
# - Runs in the context of the BASE repository, not the fork
# - Has access to repository secrets
# - Can commit changes to protected branches
# - SECURITY NOTE: Be careful when checking out PR code with this event type
pull_request_target:
types: [closed]
branches:
- main
workflow_dispatch:
inputs:
release_type:
description: 'Force a specific release type (leave empty for auto-detection)'
required: false
type: choice
options:
- auto
- major
- minor
- patch
default: 'auto'
target_branch:
description: 'Target branch for manual release (usually develop)'
required: false
default: 'develop'
schedule:
# Run on the 1st and 15th of each month
- cron: '0 0 1,15 * *'
jobs:
prepare-release:
# Only run if:
# 1. PR from develop to main is merged, OR
# 2. Manually triggered, OR
# 3. Scheduled run
if: (github.event_name == 'pull_request_target' && github.event.pull_request.merged == true && github.event.pull_request.head.ref == 'develop') || github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
runs-on: ubuntu-latest
steps:
- name: Set checkout ref
id: set_ref
run: |
if [[ "${{ github.event_name }}" == "pull_request_target" ]]; then
echo "ref=main" >> $GITHUB_OUTPUT
else
echo "ref=${{ github.event.inputs.target_branch || 'develop' }}" >> $GITHUB_OUTPUT
fi
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 0
# For PR events, check out the main branch
# For manual/scheduled events, check out the specified target branch or develop
ref: ${{ steps.set_ref.outputs.ref }}
# Use a personal access token with repo scope for better permissions
token: ${{ secrets.REPO_PAT }}
- name: Setup Node.js
uses: actions/setup-node@v3
with:
node-version: '16'
- name: Install dependencies
run: npm ci
- name: Check for changesets
id: check_changesets
run: |
if [ -d ".changesets" ] && [ "$(ls -A .changesets)" ]; then
echo "has_changesets=true" >> $GITHUB_OUTPUT
else
echo "has_changesets=false" >> $GITHUB_OUTPUT
echo "No changesets found. Proceeding with release without changesets."
fi
- name: Determine version bump
id: version_bump
run: |
if [[ "${{ github.event.inputs.release_type }}" != "auto" && "${{ github.event.inputs.release_type }}" != "" ]]; then
# Use the specified release type
npm run version:bump -- --type=${{ github.event.inputs.release_type }}
else
# Auto-detect release type from changesets, or default to patch if none found
if [[ "${{ steps.check_changesets.outputs.has_changesets }}" == "true" ]]; then
npm run version:bump
else
npm run version:bump -- --type=patch
fi
fi
# Get the new version after bump
NEW_VERSION=$(grep -oP "define\('AUTOMATION_TESTS_VERSION', '\K[^']+" constants.php)
echo "version=${NEW_VERSION}" >> $GITHUB_OUTPUT
- name: Update @since and deprecated version placeholders
id: update_since_tags
run: |
# Run the since-tags update script with the new version to update @since and deprecated version placeholders
npm run since-tags:update -- ${{ steps.version_bump.outputs.version }}
# Check if summary file exists and has content
if [ -f "/tmp/since-tags-summary.md" ]; then
# Read the summary file
SINCE_SUMMARY=$(cat /tmp/since-tags-summary.md)
# Set the output without any encoding (we'll handle that in the release notes step)
echo "has_updates=true" >> $GITHUB_OUTPUT
echo "summary<<EOF" >> $GITHUB_OUTPUT
echo "$SINCE_SUMMARY" >> $GITHUB_OUTPUT
echo "EOF" >> $GITHUB_OUTPUT
else
echo "has_updates=false" >> $GITHUB_OUTPUT
fi
# Generate release notes BEFORE updating changelog or archiving changesets
- name: Generate release notes
id: release_notes
run: |
# Create a temporary directory outside the repository with proper permissions
mkdir -p /tmp/release-notes
chmod 777 /tmp/release-notes
# Initialize release notes content
RELEASE_NOTES="## Release Notes"
RELEASE_NOTES="${RELEASE_NOTES}"$'\n\n'
# Check if this is a PR from develop to main
if [[ "${{ github.event_name }}" == "pull_request_target" && "${{ github.event.pull_request.head.ref }}" == "develop" ]]; then
# Get the PR body content, carefully excluding the pending @since section
PR_CONTENT=$(echo '${{ github.event.pull_request.body }}' | awk '
BEGIN { p=0; in_since=0 }
/^## Changelog/ { p=1; print; next }
/^### 🔄 Pending `@since` Tag/ { in_since=1; next }
/^###/ && in_since { in_since=0 } # Reset when we hit the next section
/^This PR contains all changes/ { p=0; next }
!in_since && p { print }
')
# For debugging
echo "Extracted PR content:"
echo "$PR_CONTENT"
RELEASE_NOTES="${RELEASE_NOTES}${PR_CONTENT}"
else
# Generate release notes in markdown format for release body
GENERATED_NOTES=$(npm run release:notes 2>/dev/null | grep -v "^>")
if [[ -n "$GENERATED_NOTES" ]]; then
RELEASE_NOTES="${RELEASE_NOTES}${GENERATED_NOTES}"
fi
fi
# Add @since updates section if there are any
if [[ "${{ steps.update_since_tags.outputs.has_updates }}" == "true" ]]; then
# Store the summary in a variable with proper quoting
SINCE_SUMMARY='${{ steps.update_since_tags.outputs.summary }}'
RELEASE_NOTES="${RELEASE_NOTES}"$'\n\n'
RELEASE_NOTES="${RELEASE_NOTES}${SINCE_SUMMARY}"
fi
# If no content was added (after all attempts), provide default message
if [[ "${RELEASE_NOTES}" == "## Release Notes"$'\n\n' ]]; then
if [[ "${{ github.event_name }}" == "workflow_dispatch" ]]; then
RELEASE_NOTES="${RELEASE_NOTES}This release was manually triggered with version bump type: ${{ github.event.inputs.release_type || 'auto' }}"
elif [[ "${{ github.event_name }}" == "schedule" ]]; then
RELEASE_NOTES="${RELEASE_NOTES}This is a scheduled release."
else
RELEASE_NOTES="${RELEASE_NOTES}This release was triggered by merging a PR from develop to main."
fi
RELEASE_NOTES="${RELEASE_NOTES}"$'\n\n'
RELEASE_NOTES="${RELEASE_NOTES}No changesets were found for this release. This typically means:"$'\n'
RELEASE_NOTES="${RELEASE_NOTES}- No features, fixes, or breaking changes were added, or"$'\n'
RELEASE_NOTES="${RELEASE_NOTES}- The changes made did not require a changeset"
fi
# Save the release notes to a file that will be used by both the changelog update and release creation
echo "$RELEASE_NOTES" > "$GITHUB_WORKSPACE/release_notes.md"
# For debugging
echo "Generated release notes:"
cat "$GITHUB_WORKSPACE/release_notes.md"
# Set the content for GitHub Actions output using jq to properly escape the content
echo "content=$(echo "$RELEASE_NOTES" | jq -Rsa .)" >> $GITHUB_OUTPUT
- name: Update changelogs
run: |
# First, check if this is a breaking change release by using our new script
# This will automatically update the upgrade notice section if breaking changes are found
npm run upgrade-notice:update -- --version=${{ steps.version_bump.outputs.version }} --notes-file="$GITHUB_WORKSPACE/release_notes.md"
# Now update the changelogs using the same release notes
npm run changelogs:update -- --version=${{ steps.version_bump.outputs.version }} --notes-file="$GITHUB_WORKSPACE/release_notes.md"
- name: Commit changes to main
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "[email protected]"
# Check if there are any changes to commit
if [[ -n "$(git status --porcelain)" ]]; then
echo "Changes detected, committing to main branch"
git add *.php *.md *.txt package.json
git commit -m "release: prepare v${{ steps.version_bump.outputs.version }}"
git push origin main
else
echo "No changes to commit"
fi
- name: Create and push tag
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
run: |
git config --global user.name "GitHub Actions"
git config --global user.email "[email protected]"
# Check if tag already exists
if git rev-parse "v${{ steps.version_bump.outputs.version }}" >/dev/null 2>&1; then
echo "Tag v${{ steps.version_bump.outputs.version }} already exists. Skipping tag creation."
else
# Create an annotated tag with the changelog as the message
git tag -a "v${{ steps.version_bump.outputs.version }}" -m "Release v${{ steps.version_bump.outputs.version }}"
# Push the tag
git push origin "v${{ steps.version_bump.outputs.version }}"
fi
- name: Create GitHub Release
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
uses: actions/create-release@v1
id: create_release
continue-on-error: true
env:
GITHUB_TOKEN: ${{ secrets.REPO_PAT }}
with:
tag_name: v${{ steps.version_bump.outputs.version }}
release_name: Release v${{ steps.version_bump.outputs.version }}
body_path: ${{ github.workspace }}/release_notes.md
draft: false
prerelease: false
- name: Handle release creation failure
if: (github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch') && steps.create_release.outcome == 'failure'
id: handle_failure
run: |
echo "Failed to create release. This could be because the tag already exists."
echo "Checking if release exists..."
RELEASE_EXISTS=$(curl -s -o /dev/null -w "%{http_code}" \
-H "Authorization: token ${{ secrets.REPO_PAT }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/repos/${{ github.repository }}/releases/tags/v${{ steps.version_bump.outputs.version }}")
if [[ "$RELEASE_EXISTS" == "200" ]]; then
echo "Release for v${{ steps.version_bump.outputs.version }} already exists. Marking as successful."
echo "release_exists=true" >> $GITHUB_OUTPUT
else
echo "Release creation failed for an unknown reason."
echo "release_exists=false" >> $GITHUB_OUTPUT
# Check for rate limiting
RATE_LIMIT=$(curl -s \
-H "Authorization: token ${{ secrets.REPO_PAT }}" \
-H "Accept: application/vnd.github.v3+json" \
"https://api.github.com/rate_limit")
REMAINING=$(echo "$RATE_LIMIT" | jq -r '.resources.core.remaining')
RESET_TIME=$(echo "$RATE_LIMIT" | jq -r '.resources.core.reset')
RESET_TIME_HUMAN=$(date -d @$RESET_TIME)
if [[ "$REMAINING" -le 10 ]]; then
echo "::warning::GitHub API rate limit is low: $REMAINING requests remaining. Resets at $RESET_TIME_HUMAN"
fi
# Try again with a different approach
echo "Attempting to create release using GitHub CLI..."
# Install GitHub CLI if not already installed
if ! command -v gh &> /dev/null; then
echo "Installing GitHub CLI..."
curl -fsSL https://cli.github.com/packages/githubcli-archive-keyring.gpg | sudo dd of=/usr/share/keyrings/githubcli-archive-keyring.gpg
echo "deb [arch=$(dpkg --print-architecture) signed-by=/usr/share/keyrings/githubcli-archive-keyring.gpg] https://cli.github.com/packages stable main" | sudo tee /etc/apt/sources.list.d/github-cli.list > /dev/null
sudo apt update
sudo apt install gh
fi
# Authenticate with GitHub CLI
echo "${{ secrets.REPO_PAT }}" | gh auth login --with-token
# Verify the release notes file exists
if [ -f "${{ github.workspace }}/release_notes.md" ]; then
echo "Using release notes from workspace:"
cat "${{ github.workspace }}/release_notes.md"
# Try to create the release with GitHub CLI
if gh release create "v${{ steps.version_bump.outputs.version }}" \
--title "Release v${{ steps.version_bump.outputs.version }}" \
--notes-file "${{ github.workspace }}/release_notes.md"; then
echo "Successfully created release using GitHub CLI"
echo "release_exists=true" >> $GITHUB_OUTPUT
else
echo "::error::Failed to create release using GitHub CLI with notes file."
# Try one more time with inline notes from the output
echo "Trying with inline notes..."
if gh release create "v${{ steps.version_bump.outputs.version }}" \
--title "Release v${{ steps.version_bump.outputs.version }}" \
--notes "${{ steps.release_notes.outputs.content }}"; then
echo "Successfully created release using GitHub CLI with inline notes"
echo "release_exists=true" >> $GITHUB_OUTPUT
else
echo "::error::Failed to create release using all methods. Please check logs for details."
echo "release_exists=false" >> $GITHUB_OUTPUT
exit 1
fi
fi
else
echo "Release notes file not found in workspace. Using inline notes..."
# Try with inline notes
if gh release create "v${{ steps.version_bump.outputs.version }}" \
--title "Release v${{ steps.version_bump.outputs.version }}" \
--notes "${{ steps.release_notes.outputs.content }}"; then
echo "Successfully created release using GitHub CLI with inline notes"
echo "release_exists=true" >> $GITHUB_OUTPUT
else
echo "::error::Failed to create release using all methods. Please check logs for details."
echo "release_exists=false" >> $GITHUB_OUTPUT
exit 1
fi
fi
fi
# Add a cleanup step to ensure temporary files are removed
# This step runs AFTER the GitHub release is created
- name: Cleanup temporary files
if: always()
run: |
# Check if the temporary directory exists before attempting to remove it
if [ -d "/tmp/release-notes" ]; then
echo "Cleaning up temporary files..."
rm -rf /tmp/release-notes
echo "Temporary files removed."
else
echo "No temporary files to clean up."
fi
# Also remove the release notes file from the workspace
# This prevents it from being versioned in the repository
if [ -f "$GITHUB_WORKSPACE/release_notes.md" ]; then
echo "Removing release notes file from workspace..."
rm -f "$GITHUB_WORKSPACE/release_notes.md"
echo "Workspace release notes file removed."
fi
- name: Delete processed changesets
# Only delete changesets if the release was successful or already exists
if: (github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch') && (steps.create_release.outcome == 'success' || steps.handle_failure.outputs.release_exists == 'true')
run: |
# Configure Git
git config --global user.name "GitHub Actions"
git config --global user.email "[email protected]"
# Check if the .changesets directory exists and contains files
if [ -d ".changesets" ] && [ "$(find .changesets -type f -name "*.md" | wc -l)" -gt 0 ]; then
# List all changesets before deleting (for logging purposes)
echo "Changesets found in directory:"
ls -la .changesets/
# Remove all .md files in the .changesets directory
find .changesets -type f -name "*.md" -exec rm -f {} \;
# Check if any files were removed
if [ $? -eq 0 ]; then
echo "Changesets removed successfully"
# Stage the deletions
git add -A .changesets/
# Also make sure the release_notes.md file is not staged
git reset -- release_notes.md || true
# Commit the deleted changesets
git commit -m "chore: delete changesets after release v${{ steps.version_bump.outputs.version }}"
git push origin main
echo "Deleted changesets for v${{ steps.version_bump.outputs.version }}"
else
echo "Error removing changesets"
exit 1
fi
else
echo "No changeset files found to delete"
fi
- name: Update develop branch
if: github.event_name == 'pull_request_target' || github.event_name == 'workflow_dispatch'
run: |
# Configure Git
git config --global user.name "GitHub Actions"
git config --global user.email "[email protected]"
# Fetch all branches
git fetch --unshallow || git fetch
# Checkout develop branch
git checkout develop
git pull
# Merge main into develop with a descriptive message
git merge --no-ff origin/main -m "chore: sync main back to develop after release v${{ steps.version_bump.outputs.version }} [skip ci]"
# Push changes to develop
git push origin develop
echo "Successfully synced main back to develop branch"
- name: Commit changes to develop
if: github.event_name == 'workflow_dispatch' || github.event_name == 'schedule'
uses: stefanzweifel/git-auto-commit-action@v4
with:
commit_message: "release: prepare v${{ steps.version_bump.outputs.version }}"
file_pattern: "*.php *.md *.txt package.json"
# For manual/scheduled events, commit to the specified target branch or develop
branch: ${{ github.event.inputs.target_branch || 'develop' }}
env:
GITHUB_TOKEN: ${{ secrets.REPO_PAT }}