Skip to content

Commit 7432bad

Browse files
authored
feat: add Google Cloud Build CI support (#418)
1 parent d3f982f commit 7432bad

3 files changed

Lines changed: 285 additions & 0 deletions

File tree

codecov_cli/helpers/ci_adapters/__init__.py

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from codecov_cli.helpers.ci_adapters.buildkite import BuildkiteAdapter
88
from codecov_cli.helpers.ci_adapters.circleci import CircleCICIAdapter
99
from codecov_cli.helpers.ci_adapters.cirrus_ci import CirrusCIAdapter
10+
from codecov_cli.helpers.ci_adapters.cloudbuild import GoogleCloudBuildAdapter
1011
from codecov_cli.helpers.ci_adapters.codebuild import AWSCodeBuildCIAdapter
1112
from codecov_cli.helpers.ci_adapters.droneci import DroneCIAdapter
1213
from codecov_cli.helpers.ci_adapters.github_actions import GithubActionsCIAdapter
@@ -54,6 +55,7 @@ def get_ci_providers_list():
5455
TeamcityAdapter(),
5556
TravisCIAdapter(),
5657
AWSCodeBuildCIAdapter(),
58+
GoogleCloudBuildAdapter(),
5759
# local adapter should always be the last one
5860
LocalAdapter(),
5961
]
Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
import os
2+
3+
from codecov_cli.helpers.ci_adapters.base import CIAdapterBase
4+
5+
6+
class GoogleCloudBuildAdapter(CIAdapterBase):
7+
"""
8+
Google Cloud Build uses variable substitutions in the builds
9+
https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values
10+
For these to be available as environment variables, so this adapter
11+
can read the values, you have to manually map the substitution variables to
12+
environment variables on the build step, like this
13+
env:
14+
- '_PR_NUMBER=$_PR_NUMBER'
15+
- 'BRANCH_NAME=$BRANCH_NAME'
16+
- 'BUILD_ID=$BUILD_ID'
17+
- 'COMMIT_SHA=$COMMIT_SHA'
18+
- 'LOCATION=$LOCATION'
19+
- 'PROJECT_ID=$PROJECT_ID'
20+
- 'PROJECT_NUMBER=$PROJECT_NUMBER'
21+
- 'REF_NAME=$REF_NAME'
22+
- 'REPO_FULL_NAME=$REPO_FULL_NAME'
23+
- 'TRIGGER_NAME=$TRIGGER_NAME'
24+
Read more about manual substitution mapping here:
25+
https://cloud.google.com/build/docs/configuring-builds/substitute-variable-values#map_substitutions_manually
26+
"""
27+
28+
def detect(self) -> bool:
29+
return all(
30+
list(
31+
map(os.getenv, ["LOCATION", "PROJECT_NUMBER", "PROJECT_ID", "BUILD_ID"])
32+
)
33+
)
34+
35+
def _get_branch(self):
36+
return os.getenv("BRANCH_NAME")
37+
38+
def _get_build_code(self):
39+
return os.getenv("BUILD_ID")
40+
41+
def _get_commit_sha(self):
42+
return os.getenv("COMMIT_SHA")
43+
44+
def _get_slug(self):
45+
return os.getenv("REPO_FULL_NAME")
46+
47+
def _get_build_url(self):
48+
# to build the url, the environment variables LOCATION, PROJECT_ID and BUILD_ID are needed
49+
if not all(list(map(os.getenv, ["LOCATION", "PROJECT_ID", "BUILD_ID"]))):
50+
return None
51+
52+
location = os.getenv("LOCATION")
53+
project_id = os.getenv("PROJECT_ID")
54+
build_id = os.getenv("BUILD_ID")
55+
56+
return f"https://console.cloud.google.com/cloud-build/builds;region={location}/{build_id}?project={project_id}"
57+
58+
def _get_pull_request_number(self):
59+
pr_num = os.getenv("_PR_NUMBER")
60+
return pr_num if pr_num != "" else None
61+
62+
def _get_job_code(self):
63+
job_code = os.getenv("TRIGGER_NAME")
64+
return job_code if job_code != "" else None
65+
66+
def _get_service(self):
67+
return "google_cloud_build"
68+
69+
def get_service_name(self):
70+
return "GoogleCloudBuild"
Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
import os
2+
from enum import Enum
3+
4+
import pytest
5+
6+
from codecov_cli.fallbacks import FallbackFieldEnum
7+
from codecov_cli.helpers.ci_adapters.cloudbuild import GoogleCloudBuildAdapter
8+
9+
10+
class CloudBuildEnvEnum(str, Enum):
11+
BRANCH_NAME = "BRANCH_NAME"
12+
BUILD_ID = "BUILD_ID"
13+
COMMIT_SHA = "COMMIT_SHA"
14+
LOCATION = "LOCATION"
15+
PROJECT_ID = "PROJECT_ID"
16+
PROJECT_NUMBER = "PROJECT_NUMBER"
17+
REPO_FULL_NAME = "REPO_FULL_NAME"
18+
_PR_NUMBER = "_PR_NUMBER"
19+
TRIGGER_NAME = "TRIGGER_NAME"
20+
21+
22+
class TestCloudBuild(object):
23+
@pytest.mark.parametrize(
24+
"env_dict,expected",
25+
[
26+
({}, False),
27+
(
28+
{
29+
CloudBuildEnvEnum.LOCATION: "global",
30+
CloudBuildEnvEnum.PROJECT_ID: "my_project",
31+
CloudBuildEnvEnum.PROJECT_NUMBER: "123",
32+
},
33+
False,
34+
),
35+
(
36+
{
37+
CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de",
38+
CloudBuildEnvEnum.PROJECT_ID: "my_project",
39+
CloudBuildEnvEnum.PROJECT_NUMBER: "123",
40+
},
41+
False,
42+
),
43+
(
44+
{
45+
CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de",
46+
CloudBuildEnvEnum.LOCATION: "global",
47+
CloudBuildEnvEnum.PROJECT_NUMBER: "123",
48+
},
49+
False,
50+
),
51+
(
52+
{
53+
CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de",
54+
CloudBuildEnvEnum.LOCATION: "global",
55+
CloudBuildEnvEnum.PROJECT_ID: "my_project",
56+
},
57+
False,
58+
),
59+
(
60+
{
61+
CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de",
62+
CloudBuildEnvEnum.LOCATION: "global",
63+
CloudBuildEnvEnum.PROJECT_ID: "my_project",
64+
CloudBuildEnvEnum.PROJECT_NUMBER: "123",
65+
},
66+
True,
67+
),
68+
],
69+
)
70+
def test_detect(self, env_dict, expected, mocker):
71+
mocker.patch.dict(os.environ, env_dict)
72+
actual = GoogleCloudBuildAdapter().detect()
73+
assert actual == expected
74+
75+
@pytest.mark.parametrize(
76+
"env_dict,expected",
77+
[
78+
({}, None),
79+
({CloudBuildEnvEnum.BRANCH_NAME: "abc"}, "abc"),
80+
],
81+
)
82+
def test_branch(self, env_dict, expected, mocker):
83+
mocker.patch.dict(os.environ, env_dict)
84+
actual = GoogleCloudBuildAdapter().get_fallback_value(FallbackFieldEnum.branch)
85+
86+
assert actual == expected
87+
88+
@pytest.mark.parametrize(
89+
"env_dict,expected",
90+
[
91+
({}, None),
92+
(
93+
{CloudBuildEnvEnum.BUILD_ID: "52cbb633-aca0-4289-90bd-76e4e60baf82"},
94+
"52cbb633-aca0-4289-90bd-76e4e60baf82",
95+
),
96+
],
97+
)
98+
def test_build_code(self, env_dict, expected, mocker):
99+
mocker.patch.dict(os.environ, env_dict)
100+
actual = GoogleCloudBuildAdapter().get_fallback_value(
101+
FallbackFieldEnum.build_code
102+
)
103+
104+
assert actual == expected
105+
106+
@pytest.mark.parametrize(
107+
"env_dict,expected",
108+
[
109+
({}, None),
110+
(
111+
{
112+
CloudBuildEnvEnum.LOCATION: "global",
113+
CloudBuildEnvEnum.PROJECT_ID: "my_project",
114+
},
115+
None,
116+
),
117+
(
118+
{
119+
CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de",
120+
CloudBuildEnvEnum.PROJECT_ID: "my_project",
121+
},
122+
None,
123+
),
124+
(
125+
{
126+
CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de",
127+
CloudBuildEnvEnum.LOCATION: "global",
128+
},
129+
None,
130+
),
131+
(
132+
{
133+
CloudBuildEnvEnum.BUILD_ID: "fd02b20f-72a3-41b5-862d-2c15e5f289de",
134+
CloudBuildEnvEnum.LOCATION: "global",
135+
CloudBuildEnvEnum.PROJECT_ID: "my_project",
136+
},
137+
"https://console.cloud.google.com/cloud-build/builds;region=global/fd02b20f-72a3-41b5-862d-2c15e5f289de?project=my_project",
138+
),
139+
],
140+
)
141+
def test_build_url(self, env_dict, expected, mocker):
142+
mocker.patch.dict(os.environ, env_dict)
143+
actual = GoogleCloudBuildAdapter().get_fallback_value(
144+
FallbackFieldEnum.build_url
145+
)
146+
147+
assert actual == expected
148+
149+
@pytest.mark.parametrize(
150+
"env_dict,expected",
151+
[
152+
({}, None),
153+
({CloudBuildEnvEnum.COMMIT_SHA: "123456789000111"}, "123456789000111"),
154+
],
155+
)
156+
def test_commit_sha(self, env_dict, expected, mocker):
157+
mocker.patch.dict(os.environ, env_dict)
158+
actual = GoogleCloudBuildAdapter().get_fallback_value(
159+
FallbackFieldEnum.commit_sha
160+
)
161+
162+
assert actual == expected
163+
164+
@pytest.mark.parametrize(
165+
"env_dict,expected",
166+
[
167+
({}, None),
168+
({CloudBuildEnvEnum.TRIGGER_NAME: ""}, None),
169+
({CloudBuildEnvEnum.TRIGGER_NAME: "build-job-name"}, "build-job-name"),
170+
],
171+
)
172+
def test_job_code(self, env_dict, expected, mocker):
173+
mocker.patch.dict(os.environ, env_dict)
174+
actual = GoogleCloudBuildAdapter().get_fallback_value(
175+
FallbackFieldEnum.job_code
176+
)
177+
178+
assert actual == expected
179+
180+
@pytest.mark.parametrize(
181+
"env_dict,expected",
182+
[
183+
({}, None),
184+
({CloudBuildEnvEnum._PR_NUMBER: ""}, None),
185+
({CloudBuildEnvEnum._PR_NUMBER: "123"}, "123"),
186+
],
187+
)
188+
def test_pull_request_number(self, env_dict, expected, mocker):
189+
mocker.patch.dict(os.environ, env_dict)
190+
actual = GoogleCloudBuildAdapter().get_fallback_value(
191+
FallbackFieldEnum.pull_request_number
192+
)
193+
194+
assert actual == expected
195+
196+
@pytest.mark.parametrize(
197+
"env_dict,expected",
198+
[
199+
({}, None),
200+
({CloudBuildEnvEnum.REPO_FULL_NAME: "owner/repo"}, "owner/repo"),
201+
],
202+
)
203+
def test_slug(self, env_dict, expected, mocker):
204+
mocker.patch.dict(os.environ, env_dict)
205+
actual = GoogleCloudBuildAdapter().get_fallback_value(FallbackFieldEnum.slug)
206+
207+
assert actual == expected
208+
209+
def test_service(self):
210+
assert (
211+
GoogleCloudBuildAdapter().get_fallback_value(FallbackFieldEnum.service)
212+
== "google_cloud_build"
213+
)

0 commit comments

Comments
 (0)