From f91af15ece840cca25b0d0dd6c051f523ae7687a Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 11 Apr 2020 23:30:42 +0200 Subject: [PATCH 01/16] Description update --- action.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/action.yml b/action.yml index d13a6e2..48fd1b6 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,5 @@ -name: 'Coveralls' -description: 'Reports to coveralls.io' +name: 'Coveralls Python' +description: 'Python coverage reports via coveralls.io' runs: using: 'docker' image: 'Dockerfile' From 2f945fb5393caaef3e8acca5256729beab003222 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sat, 11 Apr 2020 23:45:44 +0200 Subject: [PATCH 02/16] Coveralls.io badge --- README.md | 4 ++++ action.yml | 4 ++++ 2 files changed, 8 insertions(+) diff --git a/README.md b/README.md index db6e023..311c211 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,7 @@ # coveralls-python-action [![push](https://github.com/AndreMiras/coveralls-python-action/workflows/push/badge.svg?branch=develop)](https://github.com/AndreMiras/coveralls-python-action/actions?query=workflow%3Apush) +[![Coverage Status](https://coveralls.io/repos/github/AndreMiras/coveralls-python-action/badge.svg?branch=develop)](https://coveralls.io/github/AndreMiras/coveralls-python-action?branch=develop) GitHub Action for Python Coveralls.io @@ -9,6 +10,9 @@ You simply need to set one of the following two environment variables: - `GITHUB_TOKEN` - `COVERALLS_REPO_TOKEN` +Also configure your `coverage.py` with `relative_files = True`. +https://coverage.readthedocs.io/en/coverage-5.0.4/config.html#config-run-relative-files + ## Example usage Assuming you have a `make test` that runs coverage testing. The following will upload it to coveralls.io. diff --git a/action.yml b/action.yml index 48fd1b6..c4a2233 100644 --- a/action.yml +++ b/action.yml @@ -1,5 +1,9 @@ name: 'Coveralls Python' +author: 'Andre Miras' description: 'Python coverage reports via coveralls.io' +branding: + color: 'green' + icon: 'percent' runs: using: 'docker' image: 'Dockerfile' From 439936760d8ccaa044720f4083ba106504f8625e Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 00:25:54 +0200 Subject: [PATCH 03/16] Increases coverage to 80% --- src/entrypoint.py | 1 + tests/test_entrypoint.py | 33 +++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/entrypoint.py b/src/entrypoint.py index 2fd4d5e..5d53fff 100755 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -36,6 +36,7 @@ def run_coveralls(): # (depending on where it's ran from?) service_names = ("github", "github-actions") kwargs = {"repo_token": repo_token} + result = None for service_name in service_names: log.info(f"Trying submitting coverage with service_name: {service_name}...") coveralls = Coveralls(service_name=service_name, **kwargs) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 824b28d..8486180 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -1,10 +1,43 @@ from unittest import mock +import pytest + import entrypoint +def patch_os_envirion(environ): + return mock.patch.dict("os.environ", environ, clear=True) + + +def patch_coveralls_wear(): + return mock.patch("entrypoint.Coveralls.wear") + + +def patch_log(): + return mock.patch("entrypoint.log") + + class TestEntryPoint: def test_main(self): with mock.patch("entrypoint.run_coveralls") as m_run_coveralls: entrypoint.main() assert m_run_coveralls.call_args_list == [mock.call()] + + def test_run_coveralls_no_token(self): + with pytest.raises(AssertionError) as ex_info: + entrypoint.run_coveralls() + assert ex_info.value.args == ( + "Either GITHUB_TOKEN or COVERALLS_REPO_TOKEN must be set.", + ) + + def test_run_coveralls_github_token(self): + """Simple case when Coveralls.wear() returns some results.""" + with patch_os_envirion( + {"GITHUB_TOKEN": "TOKEN"} + ), patch_coveralls_wear() as m_wear, patch_log() as m_log: + entrypoint.run_coveralls() + assert m_wear.call_args_list == [mock.call()] + assert m_log.method_calls == [ + mock.call.info("Trying submitting coverage with service_name: github..."), + mock.call.info(m_wear.return_value), + ] From ad6816b27e024c7bf826e8c4bd78c17918afa433 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 00:43:40 +0200 Subject: [PATCH 04/16] Mount Docker volume Also use the same workdir as github. --- Makefile | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/Makefile b/Makefile index 0380589..8c518b4 100644 --- a/Makefile +++ b/Makefile @@ -12,6 +12,11 @@ COVERAGE=$(VIRTUAL_ENV)/bin/coverage BLACK=$(VIRTUAL_ENV)/bin/black SOURCES=src/ tests/ DOCKER_IMAGE_LINUX=andremiras/coveralls-python-action +DOCKER_WORKDIR=/github/workspace +DOCKER_WORKDIR_FLAG=--workdir $(DOCKER_WORKDIR) +DOCKER_VOLUME=$(CURDIR):$(DOCKER_WORKDIR) +DOCKER_VOLUME_FLAG=--volume $(DOCKER_VOLUME) + $(VIRTUAL_ENV): @@ -57,7 +62,7 @@ docker/build: docker build --tag=$(DOCKER_IMAGE_LINUX) . docker/run: - docker run -it --rm --env-file .env $(DOCKER_IMAGE_LINUX) + docker run -it --rm --env-file .env $(DOCKER_WORKDIR_FLAG) $(DOCKER_VOLUME_FLAG) $(DOCKER_IMAGE_LINUX) docker/run/shell: - docker run -it --rm --env-file .env --entrypoint /bin/sh $(DOCKER_IMAGE_LINUX) + docker run -it --rm --env-file .env $(DOCKER_WORKDIR_FLAG) $(DOCKER_VOLUME_FLAG) --entrypoint /bin/sh $(DOCKER_IMAGE_LINUX) From 4e7731f1f2b0978569145e9e3c5166a50da5db81 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 01:03:47 +0200 Subject: [PATCH 05/16] Cleaner logs --- src/entrypoint.py | 3 ++- tests/test_entrypoint.py | 8 +++++++- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/entrypoint.py b/src/entrypoint.py index 5d53fff..e974658 100755 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -48,7 +48,8 @@ def run_coveralls(): log.warning(e) if result is None: set_failed("Failed to submit coverage") - log.info(result) + log.debug(result) + log.info(result["url"]) def parse_args(): diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 8486180..421a2b6 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -32,12 +32,18 @@ def test_run_coveralls_no_token(self): def test_run_coveralls_github_token(self): """Simple case when Coveralls.wear() returns some results.""" + url = "https://coveralls.io/jobs/1234" with patch_os_envirion( {"GITHUB_TOKEN": "TOKEN"} ), patch_coveralls_wear() as m_wear, patch_log() as m_log: + m_wear.return_value = { + "message": "Job ##12.34", + "url": url, + } entrypoint.run_coveralls() assert m_wear.call_args_list == [mock.call()] assert m_log.method_calls == [ mock.call.info("Trying submitting coverage with service_name: github..."), - mock.call.info(m_wear.return_value), + mock.call.debug(m_wear.return_value), + mock.call.info(url), ] From bec7a0d4065a20e08c38b6d374b82d1430daa3e4 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 01:14:24 +0200 Subject: [PATCH 06/16] Handles --verbose argument --- .github/workflows/push.yml | 7 +++++++ README.md | 11 ++++++++++- action.yml | 5 +++++ 3 files changed, 22 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index d97e7a8..2f11a4d 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -26,3 +26,10 @@ jobs: env: GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + + - name: With --verbose + uses: ./ + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + with: + verbose: '--verbose' diff --git a/README.md b/README.md index 311c211..239272b 100644 --- a/README.md +++ b/README.md @@ -12,11 +12,20 @@ You simply need to set one of the following two environment variables: Also configure your `coverage.py` with `relative_files = True`. https://coverage.readthedocs.io/en/coverage-5.0.4/config.html#config-run-relative-files +```yaml +- uses: AndreMiras/coveralls-python-action@develop + env: + GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + with: + # Increase logger verbosity with `--verbose` + verbose: '' +``` ## Example usage Assuming you have a `make test` that runs coverage testing. The following will upload it to coveralls.io. -```yml +```yaml name: push on: [push, pull_request] diff --git a/action.yml b/action.yml index c4a2233..8593f21 100644 --- a/action.yml +++ b/action.yml @@ -4,6 +4,11 @@ description: 'Python coverage reports via coveralls.io' branding: color: 'green' icon: 'percent' +inputs: + verbose: + description: 'Increase logger verbosity with `--verbose`' runs: using: 'docker' image: 'Dockerfile' + args: + - ${{ inputs.verbose }} From b565df3bbc036fee494528f003039e743b2b4dd0 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 01:47:06 +0200 Subject: [PATCH 07/16] API refactoring leverage `with` keyword - ditch environment variables use arguments - token set by default - prepare ground for parallel build and webhook --- .github/workflows/push.yml | 27 +++++---- README.md | 26 +++++---- action.yml | 23 +++++++- src/entrypoint.py | 113 +++++++++++++++++++++++++++++-------- tests/test_entrypoint.py | 36 +++++++----- 5 files changed, 159 insertions(+), 66 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 2f11a4d..e1b7c8a 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -11,25 +11,24 @@ jobs: - name: Unit tests run: make test - - name: With GITHUB_TOKEN + - name: Default arguments uses: ./ - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - - name: With COVERALLS_REPO_TOKEN + - name: With --github-token GITHUB_TOKEN uses: ./ - env: - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + with: + github-token: ${{ secrets.GITHUB_TOKEN }} - - name: With GITHUB_TOKEN and COVERALLS_REPO_TOKEN + - name: With --github-token COVERALLS_REPO_TOKEN uses: ./ - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} + with: + github-token: ${{ secrets.COVERALLS_REPO_TOKEN }} - - name: With --verbose + - name: With --debug uses: ./ - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} with: - verbose: '--verbose' + debug: true + + # TODO + - name: With --parallel + uses: ./ diff --git a/README.md b/README.md index 239272b..672ddb3 100644 --- a/README.md +++ b/README.md @@ -6,20 +6,24 @@ GitHub Action for Python Coveralls.io ## Usage -You simply need to set one of the following two environment variables: -- `GITHUB_TOKEN` -- `COVERALLS_REPO_TOKEN` - -Also configure your `coverage.py` with `relative_files = True`. +Makes sure your `coverage.py` is configured with `relative_files = True`. https://coverage.readthedocs.io/en/coverage-5.0.4/config.html#config-run-relative-files + ```yaml - uses: AndreMiras/coveralls-python-action@develop - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_REPO_TOKEN: ${{ secrets.COVERALLS_REPO_TOKEN }} with: - # Increase logger verbosity with `--verbose` - verbose: '' + # The `GITHUB_TOKEN` or `COVERALLS_REPO_TOKEN` + # Default: ${{ github.token }} + github-token: '' + # Set to `true` if you are running parallel jobs, then use `parallel-finished: true` for the last action + # Default: false + parallel: '' + # Set to `true` for the last action when using `parallel: true` + # Default: false + parallel-finished: '' + # Set to true to increase logger verbosity + # Default: false + debug: '' ``` ## Example usage @@ -41,6 +45,4 @@ jobs: - name: Coveralls uses: AndreMiras/coveralls-python-action@develop - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} ``` diff --git a/action.yml b/action.yml index 8593f21..3268d28 100644 --- a/action.yml +++ b/action.yml @@ -5,10 +5,27 @@ branding: color: 'green' icon: 'percent' inputs: - verbose: - description: 'Increase logger verbosity with `--verbose`' + github-token: + description: 'The `GITHUB_TOKEN` or `COVERALLS_REPO_TOKEN`' + default: ${{ github.token }} + parallel: + description: 'Set to true if you are running parallel jobs, then use `parallel-finished: true` for the last action' + default: false + parallel-finished: + description: 'Set to true for the last action when using `parallel: true`' + default: false + debug: + description: 'Set to `true` to increase logger verbosity' + default: false runs: using: 'docker' image: 'Dockerfile' args: - - ${{ inputs.verbose }} + - --github-token + - ${{ inputs.github-token }} + - --parallel + - ${{ inputs.parallel }} + - --parallel-finished + - ${{ inputs.parallel-finished }} + - --debug + - ${{ inputs.debug }} diff --git a/src/entrypoint.py b/src/entrypoint.py index e974658..c3d72df 100755 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -2,10 +2,11 @@ import argparse import logging -import os import sys from enum import Enum +from unittest import mock +import requests from coveralls.api import Coveralls, CoverallsException log = logging.getLogger(__name__) @@ -21,55 +22,119 @@ def set_failed(message): sys.exit(ExitCode.FAILURE) -def run_coveralls(): - """Submits to coveralls using either GITHUB_TOKEN or COVERALLS_REPO_TOKEN.""" +def patch_os_environ(repo_token, parallel): + """ + Temporarily updates the environment variable to satisfy coveralls Python API. + That is because the coveralls package API consumes mostly environment variables. + """ + # https://github.com/coveralls-clients/coveralls-python/blob/2.0.0/coveralls/api.py#L146 + parallel = "true" if parallel else "" + environ = {"COVERALLS_REPO_TOKEN": repo_token, "COVERALLS_PARALLEL": parallel} + log.debug(f"Patching os.environ with: {environ}") + return mock.patch.dict("os.environ", environ) + + +def run_coveralls(repo_token, parallel=False): + """Submits job to coveralls.""" # note that coveralls.io "service_name" can either be: # - "github-actions" (local development?) # - "github" (from GitHub jobs?) - GITHUB_TOKEN = os.environ.get("GITHUB_TOKEN") - COVERALLS_REPO_TOKEN = os.environ.get("COVERALLS_REPO_TOKEN") - repo_token = GITHUB_TOKEN or COVERALLS_REPO_TOKEN - if GITHUB_TOKEN and COVERALLS_REPO_TOKEN: - log.warning("Both GITHUB_TOKEN and COVERALLS_REPO_TOKEN defined.") - assert repo_token, "Either GITHUB_TOKEN or COVERALLS_REPO_TOKEN must be set." # for some reasons the "service_name" can be one or the other # (depending on where it's ran from?) service_names = ("github", "github-actions") - kwargs = {"repo_token": repo_token} result = None for service_name in service_names: log.info(f"Trying submitting coverage with service_name: {service_name}...") - coveralls = Coveralls(service_name=service_name, **kwargs) - try: - result = coveralls.wear() - break - except CoverallsException as e: - log.warning(f"Failed submitting coverage with service_name: {service_name}") - log.warning(e) + with patch_os_environ(repo_token, parallel): + coveralls = Coveralls(service_name=service_name) + try: + result = coveralls.wear() + break + except CoverallsException as e: + log.warning( + f"Failed submitting coverage with service_name: {service_name}" + ) + log.warning(e) if result is None: set_failed("Failed to submit coverage") log.debug(result) log.info(result["url"]) +def post_webhook(repo_token, build_num): + """" + # https://docs.coveralls.io/parallel-build-webhook + coveralls_finish: + name: Coveralls finished webhook + needs: ["Tests"] + runs-on: ubuntu-latest + steps: + - name: webhook + env: + COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} + COVERALLS_SERVICE_NAME: github + TRAVIS_JOB_ID: ${{ github.ref }}:${{ github.sha }} + run: | + curl "https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN" \ + --data "payload[job_id]=$TRAVIS_JOB_ID&payload[status]=done" + """ + url = f"https://coveralls.io/webhook?repo_token={repo_token}" + # TRAVIS_JOB_ID: ${{ github.ref }}:${{ github.sha }} + # data = "payload[job_id]=$TRAVIS_JOB_ID&payload[status]=done" + data = { + "payload": { + # TODO job_id? + "build_num": build_num, + "status": "done", + } + } + requests.post(url, data) + + +def str_to_bool(value): + if isinstance(value, bool): + return value + if value.lower() in {"false", "f", "0", "no", "n"}: + return False + elif value.lower() in {"true", "t", "1", "yes", "y"}: + return True + raise ValueError(f"{value} is not a valid boolean value") + + def parse_args(): parser = argparse.ArgumentParser(description="Greetings") - parser.add_argument("who_to_greet", help="Who to greet", nargs="?", default="World") - parser.add_argument("--verbose", action="store_true") + parser.add_argument("--github-token", nargs=1, required=True) + parser.add_argument( + "--parallel", type=str_to_bool, nargs="?", const=True, default=False + ) + parser.add_argument( + "--parallel-finished", type=str_to_bool, nargs="?", const=True, default=False + ) + parser.add_argument( + "--debug", type=str_to_bool, nargs="?", const=True, default=False + ) return parser.parse_args() -def set_log_level(verbose): - level = logging.DEBUG if verbose else logging.INFO +def set_log_level(debug): + level = logging.DEBUG if debug else logging.INFO log.addHandler(logging.StreamHandler()) log.setLevel(level) def main(): args = parse_args() - verbose = args.verbose - set_log_level(verbose) - run_coveralls() + debug = args.debug + repo_token = args.github_token[0] + parallel = args.parallel + parallel_finished = args.parallel_finished + set_log_level(debug) + if parallel_finished: + # TODO + # post_webhook(repo_token, build_num) + pass + else: + run_coveralls(repo_token, parallel) if __name__ == "__main__": diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 421a2b6..ec84157 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -1,3 +1,4 @@ +import signal from unittest import mock import pytest @@ -17,33 +18,42 @@ def patch_log(): return mock.patch("entrypoint.log") +def patch_sys_argv(argv): + return mock.patch("sys.argv", argv) + + class TestEntryPoint: - def test_main(self): - with mock.patch("entrypoint.run_coveralls") as m_run_coveralls: + def test_main_no_token(self): + """Argument `--github-token` is required.""" + argv = ["src/entrypoint.py"] + with patch_sys_argv(argv), pytest.raises(SystemExit) as ex_info: entrypoint.main() - assert m_run_coveralls.call_args_list == [mock.call()] + assert ex_info.value.args == (signal.SIGINT.value,) - def test_run_coveralls_no_token(self): - with pytest.raises(AssertionError) as ex_info: - entrypoint.run_coveralls() - assert ex_info.value.args == ( - "Either GITHUB_TOKEN or COVERALLS_REPO_TOKEN must be set.", - ) + def test_main(self): + argv = ["src/entrypoint.py", "--github-token", "TOKEN"] + with patch_sys_argv(argv), mock.patch( + "entrypoint.run_coveralls" + ) as m_run_coveralls: + entrypoint.main() + assert m_run_coveralls.call_args_list == [mock.call("TOKEN", False)] def test_run_coveralls_github_token(self): """Simple case when Coveralls.wear() returns some results.""" url = "https://coveralls.io/jobs/1234" - with patch_os_envirion( - {"GITHUB_TOKEN": "TOKEN"} - ), patch_coveralls_wear() as m_wear, patch_log() as m_log: + with patch_coveralls_wear() as m_wear, patch_log() as m_log: m_wear.return_value = { "message": "Job ##12.34", "url": url, } - entrypoint.run_coveralls() + entrypoint.run_coveralls(repo_token="TOKEN") assert m_wear.call_args_list == [mock.call()] assert m_log.method_calls == [ mock.call.info("Trying submitting coverage with service_name: github..."), + mock.call.debug( + "Patching os.environ with: " + "{'COVERALLS_REPO_TOKEN': 'TOKEN', 'COVERALLS_PARALLEL': ''}" + ), mock.call.debug(m_wear.return_value), mock.call.info(url), ] From 7a24604441f488589cb81197d8f23a56d2d5456c Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 13:53:39 +0200 Subject: [PATCH 08/16] Draft webhook support --- .github/workflows/push.yml | 15 +++++- README.md | 21 ++++++-- src/entrypoint.py | 82 ++++++++++++++++++----------- tests/test_entrypoint.py | 103 +++++++++++++++++++++++++++++++++++++ 4 files changed, 187 insertions(+), 34 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index e1b7c8a..79091fb 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -29,6 +29,19 @@ jobs: with: debug: true - # TODO - name: With --parallel uses: ./ + with: + parallel: true + + coveralls_finish: + needs: test + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v2 + - name: Coveralls Finished + uses: ./ + with: + github-token: ${{ secrets.COVERALLS_REPO_TOKEN }} + parallel-finished: true + debug: true diff --git a/README.md b/README.md index 672ddb3..a07c785 100644 --- a/README.md +++ b/README.md @@ -12,16 +12,17 @@ https://coverage.readthedocs.io/en/coverage-5.0.4/config.html#config-run-relativ ```yaml - uses: AndreMiras/coveralls-python-action@develop with: - # The `GITHUB_TOKEN` or `COVERALLS_REPO_TOKEN` + # The `GITHUB_TOKEN` or `COVERALLS_REPO_TOKEN`. # Default: ${{ github.token }} github-token: '' - # Set to `true` if you are running parallel jobs, then use `parallel-finished: true` for the last action + # Set to `true` if you are running parallel jobs, then use `parallel-finished: true` for the last action. # Default: false parallel: '' - # Set to `true` for the last action when using `parallel: true` + # Set to `true` for the last action when using `parallel: true`. + # Note this phase requires `github-token: ${{ secrets.COVERALLS_REPO_TOKEN }}`. # Default: false parallel-finished: '' - # Set to true to increase logger verbosity + # Set to true to increase logger verbosity. # Default: false debug: '' ``` @@ -45,4 +46,16 @@ jobs: - name: Coveralls uses: AndreMiras/coveralls-python-action@develop + with: + parallel: true + + coveralls_finish: + needs: test + runs-on: ubuntu-latest + steps: + - name: Coveralls Finished + uses: AndreMiras/coveralls-python-action@develop + with: + parallel-finished: true + github-token: ${{ secrets.COVERALLS_REPO_TOKEN }} ``` diff --git a/src/entrypoint.py b/src/entrypoint.py index c3d72df..a0dd16c 100755 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -2,6 +2,7 @@ import argparse import logging +import os import sys from enum import Enum from unittest import mock @@ -61,34 +62,58 @@ def run_coveralls(repo_token, parallel=False): log.info(result["url"]) -def post_webhook(repo_token, build_num): +def get_github_sha(): + """e.g. ffac537e6cbbf934b08745a378932722df287a53""" + return os.environ.get("GITHUB_SHA") + + +def get_github_ref(): + """ + The branch or tag ref that triggered the workflow. + For example, refs/heads/feature-branch-1. + If neither a branch or tag is available for the variable will not exist. + - for pull_request events: refs/pull//merge + - for push event: refs/heads/ + https://help.github.com/en/actions/configuring-and-managing-workflows/using-environment-variables + """ + return os.environ.get("GITHUB_REF") + + +def get_pull_request_number(github_ref): + """ + >>> get_pull_request_number("refs/pull//merge") + "" + """ + return github_ref.split("/")[2] + + +def is_pull_request(github_ref): + return github_ref and github_ref.startswith("refs/pull/") + + +def get_build_number(github_sha, github_ref): + build_number = github_sha + if is_pull_request(github_ref): + pull_request_number = get_pull_request_number(github_ref) + build_number = f"{github_sha}-PR-{pull_request_number}" + return build_number + + +def post_webhook(repo_token): """" - # https://docs.coveralls.io/parallel-build-webhook - coveralls_finish: - name: Coveralls finished webhook - needs: ["Tests"] - runs-on: ubuntu-latest - steps: - - name: webhook - env: - COVERALLS_REPO_TOKEN: ${{ secrets.GITHUB_TOKEN }} - COVERALLS_SERVICE_NAME: github - TRAVIS_JOB_ID: ${{ github.ref }}:${{ github.sha }} - run: | - curl "https://coveralls.io/webhook?repo_token=$COVERALLS_REPO_TOKEN" \ - --data "payload[job_id]=$TRAVIS_JOB_ID&payload[status]=done" + Note for this call, the repo token is always `COVERALLS_REPO_TOKEN`. + It cannot be the `GITHUB_TOKEN`. + https://docs.coveralls.io/parallel-build-webhook """ - url = f"https://coveralls.io/webhook?repo_token={repo_token}" - # TRAVIS_JOB_ID: ${{ github.ref }}:${{ github.sha }} - # data = "payload[job_id]=$TRAVIS_JOB_ID&payload[status]=done" - data = { - "payload": { - # TODO job_id? - "build_num": build_num, - "status": "done", - } - } - requests.post(url, data) + url = "https://coveralls.io/webhook" + build_num = get_build_number(get_github_sha(), get_github_ref()) + params = {"repo_token": repo_token} + json = {"payload": {"build_num": build_num, "status": "done"}} + log.debug(f'requests.post("{url}", params={params}, json={json})') + response = requests.post(url, params=params, json=json) + response.raise_for_status() + log.debug(f"response.json(): {response.json()}") + assert response.json() == {"done": True}, response.json() def str_to_bool(value): @@ -129,10 +154,9 @@ def main(): parallel = args.parallel parallel_finished = args.parallel_finished set_log_level(debug) + log.debug(f"args: {args}") if parallel_finished: - # TODO - # post_webhook(repo_token, build_num) - pass + post_webhook(repo_token) else: run_coveralls(repo_token, parallel) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index ec84157..91a2518 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -22,6 +22,10 @@ def patch_sys_argv(argv): return mock.patch("sys.argv", argv) +def patch_requests_post(): + return mock.patch("entrypoint.requests.post") + + class TestEntryPoint: def test_main_no_token(self): """Argument `--github-token` is required.""" @@ -57,3 +61,102 @@ def test_run_coveralls_github_token(self): mock.call.debug(m_wear.return_value), mock.call.info(url), ] + + def test_get_build_number(self): + github_sha = "ffac537e6cbbf934b08745a378932722df287a53" + github_ref = "refs/pull/123/merge" + assert ( + entrypoint.get_build_number(github_sha, github_ref) + == "ffac537e6cbbf934b08745a378932722df287a53-PR-123" + ) + github_ref = "refs/heads/feature-branch-1" + assert ( + entrypoint.get_build_number(github_sha, github_ref) + == "ffac537e6cbbf934b08745a378932722df287a53" + ) + github_ref = None + assert ( + entrypoint.get_build_number(github_sha, github_ref) + == "ffac537e6cbbf934b08745a378932722df287a53" + ) + + def test_post_webhook(self): + """ + Tests different uses cases: + 1) default, no environment variable + 2) only `GITHUB_SHA` is set + 3) `GITHUB_REF` is a branch + 4) `GITHUB_REF` is a pull request + """ + repo_token = "TOKEN" + # 1) default, no environment variable + environ = {} + with patch_requests_post() as m_post, patch_os_envirion(environ): + m_post.return_value.json.return_value = {"done": True} + entrypoint.post_webhook(repo_token) + assert m_post.call_args_list == [ + mock.call( + "https://coveralls.io/webhook", + params={"repo_token": "TOKEN"}, + json={"payload": {"build_num": None, "status": "done"}}, + ) + ] + # 2) only `GITHUB_SHA` is set + environ = { + "GITHUB_SHA": "ffac537e6cbbf934b08745a378932722df287a53", + } + with patch_requests_post() as m_post, patch_os_envirion(environ): + m_post.return_value.json.return_value = {"done": True} + entrypoint.post_webhook(repo_token) + assert m_post.call_args_list == [ + mock.call( + "https://coveralls.io/webhook", + params={"repo_token": "TOKEN"}, + json={ + "payload": { + "build_num": "ffac537e6cbbf934b08745a378932722df287a53", + "status": "done", + } + }, + ) + ] + # 3) `GITHUB_REF` is a branch + environ = { + "GITHUB_SHA": "ffac537e6cbbf934b08745a378932722df287a53", + "GITHUB_REF": "refs/heads/feature-branch-1", + } + with patch_requests_post() as m_post, patch_os_envirion(environ): + m_post.return_value.json.return_value = {"done": True} + entrypoint.post_webhook(repo_token) + assert m_post.call_args_list == [ + mock.call( + "https://coveralls.io/webhook", + params={"repo_token": "TOKEN"}, + json={ + "payload": { + "build_num": "ffac537e6cbbf934b08745a378932722df287a53", + "status": "done", + } + }, + ) + ] + # 4) `GITHUB_REF` is a pull request + environ = { + "GITHUB_SHA": "ffac537e6cbbf934b08745a378932722df287a53", + "GITHUB_REF": "refs/pull/123/merge", + } + with patch_requests_post() as m_post, patch_os_envirion(environ): + m_post.return_value.json.return_value = {"done": True} + entrypoint.post_webhook(repo_token) + assert m_post.call_args_list == [ + mock.call( + "https://coveralls.io/webhook", + params={"repo_token": "TOKEN"}, + json={ + "payload": { + "build_num": "ffac537e6cbbf934b08745a378932722df287a53-PR-123", + "status": "done", + } + }, + ) + ] From e55199143616afee318427240587c8559a558a1d Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 16:08:25 +0200 Subject: [PATCH 09/16] test_str_to_bool() --- tests/test_entrypoint.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 91a2518..7b5168a 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -160,3 +160,40 @@ def test_post_webhook(self): }, ) ] + + @pytest.mark.parametrize( + "value,expected", + [ + ("false", False), + ("f", False), + ("0", False), + ("no", False), + ("n", False), + ("true", True), + ("t", True), + ("1", True), + ("yes", True), + ("y", True), + ], + ) + def test_str_to_bool(self, value, expected): + """Possible recognised values.""" + assert entrypoint.str_to_bool(value) is expected + + @pytest.mark.parametrize( + "value", ["", "yesn't"], + ) + def test_str_to_bool_value_error(self, value): + """Other unrecognised string values raise a `ValueError`.""" + with pytest.raises(ValueError) as ex_info: + entrypoint.str_to_bool(value) + assert ex_info.value.args == (f"{value} is not a valid boolean value",) + + @pytest.mark.parametrize( + "value", [None, 0], + ) + def test_str_to_bool_attribute_error(self, value): + """Other unrecognised non-string values raise an `AttributeError`.""" + with pytest.raises(AttributeError) as ex_info: + entrypoint.str_to_bool(value) + assert ex_info.value.args[0].endswith(" object has no attribute 'lower'") From 0a769a8103cfbab68c3ebef1cc83761988034ded Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 16:27:33 +0200 Subject: [PATCH 10/16] test_post_webhook_error() --- tests/test_entrypoint.py | 39 +++++++++++++++++++++++++++++---------- 1 file changed, 29 insertions(+), 10 deletions(-) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 7b5168a..68d293b 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -22,8 +22,11 @@ def patch_sys_argv(argv): return mock.patch("sys.argv", argv) -def patch_requests_post(): - return mock.patch("entrypoint.requests.post") +def patch_requests_post(json_response=None): + new_mock = mock.Mock() + if json_response: + new_mock.return_value.json.return_value = json_response + return mock.patch("entrypoint.requests.post", new_mock) class TestEntryPoint: @@ -89,10 +92,10 @@ def test_post_webhook(self): 4) `GITHUB_REF` is a pull request """ repo_token = "TOKEN" + json_response = {"done": True} # 1) default, no environment variable environ = {} - with patch_requests_post() as m_post, patch_os_envirion(environ): - m_post.return_value.json.return_value = {"done": True} + with patch_requests_post(json_response) as m_post, patch_os_envirion(environ): entrypoint.post_webhook(repo_token) assert m_post.call_args_list == [ mock.call( @@ -105,8 +108,7 @@ def test_post_webhook(self): environ = { "GITHUB_SHA": "ffac537e6cbbf934b08745a378932722df287a53", } - with patch_requests_post() as m_post, patch_os_envirion(environ): - m_post.return_value.json.return_value = {"done": True} + with patch_requests_post(json_response) as m_post, patch_os_envirion(environ): entrypoint.post_webhook(repo_token) assert m_post.call_args_list == [ mock.call( @@ -125,8 +127,7 @@ def test_post_webhook(self): "GITHUB_SHA": "ffac537e6cbbf934b08745a378932722df287a53", "GITHUB_REF": "refs/heads/feature-branch-1", } - with patch_requests_post() as m_post, patch_os_envirion(environ): - m_post.return_value.json.return_value = {"done": True} + with patch_requests_post(json_response) as m_post, patch_os_envirion(environ): entrypoint.post_webhook(repo_token) assert m_post.call_args_list == [ mock.call( @@ -145,8 +146,7 @@ def test_post_webhook(self): "GITHUB_SHA": "ffac537e6cbbf934b08745a378932722df287a53", "GITHUB_REF": "refs/pull/123/merge", } - with patch_requests_post() as m_post, patch_os_envirion(environ): - m_post.return_value.json.return_value = {"done": True} + with patch_requests_post(json_response) as m_post, patch_os_envirion(environ): entrypoint.post_webhook(repo_token) assert m_post.call_args_list == [ mock.call( @@ -161,6 +161,25 @@ def test_post_webhook(self): ) ] + def test_post_webhook_error(self): + """Coveralls.io json error response should raise an exception.""" + repo_token = "TOKEN" + json_response = {"error": "Invalid repo token"} + # 1) default, no environment variable + environ = {} + with patch_requests_post(json_response) as m_post, patch_os_envirion( + environ + ), pytest.raises(AssertionError) as ex_info: + entrypoint.post_webhook(repo_token) + assert m_post.call_args_list == [ + mock.call( + "https://coveralls.io/webhook", + params={"repo_token": "TOKEN"}, + json={"payload": {"build_num": None, "status": "done"}}, + ) + ] + assert ex_info.value.args == (json_response,) + @pytest.mark.parametrize( "value,expected", [ From 85fbcaadec503cf19ec9cb1eb5fc48787438dad6 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 16:37:48 +0200 Subject: [PATCH 11/16] try_main() --- src/entrypoint.py | 6 +++++- tests/test_entrypoint.py | 8 ++++++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/entrypoint.py b/src/entrypoint.py index a0dd16c..925f625 100755 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -161,8 +161,12 @@ def main(): run_coveralls(repo_token, parallel) -if __name__ == "__main__": +def try_main(): try: main() except Exception as e: set_failed(e) + + +if __name__ == "__main__": + try_main() diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 68d293b..298451c 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -45,6 +45,14 @@ def test_main(self): entrypoint.main() assert m_run_coveralls.call_args_list == [mock.call("TOKEN", False)] + def test_try_main(self): + with mock.patch( + "entrypoint.main", side_effect=Exception + ) as m_main, pytest.raises(SystemExit) as ex_info: + entrypoint.try_main() + assert m_main.call_args_list == [mock.call()] + assert ex_info.value.args == (entrypoint.ExitCode.FAILURE,) + def test_run_coveralls_github_token(self): """Simple case when Coveralls.wear() returns some results.""" url = "https://coveralls.io/jobs/1234" From cd28179dd00b84d85c87dae84b9f50d2b3fc8187 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 16:49:46 +0200 Subject: [PATCH 12/16] test_main_parallel_finished() --- tests/test_entrypoint.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index 298451c..a7483ac 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -45,6 +45,14 @@ def test_main(self): entrypoint.main() assert m_run_coveralls.call_args_list == [mock.call("TOKEN", False)] + def test_main_parallel_finished(self): + argv = ["src/entrypoint.py", "--github-token", "TOKEN", "--parallel-finished"] + with patch_sys_argv(argv), mock.patch( + "entrypoint.post_webhook" + ) as m_post_webhook: + entrypoint.main() + assert m_post_webhook.call_args_list == [mock.call("TOKEN")] + def test_try_main(self): with mock.patch( "entrypoint.main", side_effect=Exception @@ -191,11 +199,13 @@ def test_post_webhook_error(self): @pytest.mark.parametrize( "value,expected", [ + (False, False), ("false", False), ("f", False), ("0", False), ("no", False), ("n", False), + (True, True), ("true", True), ("t", True), ("1", True), From 68357a67db2bfb60e91ba8e9c315d6eeded80167 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 17:11:52 +0200 Subject: [PATCH 13/16] test_run_coveralls_wear_error*() - test_run_coveralls_wear_error_once() - test_run_coveralls_wear_error_twice() --- README.md | 2 +- src/entrypoint.py | 2 +- tests/test_entrypoint.py | 42 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 44 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index a07c785..180d3aa 100644 --- a/README.md +++ b/README.md @@ -27,7 +27,7 @@ https://coverage.readthedocs.io/en/coverage-5.0.4/config.html#config-run-relativ debug: '' ``` -## Example usage +## Usage example Assuming you have a `make test` that runs coverage testing. The following will upload it to coveralls.io. ```yaml diff --git a/src/entrypoint.py b/src/entrypoint.py index 925f625..443bbe9 100755 --- a/src/entrypoint.py +++ b/src/entrypoint.py @@ -169,4 +169,4 @@ def try_main(): if __name__ == "__main__": - try_main() + try_main() # pragma: no cover diff --git a/tests/test_entrypoint.py b/tests/test_entrypoint.py index a7483ac..05a5f5c 100644 --- a/tests/test_entrypoint.py +++ b/tests/test_entrypoint.py @@ -2,6 +2,7 @@ from unittest import mock import pytest +from coveralls.api import CoverallsException import entrypoint @@ -81,6 +82,47 @@ def test_run_coveralls_github_token(self): mock.call.info(url), ] + def test_run_coveralls_wear_error_once(self): + """On Coveralls.wear() error we should try another `service_name`.""" + url = "https://coveralls.io/jobs/1234" + side_effect = ( + CoverallsException("Error"), + {"message": "Job ##12.34", "url": url}, + ) + with patch_coveralls_wear() as m_wear, patch_log() as m_log: + m_wear.side_effect = side_effect + entrypoint.run_coveralls(repo_token="TOKEN") + assert m_wear.call_args_list == [mock.call(), mock.call()] + assert m_log.method_calls == [ + mock.call.info("Trying submitting coverage with service_name: github..."), + mock.call.debug( + "Patching os.environ with: " + "{'COVERALLS_REPO_TOKEN': 'TOKEN', 'COVERALLS_PARALLEL': ''}" + ), + mock.call.warning("Failed submitting coverage with service_name: github"), + mock.call.warning(side_effect[0]), + mock.call.info( + "Trying submitting coverage with service_name: github-actions..." + ), + mock.call.debug( + "Patching os.environ with: " + "{'COVERALLS_REPO_TOKEN': 'TOKEN', 'COVERALLS_PARALLEL': ''}" + ), + mock.call.debug(side_effect[1]), + mock.call.info(url), + ] + + def test_run_coveralls_wear_error_twice(self): + """Exits with error code if Coveralls.wear() fails twice.""" + side_effect = ( + CoverallsException("Error 1"), + CoverallsException("Error 2"), + ) + with patch_coveralls_wear() as m_wear, pytest.raises(SystemExit) as ex_info: + m_wear.side_effect = side_effect + entrypoint.run_coveralls(repo_token="TOKEN") + assert ex_info.value.args == (entrypoint.ExitCode.FAILURE,) + def test_get_build_number(self): github_sha = "ffac537e6cbbf934b08745a378932722df287a53" github_ref = "refs/pull/123/merge" From c2c6af368bc7624344cac6f7f6ce3d24f1f929b9 Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 17:26:25 +0200 Subject: [PATCH 14/16] Instructions update --- README.md | 47 +++++++++++++++++++++++------------------------ action.yml | 8 ++++---- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/README.md b/README.md index 180d3aa..d6402f1 100644 --- a/README.md +++ b/README.md @@ -3,33 +3,13 @@ [![push](https://github.com/AndreMiras/coveralls-python-action/workflows/push/badge.svg?branch=develop)](https://github.com/AndreMiras/coveralls-python-action/actions?query=workflow%3Apush) [![Coverage Status](https://coveralls.io/repos/github/AndreMiras/coveralls-python-action/badge.svg?branch=develop)](https://coveralls.io/github/AndreMiras/coveralls-python-action?branch=develop) -GitHub Action for Python Coveralls.io +GitHub Action for Python [Coveralls.io](https://coveralls.io/) ## Usage -Makes sure your `coverage.py` is configured with `relative_files = True`. -https://coverage.readthedocs.io/en/coverage-5.0.4/config.html#config-run-relative-files +First make sure your `coverage.py` is configured with [`relative_files = True`](https://coverage.readthedocs.io/en/coverage-5.0.4/config.html#config-run-relative-files). -```yaml -- uses: AndreMiras/coveralls-python-action@develop - with: - # The `GITHUB_TOKEN` or `COVERALLS_REPO_TOKEN`. - # Default: ${{ github.token }} - github-token: '' - # Set to `true` if you are running parallel jobs, then use `parallel-finished: true` for the last action. - # Default: false - parallel: '' - # Set to `true` for the last action when using `parallel: true`. - # Note this phase requires `github-token: ${{ secrets.COVERALLS_REPO_TOKEN }}`. - # Default: false - parallel-finished: '' - # Set to true to increase logger verbosity. - # Default: false - debug: '' -``` - -## Usage example -Assuming you have a `make test` that runs coverage testing. -The following will upload it to coveralls.io. +Then assuming you have a `make test` that runs coverage testing. +The following workflow will upload it to coveralls.io. ```yaml name: push on: [push, pull_request] @@ -59,3 +39,22 @@ jobs: parallel-finished: true github-token: ${{ secrets.COVERALLS_REPO_TOKEN }} ``` + +## Configuration +```yaml +- uses: AndreMiras/coveralls-python-action@develop + with: + # The `GITHUB_TOKEN` or `COVERALLS_REPO_TOKEN`. + # Default: ${{ github.token }} + github-token: '' + # Set to `true` if you are using parallel jobs, then use `parallel-finished: true` for the last action. + # Default: false + parallel: '' + # Set to `true` for the last action when using `parallel: true`. + # Note this phase requires `github-token: ${{ secrets.COVERALLS_REPO_TOKEN }}`. + # Default: false + parallel-finished: '' + # Set to true to increase logger verbosity. + # Default: false + debug: '' +``` diff --git a/action.yml b/action.yml index 3268d28..09bde97 100644 --- a/action.yml +++ b/action.yml @@ -6,16 +6,16 @@ branding: icon: 'percent' inputs: github-token: - description: 'The `GITHUB_TOKEN` or `COVERALLS_REPO_TOKEN`' + description: 'The `GITHUB_TOKEN` or `COVERALLS_REPO_TOKEN`.' default: ${{ github.token }} parallel: - description: 'Set to true if you are running parallel jobs, then use `parallel-finished: true` for the last action' + description: 'Set to true if you are using parallel jobs, then use `parallel-finished: true` for the last action.' default: false parallel-finished: - description: 'Set to true for the last action when using `parallel: true`' + description: 'Set to true for the last action when using `parallel: true`.' default: false debug: - description: 'Set to `true` to increase logger verbosity' + description: 'Set to `true` to increase logger verbosity.' default: false runs: using: 'docker' From ed88bd1eb0dae9cc7e85297e4f79c9dc9550838a Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 17:35:38 +0200 Subject: [PATCH 15/16] CHANGELOG.md --- CHANGELOG.md | 13 +++++++++++++ 1 file changed, 13 insertions(+) create mode 100644 CHANGELOG.md diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 0000000..890cad0 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,13 @@ +# Change Log + +## [Unreleased] + + - Leverages `with` keyword to configure the action + - Adds parallel support + - Adds webhook support + - Increases test coverage to 100% + + +## [v20200411] + + - Initial release From 8328d942624ad910c06b0bafa159da81116f305c Mon Sep 17 00:00:00 2001 From: Andre Miras Date: Sun, 12 Apr 2020 17:37:02 +0200 Subject: [PATCH 16/16] v20200412 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 890cad0..aa0bd52 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,6 +1,6 @@ # Change Log -## [Unreleased] +## [v20200412] - Leverages `with` keyword to configure the action - Adds parallel support