Skip to content

Commit f86033d

Browse files
feat: create a lint action (#14264)
* feat: create a lint action * feat: add lint-changed.sh shell script * fix: Update lint-changed.sh * fix: add license header to lint-changed.sh * fix: minor update to GitHub action * fix: remove checking for pushes against main. we never push to main. * fix: update based on never pushing to main. * fix: address action scanning issue (maybe) * feat: add a failing test fixture for linting * feat: add a passing test python file for linting * fix: address header issues * fix: address header issues * fix: create updated noxfile template so that only targeted files are linted * fix: use customized noxfile template * fix: address on lint passing test Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com> * fix: address some of the noisy logging in the output to the action * chore: remove failing lint file for now .internal/tests/fixtures/test_fail.py * fix: no quiet mode :( * fix: address linting issues with lib and readd comment --------- Co-authored-by: Copilot Autofix powered by AI <223894421+github-code-quality[bot]@users.noreply.github.com>
1 parent 9587aa3 commit f86033d

4 files changed

Lines changed: 238 additions & 0 deletions

File tree

.github/scripts/lint-changed.sh

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,70 @@
1+
#!/usr/bin/env bash
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+
set -euo pipefail
17+
18+
EVENT_NAME="${1:-local}"
19+
BASE_REF="${2:-main}"
20+
21+
echo "Configuring target diff for event: $EVENT_NAME"
22+
23+
# Determine the target reference to diff against
24+
if [ "$EVENT_NAME" = "pull_request" ]; then
25+
echo "Fetching origin/$BASE_REF metadata..."
26+
# Ensure we have the target branch metadata fetched.
27+
git fetch origin "$BASE_REF" --depth=1 --quiet
28+
29+
# Isolate to only changes in the PR
30+
DIFF_COMMAND="origin/$BASE_REF..."
31+
else
32+
# Local fallback
33+
# diff against local main branch.
34+
echo "Running locally. Diffing against local $BASE_REF..."
35+
DIFF_COMMAND="$BASE_REF"
36+
fi
37+
38+
# Gather modified/added Python files, explicitly ignoring deleted files via --diff-filter=d
39+
DIFF_OUTPUT=$(git diff --name-only --diff-filter=d "$DIFF_COMMAND" -- '*.py' 2>/dev/null || true)
40+
41+
if [ -n "$DIFF_OUTPUT" ]; then
42+
mapfile -t CHANGED_FILES <<< "$DIFF_OUTPUT"
43+
else
44+
CHANGED_FILES=()
45+
fi
46+
47+
# Execute linters if changed Python files exist
48+
if [ ${#CHANGED_FILES[@]} -gt 0 ]; then
49+
echo "Files to lint:"
50+
printf ' - %s\n' "${CHANGED_FILES[@]}"
51+
52+
# Track execution success manually so both tools get a chance to run.
53+
# This prevents the workflow from dying on Black without showing Flake8 errors.
54+
BLACK_EXIT=0
55+
LINT_EXIT=0
56+
57+
# adding -q to silence some of the extraneous logging
58+
echo "Running blacken..."
59+
nox -s blacken -- "${CHANGED_FILES[@]}" || BLACK_EXIT=$?
60+
61+
echo "Running flake8 lint..."
62+
nox -s lint -- "${CHANGED_FILES[@]}" || LINT_EXIT=$?
63+
64+
if [ $BLACK_EXIT -ne 0 ] || [ $LINT_EXIT -ne 0 ]; then
65+
echo "❌ One or more linting checks failed."
66+
exit 1
67+
fi
68+
else
69+
echo "✅ No Python files changed in this scope. Skipping checks."
70+
fi

.github/scripts/noxfile-lint.py

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
from __future__ import annotations
16+
17+
import os
18+
19+
import nox
20+
21+
# Use a stable Python version for running the style utilities
22+
LINTING_VERSION = "3.10"
23+
24+
# Error out if the runner is missing the target python interpreter
25+
nox.options.error_on_missing_interpreters = True
26+
27+
28+
def _determine_local_import_names(start_dir: str) -> list[str]:
29+
"""Determines local import names to assist Flake8 with import order checks."""
30+
try:
31+
file_ext_pairs = [os.path.splitext(path) for path in os.listdir(start_dir)]
32+
return [
33+
basename
34+
for basename, extension in file_ext_pairs
35+
if extension == ".py"
36+
or (os.path.isdir(os.path.join(start_dir, basename)) and basename != "__pycache__")
37+
]
38+
except Exception:
39+
return []
40+
41+
# Linting with flake8.
42+
#
43+
# We ignore the following rules:
44+
# ANN101: missing type annotation for `self` in method
45+
# ANN102: missing type annotation for `cls` in method
46+
# E203: whitespace before ‘:’
47+
# E266: too many leading ‘#’ for block comment
48+
# E501: line too long
49+
# I202: Additional newline in a section of imports
50+
#
51+
# We also need to specify the rules which are ignored by default:
52+
# ['E226', 'W504', 'E126', 'E123', 'W503', 'E24', 'E704', 'E121']
53+
#
54+
# For more information see: https://pypi.org/project/flake8-annotations
55+
56+
# Standardize style configuration parameters
57+
FLAKE8_COMMON_ARGS = [
58+
"--show-source",
59+
"--builtin=gettext",
60+
"--max-complexity=20",
61+
"--import-order-style=google",
62+
"--exclude=.nox,.cache,env,lib,generated_pb2,*_pb2.py,*_pb2_grpc.py",
63+
"--ignore=ANN101,ANN102,E121,E123,E126,E203,E226,E24,E266,E501,E704,W503,W504,I202",
64+
"--max-line-length=88",
65+
]
66+
67+
68+
@nox.session(python=LINTING_VERSION)
69+
def lint(session: nox.sessions.Session) -> None:
70+
"""Runs flake8 linting checks. Honors incremental PR file arguments."""
71+
session.install("flake8", "flake8-import-order")
72+
73+
local_names = _determine_local_import_names(".")
74+
args = FLAKE8_COMMON_ARGS + [
75+
"--application-import-names",
76+
",".join(local_names),
77+
]
78+
79+
if session.posargs:
80+
args.extend(session.posargs)
81+
else:
82+
args.append(".")
83+
84+
session.run("flake8", *args)
85+
86+
87+
@nox.session(python=LINTING_VERSION)
88+
def blacken(session: nox.sessions.Session) -> None:
89+
"""Runs black code formatting checks. Honors incremental PR file arguments."""
90+
session.install("black")
91+
92+
# If explicit target files are passed via posargs, target ONLY those files.
93+
if session.posargs:
94+
targets = session.posargs
95+
else:
96+
# Fallback to scanning immediate root Python files if run purely locally without args
97+
targets = [path for path in os.listdir(".") if path.endswith(".py")]
98+
99+
if targets:
100+
session.run("black", *targets)
101+
else:
102+
session.log("No specific Python targets identified for formatting validations.")

.github/workflows/lint.yml

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
name: Lint
2+
3+
on:
4+
pull_request:
5+
branches: [ main ]
6+
7+
permissions:
8+
contents: read
9+
10+
jobs:
11+
lint:
12+
runs-on: ubuntu-latest
13+
steps:
14+
- name: Checkout
15+
uses: actions/checkout@df4cb1c069e1874edd31b4311f1884172cec0e10 # v6
16+
with:
17+
fetch-depth: 0
18+
19+
- name: Setup Python
20+
uses: actions/setup-python@a309ff8b426b58ec0e2a45f0f869d46889d02405 # v6.2.0
21+
with:
22+
python-version: '3.10'
23+
24+
- name: Install nox
25+
run: |
26+
python -m pip install --upgrade pip
27+
python -m pip install nox
28+
29+
- name: Prepare noxfile
30+
# copy from customized noxfile so that we don't lint everything in the repository
31+
# we don't expect contributors to fix past lint issues
32+
run: cp .github/scripts/noxfile-lint.py noxfile.py
33+
34+
- name: Make script executable
35+
run: chmod +x .github/scripts/lint-changed.sh
36+
37+
- name: Run lint script for changed files
38+
run: |
39+
.github/scripts/lint-changed.sh "pull_request" "${{ github.base_ref }}"
Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# Copyright 2026 Google LLC
2+
#
3+
# Licensed under the Apache License, Version 2.0 (the "License");
4+
# you may not use this file except in compliance with the License.
5+
# You may obtain a copy of the License at
6+
#
7+
# http://www.apache.org/licenses/LICENSE-2.0
8+
#
9+
# Unless required by applicable law or agreed to in writing, software
10+
# distributed under the License is distributed on an "AS IS" BASIS,
11+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
# See the License for the specific language governing permissions and
13+
# limitations under the License.
14+
15+
# test_pass.py
16+
17+
18+
def calculate_square(number: int) -> int:
19+
"""Calculates the square of a given integer."""
20+
result = number * number
21+
print(f"The result is: {result}")
22+
return result
23+
24+
25+
if __name__ == "__main__":
26+
# Ensure a basic execution works cleanly
27+
calculate_square(5)

0 commit comments

Comments
 (0)