From 8f7f494fb783a3274bc0e57c16f3923bf875a650 Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 8 Sep 2025 07:44:01 +0700 Subject: [PATCH 1/5] ci: add github action workflow for automatic backmerge to develop branch --- .github/workflows/backmerge-to-develop.yml | 186 +++++++++++++++++++++ 1 file changed, 186 insertions(+) create mode 100644 .github/workflows/backmerge-to-develop.yml diff --git a/.github/workflows/backmerge-to-develop.yml b/.github/workflows/backmerge-to-develop.yml new file mode 100644 index 0000000..7fd2eff --- /dev/null +++ b/.github/workflows/backmerge-to-develop.yml @@ -0,0 +1,186 @@ +name: Backmerge to Develop + +on: + pull_request: + types: [closed] + branches: + - main + +jobs: + backmerge: + # Only run if PR was merged (not just closed) and came from release/* or hotfix/* branch + if: | + 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 + 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: Check if develop branch exists + id: check_develop + run: | + if git ls-remote --heads origin develop | grep -q develop; then + echo "exists=true" >> $GITHUB_OUTPUT + else + 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: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + 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" + + # Push the backmerge branch + git push origin $BACKMERGE_BRANCH + + # Create PR using GitHub CLI + PR_TITLE="๐Ÿ”„ Backmerge: main โ†’ develop" + + cat > pr_body.md << 'PREOF' + ## Automated Backmerge + + This PR automatically backmerges changes from `main` to `develop` branch. + + ### Source + - **Original PR:** #${{ github.event.pull_request.number }} + - **Title:** ${{ github.event.pull_request.title }} + - **Merged by:** @${{ github.event.pull_request.merged_by.login }} + - **Merged at:** ${{ github.event.pull_request.merged_at }} + + ### Branch Information + - **Source branch:** `${{ github.event.pull_request.head.ref }}` + - **Type:** ${{ github.event.pull_request.head.ref }} + + ### Actions Required + - ๐Ÿ‘€ **Review Required** - This PR needs approval before merging + - โœ… Ensure all CI/CD checks pass + - โœ… Review changes for any conflicts or issues + - โœ… Merge after approval + + --- + *This PR was automatically created by GitHub Actions workflow.* + PREOF + + # Create PR with review requirements + # Uncomment --draft if you want PRs to be created as draft + # Add --reviewer "username1,username2" to auto-assign reviewers + gh pr create \ + --base develop \ + --head $BACKMERGE_BRANCH \ + --title "$PR_TITLE" \ + --body-file pr_body.md \ + --label "chore,auto-generated" \ + --no-maintainer-edit + + echo "โœ… Pull request created successfully" + + else + echo "โŒ Merge conflicts detected" + + # Reset merge + git merge --abort + + # Push the branch anyway for manual resolution + git push origin $BACKMERGE_BRANCH + + # Create PR with conflict warning + PR_TITLE="๐Ÿ”„ Backmerge: main โ†’ develop (Manual Resolution Required)" + + cat > pr_conflict_body.md << 'PREOF' + ## โš ๏ธ Automated Backmerge with Conflicts + + This PR attempts to backmerge changes from `main` to `develop` branch, but **conflicts were detected**. + + ### Source + - **Original PR:** #${{ github.event.pull_request.number }} + - **Title:** ${{ github.event.pull_request.title }} + - **Merged by:** @${{ github.event.pull_request.merged_by.login }} + - **Merged at:** ${{ github.event.pull_request.merged_at }} + + ### Branch Information + - **Source branch:** `${{ github.event.pull_request.head.ref }}` + - **Type:** ${{ github.event.pull_request.head.ref }} + + ### โš ๏ธ Manual Actions Required + 1. Checkout the branch locally + 2. Merge `main` and resolve conflicts manually + 3. Push the resolved changes + 4. Request review and merge this PR + + ```bash + git fetch origin + git checkout -b ${BACKMERGE_BRANCH} origin/${BACKMERGE_BRANCH} + git merge origin/main + # Resolve conflicts in your editor + git add . + git commit + git push origin ${BACKMERGE_BRANCH} + ``` + + --- + *This PR was automatically created by GitHub Actions workflow. Manual conflict resolution is required.* + PREOF + + gh pr create \ + --base develop \ + --head $BACKMERGE_BRANCH \ + --title "$PR_TITLE" \ + --body-file pr_conflict_body.md \ + --label "chore,auto-generated,has-conflicts" \ + --no-maintainer-edit + + echo "โš ๏ธ Pull request created with conflict warning" + + # Exit with error to mark the job as failed + exit 1 + fi + + - name: Summary + if: always() + run: | + echo "## Backmerge Summary" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "- **Source PR:** #${{ github.event.pull_request.number }}" >> $GITHUB_STEP_SUMMARY + echo "- **Source Branch:** \`${{ github.event.pull_request.head.ref }}\`" >> $GITHUB_STEP_SUMMARY + 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 From 05d2b27bd1ce6a57fb9446f75bdc3e6ed3ec5579 Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 8 Sep 2025 07:51:13 +0700 Subject: [PATCH 2/5] feat: add changelog generation script and release automation workflow - Add Ruby script to generate conventional commit changelogs - Add GitHub Actions workflow for release/hotfix automation - Workflow triggers on release/v* or hotfix/v* branch creation - Automatically bumps package.json version and generates changelog - Creates PR with conventional commit title for review --- .github/workflows/release-automation.yml | 217 +++++++++++++++++++ scripts/generate_changelog.rb | 255 +++++++++++++++++++++++ 2 files changed, 472 insertions(+) create mode 100644 .github/workflows/release-automation.yml create mode 100755 scripts/generate_changelog.rb diff --git a/.github/workflows/release-automation.yml b/.github/workflows/release-automation.yml new file mode 100644 index 0000000..a0b0b91 --- /dev/null +++ b/.github/workflows/release-automation.yml @@ -0,0 +1,217 @@ +name: Release Automation + +on: + create: + +jobs: + prepare-release: + if: | + github.event.ref_type == 'branch' && + (startsWith(github.event.ref, 'release/v') || startsWith(github.event.ref, 'hotfix/v')) + + runs-on: ubuntu-latest + + permissions: + contents: write + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + 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' + + - 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 + 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 + 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 + 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" + else + echo "โš ๏ธ package.json not found, skipping version update" + fi + + - name: Generate changelog + id: changelog + 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 + 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" + else + echo "โŒ Failed to generate changelog" + exit 1 + 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 + run: | + VERSION="${{ steps.extract_version.outputs.version }}" + BRANCH_TYPE="${{ steps.extract_version.outputs.branch_type }}" + + # 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 + 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 + + # Push changes + git push origin "${{ github.event.ref }}" + + echo "has_changes=true" >> $GITHUB_OUTPUT + echo "โœ… Changes committed and pushed" + fi + + - name: Create Pull Request + if: steps.commit.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + VERSION="${{ steps.extract_version.outputs.version }}" + BRANCH_TYPE="${{ steps.extract_version.outputs.branch_type }}" + BRANCH_NAME="${{ github.event.ref }}" + + # Determine target branch + if [ "$BRANCH_TYPE" = "release" ]; then + TARGET_BRANCH="main" + PR_TITLE="chore(release): release v$VERSION" + LABELS="release,auto-generated" + else + TARGET_BRANCH="main" + PR_TITLE="fix(hotfix): hotfix v$VERSION" + LABELS="hotfix,auto-generated,priority:high" + fi + + # Create PR body + cat > pr_body.md << 'PREOF' + ## ๐Ÿš€ ${{ steps.extract_version.outputs.branch_type == 'release' && 'Release' || 'Hotfix' }} v${{ steps.extract_version.outputs.version }} + + This PR contains 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 }} + + ### โœ”๏ธ Checklist + - [ ] All tests passing + - [ ] Documentation updated (if needed) + - [ ] Breaking changes documented (if any) + - [ ] Ready for production + + ### ๐Ÿ”„ Post-Merge Actions + After merging this PR: + 1. A tag `v${{ steps.extract_version.outputs.version }}` will be created automatically + 2. A GitHub release will be created with the changelog + 3. 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 + gh pr create \ + --base "$TARGET_BRANCH" \ + --head "$BRANCH_NAME" \ + --title "$PR_TITLE" \ + --body-file pr_body.md \ + --label "$LABELS" + + echo "โœ… Pull request created successfully" + + - name: Summary + if: always() + run: | + echo "## Release Preparation Summary" >> $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 "" >> $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 + else + echo "โš ๏ธ **Status:** No changes needed" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file diff --git a/scripts/generate_changelog.rb b/scripts/generate_changelog.rb new file mode 100755 index 0000000..696954a --- /dev/null +++ b/scripts/generate_changelog.rb @@ -0,0 +1,255 @@ +#!/usr/bin/env ruby + +require 'json' +require 'date' +require 'optparse' + +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' + }.freeze + + BREAKING_CHANGE_HEADER = '๐Ÿ’ฅ BREAKING CHANGES' + + 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 + end + + def generate + # Get the latest tag if not specified + @from_tag ||= get_latest_tag + + if @from_tag.nil? || @from_tag.empty? + puts "No previous tags found. Generating changelog from beginning." + @from_tag = nil + end + + commits = get_commits + grouped_commits = group_commits_by_type(commits) + changelog = format_changelog(grouped_commits) + + if @output_file + write_to_file(changelog) + else + puts changelog + end + + changelog + end + + private + + def get_latest_tag + `git describe --tags --abbrev=0 2>/dev/null`.strip + rescue + 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 + format = '%H|%s|%b|%an|%ae|%ad' + commits_raw = `git log #{merge_flag} --format="#{format}" --date=short #{range}` + + commits_raw.split("\n").map do |line| + parts = line.split('|') + { + hash: parts[0][0..7], # Short hash + subject: parts[1], + body: parts[2], + author: parts[3], + email: parts[4], + date: parts[5] + } + end + end + + def group_commits_by_type(commits) + grouped = { + breaking: [], + types: Hash.new { |h, k| h[k] = [] } + } + + 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 for breaking changes + if commit[:subject].include?('!:') || commit[:body].to_s.downcase.include?('breaking change') + grouped[:breaking] << commit_info + end + + # 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 + end + + grouped + end + + def format_changelog(grouped_commits) + lines = [] + + # Header + if @version + lines << "# Changelog for v#{@version}" + else + lines << "# Changelog" + end + + lines << "" + lines << "Generated on #{Date.today.strftime('%Y-%m-%d')}" + + if @from_tag + lines << "Changes since #{@from_tag}" + else + lines << "All changes" + end + + 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 << "" + end + + # Regular commits by type + COMMIT_TYPES.each do |type, header| + commits = grouped_commits[:types][type] + next if commits.empty? + + lines << "## #{header}" + lines << "" + + commits.each do |commit| + scope_text = commit[:scope] ? "**#{commit[:scope]}:** " : "" + lines << "- #{scope_text}#{commit[:description]} (#{commit[:hash]})" + end + lines << "" + end + + # Statistics + lines << "---" + lines << "" + lines << "## ๐Ÿ“Š Statistics" + lines << "" + + total_commits = grouped_commits[:types].values.flatten.size + lines << "- Total commits: #{total_commits}" + + 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 + end + + lines.join("\n") + end + + def get_contributors + range = @from_tag ? "#{@from_tag}..#{@to_ref}" : @to_ref + merge_flag = @include_merge_commits ? '' : '--no-merges' + + contributors_raw = `git shortlog -sn #{merge_flag} #{range}` + + 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] } + end + + def write_to_file(content) + File.write(@output_file, content) + puts "Changelog written to #{@output_file}" + end +end + +# CLI interface +if __FILE__ == $0 + options = {} + + OptionParser.new do |opts| + opts.banner = "Usage: generate_changelog.rb [options]" + + opts.on("-f", "--from TAG", "Starting tag (default: latest tag)") do |tag| + options[:from_tag] = tag + end + + opts.on("-t", "--to REF", "Ending reference (default: HEAD)") do |ref| + options[:to_ref] = ref + end + + opts.on("-v", "--version VERSION", "Version for the changelog") do |version| + options[:version] = version + end + + opts.on("-o", "--output FILE", "Output file (default: stdout)") do |file| + options[:output_file] = file + end + + opts.on("-m", "--include-merges", "Include merge commits") do + options[:include_merge_commits] = true + end + + opts.on("-h", "--help", "Show this help message") do + puts opts + exit + end + end.parse! + + generator = ChangelogGenerator.new(options) + generator.generate +end \ No newline at end of file From 99f59c76272a0c2d53f71192f051f48d7263d222 Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 8 Sep 2025 07:53:11 +0700 Subject: [PATCH 3/5] fix: update backmerge PR titles to use conventional commit style --- .github/workflows/backmerge-to-develop.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backmerge-to-develop.yml b/.github/workflows/backmerge-to-develop.yml index 7fd2eff..8931a4c 100644 --- a/.github/workflows/backmerge-to-develop.yml +++ b/.github/workflows/backmerge-to-develop.yml @@ -67,7 +67,7 @@ jobs: git push origin $BACKMERGE_BRANCH # Create PR using GitHub CLI - PR_TITLE="๐Ÿ”„ Backmerge: main โ†’ develop" + PR_TITLE="chore: backmerge main to develop" cat > pr_body.md << 'PREOF' ## Automated Backmerge @@ -117,7 +117,7 @@ jobs: git push origin $BACKMERGE_BRANCH # Create PR with conflict warning - PR_TITLE="๐Ÿ”„ Backmerge: main โ†’ develop (Manual Resolution Required)" + PR_TITLE="chore: backmerge main to develop (conflicts)" cat > pr_conflict_body.md << 'PREOF' ## โš ๏ธ Automated Backmerge with Conflicts From a2f029cf57ddae6b95466a04b42b799961e82884 Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 8 Sep 2025 07:57:42 +0700 Subject: [PATCH 4/5] feat: include version in backmerge PR titles - Extract version from release/hotfix branch names - Update PR titles to include version (e.g. 'chore: backmerge v1.2.3 to develop') - Handle both conflict and non-conflict scenarios with versioned titles --- .github/workflows/backmerge-to-develop.yml | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/.github/workflows/backmerge-to-develop.yml b/.github/workflows/backmerge-to-develop.yml index 8931a4c..8906ac1 100644 --- a/.github/workflows/backmerge-to-develop.yml +++ b/.github/workflows/backmerge-to-develop.yml @@ -32,6 +32,23 @@ jobs: 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" + elif [[ $BRANCH_NAME == hotfix/v* ]]; then + 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: | @@ -67,7 +84,7 @@ jobs: git push origin $BACKMERGE_BRANCH # Create PR using GitHub CLI - PR_TITLE="chore: backmerge main to develop" + PR_TITLE="chore: backmerge v${{ steps.version.outputs.version }} to develop" cat > pr_body.md << 'PREOF' ## Automated Backmerge @@ -117,7 +134,7 @@ jobs: git push origin $BACKMERGE_BRANCH # Create PR with conflict warning - PR_TITLE="chore: backmerge main to develop (conflicts)" + PR_TITLE="chore: backmerge v${{ steps.version.outputs.version }} to develop (conflicts)" cat > pr_conflict_body.md << 'PREOF' ## โš ๏ธ Automated Backmerge with Conflicts From ecafbf5cce1ad2c537585d95ee6cc9a3f68d88c6 Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 8 Sep 2025 08:07:09 +0700 Subject: [PATCH 5/5] feat: add comprehensive deployment system with health checks and rollback - Create deployment script for static site preparation - Add GitHub workflow for automated deployment on tag push - Implement robust health check system with multiple retry attempts - Add automatic rollback to previous version if health checks fail - Clean up backup files only after successful health verification - Support for SSH deployment to Hostinger shared hosting --- .github/workflows/deploy.yml | 414 +++++++++++++++++++++++++++++++++++ deploy.sh | 139 ++++++++++++ 2 files changed, 553 insertions(+) create mode 100644 .github/workflows/deploy.yml create mode 100755 deploy.sh diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml new file mode 100644 index 0000000..0487caa --- /dev/null +++ b/.github/workflows/deploy.yml @@ -0,0 +1,414 @@ +name: Deploy Documentation to Production + +on: + push: + tags: + - '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: Set up Node.js + uses: actions/setup-node@v4 + with: + node-version: '18' + cache: 'npm' + + - name: Install pnpm + uses: pnpm/action-setup@v2 + with: + version: 8 + + - 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 --frozen-lockfile + + - 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..." + + # Check if site responds with 200 + if curl -f -s -I "${{ secrets.DOCS_URL }}" | head -n1 | grep -q "200 OK"; 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 + + 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!" + + # 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 + + # Clean up backup path file + rm -f /tmp/backup_path.txt + + echo "โœ… Deployment ${{ env.VERSION }} completed successfully!" + + else + 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 + 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 + + create-release: + runs-on: ubuntu-latest + needs: build-and-deploy + 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 + + 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" + else + echo "โš ๏ธ GitHub release creation failed, but deployment succeeded" + fi + else + echo "โŒ Documentation deployment failed for ${{ github.ref_name }}" + exit 1 + fi \ No newline at end of file diff --git a/deploy.sh b/deploy.sh new file mode 100755 index 0000000..dae0082 --- /dev/null +++ b/deploy.sh @@ -0,0 +1,139 @@ +#!/bin/bash + +# Deployment script for PICO SulTeng COVID-19 API Documentation +# Prepares files for deployment to Hostinger shared hosting + +echo "๐Ÿš€ Preparing PICO API Documentation for deployment..." + +# Build the Vue.js application +echo "๐Ÿ“ฆ Building Vue.js application..." +pnpm run build + +if [ ! -d "dist" ]; then + echo "โŒ Build failed - dist directory not found" + exit 1 +fi + +# Create deployment directory +echo "๐Ÿ“ Creating deployment directory..." +rm -rf deploy +mkdir -p deploy + +# Copy built files +echo "๐Ÿ“‹ Copying built files..." +cp -r dist/* deploy/ + +# Copy essential server files +echo "๐Ÿ”ง Copying server configuration files..." +cp .htaccess deploy/ +cp public/api-down.html deploy/ +cp public/favicon.ico deploy/ 2>/dev/null || echo "โš ๏ธ favicon.ico not found, skipping" + +# Copy logos and assets that might not be in dist +echo "๐Ÿ–ผ๏ธ Ensuring all assets are included..." +mkdir -p deploy/assets +cp -r public/sulteng-*.webp deploy/assets/ 2>/dev/null || echo "โš ๏ธ Logo files not found in public, checking dist" + +# Create deployment info file +cat > deploy/DEPLOY_INFO.txt << EOF +PICO SulTeng COVID-19 API Documentation +Deployment prepared on: $(date) +Built with: Vue.js + Vite +Target: Hostinger Shared Hosting + +Deployment Structure: +- All files in this directory should be uploaded to public_html/ +- .htaccess handles Vue Router routing and API proxy +- api-down.html is the maintenance page for API downtime + +Post-deployment steps: +1. Ensure .htaccess is in the document root +2. Verify Vue router is working by testing routes +3. Test API proxy functionality +4. Check that all assets load correctly +EOF + +# Note: No server startup needed - this is a static website served by Apache + +# Create health check for the static site +cat > deploy/health-check.sh << 'EOF' +#!/bin/bash +# Health check for the documentation site + +echo "๐Ÿ” Checking documentation site health..." + +# Check if main files exist +if [ -f "index.html" ]; then + echo "โœ… index.html found" +else + echo "โŒ index.html missing" + exit 1 +fi + +if [ -f ".htaccess" ]; then + echo "โœ… .htaccess found" +else + echo "โš ๏ธ .htaccess missing - Vue routing may not work" +fi + +if [ -f "api-down.html" ]; then + echo "โœ… api-down.html found" +else + echo "โš ๏ธ api-down.html missing - API maintenance page unavailable" +fi + +echo "๐Ÿ“Š Site files:" +ls -la *.html *.js *.css 2>/dev/null | head -10 + +echo "โœ… Documentation site health check complete" +EOF + +chmod +x deploy/health-check.sh + +# Verify deployment package +echo "๐Ÿ” Verifying deployment package..." +cd deploy + +# Check essential files +ESSENTIAL_FILES=("index.html" ".htaccess" "api-down.html") +MISSING_FILES=() + +for file in "${ESSENTIAL_FILES[@]}"; do + if [ ! -f "$file" ]; then + MISSING_FILES+=("$file") + fi +done + +if [ ${#MISSING_FILES[@]} -eq 0 ]; then + echo "โœ… All essential files present" +else + echo "โš ๏ธ Missing files: ${MISSING_FILES[*]}" +fi + +cd .. + +# Display deployment summary +echo "" +echo "โœ… Deployment package ready in ./deploy/ directory" +echo "" +echo "๐Ÿ“‹ Deployment Summary:" +echo "- Vue.js app built successfully" +echo "- Static files prepared for upload" +echo "- Apache .htaccess configured for SPA routing" +echo "- API maintenance page included" +echo "- Total files: $(find deploy -type f | wc -l)" +echo "- Package size: $(du -sh deploy | cut -f1)" +echo "" +echo "๐Ÿ“ค Next steps:" +echo "1. Upload all files in ./deploy/ to your Hostinger public_html/" +echo "2. Ensure .htaccess is in the document root" +echo "3. Test the site at your domain" +echo "4. Verify API proxy is working (if backend is running)" +echo "" +echo "๐ŸŒ Expected URLs:" +echo "- Documentation: https://your-domain.com/" +echo "- API Proxy: https://your-domain.com/api/v1/" +echo "- Maintenance: https://your-domain.com/api-down.html" +echo "" +echo "๐Ÿ“ Files ready for upload:" +ls -la deploy/ | head -20 \ No newline at end of file