fix: add automatic conflict resolution to back-merge workflow #6
Workflow file for this run
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| name: Deploy to Production | |
| on: | |
| push: | |
| tags: | |
| - 'v*.*.*' # Triggers on version tags like v1.0.0, v2.1.3, etc. | |
| env: | |
| GO_VERSION: '1.25.x' | |
| jobs: | |
| build-and-deploy: | |
| runs-on: ubuntu-latest | |
| steps: | |
| - name: Checkout code | |
| uses: actions/checkout@v4 | |
| with: | |
| fetch-depth: 0 # Fetch all history for proper version info | |
| - name: Set up Go | |
| uses: actions/setup-go@v4 | |
| with: | |
| go-version: ${{ env.GO_VERSION }} | |
| - name: Get version from tag | |
| run: | | |
| echo "VERSION=${GITHUB_REF#refs/tags/}" >> $GITHUB_ENV | |
| echo "Deploying version: ${GITHUB_REF#refs/tags/}" | |
| - name: Build binary for Linux | |
| run: | | |
| echo "Building ${{ secrets.BINARY_NAME }} for Linux..." | |
| CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-w -s" -o ${{ secrets.BINARY_NAME }} ./cmd/main.go | |
| ls -la ${{ secrets.BINARY_NAME }} | |
| - name: Verify binary | |
| run: | | |
| file ${{ secrets.BINARY_NAME }} | |
| echo "Binary size: $(du -h ${{ secrets.BINARY_NAME }} | cut -f1)" | |
| - name: Generate Swagger documentation | |
| run: | | |
| echo "📚 Generating Swagger documentation..." | |
| # Install swag tool | |
| go install github.com/swaggo/swag/cmd/swag@latest | |
| # Generate documentation including HTML | |
| swag init -g cmd/main.go -o ./docs --outputTypes go,json,yaml,html | |
| # Verify generated files | |
| echo "Generated documentation files:" | |
| ls -la docs/ | |
| # Check if HTML was generated | |
| if [ -f "docs/swagger.html" ]; then | |
| echo "✅ HTML documentation generated successfully" | |
| else | |
| echo "⚠️ HTML documentation not found, continuing without it" | |
| fi | |
| - name: Setup SSH Agent | |
| uses: webfactory/[email protected] | |
| 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: | | |
| 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 | |
| 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 | |
| run: | | |
| echo "🚀 Starting deployment of ${{ env.VERSION }} to production..." | |
| # 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 }} | |
| # 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 | |
| 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 | |
| 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 | |
| echo "✅ YAML documentation uploaded" | |
| fi | |
| # Execute deployment script on remote server | |
| ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' | |
| set -e | |
| DEPLOY_PATH="${{ secrets.DEPLOY_PATH }}" | |
| BINARY_NAME="${{ secrets.BINARY_NAME }}" | |
| VERSION="${{ env.VERSION }}" | |
| TEMP_BINARY="/tmp/${BINARY_NAME}-${VERSION}" | |
| echo "🔍 Checking deployment environment..." | |
| cd "$DEPLOY_PATH" | |
| pwd | |
| ls -la | |
| echo "⏹️ Stopping application..." | |
| if [ -f "./stop.sh" ]; then | |
| ./stop.sh | |
| echo "✅ Application stopped successfully" | |
| else | |
| echo "⚠️ stop.sh not found, skipping stop step" | |
| fi | |
| echo "💾 Backing up current binary..." | |
| if [ -f "$BINARY_NAME" ]; then | |
| cp "$BINARY_NAME" "${BINARY_NAME}.backup.$(date +%Y%m%d_%H%M%S)" | |
| echo "✅ Backup created" | |
| fi | |
| echo "🔄 Replacing binary..." | |
| mv "$TEMP_BINARY" "$BINARY_NAME" | |
| chmod +x "$BINARY_NAME" | |
| echo "✅ Binary replaced and made executable" | |
| echo "📚 Updating documentation..." | |
| # Create docs directory if it doesn't exist | |
| mkdir -p docs | |
| # Move documentation files if they exist | |
| if [ -f "/tmp/swagger-${VERSION}.html" ]; then | |
| mv "/tmp/swagger-${VERSION}.html" "docs/swagger.html" | |
| echo "✅ HTML documentation updated" | |
| fi | |
| if [ -f "/tmp/swagger-${VERSION}.json" ]; then | |
| mv "/tmp/swagger-${VERSION}.json" "docs/swagger.json" | |
| echo "✅ JSON documentation updated" | |
| fi | |
| if [ -f "/tmp/swagger-${VERSION}.yaml" ]; then | |
| mv "/tmp/swagger-${VERSION}.yaml" "docs/swagger.yaml" | |
| echo "✅ YAML documentation updated" | |
| fi | |
| echo "🚀 Starting application..." | |
| if [ -f "./start.sh" ]; then | |
| ./start.sh | |
| echo "✅ Application started successfully" | |
| else | |
| echo "⚠️ start.sh not found, skipping start step" | |
| fi | |
| echo "🎉 Initial deployment of ${VERSION} completed!" | |
| EOF | |
| - name: Health check and rollback management | |
| id: health_check | |
| run: | | |
| echo "🔍 Performing comprehensive health check..." | |
| # Wait for service to fully start | |
| echo "⏳ Waiting 10 seconds for service to initialize..." | |
| sleep 10 | |
| HEALTH_CHECK_PASSED="false" | |
| MAX_RETRIES=3 | |
| RETRY_COUNT=0 | |
| # Retry health check up to MAX_RETRIES times | |
| while [ $RETRY_COUNT -lt $MAX_RETRIES ]; do | |
| RETRY_COUNT=$((RETRY_COUNT + 1)) | |
| echo "🔍 Health check attempt $RETRY_COUNT of $MAX_RETRIES..." | |
| if [ -n "${{ secrets.HEALTH_CHECK_URL }}" ]; then | |
| # Perform detailed health check | |
| RESPONSE=$(curl -s -w "HTTPSTATUS:%{http_code}" "${{ secrets.HEALTH_CHECK_URL }}" || echo "HTTPSTATUS:000") | |
| HTTP_STATUS=$(echo $RESPONSE | grep -o 'HTTPSTATUS:[0-9]*' | cut -d: -f2) | |
| RESPONSE_BODY=$(echo $RESPONSE | sed 's/HTTPSTATUS:[0-9]*$//') | |
| echo "HTTP Status: $HTTP_STATUS" | |
| echo "Response: $RESPONSE_BODY" | |
| if [ "$HTTP_STATUS" = "200" ]; then | |
| # Additional validation - check if response contains expected data | |
| if echo "$RESPONSE_BODY" | grep -q '"status".*"success"'; then | |
| echo "✅ Health check passed - API is responding correctly" | |
| HEALTH_CHECK_PASSED="true" | |
| break | |
| else | |
| echo "⚠️ Health check failed - API responding but with unexpected data" | |
| fi | |
| else | |
| echo "⚠️ Health check failed - HTTP $HTTP_STATUS" | |
| fi | |
| else | |
| echo "⚠️ No health check URL configured - cannot verify deployment" | |
| # Without health check URL, assume success (maintain backward compatibility) | |
| HEALTH_CHECK_PASSED="true" | |
| break | |
| fi | |
| if [ $RETRY_COUNT -lt $MAX_RETRIES ]; then | |
| echo "⏳ Waiting 5 seconds before retry..." | |
| sleep 5 | |
| fi | |
| done | |
| echo "health_check_passed=$HEALTH_CHECK_PASSED" >> $GITHUB_OUTPUT | |
| # Handle rollback if health check failed | |
| if [ "$HEALTH_CHECK_PASSED" = "false" ]; then | |
| echo "❌ Health check failed after $MAX_RETRIES attempts - initiating rollback..." | |
| ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' | |
| set -e | |
| DEPLOY_PATH="${{ secrets.DEPLOY_PATH }}" | |
| BINARY_NAME="${{ secrets.BINARY_NAME }}" | |
| VERSION="${{ env.VERSION }}" | |
| echo "🔄 Rolling back to previous version..." | |
| cd "$DEPLOY_PATH" | |
| # Stop the failed application | |
| if [ -f "./stop.sh" ]; then | |
| ./stop.sh || echo "⚠️ Stop script failed, but continuing rollback" | |
| fi | |
| # Find the most recent backup | |
| BACKUP_FILE=$(ls -t ${BINARY_NAME}.backup.* 2>/dev/null | head -n1) | |
| if [ -n "$BACKUP_FILE" ] && [ -f "$BACKUP_FILE" ]; then | |
| echo "📦 Restoring from backup: $BACKUP_FILE" | |
| cp "$BACKUP_FILE" "$BINARY_NAME" | |
| chmod +x "$BINARY_NAME" | |
| echo "✅ Binary restored from backup" | |
| # Restart with previous version | |
| if [ -f "./start.sh" ]; then | |
| ./start.sh | |
| echo "✅ Previous version restarted" | |
| fi | |
| echo "🔄 Rollback completed successfully" | |
| else | |
| echo "❌ No backup file found - cannot rollback automatically" | |
| echo "Manual intervention required!" | |
| exit 1 | |
| fi | |
| EOF | |
| echo "❌ Deployment failed and rolled back" | |
| exit 1 | |
| else | |
| echo "✅ Health check passed - proceeding with cleanup" | |
| fi | |
| - name: Cleanup backup files | |
| if: steps.health_check.outputs.health_check_passed == 'true' | |
| run: | | |
| echo "🧹 Cleaning up backup files after successful deployment..." | |
| ssh -p ${{ secrets.DEPLOY_PORT }} ${{ secrets.DEPLOY_USER }}@${{ secrets.DEPLOY_HOST }} << 'EOF' | |
| DEPLOY_PATH="${{ secrets.DEPLOY_PATH }}" | |
| BINARY_NAME="${{ secrets.BINARY_NAME }}" | |
| cd "$DEPLOY_PATH" | |
| # Remove backup files (keep only the most recent one as safety measure) | |
| BACKUP_FILES=$(ls -t ${BINARY_NAME}.backup.* 2>/dev/null || true) | |
| BACKUP_COUNT=$(echo "$BACKUP_FILES" | wc -l) | |
| if [ "$BACKUP_COUNT" -gt 1 ]; then | |
| # Keep the most recent backup, remove others | |
| echo "$BACKUP_FILES" | tail -n +2 | while read backup_file; do | |
| if [ -n "$backup_file" ] && [ -f "$backup_file" ]; then | |
| rm "$backup_file" | |
| echo "🗑️ Removed old backup: $backup_file" | |
| fi | |
| done | |
| echo "✅ Cleaned up old backups (kept most recent for safety)" | |
| else | |
| echo "ℹ️ Only one or no backup files found, no cleanup needed" | |
| fi | |
| EOF | |
| - name: Create deployment summary | |
| run: | | |
| if [[ "${{ steps.health_check.outputs.health_check_passed }}" == "true" ]]; then | |
| echo "## 🚀 Deployment Summary" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: ${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Binary**: ${{ secrets.BINARY_NAME }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Target**: ${{ secrets.DEPLOY_PATH }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Status**: ✅ Deployed successfully" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Health Check**: ✅ Passed" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Documentation**: 📚 Updated" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Completed Actions" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Binary deployed and started" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Documentation updated (HTML, JSON, YAML)" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Health check verification passed" >> $GITHUB_STEP_SUMMARY | |
| echo "- ✅ Old backups cleaned up" >> $GITHUB_STEP_SUMMARY | |
| else | |
| echo "## ❌ Deployment Failed" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Version**: ${{ env.VERSION }}" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Status**: ❌ Failed and rolled back" >> $GITHUB_STEP_SUMMARY | |
| echo "- **Health Check**: ❌ Failed" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### What Happened" >> $GITHUB_STEP_SUMMARY | |
| echo "- ⚠️ Deployment completed but health check failed" >> $GITHUB_STEP_SUMMARY | |
| echo "- 🔄 Automatic rollback to previous version executed" >> $GITHUB_STEP_SUMMARY | |
| echo "- 🛡️ Service restored to last known good state" >> $GITHUB_STEP_SUMMARY | |
| echo "" >> $GITHUB_STEP_SUMMARY | |
| echo "### Next Steps" >> $GITHUB_STEP_SUMMARY | |
| echo "1. 🔍 Check application logs for errors" >> $GITHUB_STEP_SUMMARY | |
| echo "2. 🐛 Debug the issue in the new version" >> $GITHUB_STEP_SUMMARY | |
| echo "3. 🔧 Fix the problem and redeploy" >> $GITHUB_STEP_SUMMARY | |
| fi | |
| 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 | |
| # Check if this is a hotfix or regular release | |
| if [[ $VERSION == *"-hotfix"* ]] || git log --oneline --grep="hotfix" -1 | grep -q "hotfix"; then | |
| echo "type=hotfix" >> $GITHUB_OUTPUT | |
| else | |
| echo "type=release" >> $GITHUB_OUTPUT | |
| fi | |
| # Get the previous tag for changelog range | |
| 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 }} | |
| TYPE=${{ steps.release_info.outputs.type }} | |
| # Create release notes file | |
| cat > release_notes.md << 'EOF' | |
| ## 🚀 Release ${{ steps.release_info.outputs.version }} | |
| **Release Type**: ${{ steps.release_info.outputs.type }} | |
| **Deployment**: ✅ Successfully deployed to production | |
| ### What's Changed | |
| EOF | |
| # Get commits since last tag with better formatting | |
| if [ -n "$PREVIOUS_TAG" ]; then | |
| echo "Changes since $PREVIOUS_TAG:" >> release_notes.md | |
| echo "" >> release_notes.md | |
| # Get commits with type categorization | |
| git log --pretty=format:"- %s" "${PREVIOUS_TAG}..${VERSION}" | \ | |
| grep -v "Merge branch\|Merge pull request" | \ | |
| head -20 >> release_notes.md | |
| else | |
| echo "- Initial release" >> release_notes.md | |
| fi | |
| # Add deployment information | |
| cat >> release_notes.md << 'EOF' | |
| ### 📋 Deployment Details | |
| - **Binary**: Built with Go ${{ env.GO_VERSION }} | |
| - **Platform**: Linux AMD64 | |
| - **Deployment Time**: $(date -u '+%Y-%m-%d %H:%M:%S UTC') | |
| - **Health Check**: ${{ secrets.HEALTH_CHECK_URL || 'Manual verification required' }} | |
| ### 🔗 Links | |
| - [API Documentation](https://pico-api.banuacoder.com/swagger/index.html) | |
| - [Repository](https://github.com/banua-coder/pico-api-go) | |
| - [Health Check](https://pico-api.banuacoder.com/api/v1/health) | |
| EOF | |
| # Set multiline output for GitHub Actions | |
| echo 'RELEASE_NOTES<<EOF' >> $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 }} | |
| TYPE=${{ steps.release_info.outputs.type }} | |
| # Determine if this is a prerelease | |
| if [[ $VERSION == *"rc"* ]] || [[ $VERSION == *"beta"* ]] || [[ $VERSION == *"alpha"* ]]; then | |
| PRERELEASE="--prerelease" | |
| else | |
| PRERELEASE="" | |
| fi | |
| # Create the release | |
| gh release create "$VERSION" \ | |
| --title "🚀 $VERSION" \ | |
| --notes "${{ steps.release_notes.outputs.RELEASE_NOTES }}" \ | |
| $PRERELEASE \ | |
| --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 "✅ Deployment successful for ${{ github.ref_name }}" | |
| if [ "$RELEASE_STATUS" == "success" ]; then | |
| echo "✅ GitHub release created successfully" | |
| else | |
| echo "⚠️ GitHub release creation failed, but deployment succeeded" | |
| fi | |
| else | |
| echo "❌ Deployment failed for ${{ github.ref_name }}" | |
| exit 1 | |
| fi |