diff --git a/.github/workflows/backmerge-to-develop.yml b/.github/workflows/backmerge-to-develop.yml index cb3661b..11df6d1 100644 --- a/.github/workflows/backmerge-to-develop.yml +++ b/.github/workflows/backmerge-to-develop.yml @@ -13,31 +13,31 @@ jobs: github.event.pull_request.merged == true && (startsWith(github.event.pull_request.head.ref, 'release/') || startsWith(github.event.pull_request.head.ref, 'hotfix/')) - + runs-on: ubuntu-latest - + permissions: contents: write pull-requests: write - + steps: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 # Fetch all history for all branches + fetch-depth: 0 # Fetch all history for all branches token: ${{ secrets.GITHUB_TOKEN }} - + - name: Configure Git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - + - name: Extract version from branch name id: version run: | BRANCH_NAME="${{ github.event.pull_request.head.ref }}" echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - + if [[ $BRANCH_NAME == release/v* ]]; then VERSION=$(echo $BRANCH_NAME | sed 's/release\/v//') TYPE="release" @@ -45,10 +45,10 @@ jobs: VERSION=$(echo $BRANCH_NAME | sed 's/hotfix\/v//') TYPE="hotfix" fi - + echo "version=$VERSION" >> $GITHUB_OUTPUT echo "type=$TYPE" >> $GITHUB_OUTPUT - + - name: Check if develop branch exists id: check_develop run: | @@ -58,7 +58,7 @@ jobs: echo "exists=false" >> $GITHUB_OUTPUT echo "⚠️ Develop branch does not exist. Skipping backmerge." fi - + - name: Create backmerge pull request if: steps.check_develop.outputs.exists == 'true' env: @@ -66,16 +66,16 @@ jobs: run: | # Create a unique branch name for the backmerge BACKMERGE_BRANCH="backmerge/main-to-develop-$(date +%Y%m%d-%H%M%S)" - + # Fetch and checkout develop git fetch origin develop:develop - + # Create new branch from develop git checkout -b $BACKMERGE_BRANCH develop - + # Merge main into the backmerge branch echo "Attempting to merge main into $BACKMERGE_BRANCH..." - + # Try to merge main if git merge origin/main --no-edit; then echo "✅ Merge successful, no conflicts" @@ -184,7 +184,7 @@ jobs: # Exit with error to mark the job as failed exit 1 fi - + - name: Summary if: always() run: | @@ -195,9 +195,9 @@ jobs: echo "- **Target Branch:** \`develop\`" >> $GITHUB_STEP_SUMMARY echo "- **Triggered by:** @${{ github.event.pull_request.merged_by.login }}" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - + if [ "${{ steps.check_develop.outputs.exists }}" == "false" ]; then echo "⚠️ **Status:** Skipped - develop branch does not exist" >> $GITHUB_STEP_SUMMARY else echo "✅ **Status:** Backmerge PR created" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + fi diff --git a/.github/workflows/create-release.yml b/.github/workflows/create-release.yml index fede002..f25e944 100644 --- a/.github/workflows/create-release.yml +++ b/.github/workflows/create-release.yml @@ -13,36 +13,36 @@ jobs: github.event.pull_request.merged == true && (startsWith(github.event.pull_request.head.ref, 'release/') || startsWith(github.event.pull_request.head.ref, 'hotfix/')) - + runs-on: ubuntu-latest - + permissions: contents: write pull-requests: read - + steps: - name: Checkout repository uses: actions/checkout@v4 with: - fetch-depth: 0 # Need full history for changelog generation + fetch-depth: 0 # Need full history for changelog generation token: ${{ secrets.GITHUB_TOKEN }} - + - name: Setup Ruby uses: ruby/setup-ruby@v1 with: - ruby-version: '3.2' - + ruby-version: "3.2" + - name: Configure Git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - + - name: Extract version from branch name id: version run: | BRANCH_NAME="${{ github.event.pull_request.head.ref }}" echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT - + if [[ $BRANCH_NAME == release/v* ]]; then VERSION=$(echo $BRANCH_NAME | sed 's/release\/v//') TYPE="release" @@ -52,26 +52,26 @@ jobs: TYPE="hotfix" IS_PRERELEASE="false" fi - + # Validate version format if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then echo "❌ Invalid version format: $VERSION" echo "Version must be in format x.y.z" exit 1 fi - + echo "version=$VERSION" >> $GITHUB_OUTPUT echo "type=$TYPE" >> $GITHUB_OUTPUT echo "tag_name=v$VERSION" >> $GITHUB_OUTPUT echo "is_prerelease=$IS_PRERELEASE" >> $GITHUB_OUTPUT - + echo "✅ Extracted version: $VERSION ($TYPE)" - + - name: Check if tag already exists id: check_tag run: | TAG_NAME="v${{ steps.version.outputs.version }}" - + if git rev-parse "$TAG_NAME" >/dev/null 2>&1; then echo "exists=true" >> $GITHUB_OUTPUT echo "⚠️ Tag $TAG_NAME already exists" @@ -79,19 +79,19 @@ jobs: echo "exists=false" >> $GITHUB_OUTPUT echo "✅ Tag $TAG_NAME does not exist, ready to create" fi - + - name: Generate changelog for release id: changelog run: | VERSION="${{ steps.version.outputs.version }}" - + echo "Generating changelog for version $VERSION..." - + # Generate changelog using the Ruby script ruby scripts/generate_changelog.rb \ --version "$VERSION" \ --output "RELEASE_CHANGELOG.md" - + if [ -f "RELEASE_CHANGELOG.md" ]; then echo "✅ Changelog generated successfully" @@ -103,26 +103,26 @@ jobs: echo "❌ Failed to generate changelog" exit 1 fi - + - name: Create Git tag if: steps.check_tag.outputs.exists == 'false' run: | TAG_NAME="${{ steps.version.outputs.tag_name }}" VERSION="${{ steps.version.outputs.version }}" TYPE="${{ steps.version.outputs.type }}" - + # Create annotated tag if [ "$TYPE" = "release" ]; then TAG_MESSAGE="Release v$VERSION" else TAG_MESSAGE="Hotfix v$VERSION" fi - + git tag -a "$TAG_NAME" -m "$TAG_MESSAGE" git push origin "$TAG_NAME" - + echo "✅ Created and pushed tag: $TAG_NAME" - + - name: Create GitHub Release if: steps.check_tag.outputs.exists == 'false' env: @@ -132,7 +132,7 @@ jobs: VERSION="${{ steps.version.outputs.version }}" TYPE="${{ steps.version.outputs.type }}" IS_PRERELEASE="${{ steps.version.outputs.is_prerelease }}" - + # Set release title and flags if [ "$TYPE" = "release" ]; then RELEASE_TITLE="🚀 Release v$VERSION" @@ -141,52 +141,52 @@ jobs: RELEASE_TITLE="🔥 Hotfix v$VERSION" RELEASE_FLAGS="" fi - + # Add prerelease flag if needed if [ "$IS_PRERELEASE" = "true" ]; then RELEASE_FLAGS="$RELEASE_FLAGS --prerelease" fi - + # Create the GitHub release cat > release_body.md << 'RELEASE_EOF' ${{ steps.changelog.outputs.changelog }} - + --- - + ## 📦 Installation - + ### Docker ```bash docker pull ghcr.io/banua-coder/pico-api-docs:v${{ steps.version.outputs.version }} ``` - + ### Manual Download Download the latest build artifacts from this release. - + ## 🔗 Related Links - + - **API Documentation**: https://pico-api-docs.banuacoder.com - **Source Code**: https://github.com/banua-coder/pico-api-docs - **Issues**: https://github.com/banua-coder/pico-api-docs/issues - + ## 📋 Changelog - + For detailed changes, see [CHANGELOG.md](https://github.com/banua-coder/pico-api-docs/blob/main/CHANGELOG.md) RELEASE_EOF - + # Create the release gh release create "$TAG_NAME" \ --title "$RELEASE_TITLE" \ --notes-file release_body.md \ --target main \ $RELEASE_FLAGS - + echo "✅ GitHub release created: $TAG_NAME" - + # Get release URL for summary RELEASE_URL=$(gh release view "$TAG_NAME" --json url -q '.url') echo "release_url=$RELEASE_URL" >> $GITHUB_OUTPUT - + - name: Summary if: always() run: | @@ -198,7 +198,7 @@ jobs: echo "- **Type:** ${{ steps.version.outputs.type }}" >> $GITHUB_STEP_SUMMARY echo "- **Tag:** \`${{ steps.version.outputs.tag_name }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - + if [ "${{ steps.check_tag.outputs.exists }}" == "true" ]; then echo "⚠️ **Status:** Skipped - tag already exists" >> $GITHUB_STEP_SUMMARY else @@ -207,9 +207,9 @@ jobs: echo "🔗 **Release URL:** ${{ steps.create-release.outputs.release_url }}" >> $GITHUB_STEP_SUMMARY fi fi - + echo "" >> $GITHUB_STEP_SUMMARY echo "### Next Steps" >> $GITHUB_STEP_SUMMARY echo "1. ✅ Tag and release created" >> $GITHUB_STEP_SUMMARY echo "2. 🔄 Backmerge to develop will run automatically" >> $GITHUB_STEP_SUMMARY - echo "3. 🚀 Deployment pipeline will trigger automatically" >> $GITHUB_STEP_SUMMARY \ No newline at end of file + echo "3. 🚀 Deployment pipeline will trigger automatically" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index b0f78c3..a25bb3b 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -3,292 +3,292 @@ name: Deploy Documentation to Production on: push: tags: - - 'v*.*.*' # Triggers on version tags like v1.0.0, v2.1.3, etc. + - "v*.*.*" # Triggers on version tags like v1.0.0, v2.1.3, etc. jobs: build-and-deploy: runs-on: ubuntu-latest - + steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Install pnpm - uses: pnpm/action-setup@v2 - with: - version: 8 - - - name: Set up Node.js - uses: actions/setup-node@v4 - with: - node-version: '20' - # No cache since pnpm-lock.yaml is excluded from git - - - name: Get version from tag - run: | - echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV - echo "Deploying documentation version: ${GITHUB_REF#refs/tags/}" - - - name: Install dependencies - run: pnpm install - - - name: Build documentation site - run: | - echo "📦 Building Vue.js documentation site..." - pnpm run build - - echo "✅ Build completed successfully" - echo "📊 Build output:" - ls -la dist/ - - - name: Prepare deployment package - run: | - echo "📋 Preparing deployment package..." - - # Create deployment directory - mkdir -p deploy-package - - # Copy built files - cp -r dist/* deploy-package/ - - # Copy essential server files - cp .htaccess deploy-package/ - cp public/api-down.html deploy-package/ - - # Copy favicon if exists - cp public/favicon.ico deploy-package/ 2>/dev/null || echo "⚠️ favicon.ico not found, skipping" - - # Ensure logos are included - mkdir -p deploy-package/assets - cp public/sulteng-*.webp deploy-package/assets/ 2>/dev/null || echo "ℹ️ Logos already in dist or not found" - - # Create deployment info - cat > deploy-package/DEPLOY_INFO.txt << EOF - PICO SulTeng COVID-19 API Documentation - Version: ${{ env.VERSION }} - Deployed: $(date -u '+%Y-%m-%d %H:%M:%S UTC') - Built with: Vue.js + Vite - Target: Hostinger Shared Hosting - EOF - - echo "✅ Deployment package prepared" - echo "📊 Package contents:" - ls -la deploy-package/ - echo "📏 Package size: $(du -sh deploy-package | cut -f1)" - - - name: Setup SSH Agent - uses: webfactory/ssh-agent@v0.8.0 - with: - ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }} - log-public-key: false - - - name: Add server to known hosts - run: | - mkdir -p ~/.ssh - echo "Adding ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PORT }} to known hosts..." - ssh-keyscan -H -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts - - - name: Test SSH connection - run: | - echo "Testing SSH connection to ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PORT }}..." - ssh -p ${{ secrets.DEPLOY_PORT }} -o ConnectTimeout=10 -o BatchMode=yes ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} 'echo "SSH connection successful"' - - - name: Deploy to production server - run: | - echo "🚀 Starting deployment of documentation ${{ env.VERSION }} to production..." - - # Create temporary deployment archive - tar -czf docs-${{ env.VERSION }}.tar.gz -C deploy-package . - - # Upload deployment package - echo "📤 Uploading deployment package..." - scp -P ${{ secrets.DEPLOY_PORT }} docs-${{ env.VERSION }}.tar.gz ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/ - - # Execute deployment script on remote server - ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' - set -e - - DEPLOY_PATH="${{ secrets.DOCS_DEPLOY_PATH }}" - VERSION="${{ env.VERSION }}" - TEMP_ARCHIVE="/tmp/docs-${VERSION}.tar.gz" - BACKUP_DIR="" - - echo "🔍 Checking deployment environment..." - echo "Deploy path: $DEPLOY_PATH" - - # Create deployment directory if it doesn't exist - mkdir -p "$DEPLOY_PATH" - cd "$DEPLOY_PATH" - - echo "📁 Current directory contents:" - ls -la - - echo "💾 Creating backup of current deployment..." - if [ -f "index.html" ]; then - BACKUP_DIR="../docs-backup-$(date +%Y%m%d_%H%M%S)" - mkdir -p "$BACKUP_DIR" - cp -r * "$BACKUP_DIR/" 2>/dev/null || echo "⚠️ Some files couldn't be backed up" - echo "✅ Backup created at: $BACKUP_DIR" - echo "BACKUP_DIR=$BACKUP_DIR" > /tmp/backup_path.txt - else - echo "ℹ️ No existing deployment found, skipping backup" - echo "BACKUP_DIR=" > /tmp/backup_path.txt - fi - - echo "📦 Extracting new deployment..." - tar -xzf "$TEMP_ARCHIVE" -C . - - echo "🔧 Setting proper permissions..." - find . -type f -name "*.html" -exec chmod 644 {} \; - find . -type f -name "*.css" -exec chmod 644 {} \; - find . -type f -name "*.js" -exec chmod 644 {} \; - find . -type f -name "*.json" -exec chmod 644 {} \; - find . -type f -name ".htaccess" -exec chmod 644 {} \; - find . -type d -exec chmod 755 {} \; - - echo "🧹 Cleaning up temporary files..." - rm -f "$TEMP_ARCHIVE" - - echo "✅ New deployment files deployed!" - echo "📊 Deployed files:" - ls -la | head -20 - EOF - - - name: Verify deployment and handle rollback - run: | - echo "🔍 Verifying deployment..." - sleep 5 # Give the site time to propagate - - HEALTH_CHECK_PASSED=false - - # Check if the documentation site is accessible - if [ -n "${{ secrets.DOCS_URL }}" ]; then - echo "Testing documentation site at: ${{ secrets.DOCS_URL }}" - - # Perform multiple health checks - CHECKS_PASSED=0 - TOTAL_CHECKS=3 - - for i in $(seq 1 $TOTAL_CHECKS); do - echo "Health check attempt $i/$TOTAL_CHECKS..." + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - name: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: "20" + # No cache since pnpm-lock.yaml is excluded from git + + - name: Get version from tag + run: | + echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV + echo "Deploying documentation version: ${GITHUB_REF#refs/tags/}" + + - name: Install dependencies + run: pnpm install + + - name: Build documentation site + run: | + echo "📦 Building Vue.js documentation site..." + pnpm run build + + echo "✅ Build completed successfully" + echo "📊 Build output:" + ls -la dist/ + + - name: Prepare deployment package + run: | + echo "📋 Preparing deployment package..." + + # Create deployment directory + mkdir -p deploy-package + + # Copy built files + cp -r dist/* deploy-package/ + + # Copy essential server files + cp .htaccess deploy-package/ + cp public/api-down.html deploy-package/ + + # Copy favicon if exists + cp public/favicon.ico deploy-package/ 2>/dev/null || echo "⚠️ favicon.ico not found, skipping" + + # Ensure logos are included + mkdir -p deploy-package/assets + cp public/sulteng-*.webp deploy-package/assets/ 2>/dev/null || echo "ℹ️ Logos already in dist or not found" + + # Create deployment info + cat > deploy-package/DEPLOY_INFO.txt << EOF + PICO SulTeng COVID-19 API Documentation + Version: ${{ env.VERSION }} + Deployed: $(date -u '+%Y-%m-%d %H:%M:%S UTC') + Built with: Vue.js + Vite + Target: Hostinger Shared Hosting + EOF + + echo "✅ Deployment package prepared" + echo "📊 Package contents:" + ls -la deploy-package/ + echo "📏 Package size: $(du -sh deploy-package | cut -f1)" + + - name: Setup SSH Agent + uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }} + log-public-key: false + + - name: Add server to known hosts + run: | + mkdir -p ~/.ssh + echo "Adding ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PORT }} to known hosts..." + ssh-keyscan -H -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts + + - name: Test SSH connection + run: | + echo "Testing SSH connection to ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PORT }}..." + ssh -p ${{ secrets.DEPLOY_PORT }} -o ConnectTimeout=10 -o BatchMode=yes ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} 'echo "SSH connection successful"' + + - name: Deploy to production server + run: | + echo "🚀 Starting deployment of documentation ${{ env.VERSION }} to production..." + + # Create temporary deployment archive + tar -czf docs-${{ env.VERSION }}.tar.gz -C deploy-package . + + # Upload deployment package + echo "📤 Uploading deployment package..." + scp -P ${{ secrets.DEPLOY_PORT }} docs-${{ env.VERSION }}.tar.gz ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/ + + # Execute deployment script on remote server + ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' + set -e - # Check if site responds with 200 - if curl -f -s -I "${{ secrets.DOCS_URL }}" | head -n1 | grep -q "200"; then - echo "✅ HTTP status check passed" - - # Check if it's serving the Vue.js app content - if curl -f -s "${{ secrets.DOCS_URL }}" | grep -q "PICO SulTeng\|pico-api-docs"; then - echo "✅ Content verification passed" - CHECKS_PASSED=$((CHECKS_PASSED + 1)) - else - echo "❌ Content verification failed" - fi + DEPLOY_PATH="${{ secrets.DOCS_DEPLOY_PATH }}" + VERSION="${{ env.VERSION }}" + TEMP_ARCHIVE="/tmp/docs-${VERSION}.tar.gz" + BACKUP_DIR="" + + echo "🔍 Checking deployment environment..." + echo "Deploy path: $DEPLOY_PATH" + + # Create deployment directory if it doesn't exist + mkdir -p "$DEPLOY_PATH" + cd "$DEPLOY_PATH" + + echo "📁 Current directory contents:" + ls -la + + echo "💾 Creating backup of current deployment..." + if [ -f "index.html" ]; then + BACKUP_DIR="../docs-backup-$(date +%Y%m%d_%H%M%S)" + mkdir -p "$BACKUP_DIR" + cp -r * "$BACKUP_DIR/" 2>/dev/null || echo "⚠️ Some files couldn't be backed up" + echo "✅ Backup created at: $BACKUP_DIR" + echo "BACKUP_DIR=$BACKUP_DIR" > /tmp/backup_path.txt else - echo "❌ HTTP status check failed" + echo "ℹ️ No existing deployment found, skipping backup" + echo "BACKUP_DIR=" > /tmp/backup_path.txt fi - if [ $i -lt $TOTAL_CHECKS ]; then - sleep 3 - fi - done - - # Determine if health check passed (majority of checks must pass) - if [ $CHECKS_PASSED -ge 2 ]; then - HEALTH_CHECK_PASSED=true - echo "✅ Health checks passed ($CHECKS_PASSED/$TOTAL_CHECKS)" - else - HEALTH_CHECK_PASSED=false - echo "❌ Health checks failed ($CHECKS_PASSED/$TOTAL_CHECKS)" - fi - else - echo "ℹ️ No documentation URL configured - skipping health check" - HEALTH_CHECK_PASSED=true # Assume success if no URL to test - fi - - # Handle success/failure - ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << EOF - # Read backup path from previous step - if [ -f "/tmp/backup_path.txt" ]; then - BACKUP_DIR=\$(grep "BACKUP_DIR=" /tmp/backup_path.txt | cut -d'=' -f2) - else - BACKUP_DIR="" - fi - - if [ "$HEALTH_CHECK_PASSED" = "true" ]; then - echo "🎉 Deployment verification successful!" + echo "📦 Extracting new deployment..." + tar -xzf "$TEMP_ARCHIVE" -C . - # Clean up backup if it exists - if [ -n "\$BACKUP_DIR" ] && [ -d "\$BACKUP_DIR" ]; then - echo "🧹 Removing backup directory: \$BACKUP_DIR" - rm -rf "\$BACKUP_DIR" - echo "✅ Backup cleaned up successfully" - fi + echo "🔧 Setting proper permissions..." + find . -type f -name "*.html" -exec chmod 644 {} \; + find . -type f -name "*.css" -exec chmod 644 {} \; + find . -type f -name "*.js" -exec chmod 644 {} \; + find . -type f -name "*.json" -exec chmod 644 {} \; + find . -type f -name ".htaccess" -exec chmod 644 {} \; + find . -type d -exec chmod 755 {} \; - # Clean up backup path file - rm -f /tmp/backup_path.txt + echo "🧹 Cleaning up temporary files..." + rm -f "$TEMP_ARCHIVE" - echo "✅ Deployment ${{ env.VERSION }} completed successfully!" + echo "✅ New deployment files deployed!" + echo "📊 Deployed files:" + ls -la | head -20 + EOF + + - name: Verify deployment and handle rollback + run: | + echo "🔍 Verifying deployment..." + sleep 5 # Give the site time to propagate + + HEALTH_CHECK_PASSED=false + + # Check if the documentation site is accessible + if [ -n "${{ secrets.DOCS_URL }}" ]; then + echo "Testing documentation site at: ${{ secrets.DOCS_URL }}" - else - echo "❌ Deployment verification failed! Rolling back..." + # Perform multiple health checks + CHECKS_PASSED=0 + TOTAL_CHECKS=3 - if [ -n "\$BACKUP_DIR" ] && [ -d "\$BACKUP_DIR" ]; then - DEPLOY_PATH="${{ secrets.DOCS_DEPLOY_PATH }}" - cd "\$DEPLOY_PATH" + for i in $(seq 1 $TOTAL_CHECKS); do + echo "Health check attempt $i/$TOTAL_CHECKS..." - echo "🔄 Restoring from backup: \$BACKUP_DIR" + # Check if site responds with 200 + if curl -f -s -I "${{ secrets.DOCS_URL }}" | head -n1 | grep -q "200"; then + echo "✅ HTTP status check passed" + + # Check if it's serving the Vue.js app content + if curl -f -s "${{ secrets.DOCS_URL }}" | grep -q "PICO SulTeng\|pico-api-docs"; then + echo "✅ Content verification passed" + CHECKS_PASSED=$((CHECKS_PASSED + 1)) + else + echo "❌ Content verification failed" + fi + else + echo "❌ HTTP status check failed" + fi - # Remove failed deployment - rm -rf ./* .[^.]* 2>/dev/null || true + if [ $i -lt $TOTAL_CHECKS ]; then + sleep 3 + fi + done + + # Determine if health check passed (majority of checks must pass) + if [ $CHECKS_PASSED -ge 2 ]; then + HEALTH_CHECK_PASSED=true + echo "✅ Health checks passed ($CHECKS_PASSED/$TOTAL_CHECKS)" + else + HEALTH_CHECK_PASSED=false + echo "❌ Health checks failed ($CHECKS_PASSED/$TOTAL_CHECKS)" + fi + else + echo "ℹ️ No documentation URL configured - skipping health check" + HEALTH_CHECK_PASSED=true # Assume success if no URL to test + fi + + # Handle success/failure + ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << EOF + # Read backup path from previous step + if [ -f "/tmp/backup_path.txt" ]; then + BACKUP_DIR=\$(grep "BACKUP_DIR=" /tmp/backup_path.txt | cut -d'=' -f2) + else + BACKUP_DIR="" + fi + + if [ "$HEALTH_CHECK_PASSED" = "true" ]; then + echo "🎉 Deployment verification successful!" - # Restore backup - cp -r "\$BACKUP_DIR"/* . 2>/dev/null || echo "⚠️ Some backup files couldn't be restored" - cp -r "\$BACKUP_DIR"/.[^.]* . 2>/dev/null || true + # Clean up backup if it exists + if [ -n "\$BACKUP_DIR" ] && [ -d "\$BACKUP_DIR" ]; then + echo "🧹 Removing backup directory: \$BACKUP_DIR" + rm -rf "\$BACKUP_DIR" + echo "✅ Backup cleaned up successfully" + fi - echo "✅ Rollback completed - previous version restored" + # Clean up backup path file + rm -f /tmp/backup_path.txt - # Keep backup for investigation - echo "📁 Backup preserved for investigation: \$BACKUP_DIR" + echo "✅ Deployment ${{ env.VERSION }} completed successfully!" else - echo "❌ No backup available for rollback!" - echo "⚠️ Manual intervention required" + echo "❌ Deployment verification failed! Rolling back..." + + if [ -n "\$BACKUP_DIR" ] && [ -d "\$BACKUP_DIR" ]; then + DEPLOY_PATH="${{ secrets.DOCS_DEPLOY_PATH }}" + cd "\$DEPLOY_PATH" + + echo "🔄 Restoring from backup: \$BACKUP_DIR" + + # Remove failed deployment + rm -rf ./* .[^.]* 2>/dev/null || true + + # Restore backup + cp -r "\$BACKUP_DIR"/* . 2>/dev/null || echo "⚠️ Some backup files couldn't be restored" + cp -r "\$BACKUP_DIR"/.[^.]* . 2>/dev/null || true + + echo "✅ Rollback completed - previous version restored" + + # Keep backup for investigation + echo "📁 Backup preserved for investigation: \$BACKUP_DIR" + + else + echo "❌ No backup available for rollback!" + echo "⚠️ Manual intervention required" + fi + + # Clean up backup path file + rm -f /tmp/backup_path.txt + + echo "❌ Deployment ${{ env.VERSION }} failed and rolled back" + exit 1 fi - - # Clean up backup path file - rm -f /tmp/backup_path.txt - - echo "❌ Deployment ${{ env.VERSION }} failed and rolled back" + EOF + + # Exit with error if health check failed + if [ "$HEALTH_CHECK_PASSED" = "false" ]; then + echo "❌ Deployment failed health checks and was rolled back" exit 1 fi - EOF - - # Exit with error if health check failed - if [ "$HEALTH_CHECK_PASSED" = "false" ]; then - echo "❌ Deployment failed health checks and was rolled back" - exit 1 - fi - - - name: Create deployment summary - run: | - echo "## 📚 Documentation Deployment Summary" >> $GITHUB_STEP_SUMMARY - echo "- **Version**: ${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY - echo "- **Target**: ${{ secrets.DOCS_DEPLOY_PATH }}" >> $GITHUB_STEP_SUMMARY - echo "- **Status**: ✅ Deployed successfully" >> $GITHUB_STEP_SUMMARY - echo "- **Site**: ${{ secrets.DOCS_URL || 'URL not configured' }}" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### 📋 What was deployed" >> $GITHUB_STEP_SUMMARY - echo "- Vue.js documentation site" >> $GITHUB_STEP_SUMMARY - echo "- API proxy configuration (.htaccess)" >> $GITHUB_STEP_SUMMARY - echo "- Maintenance page (api-down.html)" >> $GITHUB_STEP_SUMMARY - echo "- Static assets and images" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ✅ Next Steps" >> $GITHUB_STEP_SUMMARY - echo "1. Verify site functionality at ${{ secrets.DOCS_URL }}" >> $GITHUB_STEP_SUMMARY - echo "2. Test Vue Router navigation" >> $GITHUB_STEP_SUMMARY - echo "3. Verify API proxy is working (if backend is running)" >> $GITHUB_STEP_SUMMARY + + - name: Create deployment summary + run: | + echo "## 📚 Documentation Deployment Summary" >> $GITHUB_STEP_SUMMARY + echo "- **Version**: ${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY + echo "- **Target**: ${{ secrets.DOCS_DEPLOY_PATH }}" >> $GITHUB_STEP_SUMMARY + echo "- **Status**: ✅ Deployed successfully" >> $GITHUB_STEP_SUMMARY + echo "- **Site**: ${{ secrets.DOCS_URL || 'URL not configured' }}" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 📋 What was deployed" >> $GITHUB_STEP_SUMMARY + echo "- Vue.js documentation site" >> $GITHUB_STEP_SUMMARY + echo "- API proxy configuration (.htaccess)" >> $GITHUB_STEP_SUMMARY + echo "- Maintenance page (api-down.html)" >> $GITHUB_STEP_SUMMARY + echo "- Static assets and images" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Next Steps" >> $GITHUB_STEP_SUMMARY + echo "1. Verify site functionality at ${{ secrets.DOCS_URL }}" >> $GITHUB_STEP_SUMMARY + echo "2. Test Vue Router navigation" >> $GITHUB_STEP_SUMMARY + echo "3. Verify API proxy is working (if backend is running)" >> $GITHUB_STEP_SUMMARY create-release: runs-on: ubuntu-latest @@ -296,119 +296,119 @@ jobs: if: needs.build-and-deploy.result == 'success' permissions: contents: write - + steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 - - - name: Get version and release info - id: release_info - run: | - VERSION=${GITHUB_REF#refs/tags/} - echo "version=$VERSION" >> $GITHUB_OUTPUT - - # Get the previous tag for changelog - PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -A1 "^${VERSION}$" | tail -n1) - if [ -z "$PREVIOUS_TAG" ] || [ "$PREVIOUS_TAG" = "$VERSION" ]; then - PREVIOUS_TAG=$(git tag --sort=-version:refname | head -n2 | tail -n1) - fi - echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT - - - name: Generate release notes - id: release_notes - run: | - VERSION=${{ steps.release_info.outputs.version }} - PREVIOUS_TAG=${{ steps.release_info.outputs.previous_tag }} - - # Create release notes - cat > release_notes.md << 'EOF' - ## 📚 PICO SulTeng COVID-19 API Documentation ${{ steps.release_info.outputs.version }} - - **Deployment**: ✅ Successfully deployed to production - **Site**: ${{ secrets.DOCS_URL || 'Documentation site' }} - - ### 🆕 What's New - - EOF - - # Get commits since last tag - if [ -n "$PREVIOUS_TAG" ]; then - echo "Changes since $PREVIOUS_TAG:" >> release_notes.md - echo "" >> release_notes.md - - git log --pretty=format:"- %s" "${PREVIOUS_TAG}..${VERSION}" | \ - grep -v "Merge branch\|Merge pull request" | \ - head -20 >> release_notes.md - else - echo "- Initial documentation release" >> release_notes.md - fi - - # Add deployment details - cat >> release_notes.md << 'EOF' - - ### 🚀 Deployment Details - - - **Built with**: Vue.js 3 + Vite + TypeScript - - **Features**: Responsive design, bilingual support (ID/EN), API integration - - **Deployment Time**: $(date -u '+%Y-%m-%d %H:%M:%S UTC') - - **Server**: Hostinger Shared Hosting - - ### 🔗 Quick Links - - - [Documentation Site](${{ secrets.DOCS_URL || '#' }}) - - [API Health Check](${{ secrets.DOCS_URL || 'https://pico-api.banuacoder.com' }}/api/v1/health) - - [Repository](https://github.com/banua-coder/pico-api-docs) - - ### 📱 Features Included - - - 🌐 Bilingual support (Indonesian/English) - - 📊 Interactive API documentation - - 💻 Responsive design for all devices - - 🔄 Real-time API integration - - 📈 COVID-19 data visualization - - 🖼️ Official Central Sulawesi branding - EOF - - # Set output for GitHub Actions - echo 'RELEASE_NOTES<> $GITHUB_OUTPUT - cat release_notes.md >> $GITHUB_OUTPUT - echo 'EOF' >> $GITHUB_OUTPUT - - - name: Create GitHub Release - env: - GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} - run: | - VERSION=${{ steps.release_info.outputs.version }} - - # Create the release - gh release create "$VERSION" \ - --title "📚 Documentation $VERSION" \ - --notes "${{ steps.release_notes.outputs.RELEASE_NOTES }}" \ - --target main + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Get version and release info + id: release_info + run: | + VERSION=${GITHUB_REF#refs/tags/} + echo "version=$VERSION" >> $GITHUB_OUTPUT + + # Get the previous tag for changelog + PREVIOUS_TAG=$(git tag --sort=-version:refname | grep -A1 "^${VERSION}$" | tail -n1) + if [ -z "$PREVIOUS_TAG" ] || [ "$PREVIOUS_TAG" = "$VERSION" ]; then + PREVIOUS_TAG=$(git tag --sort=-version:refname | head -n2 | tail -n1) + fi + echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT + + - name: Generate release notes + id: release_notes + run: | + VERSION=${{ steps.release_info.outputs.version }} + PREVIOUS_TAG=${{ steps.release_info.outputs.previous_tag }} + + # Create release notes + cat > release_notes.md << 'EOF' + ## 📚 PICO SulTeng COVID-19 API Documentation ${{ steps.release_info.outputs.version }} + + **Deployment**: ✅ Successfully deployed to production + **Site**: ${{ secrets.DOCS_URL || 'Documentation site' }} + + ### 🆕 What's New + + EOF + + # Get commits since last tag + if [ -n "$PREVIOUS_TAG" ]; then + echo "Changes since $PREVIOUS_TAG:" >> release_notes.md + echo "" >> release_notes.md + + git log --pretty=format:"- %s" "${PREVIOUS_TAG}..${VERSION}" | \ + grep -v "Merge branch\|Merge pull request" | \ + head -20 >> release_notes.md + else + echo "- Initial documentation release" >> release_notes.md + fi + + # Add deployment details + cat >> release_notes.md << 'EOF' + + ### 🚀 Deployment Details + + - **Built with**: Vue.js 3 + Vite + TypeScript + - **Features**: Responsive design, bilingual support (ID/EN), API integration + - **Deployment Time**: $(date -u '+%Y-%m-%d %H:%M:%S UTC') + - **Server**: Hostinger Shared Hosting + + ### 🔗 Quick Links + + - [Documentation Site](${{ secrets.DOCS_URL || '#' }}) + - [API Health Check](${{ secrets.DOCS_URL || 'https://pico-api.banuacoder.com' }}/api/v1/health) + - [Repository](https://github.com/banua-coder/pico-api-docs) + + ### 📱 Features Included + + - 🌐 Bilingual support (Indonesian/English) + - 📊 Interactive API documentation + - 💻 Responsive design for all devices + - 🔄 Real-time API integration + - 📈 COVID-19 data visualization + - 🖼️ Official Central Sulawesi branding + EOF + + # Set output for GitHub Actions + echo 'RELEASE_NOTES<> $GITHUB_OUTPUT + cat release_notes.md >> $GITHUB_OUTPUT + echo 'EOF' >> $GITHUB_OUTPUT + + - name: Create GitHub Release + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION=${{ steps.release_info.outputs.version }} + + # Create the release + gh release create "$VERSION" \ + --title "📚 Documentation $VERSION" \ + --notes "${{ steps.release_notes.outputs.RELEASE_NOTES }}" \ + --target main notification: runs-on: ubuntu-latest needs: [build-and-deploy, create-release] if: always() - + steps: - - name: Notify deployment status - run: | - DEPLOY_STATUS="${{ needs.build-and-deploy.result }}" - RELEASE_STATUS="${{ needs.create-release.result }}" - - if [ "$DEPLOY_STATUS" == "success" ]; then - echo "✅ Documentation deployment successful for ${{ github.ref_name }}" - echo "🌐 Site should be available at: ${{ secrets.DOCS_URL }}" - - if [ "$RELEASE_STATUS" == "success" ]; then - echo "✅ GitHub release created successfully" + - name: Notify deployment status + run: | + DEPLOY_STATUS="${{ needs.build-and-deploy.result }}" + RELEASE_STATUS="${{ needs.create-release.result }}" + + if [ "$DEPLOY_STATUS" == "success" ]; then + echo "✅ Documentation deployment successful for ${{ github.ref_name }}" + echo "🌐 Site should be available at: ${{ secrets.DOCS_URL }}" + + if [ "$RELEASE_STATUS" == "success" ]; then + echo "✅ GitHub release created successfully" + else + echo "⚠️ GitHub release creation failed, but deployment succeeded" + fi else - echo "⚠️ GitHub release creation failed, but deployment succeeded" + echo "❌ Documentation deployment failed for ${{ github.ref_name }}" + exit 1 fi - else - echo "❌ Documentation deployment failed for ${{ github.ref_name }}" - exit 1 - fi \ No newline at end of file diff --git a/.github/workflows/release-automation.yml b/.github/workflows/release-automation.yml index 422a1ca..de92612 100644 --- a/.github/workflows/release-automation.yml +++ b/.github/workflows/release-automation.yml @@ -1,218 +1,351 @@ -name: Release Automation +name: Release Workflow - Tag, Deploy & Back-merge on: - create: + pull_request: + branches: [main] + types: [closed] jobs: - prepare-release: - if: | - github.event.ref_type == 'branch' && - (startsWith(github.event.ref, 'release/v') || startsWith(github.event.ref, 'hotfix/v')) - + release-process: + if: github.event.pull_request.merged == true && (startsWith(github.head_ref, 'release/') || startsWith(github.head_ref, 'hotfix/')) runs-on: ubuntu-latest - permissions: contents: write pull-requests: write - + actions: write + steps: - - name: Checkout repository + - name: Checkout main branch uses: actions/checkout@v4 with: - fetch-depth: 0 # Need full history for changelog generation + ref: main + fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} - - - name: Setup Ruby - uses: ruby/setup-ruby@v1 - with: - ruby-version: '3.2' - + - name: Configure Git run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - + - name: Extract version from branch name - id: extract_version + id: version run: | - BRANCH_NAME="${{ github.event.ref }}" - # Extract version from branch name (release/v1.2.3 or hotfix/v1.2.3) - VERSION=$(echo "$BRANCH_NAME" | sed 's/.*\/v//') - - if [[ ! "$VERSION" =~ ^[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "❌ Invalid version format: $VERSION" - echo "Version must be in format x.y.z" - exit 1 + BRANCH_NAME="${{ github.event.pull_request.head.ref }}" + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + + if [[ $BRANCH_NAME == release/* ]]; then + VERSION=$(echo $BRANCH_NAME | sed 's/release\///') + TYPE="release" + BASE_BRANCH="develop" + elif [[ $BRANCH_NAME == hotfix/* ]]; then + VERSION=$(echo $BRANCH_NAME | sed 's/hotfix\///') + TYPE="hotfix" + BASE_BRANCH="main" fi - + + # Ensure version starts with 'v' + if [[ ! $VERSION == v* ]]; then + VERSION="v$VERSION" + fi + echo "version=$VERSION" >> $GITHUB_OUTPUT - echo "branch_type=$(echo "$BRANCH_NAME" | cut -d'/' -f1)" >> $GITHUB_OUTPUT - echo "✅ Extracted version: $VERSION" - - - name: Update package.json version + echo "type=$TYPE" >> $GITHUB_OUTPUT + echo "base_branch=$BASE_BRANCH" >> $GITHUB_OUTPUT + echo "clean_version=$(echo $VERSION | sed 's/^v//')" >> $GITHUB_OUTPUT + + # STEP 1: CREATE TAG AND TRIGGER DEPLOYMENT + - name: Check if tag already exists + id: check_tag run: | - VERSION="${{ steps.extract_version.outputs.version }}" - - # Update version in package.json - if [ -f "package.json" ]; then - # Use jq to update version if available, otherwise use sed - if command -v jq >/dev/null 2>&1; then - jq ".version = \"$VERSION\"" package.json > package.json.tmp && mv package.json.tmp package.json - else - sed -i.bak "s/\"version\": \".*\"/\"version\": \"$VERSION\"/" package.json && rm package.json.bak - fi - echo "✅ Updated package.json to version $VERSION" + VERSION="${{ steps.version.outputs.version }}" + + if git tag -l | grep -q "^${VERSION}$"; then + echo "tag_exists=true" >> $GITHUB_OUTPUT + echo "⚠️ Tag $VERSION already exists" else - echo "⚠️ package.json not found, skipping version update" + echo "tag_exists=false" >> $GITHUB_OUTPUT + echo "✅ Tag $VERSION does not exist, will create" fi - - - name: Generate changelog - id: changelog + + - name: Get previous tag for changelog + id: previous_tag run: | - VERSION="${{ steps.extract_version.outputs.version }}" - - # Generate changelog using the Ruby script - echo "Generating changelog for version $VERSION..." - - # Create CHANGELOG.md if it doesn't exist - if [ ! -f "CHANGELOG.md" ]; then - echo "# Changelog" > CHANGELOG.md - echo "" >> CHANGELOG.md - echo "All notable changes to this project will be documented in this file." >> CHANGELOG.md - echo "" >> CHANGELOG.md + # Get the most recent tag before this one + PREVIOUS_TAG=$(git tag --sort=-version:refname | head -n1 || echo "") + echo "previous_tag=$PREVIOUS_TAG" >> $GITHUB_OUTPUT + echo "Previous tag: $PREVIOUS_TAG" + + # Determine base commit for changelog + if [ -n "$PREVIOUS_TAG" ]; then + BASE_COMMIT="$PREVIOUS_TAG" + else + # If no tags exist, use first commit + BASE_COMMIT=$(git rev-list --max-parents=0 HEAD) fi - - # Generate changelog for this version - ruby scripts/generate_changelog.rb \ - --version "$VERSION" \ - --output "CHANGELOG_NEW.md" - - # Prepend new changelog to existing one - if [ -f "CHANGELOG_NEW.md" ]; then - # Add separator - echo "" >> CHANGELOG_NEW.md - echo "---" >> CHANGELOG_NEW.md - echo "" >> CHANGELOG_NEW.md - - # Append existing changelog (skip first 3 lines if they're the header) - if [ -f "CHANGELOG.md" ]; then - tail -n +4 CHANGELOG.md >> CHANGELOG_NEW.md 2>/dev/null || cat CHANGELOG.md >> CHANGELOG_NEW.md - fi - - mv CHANGELOG_NEW.md CHANGELOG.md - echo "✅ Changelog generated successfully" + echo "base_commit=$BASE_COMMIT" >> $GITHUB_OUTPUT + + - name: Generate tag message and changelog + id: tag_info + run: | + VERSION="${{ steps.version.outputs.version }}" + TYPE="${{ steps.version.outputs.type }}" + PREVIOUS_TAG="${{ steps.previous_tag.outputs.previous_tag }}" + BASE_COMMIT="${{ steps.previous_tag.outputs.base_commit }}" + + # Create tag message + TAG_MESSAGE="$TYPE: $VERSION" + echo "tag_message=$TAG_MESSAGE" >> $GITHUB_OUTPUT + + # Generate categorized changelog for tag annotation + CHANGELOG_FILE="tag_changelog.txt" + + echo "$TYPE $VERSION" > $CHANGELOG_FILE + echo "" >> $CHANGELOG_FILE + echo "Merged PR: ${{ github.event.pull_request.html_url }}" >> $CHANGELOG_FILE + echo "Merged by: @${{ github.event.pull_request.merged_by.login }}" >> $CHANGELOG_FILE + echo "" >> $CHANGELOG_FILE + + if [ -n "$PREVIOUS_TAG" ]; then + echo "Changes since $PREVIOUS_TAG:" >> $CHANGELOG_FILE else - echo "❌ Failed to generate changelog" - exit 1 + echo "Initial release changes:" >> $CHANGELOG_FILE fi - - # Save changelog content for PR body - echo "changelog<> $GITHUB_OUTPUT - ruby scripts/generate_changelog.rb --version "$VERSION" | head -50 >> $GITHUB_OUTPUT - echo "EOF" >> $GITHUB_OUTPUT - - - name: Commit changes - id: commit + echo "" >> $CHANGELOG_FILE + + # Categorize changes + echo "🚀 Features:" >> $CHANGELOG_FILE + git log --pretty=format:"- %s (%h)" "$BASE_COMMIT..HEAD" | \ + grep -E "^- (feat|feature):" | head -10 >> $CHANGELOG_FILE || echo "- No new features" >> $CHANGELOG_FILE + echo "" >> $CHANGELOG_FILE + + echo "🐛 Bug Fixes:" >> $CHANGELOG_FILE + git log --pretty=format:"- %s (%h)" "$BASE_COMMIT..HEAD" | \ + grep -E "^- (fix|bugfix):" | head -10 >> $CHANGELOG_FILE || echo "- No bug fixes" >> $CHANGELOG_FILE + echo "" >> $CHANGELOG_FILE + + echo "🛠️ Other Changes:" >> $CHANGELOG_FILE + git log --pretty=format:"- %s (%h)" "$BASE_COMMIT..HEAD" | \ + grep -vE "^- (feat|feature|fix|bugfix):|Merge branch|Merge pull request|chore.*auto-bump|chore.*back-merge" | \ + head -10 >> $CHANGELOG_FILE || echo "- No other changes" >> $CHANGELOG_FILE + echo "" >> $CHANGELOG_FILE + echo "🚀 Deployment will be triggered automatically." >> $CHANGELOG_FILE + + echo "changelog_file=$CHANGELOG_FILE" >> $GITHUB_OUTPUT + + - name: Create and push tag + if: steps.check_tag.outputs.tag_exists == 'false' run: | - VERSION="${{ steps.extract_version.outputs.version }}" - BRANCH_TYPE="${{ steps.extract_version.outputs.branch_type }}" - - # Create a new branch for the release preparation - PREP_BRANCH="chore/prepare-${BRANCH_TYPE}-v${VERSION}" - git checkout -b "$PREP_BRANCH" - - # Stage changes - git add -A - - # Check if there are changes to commit - if git diff --staged --quiet; then - echo "No changes to commit" - echo "has_changes=false" >> $GITHUB_OUTPUT + VERSION="${{ steps.version.outputs.version }}" + TAG_MESSAGE="${{ steps.tag_info.outputs.tag_message }}" + CHANGELOG_FILE="${{ steps.tag_info.outputs.changelog_file }}" + + echo "🏷️ Creating annotated tag $VERSION..." + + # Create annotated tag with changelog + git tag -a "$VERSION" -F "$CHANGELOG_FILE" + + # Push the tag + git push origin "$VERSION" + + echo "✅ Tag $VERSION created and pushed successfully" + + # STEP 2: CREATE BACK-MERGE TO DEVELOP + - name: Generate unique back-merge branch name + id: branch_name + run: | + BASE_BRANCH="chore/back-merge-${{ steps.version.outputs.version }}-to-develop" + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + UNIQUE_BRANCH="${BASE_BRANCH}-${TIMESTAMP}" + + echo "base_branch=$BASE_BRANCH" >> $GITHUB_OUTPUT + echo "unique_branch=$UNIQUE_BRANCH" >> $GITHUB_OUTPUT + echo "Generated unique branch name: $UNIQUE_BRANCH" + + - name: Check for existing back-merge branches + id: check_branch + run: | + BASE_BRANCH="${{ steps.branch_name.outputs.base_branch }}" + UNIQUE_BRANCH="${{ steps.branch_name.outputs.unique_branch }}" + + # Check if base branch name already exists + if git ls-remote --heads origin "$BASE_BRANCH" | grep -q "$BASE_BRANCH"; then + echo "base_branch_exists=true" >> $GITHUB_OUTPUT + echo "⚠️ Base branch $BASE_BRANCH already exists, using unique name" + FINAL_BRANCH="$UNIQUE_BRANCH" else - # Commit with conventional commit message - if [ "$BRANCH_TYPE" = "release" ]; then - git commit -m "chore(release): prepare release v$VERSION" - else - git commit -m "fix(hotfix): prepare hotfix v$VERSION" - fi + echo "base_branch_exists=false" >> $GITHUB_OUTPUT + echo "✅ Base branch $BASE_BRANCH available" + FINAL_BRANCH="$BASE_BRANCH" + fi + + echo "final_branch=$FINAL_BRANCH" >> $GITHUB_OUTPUT + echo "Final branch name: $FINAL_BRANCH" + + - name: Create back-merge branch + id: backmerge + run: | + BACKMERGE_BRANCH="${{ steps.check_branch.outputs.final_branch }}" + echo "backmerge_branch=$BACKMERGE_BRANCH" >> $GITHUB_OUTPUT + + # Fetch latest develop + git fetch origin develop:develop + + # Create and checkout new branch from develop + echo "🌿 Creating branch $BACKMERGE_BRANCH from develop" + git checkout -b "$BACKMERGE_BRANCH" develop + + # Verify we're on the correct branch + CURRENT_BRANCH=$(git branch --show-current) + echo "Current branch: $CURRENT_BRANCH" + + if [ "$CURRENT_BRANCH" != "$BACKMERGE_BRANCH" ]; then + echo "❌ Failed to create/checkout branch $BACKMERGE_BRANCH" + exit 1 + fi + + # Merge main into the back-merge branch with conflict resolution + echo "🔄 Merging main into $BACKMERGE_BRANCH" + + # Attempt merge, if conflicts occur, resolve them automatically + if ! git merge --no-ff main -m "chore: back-merge ${{ steps.version.outputs.version }} from main to develop + + Automated back-merge of ${{ steps.version.outputs.type }} ${{ steps.version.outputs.version }} from main branch. + + Original PR: ${{ github.event.pull_request.html_url }} + Merged commit: ${{ github.event.pull_request.merge_commit_sha }} + Tag created: ${{ steps.version.outputs.version }} + Branch: $BACKMERGE_BRANCH"; then - # Push the new branch - git push origin "$PREP_BRANCH" + echo "⚠️ Merge conflicts detected, resolving automatically..." - echo "has_changes=true" >> $GITHUB_OUTPUT - echo "prep_branch=$PREP_BRANCH" >> $GITHUB_OUTPUT - echo "✅ Changes committed and pushed to $PREP_BRANCH" + # For version-related conflicts, prefer develop branch (newer) changes + if git status --porcelain | grep -q "package.json"; then + echo "🔄 Resolving version conflict in package.json (keeping develop version)" + git checkout --ours package.json + fi + + # Check for any remaining conflicts + if git status --porcelain | grep -q "^UU\|^AA"; then + echo "❌ Unresolved conflicts remain after automatic resolution" + git status --porcelain + echo "Manual intervention required - marking PR for review" + + # Stage resolved files + git add . + git commit -m "resolve: automatic conflict resolution for back-merge + + Conflicts resolved automatically: + - Version files: kept develop branch versions (newer) + - Other conflicts may require manual review" + else + # All conflicts resolved, stage and commit + git add . + git commit -m "resolve: automatic conflict resolution for back-merge + + Successfully resolved version conflicts by keeping develop branch changes." + fi fi - - - name: Create Pull Request - if: steps.commit.outputs.has_changes == 'true' + + # Push the back-merge branch + echo "⬆️ Pushing $BACKMERGE_BRANCH to origin" + git push origin "$BACKMERGE_BRANCH" + + echo "✅ Back-merge branch created and pushed successfully" + + - name: Create back-merge PR env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | - VERSION="${{ steps.extract_version.outputs.version }}" - BRANCH_TYPE="${{ steps.extract_version.outputs.branch_type }}" - PREP_BRANCH="${{ steps.commit.outputs.prep_branch }}" - SOURCE_BRANCH="${{ github.event.ref }}" - - # Determine target branch (should be the release branch) - TARGET_BRANCH="$SOURCE_BRANCH" - - if [ "$BRANCH_TYPE" = "release" ]; then - PR_TITLE="chore(release): prepare release v$VERSION" - LABELS="release,auto-generated" + BACKMERGE_BRANCH="${{ steps.backmerge.outputs.backmerge_branch }}" + BASE_BRANCH_EXISTS="${{ steps.check_branch.outputs.base_branch_exists }}" + + # Add branch conflict info to description if needed + if [[ "$BASE_BRANCH_EXISTS" == "true" ]]; then + BRANCH_INFO="- **Branch**: \`$BACKMERGE_BRANCH\` ⚠️ (unique name due to conflict)" else - PR_TITLE="fix(hotfix): prepare hotfix v$VERSION" - LABELS="hotfix,auto-generated,priority:high" + BRANCH_INFO="- **Branch**: \`$BACKMERGE_BRANCH\`" fi - - # Create PR body - cat > pr_body.md << 'PREOF' - ## 🚀 Prepare ${{ steps.extract_version.outputs.branch_type == 'release' && 'Release' || 'Hotfix' }} v${{ steps.extract_version.outputs.version }} - - This PR prepares the following changes for v${{ steps.extract_version.outputs.version }}: - - ### 📋 Changes Included - - ✅ Version bumped to v${{ steps.extract_version.outputs.version }} - - ✅ Changelog updated - - ### 📝 Changelog Preview - - ${{ steps.changelog.outputs.changelog }} - - ### ✔️ Next Steps - 1. Review and merge this PR into `${{ github.event.ref }}` - 2. Create a PR from `${{ github.event.ref }}` to `main` - 3. After merging to `main`, a tag `v${{ steps.extract_version.outputs.version }}` will be created - 4. A GitHub release will be created with the changelog - 5. Changes will be backmerged to `develop` branch - - --- - *This PR was automatically generated when the ${{ github.event.ref }} branch was created.* - PREOF - - # Create the pull request from prep branch to release/hotfix branch + gh pr create \ - --base "$TARGET_BRANCH" \ - --head "$PREP_BRANCH" \ - --title "$PR_TITLE" \ - --body-file pr_body.md \ - --label "$LABELS" - - echo "✅ Pull request created successfully" - - - name: Summary - if: always() + --base develop \ + --head "$BACKMERGE_BRANCH" \ + --title "chore: back-merge ${{ steps.version.outputs.version }} from main to develop" \ + --body "$(cat <> $GITHUB_STEP_SUMMARY + BRANCH_NAME="${{ steps.version.outputs.branch_name }}" + + echo "🧹 Cleaning up merged branch: $BRANCH_NAME" + + # Delete the remote branch + git push origin --delete "$BRANCH_NAME" || echo "⚠️ Branch $BRANCH_NAME may have already been deleted" + + echo "✅ Branch cleanup completed" + + - name: Create workflow summary + run: | + VERSION="${{ steps.version.outputs.version }}" + TYPE="${{ steps.version.outputs.type }}" + BRANCH_NAME="${{ steps.version.outputs.branch_name }}" + BACKMERGE_BRANCH="${{ steps.backmerge.outputs.backmerge_branch }}" + BASE_BRANCH_EXISTS="${{ steps.check_branch.outputs.base_branch_exists }}" + + echo "## 🚀 Release Workflow Completed" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - echo "- **Branch:** ${{ github.event.ref }}" >> $GITHUB_STEP_SUMMARY - echo "- **Type:** ${{ steps.extract_version.outputs.branch_type }}" >> $GITHUB_STEP_SUMMARY - echo "- **Version:** v${{ steps.extract_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY - echo "- **Changes Committed:** ${{ steps.commit.outputs.has_changes == 'true' && '✅ Yes' || '❌ No' }}" >> $GITHUB_STEP_SUMMARY + echo "**Version**: \`$VERSION\`" >> $GITHUB_STEP_SUMMARY + echo "**Type**: $TYPE" >> $GITHUB_STEP_SUMMARY + echo "**Original Branch**: \`$BRANCH_NAME\` (cleaned up)" >> $GITHUB_STEP_SUMMARY + echo "**Back-merge Branch**: \`$BACKMERGE_BRANCH\`" >> $GITHUB_STEP_SUMMARY + echo "**Merge Commit**: \`${{ github.event.pull_request.merge_commit_sha }}\`" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY - - if [ "${{ steps.commit.outputs.has_changes }}" == "true" ]; then - echo "✅ **Status:** Pull request created for v${{ steps.extract_version.outputs.version }}" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Completed Actions" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.check_tag.outputs.tag_exists }}" == "true" ]]; then + echo "- ⚠️ Tag \`$VERSION\` already existed (no new deployment)" >> $GITHUB_STEP_SUMMARY + else + echo "- 🏷️ Created and pushed tag \`$VERSION\`" >> $GITHUB_STEP_SUMMARY + echo "- 🚀 Triggered deployment workflow" >> $GITHUB_STEP_SUMMARY + fi + + if [[ "$BASE_BRANCH_EXISTS" == "true" ]]; then + echo "- 🔄 Created back-merge PR to develop (unique branch name due to conflict)" >> $GITHUB_STEP_SUMMARY else - echo "⚠️ **Status:** No changes needed" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + echo "- 🔄 Created back-merge PR to develop" >> $GITHUB_STEP_SUMMARY + fi + + echo "- 🧹 Deleted merged branch \`$BRANCH_NAME\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### 🔗 Next Steps" >> $GITHUB_STEP_SUMMARY + echo "1. 🔍 [Monitor deployment progress](${{ github.server_url }}/${{ github.repository }}/actions)" >> $GITHUB_STEP_SUMMARY + echo "2. ✅ Review and merge the back-merge PR to develop" >> $GITHUB_STEP_SUMMARY + echo "3. 🎉 Release process complete!" >> $GITHUB_STEP_SUMMARY diff --git a/.github/workflows/release-branch-creation.yml b/.github/workflows/release-branch-creation.yml new file mode 100644 index 0000000..7de8a22 --- /dev/null +++ b/.github/workflows/release-branch-creation.yml @@ -0,0 +1,364 @@ +name: Release Branch Creation & Changelog + +on: + create: + push: + branches: + - "release/**" + - "hotfix/**" + +jobs: + release-branch-setup: + if: github.event_name == 'create' && github.event.ref_type == 'branch' && (startsWith(github.event.ref, 'release/') || startsWith(github.event.ref, 'hotfix/')) + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout the new branch + uses: actions/checkout@v4 + with: + ref: ${{ github.event.ref }} + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Analyze branch and version + id: version_info + run: | + BRANCH_NAME="${{ github.event.ref }}" + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + + if [[ $BRANCH_NAME == release/* ]]; then + VERSION=$(echo $BRANCH_NAME | sed 's/release\///') + TYPE="release" + BASE_BRANCH="develop" + SHOULD_BUMP_DEVELOP="true" + elif [[ $BRANCH_NAME == hotfix/* ]]; then + VERSION=$(echo $BRANCH_NAME | sed 's/hotfix\///') + TYPE="hotfix" + BASE_BRANCH="main" + SHOULD_BUMP_DEVELOP="false" + fi + + # Ensure version starts with 'v' + if [[ ! $VERSION == v* ]]; then + VERSION="v$VERSION" + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "type=$TYPE" >> $GITHUB_OUTPUT + echo "base_branch=$BASE_BRANCH" >> $GITHUB_OUTPUT + echo "should_bump_develop=$SHOULD_BUMP_DEVELOP" >> $GITHUB_OUTPUT + echo "clean_version=$(echo $VERSION | sed 's/^v//')" >> $GITHUB_OUTPUT + + - name: Setup Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: "3.2" + + - name: Generate changelog using Ruby script + id: changelog + run: | + VERSION="${{ steps.version_info.outputs.version }}" + echo "🚀 Generating changelog for $VERSION..." + + # Make the script executable + chmod +x scripts/generate_changelog.rb + + # Run changelog generation + if ruby scripts/generate_changelog.rb --version "$VERSION" --force 2>&1; then + echo "✅ Changelog generation completed" + + # Check if CHANGELOG.md was actually updated + if git diff --quiet CHANGELOG.md; then + echo "⚠️ CHANGELOG.md was not modified by Ruby script" + CHANGELOG_STATUS="false" + else + echo "✅ CHANGELOG.md was updated" + git diff --stat CHANGELOG.md + CHANGELOG_STATUS="true" + fi + + echo "changelog_updated=$CHANGELOG_STATUS" >> $GITHUB_OUTPUT + else + echo "❌ Changelog generation failed" + echo "changelog_updated=false" >> $GITHUB_OUTPUT + fi + + - name: Update version in package.json + run: | + VERSION="${{ steps.version_info.outputs.clean_version }}" + echo "📝 Updating version to $VERSION in package.json..." + + if [ -f "package.json" ]; then + node -e " + const fs = require('fs'); + const pkg = require('./package.json'); + pkg.version = '$VERSION'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\\n'); + " + echo "✅ Updated package.json to version $VERSION" + fi + + - name: Create preparation PR branch + id: pr_branch + run: | + VERSION="${{ steps.version_info.outputs.version }}" + TYPE="${{ steps.version_info.outputs.type }}" + RELEASE_BRANCH="${{ steps.version_info.outputs.branch_name }}" + + # Create PR branch for changelog and version updates + PR_BRANCH="chore/prepare-$TYPE-$VERSION" + git checkout -b "$PR_BRANCH" + echo "pr_branch=$PR_BRANCH" >> $GITHUB_OUTPUT + + # Add all changes + git add . + + # Check if there are changes to commit + if git diff --cached --quiet; then + echo "No changes to commit" + echo "has_changes=false" >> $GITHUB_OUTPUT + else + echo "has_changes=true" >> $GITHUB_OUTPUT + + # Create commit message + CHANGELOG_STATUS="${{ steps.changelog.outputs.changelog_updated }}" + if [[ "$CHANGELOG_STATUS" == "true" ]]; then + CHANGELOG_INFO="- Generate release changelog" + else + CHANGELOG_INFO="- Changelog generation skipped (manual update needed)" + fi + + # Commit with multiline message + git commit -m "chore: prepare $VERSION $TYPE" \ + -m "" \ + -m "- Update version to ${{ steps.version_info.outputs.clean_version }} in package.json" \ + -m "$CHANGELOG_INFO" \ + -m "" \ + -m "This commit prepares the $RELEASE_BRANCH branch for $TYPE." + + # Push the PR branch + git push origin "$PR_BRANCH" + fi + + - name: Create preparation PR + if: steps.pr_branch.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.version_info.outputs.version }}" + TYPE="${{ steps.version_info.outputs.type }}" + RELEASE_BRANCH="${{ steps.version_info.outputs.branch_name }}" + PR_BRANCH="${{ steps.pr_branch.outputs.pr_branch }}" + CHANGELOG_STATUS="${{ steps.changelog.outputs.changelog_updated }}" + + # Build PR body based on changelog status + if [[ "$CHANGELOG_STATUS" == "true" ]]; then + CHANGELOG_SECTION="- ✅ Generated changelog entries\n- Categorized commits since last release" + else + CHANGELOG_SECTION="- ⚠️ Changelog generation was skipped (manual update needed)\n- Please manually update CHANGELOG.md before merging" + fi + + PR_BODY="## Summary + Automated preparation for $TYPE $VERSION. + + This PR was automatically created when the $RELEASE_BRANCH branch was detected. + + ## Changes Made + + ### 📝 Version Updates + - Updated version to ${{ steps.version_info.outputs.clean_version }} in package.json + + ### 📋 Changelog + $CHANGELOG_SECTION + + ## Auto-generated + This PR was automatically created by GitHub Actions when the $TYPE branch was created. + + **Review and merge when ready** - this prepares the $TYPE branch for completion." + + # Create PR to the release branch + gh pr create \ + --base "$RELEASE_BRANCH" \ + --head "$PR_BRANCH" \ + --title "chore: prepare $VERSION $TYPE" \ + --body "$PR_BODY" \ + --label "chore" \ + --label "auto-generated" \ + --label "$TYPE" + + - name: Create release preparation summary + run: | + VERSION="${{ steps.version_info.outputs.version }}" + TYPE="${{ steps.version_info.outputs.type }}" + BRANCH_NAME="${{ steps.version_info.outputs.branch_name }}" + CHANGELOG_STATUS="${{ steps.changelog.outputs.changelog_updated }}" + + echo "## 📋 $TYPE $VERSION Preparation Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Branch**: \`$BRANCH_NAME\`" >> $GITHUB_STEP_SUMMARY + echo "**Version**: $VERSION" >> $GITHUB_STEP_SUMMARY + echo "**Type**: $TYPE" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Completed Actions" >> $GITHUB_STEP_SUMMARY + if [[ "$CHANGELOG_STATUS" == "true" ]]; then + echo "- 📋 Generated changelog" >> $GITHUB_STEP_SUMMARY + else + echo "- ⚠️ Changelog generation skipped" >> $GITHUB_STEP_SUMMARY + fi + echo "- 📝 Updated version in package.json" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.pr_branch.outputs.has_changes }}" == "true" ]]; then + echo "- 📋 Created preparation PR to \`$BRANCH_NAME\` branch" >> $GITHUB_STEP_SUMMARY + else + echo "- ℹ️ No changes needed (already up to date)" >> $GITHUB_STEP_SUMMARY + fi + + # JOB 2: Bump develop branch version (only for releases, not hotfixes) + bump-develop-version: + if: github.event_name == 'create' && github.event.ref_type == 'branch' && startsWith(github.event.ref, 'release/') + needs: release-branch-setup + runs-on: ubuntu-latest + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout develop branch + uses: actions/checkout@v4 + with: + ref: develop + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Configure Git + run: | + git config user.name "github-actions[bot]" + git config user.email "github-actions[bot]@users.noreply.github.com" + + - name: Calculate next development version + id: next_version + run: | + RELEASE_BRANCH="${{ github.event.ref }}" + CURRENT_VERSION=$(echo $RELEASE_BRANCH | sed 's/release\///') + + # Ensure version starts with 'v' + if [[ ! $CURRENT_VERSION == v* ]]; then + CURRENT_VERSION="v$CURRENT_VERSION" + fi + + # Auto-calculate next version (default behavior - always minor bump) + MAJOR=$(echo $CURRENT_VERSION | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1/') + MINOR=$(echo $CURRENT_VERSION | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\2/') + + # Increment minor version for next development cycle + NEXT_MINOR=$((MINOR + 1)) + NEXT_VERSION="v$MAJOR.$NEXT_MINOR.0" + CLEAN_NEXT_VERSION="$MAJOR.$NEXT_MINOR.0" + + echo "current_version=$CURRENT_VERSION" >> $GITHUB_OUTPUT + echo "next_version=$NEXT_VERSION" >> $GITHUB_OUTPUT + echo "clean_next_version=$CLEAN_NEXT_VERSION" >> $GITHUB_OUTPUT + + echo "Current release: $CURRENT_VERSION" + echo "Next development version: $NEXT_VERSION" + + - name: Update develop branch with next version + run: | + CLEAN_VERSION="${{ steps.next_version.outputs.clean_next_version }}" + echo "📝 Updating develop branch to $CLEAN_VERSION..." + + if [ -f "package.json" ]; then + node -e " + const fs = require('fs'); + const pkg = require('./package.json'); + pkg.version = '$CLEAN_VERSION'; + fs.writeFileSync('package.json', JSON.stringify(pkg, null, 2) + '\\n'); + " + echo "✅ Updated package.json to version $CLEAN_VERSION" + fi + + - name: Create version bump PR + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + RELEASE_VERSION="${{ steps.next_version.outputs.current_version }}" + NEXT_VERSION="${{ steps.next_version.outputs.next_version }}" + + # Create PR branch + PR_BRANCH="chore/bump-version-to-$NEXT_VERSION-dev" + git checkout -b "$PR_BRANCH" + + # Add and commit changes + git add . + git commit -m "chore: bump version to $NEXT_VERSION for next development cycle" \ + -m "" \ + -m "Following release branch creation for $RELEASE_VERSION, updating develop" \ + -m "branch to target the next minor version $NEXT_VERSION." \ + -m "" \ + -m "Changes:" \ + -m "- Update package.json version to ${{ steps.next_version.outputs.clean_next_version }}" \ + -m "- Prepare for next development cycle" \ + -m "" \ + -m "This maintains the Git Flow pattern where develop always contains" \ + -m "the next planned version." + + # Push PR branch + git push origin "$PR_BRANCH" + + # Create PR body + PR_BODY="## Summary + Automatic version bump following release branch creation. + + ## Details + - **Release Branch Created**: \`${{ github.event.ref }}\` + - **Release Version**: $RELEASE_VERSION + - **Next Development Version**: $NEXT_VERSION + + ## Changes Made + - 📝 Updated version in package.json to ${{ steps.next_version.outputs.clean_next_version }} + - 🎯 Prepared develop branch for next development cycle + + ## Git Flow Pattern + This maintains the Git Flow pattern where: + - \`develop\` always contains the next planned version + - Release branches contain the current release version + - After release cutoff, develop moves to next minor version + + ## Auto-generated + This PR was automatically created when the release branch was created. + + **Safe to merge** - contains only version bumps." + + # Create pull request + gh pr create \ + --base develop \ + --head "$PR_BRANCH" \ + --title "chore: bump version to $NEXT_VERSION for next development cycle" \ + --body "$PR_BODY" \ + --label "chore" \ + --label "auto-generated" \ + --label "version-bump" + + - name: Create develop bump summary + run: | + RELEASE_VERSION="${{ steps.next_version.outputs.current_version }}" + NEXT_VERSION="${{ steps.next_version.outputs.next_version }}" + + echo "## 🔄 Develop Version Bump" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Release Branch**: \`${{ github.event.ref }}\`" >> $GITHUB_STEP_SUMMARY + echo "**Release Version**: $RELEASE_VERSION" >> $GITHUB_STEP_SUMMARY + echo "**Next Dev Version**: $NEXT_VERSION" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### ✅ Actions Completed" >> $GITHUB_STEP_SUMMARY + echo "- 🎯 Calculated next minor version: $NEXT_VERSION" >> $GITHUB_STEP_SUMMARY + echo "- 📝 Updated develop branch package.json" >> $GITHUB_STEP_SUMMARY + echo "- 🔄 Created PR to merge version bump" >> $GITHUB_STEP_SUMMARY diff --git a/.htaccess b/.htaccess index 9402315..965b5e0 100644 --- a/.htaccess +++ b/.htaccess @@ -1,56 +1,67 @@ # PICO SulTeng API Documentation Site Configuration -# Combines Vue SPA routing + COVID-19 API Reverse Proxy +# Combines Vue SPA routing + COVID-19 API Reverse Proxy + Swagger UI # Enable rewrite engine RewriteEngine On RewriteBase / # ============================================================================= -# API PROXY CONFIGURATION (from your current setup) +# API PROXY CONFIGURATION +# ============================================================================= + +# Proxy API requests to port 8080 (main API proxy rules) +RewriteCond %{REQUEST_URI} ^/api/v1/ +RewriteRule ^api/v1/(.*)$ http://localhost:8080/api/v1/$1 [P,L] + +# Handle root API endpoint +RewriteRule ^api/v1/?$ http://localhost:8080/api/v1/ [P,L] + +# Serve Swagger UI static files (no proxy needed) +# Static files will be served directly by web server + +# Handle docs route for Vue documentation site +RewriteRule ^docs/?$ /index.html [L] + +# Optional: API documentation redirect +RewriteRule ^api/?$ /api/v1/health [R=302,L] + +# ============================================================================= +# CORS HEADERS CONFIGURATION # ============================================================================= -# Add CORS headers for API requests # Handle preflight OPTIONS requests RewriteCond %{REQUEST_METHOD} OPTIONS RewriteRule ^api/v1/ [R=200,L] # Add CORS headers for API responses - + Header always set Access-Control-Allow-Origin "*" Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" Header always set Access-Control-Max-Age "86400" - # Add CORS headers for API routes - + # Add CORS headers for API and Swagger routes + Header always set Access-Control-Allow-Origin "*" Header always set Access-Control-Allow-Methods "GET, POST, PUT, DELETE, OPTIONS" Header always set Access-Control-Allow-Headers "Content-Type, Authorization, X-Requested-With" -# Proxy API requests to port 8080 (main API proxy rules) -RewriteCond %{REQUEST_URI} ^/api/v1/ -RewriteRule ^api/v1/(.*)$ http://localhost:8080/api/v1/$1 [P,L] - -# Handle root API endpoint -RewriteRule ^api/v1/?$ http://localhost:8080/api/v1/ [P,L] - -# Optional: API documentation redirect -RewriteRule ^api/?$ /api/v1/health [R=302,L] - # ============================================================================= # VUE SPA ROUTING CONFIGURATION (for documentation site) # ============================================================================= # Handle Vue Router - redirect all non-file requests to index.html -# BUT exclude API routes which are handled above +# BUT exclude API and Swagger routes which are handled above RewriteRule ^index\.html$ - [L] RewriteCond %{REQUEST_FILENAME} !-f RewriteCond %{REQUEST_FILENAME} !-d RewriteCond %{REQUEST_URI} !^/api/ +RewriteCond %{REQUEST_URI} !^/swagger/ +RewriteCond %{REQUEST_URI} !^/docs/?$ RewriteRule . /index.html [L] # ============================================================================= @@ -58,8 +69,8 @@ RewriteRule . /index.html [L] # ============================================================================= - # Security headers for API endpoints - + # Security headers for API and Swagger endpoints + Header always set X-Content-Type-Options nosniff Header always set X-Frame-Options DENY Header always set X-XSS-Protection "1; mode=block" @@ -67,7 +78,7 @@ RewriteRule . /index.html [L] # Security headers for documentation site - + Header always set X-Frame-Options "SAMEORIGIN" Header always set X-Content-Type-Options "nosniff" Header always set X-XSS-Protection "1; mode=block" @@ -93,6 +104,24 @@ RewriteRule . /index.html [L] ExpiresByType application/woff2 "access plus 1 year" +# Compress text files + + AddOutputFilterByType DEFLATE text/plain + AddOutputFilterByType DEFLATE text/html + AddOutputFilterByType DEFLATE text/xml + AddOutputFilterByType DEFLATE text/css + AddOutputFilterByType DEFLATE application/xml + AddOutputFilterByType DEFLATE application/xhtml+xml + AddOutputFilterByType DEFLATE application/rss+xml + AddOutputFilterByType DEFLATE application/javascript + AddOutputFilterByType DEFLATE application/x-javascript + AddOutputFilterByType DEFLATE application/json + + +# Serve static files for everything else +# (index.html, assets, images, etc.) +DirectoryIndex index.html + # ============================================================================= # ERROR HANDLING # ============================================================================= @@ -120,4 +149,4 @@ ErrorDocument 503 /api-down.html Order allow,deny Deny from all - \ No newline at end of file + diff --git a/CHANGELOG.md b/CHANGELOG.md index fd9e364..6401d2c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,31 @@ +## [v1.1.0] - 2025-09-08 + +### Added + +- Update api documentation to match latest backend implementation (79c1094) +- Enhance national historical data documentation (ae9edbb) +- Implement gitflow workflow automation following pico-api-go pattern (1178589) +- Complete api documentation overhaul with logo-matching design (04a2e51) +- Update api documentation with correct parameters and new endpoints (03eba28) + +### Changed + +- Redesign documentation sections with clean table-based layout (3b9de06) + +### Fixed + +- Workflow syntax and formatting improvements (5a0dca6) +- Remove outdated exclude_latest_case parameter from provinces endpoint (f1828d5) + +### Maintenance + +- Prepare v1.1.0 release (c269896) +- Remove unnecessary image.png file (e7807a8) + +### Style + +- Add trailing newlines to all source files (a970f94) + # Changelog for v1.0.0 Generated on 2025-09-08 @@ -8,94 +36,95 @@ All changes ## ✨ Features - implement complete i18n translations for documentation sections ( -c24cc38) + c24cc38) - redesign Documentation page with sidebar layout ( -7b7f287) + 7b7f287) - implement comprehensive national endpoint documentation ( -94f6aab) + 94f6aab) - add MIT license and API source code reference ( -95ed65b) + 95ed65b) - create reusable Navigation component with mobile support ( -49f042a) + 49f042a) - implement comprehensive responsive design and mobile navigation ( -073591f) + 073591f) - add comprehensive deployment system with health checks and rollback ( -ecafbf5) + ecafbf5) - include version in backmerge PR titles ( -a2f029c) + a2f029c) - add changelog generation script and release automation workflow ( -05d2b27) + 05d2b27) - add htaccess with api proxy and spa routing configuration ( -fe0d0e3) + fe0d0e3) - add partner logos and maintenance page ( -fd23051) + fd23051) - implement vue spa with modern hero, features, and responsive design ( -cee2d15) + cee2d15) - add html entry point with comprehensive seo meta tags ( -1fc22b6) + 1fc22b6) ## 🐛 Bug Fixes - create separate branch for release preparation to avoid conflicts (c197f16f) - handle multiline commit messages in changelog generation for first release ( -5edb93c) + 5edb93c) - update API response examples to match actual response structure ( -3d09518) + 3d09518) - update branding and contact links ( -3f1961d) + 3f1961d) - resolve CSS conflict in DataSources component ( -136a545) + 136a545) - update data source URLs to correct endpoints ( -25ee3b8) + 25ee3b8) - update backmerge PR titles to use conventional commit style ( -99f59c7) + 99f59c7) ## 📝 Documentation - update README with MIT license and API source references ( -17c22a2) + 17c22a2) ## 💎 Style - add trailing newlines to all code files ( -71dcbee) + 71dcbee) ## 📦 Build System - add vite, typescript, and tailwind configuration ( -9876bed) + 9876bed) ## 👷 CI/CD - add github action workflow for automatic backmerge to develop branch ( -8f7f494) + 8f7f494) ## 🔧 Chores - Fix LaTeX rendering and clean up duplicate i18n setup ( -cc77722) + cc77722) - Add LaTeX rendering for mathematical formulas in Rt calculation ( -495256f) + 495256f) - Fix glossary section by removing broken i18n implementation ( -031088b) + 031088b) - Fix i18n implementation in GlossarySection component ( -0aff2ef) + 0aff2ef) - Add Rt formula, references, and Vue i18n internationalization ( -98933e0) + 98933e0) - Add Indonesian COVID-19 terminology to glossary ( -2ea7fde) + 2ea7fde) - Refactor Documentation into subcomponents and add Glossary ( -5eed45c) + 5eed45c) - Fix mobile layout spacing and improve Coming Soon styling ( -0685343) + +685343. + - initial project setup with gitignore and readme ( -2960bc5) + 2960bc5) --- ## 📊 Statistics - Total commits: 33 ---- - +--- diff --git a/LICENSE b/LICENSE index ad9b30a..3e7a79f 100644 --- a/LICENSE +++ b/LICENSE @@ -18,4 +18,4 @@ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. \ No newline at end of file +SOFTWARE. diff --git a/image.png b/image.png deleted file mode 100644 index 062ce7a..0000000 Binary files a/image.png and /dev/null differ diff --git a/package.json b/package.json index e5eb540..3040cf3 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "pico-api-docs", - "version": "1.0.0", + "version": "1.1.0", "description": "PICO SulTeng API Documentation - COVID-19 Sulawesi Tengah Data API", "main": "index.js", "scripts": { diff --git a/scripts/generate_changelog.rb b/scripts/generate_changelog.rb index 47ba070..82facd0 100755 --- a/scripts/generate_changelog.rb +++ b/scripts/generate_changelog.rb @@ -1,263 +1,602 @@ #!/usr/bin/env ruby +# frozen_string_literal: true -require 'json' require 'date' require 'optparse' +## +# Automatic Changelog Generator for PICO API Documentation +# +# This script automatically generates changelog entries based on git commits +# following the Keep a Changelog format and Semantic Versioning principles. +# +# Features: +# - Categorizes commits by conventional commit types +# - Updates CHANGELOG.md with proper formatting +# - Robust error handling and validation +# - Force mode for bypassing uncommitted changes check +# +# Usage: +# ruby scripts/generate_changelog.rb --version 1.2.3 +# ruby scripts/generate_changelog.rb --version v1.2.3 --force class ChangelogGenerator - COMMIT_TYPES = { - 'feat' => '✨ Features', - 'fix' => '🐛 Bug Fixes', - 'docs' => '📝 Documentation', - 'style' => '💎 Style', - 'refactor' => '♻️ Code Refactoring', - 'perf' => '⚡ Performance Improvements', - 'test' => '✅ Tests', - 'build' => '📦 Build System', - 'ci' => '👷 CI/CD', - 'chore' => '🔧 Chores', - 'revert' => '⏪ Reverts' + # Conventional commit types and their changelog categories + COMMIT_CATEGORIES = { + 'feat' => { category: 'Added', breaking: false }, + 'fix' => { category: 'Fixed', breaking: false }, + 'hotfix' => { category: 'Hotfixes', breaking: false }, + 'docs' => { category: 'Documentation', breaking: false }, + 'style' => { category: 'Style', breaking: false }, + 'refactor' => { category: 'Changed', breaking: false }, + 'perf' => { category: 'Performance', breaking: false }, + 'test' => { category: 'Tests', breaking: false }, + 'chore' => { category: 'Maintenance', breaking: false }, + 'ci' => { category: 'CI/CD', breaking: false }, + 'build' => { category: 'Build', breaking: false }, + 'revert' => { category: 'Reverted', breaking: false }, + 'merge' => { category: 'Merged Features', breaking: false } }.freeze - BREAKING_CHANGE_HEADER = '💥 BREAKING CHANGES' + # Release and hotfix branch patterns + RELEASE_BRANCH_PATTERN = /^release\/v?(\d+)\.(\d+)\.(\d+)$/ + HOTFIX_BRANCH_PATTERN = /^hotfix\/v?(\d+)\.(\d+)\.(\d+)$/ + # Changelog file path + CHANGELOG_PATH = 'CHANGELOG.md' + + attr_reader :options, :current_branch, :version_info + + ## + # Initialize the changelog generator + # + # @param options [Hash] Configuration options def initialize(options = {}) - @from_tag = options[:from_tag] - @to_ref = options[:to_ref] || 'HEAD' - @version = options[:version] - @output_file = options[:output_file] - @include_merge_commits = options[:include_merge_commits] || false + @options = default_options.merge(options) + @current_branch = get_current_branch + @version_info = parse_version_from_options_or_branch + validate_environment! end - def generate - # Get the latest tag if not specified - @from_tag ||= get_latest_tag + ## + # Generate the changelog entry + # + # @return [Boolean] true if successful, false otherwise + def generate! + puts "🚀 Generating changelog for version #{version_string}..." - if @from_tag.nil? || @from_tag.empty? - puts "No previous tags found. Generating changelog from beginning." - @from_tag = nil + commits = fetch_commits_since_last_release + if commits.empty? + puts "⚠️ No commits found since last release. Nothing to generate." + return false end - commits = get_commits - grouped_commits = group_commits_by_type(commits) - changelog = format_changelog(grouped_commits) - - if @output_file - write_to_file(changelog) + categorized_commits = categorize_commits(commits) + version_bump = determine_version_bump(commits) + + if options[:dry_run] + preview_changelog(categorized_commits, version_bump) else - puts changelog + update_changelog(categorized_commits, version_bump) + puts "✅ Changelog updated successfully!" end - changelog + true + rescue StandardError => e + puts "❌ Error generating changelog: #{e.message}" + puts e.backtrace if options[:debug] + false end private - def get_latest_tag - `git describe --tags --abbrev=0 2>/dev/null`.strip - rescue - nil + ## + # Default configuration options + # + # @return [Hash] Default options + def default_options + { + dry_run: false, + debug: false, + force: false, + output_format: :markdown, + version: nil + } end - def get_commits - range = @from_tag ? "#{@from_tag}..#{@to_ref}" : @to_ref - merge_flag = @include_merge_commits ? '' : '--no-merges' - - # Get commit information in a parseable format using a unique delimiter - delimiter = '|||DELIMITER|||' - format = "%H#{delimiter}%s#{delimiter}%b#{delimiter}%an#{delimiter}%ae#{delimiter}%ad" - commits_raw = `git log #{merge_flag} --format="#{format}#{delimiter}END" --date=short #{range}` - - commits = [] - commits_raw.split("#{delimiter}END").each do |commit_block| - next if commit_block.strip.empty? - - parts = commit_block.split(delimiter) - next unless parts && parts.length >= 6 + ## + # Get the current git branch name + # + # @return [String] Current branch name + # @raise [RuntimeError] if not in a git repository + def get_current_branch + branch = `git branch --show-current`.strip + raise "Not in a git repository" if branch.empty? + branch + end + + ## + # Parse version information from options or branch name + # + # @return [Hash] Version components (major, minor, patch) + def parse_version_from_options_or_branch + # If version is provided via command line, use that + if options[:version] + version = options[:version].to_s + # Remove 'v' prefix if present + version = version.sub(/^v/, '') - commits << { - hash: parts[0][0..7], # Short hash - subject: parts[1] || '', - body: parts[2] || '', - author: parts[3] || '', - email: parts[4] || '', - date: parts[5] || '' - } + if version.match(/^(\d+)\.(\d+)\.(\d+)$/) + match = version.match(/^(\d+)\.(\d+)\.(\d+)$/) + return { + major: match[1].to_i, + minor: match[2].to_i, + patch: match[3].to_i + } + else + raise "Invalid version format: #{options[:version]}. Expected format: x.y.z or vx.y.z" + end end + + # Fallback to parsing from branch name + release_match = current_branch.match(RELEASE_BRANCH_PATTERN) + hotfix_match = current_branch.match(HOTFIX_BRANCH_PATTERN) + match = release_match || hotfix_match - commits + if match + { + major: match[1].to_i, + minor: match[2].to_i, + patch: match[3].to_i + } + else + # Allow any branch if version is explicitly provided + if options[:version] + raise "Invalid version format: #{options[:version]}" + else + raise "Not on a release or hotfix branch and no version specified. Expected format: release/vX.Y.Z or hotfix/vX.Y.Z, or use --version flag" + end + end end - def group_commits_by_type(commits) - grouped = { - breaking: [], - types: Hash.new { |h, k| h[k] = [] } - } + ## + # Get the current version string + # + # @return [String] Version string (e.g., "v1.2.3") + def version_string + "v#{version_info[:major]}.#{version_info[:minor]}.#{version_info[:patch]}" + end - commits.each do |commit| - # Parse conventional commit format - if commit[:subject] =~ /^(\w+)(?:\(([^)]+)\))?: (.+)$/ - type = $1 - scope = $2 - description = $3 - - commit_info = { - hash: commit[:hash], - scope: scope, - description: description, - author: commit[:author], - date: commit[:date] - } + ## + # Check if we're currently on a hotfix branch + # + # @return [Boolean] true if on hotfix branch, false if on release branch + def hotfix_branch? + current_branch.match?(HOTFIX_BRANCH_PATTERN) + end - # Check for breaking changes - if commit[:subject].include?('!:') || commit[:body].to_s.downcase.include?('breaking change') - grouped[:breaking] << commit_info - end + ## + # Validate the environment before proceeding + # + # @raise [RuntimeError] if environment is invalid + def validate_environment! + # Check if git is available + system('git --version > /dev/null 2>&1') || raise("Git is not installed or not in PATH") - # Group by type - if COMMIT_TYPES.key?(type) - grouped[:types][type] << commit_info - else - grouped[:types]['chore'] << commit_info - end - else - # Non-conventional commits go to chore - grouped[:types]['chore'] << { - hash: commit[:hash], - description: commit[:subject], - author: commit[:author], - date: commit[:date] - } - end + # Check if we're in a git repository + system('git rev-parse --git-dir > /dev/null 2>&1') || raise("Not in a git repository") + + # Create CHANGELOG.md if it doesn't exist + unless File.exist?(CHANGELOG_PATH) + puts "📝 Creating #{CHANGELOG_PATH}..." + File.write(CHANGELOG_PATH, <<~CHANGELOG) + # Changelog + + All notable changes to this project will be documented in this file. + + The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), + and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). + + ## [Unreleased] + + CHANGELOG + end + + # Warn if there are uncommitted changes + if has_uncommitted_changes? && !options[:force] + raise "There are uncommitted changes. Use --force to proceed anyway." end - grouped + puts "✅ Environment validation passed" end - def format_changelog(grouped_commits) - lines = [] + ## + # Check if there are uncommitted changes + # + # @return [Boolean] true if there are uncommitted changes + def has_uncommitted_changes? + !system('git diff --quiet && git diff --cached --quiet') + end + + ## + # Fetch commits since the last release tag + # + # @return [Array] Array of commit information + def fetch_commits_since_last_release + last_tag = get_last_release_tag + range = last_tag ? "#{last_tag}..HEAD" : "HEAD" - # Header - if @version - lines << "# Changelog for v#{@version}" - else - lines << "# Changelog" - end + puts "📋 Fetching commits since #{last_tag || 'beginning'}..." - lines << "" - lines << "Generated on #{Date.today.strftime('%Y-%m-%d')}" + commit_format = '%H|%s|%an|%ae|%ad' + commits_output = `git log #{range} --pretty=format:"#{commit_format}" --date=iso` - if @from_tag - lines << "Changes since #{@from_tag}" - else - lines << "All changes" - end + commits = commits_output.split("\n").map do |line| + parts = line.split('|', 5) + next if parts.length < 5 + + { + hash: parts[0], + subject: parts[1], + body: '', + author_name: parts[2], + author_email: parts[3], + date: parts[4] + } + end.compact - lines << "" - lines << "---" - lines << "" - - # Breaking changes - unless grouped_commits[:breaking].empty? - lines << "## #{BREAKING_CHANGE_HEADER}" - lines << "" - grouped_commits[:breaking].each do |commit| - scope_text = commit[:scope] ? "**#{commit[:scope]}:** " : "" - lines << "- #{scope_text}#{commit[:description]} (#{commit[:hash]})" - end - lines << "" + # Filter out merge commits and automated commits + commits.reject! do |commit| + commit[:subject].start_with?('Merge ') || + commit[:subject].include?('auto-generated') || + commit[:subject].include?('back-merge') end + + commits + end - # Regular commits by type - COMMIT_TYPES.each do |type, header| - commits = grouped_commits[:types][type] - next if commits.empty? + ## + # Get the last release tag + # + # @return [String, nil] Last release tag or nil if none exists + def get_last_release_tag + tags = `git tag -l --sort=-version:refname`.split("\n") + tags.find { |tag| tag.match?(/^v\d+\.\d+\.\d+$/) } + end - lines << "## #{header}" - lines << "" + ## + # Categorize commits by their conventional commit type + # + # @param commits [Array] Array of commit information + # @return [Hash] Commits grouped by category + def categorize_commits(commits) + categories = Hash.new { |h, k| h[k] = [] } + + commits.each do |commit| + type, scope, description, breaking = parse_conventional_commit(commit[:subject]) - commits.each do |commit| - scope_text = commit[:scope] ? "**#{commit[:scope]}:** " : "" - lines << "- #{scope_text}#{commit[:description]} (#{commit[:hash]})" - end - lines << "" + # Determine category + category_info = COMMIT_CATEGORIES[type] || { category: 'Other', breaking: false } + category = breaking ? 'Breaking Changes' : category_info[:category] + + # Skip certain types if configured + next if should_skip_commit?(type, commit) + + categories[category] << { + type: type, + scope: scope, + description: description, + breaking: breaking, + commit: commit + } end + + # Remove empty categories and sort + categories.reject { |_, commits| commits.empty? } + .sort_by { |category, _| category_priority(category) } + .to_h + end - # Statistics - lines << "---" - lines << "" - lines << "## 📊 Statistics" - lines << "" + ## + # Parse a conventional commit message + # + # @param subject [String] Commit subject line + # @return [Array] [type, scope, description, breaking] + def parse_conventional_commit(subject) + # Handle merge commits from pull requests + if subject.start_with?('Merge pull request') + # Extract PR info and try to parse meaningful content + pr_match = subject.match(/Merge pull request #(\d+) from .+\/(.+)/) + if pr_match + branch_name = pr_match[2] + # Try to infer type from branch name (feature/fix/etc) + if branch_name.match(/^(feature|feat)\//) + return ['feat', nil, "Merge #{branch_name}", false] + elsif branch_name.match(/^(fix|bugfix|hotfix)\//) + return ['fix', nil, "Merge #{branch_name}", false] + elsif branch_name.match(/^chore\//) + return ['chore', nil, "Merge #{branch_name}", false] + else + return ['merge', nil, "Merge #{branch_name}", false] + end + end + return ['merge', nil, subject, false] + end - total_commits = grouped_commits[:types].values.flatten.size - lines << "- Total commits: #{total_commits}" + # Match conventional commit format: type(scope): description + match = subject.match(/^(\w+)(?:\(([^)]+)\))?(!)?: (.+)$/) - if @from_tag - contributors = get_contributors - lines << "- Contributors: #{contributors.size}" - lines << "" - lines << "### Contributors" - lines << "" - contributors.each do |contributor| - lines << "- #{contributor[:name]} (#{contributor[:commits]} commits)" - end + if match + type = match[1].downcase + scope = match[2] + breaking_marker = match[3] == '!' + description = match[4] + + # Check for BREAKING CHANGE in description + breaking = breaking_marker || description.include?('BREAKING CHANGE') + + [type, scope, description, breaking] + else + # Fallback for non-conventional commits + ['other', nil, subject, false] end + end + + ## + # Determine if a commit should be skipped + # + # @param type [String] Commit type + # @param commit [Hash] Commit information + # @return [Boolean] true if commit should be skipped + def should_skip_commit?(type, commit) + # Skip certain types if configured + skip_types = options[:skip_types] || [] + skip_types.include?(type) + end - lines.join("\n") + ## + # Get priority for category ordering + # + # @param category [String] Category name + # @return [Integer] Priority (lower = higher priority) + def category_priority(category) + priorities = { + 'Breaking Changes' => 1, + 'Hotfixes' => 2, + 'Added' => 3, + 'Changed' => 4, + 'Fixed' => 5, + 'Deprecated' => 6, + 'Removed' => 7, + 'Security' => 8, + 'Performance' => 9, + 'Documentation' => 10, + 'Tests' => 11, + 'CI/CD' => 12, + 'Build' => 13, + 'Maintenance' => 14, + 'Other' => 15 + } + priorities[category] || 99 end - def get_contributors - range = @from_tag ? "#{@from_tag}..#{@to_ref}" : @to_ref - merge_flag = @include_merge_commits ? '' : '--no-merges' + ## + # Determine the version bump type based on commits + # + # @param commits [Array] Array of commits + # @return [Symbol] :major, :minor, or :patch + def determine_version_bump(commits) + has_breaking = commits.any? do |commit| + _, _, _, breaking = parse_conventional_commit(commit[:subject]) + breaking || commit[:body].include?('BREAKING CHANGE') + end - contributors_raw = `git shortlog -sn #{merge_flag} #{range}` + return :major if has_breaking - contributors_raw.split("\n").map do |line| - if line =~ /^\s*(\d+)\s+(.+)$/ - { commits: $1.to_i, name: $2.strip } - end - end.compact.sort_by { |c| -c[:commits] } + has_features = commits.any? do |commit| + type, _, _, _ = parse_conventional_commit(commit[:subject]) + type == 'feat' + end + + has_features ? :minor : :patch end - def write_to_file(content) - File.write(@output_file, content) - puts "Changelog written to #{@output_file}" + ## + # Preview the changelog without writing to file + # + # @param categorized_commits [Hash] Commits grouped by category + # @param version_bump [Symbol] Type of version bump + def preview_changelog(categorized_commits, version_bump) + puts "\n" + "="*50 + puts "CHANGELOG PREVIEW (#{version_bump.upcase} BUMP)" + puts "="*50 + puts + puts generate_changelog_content(categorized_commits) end -end -# CLI interface -if __FILE__ == $0 - options = {} - - OptionParser.new do |opts| - opts.banner = "Usage: generate_changelog.rb [options]" + ## + # Update the CHANGELOG.md file + # + # @param categorized_commits [Hash] Commits grouped by category + # @param version_bump [Symbol] Type of version bump + def update_changelog(categorized_commits, version_bump) + current_content = File.read(CHANGELOG_PATH) + new_content = generate_changelog_content(categorized_commits) - opts.on("-f", "--from TAG", "Starting tag (default: latest tag)") do |tag| - options[:from_tag] = tag - end + # Find the position to insert new content (after ## [Unreleased]) + unreleased_pattern = /^## \[Unreleased\]\s*\n/ + match = current_content.match(unreleased_pattern) - opts.on("-t", "--to REF", "Ending reference (default: HEAD)") do |ref| - options[:to_ref] = ref + unless match + # If no [Unreleased] section exists, add after the main header + header_pattern = /^# Changelog\s*\n/ + header_match = current_content.match(header_pattern) + + if header_match + insertion_point = header_match.end(0) + # Insert unreleased section and new release + updated_content = current_content[0...insertion_point] + + "\n## [Unreleased]\n\n" + + new_content + + "\n" + + current_content[insertion_point..-1] + else + # Prepend to the entire file + updated_content = new_content + "\n\n" + current_content + end + else + # Insert new release section after the unreleased section + insertion_point = match.end(0) + + updated_content = current_content[0...insertion_point] + + "\n" + + new_content + + "\n" + + current_content[insertion_point..-1] end - opts.on("-v", "--version VERSION", "Version for the changelog") do |version| - options[:version] = version + # Write back to file + File.write(CHANGELOG_PATH, updated_content) + end + + ## + # Generate changelog content for the new release + # + # @param categorized_commits [Hash] Commits grouped by category + # @return [String] Formatted changelog content + def generate_changelog_content(categorized_commits) + content = [] + content << "## [#{version_string}] - #{Date.today.strftime('%Y-%m-%d')}" + content << "" + + if categorized_commits.empty? + content << "### Changed" + content << "- Minor improvements and bug fixes" + content << "" + else + categorized_commits.each do |category, commits| + content << "### #{category}" + content << "" + + commits.each do |commit_info| + line = format_changelog_line(commit_info) + content << line if line + end + + content << "" + end end - opts.on("-o", "--output FILE", "Output file (default: stdout)") do |file| - options[:output_file] = file + content.join("\n") + end + + ## + # Format a single changelog line + # + # @param commit_info [Hash] Commit information + # @return [String, nil] Formatted changelog line + def format_changelog_line(commit_info) + description = commit_info[:description] + scope = commit_info[:scope] + + # Use original commit description or enhanced description + text = description || commit_info.dig(:commit, :subject) || 'Unknown change' + + # Clean up text - remove conventional commit prefix if it exists + text = text.gsub(/^(feat|fix|docs|style|refactor|perf|test|chore|ci|build|hotfix):\s*/i, '') + + # Get commit hash (short form) + commit_hash = commit_info.dig(:commit, :hash) + short_hash = commit_hash ? commit_hash[0..6] : nil + + # Format: "- description (scope if present) (hash)" + line = "- #{text.capitalize}" + line += " (#{scope})" if scope && !scope.empty? + line += " (#{short_hash})" if short_hash + + # Add breaking change marker + if commit_info[:breaking] + line = "- **BREAKING**: #{text.capitalize}" + line += " (#{short_hash})" if short_hash end - opts.on("-m", "--include-merges", "Include merge commits") do - options[:include_merge_commits] = true + line + end + +end + +## +# Command line interface +class CLI + def self.run(args = ARGV) + options = {} + + parser = OptionParser.new do |opts| + opts.banner = "Usage: #{$0} [options]" + opts.separator "" + opts.separator "Automatic Changelog Generator for PICO API Documentation" + opts.separator "" + opts.separator "This script generates changelog entries from git commits" + opts.separator "following conventional commit format and Keep a Changelog style." + opts.separator "" + opts.separator "Requirements:" + opts.separator "- Git repository with existing tags" + opts.separator "- CHANGELOG.md file (will be created if missing)" + opts.separator "" + + opts.on("-v", "--version VERSION", "Version to generate changelog for (e.g., 1.2.3 or v1.2.3)") do |version| + options[:version] = version + end + + opts.on("-d", "--dry-run", "Preview changes without modifying files") do + options[:dry_run] = true + end + + opts.on("-f", "--force", "Proceed even with uncommitted changes") do + options[:force] = true + end + + opts.on("--debug", "Enable debug output") do + options[:debug] = true + end + + opts.on("-h", "--help", "Show this help message") do + puts opts + exit 0 + end + + opts.separator "" + opts.separator "Examples:" + opts.separator " #{$0} --version 1.2.3 # Generate changelog for version 1.2.3" + opts.separator " #{$0} --version v1.2.3 --dry-run # Preview without changes" + opts.separator " #{$0} --version 1.2.3 --force # Ignore uncommitted changes" end - opts.on("-h", "--help", "Show this help message") do - puts opts - exit + begin + parser.parse!(args) + + unless options[:version] + puts "Error: Version is required. Use --version flag or run from a release/hotfix branch." + puts parser + exit 1 + end + + generator = ChangelogGenerator.new(options) + success = generator.generate! + + exit(success ? 0 : 1) + + rescue OptionParser::InvalidOption => e + puts "Error: #{e.message}" + puts parser + exit 1 + rescue StandardError => e + puts "Error: #{e.message}" + exit 1 end - end.parse! - - generator = ChangelogGenerator.new(options) - generator.generate -end \ No newline at end of file + end +end + +# Run the CLI if this file is executed directly +if __FILE__ == $0 + CLI.run +end diff --git a/src/components/MathFormula.vue b/src/components/MathFormula.vue index 7271c96..f0fb788 100644 --- a/src/components/MathFormula.vue +++ b/src/components/MathFormula.vue @@ -34,4 +34,4 @@ const renderedMath = computed(() => { display: block; text-align: center; } - \ No newline at end of file + diff --git a/src/components/Navigation.vue b/src/components/Navigation.vue index b4e4cd1..26d0e57 100644 --- a/src/components/Navigation.vue +++ b/src/components/Navigation.vue @@ -14,33 +14,33 @@
{{ t('nav.home') }} {{ t('nav.documentation') }} {{ t('nav.apiReference') }} - + {{ t('nav.liveApi') }}
+ + +
+

