#!/usr/bin/env bash
# Copyright 2025 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
set -euo pipefail
TOP_DIR=$(git rev-parse --show-toplevel)
# shellcheck disable=SC2034
GO_DIR="${TOP_DIR}/go" # Reserved for future Go linting
PY_DIR="${TOP_DIR}/py"
# shellcheck disable=SC2034
JS_DIR="${TOP_DIR}/js" # Reserved for future JS linting
# ── Phase 1: Sequential (modifies files) ──────────────────────────────
# ruff fix/format must complete before any read-only checks see the source.
uv run --directory "${PY_DIR}" ruff check --fix --preview --unsafe-fixes .
uv run --directory "${PY_DIR}" ruff format --preview .
echo "--- 🔒 Checking lockfile is up to date ---"
uv lock --check --directory "${PY_DIR}"
# ── Phase 2: Parallel read-only checks ────────────────────────────────
# All checks below are read-only and independent. Run them concurrently
# and collect results at the end.
TMPDIR_LINT=$(mktemp -d)
# shellcheck disable=SC2064
trap "rm -rf '${TMPDIR_LINT}'" EXIT
declare -a PIDS=()
declare -a NAMES=()
# Helper: launch a check in the background, capturing output.
run_check() {
local name="$1"; shift
local logfile="${TMPDIR_LINT}/${name}.log"
(
echo "--- ${name} ---"
"$@" 2>&1
) > "${logfile}" 2>&1 &
PIDS+=($!)
NAMES+=("${name}")
}
# Type checkers
run_check "🔍 Ty Type Check" uv run --directory "${PY_DIR}" ty check .
run_check "🔍 Pyrefly Type Check" uv run --directory "${PY_DIR}" pyrefly check .
run_check "🔍 Pyright Type Check" uv run --directory "${PY_DIR}" pyright packages/
# Security
run_check "🔒 Security Checks" "${PY_DIR}/bin/run_python_security_checks"
# License
run_check "📜 License Check" "${TOP_DIR}/bin/check_license"
run_check "📜 Dep License Check" uv run --directory "${PY_DIR}" liccheck -s pyproject.toml
# Consistency + releasekit
run_check "🔍 Consistency Checks" "${PY_DIR}/bin/check_consistency"
run_check "📦 Releasekit Checks" uv run --directory "${TOP_DIR}/py/tools/releasekit" releasekit check
# Actionlint (GitHub Actions workflow validation)
_run_actionlint() {
if ! command -v actionlint &> /dev/null; then
echo "⚠️ actionlint not found."
# Auto-install prompt (only if stdin is a terminal)
if [ -t 0 ]; then
read -r -p " Install actionlint via 'go install'? [Y/n] " answer
case "${answer:-Y}" in
[Yy]*)
if command -v go &> /dev/null; then
go install github.com/rhysd/actionlint/cmd/actionlint@latest
elif command -v brew &> /dev/null; then
brew install actionlint
else
echo " ❌ Neither 'go' nor 'brew' found. Install manually:"
echo " https://github.com/rhysd/actionlint#quick-start"
return 0
fi
echo "✅ actionlint installed. Please re-run the linter."
return 0
;;
*)
echo " Skipping actionlint (install: go install github.com/rhysd/actionlint/cmd/actionlint@latest)"
return 0
;;
esac
else
echo " Skipping (install: go install github.com/rhysd/actionlint/cmd/actionlint@latest)"
return 0
fi
fi
local workflow_files=()
local workflow_dirs=(
"${TOP_DIR}/.github/workflows"
"${TOP_DIR}/py/tools/releasekit/github/workflows"
)
for dir in "${workflow_dirs[@]}"; do
while IFS= read -r -d '' f; do
workflow_files+=("$f")
done < <(find "${dir}" \( -name '*.yml' -o -name '*.yaml' \) -type f -print0 2>/dev/null)
done
if [ ${#workflow_files[@]} -eq 0 ]; then
echo "ℹ️ No workflow files found — skipping"
return 0
fi
# -ignore 'shellcheck' suppresses all embedded shellcheck warnings since
# we already run shellcheck separately. Actionlint's shellcheck integration
# also produces false positives for ${{ }} expression expansions.
if actionlint -ignore 'shellcheck' "${workflow_files[@]}" 2>&1; then
echo "✅ All ${#workflow_files[@]} workflow files pass actionlint"
else
return 1
fi
}
run_check "🔧 Actionlint" _run_actionlint
# Shellcheck (inline — slightly more complex, but still read-only)
_run_shellcheck() {
if ! command -v shellcheck &> /dev/null; then
echo "⚠️ shellcheck not found."
if [ -t 0 ]; then
read -r -p " Install shellcheck via 'brew install'? [Y/n] " answer
case "${answer:-Y}" in
[Yy]*)
if command -v brew &> /dev/null; then
brew install shellcheck
else
echo " ❌ 'brew' not found. Install manually: https://github.com/koalaman/shellcheck#installing"
return 0
fi
echo "✅ shellcheck installed. Please re-run the linter."
return 0
;;
*)
echo " Skipping shellcheck (install: brew install shellcheck)"
return 0
;;
esac
else
echo " Skipping (install: brew install shellcheck)"
return 0
fi
fi
local shell_errors=0
local shell_scripts=()
for script in "${TOP_DIR}"/bin/* "${PY_DIR}"/bin/*; do
if [ -f "$script" ] && file "$script" | grep -qE "shell|bash|sh script" 2>/dev/null; then
local script_name
script_name=$(basename "$script")
if [[ "$script_name" == *.py ]] || [[ "$script" == */.venv/* ]]; then
continue
fi
shell_scripts+=("$script")
fi
done
while IFS= read -r -d '' script; do
shell_scripts+=("$script")
done < <(find "${PY_DIR}/samples" -not -path '*/.venv/*' -name '*.sh' -type f -print0 2>/dev/null)
for script in "${shell_scripts[@]}"; do
if ! shellcheck -x -e SC1091 "$script" 2>&1; then
shell_errors=$((shell_errors + 1))
fi
done
if [ $shell_errors -gt 0 ]; then
echo "⚠️ $shell_errors shell script(s) have shellcheck warnings"
return 1
else
echo "✅ All ${#shell_scripts[@]} shell scripts pass shellcheck"
fi
}
run_check "🐚 Shellcheck" _run_shellcheck
# ── Collect results ───────────────────────────────────────────────────
failures=0
for i in "${!PIDS[@]}"; do
pid="${PIDS[$i]}"
name="${NAMES[$i]}"
if ! wait "${pid}"; then
echo ""
echo "❌ FAILED: ${name}"
cat "${TMPDIR_LINT}/${name}.log"
failures=$((failures + 1))
else
echo "✅ ${name}"
fi
done
if [ $failures -gt 0 ]; then
echo ""
echo "❌ ${failures} check(s) failed"
exit 1
fi
# Disabled because there are many lint errors.
#pushd "${GO_DIR}" &>/dev/null
#golangci-lint run ./...
#go vet -v ./...
#popd &>/dev/null
#pnpm run -C ${JS_DIR} lint