-
Notifications
You must be signed in to change notification settings - Fork 7
Code coverage support #49
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: v1
Are you sure you want to change the base?
Changes from 4 commits
6bb4ebb
f6da824
39189e4
b733f71
a9d3206
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,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 | ||
|
|
@@ -262,18 +282,55 @@ 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: | ||
| 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 [[ "${{ 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" | ||
|
Comment on lines
+312
to
+313
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I think the extention should be always loaded, but maybe with a The only requirement here would the be to enable pcov through the |
||
| 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: "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: | | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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." |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -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" | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. I guess it should not be hardcoded. |
||
|
|
||
| - name: "Generating Markdown report" | ||
| if: github.event_name == 'pull_request' && steps.coverage-config.outputs.skip != 'true' && steps.coverage-report.outputs.file != '' | ||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
| 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 <details> | ||
| 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 "<details>" | ||
| echo "<summary>📋 Details</summary>" | ||
| echo "" | ||
| tail -n "+${FIRST_TABLE_LINE}" "$REPORT_FILE" | ||
| echo "" | ||
| echo "$ARTIFACT_LINK" | ||
| echo "" | ||
| echo "</details>" | ||
| } > "${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 | ||
| Original file line number | Diff line number | Diff line change | ||||||
|---|---|---|---|---|---|---|---|---|
|
|
@@ -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" | ||||||||
|
|
@@ -62,6 +62,75 @@ The `db-image` parameter is a combination of the DB server engine (`mysql`, `mar | |||||||
| An optional `init-script` parameter can be used to define the path of an initialization script. This script will be executed with `bash`. | ||||||||
| It can be used, for instance, to install a specific PHP extension. | ||||||||
|
|
||||||||
| ## 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/<plugin-key>/`. | ||||||||
| - `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" | ||||||||
| ``` | ||||||||
|
|
||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. IMHO, this is not required. Indeed, we could just add a comment in the |
||||||||
| ## 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. | ||||||||
|
|
@@ -107,4 +176,10 @@ jobs: | |||||||
| glpi-version: "${{ matrix.glpi-version }}" | ||||||||
| php-version: "${{ matrix.php-version }}" | ||||||||
| db-image: "${{ matrix.db-image }}" | ||||||||
|
|
||||||||
| coverage-report: | ||||||||
| needs: "ci" | ||||||||
|
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Maybe it should be trigerred only for pull requests.
Suggested change
|
||||||||
| uses: "glpi-project/plugin-ci-workflows/.github/workflows/coverage-report.yml@v1" | ||||||||
| with: | ||||||||
| plugin-key: "myplugin" | ||||||||
| ``` | ||||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Please add
pcovin thegithubactions-php-apacheimage (inherited bygithubactions-glpi-apache).