diff --git a/.editorconfig b/.editorconfig new file mode 100644 index 0000000..7ab6677 --- /dev/null +++ b/.editorconfig @@ -0,0 +1,143 @@ +# EditorConfig helps maintain consistent coding styles across various editors and IDEs +# See https://editorconfig.org for more details + +# top-most EditorConfig file +root = true + +# All files +[*] +charset = utf-8 +end_of_line = lf +indent_style = space +indent_size = 2 +tab_width = 2 +insert_final_newline = true +trim_trailing_whitespace = true +max_line_length = 120 + +# Kotlin files - Official Kotlin Style Guide with 2-space tabs +[{*.kt,*.kts}] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +max_line_length = 120 +ij_kotlin_code_style_defaults = KOTLIN_OFFICIAL + +# Basic formatting +ij_kotlin_keep_blank_lines_in_code = 2 +ij_kotlin_keep_blank_lines_in_declarations = 2 +ij_kotlin_keep_blank_lines_before_right_brace = 2 +ij_kotlin_blank_lines_after_class_header = 0 +ij_kotlin_blank_lines_around_block_when_branches = 0 +ij_kotlin_blank_lines_before_declaration_with_comment_or_annotation_on_separate_line = 1 + +# Imports - No star imports for better consistency and performance +ij_kotlin_imports_layout = *, java.**, javax.**, kotlin.**, ^ +ij_kotlin_name_count_to_use_star_import = 999 +ij_kotlin_name_count_to_use_star_import_for_members = 999 +ij_kotlin_packages_to_use_import_on_demand = + +# Alignment and wrapping +ij_kotlin_align_multiline_parameters = true +ij_kotlin_align_multiline_parameters_in_calls = false +ij_kotlin_continuation_indent_for_chained_calls = true +ij_kotlin_continuation_indent_for_expression_bodies = true +ij_kotlin_wrap_first_method_in_call_chain = false + +# Method parameters +ij_kotlin_method_parameters_wrap = on_every_item +ij_kotlin_method_parameters_new_line_after_left_paren = false +ij_kotlin_method_parameters_right_paren_on_new_line = false + +# Call parameters +ij_kotlin_call_parameters_wrap = on_every_item +ij_kotlin_call_parameters_new_line_after_left_paren = false +ij_kotlin_call_parameters_right_paren_on_new_line = false + +# Annotations +ij_kotlin_class_annotation_wrap = split_into_lines +ij_kotlin_method_annotation_wrap = split_into_lines +ij_kotlin_field_annotation_wrap = split_into_lines +ij_kotlin_parameter_annotation_wrap = off +ij_kotlin_variable_annotation_wrap = off + +# Spaces +ij_kotlin_space_after_comma = true +ij_kotlin_space_after_extend_colon = true +ij_kotlin_space_after_type_colon = true +ij_kotlin_space_before_comma = false +ij_kotlin_space_before_extend_colon = true +ij_kotlin_space_before_type_colon = false +ij_kotlin_space_before_lambda_arrow = true +ij_kotlin_spaces_around_additive_operators = true +ij_kotlin_spaces_around_assignment_operators = true +ij_kotlin_spaces_around_equality_operators = true +ij_kotlin_spaces_around_function_type_arrow = true +ij_kotlin_spaces_around_logical_operators = true +ij_kotlin_spaces_around_multiplicative_operators = true +ij_kotlin_spaces_around_relational_operators = true +ij_kotlin_spaces_around_unary_operator = false +ij_kotlin_spaces_around_when_arrow = true + +# Control flow +ij_kotlin_space_before_if_parentheses = true +ij_kotlin_space_before_for_parentheses = true +ij_kotlin_space_before_while_parentheses = true +ij_kotlin_space_before_catch_parentheses = true +ij_kotlin_space_before_when_parentheses = true + +# Other formatting +ij_kotlin_allow_trailing_comma = true +ij_kotlin_allow_trailing_comma_on_call_site = true +ij_kotlin_insert_whitespaces_in_simple_one_line_method = true +ij_kotlin_keep_line_breaks = true +ij_kotlin_lbrace_on_next_line = false + +# Java files +[*.java] +indent_size = 2 +tab_width = 2 +ij_continuation_indent_size = 4 +max_line_length = 120 +# Java imports - No star imports +ij_java_class_count_to_use_import_on_demand = 999 +ij_java_names_count_to_use_import_on_demand = 999 +ij_java_packages_to_use_import_on_demand = + +# Gradle files +[{*.gradle,*.gradle.kts}] +indent_size = 2 +tab_width = 2 + +# YAML files +[*.{yml,yaml}] +indent_size = 2 +tab_width = 2 + +# JSON files +[{*.json,*.jsonc}] +indent_size = 2 +tab_width = 2 + +# XML files +[*.xml] +indent_size = 2 +tab_width = 2 + +# Properties files +[*.properties] +indent_size = 2 +tab_width = 2 + +# Markdown files +[*.md] +trim_trailing_whitespace = false +insert_final_newline = true +indent_size = 2 +tab_width = 2 + +# Shell scripts +[{*.sh,*.bash}] +indent_size = 2 +tab_width = 2 +end_of_line = lf diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..8af972c --- /dev/null +++ b/.gitattributes @@ -0,0 +1,3 @@ +/gradlew text eol=lf +*.bat text eol=crlf +*.jar binary diff --git a/.github/CODEOWNERS b/.github/CODEOWNERS new file mode 100644 index 0000000..970b6bf --- /dev/null +++ b/.github/CODEOWNERS @@ -0,0 +1,20 @@ +# Global code owners - require review from these users for all changes +* @programmer-newbie-code + +# Core library components - require additional scrutiny +/core-module/ @programmer-newbie-code +/scripts/ @programmer-newbie-code + +# Configuration files - require admin review +/.github/ @programmer-newbie-code +/gradle.properties @programmer-newbie-code +/settings.gradle @programmer-newbie-code +/build.gradle @programmer-newbie-code + +# Documentation - can be reviewed by any maintainer +/docs/ @programmer-newbie-code +*.md @programmer-newbie-code + +# Security-related files +/SECURITY.md @programmer-newbie-code +/.github/workflows/ @programmer-newbie-code diff --git a/.github/ISSUE_TEMPLATE/bug_report.md b/.github/ISSUE_TEMPLATE/bug_report.md new file mode 100644 index 0000000..f6ca197 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/bug_report.md @@ -0,0 +1,35 @@ +--- +name: Bug report +about: Create a report to help us improve +title: '[BUG] Brief description' +labels: 'bug' +assignees: '' + +--- + +## Describe the bug + +A clear and concise description of what the bug is. + +## To Reproduce + +Steps to reproduce the behavior: + +1. Use component '...' +2. Configure with '...' +3. Execute '...' +4. See error + +## Expected behavior + +A clear and concise description of what you expected to happen. + +## Environment: + +- Java version: [e.g. 21] +- Spring Boot version: [e.g. 3.5.3] +- Library version: [e.g. 1.0.0] + +## Additional context + +Add any other context about the problem here. diff --git a/.github/ISSUE_TEMPLATE/feature_request.md b/.github/ISSUE_TEMPLATE/feature_request.md new file mode 100644 index 0000000..8e52294 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/feature_request.md @@ -0,0 +1,24 @@ +--- +name: Feature request +about: Suggest an idea for this project +title: '[FEATURE] Brief description' +labels: 'enhancement' +assignees: '' + +--- + +## Is your feature request related to a problem? Please describe. + +A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] + +## Describe the solution you'd like + +A clear and concise description of what you want to happen. + +## Describe alternatives you've considered + +A clear and concise description of any alternative solutions or features you've considered. + +## Additional context + +Add any other context or screenshots about the feature request here. diff --git a/.github/ISSUE_TEMPLATE/performance_issue.md b/.github/ISSUE_TEMPLATE/performance_issue.md new file mode 100644 index 0000000..d319b18 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/performance_issue.md @@ -0,0 +1,53 @@ +--- +name: Performance Issue +about: Report performance problems or optimization opportunities +title: '[PERFORMANCE] Brief description' +labels: 'performance' +assignees: '' + +--- + +## Performance Issue Description + + + +## Environment + +- **Java version**: [e.g. 21] +- **Spring Boot version**: [e.g. 3.5.3] +- **Library version**: [e.g. 1.0.0] +- **Cache implementation**: [e.g. Redis, Caffeine] +- **Hardware specs**: [e.g. CPU, RAM, Storage type] + +## Current Behavior + + + +## Expected Performance + + + +## Reproduction Steps + +1. Configure cache with: '...' +2. Execute operation: '...' +3. Measure performance using: '...' +4. Observe results: '...' + +## Performance Measurements + + + +``` +Metric: [e.g. throughput, latency, memory usage] +Current: [measurement] +Expected: [measurement] +``` + +## Potential Solutions + + + +## Additional Context + + diff --git a/.github/ISSUE_TEMPLATE/question.md b/.github/ISSUE_TEMPLATE/question.md new file mode 100644 index 0000000..12b7ba3 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/question.md @@ -0,0 +1,34 @@ +--- +name: Question +about: Ask a question about usage, implementation, or project direction +title: '[QUESTION] Brief description' +labels: 'question' +assignees: '' + +--- + +## Question + + + +## Context + + + +## What I've Tried + + + +- [ ] Checked the documentation +- [ ] Searched existing issues +- [ ] Reviewed the code examples + +## Environment (if relevant) + +- **Java version**: [e.g. 21] +- **Spring Boot version**: [e.g. 3.5.3] +- **Library version**: [e.g. 1.0.0] + +## Additional Information + + diff --git a/.github/ISSUE_TEMPLATE/security_report.md b/.github/ISSUE_TEMPLATE/security_report.md new file mode 100644 index 0000000..a0e9832 --- /dev/null +++ b/.github/ISSUE_TEMPLATE/security_report.md @@ -0,0 +1,49 @@ +--- +name: Security Vulnerability Report +about: Report a security vulnerability (Please use private disclosure) +title: '[SECURITY] Brief description' +labels: 'security, vulnerability' +assignees: '' + +--- + +**⚠️ IMPORTANT: For security vulnerabilities, please use GitHub's private vulnerability disclosure instead of public +issues.** + +## Security Issue Description + + + +## Impact Assessment + + + +- [ ] Low impact (minimal risk) +- [ ] Medium impact (moderate risk) +- [ ] High impact (significant risk) +- [ ] Critical impact (severe risk) + +## Affected Components + + + +- [ ] bulk-core +- [ ] bulk-redis +- [ ] bulk-caffeine +- [ ] bulk-spring +- [ ] Configuration/Build system + +## Steps to Reproduce + + + +## Proposed Solution + + + +## Additional Information + + + +--- +**Note**: We take security seriously. Please follow responsible disclosure practices. diff --git a/.github/branch-protection.yml b/.github/branch-protection.yml new file mode 100644 index 0000000..3ef0e6a --- /dev/null +++ b/.github/branch-protection.yml @@ -0,0 +1,47 @@ +name: Branch Protection Rules + +on: + # Disabled automatic triggers to prevent API permission issues + # when pushing directly to main branch + # push: + # branches: + # - main + workflow_dispatch: # Allow manual triggering only + +jobs: + setup-branch-protection: + runs-on: ubuntu-latest + permissions: + contents: read + administration: write + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set Branch Protection + uses: octokit/request-action@v2.x + with: + route: PUT /repos/{owner}/{repo}/branches/main/protection + owner: ${{ github.repository_owner }} + repo: ${{ github.event.repository.name }} + required_status_checks: | + { + "strict": true, + "contexts": ["build-and-test"] + } + enforce_admins: false + required_pull_request_reviews: | + { + "dismissal_restrictions": {}, + "dismiss_stale_reviews": true, + "require_code_owner_reviews": true, + "required_approving_review_count": 1, + "require_last_push_approval": true + } + restrictions: null + required_signatures: true + required_linear_history: true + allow_force_pushes: false + allow_deletions: false + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md new file mode 100644 index 0000000..306000f --- /dev/null +++ b/.github/pull_request_template.md @@ -0,0 +1,58 @@ +# Pull Request + +> ⚠️ **IMPORTANT**: PR title must follow conventional commit format: +> `(): ` +> +> **Examples**: +> - `feat(core): add bulk fetch operation` +> - `fix(redis): resolve connection leak issue` +> - `docs: update API documentation` +> +> **Types**: `feat`, `fix`, `docs`, `style`, `refactor`, `perf`, `test`, `chore`, `ci`, `revert` +> See [CONTRIBUTING.md](../CONTRIBUTING.md#commit-message-and-pr-title-conventions) for details. + +## Description + + + +## Related Issue + + +Fixes # (issue) + +## Type of Change + + + +- [ ] Bug fix (non-breaking change which fixes an issue) +- [ ] New feature (non-breaking change which adds functionality) +- [ ] Breaking change (fix or feature that would cause existing functionality to not work as expected) +- [ ] Documentation update +- [ ] Code refactoring +- [ ] Build/dependency changes + +## How Has This Been Tested? + + + +- [ ] Unit tests +- [ ] Integration tests +- [ ] Manual testing + +## Checklist + + + +- [ ] My code follows the style guidelines of this project +- [ ] I have performed a self-review of my own code +- [ ] I have commented my code, particularly in hard-to-understand areas +- [ ] I have made corresponding changes to the documentation +- [ ] My changes generate no new warnings +- [ ] I have added tests that prove my fix is effective or that my feature works +- [ ] New and existing unit tests pass locally with my changes +- [ ] Any dependent changes have been merged and published +- [ ] I have signed all my commits with my GPG key + +## Additional Context + + diff --git a/.github/template-config.yml b/.github/template-config.yml new file mode 100644 index 0000000..7c8df99 --- /dev/null +++ b/.github/template-config.yml @@ -0,0 +1,58 @@ +# GitHub Repository Template Configuration + +# Template Metadata +template_name: "Kotlin Multimodule Template" +template_description: "A production-ready Kotlin multimodule template for Spring Boot applications with 85% test coverage, TestNG groups, and Spring-Kotlin integration" + +# Files to process during template creation +template_files: + - "**/*.kt" + - "**/*.gradle" + - "**/*.md" + - "settings.gradle" + - "gradle.properties" + +# Template variables that will be replaced +template_variables: + # Package naming + TEMPLATE_PACKAGE: "io.programmernewbie.template" + TEMPLATE_GROUP_ID: "io.programmernewbie.template" + TEMPLATE_ARTIFACT_PREFIX: "kotlin-multimodule-template" + + # Project naming + TEMPLATE_PROJECT_NAME: "kotlin-multimodule-template" + TEMPLATE_MAIN_CLASS: "KotlinMultimoduleTemplateApplication" + + # Organization + TEMPLATE_ORG: "programmer-newbie-code" + TEMPLATE_GITHUB_URL: "https://github.com/programmer-newbie-code/kotlin-multimodule-template" + +# Instructions for template users +setup_instructions: | + After creating a repository from this template: + + 1. Run the customization script: + - Linux/Mac: ./customize.sh + - Windows: ./customize.ps1 + + 2. Or manually update: + - Replace package names in all .kt files + - Update project name in settings.gradle + - Update group ID in build.gradle + - Update GitHub URLs for publishing + + 3. Test your setup: + - ./gradlew build + - ./gradlew test -PtestGroups=small + +# Features included in this template +features: + - "Kotlin multimodule architecture (2 modules)" + - "Spring Boot with Spring-Kotlin integration (allopen plugin)" + - "TestNG with groups (small, medium, integration)" + - "85% minimum test coverage with JaCoCo" + - "Dependency locking for reproducible builds" + - "GitHub Actions CI/CD ready" + - "Comprehensive documentation" + - "Code quality tools (OWASP, license reporting)" + - "Customization scripts for easy setup" diff --git a/.github/workflows/build-test-publish.yml b/.github/workflows/build-test-publish.yml new file mode 100644 index 0000000..3644c93 --- /dev/null +++ b/.github/workflows/build-test-publish.yml @@ -0,0 +1,385 @@ +name: Build, Test, and Publish + +on: + push: + branches: [ main ] + tags: + - 'v*' + pull_request: + branches: [ main ] + +jobs: + build-and-test: + runs-on: ubuntu-latest + permissions: + contents: read + packages: write + checks: write + pull-requests: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # Required for code coverage reporting + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Validate Gradle wrapper + uses: gradle/wrapper-validation-action@v2 + continue-on-error: true # Don't fail the entire build if validation times out + timeout-minutes: 2 # Set a reasonable timeout + + - name: Fallback wrapper validation + if: failure() + run: | + echo "⚠️ Gradle wrapper validation timed out, performing local validation" + # Basic validation - check if wrapper files exist and are executable + if [ -f "./gradlew" ] && [ -x "./gradlew" ]; then + echo "✅ Gradle wrapper executable found" + else + echo "❌ Gradle wrapper not found or not executable" + exit 1 + fi + + if [ -f "gradle/wrapper/gradle-wrapper.jar" ]; then + echo "✅ Gradle wrapper JAR found" + else + echo "❌ Gradle wrapper JAR not found" + exit 1 + fi + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Build with Gradle + run: ./gradlew build + + - name: Run tests with coverage + run: ./gradlew test jacocoRootReport + + - name: Check if coverage report exists + id: check_coverage + run: | + # Look for the aggregate JaCoCo CSV report in the root project + if [ -f "build/reports/jacoco/test/jacocoTestReport.csv" ]; then + echo "coverage_report=build/reports/jacoco/test/jacocoTestReport.csv" >> $GITHUB_OUTPUT + echo "coverage_exists=true" >> $GITHUB_OUTPUT + echo "✅ Found aggregate coverage report: build/reports/jacoco/test/jacocoTestReport.csv" + else + echo "coverage_exists=false" >> $GITHUB_OUTPUT + echo "⚠️ No aggregate JaCoCo coverage report found" + echo "Looking for any CSV reports..." + find . -name "jacocoTestReport.csv" -type f || echo "No CSV reports found anywhere" + fi + + - name: Upload build artifacts + uses: actions/upload-artifact@v4 + with: + name: jar-files + path: | + bulk-*/build/libs/*.jar + !bulk-*/build/libs/*-plain.jar + retention-days: 7 + + - name: Upload coverage report + uses: actions/upload-artifact@v4 + with: + name: coverage-report + path: | + **/build/reports/jacoco/ + **/build/reports/tests/ + retention-days: 7 + + # Create badges directory to ensure it exists + - name: Create badges directory + run: mkdir -p .github/badges + + - name: Generate JaCoCo Badge + if: steps.check_coverage.outputs.coverage_exists == 'true' + id: jacoco + uses: cicirello/jacoco-badge-generator@v2 + with: + generate-branches-badge: false + jacoco-csv-file: ${{ steps.check_coverage.outputs.coverage_report }} + badges-directory: .github/badges + coverage-badge-filename: jacoco.svg + fail-if-coverage-less-than: 85 + + - name: Log coverage percentage + if: steps.check_coverage.outputs.coverage_exists == 'true' + run: | + echo "Coverage percentage: ${{ steps.jacoco.outputs.coverage }}%" + echo "Branch coverage: ${{ steps.jacoco.outputs.branches }}%" + + # Convert decimal coverage values to percentages for display + - name: Convert coverage values + if: steps.check_coverage.outputs.coverage_exists == 'true' + id: coverage_conversion + run: | + # JaCoCo badge generator outputs as decimal (0.95 = 95%) + COVERAGE_RAW="${{ steps.jacoco.outputs.coverage }}" + BRANCH_RAW="${{ steps.jacoco.outputs.branches }}" + + # Convert to percentage if value is less than 1 (decimal format) + if (( $(echo "$COVERAGE_RAW < 1" | bc -l) )); then + COVERAGE_PCT=$(echo "$COVERAGE_RAW * 100" | bc -l | xargs printf "%.2f\n") + else + COVERAGE_PCT=$COVERAGE_RAW + fi + + if (( $(echo "$BRANCH_RAW < 1" | bc -l) )); then + BRANCH_PCT=$(echo "$BRANCH_RAW * 100" | bc -l | xargs printf "%.2f\n") + else + BRANCH_PCT=$BRANCH_RAW + fi + + echo "coverage_display=${COVERAGE_PCT}" >> $GITHUB_OUTPUT + + # Set boolean flag for pass/fail status (only coverage, no branch coverage) + COVERAGE_PASS=$(echo "$COVERAGE_PCT >= 85" | bc -l) + + echo "coverage_pass=${COVERAGE_PASS}" >> $GITHUB_OUTPUT + + echo "Converted coverage values:" + echo " Overall: ${COVERAGE_PCT}% (pass: ${COVERAGE_PASS})" + + - name: Comment coverage on PR + if: github.event_name == 'pull_request' && steps.check_coverage.outputs.coverage_exists == 'true' + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: coverage-report + message: | + ## 📊 Coverage Report + + | Metric | Coverage | Status | + |--------|----------|--------| + | **Overall Coverage** | ${{ steps.coverage_conversion.outputs.coverage_display }}% | ${{ steps.coverage_conversion.outputs.coverage_pass == '1' && '✅ PASS' || '❌ FAIL' }} | + + ### Requirements + - ✅ **Minimum Overall Coverage:** 85% + + ${{ steps.coverage_conversion.outputs.coverage_pass == '0' && format('{0}', '> [!WARNING] + > **Coverage Below Threshold!** + > + > Your overall code coverage ({0}%) is below the required 85% minimum. + > Please add more tests to increase coverage before this PR can be merged. + > + > **Need help?** Check our [testing guidelines](../CONTRIBUTING.md#testing-requirements) for best practices.', steps.coverage_conversion.outputs.coverage_display) || '> [!NOTE] + > **Coverage Requirements Met!** ✅ + > + > Great job maintaining good test coverage!' }} + + ### 📈 Coverage Details + You can download the full coverage report from the **Artifacts** section of this workflow run. + + - name: Comment when no coverage report exists + if: github.event_name == 'pull_request' && steps.check_coverage.outputs.coverage_exists == 'false' + uses: marocchino/sticky-pull-request-comment@v2 + with: + header: coverage-report + message: | + ## 📊 Coverage Report + + ⚠️ **No coverage report found** + + It appears that no JaCoCo coverage report was generated for this PR. This could happen if: + - No tests were run + - JaCoCo is not properly configured + - Tests exist but coverage reporting is disabled + + Please ensure: + 1. Tests are written and executed: `./gradlew test` + 2. Coverage reports are generated: `./gradlew jacocoTestReport` + 3. Check the build logs for any errors + + > **Note**: All PRs require minimum 85% code coverage to be merged. + + - name: Verify minimum coverage threshold + if: steps.check_coverage.outputs.coverage_exists == 'true' + run: ./gradlew jacocoRootCoverageVerification + + # Add a step that explicitly fails the build if coverage is too low (for branch protection) + - name: Check coverage threshold for PR + if: github.event_name == 'pull_request' && steps.check_coverage.outputs.coverage_exists == 'true' + run: | + COVERAGE=${{ steps.jacoco.outputs.coverage }} + BRANCH_COVERAGE=${{ steps.jacoco.outputs.branches }} + + echo "Checking coverage thresholds..." + echo "Overall coverage: ${COVERAGE}%" + echo "Branch coverage: ${BRANCH_COVERAGE}%" + + # Convert decimal format to percentage if needed + # JaCoCo badge generator outputs as decimal (0.95 = 95%) + if (( $(echo "$COVERAGE < 1" | bc -l) )); then + COVERAGE_PCT=$(echo "$COVERAGE * 100" | bc -l) + echo "Converted coverage from decimal: ${COVERAGE_PCT}%" + else + COVERAGE_PCT=$COVERAGE + fi + + if (( $(echo "$BRANCH_COVERAGE < 1" | bc -l) )); then + BRANCH_COVERAGE_PCT=$(echo "$BRANCH_COVERAGE * 100" | bc -l) + echo "Converted branch coverage from decimal: ${BRANCH_COVERAGE_PCT}%" + else + BRANCH_COVERAGE_PCT=$BRANCH_COVERAGE + fi + + if (( $(echo "$COVERAGE_PCT < 85" | bc -l) )); then + echo "❌ Overall coverage ${COVERAGE_PCT}% is below the required 85% threshold" + exit 1 + fi + + echo "✅ All coverage thresholds met!" + echo " - Overall coverage: ${COVERAGE_PCT}% (≥ 85% ✓)" + + # Fail PR if no coverage report exists (enforce testing) + - name: Enforce coverage requirement for PR + if: github.event_name == 'pull_request' && steps.check_coverage.outputs.coverage_exists == 'false' + run: | + echo "⚠️ No coverage report found - checking if code changes were made..." + + # Check if any Java/Kotlin source files were modified in this PR + # Only enforce coverage for PRs that modify actual source code + CODE_CHANGES=$(git diff --name-only ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }} | grep -E '\.(java|kt)$' | head -10 || echo "") + + if [ -n "$CODE_CHANGES" ]; then + echo "❌ Code changes detected but no coverage report found" + echo "Modified source files:" + echo "$CODE_CHANGES" + echo "" + echo "Coverage reports are required when source code is modified." + echo "Please ensure tests are written and coverage reports are generated:" + echo " ./gradlew test jacocoTestReport" + exit 1 + else + echo "✅ No source code changes detected - coverage report not required" + echo "This PR appears to contain only:" + echo "- Documentation updates" + echo "- Configuration changes" + echo "- CI/CD workflow updates" + echo "- Other non-source files" + echo "" + echo "Coverage validation skipped for non-code changes." + fi + + # Commit the badges to the repository (only on main branch) + - name: Commit and push badges if changed + if: github.ref == 'refs/heads/main' && steps.check_coverage.outputs.coverage_exists == 'true' + uses: EndBug/add-and-commit@v9 + with: + default_author: github_actions + message: 'docs: update code coverage badges [skip ci]' + add: '.github/badges/jacoco.svg' + pull: '--rebase --autostash' # Handle conflicts gracefully + continue-on-error: true # Don't fail the workflow if badge commit fails + + # Development release job - runs when pushing a tag with -dev suffix + dev-release: + needs: build-and-test + if: success() && startsWith(github.ref, 'refs/tags/v') && endsWith(github.ref, '-dev') + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: jar-files + path: artifacts + + - name: Generate Release Version + id: generate_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Create Development Release + id: create_dev_release + uses: softprops/action-gh-release@v1 + with: + name: Development Release ${{ steps.generate_version.outputs.VERSION }} + files: | + artifacts/**/*.jar + body: | + Development build for testing purposes. + This is a prerelease build and not intended for production use. + draft: false + prerelease: true + token: ${{ secrets.GITHUB_TOKEN }} + + # Save artifact to repository (similar to a local Maven repository) + - name: Create Artifact Directory Structure + run: | + mkdir -p .repo/dev-releases/${{ steps.generate_version.outputs.VERSION }} + cp artifacts/**/*.jar .repo/dev-releases/${{ steps.generate_version.outputs.VERSION }}/ + + - name: Commit and Push Dev Artifacts + uses: EndBug/add-and-commit@v9 + with: + add: '.repo/dev-releases/${{ steps.generate_version.outputs.VERSION }}/*' + message: 'Add development artifacts for ${{ steps.generate_version.outputs.VERSION }}' + push: true + + # Production release job - runs when pushing a tag without -dev suffix + publish-release: + needs: build-and-test + if: success() && startsWith(github.ref, 'refs/tags/v') && !endsWith(github.ref, '-dev') + runs-on: ubuntu-latest + permissions: + contents: write + packages: write + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x ./gradlew + + - name: Download artifacts + uses: actions/download-artifact@v4 + with: + name: jar-files + path: artifacts + + - name: Generate Release Version + id: generate_version + run: echo "VERSION=${GITHUB_REF#refs/tags/v}" >> $GITHUB_OUTPUT + + - name: Create Production Release + id: create_release + uses: softprops/action-gh-release@v1 + with: + name: Release ${{ steps.generate_version.outputs.VERSION }} + files: | + artifacts/**/*.jar + draft: false + prerelease: false + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Publish to GitHub Packages + run: | + ./gradlew publish + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/changelog.yml b/.github/workflows/changelog.yml new file mode 100644 index 0000000..1c4f547 --- /dev/null +++ b/.github/workflows/changelog.yml @@ -0,0 +1,75 @@ +name: Generate Changelog + +on: + push: + branches: [ main ] + tags: + - 'v*' + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + changelog: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Check if cliff.toml exists + id: check_config + run: | + if [ -f "cliff.toml" ]; then + echo "config_exists=true" >> $GITHUB_OUTPUT + else + echo "config_exists=false" >> $GITHUB_OUTPUT + echo "⚠️ cliff.toml not found, skipping changelog generation" + fi + + # Use a more recent version or alternative approach + - name: Install git-cliff + if: steps.check_config.outputs.config_exists == 'true' + run: | + # Install git-cliff directly using cargo or from GitHub releases + curl -L https://github.com/orhun/git-cliff/releases/download/v1.4.0/git-cliff-1.4.0-x86_64-unknown-linux-gnu.tar.gz | tar -xz + sudo mv git-cliff-1.4.0/git-cliff /usr/local/bin/ + git-cliff --version + + - name: Generate changelog + if: steps.check_config.outputs.config_exists == 'true' + id: git-cliff + run: | + # Generate changelog using the installed binary + git-cliff --config cliff.toml --verbose --output CHANGELOG.md + + # Set output for use in subsequent steps + if [ -f "CHANGELOG.md" ]; then + echo "content<> $GITHUB_OUTPUT + cat CHANGELOG.md >> $GITHUB_OUTPUT + echo "EOF" >> $GITHUB_OUTPUT + fi + + - name: Commit changelog + if: steps.check_config.outputs.config_exists == 'true' && hashFiles('CHANGELOG.md') != '' + run: | + git config user.name 'github-actions[bot]' + git config user.email 'github-actions[bot]@users.noreply.github.com' + git add CHANGELOG.md + git commit -m "docs: update changelog" || exit 0 + git push + + # Create release notes for tags + - name: Create Release Notes + if: startsWith(github.ref, 'refs/tags/v') && steps.check_config.outputs.config_exists == 'true' + uses: softprops/action-gh-release@v1 + with: + body: ${{ steps.git-cliff.outputs.content }} + draft: false + prerelease: ${{ contains(github.ref, '-dev') }} + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} diff --git a/.github/workflows/dependency-check.yml b/.github/workflows/dependency-check.yml new file mode 100644 index 0000000..3f4b26b --- /dev/null +++ b/.github/workflows/dependency-check.yml @@ -0,0 +1,81 @@ +name: Dependency Security Check + +on: + # Temporarily disabled due to NVD maintenance (July 24, 2025 until 2:00 PM EDT) + # schedule: + # - cron: '0 0 * * 0' # Run weekly on Sunday at midnight + workflow_dispatch: # Allow manual triggering only + # pull_request: + # paths: + # - '**/build.gradle' + # - 'gradle.properties' + # - 'gradle.lockfile' + # - '**/gradle.lockfile' + +permissions: + contents: read + security-events: write + +jobs: + dependency-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + - name: Run OWASP dependency check + run: | + ./gradlew dependencyCheckAggregate --info + + - name: Upload dependency check report + uses: actions/upload-artifact@v4 + with: + name: dependency-check-report + path: build/reports/dependency-check-report.html + retention-days: 30 + + # Fail the build if high/critical vulnerabilities are found + - name: Check for vulnerabilities + run: | + if grep -q "severity.*High\|severity.*Critical" build/reports/dependency-check-report.html; then + echo "❌ High or Critical vulnerabilities found!" + echo "Please review the dependency check report and update vulnerable dependencies." + exit 1 + else + echo "✅ No high or critical vulnerabilities found." + fi + + # Create issue if vulnerabilities found (only on scheduled runs) + - name: Create security issue + if: failure() && github.event_name == 'schedule' + uses: actions/github-script@v7 + with: + script: | + github.rest.issues.create({ + owner: context.repo.owner, + repo: context.repo.repo, + title: '🚨 Security Alert: Vulnerable Dependencies Detected', + body: `## Security Alert + + Our automated security scan has detected vulnerable dependencies in the project. + + **Action Required:** + 1. Review the [dependency check report](https://github.com/${context.repo.owner}/${context.repo.repo}/actions/runs/${context.runId}) + 2. Update vulnerable dependencies + 3. Regenerate lock files: \`./gradlew resolveAndLockAll --write-locks\` + + **Priority:** High/Critical vulnerabilities require immediate attention. + + This issue was automatically created by the security scan workflow.`, + labels: ['security', 'dependencies', 'high-priority'] + }) diff --git a/.github/workflows/dependency-updates.yml b/.github/workflows/dependency-updates.yml new file mode 100644 index 0000000..2f3882f --- /dev/null +++ b/.github/workflows/dependency-updates.yml @@ -0,0 +1,88 @@ +name: Update Dependencies + +on: + schedule: + - cron: '0 3 * * 1' # Run weekly on Monday at 3 AM + workflow_dispatch: + +permissions: + contents: write + pull-requests: write + +jobs: + update-dependencies: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + token: ${{ secrets.GITHUB_TOKEN }} + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Update Gradle wrapper + - name: Update Gradle Wrapper + run: ./gradlew wrapper --gradle-version=latest + + # Check for dependency updates + - name: Check for dependency updates + run: ./gradlew dependencyUpdates + + # Update lock files with latest versions + - name: Update dependency lock files + run: ./gradlew resolveAndLockAll --write-locks + + # Check if there are changes + - name: Check for changes + id: changes + run: | + if [[ -n $(git status --porcelain) ]]; then + echo "changes=true" >> $GITHUB_OUTPUT + echo "Changes detected in dependencies" + else + echo "changes=false" >> $GITHUB_OUTPUT + echo "No dependency changes" + fi + + # Create PR for dependency updates + - name: Create Pull Request + if: steps.changes.outputs.changes == 'true' + uses: peter-evans/create-pull-request@v5 + with: + token: ${{ secrets.GITHUB_TOKEN }} + commit-message: 'chore(deps): update dependencies and lock files' + title: '🔄 Automated Dependency Updates' + body: | + ## Automated Dependency Updates + + This PR contains automated dependency updates: + + - ⬆️ Updated Gradle wrapper (if newer version available) + - 🔒 Regenerated dependency lock files + - 📦 Updated to latest compatible versions + + ### What's Changed + - Dependency versions updated to latest compatible releases + - Lock files regenerated to ensure reproducible builds + + ### Testing + - [ ] All tests pass + - [ ] Security scan passes + - [ ] No breaking changes detected + + > 🤖 This PR was automatically created by the dependency update workflow. + > Please review the changes and ensure all tests pass before merging. + branch: automated/dependency-updates + delete-branch: true + labels: | + dependencies + automated + chore diff --git a/.github/workflows/license-compliance.yml b/.github/workflows/license-compliance.yml new file mode 100644 index 0000000..194db3d --- /dev/null +++ b/.github/workflows/license-compliance.yml @@ -0,0 +1,70 @@ +name: License and Compliance Check + +on: + push: + branches: [ main ] + pull_request: + branches: [ main ] + schedule: + - cron: '0 2 * * 1' # Run weekly on Monday at 2 AM + +permissions: + contents: read + security-events: write + +jobs: + license-check: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Check for license compliance + - name: Generate license report + run: ./gradlew generateLicenseReport + + - name: Upload license report + uses: actions/upload-artifact@v4 + with: + name: license-report + path: build/reports/dependency-license/ + retention-days: 30 + + # Check for restrictive licenses + - name: Check for restrictive licenses + run: | + # List of restrictive licenses to avoid + RESTRICTIVE_LICENSES=("GPL-2.0" "GPL-3.0" "AGPL-3.0" "LGPL-2.1" "LGPL-3.0" "CDDL-1.0" "EPL-1.0" "EPL-2.0") + + if [ -f "build/reports/dependency-license/dependency-license.json" ]; then + for license in "${RESTRICTIVE_LICENSES[@]}"; do + if grep -q "$license" build/reports/dependency-license/dependency-license.json; then + echo "❌ Found restrictive license: $license" + echo "Please review dependencies with restrictive licenses" + exit 1 + fi + done + echo "✅ No restrictive licenses found" + else + echo "⚠️ License report not found, skipping check" + fi + + # Scan for secrets in new commits + - name: Scan for secrets + uses: trufflesecurity/trufflehog@main + continue-on-error: true # Don't fail if scanning issues occur + with: + path: ./ + base: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.sha || (github.event.before != '0000000000000000000000000000000000000000' && github.event.before || 'HEAD~1') }} + head: HEAD + extra_args: --debug --only-verified diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml new file mode 100644 index 0000000..ba67a91 --- /dev/null +++ b/.github/workflows/pages.yml @@ -0,0 +1,50 @@ +name: Deploy GitHub Pages + +on: + # Disable this workflow until Pages is properly configured + # push: + # branches: + # - main + # - master + # paths: + # - 'docs/**' + workflow_dispatch: # Allow manual triggering only + +permissions: + contents: read + pages: write + id-token: write + +concurrency: + group: "pages" + cancel-in-progress: false + +jobs: + build: + runs-on: ubuntu-latest + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Pages + uses: actions/configure-pages@v4 + + - name: Build with Jekyll + uses: actions/jekyll-build-pages@v1 + with: + source: ./docs + destination: ./_site + + - name: Upload artifact + uses: actions/upload-pages-artifact@v3 + + deploy: + environment: + name: github-pages + url: ${{ steps.deployment.outputs.page_url }} + runs-on: ubuntu-latest + needs: build + steps: + - name: Deploy to GitHub Pages + id: deployment + uses: actions/deploy-pages@v4 diff --git a/.github/workflows/performance-test.yml b/.github/workflows/performance-test.yml new file mode 100644 index 0000000..64470b3 --- /dev/null +++ b/.github/workflows/performance-test.yml @@ -0,0 +1,117 @@ +name: Performance Testing + +on: + pull_request: + branches: [ main ] + paths: + - 'bulk-*/src/**/*.kt' + - 'bulk-*/src/**/*.java' + push: + branches: [ main ] + workflow_dispatch: + +permissions: + contents: read + pull-requests: write + +jobs: + performance-test: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Check if any JMH benchmarks exist + - name: Check for JMH benchmarks + id: check_benchmarks + run: | + if find . -path "*/src/jmh" -type d | grep -q .; then + echo "benchmarks_exist=true" >> $GITHUB_OUTPUT + echo "✅ JMH benchmark directories found" + else + echo "benchmarks_exist=false" >> $GITHUB_OUTPUT + echo "⚠️ No JMH benchmark directories found, skipping performance tests" + fi + + # Run performance tests only if benchmarks exist + - name: Run performance benchmarks + if: steps.check_benchmarks.outputs.benchmarks_exist == 'true' + run: | + ./gradlew jmh --info + echo "Performance tests completed" + + - name: Upload benchmark results + if: steps.check_benchmarks.outputs.benchmarks_exist == 'true' + uses: actions/upload-artifact@v4 + with: + name: benchmark-results + path: | + **/build/reports/jmh/ + **/jmh-result.json + retention-days: 30 + + # Comment on PR with benchmark results (if available) + - name: Comment benchmark results on PR + if: github.event_name == 'pull_request' && steps.check_benchmarks.outputs.benchmarks_exist == 'true' + uses: actions/github-script@v7 + with: + script: | + const fs = require('fs'); + const path = require('path'); + + // Look for JMH results + const findJmhResults = (dir) => { + const files = fs.readdirSync(dir, { withFileTypes: true }); + for (const file of files) { + if (file.isDirectory()) { + const result = findJmhResults(path.join(dir, file.name)); + if (result) return result; + } else if (file.name === 'jmh-result.json') { + return path.join(dir, file.name); + } + } + return null; + }; + + try { + const resultsFile = findJmhResults('.'); + if (resultsFile && fs.existsSync(resultsFile)) { + const results = JSON.parse(fs.readFileSync(resultsFile, 'utf8')); + + let comment = '## 🚀 Performance Benchmark Results\n\n'; + comment += '| Benchmark | Score | Unit | Error |\n'; + comment += '|-----------|-------|------|-------|\n'; + + results.forEach(result => { + const score = result.primaryMetric.score.toFixed(2); + const error = result.primaryMetric.scoreError.toFixed(2); + const unit = result.primaryMetric.scoreUnit; + comment += `| ${result.benchmark} | ${score} | ${unit} | ±${error} |\n`; + }); + + comment += '\n> 📊 Full benchmark report is available in the workflow artifacts.'; + + github.rest.issues.createComment({ + issue_number: context.issue.number, + owner: context.repo.owner, + repo: context.repo.repo, + body: comment + }); + } else { + console.log('No JMH results found to comment on PR'); + } + } catch (error) { + console.log('Error processing benchmark results:', error.message); + } diff --git a/.github/workflows/security-scan.yml b/.github/workflows/security-scan.yml new file mode 100644 index 0000000..0dc6f67 --- /dev/null +++ b/.github/workflows/security-scan.yml @@ -0,0 +1,67 @@ +name: Security Scan + +on: + # Temporarily disabled due to NVD maintenance (July 24, 2025 until 2:00 PM EDT) + # push: + # branches: [ main ] + # pull_request: + # branches: [ main ] + # schedule: + # - cron: '0 6 * * 1' # Run weekly on Monday at 6 AM + workflow_dispatch: # Allow manual triggering only + +permissions: + contents: read + security-events: write + actions: read + +jobs: + security-scan: + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + cache: gradle + + - name: Grant execute permission for gradlew + run: chmod +x gradlew + + # Run OWASP dependency check + - name: Run OWASP Dependency Check + run: ./gradlew dependencyCheckAggregate + + - name: Upload OWASP report + uses: actions/upload-artifact@v4 + with: + name: owasp-report + path: build/reports/dependency-check-report.html + retention-days: 30 + + # CodeQL Analysis + - name: Initialize CodeQL + uses: github/codeql-action/init@v3 + with: + languages: java + + - name: Build for CodeQL + run: ./gradlew build -x test + + - name: Perform CodeQL Analysis + uses: github/codeql-action/analyze@v3 + + # Secrets scanning + - name: Run TruffleHog OSS + uses: trufflesecurity/trufflehog@main + with: + path: ./ + base: main + head: HEAD + extra_args: --debug --only-verified diff --git a/.github/workflows/stale.yml b/.github/workflows/stale.yml new file mode 100644 index 0000000..44fc187 --- /dev/null +++ b/.github/workflows/stale.yml @@ -0,0 +1,37 @@ +name: Mark stale issues and pull requests + +on: + schedule: + - cron: '30 1 * * *' # Run at 1:30 AM every day + +permissions: + issues: write + pull-requests: write + +jobs: + stale: + runs-on: ubuntu-latest + steps: + - uses: actions/stale@v8 + with: + repo-token: ${{ secrets.GITHUB_TOKEN }} + stale-issue-message: 'This issue has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' + stale-pr-message: 'This pull request has been automatically marked as stale because it has not had recent activity. It will be closed if no further activity occurs. Thank you for your contributions.' + stale-issue-label: 'stale' + stale-pr-label: 'stale' + exempt-issue-labels: 'bug,security,enhancement,documentation' + exempt-pr-labels: 'work-in-progress,awaiting-review' + days-before-stale: 60 + days-before-close: 14 + close-issue-reason: 'not_planned' + exempt-all-milestones: false + operations-per-run: 30 + remove-stale-when-updated: true + debug-only: false + ascending: false + delete-branch: false + exempt-all-assignees: false + exempt-draft-pr: false + enable-statistics: true + ignore-updates: false + include-only-assigned: false diff --git a/.github/workflows/template-validation.yml b/.github/workflows/template-validation.yml new file mode 100644 index 0000000..3d65711 --- /dev/null +++ b/.github/workflows/template-validation.yml @@ -0,0 +1,132 @@ +name: Template Repository Setup +description: Validate template repository structure and features +on: + push: + branches: [ main, master ] + pull_request: + branches: [ main, master ] + +jobs: + template-validation: + runs-on: ubuntu-latest + name: Validate Template Structure + + steps: + - name: Checkout code + uses: actions/checkout@v4 + + - name: Set up JDK 21 + uses: actions/setup-java@v4 + with: + java-version: '21' + distribution: 'temurin' + + - name: Cache Gradle packages + uses: actions/cache@v4 + with: + path: | + ~/.gradle/caches + ~/.gradle/wrapper + key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }} + restore-keys: | + ${{ runner.os }}-gradle- + + - name: Make gradlew executable + run: chmod +x gradlew + + - name: Validate template structure + run: | + echo "🔍 Validating template structure..." + + # Check required files exist + required_files=( + "build.gradle" + "settings.gradle" + "gradle.properties" + "README.md" + "customize.sh" + "customize.ps1" + "service-module/build.gradle" + "springboot-application/build.gradle" + ) + + for file in "${required_files[@]}"; do + if [ ! -f "$file" ]; then + echo "❌ Missing required file: $file" + exit 1 + else + echo "✅ Found: $file" + fi + done + + - name: Validate customization scripts + run: | + echo "🔧 Validating customization scripts..." + + # Check bash script syntax + if ! bash -n customize.sh; then + echo "❌ Bash script has syntax errors" + exit 1 + fi + echo "✅ Bash script syntax is valid" + + # Check PowerShell script syntax (basic check) + if ! grep -q "param" customize.ps1; then + echo "❌ PowerShell script missing parameters" + exit 1 + fi + echo "✅ PowerShell script structure is valid" + + - name: Test build without customization + run: | + echo "🏗️ Testing default build..." + ./gradlew build --no-daemon + + - name: Test small tests only + run: | + echo "🧪 Testing small test group..." + ./gradlew test -PtestGroups=small --no-daemon + + - name: Test coverage verification + run: | + echo "📊 Testing coverage verification..." + ./gradlew jacocoTestCoverageVerification --no-daemon + + - name: Validate template placeholders + run: | + echo "🔍 Checking for template placeholders..." + + # Check for common placeholder patterns that should be customizable + template_checks=( + "io.programmernewbie.template" + "kotlin-multimodule-template" + "KotlinMultimoduleTemplateApplication" + "programmer-newbie-code" + ) + + found_placeholders=0 + for placeholder in "${template_checks[@]}"; do + if grep -r "$placeholder" . --exclude-dir=.git --exclude-dir=build --exclude="*.md" --exclude="template-config.yml" > /dev/null; then + echo "✅ Found customizable placeholder: $placeholder" + found_placeholders=$((found_placeholders + 1)) + fi + done + + if [ $found_placeholders -lt 3 ]; then + echo "❌ Not enough customizable placeholders found" + exit 1 + fi + + echo "✅ Template has sufficient customizable content" + + - name: Generate template report + run: | + echo "📋 Template Validation Report" + echo "==============================" + echo "✅ All required files present" + echo "✅ Build successful" + echo "✅ Tests passing with coverage" + echo "✅ Customization scripts valid" + echo "✅ Template placeholders found" + echo "" + echo "🎉 This repository is ready to be used as a GitHub template!" diff --git a/.github/workflows/validate-naming.yml b/.github/workflows/validate-naming.yml new file mode 100644 index 0000000..79f8f3c --- /dev/null +++ b/.github/workflows/validate-naming.yml @@ -0,0 +1,162 @@ +name: Validate Naming Conventions + +on: + pull_request: + types: [ opened, edited, synchronize, reopened ] + push: + branches: [ main, dev ] + +jobs: + validate-pr-title: + if: github.event_name == 'pull_request' + runs-on: ubuntu-latest + steps: + - name: Validate PR Title Format + run: | + PR_TITLE="${{ github.event.pull_request.title }}" + echo "Validating PR title: '$PR_TITLE'" + + # Check if PR title follows conventional commit format + if echo "$PR_TITLE" | grep -qE '^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{3,}$'; then + echo "✅ PR title follows conventional commit format" + + # Extract type and scope + TYPE=$(echo "$PR_TITLE" | sed -E 's/^([a-z]+)(\(.+\))?: .+$/\1/') + + echo "📝 Type: $TYPE" + + # Check if there's a scope + if echo "$PR_TITLE" | grep -q '('; then + SCOPE=$(echo "$PR_TITLE" | sed -E 's/^[a-z]+\((.+)\): .+$/\1/') + echo "📝 Scope: $SCOPE" + else + echo "📝 Scope: (none)" + fi + else + echo "❌ PR title does not follow conventional commit format" + echo "" + echo "Expected format: type(scope): description" + echo "Examples:" + echo " - feat: add new caching layer" + echo " - feat(redis): implement Redis cache adapter" + echo " - fix(core): resolve memory leak in cache manager" + echo " - docs: update API documentation" + echo " - test(caffeine): add integration tests" + echo "" + echo "Valid types: feat, fix, docs, style, refactor, test, chore, perf, ci, build, revert" + echo "Valid scopes: core, redis, caffeine, spring, cache, config, api, deps, ci, docs, test, security" + echo "" + echo "📖 See our commit conventions: https://github.com/${{ github.repository }}/blob/main/docs/commit-conventions.md" + exit 1 + fi + + validate-commit-messages: + # Only validate commit messages if squash merging is disabled + # Since you use squash merging only, PR title validation is sufficient + if: false # Disable this job completely for squash-merge-only repositories + runs-on: ubuntu-latest + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Validate Commit Messages + run: | + echo "ℹ️ Commit message validation is disabled for squash-merge-only repositories" + echo "PR title validation ensures the final squashed commit follows conventional format" + + validate-signed-commits: + # For now, make GPG validation informational only to avoid CI issues + runs-on: ubuntu-latest + continue-on-error: true # Don't fail the workflow if GPG validation fails + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - name: Check GPG Signatures (Informational) + run: | + echo "🔐 Checking GPG signatures for commits (informational only)..." + + # Get list of commits to check + if [ "${{ github.event_name }}" == "pull_request" ]; then + # For PRs, check commits in the PR + COMMITS=$(git rev-list --no-merges ${{ github.event.pull_request.base.sha }}..${{ github.event.pull_request.head.sha }}) + echo "📋 Checking PR commits from ${{ github.event.pull_request.base.sha }} to ${{ github.event.pull_request.head.sha }}" + else + # For push events, check the pushed commits + if [ "${{ github.event.before }}" != "0000000000000000000000000000000000000000" ]; then + COMMITS=$(git rev-list ${{ github.event.before }}..${{ github.event.after }}) + echo "📋 Checking push commits from ${{ github.event.before }} to ${{ github.event.after }}" + else + # First push to branch - check only the latest commit + COMMITS="${{ github.event.after }}" + echo "📋 Checking first push commit: ${{ github.event.after }}" + fi + fi + + if [ -z "$COMMITS" ]; then + echo "ℹ️ No commits to check" + exit 0 + fi + + SIGNED_COUNT=0 + UNSIGNED_COUNT=0 + TOTAL_COMMITS=0 + + for commit in $COMMITS; do + TOTAL_COMMITS=$((TOTAL_COMMITS + 1)) + COMMIT_MSG=$(git log --format=%s -n 1 "$commit") + AUTHOR=$(git log --format="%an <%ae>" -n 1 "$commit") + + echo "Checking commit: $commit" + echo "Message: $COMMIT_MSG" + echo "Author: $AUTHOR" + + # Check if commit has GPG signature using git log + GPG_SIG=$(git log --format="%G?" -n 1 "$commit") + SIGNER=$(git log --format="%GS" -n 1 "$commit") + + case "$GPG_SIG" in + "G") + echo "✅ Valid GPG signature by: $SIGNER" + SIGNED_COUNT=$((SIGNED_COUNT + 1)) + ;; + "U") + echo "⚠️ Good signature, unknown validity by: $SIGNER" + SIGNED_COUNT=$((SIGNED_COUNT + 1)) + ;; + "B"|"X"|"Y"|"R"|"E") + echo "⚠️ Signature issue (status: $GPG_SIG)" + UNSIGNED_COUNT=$((UNSIGNED_COUNT + 1)) + ;; + "N"|"") + echo "ℹ️ No GPG signature found" + UNSIGNED_COUNT=$((UNSIGNED_COUNT + 1)) + ;; + *) + echo "❓ Unknown signature status: $GPG_SIG" + UNSIGNED_COUNT=$((UNSIGNED_COUNT + 1)) + ;; + esac + echo "" + done + + echo "📊 GPG Signature Summary:" + echo "Total commits: $TOTAL_COMMITS" + echo "Signed commits: $SIGNED_COUNT" + echo "Unsigned/problematic commits: $UNSIGNED_COUNT" + + if [ $UNSIGNED_COUNT -gt 0 ]; then + echo "" + echo "💡 Note: Some commits may not have GPG signatures." + echo "While not enforced in CI, GPG signing is recommended for security." + echo "📖 See our GPG setup guide: https://github.com/${{ github.repository }}/blob/main/docs/gpg-setup.md" + else + echo "✅ All commits are GPG signed!" + fi + + echo "" + echo "ℹ️ This check is informational only and won't fail the workflow." diff --git a/.gitignore b/.gitignore index 566e06b..9d4a595 100644 --- a/.gitignore +++ b/.gitignore @@ -19,9 +19,40 @@ *.tar.gz *.rar +# Except Gradle Wrapper JAR +!gradle/wrapper/gradle-wrapper.jar + # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml hs_err_pid* replay_pid* # Kotlin Gradle plugin data, see https://kotlinlang.org/docs/whatsnew20.html#new-directory-for-kotlin-data-in-gradle-projects -.kotlin/ \ No newline at end of file +.kotlin/ + +# Build - ignore all build directories +build/ +*/build/ +**/build/ +target/ +out/ + +# IDEs +.idea/ +*.iml +*.ipr +*.iws +.vscode/ + +# Except code style configuration +!.idea/codeStyles/ +!.idea/copyright/ +!.idea/inspectionProfiles/ +!.vscode/settings.json +!.vscode/java-formatter.xml +!.editorconfig + +# Others +*.DS_Store +.gradle/ +*/.gradle/ +**/.gradle/ diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..124f9ba --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,46 @@ +# Pre-commit hooks configuration +# Install with: pip install pre-commit && pre-commit install + +repos: + # Code formatting and linting + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.4.0 + hooks: + - id: trailing-whitespace + - id: end-of-file-fixer + - id: check-yaml + - id: check-json + - id: check-merge-conflict + - id: check-added-large-files + args: [ '--maxkb=1000' ] + - id: detect-private-key + + # Kotlin/Java formatting + - repo: https://github.com/macisamuele/language-formatters-pre-commit-hooks + rev: v2.10.0 + hooks: + - id: pretty-format-kotlin + args: [ --autofix ] + + # Gradle wrapper validation + - repo: local + hooks: + - id: gradle-wrapper-validation + name: Validate Gradle Wrapper + entry: gradle/wrapper-validation-action + language: system + pass_filenames: false + + # Security scanning + - repo: https://github.com/Yelp/detect-secrets + rev: v1.4.0 + hooks: + - id: detect-secrets + args: [ '--baseline', '.secrets.baseline' ] + + # Commit message validation + - repo: https://github.com/compilerla/conventional-pre-commit + rev: v3.0.0 + hooks: + - id: conventional-pre-commit + stages: [ commit-msg ] diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..0de8f90 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,22 @@ +# 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] + +### Bug Fixes + +### Documentation + +### Features + +### Miscellaneous Tasks + +### Testing + +### Ci + + diff --git a/CODE_OF_CONDUCT.md b/CODE_OF_CONDUCT.md new file mode 100644 index 0000000..5745b7f --- /dev/null +++ b/CODE_OF_CONDUCT.md @@ -0,0 +1,62 @@ +# Contributor Covenant Code of Conduct + +## Our Pledge + +We as members, contributors, and leaders pledge to make participation in our +community a harassment-free experience for everyone, regardless of age, body +size, visible or invisible disability, ethnicity, sex characteristics, gender +identity and expression, level of experience, education, socio-economic status, +nationality, personal appearance, race, religion, or sexual identity +and orientation. + +We pledge to act and interact in ways that contribute to an open, welcoming, +diverse, inclusive, and healthy community. + +## Our Standards + +Examples of behavior that contributes to a positive environment for our +community include: + +* Demonstrating empathy and kindness toward other people +* Being respectful of differing opinions, viewpoints, and experiences +* Giving and gracefully accepting constructive feedback +* Accepting responsibility and apologizing to those affected by our mistakes, + and learning from the experience +* Focusing on what is best not just for us as individuals, but for the + overall community + +Examples of unacceptable behavior include: + +* The use of sexualized language or imagery, and sexual attention or + advances of any kind +* Trolling, insulting or derogatory comments, and personal or political attacks +* Public or private harassment +* Publishing others' private information, such as a physical or email + address, without their explicit permission +* Other conduct which could reasonably be considered inappropriate in a + professional setting + +## Enforcement Responsibilities + +Community leaders are responsible for clarifying and enforcing our standards of +acceptable behavior and will take appropriate and fair corrective action in +response to any behavior that they deem inappropriate, threatening, offensive, +or harmful. + +## Scope + +This Code of Conduct applies within all community spaces, and also applies when +an individual is officially representing the community in public spaces. + +## Enforcement + +Instances of abusive, harassing, or otherwise unacceptable behavior may be +reported to the community leaders responsible for enforcement at +[INSERT CONTACT METHOD]. +All complaints will be reviewed and investigated promptly and fairly. + +## Attribution + +This Code of Conduct is adapted from the [Contributor Covenant](https://www.contributor-covenant.org), +version 2.0, available at +https://www.contributor-covenant.org/version/2/0/code_of_conduct.html. diff --git a/CODE_STYLE.md b/CODE_STYLE.md new file mode 100644 index 0000000..0c10af1 --- /dev/null +++ b/CODE_STYLE.md @@ -0,0 +1,412 @@ +# Code Style Guide + +This document outlines the code style conventions for this project and provides instructions for setting up your +development environment to adhere to these conventions. + +## Style Overview + +We follow a style based on Google's style guide with the following key aspects: + +- **Indentation**: 2 spaces (no tabs) +- **Line length**: 120 characters maximum +- **File encoding**: UTF-8 +- **Line endings**: LF (Unix-style) +- **No wildcard imports**: Each class should be imported explicitly +- **Final newline**: All files end with a newline + +## Language-Specific Guidelines + +### Kotlin Style + +As this project is primarily written in Kotlin, we follow +the [Kotlin coding conventions](https://kotlinlang.org/docs/coding-conventions.html) with Google-style 2-space +indentation: + +- **Naming conventions**: + - Use camelCase for properties, variables, functions, and parameters + - Use PascalCase for classes, interfaces, and type parameters + - Use UPPER_SNAKE_CASE for constants + +- **Class structure**: + ```kotlin + class Example( + val property1: String, + private val property2: Int + ) { + // Properties first + val derivedProperty = property1.length + + // Methods grouped by functionality + fun method1() { + // 2-space indentation + } + + // Inner classes last + inner class InnerExample { + // ... + } + } + ``` + +- **Function declarations**: + - Keep parameter lists on a single line for short functions + - For functions with many parameters, break after the opening parenthesis and indent parameters by 2 spaces: + ```kotlin + fun longFunctionName( + param1: String, + param2: Int, + param3: Boolean + ): ReturnType { + // Function body + } + ``` + +### Java Style + +For Java code, we follow the [Google Java Style Guide](https://google.github.io/styleguide/javaguide.html) with 2-space +indentation. + +## Module-Specific Guidelines + +### Bulk Cache Components + +Our project consists of several modules (`bulk-core`, `bulk-caffeine`, `bulk-redis`, `bulk-spring`). Each module has its +own conventions: + +#### Annotations + +For annotations in `bulk-core`: + +- Place documentation above annotations +- Use descriptive parameter names +- Include examples in KDoc comments + +#### Cache Implementations + +For cache implementations in `bulk-caffeine` and `bulk-redis`: + +- Group related functionality into separate files +- Use descriptive method names that clearly indicate their purpose +- Document public APIs with KDoc comments + +## File Organization + +### Directory Structure + +Maintain the following directory structure: + + +- `io.programmernewbie.template.annotation` - Annotation classes +- `io.programmernewbie.template.core` - Core interfaces and implementations +- `io.programmernewbie.template.service` - Service implementations + +### Package Declarations + +Package declarations should: + +- Align with the directory structure +- Be the first non-comment line in the file +- Be followed by an empty line + +### Import Statements + +- Group imports by package, separated by empty lines +- Do not use wildcard imports +- Sort imports alphabetically within groups + +## Build Files + +### Gradle Build Files + +Each module has its own `build.gradle` file. These files should: + +- Use consistent dependency declarations +- Reference versions from `gradle.properties` using `${property_name}` syntax +- Group dependencies by type (implementation, testImplementation, etc.) + +## Gradle Script Formatting + +Our project uses several Gradle scripts in the `scripts/` directory (e.g., `kotlin.gradle`, `jacoco.gradle`, +`tasks.gradle`, `spring_boot.gradle`) that should follow the same formatting standards as our Kotlin and Java code: + +### Gradle Script Guidelines + +1. **Indentation**: Use 2 spaces for indentation in all Gradle files +2. **Block style**: Opening braces should be on the same line as the declaration +3. **String quotes**: Prefer double quotes for strings in Gradle files +4. **Plugin application**: Put each plugin application on its own line +5. **Dependencies**: Format multi-line dependencies with proper indentation + +Example Gradle script format: + +```groovy +apply plugin: "java" +apply plugin: "kotlin" + +dependencies { + implementation "org.springframework:spring-core:${spring_version}" + implementation "org.jetbrains.kotlin:kotlin-stdlib:${kotlin_version}" + + testImplementation "org.testng:testng:${testng_version}" +} + +tasks.register("customTask") { + group = "Custom" + description = "Example task" + + doLast { + println "Running custom task" + } +} +``` + +### Formatting Gradle Scripts + +For formatting Gradle scripts: + +- In **IntelliJ IDEA**: Use Code → Reformat Code (Ctrl+Alt+L / Cmd+Option+L) +- In **VS Code**: With the Gradle extension installed, use Format Document (Shift+Alt+F / Shift+Option+F) +- Consider using a Gradle-specific formatter like [Spotless](https://github.com/diffplug/spotless) with the Gradle + extension to automate formatting + +## Documentation + +### KDoc/JavaDoc Comments + +- Document all public APIs with KDoc (Kotlin) or JavaDoc (Java) +- Include parameter descriptions, return values, and examples where appropriate +- Use markdown syntax in KDoc comments + +Example: + +```kotlin +/** + * Retrieves cached values in bulk for multiple keys. + * + * @param keys Collection of keys to retrieve from cache + * @return Map of keys to their cached values, omitting any keys not found + * @throws CacheException if there is an error accessing the cache + * + * Example: + * ``` + +* val values = bulkCache.getAll(listOf("key1", "key2", "key3")) +* ``` + +*/ +fun getAll(keys: Collection): Map + +``` + +## Commit Guidelines + +- Use [Conventional Commits](https://www.conventionalcommits.org/) format +- Reference issue numbers in commit messages when applicable +- Keep commits focused on a single logical change +- Sign all commits with your GPG key + +## Testing Standards + +- Write unit tests for all public APIs +- Name test classes with a `Test` suffix +- Name test methods to clearly describe what they're testing +- Maintain at least 70% code coverage + +## IDE/Editor Setup + +This project includes configuration files for various IDEs and editors to make it easier to follow the code style. + +### IntelliJ IDEA + +The `.idea/codeStyles/Project.xml` file configures IntelliJ IDEA to use the project code style. When you open the project in IntelliJ IDEA: + +1. Go to Settings → Editor → Code Style +2. Ensure "Project" is selected in the Scheme dropdown +3. The IDE will automatically apply the correct formatting + +You can reformat code at any time with: +- **Windows/Linux**: Ctrl+Alt+L +- **macOS**: Option+Command+L + +### Visual Studio Code + +The project includes VS Code settings in the `.vscode` directory: + +1. Install the recommended extensions: + - "EditorConfig for VS Code" + - "Extension Pack for Java" + - "Kotlin Language" + +2. VS Code will automatically apply the formatting settings from `.vscode/settings.json` + +3. Format your code with: + - **Windows/Linux**: Shift+Alt+F + - **macOS**: Shift+Option+F + +### Eclipse + +For Eclipse users: + +1. Import the formatter settings from `.vscode/java-formatter.xml`: + - Go to Window → Preferences → Java → Code Style → Formatter + - Click "Import" and select the java-formatter.xml file + - Select "SpringBulkLayeredCache" profile + +### Other Editors + +This project includes an `.editorconfig` file that is supported by many editors. Most modern editors either natively support EditorConfig or have plugins available. + +## Automatic Formatting on Save + +When possible, enable "Format on Save" in your IDE/editor to ensure your code always adheres to the style guidelines. This is already configured in the VS Code settings. + +## Pre-commit Checks + +Consider setting up a Git pre-commit hook to verify formatting before committing: + +```bash +#!/bin/sh +# Add formatting verification command here +# Example for Kotlin: +# ./gradlew ktlintCheck +``` + +## Project-Specific Script Guidelines + +### Scripts Organization + +This project uses a modular script structure in the `scripts/` directory. Each script has a specific purpose: + +- **kotlin.gradle**: Kotlin language configuration and dependencies +- **spring_boot.gradle**: Spring Boot plugin and dependency configuration +- **tasks.gradle**: Custom Gradle tasks like `resolveAndLockAll` +- **jacoco.gradle**: Code coverage configuration and reporting + +### Script-Specific Formatting + +#### JaCoCo Script Formatting + +For the `jacoco.gradle` script: + +- Use comments to explain coverage thresholds and exclusion patterns +- Use consistent formatting for exclusion patterns, each on a separate line with explanatory comments +- Format the nested `reports` block with proper indentation + +Example: + +```groovy +jacocoTestReport { + dependsOn test + reports { + xml.required = true // XML report needed for coverage tools in CI + csv.required = true // CSV report used for badge generation + html.required = true + html.outputLocation = layout.buildDirectory.dir('reports/jacoco') + } +} +``` + +#### Task Script Formatting + +For the `tasks.gradle` script: + +- Separate task registration with empty lines +- Place task group and description fields at the top of the task configuration +- Use proper indentation for nested closures like `doLast` and `doFirst` + +Example: + +```groovy +tasks.register('resolveAndLockAll') { + group = "dependency locking" + description = "Resolves and locks all project dependencies" + + doLast { + println "Resolving dependencies..." + // Task implementation + } +} +``` + +## Cache Implementation Guidelines + +### Layered Caching Patterns + +When implementing cache providers: + +1. **Cache Operations**: + - Keep bulk operations and single operations consistent + - Use descriptive method names that clearly indicate their purpose + - Return empty collections rather than null for bulk operations + +2. **Performance Considerations**: + - Minimize object allocations in hot paths + - Use proper nullability annotations to prevent NPEs + - Consider thread safety in cache implementation + +3. **Error Handling**: + - Use specific exception types for different error conditions + - Catch and handle provider-specific exceptions, translating to common exceptions + - Log errors with appropriate detail at appropriate levels + +Example: + +```kotlin +override fun getAll(keys: Collection): Map { + if (keys.isEmpty()) { + return emptyMap() + } + + try { + // Provider-specific implementation + return providerCache.getAll(keys) + } catch (e: ProviderSpecificException) { + logger.error("Failed to retrieve items from cache", e) + throw CacheOperationException("Failed to retrieve items from cache", e) + } +} +``` + +### Spring Integration Guidelines + +For Spring integration in the `bulk-spring` module: + +1. **Bean Configuration**: + - Keep bean definitions minimal and focused + - Use `@ConditionalOn...` annotations appropriately + - Provide sensible defaults that work well in most environments + +2. **Annotation Processing**: + - Follow Spring's annotation processing patterns + - Document public APIs thoroughly with examples + - Handle edge cases explicitly + +### Multi-Module Structure + +Our project follows a modular structure to separate concerns: + +- **bulk-core**: Base interfaces and annotations +- **bulk-caffeine**: Caffeine-based cache implementation +- **bulk-redis**: Redis-based cache implementation +- **bulk-spring**: Spring framework integration + +When contributing: + +1. Place code in the appropriate module based on its dependencies +2. Avoid circular dependencies between modules +3. Minimize dependency leakage (e.g., don't expose Redis-specific types in interfaces) + +## Performance Sensitive Code + +For performance-critical sections: + +- Add comments explaining performance considerations +- Consider using inline functions for small, frequently called methods +- Use primitive collections when appropriate to reduce boxing/unboxing +- Add benchmarks for critical sections in separate benchmark modules +- Document performance characteristics in KDoc comments + +## Questions and Style Discussions + +If you have questions about the code style or would like to propose changes, please open a discussion issue on the +GitHub repository. diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..8dfa3c5 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,194 @@ +# Contributing to Kotlin Multimodule Template + +Thanks for your interest! Here's how you can help: + +## Documentation + +Our full documentation is available at: + +* +*[https://programmer-newbie-code.github.io/kotlin-multimodule-template/](https://programmer-newbie-code.github.io/kotlin-multimodule-template/) +** + +Please refer to the documentation for detailed guides on project architecture and design principles. + +## How to Contribute + +### Fork and Pull Request Workflow + +1. **Fork the repository** to your own GitHub account +2. **Clone your fork** to your local machine: + ``` + git clone https://github.com/YOUR-USERNAME/kotlin-multimodule-template.git + ``` +3. **Add the original repo as a remote** to keep your fork in sync: + ``` + git remote add upstream https://github.com/programmer-newbie-code/kotlin-multimodule-template.git + ``` +4. **Create a new branch** for your changes: + ``` + git checkout -b feature/your-feature-name + ``` +5. **Make your changes** and commit them with signed commits (see below) +6. **Rebase on the latest main branch** before submitting your PR: + ``` + git fetch upstream + git rebase upstream/main + ``` +7. **Push your branch** to your fork: + ``` + git push origin feature/your-feature-name + ``` +8. **Create a Pull Request** from your fork to the original repository + +### Dependency Management + +We use Gradle's dependency locking to ensure reproducible builds. All dependency versions are centrally defined in +`gradle.properties`. + +When adding or updating dependencies: + +1. **Add the dependency** to the appropriate `build.gradle` file +2. **Regenerate lock files** with: + ``` + ./gradlew resolveAndLockAll --write-locks + ``` +3. **Commit both the build.gradle changes and the updated lock files** + +### Code Signing Requirements + +All commits must be signed with your GPG key. This helps verify the authenticity of contributions. + +1. **Set up GPG** if you haven't already: + ``` + git config --global user.signingkey YOUR_GPG_KEY_ID + git config --global commit.gpgsign true + ``` + +2. **Sign your commits** when committing: + ``` + git commit -S -m "Your commit message" + ``` + +3. **Verify** that commits are signed before pushing + +## 🔐 **IMPORTANT: All commits must be GPG signed** + +This repository requires all commits to be cryptographically signed for security and authenticity. + +### Quick Setup + +- **New to GPG?** See our [GPG Setup Guide](docs/gpg-setup.md) +- **Need help?** Check the platform-specific guides in the `docs/` folder + +### Verification + +Before contributing, verify your setup: + +```bash +# Check if signing is enabled +git config commit.gpgsign +# Should return: true + +# Check your signing key +git config user.signingkey +# Should return your GPG key ID + +# Test with a signed commit +git commit --allow-empty -m "test: verify GPG signing" +git log --show-signature -1 +# Should show "Good signature from [your name]" +``` + +**❌ Unsigned commits will be rejected during code review.** + +### Branch Protection and Review Process + +Our repository has branch protection rules in place: + +- **Direct pushes to main are not allowed** +- **All PRs require at least one approving review** +- **All PRs must pass status checks** (including code coverage of at least 70%) +- **Commits must be signed** + +### Commit Message and PR Title Conventions + +We follow the [Conventional Commits](https://www.conventionalcommits.org/) specification for commit messages and pull +request titles to ensure consistency and enable automated tooling for releases and changelogs. + +**Format**: `(): ` + +- **type**: Describes the kind of change (required) +- **scope**: Component affected by the change (optional) +- **description**: Brief description of the change + +**Types**: + +- `feat`: A new feature +- `fix`: A bug fix +- `docs`: Documentation changes only +- `style`: Changes that don't affect code functionality (formatting, etc.) +- `refactor`: Code refactoring without feature changes or bug fixes +- `perf`: Performance improvements +- `test`: Adding or fixing tests +- `chore`: Changes to build process, tooling, etc. +- `ci`: Changes to CI configuration files and scripts +- `revert`: Reverts a previous commit + +**Examples**: + +- `feat(core): add bulk fetch operation` +- `fix(redis): resolve connection leak issue` +- `docs: update API documentation` +- `test(caffeine): add coverage for eviction policy` + +Both commit messages and PR titles should follow these conventions. The PR title will be used as the squash commit +message when merging. + +### Development Tags + +For development versions: + +- Use `-dev` suffix on version tags (e.g., `v1.0.0-dev`) +- Development versions will be built but not published to the package registry + +For release versions: + +- Use clean version tags without suffixes (e.g., `v1.0.0`) +- Release versions will be built and published to the GitHub Package Registry + +## Code Quality and Testing + +### Code Style + +We use a standardized code style with 2 space indentation. Please configure your IDE accordingly: + +- For IntelliJ IDEA, import the provided `.idea` configuration +- For VS Code, use the settings in `.vscode` directory + +### Testing Requirements + +- **Maintain code coverage** of at least 70% +- **Add tests** for any new features or bug fixes +- **Run the full test suite** before submitting PRs: + ``` + ./gradlew test + ``` + +### Code Coverage + +Check code coverage with: + +``` +./gradlew jacocoTestReport +``` + +Verify coverage meets our 70% threshold: + +``` +./gradlew jacocoTestCoverageVerification +``` + +## Questions? + +If you have any questions or need help, please open an issue with the label "question". diff --git a/CONTRIBUTORS.md b/CONTRIBUTORS.md new file mode 100644 index 0000000..cfc0950 --- /dev/null +++ b/CONTRIBUTORS.md @@ -0,0 +1,31 @@ +# Contributors ✨ + +Thanks goes to these wonderful people ([emoji key](https://allcontributors.org/docs/en/emoji-key)): + + + + + + + + + +

