Skip to content

Build, Test, and Publish #13

Build, Test, and Publish

Build, Test, and Publish #13

name: Build, Test, and Publish
# Manual workflow trigger with version input and publish option
on:
workflow_dispatch:
inputs:
version:
description: 'Release version (semantic versioning: X.Y.Z)'
required: true
type: string
publish:
description: 'Publish artifacts to GitHub Releases'
required: false
type: boolean
default: false
pr_number:
description: 'PR number that triggered this release (auto-filled by auto-version)'
required: false
type: string
default: ''
# Least-privilege permissions: read-only at workflow level, write only where needed
permissions:
contents: read
actions: read
# Environment variables used across jobs
env:
JAVA_VERSION: '17'
JAVA_DISTRIBUTION: 'temurin'
GRADLE_OPTS: -Dorg.gradle.daemon=false -Dorg.gradle.parallel=true -Dorg.gradle.caching=true
jobs:
# ============================================================================
# JOB 1: VALIDATE
# Validate version format and ensure version doesn't already exist
# ============================================================================
validate:
name: Validate Version
runs-on: ubuntu-latest
steps:
- name: Check out repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
fetch-depth: 0 # Fetch all history for tag checking
- name: Validate semantic version format
env:
VERSION: ${{ inputs.version }}
run: |
# Validate semantic versioning format (X.Y.Z or X.Y.Z-suffix)
if ! echo "$VERSION" | grep -qE '^[0-9]+\.[0-9]+\.[0-9]+(-[a-zA-Z0-9.-]+)?$'; then
echo "::error::Invalid version format: $VERSION"
echo "::error::Expected semantic versioning format: X.Y.Z or X.Y.Z-suffix"
echo "::error::Examples: 1.0.0, 1.2.3, 2.0.0-beta.1"
exit 1
fi
echo "Version format is valid: $VERSION"
- name: Check if version/tag already exists
env:
VERSION: ${{ inputs.version }}
run: |
# Check if tag already exists locally or remotely
if git tag -l | grep -qx "v${VERSION}" || git tag -l | grep -qx "${VERSION}"; then
echo "::info::Version tag already exists on the branch: ${VERSION}"
fi
echo "Version is correct: $VERSION"
- name: Validation summary
env:
VERSION: ${{ inputs.version }}
PUBLISH: ${{ inputs.publish }}
run: |
{
echo "### Validation Passed"
echo "- **Version**: ${VERSION}"
echo "- **Publish**: ${PUBLISH}"
echo "- **Format**: Valid semantic version"
echo "- **Uniqueness**: No existing tag found"
} >> "$GITHUB_STEP_SUMMARY"
# ============================================================================
# JOB 2: SECURITY SCAN - REPOSITORY
# Scans repository for IaC misconfigurations, secrets, and security issues
# Runs in parallel with build for efficiency
# ============================================================================
security-scan-repo:
name: Security Scan - Repository
runs-on: ubuntu-latest
needs: validate
steps:
- name: Check out repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Run Trivy repository scan
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
scan-type: 'config'
scan-ref: '.'
format: 'sarif'
output: 'trivy-repo-results.sarif'
severity: 'MEDIUM,HIGH,CRITICAL'
exit-code: '1' # Fail on vulnerabilities
hide-progress: false
scanners: 'misconfig,secret'
- name: Repository scan summary
if: success()
run: |
{
echo "### Repository Security Scan Passed"
echo "- **Scanner**: Trivy config + secrets"
echo "- **Severity Threshold**: MEDIUM+"
echo "- **Status**: No vulnerabilities detected"
} >> "$GITHUB_STEP_SUMMARY"
# ============================================================================
# JOB 3: BUILD
# Compiles code, runs linting, creates JAR artifacts with version override
# ============================================================================
build:
name: Build Application
runs-on: ubuntu-latest
needs: validate
outputs:
version: ${{ inputs.version }}
steps:
- name: Check out repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Set up JDK ${{ env.JAVA_VERSION }}
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5
with:
cache-read-only: false # Build job writes to cache
- name: Grant execute permission to gradlew
run: chmod +x ./gradlew
- name: Run Kotlin linting (ktlint)
run: ./gradlew ktlintCheck --no-daemon --stacktrace
- name: Build application (skip tests)
env:
VERSION: ${{ inputs.version }}
run: |
echo "Building with version override: ${VERSION}"
./gradlew clean build -x test \
-Pversion="${VERSION}" \
--no-daemon \
--stacktrace \
--warning-mode all
- name: Build shadow JAR (fat JAR)
env:
VERSION: ${{ inputs.version }}
run: |
./gradlew shadowJar \
-Pversion="${VERSION}" \
--no-daemon \
--stacktrace
- name: List build artifacts
env:
VERSION: ${{ inputs.version }}
run: |
echo "=== Files in build/libs/ ==="
ls -lh build/libs/
echo ""
echo "Expected version: ${VERSION}"
{
echo "### Build Artifacts:"
echo '```'
ls -lh build/libs/
echo '```'
} >> "$GITHUB_STEP_SUMMARY"
- name: Upload main JAR
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: main-jar
path: build/libs/*-${{ inputs.version }}.jar
if-no-files-found: error
retention-days: 30
- name: Upload shadow JAR
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: shadow-jar
path: build/libs/*-${{ inputs.version }}-all.jar
if-no-files-found: error
retention-days: 30
- name: Upload build outputs for reuse
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: build-outputs
path: |
build/classes/
build/resources/
build/libs/
build/kotlin/
.gradle/
retention-days: 1
- name: Build summary
env:
VERSION: ${{ inputs.version }}
run: |
{
echo "### Build Completed Successfully"
echo "- **Version**: ${VERSION}"
echo "- **Java Version**: ${JAVA_VERSION}"
echo "- **Kotlin Linting**: Passed"
echo "- **Artifacts**: Main JAR + Shadow JAR"
} >> "$GITHUB_STEP_SUMMARY"
# ============================================================================
# JOB 4: TEST
# Runs unit tests with JaCoCo code coverage
# Sequential after build to catch integration issues
# ============================================================================
test:
name: Run Tests & Coverage
runs-on: ubuntu-latest
needs: build
steps:
- name: Check out repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Set up JDK ${{ env.JAVA_VERSION }}
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Setup Gradle
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5
with:
cache-read-only: true # Test job reads from cache
- name: Download build outputs
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: build-outputs
path: .
- name: Grant execute permission to gradlew
run: chmod +x ./gradlew
- name: Run tests with JaCoCo coverage
env:
VERSION: ${{ inputs.version }}
run: |
./gradlew test jacocoTestReport \
-Pversion="${VERSION}" \
--no-daemon \
--stacktrace
- name: Upload test results
if: always() # Upload even if tests fail
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: test-results
path: |
build/reports/tests/test/
build/test-results/test/
retention-days: 30
- name: Upload coverage report
if: always()
uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5
with:
name: coverage-report
path: build/reports/jacoco/test/
retention-days: 30
- name: Test summary
if: success()
run: |
{
echo "### Tests Passed"
echo "- **Test Framework**: JUnit 5 + Kotlin Test"
echo "- **Coverage Tool**: JaCoCo"
echo "- **Status**: All tests passed"
# Count test results if available
if [ -d "build/test-results/test" ]; then
TEST_COUNT=$(find build/test-results/test -name "*.xml" | wc -l)
echo "- **Test Files**: $TEST_COUNT"
fi
} >> "$GITHUB_STEP_SUMMARY"
# ============================================================================
# JOB 5: SECURITY SCAN - DEPENDENCIES
# Scans project dependencies for known vulnerabilities (CVEs)
# ============================================================================
security-scan-dependencies:
name: Security Scan - Dependencies
runs-on: ubuntu-latest
needs: build
steps:
- name: Check out repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Download main JAR
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: main-jar
path: artifacts/
- name: Download shadow JAR
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: shadow-jar
path: artifacts/
- name: List artifacts for scanning
run: |
echo "Artifacts to scan:"
ls -lh artifacts/
- name: Run Trivy filesystem scan on JARs
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
scan-type: 'fs'
scan-ref: 'artifacts/'
format: 'sarif'
output: 'trivy-dependencies-results.sarif'
severity: 'MEDIUM,HIGH,CRITICAL'
exit-code: '1' # Fail on vulnerabilities
scanners: 'vuln'
- name: Dependency scan summary
if: success()
run: |
{
echo "### Dependency Security Scan Passed"
echo "- **Scanner**: Trivy filesystem"
echo "- **Target**: Project dependencies"
echo "- **Severity Threshold**: MEDIUM+"
echo "- **Status**: No vulnerable dependencies detected"
} >> "$GITHUB_STEP_SUMMARY"
# ============================================================================
# JOB 6: SECURITY SCAN - ARTIFACTS
# Scans built JAR files for embedded vulnerabilities
# ============================================================================
security-scan-artifacts:
name: Security Scan - Artifacts
runs-on: ubuntu-latest
needs: build
steps:
- name: Download main JAR
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: main-jar
path: artifacts/
- name: Download shadow JAR
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: shadow-jar
path: artifacts/
- name: List artifacts for scanning
run: |
echo "Artifacts to scan:"
ls -lh artifacts/
- name: Run Trivy scan on main JAR
uses: aquasecurity/trivy-action@57a97c7e7821a5776cebc9bb87c984fa69cba8f1 # 0.35.0
with:
scan-type: 'fs'
scan-ref: 'artifacts/'
format: 'sarif'
output: 'trivy-artifacts-results.sarif'
severity: 'MEDIUM,HIGH,CRITICAL'
exit-code: '1' # Fail on vulnerabilities
scanners: 'vuln'
- name: Artifact scan summary
if: success()
run: |
{
echo "### Artifact Security Scan Passed"
echo "- **Scanner**: Trivy filesystem"
echo "- **Targets**: Main JAR + Shadow JAR"
echo "- **Severity Threshold**: MEDIUM+"
echo "- **Status**: No vulnerabilities in artifacts"
} >> "$GITHUB_STEP_SUMMARY"
# ============================================================================
# JOB 7: CODEQL ANALYSIS
# Scans source code for security vulnerabilities
# Only runs when preparing to publish a release
# ============================================================================
codeql-scan:
name: CodeQL Security Analysis
runs-on: ubuntu-latest
needs: build
if: inputs.publish == true
steps:
- name: Checkout repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
- name: Set up JDK ${{ env.JAVA_VERSION }}
uses: actions/setup-java@be666c2fcd27ec809703dec50e508c2fdc7f6654 # v5
with:
java-version: ${{ env.JAVA_VERSION }}
distribution: ${{ env.JAVA_DISTRIBUTION }}
- name: Initialize CodeQL
uses: github/codeql-action/init@c10b8064de6f491fea524254123dbe5e09572f13 # v4
with:
languages: java
queries: security-extended
- name: Setup Gradle
uses: gradle/actions/setup-gradle@0723195856401067f7a2779048b490ace7a47d7c # v5
with:
cache-read-only: true
- name: Grant execute permission to gradlew
run: chmod +x ./gradlew
- name: Build for CodeQL analysis
env:
VERSION: ${{ inputs.version }}
run: |
./gradlew clean build -x test \
-Pversion="${VERSION}" \
--rerun-tasks \
--no-daemon \
--stacktrace
- name: Perform CodeQL Analysis
uses: github/codeql-action/analyze@c10b8064de6f491fea524254123dbe5e09572f13 # v4
with:
category: "release-scan"
upload: false
- name: CodeQL scan summary
if: success()
run: |
{
echo "### CodeQL Security Scan Passed"
echo "- **Language**: Java/Kotlin"
echo "- **Queries**: Security-only"
echo "- **Status**: No code vulnerabilities detected"
} >> "$GITHUB_STEP_SUMMARY"
# ============================================================================
# JOB 8: PUBLISH
# Creates GitHub Release and attaches JAR artifacts
# Only runs if publish checkbox is enabled and all checks pass
# ============================================================================
publish:
name: Publish to GitHub Releases
runs-on: ubuntu-latest
needs: [test, security-scan-repo, security-scan-dependencies, security-scan-artifacts, codeql-scan]
if: inputs.publish == true
permissions:
contents: write # Required for creating releases and tags
steps:
- name: Check out repository
uses: actions/checkout@93cb6efe18208431cddfb8368fd83d5badbf9bfd # v5
with:
persist-credentials: false
fetch-depth: 0
- name: Download main JAR
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: main-jar
path: release-artifacts/
- name: Download shadow JAR
uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6
with:
name: shadow-jar
path: release-artifacts/
- name: List release artifacts
run: |
echo "Artifacts to publish:"
ls -lh release-artifacts/
- name: Generate release notes
id: release-notes
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
PR_NUMBER: ${{ inputs.pr_number }}
run: |
# Get latest tag for comparison
LATEST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
# Start release notes
: > release-notes.md
# If triggered by a tagged PR, prepend PR content
if [ -n "$PR_NUMBER" ]; then
PR_TITLE=$(gh pr view "$PR_NUMBER" --json title --jq '.title' 2>/dev/null || true)
PR_BODY=$(gh pr view "$PR_NUMBER" --json body --jq '.body' 2>/dev/null || true)
if [ -n "$PR_TITLE" ]; then
{
echo "## What's New in v${VERSION}"
echo ""
echo "### ${PR_TITLE} (#${PR_NUMBER})"
echo ""
echo "${PR_BODY}"
echo ""
echo "---"
echo ""
} >> release-notes.md
fi
fi
cat >> release-notes.md << EOF
## Release v${VERSION}
### Artifacts
This release includes:
- **Main JAR**: Standard library JAR (without dependencies)
- **Shadow JAR**: All-in-one fat JAR with embedded dependencies
### Security
All artifacts have been scanned and verified:
- [PASS] Repository configuration scan passed
- [PASS] Dependency vulnerability scan passed
- [PASS] Artifact security scan passed
- [PASS] All tests passed with coverage
### Installation
**Gradle (Kotlin DSL)**:
\`\`\`kotlin
repositories {
maven {
url = uri("https://github.com/CDCgov/prime-fhir-converter/releases/download/v${VERSION}/")
}
}
dependencies {
implementation("gov.cdc.prime:prime-fhir-converter:${VERSION}")
}
\`\`\`
**Maven**:
\`\`\`xml
<dependency>
<groupId>gov.cdc.prime</groupId>
<artifactId>fhirconverter</artifactId>
<version>${VERSION}</version>
</dependency>
\`\`\`
Or download the JARs directly from the assets below.
EOF
if [ -n "$LATEST_TAG" ]; then
{
echo ""
echo "### Changes Since $LATEST_TAG"
echo ""
git log "${LATEST_TAG}..HEAD" --pretty=format:"- %s (%h)"
} >> release-notes.md
fi
cat release-notes.md
- name: Create GitHub Release
env:
GH_TOKEN: ${{ secrets.GITHUB_TOKEN }}
VERSION: ${{ inputs.version }}
run: |
gh release create "v${VERSION}" \
--title "Release v${VERSION}" \
--notes-file release-notes.md \
release-artifacts/*.jar
- name: Publish summary
env:
VERSION: ${{ inputs.version }}
SERVER_URL: ${{ github.server_url }}
REPO: ${{ github.repository }}
run: |
{
echo "### Release Published Successfully"
echo ""
echo "- **Version**: v${VERSION}"
echo "- **Tag**: v${VERSION}"
echo "- **Artifacts**: 2 JARs attached"
echo "- **URL**: ${SERVER_URL}/${REPO}/releases/tag/v${VERSION}"
echo ""
echo "[PASS] **All security scans passed**"
echo "[PASS] **All tests passed**"
echo "[PASS] **Build artifacts verified**"
} >> "$GITHUB_STEP_SUMMARY"
# ============================================================================
# WORKFLOW SUMMARY
# This job always runs and provides final status
# ============================================================================
workflow-summary:
name: Workflow Summary
runs-on: ubuntu-latest
needs: [validate, build, test, security-scan-repo, security-scan-dependencies, security-scan-artifacts]
if: always()
steps:
- name: Generate workflow summary
env:
VALIDATE_RESULT: ${{ needs.validate.result }}
BUILD_RESULT: ${{ needs.build.result }}
TEST_RESULT: ${{ needs.test.result }}
REPO_SCAN_RESULT: ${{ needs.security-scan-repo.result }}
DEP_SCAN_RESULT: ${{ needs.security-scan-dependencies.result }}
ART_SCAN_RESULT: ${{ needs.security-scan-artifacts.result }}
PUBLISH: ${{ inputs.publish }}
run: |
status() { [ "$1" = "success" ] && echo "[PASS]" || echo "[FAIL]"; }
{
echo "## Workflow Execution Summary"
echo ""
echo "| Stage | Status |"
echo "|-------|--------|"
echo "| Validation | $(status "$VALIDATE_RESULT") |"
echo "| Build | $(status "$BUILD_RESULT") |"
echo "| Tests | $(status "$TEST_RESULT") |"
echo "| Repo Security Scan | $(status "$REPO_SCAN_RESULT") |"
echo "| Dependency Scan | $(status "$DEP_SCAN_RESULT") |"
echo "| Artifact Scan | $(status "$ART_SCAN_RESULT") |"
echo ""
if [ "$PUBLISH" == "true" ]; then
echo "**Publish**: Enabled (conditional on all checks passing)"
else
echo "**Publish**: Disabled"
fi
} >> "$GITHUB_STEP_SUMMARY"