| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210 |
- #!/bin/sh
- #
- # ArozOS contribution convention checker
- # =====================================
- #
- # Enforces the contribution rules documented in CLAUDE.md against *new* code.
- # It is intentionally written in portable POSIX sh with only the standard
- # git/grep tooling so it runs the same way on a contributor's machine, inside
- # the Claude Code PostToolUse hook and in CI (rule 5: no system dependencies).
- #
- # Usage:
- # scripts/check-conventions.sh <file> [<file> ...] Check specific files
- # scripts/check-conventions.sh --diff <base-ref> Check files changed vs base-ref
- # scripts/check-conventions.sh --hook Read a Claude Code hook
- # payload (JSON) from stdin
- #
- # Exit status:
- # 0 no ERROR-level violations (WARN findings may still be printed)
- # 1 at least one ERROR-level violation was found (CI / direct invocation)
- # 2 findings in --hook mode (surfaces the report back to Claude)
- #
- # Escape hatch:
- # Append the marker arozos-lint-ignore to a source line to skip the
- # line-level checks (raw logger / hardcoded path) for that single line.
- # Use it only with a short justification comment.
- set -u
- errors=0
- warns=0
- err() { printf ' [ERROR] %s\n' "$1" >&2; errors=$((errors + 1)); }
- warn() { printf ' [WARN] %s\n' "$1" >&2; warns=$((warns + 1)); }
- repo_root=$(git rev-parse --show-toplevel 2>/dev/null || pwd)
- # is_platform_file returns success when a Go file is scoped to a single OS/arch
- # by its filename suffix (e.g. foo_linux.go, bar_windows_amd64.go). Such files
- # are the project's sanctioned home for platform-specific code, so the
- # portability checks do not apply to them.
- is_platform_file() {
- printf '%s' "$1" | grep -Eq \
- '_(linux|windows|darwin|freebsd|openbsd|netbsd|dragonfly|solaris|illumos|aix|android|js|wasm|plan9)(_[a-z0-9]+)?\.go$'
- }
- has_build_constraint() {
- [ -f "$1" ] && grep -Eq '^//go:build|^// \+build' "$1"
- }
- # check_lines applies the per-line rules to a stream of source lines supplied on
- # stdin. $1 is the file the lines belong to (used for context + exemptions).
- check_lines() {
- file=$1
- # Skip the marker so opted-out lines are not re-flagged.
- scan=$(grep -v 'arozos-lint-ignore' || true)
- # --- Rule 1: managed logger, not the standard log package -------------
- # The logger package itself legitimately wraps "log"; everything else must
- # route through logger.PrintAndLog so output lands in the system log.
- case "$file" in
- *mod/info/logger/*) ;;
- *)
- hits=$(printf '%s\n' "$scan" |
- grep -E 'log\.(Print|Printf|Println|Fatal|Fatalf|Fatalln|Panic|Panicf|Panicln)\(' || true)
- if [ -n "$hits" ]; then
- err "$file: uses the standard \"log\" package. New code must call logger.PrintAndLog(title, message, err) instead (rule 1)."
- fi
- ;;
- esac
- # --- Rule 5: portability, no hardcoded OS paths -----------------------
- if ! is_platform_file "$file"; then
- paths=$(printf '%s\n' "$scan" |
- grep -E '"(/usr/|/etc/|/var/|/bin/|/sbin/|/opt/|/root/|/home/)|"[A-Za-z]:\\\\|"[A-Za-z]:/' || true)
- if [ -n "$paths" ]; then
- err "$file: contains a hardcoded OS path literal. Build paths with filepath.Join and os.TempDir/UserHomeDir so ArozOS stays cross-platform (rule 5)."
- fi
- fi
- # --- Rule 4: new HTTP endpoints need a deliberate security decision ---
- endpoints=$(printf '%s\n' "$scan" | grep -E 'http\.HandleFunc\(' || true)
- if [ -n "$endpoints" ]; then
- case "$file" in
- *mod/prouter/*) ;; # the permission router wraps http.HandleFunc by design
- *)
- warn "$file: registers an endpoint with raw http.HandleFunc. Prefer prout.NewModuleRouter(...).HandleFunc for auth/permission, or confirm the endpoint is intentionally public (rule 4)."
- ;;
- esac
- fi
- }
- # check_file applies the file-level rules to a single Go source file path.
- check_file() {
- file=$1
- case "$file" in
- *_test.go) return ;; # test files are not themselves subject to these rules
- esac
- # --- Rule 5 (soft): isolate platform calls in build-tagged files -----
- if ! is_platform_file "$file" && ! has_build_constraint "$file"; then
- if grep -Eq 'exec\.Command\(|(^|[^.])syscall\.' "$file" 2>/dev/null; then
- warn "$file: calls exec.Command/syscall in a cross-platform file. Move OS-specific code into a *_linux.go / *_windows.go / *_darwin.go file or guard it with a //go:build tag (rule 5)."
- fi
- fi
- # --- Rule 2: every package ships tests -------------------------------
- case "$file" in
- *mod/*)
- dir=$(dirname "$file")
- if ! ls "$dir"/*_test.go >/dev/null 2>&1; then
- warn "$file: package $dir has no *_test.go file. New functions must ship with tests (rule 2)."
- fi
- ;;
- esac
- }
- # license_reminder fires once when dependency manifests change.
- license_reminder() {
- warn "go.mod/go.sum changed: confirm every new dependency is MIT, BSD, Apache-2.0, MPL-2.0 or ISC (GPL-compatible and OK for commercial use). Reject GPL/AGPL/unknown-licensed modules (rule 3)."
- }
- scan_one() {
- file=$1
- case "$file" in
- go.mod | go.sum | */go.mod | */go.sum)
- license_reminder
- return
- ;;
- *.go) ;;
- *) return ;;
- esac
- # Single-file / hook mode scans the whole file content.
- check_lines "$file" <"$file"
- check_file "$file"
- }
- mode=${1:---help}
- case "$mode" in
- --hook)
- # Extract tool_input.file_path from the hook JSON payload on stdin without
- # requiring jq (rule 5: no extra system dependencies).
- payload=$(cat)
- file=$(printf '%s' "$payload" |
- sed -n 's/.*"file_path"[[:space:]]*:[[:space:]]*"\([^"]*\)".*/\1/p' | head -n 1)
- [ -z "$file" ] && exit 0
- scan_one "$file"
- if [ "$errors" -gt 0 ] || [ "$warns" -gt 0 ]; then
- printf '\nArozOS convention check: %d error(s), %d warning(s). See CLAUDE.md.\n' \
- "$errors" "$warns" >&2
- exit 2
- fi
- exit 0
- ;;
- --diff)
- base=${2:-}
- if [ -z "$base" ]; then
- echo "usage: $0 --diff <base-ref>" >&2
- exit 1
- fi
- cd "$repo_root" || exit 1
- changed=$(git diff --name-only --diff-filter=ACM "$base" -- '*.go' 'go.mod' 'go.sum' '**/go.mod' '**/go.sum')
- [ -z "$changed" ] && {
- echo "No Go/module changes to check." >&2
- exit 0
- }
- # Iterate over a temp file rather than a pipe so the err/warn counters,
- # which live in this shell, survive (a piped while-loop runs in a subshell).
- tmp=$(mktemp)
- added=$(mktemp)
- printf '%s\n' "$changed" >"$tmp"
- while IFS= read -r f; do
- [ -n "$f" ] || continue
- case "$f" in
- go.mod | go.sum | */go.mod | */go.sum)
- license_reminder
- continue
- ;;
- *.go) ;;
- *) continue ;;
- esac
- printf 'Checking %s\n' "$f" >&2
- # Diff mode only scans *added* lines for the per-line rules. Feed them
- # via redirection (not a pipe) so check_lines runs in this shell.
- git diff -U0 "$base" -- "$f" | grep -E '^\+' | grep -Ev '^\+\+\+' | sed 's/^+//' >"$added"
- check_lines "$f" <"$added"
- check_file "$f"
- done <"$tmp"
- rm -f "$tmp" "$added"
- ;;
- --help | -h)
- sed -n '2,40p' "$0"
- exit 0
- ;;
- *)
- # Treat all arguments as explicit file paths.
- for f in "$@"; do
- scan_one "$f"
- done
- ;;
- esac
- printf '\nArozOS convention check: %d error(s), %d warning(s).\n' "$errors" "$warns" >&2
- [ "$errors" -gt 0 ] && exit 1
- exit 0
|