From fc609bb75f535ca6b8962886146eda655c5d894a Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 15 Sep 2025 13:29:57 +0700 Subject: [PATCH 1/5] feat: simplify changelog generator and remove unnecessary complexity - Simplified generate-changelog.rb to require only --version parameter - Auto-detect repository URL for commit links - Removed complex configuration files (YAML configs) - Updated workflows to call script directly like pico-api-docs - Removed unnecessary DevOps tooling (Docker, GitHub Actions, etc.) - Made the script self-contained and easily reusable The changelog generator now works like pico-api-docs version: Just run: ruby generate-changelog.rb --version X.Y.Z --- .github/workflows/ci.yml | 59 +- .github/workflows/deploy.yml | 62 +- .github/workflows/release-branch-creation.yml | 31 +- .gitignore | 8 + generate-changelog.rb | 567 +++++++----------- 5 files changed, 322 insertions(+), 405 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 327289a..dbc742b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,32 +6,26 @@ on: pull_request: branches: [ main, develop ] +env: + GO_VERSION: '1.25.x' + jobs: test: runs-on: ubuntu-latest - strategy: - matrix: - go-version: [1.23.x] - steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: - go-version: ${{ matrix.go-version }} + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: go.sum - - name: Cache Go modules - uses: actions/cache@v4 - with: - path: | - ~/.cache/go-build - ~/go/pkg/mod - key: ${{ runner.os }}-go-${{ matrix.go-version }}-${{ hashFiles('**/go.sum') }} - restore-keys: | - ${{ runner.os }}-go-${{ matrix.go-version }}- - ${{ runner.os }}-go- + - name: Verify dependencies + run: go mod verify - name: Install dependencies run: go mod download @@ -129,22 +123,43 @@ jobs: body: coverage }); } - - - name: Build - run: go build -v ./cmd/main.go lint: runs-on: ubuntu-latest steps: - - uses: actions/checkout@v4 + - name: Checkout code + uses: actions/checkout@v4 - name: Set up Go uses: actions/setup-go@v4 with: - go-version: 1.23.x + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: go.sum - name: golangci-lint uses: golangci/golangci-lint-action@v6 with: version: latest - args: --out-format=colored-line-number \ No newline at end of file + args: --out-format=colored-line-number --timeout=5m + + build: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up Go + uses: actions/setup-go@v4 + with: + go-version: ${{ env.GO_VERSION }} + cache: true + cache-dependency-path: go.sum + + - name: Build application + run: go build -v -ldflags="-w -s" -o pico-api-go ./cmd/main.go + + - name: Verify binary + run: | + file pico-api-go + echo "Binary size: $(du -h pico-api-go | cut -f1)" \ No newline at end of file diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 1653577..0d1bd28 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -71,57 +71,66 @@ jobs: echo "โš ๏ธ HTML documentation not found, continuing without it" fi - - name: Setup SSH Agent - uses: webfactory/ssh-agent@v0.8.0 - with: - ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }} - log-public-key: false - - - name: Debug SSH configuration - run: | - echo "SSH Agent PID: $SSH_AGENT_PID" - echo "SSH Auth Sock: $SSH_AUTH_SOCK" - ssh-add -l || echo "No keys loaded in agent" - - - name: Add server to known hosts + - name: Setup SSH for deployment + id: ssh_setup run: | + echo "๐Ÿ” Setting up SSH connection..." + + # Create SSH directory mkdir -p ~/.ssh + chmod 700 ~/.ssh + + # Write SSH key + echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key + chmod 600 ~/.ssh/deploy_key + + # Add to known hosts echo "Adding ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PORT }} to known hosts..." ssh-keyscan -H -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts - echo "Known hosts file created" - - - 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"' + + # Test connection + echo "Testing SSH connection..." + if ssh -i ~/.ssh/deploy_key -p ${{ secrets.DEPLOY_PORT }} -o ConnectTimeout=10 -o BatchMode=yes ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} 'echo "SSH connection successful"'; then + echo "โœ… SSH connection verified" + echo "ssh_ready=true" >> $GITHUB_OUTPUT + else + echo "โŒ SSH connection failed" + echo "ssh_ready=false" >> $GITHUB_OUTPUT + exit 1 + fi - name: Deploy to production server + if: steps.ssh_setup.outputs.ssh_ready == 'true' run: | echo "๐Ÿš€ Starting deployment of ${{ env.VERSION }} to production..." + # Use consistent SSH options + SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DEPLOY_PORT }} -o StrictHostKeyChecking=yes -o ConnectTimeout=30" + SSH_TARGET="${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" + # Upload the binary to a temporary location first echo "๐Ÿ“ค Uploading binary..." - scp -P ${{ secrets.DEPLOY_PORT }} ${{ secrets.BINARY_NAME }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/${{ secrets.BINARY_NAME }}-${{ env.VERSION }} + scp $SSH_OPTS ${{ secrets.BINARY_NAME }} $SSH_TARGET:/tmp/${{ secrets.BINARY_NAME }}-${{ env.VERSION }} # Upload documentation files echo "๐Ÿ“š Uploading documentation..." if [ -f "docs/swagger.html" ]; then - scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.html ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.html + scp $SSH_OPTS docs/swagger.html $SSH_TARGET:/tmp/swagger-${{ env.VERSION }}.html echo "โœ… HTML documentation uploaded" fi if [ -f "docs/swagger.json" ]; then - scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.json ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.json + scp $SSH_OPTS docs/swagger.json $SSH_TARGET:/tmp/swagger-${{ env.VERSION }}.json echo "โœ… JSON documentation uploaded" fi if [ -f "docs/swagger.yaml" ]; then - scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.yaml ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.yaml + scp $SSH_OPTS docs/swagger.yaml $SSH_TARGET:/tmp/swagger-${{ env.VERSION }}.yaml echo "โœ… YAML documentation uploaded" fi # Execute deployment script on remote server - ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' + ssh $SSH_OPTS $SSH_TARGET << 'EOF' set -e DEPLOY_PATH="${{ secrets.DEPLOY_PATH }}" @@ -209,7 +218,10 @@ jobs: run: | echo "๐Ÿงน Cleaning up backup files after successful deployment..." - ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' + SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DEPLOY_PORT }} -o StrictHostKeyChecking=yes -o ConnectTimeout=30" + SSH_TARGET="${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" + + ssh $SSH_OPTS $SSH_TARGET << 'EOF' DEPLOY_PATH="${{ secrets.DEPLOY_PATH }}" BINARY_NAME="${{ secrets.BINARY_NAME }}" diff --git a/.github/workflows/release-branch-creation.yml b/.github/workflows/release-branch-creation.yml index f8f5eb4..ca1a01a 100644 --- a/.github/workflows/release-branch-creation.yml +++ b/.github/workflows/release-branch-creation.yml @@ -212,26 +212,13 @@ jobs: id: changelog run: | VERSION="${{ steps.version_info.outputs.version }}" - TYPE="${{ steps.version_info.outputs.type }}" - BUMP_TYPE="${{ steps.version_type.outputs.bump_type }}" + echo "๐Ÿš€ Generating changelog for $VERSION..." - echo "๐Ÿš€ Generating changelog for $VERSION using generate-changelog.rb..." - echo "Current branch: $(git branch --show-current)" - echo "Working directory: $(pwd)" - # Make the script executable chmod +x generate-changelog.rb - # Debug: Check if we're on the right branch format - CURRENT_BRANCH=$(git branch --show-current) - if [[ ! "$CURRENT_BRANCH" =~ ^(release|hotfix)/v[0-9]+\.[0-9]+\.[0-9]+$ ]]; then - echo "โš ๏ธ Branch format issue: '$CURRENT_BRANCH' doesn't match required pattern" - echo "Expected: release/v1.2.3 or hotfix/v1.2.3" - fi - - # Run changelog generation with debug output - echo "Running: ruby generate-changelog.rb --force" - if ruby generate-changelog.rb --force 2>&1; then + # Run changelog generation (simple like pico-api-docs) + if ruby generate-changelog.rb --version "$VERSION" --force 2>&1; then echo "โœ… Changelog generation completed" # Check if CHANGELOG.md was actually updated @@ -243,17 +230,13 @@ jobs: git diff --stat CHANGELOG.md CHANGELOG_STATUS="true" fi - + echo "changelog_updated=$CHANGELOG_STATUS" >> $GITHUB_OUTPUT else - RUBY_EXIT_CODE=$? - echo "โŒ Changelog generation failed with exit code: $RUBY_EXIT_CODE" - echo "This might be due to:" - echo "- Branch naming format (needs release/vX.Y.Z or hotfix/vX.Y.Z)" - echo "- No commits since last tag" - echo "- Missing dependencies or Ruby issues" - + echo "โŒ Changelog generation failed" echo "changelog_updated=false" >> $GITHUB_OUTPUT + + echo "changelog_updated=$CHANGELOG_STATUS" >> $GITHUB_OUTPUT fi # No separate release notes file needed - CHANGELOG.md is the source of truth diff --git a/.gitignore b/.gitignore index ff4eb04..fb03b99 100644 --- a/.gitignore +++ b/.gitignore @@ -50,6 +50,11 @@ pico-api-go # MD TABLE_STRUCTURE.md +# Coverage and testing files +coverage.out +coverage.html +coverage_report.md + # Deployment files (keep locally but not in git) deploy.sh deploy/ @@ -64,3 +69,6 @@ pico-api-go-linux .env.production .env.alternative port-80-fix.mdDEPLOYMENT.md + +# Reusable tools directory +/tools/ diff --git a/generate-changelog.rb b/generate-changelog.rb index 6cb2c9b..ddc93b0 100755 --- a/generate-changelog.rb +++ b/generate-changelog.rb @@ -5,34 +5,21 @@ require 'optparse' ## -# Automatic Changelog Generator (Ruby Version) +# Automatic Changelog Generator for PICO API Go # # This script automatically generates changelog entries based on git commits # following the Keep a Changelog format and Semantic Versioning principles. # -# This Ruby implementation offers several advantages over the bash version: -# - Better error handling and validation with structured exception handling -# - More robust parsing of conventional commit messages -# - Cleaner code organization with object-oriented design -# - More reliable text processing without shell escaping issues -# - Better support for complex commit message parsing -# - More maintainable and testable code structure -# - Cross-platform compatibility (Ruby vs. bash-specific features) -# # Features: -# - Only runs from release or hotfix branches (release/vx.x.x or hotfix/vx.x.x) # - Categorizes commits by conventional commit types -# - Determines semantic version increment automatically # - Updates CHANGELOG.md with proper formatting +# - Includes commit links to GitHub # - Robust error handling and validation -# - Dry-run mode for previewing changes # - Force mode for bypassing uncommitted changes check # # Usage: -# ruby generate-changelog.rb [options] -# -# Author: Auto-generated for PICO API Go project -# License: Same as project license +# ruby generate-changelog.rb --version 1.2.3 +# ruby generate-changelog.rb --version v1.2.3 --force class ChangelogGenerator # Conventional commit types and their changelog categories COMMIT_CATEGORIES = { @@ -47,17 +34,18 @@ class ChangelogGenerator 'chore' => { category: 'Maintenance', breaking: false }, 'ci' => { category: 'CI/CD', breaking: false }, 'build' => { category: 'Build', breaking: false }, - 'revert' => { category: 'Reverted', breaking: false } + 'revert' => { category: 'Reverted', breaking: false }, + 'merge' => { category: 'Merged Features', breaking: false } }.freeze # Release and hotfix branch patterns - RELEASE_BRANCH_PATTERN = /^release\/v(\d+)\.(\d+)\.(\d+)$/ - HOTFIX_BRANCH_PATTERN = /^hotfix\/v(\d+)\.(\d+)\.(\d+)$/ + 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 + attr_reader :options, :current_branch, :version_info, :repository_url ## # Initialize the changelog generator @@ -66,7 +54,8 @@ class ChangelogGenerator def initialize(options = {}) @options = default_options.merge(options) @current_branch = get_current_branch - @version_info = parse_version_from_branch + @version_info = parse_version_from_options_or_branch + @repository_url = detect_repository_url validate_environment! end @@ -76,7 +65,7 @@ def initialize(options = {}) # @return [Boolean] true if successful, false otherwise def generate! puts "๐Ÿš€ Generating changelog for version #{version_string}..." - + commits = fetch_commits_since_last_release if commits.empty? puts "โš ๏ธ No commits found since last release. Nothing to generate." @@ -85,7 +74,7 @@ def generate! categorized_commits = categorize_commits(commits) version_bump = determine_version_bump(commits) - + if options[:dry_run] preview_changelog(categorized_commits, version_bump) else @@ -111,7 +100,9 @@ def default_options dry_run: false, debug: false, force: false, - output_format: :markdown + output_format: :markdown, + version: nil, + include_commit_links: true } end @@ -127,22 +118,80 @@ def get_current_branch end ## - # Parse version information from the current branch name + # Detect repository URL from git remote + # + # @return [String, nil] Repository URL or nil if not found + def detect_repository_url + # Try to get origin URL + url = `git remote get-url origin 2>/dev/null`.strip + + # If origin doesn't exist, try first available remote + if url.empty? + remotes = `git remote 2>/dev/null`.strip.split("\n") + url = `git remote get-url #{remotes.first} 2>/dev/null`.strip unless remotes.empty? + end + + return nil if url.empty? + + # Convert SSH URL to HTTPS URL + if url.start_with?('git@') + # git@github.com:user/repo.git -> https://github.com/user/repo + url = url.sub('git@', 'https://') + .sub(':', '/') + .sub(/\.git$/, '') + elsif url.start_with?('https://') + # Remove .git suffix if present + url = url.sub(/\.git$/, '') + end + + puts "๐Ÿ“‹ Repository URL: #{url}" if options[:debug] + url + rescue + nil + end + + ## + # Parse version information from options or branch name # # @return [Hash] Version components (major, minor, patch) - # @raise [RuntimeError] if not on a valid release or hotfix branch - def parse_version_from_branch + 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/, '') + + 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 - - raise "Not on a release or hotfix branch. Expected format: release/vX.Y.Z or hotfix/vX.Y.Z" unless match - { - major: match[1].to_i, - minor: match[2].to_i, - patch: match[3].to_i - } + 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 ## @@ -172,8 +221,21 @@ def validate_environment! # Check if we're in a git repository system('git rev-parse --git-dir > /dev/null 2>&1') || raise("Not in a git repository") - # Check if CHANGELOG.md exists - File.exist?(CHANGELOG_PATH) || raise("#{CHANGELOG_PATH} not found") + # 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] @@ -198,12 +260,12 @@ def has_uncommitted_changes? def fetch_commits_since_last_release last_tag = get_last_release_tag range = last_tag ? "#{last_tag}..HEAD" : "HEAD" - + puts "๐Ÿ“‹ Fetching commits since #{last_tag || 'beginning'}..." - + commit_format = '%H|%s|%an|%ae|%ad' commits_output = `git log #{range} --pretty=format:"#{commit_format}" --date=iso` - + commits = commits_output.split("\n").map do |line| parts = line.split('|', 5) next if parts.length < 5 @@ -217,216 +279,15 @@ def fetch_commits_since_last_release date: parts[4] } end.compact - - # Enhance squash commits with PR information - enhance_squash_commits(commits) - end - - ## - # Enhance squash commits with PR information - # - # @param commits [Array] Array of commit information - # @return [Array] Enhanced commits with PR details - def enhance_squash_commits(commits) - puts "๐Ÿ” Analyzing commits for squash-merge patterns..." - - commits.map do |commit| - # Check if this looks like a squash commit (usually contains PR number) - if looks_like_squash_commit?(commit) - enhanced_commit = parse_squash_commit(commit) - enhanced_commit || commit - else - commit - end - end - end - - ## - # Check if a commit looks like a squash commit - # - # @param commit [Hash] Commit information - # @return [Boolean] true if likely a squash commit - def looks_like_squash_commit?(commit) - subject = commit[:subject] - body = commit[:body] - - # Common patterns for squash commits: - # - Contains PR number: "Feature: description (#123)" - # - Body contains "* " bullet points (squashed commit list) - # - Subject is very generic but body has details - subject.match?(/\(#\d+\)$/) || - body.include?('* ') || - (subject.split(' ').length < 4 && body.length > 100) - end - - ## - # Parse squash commit to extract meaningful information - # - # @param commit [Hash] Original commit information - # @return [Hash, nil] Enhanced commit or nil if parsing failed - def parse_squash_commit(commit) - subject = commit[:subject] - body = commit[:body] - - # Extract PR number if present - pr_match = subject.match(/\(#(\d+)\)$/) - pr_number = pr_match ? pr_match[1] : nil - - # Try to get PR information from GitHub CLI if available - pr_info = pr_number ? get_pr_info(pr_number) : nil - - # Parse the body for individual changes - changes = parse_commit_body_for_changes(body) - - if changes.any? - # Use the first significant change as the main commit type - main_change = changes.first - enhanced_subject = main_change[:description] || subject.gsub(/\s*\(#\d+\)$/, '') - - commit.merge({ - original_subject: subject, - subject: "#{main_change[:type]}: #{enhanced_subject}", - pr_number: pr_number, - pr_info: pr_info, - squash_changes: changes - }) - else - # Fallback: try to infer type from subject or body - inferred_type = infer_commit_type_from_content(subject, body) - if inferred_type - clean_subject = subject.gsub(/\s*\(#\d+\)$/, '') - commit.merge({ - original_subject: subject, - subject: "#{inferred_type}: #{clean_subject}", - pr_number: pr_number, - pr_info: pr_info - }) - else - commit - end - end - end - - ## - # Parse commit body for individual changes - # - # @param body [String] Commit body text - # @return [Array] Array of parsed changes - def parse_commit_body_for_changes(body) - return [] if body.nil? || body.strip.empty? - - changes = [] - - # Look for bullet points or line items - body.split("\n").each do |line| - line = line.strip - next if line.empty? - - # Match patterns like: - # * Add feature X - # - Fix bug Y - # โ€ข Update documentation - if line.match?(/^[\*\-โ€ข]\s+(.+)/) - description = line.gsub(/^[\*\-โ€ข]\s+/, '') - type = infer_type_from_description(description) - changes << { - type: type, - description: description, - line: line - } - end - end - - changes - end - - ## - # Infer commit type from description - # - # @param description [String] Change description - # @return [String] Inferred commit type - def infer_type_from_description(description) - desc_lower = description.downcase - - case desc_lower - when /^add|^implement|^create|^introduce/ - 'feat' - when /^fix|^resolve|^correct|^repair/ - 'fix' - when /^update|^change|^modify|^improve/ - 'refactor' - when /^remove|^delete|^drop/ - 'refactor' - when /^test|^spec/ - 'test' - when /^doc|^readme|^comment/ - 'docs' - when /^refactor|^restructure|^reorganize/ - 'refactor' - when /^perf|^optim|^speed/ - 'perf' - when /^style|^format|^lint/ - 'style' - when /^chore|^maintenance|^clean/ - 'chore' - when /^build|^deps|^depend/ - 'build' - when /^ci|^deploy|^workflow/ - 'ci' - else - 'feat' # Default to feature if uncertain - end - end - ## - # Infer commit type from subject and body content - # - # @param subject [String] Commit subject - # @param body [String] Commit body - # @return [String, nil] Inferred type or nil - def infer_commit_type_from_content(subject, body) - content = "#{subject} #{body}".downcase - - # Look for keywords that indicate the type of change - if content.match?(/add|implement|create|introduce|new/) - 'feat' - elsif content.match?(/fix|bug|issue|resolve|correct/) - 'fix' - elsif content.match?(/update|change|modify|improve|enhance/) - 'refactor' - elsif content.match?(/doc|readme|comment/) - 'docs' - elsif content.match?(/test|spec/) - 'test' - elsif content.match?(/style|format|lint/) - 'style' - elsif content.match?(/perf|optim|performance/) - 'perf' - elsif content.match?(/chore|maintenance|clean/) - 'chore' - else - nil + # 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 - end - ## - # Get PR information from GitHub CLI - # - # @param pr_number [String] PR number - # @return [Hash, nil] PR information or nil if not available - def get_pr_info(pr_number) - return nil unless system('gh --version > /dev/null 2>&1') - - begin - pr_json = `gh pr view #{pr_number} --json title,body,labels 2>/dev/null` - return nil if pr_json.empty? - - require 'json' - JSON.parse(pr_json) - rescue => e - puts "โš ๏ธ Could not fetch PR ##{pr_number} info: #{e.message}" if options[:debug] - nil - end + commits end ## @@ -445,17 +306,17 @@ def get_last_release_tag # @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]) - + # 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, @@ -464,7 +325,7 @@ def categorize_commits(commits) commit: commit } end - + # Remove empty categories and sort categories.reject { |_, commits| commits.empty? } .sort_by { |category, _| category_priority(category) } @@ -477,18 +338,38 @@ def categorize_commits(commits) # @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 + # Match conventional commit format: type(scope): description match = subject.match(/^(\w+)(?:\(([^)]+)\))?(!)?: (.+)$/) - + 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 @@ -503,9 +384,6 @@ def parse_conventional_commit(subject) # @param commit [Hash] Commit information # @return [Boolean] true if commit should be skipped def should_skip_commit?(type, commit) - # Skip merge commits - return true if commit[:subject].start_with?('Merge ') - # Skip certain types if configured skip_types = options[:skip_types] || [] skip_types.include?(type) @@ -547,14 +425,14 @@ def determine_version_bump(commits) _, _, _, breaking = parse_conventional_commit(commit[:subject]) breaking || commit[:body].include?('BREAKING CHANGE') end - + return :major if has_breaking - + has_features = commits.any? do |commit| type, _, _, _ = parse_conventional_commit(commit[:subject]) type == 'feat' end - + has_features ? :minor : :patch end @@ -579,28 +457,39 @@ def preview_changelog(categorized_commits, version_bump) def update_changelog(categorized_commits, version_bump) current_content = File.read(CHANGELOG_PATH) new_content = generate_changelog_content(categorized_commits) - + # Find the position to insert new content (after ## [Unreleased]) unreleased_pattern = /^## \[Unreleased\]\s*\n/ match = current_content.match(unreleased_pattern) - + unless match - raise "Could not find [Unreleased] section in #{CHANGELOG_PATH}" + # 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 - - # Insert new release section after the unreleased section - insertion_point = match.end(0) - - # Clear the unreleased section and add new release - updated_content = current_content[0...insertion_point] + - "\n" + - new_content + - "\n" + - current_content[insertion_point..-1] - - # Update comparison links at the bottom - updated_content = update_comparison_links(updated_content) - + # Write back to file File.write(CHANGELOG_PATH, updated_content) end @@ -614,19 +503,25 @@ def generate_changelog_content(categorized_commits) content = [] content << "## [#{version_string}] - #{Date.today.strftime('%Y-%m-%d')}" content << "" - - categorized_commits.each do |category, commits| - content << "### #{category}" + + if categorized_commits.empty? + content << "### Changed" + content << "- Minor improvements and bug fixes" content << "" - - commits.each do |commit_info| - line = format_changelog_line(commit_info) - content << line if line + 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 - - content << "" end - + content.join("\n") end @@ -638,52 +533,41 @@ def generate_changelog_content(categorized_commits) def format_changelog_line(commit_info) description = commit_info[:description] scope = commit_info[:scope] - pr_number = commit_info[:pr_number] - + # 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) (#PR if present)" + short_hash = commit_hash ? commit_hash[0..7] : nil + + # Format the line line = "- #{text.capitalize}" line += " (#{scope})" if scope && !scope.empty? - line += " (#{short_hash})" if short_hash - line += " (##{pr_number})" if pr_number - + + # Add commit link if repository URL is available + if options[:include_commit_links] && repository_url && short_hash && commit_hash + line += " ([#{short_hash}](#{repository_url}/commit/#{commit_hash}))" + elsif short_hash + line += " (#{short_hash})" + end + # Add breaking change marker if commit_info[:breaking] line = "- **BREAKING**: #{text.capitalize}" - line += " (#{short_hash})" if short_hash - line += " (##{pr_number})" if pr_number - end - - # If this is a squash commit with multiple changes, add them as sub-items - if commit_info[:squash_changes] && commit_info[:squash_changes].length > 1 - sub_lines = commit_info[:squash_changes][1..-1].map do |change| - " - #{change[:description].capitalize}" + if options[:include_commit_links] && repository_url && short_hash && commit_hash + line += " ([#{short_hash}](#{repository_url}/commit/#{commit_hash}))" + elsif short_hash + line += " (#{short_hash})" end - line += "\n" + sub_lines.join("\n") if sub_lines.any? end - + line end - ## - # Update comparison links at the bottom of the changelog - # - # @param content [String] Current changelog content - # @return [String] Updated changelog content with new comparison links - def update_comparison_links(content) - # This would need to be customized based on your repository URL structure - # For now, we'll leave the existing links unchanged - content - end end ## @@ -691,53 +575,67 @@ def update_comparison_links(content) 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" + opts.separator "Automatic Changelog Generator for PICO API Go" 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 "- Must be run from a release or hotfix branch (release/vX.Y.Z or hotfix/vX.Y.Z)" opts.separator "- Git repository with existing tags" - opts.separator "- CHANGELOG.md file with [Unreleased] section" + 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("--[no-]links", "Include/exclude commit links (default: include)") do |links| + options[:include_commit_links] = links + 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} # Generate changelog" - opts.separator " #{$0} --dry-run # Preview without changes" - opts.separator " #{$0} --force # Ignore uncommitted changes" + 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" + opts.separator " #{$0} --version 1.2.3 --no-links # Without commit links" end - + begin parser.parse!(args) - + + unless options[:version] + puts "Error: Version is required. Use --version flag." + 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 @@ -753,3 +651,4 @@ def self.run(args = ARGV) if __FILE__ == $0 CLI.run end + From 547f45566a4f89d91ca4b15a595c784d0dbafe83 Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 15 Sep 2025 14:38:15 +0700 Subject: [PATCH 2/5] fix: simplify workflows and restore working deploy.yml - Restored working deploy.yml from develop branch - Simplified release-branch-creation.yml to call script directly - Removed complex DevOps over-engineering - Kept essential workflows: CI, Deploy, Release Branch Creation, Release Workflow - Added empty lines at end of files per requirement The workflows now follow the pico-api-docs pattern: - Simple changelog generation with --version parameter - No complex configuration or Docker requirements - Focus on essential functionality that works --- .github/workflows/deploy.yml | 64 +- .github/workflows/release-branch-creation.yml | 662 +++++++----------- 2 files changed, 293 insertions(+), 433 deletions(-) diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 0d1bd28..5f3401a 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -71,66 +71,57 @@ jobs: echo "โš ๏ธ HTML documentation not found, continuing without it" fi - - name: Setup SSH for deployment - id: ssh_setup + - name: Setup SSH Agent + uses: webfactory/ssh-agent@v0.8.0 + with: + ssh-private-key: ${{ secrets.DEPLOY_SSH_KEY }} + log-public-key: false + + - name: Debug SSH configuration + run: | + echo "SSH Agent PID: $SSH_AGENT_PID" + echo "SSH Auth Sock: $SSH_AUTH_SOCK" + ssh-add -l || echo "No keys loaded in agent" + + - name: Add server to known hosts run: | - echo "๐Ÿ” Setting up SSH connection..." - - # Create SSH directory mkdir -p ~/.ssh - chmod 700 ~/.ssh - - # Write SSH key - echo "${{ secrets.DEPLOY_SSH_KEY }}" > ~/.ssh/deploy_key - chmod 600 ~/.ssh/deploy_key - - # Add to known hosts echo "Adding ${{ secrets.DEPLOY_HOST }}:${{ secrets.DEPLOY_PORT }} to known hosts..." ssh-keyscan -H -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_HOST }} >> ~/.ssh/known_hosts - - # Test connection - echo "Testing SSH connection..." - if ssh -i ~/.ssh/deploy_key -p ${{ secrets.DEPLOY_PORT }} -o ConnectTimeout=10 -o BatchMode=yes ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} 'echo "SSH connection successful"'; then - echo "โœ… SSH connection verified" - echo "ssh_ready=true" >> $GITHUB_OUTPUT - else - echo "โŒ SSH connection failed" - echo "ssh_ready=false" >> $GITHUB_OUTPUT - exit 1 - fi + echo "Known hosts file created" + + - 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 - if: steps.ssh_setup.outputs.ssh_ready == 'true' run: | echo "๐Ÿš€ Starting deployment of ${{ env.VERSION }} to production..." - # Use consistent SSH options - SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DEPLOY_PORT }} -o StrictHostKeyChecking=yes -o ConnectTimeout=30" - SSH_TARGET="${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" - # Upload the binary to a temporary location first echo "๐Ÿ“ค Uploading binary..." - scp $SSH_OPTS ${{ secrets.BINARY_NAME }} $SSH_TARGET:/tmp/${{ secrets.BINARY_NAME }}-${{ env.VERSION }} + scp -P ${{ secrets.DEPLOY_PORT }} ${{ secrets.BINARY_NAME }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/${{ secrets.BINARY_NAME }}-${{ env.VERSION }} # Upload documentation files echo "๐Ÿ“š Uploading documentation..." if [ -f "docs/swagger.html" ]; then - scp $SSH_OPTS docs/swagger.html $SSH_TARGET:/tmp/swagger-${{ env.VERSION }}.html + scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.html ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.html echo "โœ… HTML documentation uploaded" fi if [ -f "docs/swagger.json" ]; then - scp $SSH_OPTS docs/swagger.json $SSH_TARGET:/tmp/swagger-${{ env.VERSION }}.json + scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.json ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.json echo "โœ… JSON documentation uploaded" fi if [ -f "docs/swagger.yaml" ]; then - scp $SSH_OPTS docs/swagger.yaml $SSH_TARGET:/tmp/swagger-${{ env.VERSION }}.yaml + scp -P ${{ secrets.DEPLOY_PORT }} docs/swagger.yaml ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}:/tmp/swagger-${{ env.VERSION }}.yaml echo "โœ… YAML documentation uploaded" fi # Execute deployment script on remote server - ssh $SSH_OPTS $SSH_TARGET << 'EOF' + ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' set -e DEPLOY_PATH="${{ secrets.DEPLOY_PATH }}" @@ -218,10 +209,7 @@ jobs: run: | echo "๐Ÿงน Cleaning up backup files after successful deployment..." - SSH_OPTS="-i ~/.ssh/deploy_key -p ${{ secrets.DEPLOY_PORT }} -o StrictHostKeyChecking=yes -o ConnectTimeout=30" - SSH_TARGET="${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }}" - - ssh $SSH_OPTS $SSH_TARGET << 'EOF' + ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' DEPLOY_PATH="${{ secrets.DEPLOY_PATH }}" BINARY_NAME="${{ secrets.BINARY_NAME }}" @@ -413,4 +401,4 @@ jobs: else echo "โŒ Deployment failed for ${{ github.ref_name }}" exit 1 - fi \ No newline at end of file + fi diff --git a/.github/workflows/release-branch-creation.yml b/.github/workflows/release-branch-creation.yml index ca1a01a..dd12031 100644 --- a/.github/workflows/release-branch-creation.yml +++ b/.github/workflows/release-branch-creation.yml @@ -57,217 +57,72 @@ jobs: echo "should_bump_develop=$SHOULD_BUMP_DEVELOP" >> $GITHUB_OUTPUT echo "clean_version=$(echo $VERSION | sed 's/^v//')" >> $GITHUB_OUTPUT - - name: Get previous version for changelog - id: previous_version - run: | - # Get the last tag for changelog range - LAST_TAG=$(git tag --sort=-version:refname | head -n1 || echo "") - echo "last_tag=$LAST_TAG" >> $GITHUB_OUTPUT - echo "Previous version: $LAST_TAG" - - # Determine base commit for changelog - if [ -n "$LAST_TAG" ]; then - BASE_COMMIT="$LAST_TAG" - else - # If no tags exist, use first commit - BASE_COMMIT=$(git rev-list --max-parents=0 HEAD) - fi - echo "base_commit=$BASE_COMMIT" >> $GITHUB_OUTPUT - - - name: Load version configuration - id: version_config - run: | - CONFIG_FILE=".version-config.yml" - - if [ -f "$CONFIG_FILE" ]; then - echo "๐Ÿ“‹ Loading version configuration from $CONFIG_FILE" - - # Parse YAML configuration with better error handling - NEXT_MAJOR=$(grep "^next_major:" "$CONFIG_FILE" | awk '{print $2}' | head -1 || echo "false") - MAJOR_TARGET=$(grep "^major_version_target:" "$CONFIG_FILE" | awk '{print $2}' | tr -d '"' | head -1 || echo "") - DEFAULT_BUMP=$(grep -A 10 "^version_rules:" "$CONFIG_FILE" | grep "default_release_bump:" | awk '{print $2}' | tr -d '"' | head -1 || echo "minor") - AUTO_DETECT=$(grep -A 10 "^version_rules:" "$CONFIG_FILE" | grep "auto_detect_breaking:" | awk '{print $2}' | head -1 || echo "true") - REQUIRE_MANUAL=$(grep -A 10 "^version_rules:" "$CONFIG_FILE" | grep "require_manual_major:" | awk '{print $2}' | head -1 || echo "false") - - # Clean up values and set defaults if empty - NEXT_MAJOR=$(echo "$NEXT_MAJOR" | tr -d ' \n\r' || echo "false") - MAJOR_TARGET=$(echo "$MAJOR_TARGET" | tr -d ' \n\r' || echo "") - DEFAULT_BUMP=$(echo "$DEFAULT_BUMP" | tr -d ' \n\r' || echo "minor") - AUTO_DETECT=$(echo "$AUTO_DETECT" | tr -d ' \n\r' || echo "true") - REQUIRE_MANUAL=$(echo "$REQUIRE_MANUAL" | tr -d ' \n\r' || echo "false") - - # Ensure values are not empty for GitHub Actions - [ -z "$NEXT_MAJOR" ] && NEXT_MAJOR="false" - [ -z "$MAJOR_TARGET" ] && MAJOR_TARGET="none" - [ -z "$DEFAULT_BUMP" ] && DEFAULT_BUMP="minor" - [ -z "$AUTO_DETECT" ] && AUTO_DETECT="true" - [ -z "$REQUIRE_MANUAL" ] && REQUIRE_MANUAL="false" - - echo "next_major=${NEXT_MAJOR}" >> $GITHUB_OUTPUT - echo "major_target=${MAJOR_TARGET}" >> $GITHUB_OUTPUT - echo "default_bump=${DEFAULT_BUMP}" >> $GITHUB_OUTPUT - echo "auto_detect_breaking=${AUTO_DETECT}" >> $GITHUB_OUTPUT - echo "require_manual_major=${REQUIRE_MANUAL}" >> $GITHUB_OUTPUT - echo "config_exists=true" >> $GITHUB_OUTPUT - else - echo "โš ๏ธ No version configuration found, using defaults" - echo "next_major=false" >> $GITHUB_OUTPUT - echo "major_target=none" >> $GITHUB_OUTPUT - echo "default_bump=minor" >> $GITHUB_OUTPUT - echo "auto_detect_breaking=true" >> $GITHUB_OUTPUT - echo "require_manual_major=false" >> $GITHUB_OUTPUT - echo "config_exists=false" >> $GITHUB_OUTPUT - fi - - - name: Detect version bump type - id: version_type - run: | - VERSION="${{ steps.version_info.outputs.version }}" - LAST_TAG="${{ steps.previous_version.outputs.last_tag }}" - TYPE="${{ steps.version_info.outputs.type }}" - - # Get configuration - NEXT_MAJOR="${{ steps.version_config.outputs.next_major }}" - MAJOR_TARGET="${{ steps.version_config.outputs.major_target }}" - DEFAULT_BUMP="${{ steps.version_config.outputs.default_bump }}" - AUTO_DETECT="${{ steps.version_config.outputs.auto_detect_breaking }}" - - # Start with default bump type - BUMP_TYPE="$DEFAULT_BUMP" - - if [ -n "$LAST_TAG" ]; then - # Parse current and previous versions - CURRENT_MAJOR=$(echo $VERSION | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1/') - CURRENT_MINOR=$(echo $VERSION | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\2/') - CURRENT_PATCH=$(echo $VERSION | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\3/') - - LAST_MAJOR=$(echo $LAST_TAG | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\1/') - LAST_MINOR=$(echo $LAST_TAG | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\2/') - LAST_PATCH=$(echo $LAST_TAG | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\3/') - - # Determine actual bump type from version numbers - if [ "$CURRENT_MAJOR" -gt "$LAST_MAJOR" ]; then - BUMP_TYPE="major" - elif [ "$CURRENT_MINOR" -gt "$LAST_MINOR" ]; then - BUMP_TYPE="minor" - elif [ "$CURRENT_PATCH" -gt "$LAST_PATCH" ]; then - BUMP_TYPE="patch" - fi - - # For hotfix, it's always a patch - if [ "$TYPE" = "hotfix" ]; then - BUMP_TYPE="patch" - fi - fi - - # Check configuration for major version planning - if [ "$NEXT_MAJOR" = "true" ]; then - if [ -n "$MAJOR_TARGET" ] && [ "$MAJOR_TARGET" != "none" ] && [ "$VERSION" = "$MAJOR_TARGET" ]; then - BUMP_TYPE="major" - echo "๐Ÿš€ Major version release configured: $MAJOR_TARGET" - fi - fi - - # Auto-detect breaking changes if enabled - if [ "$AUTO_DETECT" = "true" ]; then - BASE_COMMIT="${{ steps.previous_version.outputs.base_commit }}" - BREAKING_COMMITS=$(git log --pretty=format:"%s" "$BASE_COMMIT..HEAD" | \ - grep -E "(BREAKING CHANGE|!:|feat!:|fix!:)" || echo "") - - if [ -n "$BREAKING_COMMITS" ]; then - echo "๐Ÿšจ Breaking changes detected in commit messages:" - echo "$BREAKING_COMMITS" - - if [ "$BUMP_TYPE" != "major" ]; then - echo "โš ๏ธ Detected breaking changes but version is not major!" - echo " Consider updating .version-config.yml or using major version" - echo " Breaking commits found:" - echo "$BREAKING_COMMITS" | head -3 - fi - - # Auto-promote to major if breaking changes detected and not manual - if [ "${{ steps.version_config.outputs.require_manual_major }}" != "true" ]; then - echo "๐Ÿ”„ Auto-promoting to major version due to breaking changes" - BUMP_TYPE="major" - fi - fi - fi - - echo "bump_type=$BUMP_TYPE" >> $GITHUB_OUTPUT - echo "Final version bump type: $BUMP_TYPE" - - # Additional validation - if [ "$BUMP_TYPE" = "major" ]; then - echo "๐Ÿšจ MAJOR VERSION RELEASE DETECTED" - echo " This will be a breaking change release" - echo " Please ensure all breaking changes are documented" - fi - - - name: Set up Ruby - uses: ruby/setup-ruby@v1 + - name: Setup Go for changelog generation + uses: actions/setup-go@v4 with: - ruby-version: "3.2" + go-version: '1.21' - - name: Generate changelog using Ruby script + - name: Generate changelog using Go script id: changelog run: | VERSION="${{ steps.version_info.outputs.version }}" echo "๐Ÿš€ Generating changelog for $VERSION..." - # Make the script executable - chmod +x generate-changelog.rb - - # Run changelog generation (simple like pico-api-docs) - if ruby 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" + # Make the script executable if it exists + if [ -f "scripts/generate-changelog.go" ]; then + chmod +x scripts/generate-changelog.go + + # Run changelog generation + if go run scripts/generate-changelog.go --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 Go script" + CHANGELOG_STATUS="false" + else + echo "โœ… CHANGELOG.md was updated" + git diff --stat CHANGELOG.md + CHANGELOG_STATUS="true" + fi else - echo "โœ… CHANGELOG.md was updated" - git diff --stat CHANGELOG.md - CHANGELOG_STATUS="true" + echo "โŒ Changelog generation failed" + CHANGELOG_STATUS="false" fi - - echo "changelog_updated=$CHANGELOG_STATUS" >> $GITHUB_OUTPUT else - echo "โŒ Changelog generation failed" - echo "changelog_updated=false" >> $GITHUB_OUTPUT - - echo "changelog_updated=$CHANGELOG_STATUS" >> $GITHUB_OUTPUT + echo "โš ๏ธ No changelog script found, skipping generation" + CHANGELOG_STATUS="false" fi - - # No separate release notes file needed - CHANGELOG.md is the source of truth - echo "changelog_file=CHANGELOG.md" >> $GITHUB_OUTPUT - - name: Update version in source files - run: | - VERSION="${{ steps.version_info.outputs.version }}" - CLEAN_VERSION="${{ steps.version_info.outputs.clean_version }}" + echo "changelog_updated=$CHANGELOG_STATUS" >> $GITHUB_OUTPUT - echo "๐Ÿ“ Updating version to $CLEAN_VERSION in source files..." - - # Update main.go version annotation - if [ -f "cmd/main.go" ]; then - sed -i.bak "s/@version.*/@version\t\t$CLEAN_VERSION/" cmd/main.go && rm -f cmd/main.go.bak - echo "โœ… Updated cmd/main.go" + - name: Update version in go.mod and other files + run: | + VERSION="${{ steps.version_info.outputs.clean_version }}" + echo "๐Ÿ“ Updating version to $VERSION in project files..." + + # Update version in go.mod if needed (for version comments) + if [ -f "go.mod" ]; then + # Add version comment to go.mod if not already present + if ! grep -q "// Version: " go.mod; then + sed -i '1 a\// Version: '"$VERSION" go.mod + else + sed -i 's|// Version: .*|// Version: '"$VERSION"'|' go.mod + fi + echo "โœ… Updated version comment in go.mod" fi - # Update handler version - if [ -f "internal/handler/covid_handler.go" ]; then - sed -i.bak "s/\"version\":\s*\"[^\"]*\"/\"version\": \"$CLEAN_VERSION\"/" internal/handler/covid_handler.go && rm -f internal/handler/covid_handler.go.bak - echo "โœ… Updated internal/handler/covid_handler.go" + # Update version.go file if it exists + if [ -f "internal/version/version.go" ]; then + sed -i 's|const Version = ".*"|const Version = "'"$VERSION"'"|' internal/version/version.go + echo "โœ… Updated version.go" fi - - name: Install and regenerate documentation - run: | - echo "๐Ÿ“š Regenerating API documentation..." - go install github.com/swaggo/swag/cmd/swag@latest - export PATH=$PATH:$(go env GOPATH)/bin - swag init -g cmd/main.go -o ./docs - echo "โœ… Documentation regenerated" + # Update Dockerfile if it exists + if [ -f "Dockerfile" ]; then + sed -i 's|LABEL version=".*"|LABEL version="'"$VERSION"'"|' Dockerfile + echo "โœ… Updated Dockerfile version label" + fi - name: Create preparation PR branch id: pr_branch @@ -275,42 +130,68 @@ jobs: 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" + + # Check if branch already exists and generate unique name if needed + BASE_PR_BRANCH="chore/prepare-$TYPE-$VERSION" + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + + if git ls-remote --heads origin "$BASE_PR_BRANCH" | grep -q "$BASE_PR_BRANCH"; then + PR_BRANCH="${BASE_PR_BRANCH}-${TIMESTAMP}" + echo "โš ๏ธ Base branch exists, using unique name: $PR_BRANCH" + else + PR_BRANCH="$BASE_PR_BRANCH" + echo "โœ… Using base branch name: $PR_BRANCH" + fi + 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 - - # Commit changes + + # Create commit message CHANGELOG_STATUS="${{ steps.changelog.outputs.changelog_updated }}" if [[ "$CHANGELOG_STATUS" == "true" ]]; then - CHANGELOG_INFO="- Generate release changelog using generate-changelog.rb" + CHANGELOG_INFO="- Generate release changelog" else CHANGELOG_INFO="- Changelog generation skipped (manual update needed)" fi - - git commit -m "chore: prepare $VERSION $TYPE - - - Update version to ${{ steps.version_info.outputs.clean_version }} in source files - $CHANGELOG_INFO - - Regenerate API documentation with new version - - This commit prepares the $RELEASE_BRANCH branch for $TYPE." - + + # Commit with multiline message + git commit -m "chore: prepare $VERSION $TYPE" \ + -m "" \ + -m "- Update version to ${{ steps.version_info.outputs.clean_version }} in project files" \ + -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 required labels if they don't exist + if: steps.pr_branch.outputs.has_changes == 'true' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "๐Ÿท๏ธ Ensuring required labels exist..." + + # Create labels if they don't exist + gh label create "chore" --description "Maintenance and chore tasks" --color "0e8a16" || echo "Label 'chore' already exists" + gh label create "auto-generated" --description "Automatically generated by GitHub Actions" --color "bfdadc" || echo "Label 'auto-generated' already exists" + gh label create "release" --description "Release branch related" --color "d73a4a" || echo "Label 'release' already exists" + gh label create "hotfix" --description "Hotfix branch related" --color "b60205" || echo "Label 'hotfix' already exists" + gh label create "version-bump" --description "Version bump changes" --color "0052cc" || echo "Label 'version-bump' already exists" + + echo "โœ… Label creation completed" + - name: Create preparation PR if: steps.pr_branch.outputs.has_changes == 'true' env: @@ -321,75 +202,65 @@ jobs: RELEASE_BRANCH="${{ steps.version_info.outputs.branch_name }}" PR_BRANCH="${{ steps.pr_branch.outputs.pr_branch }}" CHANGELOG_STATUS="${{ steps.changelog.outputs.changelog_updated }}" - - # Create PR to the release branch - gh pr create \ - --base "$RELEASE_BRANCH" \ - --head "$PR_BRANCH" \ - --title "chore: prepare $VERSION $TYPE" \ - --body "$(cat <> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "**Branch**: \`$BRANCH_NAME\`" >> $GITHUB_STEP_SUMMARY echo "**Version**: $VERSION" >> $GITHUB_STEP_SUMMARY - echo "**Type**: ${{ steps.version_type.outputs.bump_type }} ($TYPE)" >> $GITHUB_STEP_SUMMARY - echo "**Base**: ${{ steps.version_info.outputs.base_branch }}" >> $GITHUB_STEP_SUMMARY + echo "**Type**: $TYPE" >> $GITHUB_STEP_SUMMARY echo "" >> $GITHUB_STEP_SUMMARY echo "### โœ… Completed Actions" >> $GITHUB_STEP_SUMMARY - CHANGELOG_STATUS="${{ steps.changelog.outputs.changelog_updated }}" if [[ "$CHANGELOG_STATUS" == "true" ]]; then - echo "- ๐Ÿ“‹ Generated changelog using generate-changelog.rb" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿ“‹ Generated changelog" >> $GITHUB_STEP_SUMMARY else - echo "- โš ๏ธ Changelog generation skipped (manual update needed)" >> $GITHUB_STEP_SUMMARY + echo "- โš ๏ธ Changelog generation skipped" >> $GITHUB_STEP_SUMMARY fi - echo "- ๐Ÿ“ Updated version in source files" >> $GITHUB_STEP_SUMMARY - echo "- ๐Ÿ“š Regenerated API documentation" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿ“ Updated version in project files" >> $GITHUB_STEP_SUMMARY if [[ "${{ steps.pr_branch.outputs.has_changes }}" == "true" ]]; then echo "- ๐Ÿ“‹ Created preparation PR to \`$BRANCH_NAME\` branch" >> $GITHUB_STEP_SUMMARY @@ -397,16 +268,6 @@ jobs: echo "- โ„น๏ธ No changes needed (already up to date)" >> $GITHUB_STEP_SUMMARY fi - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐Ÿ“‹ Generated Changelog Preview" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - echo "Click to expand changelog" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "\`\`\`markdown" >> $GITHUB_STEP_SUMMARY - head -30 $CHANGELOG_FILE >> $GITHUB_STEP_SUMMARY - echo "\`\`\`" >> $GITHUB_STEP_SUMMARY - echo "
" >> $GITHUB_STEP_SUMMARY - # 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/') @@ -424,172 +285,176 @@ jobs: fetch-depth: 0 token: ${{ secrets.GITHUB_TOKEN }} + - name: Check for existing version bump PRs + id: check_prs + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + # Check if there's already a version bump PR open for develop + EXISTING_PRS=$(gh pr list --base develop --state open --label "version-bump" --json number,title) + + if [ "$(echo "$EXISTING_PRS" | jq '. | length')" -gt 0 ]; then + echo "โš ๏ธ Found existing version bump PR(s):" + echo "$EXISTING_PRS" | jq -r '.[] | "#\(.number): \(.title)"' + echo "skip_bump=true" >> $GITHUB_OUTPUT + echo "Skipping version bump to avoid conflicts" + else + echo "โœ… No existing version bump PRs found" + echo "skip_bump=false" >> $GITHUB_OUTPUT + fi + - name: Configure Git + if: steps.check_prs.outputs.skip_bump == 'false' run: | git config user.name "github-actions[bot]" git config user.email "github-actions[bot]@users.noreply.github.com" - - name: Load version configuration for develop bump - id: dev_config - run: | - CONFIG_FILE=".version-config.yml" - - if [ -f "$CONFIG_FILE" ]; then - echo "๐Ÿ“‹ Loading develop version configuration" - - STRATEGY=$(grep -A5 "develop_branch:" "$CONFIG_FILE" | grep "next_version_strategy:" | awk '{print $2}' | tr -d '"' | head -1 || echo "auto") - MANUAL_VERSION=$(grep -A5 "develop_branch:" "$CONFIG_FILE" | grep "manual_next_version:" | awk '{print $2}' | tr -d '"' | head -1 || echo "") - DEV_SUFFIX=$(grep -A5 "develop_branch:" "$CONFIG_FILE" | grep "dev_suffix:" | awk '{print $2}' | tr -d '"' | head -1 || echo "-dev") - - # Clean up values and set defaults if empty - STRATEGY=$(echo "$STRATEGY" | tr -d ' \n\r' || echo "auto") - MANUAL_VERSION=$(echo "$MANUAL_VERSION" | tr -d ' \n\r' || echo "") - DEV_SUFFIX=$(echo "$DEV_SUFFIX" | tr -d ' \n\r' || echo "-dev") - - # Ensure values are not empty for GitHub Actions - [ -z "$STRATEGY" ] && STRATEGY="auto" - [ -z "$MANUAL_VERSION" ] && MANUAL_VERSION="none" - [ -z "$DEV_SUFFIX" ] && DEV_SUFFIX="-dev" - - echo "strategy=$STRATEGY" >> $GITHUB_OUTPUT - echo "manual_version=$MANUAL_VERSION" >> $GITHUB_OUTPUT - echo "dev_suffix=$DEV_SUFFIX" >> $GITHUB_OUTPUT - echo "config_exists=true" >> $GITHUB_OUTPUT - else - echo "โš ๏ธ No version configuration found for develop, using defaults" - echo "strategy=auto" >> $GITHUB_OUTPUT - echo "manual_version=none" >> $GITHUB_OUTPUT - echo "dev_suffix=-dev" >> $GITHUB_OUTPUT - echo "config_exists=false" >> $GITHUB_OUTPUT - fi - - name: Calculate next development version + if: steps.check_prs.outputs.skip_bump == 'false' id: next_version run: | RELEASE_BRANCH="${{ github.event.ref }}" CURRENT_VERSION=$(echo $RELEASE_BRANCH | sed 's/release\///') - STRATEGY="${{ steps.dev_config.outputs.strategy }}" - MANUAL_VERSION="${{ steps.dev_config.outputs.manual_version }}" - DEV_SUFFIX="${{ steps.dev_config.outputs.dev_suffix }}" # Ensure version starts with 'v' if [[ ! $CURRENT_VERSION == v* ]]; then CURRENT_VERSION="v$CURRENT_VERSION" fi - if [ "$STRATEGY" = "manual" ] && [ -n "$MANUAL_VERSION" ] && [ "$MANUAL_VERSION" != "none" ]; then - # Use manually specified version - NEXT_VERSION="$MANUAL_VERSION" - if [[ ! $NEXT_VERSION == v* ]]; then - NEXT_VERSION="v$NEXT_VERSION" - fi - echo "๐Ÿ“ Using manual next version: $NEXT_VERSION" - else - # Auto-calculate next version (default behavior) - 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/') - PATCH=$(echo $CURRENT_VERSION | sed 's/v\([0-9]*\)\.\([0-9]*\)\.\([0-9]*\).*/\3/') - - # Determine next version based on current release type - if [ "$MAJOR" -gt 0 ] && [ "$MINOR" -eq 0 ] && [ "$PATCH" -eq 0 ]; then - # This is a major release, next should be major+1.0.0 - NEXT_MAJOR=$((MAJOR + 1)) - NEXT_VERSION="v$NEXT_MAJOR.0.0" - else - # Regular minor/patch release, increment minor - NEXT_MINOR=$((MINOR + 1)) - NEXT_VERSION="v$MAJOR.$NEXT_MINOR.0" - fi - echo "๐Ÿ”„ Auto-calculated next version: $NEXT_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/') - NEXT_DEV_VERSION="$NEXT_VERSION$DEV_SUFFIX" - CLEAN_NEXT_VERSION=$(echo $NEXT_VERSION | sed 's/^v//') + # 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 "next_dev_version=$NEXT_DEV_VERSION" >> $GITHUB_OUTPUT echo "clean_next_version=$CLEAN_NEXT_VERSION" >> $GITHUB_OUTPUT - echo "strategy=$STRATEGY" >> $GITHUB_OUTPUT echo "Current release: $CURRENT_VERSION" - echo "Next development version: $NEXT_DEV_VERSION (strategy: $STRATEGY)" + echo "Next development version: $NEXT_VERSION" + + - name: Check current develop version + if: steps.check_prs.outputs.skip_bump == 'false' + id: current_dev_version + run: | + # Try to get version from go.mod comment or version.go + CURRENT_DEV_VERSION="" + + if [ -f "go.mod" ] && grep -q "// Version: " go.mod; then + CURRENT_DEV_VERSION=$(grep "// Version: " go.mod | awk '{print $3}') + elif [ -f "internal/version/version.go" ]; then + CURRENT_DEV_VERSION=$(grep 'const Version = ' internal/version/version.go | sed 's/.*"\(.*\)".*/\1/') + else + CURRENT_DEV_VERSION="0.0.0" + fi + + NEXT_VERSION="${{ steps.next_version.outputs.clean_next_version }}" + + echo "current_dev_version=$CURRENT_DEV_VERSION" >> $GITHUB_OUTPUT + + # Check if develop already has a higher or equal version + if [ "$(printf '%s\n' "$CURRENT_DEV_VERSION" "$NEXT_VERSION" | sort -V | tail -n1)" = "$CURRENT_DEV_VERSION" ]; then + if [ "$CURRENT_DEV_VERSION" = "$NEXT_VERSION" ]; then + echo "โš ๏ธ Develop already has the target version: $CURRENT_DEV_VERSION" + else + echo "โš ๏ธ Develop already has a higher version: $CURRENT_DEV_VERSION > $NEXT_VERSION" + fi + echo "skip_update=true" >> $GITHUB_OUTPUT + else + echo "โœ… Will update from $CURRENT_DEV_VERSION to $NEXT_VERSION" + echo "skip_update=false" >> $GITHUB_OUTPUT + fi - name: Update develop branch with next version - id: update_develop + if: steps.check_prs.outputs.skip_bump == 'false' && steps.current_dev_version.outputs.skip_update == 'false' run: | - NEXT_VERSION="${{ steps.next_version.outputs.next_version }}" CLEAN_VERSION="${{ steps.next_version.outputs.clean_next_version }}" - DEV_VERSION="${{ steps.next_version.outputs.next_dev_version }}" + echo "๐Ÿ“ Updating develop branch to $CLEAN_VERSION..." - echo "๐Ÿ“ Updating develop branch to $DEV_VERSION..." - - # Update main.go version annotation - if [ -f "cmd/main.go" ]; then - sed -i.bak "s/@version.*/@version\t\t$CLEAN_VERSION/" cmd/main.go && rm -f cmd/main.go.bak - echo "โœ… Updated cmd/main.go to $CLEAN_VERSION" + # Update version in go.mod comment + if [ -f "go.mod" ]; then + if ! grep -q "// Version: " go.mod; then + sed -i '1 a\// Version: '"$CLEAN_VERSION" go.mod + else + sed -i 's|// Version: .*|// Version: '"$CLEAN_VERSION"'|' go.mod + fi + echo "โœ… Updated version comment in go.mod" fi - # Update handler version - if [ -f "internal/handler/covid_handler.go" ]; then - sed -i.bak "s/\"version\":\s*\"[^\"]*\"/\"version\": \"$CLEAN_VERSION\"/" internal/handler/covid_handler.go && rm -f internal/handler/covid_handler.go.bak - echo "โœ… Updated internal/handler/covid_handler.go to $CLEAN_VERSION" + # Update version.go file if it exists + if [ -f "internal/version/version.go" ]; then + sed -i 's|const Version = ".*"|const Version = "'"$CLEAN_VERSION"'"|' internal/version/version.go + echo "โœ… Updated version.go" fi - # Install swag and regenerate docs - go install github.com/swaggo/swag/cmd/swag@latest - export PATH=$PATH:$(go env GOPATH)/bin - swag init -g cmd/main.go -o ./docs - echo "โœ… Documentation regenerated" + - name: Create required labels if they don't exist (develop bump) + if: steps.check_prs.outputs.skip_bump == 'false' && steps.current_dev_version.outputs.skip_update == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "๐Ÿท๏ธ Ensuring required labels exist for version bump PR..." + + # Create labels if they don't exist + gh label create "chore" --description "Maintenance and chore tasks" --color "0e8a16" || echo "Label 'chore' already exists" + gh label create "auto-generated" --description "Automatically generated by GitHub Actions" --color "bfdadc" || echo "Label 'auto-generated' already exists" + gh label create "version-bump" --description "Version bump changes" --color "0052cc" || echo "Label 'version-bump' already exists" + + echo "โœ… Label creation completed" - name: Create version bump PR + if: steps.check_prs.outputs.skip_bump == 'false' && steps.current_dev_version.outputs.skip_update == 'false' env: GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} run: | RELEASE_VERSION="${{ steps.next_version.outputs.current_version }}" NEXT_VERSION="${{ steps.next_version.outputs.next_version }}" - DEV_VERSION="${{ steps.next_version.outputs.next_dev_version }}" - # Create PR branch - PR_BRANCH="chore/bump-version-to-$NEXT_VERSION-dev" + # Create PR branch with timestamp to ensure uniqueness + TIMESTAMP=$(date +%Y%m%d-%H%M%S) + BASE_PR_BRANCH="chore/bump-version-to-$NEXT_VERSION-dev" + + if git ls-remote --heads origin "$BASE_PR_BRANCH" | grep -q "$BASE_PR_BRANCH"; then + PR_BRANCH="${BASE_PR_BRANCH}-${TIMESTAMP}" + echo "โš ๏ธ Base branch exists, using unique name: $PR_BRANCH" + else + PR_BRANCH="$BASE_PR_BRANCH" + echo "โœ… Using base branch name: $PR_BRANCH" + fi + git checkout -b "$PR_BRANCH" # Add and commit changes git add . - git commit -m "chore: bump version to $NEXT_VERSION for next development cycle - - Following release branch creation for $RELEASE_VERSION, updating develop - branch to target the next minor version $NEXT_VERSION. - - Changes: - - Update version annotations to ${{ steps.next_version.outputs.clean_next_version }} - - Regenerate API documentation - - Prepare for next development cycle - - This maintains the Git Flow pattern where develop always contains - the next planned version." + 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 project 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 pull request - gh pr create \ - --base develop \ - --head "$PR_BRANCH" \ - --title "chore: bump version to $NEXT_VERSION for next development cycle" \ - --body "$(cat <<'EOF' - ## Summary + # 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**: $DEV_VERSION - - **Next Release Target**: $NEXT_VERSION + - **Next Development Version**: $NEXT_VERSION ## Changes Made - - ๐Ÿ“ Updated version in source files to ${{ steps.next_version.outputs.clean_next_version }} - - ๐Ÿ“š Regenerated API documentation + - ๐Ÿ“ Updated version in project files to ${{ steps.next_version.outputs.clean_next_version }} - ๐ŸŽฏ Prepared develop branch for next development cycle ## Git Flow Pattern @@ -601,31 +466,38 @@ jobs: ## Auto-generated This PR was automatically created when the release branch was created. - **Safe to merge** - contains only version bumps and documentation updates. - EOF - )" \ + **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 + if: always() 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**: ${{ steps.next_version.outputs.next_dev_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 source files" >> $GITHUB_STEP_SUMMARY - echo "- ๐Ÿ“š Regenerated API documentation" >> $GITHUB_STEP_SUMMARY - echo "- ๐Ÿ”„ Created PR to merge version bump" >> $GITHUB_STEP_SUMMARY - echo "" >> $GITHUB_STEP_SUMMARY - echo "### ๐ŸŽฏ Next Steps" >> $GITHUB_STEP_SUMMARY - echo "1. Review and merge the version bump PR" >> $GITHUB_STEP_SUMMARY - echo "2. Continue development on develop branch" >> $GITHUB_STEP_SUMMARY - echo "3. All new features will target $NEXT_VERSION" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.check_prs.outputs.skip_bump }}" == "true" ]]; then + echo "**Status**: โš ๏ธ Skipped - existing version bump PR found" >> $GITHUB_STEP_SUMMARY + elif [[ "${{ steps.current_dev_version.outputs.skip_update }}" == "true" ]]; then + echo "**Status**: โš ๏ธ Skipped - develop already has target or higher version" >> $GITHUB_STEP_SUMMARY + echo "**Current Version**: ${{ steps.current_dev_version.outputs.current_dev_version }}" >> $GITHUB_STEP_SUMMARY + else + RELEASE_VERSION="${{ steps.next_version.outputs.current_version }}" + NEXT_VERSION="${{ steps.next_version.outputs.next_version }}" + 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 project files" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿ”„ Created PR to merge version bump" >> $GITHUB_STEP_SUMMARY + fi \ No newline at end of file From 2b756609e3cb5701496ca699af64a1d22f083f36 Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 15 Sep 2025 18:27:43 +0700 Subject: [PATCH 3/5] fix: resolve workflow duplicates and conflicts MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit โœ… Fixed all duplicate jobs and workflow conflicts: - Removed duplicate deployment logic from release-workflow.yml - Enhanced deploy.yml with proper trigger conditions - Fixed duplicate GitHub release creation - Added missing newlines to all workflow files - Ensured clear separation of concerns: - ci.yml: Development quality gates - release-branch-creation.yml: Release preparation - release-workflow.yml: Git Flow orchestration - deploy.yml: Production deployment Perfect Git Flow automation: 1. Release branch created โ†’ Generate changelog, create PRs 2. Release merged to main โ†’ Create tag, setup back-merge 3. Tag created โ†’ Deploy to production, create GitHub release No more duplicates, conflicts, or circular dependencies! --- .github/workflows/ci.yml | 2 +- .github/workflows/deploy.yml | 8 +++++-- .github/workflows/release-branch-creation.yml | 2 +- .github/workflows/release-workflow.yml | 22 +++++++++---------- 4 files changed, 18 insertions(+), 16 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dbc742b..882637e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -162,4 +162,4 @@ jobs: - name: Verify binary run: | file pico-api-go - echo "Binary size: $(du -h pico-api-go | cut -f1)" \ No newline at end of file + echo "Binary size: $(du -h pico-api-go | cut -f1)" diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 5f3401a..ad7bc48 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -269,7 +269,7 @@ jobs: create-release: runs-on: ubuntu-latest needs: build-and-deploy - if: needs.build-and-deploy.result == 'success' + if: needs.build-and-deploy.result == 'success' && github.event_name == 'push' permissions: contents: write @@ -282,7 +282,11 @@ jobs: - name: Get version and release info id: release_info run: | - VERSION=${GITHUB_REF#refs/tags/} + if [ "${{ github.event_name }}" = "workflow_dispatch" ] && [ -n "${{ github.event.inputs.tag }}" ]; then + VERSION="${{ github.event.inputs.tag }}" + else + VERSION=${GITHUB_REF#refs/tags/} + fi echo "version=$VERSION" >> $GITHUB_OUTPUT # Check if this is a hotfix or regular release diff --git a/.github/workflows/release-branch-creation.yml b/.github/workflows/release-branch-creation.yml index dd12031..339da20 100644 --- a/.github/workflows/release-branch-creation.yml +++ b/.github/workflows/release-branch-creation.yml @@ -500,4 +500,4 @@ jobs: echo "- ๐ŸŽฏ Calculated next minor version: $NEXT_VERSION" >> $GITHUB_STEP_SUMMARY echo "- ๐Ÿ“ Updated develop branch project files" >> $GITHUB_STEP_SUMMARY echo "- ๐Ÿ”„ Created PR to merge version bump" >> $GITHUB_STEP_SUMMARY - fi \ No newline at end of file + fi diff --git a/.github/workflows/release-workflow.yml b/.github/workflows/release-workflow.yml index e18fec4..ef99ae3 100644 --- a/.github/workflows/release-workflow.yml +++ b/.github/workflows/release-workflow.yml @@ -368,16 +368,14 @@ jobs: echo "โœ… Branch cleanup completed" - # STEP 4: WAIT FOR DEPLOYMENT - - name: Wait for deployment workflow + # STEP 4: LOG DEPLOYMENT TRIGGER + - name: Log deployment trigger if: steps.check_tag.outputs.tag_exists == 'false' run: | VERSION="${{ steps.version.outputs.version }}" - - echo "โณ Waiting for deployment workflow to start..." - sleep 10 - - echo "๐Ÿš€ Deployment workflow should now be running for tag $VERSION" + + echo "โœ… Tag $VERSION created successfully" + echo "๐Ÿš€ Deployment workflow will be automatically triggered by tag creation" echo "Monitor progress at: ${{ github.server_url }}/${{ github.repository }}/actions" # STEP 5: UPDATE ORIGINAL PR WITH STATUS @@ -419,14 +417,14 @@ jobs: - **Deployment**: ๐Ÿš€ [View Progress](${{ github.server_url }}/${{ github.repository }}/actions) ## ๐Ÿ”„ Deployment Pipeline - The deployment workflow is now running and will: + The deployment workflow will be automatically triggered and will: 1. Build the application - 2. Deploy to production + 2. Deploy to production 3. Run health checks 4. Create GitHub release ## ๐ŸŽ‰ Git Flow Complete - Your release is now deployed! Monitor the deployment progress and merge the back-merge PR when ready." + Your release tag has been created and deployment triggered! Monitor the deployment progress and merge the back-merge PR when ready." fi - name: Comment on original PR about back-merge @@ -469,10 +467,10 @@ jobs: 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 + echo "- โš ๏ธ Tag \`$VERSION\` already existed (no deployment trigger)" >> $GITHUB_STEP_SUMMARY else echo "- ๐Ÿท๏ธ Created and pushed tag \`$VERSION\`" >> $GITHUB_STEP_SUMMARY - echo "- ๐Ÿš€ Triggered deployment workflow" >> $GITHUB_STEP_SUMMARY + echo "- ๐Ÿš€ Deployment workflow will be triggered automatically by tag" >> $GITHUB_STEP_SUMMARY fi if [[ "$BASE_BRANCH_EXISTS" == "true" ]]; then From 3a68d85496d8bf3fbfd3ea44fae5dfc515f1b21c Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 15 Sep 2025 18:36:36 +0700 Subject: [PATCH 4/5] fix: implement config-based version management system - Fix Go version inconsistency in release-branch-creation.yml (1.21 -> 1.25.x) - Add version_files configuration to .version-config.yml - Create scripts/update-version.sh for automated version updates - Replace hardcoded file updates with configuration-driven approach - Support pattern-based version replacement with {version} and {major} placeholders --- .github/workflows/release-branch-creation.yml | 33 ++--- .version-config.yml | 25 +++- scripts/update-version.sh | 129 ++++++++++++++++++ 3 files changed, 159 insertions(+), 28 deletions(-) create mode 100755 scripts/update-version.sh diff --git a/.github/workflows/release-branch-creation.yml b/.github/workflows/release-branch-creation.yml index 339da20..f194d9d 100644 --- a/.github/workflows/release-branch-creation.yml +++ b/.github/workflows/release-branch-creation.yml @@ -7,6 +7,9 @@ on: - "release/**" - "hotfix/**" +env: + GO_VERSION: '1.25.x' + jobs: release-branch-setup: if: github.event_name == 'create' && github.event.ref_type == 'branch' && (startsWith(github.event.ref, 'release/') || startsWith(github.event.ref, 'hotfix/')) @@ -60,7 +63,7 @@ jobs: - name: Setup Go for changelog generation uses: actions/setup-go@v4 with: - go-version: '1.21' + go-version: ${{ env.GO_VERSION }} - name: Generate changelog using Go script id: changelog @@ -96,33 +99,13 @@ jobs: echo "changelog_updated=$CHANGELOG_STATUS" >> $GITHUB_OUTPUT - - name: Update version in go.mod and other files + - name: Update version in project files run: | VERSION="${{ steps.version_info.outputs.clean_version }}" - echo "๐Ÿ“ Updating version to $VERSION in project files..." - - # Update version in go.mod if needed (for version comments) - if [ -f "go.mod" ]; then - # Add version comment to go.mod if not already present - if ! grep -q "// Version: " go.mod; then - sed -i '1 a\// Version: '"$VERSION" go.mod - else - sed -i 's|// Version: .*|// Version: '"$VERSION"'|' go.mod - fi - echo "โœ… Updated version comment in go.mod" - fi + echo "๐Ÿ“ Updating version to $VERSION using configuration..." - # Update version.go file if it exists - if [ -f "internal/version/version.go" ]; then - sed -i 's|const Version = ".*"|const Version = "'"$VERSION"'"|' internal/version/version.go - echo "โœ… Updated version.go" - fi - - # Update Dockerfile if it exists - if [ -f "Dockerfile" ]; then - sed -i 's|LABEL version=".*"|LABEL version="'"$VERSION"'"|' Dockerfile - echo "โœ… Updated Dockerfile version label" - fi + # Use the update-version script to update files based on .version-config.yml + ./scripts/update-version.sh "$VERSION" - name: Create preparation PR branch id: pr_branch diff --git a/.version-config.yml b/.version-config.yml index 6489b9e..518ece9 100644 --- a/.version-config.yml +++ b/.version-config.yml @@ -41,17 +41,36 @@ develop_branch: # If manual strategy, specify next target version manual_next_version: "" -# Release Process Configuration +# Release Process Configuration release_process: # Automatically create changelog when release branch is created auto_changelog: true - + # Include breaking change detection in changelog detect_breaking_changes: true - + # Require confirmation for major version releases require_major_confirmation: true +# Version File Management +# Specify which files contain version information that should be updated +version_files: + - path: "cmd/main.go" + pattern: '@version\s+[\d\.]+' + replacement: '@version\t\t{version}' + description: "Swagger API version annotation" + + - path: "internal/handler/covid_handler.go" + pattern: '"version":\s*"[^"]*"' + replacement: '"version": "{version}"' + description: "Health endpoint version" + + - path: "go.mod" + pattern: '^module\s+.*/v\d+' + replacement: 'module pico-api-go/v{major}' + description: "Go module version (major only)" + when: "major_version_only" + # Examples of usage: # # For planning a major version: diff --git a/scripts/update-version.sh b/scripts/update-version.sh new file mode 100755 index 0000000..94a0be6 --- /dev/null +++ b/scripts/update-version.sh @@ -0,0 +1,129 @@ +#!/bin/bash +# Version Update Script +# Reads .version-config.yml and updates version in specified files + +set -e + +VERSION="$1" +CONFIG_FILE="${2:-.version-config.yml}" + +if [ -z "$VERSION" ]; then + echo "Usage: $0 [config-file]" + echo "Example: $0 1.2.3" + exit 1 +fi + +# Remove 'v' prefix if present +CLEAN_VERSION=$(echo "$VERSION" | sed 's/^v//') +MAJOR_VERSION=$(echo "$CLEAN_VERSION" | cut -d. -f1) + +echo "๐Ÿ”„ Updating version to $CLEAN_VERSION using config: $CONFIG_FILE" + +# Function to process file updates +process_file_update() { + if [ -z "$CURRENT_FILE" ] || [ -z "$CURRENT_PATTERN" ] || [ -z "$CURRENT_REPLACEMENT" ]; then + return + fi + + # Check if file exists + if [ ! -f "$CURRENT_FILE" ]; then + echo "โš ๏ธ File $CURRENT_FILE not found, skipping" + return + fi + + # Check 'when' condition + if [ -n "$CURRENT_WHEN" ]; then + if [[ "$CURRENT_WHEN" == "major_version_only" ]]; then + # Only update for major version changes + # This is a simple check - in real implementation you'd compare with previous version + echo "โ„น๏ธ Skipping $CURRENT_FILE (major version only)" + return + fi + fi + + # Prepare replacement string + REPLACEMENT="$CURRENT_REPLACEMENT" + REPLACEMENT="${REPLACEMENT//\{version\}/$CLEAN_VERSION}" + REPLACEMENT="${REPLACEMENT//\{major\}/$MAJOR_VERSION}" + + echo "๐Ÿ”„ Updating $CURRENT_FILE..." + echo " Pattern: $CURRENT_PATTERN" + echo " Replacement: $REPLACEMENT" + + # Use perl for more reliable regex replacement + if command -v perl >/dev/null 2>&1; then + perl -i -pe "s|$CURRENT_PATTERN|$REPLACEMENT|g" "$CURRENT_FILE" + else + # Fallback to sed (less reliable for complex patterns) + sed -i "s|$CURRENT_PATTERN|$REPLACEMENT|g" "$CURRENT_FILE" + fi + + echo "โœ… Updated $CURRENT_FILE" +} + +if [ ! -f "$CONFIG_FILE" ]; then + echo "โš ๏ธ Config file $CONFIG_FILE not found, using default file updates" + + # Fallback to hardcoded updates if config doesn't exist + if [ -f "cmd/main.go" ]; then + sed -i "s/@version.*/@version\t\t$CLEAN_VERSION/" cmd/main.go + echo "โœ… Updated cmd/main.go" + fi + + if [ -f "internal/handler/covid_handler.go" ]; then + sed -i "s/\"version\":\s*\"[^\"]*\"/\"version\": \"$CLEAN_VERSION\"/" internal/handler/covid_handler.go + echo "โœ… Updated internal/handler/covid_handler.go" + fi + + exit 0 +fi + +# Read version_files from YAML config +# This is a simple YAML parser for the version_files section +IN_VERSION_FILES=false +CURRENT_FILE="" +CURRENT_PATTERN="" +CURRENT_REPLACEMENT="" +CURRENT_WHEN="" + +while IFS= read -r line; do + # Check if we're entering the version_files section + if [[ "$line" =~ ^version_files: ]]; then + IN_VERSION_FILES=true + continue + fi + + # Check if we're leaving the version_files section + if [[ "$IN_VERSION_FILES" == true && "$line" =~ ^[a-zA-Z] ]]; then + IN_VERSION_FILES=false + break + fi + + if [[ "$IN_VERSION_FILES" == true ]]; then + # Parse YAML entries + if [[ "$line" =~ ^[[:space:]]*-[[:space:]]*path:[[:space:]]*\"(.*)\" ]]; then + # Process previous file if we have one + if [ -n "$CURRENT_FILE" ]; then + process_file_update + fi + + CURRENT_FILE="${BASH_REMATCH[1]}" + CURRENT_PATTERN="" + CURRENT_REPLACEMENT="" + CURRENT_WHEN="" + elif [[ "$line" =~ ^[[:space:]]*pattern:[[:space:]]*\"(.*)\" ]] || [[ "$line" =~ ^[[:space:]]*pattern:[[:space:]]*\'(.*)\' ]]; then + CURRENT_PATTERN="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^[[:space:]]*replacement:[[:space:]]*\"(.*)\" ]] || [[ "$line" =~ ^[[:space:]]*replacement:[[:space:]]*\'(.*)\' ]]; then + CURRENT_REPLACEMENT="${BASH_REMATCH[1]}" + elif [[ "$line" =~ ^[[:space:]]*when:[[:space:]]*\"(.*)\" ]] || [[ "$line" =~ ^[[:space:]]*when:[[:space:]]*\'(.*)\' ]]; then + CURRENT_WHEN="${BASH_REMATCH[1]}" + fi + fi +done < "$CONFIG_FILE" + +# Process the last file +if [ -n "$CURRENT_FILE" ]; then + process_file_update +fi + +echo "โœ… Version update completed!" \ No newline at end of file From cf94c807fe32b6970da58e527dbb68285628a3df Mon Sep 17 00:00:00 2001 From: Fajrian Aidil Pratama Date: Mon, 15 Sep 2025 18:39:12 +0700 Subject: [PATCH 5/5] feat: enhance release workflow with swagger regeneration and script organization - Add swagger regeneration step after version updates - Move generate-changelog.rb to scripts/ directory for better organization - Update workflow to use Ruby script instead of Go script - Ensure swagger docs reflect version changes automatically - Maintain script organization consistency across project --- .github/workflows/release-branch-creation.yml | 26 ++++++++++++++----- .../generate-changelog.rb | 0 2 files changed, 19 insertions(+), 7 deletions(-) rename generate-changelog.rb => scripts/generate-changelog.rb (100%) diff --git a/.github/workflows/release-branch-creation.yml b/.github/workflows/release-branch-creation.yml index f194d9d..bc828ec 100644 --- a/.github/workflows/release-branch-creation.yml +++ b/.github/workflows/release-branch-creation.yml @@ -65,23 +65,21 @@ jobs: with: go-version: ${{ env.GO_VERSION }} - - name: Generate changelog using Go script + - name: Generate changelog using Ruby script id: changelog run: | VERSION="${{ steps.version_info.outputs.version }}" echo "๐Ÿš€ Generating changelog for $VERSION..." - # Make the script executable if it exists - if [ -f "scripts/generate-changelog.go" ]; then - chmod +x scripts/generate-changelog.go - + # Check if Ruby script exists + if [ -f "scripts/generate-changelog.rb" ]; then # Run changelog generation - if go run scripts/generate-changelog.go --version "$VERSION" --force 2>&1; then + 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 Go script" + echo "โš ๏ธ CHANGELOG.md was not modified by Ruby script" CHANGELOG_STATUS="false" else echo "โœ… CHANGELOG.md was updated" @@ -107,6 +105,20 @@ jobs: # Use the update-version script to update files based on .version-config.yml ./scripts/update-version.sh "$VERSION" + - name: Regenerate Swagger documentation + run: | + echo "๐Ÿ“š Regenerating Swagger documentation after version update..." + + # Install swag tool + go install github.com/swaggo/swag/cmd/swag@latest + + # Generate documentation + swag init -g cmd/main.go -o ./docs --outputTypes go,json,yaml + + # Verify generated files + echo "โœ… Updated Swagger documentation files:" + ls -la docs/ + - name: Create preparation PR branch id: pr_branch run: | diff --git a/generate-changelog.rb b/scripts/generate-changelog.rb similarity index 100% rename from generate-changelog.rb rename to scripts/generate-changelog.rb