1name: Code Coverage with codecov
2
3on:
4  push:
5    branches:
6      - main
7      - v*-branch
8      - collab-*
9
10permissions:
11  contents: read
12
13concurrency:
14  group: ${{ github.workflow }}-${{ github.event_name }}-${{ github.head_ref || github.ref }}
15  cancel-in-progress: true
16
17jobs:
18  codecov:
19    if: github.repository_owner == 'zephyrproject-rtos'
20    runs-on:
21      group: zephyr-runner-v2-linux-x64-4xlarge
22    container:
23      image: ghcr.io/zephyrproject-rtos/ci-repo-cache:v0.28.7.20251127
24      options: '--entrypoint /bin/bash'
25    strategy:
26      fail-fast: false
27      matrix:
28        platform: ["mps2/an385", "native_sim", "qemu_x86", "unit_testing"]
29        include:
30          - platform: 'mps2/an385'
31            normalized: 'mps2_an385'
32          - platform: 'native_sim'
33            normalized: 'native_sim'
34          - platform: 'qemu_x86'
35            normalized: 'qemu_x86'
36          - platform: 'unit_testing'
37            normalized: 'unit_testing'
38    env:
39      CCACHE_DIR: /node-cache/ccache-zephyr
40      CCACHE_REMOTE_STORAGE: "redis://cache-*.keydb-cache.svc.cluster.local|shards=1,2,3"
41      CCACHE_REMOTE_ONLY: "true"
42      # `--specs` is ignored because ccache is unable to resovle the toolchain specs file path.
43      CCACHE_IGNOREOPTIONS: '-specs=* --specs=*'
44    steps:
45      - name: Apply container owner mismatch workaround
46        run: |
47          # FIXME: The owner UID of the GITHUB_WORKSPACE directory may not
48          #        match the container user UID because of the way GitHub
49          #        Actions runner is implemented. Remove this workaround when
50          #        GitHub comes up with a fundamental fix for this problem.
51          git config --global --add safe.directory ${GITHUB_WORKSPACE}
52
53      - name: Print cloud service information
54        run: |
55          echo "ZEPHYR_RUNNER_CLOUD_PROVIDER = ${ZEPHYR_RUNNER_CLOUD_PROVIDER}"
56          echo "ZEPHYR_RUNNER_CLOUD_NODE = ${ZEPHYR_RUNNER_CLOUD_NODE}"
57          echo "ZEPHYR_RUNNER_CLOUD_POD = ${ZEPHYR_RUNNER_CLOUD_POD}"
58
59      - name: Update PATH for west
60        run: |
61          echo "$HOME/.local/bin" >> $GITHUB_PATH
62
63      - name: Clone cached Zephyr repository
64        continue-on-error: true
65        run: |
66          git clone --shared /repo-cache/zephyrproject/zephyr .
67          git remote set-url origin ${GITHUB_SERVER_URL}/${GITHUB_REPOSITORY}
68
69      - name: checkout
70        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
71        with:
72          fetch-depth: 0
73
74      - name: Set up Python
75        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
76        with:
77          python-version: 3.12
78          cache: pip
79          cache-dependency-path: scripts/requirements-actions.txt
80
81      - name: Install Python packages
82        run: |
83          pip install -r scripts/requirements-actions.txt --require-hashes
84
85      - name: west setup
86        run: |
87          west init -l . || true
88          west update 1> west.update.log || west update 1> west.update-2.log
89
90      - name: Environment Setup
91        run: |
92          cmake --version
93          gcc --version
94          ls -la
95
96          echo "ZEPHYR_SDK_INSTALL_DIR=/opt/toolchains/zephyr-sdk-$( cat SDK_VERSION )" >> $GITHUB_ENV
97
98      - name: Set up ccache
99        run: |
100          mkdir -p ${CCACHE_DIR}
101          ccache -M 10G
102          ccache -p
103          ccache -z -s -vv
104
105      - name: Run Tests with Twister (Push)
106        continue-on-error: true
107        run: |
108          export ZEPHYR_BASE=${PWD}
109          export ZEPHYR_TOOLCHAIN_VARIANT=zephyr
110          mkdir -p coverage/reports
111          ./scripts/twister --save-tests ${{matrix.normalized}}-testplan.json
112          ls -la
113          ./scripts/twister \
114            -i --force-color -N -v --filter runnable -p ${{ matrix.platform }} --coverage \
115            -T tests --coverage-tool gcovr -xCONFIG_TEST_EXTRA_STACK_SIZE=4096 -e nano \
116            --timeout-multiplier 2
117
118      - name: Build Doxygen Coverage
119        if: matrix.platform == 'unit_testing'
120        run: |
121          pip install -r doc/requirements.txt --require-hashes
122          sudo apt-get update
123          sudo apt-get install -y graphviz # dot is needed but currently missing from the Docker image
124          cmake -B doc/_build -S doc
125          cmake --build doc/_build --target doxygen-coverage
126
127      - name: Upload Doxygen Coverage Results
128        if: matrix.platform == 'unit_testing'
129        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
130        with:
131          name: doxygen-coverage-results
132          path: |
133            doc/_build/new.info
134            doc/_build/coverage-report
135
136      - name: Print ccache stats
137        if: always()
138        run: |
139          ccache -s -vv
140
141      - name: Rename coverage files
142        if: always()
143        run: |
144          mv twister-out/coverage.json coverage/reports/${{matrix.normalized}}.json
145
146      - name: Upload Coverage Results
147        if: always()
148        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
149        with:
150          name: Coverage Data (Subset ${{ matrix.normalized }})
151          path: |
152            coverage/reports/${{ matrix.normalized }}.json
153            ${{ matrix.normalized }}-testplan.json
154
155  codecov-results:
156    name: "Publish Coverage Results"
157    needs: codecov
158    runs-on: ubuntu-24.04
159    # the codecov job might be skipped, we don't need to run this job then
160    if: success() || failure()
161
162    steps:
163      - name: checkout
164        uses: actions/checkout@08c6903cd8c0fde910a37f88322edcfb5dd907a8 # v5.0.0
165        with:
166          fetch-depth: 0
167
168      - name: Set up Python
169        uses: actions/setup-python@e797f83bcb11b83ae66e0230d6156d7c80228e7c # v6.0.0
170        with:
171          python-version: 3.12
172          cache: pip
173          cache-dependency-path: scripts/requirements-actions.txt
174
175      - name: Install Python packages
176        run: |
177          pip install -r scripts/requirements-actions.txt --require-hashes
178
179      - name: Download Artifacts
180        uses: actions/download-artifact@018cc2cf5baa6db3ef3c5f8a56943fffe632ef53 # v6.0.0
181        with:
182          path: coverage/reports
183
184      - name: Move coverage files
185        run: |
186          ls -lRt  ./coverage/reports
187          mv ./coverage/reports/*/*testplan.json .
188          mv ./coverage/reports/*/coverage/reports/*.json ./coverage/reports
189          ls -la ./coverage/reports
190
191      - name: Generate list of coverage files
192        id: get-coverage-files
193        shell: cmake -P {0}
194        run: |
195          file(GLOB INPUT_FILES_LIST  "coverage/reports/*.json")
196          set(MERGELIST "")
197          set(FILELIST "")
198          foreach(ITEM ${INPUT_FILES_LIST})
199            get_filename_component(f ${ITEM} NAME)
200            if(FILELIST STREQUAL "")
201              set(FILELIST "${f}")
202            else()
203              set(FILELIST "${FILELIST},${f}")
204            endif()
205          endforeach()
206          foreach(ITEM ${INPUT_FILES_LIST})
207            get_filename_component(f ${ITEM} NAME)
208            if(MERGELIST STREQUAL "")
209              set(MERGELIST "--add-tracefile ${f}")
210            else()
211              set(MERGELIST "${MERGELIST} -a ${f}")
212            endif()
213          endforeach()
214          file(APPEND $ENV{GITHUB_OUTPUT} "mergefiles=${MERGELIST}\n")
215          file(APPEND $ENV{GITHUB_OUTPUT} "covfiles=${FILELIST}\n")
216
217      - name: Merge coverage files
218        run: |
219          pushd ./coverage/reports
220          gcovr ${{ steps.get-coverage-files.outputs.mergefiles }}  --merge-mode-functions=separate --json merged.json
221          gcovr ${{ steps.get-coverage-files.outputs.mergefiles }} --merge-mode-functions=separate --cobertura merged.xml
222          popd
223
224      - name: Get current date
225        id: run_date
226        run: |
227            echo "run_date=$(date --iso-8601=minutes)" >> "$GITHUB_OUTPUT"
228            echo "run_date_short=$(date +'%Y-%m-%d')" >> "$GITHUB_OUTPUT"
229            echo "run_date_year=$(date +'%Y')" >> "$GITHUB_OUTPUT"
230            echo "run_date_month=$(date +'%m')" >> "$GITHUB_OUTPUT"
231
232      - name: Generate Coverage Report
233        if: always()
234        run: |
235          python3 ./scripts/ci/coverage/coverage_analysis.py \
236            -t native_sim-testplan.json \
237            -m MAINTAINERS.yml \
238            -c coverage/reports/merged.json \
239            -o coverage-report-${{ steps.run_date.outputs.run_date_short }} \
240            -f all
241          cp coverage-report-* coverage/reports/
242
243      - name: Upload Merged Coverage Results and Report
244        if: always()
245        uses: actions/upload-artifact@330a01c490aca151604b8cf639adc76d48f6c5d4 # v5.0.0
246        with:
247          name: Coverage Data and report
248          path: |
249            coverage/reports/merged.json
250            coverage/reports/merged.xml
251            coverage/reports/coverage-report-${{ steps.run_date.outputs.run_date_short }}.json
252            coverage/reports/coverage-report-${{ steps.run_date.outputs.run_date_short }}.xlsx
253
254      - name: Upload test coverage to Codecov
255        if: always()
256        uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
257        with:
258          env_vars: OS,PYTHON
259          fail_ci_if_error: false
260          verbose: true
261          token: ${{ secrets.CODECOV_TOKEN }}
262          files: coverage/reports/merged.xml
263          flags: unittests-coverage
264
265      - name: Upload Doxygen coverage to Codecov
266        if: always()
267        uses: codecov/codecov-action@5a1091511ad55cbe89839c7260b706298ca349f7 # v5.5.1
268        with:
269          env_vars: OS,PYTHON
270          fail_ci_if_error: false
271          verbose: true
272          token: ${{ secrets.CODECOV_TOKEN }}
273          files: coverage/reports/doxygen-coverage-results/new.info
274          disable_search: true
275          flags: doxygen-coverage
276