diff --git a/README.md b/README.md index 1e4e7de2..a861086e 100644 --- a/README.md +++ b/README.md @@ -59,6 +59,36 @@ These files include: See [an example](https://github.com/py-cov-action/python-coverage-comment-action-v3-example) +### Determining the mode + +By default, the action will attempt to pick the appropriate mode based on the +current branch, whether or not it's in a pull request, and if that pull request +is open or closed. This frequently results in the correct action taking place, +but is only a heuristic. If you need more precise control, you should specify +the `ACTIVITY` parameter to directly choose the mode. It may be one of: + +- `process_pr`, to select [PR mode](#pr-mode) +- `save_coverage_data_files`, to select [Default branch mode](#default-branch-mode) +- `post_comment`, to select [Commenting on the PR on the `push` event](#commenting-on-the-pr-on-the-push-event) + +Combining this with [Github's Expressions] +(https://docs.github.com/en/actions/reference/workflows-and-actions/expressions) you can +build out the the custom handling needed. For example: + +```yaml + - name: Coverage comment + id: coverage_comment + uses: py-cov-action/python-coverage-comment-action@v3 + with: + GITHUB_TOKEN: ${{ github.token }} + activity: "${{ github.event_name == 'push' && 'save_coverage_data_files' || 'process_pr' }}" + + # or + + with: + activity: "${{ (github.event_name == 'push' && github.ref_name == 'main') && 'save_coverage_data_files' || 'process_pr' }}" +``` + ## Usage ### Setup @@ -498,6 +528,10 @@ Usage may look like this # Deprecated, see https://docs.github.com/en/actions/monitoring-and-troubleshooting-workflows/enabling-debug-logging VERBOSE: false + + # The specific activity that should be taken on this event, see + # [Determining the mode](#determining-the-mode) above. + ACTIVITY: "" ``` ### Commenting on the PR on the `push` event diff --git a/coverage_comment/activity.py b/coverage_comment/activities.py similarity index 74% rename from coverage_comment/activity.py rename to coverage_comment/activities.py index 9444363f..dab3fb3d 100644 --- a/coverage_comment/activity.py +++ b/coverage_comment/activities.py @@ -8,20 +8,32 @@ from __future__ import annotations +from enum import Enum + + +class Activity(Enum): + PROCESS_PR = "process_pr" + POST_COMMENT = "post_comment" + SAVE_COVERAGE_DATA_FILES = "save_coverage_data_files" + class ActivityNotFound(Exception): pass +class ActivityConfigError(Exception): + pass + + def find_activity( event_name: str, is_default_branch: bool, event_type: str | None, is_pr_merged: bool, -) -> str: +) -> Activity: """Find the activity to perform based on the event type and payload.""" if event_name == "workflow_run": - return "post_comment" + return Activity.POST_COMMENT if ( (event_name == "push" and is_default_branch) @@ -31,9 +43,9 @@ def find_activity( ): if event_name == "pull_request" and event_type == "closed" and not is_pr_merged: raise ActivityNotFound - return "save_coverage_data_files" + return Activity.SAVE_COVERAGE_DATA_FILES if event_name not in {"pull_request", "push", "merge_group"}: raise ActivityNotFound - return "process_pr" + return Activity.PROCESS_PR diff --git a/coverage_comment/main.py b/coverage_comment/main.py index 1f377eb0..20ee229c 100644 --- a/coverage_comment/main.py +++ b/coverage_comment/main.py @@ -7,7 +7,7 @@ import httpx -from coverage_comment import activity as activity_module +from coverage_comment import activities as activity_module from coverage_comment import ( comment_file, communication, @@ -39,22 +39,26 @@ def action( github=gh, repository=config.GITHUB_REPOSITORY ) try: - activity = activity_module.find_activity( - event_name=event_name, - is_default_branch=repo_info.is_default_branch(ref=config.GITHUB_REF), - event_type=config.GITHUB_EVENT_TYPE, - is_pr_merged=config.IS_PR_MERGED, - ) + activity = config.ACTIVITY + if not activity: + activity = activity_module.find_activity( + event_name=event_name, + is_default_branch=repo_info.is_default_branch(ref=config.GITHUB_REF), + event_type=config.GITHUB_EVENT_TYPE, + is_pr_merged=config.IS_PR_MERGED, + ) except activity_module.ActivityNotFound: log.error( - 'This action has only been designed to work for "pull_request", "push", ' - f'"workflow_run", "schedule" or "merge_group" actions, not "{event_name}". Because there ' - "are security implications. If you have a different usecase, please open an issue, " - "we'll be glad to add compatibility." + "This action's default behavior is to determine the appropriate " + "mode based on the current branch, whether or not it's in a pull " + "request, and if that pull request is open or closed. This " + "frequently results in the correct action taking place, but is " + "only a heuristic. If you need more precise control, you should " + 'specify the "ACTIVITY" parameter as described in the documentation.' ) return 1 - if activity == "save_coverage_data_files": + if activity == activity_module.Activity.SAVE_COVERAGE_DATA_FILES: return save_coverage_data_files( config=config, git=git, @@ -62,7 +66,7 @@ def action( repo_info=repo_info, ) - elif activity == "process_pr": + elif activity == activity_module.Activity.PROCESS_PR: return process_pr( config=config, gh=gh, @@ -70,7 +74,7 @@ def action( ) else: - # activity == "post_comment": + # activity == activity_module.Activity.POST_COMMENT: return post_comment( config=config, gh=gh, diff --git a/coverage_comment/settings.py b/coverage_comment/settings.py index 8d122679..4f7aa9ae 100644 --- a/coverage_comment/settings.py +++ b/coverage_comment/settings.py @@ -10,7 +10,7 @@ from coverage_comment import log -from . import json +from . import activities, json class MissingEnvironmentVariable(Exception): @@ -67,6 +67,7 @@ class Config: ANNOTATION_TYPE: str = "warning" MAX_FILES_IN_COMMENT: int = 25 USE_GH_PAGES_HTML_URL: bool = False + ACTIVITY: activities.Activity | None = None VERBOSE: bool = False # Only for debugging, not exposed in the action: FORCE_WORKFLOW_RUN: bool = False @@ -132,6 +133,10 @@ def clean_github_output(cls, value: str) -> pathlib.Path: def clean_github_event_path(cls, value: str) -> pathlib.Path: return pathlib.Path(value) + @classmethod + def clean_activity(cls, activity: str) -> activities.Activity: + return activities.Activity(activity) + @property def GITHUB_PR_NUMBER(self) -> int | None: # "refs/pull/2/merge" @@ -181,7 +186,7 @@ def FINAL_COVERAGE_DATA_BRANCH(self): # os.environ is, and just saying `dict[str, str]` is not enough to make # mypy happy @classmethod - def from_environ(cls, environ: MutableMapping[str, str]) -> Config: + def from_environ(cls, environ: MutableMapping[str, Any]) -> Config: possible_variables = [e for e in inspect.signature(cls).parameters] config: dict[str, Any] = { k: v for k, v in environ.items() if k in possible_variables diff --git a/tests/integration/test_main.py b/tests/integration/test_main.py index 57a2620c..04b184c5 100644 --- a/tests/integration/test_main.py +++ b/tests/integration/test_main.py @@ -5,7 +5,7 @@ import pytest -from coverage_comment import main +from coverage_comment import activities, main DIFF_STDOUT = """diff --git a/foo.py b/foo.py index 6c08c94..b65c612 100644 @@ -61,7 +61,76 @@ def test_action__invalid_event_name(session, push_config, in_integration_env, ge ) assert result == 1 - assert get_logs("ERROR", "This action has only been designed to work for") + assert get_logs("ERROR", "This action's default behavior is to determine") + + +def test_action__explicit_activity( + session, workflow_run_config, in_integration_env, get_logs, zip_bytes +): + session.register( + "GET", + "/repos/py-cov-action/foobar", + json={"default_branch": "main", "visibility": "public"}, + ) + session.register("GET", "/user", json={"login": "foo"}) + session.register( + "GET", + "/repos/py-cov-action/foobar/actions/runs/123", + json={ + "head_branch": "branch", + "head_repository": {"owner": {"login": "bar/repo-name"}}, + }, + ) + + session.register( + "GET", + "/repos/py-cov-action/foobar/pulls", + match_params={ + "head": "bar/repo-name:branch", + "sort": "updated", + "direction": "desc", + "state": "open", + }, + json=[{"number": 456}], + ) + + session.register( + "GET", + "/repos/py-cov-action/foobar/actions/runs/123/artifacts", + match_params={"page": "1"}, + json={ + "artifacts": [{"name": "python-coverage-comment-action", "id": 789}], + "total_count": 1, + }, + ) + + session.register( + "GET", + "/repos/py-cov-action/foobar/actions/artifacts/789/zip", + content=zip_bytes( + filename="python-coverage-comment-action.txt", content="Hey!" + ), + ) + + session.register("GET", "/repos/py-cov-action/foobar/issues/456/comments", json=[]) + + session.register( + "POST", + "/repos/py-cov-action/foobar/issues/456/comments", + json={"body": "Hey!"}, + ) + + result = main.action( + config=workflow_run_config( + GITHUB_EVENT_NAME="something_else", + ACTIVITY=activities.Activity.POST_COMMENT, + ), + github_session=session, + http_session=session, + git=None, + ) + + assert result == 0 def get_expected_output( diff --git a/tests/unit/test_activity.py b/tests/unit/test_activity.py index b67627b4..b598369c 100644 --- a/tests/unit/test_activity.py +++ b/tests/unit/test_activity.py @@ -2,26 +2,38 @@ import pytest -from coverage_comment import activity +from coverage_comment import activities @pytest.mark.parametrize( "event_name, is_default_branch, event_type, is_pr_merged, expected_activity", [ - ("workflow_run", True, None, False, "post_comment"), - ("push", True, None, False, "save_coverage_data_files"), - ("push", False, None, False, "process_pr"), - ("pull_request", True, "closed", True, "save_coverage_data_files"), - ("pull_request", True, None, False, "process_pr"), - ("pull_request", False, None, False, "process_pr"), - ("schedule", False, None, False, "save_coverage_data_files"), - ("merge_group", False, None, False, "save_coverage_data_files"), + ("workflow_run", True, None, False, activities.Activity.POST_COMMENT), + ("push", True, None, False, activities.Activity.SAVE_COVERAGE_DATA_FILES), + ("push", False, None, False, activities.Activity.PROCESS_PR), + ( + "pull_request", + True, + "closed", + True, + activities.Activity.SAVE_COVERAGE_DATA_FILES, + ), + ("pull_request", True, None, False, activities.Activity.PROCESS_PR), + ("pull_request", False, None, False, activities.Activity.PROCESS_PR), + ("schedule", False, None, False, activities.Activity.SAVE_COVERAGE_DATA_FILES), + ( + "merge_group", + False, + None, + False, + activities.Activity.SAVE_COVERAGE_DATA_FILES, + ), ], ) def test_find_activity( event_name, is_default_branch, event_type, is_pr_merged, expected_activity ): - result = activity.find_activity( + result = activities.find_activity( event_name=event_name, is_default_branch=is_default_branch, event_type=event_type, @@ -31,8 +43,8 @@ def test_find_activity( def test_find_activity_not_found(): - with pytest.raises(activity.ActivityNotFound): - activity.find_activity( + with pytest.raises(activities.ActivityNotFound): + activities.find_activity( event_name="not_found", is_default_branch=False, event_type="not_found", @@ -41,8 +53,8 @@ def test_find_activity_not_found(): def test_find_activity_pr_closed_not_merged(): - with pytest.raises(activity.ActivityNotFound): - activity.find_activity( + with pytest.raises(activities.ActivityNotFound): + activities.find_activity( event_name="pull_request", is_default_branch=False, event_type="closed", diff --git a/tests/unit/test_settings.py b/tests/unit/test_settings.py index 314a1dcf..21a53544 100644 --- a/tests/unit/test_settings.py +++ b/tests/unit/test_settings.py @@ -7,7 +7,7 @@ import pytest -from coverage_comment import settings +from coverage_comment import activities, settings @pytest.mark.parametrize("path", ["a", "a/b/.."]) @@ -51,6 +51,7 @@ def test_config__from_environ__ok(): "ANNOTATION_TYPE": "error", "VERBOSE": "false", "FORCE_WORKFLOW_RUN": "false", + "ACTIVITY": "process_pr", } ) == settings.Config( GITHUB_BASE_REF="master", @@ -75,6 +76,7 @@ def test_config__from_environ__ok(): ANNOTATION_TYPE="error", VERBOSE=False, FORCE_WORKFLOW_RUN=False, + ACTIVITY=activities.Activity.PROCESS_PR, ) @@ -86,7 +88,7 @@ def test_config__verbose_deprecated(get_logs): "GITHUB_REPOSITORY": "owner/repo", "GITHUB_REF": "master", "GITHUB_EVENT_NAME": "pull", - "GITHUB_EVENT_PATH": pathlib.Path("test_event_path"), + "GITHUB_EVENT_PATH": "test_event_path", "GITHUB_PR_RUN_ID": "123", "GITHUB_STEP_SUMMARY": "step_summary", "VERBOSE": "true",