CI workflow (mockito/mockito)
The CI workflow from mockito/mockito, explained and optimized by Latchkey.
CI health: C - fair
Point runs-on at Latchkey and get run de-duplication, job timeouts, SHA-pinned actions, self-healing for flaky steps, and up to 58% lower cost, applied automatically.
What it does
This is the CI workflow from the mockito/mockito repository, a real project running GitHub Actions. It is shown here with attribution under its MIT license.
Below, Latchkey shows a faster, safer version produced by its optimization engine.
The workflow
#
# CI build that assembles artifacts and runs tests.
# If validation is successful this workflow releases from the main dev branch.
#
# - skipping CI: add [skip ci] to the commit message
# - skipping release: add [skip release] to the commit message
#
name: CI
on:
push:
branches: ['main']
tags: [v*]
pull_request:
branches: ['**']
permissions:
contents: read
jobs:
#
# Main build job
#
java:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
# Definition of the build matrix
strategy:
matrix:
java: [11, 17, 21]
entry:
- { mock-maker: 'mock-maker-default', member-accessor: 'member-accessor-default' }
- { mock-maker: 'mock-maker-inline', member-accessor: 'member-accessor-module' }
- { mock-maker: 'mock-maker-subclass', member-accessor: 'member-accessor-module' }
- { mock-maker: 'mock-maker-subclass', member-accessor: 'member-accessor-reflection' }
- { mock-maker: 'mock-maker-inline', member-accessor: 'member-accessor-reflection' }
# All build steps
# SINGLE-MATRIX-JOB means that the step does not need to be executed on every job in the matrix
steps:
- name: 1. Check out code
uses: actions/checkout@v7 # https://github.com/actions/checkout
with:
fetch-depth: '0' # https://github.com/shipkit/shipkit-changelog#fetch-depth-on-ci
- name: 2. Set up Java for running Gradle build
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
cache: 'gradle'
- name: 3. Validate Gradle wrapper
if: matrix.java == 11 && matrix.entry.mock-maker == 'mock-maker-default' # SINGLE-MATRIX-JOB
uses: gradle/actions/wrapper-validation@v5 # https://github.com/gradle/wrapper-validation-action
- name: 4. Build and check reproducibility of artifacts (single job only)
if: matrix.java == 11 && matrix.entry.mock-maker == 'mock-maker-default' # SINGLE-MATRIX-JOB
run: ./check_reproducibility.sh
- name: 5. Spotless check (single job only). Run './gradlew spotlessApply' locally if this job fails.
if: matrix.java == 11 && matrix.entry.mock-maker == 'mock-maker-default' # SINGLE-MATRIX-JOB
run: >
./gradlew
spotlessCheck
--stacktrace
--scan
- name: 6. Build on Java ${{ matrix.java }} with ${{ matrix.entry.mock-maker }} and ${{ matrix.entry.member-accessor }}
run: >
./gradlew
-Pmockito.test.java=${{ matrix.java }}
build
--stacktrace
--scan
env:
MOCK_MAKER: ${{ matrix.entry.mock-maker }}
MEMBER_ACCESSOR: ${{ matrix.entry.member-accessor }}
- name: 7. Generate coverage report
run: >
./gradlew
-Pmockito.test.java=${{ matrix.java }}
coverageReport
--stacktrace
--scan
- name: 8. Upload coverage report
uses: codecov/codecov-action@v7
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: mockito-core/build/reports/jacoco/mockitoCoverage/mockitoCoverage.xml
fail_ci_if_error: true
#
# Android build job
#
android:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
timeout-minutes: 30
# Definition of the build matrix
strategy:
matrix:
# Minimum supported and maximum available.
android-api: [ 28, 33, 36 ]
# All build steps
steps:
- name: 1. Check out code
uses: actions/checkout@v7
with:
fetch-depth: '0' # https://github.com/shipkit/shipkit-changelog#fetch-depth-on-ci
- name: 2. Set up Java for running Gradle build
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 17
cache: 'gradle'
- name: 3. Enable KVM.
run: |
echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules
sudo udevadm control --reload-rules
sudo udevadm trigger --name-match=kvm
- name: 4. Run Android tests on Android API level ${{ matrix.android-api }}
uses: reactivecircus/android-emulator-runner@v2
with:
arch: x86_64
api-level: ${{ matrix.android-api }}
# Override default "-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim"
# See emulator manual for reference: https://developer.android.com/studio/run/emulator-commandline
emulator-options: >
-no-window
-gpu swiftshader_indirect
-no-snapshot
-noaudio
-no-boot-anim
-camera-back none
-camera-front none
# See logcat manual for reference: https://developer.android.com/studio/command-line/logcat
script: |
# Capture logcat output from "Launch Emulator" to a file.
adb logcat -d > emulator-startup.log
# Shorten the logcat output, by truncating at this point, the relevant part is yet to come.
# Best effort, could fail with "failed to clear the 'main' log",
# because something is locking logcat, so try a few times, and ignore errors each time.
adb logcat --clear || true
adb logcat --clear || true
adb logcat --clear || true
# Capture full logcat output to a file.
adb logcat > emulator.log & echo $! > logcat_file.pid
# Output instrumentation test logs to the GitHub Actions output.
adb logcat "*:S MonitoringInstr:V AndroidJUnitRunner:V TestRequestBuilder:V TestExecutor:V TestRunner:V" --format=color & echo $! > logcat_console.pid
echo 0 > gradle.exit # Set a default exit code.
# Run the actual tests and generate coverage report including mockito-core classes.
./gradlew :mockito-integration-tests:android-tests:connectedCheck :mockito-integration-tests:android-tests:createMockitoCoverageReport --no-daemon --no-build-cache || echo $? > gradle.exit
# Stop capturing logcat output.
kill $(cat logcat_file.pid) || echo "::warning file=.github/workflows/ci.yml::Logcat process $(cat logcat_file.pid) didn't exist."
kill $(cat logcat_console.pid) || echo "::warning file=.github/workflows/ci.yml::Logcat process $(cat logcat_console.pid) didn't exist."
# Make sure the step fails if the tests failed.
exit $(cat gradle.exit)
- name: 5. Upload artifact "android-tests-results-${{ matrix.android-api }}"
if: success() || failure()
uses: actions/upload-artifact@v6
with:
name: androidTest-results-${{ matrix.android-api }}
path: |
${{ github.workspace }}/mockito-integration-tests/android-tests/build/reports/android-tests/connected/**
${{ github.workspace }}/emulator.log
${{ github.workspace }}/emulator-startup.log
- name: 6. Upload coverage report
uses: codecov/codecov-action@v7
env:
CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }}
with:
files: mockito-integration-tests/android-tests/build/reports/jacoco/createMockitoCoverageReport/createMockitoCoverageReport.xml
fail_ci_if_error: true
#
# GraalVM native tests job
#
graalvm:
runs-on: ubuntu-latest
if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')"
steps:
- name: 1. Check out code
uses: actions/checkout@v7
with:
fetch-depth: '0'
- name: 2. Setup GraalVM
uses: graalvm/setup-graalvm@v1.6.0
with:
java-version: "21"
distribution: "graalvm"
cache: gradle
- name: 3. Run GraalVM native tests with metadata
run: ./gradlew :mockito-integration-tests:graalvm-tests:nativeTest -PenableGraalTracingAgent --scan
- name: 4. Verify GraalVM metadata generation
run: |
REFLECT_CONFIG="mockito-integration-tests/graalvm-tests/src/test/resources/META-INF/native-image/org.mockito/graalvm-tests/reflect-config.json"
test -f "$REFLECT_CONFIG" || {
echo "reflect-config.json not found at $REFLECT_CONFIG"
exit 1
}
grep -q "org\.mockito\.internal\.creation\.bytebuddy\.codegen\.DummyObject\$MockitoMock" "$REFLECT_CONFIG" || {
echo "DummyObject mock not found in reflect-config.json"
exit 1
}
#
# Release job, only for pushes to the main development branch
#
release:
permissions:
contents: write
runs-on: ubuntu-latest
needs: [java, android, graalvm] # all build jobs must pass before we can release
if: github.event_name == 'push'
&& (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v'))
&& github.repository == 'mockito/mockito'
&& !contains(toJSON(github.event.commits.*.message), '[skip release]')
steps:
- name: 1. Check out code
uses: actions/checkout@v7 # https://github.com/actions/checkout
with:
fetch-depth: '0' # https://github.com/shipkit/shipkit-changelog#fetch-depth-on-ci
- name: 2. Set up Java for running Gradle build
uses: actions/setup-java@v5
with:
distribution: 'zulu'
java-version: 21
cache: 'gradle'
- name: 3. Build and release
run: >
./gradlew
githubRelease
publishToSonatype
closeAndReleaseStagingRepositories
releaseSummary
--info
--stacktrace
env:
GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}}
NEXUS_TOKEN_USER: ${{secrets.NEXUS_TOKEN_USER}}
NEXUS_TOKEN_PWD: ${{secrets.NEXUS_TOKEN_PWD}}
PGP_KEY: ${{secrets.PGP_KEY}}
PGP_PWD: ${{secrets.PGP_PWD}}
The same workflow, on Latchkey
Removes redundant runs and caps runaway jobs. Added and changed lines are highlighted.
# # CI build that assembles artifacts and runs tests. # If validation is successful this workflow releases from the main dev branch. # # - skipping CI: add [skip ci] to the commit message # - skipping release: add [skip release] to the commit message # name: CI on: push: branches: ['main'] tags: [v*] pull_request: branches: ['**'] permissions: contents: read concurrency: group: ${{ github.workflow }}-${{ github.ref }} cancel-in-progress: true jobs: # # Main build job # java: timeout-minutes: 30 runs-on: latchkey-small if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" # Definition of the build matrix strategy: matrix: java: [11, 17, 21] entry: - { mock-maker: 'mock-maker-default', member-accessor: 'member-accessor-default' } - { mock-maker: 'mock-maker-inline', member-accessor: 'member-accessor-module' } - { mock-maker: 'mock-maker-subclass', member-accessor: 'member-accessor-module' } - { mock-maker: 'mock-maker-subclass', member-accessor: 'member-accessor-reflection' } - { mock-maker: 'mock-maker-inline', member-accessor: 'member-accessor-reflection' } # All build steps # SINGLE-MATRIX-JOB means that the step does not need to be executed on every job in the matrix steps: - name: 1. Check out code uses: actions/checkout@v7 # https://github.com/actions/checkout with: fetch-depth: '0' # https://github.com/shipkit/shipkit-changelog#fetch-depth-on-ci - name: 2. Set up Java for running Gradle build uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 17 cache: 'gradle' - name: 3. Validate Gradle wrapper if: matrix.java == 11 && matrix.entry.mock-maker == 'mock-maker-default' # SINGLE-MATRIX-JOB uses: gradle/actions/wrapper-validation@v5 # https://github.com/gradle/wrapper-validation-action - name: 4. Build and check reproducibility of artifacts (single job only) if: matrix.java == 11 && matrix.entry.mock-maker == 'mock-maker-default' # SINGLE-MATRIX-JOB run: ./check_reproducibility.sh - name: 5. Spotless check (single job only). Run './gradlew spotlessApply' locally if this job fails. if: matrix.java == 11 && matrix.entry.mock-maker == 'mock-maker-default' # SINGLE-MATRIX-JOB run: > ./gradlew spotlessCheck --stacktrace --scan - name: 6. Build on Java ${{ matrix.java }} with ${{ matrix.entry.mock-maker }} and ${{ matrix.entry.member-accessor }} run: > ./gradlew -Pmockito.test.java=${{ matrix.java }} build --stacktrace --scan env: MOCK_MAKER: ${{ matrix.entry.mock-maker }} MEMBER_ACCESSOR: ${{ matrix.entry.member-accessor }} - name: 7. Generate coverage report run: > ./gradlew -Pmockito.test.java=${{ matrix.java }} coverageReport --stacktrace --scan - name: 8. Upload coverage report uses: codecov/codecov-action@v7 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: files: mockito-core/build/reports/jacoco/mockitoCoverage/mockitoCoverage.xml fail_ci_if_error: true # # Android build job # android: runs-on: latchkey-small if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" timeout-minutes: 30 # Definition of the build matrix strategy: matrix: # Minimum supported and maximum available. android-api: [ 28, 33, 36 ] # All build steps steps: - name: 1. Check out code uses: actions/checkout@v7 with: fetch-depth: '0' # https://github.com/shipkit/shipkit-changelog#fetch-depth-on-ci - name: 2. Set up Java for running Gradle build uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 17 cache: 'gradle' - name: 3. Enable KVM. run: | echo 'KERNEL=="kvm", GROUP="kvm", MODE="0666", OPTIONS+="static_node=kvm"' | sudo tee /etc/udev/rules.d/99-kvm4all.rules sudo udevadm control --reload-rules sudo udevadm trigger --name-match=kvm - name: 4. Run Android tests on Android API level ${{ matrix.android-api }} uses: reactivecircus/android-emulator-runner@v2 with: arch: x86_64 api-level: ${{ matrix.android-api }} # Override default "-no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim" # See emulator manual for reference: https://developer.android.com/studio/run/emulator-commandline emulator-options: > -no-window -gpu swiftshader_indirect -no-snapshot -noaudio -no-boot-anim -camera-back none -camera-front none # See logcat manual for reference: https://developer.android.com/studio/command-line/logcat script: | # Capture logcat output from "Launch Emulator" to a file. adb logcat -d > emulator-startup.log # Shorten the logcat output, by truncating at this point, the relevant part is yet to come. # Best effort, could fail with "failed to clear the 'main' log", # because something is locking logcat, so try a few times, and ignore errors each time. adb logcat --clear || true adb logcat --clear || true adb logcat --clear || true # Capture full logcat output to a file. adb logcat > emulator.log & echo $! > logcat_file.pid # Output instrumentation test logs to the GitHub Actions output. adb logcat "*:S MonitoringInstr:V AndroidJUnitRunner:V TestRequestBuilder:V TestExecutor:V TestRunner:V" --format=color & echo $! > logcat_console.pid echo 0 > gradle.exit # Set a default exit code. # Run the actual tests and generate coverage report including mockito-core classes. ./gradlew :mockito-integration-tests:android-tests:connectedCheck :mockito-integration-tests:android-tests:createMockitoCoverageReport --no-daemon --no-build-cache || echo $? > gradle.exit # Stop capturing logcat output. kill $(cat logcat_file.pid) || echo "::warning file=.github/workflows/ci.yml::Logcat process $(cat logcat_file.pid) didn't exist." kill $(cat logcat_console.pid) || echo "::warning file=.github/workflows/ci.yml::Logcat process $(cat logcat_console.pid) didn't exist." # Make sure the step fails if the tests failed. exit $(cat gradle.exit) - name: 5. Upload artifact "android-tests-results-${{ matrix.android-api }}" if: success() || failure() uses: actions/upload-artifact@v6 with: name: androidTest-results-${{ matrix.android-api }} path: | ${{ github.workspace }}/mockito-integration-tests/android-tests/build/reports/android-tests/connected/** ${{ github.workspace }}/emulator.log ${{ github.workspace }}/emulator-startup.log - name: 6. Upload coverage report uses: codecov/codecov-action@v7 env: CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} with: files: mockito-integration-tests/android-tests/build/reports/jacoco/createMockitoCoverageReport/createMockitoCoverageReport.xml fail_ci_if_error: true # # GraalVM native tests job # graalvm: timeout-minutes: 30 runs-on: latchkey-small if: "! contains(toJSON(github.event.commits.*.message), '[skip ci]')" steps: - name: 1. Check out code uses: actions/checkout@v7 with: fetch-depth: '0' - name: 2. Setup GraalVM uses: graalvm/setup-graalvm@v1.6.0 with: java-version: "21" distribution: "graalvm" cache: gradle - name: 3. Run GraalVM native tests with metadata run: ./gradlew :mockito-integration-tests:graalvm-tests:nativeTest -PenableGraalTracingAgent --scan - name: 4. Verify GraalVM metadata generation run: | REFLECT_CONFIG="mockito-integration-tests/graalvm-tests/src/test/resources/META-INF/native-image/org.mockito/graalvm-tests/reflect-config.json" test -f "$REFLECT_CONFIG" || { echo "reflect-config.json not found at $REFLECT_CONFIG" exit 1 } grep -q "org\.mockito\.internal\.creation\.bytebuddy\.codegen\.DummyObject\$MockitoMock" "$REFLECT_CONFIG" || { echo "DummyObject mock not found in reflect-config.json" exit 1 } # # Release job, only for pushes to the main development branch # release: timeout-minutes: 30 permissions: contents: write runs-on: latchkey-small needs: [java, android, graalvm] # all build jobs must pass before we can release if: github.event_name == 'push' && (github.ref == 'refs/heads/main' || startsWith(github.ref, 'refs/tags/v')) && github.repository == 'mockito/mockito' && !contains(toJSON(github.event.commits.*.message), '[skip release]') steps: - name: 1. Check out code uses: actions/checkout@v7 # https://github.com/actions/checkout with: fetch-depth: '0' # https://github.com/shipkit/shipkit-changelog#fetch-depth-on-ci - name: 2. Set up Java for running Gradle build uses: actions/setup-java@v5 with: distribution: 'zulu' java-version: 21 cache: 'gradle' - name: 3. Build and release run: > ./gradlew githubRelease publishToSonatype closeAndReleaseStagingRepositories releaseSummary --info --stacktrace env: GITHUB_TOKEN: ${{secrets.GITHUB_TOKEN}} NEXUS_TOKEN_USER: ${{secrets.NEXUS_TOKEN_USER}} NEXUS_TOKEN_PWD: ${{secrets.NEXUS_TOKEN_PWD}} PGP_KEY: ${{secrets.PGP_KEY}} PGP_PWD: ${{secrets.PGP_PWD}}
What changed
- Run on Latchkey managed runners with one line (
runs-on), which apply the fixes below automatically and self-heal transient failures. This example useslatchkey-small; pick the runner size that fits the job. - Cancel superseded runs when a branch or PR gets a newer push.
- Add a job timeout so a hung step cannot burn hours of runner time.
4 third-party actions are referenced by a movable tag. Pin them to the commit SHA (Latchkey resolves and applies this automatically) so a repointed tag cannot change what runs.
What Latchkey heals here
This workflow has steps that commonly fail on transient issues (network, registries, flaky browsers). On Latchkey managed runners they are detected, retried, and self-healed instead of failing your build:
- Dependency installs
This workflow runs 4 jobs (20 with the matrix expanded) per trigger. On Latchkey the same minutes cost up to 58% less than GitHub-hosted, with zero queue time.