diff --git a/.github/hooks/check-dco.sh b/.github/hooks/check-dco.sh new file mode 100755 index 0000000000..5f7f88654b --- /dev/null +++ b/.github/hooks/check-dco.sh @@ -0,0 +1,33 @@ +#!/usr/bin/env bash +# DCO sign-off check for the pre-commit commit-msg stage. +# +# Mirrors the GitHub DCO app requirement: every commit must carry a +# "Signed-off-by:" line identifying the author. +# +# Usage: check-dco.sh + +set -euo pipefail + +msg_file="${1:-}" + +if [[ -z "${msg_file}" || ! -f "${msg_file}" ]]; then + echo "DCO check: no commit message file supplied." >&2 + exit 1 +fi + +if grep -qE '^Signed-off-by: .+ <[^@ ]+@[^@ ]+>$' "${msg_file}"; then + exit 0 +fi + +cat >&2 <<'EOF' +DCO check failed: commit message is missing a "Signed-off-by:" line. + +Add a sign-off using one of: + git commit -s # sign as you create the commit + git commit --amend -s # sign the most recent commit + +The line must identify the commit author, for example: + Signed-off-by: Your Name +EOF + +exit 1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml index ae03b5bae9..a3fe2d7d83 100644 --- a/.pre-commit-config.yaml +++ b/.pre-commit-config.yaml @@ -1,6 +1,8 @@ default_language_version: python: python3 +default_install_hook_types: [pre-commit, commit-msg] + ci: autofix_prs: true autoupdate_commit_msg: '[pre-commit.ci] pre-commit suggestions' @@ -62,3 +64,10 @@ repos: ^versioneer.py| ^monai/_version.py ) + - repo: local + hooks: + - id: dco + name: DCO sign-off + entry: .github/hooks/check-dco.sh + language: script + stages: [commit-msg] diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fb68b4b58..318433a48e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -51,7 +51,19 @@ To collaborate efficiently, please read through this section and follow them. #### Checking the coding style -Coding style is checked and enforced by black, isort, and ruff. +Coding style is checked and enforced by black, isort, and ruff, and every commit must carry a DCO sign-off. +To catch formatting and DCO failures before they reach CI, install the git pre-commit hooks once per checkout: + +```bash +# install the git hooks: black, isort, ruff, and the DCO sign-off check +pre-commit install + +# or, via the test runner: +./runtests.sh --setup +``` + +These hooks run automatically on every `git commit`: `black`, `isort`, and `ruff` reformat the staged files, and the `commit-msg` hook blocks a commit that is missing a `Signed-off-by` line. + Before submitting a pull request, we recommend that all linting should pass, by running the following command locally: ```bash @@ -247,6 +259,8 @@ Git has a `-s` (or `--signoff`) command-line option to append this automatically git commit -s -m 'a new commit' ``` +If the git pre-commit hooks are installed (`pre-commit install` or `./runtests.sh --setup`), the local `commit-msg` hook blocks any commit that is missing this line, so the DCO check fails locally rather than in CI. + The commit message will be: ``` diff --git a/runtests.sh b/runtests.sh index 0fc18b36ec..6a092f57bf 100755 --- a/runtests.sh +++ b/runtests.sh @@ -53,6 +53,7 @@ doPyreflyFormat=false doCleanup=false doDistTests=false doPrecommit=false +doSetup=false testTimeout=0 NUM_PARALLEL=1 @@ -61,7 +62,7 @@ PY_EXE=${MONAI_PY_EXE:-$(which python)} function print_usage { echo "runtests.sh [--codeformat] [--autofix] [--black] [--isort] [--pylint] [--ruff]" - echo " [--clangformat] [--precommit] [--pytype] [-j number] [--pyrefly]" + echo " [--clangformat] [--precommit] [--pytype] [-j number] [--pyrefly] [--setup]" echo " [--unittests] [--disttests] [--coverage] [--quick] [--min] [--net] [--build] [--list_tests]" echo " [--dryrun] [--copyright] [--clean] [--help] [--version] [--path] [--formatfix]" echo "" @@ -103,6 +104,7 @@ function print_usage { echo "" echo "Misc. options:" echo " --dryrun : display the commands to the screen without running" + echo " --setup : install git pre-commit hooks (black, isort, ruff, DCO sign-off)" echo " --copyright : check whether every source code has a copyright header" echo " -f, --codeformat : shorthand to run all code style and static analysis tests" echo " -c, --clean : clean temporary files from tests and exit" @@ -320,6 +322,9 @@ do --precommit) doPrecommit=true ;; + --setup) + doSetup=true + ;; --pytype) echo "${yellow}WARNING: --pytype is deprecated and may be removed in a future release.${noColor}" doPytypeFormat=true @@ -429,6 +434,21 @@ then echo "${green}done!${noColor}" fi +if [ $doSetup = true ] +then + echo "${separator}${blue}setup${noColor}" + + # ensure pre-commit is available + if ! is_pip_installed pre_commit + then + install_deps + fi + + ${cmdPrefix}"${PY_EXE}" -m pre_commit install + + echo "${green}done! git hooks installed (black, isort, ruff, DCO sign-off).${noColor}" +fi + # unconditionally report on the state of monai print_version