Skip to content

Commit dd37bb1

Browse files
committed
update hooks, add dco check
Signed-off-by: Zoe Blevins <zblevins@nvidia.com>
1 parent 1e7cfc2 commit dd37bb1

6 files changed

Lines changed: 176 additions & 24 deletions

File tree

CONTRIBUTING.md

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -106,7 +106,13 @@ git rebase --signoff HEAD~<number_of_commits>
106106
```bash
107107
./scripts/install-hooks.sh
108108
```
109-
This installs a pre-commit hook that auto-formats staged Python files with `ruff format` and verifies SPDX license headers are present in all source files.
109+
This installs:
110+
111+
- `pre-commit`, which auto-formats staged Python files outside ignored/generated
112+
directories with `ruff format` and verifies SPDX license headers in supported
113+
source files.
114+
- `commit-msg`, which rejects commit messages missing a DCO
115+
`Signed-off-by: Name <email>` trailer.
110116

111117
5. **Create a branch** for your changes:
112118
```bash

README.md

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,29 @@ uv sync --dev
5252
./scripts/install-hooks.sh
5353
```
5454

55+
### Git Hooks
56+
57+
Install the repository hooks after cloning and whenever hook scripts change:
58+
59+
```bash
60+
./scripts/install-hooks.sh
61+
```
62+
63+
Installed hooks:
64+
65+
- `pre-commit`: formats all staged Python files outside ignored/generated
66+
directories with `uv run ruff format`, re-stages those files, and checks SPDX
67+
license headers for supported source files (`.py`, `.ts`, `.tsx`, `.js`,
68+
`.jsx`, `.mjs`, `.cjs`, and `.go`) under `src/`, `ui/src/`, `ui/tests/`,
69+
`components/`, `db/migrations/`, `scripts/`, `development/`,
70+
`installer/src/`, `installer/tests/`, and `installer/scripts/`.
71+
- `commit-msg`: rejects commits that do not include a valid DCO
72+
`Signed-off-by: Name <email>` trailer. Use `git commit -s` or
73+
`git commit --amend -s` to add the trailer automatically.
74+
75+
Local hooks can be skipped with `git commit --no-verify`, so the organization DCO
76+
app remains the merge-time enforcement gate in CI.
77+
5578
For UI work:
5679

