verify-license.sh
1 #!/bin/bash 2 # Copyright 2025 Alibaba Group Holding Ltd. 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 # This script verifies that required files contain the Apache 2.0 license header. 17 # It scans tracked source files and fails with a list of violations if any header 18 # is missing. 19 20 set -euo pipefail 21 22 REPO_ROOT="$(cd "$(dirname "$0")/.." && pwd)" 23 CURRENT_YEAR="$(date +%Y)" 24 MIN_YEAR="2025" 25 LICENSE_OWNER="Alibaba Group Holding Ltd." 26 LICENSE_REGEX="Copyright [0-9]{4} ${LICENSE_OWNER// / }" 27 28 # File extensions that are expected to carry a license header. 29 LICENSE_EXTS=( 30 go py sh kt kts java ts tsx js jsx toml html css sql tf 31 ) 32 33 # Explicit file basenames that should also be checked (e.g., Dockerfile) 34 LICENSE_BASENAMES=( 35 Dockerfile 36 ) 37 38 # Paths to ignore entirely. 39 IGNORED_PATHS=( 40 "LICENSE" 41 "NOTICE" 42 "docs/" 43 "scripts/spec-doc/index.html" # Generated doc 44 ) 45 46 is_k8s_mock_go() { 47 local file="${1-}" 48 [[ -z "$file" ]] && return 1 49 # Skip any Go mocks under kubernetes/internal: 50 # - filenames ending with _mock.go 51 # - any file under a /mock/ directory 52 if [[ "$file" != kubernetes/internal/* ]]; then 53 return 1 54 fi 55 if [[ "$file" == *"_mock.go" ]]; then 56 return 0 57 fi 58 if [[ "$file" == */mock/*.go ]]; then 59 return 0 60 fi 61 return 1 62 } 63 64 is_generated_to_skip() { 65 local file="$1" 66 # Skip common generated files 67 if [[ "$file" == *"deepcopy.go" ]]; then 68 return 0 69 fi 70 return 1 71 } 72 73 cd "$REPO_ROOT" 74 75 is_ignored() { 76 local file="$1" 77 for ignore in "${IGNORED_PATHS[@]}"; do 78 if [[ "$ignore" == */ ]]; then 79 if [[ "$file" == "$ignore"* ]]; then 80 return 0 81 fi 82 elif [[ "$file" == "$ignore" ]]; then 83 return 0 84 fi 85 done 86 return 1 87 } 88 89 has_expected_extension() { 90 local file="$1" 91 local ext="${file##*.}" 92 for candidate in "${LICENSE_EXTS[@]}"; do 93 if [[ "$ext" == "$candidate" ]]; then 94 return 0 95 fi 96 done 97 return 1 98 } 99 100 has_expected_basename() { 101 local file="$1" 102 local base 103 base="$(basename "$file")" 104 for candidate in "${LICENSE_BASENAMES[@]}"; do 105 if [[ "$base" == "$candidate" ]]; then 106 return 0 107 fi 108 done 109 return 1 110 } 111 112 missing=() 113 114 while IFS= read -r file; do 115 # Skip ignored paths 116 if is_ignored "$file"; then 117 continue 118 fi 119 # Skip kubernetes internal mock go files 120 if is_k8s_mock_go "$file"; then 121 continue 122 fi 123 # Skip generated files 124 if is_generated_to_skip "$file"; then 125 continue 126 fi 127 128 # Only check files with expected extensions or basenames 129 if ! has_expected_extension "$file" && ! has_expected_basename "$file"; then 130 continue 131 fi 132 133 # Limit scan to the first 25 lines to allow shebangs/DOCTYPE above the header. 134 header="$(head -n 25 "$file")" 135 if ! echo "$header" | grep -Eq "$LICENSE_REGEX"; then 136 missing+=("$file") 137 continue 138 fi 139 found_year="$(echo "$header" | grep -Eo "$LICENSE_REGEX" | head -n1 | grep -Eo '[0-9]{4}')" 140 if [[ -z "$found_year" || "$found_year" -gt "$CURRENT_YEAR" || "$found_year" -lt "$MIN_YEAR" ]]; then 141 missing+=("$file") 142 fi 143 done < <(git -C "$REPO_ROOT" ls-files) 144 145 if ((${#missing[@]} > 0)); then 146 echo "Missing license header in the following files:" 147 printf ' - %s\n' "${missing[@]}" 148 exit 1 149 fi 150 151 echo "License headers verified."