diff --git a/.github/workflows/continuous-integration.yml b/.github/workflows/continuous-integration.yml index 9a6994a..b50b94b 100644 --- a/.github/workflows/continuous-integration.yml +++ b/.github/workflows/continuous-integration.yml @@ -69,6 +69,32 @@ 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: | + 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") + 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 @@ -290,16 +316,41 @@ 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" + + if [[ "${{ steps.coverage-config.outputs.coverage-enabled }}" == "true" ]]; then + PHPUNIT_FLAGS="$PHPUNIT_FLAGS --coverage-text --coverage-cobertura=cobertura.xml --coverage-clover=clover.xml" + fi + if [[ -f "vendor/bin/phpunit" ]]; then - vendor/bin/phpunit --colors=always + php vendor/bin/phpunit $PHPUNIT_FLAGS elif [[ -f "../../vendor/bin/phpunit" ]]; then - ../../vendor/bin/phpunit --colors=always + php ../../vendor/bin/phpunit $PHPUNIT_FLAGS else 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@v6" + if: ${{ !cancelled() && steps.coverage-config.outputs.coverage-enabled == 'true' }} + with: + name: "coverage-report" + 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)) != '' }} run: | diff --git a/.github/workflows/coverage-report.yml b/.github/workflows/coverage-report.yml new file mode 100644 index 0000000..a49fdd6 --- /dev/null +++ b/.github/workflows/coverage-report.yml @@ -0,0 +1,125 @@ +name: "Coverage report" + +on: + workflow_call: + inputs: + 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 + actions: read + +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.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" + + - 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." + 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: "${{ inputs.workflow-name }}" + + - name: "Generating Markdown report" + 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" + 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) + + 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: steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' + uses: "marocchino/sticky-pull-request-comment@v3" + with: + header: coverage + path: code-coverage-results.md diff --git a/README.md b/README.md index d22b419..ba5822c 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" @@ -52,12 +52,16 @@ 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 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). @@ -67,10 +71,69 @@ It can be used, for instance, to install a specific PHP extension. On pull requests, the workflow checks that the `CHANGELOG` file has been updated. This check is automatically skipped for Dependabot PRs and when all changed files are in `locales/` or `.github/` (e.g. locale-update PRs). It can also be fully disabled via the `skip-changelog-check` parameter. +## 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" +``` + +> **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" @@ -112,4 +175,11 @@ jobs: glpi-version: "${{ matrix.glpi-version }}" php-version: "${{ matrix.php-version }}" 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: + plugin-key: "myplugin" ```