diff --git a/.github/hooks/check-dco.sh b/.github/hooks/check-dco.sh new file mode 100755 index 0000000000..82f3c64fbc --- /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: .+ <[^@ ]+@[^@ ]+>\r?$' "${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..630fbfbb8e 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -52,6 +52,18 @@ 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. +To catch formatting failures before they reach CI, install the git pre-commit hooks once per checkout: + +```bash +# install the git hooks: black, isort, ruff +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. The same install also wires up the `commit-msg` hook that enforces the DCO sign-off described in [Signing your work](#signing-your-work). + Before submitting a pull request, we recommend that all linting should pass, by running the following command locally: ```bash @@ -178,18 +190,9 @@ Please type `make help` in `docs/` folder for all supported format options. #### Automatic code formatting -MONAI provides support of automatic Python code formatting via [a customised GitHub action](https://github.com/Project-MONAI/monai-code-formatter). -This makes the project's Python coding style consistent and reduces maintenance burdens. -Commenting a pull request with `/black` triggers the formatting action based on [`psf/Black`](https://github.com/psf/black) (this is implemented with [`slash command dispatch`](https://github.com/marketplace/actions/slash-command-dispatch)). - -Steps for the formatting process: +Code formatting is now handled locally via the [pre-commit](https://pre-commit.com/) hooks described in [Checking the coding style](#checking-the-coding-style): once installed, `black`, `isort`, and `ruff` reformat staged files automatically on every `git commit`, so formatting issues are caught before a pull request is even opened. -- After submitting a pull request or push to an existing pull request, -make a comment to the pull request to trigger the formatting action. -The first line of the comment must be `/black` so that it will be interpreted by [the comment parser](https://github.com/marketplace/actions/slash-command-dispatch#how-are-comments-parsed-for-slash-commands). -- [Auto] The GitHub action tries to format all Python files (using [`psf/Black`](https://github.com/psf/black)) in the branch and makes a commit under the name "MONAI bot" if there's code change. The actual formatting action is deployed at [project-monai/monai-code-formatter](https://github.com/Project-MONAI/monai-code-formatter). -- [Auto] After the formatting commit, the GitHub action adds an emoji to the comment that triggered the process. -- Repeat the above steps if necessary. +MONAI previously offered a `/black` slash command that triggered [a customised GitHub action](https://github.com/Project-MONAI/monai-code-formatter) to auto-format a pull request's branch based on [`psf/Black`](https://github.com/psf/black). This action hasn't been used in a long while and is no longer the recommended workflow. If a pull request still fails formatting checks in CI, install the pre-commit hooks locally and run `./runtests.sh --autofix` to fix the branch instead. #### Adding new optional dependencies @@ -247,6 +250,19 @@ Git has a `-s` (or `--signoff`) command-line option to append this automatically git commit -s -m 'a new commit' ``` +For `-s` to add the correct identity, set your name and email in your git configuration (`git config --global --edit`, or the commands below): + +```bash +git config --global user.name "Your Name" +git config --global user.email "you@example.com" +``` + +If you'd rather not use a personal address, GitHub provides a no-reply email tied to your account under [Settings > Emails](https://github.com/settings/emails), for example `12345678+yourusername@users.noreply.github.com`. Using it still associates the sign-off with your GitHub username without exposing a personal email. + +VS Code can also be configured to sign off every commit automatically: enable the `git.alwaysSignOff` setting (**Settings > Git: Always Sign Off**). + +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: ``` @@ -453,7 +469,7 @@ All code review comments should be specific, constructive, and actionable. 1. Read carefully the descriptions of the pull request and the files changed, write comments if needed. 1. Make in-line comments to specific code segments, [request for changes](https://help.github.com/en/github/collaborating-with-issues-and-pull-requests/about-pull-request-reviews) if needed. 1. Review any further code changes until all comments addressed by the contributors. -1. Comment to trigger `/black` and/or `/integration-test` for optional auto code formatting and [integration tests](.github/workflows/integration.yml). +1. If formatting checks fail, ask the contributor to run `./runtests.sh --autofix`, commit the resulting changes, and re-push; suggest installing the pre-commit hooks (see [Checking the coding style](#checking-the-coding-style)) to catch this locally next time. Comment `/integration-test` to trigger optional [integration tests](.github/workflows/integration.yml) if needed. 1. [Maintainers] Review the changes and comment `/build` to trigger internal full tests. 1. Merge the pull request to the dev branch. 1. Close the corresponding task ticket on [the issue list][monai issue list]. diff --git a/runtests.sh b/runtests.sh index 0fc18b36ec..5b48c6a1ba 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,25 @@ 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 + + if [[ -z "$cmdPrefix" ]]; then + echo "${green}done! git hooks installed (black, isort, ruff, DCO sign-off).${noColor}" + else + echo "dry-run: git hooks would be installed (black, isort, ruff, DCO sign-off)." + fi +fi + # unconditionally report on the state of monai print_version