5780
```bash
@@ -334,8 +357,9 @@ Developer Certificate of Origin sign-off process described there.
334357
1. Fork the repository.
335358
2. Create a feature branch.
336359
3. Make changes.
337-
4. Run tests and linting with `make test lint`.
338-
5. Submit a pull request using the repository PR template.
360+
4. Install local hooks with `./scripts/install-hooks.sh`.
361+
5. Run tests and linting with `make test lint`.
362+
6. Submit a pull request using the repository PR template.
339363

340364
Please also follow the [Code of Conduct](CODE_OF_CONDUCT.md).
341365

scripts/add_spdx_headers.py

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,8 @@
8282
"components/network-templates",
8383
"development/mock_topology",
8484
"installer/src",
85+
"installer/tests",
86+
"installer/scripts",
8587
]
8688

8789
JS_TS_DIRS = [
@@ -275,9 +277,9 @@ def main() -> None:
275277
total_modified += modified
276278
total_skipped += skipped
277279

278-
print("\nProcessing TypeScript files...")
280+
print("\nProcessing TypeScript/JavaScript files...")
279281
for dir_path in JS_TS_DIRS:
280-
for ext in (".ts", ".tsx"):
282+
for ext in (".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"):
281283
modified, skipped = process_directory(repo_root, dir_path, ext, add_header_to_js_ts)
282284
total_modified += modified
283285
total_skipped += skipped

scripts/hooks/commit-msg

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
#!/bin/bash
2+
# SPDX-FileCopyrightText: Copyright (c) 2024-2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
3+
# SPDX-License-Identifier: Apache-2.0
4+
#
5+
# Licensed under the Apache License, Version 2.0 (the "License");
6+
# you may not use this file except in compliance with the License.
7+
# You may obtain a copy of the License at
8+
#
9+
# http://www.apache.org/licenses/LICENSE-2.0
10+
#
11+
# Unless required by applicable law or agreed to in writing, software
12+
# distributed under the License is distributed on an "AS IS" BASIS,
13+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14+
# See the License for the specific language governing permissions and
15+
# limitations under the License.
16+
#
17+
# Commit message hook to require Developer Certificate of Origin sign-off.
18+
# Install with: ./scripts/install-hooks.sh
19+
20+
set -euo pipefail
21+
22+
RED='\033[0;31m'
23+
GREEN='\033[0;32m'
24+
YELLOW='\033[1;33m'
25+
NC='\033[0m'
26+
27+
COMMIT_MSG_FILE="${1:-}"
28+
29+
if [[ -z "$COMMIT_MSG_FILE" || ! -f "$COMMIT_MSG_FILE" ]]; then
30+
echo -e "${RED}ERROR: commit-msg hook did not receive a commit message file.${NC}"
31+
exit 1
32+
fi
33+
34+
trim() {
35+
local value="$1"
36+
value="${value#"${value%%[![:space:]]*}"}"
37+
value="${value%"${value##*[![:space:]]}"}"
38+
printf '%s' "$value"
39+
}
40+
41+
trailers="$(git interpret-trailers --parse "$COMMIT_MSG_FILE")"
42+
valid_signoff_count=0
43+
invalid_signoffs=()
44+
git_user_name="$(git config user.name || true)"
45+
git_user_email="$(git config user.email || true)"
46+
47+
while IFS= read -r line; do
48+
token="$(trim "${line%%:*}")"
49+
if [[ "${token,,}" != "signed-off-by" ]]; then
50+
continue
51+
fi
52+
53+
value="$(trim "${line#*:}")"
54+
if [[ "$value" =~ ^[^[:space:]\<\>][^\<\>]*[[:space:]]\<[^\<\>@[:space:]]+@[^\<\>[:space:]]+\>$ ]] \
55+
&& [[ "${value,,}" != *"your name"* ]] \
56+
&& [[ "${value,,}" != *"your.email@example.com"* ]]; then
57+
((valid_signoff_count += 1))
58+
else
59+
invalid_signoffs+=("$value")
60+
fi
61+
done <<< "$trailers"
62+
63+
if [[ "$valid_signoff_count" -gt 0 ]]; then
64+
echo -e "${GREEN}✓ DCO sign-off present${NC}"
65+
exit 0
66+
fi
67+
68+
echo -e "${RED}ERROR: commit message is missing a valid DCO sign-off.${NC}"
69+
echo ""
70+
if [[ -n "$git_user_name" && -n "$git_user_email" ]]; then
71+
echo "Add a Signed-off-by trailer using your real name and email:"
72+
echo ""
73+
echo " Signed-off-by: $git_user_name <$git_user_email>"
74+
echo ""
75+
else
76+
echo "Add a Signed-off-by trailer using your real name and email."
77+
echo ""
78+
fi
79+
echo "Usually you can let Git add this automatically:"
80+
echo ""
81+
echo " git commit -s"
82+
echo " git commit --amend -s"
83+
echo ""
84+
85+
if [[ "${#invalid_signoffs[@]}" -gt 0 ]]; then
86+
echo -e "${YELLOW}Found malformed sign-off trailer(s):${NC}"
87+
for signoff in "${invalid_signoffs[@]}"; do
88+
echo " Signed-off-by: $signoff"
89+
done
90+
echo ""
91+
fi
92+
93+
echo "The organization DCO check is the final enforcement gate in CI; this hook catches"
94+
echo "the issue before you push."
95+
96+
exit 1

scripts/hooks/pre-commit

Lines changed: 30 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,7 @@
1414
# See the License for the specific language governing permissions and
1515
# limitations under the License.
1616
#
17-
# Pre-commit hook to verify SPDX license headers are present in source files.
17+
# Pre-commit hook to format staged Python files and verify SPDX license headers.
1818
# Install with: ./scripts/install-hooks.sh
1919

2020
set -e
@@ -24,14 +24,31 @@ GREEN='\033[0;32m'
2424
YELLOW='\033[1;33m'
2525
NC='\033[0m'
2626

27-
EXTENSIONS=("py" "ts" "tsx" "go")
28-
29-
CHECK_DIRS=("src/" "ui/src/" "ui/tests/" "components/" "db/migrations/" "scripts/" "development/" "installer/src/")
27+
EXTENSIONS=("py" "ts" "tsx" "js" "jsx" "mjs" "cjs" "go")
28+
29+
CHECK_DIRS=(
30+
"src/"
31+
"ui/src/"
32+
"ui/tests/"
33+
"components/"
34+
"db/migrations/"
35+
"scripts/"
36+
"development/"
37+
"installer/src/"
38+
"installer/tests/"
39+
"installer/scripts/"
40+
)
3041

3142
SKIP_PATTERNS=(
3243
"__pycache__"
3344
"node_modules"
3445
".git"
46+
".mypy_cache"
47+
".pytest_cache"
48+
".ruff_cache"
49+
".venv"
50+
"build/"
51+
"dist/"
3552
)
3653

3754
SPDX_IDENTIFIER="SPDX-License-Identifier: Apache-2.0"
@@ -40,7 +57,7 @@ LICENSE_TEXT="Licensed under the Apache License"
4057
missing_files=()
4158
checked_count=0
4259

43-
staged_files=$(git diff --cached --name-only --diff-filter=ACM)
60+
mapfile -d '' -t staged_files < <(git diff --cached --name-only -z --diff-filter=ACM)
4461

4562
should_skip() {
4663
local file="$1"
@@ -93,8 +110,12 @@ check_spdx_header() {
93110

94111
# --- Ruff formatting ---
95112
staged_py_files=()
96-
for file in $staged_files; do
97-
if [[ ( "$file" == src/* || "$file" == installer/src/* ) && "$file" == *.py ]] && [[ -f "$file" ]]; then
113+
for file in "${staged_files[@]}"; do
114+
if should_skip "$file"; then
115+
continue
116+
fi
117+
118+
if [[ "$file" == *.py && -f "$file" ]]; then
98119
staged_py_files+=("$file")
99120
fi
100121
done
@@ -109,7 +130,7 @@ fi
109130
# --- SPDX header checks ---
110131
echo -e "${YELLOW}Checking SPDX license headers...${NC}"
111132

112-
for file in $staged_files; do
133+
for file in "${staged_files[@]}"; do
113134
if ! is_in_check_dir "$file"; then
114135
continue
115136
fi
@@ -122,7 +143,7 @@ for file in $staged_files; do
122143
continue
123144
fi
124145

125-
((checked_count++))
146+
((checked_count += 1))
126147

127148
if ! check_spdx_header "$file"; then
128149
missing_files+=("$file")

scripts/install-hooks.sh

Lines changed: 13 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -27,20 +27,23 @@ echo "Installing git hooks..."
2727

2828
mkdir -p "$HOOKS_DIR"
2929

30-
if [[ -f "$SOURCE_HOOKS_DIR/pre-commit" ]]; then
31-
cp "$SOURCE_HOOKS_DIR/pre-commit" "$HOOKS_DIR/pre-commit"
32-
chmod +x "$HOOKS_DIR/pre-commit"
33-
echo " ✓ Installed pre-commit hook"
34-
else
35-
echo " ✗ pre-commit hook not found at $SOURCE_HOOKS_DIR/pre-commit"
36-
exit 1
37-
fi
30+
for hook in pre-commit commit-msg; do
31+
if [[ -f "$SOURCE_HOOKS_DIR/$hook" ]]; then
32+
cp "$SOURCE_HOOKS_DIR/$hook" "$HOOKS_DIR/$hook"
33+
chmod +x "$HOOKS_DIR/$hook"
34+
echo " ✓ Installed $hook hook"
35+
else
36+
echo "$hook hook not found at $SOURCE_HOOKS_DIR/$hook"
37+
exit 1
38+
fi
39+
done
3840

3941
echo ""
4042
echo "Git hooks installed successfully!"
4143
echo ""
4244
echo "Hooks installed:"
43-
echo " - pre-commit: Auto-formats Python files with ruff, checks SPDX license headers"
45+
echo " - pre-commit: Auto-formats staged Python files with ruff, checks SPDX license headers"
46+
echo " - commit-msg: Requires a DCO Signed-off-by trailer"
4447
echo ""
45-
echo "To skip the pre-commit hook for a specific commit, use:"
48+
echo "To skip local hooks for a specific commit, use:"
4649
echo " git commit --no-verify"

0 commit comments

Comments
 (0)