Example User

💻 📖 🚧
+ + + + +This project follows the [all-contributors](https://github.com/all-contributors/all-contributors) specification. +Contributions of any kind are welcome! + +## How to be added to this list + +When you open a pull request that gets merged, you'll automatically be considered for addition to the contributors list. +Alternatively, you can: + +1. Comment on an issue or pull request, asking @all-contributors to add you: + ``` + @all-contributors please add for + ``` + +2. Create a new issue using the "contributor_request" template diff --git a/DEVELOPMENT.md b/DEVELOPMENT.md new file mode 100644 index 0000000..7d5f667 --- /dev/null +++ b/DEVELOPMENT.md @@ -0,0 +1,111 @@ +# Development Guide + +This guide provides detailed instructions for setting up a development environment and working on Kotlin Multimodule +Template. + +## Prerequisites + +- JDK 21 +- Gradle (wrapper included) +- Git with commit signing configured + +## Development Environment Setup + +1. **Clone your fork**: + ``` + git clone https://github.com/YOUR-USERNAME/kotlin-multimodule-template.git + cd kotlin-multimodule-template + ``` + +2. **Set up Git hooks** (optional but recommended): + + Create a pre-commit hook to verify your commits are signed: + + ```bash + # Create .git/hooks/pre-commit + echo '#!/bin/sh + if git config --get commit.gpgsign | grep -q "true"; then + echo "Commit signing is enabled." + exit 0 + else + echo "Error: Commit signing is not enabled." + echo "Please run: git config commit.gpgsign true" + exit 1 + fi' > .git/hooks/pre-commit + + # Make it executable + chmod +x .git/hooks/pre-commit + ``` + +3. **Build the project**: + ``` + ./gradlew build + ``` + +4. **Run tests**: + ``` + ./gradlew test + ``` + +## Project Structure + +- `springboot-application` - Example Spring Boot application +- `core-module` - Core interfaces and models (documentation) +- `feature-module` - Feature implementations (documentation) +- `scripts` - Gradle build scripts + +## Development Workflow + +1. Create a feature branch with a descriptive name +2. Make changes and write tests +3. Ensure all tests pass with `./gradlew test` +4. Commit with signed commits +5. Rebase on the latest main branch before submitting a PR +6. Submit a PR with a clear description of changes + +## Dependency Management + +We use Gradle dependency locking for reproducible builds: + +1. Add dependencies in appropriate build.gradle file +2. Update dependency lock files: + ``` + ./gradlew resolveAndLockAll --write-locks + ``` +3. Commit updated lock files + +## Code Style + +- Follow the Kotlin coding conventions +- Use meaningful variable and function names +- Document public APIs with KDoc comments +- Write tests for new functionality + +## Useful Gradle Commands + +- `./gradlew clean` - Clean the build +- `./gradlew build` - Build the project +- `./gradlew test` - Run tests +- `./gradlew resolveAndLockAll --write-locks` - Update dependency lock files +- `./gradlew publishToMavenLocal` - Publish to local Maven repository + +## IDE Setup + +### IntelliJ IDEA + +1. Import the project as a Gradle project +2. Use the Kotlin plugin version compatible with the project +3. Enable the following settings: + - Code Style > Kotlin > Use Kotlin code style + - Build, Execution, Deployment > Build Tools > Gradle > Use Gradle for builds + +## Troubleshooting + +If you encounter issues: + +1. Verify your Java version with `java -version` +2. Ensure dependency lock files are up to date +3. Try cleaning the build with `./gradlew clean` +4. Check Gradle wrapper version with `./gradlew --version` + +If you need help, create an issue with the "help wanted" label. diff --git a/HELP.md b/HELP.md new file mode 100644 index 0000000..0845b9a --- /dev/null +++ b/HELP.md @@ -0,0 +1,16 @@ +# Getting Started + +### Reference Documentation + +For further reference, please consider the following sections: + +* [Official Gradle documentation](https://docs.gradle.org) +* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/3.5.3/gradle-plugin) +* [Create an OCI image](https://docs.spring.io/spring-boot/3.5.3/gradle-plugin/packaging-oci-image.html) + +### Additional Links + +These additional references should also help you: + +* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle) + diff --git a/README.md b/README.md index f43cb8a..6488590 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,220 @@ -# kotlin-multimodule-template -Kickstart your next Kotlin project with a clean multi-module setup, CI pipelines, and full flexibility — whether you're building with Spring, Ktor, or beyond. Open to contributions! +# Kotlin Multimodule Template + +A minimal, production-ready Kotlin multimodule template for Spring Boot applications. This template provides a clean +foundation for building scalable microservices with proper module separation and **Spring-Kotlin integration**. + +## 🏗️ Architecture + +This template follows a simple two-module architecture: + +- **service-module**: Contains business logic and services +- **springboot-application**: Contains REST controllers and application configuration + +## 🔧 Spring-Kotlin Integration + +This template includes proper **Kotlin `allopen` plugin configuration** to solve the common issue where Spring cannot +create beans from Kotlin classes (since Kotlin classes are `final` by default). + +### What's Configured: + +The `allopen` plugin automatically makes Kotlin classes **open** (non-final) when annotated with: + +- `@Component`, `@Service`, `@Repository` +- `@Controller`, `@RestController` +- `@Configuration`, `@SpringBootApplication` +- `@Transactional` + +### Why This Matters: + +- ✅ **Spring dependency injection works correctly** +- ✅ **Spring AOP and transaction proxies work** +- ✅ **No need to manually add `open` keywords** +- ✅ **Your `@Service` and `@Controller` classes work out of the box** + +## 🚀 Quick Start + +### 1. Use This Template + +Click "Use this template" button on GitHub to create a new repository from this template. + +### 2. Customize for Your Project + +Run the customization script or manually update: + +```bash +# Linux/Mac +./customize.sh + +# Windows PowerShell +./customize.ps1 +``` + +**Package Names**: Replace `io.programmernewbie.template` with your desired package: + +- Update package declarations in all `.kt` files +- Update `scanBasePackages` in `KotlinMultimoduleTemplateApplication.kt` +- Update `group` in `build.gradle` + +**Project Name**: + +- Update `rootProject.name` in `settings.gradle` +- Update artifact names in `build.gradle` files + +### 3. Build and Run + +```bash +# Build the project +./gradlew build + +# Run the application +./gradlew :springboot-application:bootRun + +# Run tests +./gradlew test +``` + +### 4. Verify Setup + +Once running, test the example endpoints: + +```bash +# Health check +curl http://localhost:8080/api/example/health + +# Welcome message +curl http://localhost:8080/api/example/welcome?name=YourName + +# Async welcome message +curl http://localhost:8080/api/example/welcome-async?name=YourName +``` + +## 📁 Project Structure + +``` +kotlin-multimodule-template/ +├── service-module/ # Business logic layer +│ └── src/main/kotlin/ +│ └── io/programmernewbie/template/service/ +│ └── ExampleService.kt # Example service implementation +├── springboot-application/ # Application layer +│ └── src/main/kotlin/ +│ └── io/programmernewbie/template/ +│ ├── KotlinMultimoduleTemplateApplication.kt # Main application +│ └── controller/ +│ └── ExampleController.kt # Example REST controller +├── scripts/ # Gradle build scripts +│ ├── kotlin.gradle # Kotlin + allopen plugin config +│ ├── spring_library.gradle # Spring library configuration +│ └── spring_boot.gradle # Spring Boot application config +├── docs/ # Documentation +├── build.gradle # Root build configuration +├── settings.gradle # Module configuration +└── gradle.properties # Dependency versions +``` + +## 🛠️ Development + +### Adding New Modules + +1. Create a new directory for your module +2. Add a `build.gradle` file +3. The module will be automatically included (see `settings.gradle`) + +### Adding Dependencies + +Update `gradle.properties` with version numbers and reference them in module `build.gradle` files. + +### Spring Configuration Tips + +**Services**: Just use `@Service` - no need for `open` keyword: + +```kotlin +@Service +@Transactional +class YourService { + // Spring will create proxies correctly +} +``` + +**Controllers**: Just use `@RestController` - works automatically: + +```kotlin +@RestController +@RequestMapping("/api/your-resource") +class YourController( + private val yourService: YourService // Dependency injection works +) { + // Your endpoints here +} +``` + +### Code Quality + +The template includes: + +- JaCoCo for code coverage +- OWASP dependency check +- License reporting +- Kotlin code style enforcement + +Run quality checks: + +```bash +./gradlew check +./gradlew jacocoTestReport +./gradlew dependencyCheckAnalyze + +# Update dependency locks after adding new dependencies +./gradlew resolveAndLockAll --write-locks +``` + +## 🔧 Configuration + +### Environment Variables + +- `SPRING_PROFILES_ACTIVE`: Set active Spring profiles +- `SERVER_PORT`: Override default port (8080) + +### Application Properties + +Configure in `springboot-application/src/main/resources/application.yml` + +## 📦 Deployment + +### JAR Build + +```bash +./gradlew :springboot-application:bootJar +``` + +### Docker + +```dockerfile +FROM openjdk:17-jre-slim +COPY springboot-application/build/libs/*.jar app.jar +ENTRYPOINT ["java", "-jar", "/app.jar"] +``` + +## 🤝 Contributing + +1. Fork this repository +2. Create your feature branch +3. Commit your changes +4. Push to the branch +5. Create a Pull Request + +## 📝 License + +This project is licensed under the MIT License - see the [LICENSE](LICENSE) file for details. + +## 🆘 Getting Help + +- Check the [HELP.md](HELP.md) for troubleshooting +- Review [CONTRIBUTING.md](CONTRIBUTING.md) for contribution guidelines +- Open an issue for bugs or feature requests + +--- + +**Template Version**: 1.0.0 +**Kotlin Version**: 1.9.25 +**Spring Boot Version**: 3.5.3 diff --git a/SECURITY.md b/SECURITY.md new file mode 100644 index 0000000..9384de6 --- /dev/null +++ b/SECURITY.md @@ -0,0 +1,148 @@ +# Security Policy + +## Supported Versions + +We actively maintain security updates for the following versions: + +| Version | Supported | End of Life | +|---------|--------------------|-------------| +| 1.x.x | :white_check_mark: | TBD | +| 0.x.x | :x: | 2024-12-31 | + +## Security Standards + +This project follows these security best practices: + +- **Dependency Scanning**: Automatic OWASP dependency checks +- **Code Analysis**: CodeQL security analysis on all commits +- **Secret Scanning**: Automated detection of exposed secrets +- **Signed Commits**: All commits must be cryptographically signed +- **Branch Protection**: Main branch requires reviews and status checks + +## Reporting a Vulnerability + +We take security vulnerabilities seriously. Please follow responsible disclosure: + +### 🔒 Private Disclosure (Recommended) + +1. Use + GitHub's [Security Advisories](https://github.com/programmer-newbie-code/kotlin-multimodule-template/security/advisories) +2. Click "Report a vulnerability" +3. Provide detailed information about the vulnerability + +### 📧 Email Disclosure + + +If you prefer email, contact: security@programmer-newbie.io + +- Use PGP encryption if possible +- Include "SECURITY" in the subject line + +### ⚠️ What NOT to do + +- Do not open public issues for security vulnerabilities +- Do not disclose vulnerabilities on social media +- Do not attempt to exploit vulnerabilities on production systems + +## Information to Include + +When reporting a vulnerability, please include: + +- **Description**: Clear explanation of the vulnerability +- **Impact**: Potential security impact and affected components +- **Reproduction**: Step-by-step instructions to reproduce +- **Environment**: Affected versions, configurations, dependencies +- **Fix Suggestions**: Any ideas for remediation (optional) + +## Response Process + +1. **Acknowledgment**: We'll acknowledge receipt within 24 hours +2. **Investigation**: Initial assessment within 72 hours +3. **Updates**: Regular progress updates every 7 days +4. **Resolution**: Security patch and advisory publication +5. **Recognition**: Credit to reporter (if desired) + +## Security Updates + +- Security fixes are prioritized and released as soon as possible +- Critical vulnerabilities may trigger emergency releases +- Security advisories are published for all confirmed vulnerabilities +- Users are notified through GitHub releases and security advisories + +## Scope + +### In Scope + +- Spring Bulk Layered Cache library code +- Build and deployment configurations +- Documentation that could lead to insecure usage + +### Out of Scope + +- Third-party dependencies (report to upstream projects) +- Issues in example applications or demos +- General configuration issues not related to security + +## Bug Bounty + +Currently, we do not offer a formal bug bounty program, but we recognize and credit security researchers who help +improve our project's security. + +## Security Configuration + +### NVD API Key (Optional but Recommended - FREE) + +To speed up OWASP dependency vulnerability scanning, you can optionally configure a **free** NVD API key: + +**⚠️ Without API Key:** + +- Security scans still work perfectly +- Database updates are just **much slower** (5-10 minutes vs 30 seconds) +- You'll see the warning: "An NVD API Key was not provided" +- **No functionality is lost** - just slower performance + +**✅ With Free API Key:** + +- Much faster vulnerability database updates +- Same functionality, better performance +- No cost involved + +**How to get your FREE API key:** + +1. **Visit**: [NVD API Key Request](https://nvd.nist.gov/developers/request-an-api-key) (FREE) +2. **Provide**: Just your email address +3. **Receive**: API key sent instantly to your email +4. **Add to GitHub Secrets**: Go to repository Settings → Secrets and variables → Actions +5. **Add secret**: `NVD_API_KEY` with your API key value + +**Cost**: $0.00 - Completely free service from NIST + +### OWASP Dependency Check + +The security scanning workflow uses OWASP Dependency Check to identify vulnerable dependencies: + +- **Automatic Updates**: Downloads latest vulnerability database +- **Fail Threshold**: Builds fail on High/Critical vulnerabilities (CVSS ≥ 7.0) +- **Suppressions**: Use `owasp-suppressions.xml` to suppress false positives +- **Reports**: Generates HTML and JSON reports for analysis + +**Commands:** + +```bash +# Run security scan +./gradlew dependencyCheckAggregate + +# View report +open build/reports/dependency-check-report.html +``` + +### Vulnerability Management + +1. **Automatic Detection**: Weekly scans create GitHub issues for new vulnerabilities +2. **Dependency Updates**: Automated PRs update vulnerable dependencies +3. **Build Protection**: High/Critical vulnerabilities block PR merges +4. **Suppression**: Use suppressions for false positives or accepted risks + +--- + +Thank you for helping keep Spring Bulk Layered Cache secure! 🔒 diff --git a/TEMPLATE_SETUP.md b/TEMPLATE_SETUP.md new file mode 100644 index 0000000..1846fbd --- /dev/null +++ b/TEMPLATE_SETUP.md @@ -0,0 +1,218 @@ +# Template Setup Guide + +This guide helps you customize the Kotlin Multimodule Template for your specific project. + +## 🎯 Quick Customization Checklist + +### 1. Project Identity + +- [ ] Update `rootProject.name` in `settings.gradle` +- [ ] Update `group` in root `build.gradle` +- [ ] Update GitHub repository URL in `build.gradle` publishing section + +### 2. Package Structure + +- [ ] Replace `io.programmernewbie.template` with your package name in: + - All `.kt` files + - `scanBasePackages` in main application class +- [ ] Update directory structure to match new package name + +### 3. Application Naming + +- [ ] Rename `KotlinMultimoduleTemplateApplication.kt` to match your project +- [ ] Update class name inside the file +- [ ] Update main application class reference + +### 4. Example Code Replacement + +- [ ] Replace `ExampleService` with your business logic +- [ ] Replace `ExampleController` with your REST endpoints +- [ ] Update or remove example endpoints + +## 🛠️ Step-by-Step Customization + +### Step 1: Basic Project Settings + +1. **Update settings.gradle**: + +```gradle +rootProject.name = "your-project-name" +``` + +2. **Update root build.gradle**: + +```gradle +group = "com.yourcompany.yourproject" +``` + +3. **Update publishing URL** (if using GitHub Packages): + +```gradle +url = uri("https://maven.pkg.github.com/your-org/your-repo") +``` + +### Step 2: Package Structure Changes + +**Option A: Manual Update** + +1. Rename directories under `src/main/kotlin/` to match your package +2. Update package declarations in all `.kt` files +3. Update import statements if needed + +**Option B: Use IDE Refactoring** + +1. In IntelliJ IDEA: Right-click package → Refactor → Rename +2. Choose "Rename package" and update all occurrences + +### Step 3: Application Class Updates + +1. **Rename the main application file**: + - From: `KotlinMultimoduleTemplateApplication.kt` + - To: `YourProjectNameApplication.kt` + +2. **Update the class content**: + +```kotlin +@SpringBootApplication( + scanBasePackages = [ + "com.yourcompany.yourproject.service", + "com.yourcompany.yourproject" + ] +) +class YourProjectNameApplication + +fun main(args: Array) { + runApplication(*args) +} +``` + +### Step 4: Replace Example Code + +1. **Service Layer**: Replace `ExampleService` with your business logic: + +```kotlin +@Service +@Transactional +class YourBusinessService { + // Your actual business methods here +} +``` + +2. **Controller Layer**: Replace `ExampleController` with your REST endpoints: + +```kotlin +@RestController +@RequestMapping("/api/your-resource") +class YourResourceController( + private val yourBusinessService: YourBusinessService +) { + // Your actual REST endpoints here +} +``` + +## 🔧 Advanced Customization + +### Adding New Modules + +1. Create new directory: `your-new-module/` +2. Add `build.gradle` file +3. Create `src/main/kotlin/` structure +4. Module will be auto-discovered by settings.gradle + +### Database Configuration + +1. **For PostgreSQL**: + +```gradle +// In springboot-application/build.gradle +runtimeOnly "org.postgresql:postgresql" +``` + +2. **For MySQL**: + +```gradle +// In springboot-application/build.gradle +runtimeOnly "com.mysql:mysql-connector-j" +``` + +3. **Update application.yml**: + +```yaml +spring: + datasource: + url: jdbc:postgresql://localhost:5432/yourdb + username: ${DB_USER:user} + password: ${DB_PASSWORD:password} +``` + +### Adding Security + +1. **Add Spring Security dependency**: + +```gradle +implementation "org.springframework.boot:spring-boot-starter-security" +``` + +2. **Create security configuration**: + +```kotlin +@Configuration +@EnableWebSecurity +class SecurityConfig { + // Your security configuration +} +``` + +## 📋 Verification Steps + +After customization, verify everything works: + +1. **Build successfully**: + +```bash +./gradlew build +``` + +2. **Run application**: + +```bash +./gradlew :springboot-application:bootRun +``` + +3. **Test endpoints**: + +```bash +curl http://localhost:8080/api/your-resource/endpoint +``` + +4. **Run tests**: + +```bash +./gradlew test +``` + +## 🚨 Common Issues + +### Build Failures + +- Check package names match directory structure +- Verify all imports are updated +- Ensure gradle.properties versions are correct + +### Runtime Issues + +- Verify scanBasePackages includes your service packages +- Check Spring Boot auto-configuration +- Review application logs for configuration errors + +### IDE Issues + +- Refresh Gradle project after changes +- Invalidate caches and restart if needed +- Check Project Structure settings + +## 📞 Need Help? + +- Check existing issues in the template repository +- Create a new issue with your specific problem +- Include error messages and configuration details diff --git a/TEMPLATE_USAGE.md b/TEMPLATE_USAGE.md new file mode 100644 index 0000000..c90d9ce --- /dev/null +++ b/TEMPLATE_USAGE.md @@ -0,0 +1,146 @@ +# 🚀 Using This Template + +This repository is configured as a **GitHub Template Repository**. Here's how to use it effectively: + +## 📋 Quick Start Guide + +### Step 1: Create Repository from Template + +1. Click the **"Use this template"** button on GitHub +2. Choose **"Create a new repository"** +3. Fill in your repository details: + - Repository name (e.g., `my-awesome-service`) + - Description + - Public/Private visibility +4. Click **"Create repository from template"** + +### Step 2: Clone Your New Repository + +```bash +git clone https://github.com/YOUR-USERNAME/YOUR-REPO-NAME.git +cd YOUR-REPO-NAME +``` + +### Step 3: Customize the Template + +Choose one of these methods: + +#### Option A: Automated Customization (Recommended) + +```bash +# For Linux/Mac +./customize.sh + +# For Windows PowerShell +./customize.ps1 +``` + +The script will prompt you for: + +- **Project name** (e.g., "my-awesome-service") +- **Organization domain** (e.g., "com.mycompany") +- **GitHub organization** (e.g., "my-org") + +#### Option B: Manual Customization + +See [TEMPLATE_SETUP.md](TEMPLATE_SETUP.md) for detailed manual instructions. + +### Step 4: Verify Setup + +```bash +# Build the project +./gradlew build + +# Run fast tests +./gradlew test -PtestGroups=small + +# Run the application +./gradlew :springboot-application:bootRun + +# Test your endpoints +curl http://localhost:8080/api/example/health +``` + +## 🎯 What This Template Provides + +### 🏗️ **Architecture** + +- **2-module structure**: `service-module` (business logic) + `springboot-application` (REST API) +- **Spring Boot 3.5.3** with Kotlin 1.9.25 +- **Spring-Kotlin integration** (allopen plugin configured) + +### 🧪 **Testing Strategy** + +- **TestNG with groups**: `@Test(groups = ["small"])` for fast tests +- **85% coverage requirement** enforced by JaCoCo +- **MockK integration** for Kotlin-friendly mocking +- **Test naming convention**: `fun \`function, condition, expectation\`()` + +### 📦 **Build & Dependencies** + +- **Gradle dependency locking** for reproducible builds +- **Version centralization** in `gradle.properties` +- **Multi-module build** with shared configuration scripts + +### 🔧 **Development Tools** + +- **Code quality**: OWASP dependency scanning, license reporting +- **CI/CD ready**: GitHub Actions workflows included +- **Documentation**: Comprehensive GitHub Pages docs + +## 🛠️ **Customization Points** + +After using the template, you'll typically want to customize: + +### 1. **Package Structure** + +``` +FROM: io.programmernewbie.template +TO: com.yourcompany.yourproject +``` + +### 2. **Project Identity** + +- Repository name +- Artifact names +- Main application class name +- GitHub URLs for publishing + +### 3. **Business Logic** + +- Replace `ExampleService` with your services +- Replace `ExampleController` with your REST endpoints +- Add your domain models and repositories + +### 4. **Dependencies** + +- Add your specific dependencies to `gradle.properties` +- Update module `build.gradle` files as needed + +## 📚 **Next Steps After Customization** + +1. **Remove example code**: Delete `ExampleService` and `ExampleController` +2. **Add your business logic**: Implement your actual services and controllers +3. **Configure database**: Update `application.yml` for your database +4. **Add authentication**: Implement security if needed +5. **Update documentation**: Customize README.md for your project + +## 🆘 **Getting Help** + +- 📖 **Full documentation**: Check the `docs/` folder or GitHub Pages +- 🐛 **Issues**: Report problems in the template repository +- 💬 **Discussions**: Ask questions in GitHub Discussions +- 📧 **Contributing**: See [CONTRIBUTING.md](CONTRIBUTING.md) + +## ✅ **Template Validation** + +This template includes automated validation to ensure it works correctly: + +- ✅ Builds successfully out of the box +- ✅ All tests pass with 85% coverage +- ✅ Customization scripts work properly +- ✅ All required files are present + +--- + +**Happy coding! 🎉** Your Kotlin multimodule project is ready for development. diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000..d807fa5 --- /dev/null +++ b/build.gradle @@ -0,0 +1,92 @@ +plugins { + id "org.jetbrains.kotlin.jvm" version "${kotlin_version}" apply false + id "org.jetbrains.kotlin.plugin.spring" version "${kotlin_version}" apply false + id "org.jetbrains.kotlin.plugin.allopen" version "${kotlin_version}" apply false + id "org.springframework.boot" version "${spring_boot_version}" apply false + id "io.spring.dependency-management" version "${spring_dependency_mgmt_version}" apply false + id "maven-publish" + id "jacoco" // Add JaCoCo plugin to the root project + id "org.owasp.dependencycheck" version "${owasp_dependency_check_version}" + id "com.github.jk1.dependency-license-report" version "${license_report_version}" + id "me.champeau.jmh" version "${jmh_version}" apply false + id "com.github.ben-manes.versions" version "${gradle_versions_version}" +} + +apply plugin: "java" +apply from: "$rootDir/scripts/gradle/tasks.gradle" + +// TODO: Change this to your organization's group ID +// Example: io.programmernewbie.yourproject +group = "io.programmernewbie.template" + +// Java toolchain setup should be inside subprojects that apply Java plugin +allprojects { + // Repositories are already configured in settings.gradle + // Remove redundant repository configuration to fix warning + + // Enable dependency locking for all configurations + dependencyLocking { + lockAllConfigurations() + lockMode = LockMode.STRICT // Forces all dependencies to be locked + } +} + +subprojects { + apply from: "$rootDir/scripts/gradle/kotlin.gradle" + apply from: "$rootDir/scripts/gradle/testing.gradle" // Apply testing configuration + // Apply JMH performance testing configuration + apply from: "$rootDir/scripts/gradle/jmh.gradle" + apply plugin: "maven-publish" + + // Only apply JaCoCo to modules not excluded from coverage + if (!project.rootProject.hasProperty('modulesExcludedFromCoverage') || + !project.rootProject.modulesExcludedFromCoverage.contains(project.name)) { + apply from: "$rootDir/scripts/gradle/jacoco.gradle" + } + + // This ensures each subproject processes its dependencies + configurations.all { + resolutionStrategy { + activateDependencyLocking() + } + } + + // Only configure publishing for projects with source code + if (file("$projectDir/src").exists()) { + publishing { + publications { + maven(MavenPublication) { + groupId = project.group + artifactId = project.name + version = project.version ?: '0.0.1-SNAPSHOT' + + from components.java + } + } + + repositories { + maven { + name = "GitHubPackages" + // TODO: Change this to your organization's GitHub repository URL + url = uri("https://maven.pkg.github.com/programmer-newbie-code/kotlin-multimodule-template") + credentials { + username = System.getenv("GITHUB_ACTOR") ?: project.findProperty("gpr.user") ?: "" + password = System.getenv("GITHUB_TOKEN") ?: project.findProperty("gpr.key") ?: "" + } + } + } + } + } +} + +ext { + license = "MIT" +} + +// Apply configuration scripts for various tools +apply from: "$rootDir/scripts/gradle/dependency-check.gradle" +apply from: "$rootDir/scripts/gradle/license-report.gradle" +apply from: "$rootDir/scripts/gradle/dependency-updates.gradle" + +// Apply JaCoCo script to root project to get the aggregate tasks +apply from: "$rootDir/scripts/gradle/jacoco.gradle" diff --git a/cliff.toml b/cliff.toml new file mode 100644 index 0000000..6b8d624 --- /dev/null +++ b/cliff.toml @@ -0,0 +1,159 @@ +[changelog] +# changelog header +header = """ +# 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). + +""" +# template for the changelog body +body = """ +{% if version %}\ + ## [{{ version | trim_start_matches(pat="v") }}] - {{ timestamp | date(format="%Y-%m-%d") }} +{% else %}\ + ## [Unreleased] +{% endif %}\ +{% for group, commits in commits | group_by(attribute="group") %} + ### {{ group }} + {% for commit in commits %} + - {% if commit.scope %}**{{ commit.scope }}**: {% endif %}{{ commit.message | upper_first }}{% if commit.breaking %} **[BREAKING]**{% endif %}{% if commit.github.pr_number %} ([#{{ commit.github.pr_number }}](https://github.com/{{ remote.github.owner }}/{{ remote.github.repo }}/pull/{{ commit.github.pr_number }})){% endif %}\ + {% endfor %} +{% endfor %}\n +""" +# remove the leading and trailing whitespace from the template +trim = true +# changelog footer +footer = """ + +""" + +[git] +# parse the commits based on https://www.conventionalcommits.org +conventional_commits = true +# filter out the commits that are not conventional +filter_unconventional = true +# process each line of a commit as an individual commit +split_commits = false +# regex for preprocessing the commit messages +commit_preprocessors = [ + # Clean up scope formatting + { pattern = '\((\w+)\)', replace = "($1)" }, + # Handle breaking changes + { pattern = '!:', replace = ":" }, +] +# regex for parsing and grouping commits +commit_parsers = [ + # Features and enhancements + { message = "^feat", group = "🚀 Features" }, + { message = "^feature", group = "🚀 Features" }, + { message = "^add", group = "🚀 Features" }, + { message = "^implement", group = "🚀 Features" }, + # Bug fixes + { message = "^fix", group = "🐛 Bug Fixes" }, + { message = "^bug", group = "🐛 Bug Fixes" }, + { message = "^resolve", group = "🐛 Bug Fixes" }, + { message = "^patch", group = "🐛 Bug Fixes" }, + # Performance improvements + { message = "^perf", group = "⚡ Performance" }, + { message = "^performance", group = "⚡ Performance" }, + { message = "^optimize", group = "⚡ Performance" }, + { message = "^speed", group = "⚡ Performance" }, + # Security fixes + { message = "^security", group = "🔒 Security" }, + { message = "^sec", group = "🔒 Security" }, + { body = ".*security", group = "🔒 Security" }, + { body = ".*vulnerability", group = "🔒 Security" }, + # Refactoring + { message = "^refactor", group = "♻️ Refactoring" }, + { message = "^refact", group = "♻️ Refactoring" }, + { message = "^cleanup", group = "♻️ Refactoring" }, + { message = "^clean", group = "♻️ Refactoring" }, + { message = "^restructure", group = "♻️ Refactoring" }, + # Documentation + { message = "^docs?", group = "📚 Documentation" }, + { message = "^doc", group = "📚 Documentation" }, + { message = "^documentation", group = "📚 Documentation" }, + { message = "^readme", group = "📚 Documentation" }, + { message = "^comment", group = "📚 Documentation" }, + # Testing + { message = "^test", group = "🧪 Testing" }, + { message = "^tests", group = "🧪 Testing" }, + { message = "^testing", group = "🧪 Testing" }, + { message = "^spec", group = "🧪 Testing" }, + { message = "^unit", group = "🧪 Testing" }, + { message = "^integration", group = "🧪 Testing" }, + # Build and CI/CD + { message = "^build", group = "🔧 Build System" }, + { message = "^gradle", group = "🔧 Build System" }, + { message = "^maven", group = "🔧 Build System" }, + { message = "^ci", group = "👷 CI/CD" }, + { message = "^workflow", group = "👷 CI/CD" }, + { message = "^github", group = "👷 CI/CD" }, + { message = "^action", group = "👷 CI/CD" }, + # Code style and formatting + { message = "^style", group = "💄 Styling" }, + { message = "^format", group = "💄 Styling" }, + { message = "^lint", group = "💄 Styling" }, + { message = "^prettier", group = "💄 Styling" }, + # Reverts + { message = "^revert", group = "⏪ Reverts" }, + { message = "^rollback", group = "⏪ Reverts" }, + { message = "^undo", group = "⏪ Reverts" }, + # Dependencies and package management + { message = "^chore\\(deps\\)", group = "📦 Dependencies" }, + { message = "^deps", group = "📦 Dependencies" }, + { message = "^dependencies", group = "📦 Dependencies" }, + { message = "^dependency", group = "📦 Dependencies" }, + { message = "^update.*depend", group = "📦 Dependencies" }, + { message = "^bump", group = "📦 Dependencies" }, + { message = "^upgrade", group = "📦 Dependencies" }, + # Configuration changes + { message = "^config", group = "⚙️ Configuration" }, + { message = "^configuration", group = "⚙️ Configuration" }, + { message = "^settings", group = "⚙️ Configuration" }, + { message = "^properties", group = "⚙️ Configuration" }, + # Initial commits and major changes + { message = "^initial", group = "🎉 Initial" }, + { message = "^init", group = "🎉 Initial" }, + { message = "^first", group = "🎉 Initial" }, + { message = "^create", group = "🎉 Initial" }, + { message = "^setup", group = "🎉 Initial" }, + # Release and version related + { message = "^chore\\(release\\): prepare for", skip = true }, + { message = "^release", group = "🏷️ Release" }, + { message = "^version", group = "🏷️ Release" }, + { message = "^tag", group = "🏷️ Release" }, + # Skip certain patterns (keep these towards the end) + { message = "^docs: update.*badges.*\\[skip ci\\]", skip = true }, + { message = ".*\\[skip ci\\]", skip = true }, + { message = "^merge", skip = true }, + { message = "^wip", skip = true }, + { message = "^work in progress", skip = true }, + # Chores and maintenance (broad catch-all) + { message = "^chore", group = "🔨 Maintenance" }, + { message = "^maintenance", group = "🔨 Maintenance" }, + { message = "^maint", group = "🔨 Maintenance" }, + { message = "^housekeeping", group = "🔨 Maintenance" }, + # Catch-all for any remaining commits (this should be last) + { message = ".*", group = "📝 Other Changes" }, +] +# protect breaking changes from being skipped due to matching a skipping commit_parser +protect_breaking_commits = true +# filter out the commits that are not matched by commit parsers +filter_commits = false # Changed to false to include old commits +# glob pattern for matching git tags +tag_pattern = "v[0-9]*" +# regex for skipping tags +skip_tags = ".*-dev$" +# regex for ignoring tags +ignore_tags = "" +# sort the tags topologically +topo_order = false +# sort the commits inside sections by oldest/newest order +sort_commits = "oldest" + +# Git-cliff will automatically detect GitHub remote information +# No need to manually specify [remote.github] section diff --git a/customize.ps1 b/customize.ps1 new file mode 100644 index 0000000..98fee11 --- /dev/null +++ b/customize.ps1 @@ -0,0 +1,119 @@ +# Kotlin Multimodule Template Customization Script (PowerShell) +# This script helps you quickly customize the template for your project + +param( + [string]$ProjectName, + [string]$OrganizationDomain, + [string]$GitHubOrg +) + +Write-Host "🚀 Kotlin Multimodule Template Customization" -ForegroundColor Green +Write-Host "==============================================`n" -ForegroundColor Green + +# Get user input if not provided as parameters +if (-not $ProjectName) { + $ProjectName = Read-Host "Enter your project name (e.g., my-awesome-project)" +} +if (-not $OrganizationDomain) { + $OrganizationDomain = Read-Host "Enter your organization domain (e.g., com.mycompany)" +} +if (-not $GitHubOrg) { + $GitHubOrg = Read-Host "Enter your GitHub organization/username" +} + +# Validate inputs +if (-not $ProjectName -or -not $OrganizationDomain -or -not $GitHubOrg) { + Write-Host "❌ Error: All fields are required" -ForegroundColor Red + exit 1 +} + +# Convert project name to appropriate formats +$ProjectNameKebab = $ProjectName.ToLower() -replace '[^a-z0-9]', '-' -replace '--+', '-' -replace '^-|-$', '' +$ProjectNameCamel = (Get-Culture).TextInfo.ToTitleCase($ProjectName -replace '[^a-zA-Z0-9]', ' ') -replace ' ', '' +$PackageName = "$OrganizationDomain.$ProjectNameKebab" +$PackagePath = $PackageName -replace '\.', '/' + +Write-Host "`n📋 Configuration Summary:" -ForegroundColor Yellow +Write-Host " Project Name: $ProjectName" +Write-Host " Kebab Case: $ProjectNameKebab" +Write-Host " Camel Case: $ProjectNameCamel" +Write-Host " Package Name: $PackageName" +Write-Host " GitHub Org: $GitHubOrg`n" + +$Confirm = Read-Host "Continue with these settings? (y/N)" +if ($Confirm -ne "y" -and $Confirm -ne "Y") { + Write-Host "❌ Customization cancelled" -ForegroundColor Red + exit 0 +} + +Write-Host "`n🔄 Starting customization..." -ForegroundColor Green + +try { + # Update settings.gradle + Write-Host "📝 Updating settings.gradle..." + (Get-Content "settings.gradle") -replace "kotlin-multimodule-template", $ProjectNameKebab | Set-Content "settings.gradle" + + # Update root build.gradle + Write-Host "📝 Updating root build.gradle..." + (Get-Content "build.gradle") -replace "io\.programmernewbie\.template", $PackageName | Set-Content "build.gradle" + (Get-Content "build.gradle") -replace "programmer-newbie-code/kotlin-multimodule-template", "$GitHubOrg/$ProjectNameKebab" | Set-Content "build.gradle" + + # Create new package structure + Write-Host "📁 Creating new package structure..." + $OldPackagePath = "io/programmernewbie/template" + + # Service module + $ServiceNewPath = "service-module/src/main/kotlin/$PackagePath/service" + New-Item -ItemType Directory -Path $ServiceNewPath -Force | Out-Null + if (Test-Path "service-module/src/main/kotlin/$OldPackagePath/service") { + Copy-Item "service-module/src/main/kotlin/$OldPackagePath/service/*" $ServiceNewPath -Force -ErrorAction SilentlyContinue + } + + # SpringBoot application + $AppNewPath = "springboot-application/src/main/kotlin/$PackagePath" + $ControllerNewPath = "$AppNewPath/controller" + New-Item -ItemType Directory -Path $AppNewPath -Force | Out-Null + New-Item -ItemType Directory -Path $ControllerNewPath -Force | Out-Null + + if (Test-Path "springboot-application/src/main/kotlin/$OldPackagePath") { + Copy-Item "springboot-application/src/main/kotlin/$OldPackagePath/*" $AppNewPath -Force -ErrorAction SilentlyContinue + if (Test-Path "springboot-application/src/main/kotlin/$OldPackagePath/controller") { + Copy-Item "springboot-application/src/main/kotlin/$OldPackagePath/controller/*" $ControllerNewPath -Force -ErrorAction SilentlyContinue + } + } + + # Update package declarations in Kotlin files + Write-Host "📝 Updating package declarations..." + Get-ChildItem -Path . -Filter "*.kt" -Recurse | ForEach-Object { + (Get-Content $_.FullName) -replace "package io\.programmernewbie\.template", "package $PackageName" | Set-Content $_.FullName + (Get-Content $_.FullName) -replace "import io\.programmernewbie\.template", "import $PackageName" | Set-Content $_.FullName + (Get-Content $_.FullName) -replace "`"io\.programmernewbie\.template\.service`"", "`"$PackageName.service`"" | Set-Content $_.FullName + (Get-Content $_.FullName) -replace "`"io\.programmernewbie\.template`"", "`"$PackageName`"" | Set-Content $_.FullName + } + + # Rename main application class + $OldAppFile = "springboot-application/src/main/kotlin/$PackagePath/KotlinMultimoduleTemplateApplication.kt" + $NewAppFile = "springboot-application/src/main/kotlin/$PackagePath/${ProjectNameCamel}Application.kt" + + if (Test-Path $OldAppFile) { + Move-Item $OldAppFile $NewAppFile -Force + (Get-Content $NewAppFile) -replace "KotlinMultimoduleTemplateApplication", "${ProjectNameCamel}Application" | Set-Content $NewAppFile + } + + # Remove old package directories + Write-Host "🧹 Cleaning up old package structure..." + Remove-Item "service-module/src/main/kotlin/$OldPackagePath" -Recurse -Force -ErrorAction SilentlyContinue + Remove-Item "springboot-application/src/main/kotlin/$OldPackagePath" -Recurse -Force -ErrorAction SilentlyContinue + + Write-Host "`n✅ Customization completed successfully!" -ForegroundColor Green + Write-Host "`n🔧 Next steps:" -ForegroundColor Yellow + Write-Host "1. Build the project: ./gradlew build" + Write-Host "2. Run the application: ./gradlew :springboot-application:bootRun" + Write-Host "3. Test the endpoints: curl http://localhost:8080/api/example/health" + Write-Host "4. Start building your application!" + Write-Host "`n📚 See TEMPLATE_SETUP.md for detailed customization guide" + +} catch { + Write-Host "❌ Error during customization: $($_.Exception.Message)" -ForegroundColor Red + exit 1 +} diff --git a/customize.sh b/customize.sh new file mode 100644 index 0000000..82d2a33 --- /dev/null +++ b/customize.sh @@ -0,0 +1,105 @@ +#!/bin/bash + +# Kotlin Multimodule Template Customization Script +# This script helps you quickly customize the template for your project + +set -e + +echo "🚀 Kotlin Multimodule Template Customization" +echo "==============================================" + +# Get user input +read -p "Enter your project name (e.g., my-awesome-project): " PROJECT_NAME +read -p "Enter your organization domain (e.g., com.mycompany): " ORGANIZATION_DOMAIN +read -p "Enter your GitHub organization/username: " GITHUB_ORG + +# Validate inputs +if [[ -z "$PROJECT_NAME" || -z "$ORGANIZATION_DOMAIN" || -z "$GITHUB_ORG" ]]; then + echo "❌ Error: All fields are required" + exit 1 +fi + +# Convert project name to appropriate formats +PROJECT_NAME_KEBAB=$(echo "$PROJECT_NAME" | tr '[:upper:]' '[:lower:]' | sed 's/[^a-z0-9]/-/g' | sed 's/--*/-/g' | sed 's/^-\|-$//g') +PROJECT_NAME_CAMEL=$(echo "$PROJECT_NAME" | sed 's/[^a-zA-Z0-9]/ /g' | sed 's/\b\w/\U&/g' | sed 's/ //g') +PACKAGE_NAME="$ORGANIZATION_DOMAIN.$PROJECT_NAME_KEBAB" +PACKAGE_PATH=$(echo "$PACKAGE_NAME" | tr '.' '/') + +echo "" +echo "📋 Configuration Summary:" +echo " Project Name: $PROJECT_NAME" +echo " Kebab Case: $PROJECT_NAME_KEBAB" +echo " Camel Case: $PROJECT_NAME_CAMEL" +echo " Package Name: $PACKAGE_NAME" +echo " GitHub Org: $GITHUB_ORG" +echo "" + +read -p "Continue with these settings? (y/N): " CONFIRM +if [[ "$CONFIRM" != "y" && "$CONFIRM" != "Y" ]]; then + echo "❌ Customization cancelled" + exit 0 +fi + +echo "" +echo "🔄 Starting customization..." + +# Update settings.gradle +echo "📝 Updating settings.gradle..." +sed -i.bak "s/kotlin-multimodule-template/$PROJECT_NAME_KEBAB/g" settings.gradle + +# Update root build.gradle +echo "📝 Updating root build.gradle..." +sed -i.bak "s/io\.programmernewbie\.template/$PACKAGE_NAME/g" build.gradle +sed -i.bak "s/programmer-newbie-code\/kotlin-multimodule-template/$GITHUB_ORG\/$PROJECT_NAME_KEBAB/g" build.gradle + +# Update gradle.properties (no changes needed for basic setup) + +# Create new package structure +echo "📁 Creating new package structure..." +OLD_PACKAGE_PATH="io/programmernewbie/template" + +# Service module +mkdir -p "service-module/src/main/kotlin/$PACKAGE_PATH/service" +cp "service-module/src/main/kotlin/$OLD_PACKAGE_PATH/service/"* "service-module/src/main/kotlin/$PACKAGE_PATH/service/" 2>/dev/null || true + +# SpringBoot application +mkdir -p "springboot-application/src/main/kotlin/$PACKAGE_PATH/controller" +cp "springboot-application/src/main/kotlin/$OLD_PACKAGE_PATH/"* "springboot-application/src/main/kotlin/$PACKAGE_PATH/" 2>/dev/null || true +cp "springboot-application/src/main/kotlin/$OLD_PACKAGE_PATH/controller/"* "springboot-application/src/main/kotlin/$PACKAGE_PATH/controller/" 2>/dev/null || true + +# Update package declarations in Kotlin files +echo "📝 Updating package declarations..." +find . -name "*.kt" -type f -exec sed -i.bak "s/package io\.programmernewbie\.template/package $PACKAGE_NAME/g" {} \; +find . -name "*.kt" -type f -exec sed -i.bak "s/import io\.programmernewbie\.template/import $PACKAGE_NAME/g" {} \; + +# Update scanBasePackages +find . -name "*.kt" -type f -exec sed -i.bak "s/\"io\.programmernewbie\.template\.service\"/\"$PACKAGE_NAME.service\"/g" {} \; +find . -name "*.kt" -type f -exec sed -i.bak "s/\"io\.programmernewbie\.template\"/\"$PACKAGE_NAME\"/g" {} \; + +# Rename main application class +OLD_APP_FILE="springboot-application/src/main/kotlin/$PACKAGE_PATH/KotlinMultimoduleTemplateApplication.kt" +NEW_APP_FILE="springboot-application/src/main/kotlin/$PACKAGE_PATH/${PROJECT_NAME_CAMEL}Application.kt" + +if [[ -f "$OLD_APP_FILE" ]]; then + mv "$OLD_APP_FILE" "$NEW_APP_FILE" + sed -i.bak "s/KotlinMultimoduleTemplateApplication/${PROJECT_NAME_CAMEL}Application/g" "$NEW_APP_FILE" +fi + +# Remove old package directories +echo "🧹 Cleaning up old package structure..." +rm -rf "service-module/src/main/kotlin/$OLD_PACKAGE_PATH" 2>/dev/null || true +rm -rf "springboot-application/src/main/kotlin/$OLD_PACKAGE_PATH" 2>/dev/null || true + +# Clean up backup files +find . -name "*.bak" -delete + +echo "" +echo "✅ Customization completed successfully!" +echo "" +echo "🔧 Next steps:" +echo "1. Build the project: ./gradlew build" +echo "2. Run the application: ./gradlew :springboot-application:bootRun" +echo "3. Test the endpoints: curl http://localhost:8080/api/example/health" +echo "4. Start building your application!" +echo "" +echo "📚 See TEMPLATE_SETUP.md for detailed customization guide" diff --git a/docs/_config.yml b/docs/_config.yml new file mode 100644 index 0000000..3b50f30 --- /dev/null +++ b/docs/_config.yml @@ -0,0 +1,33 @@ +remote_theme: just-the-docs/just-the-docs +title: Kotlin Multimodule Template +description: A starter template for building modular Kotlin applications +baseurl: "/kotlin-multimodule-template" +url: "https://programmer-newbie-code.github.io" + +# Enable search +search_enabled: true + +# Enable copy code button +enable_copy_code_button: true + +# Color scheme +color_scheme: light + +# Aux links for the upper right navigation +aux_links: + "GitHub Repository": + - "https://github.com/programmer-newbie-code/kotlin-multimodule-template" + +# Footer content +footer_content: "Copyright © 2025 Programmer Newbie IO. Distributed under the MIT License." + +# Collections for API documentation +collections: + docs: + permalink: "/:collection/:path/" + output: true + +just_the_docs: + collections: + docs: + name: Documentation diff --git a/docs/api/core-module.md b/docs/api/core-module.md new file mode 100644 index 0000000..e9adafb --- /dev/null +++ b/docs/api/core-module.md @@ -0,0 +1,49 @@ +--- +layout: default +title: Core Module +parent: API Reference +nav_order: 1 +--- + +# Core Module + +The `core-module` provides the foundational interfaces and classes for your application. + +## Key Components + +### Core Interfaces + +The module defines key interfaces that represent the domain model: + +- `Repository` - Data access layer interface +- `Service` - Business logic layer interface +- `Model` - Domain model interfaces + +### Usage Example + +```kotlin +// Example service implementation +class UserServiceImpl( + private val userRepository: UserRepository +) : UserService { + + override fun getUser(id: String): User? { + return userRepository.findById(id) + } + + override fun createUser(user: User): User { + return userRepository.save(user) + } +} +``` + +## Module Dependencies + +To use the core module, include the dependency: + +```kotlin +implementation(project(":core-module")) +``` + +This module should be kept free of implementation dependencies and should only define the contracts that other modules +implement. diff --git a/docs/branch-protection.md b/docs/branch-protection.md new file mode 100644 index 0000000..3cc7930 --- /dev/null +++ b/docs/branch-protection.md @@ -0,0 +1,51 @@ +# Branch Protection Rule Configuration + +This document describes the branch protection rules configured for this repository. + +## Main Branch Protection + +The following protections are in place for the `main` branch: + +### Required Rules + +- **Require a pull request before merging** + - At least 1 approval is required before merging + - Dismiss stale pull request approvals when new commits are pushed + - Require review from Code Owners + +- **Require status checks to pass before merging** + - Required status checks: + - Build and test workflow + - Code coverage minimum threshold (70%) + +- **Require conversation resolution before merging** + - All conversations must be resolved before a PR can be merged + +- **Require signed commits** + - All commits must be signed with a verified signature + +- **Require linear history** + - Prevents merge commits, ensuring a clean, linear git history + +- **Do not allow bypassing the above settings** + - These rules apply to all contributors including administrators + +## Development Branch Protection + +For `dev` branches, the following protections apply: + +- **Require signed commits** +- **Require status checks to pass** + +## Implementation Guide for Repository Administrators + +To implement these rules: + +1. Go to the repository Settings +2. Navigate to "Branches" +3. Click "Add rule" or edit an existing rule +4. Configure the protections as described above +5. Save changes + +These protection rules ensure code quality, maintain security through signed commits, and establish a structured +contribution workflow. diff --git a/docs/code-quality.md b/docs/code-quality.md new file mode 100644 index 0000000..846abe8 --- /dev/null +++ b/docs/code-quality.md @@ -0,0 +1,315 @@ +--- +layout: default +title: Code Quality +nav_order: 7 +--- + +# Code Quality & Coverage Standards + +This template enforces strict code quality standards including **85% minimum test coverage** to ensure maintainable, +reliable code. + +## 📊 Coverage Requirements + +### Minimum Coverage Standards + +- **Overall Project Coverage**: 85% instruction and branch coverage +- **Individual Class Coverage**: 85% instruction coverage +- **Method Coverage**: 76.5% instruction coverage (90% of overall requirement) + +### Coverage Verification + +```bash +# Run tests with coverage verification +./gradlew test jacocoTestCoverageVerification + +# Generate coverage reports +./gradlew jacocoTestReport + +# Aggregate coverage across all modules +./gradlew jacocoRootReport +``` + +### Coverage Reports + +- **HTML Report**: `build/reports/jacoco/test/html/index.html` +- **XML Report**: `build/reports/jacoco/test/jacocoTestReport.xml` (for CI tools) +- **CSV Report**: `build/reports/jacoco/test/jacocoTestReport.csv` (for badges) +- **Aggregate Report**: `build/reports/jacoco/aggregate/index.html` + +## 🎯 What's Excluded from Coverage + +### Automatically Excluded Classes + +The template intelligently excludes boilerplate code that doesn't require testing: + +```groovy +// Configuration and framework classes +'**/*Config*', '**/*Configuration*', '*Application*' + +// Data classes and DTOs +'*.dto.*', '*.entity.*', '*.model.*' + +// Exception classes (definition only) +'*.exception.*', '**/*Exception*' + +// Kotlin-generated code +'**/*\$Companion*', '**/*\$WhenMappings*' + +// Spring auto-configuration +'**/*AutoConfiguration*', '**/autoconfigure/**' +``` + +### Smart Exclusions + +- **Properties classes**: Configuration binding classes +- **Builder patterns**: Often boilerplate code +- **Generated code**: Serializers, creators, etc. +- **Framework classes**: Spring context and configuration + +## 🧪 Coverage-Driven Testing Strategy + +### Test Organization for Coverage + +``` +src/test/kotlin/ +├── unit/small/ # Fast tests (< 100ms) - High coverage impact +├── unit/medium/ # Tests with mocks (< 1s) - Medium coverage +├── integration/ # Full context tests - Lower coverage priority +└── performance/ # JMH benchmarks - Excluded from coverage +``` + +### Writing Tests for 85% Coverage + +**Example Service with Full Coverage:** + +```kotlin +@Service +@Transactional +class ExampleService { + fun getWelcomeMessage(name: String = "World"): String { + return "Hello, $name! This is your Kotlin Multimodule Template." + } + + suspend fun getAsyncWelcomeMessage(name: String = "World"): String { + kotlinx.coroutines.delay(100) + return "Hello async, $name! This is your Kotlin Multimodule Template." + } +} +``` + +**Comprehensive Test Coverage:** + +```kotlin +class ExampleServiceUnitTest { + @Test + fun getWelcomeMessage_WithCustomName_ReturnsPersonalizedMessage() { + // Covers: main path with parameter + } + + @Test + fun getWelcomeMessage_WithDefaultName_ReturnsDefaultMessage() { + // Covers: default parameter branch + } + + @Test + fun getWelcomeMessage_WithEmptyString_ReturnsMessageWithEmptyName() { + // Covers: edge case handling + } + + @Test + fun getAsyncWelcomeMessage_WithCustomName_ReturnsAsyncMessage() { + // Covers: async method main path + } + + @Test + fun getAsyncWelcomeMessage_ExecutionTime_CompletesWithinReasonableTime() { + // Covers: async delay behavior + } +} +``` + +## 🚦 Coverage Quality Gates + +### Build Integration + +```yaml +# GitHub Actions example +- name: Run Tests with Coverage + run: ./gradlew test jacocoTestCoverageVerification + +- name: Upload Coverage Reports + uses: codecov/codecov-action@v3 + with: + file: build/reports/jacoco/test/jacocoTestReport.xml +``` + +### Local Development + +```bash +# Quick coverage check during development +./gradlew test jacocoTestReport + +# View coverage in browser +open build/reports/jacoco/test/html/index.html + +# Verify coverage meets standards +./gradlew jacocoTestCoverageVerification +``` + +### Coverage Failure Handling + +When coverage falls below 85%: + +1. **Identify uncovered code**: + +```bash +# Generate detailed report +./gradlew jacocoTestReport --info +``` + +2. **Add targeted tests**: + - Focus on uncovered branches + - Test edge cases and error paths + - Ensure all public methods are tested + +3. **Review exclusions**: + - Verify excluded classes are actually boilerplate + - Consider if complex logic needs coverage + +## 📈 Coverage Best Practices + +### 1. Write Testable Code + +```kotlin +// ✅ Good: Testable with clear dependencies +@Service +class UserService(private val repository: UserRepository) { + fun createUser(userData: UserData): User { + return repository.save(User(userData)) + } +} + +// ❌ Avoid: Hard to test with static dependencies +@Service +class UserService { + fun createUser(userData: UserData): User { + return StaticRepository.save(User(userData)) + } +} +``` + +### 2. Test Both Success and Failure Paths + +```kotlin +@Test +fun createUser_WithValidData_ReturnsUser() { + // Test happy path +} + +@Test +fun createUser_WithInvalidData_ThrowsValidationException() { + // Test error path - important for coverage! +} +``` + +### 3. Cover Edge Cases + +```kotlin +@Test +fun processInput_WithEmptyString_HandlesGracefully() { + // Edge case coverage +} + +@Test +fun processInput_WithNullValue_ThrowsAppropriateException() { + // Null handling coverage +} +``` + +### 4. Mock External Dependencies + +```kotlin +@Test +fun serviceMethod_WithMockedDependency_ProcessesCorrectly() { + // Given + every { mockRepository.findById(any()) } returns testUser + + // When & Then - focuses on service logic only +} +``` + +## 🔧 Configuration Details + +### Gradle Configuration + +The template uses these coverage settings in `gradle.properties`: + +```properties +code_coverage_minimum=0.85 +``` + +### JaCoCo Rules + +```groovy +violationRules { + rule { + limit { + counter = 'INSTRUCTION' + value = 'COVEREDRATIO' + minimum = 0.85 + } + limit { + counter = 'BRANCH' + value = 'COVEREDRATIO' + minimum = 0.85 + } + } +} +``` + +## 🎯 Coverage Metrics Explained + +### Instruction Coverage (85% required) + +- **What**: Percentage of bytecode instructions executed +- **Why**: Most accurate measure of code execution +- **Focus**: Ensure all code paths are tested + +### Branch Coverage (85% required) + +- **What**: Percentage of decision branches taken +- **Why**: Ensures conditional logic is tested +- **Focus**: Test both true/false paths of conditions + +### Method Coverage (76.5% required) + +- **What**: Percentage of methods called during tests +- **Why**: Ensures public API is tested +- **Focus**: All public methods should have tests + +## 🚨 Troubleshooting Coverage Issues + +### Common Problems + +**Issue**: Coverage below 85% but all logic seems tested +**Solution**: Check for uncovered exception handling or default branches + +**Issue**: Kotlin companion objects affecting coverage +**Solution**: Already excluded via `'**/*\$Companion*'` pattern + +**Issue**: Spring configuration classes lowering coverage +**Solution**: Already excluded via `'**/*Config*'` pattern + +### Debugging Low Coverage + +```bash +# Generate detailed HTML report +./gradlew jacocoTestReport + +# Look for red/yellow highlighted code in: +# build/reports/jacoco/test/html/index.html +``` + +This comprehensive coverage strategy ensures your template projects maintain high quality standards while providing +clear guidelines for developers to achieve and maintain 85% test coverage. diff --git a/docs/commit-conventions.md b/docs/commit-conventions.md new file mode 100644 index 0000000..a6a6c67 --- /dev/null +++ b/docs/commit-conventions.md @@ -0,0 +1,264 @@ +# Commit Message and PR Title Conventions + +This project follows the **Conventional Commits** specification, with a focus on **PR titles** since we use **squash +merging only**. + +## 🔄 **Important: Squash Merging Only** + +This repository is configured for **squash merging only**. This means: + +- ✅ **PR Title is crucial** - becomes the final commit message after squash +- ℹ️ **Individual commit messages** - less critical, used only during development +- 🎯 **Focus on PR titles** - ensure they follow conventional format + +## Format + +``` +(): + +[optional body] + +[optional footer(s)] +``` + +## Types + +| Type | Description | Example | +|------------|-----------------------------------------------------------|---------------------------------------------------| +| `feat` | A new feature | `feat(core): add bulk fetch operation` | +| `fix` | A bug fix | `fix(redis): resolve connection leak issue` | +| `docs` | Documentation only changes | `docs: update API documentation` | +| `style` | Code style changes (formatting, missing semi colons, etc) | `style: fix code formatting` | +| `refactor` | Code change that neither fixes a bug nor adds a feature | `refactor(cache): simplify key generation logic` | +| `perf` | Performance improvements | `perf(core): optimize batch processing` | +| `test` | Adding missing tests or correcting existing tests | `test(redis): add integration tests` | +| `chore` | Changes to build process, auxiliary tools, libraries | `chore(deps): update spring boot to 3.5.4` | +| `ci` | Changes to CI configuration files and scripts | `ci: add coverage reporting workflow` | +| `build` | Changes to build system or external dependencies | `build: update gradle wrapper to 8.5` | +| `revert` | Reverts a previous commit | `revert: revert "feat: add experimental feature"` | + +## Scopes + +Common scopes for this project: + +| Scope | Description | +|------------|---------------------------------| +| `core` | Changes to bulk-core module | +| `redis` | Changes to bulk-redis module | +| `caffeine` | Changes to bulk-caffeine module | +| `spring` | Changes to bulk-spring module | +| `cache` | General caching functionality | +| `config` | Configuration-related changes | +| `api` | Public API changes | +| `deps` | Dependency updates | +| `ci` | CI/CD pipeline changes | +| `docs` | Documentation changes | +| `test` | Testing infrastructure | +| `security` | Security-related changes | + +## 🎯 **PR Title Requirements (CRITICAL)** + +**Pull Request titles MUST follow conventional commit format since they become the final commit message:** + +- ✅ `feat(redis): implement connection pooling` +- ✅ `feat(redis): Implement connection pooling` +- ✅ `fix: resolve memory leak in cache manager` +- ✅ `fix: Resolve memory leak in cache manager` +- ✅ `docs: update contributing guidelines` +- ✅ `docs: Update contributing guidelines` +- ❌ `Add new feature` (missing type) +- ❌ `Fix bug` (missing description) +- ❌ `FEAT: new feature` (uppercase type) + +**Note**: The description can start with either lowercase or uppercase - choose what feels natural! + +## 💻 **Individual Commit Messages (Development Only)** + +Since we use squash merging, individual commit messages are used only during development and won't appear in the main +branch history. However, for good development practices: + +### ✅ **Recommended** (but not enforced): + +```bash +feat(redis): add connection pooling logic +fix: resolve timeout issue +test: add unit tests for cache manager +docs: update README +``` + +### ✅ **Also acceptable** (for development commits): + +```bash +WIP: working on redis integration +temp: debugging connection issue +checkpoint: basic implementation done +fix typo +``` + +## Branch Naming Convention + +Branch names must follow this pattern: `/` + +### Valid Prefixes + +| Prefix | Purpose | Example | +|------------|--------------------------|------------------------------------| +| `feature` | New features | `feature/redis-connection-pooling` | +| `fix` | Bug fixes | `fix/memory-leak-core` | +| `hotfix` | Critical fixes | `hotfix/security-vulnerability` | +| `chore` | Maintenance tasks | `chore/update-dependencies` | +| `docs` | Documentation updates | `docs/api-documentation` | +| `refactor` | Code refactoring | `refactor/simplify-cache-logic` | +| `test` | Testing improvements | `test/integration-tests` | +| `perf` | Performance improvements | `perf/optimize-batch-operations` | + +### Branch Naming Rules + +- Use lowercase letters, numbers, and hyphens only +- Separate words with hyphens (`-`) +- Be descriptive but concise +- Maximum 50 characters + +### Examples + +#### Good Branch Names ✅ + +```bash +feature/bulk-redis-adapter +fix/connection-timeout-issue +docs/update-readme +test/caffeine-integration +chore/gradle-wrapper-update +``` + +#### Bad Branch Names ❌ + +```bash +Feature/NewStuff # Uppercase letters +fix_memory_leak # Underscores instead of hyphens +feature/ABC-123 # Uppercase in description +my-random-branch # No prefix +feature/ # Empty description +``` + +## Examples + +### ✅ **Good PR Titles** (Critical - these become final commits): + +```bash +feat(core): add bulk fetch operation with timeout support +fix(redis): resolve connection leak in batch operations +docs: update README with new installation instructions +perf(caffeine): optimize cache eviction algorithm +test(spring): add integration tests for autoconfiguration +chore(deps): update spring boot to 3.5.4 +ci: add automated dependency updates +refactor(api): simplify cache key generation +``` + +### ✅ **Good Development Commits** (Optional - for your workflow): + +```bash +feat(redis): initial connection pooling implementation +WIP: working on timeout handling +fix: resolve compilation error +test: add basic unit tests +checkpoint: basic redis adapter working +fix typo in documentation +temp: debugging connection issue +``` + +### ❌ **Bad PR Titles** (Will be rejected): + +```bash +Add feature # Missing type and scope +fix bug # Too vague +FEAT: new cache layer # Uppercase type +Fixed the redis thing # Not conventional format +Update stuff # Not descriptive +``` + +## Commit Signing Requirement + +All commits MUST be signed with GPG: + +```bash +# Sign individual commit +git commit -S -m "feat(core): add new feature" + +# Configure Git to always sign commits +git config --global commit.gpgsign true +``` + +See our [GPG Setup Guide](gpg-setup.md) for detailed instructions. + +## Validation + +Our CI/CD pipeline automatically validates: + +- ✅ **PR title format** (CRITICAL - enforced strictly) +- ℹ️ ~~Individual commit messages~~ (disabled for squash-merge-only repos) +- ℹ️ **GPG signatures** (informational only) + +**Only PR titles are strictly enforced** since they become the final commit messages after squash merge. + +## Workflow Summary + +### 📝 **For Contributors:** + +1. **Create feature branch**: `feature/your-feature-name` +2. **Make commits**: Use any commit style during development +3. **Create PR**: Ensure PR title follows `type(scope): description` format +4. **After merge**: Your PR title becomes the final commit message + +### 🔍 **What Gets Validated:** + +| Element | Validation Level | Impact | +|--------------------|----------------------|------------------------------| +| **PR Title** | ✅ **Strict** | **Blocks merge if invalid** | +| Individual Commits | ℹ️ **None** | No validation (squash merge) | +| Branch Name | ✅ **Strict** | **Blocks PR if invalid** | +| GPG Signatures | ℹ️ **Informational** | Shows status, doesn't block | + +## Tools + +### Local PR Title Validation + +You can validate PR titles locally using simple bash commands: + +```bash +# Test a PR title format +PR_TITLE="feat(core): add new caching layer" +if echo "$PR_TITLE" | grep -qE '^(feat|fix|docs|style|refactor|test|chore|perf|ci|build|revert)(\(.+\))?: .{3,}$'; then + echo "✅ Valid PR title format" +else + echo "❌ Invalid PR title format" +fi +``` + +### IDE Integration + +Most IDEs support conventional commits plugins that can help with formatting: + +- **IntelliJ IDEA**: Conventional Commits plugin +- **VS Code**: Conventional Commits extension +- **Vim**: vim-conventional-commits plugin + +**Note**: These plugins help with formatting but remember that individual commits are optional since we use squash +merging. + +## Questions? + +If you have questions about commit conventions, please: + +1. Check this documentation +2. Look at recent PR titles in the repository +3. Ask in a GitHub issue with the `question` label + +--- + +## 📖 **Quick Reference:** + +**✅ PR Title (CRITICAL):** `type(scope): description` +**✅ Branch Name:** `prefix/description` +**ℹ️ Individual Commits:** Any format (optional, for development) diff --git a/docs/dependency-management.md b/docs/dependency-management.md new file mode 100644 index 0000000..14a252c --- /dev/null +++ b/docs/dependency-management.md @@ -0,0 +1,244 @@ +--- +layout: default +title: Dependency Management +nav_order: 4 +--- + +# Dependency Management + +This template uses Gradle dependency locking to ensure reproducible builds across different environments and team +members. + +## 📦 Dependency Locking Overview + +Dependency locking prevents the "works on my machine" problem by: + +- **Locking exact versions** of all transitive dependencies +- **Ensuring reproducible builds** across environments +- **Preventing unexpected version changes** during builds +- **Providing security** through consistent dependency versions + +## 🔧 How It Works + +### Lock Files + +The template generates lock files for each module: + +``` +├── gradle.lockfile # Root project locks +├── service-module/ +│ └── gradle.lockfile # Service module locks +└── springboot-application/ + └── gradle.lockfile # Application module locks +``` + +### Version Management + +All dependency versions are centralized in `gradle.properties`: + +```properties +# Dependency versions +testng_version=7.7.0 +mockk_version=1.13.10 +h2_version=2.2.224 +kotlin_coroutines_version=1.8.0 + +# Gradle plugin versions +kotlin_version=1.9.25 +spring_boot_version=3.5.3 +``` + +## 🛠️ Common Commands + +### Adding New Dependencies + +1. **Add to gradle.properties** (if new version): + +```properties +new_library_version=1.2.3 +``` + +2. **Add to module build.gradle**: + +```groovy +dependencies { + implementation "com.example:new-library:$new_library_version" +} +``` + +3. **Update locks**: + +```bash +./gradlew resolveAndLockAll --write-locks +``` + +### Updating Dependencies + +```bash +# Check for available updates +./gradlew dependencyUpdates + +# Update specific version in gradle.properties +# Then regenerate locks +./gradlew resolveAndLockAll --write-locks +``` + +### Troubleshooting Lock Issues + +```bash +# If you get "not part of dependency lock state" errors: +./gradlew resolveAndLockAll --write-locks + +# To see dependency tree +./gradlew dependencies + +# To see specific configuration dependencies +./gradlew :service-module:dependencies --configuration runtimeClasspath +``` + +## 🔍 Dependency Analysis + +### Security Scanning + +```bash +# Run OWASP dependency check +./gradlew dependencyCheckAnalyze + +# View report at: build/reports/dependency-check-report.html +``` + +### License Compliance + +```bash +# Generate license report +./gradlew generateLicenseReport + +# View report at: build/reports/dependency-license/ +``` + +### Version Analysis + +```bash +# Check for outdated dependencies +./gradlew dependencyUpdates + +# View report at: build/dependencyUpdates/report.html +``` + +## 📋 Best Practices + +### 1. Version Management + +- ✅ **Use variables** in `gradle.properties` for all versions +- ✅ **Group related versions** (e.g., all Spring Boot versions) +- ✅ **Document version choices** for major dependencies + +### 2. Lock File Management + +- ✅ **Commit lock files** to version control +- ✅ **Update locks** after any dependency changes +- ✅ **Review lock changes** in pull requests + +### 3. Security + +- ✅ **Run security scans** regularly +- ✅ **Update dependencies** promptly for security fixes +- ✅ **Monitor vulnerability reports** + +## 🚨 Common Issues + +### Lock State Errors + +**Problem**: `Could not resolve all files... not part of dependency lock state` + +**Solution**: + +```bash +./gradlew resolveAndLockAll --write-locks +./gradlew clean build +``` + +### Version Conflicts + +**Problem**: Different modules requiring different versions + +**Solution**: + +1. Use `gradle.properties` to enforce consistent versions +2. Add explicit dependency management in root `build.gradle` +3. Use `force` in resolution strategy if needed + +### Plugin Version Issues + +**Problem**: Plugin versions causing conflicts + +**Solution**: + +1. Update plugin versions in `gradle.properties` +2. Ensure plugin compatibility +3. Check plugin documentation for version requirements + +## 📊 Dependency Categories + +### Core Dependencies + +```groovy +// Kotlin essentials +implementation "org.jetbrains.kotlin:kotlin-reflect" +implementation "org.jetbrains.kotlin:kotlin-stdlib" + +// Coroutines +implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" +``` + +### Spring Dependencies + +```groovy +// Spring Boot starters +implementation "org.springframework.boot:spring-boot-starter-web" +implementation "org.springframework.boot:spring-boot-starter-data-jpa" + +// Spring validation +implementation "org.springframework.boot:spring-boot-starter-validation" +``` + +### Testing Dependencies + +```groovy +// Test frameworks +testImplementation "org.testng:testng:$testng_version" +testImplementation "io.mockk:mockk:$mockk_version" +testImplementation "org.springframework.boot:spring-boot-starter-test" +``` + +## 🎯 Template-Specific Configuration + +### Automatic Module Discovery + +The template uses automatic module discovery in `settings.gradle`: + +```groovy +// Auto-include all folders with build.gradle +file(".").listFiles() + .findAll { it.isDirectory() && new File(it, "build.gradle").exists() } + .each { include it.name } +``` + +### Publishing Configuration + +Ready for GitHub Packages publishing: + +```groovy +repositories { + maven { + name = "GitHubPackages" + url = uri("https://maven.pkg.github.com/your-org/your-repo") + credentials { + username = System.getenv("GITHUB_ACTOR") + password = System.getenv("GITHUB_TOKEN") + } + } +} +``` + +This ensures your template projects can easily publish to GitHub Packages with proper authentication. diff --git a/docs/gpg-setup-linux.md b/docs/gpg-setup-linux.md new file mode 100644 index 0000000..d6c99a4 --- /dev/null +++ b/docs/gpg-setup-linux.md @@ -0,0 +1,148 @@ +# GPG Setup for Signed Commits - Linux Guide + +## Quick Setup via Terminal + +### Step 1: Install GPG + +Most Linux distributions come with GPG pre-installed. If not, install it using your package manager: + +```bash +# Ubuntu/Debian +sudo apt update && sudo apt install gnupg + +# CentOS/RHEL/Fedora +sudo dnf install gnupg2 +# or for older versions: +sudo yum install gnupg2 + +# Arch Linux +sudo pacman -S gnupg + +# openSUSE +sudo zypper install gpg2 +``` + +### Step 2: Verify GPG Installation + +```bash +gpg --version +``` + +### Step 3: Generate GPG Key + +```bash +gpg --full-generate-key +``` + +**When prompted, choose:** + +- Key type: `1` (RSA and RSA) +- Key size: `4096` +- Key validity: `0` (key does not expire, or choose a specific time) +- Real name: `Your Full Name` (use your real name) +- Email: `your-github-email@example.com` (use your GitHub email) +- Comment: Leave blank or add a comment +- Passphrase: Choose a strong passphrase (remember this!) + +### Step 4: Get Your Key ID + +```bash +gpg --list-secret-keys --keyid-format=long +``` + +Look for output like: + +``` +sec rsa4096/ABC123DEF456 2025-07-24 [SC] + 1234567890ABCDEF1234567890ABCDEF12345678 +uid [ultimate] Your Name +ssb rsa4096/XYZ789 2025-07-24 [E] +``` + +The key ID is `ABC123DEF456` (after rsa4096/) + +### Step 5: Export Your Public Key + +```bash +gpg --armor --export YOUR_KEY_ID +``` + +(Replace YOUR_KEY_ID with your actual key ID) + +Copy the entire output (including -----BEGIN PGP PUBLIC KEY BLOCK----- and -----END PGP PUBLIC KEY BLOCK-----) + +### Step 6: Add Key to GitHub + +1. Go to GitHub → Settings → SSH and GPG keys +2. Click "New GPG key" +3. Paste your public key +4. Click "Add GPG key" + +### Step 7: Configure Git + +```bash +git config --global user.signingkey YOUR_KEY_ID +git config --global commit.gpgsign true +``` + +### Step 8: Test Signed Commit + +```bash +cd /path/to/your/project +git commit --allow-empty -m "test: verify GPG signed commits are working" + +# Verify the signature +git log --show-signature -1 +``` + +You should see output like: + +``` +gpg: Good signature from "Your Name " [ultimate] +``` + +## Additional Configuration + +### Set GPG TTY (if needed) + +Add to your shell profile (~/.bashrc, ~/.zshrc, etc.): + +```bash +export GPG_TTY=$(tty) +``` + +### Configure GPG Agent (for passphrase caching) + +```bash +# Add to ~/.gnupg/gpg-agent.conf +default-cache-ttl 28800 +max-cache-ttl 86400 +``` + +Then restart the agent: + +```bash +gpgconf --kill gpg-agent +gpg-agent --daemon +``` + +## Troubleshooting + +### GPG Agent Issues + +```bash +# Restart GPG agent +gpgconf --kill all +gpg-agent --daemon + +# Check GPG agent status +gpg-agent --version +``` + +### Permission Issues + +```bash +# Fix GPG directory permissions +chmod 700 ~/.gnupg +chmod 600 ~/.gnupg/* +``` diff --git a/docs/gpg-setup-macos.md b/docs/gpg-setup-macos.md new file mode 100644 index 0000000..656aa27 --- /dev/null +++ b/docs/gpg-setup-macos.md @@ -0,0 +1,167 @@ +# GPG Setup for Signed Commits - macOS Guide + +## Quick Setup via Terminal + +### Step 1: Install GPG + +Install GPG using Homebrew (recommended) or MacPorts: + +```bash +# Using Homebrew (recommended) +brew install gnupg + +# Using MacPorts (alternative) +sudo port install gnupg2 + +# Using direct download (if no package manager) +# Download from https://gpgtools.org/ and install GPG Suite +``` + +### Step 2: Verify GPG Installation + +```bash +gpg --version +``` + +### Step 3: Generate GPG Key + +```bash +gpg --full-generate-key +``` + +**When prompted, choose:** + +- Key type: `1` (RSA and RSA) +- Key size: `4096` +- Key validity: `0` (key does not expire, or choose a specific time) +- Real name: `Your Full Name` (use your real name) +- Email: `your-github-email@example.com` (use your GitHub email) +- Comment: Leave blank or add a comment +- Passphrase: Choose a strong passphrase (remember this!) + +### Step 4: Get Your Key ID + +```bash +gpg --list-secret-keys --keyid-format=long +``` + +Look for output like: + +``` +sec rsa4096/ABC123DEF456 2025-07-24 [SC] + 1234567890ABCDEF1234567890ABCDEF12345678 +uid [ultimate] Your Name +ssb rsa4096/XYZ789 2025-07-24 [E] +``` + +The key ID is `ABC123DEF456` (after rsa4096/) + +### Step 5: Export Your Public Key + +```bash +gpg --armor --export YOUR_KEY_ID +``` + +(Replace YOUR_KEY_ID with your actual key ID) + +Copy the entire output (including -----BEGIN PGP PUBLIC KEY BLOCK----- and -----END PGP PUBLIC KEY BLOCK-----) + +### Step 6: Add Key to GitHub + +1. Go to GitHub → Settings → SSH and GPG keys +2. Click "New GPG key" +3. Paste your public key +4. Click "Add GPG key" + +### Step 7: Configure Git + +```bash +git config --global user.signingkey YOUR_KEY_ID +git config --global commit.gpgsign true + +# If using Homebrew GPG, you might need to specify the path +git config --global gpg.program $(which gpg) +``` + +### Step 8: Test Signed Commit + +```bash +cd /path/to/your/project +git commit --allow-empty -m "test: verify GPG signed commits are working" + +# Verify the signature +git log --show-signature -1 +``` + +You should see output like: + +``` +gpg: Good signature from "Your Name " [ultimate] +``` + +## macOS-Specific Configuration + +### Set GPG TTY for Terminal + +Add to your shell profile (~/.zshrc, ~/.bash_profile, etc.): + +```bash +export GPG_TTY=$(tty) +``` + +### Configure Pinentry for macOS + +Install pinentry-mac for better password prompts: + +```bash +brew install pinentry-mac + +# Add to ~/.gnupg/gpg-agent.conf +echo "pinentry-program $(which pinentry-mac)" >> ~/.gnupg/gpg-agent.conf + +# Restart GPG agent +gpgconf --kill gpg-agent +``` + +### Using GPG Suite (Alternative) + +If you prefer a GUI approach: + +1. Download GPG Suite from https://gpgtools.org/ +2. Install the package +3. Use GPG Keychain Access to generate and manage keys +4. Export public key and add to GitHub as described above + +## Troubleshooting + +### GPG Agent Issues on macOS + +```bash +# Restart GPG agent +gpgconf --kill all + +# Check if agent is running +ps aux | grep gpg-agent +``` + +### Homebrew vs System GPG + +```bash +# Check which GPG Git is using +git config --global gpg.program + +# Force use of Homebrew GPG +git config --global gpg.program /opt/homebrew/bin/gpg +# or for Intel Macs: +git config --global gpg.program /usr/local/bin/gpg +``` + +### Keychain Integration + +If using GPG Suite, you can integrate with macOS Keychain: + +```bash +# Add to ~/.gnupg/gpg-agent.conf +use-standard-socket +enable-ssh-support +``` diff --git a/docs/gpg-setup-windows.md b/docs/gpg-setup-windows.md new file mode 100644 index 0000000..7d15dc9 --- /dev/null +++ b/docs/gpg-setup-windows.md @@ -0,0 +1,99 @@ +# GPG Setup for Signed Commits - Windows Guide + +## Quick Setup via Windows Terminal (Recommended) + +### Step 1: Install GPG4Win via Winget + +```powershell +# Install GPG4Win using Windows Package Manager +winget install GnuPG.Gpg4win +``` + +**Alternative package managers:** + +```powershell +# Using Chocolatey (if you have it) +choco install gpg4win + +# Using Scoop (if you have it) +scoop install gpg4win +``` + +### Step 2: Generate GPG Key + +```powershell +# Generate a new GPG key (use full path initially) +& "C:\Program Files (x86)\GnuPG\bin\gpg.exe" --full-generate-key +``` + +**When prompted, choose:** + +- Key type: `1` (RSA and RSA) +- Key size: `4096` +- Key validity: `0` (key does not expire, or choose a specific time) +- Real name: `Your Full Name` (use your real name) +- Email: `your-github-email@example.com` (use your GitHub email) +- Comment: Leave blank or add a comment +- Passphrase: Choose a strong passphrase (remember this!) + +### Step 3: Get Your Key ID + +```powershell +& "C:\Program Files (x86)\GnuPG\bin\gpg.exe" --list-secret-keys --keyid-format=long +``` + +Look for output like: + +``` +sec rsa4096/ABC123DEF456 2025-07-24 [SC] + 1234567890ABCDEF1234567890ABCDEF12345678 +uid [ultimate] Your Name +ssb rsa4096/XYZ789 2025-07-24 [E] +``` + +The key ID is `ABC123DEF456` (after rsa4096/) + +### Step 4: Export Your Public Key + +```powershell +& "C:\Program Files (x86)\GnuPG\bin\gpg.exe" --armor --export YOUR_KEY_ID +``` + +(Replace YOUR_KEY_ID with your actual key ID) + +Copy the entire output (including -----BEGIN PGP PUBLIC KEY BLOCK----- and -----END PGP PUBLIC KEY BLOCK-----) + +### Step 5: Add Key to GitHub + +1. Go to GitHub → Settings → SSH and GPG keys +2. Click "New GPG key" +3. Paste your public key +4. Click "Add GPG key" + +### Step 6: Configure Git + +```powershell +git config --global user.signingkey YOUR_KEY_ID +git config --global commit.gpgsign true +git config --global gpg.program "C:\Program Files (x86)\GnuPG\bin\gpg.exe" +``` + +### Step 7: Test Signed Commit + +```powershell +cd C:\Users\Davit\Documents\Project\spring-bulk-layered-cache +git commit --allow-empty -m "test: verify GPG signed commits are working" + +# Verify the signature +git log --show-signature -1 +``` + +You should see output like: + +``` +gpg: Good signature from "Your Name " [ultimate] +``` + +## Alternative: Manual Download + +If winget is not available, download directly from https://www.gpg4win.org/ diff --git a/docs/gpg-setup.md b/docs/gpg-setup.md new file mode 100644 index 0000000..3b6698e --- /dev/null +++ b/docs/gpg-setup.md @@ -0,0 +1,123 @@ +# GPG Setup for Signed Commits + +This guide helps you set up GPG (GNU Privacy Guard) for signing your Git commits across different operating systems. +Signed commits provide cryptographic proof that commits come from a trusted source. + +## Quick Platform Selection + +Choose your operating system to get started: + +| Platform | Guide | Package Manager | +|----------------|---------------------------------------------|----------------------------------------| +| 🪟 **Windows** | [Windows Setup Guide](gpg-setup-windows.md) | winget, Chocolatey, or Direct Download | +| 🐧 **Linux** | [Linux Setup Guide](gpg-setup-linux.md) | apt, dnf, pacman, zypper | +| 🍎 **macOS** | [macOS Setup Guide](gpg-setup-macos.md) | Homebrew, MacPorts, or GPG Suite | + +## Why Sign Your Commits? + +- **🔒 Authentication**: Proves commits actually came from you +- **🛡️ Integrity**: Ensures commits haven't been tampered with +- **✅ Trust**: GitHub shows "Verified" badge for signed commits +- **🏢 Compliance**: Required by many organizations and projects + +## What You'll Need + +- **Email**: The same email address used for your GitHub account +- **Strong Passphrase**: To protect your private key +- **5-10 minutes**: For initial setup + +## Quick Overview + +All platforms follow these general steps: + +1. **Install GPG** (varies by platform) +2. **Generate RSA 4096-bit key pair** +3. **Export public key** +4. **Add public key to GitHub** +5. **Configure Git for automatic signing** +6. **Test with a signed commit** + +## After Setup + +Once you've completed the setup for your platform: + +### Verify Your Setup + +```bash +# Check your configuration +git config --global user.signingkey +git config --global commit.gpgsign + +# Test with a signed commit +git commit --allow-empty -m "test: verify GPG signed commits" +git log --show-signature -1 +``` + +### GitHub Integration + +After adding your public key to GitHub, you'll see: + +- ✅ **"Verified" badge** next to your commits +- 🔒 **Green shield icon** indicating cryptographic verification +- 👤 **Commit author verification** in the GitHub UI + +## Troubleshooting + +### Common Issues Across Platforms + +**GPG not found:** + +- Ensure GPG is installed and in your PATH +- Restart your terminal after installation + +**Passphrase prompts:** + +- Configure GPG agent for passphrase caching +- Set up appropriate pinentry program for your OS + +**Git can't find GPG:** + +- Set explicit GPG program path: `git config --global gpg.program /path/to/gpg` + +**Permission denied:** + +- Check GPG directory permissions: `chmod 700 ~/.gnupg` + +### Platform-Specific Help + +Each platform guide includes detailed troubleshooting sections for OS-specific issues. + +## Security Best Practices + +- **🔐 Use strong passphrases** (12+ characters with mixed case, numbers, symbols) +- **💾 Backup your private key** securely +- **⏰ Consider key expiration** (1-2 years) for enhanced security +- **🚫 Never share your private key** or passphrase +- **🔄 Keep your GPG software updated** + +## Team Usage + +For teams requiring signed commits: + +1. **Document the requirement** in your project README +2. **Link to these setup guides** for new contributors +3. **Configure branch protection** to require signed commits +4. **Consider key management** for organization keys + +## Repository Configuration + +To enforce signed commits in your repository: + +```yaml +# In GitHub Actions workflows +- name: Verify signed commits + run: | + # Check that recent commits are signed + git log --show-signature -5 +``` + +Or configure branch protection rules in GitHub to require signed commits. + +--- + +**Need help?** Check the platform-specific guides linked above, or refer to the troubleshooting sections in each guide. diff --git a/docs/index.md b/docs/index.md new file mode 100644 index 0000000..86550aa --- /dev/null +++ b/docs/index.md @@ -0,0 +1,132 @@ +--- +layout: home +title: Home +nav_order: 1 +--- + +# Kotlin Multimodule Template + +A minimal, production-ready Kotlin multimodule template for Spring Boot applications with proper Spring-Kotlin +integration, comprehensive testing, and GitHub template features. + +## 🎯 Overview + +This template provides a clean foundation for creating scalable Kotlin microservices with: + +- **Simple 2-module architecture** (service + application layers) +- **Spring-Kotlin integration** with `allopen` plugin configured +- **Dependency locking** for reproducible builds +- **Comprehensive testing setup** with unit test grouping +- **GitHub template ready** with customization scripts +- **Production-ready CI/CD** and security scanning + +## 🚀 Quick Start + +### 1. Use This Template + +Click "Use this template" button on GitHub to create your new repository. + +### 2. Customize Your Project + +```bash +# Linux/Mac +./customize.sh + +# Windows PowerShell +./customize.ps1 +``` + +### 3. Build and Run + +```bash +./gradlew build +./gradlew :springboot-application:bootRun +``` + +### 4. Test Your Setup + +```bash +curl http://localhost:8080/api/example/health +``` + +## 📚 Documentation Sections + +### Getting Started + +- [Template Setup Guide](./template-setup.md) - Step-by-step customization +- [Project Structure](./project-structure.md) - Understanding the architecture +- [Dependency Management](./dependency-management.md) - Gradle locks and versions + +### Development + +- [Unit Testing Guide](./unit-testing.md) - Testing strategies and grouping +- [Spring-Kotlin Integration](./spring-kotlin.md) - Allopen plugin and beans +- [Code Quality](./code-quality.md) - Coverage, security, and style + +### CI/CD & Deployment + +- [GitHub Workflows](./workflows.md) - Automated testing and deployment +- [Branch Protection](./branch-protection.md) - Repository security +- [Commit Conventions](./commit-conventions.md) - Git workflow standards + +### Reference + +- [API Documentation](./api/) - Module APIs and interfaces +- [Troubleshooting](./troubleshooting.md) - Common issues and solutions + +## 🏗️ Architecture + +### Current Modules + +- **service-module**: Business logic and services with Spring annotations +- **springboot-application**: REST controllers and application configuration + +### Key Features + +- ✅ **Spring Bean Creation**: Kotlin `allopen` plugin configured for Spring annotations +- ✅ **Dependency Injection**: Works seamlessly with `@Service`, `@Controller` annotations +- ✅ **Transaction Management**: `@Transactional` support with AOP proxies +- ✅ **Reproducible Builds**: Dependency locking enabled +- ✅ **Modular Testing**: Unit tests grouped by scope and performance + +## 🧪 Testing Strategy + +This template includes comprehensive testing setup: + +```bash +# Run all tests +./gradlew test + +# Run specific test groups +./gradlew test --tests "*Unit*" +./gradlew test --tests "*Integration*" + +# Performance tests +./gradlew jmh +``` + +## 📦 Dependency Management + +Uses Gradle dependency locking for reproducible builds: + +```bash +# Update locks after adding dependencies +./gradlew resolveAndLockAll --write-locks + +# Check for dependency updates +./gradlew dependencyUpdates +``` + +## 🔒 Security + +- **Dependency vulnerability scanning** with OWASP +- **License compliance checking** +- **GPG commit signing** (optional) +- **Branch protection** for main branches + +## 📈 Getting Help + +- 📖 [Full Documentation](./template-setup.md) +- 🐛 [Issue Tracker](https://github.com/programmer-newbie-code/kotlin-multimodule-template/issues) +- 💬 [Discussions](https://github.com/programmer-newbie-code/kotlin-multimodule-template/discussions) +- 📧 [Contributing](./contributing.md) diff --git a/docs/spring-kotlin.md b/docs/spring-kotlin.md new file mode 100644 index 0000000..8ea0efa --- /dev/null +++ b/docs/spring-kotlin.md @@ -0,0 +1,321 @@ +--- +layout: default +title: Spring-Kotlin Integration +nav_order: 6 +--- + +# Spring-Kotlin Integration + +This template includes proper configuration for seamless Spring Framework integration with Kotlin, solving common issues +developers face when using Kotlin with Spring. + +## 🎯 The Kotlin-Spring Challenge + +### The Problem + +Kotlin classes are **final by default**, which creates issues with Spring Framework: + +- Spring cannot create **CGLIB proxies** for dependency injection +- **AOP (Aspect-Oriented Programming)** doesn't work +- **@Transactional** annotations fail to create transaction proxies +- **@Configuration** classes can't be extended by Spring + +### The Solution: AllOpen Plugin + +This template uses the Kotlin `allopen` plugin to automatically make classes **open** (non-final) when annotated with +specific Spring annotations. + +## 🔧 Configuration Details + +### Plugin Setup + +The `allopen` plugin is configured in `scripts/kotlin.gradle`: + +```groovy +apply plugin: "org.jetbrains.kotlin.plugin.allopen" + +allOpen { + annotations( + "org.springframework.stereotype.Component", + "org.springframework.stereotype.Service", + "org.springframework.stereotype.Repository", + "org.springframework.stereotype.Controller", + "org.springframework.web.bind.annotation.RestController", + "org.springframework.boot.autoconfigure.SpringBootApplication", + "org.springframework.context.annotation.Configuration", + "org.springframework.transaction.annotation.Transactional" + ) +} +``` + +### What This Means + +When you annotate a Kotlin class with any of these annotations, the compiler automatically makes it `open`: + +```kotlin +// This class is automatically made 'open' by the allopen plugin +@Service +@Transactional +class ExampleService { + // Spring can create transaction proxies for this class + fun someBusinessMethod() { } +} +``` + +## ✅ Working Examples + +### Service Layer + +```kotlin +@Service +@Transactional +class UserService( + private val userRepository: UserRepository +) { + // ✅ Transaction management works + // ✅ Dependency injection works + // ✅ AOP proxies work + + fun createUser(userData: UserData): User { + // This method is automatically wrapped in a transaction + return userRepository.save(User(userData)) + } + + @Transactional(readOnly = true) + fun findUser(id: UUID): User? { + // Read-only transaction + return userRepository.findById(id) + } +} +``` + +### Controller Layer + +```kotlin +@RestController +@RequestMapping("/api/users") +class UserController( + private val userService: UserService // ✅ DI works automatically +) { + // ✅ Spring can create proxies for this controller + // ✅ All Spring MVC features work + + @PostMapping + fun createUser(@RequestBody userData: UserData): ResponseEntity { + val user = userService.createUser(userData) + return ResponseEntity.ok(user) + } + + @GetMapping("/{id}") + fun getUser(@PathVariable id: UUID): ResponseEntity { + val user = userService.findUser(id) + return ResponseEntity.of(Optional.ofNullable(user)) + } +} +``` + +### Configuration Classes + +```kotlin +@Configuration +@EnableJpaAuditing +class DatabaseConfig { + // ✅ Spring can extend this configuration class + + @Bean + fun auditingHandler(): AuditingHandler { + return AuditingHandler(PersistenceContext()) + } +} +``` + +## 🚨 What You DON'T Need to Do + +### ❌ Manual `open` Keywords + +```kotlin +// DON'T do this - the plugin handles it automatically +open class ExampleService { } +``` + +### ❌ Interface-Based Proxies Only + +```kotlin +// You CAN do this, but it's not required +interface UserService { + fun createUser(userData: UserData): User +} + +@Service +class UserServiceImpl : UserService { + override fun createUser(userData: UserData): User { } +} +``` + +### ❌ Workarounds with Companion Objects + +```kotlin +// DON'T need complex workarounds +class ExampleService { + companion object { + // Complex static initialization + } +} +``` + +## 🔍 Verification + +### Check That It's Working + +```kotlin +@SpringBootTest +class SpringKotlinIntegrationTest { + + @Autowired + private lateinit var exampleService: ExampleService + + @Test + fun verifyProxyCreation() { + // This will pass if proxies are created correctly + assertTrue(AopUtils.isAopProxy(exampleService)) + } + + @Test + fun verifyTransactionSupport() { + // This will pass if @Transactional works + assertTrue(TransactionSynchronizationManager.isActualTransactionActive()) + } +} +``` + +### Debug Information + +```kotlin +// In your application, you can check if proxies are created: +@Component +class ProxyChecker( + @Autowired private val services: List +) { + + @PostConstruct + fun checkProxies() { + services.forEach { service -> + val isProxy = AopUtils.isAopProxy(service) + logger.info("${service.javaClass.simpleName} is proxy: $isProxy") + } + } +} +``` + +## 🧪 Testing Spring-Kotlin Integration + +### Unit Tests with MockK + +```kotlin +class ExampleServiceTest { + + @MockK + private lateinit var dependency: SomeDependency + + @InjectMockKs + private lateinit var service: ExampleService + + @BeforeEach + fun setup() { + MockKAnnotations.init(this) + } + + @Test + fun testServiceMethod() { + // Given + every { dependency.someMethod() } returns "test" + + // When + val result = service.getWelcomeMessage("test") + + // Then + assertEquals("Hello, test! This is your Kotlin Multimodule Template.", result) + verify { dependency.someMethod() } + } +} +``` + +### Integration Tests + +```kotlin +@SpringBootTest +@TestPropertySource(properties = ["spring.jpa.hibernate.ddl-auto=create-drop"]) +class ServiceIntegrationTest { + + @Autowired + private lateinit var userService: UserService + + @Test + @Transactional + fun testTransactionalBehavior() { + // This test verifies that @Transactional works correctly + val userData = UserData("test@example.com", "Test User") + + val user = userService.createUser(userData) + + assertNotNull(user.id) + assertEquals("test@example.com", user.email) + } +} +``` + +## 🎓 Advanced Features + +### Custom Annotations + +You can add your own annotations to the `allopen` configuration: + +```groovy +allOpen { + annotations( + // Spring annotations... + "com.yourcompany.CustomTransactional", + "com.yourcompany.ProxyRequired" + ) +} +``` + +### Conditional Configuration + +```kotlin +@Configuration +@ConditionalOnProperty(name = "app.feature.enabled", havingValue = "true") +class FeatureConfig { + // This configuration is automatically made 'open' + + @Bean + @ConditionalOnMissingBean + fun featureService(): FeatureService { + return FeatureServiceImpl() + } +} +``` + +## 📊 Performance Considerations + +### Proxy Creation Impact + +- **CGLIB proxies**: Slight memory overhead, negligible performance impact +- **JDK proxies**: Minimal overhead, used when interfaces are available +- **No proxies**: For classes without Spring annotations, no overhead + +### Best Practices + +- ✅ Use `@Transactional` judiciously (not on every method) +- ✅ Consider `@Transactional(readOnly = true)` for read operations +- ✅ Group related operations in single transactional methods +- ✅ Use `@Service` for business logic, `@Component` for utilities + +## 🔗 Related Documentation + +- [Spring Framework Reference - Kotlin Support](https://docs.spring.io/spring-framework/docs/current/reference/html/languages.html#kotlin) +- [Kotlin AllOpen Plugin](https://kotlinlang.org/docs/all-open-plugin.html) +- [Spring Boot Kotlin Guide](https://spring.io/guides/tutorials/spring-boot-kotlin/) + +This configuration ensures your Kotlin Spring applications work seamlessly without the common pitfalls of final classes +and proxy creation issues. diff --git a/docs/template-setup.md b/docs/template-setup.md new file mode 100644 index 0000000..e1af731 --- /dev/null +++ b/docs/template-setup.md @@ -0,0 +1,341 @@ +--- +layout: default +title: Template Setup Guide +nav_order: 2 +--- + +# Template Setup Guide + +This comprehensive guide walks you through customizing the Kotlin Multimodule Template for your specific project needs. + +## 🚀 Quick Setup (Automated) + +### Using the Customization Scripts + +The template includes automated scripts for quick setup: + +```bash +# For Linux/Mac users +./customize.sh + +# For Windows PowerShell users +./customize.ps1 +``` + +These scripts will prompt you for: + +- **Project name** (e.g., "my-awesome-service") +- **Organization domain** (e.g., "com.mycompany") +- **GitHub organization** (e.g., "my-org") + +The scripts automatically: + +- ✅ Update package names throughout the project +- ✅ Rename the main application class +- ✅ Update build configuration +- ✅ Configure GitHub publishing settings +- ✅ Clean up old package structures + +## 🛠️ Manual Setup (Step-by-Step) + +### Step 1: Project Configuration + +**Update `settings.gradle`:** + +```groovy +rootProject.name = "your-project-name" +``` + +**Update `build.gradle` group:** + +```groovy +group = "com.yourcompany.yourproject" +``` + +**Update GitHub publishing URL:** + +```groovy +url = uri("https://maven.pkg.github.com/your-org/your-repo") +``` + +### Step 2: Package Structure + +**Current structure:** + +``` +io.programmernewbie.template +``` + +**Change to your structure:** + +``` +com.yourcompany.yourproject +``` + +**Required changes:** + +1. Rename directories under `src/main/kotlin/` +2. Update package declarations in all `.kt` files +3. Update import statements +4. Update `scanBasePackages` in main application + +### Step 3: Application Class + +**Rename the file:** + +- From: `KotlinMultimoduleTemplateApplication.kt` +- To: `YourProjectNameApplication.kt` + +**Update the class content:** + +```kotlin +@SpringBootApplication( + scanBasePackages = [ + "com.yourcompany.yourproject.service", + "com.yourcompany.yourproject" + ] +) +class YourProjectNameApplication + +fun main(args: Array) { + runApplication(*args) +} +``` + +### Step 4: Replace Example Code + +**Service Layer (`service-module`):** + +```kotlin +@Service +@Transactional +class YourBusinessService { + + fun performBusinessLogic(input: String): String { + // Your actual business logic here + return "Processed: $input" + } +} +``` + +**Controller Layer (`springboot-application`):** + +```kotlin +@RestController +@RequestMapping("/api/your-resource") +class YourResourceController( + private val businessService: YourBusinessService +) { + + @GetMapping + fun getResource(): ResponseEntity { + val result = businessService.performBusinessLogic("example") + return ResponseEntity.ok(result) + } +} +``` + +## 🔧 Advanced Customization + +### Adding New Modules + +1. **Create module directory:** + +```bash +mkdir your-new-module +``` + +2. **Create `build.gradle`:** + +```groovy +apply from: "$rootDir/scripts/gradle/spring_library.gradle" + +dependencies { + // Module-specific dependencies + implementation project(':service-module') +} +``` + +3. **Module auto-discovery:** + The module will be automatically included via `settings.gradle` + +### Database Configuration + +**For PostgreSQL:** + +```groovy +// In build.gradle +runtimeOnly "org.postgresql:postgresql" +``` + +```yaml +# In application.yml +spring: + datasource: + url: jdbc:postgresql://localhost:5432/yourdb + username: ${DB_USER:user} + password: ${DB_PASSWORD:password} + jpa: + database-platform: org.hibernate.dialect.PostgreSQLDialect +``` + +**For MySQL:** + +```groovy +runtimeOnly "com.mysql:mysql-connector-j" +``` + +```yaml +spring: + datasource: + url: jdbc:mysql://localhost:3306/yourdb + username: ${DB_USER:user} + password: ${DB_PASSWORD:password} + jpa: + database-platform: org.hibernate.dialect.MySQLDialect +``` + +### Adding Security + +**Add dependency:** + +```groovy +implementation "org.springframework.boot:spring-boot-starter-security" +``` + +**Create security configuration:** + +```kotlin +@Configuration +@EnableWebSecurity +class SecurityConfig { + + @Bean + fun filterChain(http: HttpSecurity): SecurityFilterChain { + return http + .authorizeHttpRequests { auth -> + auth.requestMatchers("/api/public/**").permitAll() + .anyRequest().authenticated() + } + .httpBasic(withDefaults()) + .build() + } +} +``` + +## 📦 Dependency Management + +### Adding New Dependencies + +1. **Add version to `gradle.properties`:** + +```properties +new_library_version=1.2.3 +``` + +2. **Add to module `build.gradle`:** + +```groovy +implementation "com.example:new-library:$new_library_version" +``` + +3. **Update dependency locks:** + +```bash +./gradlew resolveAndLockAll --write-locks +``` + +### Version Updates + +```bash +# Check for updates +./gradlew dependencyUpdates + +# Update specific versions in gradle.properties +# Then regenerate locks +./gradlew resolveAndLockAll --write-locks +``` + +## 🧪 Testing Setup + +### Test Structure + +``` +src/test/kotlin/ +├── unit/ +│ ├── small/ # Fast unit tests (< 100ms) +│ └── medium/ # Tests with mocks (< 1s) +├── integration/ # Full Spring context tests +└── performance/ # JMH benchmarks +``` + +### Running Tests by Group + +```bash +# Small unit tests only +./gradlew test --tests "**/unit/small/**" + +# Integration tests +./gradlew test --tests "**/integration/**" + +# Performance tests +./gradlew jmh +``` + +## 🔍 Verification Checklist + +After customization, verify everything works: + +- [ ] **Build successful**: `./gradlew build` +- [ ] **Application starts**: `./gradlew :springboot-application:bootRun` +- [ ] **Tests pass**: `./gradlew test` +- [ ] **Endpoints work**: `curl http://localhost:8080/api/your-endpoint` +- [ ] **Dependency locks updated**: Check for `.lockfile` files +- [ ] **Package names consistent**: No references to old packages + +## 🚨 Common Issues + +### Build Failures + +**Issue**: Package names don't match directory structure +**Solution**: Ensure package declarations match folder hierarchy + +**Issue**: Missing dependency versions +**Solution**: Add all versions to `gradle.properties` + +### Runtime Issues + +**Issue**: Spring can't find beans +**Solution**: Verify `scanBasePackages` includes your service packages + +**Issue**: Database connection fails +**Solution**: Check `application.yml` database configuration + +### Dependency Lock Issues + +**Issue**: "Not part of dependency lock state" errors +**Solution**: Run `./gradlew resolveAndLockAll --write-locks` + +## 📋 Template Checklist + +### Before Publishing Your Project + +- [ ] Update README.md with your project details +- [ ] Replace example code with real implementation +- [ ] Configure database for your needs +- [ ] Set up CI/CD workflows +- [ ] Add proper error handling +- [ ] Configure logging +- [ ] Add API documentation +- [ ] Set up monitoring/metrics + +### Repository Settings + +- [ ] Enable branch protection +- [ ] Configure required status checks +- [ ] Set up GitHub Packages (if needed) +- [ ] Configure secrets for CI/CD +- [ ] Enable security scanning + +This guide ensures you can quickly adapt the template while maintaining all the production-ready features and best +practices. diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md new file mode 100644 index 0000000..d4b159c --- /dev/null +++ b/docs/troubleshooting.md @@ -0,0 +1,339 @@ +--- +layout: default +title: Troubleshooting +nav_order: 10 +--- + +# Troubleshooting Guide + +Common issues and solutions when using the Kotlin Multimodule Template. + +## 🔧 Build Issues + +### Dependency Lock State Errors + +**Error**: `Could not resolve all files... not part of dependency lock state` + +**Cause**: Dependency locks are out of sync with current dependencies + +**Solution**: + +```bash +# Regenerate all lock files +./gradlew resolveAndLockAll --write-locks + +# Clean and rebuild +./gradlew clean build +``` + +### Kotlin Compilation Errors + +**Error**: `Unresolved reference` or `Type mismatch` + +**Common Causes & Solutions**: + +1. **Package name mismatch**: + - Check package declarations match directory structure + - Verify import statements are correct + +2. **Missing allopen plugin**: + - Ensure `kotlin.gradle` includes allopen plugin + - Check Spring annotations are in allopen configuration + +3. **Version conflicts**: + - Use consistent versions in `gradle.properties` + - Check for transitive dependency conflicts + +### Spring Boot Issues + +**Error**: `No qualifying bean of type` or `BeanCreationException` + +**Solutions**: + +1. **Component scanning**: + +```kotlin +@SpringBootApplication( + scanBasePackages = [ + "your.package.service", + "your.package.controller" + ] +) +``` + +2. **Missing annotations**: + +```kotlin +@Service // Ensure service classes are annotated +@RestController // Ensure controllers are annotated +``` + +3. **Circular dependencies**: + +```kotlin +// Use @Lazy annotation to break cycles +@Service +class ServiceA(@Lazy private val serviceB: ServiceB) +``` + +## 🏃 Runtime Issues + +### Application Won't Start + +**Error**: `Port 8080 was already in use` + +**Solution**: + +```bash +# Change port +export SERVER_PORT=8081 +./gradlew :springboot-application:bootRun + +# Or in application.yml +server: + port: 8081 +``` + +**Error**: `Failed to configure a DataSource` + +**Solution**: + +```yaml +# For testing with H2 +spring: + datasource: + url: jdbc:h2:mem:testdb + driver-class-name: org.h2.Driver + h2: + console: + enabled: true +``` + +### Transaction Issues + +**Error**: `@Transactional` not working + +**Cause**: Kotlin classes are final, Spring can't create proxies + +**Solution**: Template already includes allopen plugin, but verify: + +```groovy +// In kotlin.gradle +allOpen { + annotations("org.springframework.transaction.annotation.Transactional") +} +``` + +## 🧪 Testing Issues + +### Test Discovery Problems + +**Error**: Tests not found or not running + +**Solutions**: + +1. **Check test location**: + +``` +src/test/kotlin/ ← Correct location +src/test/java/ ← Wrong for Kotlin +``` + +2. **TestNG configuration**: + +```kotlin +// Ensure test classes use TestNG +import org.testng.annotations.Test + +@Test +class YourTest { + // Test methods +} +``` + +3. **Test naming**: + +```bash +# Run specific test patterns +./gradlew test --tests "*Test" +./gradlew test --tests "*Integration*" +``` + +### MockK Issues + +**Error**: `MockK` mocks not working + +**Solution**: + +```kotlin +@BeforeEach +fun setup() { + MockKAnnotations.init(this) // Initialize mocks +} + +// Or use programmatic approach +val mockService = mockk() +``` + +### Spring Test Context Issues + +**Error**: `Failed to load ApplicationContext` + +**Solutions**: + +1. **Test configuration**: + +```kotlin +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +@TestPropertySource(properties = ["spring.jpa.hibernate.ddl-auto=create-drop"]) +class IntegrationTest +``` + +2. **Test profiles**: + +```kotlin +@ActiveProfiles("test") +class ServiceTest +``` + +## 📦 GitHub Template Issues + +### Customization Script Failures + +**Error**: Script can't find files or permissions denied + +**Solutions**: + +1. **Linux/Mac permissions**: + +```bash +chmod +x customize.sh +./customize.sh +``` + +2. **Windows PowerShell execution policy**: + +```powershell +Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser +./customize.ps1 +``` + +3. **Manual customization**: If scripts fail, follow the manual steps in [Template Setup Guide](./template-setup.md) + +### GitHub Actions Issues + +**Error**: Workflow failures in forked repositories + +**Solutions**: + +1. **Update repository references**: + +```yaml +# In .github/workflows/*.yml +- name: Publish to GitHub Packages + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} +``` + +2. **Enable GitHub Packages**: + - Go to repository Settings → Actions → General + - Enable "Read and write permissions" + +## 🔍 Debugging Tips + +### Enable Debug Logging + +```yaml +# In application.yml +logging: + level: + your.package: DEBUG + org.springframework: DEBUG + org.hibernate.SQL: DEBUG +``` + +### Gradle Debug Information + +```bash +# Verbose Gradle output +./gradlew build --info --stacktrace + +# Debug dependency resolution +./gradlew dependencies --configuration runtimeClasspath + +# Check for duplicate classes +./gradlew buildEnvironment +``` + +### Spring Boot Debugging + +```bash +# Enable debug mode +./gradlew :springboot-application:bootRun --args='--debug' + +# Enable actuator endpoints +# Add to application.yml: +management: + endpoints: + web: + exposure: + include: health,info,beans,env +``` + +## 📊 Performance Issues + +### Slow Build Times + +**Solutions**: + +1. **Enable Gradle daemon and parallel builds** (already configured): + +```properties +# In gradle.properties +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true +``` + +2. **Increase memory**: + +```properties +org.gradle.jvmargs=-Xmx4g -XX:MaxMetaspaceSize=512m +``` + +### Slow Test Execution + +**Solutions**: + +1. **Run test groups separately**: + +```bash +./gradlew test --tests "**/unit/small/**" # Fast tests first +``` + +2. **Parallel test execution**: + +```groovy +test { + maxParallelForks = Runtime.runtime.availableProcessors() +} +``` + +## 🆘 Getting Help + +### Template-Specific Issues + +- 📖 [GitHub Issues](https://github.com/programmer-newbie-code/kotlin-multimodule-template/issues) +- 💬 [GitHub Discussions](https://github.com/programmer-newbie-code/kotlin-multimodule-template/discussions) + +### Framework Documentation + +- [Spring Boot Reference](https://docs.spring.io/spring-boot/docs/current/reference/html/) +- [Kotlin Documentation](https://kotlinlang.org/docs/home.html) +- [Gradle User Manual](https://docs.gradle.org/current/userguide/userguide.html) + +### Community Support + +- [Spring Community](https://spring.io/community) +- [Kotlin Slack](https://kotlinlang.slack.com/) +- [Stack Overflow](https://stackoverflow.com/questions/tagged/kotlin+spring-boot) diff --git a/docs/unit-testing.md b/docs/unit-testing.md new file mode 100644 index 0000000..950b5d2 --- /dev/null +++ b/docs/unit-testing.md @@ -0,0 +1,288 @@ +--- +layout: default +title: Unit Testing Guide +nav_order: 5 +--- + +# Unit Testing Guide + +This template provides a comprehensive testing strategy using **TestNG groups** to categorize tests, allowing you to run +different types of tests efficiently. + +## 🧪 Testing Philosophy + +### Test Pyramid Structure + +- **Unit Tests** (70%): Fast, isolated tests for individual components +- **Integration Tests** (20%): Tests for module interactions +- **End-to-End Tests** (10%): Full application workflow tests + +### TestNG Groups Strategy + +Tests are organized using TestNG groups for flexible execution: + +- **`@Test(groups = ["small"])`**: Fast tests (< 100ms) for pure logic +- **`@Test(groups = ["medium"])`**: Tests with lightweight dependencies (< 1s) +- **`@Test(groups = ["large"])`**: Integration tests requiring Spring context or external systems + +## 🏗️ Test Structure & Naming + +### Test Method Naming Convention + +Use descriptive names with backticks following the pattern: **`function, condition, expectation`** + +```kotlin +@Test(groups = ["small"]) +class ExampleServiceUnitTest { + + @Test + fun `getWelcomeMessage, with custom name, returns personalized message`() { + // Given + val name = "Alice" + + // When + val result = exampleService.getWelcomeMessage(name) + + // Then + assertEquals(result, "Hello, Alice! This is your Kotlin Multimodule Template.") + } +} +``` + +### TestNG Groups Configuration + +Apply groups at the class level for consistent categorization: + +```kotlin +/** + * Small unit tests for ExampleService + */ +@Test(groups = ["small"]) +class ExampleServiceUnitTest { + // All test methods inherit the "small" group +} +``` + +## 🚀 Running Tests + +### By TestNG Groups + +```bash +# Run only small/fast tests +./gradlew test -PtestGroups=small + +# Run medium tests (with mocks/setup) +./gradlew test -PtestGroups=medium + +# Run integration tests +./gradlew test -PtestGroups=large + +# Run multiple groups +./gradlew test -PtestGroups="small,medium" + +# Run all tests (default) +./gradlew test +``` + +### Additional Test Tasks + +```bash +# Run integration tests specifically +./gradlew integrationTest + +# Run all tests including integration +./gradlew allTests + +# Run tests in parallel +./gradlew test -PtestGroups=small --parallel +``` + +## 📊 Coverage Integration + +### 85% Minimum Coverage + +The template enforces **85% instruction coverage** minimum: + +```bash +# Run tests with coverage verification +./gradlew test jacocoTestCoverageVerification + +# Generate coverage reports +./gradlew jacocoTestReport + +# View coverage report +# Opens: build/reports/jacoco/test/html/index.html +``` + +### Coverage Exclusions + +Smart exclusions are automatically applied: + +- Configuration classes (`**/*Config*`, `*Application*`) +- Data classes (`*.dto.*`, `*.entity.*`, `*.model.*`) +- Kotlin-generated code (`**/*$Companion*`, `**/*$WhenMappings*`) +- Exception classes (`*.exception.*`, `**/*Exception*`) + +## 🧪 Writing Effective Tests + +### Small Group Tests (Fast) + +**Characteristics**: Pure logic, no external dependencies, < 100ms + +```kotlin +@Test(groups = ["small"]) +class ExampleServiceUnitTest { + + @Test + fun `getWelcomeMessage, with empty string, returns message with empty name`() { + // Given + val name = "" + + // When + val result = exampleService.getWelcomeMessage(name) + + // Then + assertEquals(result, "Hello, ! This is your Kotlin Multimodule Template.") + } +} +``` + +### Medium Group Tests (With Mocks) + +**Characteristics**: Uses mocks, lightweight dependencies, < 1s + +```kotlin +@Test(groups = ["small"]) // Controller tests with mocks are still "small" +class ExampleControllerUnitTest { + + @BeforeMethod + fun setup() { + mockExampleService = mockk() + controller = ExampleController(mockExampleService) + } + + @Test + fun `getWelcome, with custom name, returns response with message and timestamp`() { + // Given + val name = "Alice" + every { mockExampleService.getWelcomeMessage(name) } returns "Hello, Alice!" + + // When + val response = controller.getWelcome(name) + + // Then + assertEquals(response["message"], "Hello, Alice!") + verify { mockExampleService.getWelcomeMessage(name) } + } +} +``` + +### Large Group Tests (Integration) + +**Characteristics**: Full Spring context, database, external systems + +```kotlin +@Test(groups = ["large"]) +@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT) +class ExampleControllerIntegrationTest : AbstractTestNGSpringContextTests() { + + @Test + fun `getWelcome, end to end, returns valid response`() { + // Full integration test with real Spring context + } +} +``` + +## 🔧 Test Configuration + +### TestNG Groups in Gradle + +The template configures TestNG groups via command-line parameters: + +```groovy +// In scripts/testing.gradle +test { + useTestNG() { + if (project.hasProperty('testGroups')) { + String groups = project.property('testGroups') + includeGroups groups + } + } +} +``` + +### Performance Settings + +```groovy +test { + // Parallel execution + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 + + // Memory settings + jvmArgs '-Xmx1g', '-XX:MaxMetaspaceSize=256m' +} +``` + +## 📈 Coverage Quality Gates + +### Build Integration + +```bash +# Coverage verification runs automatically with check +./gradlew check # Includes jacocoTestCoverageVerification + +# Quick feedback during development +./gradlew test -PtestGroups=small jacocoTestReport +``` + +### CI/CD Integration + +```yaml +# GitHub Actions example +- name: Run Small Tests + run: ./gradlew test -PtestGroups=small + +- name: Run All Tests with Coverage + run: ./gradlew test jacocoTestCoverageVerification +``` + +## 🎯 Best Practices + +### 1. Group Classification + +- **Small**: Pure business logic, no I/O, fast execution +- **Medium**: With mocks, light setup, moderate execution time +- **Large**: Full context, real dependencies, slower execution + +### 2. Test Naming + +```kotlin +// ✅ Good: Descriptive and readable +fun `getWelcomeMessage, with null input, throws IllegalArgumentException`() + +// ❌ Avoid: Unclear purpose +fun testGetWelcomeMessage1() +``` + +### 3. Coverage Strategy + +- **Focus on business logic**: Ensure all service methods are tested +- **Test edge cases**: Empty strings, null values, boundary conditions +- **Mock external dependencies**: Keep tests fast and isolated + +### 4. Group Organization + +```kotlin +@Test(groups = ["small"]) +class ServiceUnitTest { + // Fast tests for service logic +} + +@Test(groups = ["large"]) +class ServiceIntegrationTest { + // Tests with full Spring context +} +``` + +This testing strategy ensures fast feedback during development while maintaining comprehensive coverage and quality +standards. diff --git a/docs/workflows.md b/docs/workflows.md new file mode 100644 index 0000000..6661487 --- /dev/null +++ b/docs/workflows.md @@ -0,0 +1,199 @@ +# GitHub Workflows Documentation + +This document describes the function of each GitHub workflow in this repository. + +## Core Workflows + +### 1. Build, Test, and Publish (`build-test-publish.yml`) + +**Purpose**: Main CI/CD pipeline for building, testing, and publishing the library. + +**Triggers**: + +- Push to `main` branch +- Pull requests to `main` branch +- Tags starting with `v*` + +**Functions**: + +- ✅ Validates Gradle wrapper +- 🏗️ Builds all modules +- 🧪 Runs tests with coverage reporting +- 📊 Generates JaCoCo coverage badges +- 📦 Creates JAR artifacts +- 🚀 Publishes releases (production and development) +- 💬 Comments coverage results on PRs +- ❌ Fails PRs with coverage below 70% + +**Artifacts**: + +- JAR files for all modules +- Coverage reports and badges + +### 2. Security Scan (`security-scan.yml`) + +**Purpose**: Comprehensive security analysis of the codebase and dependencies. + +**Triggers**: + +- Push to `main` branch +- Pull requests to `main` branch +- Weekly schedule (Monday 6 AM) + +**Functions**: + +- 🛡️ OWASP dependency vulnerability scanning +- 🔍 CodeQL static analysis for security issues +- 🔐 TruffleHog secrets scanning +- 📄 Uploads security reports + +### 3. Dependency Check (`dependency-check.yml`) + +**Purpose**: Focused dependency vulnerability monitoring. + +**Triggers**: + +- Weekly schedule (Sunday midnight) +- Manual trigger +- Changes to build files or lock files + +**Functions**: + +- 🔍 Scans dependencies for known vulnerabilities +- 📊 Generates detailed vulnerability reports +- 🚨 Creates GitHub issues for critical vulnerabilities +- ❌ Fails builds with high/critical vulnerabilities + +### 4. License Compliance (`license-compliance.yml`) + +**Purpose**: Ensures all dependencies have compatible licenses. + +**Triggers**: + +- Push to `main` branch +- Pull requests to `main` branch +- Weekly schedule (Monday 2 AM) + +**Functions**: + +- 📜 Generates license reports for all dependencies +- ⚠️ Flags restrictive licenses (GPL, AGPL, etc.) +- 🔍 Additional secrets scanning +- 📄 Creates license compliance artifacts + +### 5. Performance Testing (`performance-test.yml`) + +**Purpose**: Runs JMH benchmarks to monitor performance. + +**Triggers**: + +- Pull requests affecting source code +- Push to `main` branch +- Manual trigger + +**Functions**: + +- 🏃 Runs JMH performance benchmarks (if available) +- 📊 Generates performance reports +- 💬 Comments benchmark results on PRs +- 📈 Tracks performance over time + +### 6. Dependency Updates (`dependency-updates.yml`) + +**Purpose**: Automated dependency management. + +**Triggers**: + +- Weekly schedule (Monday 3 AM) +- Manual trigger + +**Functions**: + +- ⬆️ Updates Gradle wrapper to latest version +- 🔄 Updates dependencies to latest compatible versions +- 🔒 Regenerates lock files +- 🔧 Creates PR with dependency updates + +### 7. Generate Changelog (`changelog.yml`) + +**Purpose**: Maintains project changelog using conventional commits. + +**Triggers**: + +- Push to `main` branch +- Tags starting with `v*` +- Manual trigger + +**Functions**: + +- 📝 Generates CHANGELOG.md from commit history +- 🏷️ Creates release notes for tags +- 🔄 Commits changelog updates automatically + +### 8. Deploy GitHub Pages (`pages.yml`) + +**Purpose**: Publishes documentation website. + +**Triggers**: + +- Manual trigger only (currently disabled) +- Can be enabled for docs changes + +**Functions**: + +- 🌐 Builds Jekyll documentation site +- 📤 Deploys to GitHub Pages +- 📚 Makes documentation accessible via web + +## Branch Protection and Code Quality + +### Coverage Requirements + +- **Minimum Overall Coverage**: 70% +- **Minimum Branch Coverage**: 60% +- All PRs must meet these thresholds to be merged + +### Security Requirements + +- No high or critical vulnerability dependencies +- All commits must be GPG signed +- Code must pass security scans + +### Testing Requirements + +- Unit tests must use `@Test(groups=["small"])` annotation +- All modules require test coverage (except excluded ones) +- Performance tests use JMH framework + +## Workflow Optimization + +The workflows are designed to: + +- ✅ Run in parallel where possible +- 🔄 Share artifacts between jobs +- 📦 Cache dependencies for faster builds +- 💾 Store reports for analysis +- 🚫 Fail fast on critical issues + +## Artifact Management + +### Development Releases (`*-dev` tags) + +- Stored in `.repo/dev-releases/` directory +- Marked as prerelease +- Available for testing purposes + +### Production Releases (version tags) + +- Published to GitHub Packages +- Created as GitHub releases +- Include changelog and artifacts + +## Manual Triggers + +Most workflows support manual triggering via GitHub Actions UI: + +1. Go to Actions tab +2. Select workflow +3. Click "Run workflow" +4. Choose branch and parameters diff --git a/gradle.lockfile b/gradle.lockfile new file mode 100644 index 0000000..fe280d8 --- /dev/null +++ b/gradle.lockfile @@ -0,0 +1,11 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +org.jacoco:org.jacoco.agent:0.8.10=jacocoAgent,jacocoAnt +org.jacoco:org.jacoco.ant:0.8.10=jacocoAnt +org.jacoco:org.jacoco.core:0.8.10=jacocoAnt +org.jacoco:org.jacoco.report:0.8.10=jacocoAnt +org.ow2.asm:asm-commons:9.5=jacocoAnt +org.ow2.asm:asm-tree:9.5=jacocoAnt +org.ow2.asm:asm:9.5=jacocoAnt +empty=annotationProcessor,compileClasspath,runtimeClasspath,testAnnotationProcessor,testCompileClasspath,testRuntimeClasspath diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..8c04f68 --- /dev/null +++ b/gradle.properties @@ -0,0 +1,24 @@ +# Dependency versions +testng_version=7.7.0 +mockk_version=1.13.10 +h2_version=2.2.224 +kotlin_coroutines_version=1.8.0 +# Gradle plugin versions +gradle_versions_version=0.51.0 +jacoco_version=0.8.10 +jmh_version=0.7.2 +kotlin_version=1.9.25 +license_report_version=2.9 +owasp_dependency_check_version=10.0.4 +spring_boot_version=3.5.3 +spring_dependency_mgmt_version=1.1.7 +# Code coverage settings +code_coverage_minimum=0.85 +# Build performance settings +#org.gradle.jvmargs=-Xmx2g -XX:MaxMetaspaceSize=512m -XX:+HeapDumpOnOutOfMemoryError +org.gradle.parallel=true +org.gradle.caching=true +org.gradle.configureondemand=true +# Kotlin settings +kotlin.code.style=official +kotlin.incremental=true diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..1b33c55 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100644 index 0000000..23d15a9 --- /dev/null +++ b/gradlew @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..db3a6ac --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem +@rem SPDX-License-Identifier: Apache-2.0 +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +@rem This is normally unused +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. 1>&2 +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2 +echo. 1>&2 +echo Please set the JAVA_HOME variable in your environment to match the 1>&2 +echo location of your Java installation. 1>&2 + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH= + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/owasp-suppressions.xml b/owasp-suppressions.xml new file mode 100644 index 0000000..9cc8d14 --- /dev/null +++ b/owasp-suppressions.xml @@ -0,0 +1,15 @@ + + + + + diff --git a/scripts/check-signatures.ps1 b/scripts/check-signatures.ps1 new file mode 100644 index 0000000..2782bf5 --- /dev/null +++ b/scripts/check-signatures.ps1 @@ -0,0 +1,59 @@ +# PowerShell script to check signature status of all commits +Write-Host "🔍 Checking signature status of all commits..." -ForegroundColor Cyan +Write-Host "==================================================" -ForegroundColor Gray + +# Get all commit hashes in reverse order (oldest first) +$commits = git log --format="%H" --reverse +$signedCount = 0 +$unsignedCount = 0 +$totalCount = 0 + +Write-Host "`n📋 COMMIT SIGNATURE ANALYSIS:`n" -ForegroundColor Yellow + +foreach ($commit in $commits) { + $totalCount++ + + # Get commit info + $commitShort = git log --format="%h" -n 1 $commit + $commitMsg = git log --format="%s" -n 1 $commit + $commitAuthor = git log --format="%an" -n 1 $commit + $commitDate = git log --format="%ad" --date=short -n 1 $commit + + # Check if commit is signed + $signatureCheck = git log --show-signature --format="" -n 1 $commit 2>&1 | Out-String + + if ($signatureCheck -match "Good signature|gpg:") { + if ($signatureCheck -match "Good signature") { + Write-Host "✅ $commitShort - $commitMsg" -ForegroundColor Green + Write-Host " 👤 $commitAuthor ($commitDate) - VERIFIED SIGNATURE" -ForegroundColor Green + $signedCount++ + } else { + Write-Host "⚠️ $commitShort - $commitMsg" -ForegroundColor Yellow + Write-Host " 👤 $commitAuthor ($commitDate) - SIGNED BUT UNVERIFIED" -ForegroundColor Yellow + $signedCount++ + } + } else { + Write-Host "❌ $commitShort - $commitMsg" -ForegroundColor Red + Write-Host " 👤 $commitAuthor ($commitDate) - NO SIGNATURE" -ForegroundColor Red + $unsignedCount++ + } + Write-Host "" +} + +Write-Host "==================================================" -ForegroundColor Gray +Write-Host "📊 SUMMARY:" -ForegroundColor Cyan +Write-Host " Total commits: $totalCount" -ForegroundColor White +Write-Host " ✅ Signed commits: $signedCount" -ForegroundColor Green +Write-Host " ❌ Unsigned commits: $unsignedCount" -ForegroundColor Red +$coverage = [math]::Round(($signedCount * 100 / $totalCount), 1) +Write-Host " 📈 Signature coverage: $coverage%" -ForegroundColor White +Write-Host "==================================================" -ForegroundColor Gray + +if ($unsignedCount -gt 0) { + Write-Host "`n⚠️ WARNING: $unsignedCount commits are not signed!" -ForegroundColor Yellow + Write-Host " This means they cannot be verified for authenticity." -ForegroundColor Yellow + Write-Host "`n💡 Options to fix this:" -ForegroundColor Cyan + Write-Host " 1. Leave as-is (historical commits often aren't signed)" -ForegroundColor White + Write-Host " 2. Rewrite history with signed commits (advanced, risky)" -ForegroundColor White + Write-Host " 3. Ensure all FUTURE commits are signed (recommended)" -ForegroundColor Green +} diff --git a/scripts/check-signatures.sh b/scripts/check-signatures.sh new file mode 100644 index 0000000..0a638bb --- /dev/null +++ b/scripts/check-signatures.sh @@ -0,0 +1,64 @@ +#!/bin/bash +# Script to check signature status of all commits +# This will show signed vs unsigned commits clearly + +echo "🔍 Checking signature status of all commits..." +echo "==================================================" + +# Get all commit hashes +commits=$(git log --format="%H" --reverse) + +signed_count=0 +unsigned_count=0 +total_count=0 + +echo -e "\n📋 COMMIT SIGNATURE ANALYSIS:\n" + +for commit in $commits; do + total_count=$((total_count + 1)) + + # Get commit info + commit_short=$(git log --format="%h" -n 1 $commit) + commit_msg=$(git log --format="%s" -n 1 $commit) + commit_author=$(git log --format="%an" -n 1 $commit) + commit_date=$(git log --format="%ad" --date=short -n 1 $commit) + + # Check if commit is signed + signature_check=$(git log --show-signature --format="" -n 1 $commit 2>&1) + + if echo "$signature_check" | grep -q "Good signature\|gpg:"; then + if echo "$signature_check" | grep -q "Good signature"; then + echo "✅ $commit_short - $commit_msg" + echo " 👤 $commit_author ($commit_date) - VERIFIED SIGNATURE" + signed_count=$((signed_count + 1)) + else + echo "⚠️ $commit_short - $commit_msg" + echo " 👤 $commit_author ($commit_date) - SIGNED BUT UNVERIFIED" + signed_count=$((signed_count + 1)) + fi + else + echo "❌ $commit_short - $commit_msg" + echo " 👤 $commit_author ($commit_date) - NO SIGNATURE" + unsigned_count=$((unsigned_count + 1)) + fi + echo "" +done + +echo "==================================================" +echo "📊 SUMMARY:" +echo " Total commits: $total_count" +echo " ✅ Signed commits: $signed_count" +echo " ❌ Unsigned commits: $unsigned_count" +echo " 📈 Signature coverage: $(( signed_count * 100 / total_count ))%" +echo "==================================================" + +if [ $unsigned_count -gt 0 ]; then + echo "" + echo "⚠️ WARNING: $unsigned_count commits are not signed!" + echo " This means they cannot be verified for authenticity." + echo "" + echo "💡 Options to fix this:" + echo " 1. Leave as-is (historical commits often aren't signed)" + echo " 2. Rewrite history with signed commits (advanced, risky)" + echo " 3. Ensure all FUTURE commits are signed (recommended)" +fi diff --git a/scripts/gradle/dependency-check.gradle b/scripts/gradle/dependency-check.gradle new file mode 100644 index 0000000..8c58bde --- /dev/null +++ b/scripts/gradle/dependency-check.gradle @@ -0,0 +1,67 @@ +// OWASP Dependency Check configuration for security vulnerability scanning +apply plugin: 'org.owasp.dependencycheck' + +dependencyCheck { + formats = ['HTML', 'JSON'] + autoUpdate = true + suppressionFile = file("$rootDir/owasp-suppressions.xml") + + // Fail build on critical/high vulnerabilities + failBuildOnCVSS = 7.0 + + analyzers { + experimentalEnabled = false + archiveEnabled = false + jarEnabled = true + centralEnabled = true + nexusEnabled = false + pyDistributionEnabled = false + pyPackageEnabled = false + rubygemsEnabled = false + opensslEnabled = false + cmakeEnabled = false + autoconfEnabled = false + composerEnabled = false + nodeEnabled = false + nuspecEnabled = false + cocoapodsEnabled = false + swiftEnabled = false + bundleAuditEnabled = false + } + + // Output directory for reports + outputDirectory = file("$buildDir/reports/dependency-check") + + // Skip configurations that don't need to be checked + skipConfigurations = ['compileClasspath', 'testCompileClasspath'] + + // Scan only runtime dependencies by default + scanConfigurations = ['runtimeClasspath'] + + // Cache directory to speed up subsequent runs + data { + directory = file("$rootDir/.gradle/dependency-check-data") + } + + // Configure NVD API if key is available + nvd { + // Use NVD API key from environment variable if available + apiKey = System.getenv('NVD_API_KEY') ?: project.findProperty('nvd.api.key') ?: '' + // Delay between API calls (in milliseconds) to avoid rate limiting + delay = apiKey ? 0 : 4000 + // Set datafeed URL if using enterprise version + datafeedUrl = project.findProperty('nvd.datafeed.url') ?: '' + } +} + +// Task to print dependency check configuration - useful for debugging +task printDependencyCheckConfig { + doLast { + println "OWASP Dependency Check Configuration for ${project.name}:" + println " - Formats: ${dependencyCheck.formats}" + println " - CVSS Threshold: ${dependencyCheck.failBuildOnCVSS}" + println " - Suppression File: ${dependencyCheck.suppressionFile}" + println " - Output Directory: ${dependencyCheck.outputDirectory}" + println " - NVD API Key: ${dependencyCheck.nvd.apiKey ? 'Configured' : 'Not configured (will use slower datafeed)'}" + } +} diff --git a/scripts/gradle/dependency-updates.gradle b/scripts/gradle/dependency-updates.gradle new file mode 100644 index 0000000..2e4e7ac --- /dev/null +++ b/scripts/gradle/dependency-updates.gradle @@ -0,0 +1,140 @@ +// Gradle Versions Plugin configuration for dependency updates +apply plugin: 'com.github.ben-manes.versions' + +dependencyUpdates { + // Reject pre-release versions unless the current version is a pre-release + rejectVersionIf { + candidate.version.contains('-alpha') || + candidate.version.contains('-beta') || + candidate.version.contains('-rc') || + candidate.version.contains('-M') || + candidate.version.contains('-SNAPSHOT') || + candidate.version.contains('.Alpha') || + candidate.version.contains('.Beta') || + candidate.version.contains('.RC') + } + + // Check for updates to buildscript dependencies + checkBuildEnvironmentConstraints = true + + // Output format + outputFormatter = "json" + outputDir = "build/reports/dependencyUpdates" + + // Revision levels to check + revision = "release" + + // Group modules by their group ID + gradleReleaseChannel = "current" + + // Additional configuration for better reporting + checkConstraints = true + checkForGradleUpdate = true + + // Filter out specific dependencies that should not be updated + // (useful for dependencies with special version constraints) + resolutionStrategy { + componentSelection { rules -> + rules.all { ComponentSelection selection -> + boolean rejected = ['alpha', 'beta', 'rc', 'cr', 'm', 'preview', 'b', 'ea'].any { qualifier -> + selection.candidate.version ==~ /(?i).*[.-]${qualifier}[.\d-+]*/ + } + if (rejected) { + selection.reject('Release candidate') + } + } + } + } +} + +// Task to check for outdated dependencies with detailed output +task checkOutdatedDependencies { + dependsOn dependencyUpdates + + doLast { + def reportFile = file("$buildDir/reports/dependencyUpdates/report.json") + if (reportFile.exists()) { + def json = new groovy.json.JsonSlurper().parseText(reportFile.text) + + println "📦 Dependency Update Report for ${project.name}:" + println "=" * 60 + + if (json.outdated?.dependencies) { + println "📈 Outdated Dependencies (${json.outdated.dependencies.size()}):" + json.outdated.dependencies.each { dep -> + println " • ${dep.group}:${dep.name} ${dep.version} → ${dep.available.release ?: dep.available.milestone}" + } + } else { + println "✅ All dependencies are up to date!" + } + + if (json.gradle?.current && json.gradle?.running) { + if (json.gradle.current.version != json.gradle.running.version) { + println "\n🔧 Gradle Update Available:" + println " • Current: ${json.gradle.running.version} → Available: ${json.gradle.current.version}" + } else { + println "\n✅ Gradle is up to date (${json.gradle.current.version})" + } + } + + if (json.exceeded?.dependencies) { + println "\n⚠️ Dependencies exceeding latest version (${json.exceeded.dependencies.size()}):" + json.exceeded.dependencies.each { dep -> + println " • ${dep.group}:${dep.name} ${dep.version} (using newer than ${dep.available.release})" + } + } + } + } +} + +// Task to generate dependency update summary for CI/CD +task generateDependencyUpdateSummary { + dependsOn dependencyUpdates + + doLast { + def reportFile = file("$buildDir/reports/dependencyUpdates/report.json") + def summaryFile = file("$buildDir/reports/dependencyUpdates/summary.txt") + + if (reportFile.exists()) { + def json = new groovy.json.JsonSlurper().parseText(reportFile.text) + def summary = [] + + summary << "Dependency Update Summary for ${project.name}" + summary << "Generated: ${new Date()}" + summary << "=" * 50 + + def outdatedCount = json.outdated?.dependencies?.size() ?: 0 + def upToDateCount = json.current?.dependencies?.size() ?: 0 + def exceededCount = json.exceeded?.dependencies?.size() ?: 0 + + summary << "Statistics:" + summary << " • Up to date: ${upToDateCount}" + summary << " • Outdated: ${outdatedCount}" + summary << " • Exceeded: ${exceededCount}" + summary << "" + + if (outdatedCount > 0) { + summary << "Outdated Dependencies:" + json.outdated.dependencies.each { dep -> + summary << " • ${dep.group}:${dep.name} ${dep.version} → ${dep.available.release ?: dep.available.milestone}" + } + } + + summaryFile.text = summary.join('\n') + println "📋 Dependency update summary saved to: ${summaryFile}" + } + } +} + +// Task to print dependency updates configuration - useful for debugging +task printDependencyUpdatesConfig { + doLast { + println "Dependency Updates Configuration for ${project.name}:" + println " - Output Directory: ${dependencyUpdates.outputDir}" + println " - Output Formatter: ${dependencyUpdates.outputFormatter}" + println " - Revision: ${dependencyUpdates.revision}" + println " - Check Build Environment: ${dependencyUpdates.checkBuildEnvironmentConstraints}" + println " - Check for Gradle Updates: ${dependencyUpdates.checkForGradleUpdate}" + println " - Gradle Release Channel: ${dependencyUpdates.gradleReleaseChannel}" + } +} diff --git a/scripts/gradle/jacoco.gradle b/scripts/gradle/jacoco.gradle new file mode 100644 index 0000000..3b241ab --- /dev/null +++ b/scripts/gradle/jacoco.gradle @@ -0,0 +1,209 @@ +// JaCoCo configuration for code coverage reporting +apply plugin: 'jacoco' + +// Define project extension for configuring coverage exclusions +// This allows customizing exclusions in each project's build.gradle file +ext { + modulesExcludedFromCoverage = [ + // Add modules that should be excluded from coverage requirements + // Example: 'demo-module', 'example-module' + ] + jacocoExclusions = [ + // Data and model classes (no business logic) + '*.dto.*', // Data transfer objects + '*.entity.*', // Database entities + '*.model.*', // Model classes + '**/*Config*', // Configuration classes + '**/*Constants*', // Constants classes + + // Exception classes (definition only) + '*.exception.*', // Exception classes + '**/*Exception*', // Exception classes by name pattern + + // Annotations (definition only) - UPDATED TO COVER ALL ANNOTATION CLASSES + '*.annotation.*', // Annotation packages + '**/*Annotation*', // Annotation classes by name pattern + '**/annotation/**', // All files in annotation directories + '**/*Cacheable*', // Cacheable annotation classes (includes BulkCacheable) + '**/*CacheEvict*', // CacheEvict annotation classes (includes BulkCacheEvict) + '**/*CachePut*', // CachePut annotation classes (includes BulkCachePut) + + // Spring Boot auto-configuration classes (mostly definition) + '**/*AutoConfiguration*', // Auto-configuration classes + '**/*Configuration*', // Configuration classes (includes AspectConfiguration) + '**/autoconfigure/**', // All autoconfiguration packages + + // Properties and configuration binding classes + '**/*Properties*', // Configuration properties classes (includes BulkCacheProperties) + '**/config/**', // All config packages + + // Data classes and value objects (Kotlin) + '**/*\$Companion*', // Kotlin companion objects + '**/*\$WhenMappings*', // Kotlin when mappings + '**/*\$DefaultImpls*', // Kotlin interface default implementations + + // Application and framework classes + '*Application', // Main application class + '*Application\$*', // Application nested classes + '*ApplicationKt', // Kotlin main function files + '*ApplicationKt\$*', // Kotlin main function nested classes + + // Auto-generated classes + '**/*\$\$serializer*', // Kotlinx serialization + '**/*\$Creator*', // Android Parcelable creators + + // Builder pattern classes (often boilerplate) + '**/*Builder*', // Builder classes + + // Context and configuration classes + '*.context.*', // Context classes + '**/*Context*' // Context classes by name + ] +} + +jacoco { + toolVersion = "${jacoco_version}" // Use version from gradle.properties +} + +// Configure the JaCoCo test report +jacocoTestReport { + dependsOn test + reports { + xml.required = true // XML report needed for coverage tools in CI + csv.required = true // CSV report used for badge generation + html.required = true + html.outputLocation = layout.buildDirectory.dir('reports/jacoco') + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, excludes: project.jacocoExclusions) + })) + } +} + +// Enforce minimum code coverage thresholds +jacocoTestCoverageVerification { + dependsOn jacocoTestReport + violationRules { + rule { + limit { + minimum = project.findProperty('code_coverage_minimum') as BigDecimal // Use threshold from gradle.properties + } + } + + // Class-level coverage rule - exclude classes without business logic + rule { + element = 'CLASS' + excludes = project.jacocoExclusions + limit { + counter = 'INSTRUCTION' + value = 'COVEREDRATIO' + minimum = project.findProperty('code_coverage_minimum') as BigDecimal + } + } + } +} + +// Make check depend on coverage verification +check.dependsOn jacocoTestCoverageVerification + +// Make the test task run coverage verification +test { + finalizedBy jacocoTestReport +} + +// Ensure coverage verification runs when tests run +check { + dependsOn jacocoTestCoverageVerification +} + +// Root project aggregate tasks (only applied when this script is used by root project) +if (project == rootProject) { + // Create an aggregate JaCoCo report that combines coverage from all submodules + task jacocoRootReport(type: JacocoReport) { + description = 'Generates an aggregate report from all subprojects' + group = 'reporting' + + // Properly declare dependencies on all test tasks (including root project if it has tests) + dependsOn(subprojects.test) + + // Also depend on root project test task if it exists + if (tasks.findByName('test')) { + dependsOn test + } + + // Depend on all jacocoTestReport tasks from subprojects + subprojects.each { subproject -> + if (subproject.tasks.findByName('jacocoTestReport')) { + dependsOn subproject.jacocoTestReport + } + } + + // Only collect execution data from subprojects, not root project + executionData fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec").exclude("build/jacoco/*.exec") + + // Collect source and class files from all subprojects that have coverage enabled + subprojects.each { subproject -> + // Only include subprojects that aren't excluded from coverage + if (!rootProject.hasProperty('modulesExcludedFromCoverage') || !rootProject.modulesExcludedFromCoverage.contains(subproject.name)) { + + if (subproject.plugins.hasPlugin('jacoco') && subproject.sourceSets.findByName('main')) { + sourceSets subproject.sourceSets.main + } + } + } + + reports { + xml.required = true + csv.required = true + html.required = true + xml.outputLocation = layout.buildDirectory.file('reports/jacoco/test/jacocoTestReport.xml') + csv.outputLocation = layout.buildDirectory.file('reports/jacoco/test/jacocoTestReport.csv') + html.outputLocation = layout.buildDirectory.dir('reports/jacoco/test/html') + } + + // Apply exclusions defined in this script + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, excludes: project.jacocoExclusions) + })) + } + } + + // Aggregate coverage verification + task jacocoRootCoverageVerification(type: JacocoCoverageVerification) { + description = 'Verifies code coverage metrics for the entire project' + group = 'verification' + dependsOn jacocoRootReport + + executionData fileTree(project.rootDir.absolutePath).include("**/build/jacoco/*.exec") + + // Collect source and class files from all subprojects that have coverage enabled + subprojects.each { subproject -> + if (!rootProject.hasProperty('modulesExcludedFromCoverage') || !rootProject.modulesExcludedFromCoverage.contains(subproject.name)) { + + if (subproject.plugins.hasPlugin('jacoco')) { + sourceSets subproject.sourceSets.main + } + } + } + + violationRules { + rule { + limit { + minimum = project.findProperty('code_coverage_minimum') as BigDecimal + } + } + } + + afterEvaluate { + classDirectories.setFrom(files(classDirectories.files.collect { + fileTree(dir: it, excludes: project.jacocoExclusions) + })) + } + } + + // Make check depend on aggregate coverage verification + check.dependsOn jacocoRootCoverageVerification +} diff --git a/scripts/gradle/jmh.gradle b/scripts/gradle/jmh.gradle new file mode 100644 index 0000000..a4d3588 --- /dev/null +++ b/scripts/gradle/jmh.gradle @@ -0,0 +1,123 @@ +// JMH (Java Microbenchmark Harness) configuration for performance testing +// This script is applied to projects that have benchmark tests in src/jmh directory + +// Only apply JMH plugin if the project has benchmark source directory +if (file("$projectDir/src/jmh").exists()) { + apply plugin: "me.champeau.jmh" + + jmh { + // JMH execution parameters + fork = 1 // Number of forked JVMs + warmupIterations = 2 // Number of warmup iterations + iterations = 3 // Number of measurement iterations + timeUnit = 'ms' // Time unit for results + + // Output configuration + resultFormat = 'JSON' // Output format (JSON for automation) + resultsFile = file("$buildDir/reports/jmh/jmh-result.json") + + // Benchmark selection + includes = ['.*Benchmark.*'] // Include classes matching benchmark pattern + + // Additional JMH options + benchmarkMode = ['avgt'] // Average time mode (can be: thrpt, avgt, sample, ss, all) + threads = 1 // Number of worker threads + + // JVM options for benchmarks + jvmArgs = [ + '-server', // Use server JVM + '-Xms512m', // Initial heap size + '-Xmx1g', // Maximum heap size + '-XX:+UseG1GC' // Use G1 garbage collector for consistent performance + ] + + // Profiler configuration (uncomment to enable) + // profilers = ['stack'] // Enable stack profiler + // profilers = ['gc'] // Enable GC profiler + + // Human-readable output + humanOutputFile = file("$buildDir/reports/jmh/human.txt") + + // Benchmark parameters (can be overridden in benchmark classes) + // benchmarkParameters = [ + // 'size': ['100', '1000', '10000'] + // ] + } + + // Task to run benchmarks with custom configuration + task benchmarkPerformance(type: me.champeau.jmh.JmhBytecodeGeneratorTask) { + dependsOn classes + + doLast { + println "🚀 Running performance benchmarks for ${project.name}..." + println "📊 Results will be available at: ${jmh.resultsFile.get()}" + } + } + + // Task to run quick benchmarks (fewer iterations for development) + task benchmarkQuick { + dependsOn jmhClasses + + doLast { + javaexec { + classpath = sourceSets.jmh.runtimeClasspath + main = 'org.openjdk.jmh.Main' + args = [ + '-wi', '1', // 1 warmup iteration + '-i', '1', // 1 measurement iteration + '-f', '1', // 1 fork + '-rf', 'json', // JSON format + '-rff', "$buildDir/reports/jmh/quick-result.json" + ] + } + println "⚡ Quick benchmark completed. Results: $buildDir/reports/jmh/quick-result.json" + } + } + + // Task to print JMH configuration - useful for debugging + task printJmhConfig { + doLast { + println "JMH Configuration for ${project.name}:" + println " - Fork: ${jmh.fork.get()}" + println " - Warmup Iterations: ${jmh.warmupIterations.get()}" + println " - Measurement Iterations: ${jmh.iterations.get()}" + println " - Time Unit: ${jmh.timeUnit.get()}" + println " - Benchmark Mode: ${jmh.benchmarkMode.get()}" + println " - Threads: ${jmh.threads.get()}" + println " - Results File: ${jmh.resultsFile.get()}" + println " - Includes: ${jmh.includes.get()}" + } + } + + // Ensure JMH reports directory exists + tasks.register('createJmhReportsDir') { + doLast { + file("$buildDir/reports/jmh").mkdirs() + } + } + + // Make JMH tasks depend on creating the reports directory + tasks.matching { it.name.startsWith('jmh') }.all { + dependsOn createJmhReportsDir + } + +} else { + // Create a placeholder task if no JMH source directory exists + task jmh { + doLast { + println "ℹ️ No JMH benchmarks found in ${project.name} (src/jmh directory doesn't exist)" + println " To add performance benchmarks:" + println " 1. Create src/jmh/java directory" + println " 2. Add benchmark classes with @Benchmark annotations" + println " 3. Run './gradlew jmh' to execute benchmarks" + } + } + + task benchmarkPerformance { + dependsOn jmh + } + + task benchmarkQuick { + dependsOn jmh + } +} diff --git a/scripts/gradle/kotlin.gradle b/scripts/gradle/kotlin.gradle new file mode 100644 index 0000000..610a122 --- /dev/null +++ b/scripts/gradle/kotlin.gradle @@ -0,0 +1,56 @@ +// Common build configuration for Kotlin-based modules +apply plugin: "org.jetbrains.kotlin.jvm" +apply plugin: "org.jetbrains.kotlin.plugin.allopen" +apply plugin: "java" + +// Configure allopen plugin for Spring annotations +// This makes Kotlin classes open (non-final) when annotated with Spring annotations +allOpen { + annotations( + "org.springframework.stereotype.Component", + "org.springframework.stereotype.Service", + "org.springframework.stereotype.Repository", + "org.springframework.stereotype.Controller", + "org.springframework.web.bind.annotation.RestController", + "org.springframework.boot.autoconfigure.SpringBootApplication", + "org.springframework.context.annotation.Configuration", + "org.springframework.transaction.annotation.Transactional" + ) +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } + sourceCompatibility = JavaVersion.VERSION_21 + targetCompatibility = JavaVersion.VERSION_21 +} + +kotlin { + jvmToolchain(21) + + compilerOptions { + // -Xjsr305=strict ensures JSR-305 annotations are treated as nullability annotations with strict semantics + // This improves null-safety when working with Java libraries that use JSR-305 annotations + freeCompilerArgs.add("-Xjsr305=strict") + } +} + +test { + useTestNG() +} + +// Add core dependencies needed for Kotlin +dependencies { + // Kotlin standard library - the versions are automatically resolved from the kotlin plugin version + // kotlin-stdlib-jdk8 includes JDK 7 and 8 extensions with Java interoperability + implementation "org.jetbrains.kotlin:kotlin-reflect" + implementation "org.jetbrains.kotlin:kotlin-stdlib" + + // Coroutines support for async programming + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactive:$kotlin_coroutines_version" + + // For Spring support specifically + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlin_coroutines_version" +} diff --git a/scripts/gradle/license-report.gradle b/scripts/gradle/license-report.gradle new file mode 100644 index 0000000..6357b79 --- /dev/null +++ b/scripts/gradle/license-report.gradle @@ -0,0 +1,70 @@ +// License compliance reporting configuration +apply plugin: 'com.github.jk1.dependency-license-report' + +licenseReport { + // Use default renderers - the plugin will automatically provide CSV, JSON, and HTML + // No need to specify renderers explicitly when using defaults + + // Exclude test dependencies from license report + excludeGroups = ['org.testng', 'io.mockk'] + + // Additional exclusions for common test and build-time dependencies + excludes = [ + // Test frameworks + 'junit:junit', + 'org.junit.jupiter:junit-jupiter', + 'org.mockito:mockito-core', + 'org.testcontainers:testcontainers', + + // Build tools + 'org.gradle:gradle-core', + 'org.jetbrains.kotlin:kotlin-gradle-plugin', + + // IDE and development tools + 'org.jetbrains:annotations' + ] + + // Output directory for license reports + outputDir = "$buildDir/reports/dependency-license" + + // Include only runtime dependencies in the report + configurations = ['runtimeClasspath'] +} + +// Task to validate license compliance +task validateLicenseCompliance { + dependsOn generateLicenseReport + + doLast { + def reportFile = file("$buildDir/reports/dependency-license/licenses.csv") + if (reportFile.exists()) { + def problematicLicenses = [] + reportFile.eachLine { line -> + // Check for common problematic licenses (customize as needed) + if (line.contains('GPL-3.0') || line.contains('AGPL') || line.contains('No license found')) { + problematicLicenses.add(line) + } + } + + if (!problematicLicenses.empty) { + println "⚠️ Potentially problematic licenses detected:" + problematicLicenses.each { println " - $it" } + println "\nReview these licenses and add suppressions if they are acceptable." + } else { + println "✅ All dependency licenses appear to be compliant." + } + } + } +} + +// Task to print license report configuration - useful for debugging +task printLicenseReportConfig { + doLast { + println "License Report Configuration for ${project.name}:" + println " - Output Directory: ${licenseReport.outputDir}" + println " - Configurations: ${licenseReport.configurations}" + println " - Excluded Groups: ${licenseReport.excludeGroups}" + println " - Excluded Dependencies: ${licenseReport.excludes.size()} items" + println " - Renderers: ${licenseReport.renderers.size()} formats" + } +} diff --git a/scripts/gradle/spring_boot.gradle b/scripts/gradle/spring_boot.gradle new file mode 100644 index 0000000..6550318 --- /dev/null +++ b/scripts/gradle/spring_boot.gradle @@ -0,0 +1,20 @@ +apply plugin: "org.springframework.boot" +apply plugin: "io.spring.dependency-management" +apply plugin: "org.jetbrains.kotlin.plugin.spring" + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(21) + } +} + +kotlin { + jvmToolchain(21) +} + +dependencies { + implementation "org.springframework.boot:spring-boot-starter" + implementation "org.springframework.boot:spring-boot-starter-web" + implementation "org.springframework.boot:spring-boot-starter-webflux" + testImplementation "org.springframework.boot:spring-boot-starter-test" +} diff --git a/scripts/gradle/spring_library.gradle b/scripts/gradle/spring_library.gradle new file mode 100644 index 0000000..26f86e3 --- /dev/null +++ b/scripts/gradle/spring_library.gradle @@ -0,0 +1,23 @@ +// Spring library configuration - for modules that use Spring but are not executable applications +// This provides Spring dependency management without the executable application features + +apply plugin: 'io.spring.dependency-management' +apply from: "$rootDir/scripts/gradle/kotlin.gradle" +apply from: "$rootDir/scripts/gradle/jacoco.gradle" +apply from: "$rootDir/scripts/gradle/testing.gradle" + +// Apply dependency management from Spring Boot BOM for consistent versions +dependencyManagement { + imports { + mavenBom "org.springframework.boot:spring-boot-dependencies:${spring_boot_version}" + } +} + +// Standard library JAR configuration +jar { + enabled = true + archiveClassifier = '' // Ensure no classifier for main jar +} + +// No bootJar for libraries - they are not executable applications +// This avoids the need to disable bootJar in each module diff --git a/scripts/gradle/tasks.gradle b/scripts/gradle/tasks.gradle new file mode 100644 index 0000000..5cf44e8 --- /dev/null +++ b/scripts/gradle/tasks.gradle @@ -0,0 +1,47 @@ +tasks.register('resolveAndLockAll') { + description = 'Resolves and locks all project dependencies' + group = 'dependency locking' + + doFirst { + println "Resolving and locking all project dependencies..." + boolean writeLocks = project.hasProperty('write-locks') + + if (writeLocks) { + println "Will write lock files for all projects" + } else { + println "Running in dry-run mode (use --write-locks to update lock files)" + } + } + + doLast { + allprojects.each { subproject -> + println "Processing project: ${subproject.name}" + + subproject.configurations.configureEach { config -> + if (config.canBeResolved) { + println " Resolving configuration: ${config.name}" + try { + config.resolve() + } catch (Exception e) { + println " Could not resolve configuration ${config.name}: ${e.message}" + } + } + } + } + + println "Dependency resolution completed. Lock files generated." + } +} + +// Hook into project evaluation to make sure --write-locks is handled +gradle.allprojects { proj -> + proj.afterEvaluate { + if (gradle.startParameter.taskNames.contains('resolveAndLockAll') && + gradle.startParameter.projectProperties.containsKey('write-locks')) { + proj.dependencyLocking { + lockAllConfigurations() + lockMode = LockMode.STRICT + } + } + } +} diff --git a/scripts/gradle/testing.gradle b/scripts/gradle/testing.gradle new file mode 100644 index 0000000..2795271 --- /dev/null +++ b/scripts/gradle/testing.gradle @@ -0,0 +1,75 @@ +// Common testing configuration for all modules +apply plugin: 'java' + +// Testing configuration for the project +// This script configures test execution to filter tests by group + +// Configure all test tasks +tasks.withType(Test).configureEach { + // Use TestNG for testing + useTestNG { + // By default, only run tests with the 'small' group (unit tests) + includeGroups 'small' + + // Allow specifying additional groups via command line + if (project.hasProperty('testGroups')) { + includeGroups project.getProperty('testGroups').split(',') + } + + // Allow running all tests with the 'all' property + if (project.hasProperty('all')) { + includeGroups = [] + } + + // Increase test output verbosity if needed + testLogging { + events "passed", "skipped", "failed" + showStandardStreams = project.hasProperty('verbose') + } + } + + // Run tests in parallel if possible + maxParallelForks = Runtime.runtime.availableProcessors().intdiv(2) ?: 1 + + // Always run all tests, don't stop on first failure + failFast = false +} + +// Register task to run all tests including integration tests (only if not already exists) +if (!tasks.findByName('allTests')) { + tasks.register('allTests') { + description = 'Run all tests including integration tests' + group = 'verification' + dependsOn tasks.test + + doFirst { + tasks.test.configure { + useTestNG { + includeGroups = [] + } + } + } + } +} + +// Register a task specifically for integration tests (only if not already exists) +if (!tasks.findByName('integrationTest')) { + tasks.register('integrationTest', Test) { + description = 'Run only integration tests' + group = 'verification' + + useTestNG { + includeGroups 'medium', 'integration' + } + } +} + +// Hook into the check task to ensure code coverage is verified +tasks.named('check').configure { + dependsOn tasks.test + + // Add jacoco test coverage verification if JaCoCo is applied + if (project.plugins.hasPlugin('jacoco')) { + dependsOn tasks.jacocoTestCoverageVerification + } +} diff --git a/scripts/sign-all-commits.ps1 b/scripts/sign-all-commits.ps1 new file mode 100644 index 0000000..f74094b --- /dev/null +++ b/scripts/sign-all-commits.ps1 @@ -0,0 +1,79 @@ +# PowerShell script to sign all commits in the repository +# This rewrites Git history to add signatures to all commits + +Write-Host "🔐 Starting to sign all commits in the repository..." -ForegroundColor Cyan +Write-Host "⚠️ WARNING: This will rewrite Git history!" -ForegroundColor Yellow +Write-Host "" + +# Check if GPG is configured +$signingKey = git config --get user.signingkey +if (-not $signingKey) { + Write-Host "❌ Error: No GPG signing key configured!" -ForegroundColor Red + Write-Host "Configure it with: git config user.signingkey YOUR_KEY_ID" -ForegroundColor Yellow + exit 1 +} + +Write-Host "✅ Using GPG key: $signingKey" -ForegroundColor Green +Write-Host "" + +# Count total commits +$totalCommits = (git rev-list --count HEAD) +Write-Host "📊 Total commits to sign: $totalCommits" -ForegroundColor Cyan +Write-Host "" + +Write-Host "🚀 Starting the signing process..." -ForegroundColor Yellow +Write-Host " This may take a few minutes depending on repository size..." -ForegroundColor Gray +Write-Host "" + +# Method 1: Use git rebase to sign commits interactively +Write-Host "Using git rebase method to sign all commits..." -ForegroundColor Cyan + +# Get the root commit (first commit) +$rootCommit = git rev-list --max-parents=0 HEAD +Write-Host "📍 Root commit: $rootCommit" -ForegroundColor Gray + +try { + # Set environment variable to automatically sign commits during rebase + $env:GIT_SEQUENCE_EDITOR = "sed -i 's/^pick/edit/g'" + + # Start interactive rebase from root + Write-Host "Starting interactive rebase from root commit..." -ForegroundColor Yellow + + # Alternative approach: Use git filter-branch with PowerShell + Write-Host "Using git filter-branch method..." -ForegroundColor Cyan + + # Create a temporary script for git filter-branch + $tempScript = @" +#!/bin/sh +export GIT_COMMITTER_NAME="`$GIT_AUTHOR_NAME" +export GIT_COMMITTER_EMAIL="`$GIT_AUTHOR_EMAIL" +export GIT_COMMITTER_DATE="`$GIT_AUTHOR_DATE" +git commit-tree -S "`$@" +"@ + + $tempScriptPath = "temp-sign-commit.sh" + $tempScript | Out-File -FilePath $tempScriptPath -Encoding ASCII + + # Make the script executable and run filter-branch + git filter-branch -f --commit-filter "sh $tempScriptPath" -- --all + + # Clean up temporary script + Remove-Item $tempScriptPath -ErrorAction SilentlyContinue + + Write-Host "" + Write-Host "✅ SUCCESS: All commits have been signed!" -ForegroundColor Green + Write-Host "" + Write-Host "📋 Next steps:" -ForegroundColor Cyan + Write-Host " 1. Verify the signatures: git log --show-signature --oneline" -ForegroundColor White + Write-Host " 2. If everything looks good, force push: git push --force-with-lease origin main" -ForegroundColor White + Write-Host " 3. Inform collaborators about the history rewrite" -ForegroundColor White + Write-Host "" + Write-Host "⚠️ Note: You'll need to force push because history was rewritten" -ForegroundColor Yellow +} +catch { + Write-Host "" + Write-Host "❌ ERROR: Failed to sign commits!" -ForegroundColor Red + Write-Host " Error: $($_.Exception.Message)" -ForegroundColor Red + Write-Host " You can restore from backup: git checkout backup-before-signing" -ForegroundColor Yellow + exit 1 +} diff --git a/scripts/sign-all-commits.sh b/scripts/sign-all-commits.sh new file mode 100644 index 0000000..a5d1aa9 --- /dev/null +++ b/scripts/sign-all-commits.sh @@ -0,0 +1,59 @@ +#!/bin/bash +# Script to sign all commits in the repository +# This rewrites Git history to add signatures to all commits + +echo "🔐 Starting to sign all commits in the repository..." +echo "⚠️ WARNING: This will rewrite Git history!" +echo "" + +# Check if GPG is configured +SIGNING_KEY=$(git config --get user.signingkey) +if [ -z "$SIGNING_KEY" ]; then + echo "❌ Error: No GPG signing key configured!" + echo "Configure it with: git config user.signingkey YOUR_KEY_ID" + exit 1 +fi + +echo "✅ Using GPG key: $SIGNING_KEY" +echo "" + +# Get the root commit (first commit in the repository) +ROOT_COMMIT=$(git rev-list --max-parents=0 HEAD) +echo "📍 Root commit: $ROOT_COMMIT" + +# Count total commits +TOTAL_COMMITS=$(git rev-list --count HEAD) +echo "📊 Total commits to sign: $TOTAL_COMMITS" +echo "" + +echo "🚀 Starting the signing process..." +echo " This may take a few minutes depending on repository size..." +echo "" + +# Use git filter-branch to sign all commits +git filter-branch -f --commit-filter ' + if [ "$GIT_COMMIT" = "'$ROOT_COMMIT'" ]; then + # For the root commit, we need to handle it specially + git commit-tree -S "$@" + else + # For all other commits + git commit-tree -S "$@" + fi +' -- --all + +if [ $? -eq 0 ]; then + echo "" + echo "✅ SUCCESS: All commits have been signed!" + echo "" + echo "📋 Next steps:" + echo " 1. Verify the signatures: git log --show-signature --oneline" + echo " 2. If everything looks good, force push: git push --force-with-lease origin main" + echo " 3. Inform collaborators about the history rewrite" + echo "" + echo "⚠️ Note: You'll need to force push because history was rewritten" +else + echo "" + echo "❌ ERROR: Failed to sign commits!" + echo " You can restore from backup: git checkout backup-before-signing" + exit 1 +fi diff --git a/service-module/build.gradle b/service-module/build.gradle new file mode 100644 index 0000000..879aa23 --- /dev/null +++ b/service-module/build.gradle @@ -0,0 +1,20 @@ +apply from: "$rootDir/scripts/gradle/spring_library.gradle" + +dependencies { + // Spring dependencies + implementation "org.springframework.boot:spring-boot-starter" + implementation "org.springframework.boot:spring-boot-starter-data-jpa" + implementation "org.springframework:spring-tx" + + // Validation + implementation "org.springframework.boot:spring-boot-starter-validation" + + // Kotlin Coroutines + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-core:$kotlin_coroutines_version" + implementation "org.jetbrains.kotlinx:kotlinx-coroutines-reactor:$kotlin_coroutines_version" + + // Testing dependencies + testImplementation "org.testng:testng:$testng_version" + testImplementation "io.mockk:mockk:$mockk_version" + testImplementation "org.springframework.boot:spring-boot-starter-test" +} diff --git a/service-module/gradle.lockfile b/service-module/gradle.lockfile new file mode 100644 index 0000000..ceecdad --- /dev/null +++ b/service-module/gradle.lockfile @@ -0,0 +1,135 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.5.18=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +ch.qos.logback:logback-core:1.5.18=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.beust:jcommander:1.82=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml:classmate:1.7.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.sun.istack:istack-commons-runtime:4.1.2=runtimeClasspath,testRuntimeClasspath +com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.zaxxer:HikariCP:6.3.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.micrometer:micrometer-commons:1.15.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.micrometer:micrometer-observation:1.15.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-agent-api-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk-agent-api:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-agent-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk-agent:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-core-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk-core:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-dsl-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk-dsl:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.projectreactor:reactor-core:3.7.7=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.smallrye:jandex:3.2.0=runtimeClasspath,testRuntimeClasspath +jakarta.activation:jakarta.activation-api:2.1.3=runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=runtimeClasspath,testRuntimeClasspath +jakarta.persistence:jakarta.persistence-api:3.1.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.transaction:jakarta.transaction-api:2.0.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +junit:junit:4.13.2=testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.17.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.17.6=runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.minidev:accessors-smart:2.5.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.minidev:json-smart:2.5.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.antlr:antlr4-runtime:4.13.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-el:10.1.42=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata +org.aspectj:aspectjweaver:1.9.24=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.assertj:assertj-core:3.27.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.awaitility:awaitility:4.2.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.eclipse.angus:angus-activation:2.0.2=runtimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:jaxb-core:4.0.5=runtimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:jaxb-runtime:4.0.5=runtimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:txw2:4.0.5=runtimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:3.0=testRuntimeClasspath +org.hamcrest:hamcrest:3.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.hibernate.common:hibernate-commons-annotations:7.0.3.Final=runtimeClasspath,testRuntimeClasspath +org.hibernate.orm:hibernate-core:6.6.18.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.hibernate.validator:hibernate-validator:8.0.2.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jacoco:org.jacoco.agent:0.8.10=jacocoAgent,jacocoAnt +org.jacoco:org.jacoco.ant:0.8.10=jacocoAnt +org.jacoco:org.jacoco.core:0.8.10=jacocoAnt +org.jacoco:org.jacoco.report:0.8.10=jacocoAnt +org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-allopen-compiler-plugin-embeddable:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-build-common:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.25=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-daemon-client:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.9.25=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.9.25=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.9.25=compileClasspath,implementationDependenciesMetadata,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.9.25=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.25=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.25=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.25=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.9.25=compileClasspath,implementationDependenciesMetadata,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlinx:atomicfu:0.23.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.0=implementationDependenciesMetadata,testImplementationDependenciesMetadata +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1=compileClasspath,kotlinBuildToolsApiClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.8.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.0=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains:annotations:23.0.0=compileClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath +org.junit:junit-bom:5.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.mockito:mockito-core:5.17.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.17.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.objenesis:objenesis:3.3=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.ow2.asm:asm-commons:9.5=jacocoAnt +org.ow2.asm:asm-tree:9.5=jacocoAnt +org.ow2.asm:asm:9.5=jacocoAnt +org.ow2.asm:asm:9.7.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-autoconfigure:3.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-data-jpa:3.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-jdbc:3.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-logging:3.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-test:3.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-validation:3.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter:3.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-test-autoconfigure:3.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot:3.5.3=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.data:spring-data-commons:3.5.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.data:spring-data-jpa:3.5.1=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-aop:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-aspects:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-beans:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-context:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-core:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-expression:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-jcl:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-jdbc:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-orm:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-test:6.2.8=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-tx:6.2.8=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.testng:testng:7.7.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.webjars:jquery:3.6.1=testRuntimeClasspath +org.xmlunit:xmlunit-core:2.10.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.yaml:snakeyaml:2.4=compileClasspath,implementationDependenciesMetadata,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +empty=annotationProcessor,apiDependenciesMetadata,compileOnlyDependenciesMetadata,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions diff --git a/service-module/src/main/kotlin/io/programmernewbie/template/service/ServiceImplementations.kt b/service-module/src/main/kotlin/io/programmernewbie/template/service/ServiceImplementations.kt new file mode 100644 index 0000000..34a03fe --- /dev/null +++ b/service-module/src/main/kotlin/io/programmernewbie/template/service/ServiceImplementations.kt @@ -0,0 +1,30 @@ +package io.programmernewbie.template.service + +import org.springframework.stereotype.Service +import org.springframework.transaction.annotation.Transactional + +/** + * Example service implementation for the template + * + * Replace this with your actual business logic services + */ +@Service +@Transactional +class ExampleService { + + /** + * Example method - replace with your actual business logic + */ + fun getWelcomeMessage(name: String = "World"): String { + return "Hello, $name! This is your Kotlin Multimodule Template." + } + + /** + * Example async method - replace with your actual business logic + */ + suspend fun getAsyncWelcomeMessage(name: String = "World"): String { + // Simulate some async work + kotlinx.coroutines.delay(100) + return "Hello async, $name! This is your Kotlin Multimodule Template." + } +} diff --git a/service-module/src/test/kotlin/io/programmernewbie/template/service/ServiceImplementationTest.kt b/service-module/src/test/kotlin/io/programmernewbie/template/service/ServiceImplementationTest.kt new file mode 100644 index 0000000..142e553 --- /dev/null +++ b/service-module/src/test/kotlin/io/programmernewbie/template/service/ServiceImplementationTest.kt @@ -0,0 +1,161 @@ +package io.programmernewbie.template.service + +import kotlinx.coroutines.runBlocking +import org.testng.Assert.assertEquals +import org.testng.Assert.assertTrue +import org.testng.annotations.BeforeMethod +import org.testng.annotations.Test + +/** + * Small unit tests for ExampleService + * + * These tests are fast (< 100ms) and test pure business logic without external dependencies. + * They ensure the service methods work correctly and meet the 85% coverage requirement. + */ +@Test(groups = ["small"]) +class ServiceImplementationTest { + + private lateinit var exampleService: ExampleService + + @BeforeMethod + fun setup() { + exampleService = ExampleService() + } + + @Test + fun `getWelcomeMessage, with custom name, returns personalized message`() { + // Given + val name = "Alice" + + // When + val result = exampleService.getWelcomeMessage(name) + + // Then + assertEquals(result, "Hello, Alice! This is your Kotlin Multimodule Template.") + } + + @Test + fun `getWelcomeMessage, with default name, returns default message`() { + // When + val result = exampleService.getWelcomeMessage() + + // Then + assertEquals(result, "Hello, World! This is your Kotlin Multimodule Template.") + } + + @Test + fun `getWelcomeMessage, with empty string, returns message with empty name`() { + // Given + val name = "" + + // When + val result = exampleService.getWelcomeMessage(name) + + // Then + assertEquals(result, "Hello, ! This is your Kotlin Multimodule Template.") + } + + @Test + fun `getWelcomeMessage, with special characters, returns message with special characters`() { + // Given + val name = "João & María" + + // When + val result = exampleService.getWelcomeMessage(name) + + // Then + assertEquals(result, "Hello, João & María! This is your Kotlin Multimodule Template.") + } + + @Test + fun `getWelcomeMessage, with long name, returns message with long name`() { + // Given + val name = "VeryLongNameThatShouldStillWorkCorrectly" + + // When + val result = exampleService.getWelcomeMessage(name) + + // Then + assertEquals(result, "Hello, VeryLongNameThatShouldStillWorkCorrectly! This is your Kotlin Multimodule Template.") + } + + @Test + fun `getAsyncWelcomeMessage, with custom name, returns async personalized message`() { + // Given + val name = "Bob" + + // When + val result = runBlocking { exampleService.getAsyncWelcomeMessage(name) } + + // Then + assertEquals(result, "Hello async, Bob! This is your Kotlin Multimodule Template.") + } + + @Test + fun `getAsyncWelcomeMessage, with default name, returns async default message`() { + // When + val result = runBlocking { exampleService.getAsyncWelcomeMessage() } + + // Then + assertEquals(result, "Hello async, World! This is your Kotlin Multimodule Template.") + } + + @Test + fun `getAsyncWelcomeMessage, with empty string, returns async message with empty name`() { + // Given + val name = "" + + // When + val result = runBlocking { exampleService.getAsyncWelcomeMessage(name) } + + // Then + assertEquals(result, "Hello async, ! This is your Kotlin Multimodule Template.") + } + + @Test + fun `getAsyncWelcomeMessage, execution time, completes within reasonable time`() { + // Given + val name = "Performance" + val startTime = System.currentTimeMillis() + + // When + runBlocking { exampleService.getAsyncWelcomeMessage(name) } + val endTime = System.currentTimeMillis() + + // Then + val executionTime = endTime - startTime + assertTrue(executionTime >= 90, "Async method should simulate delay (>=90ms)") + } + + @Test + fun `getAsyncWelcomeMessage, is actually suspending, delay is present`() { + // Given + val name = "Async" + + // When & Then - This test verifies the method is actually suspending + runBlocking { + val startTime = System.currentTimeMillis() + val result = exampleService.getAsyncWelcomeMessage(name) + val endTime = System.currentTimeMillis() + + assertEquals(result, "Hello async, Async! This is your Kotlin Multimodule Template.") + assertTrue(endTime - startTime >= 100, "Should have simulated async delay") + } + } + + @Test + fun `messageFormat, both methods, have consistent structure`() { + // Given + val name = "Consistency" + + // When + val syncMessage = exampleService.getWelcomeMessage(name) + val asyncMessage = runBlocking { exampleService.getAsyncWelcomeMessage(name) } + + // Then + assertTrue(syncMessage.startsWith("Hello, $name!")) + assertTrue(asyncMessage.startsWith("Hello async, $name!")) + assertTrue(syncMessage.endsWith("This is your Kotlin Multimodule Template.")) + assertTrue(asyncMessage.endsWith("This is your Kotlin Multimodule Template.")) + } +} diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000..b85e554 --- /dev/null +++ b/settings.gradle @@ -0,0 +1,17 @@ +rootProject.name = "kotlin-multimodule-template" + +// Enable Gradle Feature Preview if using Gradle 8+ +enableFeaturePreview("STABLE_CONFIGURATION_CACHE") + +// Dependency resolution settings +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.PREFER_SETTINGS) + repositories { + mavenCentral() + } +} + +// Auto-include all folders with build.gradle or build.gradle.kts +file(".").listFiles() + .findAll { it.isDirectory() && (new File(it, "build.gradle").exists() || new File(it, "build.gradle.kts").exists()) } + .each { include it.name } diff --git a/springboot-application/build.gradle b/springboot-application/build.gradle new file mode 100644 index 0000000..b96d826 --- /dev/null +++ b/springboot-application/build.gradle @@ -0,0 +1,19 @@ +apply from: "$rootDir/scripts/gradle/spring_boot.gradle" + +dependencies { + // Module dependencies + implementation project(':service-module') + + // Spring Boot starters + implementation "org.springframework.boot:spring-boot-starter-web" + implementation "org.springframework.boot:spring-boot-starter-data-jpa" + implementation "org.springframework.boot:spring-boot-starter-validation" + + // Database + runtimeOnly "com.h2database:h2" + + // Testing dependencies + testImplementation "org.testng:testng:$testng_version" + testImplementation "io.mockk:mockk:$mockk_version" + testImplementation "org.springframework.boot:spring-boot-starter-test" +} diff --git a/springboot-application/gradle.lockfile b/springboot-application/gradle.lockfile new file mode 100644 index 0000000..f901a59 --- /dev/null +++ b/springboot-application/gradle.lockfile @@ -0,0 +1,173 @@ +# This is a Gradle generated file for dependency locking. +# Manual edits can break the build and are not advised. +# This file is expected to be part of source control. +ch.qos.logback:logback-classic:1.5.18=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +ch.qos.logback:logback-core:1.5.18=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.beust:jcommander:1.82=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-annotations:2.19.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-core:2.19.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml.jackson.core:jackson-databind:2.19.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jdk8:2.19.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.19.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml.jackson.module:jackson-module-parameter-names:2.19.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml.jackson:jackson-bom:2.19.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.fasterxml:classmate:1.7.0=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.h2database:h2:2.3.232=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.jayway.jsonpath:json-path:2.9.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.sun.istack:istack-commons-runtime:4.1.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +com.vaadin.external.google:android-json:0.0.20131108.vaadin1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +com.zaxxer:HikariCP:6.3.0=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.micrometer:micrometer-commons:1.15.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.micrometer:micrometer-observation:1.15.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-agent-api-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk-agent-api:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-agent-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk-agent:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-core-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk-core:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-dsl-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk-dsl:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.mockk:mockk-jvm:1.13.10=testCompileClasspath,testRuntimeClasspath +io.mockk:mockk:1.13.10=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-buffer:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-codec-dns:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-codec-http2:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-codec-http:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-codec-socks:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-codec:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-common:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-handler-proxy:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-handler:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-resolver-dns-classes-macos:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-resolver-dns-native-macos:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-resolver-dns:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-resolver:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-transport-classes-epoll:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-transport-native-epoll:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-transport-native-unix-common:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.netty:netty-transport:4.1.122.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.projectreactor.netty:reactor-netty-core:1.2.7=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.projectreactor.netty:reactor-netty-http:1.2.7=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.projectreactor:reactor-core:3.7.7=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +io.smallrye:jandex:3.2.0=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +jakarta.activation:jakarta.activation-api:2.1.3=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.annotation:jakarta.annotation-api:2.1.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.inject:jakarta.inject-api:2.0.1=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +jakarta.persistence:jakarta.persistence-api:3.1.0=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.transaction:jakarta.transaction-api:2.0.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.validation:jakarta.validation-api:3.0.2=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +jakarta.xml.bind:jakarta.xml.bind-api:4.0.2=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +junit:junit:4.13.2=testRuntimeClasspath +net.bytebuddy:byte-buddy-agent:1.17.6=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.bytebuddy:byte-buddy:1.17.6=productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.minidev:accessors-smart:2.5.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +net.minidev:json-smart:2.5.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.antlr:antlr4-runtime:4.13.0=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apache.logging.log4j:log4j-api:2.24.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apache.logging.log4j:log4j-to-slf4j:2.24.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-core:10.1.42=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-el:10.1.42=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apache.tomcat.embed:tomcat-embed-websocket:10.1.42=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.apiguardian:apiguardian-api:1.1.2=testCompileClasspath,testImplementationDependenciesMetadata +org.aspectj:aspectjweaver:1.9.24=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.assertj:assertj-core:3.27.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.awaitility:awaitility:4.2.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.eclipse.angus:angus-activation:2.0.2=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:jaxb-core:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:jaxb-runtime:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.glassfish.jaxb:txw2:4.0.5=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.hamcrest:hamcrest-core:3.0=testRuntimeClasspath +org.hamcrest:hamcrest:3.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.hibernate.common:hibernate-commons-annotations:7.0.3.Final=productionRuntimeClasspath,runtimeClasspath,testRuntimeClasspath +org.hibernate.orm:hibernate-core:6.6.18.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.hibernate.validator:hibernate-validator:8.0.2.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jacoco:org.jacoco.agent:0.8.10=jacocoAgent,jacocoAnt +org.jacoco:org.jacoco.ant:0.8.10=jacocoAnt +org.jacoco:org.jacoco.core:0.8.10=jacocoAnt +org.jacoco:org.jacoco.report:0.8.10=jacocoAnt +org.jboss.logging:jboss-logging:3.6.1.Final=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.intellij.deps:trove4j:1.0.20200330=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-allopen-compiler-plugin-embeddable:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-build-common:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-build-tools-api:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-build-tools-impl:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-compiler-embeddable:1.9.25=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-compiler-runner:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-daemon-client:1.9.25=kotlinBuildToolsApiClasspath +org.jetbrains.kotlin:kotlin-daemon-embeddable:1.9.25=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-klib-commonizer-embeddable:1.9.25=kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-reflect:1.9.25=compileClasspath,implementationDependenciesMetadata,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinKlibCommonizerClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-script-runtime:1.9.25=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains.kotlin:kotlin-scripting-common:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-embeddable:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-compiler-impl-embeddable:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-scripting-jvm:1.9.25=kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest +org.jetbrains.kotlin:kotlin-stdlib-common:1.9.25=implementationDependenciesMetadata,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk7:1.9.25=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib-jdk8:1.9.25=testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlin:kotlin-stdlib:1.9.25=compileClasspath,implementationDependenciesMetadata,kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlinx:atomicfu:0.23.1=implementationDependenciesMetadata,testImplementationDependenciesMetadata +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.0=implementationDependenciesMetadata,testImplementationDependenciesMetadata +org.jetbrains.kotlinx:kotlinx-coroutines-bom:1.8.1=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core-jvm:1.8.1=compileClasspath,kotlinBuildToolsApiClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-core:1.8.0=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-reactive:1.8.0=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.8.0=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.jetbrains:annotations:13.0=kotlinBuildToolsApiClasspath,kotlinCompilerClasspath,kotlinCompilerPluginClasspathMain,kotlinCompilerPluginClasspathTest,kotlinKlibCommonizerClasspath +org.jetbrains:annotations:23.0.0=compileClasspath,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-api:5.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter-engine:5.12.2=testRuntimeClasspath +org.junit.jupiter:junit-jupiter-params:5.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.jupiter:junit-jupiter:5.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-commons:1.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.junit.platform:junit-platform-engine:1.12.2=testRuntimeClasspath +org.junit.platform:junit-platform-launcher:1.12.2=testRuntimeClasspath +org.junit:junit-bom:5.12.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.mockito:mockito-core:5.17.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.mockito:mockito-junit-jupiter:5.17.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.objenesis:objenesis:3.3=testCompileClasspath,testRuntimeClasspath +org.opentest4j:opentest4j:1.3.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.ow2.asm:asm-commons:9.5=jacocoAnt +org.ow2.asm:asm-tree:9.5=jacocoAnt +org.ow2.asm:asm:9.5=jacocoAnt +org.ow2.asm:asm:9.7.1=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.reactivestreams:reactive-streams:1.0.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.skyscreamer:jsonassert:1.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.slf4j:jul-to-slf4j:2.0.17=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.slf4j:slf4j-api:2.0.17=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-autoconfigure:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-data-jpa:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-jdbc:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-json:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-logging:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-reactor-netty:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-test:3.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-tomcat:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-validation:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-web:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter-webflux:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-starter:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-test-autoconfigure:3.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot-test:3.5.3=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.boot:spring-boot:3.5.3=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.data:spring-data-commons:3.5.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework.data:spring-data-jpa:3.5.1=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-aop:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-aspects:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-beans:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-context:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-core:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-expression:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-jcl:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-jdbc:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-orm:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-test:6.2.8=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-tx:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-web:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-webflux:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.springframework:spring-webmvc:6.2.8=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.testng:testng:7.7.0=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.webjars:jquery:3.6.1=testRuntimeClasspath +org.xmlunit:xmlunit-core:2.10.2=testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +org.yaml:snakeyaml:2.4=compileClasspath,implementationDependenciesMetadata,productionRuntimeClasspath,runtimeClasspath,testCompileClasspath,testImplementationDependenciesMetadata,testRuntimeClasspath +empty=annotationProcessor,apiDependenciesMetadata,compileOnlyDependenciesMetadata,developmentOnly,intransitiveDependenciesMetadata,kotlinCompilerPluginClasspath,kotlinNativeCompilerPluginClasspath,kotlinScriptDef,kotlinScriptDefExtensions,testAndDevelopmentOnly,testAnnotationProcessor,testApiDependenciesMetadata,testCompileOnlyDependenciesMetadata,testIntransitiveDependenciesMetadata,testKotlinScriptDef,testKotlinScriptDefExtensions diff --git a/springboot-application/src/main/kotlin/io/programmernewbie/template/KotlinMultimoduleTemplateApplication.kt b/springboot-application/src/main/kotlin/io/programmernewbie/template/KotlinMultimoduleTemplateApplication.kt new file mode 100644 index 0000000..ae2d040 --- /dev/null +++ b/springboot-application/src/main/kotlin/io/programmernewbie/template/KotlinMultimoduleTemplateApplication.kt @@ -0,0 +1,33 @@ +package io.programmernewbie.template + +import org.springframework.boot.autoconfigure.SpringBootApplication +import org.springframework.boot.runApplication +import org.springframework.data.jpa.repository.config.EnableJpaAuditing +import org.springframework.transaction.annotation.EnableTransactionManagement + +/** + * Main Spring Boot application class for Kotlin Multimodule Template + * + * This application demonstrates a minimal multimodule Kotlin architecture with: + * - Service layer with business logic + * - Spring Boot application layer with REST controllers + * + * To customize this template for your project: + * 1. Change the package name from io.programmernewbie.template to your desired package + * 2. Update the scanBasePackages to match your package structure + * 3. Add your business logic in the service-module + * 4. Add your REST controllers in this module + */ +@SpringBootApplication( + scanBasePackages = [ + "io.programmernewbie.template.service", + "io.programmernewbie.template", + ], +) +@EnableJpaAuditing +@EnableTransactionManagement +class KotlinMultimoduleTemplateApplication + +fun main(args: Array) { + runApplication(*args) +} diff --git a/springboot-application/src/main/kotlin/io/programmernewbie/template/controller/ExampleController.kt b/springboot-application/src/main/kotlin/io/programmernewbie/template/controller/ExampleController.kt new file mode 100644 index 0000000..a45ed2f --- /dev/null +++ b/springboot-application/src/main/kotlin/io/programmernewbie/template/controller/ExampleController.kt @@ -0,0 +1,59 @@ +package io.programmernewbie.template.controller + +import io.programmernewbie.template.service.ExampleService +import org.springframework.web.bind.annotation.GetMapping +import org.springframework.web.bind.annotation.RequestMapping +import org.springframework.web.bind.annotation.RequestParam +import org.springframework.web.bind.annotation.RestController + +/** + * Example REST controller for the template + * + * Replace this with your actual REST controllers + */ +@RestController +@RequestMapping("/api/example") +class ExampleController( + private val exampleService: ExampleService, +) { + + /** + * Example GET endpoint + * + * @param name Optional name parameter + * @return Welcome message + */ + @GetMapping("/welcome") + fun getWelcome(@RequestParam(defaultValue = "World") name: String): Map { + return mapOf( + "message" to exampleService.getWelcomeMessage(name), + "timestamp" to java.time.Instant.now().toString(), + ) + } + + /** + * Example async GET endpoint + * + * @param name Optional name parameter + * @return Welcome message from async service + */ + @GetMapping("/welcome-async") + suspend fun getWelcomeAsync(@RequestParam(defaultValue = "World") name: String): Map { + return mapOf( + "message" to exampleService.getAsyncWelcomeMessage(name), + "timestamp" to java.time.Instant.now().toString(), + "type" to "async", + ) + } + + /** + * Health check endpoint + */ + @GetMapping("/health") + fun health(): Map { + return mapOf( + "status" to "UP", + "service" to "kotlin-multimodule-template", + ) + } +} diff --git a/springboot-application/src/main/resources/application.yaml b/springboot-application/src/main/resources/application.yaml new file mode 100644 index 0000000..e69de29 diff --git a/springboot-application/src/test/kotlin/io/programmernewbie/template/controller/ExampleControllerTest.kt b/springboot-application/src/test/kotlin/io/programmernewbie/template/controller/ExampleControllerTest.kt new file mode 100644 index 0000000..f2a7637 --- /dev/null +++ b/springboot-application/src/test/kotlin/io/programmernewbie/template/controller/ExampleControllerTest.kt @@ -0,0 +1,167 @@ +package io.programmernewbie.template.controller + +import io.mockk.coEvery +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.verify +import io.programmernewbie.template.service.ExampleService +import kotlinx.coroutines.runBlocking +import org.testng.Assert.assertEquals +import org.testng.Assert.assertNotNull +import org.testng.Assert.assertTrue +import org.testng.Assert.fail +import org.testng.annotations.BeforeMethod +import org.testng.annotations.Test +import java.time.Instant + +/** + * Small unit tests for ExampleController + * + * These tests are fast (< 100ms) and use mocks to isolate the controller logic. + * They focus on testing the controller's behavior without Spring context. + */ +@Test(groups = ["small"]) +class ExampleControllerTest { + + private lateinit var mockExampleService: ExampleService + private lateinit var controller: ExampleController + + @BeforeMethod + fun setup() { + mockExampleService = mockk() + controller = ExampleController(mockExampleService) + } + + @Test + fun `getWelcome, with custom name, returns response with message and timestamp`() { + // Given + val name = "Alice" + val expectedMessage = "Hello, Alice! This is your Kotlin Multimodule Template." + every { mockExampleService.getWelcomeMessage(name) } returns expectedMessage + + // When + val response = controller.getWelcome(name) + + // Then + assertEquals(response["message"], expectedMessage) + assertTrue(response.containsKey("timestamp")) + assertNotNull(response["timestamp"]) + verify { mockExampleService.getWelcomeMessage(name) } + } + + @Test + fun `getWelcome, with default name, returns response with default message`() { + // Given + val expectedMessage = "Hello, World! This is your Kotlin Multimodule Template." + every { mockExampleService.getWelcomeMessage("World") } returns expectedMessage + + // When + val response = controller.getWelcome("World") + + // Then + assertEquals(response["message"], expectedMessage) + assertTrue(response.containsKey("timestamp")) + verify { mockExampleService.getWelcomeMessage("World") } + } + + @Test + fun `getWelcome, response contains timestamp, timestamp is valid instant`() { + // Given + val name = "Test" + val expectedMessage = "Hello, Test! This is your Kotlin Multimodule Template." + every { mockExampleService.getWelcomeMessage(name) } returns expectedMessage + + // When + val response = controller.getWelcome(name) + + // Then + val timestamp = response["timestamp"] + assertNotNull(timestamp) + // Verify timestamp is a valid ISO instant format + assertDoesNotThrow { + Instant.parse(timestamp as String) + } + } + + @Test + fun `getWelcomeAsync, with custom name, returns async response with correct type`() { + // Given + val name = "Bob" + val expectedMessage = "Hello async, Bob! This is your Kotlin Multimodule Template." + coEvery { mockExampleService.getAsyncWelcomeMessage(name) } returns expectedMessage + + // When + val response = runBlocking { controller.getWelcomeAsync(name) } + + // Then + assertEquals(response["message"], expectedMessage) + assertEquals(response["type"], "async") + assertTrue(response.containsKey("timestamp")) + coVerify { mockExampleService.getAsyncWelcomeMessage(name) } + } + + @Test + fun `getWelcomeAsync, with default name, returns async response with default message`() { + // Given + val expectedMessage = "Hello async, World! This is your Kotlin Multimodule Template." + coEvery { mockExampleService.getAsyncWelcomeMessage("World") } returns expectedMessage + + // When + val response = runBlocking { controller.getWelcomeAsync("World") } + + // Then + assertEquals(response["message"], expectedMessage) + assertEquals(response["type"], "async") + assertTrue(response.containsKey("timestamp")) + coVerify { mockExampleService.getAsyncWelcomeMessage("World") } + } + + @Test + fun `getWelcomeAsync, response structure, contains all required fields`() { + // Given + val name = "Structure" + val expectedMessage = "Hello async, Structure! This is your Kotlin Multimodule Template." + coEvery { mockExampleService.getAsyncWelcomeMessage(name) } returns expectedMessage + + // When + val response = runBlocking { controller.getWelcomeAsync(name) } + + // Then + assertEquals(3, response.size) // Should have exactly 3 fields + assertTrue(response.containsKey("message")) + assertTrue(response.containsKey("timestamp")) + assertTrue(response.containsKey("type")) + } + + @Test + fun `health, returns correct status, status is UP with service name`() { + // When + val response = controller.health() + + // Then + assertEquals(response["status"], "UP") + assertEquals(response["service"], "kotlin-multimodule-template") + assertEquals(2, response.size) // Should have exactly 2 fields + } + + @Test + fun `health, does not depend on external services, no service interactions`() { + // When + val response = controller.health() + + // Then + assertEquals(response["status"], "UP") + // Verify no interactions with mock service (health should be independent) + verify(exactly = 0) { mockExampleService.getWelcomeMessage(any()) } + coVerify(exactly = 0) { mockExampleService.getAsyncWelcomeMessage(any()) } + } + + private fun assertDoesNotThrow(block: () -> Unit) { + try { + block() + } catch (e: Exception) { + fail("Expected no exception but got: ${e.message}") + } + } +}