Skip to content
Latchkey

CI workflow (mockito/mockito)

The CI workflow from mockito/mockito, explained and optimized by Latchkey.

C

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.

Grade your own workflow free or run it on Latchkey →
Source: mockito/mockito.github/workflows/ci.ymlLicense MITView source

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

workflow (.yml)
#
# 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

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:

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.

Actions used in this workflow