name: Java - Main Pipeline
on: workflow_call: inputs: # Runner configuration runner: description: 'Runner type' required: false type: string default: 'ubuntu-latest' container_image: description: >- Run the JDK jobs inside this image (e.g. ghcr.io/codehunters-io/ci-base-images:1.0.0). Empty = plain runner with actions/setup-java. Applies to build, test, owasp, architecture and the GitHub-registry dependency publish; the Docker artifact and deploy jobs stay on the runner because they drive the daemon. required: false type: string default: ''
# Java configuration java_version: description: 'Java version' required: false type: string default: '21' java_distribution: description: 'Java distribution' required: false type: string default: 'temurin' build_tool: description: 'Build tool: gradle or maven' required: false type: string default: 'gradle'
# Pipeline steps control run_commit_lint: description: 'Run commit message validation' required: false type: boolean default: false run_trufflehog: description: 'Run TruffleHog secret scanning' required: false type: boolean default: false trufflehog_only_verified: description: 'Only report verified secrets' required: false type: boolean default: true trufflehog_fail_on_findings: description: 'Fail the workflow if secrets are found' required: false type: boolean default: true run_dependency_review: description: 'Run dependency review (PR only)' required: false type: boolean default: false dependency_review_severity: description: 'Minimum severity to fail: low, moderate, high, critical' required: false type: string default: 'high' run_build: description: 'Run build job' required: false type: boolean default: true run_test: description: 'Run test job' required: false type: boolean default: false run_coverage: description: 'Run JaCoCo coverage report' required: false type: boolean default: false run_code_analysis: description: 'Run code analysis' required: false type: boolean default: false code_analysis_tool: description: 'Code analysis tool: sonar or qodana' required: false type: string default: 'sonar' skip_quality_gate: description: 'Skip Quality Gate check' required: false type: boolean default: false run_owasp: description: 'Run OWASP Dependency Check' required: false type: boolean default: false owasp_fail_on_cvss: description: 'CVSS score threshold to fail (7 = HIGH+CRITICAL, 9 = CRITICAL only)' required: false type: number default: 7 run_architecture: description: 'Run architecture validation (ArchUnit + diagrams)' required: false type: boolean default: false
# Artifact options run_artifact: description: 'Build and push Docker image' required: false type: boolean default: false artifact_registry: description: 'Artifact registry: ecr, github, packages' required: false type: string default: 'ecr' publish_task: description: 'Gradle publish task for packages registry' required: false type: string default: 'publish'
# Test options coverage_instruction_threshold: description: 'Minimum instruction coverage percentage (0-100). Requires jacocoLogCodeCoverage task.' required: false type: number default: 0 coverage_branch_threshold: description: 'Minimum branch coverage percentage (0-100). Requires jacocoLogCodeCoverage task.' required: false type: number default: 0 coverage_line_threshold: description: 'Minimum line coverage percentage (0-100). Set to 0 to disable.' required: false type: number default: 0
# Artifact & Deploy options image_tag: description: 'Docker image tag (empty = commit SHA)' required: false type: string default: '' docker_platform: description: 'Docker platform (linux/amd64, linux/arm64)' required: false type: string default: 'linux/arm64' # Trivy scan of the image before it is pushed vulnerability_gate: description: 'Fail the artifact job on a fixable vulnerability at or above scan_severity. false reports only.' required: false type: boolean default: true scan_severity: description: 'Trivy severities the vulnerability gate acts on' required: false type: string default: 'CRITICAL,HIGH' trivyignores: description: 'Path to a Trivy ignore file. Empty means none.' required: false type: string default: '' ignore_policy: description: 'Path to a Trivy Rego ignore policy. Empty means none.' required: false type: string default: ''
# Deploy options run_deploy: description: 'Deploy after artifact build' required: false type: boolean default: false deploy_target: description: 'Deploy target: ec2, ec2-vpn, or eks' required: false type: string default: 'ec2' environment: description: 'GitHub environment (develop, prod)' required: false type: string default: 'develop' memory_limit: description: 'Container memory limit (e.g., 512m/512Mi)' required: false type: string default: '800m' memory_reservation: description: 'Container memory reservation (e.g., 256m/256Mi)' required: false type: string default: '256m' spring_profiles: description: 'Additional Spring profiles (e.g., awsparamstore)' required: false type: string default: '' extra_volumes: description: 'Additional docker-compose volume mounts (one per line, including the leading "- "). Example: "- ./certs:/etc/ssl/app:ro"' required: false type: string default: ''
# EKS-specific options eks_cluster_name: description: 'EKS cluster name (required when deploy_target: eks)' required: false type: string default: '' eks_namespace: description: 'Kubernetes namespace' required: false type: string default: 'default' eks_use_helm: description: 'Use Helm for EKS deployment' required: false type: boolean default: false eks_helm_chart_path: description: 'Path to Helm chart' required: false type: string default: './helm' eks_replicas: description: 'Number of EKS replicas' required: false type: number default: 1
# Issue tracking run_create_issue_on_failure: description: 'Create GitHub issue on pipeline failure' required: false type: boolean default: false issue_labels: description: 'Labels for the failure issue (comma-separated)' required: false type: string default: 'bug,pipeline-failure'
# Cleanup & Release run_cleanup: description: 'Delete merged branch after deploy' required: false type: boolean default: false run_release: description: 'Create release PR after deploy' required: false type: boolean default: false release_target_branch: description: 'Target branch for release PR' required: false type: string default: 'main' release_strict_flow: description: 'Enforce GitFlow on release PR (base must be develop, target must be main)' required: false type: boolean default: true run_tag: description: 'Create git tag and GitHub Release after deploy' required: false type: boolean default: false
aws_role_to_assume: description: >- IAM role ARN to assume via GitHub OIDC, threaded down to the AWS jobs below. Empty keeps the static-key path. required: false type: string default: ''
outputs: image_tag: description: 'Docker image tag pushed' value: ${{ jobs.artifact-ecr.outputs.image_tag || jobs.artifact-github.outputs.image_tag }} image_uri: description: 'Full Docker image URI' value: ${{ jobs.artifact-ecr.outputs.image_uri || jobs.artifact-github.outputs.image_uri }} image_digest: description: 'Digest of the pushed image' value: ${{ jobs.artifact-ecr.outputs.image_digest }} coverage: description: 'Code coverage percentage (line)' value: ${{ jobs.test.outputs.coverage_percentage }} coverage_instruction: description: 'Instruction coverage percentage' value: ${{ jobs.test.outputs.coverage_instruction }} coverage_branch: description: 'Branch coverage percentage' value: ${{ jobs.test.outputs.coverage_branch }} quality_gate: description: 'SonarQube Quality Gate status' value: ${{ jobs.test.outputs.quality_gate_status }} owasp_vulnerabilities: description: 'Number of OWASP vulnerabilities found' value: ${{ jobs.owasp.outputs.vulnerabilities }} deleted_branch: description: 'Name of deleted branch' value: ${{ jobs.cleanup.outputs.deleted_branch }} release_version: description: 'Release version created' value: ${{ jobs.release.outputs.version }} release_pr_url: description: 'Release PR URL' value: ${{ jobs.release.outputs.pr_url }} release_changelog: description: 'Release changelog' value: ${{ jobs.release.outputs.changelog }} arch_test_result: description: 'Architecture test result' value: ${{ jobs.architecture.outputs.arch_test_result }} diagrams_artifact: description: 'Architecture diagrams artifact name' value: ${{ jobs.architecture.outputs.diagrams_artifact }} package_version: description: 'Published package version' value: ${{ jobs.artifact-packages.outputs.version }} package_group: description: 'Published package group' value: ${{ jobs.artifact-packages.outputs.group }} package_artifact_name: description: 'Published package artifact name' value: ${{ jobs.artifact-packages.outputs.artifact_name }}
# Least privilege for what this workflow actually does. Without it the# job inherits whatever the calling repository's default happens to be,# which on older repositories is write-all.permissions: contents: write packages: write issues: write # For the OIDC path in the AWS workflows below; a caller's grant is the # ceiling for everything beneath it. id-token: write
jobs: # ============================================ # COMMIT LINT (parallel, no deps) # ============================================ commit-lint: name: Commit Lint if: inputs.run_commit_lint uses: ./.github/workflows/shared-commit-lint.yml with: runner: ${{ inputs.runner }}
# ============================================ # SECURITY - TruffleHog (parallel, no deps) # ============================================ security: name: Security Scan if: inputs.run_trufflehog uses: ./.github/workflows/security-trufflehog.yml with: runner: ${{ inputs.runner }} only_verified: ${{ inputs.trufflehog_only_verified }} fail_on_findings: ${{ inputs.trufflehog_fail_on_findings }} secrets: inherit
# ============================================ # SECURITY - Dependency Review (parallel, no deps) # ============================================ dependency-review: name: Dependency Review # dependency-review-action only works on pull_request events; auto-skip on push. if: inputs.run_dependency_review && github.event_name == 'pull_request' uses: ./.github/workflows/security-dependency-review.yml with: runner: ${{ inputs.runner }} fail_on_severity: ${{ inputs.dependency_review_severity }} secrets: inherit
# ============================================ # BUILD # ============================================ build: name: Build if: inputs.run_build uses: ./.github/workflows/java-build.yml with: runner: ${{ inputs.runner }} container_image: ${{ inputs.container_image }} java_version: ${{ inputs.java_version }} java_distribution: ${{ inputs.java_distribution }} build_tool: ${{ inputs.build_tool }} # Only upload the bootable JAR when a Docker artifact/deploy step will consume it. # The 'packages' registry publishes a library via `./gradlew publish` (no bootstrap # bootJar exists), so skip the JAR locate/upload to avoid a false build failure. upload_artifact: ${{ inputs.run_artifact && inputs.artifact_registry != 'packages' }} secrets: inherit
# ============================================ # TEST (needs: build) # ============================================ test: name: Test if: inputs.run_test needs: build uses: ./.github/workflows/java-test.yml with: runner: ${{ inputs.runner }} container_image: ${{ inputs.container_image }} java_version: ${{ inputs.java_version }} java_distribution: ${{ inputs.java_distribution }} run_coverage: ${{ inputs.run_coverage }} coverage_instruction_threshold: ${{ inputs.coverage_instruction_threshold }} coverage_branch_threshold: ${{ inputs.coverage_branch_threshold }} coverage_line_threshold: ${{ inputs.coverage_line_threshold }} run_code_analysis: ${{ inputs.run_code_analysis }} code_analysis_tool: ${{ inputs.code_analysis_tool }} skip_quality_gate: ${{ inputs.skip_quality_gate }} run_owasp: ${{ inputs.run_owasp }} secrets: inherit
# ============================================ # QODANA (parallel with test, needs: build) # ============================================ qodana: name: Qodana if: inputs.run_code_analysis && inputs.code_analysis_tool == 'qodana' needs: build uses: ./.github/workflows/java-qodana.yml with: runner: ${{ inputs.runner }} secrets: inherit
# ============================================ # OWASP (parallel with test, needs: build) # ============================================ owasp: name: OWASP if: inputs.run_owasp needs: build uses: ./.github/workflows/java-owasp.yml with: runner: ${{ inputs.runner }} container_image: ${{ inputs.container_image }} java_version: ${{ inputs.java_version }} java_distribution: ${{ inputs.java_distribution }} owasp_fail_on_cvss: ${{ inputs.owasp_fail_on_cvss }} build_tool: ${{ inputs.build_tool }} secrets: inherit
# ============================================ # ARCHITECTURE (parallel with test, needs: build) # ============================================ architecture: name: Architecture if: inputs.run_architecture needs: build uses: ./.github/workflows/java-architecture.yml with: runner: ${{ inputs.runner }} container_image: ${{ inputs.container_image }} java_version: ${{ inputs.java_version }} java_distribution: ${{ inputs.java_distribution }} secrets: inherit
# ============================================ # ARTIFACT - ECR # ============================================ artifact-ecr: name: Build & Push ECR if: | inputs.run_artifact && inputs.artifact_registry == 'ecr' && always() && needs.build.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped') && (needs.qodana.result == 'success' || needs.qodana.result == 'skipped') && (needs.owasp.result == 'success' || needs.owasp.result == 'skipped') && (needs.architecture.result == 'success' || needs.architecture.result == 'skipped') needs: [build, test, qodana, owasp, architecture] uses: ./.github/workflows/shared-artifact-docker-ecr.yml with: aws_role_to_assume: ${{ inputs.aws_role_to_assume }} runner: ${{ inputs.runner }} image_tag: ${{ inputs.image_tag }} docker_platform: ${{ inputs.docker_platform }} vulnerability_gate: ${{ inputs.vulnerability_gate }} scan_severity: ${{ inputs.scan_severity }} trivyignores: ${{ inputs.trivyignores }} ignore_policy: ${{ inputs.ignore_policy }} environment: ${{ inputs.environment }} # Reuse the JAR already built (and gated by passing tests) instead of recompiling. # build_from_source stays true only as a fallback when no artifact was uploaded. artifact_name: ${{ needs.build.outputs.artifact_name }} build_from_source: true java_version: ${{ inputs.java_version }} java_distribution: ${{ inputs.java_distribution }} build_tool: ${{ inputs.build_tool }} secrets: inherit
# ============================================ # ARTIFACT - GitHub Registry # ============================================ artifact-github: name: Build & Push GitHub Registry if: | inputs.run_artifact && inputs.artifact_registry == 'github' && always() && needs.build.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped') && (needs.architecture.result == 'success' || needs.architecture.result == 'skipped') needs: [build, test, architecture] uses: ./.github/workflows/java-artifact-docker-github.yml with: runner: ${{ inputs.runner }} java_version: ${{ inputs.java_version }} java_distribution: ${{ inputs.java_distribution }} image_tag: ${{ inputs.image_tag }} docker_platform: ${{ inputs.docker_platform }} vulnerability_gate: ${{ inputs.vulnerability_gate }} scan_severity: ${{ inputs.scan_severity }} trivyignores: ${{ inputs.trivyignores }} ignore_policy: ${{ inputs.ignore_policy }} artifact_name: ${{ needs.build.outputs.artifact_name }} environment: ${{ inputs.environment }} secrets: inherit
# ============================================ # ARTIFACT - GitHub Packages # ============================================ artifact-packages: name: Publish to GitHub Packages if: | inputs.run_artifact && inputs.artifact_registry == 'packages' && always() && needs.build.result == 'success' && (needs.test.result == 'success' || needs.test.result == 'skipped') && (needs.architecture.result == 'success' || needs.architecture.result == 'skipped') needs: [build, test, architecture] uses: ./.github/workflows/java-artifact-dependency-github.yml with: runner: ${{ inputs.runner }} container_image: ${{ inputs.container_image }} java_version: ${{ inputs.java_version }} java_distribution: ${{ inputs.java_distribution }} publish_task: ${{ inputs.publish_task }} secrets: inherit
# ============================================ # DEPLOY - EC2 # ============================================ deploy-ec2: name: Deploy EC2 if: | inputs.run_deploy && inputs.deploy_target == 'ec2' && always() && (needs.artifact-ecr.result == 'success' || needs.artifact-github.result == 'success' || needs.artifact-packages.result == 'success') needs: [artifact-ecr, artifact-github, artifact-packages] uses: ./.github/workflows/shared-deploy-ec2.yml with: runner: ${{ inputs.runner }} image_tag: ${{ needs.artifact-ecr.outputs.image_tag || needs.artifact-github.outputs.image_tag }} # ECR's digest only: the deploys pull from AWS_ECR_URL, and a GHCR digest # names the same bytes in a registry the deploy never reads. image_digest: ${{ needs.artifact-ecr.outputs.image_digest }} docker_platform: ${{ inputs.docker_platform }} environment: ${{ inputs.environment }} memory_limit: ${{ inputs.memory_limit }} memory_reservation: ${{ inputs.memory_reservation }} container_env_vars: | SPRING_PROFILES_ACTIVE=${{ inputs.spring_profiles != '' && format('{0},{1}', inputs.spring_profiles, inputs.environment) || inputs.environment }} SERVER_CONTEXT_PATH=/${{ github.event.repository.name }} extra_volumes: ${{ inputs.extra_volumes }} secrets: inherit
# ============================================ # DEPLOY - EC2 via WireGuard VPN # ============================================ deploy-ec2-vpn: name: Deploy EC2 (VPN) if: | inputs.run_deploy && inputs.deploy_target == 'ec2-vpn' && always() && (needs.artifact-ecr.result == 'success' || needs.artifact-github.result == 'success' || needs.artifact-packages.result == 'success') needs: [artifact-ecr, artifact-github, artifact-packages] uses: ./.github/workflows/shared-deploy-ec2-vpn.yml with: runner: ${{ inputs.runner }} image_tag: ${{ needs.artifact-ecr.outputs.image_tag || needs.artifact-github.outputs.image_tag }} # ECR's digest only: the deploys pull from AWS_ECR_URL, and a GHCR digest # names the same bytes in a registry the deploy never reads. image_digest: ${{ needs.artifact-ecr.outputs.image_digest }} docker_platform: ${{ inputs.docker_platform }} environment: ${{ inputs.environment }} memory_limit: ${{ inputs.memory_limit }} memory_reservation: ${{ inputs.memory_reservation }} container_env_vars: | SPRING_PROFILES_ACTIVE=${{ inputs.spring_profiles != '' && format('{0},{1}', inputs.spring_profiles, inputs.environment) || inputs.environment }} SERVER_CONTEXT_PATH=/${{ github.event.repository.name }} extra_volumes: ${{ inputs.extra_volumes }} secrets: inherit
# ============================================ # DEPLOY - EKS # ============================================ deploy-eks: name: Deploy EKS if: | inputs.run_deploy && inputs.deploy_target == 'eks' && always() && (needs.artifact-ecr.result == 'success' || needs.artifact-github.result == 'success') needs: [artifact-ecr, artifact-github, artifact-packages] uses: ./.github/workflows/shared-deploy-eks.yml with: aws_role_to_assume: ${{ inputs.aws_role_to_assume }} runner: ${{ inputs.runner }} image_tag: ${{ needs.artifact-ecr.outputs.image_tag || needs.artifact-github.outputs.image_tag }} # ECR's digest only: the deploys pull from AWS_ECR_URL, and a GHCR digest # names the same bytes in a registry the deploy never reads. image_digest: ${{ needs.artifact-ecr.outputs.image_digest }} environment: ${{ inputs.environment }} cluster_name: ${{ inputs.eks_cluster_name }} namespace: ${{ inputs.eks_namespace }} replicas: ${{ inputs.eks_replicas }} use_helm: ${{ inputs.eks_use_helm }} helm_chart_path: ${{ inputs.eks_helm_chart_path }} memory_limit: ${{ inputs.memory_limit }} memory_request: ${{ inputs.memory_reservation }} container_env_vars: | SPRING_PROFILES_ACTIVE=${{ inputs.spring_profiles != '' && format('{0},{1}', inputs.spring_profiles, inputs.environment) || inputs.environment }} SERVER_CONTEXT_PATH=/${{ github.event.repository.name }} secrets: inherit
# ============================================ # RELEASE - Create release PR # ============================================ release: name: Create Release if: | inputs.run_release && always() && (needs.deploy-ec2.result == 'success' || needs.deploy-ec2-vpn.result == 'success' || needs.deploy-eks.result == 'success' || (needs.deploy-ec2.result == 'skipped' && needs.deploy-ec2-vpn.result == 'skipped' && needs.deploy-eks.result == 'skipped' && (needs.artifact-ecr.result == 'success' || needs.artifact-github.result == 'success' || needs.artifact-packages.result == 'success' || (needs.artifact-ecr.result == 'skipped' && needs.artifact-github.result == 'skipped' && needs.artifact-packages.result == 'skipped' && needs.test.result == 'success')))) needs: [test, artifact-ecr, artifact-github, artifact-packages, deploy-ec2, deploy-ec2-vpn, deploy-eks] uses: ./.github/workflows/shared-release.yml with: base_branch: ${{ github.ref_name }} target_branch: ${{ inputs.release_target_branch }} strict_flow: ${{ inputs.release_strict_flow }} secrets: inherit
# ============================================ # CLEANUP - Delete merged branch (runs after release PR created) # ============================================ cleanup: name: Delete Branch # ALWAYS runs on a push/merge (no input flag, no deps, no success gating). Deletes the merged # source branch (feature/* on develop, release/vX.Y.Z on main) regardless of build/deploy outcome. # (Non-fatal handling lives inside shared-delete-branch — continue-on-error is not allowed on a # reusable-workflow `uses:` job.) if: github.event_name == 'push' uses: ./.github/workflows/shared-delete-branch.yml secrets: inherit
# ============================================ # TAG - Create git tag and GitHub Release # ============================================ tag: name: Tag Release if: | inputs.run_tag && always() && (needs.deploy-ec2.result == 'success' || needs.deploy-ec2-vpn.result == 'success' || needs.deploy-eks.result == 'success' || (needs.deploy-ec2.result == 'skipped' && needs.deploy-ec2-vpn.result == 'skipped' && needs.deploy-eks.result == 'skipped' && (needs.artifact-ecr.result == 'success' || needs.artifact-github.result == 'success' || needs.artifact-packages.result == 'success' || (needs.test.result == 'success' && needs.artifact-ecr.result == 'skipped' && needs.artifact-github.result == 'skipped' && needs.artifact-packages.result == 'skipped')))) needs: [build, test, artifact-ecr, artifact-github, artifact-packages, deploy-ec2, deploy-ec2-vpn, deploy-eks, cleanup, release] uses: ./.github/workflows/shared-tag-release.yml secrets: inherit
# ============================================ # NOTIFY - Slack notification (final, auto-derived) # ============================================ notify: name: Notify if: always() needs: [commit-lint, security, dependency-review, build, test, qodana, owasp, architecture, artifact-ecr, artifact-github, artifact-packages, deploy-ec2, deploy-ec2-vpn, deploy-eks, release, cleanup, tag] uses: ./.github/workflows/shared-notifications.yml with: status: ${{ contains(needs.*.result, 'failure') && 'failure' || contains(needs.*.result, 'cancelled') && 'cancelled' || 'success' }} environment: ${{ inputs.environment }} secrets: inherit
# ============================================ # CREATE ISSUE - On failure (runs last, always) # ============================================ create-issue: name: Create Issue if: | inputs.run_create_issue_on_failure && always() needs: [build, test, artifact-ecr, artifact-github, artifact-packages, deploy-ec2, deploy-ec2-vpn, deploy-eks, cleanup, release, tag] uses: ./.github/workflows/shared-create-issue-on-failure.yml with: status: ${{ (needs.build.result == 'failure' || needs.test.result == 'failure' || needs.artifact-ecr.result == 'failure' || needs.artifact-github.result == 'failure' || needs.artifact-packages.result == 'failure' || needs.deploy-ec2.result == 'failure' || needs.deploy-ec2-vpn.result == 'failure' || needs.deploy-eks.result == 'failure') && 'failure' || 'success' }} environment: ${{ inputs.environment }} version: ${{ needs.release.outputs.version }} changelog: ${{ needs.release.outputs.changelog }} labels: ${{ inputs.issue_labels }}Java (Spring Boot)·Reusable workflow·on: workflow_call
Java Main Pipeline
Java - Main Pipeline
.github/workflows/java-main-pipeline.yml