diff --git a/.github/workflows/deploy.yml b/.github/workflows/deploy.yml index 180afda..bea8625 100644 --- a/.github/workflows/deploy.yml +++ b/.github/workflows/deploy.yml @@ -256,7 +256,7 @@ jobs: create-release: runs-on: ubuntu-latest needs: build-and-deploy - if: needs.build-and-deploy.result == 'success' && github.event_name == 'push' + if: needs.build-and-deploy.result == 'success' permissions: contents: write diff --git a/.github/workflows/release-branch-creation.yml b/.github/workflows/release-branch-creation.yml index bc828ec..b704101 100644 --- a/.github/workflows/release-branch-creation.yml +++ b/.github/workflows/release-branch-creation.yml @@ -263,7 +263,173 @@ jobs: echo "- ℹ️ No changes needed (already up to date)" >> $GITHUB_STEP_SUMMARY fi - # JOB 2: Bump develop branch version (only for releases, not hotfixes) + # JOB 2: Create PR from release/hotfix branch to main + create-main-pr: + if: github.event_name == 'create' && github.event.ref_type == 'branch' && (startsWith(github.event.ref, 'release/') || startsWith(github.event.ref, 'hotfix/')) + needs: release-branch-setup + runs-on: ubuntu-latest + permissions: + contents: read + pull-requests: write + + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Analyze branch and version + id: version_info + run: | + BRANCH_NAME="${{ github.event.ref }}" + echo "branch_name=$BRANCH_NAME" >> $GITHUB_OUTPUT + + if [[ $BRANCH_NAME == release/* ]]; then + VERSION=$(echo $BRANCH_NAME | sed 's/release\///') + TYPE="release" + PR_TITLE="release: $VERSION" + elif [[ $BRANCH_NAME == hotfix/* ]]; then + VERSION=$(echo $BRANCH_NAME | sed 's/hotfix\///') + TYPE="hotfix" + PR_TITLE="fix: $VERSION" + fi + + # Ensure version starts with 'v' + if [[ ! $VERSION == v* ]]; then + VERSION="v$VERSION" + fi + + echo "version=$VERSION" >> $GITHUB_OUTPUT + echo "type=$TYPE" >> $GITHUB_OUTPUT + echo "pr_title=$PR_TITLE" >> $GITHUB_OUTPUT + echo "clean_version=$(echo $VERSION | sed 's/^v//')" >> $GITHUB_OUTPUT + + - name: Check for existing PR to main + id: check_existing_pr + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH_NAME="${{ steps.version_info.outputs.branch_name }}" + + # Check if there's already a PR from this branch to main + EXISTING_PRS=$(gh pr list --head "$BRANCH_NAME" --base main --state open --json number,title) + + if [ "$(echo "$EXISTING_PRS" | jq '. | length')" -gt 0 ]; then + echo "⚠️ Found existing PR from $BRANCH_NAME to main:" + echo "$EXISTING_PRS" | jq -r '.[] | "#\(.number): \(.title)"' + echo "skip_pr=true" >> $GITHUB_OUTPUT + + PR_NUMBER=$(echo "$EXISTING_PRS" | jq -r '.[0].number') + echo "existing_pr_number=$PR_NUMBER" >> $GITHUB_OUTPUT + else + echo "✅ No existing PR found from $BRANCH_NAME to main" + echo "skip_pr=false" >> $GITHUB_OUTPUT + fi + + - name: Create required labels if they don't exist + if: steps.check_existing_pr.outputs.skip_pr == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + echo "🏷️ Ensuring required labels exist..." + + TYPE="${{ steps.version_info.outputs.type }}" + + # Create labels if they don't exist + gh label create "auto-generated" --description "Automatically generated by GitHub Actions" --color "bfdadc" || echo "Label 'auto-generated' already exists" + gh label create "$TYPE" --description "$TYPE branch related" --color "d73a4a" || echo "Label '$TYPE' already exists" + gh label create "ready-to-merge" --description "Ready to be merged" --color "0e8a16" || echo "Label 'ready-to-merge' already exists" + + echo "✅ Label creation completed" + + - name: Create PR to main branch + if: steps.check_existing_pr.outputs.skip_pr == 'false' + env: + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + BRANCH_NAME="${{ steps.version_info.outputs.branch_name }}" + VERSION="${{ steps.version_info.outputs.version }}" + TYPE="${{ steps.version_info.outputs.type }}" + PR_TITLE="${{ steps.version_info.outputs.pr_title }}" + CLEAN_VERSION="${{ steps.version_info.outputs.clean_version }}" + + # Build PR body + if [[ "$TYPE" == "release" ]]; then + PR_BODY="## Summary + $TYPE $VERSION ready for merge to main branch. + + ## Changes + - New features and improvements from develop branch + - Version bumped to $CLEAN_VERSION + - Updated changelog and documentation + - All tests passing and code reviewed + + ## Deployment + This $TYPE will be deployed to production after merge. + + ## Checklist + - [ ] All preparation tasks completed + - [ ] Tests are passing + - [ ] Documentation updated + - [ ] Ready for production deployment" + else + PR_BODY="## Summary + $TYPE $VERSION ready for merge to main branch. + + ## Changes + - Critical bug fixes + - Version bumped to $CLEAN_VERSION + - Updated changelog + - Hotfix tested and verified + + ## Deployment + This $TYPE will be deployed to production immediately after merge. + + ## Checklist + - [ ] Hotfix verified and tested + - [ ] Tests are passing + - [ ] Ready for immediate production deployment" + fi + + # Create PR to main + gh pr create \ + --base main \ + --head "$BRANCH_NAME" \ + --title "$PR_TITLE" \ + --body "$PR_BODY" \ + --label "auto-generated" \ + --label "$TYPE" \ + --label "ready-to-merge" + + echo "✅ Created PR from $BRANCH_NAME to main" + + - name: Create main PR summary + run: | + BRANCH_NAME="${{ steps.version_info.outputs.branch_name }}" + VERSION="${{ steps.version_info.outputs.version }}" + TYPE="${{ steps.version_info.outputs.type }}" + + echo "## 🎯 Main Branch PR" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "**Branch**: \`$BRANCH_NAME\`" >> $GITHUB_STEP_SUMMARY + echo "**Target**: main" >> $GITHUB_STEP_SUMMARY + echo "**Version**: $VERSION" >> $GITHUB_STEP_SUMMARY + echo "**Type**: $TYPE" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + + if [[ "${{ steps.check_existing_pr.outputs.skip_pr }}" == "true" ]]; then + echo "**Status**: ⚠️ Skipped - PR already exists (#${{ steps.check_existing_pr.outputs.existing_pr_number }})" >> $GITHUB_STEP_SUMMARY + else + echo "**Status**: ✅ PR created successfully" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "### Actions Completed" >> $GITHUB_STEP_SUMMARY + echo "- 🎯 Created PR from \`$BRANCH_NAME\` to \`main\`" >> $GITHUB_STEP_SUMMARY + echo "- 🏷️ Applied appropriate labels" >> $GITHUB_STEP_SUMMARY + echo "- 📋 Added $TYPE checklist to PR description" >> $GITHUB_STEP_SUMMARY + fi + + # JOB 3: Bump develop branch version (only for releases, not hotfixes) bump-develop-version: if: github.event_name == 'create' && github.event.ref_type == 'branch' && startsWith(github.event.ref, 'release/') needs: release-branch-setup diff --git a/.version-config.yml b/.version-config.yml index 518ece9..c945b22 100644 --- a/.version-config.yml +++ b/.version-config.yml @@ -56,7 +56,7 @@ release_process: # Specify which files contain version information that should be updated version_files: - path: "cmd/main.go" - pattern: '@version\s+[\d\.]+' + pattern: '@version\s+[0-9]+\.[0-9]+\.[0-9]+' replacement: '@version\t\t{version}' description: "Swagger API version annotation" @@ -71,6 +71,16 @@ version_files: description: "Go module version (major only)" when: "major_version_only" + - path: "internal/handler/covid_handler_test.go" + pattern: 'assert\.Equal\(t, "[0-9]+\.[0-9]+\.[0-9]+", [^)]*\["version"\]\)' + replacement: 'assert.Equal(t, "{version}", $1["version"])' + description: "Test version assertions" + + - path: "internal/handler/covid_handler_test.go" + pattern: 'assert\.Equal\(t, "[0-9]+\.[0-9]+\.[0-9]+", data\["version"\]\)' + replacement: 'assert.Equal(t, "{version}", data["version"])' + description: "Health check test version assertion" + # Examples of usage: # # For planning a major version: diff --git a/CHANGELOG.md b/CHANGELOG.md index 8dfb061..e7f2a28 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,34 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 + +## [v2.4.0] - 2025-09-15 + +### Added + +- Configure deploy workflow for minimal production build (6.1mb) ([1d362189](https://github.com/banua-coder/pico-api-go/commit/1d362189e56fe7e7cea6b616593944761d1fdbbd)) +- Optimize binary size with conditional swagger compilation ([64a56304](https://github.com/banua-coder/pico-api-go/commit/64a56304257db60a1e6db0561431701b9ecd57c9)) +- Enhance ci with intelligent testing and coverage thresholds ([3058e376](https://github.com/banua-coder/pico-api-go/commit/3058e37690ea612eaa50aa4fa401cb2b366db931)) +- Enhance release workflow with swagger regeneration and script organization ([cf94c807](https://github.com/banua-coder/pico-api-go/commit/cf94c807fe32b6970da58e527dbb68285628a3df)) +- Simplify changelog generator and remove unnecessary complexity ([fc609bb7](https://github.com/banua-coder/pico-api-go/commit/fc609bb75f535ca6b8962886146eda655c5d894a)) + +### Fixed + +- Exclude test files from golangci-lint to resolve mock interface issues ([d77e2c0c](https://github.com/banua-coder/pico-api-go/commit/d77e2c0cbe5004d33c35fec60f1e16a24983689c)) +- Add golangci-lint configuration to resolve test file issues ([73b1e516](https://github.com/banua-coder/pico-api-go/commit/73b1e516277da92c5b65411f4a1b15b2ad163f81)) +- Explicitly reference embedded db methods to resolve linter issues ([f86a70b1](https://github.com/banua-coder/pico-api-go/commit/f86a70b1736cc654841f4d8c3639296f0bea1b21)) +- Resolve golangci-lint version compatibility issue in ci ([86872eec](https://github.com/banua-coder/pico-api-go/commit/86872eeca96a5e13920181a6dbfaec71ea7dc397)) +- Resolve ci failures - integration tests and code formatting ([1f26b10f](https://github.com/banua-coder/pico-api-go/commit/1f26b10f03fa297c734c27b119db04fbf55e4d51)) +- Remove redundant province data from latest_case in province list api ([00d63ebc](https://github.com/banua-coder/pico-api-go/commit/00d63ebc908a3cfcd2484a6d64aaa1fd4f402a2e)) +- Implement config-based version management system ([3a68d854](https://github.com/banua-coder/pico-api-go/commit/3a68d85496d8bf3fbfd3ea44fae5dfc515f1b21c)) +- Resolve workflow duplicates and conflicts ([2b756609](https://github.com/banua-coder/pico-api-go/commit/2b756609e3cb5701496ca699af64a1d22f083f36)) +- Simplify workflows and restore working deploy.yml ([547f4556](https://github.com/banua-coder/pico-api-go/commit/547f45566a4f89d91ca4b15a595c784d0dbafe83)) +- Fix generate changelog script (script) ([9ab39f0f](https://github.com/banua-coder/pico-api-go/commit/9ab39f0f40b73bad51fbc34b52645c97f2a25839)) + +### Documentation + +- Update readme with latest project structure and ci features ([82cc7c9a](https://github.com/banua-coder/pico-api-go/commit/82cc7c9aa86c4518b15489d15c8a41689835fa25)) + ## [v2.3.0] - 2025-09-08 ### Documentation diff --git a/cmd/main.go b/cmd/main.go index 2e560a2..9980a80 100644 --- a/cmd/main.go +++ b/cmd/main.go @@ -54,7 +54,7 @@ func main() { log.Fatalf("Failed to connect to database: %v", err) } defer func() { - if err := db.Close(); err != nil { + if err := db.DB.Close(); err != nil { log.Printf("Error closing database connection: %v", err) } }() diff --git a/internal/handler/covid_handler.go b/internal/handler/covid_handler.go index 5100ef1..3ec4e47 100644 --- a/internal/handler/covid_handler.go +++ b/internal/handler/covid_handler.go @@ -206,6 +206,7 @@ func (h *CovidHandler) GetProvinceCases(w http.ResponseWriter, r *http.Request) // Parse query parameters limit := utils.ParseIntQueryParam(r, "limit", 50) offset := utils.ParseIntQueryParam(r, "offset", 0) + page := utils.ParseIntQueryParam(r, "page", 0) all := utils.ParseBoolQueryParam(r, "all") startDate := r.URL.Query().Get("start_date") endDate := r.URL.Query().Get("end_date") @@ -213,6 +214,11 @@ func (h *CovidHandler) GetProvinceCases(w http.ResponseWriter, r *http.Request) // Parse sort parameters (default: date ascending) sortParams := utils.ParseSortParam(r, "date") + // Convert page to offset if page is specified (page-based pagination) + if page > 0 { + offset = (page - 1) * limit + } + // Validate pagination params limit, offset = utils.ValidatePaginationParams(limit, offset) diff --git a/internal/repository/national_case_repository.go b/internal/repository/national_case_repository.go index 5ff7963..7d1e4c8 100644 --- a/internal/repository/national_case_repository.go +++ b/internal/repository/national_case_repository.go @@ -3,6 +3,7 @@ package repository import ( "database/sql" "fmt" + "log" "time" "github.com/banua-coder/pico-api-go/internal/models" @@ -48,7 +49,7 @@ func (r *nationalCaseRepository) GetAllSorted(sortParams utils.SortParams) ([]mo } defer func() { if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) + log.Printf("Error closing rows: %v", err) } }() @@ -90,7 +91,7 @@ func (r *nationalCaseRepository) GetByDateRangeSorted(startDate, endDate time.Ti } defer func() { if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) + log.Printf("Error closing rows: %v", err) } }() @@ -135,10 +136,10 @@ func (r *nationalCaseRepository) GetLatest() (*models.NationalCase, error) { } func (r *nationalCaseRepository) GetByDay(day int64) (*models.NationalCase, error) { - query := `SELECT id, day, date, positive, recovered, deceased, + query := `SELECT id, day, date, positive, recovered, deceased, cumulative_positive, cumulative_recovered, cumulative_deceased, - rt, rt_upper, rt_lower - FROM national_cases + rt, rt_upper, rt_lower + FROM national_cases WHERE day = ?` var c models.NationalCase @@ -183,7 +184,7 @@ func (r *nationalCaseRepository) GetAllPaginatedSorted(limit, offset int, sortPa } defer func() { if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) + log.Printf("Error closing rows: %v", err) } }() @@ -235,7 +236,7 @@ func (r *nationalCaseRepository) GetByDateRangePaginatedSorted(startDate, endDat } defer func() { if err := rows.Close(); err != nil { - fmt.Printf("Error closing rows: %v\n", err) + log.Printf("Error closing rows: %v", err) } }() diff --git a/internal/service/covid_service.go b/internal/service/covid_service.go index 0d9ecdb..44f63a6 100644 --- a/internal/service/covid_service.go +++ b/internal/service/covid_service.go @@ -117,6 +117,58 @@ func (s *covidService) GetLatestNationalCase() (*models.NationalCase, error) { return nationalCase, nil } +func (s *covidService) GetNationalCasesPaginated(limit, offset int) ([]models.NationalCase, int, error) { + cases, total, err := s.nationalCaseRepo.GetAllPaginated(limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get paginated national cases: %w", err) + } + return cases, total, nil +} + +func (s *covidService) GetNationalCasesPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + cases, total, err := s.nationalCaseRepo.GetAllPaginatedSorted(limit, offset, sortParams) + if err != nil { + return nil, 0, fmt.Errorf("failed to get paginated sorted national cases: %w", err) + } + return cases, total, nil +} + +func (s *covidService) GetNationalCasesByDateRangePaginated(startDate, endDate string, limit, offset int) ([]models.NationalCase, int, error) { + start, err := time.Parse("2006-01-02", startDate) + if err != nil { + return nil, 0, fmt.Errorf("invalid start date format: %w", err) + } + + end, err := time.Parse("2006-01-02", endDate) + if err != nil { + return nil, 0, fmt.Errorf("invalid end date format: %w", err) + } + + cases, total, err := s.nationalCaseRepo.GetByDateRangePaginated(start, end, limit, offset) + if err != nil { + return nil, 0, fmt.Errorf("failed to get paginated national cases by date range: %w", err) + } + return cases, total, nil +} + +func (s *covidService) GetNationalCasesByDateRangePaginatedSorted(startDate, endDate string, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { + start, err := time.Parse("2006-01-02", startDate) + if err != nil { + return nil, 0, fmt.Errorf("invalid start date format: %w", err) + } + + end, err := time.Parse("2006-01-02", endDate) + if err != nil { + return nil, 0, fmt.Errorf("invalid end date format: %w", err) + } + + cases, total, err := s.nationalCaseRepo.GetByDateRangePaginatedSorted(start, end, limit, offset, sortParams) + if err != nil { + return nil, 0, fmt.Errorf("failed to get paginated sorted national cases by date range: %w", err) + } + return cases, total, nil +} + func (s *covidService) GetProvinces() ([]models.Province, error) { provinces, err := s.provinceRepo.GetAll() if err != nil { @@ -362,55 +414,3 @@ func (s *covidService) GetProvinceCasesByDateRangePaginatedSorted(provinceID, st } return cases, total, nil } - -func (s *covidService) GetNationalCasesPaginated(limit, offset int) ([]models.NationalCase, int, error) { - cases, total, err := s.nationalCaseRepo.GetAllPaginated(limit, offset) - if err != nil { - return nil, 0, fmt.Errorf("failed to get national cases paginated: %w", err) - } - return cases, total, nil -} - -func (s *covidService) GetNationalCasesPaginatedSorted(limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - cases, total, err := s.nationalCaseRepo.GetAllPaginatedSorted(limit, offset, sortParams) - if err != nil { - return nil, 0, fmt.Errorf("failed to get sorted national cases paginated: %w", err) - } - return cases, total, nil -} - -func (s *covidService) GetNationalCasesByDateRangePaginated(startDate, endDate string, limit, offset int) ([]models.NationalCase, int, error) { - start, err := time.Parse("2006-01-02", startDate) - if err != nil { - return nil, 0, fmt.Errorf("invalid start date format: %w", err) - } - - end, err := time.Parse("2006-01-02", endDate) - if err != nil { - return nil, 0, fmt.Errorf("invalid end date format: %w", err) - } - - cases, total, err := s.nationalCaseRepo.GetByDateRangePaginated(start, end, limit, offset) - if err != nil { - return nil, 0, fmt.Errorf("failed to get national cases by date range paginated: %w", err) - } - return cases, total, nil -} - -func (s *covidService) GetNationalCasesByDateRangePaginatedSorted(startDate, endDate string, limit, offset int, sortParams utils.SortParams) ([]models.NationalCase, int, error) { - start, err := time.Parse("2006-01-02", startDate) - if err != nil { - return nil, 0, fmt.Errorf("invalid start date format: %w", err) - } - - end, err := time.Parse("2006-01-02", endDate) - if err != nil { - return nil, 0, fmt.Errorf("invalid end date format: %w", err) - } - - cases, total, err := s.nationalCaseRepo.GetByDateRangePaginatedSorted(start, end, limit, offset, sortParams) - if err != nil { - return nil, 0, fmt.Errorf("failed to get sorted national cases by date range paginated: %w", err) - } - return cases, total, nil -} diff --git a/scripts/update-version.sh b/scripts/update-version.sh index 94a0be6..097dd69 100755 --- a/scripts/update-version.sh +++ b/scripts/update-version.sh @@ -66,7 +66,7 @@ if [ ! -f "$CONFIG_FILE" ]; then # 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 + sed -i "s/@version\s\+[0-9]\+\.[0-9]\+\.[0-9]\+/@version\t\t$CLEAN_VERSION/" cmd/main.go echo "✅ Updated cmd/main.go" fi