Skip to content

Commit 91aa982

Browse files
committed
feat: Better coverage report with comparaison and config file
1 parent f6da824 commit 91aa982

4 files changed

Lines changed: 301 additions & 14 deletions

File tree

.github/workflows/continuous-integration.yml

Lines changed: 35 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -24,10 +24,6 @@ on:
2424
required: false
2525
type: string
2626
default: ""
27-
code-coverage:
28-
required: false
29-
type: boolean
30-
default: false
3127

3228
jobs:
3329
ci:
@@ -68,6 +64,26 @@ jobs:
6864
uses: "actions/checkout@v6"
6965
with:
7066
path: "${{ inputs.plugin-key }}"
67+
- name: "Detect coverage configuration"
68+
id: "coverage-config"
69+
# Use default `bash` shell with `github-actions-runner` user
70+
shell: "bash"
71+
working-directory: "${{ github.workspace }}/${{ inputs.plugin-key }}"
72+
run: |
73+
CONFIG_FILE=".glpi-coverage.json"
74+
if [[ -f "$CONFIG_FILE" ]]; then
75+
ENABLED=$(jq -r '.enabled // true' "$CONFIG_FILE")
76+
if [[ "$ENABLED" != "true" ]]; then
77+
echo "coverage-enabled=false" >> $GITHUB_OUTPUT
78+
echo "ℹ️ Code coverage is disabled via $CONFIG_FILE"
79+
exit 0
80+
fi
81+
echo "coverage-enabled=true" >> $GITHUB_OUTPUT
82+
else
83+
echo "coverage-enabled=false" >> $GITHUB_OUTPUT
84+
echo "ℹ️ No $CONFIG_FILE found, code coverage is disabled."
85+
exit 0
86+
fi
7187
- name: "Execute init script"
7288
if: ${{ inputs.init-script != '' }}
7389
# Use default `bash` shell with `github-actions-runner` user
@@ -267,7 +283,7 @@ jobs:
267283
run: |
268284
sudo service apache2 start
269285
- name: "Setup coverage driver"
270-
if: ${{ !cancelled() && inputs.code-coverage == true }}
286+
if: ${{ !cancelled() && steps.coverage-config.outputs.coverage-enabled == 'true' }}
271287
shell: "bash"
272288
run: |
273289
if ! php -m | grep -q -E 'xdebug|pcov'; then
@@ -278,14 +294,14 @@ jobs:
278294
- name: "PHPUnit"
279295
if: ${{ !cancelled() && hashFiles(format('{0}/phpunit.xml', inputs.plugin-key)) != '' }}
280296
env:
281-
PCOV_ENABLED: "${{ inputs.code-coverage && '1' || '0' }}"
282-
XDEBUG_MODE: "${{ inputs.code-coverage && 'coverage' || 'off' }}"
297+
PCOV_ENABLED: "${{ steps.coverage-config.outputs.coverage-enabled == 'true' && '1' || '0' }}"
298+
XDEBUG_MODE: "${{ steps.coverage-config.outputs.coverage-enabled == 'true' && 'coverage' || 'off' }}"
283299
run: |
284300
echo -e "\033[0;33mExecuting PHPUnit...\033[0m"
285301
PHPUNIT_FLAGS="--colors=always"
286302
PHP_CMD="php"
287303
288-
if [[ "${{ inputs.code-coverage }}" == "true" ]]; then
304+
if [[ "${{ steps.coverage-config.outputs.coverage-enabled }}" == "true" ]]; then
289305
PHPUNIT_FLAGS="$PHPUNIT_FLAGS --coverage-text --coverage-cobertura=cobertura.xml"
290306
# Explicitly load PCOV if needed
291307
PHP_CMD="php -d extension=pcov.so"
@@ -299,12 +315,20 @@ jobs:
299315
echo -e "\033[0;31mPHPUnit binary not found!\033[0m"
300316
exit 1
301317
fi
318+
- name: "Fix coverage paths for IDE import"
319+
if: ${{ !cancelled() && steps.coverage-config.outputs.coverage-enabled == 'true' }}
320+
run: |
321+
echo "Sanitizing paths in cobertura.xml..."
322+
sed -i 's|/var/www/glpi/plugins/${{ inputs.plugin-key }}/|plugins/${{ inputs.plugin-key }}/|g' cobertura.xml
302323
- name: "Upload coverage report"
303-
uses: "actions/upload-artifact@v4"
304-
if: ${{ !cancelled() && inputs.code-coverage == true }}
324+
uses: "actions/upload-artifact@v6"
325+
if: ${{ !cancelled() && steps.coverage-config.outputs.coverage-enabled == 'true' }}
305326
with:
306327
name: "coverage-report"
307-
path: "/var/www/glpi/plugins/${{ inputs.plugin-key }}/cobertura.xml"
328+
path: |
329+
/var/www/glpi/plugins/${{ inputs.plugin-key }}/cobertura.xml
330+
/var/www/glpi/plugins/${{ inputs.plugin-key }}/.glpi-coverage.json
331+
include-hidden-files: true
308332
overwrite: true
309333
- name: "Jest"
310334
if: ${{ !cancelled() && hashFiles(format('{0}/jest.config.js', inputs.plugin-key)) != '' }}
Lines changed: 94 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,94 @@
1+
name: "Coverage refresh"
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
plugin-key:
7+
required: true
8+
type: string
9+
workflow-name:
10+
description: "Name of the CI workflow to trigger for coverage refresh. Must match the 'name' field in the plugin's CI workflow file."
11+
required: false
12+
type: string
13+
default: "Continuous integration"
14+
15+
jobs:
16+
check-and-refresh:
17+
name: "Check and refresh coverage artifact"
18+
runs-on: "ubuntu-latest"
19+
steps:
20+
- name: "Checkout"
21+
uses: "actions/checkout@v6"
22+
with:
23+
sparse-checkout: ".glpi-coverage.json"
24+
25+
- name: "Check coverage configuration"
26+
id: "coverage-config"
27+
run: |
28+
CONFIG_FILE=".glpi-coverage.json"
29+
if [[ ! -f "$CONFIG_FILE" ]]; then
30+
echo "ℹ️ No $CONFIG_FILE found, skipping coverage refresh."
31+
echo "skip=true" >> $GITHUB_OUTPUT
32+
exit 0
33+
fi
34+
35+
ENABLED=$(jq -r '.enabled // true' "$CONFIG_FILE")
36+
if [[ "$ENABLED" != "true" ]]; then
37+
echo "ℹ️ Code coverage is disabled via $CONFIG_FILE, skipping refresh."
38+
echo "skip=true" >> $GITHUB_OUTPUT
39+
exit 0
40+
fi
41+
42+
echo "skip=false" >> $GITHUB_OUTPUT
43+
44+
- name: "Check artifact expiry"
45+
if: steps.coverage-config.outputs.skip != 'true'
46+
id: "check-expiry"
47+
env:
48+
GH_TOKEN: ${{ github.token }}
49+
run: |
50+
echo "Checking for existing coverage artifacts..."
51+
52+
# The clearlyip action uses the naming pattern: coverage-{branch_name}
53+
DEFAULT_BRANCH="${{ github.event.repository.default_branch }}"
54+
ARTIFACT_NAME="coverage-${DEFAULT_BRANCH}"
55+
56+
# List artifacts matching the coverage pattern
57+
ARTIFACTS=$(gh api \
58+
"/repos/${{ github.repository }}/actions/artifacts?name=${ARTIFACT_NAME}&per_page=1" \
59+
--jq '.artifacts[0]' 2>/dev/null || echo "null")
60+
61+
if [[ "$ARTIFACTS" == "null" || -z "$ARTIFACTS" ]]; then
62+
echo "⚠️ No coverage artifact found. Refresh needed."
63+
echo "needs-refresh=true" >> $GITHUB_OUTPUT
64+
exit 0
65+
fi
66+
67+
EXPIRES_AT=$(echo "$ARTIFACTS" | jq -r '.expires_at // empty')
68+
if [[ -z "$EXPIRES_AT" ]]; then
69+
echo "⚠️ Could not determine artifact expiry. Refresh needed."
70+
echo "needs-refresh=true" >> $GITHUB_OUTPUT
71+
exit 0
72+
fi
73+
74+
EXPIRES_TS=$(date -d "$EXPIRES_AT" +%s)
75+
TOMORROW_TS=$(date -d "+1 day" +%s)
76+
77+
if [[ "$EXPIRES_TS" -le "$TOMORROW_TS" ]]; then
78+
echo "⏰ Coverage artifact expires at $EXPIRES_AT (within 1 day). Refresh needed."
79+
echo "needs-refresh=true" >> $GITHUB_OUTPUT
80+
else
81+
echo "✅ Coverage artifact is valid until $EXPIRES_AT. No refresh needed."
82+
echo "needs-refresh=false" >> $GITHUB_OUTPUT
83+
fi
84+
85+
- name: "Trigger CI workflow"
86+
if: steps.check-expiry.outputs.needs-refresh == 'true'
87+
env:
88+
GH_TOKEN: ${{ github.token }}
89+
run: |
90+
echo "🔄 Triggering CI workflow on default branch to refresh coverage artifact..."
91+
gh workflow run "${{ inputs.workflow-name }}" \
92+
--repo "${{ github.repository }}" \
93+
--ref "${{ github.event.repository.default_branch }}"
94+
echo "✅ Workflow dispatch triggered."
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
name: "Coverage report"
2+
3+
on:
4+
workflow_call:
5+
inputs:
6+
plugin-key:
7+
required: true
8+
type: string
9+
10+
permissions:
11+
pull-requests: write
12+
actions: read
13+
14+
jobs:
15+
coverage-report:
16+
runs-on: "ubuntu-latest"
17+
name: "Coverage report"
18+
steps:
19+
- name: "Download coverage report"
20+
uses: "actions/download-artifact@v7"
21+
with:
22+
name: "coverage-report"
23+
24+
- name: "Read coverage configuration"
25+
id: "coverage-config"
26+
run: |
27+
CONFIG_FILE=".glpi-coverage.json"
28+
if [[ ! -f "$CONFIG_FILE" ]]; then
29+
echo "⚠️ No $CONFIG_FILE found, skipping coverage report."
30+
echo "skip=true" >> $GITHUB_OUTPUT
31+
exit 0
32+
fi
33+
34+
ENABLED=$(jq -r '.enabled // true' "$CONFIG_FILE")
35+
if [[ "$ENABLED" != "true" ]]; then
36+
echo "ℹ️ Code coverage is disabled via $CONFIG_FILE"
37+
echo "skip=true" >> $GITHUB_OUTPUT
38+
exit 0
39+
fi
40+
41+
echo "skip=false" >> $GITHUB_OUTPUT
42+
echo "only-list-changed-files=$(jq -r '.only_list_changed_files // true' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
43+
echo "badge=$(jq -r '.badge // true' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
44+
echo "overall-coverage-fail-threshold=$(jq -r '.overall_coverage_fail_threshold // 0' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
45+
echo "file-coverage-error-min=$(jq -r '.file_coverage_error_min // 50' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
46+
echo "file-coverage-warning-max=$(jq -r '.file_coverage_warning_max // 75' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
47+
echo "fail-on-negative-difference=$(jq -r '.fail_on_negative_difference // false' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
48+
echo "retention-days=$(jq -r '.retention_days // 90' "$CONFIG_FILE")" >> $GITHUB_OUTPUT
49+
50+
- name: "Generate coverage report"
51+
if: steps.coverage-config.outputs.skip != 'true'
52+
uses: "clearlyip/code-coverage-report-action@v6"
53+
id: "coverage-report"
54+
with:
55+
filename: "cobertura.xml"
56+
only_list_changed_files: ${{ steps.coverage-config.outputs.only-list-changed-files }}
57+
badge: ${{ steps.coverage-config.outputs.badge }}
58+
overall_coverage_fail_threshold: ${{ steps.coverage-config.outputs.overall-coverage-fail-threshold }}
59+
file_coverage_error_min: ${{ steps.coverage-config.outputs.file-coverage-error-min }}
60+
file_coverage_warning_max: ${{ steps.coverage-config.outputs.file-coverage-warning-max }}
61+
fail_on_negative_difference: ${{ steps.coverage-config.outputs.fail-on-negative-difference }}
62+
retention_days: ${{ steps.coverage-config.outputs.retention-days }}
63+
artifact_download_workflow_names: "Continuous integration"
64+
65+
- name: "Generating Markdown report"
66+
if: github.event_name == 'pull_request' && steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != ''
67+
run: |
68+
COVERAGE="${{ steps.coverage-report.outputs.coverage }}"
69+
REPORT_FILE="code-coverage-results.md"
70+
ARTIFACT_LINK="📥 [Download coverage-report artifact](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) _(contains \`cobertura.xml\` for IDE import + config file)_"
71+
72+
# Split: keep header/badge visible, collapse the table inside <details>
73+
FIRST_TABLE_LINE=$(grep -n "^|" "$REPORT_FILE" | head -1 | cut -d: -f1)
74+
75+
if [[ -z "$FIRST_TABLE_LINE" ]]; then
76+
{
77+
cat "$REPORT_FILE"
78+
echo ""
79+
echo "$ARTIFACT_LINK"
80+
} > "${REPORT_FILE}.tmp"
81+
else
82+
{
83+
head -n "$((FIRST_TABLE_LINE - 1))" "$REPORT_FILE"
84+
echo ""
85+
echo "<details>"
86+
echo "<summary>📋 Details</summary>"
87+
echo ""
88+
tail -n "+${FIRST_TABLE_LINE}" "$REPORT_FILE"
89+
echo ""
90+
echo "$ARTIFACT_LINK"
91+
echo ""
92+
echo "</details>"
93+
} > "${REPORT_FILE}.tmp"
94+
fi
95+
96+
mv "${REPORT_FILE}.tmp" "$REPORT_FILE"
97+
98+
- name: "Add coverage PR comment"
99+
if: github.event_name == 'pull_request' && steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != ''
100+
uses: "marocchino/sticky-pull-request-comment@v2"
101+
with:
102+
header: coverage
103+
path: code-coverage-results.md

