├── .github └── workflows │ └── docker.yml ├── .gitignore ├── .pre-commit-config.yaml ├── Dockerfile ├── LICENSE ├── README.md ├── action.yml ├── setup.cfg └── src ├── add-to-wiki ├── default.md.j2 ├── entrypoint └── requirements.txt /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | # This workflow uses actions that are not certified by GitHub. 2 | # They are provided by a third-party and are governed by 3 | # separate terms of service, privacy policy, and support 4 | # documentation. 5 | 6 | name: Publish Docker image 7 | 8 | on: 9 | release: 10 | types: 11 | - published 12 | push: 13 | branches: 14 | - v1 15 | 16 | jobs: 17 | push_to_registry: 18 | name: Push Docker image to Docker Hub 19 | runs-on: ubuntu-latest 20 | steps: 21 | - name: Check out the repo 22 | uses: actions/checkout@v2 23 | 24 | - name: Log in to Docker Hub 25 | uses: docker/login-action@f054a8b539a109f9f41c372932f1ae047eff08c9 26 | with: 27 | username: ewjoachim 28 | password: ${{ secrets.DOCKER_PASSWORD }} 29 | 30 | - name: Extract metadata (tags, labels) for Docker 31 | id: meta 32 | uses: docker/metadata-action@98669ae865ea3cffbcbaa878cf57c20bbf1c6c38 33 | with: 34 | images: ewjoachim/coverage-comment-action 35 | flavor: | 36 | latest=true 37 | 38 | - name: Build and push Docker image 39 | uses: docker/build-push-action@ad44023a93711e3deb337508980b4b5e9bcdc5dc 40 | with: 41 | context: . 42 | push: true 43 | tags: ${{ steps.meta.outputs.tags }} 44 | labels: ${{ steps.meta.outputs.labels }} 45 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .mypy_cache 2 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | 4 | ci: 5 | autoupdate_schedule: quarterly 6 | 7 | repos: 8 | - repo: https://github.com/pre-commit/pre-commit-hooks 9 | rev: v5.0.0 10 | hooks: 11 | - id: trailing-whitespace 12 | - id: end-of-file-fixer 13 | - id: check-yaml 14 | - id: check-added-large-files 15 | 16 | - repo: https://github.com/psf/black 17 | rev: "25.1.0" 18 | hooks: 19 | - id: black 20 | 21 | - repo: https://github.com/PyCQA/isort 22 | rev: "6.0.1" 23 | hooks: 24 | - id: isort 25 | 26 | - repo: https://github.com/PyCQA/flake8 27 | rev: "7.2.0" 28 | hooks: 29 | - id: flake8 30 | 31 | - repo: https://github.com/pre-commit/mirrors-mypy 32 | rev: "v1.15.0" 33 | hooks: 34 | - id: mypy 35 | additional_dependencies: [types-requests] 36 | 37 | - repo: https://github.com/asottile/pyupgrade 38 | rev: "v3.19.1" 39 | hooks: 40 | - id: pyupgrade 41 | 42 | - repo: https://github.com/floatingpurr/sync_with_poetry 43 | rev: 1.2.0 44 | hooks: 45 | - id: sync_with_poetry 46 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM python:3-slim 2 | 3 | WORKDIR /src 4 | 5 | RUN set -eux; \ 6 | apt-get update; \ 7 | apt-get install -y git build-essential libffi-dev; \ 8 | rm -rf /var/lib/apt/lists/* 9 | 10 | # https://github.com/actions/runner-images/issues/6775 11 | RUN git config --system --add safe.directory * 12 | 13 | COPY src/requirements.txt ./ 14 | RUN pip install -r requirements.txt 15 | 16 | COPY src/entrypoint /usr/local/bin/ 17 | COPY src/add-to-wiki /usr/local/bin/ 18 | COPY src/default.md.j2 /var/ 19 | 20 | WORKDIR /workdir 21 | 22 | CMD [ "entrypoint" ] 23 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Joachim Jablon 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # GitHub Action: Coverage Comment 2 | 3 | ## Disclaimer 4 | 5 | I've discovered that this action actually fails for external pull requests. 6 | I'm in the process of reviewing how this action should work, but it takes 7 | some time. The problem is described [here](https://github.blog/changelog/2021-02-19-github-actions-workflows-triggered-by-dependabot-prs-will-run-with-read-only-permissions/) 8 | and [here](https://securitylab.github.com/research/github-actions-preventing-pwn-requests/). 9 | 10 | Later update: 11 | Since then, I've created [python-coverage-comment-action](https://github.com/ewjoachim/python-coverage-comment-action) which works for external PRs but only supports Python. I have no plan to port the "external PR" part to this action. 12 | 13 | ## Presentation 14 | 15 | Publish diff coverage report as PR comment, and create a coverage badge to 16 | display on the readme. 17 | 18 | See example at: https://github.com/ewjoachim/coverage-comment-action-example 19 | 20 | ## What does it do? 21 | 22 | This action operates on an already generated `coverage.xml` (Cobertura) file as 23 | generated by most tools, accross languages. 24 | 25 | It has two main modes of operation: 26 | 27 | ### PR mode 28 | 29 | If acting on a PR, it will analyze the XML file, and produce a comment that 30 | will be posted to the PR. If a comment had already previously be written, 31 | it will be updated. The comment contains information on the evolution 32 | of coverage rate attributed to this PR, as well as the rate of coverage 33 | for lines that this PR introduces. There's also a small analysis for each 34 | file in a collapsed block. 35 | 36 | See: https://github.com/ewjoachim/coverage-comment-action-example/pull/3#issuecomment-927207016 37 | 38 | ### Default branch mode 39 | 40 | If acting on the repository's default branch, it will extract the coverage 41 | rate and create a small JSON file that will be stored on the repository's wiki. 42 | This file will then have a stable URL, which means you can create a 43 | [shields.io](https://shields.io/endpoint) badge from it. 44 | 45 | See: https://github.com/ewjoachim/coverage-comment-action-example 46 | 47 | ## Usage 48 | 49 | ### Setup 50 | 51 | Please ensure that the **repository wiki has been initialized** with at least a 52 | single page created. Once it's done, you can disable the wiki for the 53 | repository. 54 | 55 | ### Minimal usage 56 | ```yaml 57 | - name: Display coverage 58 | uses: ewjoachim/coverage-comment-action@v1 59 | with: 60 | GITHUB_TOKEN: ${{ github.token }} 61 | ``` 62 | 63 | ### Maximal usage 64 | ```yaml 65 | - name: Display coverage 66 | uses: ewjoachim/coverage-comment-action@v1 67 | with: 68 | GITHUB_TOKEN: ${{ github.token }} 69 | 70 | # Path and filename of the coverage XML file to analyze. 71 | COVERAGE_FILE: "coverage.xml" 72 | 73 | # Whether or not a badge will be generated and stored. 74 | BADGE_ENABLED: "true" 75 | 76 | # Name of the json file containing badge informations stored in the repo wiki. 77 | BADGE_FILENAME: coverage-comment-badge.json 78 | 79 | # If the coverage percentage is above or equal to this value, the badge will be green. 80 | MINIMUM_GREEN: 100 81 | 82 | # Same with orange. Below is red. 83 | MINIMUM_ORANGE: 70 84 | 85 | # [Advanced] Specify a different template for the comments that will be written on the PR. 86 | COMMENT_TEMPLATE: "" 87 | 88 | # [Advanced] Additional args to pass to diff cover (one per line) 89 | DIFF_COVER_ARGS: "" 90 | ``` 91 | 92 | ### Pinning 93 | On the examples above, the version was set to `v1` (a branch). You can also pin 94 | a specific version such as `v1.0.3` (a tag). There are still things left to 95 | figure out in how to manage releases and version. If you're interested, there's 96 | a [corresponding issue](https://github.com/ewjoachim/coverage-comment-action/issues/8). 97 | 98 | ### Note on the state of this action 99 | 100 | There is no automated test and the dependencies are not frozen, so it's 101 | possible that it fails at some point if a dependency breaks compatibility. 102 | If this happens, we'll fix it and put better checks in place. 103 | 104 | It's probably usable as-is, but you're welcome to offer feedback and, if you 105 | want, contributions. 106 | 107 | ### Note on advanced options 108 | 109 | ``COMMENT_TEMPLATE`` and ``DIFF_COVER_ARGS`` are somewhat experimental. 110 | They haven't been thouroughly tested. 111 | -------------------------------------------------------------------------------- /action.yml: -------------------------------------------------------------------------------- 1 | name: Coverage Comment 2 | branding: 3 | icon: 'umbrella' 4 | color: 'purple' 5 | description: > 6 | Publish diff coverage report as PR comment, and create a coverage badge 7 | to display on the readme. 8 | inputs: 9 | GITHUB_TOKEN: 10 | description: > 11 | A GitHub token to write comments and write the badge to the wiki 12 | (``github.token``) 13 | required: true 14 | COVERAGE_FILE: 15 | description: > 16 | Path and filename of the coverage XML file to analyze. 17 | default: "coverage.xml" 18 | required: false 19 | COMMENT_TEMPLATE: 20 | description: > 21 | [Advanced] Specify a different template for the comments that will be written on the PR. 22 | required: false 23 | DIFF_COVER_ARGS: 24 | description: > 25 | [Advanced] Additional args to pass to diff cover (one per line) 26 | required: false 27 | BADGE_ENABLED: 28 | description: > 29 | Whether or not a badge will be generated and stored. 30 | default: "true" 31 | required: false 32 | BADGE_FILENAME: 33 | description: > 34 | Name of the json file containing badge informations stored in the repo 35 | wiki. 36 | default: coverage-comment-badge.json 37 | required: false 38 | MINIMUM_GREEN: 39 | description: > 40 | If the coverage percentage is above or equal to this value, the badge 41 | will be green. 42 | default: 100 43 | required: false 44 | MINIMUM_ORANGE: 45 | description: > 46 | If the coverage percentage is not green and above or equal to this value, 47 | the badge will be orange. Otherwise it will be red. 48 | default: 70 49 | required: false 50 | runs: 51 | using: docker 52 | # image: Dockerfile 53 | image: docker://ewjoachim/coverage-comment-action:latest 54 | env: 55 | GITHUB_TOKEN: ${{ inputs.GITHUB_TOKEN }} 56 | COVERAGE_FILE: ${{ inputs.COVERAGE_FILE }} 57 | COMMENT_TEMPLATE: ${{ inputs.COMMENT_TEMPLATE }} 58 | DIFF_COVER_ARGS: ${{ inputs.DIFF_COVER_ARGS }} 59 | BADGE_ENABLED: ${{ inputs.BADGE_ENABLED }} 60 | BADGE_FILENAME: ${{ inputs.BADGE_FILENAME }} 61 | MINIMUM_GREEN: ${{ inputs.MINIMUM_GREEN }} 62 | MINIMUM_ORANGE: ${{ inputs.MINIMUM_ORANGE }} 63 | -------------------------------------------------------------------------------- /setup.cfg: -------------------------------------------------------------------------------- 1 | [isort] 2 | profile = black 3 | skip = .venv,.tox 4 | 5 | [flake8] 6 | # E203: whitespace before colon on list slice: mylist[1 : 2] 7 | # E501: line too long (black knows better) 8 | extend-ignore = E203,E501 9 | extend-exclude = .venv 10 | 11 | [mypy] 12 | no_implicit_optional = True 13 | 14 | [mypy-xmltodict.*] 15 | ignore_missing_imports = True 16 | -------------------------------------------------------------------------------- /src/add-to-wiki: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Usage $0 {owner/repo} {filename} {commit_message} 3 | # Stores the content of stdin in a file named {filename} in the wiki of 4 | # the provided repo 5 | # Reads envvar GITHUB_TOKEN 6 | 7 | set -eux 8 | 9 | stdin="$(cat -)" 10 | repo_name="${1}" 11 | filename="${2}" 12 | commit_message="${3}" 13 | dir="$(mktemp -d)" 14 | cd $dir 15 | 16 | git clone "https://x-access-token:${GITHUB_TOKEN}@github.com/${repo_name}.wiki.git" . 17 | echo $stdin > "${filename}" 18 | 19 | git add "${filename}" 20 | 21 | if ! git diff --staged --exit-code 22 | then 23 | git config --global user.email "coverage-comment-action" 24 | git config --global user.name "Coverage Comment Action" 25 | git commit -m "$commit_message" 26 | 27 | git push -u origin 28 | else 29 | echo "No change detected, skipping." 30 | fi 31 | -------------------------------------------------------------------------------- /src/default.md.j2: -------------------------------------------------------------------------------- 1 | ## Coverage report 2 | {% if previous_coverage -%} 3 | The coverage rate went from `{{ previous_coverage }}%` to `{{ coverage }}%` {{ 4 | ":arrow_up:" if previous_coverage < coverage else 5 | ":arrow_down:" if previous_coverage > coverage else 6 | ":arrow_right:" 7 | }} 8 | {%- else -%} 9 | The coverage rate is `{{ coverage }}%` 10 | {%- endif %} 11 | 12 | {% if branch_coverage -%} 13 | The branch rate is `{{ branch_coverage }}%` 14 | {%- endif %} 15 | 16 | `{{ diff_coverage }}%` of new lines are covered. 17 | 18 | {%if file_info -%} 19 |
20 | Diff Coverage details (click to unfold) 21 | 22 | {% for filename, stats in file_info.items() -%} 23 | ### {{ filename }} 24 | `{{ stats.diff_coverage }}%` of new lines are covered 25 | 26 | {% endfor %} 27 | {%- endif -%} 28 |
29 | {{ marker }} 30 | -------------------------------------------------------------------------------- /src/entrypoint: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import dataclasses 4 | import inspect 5 | import json 6 | import os 7 | import pathlib 8 | import subprocess 9 | import sys 10 | import tempfile 11 | from typing import List, Optional 12 | 13 | import github 14 | import jinja2 15 | import requests 16 | import xmltodict 17 | 18 | MARKER = """""" 19 | SHIELD_URL = "https://img.shields.io/endpoint?url={url}" 20 | JSON_URL = "https://raw.githubusercontent.com/wiki/{repo_name}/{filename}" 21 | 22 | 23 | def main(): 24 | print("Starting action") 25 | config = Config.from_environ(os.environ) 26 | coverage_info = get_coverage_info(config=config) 27 | gh = get_api(config=config) 28 | print(f"Operating on {config.GITHUB_REF}") 29 | if config.GITHUB_PR_NUMBER: 30 | print(f"Commenting on the coverage on PR {config.GITHUB_PR_NUMBER}") 31 | diff_coverage_info = get_diff_coverage_info(config=config) 32 | previous_coverage_rate = get_previous_coverage_rate(config=config) 33 | 34 | comment = get_markdown_comment( 35 | coverage_info=coverage_info, 36 | diff_coverage_info=diff_coverage_info, 37 | previous_coverage_rate=previous_coverage_rate, 38 | config=config, 39 | ) 40 | post_comment(body=comment, gh=gh, config=config) 41 | 42 | if config.BADGE_ENABLED and is_main_branch(gh=gh, config=config): 43 | print("Running on default branch, saving Badge into the repo wiki") 44 | badge = compute_badge(coverage_info=coverage_info, config=config) 45 | upload_badge(badge=badge, config=config) 46 | url = get_badge_json_url(config=config) 47 | print(f"Badge JSON stored at {url}") 48 | print(f"Badge URL: {SHIELD_URL.format(url=url)}") 49 | 50 | print("Ending action") 51 | 52 | 53 | @dataclasses.dataclass 54 | class Config: 55 | """This object defines the environment variables""" 56 | 57 | GITHUB_BASE_REF: str 58 | GITHUB_TOKEN: str 59 | GITHUB_API_URL: str 60 | GITHUB_REPOSITORY: str 61 | GITHUB_REF: str 62 | BADGE_FILENAME: str = "coverage-comment-badge.json" 63 | COVERAGE_FILE: str = "coverage.xml" 64 | COMMENT_TEMPLATE: Optional[str] = None 65 | DIFF_COVER_ARGS: List[str] = dataclasses.field(default_factory=list) 66 | BADGE_ENABLED: bool = True 67 | MINIMUM_GREEN: float = 100.0 68 | MINIMUM_ORANGE: float = 70.0 69 | 70 | # Clean methods 71 | @classmethod 72 | def clean_diff_cover_args(cls, value: str) -> list: 73 | return [e.strip() for e in value.split("\n") if e.strip()] 74 | 75 | @classmethod 76 | def clean_badge_enabled(cls, value: str) -> bool: 77 | return value.lower() in ("1", "true", "yes") 78 | 79 | @classmethod 80 | def clean_minimum_green(cls, value: str) -> float: 81 | return float(value) 82 | 83 | @classmethod 84 | def clean_minimum_orange(cls, value: str) -> float: 85 | return float(value) 86 | 87 | @property 88 | def GITHUB_PR_NUMBER(self) -> Optional[int]: 89 | # "refs/pull/2/merge" 90 | if "pull" in self.GITHUB_REF: 91 | return int(self.GITHUB_REF.split("/")[2]) 92 | return None 93 | 94 | @property 95 | def GITHUB_BRANCH_NAME(self) -> Optional[str]: 96 | # "refs/pull/2/merge" 97 | if self.GITHUB_REF.startswith("refs/heads/"): 98 | return self.GITHUB_REF.split("/")[-1] 99 | return None 100 | 101 | @classmethod 102 | def from_environ(cls, environ): 103 | possible_variables = [e for e in inspect.signature(cls).parameters] 104 | config = {k: v for k, v in environ.items() if k in possible_variables} 105 | for key, value in list(config.items()): 106 | if func := getattr(cls, f"clean_{key.lower()}", None): 107 | config[key] = func(value) 108 | 109 | try: 110 | return cls(**config) 111 | except TypeError: 112 | missing = { 113 | name 114 | for name, param in inspect.signature(cls).parameters.items() 115 | if param.default is inspect.Parameter.empty 116 | } - set(os.environ) 117 | sys.exit(f" missing environment variable(s): {', '.join(missing)}") 118 | 119 | 120 | def get_api(config: Config) -> github.Github: 121 | return github.Github( 122 | login_or_token=config.GITHUB_TOKEN, base_url=config.GITHUB_API_URL 123 | ) 124 | 125 | 126 | def get_coverage_info(config: Config) -> dict: 127 | coverage_file = pathlib.Path(config.COVERAGE_FILE) 128 | if not coverage_file.exists(): 129 | raise Exit(f"Coverage file not found at {config.COVERAGE_FILE}") 130 | 131 | def convert(tuple_values): 132 | result = [] 133 | for key, value in tuple_values: 134 | result.append( 135 | ( 136 | key, 137 | { 138 | "@timestamp": int, 139 | "@lines-valid": int, 140 | "@lines-covered": int, 141 | "@line-rate": float, 142 | "@branches-valid": int, 143 | "@branches-covered": int, 144 | "@branch-rate": float, 145 | "@hits": int, 146 | "@branch": lambda x: x == "true", 147 | }.get(key, lambda x: x)(value), 148 | ) 149 | ) 150 | return dict(result) 151 | 152 | return json.loads( 153 | json.dumps(xmltodict.parse(pathlib.Path(config.COVERAGE_FILE).read_text())), 154 | object_pairs_hook=convert, 155 | )["coverage"] 156 | 157 | 158 | def get_diff_coverage_info(config: Config) -> dict: 159 | call("git", "fetch", "--depth=1000") 160 | with tempfile.NamedTemporaryFile("r") as f: 161 | call( 162 | "diff-cover", 163 | config.COVERAGE_FILE, 164 | f"--compare-branch=origin/{config.GITHUB_BASE_REF}", 165 | f"--json-report={f.name}", 166 | "--diff-range-notation=..", 167 | "--quiet", 168 | *config.DIFF_COVER_ARGS, 169 | ) 170 | return json.loads(f.read()) 171 | 172 | 173 | def get_markdown_comment( 174 | coverage_info: dict, 175 | diff_coverage_info: dict, 176 | previous_coverage_rate: Optional[float], 177 | config: Config, 178 | ): 179 | env = jinja2.Environment() 180 | template = config.COMMENT_TEMPLATE or pathlib.Path("/var/default.md.j2").read_text() 181 | previous_coverage = previous_coverage_rate * 100 if previous_coverage_rate else None 182 | coverage = coverage_info["@line-rate"] * 100 183 | branch_coverage = ( 184 | coverage_info["@branch-rate"] * 100 185 | if coverage_info.get("@branch-rate") 186 | else None 187 | ) 188 | diff_coverage = diff_coverage_info["total_percent_covered"] 189 | file_info = { 190 | file: {"diff_coverage": stats["percent_covered"]} 191 | for file, stats in diff_coverage_info["src_stats"].items() 192 | } 193 | return env.from_string(template).render( 194 | previous_coverage=previous_coverage, 195 | coverage=coverage, 196 | branch_coverage=branch_coverage, 197 | diff_coverage=diff_coverage, 198 | file_info=file_info, 199 | marker=MARKER, 200 | ) 201 | 202 | 203 | def is_main_branch(gh: github.Github, config: Config) -> bool: 204 | repo = gh.get_repo(config.GITHUB_REPOSITORY) 205 | 206 | branch = config.GITHUB_BRANCH_NAME 207 | return repo.default_branch == branch 208 | 209 | 210 | def post_comment(body: str, gh: github.Github, config: Config) -> None: 211 | try: 212 | me = gh.get_user().login 213 | except github.GithubException as exc: 214 | if exc.status == 403: 215 | me = "github-actions[bot]" 216 | else: 217 | raise 218 | repo = gh.get_repo(config.GITHUB_REPOSITORY) 219 | assert config.GITHUB_PR_NUMBER 220 | issue = repo.get_issue(config.GITHUB_PR_NUMBER) 221 | for comment in issue.get_comments(): 222 | if comment.user.login == me and MARKER in comment.body: 223 | print("Update previous comment") 224 | comment.edit(body=body) 225 | break 226 | else: 227 | print("Adding new comment") 228 | issue.create_comment(body=body) 229 | 230 | 231 | def compute_badge(coverage_info: dict, config: Config) -> str: 232 | rate = int(coverage_info["@line-rate"] * 100) 233 | 234 | if rate >= config.MINIMUM_GREEN: 235 | color = "brightgreen" 236 | elif rate >= config.MINIMUM_ORANGE: 237 | color = "orange" 238 | else: 239 | color = "red" 240 | 241 | badge = { 242 | "schemaVersion": 1, 243 | "label": "Coverage", 244 | "message": f"{rate}%", 245 | "color": color, 246 | } 247 | 248 | return json.dumps(badge) 249 | 250 | 251 | def get_badge_json_url(config: Config) -> str: 252 | return JSON_URL.format( 253 | repo_name=config.GITHUB_REPOSITORY, filename=config.BADGE_FILENAME 254 | ) 255 | 256 | 257 | def get_previous_coverage_rate(config: Config) -> Optional[float]: 258 | try: 259 | response = requests.get(get_badge_json_url(config=config)) 260 | return float(response.json()["message"][:-1]) / 100 261 | except Exception: 262 | print("Previous coverage results not found, cannot report on evolution.") 263 | return None 264 | 265 | 266 | def upload_badge(badge: str, config: Config) -> None: 267 | try: 268 | call( 269 | "add-to-wiki", 270 | config.GITHUB_REPOSITORY, 271 | config.BADGE_FILENAME, 272 | "Update badge", 273 | input=badge, 274 | ) 275 | except Exit as exc: 276 | if "remote error: access denied or repository not exported" in str(exc): 277 | print( 278 | "Wiki seems not to be activated for this project. Please activate the " 279 | "wiki. You may disable it afterwards." 280 | ) 281 | raise 282 | 283 | 284 | class Exit(Exception): 285 | pass 286 | 287 | 288 | def call(*args, **kwargs): 289 | try: 290 | return subprocess.run( 291 | args, text=True, check=True, capture_output=True, **kwargs 292 | ) 293 | except subprocess.CalledProcessError as exc: 294 | raise Exit("/n".join([exc.stdout, exc.stderr])) 295 | 296 | 297 | if __name__ == "__main__": 298 | try: 299 | main() 300 | except Exit as exc: 301 | print(exc) 302 | sys.exit(1) 303 | -------------------------------------------------------------------------------- /src/requirements.txt: -------------------------------------------------------------------------------- 1 | diff-cover 2 | jinja2 3 | PyGithub 4 | xmltodict 5 | requests 6 | --------------------------------------------------------------------------------