Rate Limit Response Headers

+
+
+
+
+ X-RateLimit-Limit + Request limit per window +
+
+ X-RateLimit-Remaining + Requests remaining in current window +
+
+ X-RateLimit-Reset + Unix timestamp when rate limit resets +
+
+ Retry-After + Seconds to wait before retrying (on 429 errors) +
+
+
+
+
@@ -67,4 +94,4 @@ const { t, tm } = useI18n() const rateLimitDetails = computed(() => tm('documentation.authentication.rateLimitDetails') as string[]) const practiceDetails = computed(() => tm('documentation.authentication.practiceDetails') as string[]) - \ No newline at end of file + diff --git a/src/components/documentation/ErrorHandlingSection.vue b/src/components/documentation/ErrorHandlingSection.vue index ca733c4..35eb7ca 100644 --- a/src/components/documentation/ErrorHandlingSection.vue +++ b/src/components/documentation/ErrorHandlingSection.vue @@ -119,4 +119,4 @@ interface Props { defineProps() const { t } = useI18n() - \ No newline at end of file + diff --git a/src/components/documentation/GlossarySection.vue b/src/components/documentation/GlossarySection.vue index 78a0621..9262fb1 100644 --- a/src/components/documentation/GlossarySection.vue +++ b/src/components/documentation/GlossarySection.vue @@ -340,4 +340,4 @@ defineProps() const { t, tm } = useI18n() const methodDetails = computed(() => tm('documentation.glossary.reproductionRate.methodDetails') as string[]) - \ No newline at end of file + diff --git a/src/components/documentation/HealthCheckSection.vue b/src/components/documentation/HealthCheckSection.vue new file mode 100644 index 0000000..3abfd99 --- /dev/null +++ b/src/components/documentation/HealthCheckSection.vue @@ -0,0 +1,121 @@ + + + diff --git a/src/components/documentation/NationalHistoricalSection.vue b/src/components/documentation/NationalHistoricalSection.vue index e810dea..751de97 100644 --- a/src/components/documentation/NationalHistoricalSection.vue +++ b/src/components/documentation/NationalHistoricalSection.vue @@ -9,75 +9,216 @@

{{ t('documentation.nationalHistorical.title') }}

-

{{ t('documentation.nationalHistorical.description') }}

+

{{ t('documentation.nationalHistorical.description') }}

+ + +
+
+ + + +
+

When to use this endpoint

+

{{ t('documentation.nationalHistorical.usageNote') }}

+
+
+
-
- GET +
+ GET /national
+

{{ t('documentation.nationalHistorical.description') }}

-

{{ t('documentation.nationalHistorical.queryParameters') }}

+

Parameters

- - +
+ - - - - + + + + - + - - + + - + - - + + + + + + + + + + + + + + + + + + + + + + + + + + - + - - + +
{{ t('documentation.nationalHistorical.parameter') }}{{ t('documentation.nationalHistorical.type') }}{{ t('documentation.nationalHistorical.description') }}{{ t('documentation.nationalHistorical.default') }}ParameterTypeDescriptionRequired
pagelimit integerPage number (1-based)1Records per page (default: 50, max: 1000)Optional
limitoffset integerItems per page (1-100)10Records to skip (default: 0)Optional
pageintegerPage number (1-based, alternative to offset)Optional
allbooleanReturn all data without paginationOptional
start_datestringStart date for filtering data (YYYY-MM-DD format)Optional
end_datestringEnd date for filtering data (YYYY-MM-DD format)Optional
sortsort stringSort order: "asc" or "desc"descSort by field:order (e.g., date:desc, positive:asc). Default: date:ascOptional
-
-
-

{{ t('documentation.nationalHistorical.exampleRequest') }}

-
-
GET /api/v1/national?page=1&limit=5&sort=desc
-
-
+ +
+

Response Fields

-
-

{{ t('documentation.nationalHistorical.tryIt') }}

- - - + +
+
+ + - {{ t('documentation.nationalHistorical.testInBrowser') }} - +
+

Array Response

+

Returns an array of daily COVID-19 data objects. Each object contains the following fields:

+
+
+ +
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
FieldTypeDescription
datestringDate of the COVID-19 case report (YYYY-MM-DD)
dayintegerDay number since first case reported
cumulative.positiveintegerTotal confirmed COVID-19 cases
cumulative.recoveredintegerTotal recovered cases
cumulative.deceasedintegerTotal death cases
cumulative.activeintegerCurrent active cases (positive - recovered - deceased)
daily.positiveintegerDaily new confirmed cases
daily.recoveredintegerDaily new recovered cases
daily.deceasedintegerDaily new death cases
daily.activeintegerDaily change in active cases
statistics.percentages.activefloatPercentage of active cases
statistics.percentages.recoveredfloatPercentage of recovered cases
statistics.percentages.deceasedfloatPercentage of deceased cases
statistics.reproduction_rate.valuefloatReproduction rate (Rt) estimate
statistics.reproduction_rate.upper_boundfloatUpper confidence bound for reproduction rate
statistics.reproduction_rate.lower_boundfloatLower confidence bound for reproduction rate
+
+
+ + +
@@ -93,4 +234,4 @@ interface Props { defineProps() const { t } = useI18n() - \ No newline at end of file + diff --git a/src/components/documentation/NationalLatestSection.vue b/src/components/documentation/NationalLatestSection.vue index dd44c1f..0628553 100644 --- a/src/components/documentation/NationalLatestSection.vue +++ b/src/components/documentation/NationalLatestSection.vue @@ -1,138 +1,86 @@