README.md

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -49,9 +49,6 @@ jobs:
4949

5050
# Optional extra services (possible values: "openldap").
5151
extra-services: "openldap"
52-
53-
# Whether to enable code coverage generation (default: false).
54-
code-coverage: true
5552
```
5653
5754
The available `glpi-version`/`php-version` combinations corresponds to the `ghcr.io/glpi-project/githubactions-glpi-apache` images tags
@@ -65,6 +62,69 @@ The `db-image` parameter is a combination of the DB server engine (`mysql`, `mar
6562
An optional `init-script` parameter can be used to define the path of an initialization script. This script will be executed with `bash`.
6663
It can be used, for instance, to install a specific PHP extension.
6764

65+
## Code coverage
66+
67+
Code coverage is automatically enabled when a `.glpi-coverage.json` configuration file is present at the root of the plugin directory.
68+
69+
If the file is not present, or if its `enabled` field is explicitly set to `false`, code coverage steps will be skipped entirely.
70+
71+
### `.glpi-coverage.json` format
72+
73+
All fields are optional. Default values are shown below:
74+
75+
```json
76+
{
77+
"enabled": true,
78+
"only_list_changed_files": true,
79+
"badge": true,
80+
"overall_coverage_fail_threshold": 0,
81+
"file_coverage_error_min": 50,
82+
"file_coverage_warning_max": 75,
83+
"fail_on_negative_difference": false,
84+
"retention_days": 90
85+
}
86+
```
87+
88+
| Field | Default | Description |
89+
|-----------------------------------|---------|-----------------------------------------------------------------------------------------------------|
90+
| `enabled` | `true` | Set to `false` to disable code coverage entirely. |
91+
| `only_list_changed_files` | `true` | Only list files changed in the PR in the coverage report. |
92+
| `badge` | `true` | Include a coverage badge in the report using shields.io. |
93+
| `overall_coverage_fail_threshold` | `0` | Fail the workflow if overall coverage is below this percentage. |
94+
| `file_coverage_error_min` | `50` | Files with coverage below this percentage are marked as error (red). |
95+
| `file_coverage_warning_max` | `75` | Files with coverage below this percentage are marked as warning (orange). Above is success (green). |
96+
| `fail_on_negative_difference` | `false` | Fail the workflow if any file coverage decreased compared to the base branch. |
97+
| `retention_days` | `90` | Number of days to retain coverage artifacts for base branch comparison. |
98+
99+
> **Tip:** To use as a reference without enabling coverage (e.g. for `glpi-empty`), create the file with `"enabled": false`.
100+
101+
### Coverage report workflow
102+
103+
The `coverage-report.yml` reusable workflow generates a PR comment with a coverage summary. It compares the coverage from the current PR against the base branch (using stored artifacts).
104+
105+
```yaml
106+
coverage-report:
107+
needs: "ci"
108+
uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-report.yml@v1"
109+
with:
110+
plugin-key: "myplugin"
111+
```
112+
113+
### Coverage refresh workflow
114+
115+
The `coverage-refresh.yml` reusable workflow ensures that the base branch coverage artifact stays available for comparison.
116+
It checks the artifact expiry date via the GitHub API and triggers the CI workflow on the default branch only if the artifact is missing or will expire within the next day.
117+
118+
It should be triggered on `schedule` events (the daily cron in the CI workflow):
119+
120+
```yaml
121+
coverage-refresh:
122+
if: github.event_name == 'schedule'
123+
uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-refresh.yml@v1"
124+
with:
125+
plugin-key: "myplugin"
126+
```
127+
68128
## Generate CI matrix
69129

70130
This workflow can be used to generate a matrix that contains the default PHP/SQL versions that are supported by the target GLPI version.
@@ -110,4 +170,10 @@ jobs:
110170
glpi-version: "${{ matrix.glpi-version }}"
111171
php-version: "${{ matrix.php-version }}"
112172
db-image: "${{ matrix.db-image }}"
173+
174+
coverage-report:
175+
needs: "ci"
176+
uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-report.yml@v1"
177+
with:
178+
plugin-key: "myplugin"
113179
```

0 commit comments

Comments
 (0)