Skip to content

Commit 8a7656b

Browse files
DeanChensjcopybara-github
authored andcommitted
refactor(ci): Consolidate compliance checks into pre-commit hook
Move custom file compliance checks (logger pattern, future annotations, cli imports, mTLS endpoints) from GHA inline bash scripts to a unified python script (compliance_checks.py) and expose it as a local pre-commit hook. Remove the compliance-check job from CI workflow. Co-authored-by: Shangjie Chen <deanchen@google.com> PiperOrigin-RevId: 940081615
1 parent 4c862b9 commit 8a7656b

4 files changed

Lines changed: 184 additions & 125 deletions

File tree

.github/workflows/continuous-integration.yml

Lines changed: 0 additions & 124 deletions
Original file line numberDiff line numberDiff line change
@@ -149,128 +149,4 @@ jobs:
149149
--ignore=tests/unittests/artifacts/test_artifact_service.py \
150150
--ignore=tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py
151151
152-
# 4. Custom file content compliance checks (PR only)
153-
compliance-check:
154-
name: File Content Compliance
155-
runs-on: ubuntu-latest
156-
if: github.event_name == 'pull_request'
157-
steps:
158-
- name: Checkout Code
159-
uses: actions/checkout@v6
160-
with:
161-
# Fetch full history (depth: 0) instead of shallow clone (depth: 2) to ensure
162-
# git diff origin/${base_ref}...HEAD can reliably find the merge base,
163-
# preventing fatal git errors on deep PRs or when the target branch has progressed.
164-
fetch-depth: 0
165-
166-
- name: Check for logger pattern in all changed Python files
167-
run: |
168-
git fetch origin ${GITHUB_BASE_REF}
169-
CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${GITHUB_BASE_REF}...HEAD | grep -E '\.py$' || true)
170-
if [ -n "$CHANGED_FILES" ]; then
171-
echo "Changed Python files to check:"
172-
echo "$CHANGED_FILES"
173-
echo ""
174-
175-
# Check for 'logger = logging.getLogger(__name__)' in changed .py files.
176-
set +e
177-
FILES_WITH_FORBIDDEN_LOGGER=$(grep -lE 'logger = logging\.getLogger\(__name__\)' $CHANGED_FILES)
178-
GREP_EXIT_CODE=$?
179-
set -e
180-
181-
if [ $GREP_EXIT_CODE -eq 0 ]; then
182-
echo "❌ Found forbidden use of 'logger = logging.getLogger(__name__)'. Please use 'logger = logging.getLogger('google_adk.' + __name__)' instead."
183-
echo "The following files contain the forbidden pattern:"
184-
echo "$FILES_WITH_FORBIDDEN_LOGGER"
185-
exit 1
186-
elif [ $GREP_EXIT_CODE -eq 1 ]; then
187-
echo "✅ No instances of 'logger = logging.getLogger(__name__)' found in changed Python files."
188-
fi
189-
else
190-
echo "✅ No relevant Python files found."
191-
fi
192-
193-
- name: Check for import pattern in certain changed Python files
194-
run: |
195-
git fetch origin ${GITHUB_BASE_REF}
196-
CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${GITHUB_BASE_REF}...HEAD | grep -E '\.py$' | grep -v -E '__init__.py$|version.py$|tests/.*|contributing/samples/' || true)
197-
if [ -n "$CHANGED_FILES" ]; then
198-
echo "Changed Python files to check:"
199-
echo "$CHANGED_FILES"
200-
echo ""
201-
202-
# Use grep -L to find files that DO NOT contain the pattern.
203-
FILES_MISSING_IMPORT=$(grep -L 'from __future__ import annotations' $CHANGED_FILES || true)
204152
205-
if [ -z "$FILES_MISSING_IMPORT" ]; then
206-
echo "✅ All modified Python files include 'from __future__ import annotations'."
207-
exit 0
208-
else
209-
echo "❌ The following files are missing 'from __future__ import annotations':"
210-
echo "$FILES_MISSING_IMPORT"
211-
echo "This import is required to allow forward references in type annotations without quotes."
212-
exit 1
213-
fi
214-
else
215-
echo "✅ No relevant Python files found."
216-
fi
217-
218-
- name: Check for import from cli package in certain changed Python files
219-
run: |
220-
git fetch origin ${GITHUB_BASE_REF}
221-
CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${GITHUB_BASE_REF}...HEAD | grep -E '\.py$' | grep -v -E 'cli/.*|src/google/adk/tools/apihub_tool/apihub_toolset.py|tests/.*|contributing/samples/' || true)
222-
if [ -n "$CHANGED_FILES" ]; then
223-
echo "Changed Python files to check:"
224-
echo "$CHANGED_FILES"
225-
echo ""
226-
227-
set +e
228-
FILES_WITH_FORBIDDEN_IMPORT=$(grep -lE '^from.*\bcli\b.*import.*$' $CHANGED_FILES)
229-
GREP_EXIT_CODE=$?
230-
set -e
231-
232-
if [[ $GREP_EXIT_CODE -eq 0 ]]; then
233-
echo "❌ Do not import from the cli package outside of the cli package. If you need to reuse the code elsewhere, please move the code outside of the cli package."
234-
echo "The following files contain the forbidden pattern:"
235-
echo "$FILES_WITH_FORBIDDEN_IMPORT"
236-
exit 1
237-
else
238-
echo "✅ No instances of importing from the cli package found in relevant changed Python files."
239-
fi
240-
else
241-
echo "✅ No relevant Python files found."
242-
fi
243-
244-
- name: Check for hardcoded googleapis.com endpoints
245-
run: |
246-
git fetch origin ${GITHUB_BASE_REF}
247-
CHANGED_FILES=$(git diff --diff-filter=ACMR --name-only origin/${GITHUB_BASE_REF}...HEAD | grep -E '\.py$' || true)
248-
if [ -n "$CHANGED_FILES" ]; then
249-
echo "Checking for hardcoded endpoints in: $CHANGED_FILES"
250-
251-
# 1. Identify files containing any googleapis.com URL.
252-
set +e
253-
FILES_WITH_ENDPOINTS=$(grep -lE 'https?://[a-zA-Z0-9.-]+\.googleapis\.com' $CHANGED_FILES)
254-
255-
# 2. From those, identify files that are MISSING the required mTLS version.
256-
if [ -n "$FILES_WITH_ENDPOINTS" ]; then
257-
FILES_MISSING_MTLS=$(grep -L '.mtls.googleapis.com' $FILES_WITH_ENDPOINTS)
258-
fi
259-
set -e
260-
261-
if [ -n "$FILES_MISSING_MTLS" ]; then
262-
echo "❌ Found hardcoded googleapis.com endpoints without mTLS support."
263-
echo "The following files must define both standard and mTLS (.mtls.googleapis.com) endpoints"
264-
echo "to support dynamic endpoint selection as required by security policy:"
265-
echo "$FILES_MISSING_MTLS"
266-
echo ""
267-
echo "To fix this, please follow these steps:"
268-
echo "1. Initialize an AuthorizedSession with your credentials."
269-
echo "2. Use 'mtls.has_default_client_cert_source() from google-auth' to check for available client certificates."
270-
echo "3. If certificates are present, use 'session.configure_mtls_channel()'."
271-
echo "4. Dynamically select the '.mtls.' variant of the endpoint when mTLS is active."
272-
exit 1
273-
else
274-
echo "✅ All hardcoded endpoints have corresponding mTLS definitions or no endpoints found."
275-
fi
276-
fi

