From 8fc53e8bd31e538091852f93119015e8aeb36f2d Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Tue, 23 Dec 2025 10:39:48 +0100 Subject: [PATCH 01/10] feat: enable code coverage --- .github/workflows/continuous-integration.yml | 33 ++++++++++++++++++-- README.md | 3 ++ 2 files changed, 34 insertions(+), 2 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1698348..d60f424 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -26,6 +26,7 @@ on: default: "" skip-changelog-check: description: "Set to true to skip the CHANGELOG update check on pull requests." + code-coverage: required: false type: boolean default: false @@ -351,18 +352,46 @@ jobs: shell: "bash" run: | sudo service apache2 start + - name: "Setup coverage driver" + if: ${{ !cancelled() && inputs.code-coverage == true }} + shell: "bash" + run: | + if ! php -m | grep -q -E 'xdebug|pcov'; then + echo -e "\033[0;33mInstalling PCOV driver...\033[0m" + sudo pecl install pcov || true + fi + - name: "PHPUnit" if: ${{ !cancelled() && hashFiles(format('{0}/phpunit.xml', inputs.plugin-key)) != '' }} + env: + PCOV_ENABLED: "${{ inputs.code-coverage && '1' || '0' }}" + XDEBUG_MODE: "${{ inputs.code-coverage && 'coverage' || 'off' }}" run: | echo -e "\033[0;33mExecuting PHPUnit...\033[0m" + PHPUNIT_FLAGS="--colors=always" + PHP_CMD="php" + + if [[ "${{ inputs.code-coverage }}" == "true" ]]; then + PHPUNIT_FLAGS="$PHPUNIT_FLAGS --coverage-text --coverage-cobertura=cobertura.xml" + # Explicitly load PCOV if needed + PHP_CMD="php -d extension=pcov.so" + fi + if [[ -f "vendor/bin/phpunit" ]]; then - vendor/bin/phpunit --colors=always + $PHP_CMD vendor/bin/phpunit $PHPUNIT_FLAGS elif [[ -f "../../vendor/bin/phpunit" ]]; then - ../../vendor/bin/phpunit --colors=always + $PHP_CMD ../../vendor/bin/phpunit $PHPUNIT_FLAGS else echo -e "\033[0;31mPHPUnit binary not found!\033[0m" exit 1 fi + - name: "Upload coverage report" + uses: "actions/upload-artifact@v4" + if: ${{ !cancelled() && inputs.code-coverage == true }} + with: + name: "coverage-report" + path: "/var/www/glpi/plugins/${{ inputs.plugin-key }}/cobertura.xml" + overwrite: true - name: "Jest" if: ${{ !cancelled() && hashFiles(format('{0}/jest.config.js', inputs.plugin-key)) != '' }} run: | diff --git a/README.md b/README.md index 45a6272..d0c2dd0 100644 --- a/README.md +++ b/README.md @@ -52,6 +52,9 @@ jobs: # Set to true to skip the CHANGELOG update check on pull requests. skip-changelog-check: true + + # Whether to enable code coverage generation (default: false). + code-coverage: true ``` The available `glpi-version`/`php-version` combinations corresponds to the `ghcr.io/glpi-project/githubactions-glpi-apache` images tags From 9cccaf4e1273241764fbadc4401988eca3e19612 Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Wed, 31 Dec 2025 11:07:19 +0100 Subject: [PATCH 02/10] chore: increase default version --- README.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index d0c2dd0..ec4221a 100644 --- a/README.md +++ b/README.md @@ -36,10 +36,10 @@ jobs: plugin-key: "myplugin" # The version of GLPI on which to run the tests. - glpi-version: "10.0.x" + glpi-version: "11.0.x" # The version of PHP on which to run the tests. - php-version: "8.1" + php-version: "8.2" # The database docker image on which to run the tests. db-image: "mariadb:11.4" From 9250bfb2c6a68ec9ee9b415319898e8c6c03636a Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Thu, 19 Feb 2026 14:49:08 +0100 Subject: [PATCH 03/10] feat: Better coverage report with comparaison and config file --- .github/workflows/continuous-integration.yml | 45 ++++++-- .github/workflows/coverage-refresh.yml | 94 +++++++++++++++++ .github/workflows/coverage-report.yml | 103 +++++++++++++++++++ README.md | 75 ++++++++++++++ 4 files changed, 309 insertions(+), 8 deletions(-) create mode 100644 .github/workflows/coverage-refresh.yml create mode 100644 .github/workflows/coverage-report.yml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index d60f424..0220dd8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -70,6 +70,26 @@ jobs: uses: "actions/checkout@v6" with: path: "${{ inputs.plugin-key }}" + - name: "Detect coverage configuration" + id: "coverage-config" + # Use default `bash` shell with `github-actions-runner` user + shell: "bash" + working-directory: "${{ github.workspace }}/${{ inputs.plugin-key }}" + run: | + CONFIG_FILE=".glpi-coverage.json" + if [[ -f "$CONFIG_FILE" ]]; then + ENABLED=$(jq -r '.enabled // true' "$CONFIG_FILE") + if [[ "$ENABLED" != "true" ]]; then + echo "coverage-enabled=false" >> $GITHUB_OUTPUT + echo "ℹ️ Code coverage is disabled via $CONFIG_FILE" + exit 0 + fi + echo "coverage-enabled=true" >> $GITHUB_OUTPUT + else + echo "coverage-enabled=false" >> $GITHUB_OUTPUT + echo "ℹ️ No $CONFIG_FILE found, code coverage is disabled." + exit 0 + fi - name: "Execute init script" if: ${{ inputs.init-script != '' }} # Use default `bash` shell with `github-actions-runner` user @@ -353,7 +373,7 @@ jobs: run: | sudo service apache2 start - name: "Setup coverage driver" - if: ${{ !cancelled() && inputs.code-coverage == true }} + if: ${{ !cancelled() && steps.coverage-config.outputs.coverage-enabled == 'true' }} shell: "bash" run: | if ! php -m | grep -q -E 'xdebug|pcov'; then @@ -364,15 +384,15 @@ jobs: - name: "PHPUnit" if: ${{ !cancelled() && hashFiles(format('{0}/phpunit.xml', inputs.plugin-key)) != '' }} env: - PCOV_ENABLED: "${{ inputs.code-coverage && '1' || '0' }}" - XDEBUG_MODE: "${{ inputs.code-coverage && 'coverage' || 'off' }}" + PCOV_ENABLED: "${{ steps.coverage-config.outputs.coverage-enabled == 'true' && '1' || '0' }}" + XDEBUG_MODE: "${{ steps.coverage-config.outputs.coverage-enabled == 'true' && 'coverage' || 'off' }}" run: | echo -e "\033[0;33mExecuting PHPUnit...\033[0m" PHPUNIT_FLAGS="--colors=always" PHP_CMD="php" - if [[ "${{ inputs.code-coverage }}" == "true" ]]; then - PHPUNIT_FLAGS="$PHPUNIT_FLAGS --coverage-text --coverage-cobertura=cobertura.xml" + if [[ "${{ steps.coverage-config.outputs.coverage-enabled }}" == "true" ]]; then + PHPUNIT_FLAGS="$PHPUNIT_FLAGS --coverage-text --coverage-cobertura=cobertura.xml --coverage-clover=clover.xml" # Explicitly load PCOV if needed PHP_CMD="php -d extension=pcov.so" fi @@ -385,12 +405,21 @@ jobs: echo -e "\033[0;31mPHPUnit binary not found!\033[0m" exit 1 fi + - name: "Fix coverage paths for IDE import" + if: ${{ !cancelled() && steps.coverage-config.outputs.coverage-enabled == 'true' }} + run: | + echo "Sanitizing paths in clover.xml..." + sed -i 's|/var/www/glpi/plugins/${{ inputs.plugin-key }}/|plugins/${{ inputs.plugin-key }}/|g' clover.xml - name: "Upload coverage report" - uses: "actions/upload-artifact@v4" - if: ${{ !cancelled() && inputs.code-coverage == true }} + uses: "actions/upload-artifact@v6" + if: ${{ !cancelled() && steps.coverage-config.outputs.coverage-enabled == 'true' }} with: name: "coverage-report" - path: "/var/www/glpi/plugins/${{ inputs.plugin-key }}/cobertura.xml" + path: | + /var/www/glpi/plugins/${{ inputs.plugin-key }}/cobertura.xml + /var/www/glpi/plugins/${{ inputs.plugin-key }}/clover.xml + /var/www/glpi/plugins/${{ inputs.plugin-key }}/.glpi-coverage.json + include-hidden-files: true overwrite: true - name: "Jest" if: ${{ !cancelled() && hashFiles(format('{0}/jest.config.js', inputs.plugin-key)) != '' }} diff --git a/.github/workflows/coverage-refresh.yml b/.github/workflows/coverage-refresh.yml new file mode 100644 index 0000000..6ee8e6c --- /dev/null +++ b/.github/workflows/coverage-refresh.yml @@ -0,0 +1,94 @@ +name: "Coverage refresh" + +on: + workflow_call: + inputs: + plugin-key: + required: true + type: string + workflow-name: + description: "Name of the CI workflow to trigger for coverage refresh. Must match the 'name' field in the plugin's CI workflow file." + required: false + type: string + default: "Continuous integration" + +jobs: + check-and-refresh: + name: "Check and refresh coverage artifact" + runs-on: "ubuntu-latest" + steps: + - name: "Checkout" + uses: "actions/checkout@v6" + with: + sparse-checkout: ".glpi-coverage.json" + + - name: "Check coverage configuration" + id: "coverage-config" + run: | + CONFIG_FILE=".glpi-coverage.json" + if [[ ! -f "$CONFIG_FILE" ]]; then + echo "ℹ️ No $CONFIG_FILE found, skipping coverage refresh." + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + + ENABLED=$(jq -r '.enabled // true' "$CONFIG_FILE") + if [[ "$ENABLED" != "true" ]]; then + echo "ℹ️ Code coverage is disabled via $CONFIG_FILE, skipping refresh." + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "skip=false" >> $GITHUB_OUTPUT + + - name: "Check artifact expiry" + if: steps.coverage-config.outputs.skip != 'true' + id: "check-expiry" + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "Checking for existing coverage artifacts..." + + # The clearlyip action uses the naming pattern: coverage-{branch_name} + DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" + ARTIFACT_NAME="coverage-${DEFAULT_BRANCH}" + + # List artifacts matching the coverage pattern + ARTIFACTS=$(gh api \ + "/repos/${{ github.repository }}/actions/artifacts?name=${ARTIFACT_NAME}&per_page=1" \ + --jq '.artifacts[0]' 2>/dev/null || echo "null") + + if [[ "$ARTIFACTS" == "null" || -z "$ARTIFACTS" ]]; then + echo "⚠️ No coverage artifact found. Refresh needed." + echo "needs-refresh=true" >> $GITHUB_OUTPUT + exit 0 + fi + + EXPIRES_AT=$(echo "$ARTIFACTS" | jq -r '.expires_at // empty') + if [[ -z "$EXPIRES_AT" ]]; then + echo "⚠️ Could not determine artifact expiry. Refresh needed." + echo "needs-refresh=true" >> $GITHUB_OUTPUT + exit 0 + fi + + EXPIRES_TS=$(date -d "$EXPIRES_AT" +%s) + TOMORROW_TS=$(date -d "+1 day" +%s) + + if [[ "$EXPIRES_TS" -le "$TOMORROW_TS" ]]; then + echo "⏰ Coverage artifact expires at $EXPIRES_AT (within 1 day). Refresh needed." + echo "needs-refresh=true" >> $GITHUB_OUTPUT + else + echo "✅ Coverage artifact is valid until $EXPIRES_AT. No refresh needed." + echo "needs-refresh=false" >> $GITHUB_OUTPUT + fi + + - name: "Trigger CI workflow" + if: steps.check-expiry.outputs.needs-refresh == 'true' + env: + GH_TOKEN: ${{ github.token }} + run: | + echo "🔄 Triggering CI workflow on default branch to refresh coverage artifact..." + gh workflow run "${{ inputs.workflow-name }}" \ + --repo "${{ github.repository }}" \ + --ref "${{ github.event.repository.default_branch }}" + echo "✅ Workflow dispatch triggered." diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml new file mode 100644 index 0000000..005c51f --- /dev/null +++ b/.github/workflows/coverage-report.yml @@ -0,0 +1,103 @@ +name: "Coverage report" + +on: + workflow_call: + inputs: + plugin-key: + required: true + type: string + +permissions: + pull-requests: write + actions: read + +jobs: + coverage-report: + runs-on: "ubuntu-latest" + name: "Coverage report" + steps: + - name: "Download coverage report" + uses: "actions/download-artifact@v7" + with: + name: "coverage-report" + + - name: "Read coverage configuration" + id: "coverage-config" + run: | + CONFIG_FILE=".glpi-coverage.json" + if [[ ! -f "$CONFIG_FILE" ]]; then + echo "⚠️ No $CONFIG_FILE found, skipping coverage report." + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + + ENABLED=$(jq -r '.enabled // true' "$CONFIG_FILE") + if [[ "$ENABLED" != "true" ]]; then + echo "ℹ️ Code coverage is disabled via $CONFIG_FILE" + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + + echo "skip=false" >> $GITHUB_OUTPUT + echo "only-list-changed-files=$(jq -r '.only_list_changed_files // true' "$CONFIG_FILE")" >> $GITHUB_OUTPUT + echo "badge=$(jq -r '.badge // true' "$CONFIG_FILE")" >> $GITHUB_OUTPUT + echo "overall-coverage-fail-threshold=$(jq -r '.overall_coverage_fail_threshold // 0' "$CONFIG_FILE")" >> $GITHUB_OUTPUT + echo "file-coverage-error-min=$(jq -r '.file_coverage_error_min // 50' "$CONFIG_FILE")" >> $GITHUB_OUTPUT + echo "file-coverage-warning-max=$(jq -r '.file_coverage_warning_max // 75' "$CONFIG_FILE")" >> $GITHUB_OUTPUT + echo "fail-on-negative-difference=$(jq -r '.fail_on_negative_difference // false' "$CONFIG_FILE")" >> $GITHUB_OUTPUT + echo "retention-days=$(jq -r '.retention_days // 90' "$CONFIG_FILE")" >> $GITHUB_OUTPUT + + - name: "Generate coverage report" + if: steps.coverage-config.outputs.skip != 'true' + uses: "clearlyip/code-coverage-report-action@v6" + id: "coverage-report" + with: + filename: "cobertura.xml" + only_list_changed_files: ${{ steps.coverage-config.outputs.only-list-changed-files }} + badge: ${{ steps.coverage-config.outputs.badge }} + overall_coverage_fail_threshold: ${{ steps.coverage-config.outputs.overall-coverage-fail-threshold }} + file_coverage_error_min: ${{ steps.coverage-config.outputs.file-coverage-error-min }} + file_coverage_warning_max: ${{ steps.coverage-config.outputs.file-coverage-warning-max }} + fail_on_negative_difference: ${{ steps.coverage-config.outputs.fail-on-negative-difference }} + retention_days: ${{ steps.coverage-config.outputs.retention-days }} + artifact_download_workflow_names: "Continuous integration" + + - name: "Generating Markdown report" + if: github.event_name == 'pull_request' && steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' + run: | + COVERAGE="${{ steps.coverage-report.outputs.coverage }}" + REPORT_FILE="code-coverage-results.md" + ARTIFACT_LINK="📥 [Download coverage-report artifact](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) _(contains \`cobertura.xml\` for IDE import + config file)_" + + # Split: keep header/badge visible, collapse the table inside
+ FIRST_TABLE_LINE=$(grep -n "^|" "$REPORT_FILE" | head -1 | cut -d: -f1) + + if [[ -z "$FIRST_TABLE_LINE" ]]; then + { + cat "$REPORT_FILE" + echo "" + echo "$ARTIFACT_LINK" + } > "${REPORT_FILE}.tmp" + else + { + head -n "$((FIRST_TABLE_LINE - 1))" "$REPORT_FILE" + echo "" + echo "
" + echo "📋 Details" + echo "" + tail -n "+${FIRST_TABLE_LINE}" "$REPORT_FILE" + echo "" + echo "$ARTIFACT_LINK" + echo "" + echo "
" + } > "${REPORT_FILE}.tmp" + fi + + mv "${REPORT_FILE}.tmp" "$REPORT_FILE" + + - name: "Add coverage PR comment" + if: github.event_name == 'pull_request' && steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' + uses: "marocchino/sticky-pull-request-comment@v2" + with: + header: coverage + path: code-coverage-results.md diff --git a/README.md b/README.md index ec4221a..883e985 100644 --- a/README.md +++ b/README.md @@ -72,6 +72,75 @@ On pull requests, the workflow checks that the `CHANGELOG` file has been updated On pull requests that modify `plugin.xml` or `.xml`, the workflow also validates that all URLs declared in the file are reachable. URLs inside `` tags that are newly introduced by the PR only produce a warning (the release archive may not be published yet), while all other invalid URLs fail the check. +## Code coverage + +Code coverage is automatically enabled when a `.glpi-coverage.json` configuration file is present at the root of the plugin directory. + +If the file is not present, or if its `enabled` field is explicitly set to `false`, code coverage steps will be skipped entirely. + +### `.glpi-coverage.json` format + +All fields are optional. Default values are shown below: + +```json +{ + "enabled": true, + "only_list_changed_files": true, + "badge": true, + "overall_coverage_fail_threshold": 0, + "file_coverage_error_min": 50, + "file_coverage_warning_max": 75, + "fail_on_negative_difference": false, + "retention_days": 90 +} +``` + +| Field | Default | Description | +|-----------------------------------|---------|-----------------------------------------------------------------------------------------------------| +| `enabled` | `true` | Set to `false` to disable code coverage entirely. | +| `only_list_changed_files` | `true` | Only list files changed in the PR in the coverage report. | +| `badge` | `true` | Include a coverage badge in the report using shields.io. | +| `overall_coverage_fail_threshold` | `0` | Fail the workflow if overall coverage is below this percentage. | +| `file_coverage_error_min` | `50` | Files with coverage below this percentage are marked as error (red). | +| `file_coverage_warning_max` | `75` | Files with coverage below this percentage are marked as warning (orange). Above is success (green). | +| `fail_on_negative_difference` | `false` | Fail the workflow if any file coverage decreased compared to the base branch. | +| `retention_days` | `90` | Number of days to retain coverage artifacts for base branch comparison. | + +> **Tip:** To use as a reference without enabling coverage (e.g. for `glpi-empty`), create the file with `"enabled": false`. + +### IDE Integration + +The workflow produces a `coverage-report` artifact containing: +- `clover.xml`: Use this file to import coverage into PhpStorm or other IDEs. Paths are automatically sanitized to match `plugins//`. +- `cobertura.xml`: Used for the PR comment report. + +### Coverage report workflow + +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). + +```yaml + coverage-report: + needs: "ci" + uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-report.yml@v1" + with: + plugin-key: "myplugin" +``` + +### Coverage refresh workflow + +The `coverage-refresh.yml` reusable workflow ensures that the base branch coverage artifact stays available for comparison. +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. + +It should be triggered on `schedule` events (the daily cron in the CI workflow): + +```yaml + coverage-refresh: + if: github.event_name == 'schedule' + uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-refresh.yml@v1" + with: + plugin-key: "myplugin" +``` + ## Generate CI matrix This workflow can be used to generate a matrix that contains the default PHP/SQL versions that are supported by the target GLPI version. @@ -117,4 +186,10 @@ jobs: glpi-version: "${{ matrix.glpi-version }}" php-version: "${{ matrix.php-version }}" db-image: "${{ matrix.db-image }}" + + coverage-report: + needs: "ci" + uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-report.yml@v1" + with: + plugin-key: "myplugin" ``` From e71cfb25aaab4226e4a5c3d948b300f339d1ceaf Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Mon, 23 Feb 2026 10:16:30 +0100 Subject: [PATCH 04/10] feat: Better coverage report with comparaison and config file --- .github/workflows/coverage-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml index 005c51f..2075139 100644 --- a/.github/workflows/coverage-report.yml +++ b/.github/workflows/coverage-report.yml @@ -67,7 +67,7 @@ jobs: run: | COVERAGE="${{ steps.coverage-report.outputs.coverage }}" REPORT_FILE="code-coverage-results.md" - ARTIFACT_LINK="📥 [Download coverage-report artifact](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) _(contains \`cobertura.xml\` for IDE import + config file)_" + ARTIFACT_LINK="📥 [Download coverage-report artifact](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) _(contains \`clover.xml\` for IDE import + config file)_" # Split: keep header/badge visible, collapse the table inside
FIRST_TABLE_LINE=$(grep -n "^|" "$REPORT_FILE" | head -1 | cut -d: -f1) From 906562ad5a38ccae8f67557c2ab4e81b232f7521 Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Mon, 23 Feb 2026 15:01:08 +0100 Subject: [PATCH 05/10] Code coverage is only run when targeting main branch --- .github/workflows/continuous-integration.yml | 6 ++++++ .github/workflows/coverage-report.yml | 16 ++++++++++++++++ 2 files changed, 22 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 0220dd8..1ff5f50 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -76,6 +76,12 @@ jobs: shell: "bash" working-directory: "${{ github.workspace }}/${{ inputs.plugin-key }}" run: | + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" != "${{ github.event.repository.default_branch }}" ]]; then + echo "coverage-enabled=false" >> $GITHUB_OUTPUT + echo "ℹ️ Code coverage is disabled for pull requests targeting non-default branch (${{ github.base_ref }})." + exit 0 + fi + CONFIG_FILE=".glpi-coverage.json" if [[ -f "$CONFIG_FILE" ]]; then ENABLED=$(jq -r '.enabled // true' "$CONFIG_FILE") diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml index 2075139..39ebed3 100644 --- a/.github/workflows/coverage-report.yml +++ b/.github/workflows/coverage-report.yml @@ -16,7 +16,18 @@ jobs: runs-on: "ubuntu-latest" name: "Coverage report" steps: + - name: "Check target branch" + id: "check-branch" + run: | + if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" != "${{ github.event.repository.default_branch }}" ]]; then + echo "ℹ️ Code coverage is disabled for pull requests targeting non-default branch (${{ github.base_ref }})." + echo "skip=true" >> $GITHUB_OUTPUT + else + echo "skip=false" >> $GITHUB_OUTPUT + fi + - name: "Download coverage report" + if: steps.check-branch.outputs.skip != 'true' uses: "actions/download-artifact@v7" with: name: "coverage-report" @@ -24,6 +35,11 @@ jobs: - name: "Read coverage configuration" id: "coverage-config" run: | + if [[ "${{ steps.check-branch.outputs.skip }}" == "true" ]]; then + echo "skip=true" >> $GITHUB_OUTPUT + exit 0 + fi + CONFIG_FILE=".glpi-coverage.json" if [[ ! -f "$CONFIG_FILE" ]]; then echo "⚠️ No $CONFIG_FILE found, skipping coverage report." From 29135f6636abe8bbd5465ae6f34767b0e1fdf193 Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Thu, 21 May 2026 09:51:07 +0200 Subject: [PATCH 06/10] Update after cedric returns --- .github/workflows/continuous-integration.yml | 16 +--- .github/workflows/coverage-refresh.yml | 94 -------------------- .github/workflows/coverage-report.yml | 14 ++- README.md | 38 ++++---- 4 files changed, 27 insertions(+), 135 deletions(-) delete mode 100644 .github/workflows/coverage-refresh.yml diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1ff5f50..436d351 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -378,15 +378,6 @@ jobs: shell: "bash" run: | sudo service apache2 start - - name: "Setup coverage driver" - if: ${{ !cancelled() && steps.coverage-config.outputs.coverage-enabled == 'true' }} - shell: "bash" - run: | - if ! php -m | grep -q -E 'xdebug|pcov'; then - echo -e "\033[0;33mInstalling PCOV driver...\033[0m" - sudo pecl install pcov || true - fi - - name: "PHPUnit" if: ${{ !cancelled() && hashFiles(format('{0}/phpunit.xml', inputs.plugin-key)) != '' }} env: @@ -395,18 +386,15 @@ jobs: run: | echo -e "\033[0;33mExecuting PHPUnit...\033[0m" PHPUNIT_FLAGS="--colors=always" - PHP_CMD="php" if [[ "${{ steps.coverage-config.outputs.coverage-enabled }}" == "true" ]]; then PHPUNIT_FLAGS="$PHPUNIT_FLAGS --coverage-text --coverage-cobertura=cobertura.xml --coverage-clover=clover.xml" - # Explicitly load PCOV if needed - PHP_CMD="php -d extension=pcov.so" fi if [[ -f "vendor/bin/phpunit" ]]; then - $PHP_CMD vendor/bin/phpunit $PHPUNIT_FLAGS + php vendor/bin/phpunit $PHPUNIT_FLAGS elif [[ -f "../../vendor/bin/phpunit" ]]; then - $PHP_CMD ../../vendor/bin/phpunit $PHPUNIT_FLAGS + php ../../vendor/bin/phpunit $PHPUNIT_FLAGS else echo -e "\033[0;31mPHPUnit binary not found!\033[0m" exit 1 diff --git a/.github/workflows/coverage-refresh.yml b/.github/workflows/coverage-refresh.yml deleted file mode 100644 index 6ee8e6c..0000000 --- a/.github/workflows/coverage-refresh.yml +++ /dev/null @@ -1,94 +0,0 @@ -name: "Coverage refresh" - -on: - workflow_call: - inputs: - plugin-key: - required: true - type: string - workflow-name: - description: "Name of the CI workflow to trigger for coverage refresh. Must match the 'name' field in the plugin's CI workflow file." - required: false - type: string - default: "Continuous integration" - -jobs: - check-and-refresh: - name: "Check and refresh coverage artifact" - runs-on: "ubuntu-latest" - steps: - - name: "Checkout" - uses: "actions/checkout@v6" - with: - sparse-checkout: ".glpi-coverage.json" - - - name: "Check coverage configuration" - id: "coverage-config" - run: | - CONFIG_FILE=".glpi-coverage.json" - if [[ ! -f "$CONFIG_FILE" ]]; then - echo "ℹ️ No $CONFIG_FILE found, skipping coverage refresh." - echo "skip=true" >> $GITHUB_OUTPUT - exit 0 - fi - - ENABLED=$(jq -r '.enabled // true' "$CONFIG_FILE") - if [[ "$ENABLED" != "true" ]]; then - echo "ℹ️ Code coverage is disabled via $CONFIG_FILE, skipping refresh." - echo "skip=true" >> $GITHUB_OUTPUT - exit 0 - fi - - echo "skip=false" >> $GITHUB_OUTPUT - - - name: "Check artifact expiry" - if: steps.coverage-config.outputs.skip != 'true' - id: "check-expiry" - env: - GH_TOKEN: ${{ github.token }} - run: | - echo "Checking for existing coverage artifacts..." - - # The clearlyip action uses the naming pattern: coverage-{branch_name} - DEFAULT_BRANCH="${{ github.event.repository.default_branch }}" - ARTIFACT_NAME="coverage-${DEFAULT_BRANCH}" - - # List artifacts matching the coverage pattern - ARTIFACTS=$(gh api \ - "/repos/${{ github.repository }}/actions/artifacts?name=${ARTIFACT_NAME}&per_page=1" \ - --jq '.artifacts[0]' 2>/dev/null || echo "null") - - if [[ "$ARTIFACTS" == "null" || -z "$ARTIFACTS" ]]; then - echo "⚠️ No coverage artifact found. Refresh needed." - echo "needs-refresh=true" >> $GITHUB_OUTPUT - exit 0 - fi - - EXPIRES_AT=$(echo "$ARTIFACTS" | jq -r '.expires_at // empty') - if [[ -z "$EXPIRES_AT" ]]; then - echo "⚠️ Could not determine artifact expiry. Refresh needed." - echo "needs-refresh=true" >> $GITHUB_OUTPUT - exit 0 - fi - - EXPIRES_TS=$(date -d "$EXPIRES_AT" +%s) - TOMORROW_TS=$(date -d "+1 day" +%s) - - if [[ "$EXPIRES_TS" -le "$TOMORROW_TS" ]]; then - echo "⏰ Coverage artifact expires at $EXPIRES_AT (within 1 day). Refresh needed." - echo "needs-refresh=true" >> $GITHUB_OUTPUT - else - echo "✅ Coverage artifact is valid until $EXPIRES_AT. No refresh needed." - echo "needs-refresh=false" >> $GITHUB_OUTPUT - fi - - - name: "Trigger CI workflow" - if: steps.check-expiry.outputs.needs-refresh == 'true' - env: - GH_TOKEN: ${{ github.token }} - run: | - echo "🔄 Triggering CI workflow on default branch to refresh coverage artifact..." - gh workflow run "${{ inputs.workflow-name }}" \ - --repo "${{ github.repository }}" \ - --ref "${{ github.event.repository.default_branch }}" - echo "✅ Workflow dispatch triggered." diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml index 39ebed3..2431360 100644 --- a/.github/workflows/coverage-report.yml +++ b/.github/workflows/coverage-report.yml @@ -6,6 +6,11 @@ on: plugin-key: required: true type: string + workflow-name: + description: "Name of the CI workflow that produces the coverage artifact." + required: false + type: string + default: "Continuous integration" permissions: pull-requests: write @@ -15,11 +20,12 @@ jobs: coverage-report: runs-on: "ubuntu-latest" name: "Coverage report" + if: github.event_name == 'pull_request' steps: - name: "Check target branch" id: "check-branch" run: | - if [[ "${{ github.event_name }}" == "pull_request" && "${{ github.base_ref }}" != "${{ github.event.repository.default_branch }}" ]]; then + if [[ "${{ github.base_ref }}" != "${{ github.event.repository.default_branch }}" ]]; then echo "ℹ️ Code coverage is disabled for pull requests targeting non-default branch (${{ github.base_ref }})." echo "skip=true" >> $GITHUB_OUTPUT else @@ -76,10 +82,10 @@ jobs: file_coverage_warning_max: ${{ steps.coverage-config.outputs.file-coverage-warning-max }} fail_on_negative_difference: ${{ steps.coverage-config.outputs.fail-on-negative-difference }} retention_days: ${{ steps.coverage-config.outputs.retention-days }} - artifact_download_workflow_names: "Continuous integration" + artifact_download_workflow_names: "${{ inputs.workflow-name }}" - name: "Generating Markdown report" - if: github.event_name == 'pull_request' && steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' + if: steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' run: | COVERAGE="${{ steps.coverage-report.outputs.coverage }}" REPORT_FILE="code-coverage-results.md" @@ -112,7 +118,7 @@ jobs: mv "${REPORT_FILE}.tmp" "$REPORT_FILE" - name: "Add coverage PR comment" - if: github.event_name == 'pull_request' && steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' + if: steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' uses: "marocchino/sticky-pull-request-comment@v2" with: header: coverage diff --git a/README.md b/README.md index 883e985..d9951ba 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ The available `glpi-version`/`php-version` combinations corresponds to the `ghcr that can be found [here](https://github.com/orgs/glpi-project/packages/container/githubactions-glpi-apache/versions?filters%5Bversion_type%5D=tagged). The `db-image` parameter is a combination of the DB server engine (`mysql`, `mariadb` or `percona`) and the server version. + - MariaDB available versions are listed [here](https://github.com/orgs/glpi-project/packages/container/githubactions-mariadb/versions?filters%5Bversion_type%5D=tagged) - MySQL available versions are listed [here](https://github.com/orgs/glpi-project/packages/container/githubactions-mysql/versions?filters%5Bversion_type%5D=tagged). - Percona available versions are listed [here](https://github.com/orgs/glpi-project/packages/container/githubactions-percona/versions?filters%5Bversion_type%5D=tagged). @@ -84,14 +85,14 @@ All fields are optional. Default values are shown below: ```json { - "enabled": true, - "only_list_changed_files": true, - "badge": true, - "overall_coverage_fail_threshold": 0, - "file_coverage_error_min": 50, - "file_coverage_warning_max": 75, - "fail_on_negative_difference": false, - "retention_days": 90 + "enabled": true, + "only_list_changed_files": true, + "badge": true, + "overall_coverage_fail_threshold": 0, + "file_coverage_error_min": 50, + "file_coverage_warning_max": 75, + "fail_on_negative_difference": false, + "retention_days": 90 } ``` @@ -111,6 +112,7 @@ All fields are optional. Default values are shown below: ### IDE Integration The workflow produces a `coverage-report` artifact containing: + - `clover.xml`: Use this file to import coverage into PhpStorm or other IDEs. Paths are automatically sanitized to match `plugins//`. - `cobertura.xml`: Used for the PR comment report. @@ -126,25 +128,14 @@ The `coverage-report.yml` reusable workflow generates a PR comment with a covera plugin-key: "myplugin" ``` -### Coverage refresh workflow - -The `coverage-refresh.yml` reusable workflow ensures that the base branch coverage artifact stays available for comparison. -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. - -It should be triggered on `schedule` events (the daily cron in the CI workflow): - -```yaml - coverage-refresh: - if: github.event_name == 'schedule' - uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-refresh.yml@v1" - with: - plugin-key: "myplugin" -``` +> **Note:** For base branch comparison to work, the CI workflow must run on every push to the default branch so that an up-to-date coverage artifact is always available. +> +> A scheduled CI run (e.g., daily cron) is also recommended to ensure the artifact is regenerated before it expires. ## Generate CI matrix This workflow can be used to generate a matrix that contains the default PHP/SQL versions that are supported by the target GLPI version. -You can use it in combination with the `Continuous Integration` workflow, as shown in the example below. +You can use it in combination with the `Continuous Integration` and the `Coverage report` workflows, as shown in the example below. ```yaml name: "Continuous integration" @@ -188,6 +179,7 @@ jobs: db-image: "${{ matrix.db-image }}" coverage-report: + if: github.event_name == 'pull_request' needs: "ci" uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-report.yml@v1" with: From cf8c318430af7d6dcd34ae0c4b35ff45a1c116be Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Thu, 21 May 2026 09:59:40 +0200 Subject: [PATCH 07/10] Rebase conflict --- .github/workflows/continuous-integration.yml | 4 ---- 1 file changed, 4 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 436d351..1ef69cb 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -26,10 +26,6 @@ on: default: "" skip-changelog-check: description: "Set to true to skip the CHANGELOG update check on pull requests." - code-coverage: - required: false - type: boolean - default: false jobs: ci: From c7da20f8e11ce94f4432e92d449a1f21a35ce83f Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Thu, 21 May 2026 10:00:06 +0200 Subject: [PATCH 08/10] Update action version --- .github/workflows/coverage-report.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml index 2431360..a49fdd6 100644 --- a/.github/workflows/coverage-report.yml +++ b/.github/workflows/coverage-report.yml @@ -119,7 +119,7 @@ jobs: - name: "Add coverage PR comment" if: steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' - uses: "marocchino/sticky-pull-request-comment@v2" + uses: "marocchino/sticky-pull-request-comment@v3" with: header: coverage path: code-coverage-results.md From 7bbb0dd247041fd970d565072eeb9bef9c22f086 Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Thu, 21 May 2026 10:00:48 +0200 Subject: [PATCH 09/10] Rebase conflict --- .github/workflows/continuous-integration.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 1ef69cb..640e216 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -26,6 +26,9 @@ on: default: "" skip-changelog-check: description: "Set to true to skip the CHANGELOG update check on pull requests." + required: false + type: boolean + default: false jobs: ci: From c00dcc37d68d83adad1839183d4d80d30872fdfa Mon Sep 17 00:00:00 2001 From: Benoit VIGNAL Date: Wed, 3 Jun 2026 10:47:24 +0200 Subject: [PATCH 10/10] PCOV can't be enabled via env var --- .github/workflows/continuous-integration.yml | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 640e216..aa167a8 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -379,21 +379,22 @@ jobs: sudo service apache2 start - name: "PHPUnit" if: ${{ !cancelled() && hashFiles(format('{0}/phpunit.xml', inputs.plugin-key)) != '' }} - env: - PCOV_ENABLED: "${{ steps.coverage-config.outputs.coverage-enabled == 'true' && '1' || '0' }}" - XDEBUG_MODE: "${{ steps.coverage-config.outputs.coverage-enabled == 'true' && 'coverage' || 'off' }}" run: | echo -e "\033[0;33mExecuting PHPUnit...\033[0m" PHPUNIT_FLAGS="--colors=always" + PHP_FLAGS="" if [[ "${{ steps.coverage-config.outputs.coverage-enabled }}" == "true" ]]; then PHPUNIT_FLAGS="$PHPUNIT_FLAGS --coverage-text --coverage-cobertura=cobertura.xml --coverage-clover=clover.xml" + # pcov is installed in the image but disabled by default (pcov.enabled=0). + # Enabling it and scoping it to only the plugin. No need to run coverage on the rest of GLPI code. + PHP_FLAGS="-d pcov.enabled=1 -d pcov.directory=." fi if [[ -f "vendor/bin/phpunit" ]]; then - php vendor/bin/phpunit $PHPUNIT_FLAGS + php $PHP_FLAGS vendor/bin/phpunit $PHPUNIT_FLAGS elif [[ -f "../../vendor/bin/phpunit" ]]; then - php ../../vendor/bin/phpunit $PHPUNIT_FLAGS + php $PHP_FLAGS ../../vendor/bin/phpunit $PHPUNIT_FLAGS else echo -e "\033[0;31mPHPUnit binary not found!\033[0m" exit 1