diff --git a/.github/actions/coverage/generate-badge/action.yaml b/.github/actions/coverage/generate-badge/action.yaml new file mode 100644 index 000000000..6cd4d62e4 --- /dev/null +++ b/.github/actions/coverage/generate-badge/action.yaml @@ -0,0 +1,60 @@ +name: Coverage Badge Generation +description: Generates a coverage badge and uploades it to an artifact + +inputs: + BADGE_ARTIFACT_NAME: + description: "Name of the Badge artifact" + required: true + COVERAGE_REPORT_ARTIFACT: + description: "Name of the XML coverage report artifact" + required: true + COVERAGE_REPORT_NAME: + description: "Name of the XML coverage report file" + required: true + LABEL: + description: "Badge label" + required: true + OUTPUT_FILE: + description: "Name of the output file" + required: true + RED_LIMIT: + description: "Percentage of the red/orange limit" + default: "50" + required: false + GREEN_LIMIT: + description: "Percentage of the orange/green limit" + default: "65" + required: false + +runs: + using: "composite" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + + - uses: actions/setup-python@v5 + with: + python-version: '3.8' + + - name: Install requests + shell: bash + run: pip install requests + + - name: Install pycobertura + shell: bash + run: pip install pycobertura + + - name: Download line coverage reports + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.COVERAGE_REPORT_ARTIFACT }} + + - name: Generate badge + shell: bash + run: python assets/badge_generator/generate_badge.py --label "${{ inputs.LABEL }}" --output "${{ inputs.OUTPUT_FILE }}" --input-report "${{ inputs.COVERAGE_REPORT_NAME }}" --red-limit "${{ inputs.RED_LIMIT }}" --green-limit "${{ inputs.GREEN_LIMIT }}" + + - uses: actions/upload-artifact@v4 + with: + name: ${{ inputs.BADGE_ARTIFACT_NAME }} + path: ${{ inputs.OUTPUT_FILE }} diff --git a/.github/actions/coverage/upload-badge/action.yaml b/.github/actions/coverage/upload-badge/action.yaml new file mode 100644 index 000000000..f6be3ffe3 --- /dev/null +++ b/.github/actions/coverage/upload-badge/action.yaml @@ -0,0 +1,55 @@ +name: Upload Badge +description: Uploads the generated badge to an specific branch + +inputs: + BADGE_ARTIFACT_NAME: + description: "Name of the Badge artifact" + required: true + BADGE_FILE_NAME: + description: "Name of the Badge file" + required: true + BRANCH_NAME: + description: "Name of the branch where you want to add the badge" + required: true + github_token: + description: "Github token" + required: true + +runs: + using: "composite" + steps: + - uses: actions/checkout@v4 + with: + fetch-depth: 0 + ref: ${{ inputs.BRANCH_NAME }} + + - name: Download coverage badge + uses: actions/download-artifact@v4 + with: + name: ${{ inputs.BADGE_ARTIFACT_NAME }} + + - name: Verify Changed files + uses: tj-actions/verify-changed-files@v16 + id: verify-changed-files + with: + files: ${{ inputs.BADGE_FILE_NAME }} + + - name: Commit badge + if: steps.verify-changed-files.outputs.files_changed == 'true' + shell: bash + run: | + git config --local user.email "<>" + git config --local user.name "GitHubActions" + git add ${{ inputs.BADGE_FILE_NAME }} + git commit -m "Add/Update badge" + + - name: Push badge commit + if: steps.verify-changed-files.outputs.files_changed == 'true' + uses: ad-m/github-push-action@master + with: + github_token: ${{ inputs.github_token }} + branch: ${{ inputs.BRANCH_NAME }} # Dedicated branch to store coverage badges + + - uses: actions/checkout@v4 # we checkout to main so we have access the actions folder and we can execute the Post Upload + with: + fetch-depth: "0" diff --git a/.github/workflows/api-unit-test.yml b/.github/workflows/api-unit-test.yml index 38574983c..33eee1d8a 100644 --- a/.github/workflows/api-unit-test.yml +++ b/.github/workflows/api-unit-test.yml @@ -68,7 +68,7 @@ jobs: name: coverage_api_xml path: ./API/coverage_api.xml - coverage-badge: + generate-coverage-badge: needs: api-unit-test runs-on: ubuntu-latest permissions: @@ -81,41 +81,38 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 - ref: coverage_badges + fetch-depth: "0" - - uses: actions/setup-python@v5 + - name: Generate Badge + uses: ./.github/actions/coverage/generate-badge with: - python-version: '3.8' - - - name: Install genbadge - run: pip install genbadge[coverage] + COVERAGE_REPORT_ARTIFACT: coverage_api_xml + COVERAGE_REPORT_NAME: coverage_api.xml + LABEL: "API coverage" + OUTPUT_FILE: api_coverage_badge.svg + RED_LIMIT: "50" + GREEN_LIMIT: "65" + BADGE_ARTIFACT_NAME: api_coverage_badge + + upload-coverage-badge: + needs: generate-coverage-badge + runs-on: ubuntu-latest + permissions: + contents: write + defaults: + run: + working-directory: ./ + if: github.ref == 'refs/heads/main' && github.event.head_commit.author.name != 'GitHubActions' - - name: Download line coverage reports - uses: actions/download-artifact@v4 + steps: + - uses: actions/checkout@v4 with: - name: coverage_api_xml - - - name: Generate badge - run: genbadge coverage -i coverage_api.xml -n "API coverage" -o api_coverage_badge.svg + fetch-depth: "0" - - name: Verify Changed files - uses: tj-actions/verify-changed-files@v16 - id: verify-changed-files - with: - files: api_coverage_badge.svg - - - name: Commit badge - if: steps.verify-changed-files.outputs.files_changed == 'true' - run: | - git config --local user.email "<>" - git config --local user.name "GitHubActions" - git add api_coverage_badge.svg - git commit -m "Add/Update badge" - - - name: Push badge commit - if: steps.verify-changed-files.outputs.files_changed == 'true' - uses: ad-m/github-push-action@master + - name: Upload Badge + uses: ./.github/actions/coverage/upload-badge with: + BADGE_ARTIFACT_NAME: api_coverage_badge + BADGE_FILE_NAME: api_coverage_badge.svg + BRANCH_NAME: coverage_badges github_token: ${{ secrets.GITHUB_TOKEN }} - branch: coverage_badges # Dedicated branch to store coverage badges diff --git a/.github/workflows/cli-unit-test.yml b/.github/workflows/cli-unit-test.yml index 10a25efba..9bd7ef999 100644 --- a/.github/workflows/cli-unit-test.yml +++ b/.github/workflows/cli-unit-test.yml @@ -11,6 +11,8 @@ name: 🕵️‍♂️ CLI Unit Tests jobs: cli-unit-test: runs-on: ubuntu-latest + # permissions: + # contents: write defaults: run: working-directory: ./CLI @@ -63,7 +65,7 @@ jobs: name: coverage_cli_xml path: ./CLI/coverage_cli.xml - coverage-badge: + generate-coverage-badge: needs: cli-unit-test runs-on: ubuntu-latest permissions: @@ -76,41 +78,38 @@ jobs: steps: - uses: actions/checkout@v4 with: - fetch-depth: 0 - ref: coverage_badges + fetch-depth: "0" - - uses: actions/setup-python@v5 + - name: Generate Badge + uses: ./.github/actions/coverage/generate-badge with: - python-version: '3.8' - - - name: Install genbadge - run: pip install genbadge[coverage] + COVERAGE_REPORT_ARTIFACT: coverage_cli_xml + COVERAGE_REPORT_NAME: coverage_cli.xml + LABEL: "CLI coverage" + OUTPUT_FILE: cli_coverage_badge.svg + RED_LIMIT: "50" + GREEN_LIMIT: "65" + BADGE_ARTIFACT_NAME: cli_coverage_badge + + upload-coverage-badge: + needs: generate-coverage-badge + runs-on: ubuntu-latest + permissions: + contents: write + defaults: + run: + working-directory: ./ + if: github.ref == 'refs/heads/main' && github.event.head_commit.author.name != 'GitHubActions' - - name: Download line coverage reports - uses: actions/download-artifact@v4 + steps: + - uses: actions/checkout@v4 with: - name: coverage_cli_xml - - - name: Generate badge - run: genbadge coverage -i coverage_cli.xml -n "CLI coverage" -o cli_coverage_badge.svg + fetch-depth: "0" - - name: Verify Changed files - uses: tj-actions/verify-changed-files@v16 - id: verify-changed-files - with: - files: cli_coverage_badge.svg - - - name: Commit badge - if: steps.verify-changed-files.outputs.files_changed == 'true' - run: | - git config --local user.email "<>" - git config --local user.name "GitHubActions" - git add cli_coverage_badge.svg - git commit -m "Add/Update CLI badge" - - - name: Push badge commit - if: steps.verify-changed-files.outputs.files_changed == 'true' - uses: ad-m/github-push-action@master + - name: Upload Badge + uses: ./.github/actions/coverage/upload-badge with: + BADGE_ARTIFACT_NAME: cli_coverage_badge + BADGE_FILE_NAME: cli_coverage_badge.svg + BRANCH_NAME: coverage_badges github_token: ${{ secrets.GITHUB_TOKEN }} - branch: coverage_badges # Dedicated branch to store coverage badges \ No newline at end of file diff --git a/assets/badge_generator/ReadMe.md b/assets/badge_generator/ReadMe.md new file mode 100644 index 000000000..6f527dbe1 --- /dev/null +++ b/assets/badge_generator/ReadMe.md @@ -0,0 +1,39 @@ +# Badge Generator +Script that generates a coverage badge. You can customize the following parameters: + +- The left label that will appear in the badge +- The color range limits. By default, the badge will be red between 0-50%, orange between 50-65% and green if higher than 65 +- The output file name + +The coverage percentage be passed in two different ways: + +- By giving the desired value using the flag `--coverage` +- By indicating the path to the XML coverage report using the flag `--input-report` + +Installation +------------ +```bash +# Generate python virtual environment and activate it +python3 -m venv venv +source venv/bin/activate + +# Install requirements +pip install -r requirements.txt +``` + +Examples +------------ + +```bash +# Run help +python generate_badge.py --help + +# Generate badge with default values and indicating the coverage percentage +python generate_badge.py --coverage 75 + +# Generate with xml report file +python generate_badge.py --input-report coverage.xml + +# Generate badge with multiple parameters +python generate_badge.py --label "API coverage" --input-report coverage.xml --output coverage_api.svg --red-limit 55 --green-limit 70 +``` diff --git a/assets/badge_generator/generate_badge.py b/assets/badge_generator/generate_badge.py new file mode 100644 index 000000000..f4bed8d73 --- /dev/null +++ b/assets/badge_generator/generate_badge.py @@ -0,0 +1,108 @@ +import sys +import argparse +import requests +import pycobertura + +DEFAULT_RED_LIMIT=50 +DEFAULT_GREEN_LIMIT=65 +DEFAULT_LABEL = "Coverage" + +def check_valid_percentage_range(percentage): + return 0 <= percentage <= 100 + +def get_limit_values(red_limit, green_limit): + """It returns the red and green limits that defines the percentage values in which the coverage change color from + red to orange and from orange to green""" + red_limit = red_limit or DEFAULT_RED_LIMIT + green_limit = green_limit or DEFAULT_GREEN_LIMIT + + if not check_valid_percentage_range(red_limit) or not check_valid_percentage_range(green_limit): + raise ValueError("Invalid value for Red Limit or Green Limit. They should be between 0 and 100") + if red_limit >= green_limit: + raise ValueError(f"Invalid limit value. Green limit ({green_limit}) should be greater than Red Limit ({red_limit}) ") + return [red_limit, green_limit] + +def parse_coverage_from_report(report_path): + """It tries to open the coverage xml report and parses it to get the line rate. It returns the line coverage percentage""" + try: + coverage_report = pycobertura.Cobertura(report_path) + return round(coverage_report.line_rate() * 100, 2) + except Exception: + raise ValueError("Invalid file path or invalid file format") + +def parse_coverage(coverage): + """Given a coverage percentage string, it parses it and checks if it is a valid percentage value. + It returns the percentage as a float""" + percentage = coverage + if not coverage: + raise ValueError("Percentage should not be empty") + if coverage[-1] == "%": + percentage = coverage[:-1] + # we try to convert it to float + try: + percentage = float(percentage) + if not check_valid_percentage_range(percentage): + raise ValueError("Invalid percentage value") + return percentage + except ValueError: + raise ValueError(f"Invalid percentage data provided: {coverage}. The value is not a valid percentage number") + +def get_color(percentage, limits): + """Returns the corresponding color depending on the percentage value and the corresponding limits""" + red_limit, green_limit = limits + if percentage >= green_limit: + return "green" + elif red_limit <= percentage < green_limit: + return "orange" + return "red" + +def get_label(label): + """Returns the label by replacing the spaces with _""" + if not label: + return DEFAULT_LABEL + return label.replace(" ","_") + +def get_destination_path(path): + if not path: + # We will save the image in the local directory + return "coverage.svg" + return path + +def download_image(label, percentage, color, path): + """It downloads the coverage badge generated by img.shields.io""" + url = f"https://img.shields.io/badge/{label}-{percentage}%25-{color}" + response = requests.get(url) + if response.status_code >= 400: + raise RuntimeError("An error ocurred while getting the badge") + with open(path, mode="wb") as file: + file.write(response.content) + +def parse_opt(): + parser = argparse.ArgumentParser(prog='Generate Badge', description='This script generates a coverage badge') + parser.add_argument('--coverage', type=str, help='Coverage percentage. If defined, input-report will be ignored. Example: 75%%') + parser.add_argument('--input-report', type=str, default="coverage.xml", help="Path to the xml coverage report") + parser.add_argument('--label', type=str, default=DEFAULT_LABEL, help='Left side label') + parser.add_argument('--output', type=str, help="Path to save the badge created") + parser.add_argument('--red-limit', type=float, default=DEFAULT_RED_LIMIT, help="Limit value to red status. It indicates the limit value in which the status change from red to orange") + parser.add_argument('--green-limit', type=float, default=DEFAULT_GREEN_LIMIT, help="Limit value to green status. It indicates the limit value in which the status change from orange to green") + + opt = parser.parse_args() + return vars(opt) + +def run(arguments): + coverage_input = arguments.get("coverage", "") + coverage_report_path = arguments.get("input_report", "") + if coverage_input: + percentage = parse_coverage(coverage_input) + else: + percentage = parse_coverage_from_report(coverage_report_path) + + limits = get_limit_values(arguments.get("red_limit", 0), arguments.get("green_limit", 0)) + color = get_color(percentage, limits) + label = get_label(arguments.get("label", "")) + destination_path = get_destination_path(arguments.get("output", "")) + download_image(label, percentage, color, destination_path) + +if __name__ == '__main__': + opt = parse_opt() + run(opt) diff --git a/assets/badge_generator/requirements.txt b/assets/badge_generator/requirements.txt new file mode 100644 index 000000000..51f7488e9 --- /dev/null +++ b/assets/badge_generator/requirements.txt @@ -0,0 +1,13 @@ +certifi==2024.2.2 +charset-normalizer==3.3.2 +click==8.1.7 +idna==3.7 +jinja2==3.1.4 +lxml==5.2.2 +MarkupSafe==2.1.5 +pycobertura==3.3.1 +requests==2.31.0 +ruamel.yaml==0.18.6 +ruamel.yaml.clib==0.2.8 +tabulate==0.9.0 +urllib3==2.2.1