diff --git a/codecov-cli/codecov_cli/helpers/ci_adapters/__init__.py b/codecov-cli/codecov_cli/helpers/ci_adapters/__init__.py index fa1422392..abb3ceb7e 100644 --- a/codecov-cli/codecov_cli/helpers/ci_adapters/__init__.py +++ b/codecov-cli/codecov_cli/helpers/ci_adapters/__init__.py @@ -14,6 +14,7 @@ from codecov_cli.helpers.ci_adapters.droneci import DroneCIAdapter from codecov_cli.helpers.ci_adapters.github_actions import GithubActionsCIAdapter from codecov_cli.helpers.ci_adapters.gitlab_ci import GitlabCIAdapter +from codecov_cli.helpers.ci_adapters.harness import HarnessAdapter from codecov_cli.helpers.ci_adapters.heroku import HerokuCIAdapter from codecov_cli.helpers.ci_adapters.jenkins import JenkinsAdapter from codecov_cli.helpers.ci_adapters.local import LocalAdapter @@ -48,6 +49,7 @@ def get_ci_providers_list(): BitriseCIAdapter(), AppveyorCIAdapter(), WoodpeckerCIAdapter(), + HarnessAdapter(), HerokuCIAdapter(), DroneCIAdapter(), BuildkiteAdapter(), diff --git a/codecov-cli/codecov_cli/helpers/ci_adapters/harness.py b/codecov-cli/codecov_cli/helpers/ci_adapters/harness.py new file mode 100644 index 000000000..18f901473 --- /dev/null +++ b/codecov-cli/codecov_cli/helpers/ci_adapters/harness.py @@ -0,0 +1,63 @@ +import os + +from codecov_cli.helpers.ci_adapters.base import CIAdapterBase +from codecov_cli.helpers.git import parse_slug + +# https://developer.harness.io/docs/continuous-integration/troubleshoot-ci/ci-env-var/ + +# Uses CI_ prefixed environment variables variant if available and DRONE_ prefixed environment variables variant if not +# The DRONE variables overlap with the Drone CI Adapter as Harness CI is built on top +class HarnessAdapter(CIAdapterBase): + def detect(self) -> bool: + return bool(os.getenv("HARNESS_BUILD_ID")) + + def _get_branch(self): + return os.getenv("DRONE_COMMIT_BRANCH") + + def _get_commit_sha(self): + return os.getenv("DRONE_COMMIT_SHA") + + def _get_pull_request_number(self): + return os.getenv("DRONE_PULL_REQUEST") + + def _get_job_code(self): + return None + + def _get_build_code(self): + return os.getenv("CI_BUILD_NUMBER") + + def _get_build_url(self): + return os.getenv("CI_BUILD_LINK") + + def _get_slug(self): + # DRONE_REPO is the canonical org/repo slug on Harness CI (Drone-compatible). + # CI_REPO is often equivalent but some setups only set the repo name; then use + # DRONE_REPO_NAMESPACE + DRONE_REPO_NAME (or short CI_REPO as the name part). + if drone_repo := os.getenv("DRONE_REPO"): + return drone_repo + ci_repo = os.getenv("CI_REPO") + if ci_repo and "/" in ci_repo: + return ci_repo + namespace = os.getenv("DRONE_REPO_NAMESPACE") or os.getenv("CI_REPO_NAMESPACE") + name = os.getenv("DRONE_REPO_NAME") or ( + ci_repo if ci_repo and "/" not in ci_repo else None + ) + if namespace and name: + return f"{namespace}/{name}" + # Many Harness builds omit CI_REPO / *_NAMESPACE but still set clone/remotes: + # https://developer.harness.io/docs/continuous-integration/troubleshoot-ci/ci-env-var/ + for env_var in ( + "DRONE_GIT_HTTP_URL", + "CI_REPO_REMOTE", + "DRONE_REMOTE_URL", + ): + if url := os.getenv(env_var): + if slug := parse_slug(url): + return slug + return None + + def _get_service(self): + return "harness" + + def get_service_name(self): + return "Harness" \ No newline at end of file diff --git a/codecov-cli/codecovcli_commands b/codecov-cli/codecovcli_commands index 691eaa476..bd72875a9 100644 --- a/codecov-cli/codecovcli_commands +++ b/codecov-cli/codecovcli_commands @@ -1,7 +1,7 @@ Usage: codecovcli [OPTIONS] COMMAND [ARGS]... Options: - --auto-load-params-from [CircleCI|GithubActions|GitlabCI|Bitbucket|Bitrise|AppVeyor|Woodpecker|Heroku|DroneCI|BuildKite|AzurePipelines|Jenkins|CirrusCI|Teamcity|Travis|AWSCodeBuild|GoogleCloudBuild|Local] + --auto-load-params-from [CircleCI|GithubActions|GitlabCI|Bitbucket|Bitrise|AppVeyor|Woodpecker|Harness|Heroku|DroneCI|BuildKite|AzurePipelines|Jenkins|CirrusCI|Teamcity|Travis|AWSCodeBuild|GoogleCloudBuild|Local] --codecov-yml-path PATH -u, --enterprise-url, --url TEXT Change the upload host (Enterprise use) diff --git a/codecov-cli/pyproject.toml b/codecov-cli/pyproject.toml index d820e703f..649220925 100644 --- a/codecov-cli/pyproject.toml +++ b/codecov-cli/pyproject.toml @@ -47,8 +47,8 @@ Homepage = "https://about.codecov.io" requires = ["setuptools"] build-backend = "setuptools.build_meta" -[tool.setuptools] -packages = ["codecov_cli"] +[tool.setuptools.packages.find] +include = ["codecov_cli*"] [tool.pytest.ini_options] env = ["CODECOV_ENV=test"] diff --git a/codecov-cli/tests/ci_adapters/test_harnessci.py b/codecov-cli/tests/ci_adapters/test_harnessci.py new file mode 100644 index 000000000..cebc83126 --- /dev/null +++ b/codecov-cli/tests/ci_adapters/test_harnessci.py @@ -0,0 +1,156 @@ +import os +from enum import Enum + +import pytest + +from codecov_cli.fallbacks import FallbackFieldEnum +from codecov_cli.helpers.ci_adapters import HarnessAdapter + + +class HarnessEnvEnum(str, Enum): + HARNESS_BUILD_ID = "HARNESS_BUILD_ID" + CI_BUILD_LINK = "CI_BUILD_LINK" + CI_BUILD_NUMBER = "CI_BUILD_NUMBER" + CI_REPO = "CI_REPO" + CI_REPO_NAMESPACE = "CI_REPO_NAMESPACE" + DRONE = "DRONE" + DRONE_COMMIT_BRANCH = "DRONE_COMMIT_BRANCH" + DRONE_COMMIT_SHA = "DRONE_COMMIT_SHA" + DRONE_PULL_REQUEST = "DRONE_PULL_REQUEST" + DRONE_GIT_HTTP_URL = "DRONE_GIT_HTTP_URL" + DRONE_REMOTE_URL = "DRONE_REMOTE_URL" + DRONE_REPO = "DRONE_REPO" + DRONE_REPO_NAME = "DRONE_REPO_NAME" + DRONE_REPO_NAMESPACE = "DRONE_REPO_NAMESPACE" + CI_REPO_REMOTE = "CI_REPO_REMOTE" + +class TestHarnessCI(object): + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, False), + ({HarnessEnvEnum.HARNESS_BUILD_ID: "123"}, True), + ], + ) + def test_detect(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = HarnessAdapter().detect() + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({HarnessEnvEnum.DRONE_COMMIT_BRANCH: "branch"}, "branch"), + ], + ) + def test_branch(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = HarnessAdapter().get_fallback_value(FallbackFieldEnum.branch) + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({HarnessEnvEnum.DRONE_COMMIT_SHA: "sha"}, "sha"), + ], + ) + def test_commit_sha(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = HarnessAdapter().get_fallback_value(FallbackFieldEnum.commit_sha) + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({HarnessEnvEnum.DRONE_PULL_REQUEST: "123"}, "123"), + ], + ) + def test_pull_request_number(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = HarnessAdapter().get_fallback_value(FallbackFieldEnum.pull_request_number) + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({HarnessEnvEnum.CI_BUILD_NUMBER: "123"}, "123"), + ], + ) + def test_build_code(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = HarnessAdapter().get_fallback_value(FallbackFieldEnum.build_code) + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({HarnessEnvEnum.CI_BUILD_LINK: "https://example.com"}, "https://example.com"), + ], + ) + def test_build_url(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = HarnessAdapter().get_fallback_value(FallbackFieldEnum.build_url) + assert actual == expected + + @pytest.mark.parametrize( + "env_dict,expected", + [ + ({}, None), + ({HarnessEnvEnum.DRONE_REPO: "owner/repo"}, "owner/repo"), + ({HarnessEnvEnum.CI_REPO: "owner/repo"}, "owner/repo"), + ( + { + HarnessEnvEnum.DRONE_REPO_NAMESPACE: "owner", + HarnessEnvEnum.DRONE_REPO_NAME: "repo", + }, + "owner/repo", + ), + ( + { + HarnessEnvEnum.DRONE_REPO_NAMESPACE: "owner", + HarnessEnvEnum.CI_REPO: "repo", + }, + "owner/repo", + ), + ( + { + HarnessEnvEnum.CI_REPO_NAMESPACE: "owner", + HarnessEnvEnum.CI_REPO: "repo", + }, + "owner/repo", + ), + ({HarnessEnvEnum.CI_REPO: "repo"}, None), + ( + { + HarnessEnvEnum.DRONE_GIT_HTTP_URL: "https://github.com/myorg/myrepo.git", + }, + "myorg/myrepo", + ), + ( + { + HarnessEnvEnum.CI_REPO_REMOTE: "https://gitlab.com/mygroup/myrepo.git", + }, + "mygroup/myrepo", + ), + ( + { + HarnessEnvEnum.DRONE_REMOTE_URL: "git@github.com:acme/coverage.git", + }, + "acme/coverage", + ), + ], + ) + def test_slug(self, env_dict, expected, mocker): + mocker.patch.dict(os.environ, env_dict) + actual = HarnessAdapter().get_fallback_value(FallbackFieldEnum.slug) + assert actual == expected + + def test_service(self): + assert ( + HarnessAdapter().get_fallback_value(FallbackFieldEnum.service) == "harness" + ) \ No newline at end of file diff --git a/codecov-cli/tests/helpers/test_ci_adapter_selection.py b/codecov-cli/tests/helpers/test_ci_adapter_selection.py index 61bf5a86a..7649b878c 100644 --- a/codecov-cli/tests/helpers/test_ci_adapter_selection.py +++ b/codecov-cli/tests/helpers/test_ci_adapter_selection.py @@ -12,6 +12,7 @@ DroneCIAdapter, GithubActionsCIAdapter, GitlabCIAdapter, + HarnessAdapter, HerokuCIAdapter, JenkinsAdapter, LocalAdapter, @@ -53,6 +54,9 @@ def test_returns_woodpecker(self): def test_returns_teamcity(self): assert isinstance(get_ci_adapter("teamcity"), TeamcityAdapter) + def test_returns_harness(self): + assert isinstance(get_ci_adapter("harness"), HarnessAdapter) + def test_returns_herokuci(self): assert isinstance(get_ci_adapter("heroku"), HerokuCIAdapter) diff --git a/prevent-cli/preventcli_commands b/prevent-cli/preventcli_commands index e7dabef99..8a860d2a1 100644 --- a/prevent-cli/preventcli_commands +++ b/prevent-cli/preventcli_commands @@ -1,7 +1,7 @@ Usage: sentry-prevent-cli [OPTIONS] COMMAND [ARGS]... Options: - --auto-load-params-from [CircleCI|GithubActions|GitlabCI|Bitbucket|Bitrise|AppVeyor|Woodpecker|Heroku|DroneCI|BuildKite|AzurePipelines|Jenkins|CirrusCI|Teamcity|Travis|AWSCodeBuild|GoogleCloudBuild|Local] + --auto-load-params-from [CircleCI|GithubActions|GitlabCI|Bitbucket|Bitrise|AppVeyor|Woodpecker|Harness|Heroku|DroneCI|BuildKite|AzurePipelines|Jenkins|CirrusCI|Teamcity|Travis|AWSCodeBuild|GoogleCloudBuild|Local] --yml-path PATH -u, --enterprise-url, --url TEXT Change the upload host (Enterprise use)