.pre-commit-config.yaml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,11 @@ repos:
5656
language: script
5757
files: ^src/google/adk/.*\.py$
5858
pass_filenames: false
59+
- id: compliance-checks
60+
name: ADK Compliance Checks
61+
entry: scripts/compliance_checks.py
62+
language: script
63+
files: \.py$
5964
- repo: https://github.com/executablebooks/mdformat
6065
rev: 0.7.22
6166
hooks:

contributing/samples/mcp/mcp_toolset_auth/oauth_mcp_server.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@
3333
import uvicorn
3434

3535
logging.basicConfig(level=logging.INFO)
36-
logger = logging.getLogger(__name__)
36+
logger = logging.getLogger('google_adk.' + __name__)
3737

3838
# Expected OAuth token for testing
3939
VALID_TOKEN = 'test_access_token_12345'

scripts/compliance_checks.py

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
#!/usr/bin/env python3
2+
# Copyright 2026 Google LLC
3+
#
4+
# Licensed under the Apache License, Version 2.0 (the "License");
5+
# you may not use this file except in compliance with the License.
6+
# You may obtain a copy of the License at
7+
#
8+
# http://www.apache.org/licenses/LICENSE-2.0
9+
#
10+
# Unless required by applicable law or agreed to in writing, software
11+
# distributed under the License is distributed on an "AS IS" BASIS,
12+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13+
# See the License for the specific language governing permissions and
14+
# limitations under the License.
15+
16+
"""Runs compliance checks on ADK source files.
17+
18+
This script is used as a pre-commit hook and in CI to enforce coding standards.
19+
"""
20+
21+
import argparse
22+
import os
23+
import re
24+
import sys
25+
26+
# Legacy files that are temporarily excluded from the mTLS check.
27+
# Do not add new files to this list. All new code must support mTLS.
28+
_EXCLUDED_FROM_MTLS = {
29+
'contributing/samples/environment_and_skills/e2b_environment/agent.py',
30+
'contributing/samples/integrations/bigquery_mcp/agent.py',
31+
'contributing/samples/integrations/bigtable/agent.py',
32+
'contributing/samples/integrations/data_agent/agent.py',
33+
'contributing/samples/integrations/gcp_auth/agent.py',
34+
'contributing/samples/integrations/gcs/agent.py',
35+
'contributing/samples/integrations/gcs_admin/agent.py',
36+
'contributing/samples/integrations/integration_connector_euc_agent/agent.py',
37+
'contributing/samples/integrations/oauth_calendar_agent/agent.py',
38+
'contributing/samples/integrations/spanner/agent.py',
39+
'contributing/samples/integrations/spanner_admin/agent.py',
40+
'contributing/samples/integrations/spanner_rag_agent/agent.py',
41+
'contributing/samples/mcp/mcp_service_account_agent/agent.py',
42+
'contributing/samples/models/interactions_api/main.py',
43+
'contributing/samples/multimodal/static_non_text_content/agent.py',
44+
'src/google/adk/auth/auth_credential.py',
45+
'src/google/adk/integrations/api_registry/api_registry.py',
46+
'src/google/adk/integrations/bigquery/bigquery_credentials.py',
47+
'src/google/adk/integrations/bigquery/data_insights_tool.py',
48+
'src/google/adk/integrations/bigquery/metadata_tool.py',
49+
'src/google/adk/integrations/gcs/gcs_credentials.py',
50+
'src/google/adk/plugins/bigquery_agent_analytics_plugin.py',
51+
'src/google/adk/tools/_google_credentials.py',
52+
'src/google/adk/tools/apihub_tool/clients/apihub_client.py',
53+
'src/google/adk/tools/application_integration_tool/application_integration_toolset.py',
54+
'src/google/adk/tools/application_integration_tool/clients/connections_client.py',
55+
'src/google/adk/tools/application_integration_tool/clients/integration_client.py',
56+
'src/google/adk/tools/bigtable/bigtable_credentials.py',
57+
'src/google/adk/tools/data_agent/credentials.py',
58+
'src/google/adk/tools/data_agent/data_agent_tool.py',
59+
'src/google/adk/tools/google_api_tool/google_api_toolset.py',
60+
'src/google/adk/tools/google_api_tool/googleapi_to_openapi_converter.py',
61+
'src/google/adk/tools/mcp_tool/mcp_session_manager.py',
62+
'src/google/adk/tools/openapi_tool/auth/auth_helpers.py',
63+
'src/google/adk/tools/openapi_tool/auth/credential_exchangers/service_account_exchanger.py',
64+
'src/google/adk/tools/pubsub/pubsub_credentials.py',
65+
'src/google/adk/tools/spanner/spanner_credentials.py',
66+
'tests/unittests/auth/test_credential_manager.py',
67+
'tests/unittests/cli/utils/test_gcp_utils.py',
68+
'tests/unittests/flows/llm_flows/test_functions_request_euc.py',
69+
'tests/unittests/integrations/api_registry/test_api_registry.py',
70+
'tests/unittests/integrations/bigquery/test_bigquery_credentials.py',
71+
'tests/unittests/tools/apihub_tool/clients/test_apihub_client.py',
72+
'tests/unittests/tools/application_integration_tool/clients/test_connections_client.py',
73+
'tests/unittests/tools/application_integration_tool/clients/test_integration_client.py',
74+
'tests/unittests/tools/application_integration_tool/test_application_integration_toolset.py',
75+
'tests/unittests/tools/data_agent/test_data_agent_tool.py',
76+
'tests/unittests/tools/google_api_tool/test_docs_batchupdate.py',
77+
'tests/unittests/tools/google_api_tool/test_google_api_toolset.py',
78+
'tests/unittests/tools/google_api_tool/test_googleapi_to_openapi_converter.py',
79+
'tests/unittests/tools/openapi_tool/auth/credential_exchangers/test_service_account_exchanger.py',
80+
'tests/unittests/tools/openapi_tool/openapi_spec_parser/test_openapi_toolset.py',
81+
'tests/unittests/tools/openapi_tool/openapi_spec_parser/test_rest_api_tool.py',
82+
'tests/unittests/tools/spanner/test_spanner_credentials.py',
83+
'tests/unittests/tools/test_base_google_credentials_manager.py',
84+
'tests/unittests/tools/test_google_tool.py',
85+
'tests/unittests/workflow/utils/test_workflow_hitl_utils.py',
86+
}
87+
88+
89+
def check_logger(content: str) -> bool:
90+
# Forbidden: 'logger = logging.getLogger(__name__)'
91+
pattern = re.compile(r'logger\s*=\s*logging\.getLogger\(__name__\)')
92+
return not pattern.search(content)
93+
94+
95+
def check_future_annotations(content: str, filename: str) -> bool:
96+
# Exclude: __init__.py, version.py, tests/, contributing/samples/
97+
if (
98+
filename.endswith('__init__.py')
99+
or filename.endswith('version.py')
100+
or 'tests/' in filename
101+
or 'contributing/samples/' in filename
102+
):
103+
return True
104+
return 'from __future__ import annotations' in content
105+
106+
107+
def check_cli_import(content: str, filename: str) -> bool:
108+
# Exclude: cli/, apihub_toolset.py, tests/, contributing/samples/
109+
if (
110+
'cli/' in filename
111+
or filename.endswith('apihub_toolset.py')
112+
or 'tests/' in filename
113+
or 'contributing/samples/' in filename
114+
):
115+
return True
116+
# Pattern: ^from.*\bcli\b.*import.*$ (multiline)
117+
pattern = re.compile(r'^from.*\bcli\b.*import.*$', re.MULTILINE)
118+
return not pattern.search(content)
119+
120+
121+
def check_mtls(content: str, filename: str) -> bool:
122+
if filename in _EXCLUDED_FROM_MTLS:
123+
return True
124+
# Pattern for googleapis: https?://[a-zA-Z0-9.-]+\.googleapis\.com
125+
endpoint_pattern = re.compile(r'https?://[a-zA-Z0-9.-]+\.googleapis\.com')
126+
if endpoint_pattern.search(content):
127+
return '.mtls.googleapis.com' in content
128+
return True
129+
130+
131+
def main() -> None:
132+
parser = argparse.ArgumentParser(description=__doc__)
133+
parser.add_argument('files', nargs='*', help='Files to check')
134+
args = parser.parse_args()
135+
136+
failed = False
137+
for f in args.files:
138+
# Skip directories if they are passed accidentally
139+
if not os.path.isfile(f):
140+
continue
141+
try:
142+
with open(f, 'r', encoding='utf-8') as file:
143+
content = file.read()
144+
except Exception as e: # pylint: disable=broad-except
145+
print(f"Error reading {f}: {e}")
146+
continue
147+
148+
# Run checks
149+
if not check_logger(content):
150+
print(
151+
f"❌ {f}: Found forbidden use of 'logger = logging.getLogger(__name__)'. "
152+
"Please use 'logger = logging.getLogger(\"google_adk.\" + __name__)' instead."
153+
)
154+
failed = True
155+
156+
if not check_future_annotations(content, f):
157+
print(f"❌ {f}: Missing 'from __future__ import annotations'.")
158+
failed = True
159+
160+
if not check_cli_import(content, f):
161+
print(
162+
f"❌ {f}: Do not import from the cli package outside of the cli package."
163+
)
164+
failed = True
165+
166+
if not check_mtls(content, f):
167+
print(
168+
f"❌ {f}: Found hardcoded googleapis.com endpoints without mTLS support."
169+
)
170+
failed = True
171+
172+
if failed:
173+
sys.exit(1)
174+
sys.exit(0)
175+
176+
177+
if __name__ == '__main__':
178+
main()

0 commit comments

Comments
 (0)