CI steps, caching, artifacts
On this page
Why CI Is a Production System
CI is not a developer convenience. It is the production gatekeeper. If CI is slow, flaky, or incomplete, teams ship risk. Production symptoms of bad CI: - Developers bypass checks - Hotfixes skip gates - Deploys rely on manual trust CI must be fast, deterministic, and complete.Incident Scenario: CI Allowed a Broken Artifact
Pipeline compiled code but skipped integration tests. The artifact deployed and failed on startup due to a migration mismatch. Rollback happened, but customers saw the outage. Root cause: Missing stage and weak gating.Core CI Stages
A practical Java CI pipeline: 1. Checkout and dependency restore 2. Compile + unit tests 3. Static analysis baseline 4. Integration tests (Testcontainers) 5. Package artifact (jar) 6. Publish artifacts and reports 7. Optional security scans (dependencies, container) 8. Optional contract verification Stages must be explicit to isolate failures quickly.Caching Done Right
Cache what is safe: - Maven local repository - Gradle caches Avoid caching compiled outputs across commits unless keys are strict. Bad caching creates heisenbugs. Use cache keys based on build files and lockfiles so cache invalidation is correct.Artifacts and Traceability
Always produce immutable artifacts: - versioned jar - build metadata (commit hash) - test reports - coverage reports If you cannot trace what was deployed, you cannot debug incidents.Developer Feedback
CI must surface failures clearly: - attach test reports - highlight failing tests - expose container logs when integration tests fail If developers cannot see why it failed, they will rerun until green.Security Gates
At minimum: - dependency vulnerability scan - fail on critical vulnerabilities introduced by new changes Tune thresholds. Too noisy kills adoption. Too loose is meaningless.Example: GitHub Actions Pipeline
This example runs: - unit tests - integration tests with Testcontainers - Gradle caching - artifact and test report upload Notes: - For integration tests, you typically need Docker available on the runner. - If your integration tests rely on containers, ensure they run in a dedicated stage so failures are obvious. - Keep unit tests fast, integration tests real.
name: CI
on:
push:
branches: [ "main" ]
pull_request:
branches: [ "main" ]
permissions:
contents: read
jobs:
build-test:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up JDK 21
uses: actions/setup-java@v4
with:
distribution: temurin
java-version: "21"
- name: Cache Gradle
uses: actions/cache@v4
with:
path: |
~/.gradle/caches
~/.gradle/wrapper
key: ${{ runner.os }}-gradle-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties', 'gradle.properties') }}
restore-keys: |
${{ runner.os }}-gradle-
- name: Grant execute permission for Gradle wrapper
run: chmod +x ./gradlew
- name: Unit tests
run: ./gradlew test --no-daemon
- name: Integration tests
run: ./gradlew integrationTest --no-daemon
- name: Build artifact
run: ./gradlew build --no-daemon
- name: Upload test reports
if: always()
uses: actions/upload-artifact@v4
with:
name: test-reports
path: |
**/build/reports/tests
**/build/test-results
- name: Upload build outputs
if: success()
uses: actions/upload-artifact@v4
with:
name: build-artifacts
path: |
**/build/libs/*.jar