diff --git a/.github/workflows/coverage_test.yml b/.github/workflows/coverage_test.yml new file mode 100644 index 00000000..db58a772 --- /dev/null +++ b/.github/workflows/coverage_test.yml @@ -0,0 +1,119 @@ +--- +name: coverage_tests + +concurrency: + group: ${{ github.workflow }}-${{ github.head_ref || github.ref }} + cancel-in-progress: true + +on: # yamllint disable-line rule:truthy + pull_request: + branches: + - main + - stable-* + push: + branches: + - main + - stable-* + workflow_dispatch: + +jobs: + coverage: + name: Integration test coverage + runs-on: ubuntu-latest + env: + ANSIBLE_CORE_REF: "milestone" + steps: + - name: Checkout repository + uses: actions/checkout@v4 + with: + # ansible-test requires cwd under .../ansible_collections/{namespace}/{name}/ + path: ansible_collections/cloud/aws_ops + + - name: Checkout amazon.aws dependency + uses: ansible-network/github_actions/.github/actions/checkout_dependency@47822b51aee957080e811cc434443c4e3bb9569a + with: + repository: ansible-collections/amazon.aws + path: ansible_collections/amazon/aws + ref: main + + - name: Checkout community.aws dependency + uses: ansible-network/github_actions/.github/actions/checkout_dependency@47822b51aee957080e811cc434443c4e3bb9569a + with: + repository: ansible-collections/community.aws + path: ansible_collections/community/aws + ref: main + + - name: Checkout community.crypto dependency + uses: ansible-network/github_actions/.github/actions/checkout_dependency@47822b51aee957080e811cc434443c4e3bb9569a + with: + repository: ansible-collections/community.crypto + path: ansible_collections/community/crypto + ref: main + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: "3.12" + + - name: Install Ansible (ansible-test) + run: | + python -m pip install --upgrade pip + python -m pip install "https://github.com/ansible/ansible/archive/${ANSIBLE_CORE_REF}.tar.gz" + + - name: Install integration test dependencies + run: | + python -m pip install -r ansible_collections/cloud/aws_ops/tests/integration/constraints.txt -r ansible_collections/cloud/aws_ops/tests/integration/requirements.txt + + - name: Create AWS/sts session credentials + uses: ansible/ansible-test-auth@008750a5bf07124c3ab86d30d73d7319d7518f36 # v1.0.0, committed on Mar 13, 2026 + + - name: Run integration tests with coverage + working-directory: ansible_collections/cloud/aws_ops + run: ansible-test integration --venv --coverage --python 3.12 --requirements + + - name: Combine and emit coverage XML + working-directory: ansible_collections/cloud/aws_ops + run: | + ansible-test coverage combine --venv --python 3.12 --requirements + ansible-test coverage xml --venv --python 3.12 --requirements + + - name: Prepare coverage.xml for SonarCloud + env: + COLLECTION_DIR: ${{ github.workspace }}/ansible_collections/cloud/aws_ops + run: | + set -euo pipefail + xml=$(find "${COLLECTION_DIR}/tests/output/reports" -maxdepth 1 -name '*.xml' ! -name '*powershell*' | head -1) + if [[ -z "${xml}" ]]; then + echo "::error::coverage XML not found under tests/output/reports" + exit 1 + fi + cp "${xml}" "${GITHUB_WORKSPACE}/coverage.xml" + sed -i "s#${GITHUB_WORKSPACE}/##g" "${GITHUB_WORKSPACE}/coverage.xml" + sed -i 's#ansible_collections/cloud/aws_ops/##g' "${GITHUB_WORKSPACE}/coverage.xml" + if ! grep -q '- + ${{ needs.coverage.result == 'success' + && (github.event_name == 'push' + || (github.event_name == 'pull_request' + && github.event.pull_request.head.repo.full_name == github.repository)) }} + uses: ./.github/workflows/sonarcloud.yml + secrets: + ANSIBLE_COLLECTIONS_ORG_SONAR_TOKEN_CICD_BOT: ${{ secrets.ANSIBLE_COLLECTIONS_ORG_SONAR_TOKEN_CICD_BOT }} diff --git a/.github/workflows/sonarcloud.yml b/.github/workflows/sonarcloud.yml index 824e7c43..54f25dcc 100644 --- a/.github/workflows/sonarcloud.yml +++ b/.github/workflows/sonarcloud.yml @@ -1,41 +1,68 @@ ---- -# SonarCloud analysis for cloud.aws_ops +## SonarCloud scan (reusable) # -# Uses the same-repo + default-branch push model: GitHub does not expose org secrets to workflows -# from fork PRs (see https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions). -# This job is gated so the Sonar token is never available in untrusted fork contexts. A follow-up -# workflow triggered by workflow_run + artifacts is an alternative if the org later requires Sonar -# with coverage on fork PRs (see README.md and https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run). +# Invoked from **coverage_test** after the aggregate gate and **coverage** succeed. Uses the **caller's** +# **pull_request** / **push** event so **actions/checkout** can use **github.event.pull_request.head.sha** +# on PRs (Sonar-compliant). Not triggered by **workflow_run** + **workflow_run.head_sha** checkout. + +--- name: SonarCloud on: - push: - branches: - - main - - stable-* - pull_request: - branches: - - main - - stable-* - workflow_dispatch: + workflow_call: + secrets: + ANSIBLE_COLLECTIONS_ORG_SONAR_TOKEN_CICD_BOT: + required: true permissions: contents: read pull-requests: read jobs: - sonarqube: - name: SonarCloud Scan + scan: + name: SonarCloud scan runs-on: ubuntu-latest - if: github.event_name != 'pull_request' || github.event.pull_request.head.repo.full_name == github.repository steps: - - name: Checkout + - name: Checkout repository uses: actions/checkout@v4 with: + ref: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} fetch-depth: 0 + show-progress: false + + - name: Download coverage artifacts + uses: actions/download-artifact@v4 + with: + name: coverage + path: . + + - name: Set coverage report paths + run: | + coverage_files=$(find . -name "coverage*.xml" -type f 2>/dev/null | tr '\n' ',' | sed 's/,$//') + echo "Found coverage files: ${coverage_files:-none}" + echo "COVERAGE_PATHS=${coverage_files}" >> "$GITHUB_ENV" + + - name: Prepare SonarCloud args + env: + COMMIT_SHA: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.sha || github.sha }} + EVENT_NAME: ${{ github.event_name }} + PR_NUMBER: ${{ github.event_name == 'pull_request' && github.event.pull_request.number || '' }} + PR_HEAD_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.head.ref || '' }} + PR_BASE_REF: ${{ github.event_name == 'pull_request' && github.event.pull_request.base.ref || '' }} + run: | + SONAR_ARGS="-Dsonar.scm.revision=\"${COMMIT_SHA}\"" + if [[ "${EVENT_NAME}" == "pull_request" ]]; then + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.key=${PR_NUMBER}" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.branch=${PR_HEAD_REF}" + SONAR_ARGS="${SONAR_ARGS} -Dsonar.pullrequest.base=${PR_BASE_REF}" + fi + if [[ -n "${COVERAGE_PATHS:-}" ]]; then + SONAR_ARGS="${SONAR_ARGS} -Dsonar.python.coverage.reportPaths=${COVERAGE_PATHS}" + fi + echo "SONAR_ARGS=${SONAR_ARGS}" >> "$GITHUB_ENV" - name: SonarCloud Scan - # Same pinned version as ansible-collections/amazon.aws sonarcloud.yml uses: SonarSource/sonarqube-scan-action@a31c9398be7ace6bbfaf30c0bd5d415f843d45e9 env: SONAR_TOKEN: ${{ secrets.ANSIBLE_COLLECTIONS_ORG_SONAR_TOKEN_CICD_BOT }} + with: + args: ${{ env.SONAR_ARGS }} diff --git a/CI.md b/CI.md index 7a75e36a..d4ccfe36 100644 --- a/CI.md +++ b/CI.md @@ -49,3 +49,12 @@ Integration tests run on real AWS infrastructure and require the "safe to test" - Integration targets are automatically split across 2 parallel jobs - Split is determined by `ansible_test_splitter` action based on changed files - Each job runs the subset of tests relevant to the PR's changes + +### SonarCloud and coverage + +| Workflow | Description | +| -------- | ----------- | +| [Integration](.github/workflows/integration.yml) | Integration tests only (no coverage) | +| [SonarCloud](.github/workflows/sonarcloud.yml) | Separate integration run with `--coverage`, **`coverage`** job, then **`finalize`** Sonar scan | + +This collection does not use an **`all_green`** gate. Coverage for Sonar is **not** produced by the Integration workflow. Details: [SONARCLOUD.md](SONARCLOUD.md). diff --git a/README.md b/README.md index 9821e71f..e8390ed7 100644 --- a/README.md +++ b/README.md @@ -1,23 +1,20 @@ +[![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=redhat-cop_cloud.aws_ops&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=redhat-cop_cloud.aws_ops) +[![Coverage](https://sonarcloud.io/api/project_badges/measure?project=redhat-cop_cloud.aws_ops&metric=coverage)](https://sonarcloud.io/summary/new_code?id=redhat-cop_cloud.aws_ops) +[![Vulnerabilities](https://sonarcloud.io/api/project_badges/measure?project=redhat-cop_cloud.aws_ops&metric=vulnerabilities)](https://sonarcloud.io/summary/new_code?id=redhat-cop_cloud.aws_ops) +[![Security Rating](https://sonarcloud.io/api/project_badges/measure?project=redhat-cop_cloud.aws_ops&metric=security_rating)](https://sonarcloud.io/summary/new_code?id=redhat-cop_cloud.aws_ops) +[![Bugs](https://sonarcloud.io/api/project_badges/measure?project=redhat-cop_cloud.aws_ops&metric=bugs)](https://sonarcloud.io/summary/new_code?id=redhat-cop_cloud.aws_ops) +[![Code Smells](https://sonarcloud.io/api/project_badges/measure?project=redhat-cop_cloud.aws_ops&metric=code_smells)](https://sonarcloud.io/summary/new_code?id=redhat-cop_cloud.aws_ops) +[![Maintainability Rating](https://sonarcloud.io/api/project_badges/measure?project=redhat-cop_cloud.aws_ops&metric=sqale_rating)](https://sonarcloud.io/summary/new_code?id=redhat-cop_cloud.aws_ops) + # cloud.aws_ops Validated Content Collection This repository hosts the `cloud.aws_ops` Ansible Collection. ## SonarCloud (code quality) -Static analysis runs on [SonarCloud](https://sonarcloud.io) using `sonar-project.properties` and -`.github/workflows/sonarcloud.yml`. **Note:** Coverage in SonarCloud is not wired yet. - -The SonarCloud project key must match `sonar.projectKey` (`ansible-collections_cloud.aws_ops`). Adding -or renaming the project is coordinated via Ansible Collections maintainers. - -GitHub does not expose organization secrets to workflows for pull requests opened from forks. The -Sonar job therefore only runs on pushes to this repository's branches and on pull requests where the -head branch is on `redhat-cop/cloud.aws_ops` (not from forks). That matches GitHub's -documented behavior for [secrets in Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions). +Static analysis and **integration-test coverage** are reported on [SonarCloud](https://sonarcloud.io/project/overview?id=redhat-cop_cloud.aws_ops). Coverage is collected in [.github/workflows/sonarcloud.yml](.github/workflows/sonarcloud.yml), separate from [.github/workflows/integration.yml](.github/workflows/integration.yml). See [SONARCLOUD.md](SONARCLOUD.md). -If the project later needs Sonar with coverage on **fork** PRs, maintainers typically add a separate -trusted job after a workflow that uploads coverage artifacts, using GitHub's `workflow_run` event. -See [workflow_run (GitHub Docs)](https://docs.github.com/en/actions/using-workflows/events-that-trigger-workflows#workflow_run). +The SonarCloud project key must match `sonar.projectKey` in [sonar-project.properties](sonar-project.properties) (`redhat-cop_cloud.aws_ops`). ## Description diff --git a/SONARCLOUD.md b/SONARCLOUD.md new file mode 100644 index 00000000..27d49fbb --- /dev/null +++ b/SONARCLOUD.md @@ -0,0 +1,30 @@ +# SonarCloud + +Dashboard: + +[SonarCloud project overview](https://sonarcloud.io/project/overview?id=redhat-cop_cloud.aws_ops) + +## CI integration + +Sonar analysis and **coverage collection** live in **[.github/workflows/sonarcloud.yml](.github/workflows/sonarcloud.yml)** only. They are **not** part of **[.github/workflows/integration.yml](.github/workflows/integration.yml)** or an **`all_green`** aggregator. + +| Workflow | Role | +| -------- | ---- | +| [integration.yml](.github/workflows/integration.yml) | Standard integration tests (`ansible_test_integration`, no coverage) | +| [sonarcloud.yml](.github/workflows/sonarcloud.yml) | Same PR gates and targets, but `ansible-test integration --coverage`; **`coverage`** job emits `coverage.xml`; **`finalize`** runs the Sonar scanner | + +Both workflows use **`pull_request_target`**, the **safe to test** label, and the same splitter/AWS setup. A labeled PR therefore runs integration tests twice (once per workflow); only the SonarCloud workflow produces coverage for Sonar. + +### SonarCloud workflow jobs + +- **`coverage-test`** (matrix) — integration tests with `--coverage`; uploads **`coverage-raw-*`** artifacts +- **`coverage`** — `ansible-test coverage combine` / `coverage xml`, path rewrite, upload artifact **`coverage`** +- **`finalize`** — downloads **`coverage`**, sets **`sonar.python.coverage.reportPaths`**, runs **`SonarSource/sonarqube-scan-action`** (same-repo PRs only, when org secret is set) + +Scanner configuration: [sonar-project.properties](sonar-project.properties) (`sonar.projectKey=redhat-cop_cloud.aws_ops`, `sonar.tests=tests/integration`). + +**`finalize`** uses org secret **`ANSIBLE_COLLECTIONS_ORG_SONAR_TOKEN_CICD_BOT`** and is gated so the token is not used for fork-head PRs. See GitHub [secrets in Actions](https://docs.github.com/en/actions/security-guides/using-secrets-in-github-actions). + +## Branch protection (repository settings) + +If **SonarCloud** / **finalize** should block merges, add the check under **Settings** > **Branches** > **Required status checks**. That is not configured in YAML. diff --git a/sonar-project.properties b/sonar-project.properties index 183f938c..252cd3f2 100644 --- a/sonar-project.properties +++ b/sonar-project.properties @@ -1,8 +1,8 @@ # SonarCloud project configuration for cloud.aws_ops # Complete documentation: https://docs.sonarqube.org/latest/analysis/analysis-parameters/ -sonar.projectKey=ansible-collections_cloud.aws_ops -sonar.organization=ansible-collections +sonar.projectKey=redhat-cop_cloud.aws_ops +sonar.organization=redhat-cop sonar.sources=. sonar.projectName=cloud.aws_ops sonar.python.coverage.reportPaths=coverage.xml